Compare commits

...

40 Commits

Author SHA1 Message Date
Riccardo Ferretti
f5f476e717 v0.17.7 2022-03-29 22:11:29 +02:00
Riccardo Ferretti
25172ee100 Preparation for 0.17.7 2022-03-29 22:10:42 +02:00
Riccardo Ferretti
cbb0dab124 Improved navigation
There was an issue with navigation that would cause multiple text editors to be opened for the same file.
Turns out the issue was related to the use of URIs that included the fragment component, as well as the interaction between the link provider and the definition provider.
This commit fixes the issue.
2022-03-29 21:24:31 +02:00
Riccardo Ferretti
d570983e16 Fix 895 - Ignore section when computing backlinks 2022-03-29 08:56:21 +02:00
Riccardo Ferretti
b5e979ead6 Fixed snippet parser test 2022-03-28 15:31:31 +02:00
Riccardo Ferretti
aed907663a Consolidated WikiLink an DirectLink into ResourceLink 2022-03-27 19:56:28 +02:00
Riccardo Ferretti
a65325a6e1 Refactoring of markdown parser and provider code
No functional change
2022-03-27 19:47:57 +02:00
Riccardo Ferretti
772cba4b43 Refactored mardown provider, workspace and graph tests 2022-03-25 21:02:34 +01:00
Riccardo Ferretti
f1a0054141 v0.17.6 2022-03-03 15:59:12 +01:00
Riccardo Ferretti
854e329c90 Preparation for next release 2022-03-03 15:59:04 +01:00
allcontributors[bot]
0978bebd5b docs: add cliffordfajardo as a contributor for tool (#949)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-02-28 18:15:45 +01:00
Clifford Fajardo
6eaae23e19 update github ISSUE_TEMPLATE files (#945) 2022-02-28 18:10:48 +01:00
Samuel Krieg
4c615bdb02 Don't fail on errors when scanning workspace for files (#943)
Fixes #941 

Unfortunately there is no way to see which errors are being skipped, but at the same time it makes sense to not be strict and have a single file block a whole scan (especially because it could be a file Foam is not even interested in).
2022-02-25 15:10:22 +01:00
Riccardo Ferretti
3adf853b89 v0.17.5 2022-02-22 23:11:44 +01:00
Riccardo Ferretti
111c7718c4 Preparation for 0.17.5 2022-02-22 23:11:14 +01:00
allcontributors[bot]
9c7f03d62e docs: add techCarpenter as a contributor for code (#939)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-02-22 19:58:21 +01:00
Brian DeVries
0d90fc5c5a FOAM_SLUG template variable addition (#865)
* create slugified title variable available in templates
* add test
* add FOAM_SLUG to documentation
* add github-slugger dependency

Co-authored-by: Brian DeVries <brian@brianjdevries.com>
2022-02-20 18:58:21 +01:00
dependabot[bot]
537c78b630 Bump markdown-it from 12.0.4 to 12.3.2 (#909)
Bumps [markdown-it](https://github.com/markdown-it/markdown-it) from 12.0.4 to 12.3.2.
- [Release notes](https://github.com/markdown-it/markdown-it/releases)
- [Changelog](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md)
- [Commits](https://github.com/markdown-it/markdown-it/compare/12.0.4...12.3.2)

---
updated-dependencies:
- dependency-name: markdown-it
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-20 17:31:18 +01:00
dependabot[bot]
6d210590b2 Bump shelljs from 0.8.4 to 0.8.5 (#912)
Bumps [shelljs](https://github.com/shelljs/shelljs) from 0.8.4 to 0.8.5.
- [Release notes](https://github.com/shelljs/shelljs/releases)
- [Changelog](https://github.com/shelljs/shelljs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/shelljs/shelljs/compare/v0.8.4...v0.8.5)

---
updated-dependencies:
- dependency-name: shelljs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-20 17:30:35 +01:00
dependabot[bot]
ab8e97ce0b Bump node-fetch from 2.6.1 to 2.6.7 (#928)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-20 17:29:54 +01:00
dependabot[bot]
f756d9c966 Bump trim-off-newlines from 1.0.1 to 1.0.3 (#929)
Bumps [trim-off-newlines](https://github.com/stevemao/trim-off-newlines) from 1.0.1 to 1.0.3.
- [Release notes](https://github.com/stevemao/trim-off-newlines/releases)
- [Commits](https://github.com/stevemao/trim-off-newlines/compare/v1.0.1...v1.0.3)

---
updated-dependencies:
- dependency-name: trim-off-newlines
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-20 17:28:46 +01:00
Radosław Kierznowski
aa7669f8ad Update web-clipper.md (#936)
I corrected the name of the plugin.
2022-02-20 17:28:05 +01:00
Francis Hamel
38bd5f67f2 Fix doc around Mermaid rendering in Github (#934)
According to this [blog post](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/), Github can now render Mermaid diagrams.
2022-02-20 17:27:12 +01:00
Radosław Kierznowski
336b8cfbba Added recommended tool (#935)
I added a recommended plugin that saves the page directly to the repository.
2022-02-20 17:15:49 +01:00
Riccardo Ferretti
ea03b86338 v0.17.4 2022-02-13 15:25:51 +01:00
Riccardo Ferretti
449c062566 Preparation for 0.17.4 2022-02-13 15:25:21 +01:00
Michael Overmeyer
880c2e3d3b Vendor a snippet parser (#882)
* Add the snippet parsing code from VSCode

From 95be30b3ac

* Remove `override` keyword

This is a TypeScript 4.3 feature, but Foam is not there yet

* Use `SnippetParser` to find Foam variables

* Return `Variable` objects from `findFoamVariables`

* Make `SnippetParser` resolve async

* Implement a `VariableResolver`

* Add start/end positions to `Variable`s

* Substitute based on indices, not regex

* Remove limitation warning from docs

* Merge `FoamVariableResolver` and `Resolver`

* Remove `extraVariablesToResolve`

It was no longer being used for `FOAM_TITLE`, and `FOAM_SELECTED_TEXT` didn't need to have it set either, so long as it appeared in `givenValues`, which it does.

* Add name filter to `resolveVariables`

You cannot call `resolve` on a `Variable` without modifying it, even if your `VariableResolver` doesn't know how to resolve the `Variable`.
For example, a `Transform` with a default value will modify the `Varible`'s `children`, even if the `VariableResolver` does not resolve a value.

Instead, we add a name filter, so that we don't resolve any `Variable`s that aren't Foam variables.

* Return `undefined` when the `VariableResolver` cannot resolve a `Variable`

This is how a `VariableResolver` is supposed to behave in these cases.

* Move variable substitution into `TextmateSnippet`

That way, the Foam `VariableResolver` code doesn't need to keep track of the text, nor interact with the `Variable` `pos`/`endPos`.
2022-02-09 00:45:54 +01:00
fuck-capitalism
17cb619480 Fixed typo (#913) 2022-01-15 17:24:57 +01:00
Riccardo Ferretti
6deae95d80 v0.17.3 2022-01-14 20:21:32 +01:00
Riccardo Ferretti
1c0ebb8af7 Preparation for 0.17.3 2022-01-14 20:20:28 +01:00
allcontributors[bot]
fe56823e76 docs: add bentongxyz as a contributor for code (#905)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-01-07 13:10:45 +01:00
bentongxyz
6956e0779a Fix commented test case that might randomly fail (#902) 2022-01-07 13:10:09 +01:00
allcontributors[bot]
c8f1f8e03a docs: add veesar as a contributor for doc (#901)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-01-04 12:40:03 +01:00
Veesar
9589071760 Remove reference to VSCode Markdown Notes (#896)
Remove mention of VSCode Markdown Notes as it is no longer a recommended extension or required for Backlinks (ca8ee63cea, https://github.com/foambubble/foam/issues/719#issuecomment-880100159)
2022-01-04 12:39:25 +01:00
allcontributors[bot]
6f65c10746 docs: add MalcolmMielle as a contributor for doc (#900)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2022-01-04 12:38:32 +01:00
Mike Cluck
e04409e74f Verify that openDailyNote receives a Date (#897) 2022-01-04 12:37:10 +01:00
Malcolm Mielle
dba0a72d98 Update publish to gitlab instructions (#898) 2022-01-04 12:35:00 +01:00
Fullchee Zhang
fd23b1d010 docs: Use ordered list to separate steps from details (#899) 2022-01-04 12:32:38 +01:00
memeplex
cfb946a5f2 Fix link autocompletion with tags (#885)
* Fix link autocompletion with tags
* Add test cases
2021-12-23 22:12:34 +01:00
memeplex
32c3a484d6 Improve linting (#887)
* Improve linting
2021-12-23 21:17:04 +01:00
70 changed files with 4682 additions and 2135 deletions

View File

@@ -806,6 +806,51 @@
"contributions": [
"doc"
]
},
{
"login": "MalcolmMielle",
"name": "Malcolm Mielle",
"avatar_url": "https://avatars.githubusercontent.com/u/4457840?v=4",
"profile": "http://malcolmmielle.wordpress.com/",
"contributions": [
"doc"
]
},
{
"login": "veesar",
"name": "Veesar",
"avatar_url": "https://avatars.githubusercontent.com/u/74916913?v=4",
"profile": "https://snippets.page/",
"contributions": [
"doc"
]
},
{
"login": "bentongxyz",
"name": "bentongxyz",
"avatar_url": "https://avatars.githubusercontent.com/u/60358804?v=4",
"profile": "https://github.com/bentongxyz",
"contributions": [
"code"
]
},
{
"login": "techCarpenter",
"name": "Brian DeVries",
"avatar_url": "https://avatars.githubusercontent.com/u/42778030?v=4",
"profile": "https://brianjdevries.com",
"contributions": [
"code"
]
},
{
"login": "cliffordfajardo",
"name": "Clifford Fajardo ",
"avatar_url": "https://avatars.githubusercontent.com/u/6743796?v=4",
"profile": "http://Cliffordfajardo.com",
"contributions": [
"tool"
]
}
],
"contributorsPerLine": 7,

View File

@@ -5,14 +5,40 @@
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "import"],
"env": { "node": true, "es6": true },
"plugins": ["@typescript-eslint", "import", "jest"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:jest/recommended"
],
"rules": {
"@typescript-eslint/class-name-casing": "warn",
"@typescript-eslint/semi": "warn",
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off",
"require-await": "warn"
}
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/interface-name-prefix": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": ["**/src/test/**", "**/src/**/*{test,spec}.ts"]
}
]
},
"settings": {
"import/core-modules": ["vscode"],
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true
}
}
},
"ignorePatterns": ["**/core/common/**", "*.js"],
"reportUnusedDisableDirectives": true
}

View File

@@ -1,23 +0,0 @@
---
name: Bug report
about: Create a report to help us be foamier
labels: bug
---
- Foam version: <!-- Check in the VSCode extension tab. -->
- Platform: Windows | Mac | Linux
- Issue occur on the [foam template](https://github.com/foambubble/foam-template) repo: Yes | No
**Summary**
<!-- A clear and concise description of what the bug is.-->
**Steps to reproduce**
1.
2.
**Additional information**
<!-- Add any other context about the problem here. -->
Feel free to attach any of the following that might help with debugging the issue:
- screenshots
- a zip with a minimal repo to reproduce the issue
- the Foam log in VsCode (see [instructions](https://github.com/foambubble/foam/blob/master/docs/features/foam-logging-in-vscode.md))

97
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
name: 'Bug report'
description: Create a report to help us improve
body:
- type: markdown
attributes:
value: |
Thank you for reporting an issue :pray:.
This issue tracker is for reporting bugs found in `foam` (https://github.com/foambubble).
If you have a question about how to achieve something and are struggling, please post a question
inside of either of the following places:
- Foam's Discussion's tab: https://github.com/foambubble/foam/discussions
- Foam's Discord channel: https://foambubble.github.io/join-discord/g
Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already:
- Foam's Issue's tab: https://github.com/foambubble/foam/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc
- Foam's closed issues tab: https://github.com/foambubble/foam/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed
- Foam's Discussions tab: https://github.com/foambubble/foam/discussions
The more information you fill in, the better the community can help you.
- type: textarea
id: description
attributes:
label: Describe the bug
description: Provide a clear and concise description of the challenge you are running into.
validations:
required: true
- type: input
id: reproducible_example
attributes:
label: Small Reproducible Example
description: |
Note:
- Your bug will may get fixed much faster if there is a way we can somehow run your example or code.
- To create a shareable example, consider cloning the following Foam Github template: https://github.com/foambubble/foam-template
- Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve.
placeholder: |
e.g. Link to your github repository containing a small reproducible example that the team can run.
validations:
required: false
- type: textarea
id: steps
attributes:
label: Steps to Reproduce the Bug or Issue
description: Describe the steps we have to take to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: Provide a clear and concise description of what you expected to happen.
placeholder: |
As a user, I expected ___ behavior but i am seeing ___
validations:
required: true
- type: textarea
id: screenshots_or_videos
attributes:
label: Screenshots or Videos
description: |
If applicable, add screenshots or a video to help explain your problem.
For more information on the supported file image/file types and the file size limits, please refer
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
placeholder: |
You can drag your video or image files inside of this editor ↓
- type: input
id: os
attributes:
label: Operating System Version
description: What opearting system are you using?
placeholder: |
- OS: [e.g. macOS, Windows, Linux]
validations:
required: true
- type: input
id: vscode_version
attributes:
label: Visual Studio Code Version
description: |
What version of Visual Studio Code are you using?
How to find Visual Studio Code Version: https://code.visualstudio.com/docs/supporting/FAQ#_how-do-i-find-the-version
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: |
Add any other context about the problem here.
The Foam log output for VSCode can be found here: https://github.com/foambubble/foam/blob/master/docs/features/foam-logging-in-vscode.md

View File

@@ -1,6 +0,0 @@
---
name: Feature request
about: Suggest an idea to help us be foamier
---
<!-- Describe the feature you'd like. -->

View File

@@ -0,0 +1,42 @@
name: Feature request
description: Suggest an idea for the `Foam` project
body:
- type: markdown
attributes:
value: |
This issue form is for requesting features only!
If you want to report a bug, please use the [bug report](https://github.com/foambubble/foam/issues/new?assignees=&labels=&template=bug_report.yml) form.
- type: textarea
validations:
required: true
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
- type: textarea
validations:
required: true
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
placeholder: |
As a user, I expected ___ behavior but ___ ...
Ideal Steps I would like to see:
1. Go to '...'
2. Click on '....'
3. ....
- type: textarea
validations:
required: true
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
attributes:
label: Screenshots or Videos
description: |
If applicable, add screenshots or a video to help explain your problem.
For more information on the supported file image/file types and the file size limits, please refer
to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files
placeholder: |
You can drag your video or image files inside of this editor ↓

View File

@@ -20,10 +20,10 @@
"**/node_modules/**/*",
"packages/**/*"
],
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.requireConfig": true,
"editor.formatOnSave": true,
"editor.tabSize": 2,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"jest.autoRun": "off",
"jest.rootPath": "packages/foam-vscode",
"jest.jestCommandLine": "yarn jest",

View File

@@ -1,6 +1,6 @@
# Backlinking
When using [[wikilinks]], you can find all notes that link to a specific note in the [VS Code Markdown Notes](https://marketplace.visualstudio.com/items?itemName=kortina.vscode-markdown-notes) **Backlinks Explorer**
When using [[wikilinks]], you can find all notes that link to a specific note in the **Backlinks Explorer**
- Run `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "backlinks" and run the **Explorer: Focus on Backlinks** view.
- Keep this pane always visible to discover relationships between your thoughts

View File

@@ -45,10 +45,9 @@ In addition, you can also use variables provided by Foam:
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new note. |
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
| `FOAM_SLUG` | The sluggified title of the note (using the default github slug method). If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
**Note:** neither the defaulting feature (eg. `${variable:default}`) nor the format feature (eg. `${variable/(.*)/${1:/upcase}/}`) (available to other variables) are available for these Foam-provided variables. See [#693](https://github.com/foambubble/foam/issues/693).
### `FOAM_DATE_*` variables
Foam defines its own set of datetime variables that have a similar behaviour as [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables).

View File

@@ -220,6 +220,13 @@ If that sounds like something you're interested in, I'd love to have you along o
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt=""/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
<td align="center"><a href="http://malcolmmielle.wordpress.com/"><img src="https://avatars.githubusercontent.com/u/4457840?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Malcolm Mielle</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MalcolmMielle" title="Documentation">📖</a></td>
<td align="center"><a href="https://snippets.page/"><img src="https://avatars.githubusercontent.com/u/74916913?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Veesar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=veesar" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bentongxyz"><img src="https://avatars.githubusercontent.com/u/60358804?v=4?s=60" width="60px;" alt=""/><br /><sub><b>bentongxyz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bentongxyz" title="Code">💻</a></td>
<td align="center"><a href="https://brianjdevries.com"><img src="https://avatars.githubusercontent.com/u/42778030?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brian DeVries</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=techCarpenter" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://Cliffordfajardo.com"><img src="https://avatars.githubusercontent.com/u/6743796?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Clifford Fajardo </b></sub></a><br /><a href="#tool-cliffordfajardo" title="Tools">🔧</a></td>
</tr>
</table>

View File

@@ -1,10 +1,10 @@
# GitHub Pages
- In VSCode workspace settings set `"foam.edit.linkReferenceDefinitions": "withoutExtensions"`
- Execute the “Foam: Run Janitor” command from the command palette.
- [Turn **GitHub Pages** on in your repository settings](https://guides.github.com/features/pages/).
- The default GitHub Pages template is called [Primer](https://github.com/pages-themes/primer). See Primer docs for how to customise html layouts and templates.
- GitHub Pages is built on [Jekyll](https://jekyllrb.com/), so it supports things like permalinks, front matter metadata etc.
1. In VSCode workspace settings set `"foam.edit.linkReferenceDefinitions": "withoutExtensions"`
2. Execute the “Foam: Run Janitor” command from the command palette.
3. [Turn **GitHub Pages** on in your repository settings](https://guides.github.com/features/pages/).
- The default GitHub Pages template is called [Primer](https://github.com/pages-themes/primer). See Primer docs for how to customise html layouts and templates.
- GitHub Pages is built on [Jekyll](https://jekyllrb.com/), so it supports things like permalinks, front matter metadata etc.
## How to publish locally

View File

@@ -2,13 +2,175 @@
You don't have to use GitHub to serve Foam pages. You can also use GitLab.
Gitlab pages can be kept private for private repo, so that your notes are still private.
## Setup a project
### Generate the directory from GitHub
Generate a solution using the [Foam template].
Generate a solution using the [Foam template](https://github.com/foambubble/foam-template).
Change the remote to GitLab, or copy all the files into a new GitLab repo.
Change the remote to GitLab, or copy all the files into a new GitLab repo
## Publishing pages with Gatsby
### Setup the Gatsby config
Add a .gatsby-config.js file where:
* `$REPO_NAME` correspond to the name of your gtlab repo.
* `$USER_NAME` correspond to your gitlab username.
```js
const path = require("path");
const pathPrefix = `/$REPO_NAME`;
// Change me
const siteMetadata = {
title: "A title",
shortName: "A short name",
description: "",
imageUrl: "/graph-visualisation.jpg",
siteUrl: "https://$USER_NAME.gitlab.io",
};
module.exports = {
siteMetadata,
pathPrefix,
flags: {
DEV_SSR: true,
},
plugins: [
`gatsby-plugin-sharp`,
{
resolve: "gatsby-theme-primer-wiki",
options: {
defaultColorMode: "night",
icon: "./path_to/logo.png",
sidebarComponents: ["tag", "category"],
nav: [
{
title: "Github",
url: "https://github.com/$USER_NAME/",
},
{
title: "Gitlab",
url: "https://gitlab.com/$USER_NAME/",
},
],
editUrl:
"https://gitlab.com/$USER_NAME/$REPO_NAME/tree/main/",
},
},
{
resolve: "gatsby-source-filesystem",
options: {
name: "content",
path: `${__dirname}`,
ignore: [`**/\.*/**/*`],
},
},
{
resolve: "gatsby-plugin-manifest",
options: {
name: siteMetadata.title,
short_name: siteMetadata.shortName,
start_url: pathPrefix,
background_color: `#f7f0eb`,
display: `standalone`,
icon: path.resolve(__dirname, "./path_to/logo.png"),
},
},
{
resolve: `gatsby-plugin-sitemap`,
},
{
resolve: "gatsby-plugin-robots-txt",
options: {
host: siteMetadata.siteUrl,
sitemap: `${siteMetadata.siteUrl}/sitemap/sitemap-index.xml`,
policy: [{ userAgent: "*", allow: "/" }],
},
},
],
};
```
And a `package.json` file containing:
```json
{
"private": true,
"name": "wiki",
"version": "1.0.0",
"license": "MIT",
"scripts": {
"develop": "gatsby develop -H 0.0.0.0",
"start": "gatsby develop -H 0.0.0.0",
"build": "gatsby build",
"clean": "gatsby clean",
"serve": "gatsby serve",
"test": "echo test"
},
"dependencies": {
"@primer/react": "^34.1.0",
"@primer/css": "^17.5.0",
"foam-cli": "^0.11.0",
"gatsby": "^3.12.0",
"gatsby-plugin-manifest": "^3.12.0",
"gatsby-plugin-robots-txt": "^1.6.9",
"gatsby-plugin-sitemap": "^5.4.0",
"gatsby-source-filesystem": "^3.12.0",
"gatsby-theme-primer-wiki": "^1.14.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
```
The theme will be based on [gatsby-theme-primer-wiki](https://github.com/theowenyoung/gatsby-theme-primer-wiki).
To test the theme locally first run `yarn install` and then use `gatsby develop` to serve the website.
See gatsby documentation for more details.
### Set-up the CI for deployment
Create a `.gitlab-ci.yml` file containing:
```yml
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Pages/Gatsby.gitlab-ci.yml
image: node:latest
stages:
- deploy
pages:
stage: deploy
# This folder is cached between builds
# https://docs.gitlab.com/ee/ci/yaml/index.html#cache
cache:
paths:
- node_modules/
# Enables git-lab CI caching. Both .cache and public must be cached, otherwise builds will fail.
- .cache/
- public/
script:
- yarn install
- ./node_modules/.bin/gatsby build --prefix-paths
artifacts:
paths:
- public
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
```
This pipeline will now serve your website on every push to the main branch of your project.
## Publish with Jekyll
### Add a _config.yaml
@@ -47,14 +209,14 @@ gem "jekyll-optional-front-matter"
Commit the file and push it to gitlab.
## Setup CI/CD
### Setup CI/CD
1. From the project home in GitLab click `Set up CI/CD`
2. Choose `Jekyll` as your template from the template dropdown
3. Click `commit`
4. Now when you go to CI / CD > Pipelines, you should see the code running
## Troubleshooting
### Troubleshooting
- *Could not locate Gemfile* - You didn't follow the steps above to [#Add a Gemlock file]
- *Conversion error: Jekyll::Converters::Scss encountered an error while converting* You need to reference a theme.

View File

@@ -11,8 +11,6 @@ We have two alternative #recipe for displaying diagrams in markdown:
You can use [Mermaid](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) plugin to draw and preview diagrams in your content.
⚠️ Be aware that Mermaid diagrams don't automatically get rendered in published Foams in [[publish-to-github-pages]], and would require you to eject to another static site generation approach that supports Mermaid plugins.
## Draw.io
[Draw.io](https://marketplace.visualstudio.com/items?itemName=hediet.vscode-drawio) extension allows you to create, edit, and display your diagrams without leaving Visual Studio Code. The `.drawio.svg` or `.drawio.png` files can be automatically embedded and displayed in published Foams, no export needed. FYI, the diagram below was made using Draw.io! You can check the diagram [here](../assets/images/diagram-drawio-demo.drawio.svg).

View File

@@ -9,3 +9,6 @@ There are a couple of options when it comes to clipping web pages:
- [Markdown Clipper](https://github.com/deathau/markdownload)
- A Firefox and Google Chrome extension to clip websites and download them into a readable markdown file.
- [Web Clipper](https://clipper.website/)
- A Firefox, Chrome and Edge extension to clip websites and save them directly to the GitHub repository into a readable markdown file.

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.17.2"
"version": "0.17.7"
}

View File

@@ -4,6 +4,34 @@ All notable changes to the "foam-vscode" extension will be documented in this fi
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [0.17.7] - 2022-03-29
Fixes and Improvements:
- Include links with sections in backlinks (#895)
- Improved navigation when document editor is already open
## [0.17.6] - 2022-03-03
Fixes and Improvements:
- Don't fail on error when scannig workspace (#943 - thanks @develmusa)
## [0.17.5] - 2022-02-22
Fixes and Improvements:
- Added FOAM_SLUG template variable (#865 - Thanks @techCarpenter)
## [0.17.4] - 2022-02-13
Fixes and Improvements:
- Improvements to Foam variables in templates (#882 - thanks @movermeyer)
- Foam variables can now be used just any other VS Code variables, including in combination with placeholders and transformers
## [0.17.3] - 2022-01-14
Fixes and Improvements:
- Fixed autocompletion with tags (#885 - thanks @memeplex)
- Improved "Open Daily Note" to be usabled in tasks (#897 - thanks @MCluck90)
## [0.17.2] - 2021-12-22
Fixes and Improvements:

View File

@@ -8,7 +8,7 @@
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.17.2",
"version": "0.17.7",
"license": "MIT",
"publisher": "foam",
"engines": {
@@ -394,10 +394,6 @@
"publish-extension": "yarn publish-extension-vscode && yarn publish-extension-openvsx && yarn npm-cleanup"
},
"devDependencies": {
"@babel/core": "^7.11.0",
"@babel/plugin-transform-runtime": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-typescript": "^7.10.4",
"@types/dateformat": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/lodash": "^4.14.157",
@@ -410,7 +406,9 @@
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"eslint": "^6.8.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jest": "^25.3.0",
"husky": "^4.2.5",
"jest": "^26.2.2",
"jest-extended": "^0.11.5",
@@ -426,6 +424,7 @@
"dateformat": "^3.0.3",
"detect-newline": "^3.1.0",
"fast-array-diff": "^1.0.1",
"github-slugger": "^1.4.0",
"glob": "^7.1.6",
"gray-matter": "^4.0.2",
"lodash": "^4.17.21",

View File

@@ -0,0 +1,811 @@
/*---------------------------------------------------------------------------------------------
* Originally taken from https://github.com/microsoft/vscode/blob/d31496c866683bdbccfc85bc11a3107d6c789b52/src/vs/editor/contrib/snippet/test/snippetParser.test.ts
* Here was the license:
*
* MIT License
*
* Copyright (c) 2015 - present Microsoft Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from './snippetParser';
describe('SnippetParser', () => {
test('Scanner', () => {
const scanner = new Scanner();
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('abc');
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('{{abc}}');
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('abc() ');
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.Format);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('abc 123');
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.Format);
assert.strictEqual(scanner.next().type, TokenType.Int);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('$foo');
assert.strictEqual(scanner.next().type, TokenType.Dollar);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('$foo_bar');
assert.strictEqual(scanner.next().type, TokenType.Dollar);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('$foo-bar');
assert.strictEqual(scanner.next().type, TokenType.Dollar);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.Dash);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('${foo}');
assert.strictEqual(scanner.next().type, TokenType.Dollar);
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('${1223:foo}');
assert.strictEqual(scanner.next().type, TokenType.Dollar);
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
assert.strictEqual(scanner.next().type, TokenType.Int);
assert.strictEqual(scanner.next().type, TokenType.Colon);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
assert.strictEqual(scanner.next().type, TokenType.EOF);
scanner.text('\\${}');
assert.strictEqual(scanner.next().type, TokenType.Backslash);
assert.strictEqual(scanner.next().type, TokenType.Dollar);
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
scanner.text('${foo/regex/format/option}');
assert.strictEqual(scanner.next().type, TokenType.Dollar);
assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.Forwardslash);
assert.strictEqual(scanner.next().type, TokenType.VariableName);
assert.strictEqual(scanner.next().type, TokenType.CurlyClose);
assert.strictEqual(scanner.next().type, TokenType.EOF);
});
function assertText(value: string, expected: string) {
const p = new SnippetParser();
const actual = p.text(value);
assert.strictEqual(actual, expected);
}
function assertMarker(input: TextmateSnippet | Marker[] | string, ...ctors: Function[]) {
let marker: Marker[];
if (input instanceof TextmateSnippet) {
marker = input.children;
} else if (typeof input === 'string') {
const p = new SnippetParser();
marker = p.parse(input).children;
} else {
marker = input;
}
while (marker.length > 0) {
let m = marker.pop();
let ctor = ctors.pop()!;
assert.ok(m instanceof ctor);
}
assert.strictEqual(marker.length, ctors.length);
assert.strictEqual(marker.length, 0);
}
function assertTextAndMarker(value: string, escaped: string, ...ctors: Function[]) {
assertText(value, escaped);
assertMarker(value, ...ctors);
}
function assertEscaped(value: string, expected: string) {
const actual = SnippetParser.escape(value);
assert.strictEqual(actual, expected);
}
test('Parser, escaped', function () {
assertEscaped('foo$0', 'foo\\$0');
assertEscaped('foo\\$0', 'foo\\\\\\$0');
assertEscaped('f$1oo$0', 'f\\$1oo\\$0');
assertEscaped('${1:foo}$0', '\\${1:foo\\}\\$0');
assertEscaped('$', '\\$');
});
test('Parser, text', () => {
assertText('$', '$');
assertText('\\\\$', '\\$');
assertText('{', '{');
assertText('\\}', '}');
assertText('\\abc', '\\abc');
assertText('foo${f:\\}}bar', 'foo}bar');
assertText('\\{', '\\{');
assertText('I need \\\\\\$', 'I need \\$');
assertText('\\', '\\');
assertText('\\{{', '\\{{');
assertText('{{', '{{');
assertText('{{dd', '{{dd');
assertText('}}', '}}');
assertText('ff}}', 'ff}}');
assertText('farboo', 'farboo');
assertText('far{{}}boo', 'far{{}}boo');
assertText('far{{123}}boo', 'far{{123}}boo');
assertText('far\\{{123}}boo', 'far\\{{123}}boo');
assertText('far{{id:bern}}boo', 'far{{id:bern}}boo');
assertText('far{{id:bern {{basel}}}}boo', 'far{{id:bern {{basel}}}}boo');
assertText('far{{id:bern {{id:basel}}}}boo', 'far{{id:bern {{id:basel}}}}boo');
assertText('far{{id:bern {{id2:basel}}}}boo', 'far{{id:bern {{id2:basel}}}}boo');
});
test('Parser, TM text', () => {
assertTextAndMarker('foo${1:bar}}', 'foobar}', Text, Placeholder, Text);
assertTextAndMarker('foo${1:bar}${2:foo}}', 'foobarfoo}', Text, Placeholder, Placeholder, Text);
assertTextAndMarker('foo${1:bar\\}${2:foo}}', 'foobar}foo', Text, Placeholder);
let [, placeholder] = new SnippetParser().parse('foo${1:bar\\}${2:foo}}').children;
let { children } = (<Placeholder>placeholder);
assert.strictEqual((<Placeholder>placeholder).index, 1);
assert.ok(children[0] instanceof Text);
assert.strictEqual(children[0].toString(), 'bar}');
assert.ok(children[1] instanceof Placeholder);
assert.strictEqual(children[1].toString(), 'foo');
});
test('Parser, placeholder', () => {
assertTextAndMarker('farboo', 'farboo', Text);
assertTextAndMarker('far{{}}boo', 'far{{}}boo', Text);
assertTextAndMarker('far{{123}}boo', 'far{{123}}boo', Text);
assertTextAndMarker('far\\{{123}}boo', 'far\\{{123}}boo', Text);
});
test('Parser, literal code', () => {
assertTextAndMarker('far`123`boo', 'far`123`boo', Text);
assertTextAndMarker('far\\`123\\`boo', 'far\\`123\\`boo', Text);
});
test('Parser, variables/tabstop', () => {
assertTextAndMarker('$far-boo', '-boo', Variable, Text);
assertTextAndMarker('\\$far-boo', '$far-boo', Text);
assertTextAndMarker('far$farboo', 'far', Text, Variable);
assertTextAndMarker('far${farboo}', 'far', Text, Variable);
assertTextAndMarker('$123', '', Placeholder);
assertTextAndMarker('$farboo', '', Variable);
assertTextAndMarker('$far12boo', '', Variable);
assertTextAndMarker('000_${far}_000', '000__000', Text, Variable, Text);
assertTextAndMarker('FFF_${TM_SELECTED_TEXT}_FFF$0', 'FFF__FFF', Text, Variable, Text, Placeholder);
});
test('Parser, variables/placeholder with defaults', () => {
assertTextAndMarker('${name:value}', 'value', Variable);
assertTextAndMarker('${1:value}', 'value', Placeholder);
assertTextAndMarker('${1:bar${2:foo}bar}', 'barfoobar', Placeholder);
assertTextAndMarker('${name:value', '${name:value', Text);
assertTextAndMarker('${1:bar${2:foobar}', '${1:barfoobar', Text, Placeholder);
});
test('Parser, variable transforms', function () {
assertTextAndMarker('${foo///}', '', Variable);
assertTextAndMarker('${foo/regex/format/gmi}', '', Variable);
assertTextAndMarker('${foo/([A-Z][a-z])/format/}', '', Variable);
// invalid regex
assertTextAndMarker('${foo/([A-Z][a-z])/format/GMI}', '${foo/([A-Z][a-z])/format/GMI}', Text);
assertTextAndMarker('${foo/([A-Z][a-z])/format/funky}', '${foo/([A-Z][a-z])/format/funky}', Text);
assertTextAndMarker('${foo/([A-Z][a-z]/format/}', '${foo/([A-Z][a-z]/format/}', Text);
// tricky regex
assertTextAndMarker('${foo/m\\/atch/$1/i}', '', Variable);
assertMarker('${foo/regex\/format/options}', Text);
// incomplete
assertTextAndMarker('${foo///', '${foo///', Text);
assertTextAndMarker('${foo/regex/format/options', '${foo/regex/format/options', Text);
// format string
assertMarker('${foo/.*/${0:fooo}/i}', Variable);
assertMarker('${foo/.*/${1}/i}', Variable);
assertMarker('${foo/.*/$1/i}', Variable);
assertMarker('${foo/.*/This-$1-encloses/i}', Variable);
assertMarker('${foo/.*/complex${1:else}/i}', Variable);
assertMarker('${foo/.*/complex${1:-else}/i}', Variable);
assertMarker('${foo/.*/complex${1:+if}/i}', Variable);
assertMarker('${foo/.*/complex${1:?if:else}/i}', Variable);
assertMarker('${foo/.*/complex${1:/upcase}/i}', Variable);
});
test('Parser, placeholder transforms', function () {
assertTextAndMarker('${1///}', '', Placeholder);
assertTextAndMarker('${1/regex/format/gmi}', '', Placeholder);
assertTextAndMarker('${1/([A-Z][a-z])/format/}', '', Placeholder);
// tricky regex
assertTextAndMarker('${1/m\\/atch/$1/i}', '', Placeholder);
assertMarker('${1/regex\/format/options}', Text);
// incomplete
assertTextAndMarker('${1///', '${1///', Text);
assertTextAndMarker('${1/regex/format/options', '${1/regex/format/options', Text);
});
test('No way to escape forward slash in snippet regex #36715', function () {
assertMarker('${TM_DIRECTORY/src\\//$1/}', Variable);
});
test('No way to escape forward slash in snippet format section #37562', function () {
assertMarker('${TM_SELECTED_TEXT/a/\\/$1/g}', Variable);
assertMarker('${TM_SELECTED_TEXT/a/in\\/$1ner/g}', Variable);
assertMarker('${TM_SELECTED_TEXT/a/end\\//g}', Variable);
});
test('Parser, placeholder with choice', () => {
assertTextAndMarker('${1|one,two,three|}', 'one', Placeholder);
assertTextAndMarker('${1|one|}', 'one', Placeholder);
assertTextAndMarker('${1|one1,two2|}', 'one1', Placeholder);
assertTextAndMarker('${1|one1\\,two2|}', 'one1,two2', Placeholder);
assertTextAndMarker('${1|one1\\|two2|}', 'one1|two2', Placeholder);
assertTextAndMarker('${1|one1\\atwo2|}', 'one1\\atwo2', Placeholder);
assertTextAndMarker('${1|one,two,three,|}', '${1|one,two,three,|}', Text);
assertTextAndMarker('${1|one,', '${1|one,', Text);
const p = new SnippetParser();
const snippet = p.parse('${1|one,two,three|}');
assertMarker(snippet, Placeholder);
const expected = [Placeholder, Text, Text, Text];
snippet.walk(marker => {
assert.strictEqual(marker, expected.shift());
return true;
});
});
test('Snippet choices: unable to escape comma and pipe, #31521', function () {
assertTextAndMarker('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(not, not);', Text, Placeholder, Text);
});
test('Marker, toTextmateString()', function () {
function assertTextsnippetString(input: string, expected: string): void {
const snippet = new SnippetParser().parse(input);
const actual = snippet.toTextmateString();
assert.strictEqual(actual, expected);
}
assertTextsnippetString('$1', '$1');
assertTextsnippetString('\\$1', '\\$1');
assertTextsnippetString('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(${1|not\\, not, five, 5, 1 23|});');
assertTextsnippetString('console.log(${1|not\\, not, \\| five, 5, 1 23|});', 'console.log(${1|not\\, not, \\| five, 5, 1 23|});');
assertTextsnippetString('this is text', 'this is text');
assertTextsnippetString('this ${1:is ${2:nested with $var}}', 'this ${1:is ${2:nested with ${var}}}');
assertTextsnippetString('this ${1:is ${2:nested with $var}}}', 'this ${1:is ${2:nested with ${var}}}\\}');
});
test('Marker, toTextmateString() <-> identity', function () {
function assertIdent(input: string): void {
// full loop: (1) parse input, (2) generate textmate string, (3) parse, (4) ensure both trees are equal
const snippet = new SnippetParser().parse(input);
const input2 = snippet.toTextmateString();
const snippet2 = new SnippetParser().parse(input2);
function checkCheckChildren(marker1: Marker, marker2: Marker) {
assert.ok(marker1 instanceof Object.getPrototypeOf(marker2).constructor);
assert.ok(marker2 instanceof Object.getPrototypeOf(marker1).constructor);
assert.strictEqual(marker1.children.length, marker2.children.length);
assert.strictEqual(marker1.toString(), marker2.toString());
for (let i = 0; i < marker1.children.length; i++) {
checkCheckChildren(marker1.children[i], marker2.children[i]);
}
}
checkCheckChildren(snippet, snippet2);
}
assertIdent('$1');
assertIdent('\\$1');
assertIdent('console.log(${1|not\\, not, five, 5, 1 23|});');
assertIdent('console.log(${1|not\\, not, \\| five, 5, 1 23|});');
assertIdent('this is text');
assertIdent('this ${1:is ${2:nested with $var}}');
assertIdent('this ${1:is ${2:nested with $var}}}');
assertIdent('this ${1:is ${2:nested with $var}} and repeating $1');
});
test('Parser, choise marker', () => {
const { placeholders } = new SnippetParser().parse('${1|one,two,three|}');
assert.strictEqual(placeholders.length, 1);
assert.ok(placeholders[0].choice instanceof Choice);
assert.ok(placeholders[0].children[0] instanceof Choice);
assert.strictEqual((<Choice>placeholders[0].children[0]).options.length, 3);
assertText('${1|one,two,three|}', 'one');
assertText('\\${1|one,two,three|}', '${1|one,two,three|}');
assertText('${1\\|one,two,three|}', '${1\\|one,two,three|}');
assertText('${1||}', '${1||}');
});
test('Backslash character escape in choice tabstop doesn\'t work #58494', function () {
const { placeholders } = new SnippetParser().parse('${1|\\,,},$,\\|,\\\\|}');
assert.strictEqual(placeholders.length, 1);
assert.ok(placeholders[0].choice instanceof Choice);
});
test('Parser, only textmate', () => {
const p = new SnippetParser();
assertMarker(p.parse('far{{}}boo'), Text);
assertMarker(p.parse('far{{123}}boo'), Text);
assertMarker(p.parse('far\\{{123}}boo'), Text);
assertMarker(p.parse('far$0boo'), Text, Placeholder, Text);
assertMarker(p.parse('far${123}boo'), Text, Placeholder, Text);
assertMarker(p.parse('far\\${123}boo'), Text);
});
test('Parser, real world', () => {
let marker = new SnippetParser().parse('console.warn(${1: $TM_SELECTED_TEXT })').children;
assert.strictEqual(marker[0].toString(), 'console.warn(');
assert.ok(marker[1] instanceof Placeholder);
assert.strictEqual(marker[2].toString(), ')');
const placeholder = <Placeholder>marker[1];
assert.strictEqual(placeholder.index, 1);
assert.strictEqual(placeholder.children.length, 3);
assert.ok(placeholder.children[0] instanceof Text);
assert.ok(placeholder.children[1] instanceof Variable);
assert.ok(placeholder.children[2] instanceof Text);
assert.strictEqual(placeholder.children[0].toString(), ' ');
assert.strictEqual(placeholder.children[1].toString(), '');
assert.strictEqual(placeholder.children[2].toString(), ' ');
const nestedVariable = <Variable>placeholder.children[1];
assert.strictEqual(nestedVariable.name, 'TM_SELECTED_TEXT');
assert.strictEqual(nestedVariable.children.length, 0);
marker = new SnippetParser().parse('$TM_SELECTED_TEXT').children;
assert.strictEqual(marker.length, 1);
assert.ok(marker[0] instanceof Variable);
});
test('Parser, transform example', () => {
let { children } = new SnippetParser().parse('${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0');
//${1:name}
assert.ok(children[0] instanceof Placeholder);
assert.strictEqual(children[0].children.length, 1);
assert.strictEqual(children[0].children[0].toString(), 'name');
assert.strictEqual((<Placeholder>children[0]).transform, undefined);
// :
assert.ok(children[1] instanceof Text);
assert.strictEqual(children[1].toString(), ' : ');
//${2:type}
assert.ok(children[2] instanceof Placeholder);
assert.strictEqual(children[2].children.length, 1);
assert.strictEqual(children[2].children[0].toString(), 'type');
//${3/\\s:=(.*)/${1:+ :=}${1}/}
assert.ok(children[3] instanceof Placeholder);
assert.strictEqual(children[3].children.length, 0);
assert.notStrictEqual((<Placeholder>children[3]).transform, undefined);
let transform = (<Placeholder>children[3]).transform!;
assert.deepStrictEqual(transform.regexp.source, /\s:=(.*)/.source);
assert.strictEqual(transform.children.length, 2);
assert.ok(transform.children[0] instanceof FormatString);
assert.strictEqual((<FormatString>transform.children[0]).index, 1);
assert.strictEqual((<FormatString>transform.children[0]).ifValue, ' :=');
assert.ok(transform.children[1] instanceof FormatString);
assert.strictEqual((<FormatString>transform.children[1]).index, 1);
assert.ok(children[4] instanceof Text);
assert.strictEqual(children[4].toString(), ';\n');
});
// TODO @jrieken making this strictEqul causes circular json conversion errors
test('Parser, default placeholder values', () => {
assertMarker('errorContext: `${1:err}`, error: $1', Text, Placeholder, Text, Placeholder);
const [, p1, , p2] = new SnippetParser().parse('errorContext: `${1:err}`, error:$1').children;
assert.strictEqual((<Placeholder>p1).index, 1);
assert.strictEqual((<Placeholder>p1).children.length, 1);
assert.strictEqual((<Text>(<Placeholder>p1).children[0]).toString(), 'err');
assert.strictEqual((<Placeholder>p2).index, 1);
assert.strictEqual((<Placeholder>p2).children.length, 1);
assert.strictEqual((<Text>(<Placeholder>p2).children[0]).toString(), 'err');
});
// TODO @jrieken making this strictEqul causes circular json conversion errors
test('Parser, default placeholder values and one transform', () => {
assertMarker('errorContext: `${1:err}`, error: ${1/err/ok/}', Text, Placeholder, Text, Placeholder);
const [, p3, , p4] = new SnippetParser().parse('errorContext: `${1:err}`, error:${1/err/ok/}').children;
assert.strictEqual((<Placeholder>p3).index, 1);
assert.strictEqual((<Placeholder>p3).children.length, 1);
assert.strictEqual((<Text>(<Placeholder>p3).children[0]).toString(), 'err');
assert.strictEqual((<Placeholder>p3).transform, undefined);
assert.strictEqual((<Placeholder>p4).index, 1);
assert.strictEqual((<Placeholder>p4).children.length, 1);
assert.strictEqual((<Text>(<Placeholder>p4).children[0]).toString(), 'err');
assert.notStrictEqual((<Placeholder>p4).transform, undefined);
});
test('Repeated snippet placeholder should always inherit, #31040', function () {
assertText('${1:foo}-abc-$1', 'foo-abc-foo');
assertText('${1:foo}-abc-${1}', 'foo-abc-foo');
assertText('${1:foo}-abc-${1:bar}', 'foo-abc-foo');
assertText('${1}-abc-${1:foo}', 'foo-abc-foo');
});
test('backspace esapce in TM only, #16212', () => {
const actual = new SnippetParser().text('Foo \\\\${abc}bar');
assert.strictEqual(actual, 'Foo \\bar');
});
test('colon as variable/placeholder value, #16717', () => {
let actual = new SnippetParser().text('${TM_SELECTED_TEXT:foo:bar}');
assert.strictEqual(actual, 'foo:bar');
actual = new SnippetParser().text('${1:foo:bar}');
assert.strictEqual(actual, 'foo:bar');
});
test('incomplete placeholder', () => {
assertTextAndMarker('${1:}', '', Placeholder);
});
test('marker#len', () => {
function assertLen(template: string, ...lengths: number[]): void {
const snippet = new SnippetParser().parse(template, true);
snippet.walk(m => {
const expected = lengths.shift();
assert.strictEqual(m.len(), expected);
return true;
});
assert.strictEqual(lengths.length, 0);
}
assertLen('text$0', 4, 0);
assertLen('$1text$0', 0, 4, 0);
assertLen('te$1xt$0', 2, 0, 2, 0);
assertLen('errorContext: `${1:err}`, error: $0', 15, 0, 3, 10, 0);
assertLen('errorContext: `${1:err}`, error: $1$0', 15, 0, 3, 10, 0, 3, 0);
assertLen('$TM_SELECTED_TEXT$0', 0, 0);
assertLen('${TM_SELECTED_TEXT:def}$0', 0, 3, 0);
});
test('parser, parent node', function () {
let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true);
assert.strictEqual(snippet.placeholders.length, 3);
let [first, second] = snippet.placeholders;
assert.strictEqual(first.index, 1);
assert.strictEqual(second.index, 2);
assert.ok(second.parent === first);
assert.ok(first.parent === snippet);
snippet = new SnippetParser().parse('${VAR:default${1:value}}$0', true);
assert.strictEqual(snippet.placeholders.length, 2);
[first] = snippet.placeholders;
assert.strictEqual(first.index, 1);
assert.ok(snippet.children[0] instanceof Variable);
assert.ok(first.parent === snippet.children[0]);
});
test('TextmateSnippet#enclosingPlaceholders', () => {
let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true);
let [first, second] = snippet.placeholders;
assert.deepStrictEqual(snippet.enclosingPlaceholders(first), []);
assert.deepStrictEqual(snippet.enclosingPlaceholders(second), [first]);
});
test('TextmateSnippet#offset', () => {
let snippet = new SnippetParser().parse('te$1xt', true);
assert.strictEqual(snippet.offset(snippet.children[0]), 0);
assert.strictEqual(snippet.offset(snippet.children[1]), 2);
assert.strictEqual(snippet.offset(snippet.children[2]), 2);
snippet = new SnippetParser().parse('${TM_SELECTED_TEXT:def}', true);
assert.strictEqual(snippet.offset(snippet.children[0]), 0);
assert.strictEqual(snippet.offset((<Variable>snippet.children[0]).children[0]), 0);
// forgein marker
assert.strictEqual(snippet.offset(new Text('foo')), -1);
});
test('TextmateSnippet#placeholder', () => {
let snippet = new SnippetParser().parse('te$1xt$0', true);
let placeholders = snippet.placeholders;
assert.strictEqual(placeholders.length, 2);
snippet = new SnippetParser().parse('te$1xt$1$0', true);
placeholders = snippet.placeholders;
assert.strictEqual(placeholders.length, 3);
snippet = new SnippetParser().parse('te$1xt$2$0', true);
placeholders = snippet.placeholders;
assert.strictEqual(placeholders.length, 3);
snippet = new SnippetParser().parse('${1:bar${2:foo}bar}$0', true);
placeholders = snippet.placeholders;
assert.strictEqual(placeholders.length, 3);
});
test('TextmateSnippet#replace 1/2', function () {
let snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true);
assert.strictEqual(snippet.placeholders.length, 3);
const [, second] = snippet.placeholders;
assert.strictEqual(second.index, 2);
const enclosing = snippet.enclosingPlaceholders(second);
assert.strictEqual(enclosing.length, 1);
assert.strictEqual(enclosing[0].index, 1);
let nested = new SnippetParser().parse('ddd$1eee$0', true);
snippet.replace(second, nested.children);
assert.strictEqual(snippet.toString(), 'aaabbbdddeee');
assert.strictEqual(snippet.placeholders.length, 4);
assert.strictEqual(snippet.placeholders[0].index, 1);
assert.strictEqual(snippet.placeholders[1].index, 1);
assert.strictEqual(snippet.placeholders[2].index, 0);
assert.strictEqual(snippet.placeholders[3].index, 0);
const newEnclosing = snippet.enclosingPlaceholders(snippet.placeholders[1]);
assert.ok(newEnclosing[0] === snippet.placeholders[0]);
assert.strictEqual(newEnclosing.length, 1);
assert.strictEqual(newEnclosing[0].index, 1);
});
test('TextmateSnippet#replace 2/2', function () {
let snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true);
assert.strictEqual(snippet.placeholders.length, 3);
const [, second] = snippet.placeholders;
assert.strictEqual(second.index, 2);
let nested = new SnippetParser().parse('dddeee$0', true);
snippet.replace(second, nested.children);
assert.strictEqual(snippet.toString(), 'aaabbbdddeee');
assert.strictEqual(snippet.placeholders.length, 3);
});
test('Snippet order for placeholders, #28185', function () {
const _10 = new Placeholder(10);
const _2 = new Placeholder(2);
assert.strictEqual(Placeholder.compareByIndex(_10, _2), 1);
});
test('Maximum call stack size exceeded, #28983', function () {
new SnippetParser().parse('${1:${foo:${1}}}');
});
test('Snippet can freeze the editor, #30407', function () {
const seen = new Set<Marker>();
seen.clear();
new SnippetParser().parse('class ${1:${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} < ${2:Application}Controller\n $3\nend').walk(marker => {
assert.ok(!seen.has(marker));
seen.add(marker);
return true;
});
seen.clear();
new SnippetParser().parse('${1:${FOO:abc$1def}}').walk(marker => {
assert.ok(!seen.has(marker));
seen.add(marker);
return true;
});
});
test('Snippets: make parser ignore `${0|choice|}`, #31599', function () {
assertTextAndMarker('${0|foo,bar|}', '${0|foo,bar|}', Text);
assertTextAndMarker('${1|foo,bar|}', 'foo', Placeholder);
});
test('Transform -> FormatString#resolve', function () {
// shorthand functions
assert.strictEqual(new FormatString(1, 'upcase').resolve('foo'), 'FOO');
assert.strictEqual(new FormatString(1, 'downcase').resolve('FOO'), 'foo');
assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar'), 'Bar');
assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat');
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo');
assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-42-foo'), 'Bar42Foo');
assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-foo'), 'barFoo');
assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-42-foo'), 'bar42Foo');
assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input');
// if
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve(undefined), '');
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve(''), '');
assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve('bar'), 'foo');
// else
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve(undefined), 'foo');
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve(''), 'foo');
assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve('bar'), 'bar');
// if-else
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve(undefined), 'foo');
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve(''), 'foo');
assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve('baz'), 'bar');
});
test('Snippet variable transformation doesn\'t work if regex is complicated and snippet body contains \'$$\' #55627', function () {
const snippet = new SnippetParser().parse('const fileName = "${TM_FILENAME/(.*)\\..+$/$1/}"');
assert.strictEqual(snippet.toTextmateString(), 'const fileName = "${TM_FILENAME/(.*)\\..+$/${1}/}"');
});
test('[BUG] HTML attribute suggestions: Snippet session does not have end-position set, #33147', function () {
const { placeholders } = new SnippetParser().parse('src="$1"', true);
const [first, second] = placeholders;
assert.strictEqual(placeholders.length, 2);
assert.strictEqual(first.index, 1);
assert.strictEqual(second.index, 0);
});
test('Snippet optional transforms are not applied correctly when reusing the same variable, #37702', function () {
const transform = new Transform();
transform.appendChild(new FormatString(1, 'upcase'));
transform.appendChild(new FormatString(2, 'upcase'));
transform.regexp = /^(.)|-(.)/g;
assert.strictEqual(transform.resolve('my-file-name'), 'MyFileName');
const clone = transform.clone();
assert.strictEqual(clone.resolve('my-file-name'), 'MyFileName');
});
test('problem with snippets regex #40570', function () {
const snippet = new SnippetParser().parse('${TM_DIRECTORY/.*src[\\/](.*)/$1/}');
assertMarker(snippet, Variable);
});
test('Variable transformation doesn\'t work if undefined variables are used in the same snippet #51769', function () {
let transform = new Transform();
transform.appendChild(new Text('bar'));
transform.regexp = new RegExp('foo', 'gi');
assert.strictEqual(transform.toTextmateString(), '/foo/bar/ig');
});
test('Snippet parser freeze #53144', function () {
let snippet = new SnippetParser().parse('${1/(void$)|(.+)/${1:?-\treturn nil;}/}');
assertMarker(snippet, Placeholder);
});
test('snippets variable not resolved in JSON proposal #52931', function () {
assertTextAndMarker('FOO${1:/bin/bash}', 'FOO/bin/bash', Text, Placeholder);
});
test('Mirroring sequence of nested placeholders not selected properly on backjumping #58736', function () {
let snippet = new SnippetParser().parse('${3:nest1 ${1:nest2 ${2:nest3}}} $3');
assert.strictEqual(snippet.children.length, 3);
assert.ok(snippet.children[0] instanceof Placeholder);
assert.ok(snippet.children[1] instanceof Text);
assert.ok(snippet.children[2] instanceof Placeholder);
function assertParent(marker: Marker) {
marker.children.forEach(assertParent);
if (!(marker instanceof Placeholder)) {
return;
}
let found = false;
let m: Marker = marker;
while (m && !found) {
if (m.parent === snippet) {
found = true;
}
m = m.parent;
}
assert.ok(found);
}
let [, , clone] = snippet.children;
assertParent(clone);
});
test('Backspace can\'t be escaped in snippet variable transforms #65412', function () {
let snippet = new SnippetParser().parse('namespace ${TM_DIRECTORY/[\\/]/\\\\/g};');
assertMarker(snippet, Text, Variable, Text);
});
test('Snippet cannot escape closing bracket inside conditional insertion variable replacement #78883', function () {
let snippet = new SnippetParser().parse('${TM_DIRECTORY/(.+)/${1:+import { hello \\} from world}/}');
let variable = <Variable>snippet.children[0];
assert.strictEqual(snippet.children.length, 1);
assert.ok(variable instanceof Variable);
assert.ok(variable.transform);
assert.strictEqual(variable.transform!.children.length, 1);
assert.ok(variable.transform!.children[0] instanceof FormatString);
assert.strictEqual((<FormatString>variable.transform!.children[0]).ifValue, 'import { hello } from world');
assert.strictEqual((<FormatString>variable.transform!.children[0]).elseValue, undefined);
});
test('Snippet escape backslashes inside conditional insertion variable replacement #80394', function () {
let snippet = new SnippetParser().parse('${CURRENT_YEAR/(.+)/${1:+\\\\}/}');
let variable = <Variable>snippet.children[0];
assert.strictEqual(snippet.children.length, 1);
assert.ok(variable instanceof Variable);
assert.ok(variable.transform);
assert.strictEqual(variable.transform!.children.length, 1);
assert.ok(variable.transform!.children[0] instanceof FormatString);
assert.strictEqual((<FormatString>variable.transform!.children[0]).ifValue, '\\');
assert.strictEqual((<FormatString>variable.transform!.children[0]).elseValue, undefined);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,8 @@ export const applyTextEdit = (text: string, textEdit: TextEdit): string => {
const eol = detectNewline(text) || os.EOL;
const lines = text.split(eol);
const characters = text.split('');
let startOffset = getOffset(lines, textEdit.range.start, eol);
let endOffset = getOffset(lines, textEdit.range.end, eol);
const startOffset = getOffset(lines, textEdit.range.start, eol);
const endOffset = getOffset(lines, textEdit.range.end, eol);
const deleteCount = endOffset - startOffset;
const textToAppend = `${textEdit.newText}`;

View File

@@ -1,6 +1,6 @@
import { generateHeading } from '.';
import { TEST_DATA_DIR } from '../../test/test-utils';
import { MarkdownResourceProvider } from '../markdown-provider';
import { MarkdownResourceProvider } from '../services/markdown-provider';
import { bootstrap } from '../model/foam';
import { Resource } from '../model/note';
import { Range } from '../model/range';

View File

@@ -1,6 +1,6 @@
import { generateLinkReferences } from '.';
import { TEST_DATA_DIR } from '../../test/test-utils';
import { MarkdownResourceProvider } from '../markdown-provider';
import { MarkdownResourceProvider } from '../services/markdown-provider';
import { bootstrap } from '../model/foam';
import { Resource } from '../model/note';
import { Range } from '../model/range';

View File

@@ -3,7 +3,7 @@ import { Range } from '../model/range';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from '../markdown-provider';
} from '../services/markdown-provider';
import { getHeadingFromFileName } from '../utils';
import { FoamWorkspace } from '../model/workspace';
@@ -56,7 +56,7 @@ export const generateLinkReferences = (
const first = note.definitions[0];
const last = note.definitions[note.definitions.length - 1];
var nonGeneratedReferenceDefinitions = note.definitions;
let nonGeneratedReferenceDefinitions = note.definitions;
// if we have more definitions then referenced pages AND the page refers to a page
// we expect non-generated link definitions to be present
@@ -115,7 +115,7 @@ export const generateLinkReferences = (
return null;
}
var fullReferences = `${newReferences}`;
let fullReferences = `${newReferences}`;
// If there are any non-generated definitions, add those to the output as well
if (
nonGeneratedReferenceDefinitions.length > 0 &&

View File

@@ -1,489 +0,0 @@
import {
createMarkdownParser,
createMarkdownReferences,
ParserPlugin,
} from './markdown-provider';
import { DirectLink, WikiLink } from './model/note';
import { Logger } from './utils/log';
import { URI } from './model/uri';
import { FoamGraph } from './model/graph';
import { Range } from './model/range';
import { createTestWorkspace, getRandomURI } from '../test/test-utils';
Logger.setLevel('error');
const pageA = `
# Page A
## Section
- [[page-b]]
- [[page-c]]
- [[Page D]]
- [[page e]]
`;
const pageB = `
# Page B
This references [[page-a]]`;
const pageC = `
# Page C
`;
const pageD = `
# Page D
`;
const pageE = `
# Page E
`;
const createNoteFromMarkdown = (content: string, path?: string) =>
createMarkdownParser([]).parse(
path ? URI.file(path) : getRandomURI(),
content
);
describe('Markdown loader', () => {
it('Converts markdown to notes', () => {
const workspace = createTestWorkspace();
workspace.set(createNoteFromMarkdown(pageA, '/page-a.md'));
workspace.set(createNoteFromMarkdown(pageB, '/page-b.md'));
workspace.set(createNoteFromMarkdown(pageC, '/page-c.md'));
workspace.set(createNoteFromMarkdown(pageD, '/page-d.md'));
workspace.set(createNoteFromMarkdown(pageE, '/page-e.md'));
expect(
workspace
.list()
.map(n => n.uri.getName())
.sort()
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
});
it('Ingores external links', () => {
const note = createNoteFromMarkdown(
`this is a [link to google](https://www.google.com)`
);
expect(note.links.length).toEqual(0);
});
it('Ignores references to sections in the same file', () => {
const note = createNoteFromMarkdown(
`this is a [link to intro](#introduction)`
);
expect(note.links.length).toEqual(0);
});
it('Parses internal links correctly', () => {
const note = createNoteFromMarkdown(
'this is a [link to page b](../doc/page-b.md)'
);
expect(note.links.length).toEqual(1);
const link = note.links[0] as DirectLink;
expect(link.type).toEqual('link');
expect(link.label).toEqual('link to page b');
expect(link.target).toEqual('../doc/page-b.md');
});
it('Parses links that have formatting in label', () => {
const note = createNoteFromMarkdown(
'this is [**link** with __formatting__](../doc/page-b.md)'
);
expect(note.links.length).toEqual(1);
const link = note.links[0] as DirectLink;
expect(link.type).toEqual('link');
expect(link.label).toEqual('link with formatting');
expect(link.target).toEqual('../doc/page-b.md');
});
it('Parses wikilinks correctly', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(pageA, '/page-a.md');
const noteB = createNoteFromMarkdown(pageB, '/page-b.md');
const noteC = createNoteFromMarkdown(pageC, '/page-c.md');
const noteD = createNoteFromMarkdown(pageD, '/Page D.md');
const noteE = createNoteFromMarkdown(pageE, '/page e.md');
workspace
.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD)
.set(noteE);
const graph = FoamGraph.fromWorkspace(workspace);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
noteB.uri,
noteC.uri,
noteD.uri,
noteE.uri,
]);
});
it('Parses backlinks with an alias', () => {
const note = createNoteFromMarkdown(
'this is [[link|link alias]]. A link with spaces [[other link | spaced]]'
);
expect(note.links.length).toEqual(2);
let link = note.links[0] as WikiLink;
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[link|link alias]]');
expect(link.label).toEqual('link alias');
expect(link.target).toEqual('link');
link = note.links[1] as WikiLink;
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[other link | spaced]]');
expect(link.label).toEqual('spaced');
expect(link.target).toEqual('other link');
});
it('Skips wikilinks in codeblocks', () => {
const noteA = createNoteFromMarkdown(`
this is some text with our [[first-wikilink]].
\`\`\`
this is inside a [[codeblock]]
\`\`\`
this is some text with our [[second-wikilink]].
`);
expect(noteA.links.map(l => l.label)).toEqual([
'first-wikilink',
'second-wikilink',
]);
});
it('Skips wikilinks in inlined codeblocks', () => {
const noteA = createNoteFromMarkdown(`
this is some text with our [[first-wikilink]].
this is \`inside a [[codeblock]]\`
this is some text with our [[second-wikilink]].
`);
expect(noteA.links.map(l => l.label)).toEqual([
'first-wikilink',
'second-wikilink',
]);
});
});
describe('Note Title', () => {
it('should initialize note title if heading exists', () => {
const note = createNoteFromMarkdown(`
# Page A
this note has a title
`);
expect(note.title).toBe('Page A');
});
it('should support wikilinks and urls in title', () => {
const note = createNoteFromMarkdown(`
# Page A with [[wikilink]] and a [url](https://google.com)
this note has a title
`);
expect(note.title).toBe('Page A with wikilink and a url');
});
it('should default to file name if heading does not exist', () => {
const note = createNoteFromMarkdown(
`This file has no heading.`,
'/page-d.md'
);
expect(note.title).toEqual('page-d');
});
it('should give precedence to frontmatter title over other headings', () => {
const note = createNoteFromMarkdown(`
---
title: Note Title
date: 20-12-12
---
# Other Note Title
`);
expect(note.title).toBe('Note Title');
});
it('should support numbers', () => {
const note1 = createNoteFromMarkdown(`hello`, '/157.md');
expect(note1.title).toBe('157');
const note2 = createNoteFromMarkdown(`# 158`, '/157.md');
expect(note2.title).toBe('158');
const note3 = createNoteFromMarkdown(
`
---
title: 159
---
# 158
`,
'/157.md'
);
expect(note3.title).toBe('159');
});
it('should not break on empty titles (see #276)', () => {
const note = createNoteFromMarkdown(
`
#
this note has an empty title line
`,
'/Hello Page.md'
);
expect(note.title).toEqual('Hello Page');
});
});
describe('frontmatter', () => {
it('should parse yaml frontmatter', () => {
const note = createNoteFromMarkdown(`
---
title: Note Title
date: 20-12-12
---
# Other Note Title`);
expect(note.properties.title).toBe('Note Title');
expect(note.properties.date).toBe('20-12-12');
});
it('should parse empty frontmatter', () => {
const note = createNoteFromMarkdown(`
---
---
# Empty Frontmatter
`);
expect(note.properties).toEqual({});
});
it('should not fail when there are issues with parsing frontmatter', () => {
const note = createNoteFromMarkdown(`
---
title: - one
- two
- #
---
`);
expect(note.properties).toEqual({});
});
});
describe('wikilinks definitions', () => {
it('can generate links without file extension when includeExtension = false', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
workspace
.set(noteA)
.set(createNoteFromMarkdown(pageB, '/dir1/page-b.md'))
.set(createNoteFromMarkdown(pageC, '/dir1/page-c.md'));
const noExtRefs = createMarkdownReferences(workspace, noteA.uri, false);
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
});
it('can generate links with file extension when includeExtension = true', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
workspace
.set(noteA)
.set(createNoteFromMarkdown(pageB, '/dir1/page-b.md'))
.set(createNoteFromMarkdown(pageC, '/dir1/page-c.md'));
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
});
it('use relative paths', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(pageA, '/dir1/page-a.md');
workspace
.set(noteA)
.set(createNoteFromMarkdown(pageB, '/dir2/page-b.md'))
.set(createNoteFromMarkdown(pageC, '/dir3/page-c.md'));
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
expect(extRefs.map(r => r.url)).toEqual([
'../dir2/page-b.md',
'../dir3/page-c.md',
]);
});
});
describe('tags plugin', () => {
it('can find tags in the text of the note', () => {
const noteA = createNoteFromMarkdown(`
# this is a #heading
#this is some #text that includes #tags we #care-about.
`);
expect(noteA.tags).toEqual([
{ label: 'heading', range: Range.create(1, 12, 1, 20) },
{ label: 'this', range: Range.create(2, 0, 2, 5) },
{ label: 'text', range: Range.create(2, 14, 2, 19) },
{ label: 'tags', range: Range.create(2, 34, 2, 39) },
{ label: 'care-about', range: Range.create(2, 43, 2, 54) },
]);
});
it('will skip tags in codeblocks', () => {
const noteA = createNoteFromMarkdown(`
this is some #text that includes #tags we #care-about.
\`\`\`
this is a #codeblock
\`\`\`
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'text',
'tags',
'care-about',
]);
});
it('will skip tags in inlined codeblocks', () => {
const noteA = createNoteFromMarkdown(`
this is some #text that includes #tags we #care-about.
this is a \`inlined #codeblock\` `);
expect(noteA.tags.map(t => t.label)).toEqual([
'text',
'tags',
'care-about',
]);
});
it('can find tags as text in yaml', () => {
const noteA = createNoteFromMarkdown(`
---
tags: hello, world this_is_good
---
# this is a heading
this is some #text that includes #tags we #care-about.
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'hello',
'world',
'this_is_good',
'text',
'tags',
'care-about',
]);
});
it('can find tags as array in yaml', () => {
const noteA = createNoteFromMarkdown(`
---
tags: [hello, world, this_is_good]
---
# this is a heading
this is some #text that includes #tags we #care-about.
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'hello',
'world',
'this_is_good',
'text',
'tags',
'care-about',
]);
});
it('provides rough range for tags in yaml', () => {
// For now it's enough to just get the YAML block range
// in the future we might want to be more specific
const noteA = createNoteFromMarkdown(`
---
tags: [hello, world, this_is_good]
---
# this is a heading
this is some text
`);
expect(noteA.tags[0]).toEqual({
label: 'hello',
range: Range.create(1, 0, 3, 3),
});
});
});
describe('Sections plugin', () => {
it('should find sections within the note', () => {
const note = createNoteFromMarkdown(`
# Section 1
This is the content of section 1.
## Section 1.1
This is the content of section 1.1.
# Section 2
This is the content of section 2.
`);
expect(note.sections).toHaveLength(3);
expect(note.sections[0].label).toEqual('Section 1');
expect(note.sections[0].range).toEqual(Range.create(1, 0, 9, 0));
expect(note.sections[1].label).toEqual('Section 1.1');
expect(note.sections[1].range).toEqual(Range.create(5, 0, 9, 0));
expect(note.sections[2].label).toEqual('Section 2');
expect(note.sections[2].range).toEqual(Range.create(9, 0, 13, 0));
});
it('should support wikilinks and links in the section label', () => {
const note = createNoteFromMarkdown(`
# Section with [[wikilink]]
This is the content of section with wikilink
## Section with [url](https://google.com)
This is the content of section with url`);
expect(note.sections).toHaveLength(2);
expect(note.sections[0].label).toEqual('Section with wikilink');
expect(note.sections[1].label).toEqual('Section with url');
});
});
describe('parser plugins', () => {
const testPlugin: ParserPlugin = {
visit: (node, note) => {
if (node.type === 'heading') {
note.properties.hasHeading = true;
}
},
};
const parser = createMarkdownParser([testPlugin]);
it('can augment the parsing of the file', () => {
const note1 = parser.parse(
URI.file('/path/to/a'),
`
This is a test note without headings.
But with some content.
`
);
expect(note1.properties.hasHeading).toBeUndefined();
const note2 = parser.parse(
URI.file('/path/to/a'),
`
# This is a note with header
and some content`
);
expect(note2.properties.hasHeading).toBeTruthy();
});
});

View File

@@ -4,7 +4,7 @@ import { FoamWorkspace } from './workspace';
import { FoamGraph } from './graph';
import { ResourceParser } from './note';
import { ResourceProvider } from './provider';
import { createMarkdownParser } from '../markdown-provider';
import { createMarkdownParser } from '../services/markdown-parser';
import { FoamTags } from './tags';
export interface Services {

View File

@@ -0,0 +1,681 @@
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
import { FoamGraph } from './graph';
import { URI } from './uri';
describe('Graph', () => {
it('should use wikilink slugs to connect nodes', () => {
const workspace = createTestWorkspace();
const noteA = createTestNote({
uri: '/page-a.md',
links: [
{ slug: 'page-b' },
{ slug: 'page-c' },
{ slug: 'Page D' },
{ slug: 'page e' },
],
});
const noteB = createTestNote({
uri: '/page-b.md',
links: [{ slug: 'page-a' }],
});
const noteC = createTestNote({ uri: '/page-c.md' });
const noteD = createTestNote({ uri: '/Page D.md' });
const noteE = createTestNote({ uri: '/page e.md' });
workspace
.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD)
.set(noteE);
const graph = FoamGraph.fromWorkspace(workspace);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
noteB.uri,
noteC.uri,
noteD.uri,
noteE.uri,
]);
});
it('should include resources and placeholders', () => {
const ws = createTestWorkspace();
ws.set(
createTestNote({
uri: '/page-a.md',
links: [{ slug: 'placeholder-link' }],
})
);
ws.set(createTestNote({ uri: '/file.pdf' }));
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getAllNodes()
.map(uri => uri.path)
.sort()
).toEqual(['/file.pdf', '/page-a.md', 'placeholder-link']);
});
it('should support multiple connections between the same resources', () => {
const noteA = createTestNote({
uri: '/path/to/note-a.md',
});
const noteB = createTestNote({
uri: '/note-b.md',
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(noteA.uri)).toEqual([
{
source: noteB.uri,
target: noteA.uri,
link: expect.objectContaining({ type: 'link' }),
},
{
source: noteB.uri,
target: noteA.uri,
link: expect.objectContaining({ type: 'link' }),
},
]);
});
it('should keep the connection when removing a single link amongst several between two resources', () => {
const noteA = createTestNote({
uri: '/path/to/note-a.md',
});
const noteB = createTestNote({
uri: '/note-b.md',
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getBacklinks(noteA.uri).length).toEqual(2);
const noteBBis = createTestNote({
uri: '/note-b.md',
links: [{ to: noteA.uri.path }],
});
ws.set(noteBBis);
expect(graph.getBacklinks(noteA.uri).length).toEqual(1);
ws.dispose();
graph.dispose();
});
it('should create inbound connections for target note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(
createTestNote({
uri: '/somewhere/page-b.md',
links: [{ slug: 'page-a' }],
})
)
.set(
createTestNote({
uri: '/path/another/page-c.md',
links: [{ slug: '/path/to/page-a' }],
})
)
.set(
createTestNote({
uri: '/absolute/path/page-d.md',
links: [{ slug: '../to/page-a.md' }],
})
);
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getBacklinks(noteA.uri)
.map(link => link.source.path)
.sort()
).toEqual(['/path/another/page-c.md', '/somewhere/page-b.md']);
});
it('should support attachments', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [
// wikilink with extension
{ slug: 'attachment-a.pdf' },
// wikilink without extension
{ slug: 'attachment-b' },
],
});
const attachmentA = createTestNote({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentB = createTestNote({
uri: '/path/to/more/attachment-b.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
// Attachments require extension
expect(graph.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([]);
});
it('should resolve conflicts alphabetically - part 1', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'attachment-a.pdf' }],
});
const attachmentA = createTestNote({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentABis = createTestNote({
uri: '/path/to/attachment-a.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentABis);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
attachmentABis.uri,
]);
});
it('should resolve conflicts alphabetically - part 2', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'attachment-a.pdf' }],
});
const attachmentA = createTestNote({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentABis = createTestNote({
uri: '/path/to/attachment-a.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentABis)
.set(attachmentA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
attachmentABis.uri,
]);
});
});
describe('Placeholders', () => {
it('should treat direct links to non-existing files as placeholders', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/somewhere/from/page-a.md',
links: [{ to: '../page-b.md' }, { to: '/path/to/page-c.md' }],
});
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getAllConnections()[0]).toEqual({
source: noteA.uri,
target: URI.placeholder('/somewhere/page-b.md'),
link: expect.objectContaining({ type: 'link' }),
});
expect(graph.getAllConnections()[1]).toEqual({
source: noteA.uri,
target: URI.placeholder('/path/to/page-c.md'),
link: expect.objectContaining({ type: 'link' }),
});
});
it('should treat wikilinks without matching file as placeholders', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/somewhere/page-a.md',
links: [{ slug: 'page-b' }],
});
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getAllConnections()[0]).toEqual({
source: noteA.uri,
target: URI.placeholder('page-b'),
link: expect.objectContaining({ type: 'wikilink' }),
});
});
it('should treat wikilink with definition to non-existing file as placeholders', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/somewhere/page-a.md',
links: [{ slug: 'page-b' }, { slug: 'page-c' }],
});
noteA.definitions.push({
label: 'page-b',
url: './page-b.md',
});
noteA.definitions.push({
label: 'page-c',
url: '/path/to/page-c.md',
});
ws.set(noteA).set(
createTestNote({ uri: '/different/location/for/note-b.md' })
);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getAllConnections()[0]).toEqual({
source: noteA.uri,
target: URI.placeholder('/somewhere/page-b.md'),
link: expect.objectContaining({ type: 'wikilink' }),
});
expect(graph.getAllConnections()[1]).toEqual({
source: noteA.uri,
target: URI.placeholder('/path/to/page-c.md'),
link: expect.objectContaining({ type: 'wikilink' }),
});
});
it('should work with a placeholder named like a JS prototype property', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/page-a.md',
links: [{ slug: 'constructor' }],
});
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getAllNodes()
.map(uri => uri.path)
.sort()
).toEqual(['/page-a.md', 'constructor']);
});
});
describe('Regenerating graph after workspace changes', () => {
it('should update links when modifying a resource', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
links: [{ slug: 'page-c' }],
});
const noteC = createTestNote({
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
let graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
noteB.uri,
]);
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-c' }],
});
ws.set(noteABis);
// change is not propagated immediately
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
noteB.uri,
]);
// recompute the links
graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
expect(
graph
.getBacklinks(noteC.uri)
.map(link => link.source.path)
.sort()
).toEqual(['/path/to/another/page-b.md', '/path/to/page-a.md']);
graph.dispose();
ws.dispose();
});
it('should produce a placeholder for wikilinks pointing to a removed resource', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
const graph2 = FoamGraph.fromWorkspace(ws);
expect(() => ws.get(noteB.uri)).toThrow();
expect(graph2.contains(URI.placeholder('page-b'))).toBeTruthy();
});
it('should turn a placeholder into a connection when adding a resource matching a wikilink', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('page-b'),
]);
expect(graph.contains(URI.placeholder('page-b'))).toBeTruthy();
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
FoamGraph.fromWorkspace(ws);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
});
it('should produce a placeholder for direct links pointing to a removed resource', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
const graph2 = FoamGraph.fromWorkspace(ws);
expect(() => ws.get(noteB.uri)).toThrow();
expect(
graph2.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
});
it('should turn a placeholder into a connection when adding a resource matching a direct link', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('/path/to/another/page-b.md'),
]);
expect(() =>
ws.get(URI.placeholder('/path/to/another/page-b.md'))
).toThrow();
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
FoamGraph.fromWorkspace(ws);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
});
it('should remove the placeholder from graph when removing all links to it', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = createTestWorkspace().set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [],
});
ws.set(noteABis);
const graph2 = FoamGraph.fromWorkspace(ws);
expect(
graph2.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeFalsy();
});
});
describe('Updating graph on workspace state', () => {
it('should automatically update the links when modifying a resource', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
links: [{ slug: 'page-c' }],
});
const noteC = createTestNote({
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
noteB.uri,
]);
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-c' }],
});
ws.set(noteABis);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
expect(
graph
.getBacklinks(noteC.uri)
.map(link => link.source.path)
.sort()
).toEqual(['/path/to/another/page-b.md', '/path/to/page-a.md']);
ws.dispose();
graph.dispose();
});
it('should produce a placeholder for wikilinks pointing to a removed resource', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
expect(() => ws.get(noteB.uri)).toThrow();
ws.dispose();
graph.dispose();
});
it('should turn a placeholder into a connection when adding a resource matching a wikilink', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('page-b'),
]);
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
ws.dispose();
graph.dispose();
});
it('should produce a placeholder for direct links pointing to a removed resource', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
expect(() => ws.get(noteB.uri)).toThrow();
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
ws.dispose();
graph.dispose();
});
it('should turn a placeholder into a connection when adding a resource matching a direct link', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('/path/to/another/page-b.md'),
]);
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
ws.dispose();
graph.dispose();
});
it('should remove the placeholder from graph when removing all links to it', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [],
});
ws.set(noteABis);
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeFalsy();
ws.dispose();
graph.dispose();
});
});

View File

@@ -76,9 +76,9 @@ export class FoamGraph implements IDisposable {
*/
public static fromWorkspace(
workspace: FoamWorkspace,
keepMonitoring: boolean = false
keepMonitoring = false
): FoamGraph {
let graph = new FoamGraph(workspace);
const graph = new FoamGraph(workspace);
workspace.list().forEach(resource => graph.resolveResource(resource));
if (keepMonitoring) {
@@ -99,7 +99,7 @@ export class FoamGraph implements IDisposable {
private updateLinksRelatedToAddedResource(resource: Resource) {
// check if any existing connection can be filled by new resource
let resourcesToUpdate: URI[] = [];
const resourcesToUpdate: URI[] = [];
for (const placeholderId of this.placeholders.keys()) {
// quick and dirty check for affected resources
if (resource.uri.path.endsWith(placeholderId + '.md')) {

View File

@@ -9,23 +9,14 @@ export interface NoteSource {
eol: string;
}
export interface WikiLink {
type: 'wikilink';
export interface ResourceLink {
type: 'wikilink' | 'link';
target: string;
label: string;
rawText: string;
range: Range;
}
export interface DirectLink {
type: 'link';
label: string;
target: string;
range: Range;
}
export type ResourceLink = WikiLink | DirectLink;
export interface NoteLinkDefinition {
label: string;
url: string;

View File

@@ -20,9 +20,9 @@ export class FoamTags implements IDisposable {
*/
public static fromWorkspace(
workspace: FoamWorkspace,
keepMonitoring: boolean = false
keepMonitoring = false
): FoamTags {
let tags = new FoamTags();
const tags = new FoamTags();
workspace
.list()

View File

@@ -58,7 +58,7 @@ export class URI {
}
static file(value: string): URI {
let [path, authority] = pathUtils.fromFsPath(value);
const [path, authority] = pathUtils.fromFsPath(value);
return new URI({ scheme: 'file', authority, path });
}
@@ -122,6 +122,13 @@ export class URI {
return new URI({ ...this, fragment });
}
/**
* Returns a URI without the fragment and query information
*/
asPlain(): URI {
return new URI({ ...this, fragment: '', query: '' });
}
isPlaceholder(): boolean {
return this.scheme === 'placeholder';
}
@@ -187,6 +194,7 @@ function encode(uri: URI, skipEncoding: boolean): string {
: encodeURIComponentMinimal;
let res = '';
// eslint-disable-next-line prefer-const
let { scheme, authority, path, query, fragment } = uri;
if (scheme) {
res += scheme;

View File

@@ -1,5 +1,4 @@
import { FoamWorkspace } from './workspace';
import { FoamGraph } from './graph';
import { Logger } from '../utils/log';
import { URI } from './uri';
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
@@ -7,7 +6,7 @@ import { createTestNote, createTestWorkspace } from '../../test/test-utils';
Logger.setLevel('error');
describe('Workspace resources', () => {
it('Adds notes to workspace', () => {
it('should allow adding notes to the workspace', () => {
const ws = createTestWorkspace();
ws.set(createTestNote({ uri: '/page-a.md' }));
ws.set(createTestNote({ uri: '/page-b.md' }));
@@ -21,7 +20,7 @@ describe('Workspace resources', () => {
).toEqual(['/page-a.md', '/page-b.md', '/page-c.md']);
});
it('Listing resources includes all notes', () => {
it('should includes all notes when listing resources', () => {
const ws = createTestWorkspace();
ws.set(createTestNote({ uri: '/page-a.md' }));
ws.set(createTestNote({ uri: '/file.pdf' }));
@@ -34,7 +33,7 @@ describe('Workspace resources', () => {
).toEqual(['/file.pdf', '/page-a.md']);
});
it('Fails if getting non-existing note', () => {
it('should fail when trying to get a non-existing note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
});
@@ -47,21 +46,21 @@ describe('Workspace resources', () => {
expect(() => ws.get(uri)).toThrow();
});
it('Should work with a resource named like a JS prototype property', () => {
it('should work with a resource named like a JS prototype property', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({ uri: '/somewhere/constructor.md' });
ws.set(noteA);
expect(ws.list()).toEqual([noteA]);
});
it('#851 - listing by ID should not return files with same suffix', () => {
it('should not return files with same suffix when listing by ID - #851', () => {
const ws = createTestWorkspace()
.set(createTestNote({ uri: 'test-file.md' }))
.set(createTestNote({ uri: 'file.md' }));
expect(ws.listByIdentifier('file').length).toEqual(1);
});
it('Support dendron-style names', () => {
it('should support dendron-style names', () => {
const ws = createTestWorkspace()
.set(createTestNote({ uri: 'note.pdf' }))
.set(createTestNote({ uri: 'note.md' }))
@@ -80,7 +79,7 @@ describe('Workspace resources', () => {
}
});
it('Should include fragment when finding resource URI', () => {
it('should keep the fragment information when finding a resource', () => {
const ws = createTestWorkspace()
.set(createTestNote({ uri: 'test-file.md' }))
.set(createTestNote({ uri: 'file.md' }));
@@ -90,79 +89,6 @@ describe('Workspace resources', () => {
});
});
describe('Graph', () => {
it('contains notes and placeholders', () => {
const ws = createTestWorkspace();
ws.set(
createTestNote({
uri: '/page-a.md',
links: [{ slug: 'placeholder-link' }],
})
);
ws.set(createTestNote({ uri: '/file.pdf' }));
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getAllNodes()
.map(uri => uri.path)
.sort()
).toEqual(['/file.pdf', '/page-a.md', 'placeholder-link']);
});
it('Supports multiple connections between the same resources', () => {
const noteA = createTestNote({
uri: '/path/to/note-a.md',
});
const noteB = createTestNote({
uri: '/note-b.md',
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(noteA.uri)).toEqual([
{
source: noteB.uri,
target: noteA.uri,
link: expect.objectContaining({ type: 'link' }),
},
{
source: noteB.uri,
target: noteA.uri,
link: expect.objectContaining({ type: 'link' }),
},
]);
});
it('Supports removing a single link amongst several between two resources', () => {
const noteA = createTestNote({
uri: '/path/to/note-a.md',
});
const noteB = createTestNote({
uri: '/note-b.md',
links: [{ to: noteA.uri.path }, { to: noteA.uri.path }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(noteB);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getBacklinks(noteA.uri).length).toEqual(2);
const noteBBis = createTestNote({
uri: '/note-b.md',
links: [{ to: noteA.uri.path }],
});
ws.set(noteBBis);
expect(graph.getBacklinks(noteA.uri).length).toEqual(1);
ws.dispose();
graph.dispose();
});
});
describe('Identifier computation', () => {
it('should compute the minimum identifier to resolve a name clash', () => {
const first = createTestNote({
@@ -210,7 +136,7 @@ describe('Identifier computation', () => {
[['/project/home/todo', '/other/todo', '/something/else'], 'car/todo'],
[['/family/car/todo', '/other/todo'], 'project/car/todo'],
[[], 'todo'],
])('Find shortest identifier', (haystack, id) => {
])('should find shortest identifier', (haystack, id) => {
expect(FoamWorkspace.getShortestIdentifier(needle, haystack)).toEqual(id);
});
@@ -225,7 +151,7 @@ describe('Identifier computation', () => {
expect(identifier).toEqual('car/todo');
});
it('should return best guess when no solution is possible', () => {
it('should return the best guess when no solution is possible', () => {
/**
* In this case there is no way to uniquely identify the element,
* our fallback is to just return the "least wrong" result, basically
@@ -242,755 +168,3 @@ describe('Identifier computation', () => {
expect(identifier).toEqual('project/car/todo');
});
});
describe('Wikilinks', () => {
it('Can be defined with basename, relative path, absolute path, extension', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [
// wikilink
{ slug: 'page-b' },
// relative path wikilink
{ slug: '../another/page-c.md' },
// absolute path wikilink
{ slug: '/absolute/path/page-d' },
// wikilink with extension
{ slug: 'page-e.md' },
// wikilink to placeholder
{ slug: 'placeholder-test' },
],
});
const ws = createTestWorkspace()
.set(noteA)
.set(createTestNote({ uri: '/somewhere/page-b.md' }))
.set(createTestNote({ uri: '/path/another/page-c.md' }))
.set(createTestNote({ uri: '/absolute/path/page-d.md' }))
.set(createTestNote({ uri: '/absolute/path/page-e.md' }));
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getLinks(noteA.uri)
.map(link => link.target.path)
.sort()
).toEqual([
'/absolute/path/page-d.md',
'/absolute/path/page-e.md',
'/path/another/page-c.md',
'/somewhere/page-b.md',
'placeholder-test',
]);
});
it('Creates inbound connections for target note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const ws = createTestWorkspace()
.set(noteA)
.set(
createTestNote({
uri: '/somewhere/page-b.md',
links: [{ slug: 'page-a' }],
})
)
.set(
createTestNote({
uri: '/path/another/page-c.md',
links: [{ slug: '/path/to/page-a' }],
})
)
.set(
createTestNote({
uri: '/absolute/path/page-d.md',
links: [{ slug: '../to/page-a.md' }],
})
);
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getBacklinks(noteA.uri)
.map(link => link.source.path)
.sort()
).toEqual(['/path/another/page-c.md', '/somewhere/page-b.md']);
});
it('Uses wikilink definitions when available to resolve target', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/somewhere/from/page-a.md',
links: [{ slug: 'page-b' }],
});
noteA.definitions.push({
label: 'page-b',
url: '../to/page-b.md',
});
const noteB = createTestNote({
uri: '/somewhere/to/page-b.md',
});
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getAllConnections()[0]).toEqual({
source: noteA.uri,
target: noteB.uri,
link: expect.objectContaining({ type: 'wikilink', label: 'page-b' }),
});
});
it('Resolves wikilink referencing more than one note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB1 = createTestNote({ uri: '/path/to/another/page-b.md' });
const noteB2 = createTestNote({ uri: '/path/to/more/page-b.md' });
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB1)
.set(noteB2);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri)).toEqual([
{
source: noteA.uri,
target: noteB1.uri,
link: expect.objectContaining({ type: 'wikilink' }),
},
]);
});
it('Resolves path wikilink in case of name conflict', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: './more/page-b' }, { slug: 'yet/page-b' }],
});
const noteB1 = createTestNote({ uri: '/path/to/another/page-b.md' });
const noteB2 = createTestNote({ uri: '/path/to/more/page-b.md' });
const noteB3 = createTestNote({ uri: '/path/to/yet/page-b.md' });
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB1)
.set(noteB2)
.set(noteB3);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
noteB2.uri,
noteB3.uri,
]);
});
it('Supports attachments', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [
// wikilink with extension
{ slug: 'attachment-a.pdf' },
// wikilink without extension
{ slug: 'attachment-b' },
],
});
const attachmentA = createTestNote({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentB = createTestNote({
uri: '/path/to/more/attachment-b.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(attachmentA.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
// Attachments require extension
expect(graph.getBacklinks(attachmentB.uri).map(l => l.source)).toEqual([]);
});
it('Resolves conflicts alphabetically - part 1', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'attachment-a.pdf' }],
});
const attachmentA = createTestNote({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentABis = createTestNote({
uri: '/path/to/attachment-a.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentABis);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
attachmentABis.uri,
]);
});
it('Resolves conflicts alphabetically - part 2', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'attachment-a.pdf' }],
});
const attachmentA = createTestNote({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentABis = createTestNote({
uri: '/path/to/attachment-a.pdf',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(attachmentABis)
.set(attachmentA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
attachmentABis.uri,
]);
});
it('Handles capitalization of files and wikilinks correctly', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [
// uppercased filename, lowercased slug
{ slug: 'page-b' },
// lowercased filename, camelcased wikilink
{ slug: 'Page-C' },
// lowercased filename, lowercased wikilink
{ slug: 'page-d' },
],
});
const ws = createTestWorkspace()
.set(noteA)
.set(createTestNote({ uri: '/somewhere/PAGE-B.md' }))
.set(createTestNote({ uri: '/path/another/page-c.md' }))
.set(createTestNote({ uri: '/path/another/page-d.md' }));
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getLinks(noteA.uri)
.map(link => link.target.path)
.sort()
).toEqual([
'/path/another/page-c.md',
'/path/another/page-d.md',
'/somewhere/PAGE-B.md',
]);
});
});
describe('Markdown direct links', () => {
it('Support absolute and relative path', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: './another/page-b.md' }, { to: 'more/page-c.md' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
links: [{ to: '../../to/page-a.md' }],
});
const noteC = createTestNote({
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getLinks(noteA.uri)
.map(link => link.target.path)
.sort()
).toEqual(['/path/to/another/page-b.md', '/path/to/more/page-c.md']);
expect(graph.getLinks(noteB.uri).map(l => l.target)).toEqual([noteA.uri]);
expect(graph.getBacklinks(noteA.uri).map(l => l.source)).toEqual([
noteB.uri,
]);
expect(graph.getConnections(noteA.uri)).toEqual([
{
source: noteA.uri,
target: noteB.uri,
link: expect.objectContaining({ type: 'link' }),
},
{
source: noteA.uri,
target: noteC.uri,
link: expect.objectContaining({ type: 'link' }),
},
{
source: noteB.uri,
target: noteA.uri,
link: expect.objectContaining({ type: 'link' }),
},
]);
});
});
describe('Placeholders', () => {
it('Treats direct links to non-existing files as placeholders', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/somewhere/from/page-a.md',
links: [{ to: '../page-b.md' }, { to: '/path/to/page-c.md' }],
});
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getAllConnections()[0]).toEqual({
source: noteA.uri,
target: URI.placeholder('/somewhere/page-b.md'),
link: expect.objectContaining({ type: 'link' }),
});
expect(graph.getAllConnections()[1]).toEqual({
source: noteA.uri,
target: URI.placeholder('/path/to/page-c.md'),
link: expect.objectContaining({ type: 'link' }),
});
});
it('Treats wikilinks without matching file as placeholders', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/somewhere/page-a.md',
links: [{ slug: 'page-b' }],
});
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getAllConnections()[0]).toEqual({
source: noteA.uri,
target: URI.placeholder('page-b'),
link: expect.objectContaining({ type: 'wikilink' }),
});
});
it('Treats wikilink with definition to non-existing file as placeholders', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/somewhere/page-a.md',
links: [{ slug: 'page-b' }, { slug: 'page-c' }],
});
noteA.definitions.push({
label: 'page-b',
url: './page-b.md',
});
noteA.definitions.push({
label: 'page-c',
url: '/path/to/page-c.md',
});
ws.set(noteA).set(
createTestNote({ uri: '/different/location/for/note-b.md' })
);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getAllConnections()[0]).toEqual({
source: noteA.uri,
target: URI.placeholder('/somewhere/page-b.md'),
link: expect.objectContaining({ type: 'wikilink' }),
});
expect(graph.getAllConnections()[1]).toEqual({
source: noteA.uri,
target: URI.placeholder('/path/to/page-c.md'),
link: expect.objectContaining({ type: 'wikilink' }),
});
});
it('Should work with a placeholder named like a JS prototype property', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/page-a.md',
links: [{ slug: 'constructor' }],
});
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph
.getAllNodes()
.map(uri => uri.path)
.sort()
).toEqual(['/page-a.md', 'constructor']);
});
});
describe('Updating workspace happy path', () => {
it('Update links when modifying note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
links: [{ slug: 'page-c' }],
});
const noteC = createTestNote({
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
let graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
noteB.uri,
]);
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-c' }],
});
ws.set(noteABis);
// change is not propagated immediately
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
noteB.uri,
]);
// recompute the links
graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
expect(
graph
.getBacklinks(noteC.uri)
.map(link => link.source.path)
.sort()
).toEqual(['/path/to/another/page-b.md', '/path/to/page-a.md']);
});
it('Removing target note should produce placeholder for wikilinks', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
const graph2 = FoamGraph.fromWorkspace(ws);
expect(() => ws.get(noteB.uri)).toThrow();
expect(graph2.contains(URI.placeholder('page-b'))).toBeTruthy();
});
it('Adding note should replace placeholder for wikilinks', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('page-b'),
]);
expect(graph.contains(URI.placeholder('page-b'))).toBeTruthy();
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
FoamGraph.fromWorkspace(ws);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
});
it('Removing target note should produce placeholder for direct links', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
const graph2 = FoamGraph.fromWorkspace(ws);
expect(() => ws.get(noteB.uri)).toThrow();
expect(
graph2.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
});
it('Adding note should replace placeholder for direct links', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('/path/to/another/page-b.md'),
]);
expect(() =>
ws.get(URI.placeholder('/path/to/another/page-b.md'))
).toThrow();
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
FoamGraph.fromWorkspace(ws);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
});
it('removing link to placeholder should remove placeholder', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = createTestWorkspace().set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [],
});
ws.set(noteABis);
const graph2 = FoamGraph.fromWorkspace(ws);
expect(
graph2.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeFalsy();
});
});
describe('Monitoring of workspace state', () => {
it('Update links when modifying note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
links: [{ slug: 'page-c' }],
});
const noteC = createTestNote({
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(graph.getBacklinks(noteC.uri).map(l => l.source)).toEqual([
noteB.uri,
]);
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-c' }],
});
ws.set(noteABis);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteC.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([]);
expect(
graph
.getBacklinks(noteC.uri)
.map(link => link.source.path)
.sort()
).toEqual(['/path/to/another/page-b.md', '/path/to/page-a.md']);
ws.dispose();
graph.dispose();
});
it('Removing target note should produce placeholder for wikilinks', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
expect(() => ws.get(noteB.uri)).toThrow();
ws.dispose();
graph.dispose();
});
it('Adding note should replace placeholder for wikilinks', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('page-b'),
]);
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
ws.dispose();
graph.dispose();
});
it('Removing target note should produce placeholder for direct links', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB.uri]);
expect(graph.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
noteA.uri,
]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
expect(() => ws.get(noteB.uri)).toThrow();
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
ws.dispose();
graph.dispose();
});
it('Adding note should replace placeholder for direct links', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([
URI.placeholder('/path/to/another/page-b.md'),
]);
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
expect(() => ws.get(URI.placeholder('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
ws.dispose();
graph.dispose();
});
it('removing link to placeholder should remove placeholder', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = createTestWorkspace();
ws.set(noteA);
const graph = FoamGraph.fromWorkspace(ws, true);
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeTruthy();
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [],
});
ws.set(noteABis);
expect(
graph.contains(URI.placeholder('/path/to/another/page-b.md'))
).toBeFalsy();
ws.dispose();
graph.dispose();
});
});

View File

@@ -61,9 +61,10 @@ export class FoamWorkspace implements IDisposable {
}
public listByIdentifier(identifier: string): Resource[] {
let needle = normalize('/' + identifier);
let mdNeedle = getExtension(needle) !== '.md' ? needle + '.md' : undefined;
let resources = [];
const needle = normalize('/' + identifier);
const mdNeedle =
getExtension(needle) !== '.md' ? needle + '.md' : undefined;
const resources = [];
for (const key of this.resources.keys()) {
if ((mdNeedle && key.endsWith(mdNeedle)) || key.endsWith(needle)) {
resources.push(this.resources.get(normalize(key)));
@@ -104,7 +105,7 @@ export class FoamWorkspace implements IDisposable {
return this.resources.get(normalize((reference as URI).path)) ?? null;
}
let resource: Resource | null = null;
let [path, fragment] = (reference as string).split('#');
const [path, fragment] = (reference as string).split('#');
if (FoamWorkspace.isIdentifier(path)) {
resource = this.listByIdentifier(path)[0];
} else {

View File

@@ -2,11 +2,12 @@ import micromatch from 'micromatch';
import fs from 'fs';
import { URI } from '../model/uri';
import { Logger } from '../utils/log';
import glob from 'glob';
import { glob } from 'glob';
import { promisify } from 'util';
import { isWindows } from '../common/platform';
const findAllFiles = promisify(glob);
export interface IMatcher {
/**
* Filters the given list of URIs, keepin only the ones that
@@ -117,6 +118,7 @@ export class FileDataStore implements IDataStore {
async list(glob: string, ignoreGlob?: string | string[]): Promise<URI[]> {
const res = await findAllFiles(glob, {
ignore: ignoreGlob,
strict: false,
});
return res.map(URI.file);
}

View File

@@ -0,0 +1,396 @@
import { createMarkdownParser, ParserPlugin } from './markdown-parser';
import { ResourceLink } from '../model/note';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
import { Range } from '../model/range';
import { getRandomURI } from '../../test/test-utils';
Logger.setLevel('error');
const parser = createMarkdownParser([]);
const createNoteFromMarkdown = (content: string, path?: string) =>
parser.parse(path ? URI.file(path) : getRandomURI(), content);
describe('Markdown parsing', () => {
it('should create a Resource from a markdown file', () => {
const note = createNoteFromMarkdown('Note content', '/a/path.md');
expect(note.uri).toEqual(URI.file('/a/path.md'));
});
describe('Links', () => {
it('should skip external links', () => {
const note = createNoteFromMarkdown(
`this is a [link to google](https://www.google.com)`
);
expect(note.links.length).toEqual(0);
});
it('should skip links to a section within the file', () => {
const note = createNoteFromMarkdown(
`this is a [link to intro](#introduction)`
);
expect(note.links.length).toEqual(0);
});
it('should detect regular markdown links', () => {
const note = createNoteFromMarkdown(
'this is a [link to page b](../doc/page-b.md)'
);
expect(note.links.length).toEqual(1);
const link = note.links[0];
expect(link.type).toEqual('link');
expect(link.label).toEqual('link to page b');
expect(link.rawText).toEqual('[link to page b](../doc/page-b.md)');
expect(link.target).toEqual('../doc/page-b.md');
});
it('should detect links that have formatting in label', () => {
const note = createNoteFromMarkdown(
'this is [**link** with __formatting__](../doc/page-b.md)'
);
expect(note.links.length).toEqual(1);
const link = note.links[0];
expect(link.type).toEqual('link');
expect(link.label).toEqual('link with formatting');
expect(link.target).toEqual('../doc/page-b.md');
});
it('should detect wikilinks', () => {
const note = createNoteFromMarkdown(
'Some content and [[a link]] to [[a file]]'
);
expect(note.links.length).toEqual(2);
let link = note.links[0];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[a link]]');
expect(link.label).toEqual('a link');
expect(link.target).toEqual('a link');
link = note.links[1];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[a file]]');
expect(link.label).toEqual('a file');
expect(link.target).toEqual('a file');
});
it('should detect wikilinks that have aliases', () => {
const note = createNoteFromMarkdown(
'this is [[link|link alias]]. A link with spaces [[other link | spaced]]'
);
expect(note.links.length).toEqual(2);
let link = note.links[0];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[link|link alias]]');
expect(link.label).toEqual('link alias');
expect(link.target).toEqual('link');
link = note.links[1];
expect(link.type).toEqual('wikilink');
expect(link.rawText).toEqual('[[other link | spaced]]');
expect(link.label).toEqual('spaced');
expect(link.target).toEqual('other link');
});
it('should skip wikilinks in codeblocks', () => {
const noteA = createNoteFromMarkdown(`
this is some text with our [[first-wikilink]].
\`\`\`
this is inside a [[codeblock]]
\`\`\`
this is some text with our [[second-wikilink]].
`);
expect(noteA.links.map(l => l.label)).toEqual([
'first-wikilink',
'second-wikilink',
]);
});
it('should skip wikilinks in inlined codeblocks', () => {
const noteA = createNoteFromMarkdown(`
this is some text with our [[first-wikilink]].
this is \`inside a [[codeblock]]\`
this is some text with our [[second-wikilink]].
`);
expect(noteA.links.map(l => l.label)).toEqual([
'first-wikilink',
'second-wikilink',
]);
});
});
describe('Note Title', () => {
it('should initialize note title if heading exists', () => {
const note = createNoteFromMarkdown(`
# Page A
this note has a title
`);
expect(note.title).toBe('Page A');
});
it('should support wikilinks and urls in title', () => {
const note = createNoteFromMarkdown(`
# Page A with [[wikilink]] and a [url](https://google.com)
this note has a title
`);
expect(note.title).toBe('Page A with wikilink and a url');
});
it('should default to file name if heading does not exist', () => {
const note = createNoteFromMarkdown(
`This file has no heading.`,
'/page-d.md'
);
expect(note.title).toEqual('page-d');
});
it('should give precedence to frontmatter title over other headings', () => {
const note = createNoteFromMarkdown(`
---
title: Note Title
date: 20-12-12
---
# Other Note Title
`);
expect(note.title).toBe('Note Title');
});
it('should support numbers as title', () => {
const note1 = createNoteFromMarkdown(`hello`, '/157.md');
expect(note1.title).toBe('157');
const note2 = createNoteFromMarkdown(`# 158`, '/157.md');
expect(note2.title).toBe('158');
const note3 = createNoteFromMarkdown(
`
---
title: 159
---
# 158
`,
'/157.md'
);
expect(note3.title).toBe('159');
});
it('should support empty titles (see #276)', () => {
const note = createNoteFromMarkdown(
`
#
this note has an empty title line
`,
'/Hello Page.md'
);
expect(note.title).toEqual('Hello Page');
});
});
describe('Frontmatter', () => {
it('should parse yaml frontmatter', () => {
const note = createNoteFromMarkdown(`
---
title: Note Title
date: 20-12-12
---
# Other Note Title`);
expect(note.properties.title).toBe('Note Title');
expect(note.properties.date).toBe('20-12-12');
});
it('should parse empty frontmatter', () => {
const note = createNoteFromMarkdown(`
---
---
# Empty Frontmatter
`);
expect(note.properties).toEqual({});
});
it('should not fail when there are issues with parsing frontmatter', () => {
const note = createNoteFromMarkdown(`
---
title: - one
- two
- #
---
`);
expect(note.properties).toEqual({});
});
});
describe('Tags', () => {
it('can find tags in the text of the note', () => {
const noteA = createNoteFromMarkdown(`
# this is a #heading
#this is some #text that includes #tags we #care-about.
`);
expect(noteA.tags).toEqual([
{ label: 'heading', range: Range.create(1, 12, 1, 20) },
{ label: 'this', range: Range.create(2, 0, 2, 5) },
{ label: 'text', range: Range.create(2, 14, 2, 19) },
{ label: 'tags', range: Range.create(2, 34, 2, 39) },
{ label: 'care-about', range: Range.create(2, 43, 2, 54) },
]);
});
it('will skip tags in codeblocks', () => {
const noteA = createNoteFromMarkdown(`
this is some #text that includes #tags we #care-about.
\`\`\`
this is a #codeblock
\`\`\`
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'text',
'tags',
'care-about',
]);
});
it('will skip tags in inlined codeblocks', () => {
const noteA = createNoteFromMarkdown(`
this is some #text that includes #tags we #care-about.
this is a \`inlined #codeblock\` `);
expect(noteA.tags.map(t => t.label)).toEqual([
'text',
'tags',
'care-about',
]);
});
it('can find tags as text in yaml', () => {
const noteA = createNoteFromMarkdown(`
---
tags: hello, world this_is_good
---
# this is a heading
this is some #text that includes #tags we #care-about.
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'hello',
'world',
'this_is_good',
'text',
'tags',
'care-about',
]);
});
it('can find tags as array in yaml', () => {
const noteA = createNoteFromMarkdown(`
---
tags: [hello, world, this_is_good]
---
# this is a heading
this is some #text that includes #tags we #care-about.
`);
expect(noteA.tags.map(t => t.label)).toEqual([
'hello',
'world',
'this_is_good',
'text',
'tags',
'care-about',
]);
});
it('provides rough range for tags in yaml', () => {
// For now it's enough to just get the YAML block range
// in the future we might want to be more specific
const noteA = createNoteFromMarkdown(`
---
tags: [hello, world, this_is_good]
---
# this is a heading
this is some text
`);
expect(noteA.tags[0]).toEqual({
label: 'hello',
range: Range.create(1, 0, 3, 3),
});
});
});
describe('Sections', () => {
it('should find sections within the note', () => {
const note = createNoteFromMarkdown(`
# Section 1
This is the content of section 1.
## Section 1.1
This is the content of section 1.1.
# Section 2
This is the content of section 2.
`);
expect(note.sections).toHaveLength(3);
expect(note.sections[0].label).toEqual('Section 1');
expect(note.sections[0].range).toEqual(Range.create(1, 0, 9, 0));
expect(note.sections[1].label).toEqual('Section 1.1');
expect(note.sections[1].range).toEqual(Range.create(5, 0, 9, 0));
expect(note.sections[2].label).toEqual('Section 2');
expect(note.sections[2].range).toEqual(Range.create(9, 0, 13, 0));
});
it('should support wikilinks and links in the section label', () => {
const note = createNoteFromMarkdown(`
# Section with [[wikilink]]
This is the content of section with wikilink
## Section with [url](https://google.com)
This is the content of section with url`);
expect(note.sections).toHaveLength(2);
expect(note.sections[0].label).toEqual('Section with wikilink');
expect(note.sections[1].label).toEqual('Section with url');
});
});
describe('Parser plugins', () => {
const testPlugin: ParserPlugin = {
visit: (node, note) => {
if (node.type === 'heading') {
note.properties.hasHeading = true;
}
},
};
const parser = createMarkdownParser([testPlugin]);
it('can augment the parsing of the file', () => {
const note1 = parser.parse(
URI.file('/path/to/a'),
`
This is a test note without headings.
But with some content.
`
);
expect(note1.properties.hasHeading).toBeUndefined();
const note2 = parser.parse(
URI.file('/path/to/a'),
`
# This is a note with header
and some content`
);
expect(note2.properties.hasHeading).toBeTruthy();
});
});
});

View File

@@ -1,31 +1,19 @@
import { Node, Position as AstPosition } from 'unist';
// eslint-disable-next-line import/no-extraneous-dependencies
import { Point, Node, Position as AstPosition } from 'unist';
import unified from 'unified';
import markdownParse from 'remark-parse';
import wikiLinkPlugin from 'remark-wiki-link';
import frontmatterPlugin from 'remark-frontmatter';
import { parse as parseYAML } from 'yaml';
import visit from 'unist-util-visit';
import { Parent, Point } from 'unist';
import detectNewline from 'detect-newline';
import os from 'os';
import {
NoteLinkDefinition,
Resource,
ResourceLink,
WikiLink,
ResourceParser,
} from './model/note';
import { Position } from './model/position';
import { Range } from './model/range';
import { extractHashtags, extractTagsFromProp, isNone, isSome } from './utils';
import { Logger } from './utils/log';
import { URI } from './model/uri';
import { FoamWorkspace } from './model/workspace';
import { IDataStore, FileDataStore, IMatcher } from './services/datastore';
import { IDisposable } from './common/lifecycle';
import { ResourceProvider } from './model/provider';
const ALIAS_DIVIDER_CHAR = '|';
import { NoteLinkDefinition, Resource, ResourceParser } from '../model/note';
import { Position } from '../model/position';
import { Range } from '../model/range';
import { extractHashtags, extractTagsFromProp, isSome } from '../utils';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
export interface ParserPlugin {
name?: string;
@@ -37,136 +25,117 @@ export interface ParserPlugin {
onDidFindProperties?: (properties: any, note: Resource, node: Node) => void;
}
export class MarkdownResourceProvider implements ResourceProvider {
private disposables: IDisposable[] = [];
const ALIAS_DIVIDER_CHAR = '|';
constructor(
private readonly matcher: IMatcher,
private readonly watcherInit?: (triggers: {
onDidChange: (uri: URI) => void;
onDidCreate: (uri: URI) => void;
onDidDelete: (uri: URI) => void;
}) => IDisposable[],
private readonly parser: ResourceParser = createMarkdownParser([]),
private readonly dataStore: IDataStore = new FileDataStore()
) {}
export function createMarkdownParser(
extraPlugins: ParserPlugin[]
): ResourceParser {
const parser = unified()
.use(markdownParse, { gfm: true })
.use(frontmatterPlugin, ['yaml'])
.use(wikiLinkPlugin, { aliasDivider: ALIAS_DIVIDER_CHAR });
async init(workspace: FoamWorkspace) {
const filesByFolder = await Promise.all(
this.matcher.include.map(glob =>
this.dataStore.list(glob, this.matcher.exclude)
)
);
const files = this.matcher
.match(filesByFolder.flat())
.filter(this.supports);
const plugins = [
titlePlugin,
wikilinkPlugin,
definitionsPlugin,
tagsPlugin,
sectionsPlugin,
...extraPlugins,
];
await Promise.all(
files.map(async uri => {
Logger.info('Found: ' + uri.toString());
const content = await this.dataStore.read(uri);
if (isSome(content)) {
workspace.set(this.parser.parse(uri, content));
}
})
);
this.disposables =
this.watcherInit?.({
onDidChange: async uri => {
if (this.matcher.isMatch(uri) && this.supports(uri)) {
const content = await this.dataStore.read(uri);
isSome(content) &&
workspace.set(await this.parser.parse(uri, content));
}
},
onDidCreate: async uri => {
if (this.matcher.isMatch(uri) && this.supports(uri)) {
const content = await this.dataStore.read(uri);
isSome(content) &&
workspace.set(await this.parser.parse(uri, content));
}
},
onDidDelete: uri => {
this.supports(uri) && workspace.delete(uri);
},
}) ?? [];
}
supports(uri: URI) {
return uri.isMarkdown();
}
read(uri: URI): Promise<string | null> {
return this.dataStore.read(uri);
}
async readAsMarkdown(uri: URI): Promise<string | null> {
let content = await this.dataStore.read(uri);
if (isSome(content) && uri.fragment) {
const resource = this.parser.parse(uri, content);
const section = Resource.findSection(resource, uri.fragment);
if (isSome(section)) {
const rows = content.split('\n');
content = rows
.slice(section.range.start.line, section.range.end.line)
.join('\n');
}
plugins.forEach(plugin => {
try {
plugin.onDidInitializeParser?.(parser);
} catch (e) {
handleError(plugin, 'onDidInitializeParser', undefined, e);
}
return content;
}
});
async fetch(uri: URI) {
const content = await this.read(uri);
return isSome(content) ? this.parser.parse(uri, content) : null;
}
const foamParser: ResourceParser = {
parse: (uri: URI, markdown: string): Resource => {
Logger.debug('Parsing:', uri.toString());
markdown = plugins.reduce((acc, plugin) => {
try {
return plugin.onWillParseMarkdown?.(acc) || acc;
} catch (e) {
handleError(plugin, 'onWillParseMarkdown', uri, e);
return acc;
}
}, markdown);
const tree = parser.parse(markdown);
const eol = detectNewline(markdown) || os.EOL;
resolveLink(
workspace: FoamWorkspace,
resource: Resource,
link: ResourceLink
) {
let targetUri: URI | undefined;
switch (link.type) {
case 'wikilink':
const definitionUri = resource.definitions.find(
def => def.label === link.target
)?.url;
if (isSome(definitionUri)) {
const definedUri = resource.uri.resolve(definitionUri);
targetUri =
workspace.find(definedUri, resource.uri)?.uri ??
URI.placeholder(definedUri.path);
} else {
const [target, section] = link.target.split('#');
targetUri =
target === ''
? resource.uri
: workspace.find(target, resource.uri)?.uri ??
URI.placeholder(link.target);
const note: Resource = {
uri: uri,
type: 'note',
properties: {},
title: '',
sections: [],
tags: [],
links: [],
definitions: [],
source: {
text: markdown,
contentStart: astPointToFoamPosition(tree.position!.start),
end: astPointToFoamPosition(tree.position!.end),
eol: eol,
},
};
if (section) {
targetUri = targetUri.withFragment(section);
plugins.forEach(plugin => {
try {
plugin.onWillVisitTree?.(tree, note);
} catch (e) {
handleError(plugin, 'onWillVisitTree', uri, e);
}
});
visit(tree, node => {
if (node.type === 'yaml') {
try {
const yamlProperties = parseYAML((node as any).value) ?? {};
note.properties = {
...note.properties,
...yamlProperties,
};
// Update the start position of the note by exluding the metadata
note.source.contentStart = Position.create(
node.position!.end.line! + 2,
0
);
for (let i = 0, len = plugins.length; i < len; i++) {
try {
plugins[i].onDidFindProperties?.(yamlProperties, note, node);
} catch (e) {
handleError(plugins[i], 'onDidFindProperties', uri, e);
}
}
} catch (e) {
Logger.warn(`Error while parsing YAML for [${uri.toString()}]`, e);
}
}
break;
case 'link':
const [target, section] = link.target.split('#');
targetUri =
workspace.find(target, resource.uri)?.uri ??
URI.placeholder(resource.uri.resolve(link.target).path);
if (section && !targetUri.isPlaceholder()) {
targetUri = targetUri.withFragment(section);
for (let i = 0, len = plugins.length; i < len; i++) {
try {
plugins[i].visit?.(node, note, markdown);
} catch (e) {
handleError(plugins[i], 'visit', uri, e);
}
}
break;
}
return targetUri;
}
dispose() {
this.disposables.forEach(d => d.dispose());
}
});
plugins.forEach(plugin => {
try {
plugin.onDidVisitTree?.(tree, note);
} catch (e) {
handleError(plugin, 'onDidVisitTree', uri, e);
}
});
Logger.debug('Result:', note);
return note;
},
};
return foamParser;
}
/**
@@ -202,7 +171,7 @@ const tagsPlugin: ParserPlugin = {
if (node.type === 'text') {
const tags = extractHashtags((node as any).value);
tags.forEach(tag => {
let start = astPointToFoamPosition(node.position!.start);
const start = astPointToFoamPosition(node.position!.start);
start.character = start.character + tag.offset;
const end: Position = {
line: start.line,
@@ -326,6 +295,7 @@ const wikilinkPlugin: ParserPlugin = {
type: 'link',
target: targetUri,
label: label,
rawText: `[${label}](${targetUri})`,
range: astPositionToFoamRange(node.position!),
});
}
@@ -364,123 +334,12 @@ const handleError = (
);
};
export function createMarkdownParser(
extraPlugins: ParserPlugin[]
): ResourceParser {
const parser = unified()
.use(markdownParse, { gfm: true })
.use(frontmatterPlugin, ['yaml'])
.use(wikiLinkPlugin, { aliasDivider: ALIAS_DIVIDER_CHAR });
const plugins = [
titlePlugin,
wikilinkPlugin,
definitionsPlugin,
tagsPlugin,
sectionsPlugin,
...extraPlugins,
];
plugins.forEach(plugin => {
try {
plugin.onDidInitializeParser?.(parser);
} catch (e) {
handleError(plugin, 'onDidInitializeParser', undefined, e);
}
});
const foamParser: ResourceParser = {
parse: (uri: URI, markdown: string): Resource => {
Logger.debug('Parsing:', uri.toString());
markdown = plugins.reduce((acc, plugin) => {
try {
return plugin.onWillParseMarkdown?.(acc) || acc;
} catch (e) {
handleError(plugin, 'onWillParseMarkdown', uri, e);
return acc;
}
}, markdown);
const tree = parser.parse(markdown);
const eol = detectNewline(markdown) || os.EOL;
var note: Resource = {
uri: uri,
type: 'note',
properties: {},
title: '',
sections: [],
tags: [],
links: [],
definitions: [],
source: {
text: markdown,
contentStart: astPointToFoamPosition(tree.position!.start),
end: astPointToFoamPosition(tree.position!.end),
eol: eol,
},
};
plugins.forEach(plugin => {
try {
plugin.onWillVisitTree?.(tree, note);
} catch (e) {
handleError(plugin, 'onWillVisitTree', uri, e);
}
});
visit(tree, node => {
if (node.type === 'yaml') {
try {
const yamlProperties = parseYAML((node as any).value) ?? {};
note.properties = {
...note.properties,
...yamlProperties,
};
// Update the start position of the note by exluding the metadata
note.source.contentStart = Position.create(
node.position!.end.line! + 2,
0
);
for (let i = 0, len = plugins.length; i < len; i++) {
try {
plugins[i].onDidFindProperties?.(yamlProperties, note, node);
} catch (e) {
handleError(plugins[i], 'onDidFindProperties', uri, e);
}
}
} catch (e) {
Logger.warn(`Error while parsing YAML for [${uri.toString()}]`, e);
}
}
for (let i = 0, len = plugins.length; i < len; i++) {
try {
plugins[i].visit?.(node, note, markdown);
} catch (e) {
handleError(plugins[i], 'visit', uri, e);
}
}
});
plugins.forEach(plugin => {
try {
plugin.onDidVisitTree?.(tree, note);
} catch (e) {
handleError(plugin, 'onDidVisitTree', uri, e);
}
});
Logger.debug('Result:', note);
return note;
},
};
return foamParser;
}
function getFoamDefinitions(
defs: NoteLinkDefinition[],
fileEndPoint: Position
): NoteLinkDefinition[] {
let previousLine = fileEndPoint.line;
let foamDefinitions = [];
const foamDefinitions = [];
// walk through each definition in reverse order
// (last one first)
@@ -500,68 +359,6 @@ function getFoamDefinitions(
return foamDefinitions;
}
export function stringifyMarkdownLinkReferenceDefinition(
definition: NoteLinkDefinition
) {
let url =
definition.url.indexOf(' ') > 0 ? `<${definition.url}>` : definition.url;
let text = `[${definition.label}]: ${url}`;
if (definition.title) {
text = `${text} "${definition.title}"`;
}
return text;
}
export function createMarkdownReferences(
workspace: FoamWorkspace,
noteUri: URI,
includeExtension: boolean
): NoteLinkDefinition[] {
const source = workspace.find(noteUri);
// Should never occur since we're already in a file,
if (source?.type !== 'note') {
console.warn(
`Note ${noteUri.toString()} note found in workspace when attempting \
to generate markdown reference list`
);
return [];
}
return source.links
.filter(isWikilink)
.map(link => {
const targetUri = workspace.resolveLink(source, link);
const target = workspace.find(targetUri);
if (isNone(target)) {
Logger.warn(
`Link ${targetUri.toString()} in ${noteUri.toString()} is not valid.`
);
return null;
}
if (target.type === 'placeholder') {
// no need to create definitions for placeholders
return null;
}
let relativeUri = target.uri.relativeTo(noteUri.getDirectory());
if (!includeExtension) {
relativeUri = relativeUri.changeExtension('*', '');
}
// [wikilink-text]: path/to/file.md "Page title"
return {
label:
link.rawText.indexOf('[[') > -1
? link.rawText.substring(2, link.rawText.length - 2)
: link.rawText || link.label,
url: relativeUri.path,
title: target.title,
};
})
.filter(isSome)
.sort();
}
/**
* Converts the 1-index Point object into the VS Code 0-index Position object
* @param point ast Point (1-indexed)
@@ -583,7 +380,3 @@ const astPositionToFoamRange = (pos: AstPosition): Range =>
pos.end.line - 1,
pos.end.column - 1
);
const isWikilink = (link: ResourceLink): link is WikiLink => {
return link.type === 'wikilink';
};

View File

@@ -0,0 +1,244 @@
import { createMarkdownParser } from './markdown-parser';
import { createMarkdownReferences } from './markdown-provider';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
import {
createTestNote,
createTestWorkspace,
getRandomURI,
} from '../../test/test-utils';
Logger.setLevel('error');
const parser = createMarkdownParser([]);
const createNoteFromMarkdown = (content: string, path?: string) =>
parser.parse(path ? URI.file(path) : getRandomURI(), content);
describe('Link resolution', () => {
describe('Wikilinks', () => {
it('should resolve basename wikilinks with files in same directory', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown('Link to [[page b]]', './page-a.md');
const noteB = createNoteFromMarkdown('Content of page b', './page b.md');
workspace.set(noteA).set(noteB);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
it('should resolve basename wikilinks with files in other directory', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown('Link to [[page b]]', './page-a.md');
const noteB = createNoteFromMarkdown('Page b', './folder/page b.md');
workspace.set(noteA).set(noteB);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
it('should resolve wikilinks that represent an absolute path', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to [[/folder/page b]]',
'/page-a.md'
);
const noteB = createNoteFromMarkdown('Page b', '/folder/page b.md');
workspace.set(noteA).set(noteB);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
it('should resolve wikilinks that represent a relative path', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to [[../two/page b]]',
'/path/one/page-a.md'
);
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b 2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
});
it('should resolve ambiguous wikilinks', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown('Link to [[page b]]', '/page-a.md');
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
it('should resolve path wikilink even with other ambiguous notes', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: './more/page-b' }, { slug: 'yet/page-b' }],
});
const noteB1 = createTestNote({ uri: '/path/to/another/page-b.md' });
const noteB2 = createTestNote({ uri: '/path/to/more/page-b.md' });
const noteB3 = createTestNote({ uri: '/path/to/yet/page-b.md' });
const ws = createTestWorkspace();
ws.set(noteA)
.set(noteB1)
.set(noteB2)
.set(noteB3);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteB3.uri);
});
it('should resolve Foam wikilinks', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to [[two/page b]] and [[one/page b]]',
'/page-a.md'
);
const noteB = createNoteFromMarkdown('Page b', '/path/one/page b.md');
const noteB2 = createNoteFromMarkdown('Page b2', '/path/two/page b.md');
workspace
.set(noteA)
.set(noteB)
.set(noteB2);
expect(workspace.resolveLink(noteA, noteA.links[0])).toEqual(noteB2.uri);
expect(workspace.resolveLink(noteA, noteA.links[1])).toEqual(noteB.uri);
});
it('should use wikilink definitions when available to resolve target', () => {
const ws = createTestWorkspace();
const noteA = createTestNote({
uri: '/somewhere/from/page-a.md',
links: [{ slug: 'page-b' }],
});
noteA.definitions.push({
label: 'page-b',
url: '../to/page-b.md',
});
const noteB = createTestNote({
uri: '/somewhere/to/page-b.md',
});
ws.set(noteA).set(noteB);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
it('should support case insensitive wikilink resolution', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [
// uppercased filename, lowercased slug
{ slug: 'page-b' },
// lowercased filename, camelcased wikilink
{ slug: 'Page-C' },
// lowercased filename, lowercased wikilink
{ slug: 'page-d' },
],
});
const noteB = createTestNote({ uri: '/somewhere/PAGE-B.md' });
const noteC = createTestNote({ uri: '/path/another/page-c.md' });
const noteD = createTestNote({ uri: '/path/another/page-d.md' });
const ws = createTestWorkspace()
.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteC.uri);
expect(ws.resolveLink(noteA, noteA.links[2])).toEqual(noteD.uri);
});
});
describe('Markdown direct links', () => {
it('should support absolute path', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: './another/page-b.md' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
links: [{ to: '../../to/page-a.md' }],
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
it('should support relative path', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: 'more/page-c.md' }],
});
const noteB = createTestNote({
uri: '/path/to/more/page-c.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
it('should default to relative path', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: 'page c.md' }],
});
const noteB = createTestNote({
uri: '/path/to/page c.md',
});
const ws = createTestWorkspace();
ws.set(noteA).set(noteB);
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(noteB.uri);
});
});
});
describe('Generation of markdown references', () => {
it('should generate links without file extension when includeExtension = false', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to [[page-b]] and [[page-c]]',
'/dir1/page-a.md'
);
workspace
.set(noteA)
.set(createNoteFromMarkdown('Content of note B', '/dir1/page-b.md'))
.set(createNoteFromMarkdown('Content of note C', '/dir1/page-c.md'));
const references = createMarkdownReferences(workspace, noteA.uri, false);
expect(references.map(r => r.url)).toEqual(['page-b', 'page-c']);
});
it('should generate links with file extension when includeExtension = true', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to [[page-b]] and [[page-c]]',
'/dir1/page-a.md'
);
workspace
.set(noteA)
.set(createNoteFromMarkdown('Content of note B', '/dir1/page-b.md'))
.set(createNoteFromMarkdown('Content of note C', '/dir1/page-c.md'));
const references = createMarkdownReferences(workspace, noteA.uri, true);
expect(references.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
});
it('should use relative paths', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to [[page-b]] and [[page-c]]',
'/dir1/page-a.md'
);
workspace
.set(noteA)
.set(createNoteFromMarkdown('Content of note B', '/dir2/page-b.md'))
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
const references = createMarkdownReferences(workspace, noteA.uri, true);
expect(references.map(r => r.url)).toEqual([
'../dir2/page-b.md',
'../dir3/page-c.md',
]);
});
});

View File

@@ -0,0 +1,210 @@
import {
NoteLinkDefinition,
Resource,
ResourceLink,
ResourceParser,
} from '../model/note';
import { isNone, isSome } from '../utils';
import { Logger } from '../utils/log';
import { URI } from '../model/uri';
import { FoamWorkspace } from '../model/workspace';
import { IDataStore, FileDataStore, IMatcher } from '../services/datastore';
import { IDisposable } from '../common/lifecycle';
import { ResourceProvider } from '../model/provider';
import { createMarkdownParser } from './markdown-parser';
export class MarkdownResourceProvider implements ResourceProvider {
private disposables: IDisposable[] = [];
constructor(
private readonly matcher: IMatcher,
private readonly watcherInit?: (triggers: {
onDidChange: (uri: URI) => void;
onDidCreate: (uri: URI) => void;
onDidDelete: (uri: URI) => void;
}) => IDisposable[],
private readonly parser: ResourceParser = createMarkdownParser([]),
private readonly dataStore: IDataStore = new FileDataStore()
) {}
async init(workspace: FoamWorkspace) {
const filesByFolder = await Promise.all(
this.matcher.include.map(glob =>
this.dataStore.list(glob, this.matcher.exclude)
)
);
const files = this.matcher
.match(filesByFolder.flat())
.filter(this.supports);
await Promise.all(
files.map(async uri => {
Logger.info('Found: ' + uri.toString());
const content = await this.dataStore.read(uri);
if (isSome(content)) {
workspace.set(this.parser.parse(uri, content));
}
})
);
this.disposables =
this.watcherInit?.({
onDidChange: async uri => {
if (this.matcher.isMatch(uri) && this.supports(uri)) {
const content = await this.dataStore.read(uri);
isSome(content) &&
workspace.set(await this.parser.parse(uri, content));
}
},
onDidCreate: async uri => {
if (this.matcher.isMatch(uri) && this.supports(uri)) {
const content = await this.dataStore.read(uri);
isSome(content) &&
workspace.set(await this.parser.parse(uri, content));
}
},
onDidDelete: uri => {
this.supports(uri) && workspace.delete(uri);
},
}) ?? [];
}
supports(uri: URI) {
return uri.isMarkdown();
}
read(uri: URI): Promise<string | null> {
return this.dataStore.read(uri);
}
async readAsMarkdown(uri: URI): Promise<string | null> {
let content = await this.dataStore.read(uri);
if (isSome(content) && uri.fragment) {
const resource = this.parser.parse(uri, content);
const section = Resource.findSection(resource, uri.fragment);
if (isSome(section)) {
const rows = content.split('\n');
content = rows
.slice(section.range.start.line, section.range.end.line)
.join('\n');
}
}
return content;
}
async fetch(uri: URI) {
const content = await this.read(uri);
return isSome(content) ? this.parser.parse(uri, content) : null;
}
resolveLink(
workspace: FoamWorkspace,
resource: Resource,
link: ResourceLink
) {
let targetUri: URI | undefined;
switch (link.type) {
case 'wikilink': {
const definitionUri = resource.definitions.find(
def => def.label === link.target
)?.url;
if (isSome(definitionUri)) {
const definedUri = resource.uri.resolve(definitionUri);
targetUri =
workspace.find(definedUri, resource.uri)?.uri ??
URI.placeholder(definedUri.path);
} else {
const [target, section] = link.target.split('#');
targetUri =
target === ''
? resource.uri
: workspace.find(target, resource.uri)?.uri ??
URI.placeholder(link.target);
if (section) {
targetUri = targetUri.withFragment(section);
}
}
break;
}
case 'link': {
const [target, section] = link.target.split('#');
targetUri =
workspace.find(target, resource.uri)?.uri ??
URI.placeholder(resource.uri.resolve(link.target).path);
if (section && !targetUri.isPlaceholder()) {
targetUri = targetUri.withFragment(section);
}
break;
}
}
return targetUri;
}
dispose() {
this.disposables.forEach(d => d.dispose());
}
}
export function createMarkdownReferences(
workspace: FoamWorkspace,
noteUri: URI,
includeExtension: boolean
): NoteLinkDefinition[] {
const source = workspace.find(noteUri);
// Should never occur since we're already in a file,
if (source?.type !== 'note') {
console.warn(
`Note ${noteUri.toString()} note found in workspace when attempting \
to generate markdown reference list`
);
return [];
}
return source.links
.filter(link => link.type === 'wikilink')
.map(link => {
const targetUri = workspace.resolveLink(source, link);
const target = workspace.find(targetUri);
if (isNone(target)) {
Logger.warn(
`Link ${targetUri.toString()} in ${noteUri.toString()} is not valid.`
);
return null;
}
if (target.type === 'placeholder') {
// no need to create definitions for placeholders
return null;
}
let relativeUri = target.uri.relativeTo(noteUri.getDirectory());
if (!includeExtension) {
relativeUri = relativeUri.changeExtension('*', '');
}
// [wikilink-text]: path/to/file.md "Page title"
return {
label:
link.rawText.indexOf('[[') > -1
? link.rawText.substring(2, link.rawText.length - 2)
: link.rawText || link.label,
url: relativeUri.path,
title: target.title,
};
})
.filter(isSome)
.sort();
}
export function stringifyMarkdownLinkReferenceDefinition(
definition: NoteLinkDefinition
) {
const url =
definition.url.indexOf(' ') > 0 ? `<${definition.url}>` : definition.url;
let text = `[${definition.label}]: ${url}`;
if (definition.title) {
text = `${text} "${definition.title}"`;
}
return text;
}

View File

@@ -1,19 +1,19 @@
import crypto from 'crypto';
export function isNotNull<T>(value: T | null): value is T {
return value != null; // eslint-disable-line
return value != null;
}
export function isSome<T>(
value: T | null | undefined | void
): value is NonNullable<T> {
return value != null; // eslint-disable-line
return value != null;
}
export function isNone<T>(
value: T | null | undefined | void
): value is null | undefined | void {
return value == null; // eslint-disable-line
return value == null;
}
export function isNumeric(value: string): boolean {

View File

@@ -58,7 +58,9 @@ export class ConsoleLogger extends BaseLogger {
}
export class NoOpLogger extends BaseLogger {
log(_l: LogLevel, _m?: string, ..._p: any[]): void {}
log(_l: LogLevel, _m?: string, ..._p: any[]): void {
// do nothing
}
}
export class Logger {

View File

@@ -16,7 +16,7 @@ import { NoteFactory } from './services/templates';
*/
export async function openDailyNoteFor(date?: Date) {
const foamConfiguration = workspace.getConfiguration('foam');
const currentDate = date !== undefined ? date : new Date();
const currentDate = date instanceof Date ? date : new Date();
const dailyNotePath = getDailyNotePath(foamConfiguration, currentDate);
@@ -114,7 +114,7 @@ export async function createDailyNoteIfNotExists(
configuration.get('openDailyNote.titleFormat') ??
configuration.get('openDailyNote.filenameFormat');
const templateFallbackText: string = `---
const templateFallbackText = `---
foam_template:
name: New Daily Note
description: Foam's default daily note template

View File

@@ -1,5 +1,5 @@
import { workspace, ExtensionContext, window } from 'vscode';
import { MarkdownResourceProvider } from './core/markdown-provider';
import { MarkdownResourceProvider } from './core/services/markdown-provider';
import { bootstrap } from './core/model/foam';
import { FileDataStore, Matcher } from './core/services/datastore';
import { Logger } from './core/utils/log';

View File

@@ -37,7 +37,7 @@ describe('Backlinks panel', () => {
const noteB = createTestNote({
root: rootUri,
uri: './note-b.md',
links: [{ slug: 'note-a' }, { slug: 'note-a' }],
links: [{ slug: 'note-a' }, { slug: 'note-a#section' }],
});
const noteC = createTestNote({
root: rootUri,

View File

@@ -63,7 +63,10 @@ export class BacklinksTreeDataProvider
const backlinkRefs = Promise.all(
resource.links
.filter(link =>
this.workspace.resolveLink(resource, link).isEqual(uri)
this.workspace
.resolveLink(resource, link)
.asPlain()
.isEqual(uri)
)
.map(async link => {
const item = new BacklinkTreeItem(resource, link);
@@ -72,7 +75,7 @@ export class BacklinksTreeDataProvider
).split('\n');
if (link.range.start.line < lines.length) {
const line = lines[link.range.start.line];
let start = Math.max(0, link.range.start.character - 15);
const start = Math.max(0, link.range.start.character - 15);
const ellipsis = start === 0 ? '' : '...';
item.label = `${link.range.start.line}: ${ellipsis}${line.substr(
@@ -93,7 +96,9 @@ export class BacklinksTreeDataProvider
}
const backlinksByResourcePath = groupBy(
this.graph.getConnections(uri).filter(c => c.target.isEqual(uri)),
this.graph
.getConnections(uri)
.filter(c => c.target.asPlain().isEqual(uri)),
b => b.source.path
);

View File

@@ -1,21 +1,17 @@
// import { env, window, Uri, Position, Selection, commands } from 'vscode';
// import * as vscode from 'vscode';
import { env, Position, Selection, commands } from 'vscode';
import {
createFile,
getUriInWorkspace,
showInEditor,
} from '../test/test-utils-vscode';
describe('copyWithoutBrackets', () => {
it('should pass CI', () => {
expect(true).toBe(true);
it('should get the input from the active editor selection', async () => {
const { uri } = await createFile('This is my [[test-content]].');
const { editor } = await showInEditor(uri);
editor.selection = new Selection(new Position(0, 0), new Position(1, 0));
await commands.executeCommand('foam-vscode.copy-without-brackets');
const value = await env.clipboard.readText();
expect(value).toEqual('This is my Test Content.');
});
// it('should get the input from the active editor selection', async () => {
// const doc = await vscode.workspace.openTextDocument(
// Uri.parse('untitled:/hello.md')
// );
// const editor = await window.showTextDocument(doc);
// editor.edit(builder => {
// builder.insert(new Position(0, 0), 'This is my [[test-content]].');
// });
// editor.selection = new Selection(new Position(0, 0), new Position(1, 0));
// await commands.executeCommand('foam-vscode.copy-without-brackets');
// const value = await env.clipboard.readText();
// expect(value).toEqual('This is my Test Content.');
// });
});

View File

@@ -1,7 +1,6 @@
import { Uri } from 'vscode';
import { Uri, commands, window, workspace } from 'vscode';
import { URI } from '../core/model/uri';
import { toVsCodeUri } from '../utils/vsc-utils';
import { commands, window, workspace } from 'vscode';
import { createFile } from '../test/test-utils-vscode';
import * as editor from '../services/editor';

View File

@@ -131,7 +131,7 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
panel.webview.onDidReceiveMessage(
async message => {
switch (message.type) {
case 'webviewDidLoad':
case 'webviewDidLoad': {
const styles = getGraphStyle();
panel.webview.postMessage({
type: 'didUpdateStyle',
@@ -139,8 +139,8 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
});
updateGraph(panel, foam);
break;
case 'webviewDidSelectNode':
}
case 'webviewDidSelectNode': {
const noteUri = vscode.Uri.parse(message.payload);
const selectedNote = foam.workspace.get(fromVsCodeUri(noteUri));
@@ -151,10 +151,11 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
}
break;
case 'error':
}
case 'error': {
Logger.error('An error occurred in the graph view', message.payload);
break;
}
}
},
undefined,

View File

@@ -36,7 +36,7 @@ const updateDecorations = (
fromVsCodeUri(editor.document.uri),
editor.document.getText()
);
let placeholderRanges = [];
const placeholderRanges = [];
note.links.forEach(link => {
const linkUri = workspace.resolveLink(note, link);
if (linkUri.isPlaceholder()) {

View File

@@ -1,8 +1,6 @@
import * as vscode from 'vscode';
import {
createMarkdownParser,
MarkdownResourceProvider,
} from '../core/markdown-provider';
import { createMarkdownParser } from '../core/services/markdown-parser';
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
import { FoamGraph } from '../core/model/graph';
import { FoamWorkspace } from '../core/model/workspace';
import { Matcher } from '../core/services/datastore';

View File

@@ -97,12 +97,12 @@ async function runJanitor(foam: Foam) {
// Apply Text Edits to Non Dirty Notes using fs module just like CLI
const fileWritePromises = nonDirtyNotes.map(note => {
let heading = generateHeading(note);
const heading = generateHeading(note);
if (heading) {
updatedHeadingCount += 1;
}
let definitions =
const definitions =
wikilinkSetting === LinkReferenceDefinitionsSetting.off
? null
: generateLinkReferences(
@@ -140,7 +140,7 @@ async function runJanitor(foam: Foam) {
// Get edits
const heading = generateHeading(note);
let definitions =
const definitions =
wikilinkSetting === LinkReferenceDefinitionsSetting.off
? null
: generateLinkReferences(

View File

@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { createMarkdownParser } from '../core/markdown-provider';
import { createMarkdownParser } from '../core/services/markdown-parser';
import { FoamGraph } from '../core/model/graph';
import { FoamWorkspace } from '../core/model/workspace';
import { createTestNote } from '../test/test-utils';
@@ -88,40 +88,48 @@ describe('Link Completion', () => {
});
it('should return notes with unique identifiers, and placeholders', async () => {
const { uri } = await createFile('[[file]] [[');
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws, graph);
for (const text of ['[[', '[[file]] [[', '[[file]] #tag [[']) {
const { uri } = await createFile(text);
const { doc } = await showInEditor(uri);
const provider = new CompletionProvider(ws, graph);
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 11)
);
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, text.length)
);
expect(links.items.length).toEqual(5);
expect(new Set(links.items.map(i => i.insertText))).toEqual(
new Set([
'to/file',
'another/file',
'File name with spaces',
'file-name',
'placeholder text',
])
);
expect(links.items.length).toEqual(5);
expect(new Set(links.items.map(i => i.insertText))).toEqual(
new Set([
'to/file',
'another/file',
'File name with spaces',
'file-name',
'placeholder text',
])
);
}
});
it('should return sections for other notes', async () => {
const { uri } = await createFile('[[file-name#');
const { doc } = await showInEditor(uri);
const provider = new SectionCompletionProvider(ws);
for (const text of [
'[[file-name#',
'[[file]] [[file-name#',
'[[file]] #tag [[file-name#',
]) {
const { uri } = await createFile(text);
const { doc } = await showInEditor(uri);
const provider = new SectionCompletionProvider(ws);
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, 12)
);
const links = await provider.provideCompletionItems(
doc,
new vscode.Position(0, text.length)
);
expect(new Set(links.items.map(i => i.label))).toEqual(
new Set(['Section One', 'Section Two'])
);
expect(new Set(links.items.map(i => i.label))).toEqual(
new Set(['Section One', 'Section Two'])
);
}
});
it('should return sections within the note', async () => {

View File

@@ -45,7 +45,6 @@ export class SectionCompletionProvider
// Requires autocomplete only if cursorPrefix matches `[[` that NOT ended by `]]`.
// See https://github.com/foambubble/foam/pull/596#issuecomment-825748205 for details.
// eslint-disable-next-line no-useless-escape
const match = cursorPrefix.match(SECTION_REGEX);
if (!match) {
@@ -104,10 +103,9 @@ export class CompletionProvider
// Requires autocomplete only if cursorPrefix matches `[[` that NOT ended by `]]`.
// See https://github.com/foambubble/foam/pull/596#issuecomment-825748205 for details.
// eslint-disable-next-line no-useless-escape
const requiresAutocomplete = cursorPrefix.match(WIKILINK_REGEX);
if (!requiresAutocomplete || cursorPrefix.indexOf('#') >= 0) {
if (!requiresAutocomplete || requiresAutocomplete[0].indexOf('#') >= 0) {
return null;
}

View File

@@ -10,7 +10,7 @@ import {
import { NavigationProvider } from './navigation-provider';
import { OPEN_COMMAND } from './utility-commands';
import { toVsCodeUri } from '../utils/vsc-utils';
import { createMarkdownParser } from '../core/markdown-provider';
import { createMarkdownParser } from '../core/services/markdown-parser';
import { FoamWorkspace } from '../core/model/workspace';
import { FoamGraph } from '../core/model/graph';
@@ -232,6 +232,6 @@ describe('Document navigation', () => {
range: new vscode.Range(0, 23, 0, 23 + 9),
});
});
it('should provide references for placeholders', async () => {});
// it('should provide references for placeholders', async () => {});
});
});

View File

@@ -115,22 +115,23 @@ export class NavigationProvider
}
const targetResource = this.workspace.get(uri);
let targetRange = Range.createFromPosition(
targetResource.source.contentStart,
targetResource.source.end
);
const section = Resource.findSection(targetResource, uri.fragment);
if (section) {
targetRange = section.range;
}
const targetRange = section
? section.range
: Range.createFromPosition(
targetResource.source.contentStart,
targetResource.source.end
);
const targetSelectionRange = section
? section.range
: Range.createFromPosition(targetRange.start);
const result: vscode.LocationLink = {
originSelectionRange: toVsCodeRange(targetLink.range),
targetUri: toVsCodeUri(uri),
targetUri: toVsCodeUri(uri.asPlain()),
targetRange: toVsCodeRange(targetRange),
targetSelectionRange: toVsCodeRange(
Range.createFromPosition(targetRange.start, targetRange.start)
),
targetSelectionRange: toVsCodeRange(targetSelectionRange),
};
return [result];
}

View File

@@ -1,5 +1,5 @@
import MarkdownIt from 'markdown-it';
import { createMarkdownParser } from '../core/markdown-provider';
import { createMarkdownParser } from '../core/services/markdown-parser';
import { FoamWorkspace } from '../core/model/workspace';
import { createTestNote } from '../test/test-utils';
import {

View File

@@ -2,7 +2,7 @@ import { createTestNote } from '../test/test-utils';
import { cleanWorkspace, closeEditors } from '../test/test-utils-vscode';
import { TagItem, TagReference, TagsProvider } from './tags-tree-view';
import { bootstrap, Foam } from '../core/model/foam';
import { MarkdownResourceProvider } from '../core/markdown-provider';
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
import { FileDataStore, Matcher } from '../core/services/datastore';
describe('Tags tree panel', () => {
@@ -62,6 +62,7 @@ describe('Tags tree panel', () => {
childTreeItems.forEach(child => {
if (child instanceof TagItem) {
// eslint-disable-next-line jest/no-conditional-expect
expect(child.title).toEqual('child');
}
});
@@ -94,7 +95,9 @@ describe('Tags tree panel', () => {
childTreeItems.forEach(child => {
if (child instanceof TagItem) {
// eslint-disable-next-line jest/no-conditional-expect
expect(['child', 'subchild']).toContain(child.title);
// eslint-disable-next-line jest/no-conditional-expect
expect(child.title).not.toEqual('parent');
}
});

View File

@@ -24,25 +24,20 @@ const feature: FoamFeature = {
async (params: { uri: URI }) => {
const uri = new URI(params.uri);
switch (uri.scheme) {
case 'file':
let selection = new vscode.Range(1, 0, 1, 0);
if (uri.fragment) {
const foam = await foamPromise;
const resource = foam.workspace.get(uri);
const section = Resource.findSection(resource, uri.fragment);
if (section) {
selection = toVsCodeRange(section.range);
}
}
case 'file': {
const targetUri =
uri.path === vscode.window.activeTextEditor?.document.uri.path
? vscode.window.activeTextEditor?.document.uri
: toVsCodeUri(uri);
return vscode.commands.executeCommand('vscode.open', targetUri, {
selection: selection,
: toVsCodeUri(uri.asPlain());
const targetEditor = vscode.window.visibleTextEditors.find(
ed => targetUri.path === ed.document.uri.path
);
const column = targetEditor?.viewColumn;
return vscode.window.showTextDocument(targetEditor.document, {
viewColumn: column,
});
case 'placeholder':
}
case 'placeholder': {
const basedir =
vscode.workspace.workspaceFolders.length > 0
? vscode.workspace.workspaceFolders[0].uri
@@ -58,6 +53,7 @@ const feature: FoamFeature = {
.changeExtension('', '.md');
await NoteFactory.createForPlaceholderWikilink(title, target);
return;
}
}
}
)

View File

@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { createMarkdownParser } from '../core/markdown-provider';
import { createMarkdownParser } from '../core/services/markdown-parser';
import { FoamWorkspace } from '../core/model/workspace';
import {
cleanWorkspace,

View File

@@ -30,7 +30,7 @@ import { FoamWorkspace } from '../core/model/workspace';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from '../core/markdown-provider';
} from '../core/services/markdown-provider';
import {
LINK_REFERENCE_DEFINITION_FOOTER,
LINK_REFERENCE_DEFINITION_HEADER,
@@ -61,7 +61,7 @@ const feature: FoamFeature = {
// when a file is created as a result of peekDefinition
// action on a wikilink, add definition update references
foam.workspace.onDidAdd(_ => {
let editor = window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor || !isMdEditor(editor)) {
return;
}
@@ -79,13 +79,13 @@ function updateDocumentInNoteGraph(foam: Foam, document: TextDocument) {
}
async function createReferenceList(foam: FoamWorkspace) {
let editor = window.activeTextEditor;
const editor = window.activeTextEditor;
if (!editor || !isMdEditor(editor)) {
return;
}
let refs = await generateReferenceList(foam, editor.document);
const refs = await generateReferenceList(foam, editor.document);
if (refs && refs.length) {
await editor.edit(function(editBuilder) {
if (editor) {
@@ -213,7 +213,7 @@ class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
): CodeLens[] | Thenable<CodeLens[]> {
loadDocConfig();
let range = detectReferenceListRange(document);
const range = detectReferenceListRange(document);
if (!range) {
return [];
}
@@ -222,7 +222,7 @@ class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
const oldRefs = getText(range).replace(/\r?\n|\r/g, docConfig.eol);
const newRefs = refs.join(docConfig.eol);
let status = oldRefs === newRefs ? 'up to date' : 'out of date';
const status = oldRefs === newRefs ? 'up to date' : 'out of date';
return [
new CodeLens(range, {

View File

@@ -125,7 +125,7 @@ foam_template: # foam template metadata
const target = getUriInWorkspace();
const resolver = new Resolver(new Map(), new Date());
await NoteFactory.createFromTemplate(templateA.uri, resolver, target);
expect(await resolver.resolve('FOAM_SELECTED_TEXT')).toEqual(
expect(await resolver.resolveFromName('FOAM_SELECTED_TEXT')).toEqual(
'first file'
);

View File

@@ -72,9 +72,8 @@ export async function getTemplates(): Promise<URI[]> {
export const NoteFactory = {
/**
* Creates a new note using a template.
* @param givenValues already resolved values of Foam template variables. These are used instead of resolving the Foam template variables.
* @param extraVariablesToResolve Foam template variables to resolve, in addition to those mentioned in the template.
* @param templateUri the URI of the template to use.
* @param resolver the Resolver to use.
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
* @param templateFallbackText the template text to use if the template does not exist. This is configurable by the caller for backwards compatibility purposes.
*/
@@ -82,7 +81,7 @@ export const NoteFactory = {
templateUri: URI,
resolver: Resolver,
filepathFallbackURI?: URI,
templateFallbackText: string = ''
templateFallbackText = ''
): Promise<void> => {
const templateText = existsSync(templateUri.toFsPath())
? await workspace.fs
@@ -92,12 +91,13 @@ export const NoteFactory = {
const selectedContent = findSelectionContent();
resolver.define('FOAM_SELECTED_TEXT', selectedContent?.content ?? '');
if (selectedContent?.content) {
resolver.define('FOAM_SELECTED_TEXT', selectedContent?.content);
}
let templateWithResolvedVariables: string;
try {
[, templateWithResolvedVariables] = await resolver.resolveText(
templateText
);
templateWithResolvedVariables = await resolver.resolveText(templateText);
} catch (err) {
if (err instanceof UserCancelledOperation) {
return;
@@ -159,11 +159,7 @@ export const NoteFactory = {
templateFallbackText: string,
targetDate: Date
): Promise<void> => {
const resolver = new Resolver(
new Map(),
targetDate,
new Set(['FOAM_SELECTED_TEXT'])
);
const resolver = new Resolver(new Map(), targetDate);
return NoteFactory.createFromTemplate(
DAILY_NOTE_TEMPLATE_URI,
resolver,
@@ -183,8 +179,7 @@ export const NoteFactory = {
): Promise<void> => {
const resolver = new Resolver(
new Map().set('FOAM_TITLE', wikilinkPlaceholder),
new Date(),
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
new Date()
);
return NoteFactory.createFromTemplate(
DEFAULT_TEMPLATE_URI,
@@ -259,7 +254,7 @@ export async function determineNewNoteFilepath(
return fallbackURI;
}
const defaultName = await resolver.resolve('FOAM_TITLE');
const defaultName = await resolver.resolveFromName('FOAM_TITLE');
const defaultFilepath = getCurrentEditorDirectory().joinPath(
`${defaultName}.md`
);

View File

@@ -1,20 +1,34 @@
import { window } from 'vscode';
import { Resolver } from './variable-resolver';
import { Variable } from '../core/common/snippetParser';
describe('substituteFoamVariables', () => {
test('Does nothing if no Foam-specific variables are used', async () => {
const input = `
# \${AnotherVariable} <-- Unrelated to foam
# \${AnotherVariable:default_value} <-- Unrelated to foam
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to foam
# $AnotherVariable} <-- Unrelated to foam
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to foam
# \${AnotherVariable} <-- Unrelated to Foam
# \${AnotherVariable:default_value} <-- Unrelated to Foam
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to Foam
# $AnotherVariable} <-- Unrelated to Foam
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to Foam
`;
const givenValues = new Map<string, string>();
givenValues.set('FOAM_TITLE', 'My note title');
const resolver = new Resolver(givenValues, new Date());
expect((await resolver.resolveText(input))[1]).toEqual(input);
expect(await resolver.resolveText(input)).toEqual(input);
});
test('Ignores variable-looking text values', async () => {
// Related to https://github.com/foambubble/foam/issues/602
const input = `
# \${CURRENT_DATE/.*/\${FOAM_TITLE}/} <-- FOAM_TITLE is not a variable here, but a text in a transform
# \${1|one,two,\${FOAM_TITLE}|} <-- FOAM_TITLE is not a variable here, but a text in a choice
`;
const givenValues = new Map<string, string>();
givenValues.set('FOAM_TITLE', 'My note title');
const resolver = new Resolver(givenValues, new Date());
expect(await resolver.resolveText(input)).toEqual(input);
});
test('Correctly substitutes variables that are substrings of one another', async () => {
@@ -22,32 +36,31 @@ describe('substituteFoamVariables', () => {
// If we're not careful with how we substitute the values
// we can end up putting the FOAM_TITLE in place FOAM_TITLE_NON_EXISTENT_VARIABLE should be.
const input = `
# \${FOAM_TITLE}
# $FOAM_TITLE
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
`;
# \${FOAM_TITLE}
# $FOAM_TITLE
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
`;
const expected = `
# My note title
# My note title
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
`;
# My note title
# My note title
# \${FOAM_TITLE_NON_EXISTENT_VARIABLE}
# $FOAM_TITLE_NON_EXISTENT_VARIABLE
`;
const givenValues = new Map<string, string>();
givenValues.set('FOAM_TITLE', 'My note title');
const resolver = new Resolver(givenValues, new Date());
expect((await resolver.resolveText(input))[1]).toEqual(expected);
expect(await resolver.resolveText(input)).toEqual(expected);
});
});
describe('resolveFoamVariables', () => {
test('Does nothing for unknown Foam-specific variables', async () => {
const variables = ['FOAM_FOO'];
const variables = [new Variable('FOAM_FOO')];
const expected = new Map<string, string>();
expected.set('FOAM_FOO', 'FOAM_FOO');
const givenValues = new Map<string, string>();
const resolver = new Resolver(givenValues, new Date());
@@ -56,7 +69,7 @@ describe('resolveFoamVariables', () => {
test('Resolves FOAM_TITLE', async () => {
const foamTitle = 'My note title';
const variables = ['FOAM_TITLE'];
const variables = [new Variable('FOAM_TITLE'), new Variable('FOAM_SLUG')];
jest
.spyOn(window, 'showInputBox')
@@ -64,6 +77,7 @@ describe('resolveFoamVariables', () => {
const expected = new Map<string, string>();
expected.set('FOAM_TITLE', foamTitle);
expected.set('FOAM_SLUG', 'my-note-title');
const givenValues = new Map<string, string>();
const resolver = new Resolver(givenValues, new Date());
@@ -72,7 +86,7 @@ describe('resolveFoamVariables', () => {
test('Resolves FOAM_TITLE without asking the user when it is provided', async () => {
const foamTitle = 'My note title';
const variables = ['FOAM_TITLE'];
const variables = [new Variable('FOAM_TITLE')];
const expected = new Map<string, string>();
expected.set('FOAM_TITLE', foamTitle);
@@ -85,18 +99,18 @@ describe('resolveFoamVariables', () => {
test('Resolves FOAM_DATE_* properties with current day by default', async () => {
const variables = [
'FOAM_DATE_YEAR',
'FOAM_DATE_YEAR_SHORT',
'FOAM_DATE_MONTH',
'FOAM_DATE_MONTH_NAME',
'FOAM_DATE_MONTH_NAME_SHORT',
'FOAM_DATE_DATE',
'FOAM_DATE_DAY_NAME',
'FOAM_DATE_DAY_NAME_SHORT',
'FOAM_DATE_HOUR',
'FOAM_DATE_MINUTE',
'FOAM_DATE_SECOND',
'FOAM_DATE_SECONDS_UNIX',
new Variable('FOAM_DATE_YEAR'),
new Variable('FOAM_DATE_YEAR_SHORT'),
new Variable('FOAM_DATE_MONTH'),
new Variable('FOAM_DATE_MONTH_NAME'),
new Variable('FOAM_DATE_MONTH_NAME_SHORT'),
new Variable('FOAM_DATE_DATE'),
new Variable('FOAM_DATE_DAY_NAME'),
new Variable('FOAM_DATE_DAY_NAME_SHORT'),
new Variable('FOAM_DATE_HOUR'),
new Variable('FOAM_DATE_MINUTE'),
new Variable('FOAM_DATE_SECOND'),
new Variable('FOAM_DATE_SECONDS_UNIX'),
];
const expected = new Map<string, string>();
@@ -123,18 +137,18 @@ describe('resolveFoamVariables', () => {
test('Resolves FOAM_DATE_* properties with given date', async () => {
const targetDate = new Date(2021, 9, 12, 1, 2, 3);
const variables = [
'FOAM_DATE_YEAR',
'FOAM_DATE_YEAR_SHORT',
'FOAM_DATE_MONTH',
'FOAM_DATE_MONTH_NAME',
'FOAM_DATE_MONTH_NAME_SHORT',
'FOAM_DATE_DATE',
'FOAM_DATE_DAY_NAME',
'FOAM_DATE_DAY_NAME_SHORT',
'FOAM_DATE_HOUR',
'FOAM_DATE_MINUTE',
'FOAM_DATE_SECOND',
'FOAM_DATE_SECONDS_UNIX',
new Variable('FOAM_DATE_YEAR'),
new Variable('FOAM_DATE_YEAR_SHORT'),
new Variable('FOAM_DATE_MONTH'),
new Variable('FOAM_DATE_MONTH_NAME'),
new Variable('FOAM_DATE_MONTH_NAME_SHORT'),
new Variable('FOAM_DATE_DATE'),
new Variable('FOAM_DATE_DAY_NAME'),
new Variable('FOAM_DATE_DAY_NAME_SHORT'),
new Variable('FOAM_DATE_HOUR'),
new Variable('FOAM_DATE_MINUTE'),
new Variable('FOAM_DATE_SECOND'),
new Variable('FOAM_DATE_SECONDS_UNIX'),
];
const expected = new Map<string, string>();
@@ -164,16 +178,14 @@ describe('resolveFoamVariables', () => {
describe('resolveFoamTemplateVariables', () => {
test('Does nothing for template without Foam-specific variables', async () => {
const input = `
# \${AnotherVariable} <-- Unrelated to foam
# \${AnotherVariable:default_value} <-- Unrelated to foam
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to foam
# $AnotherVariable} <-- Unrelated to foam
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to foam
`;
# \${AnotherVariable} <-- Unrelated to Foam
# \${AnotherVariable:default_value} <-- Unrelated to Foam
# \${AnotherVariable:default_value/(.*)/\${1:/upcase}/}} <-- Unrelated to Foam
# $AnotherVariable} <-- Unrelated to Foam
# $CURRENT_YEAR-\${CURRENT_MONTH}-$CURRENT_DAY <-- Unrelated to Foam
`;
const expectedMap = new Map<string, string>();
const expectedString = input;
const expected = [expectedMap, expectedString];
const expected = input;
const resolver = new Resolver(new Map(), new Date());
expect(await resolver.resolveText(input)).toEqual(expected);
@@ -181,48 +193,17 @@ describe('resolveFoamTemplateVariables', () => {
test('Does nothing for unknown Foam-specific variables', async () => {
const input = `
# $FOAM_FOO
# \${FOAM_FOO}
# \${FOAM_FOO:default_value}
# \${FOAM_FOO:default_value/(.*)/\${1:/upcase}/}}
`;
const expectedMap = new Map<string, string>();
const expectedString = input;
const expected = [expectedMap, expectedString];
# $FOAM_FOO
# \${FOAM_FOO}
# \${FOAM_FOO:default_value}
# \${FOAM_FOO:default_value/(.*)/\${1:/upcase}/}}
`;
const expected = input;
const resolver = new Resolver(new Map(), new Date());
expect(await resolver.resolveText(input)).toEqual(expected);
});
test('Allows extra variables to be provided; only resolves the unique set', async () => {
const foamTitle = 'My note title';
jest
.spyOn(window, 'showInputBox')
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
const input = `
# $FOAM_TITLE
`;
const expectedOutput = `
# My note title
`;
const expectedMap = new Map<string, string>();
expectedMap.set('FOAM_TITLE', foamTitle);
const expected = [expectedMap, expectedOutput];
const resolver = new Resolver(
new Map(),
new Date(),
new Set(['FOAM_TITLE'])
);
expect(await resolver.resolveText(input)).toEqual(expected);
});
test('Appends FOAM_SELECTED_TEXT with a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template ends in a newline', async () => {
const foamTitle = 'My note title';
@@ -232,20 +213,11 @@ describe('resolveFoamTemplateVariables', () => {
const input = `# \${FOAM_TITLE}\n`;
const expectedOutput = `# My note title\nSelected text\n`;
const expected = `# My note title\nSelected text\n`;
const expectedMap = new Map<string, string>();
expectedMap.set('FOAM_TITLE', foamTitle);
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
const expected = [expectedMap, expectedOutput];
const givenValues = new Map<string, string>();
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
const resolver = new Resolver(
givenValues,
new Date(),
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
);
const resolver = new Resolver(givenValues, new Date());
expect(await resolver.resolveText(input)).toEqual(expected);
});
@@ -258,20 +230,11 @@ describe('resolveFoamTemplateVariables', () => {
const input = `# \${FOAM_TITLE}\n\n`;
const expectedOutput = `# My note title\n\nSelected text\n`;
const expected = `# My note title\n\nSelected text\n`;
const expectedMap = new Map<string, string>();
expectedMap.set('FOAM_TITLE', foamTitle);
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
const expected = [expectedMap, expectedOutput];
const givenValues = new Map<string, string>();
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
const resolver = new Resolver(
givenValues,
new Date(),
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
);
const resolver = new Resolver(givenValues, new Date());
expect(await resolver.resolveText(input)).toEqual(expected);
});
@@ -284,20 +247,11 @@ describe('resolveFoamTemplateVariables', () => {
const input = `# \${FOAM_TITLE}`;
const expectedOutput = '# My note title\nSelected text';
const expected = '# My note title\nSelected text';
const expectedMap = new Map<string, string>();
expectedMap.set('FOAM_TITLE', foamTitle);
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
const expected = [expectedMap, expectedOutput];
const givenValues = new Map<string, string>();
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
const resolver = new Resolver(
givenValues,
new Date(),
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
);
const resolver = new Resolver(givenValues, new Date());
expect(await resolver.resolveText(input)).toEqual(expected);
});
@@ -309,25 +263,15 @@ describe('resolveFoamTemplateVariables', () => {
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
const input = `
# \${FOAM_TITLE}
`;
# \${FOAM_TITLE}
`;
const expectedOutput = `
# My note title
`;
const expected = `
# My note title
`;
const expectedMap = new Map<string, string>();
expectedMap.set('FOAM_TITLE', foamTitle);
expectedMap.set('FOAM_SELECTED_TEXT', '');
const expected = [expectedMap, expectedOutput];
const givenValues = new Map<string, string>();
givenValues.set('FOAM_SELECTED_TEXT', '');
const resolver = new Resolver(
givenValues,
new Date(),
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT'])
);
const resolver = new Resolver(givenValues, new Date());
expect(await resolver.resolveText(input)).toEqual(expected);
});
});

View File

@@ -1,9 +1,16 @@
import { findSelectionContent } from './editor';
import { window } from 'vscode';
import { UserCancelledOperation } from './errors';
import { toSlug } from '../utils/slug';
import {
SnippetParser,
Variable,
VariableResolver,
} from '../core/common/snippetParser';
const knownFoamVariables = new Set([
'FOAM_TITLE',
'FOAM_SLUG',
'FOAM_SELECTED_TEXT',
'FOAM_DATE_YEAR',
'FOAM_DATE_YEAR_SHORT',
@@ -19,54 +26,17 @@ const knownFoamVariables = new Set([
'FOAM_DATE_SECONDS_UNIX',
]);
export function substituteVariables(
text: string,
variables: Map<string, string>
) {
variables.forEach((value, variable) => {
const regex = new RegExp(
// Matches a limited subset of the the TextMate variable syntax:
// ${VARIABLE} OR $VARIABLE
`\\\${${variable}}|\\$${variable}(\\W|$)`,
// The latter is more complicated, since it needs to avoid replacing
// longer variable names with the values of variables that are
// substrings of the longer ones (e.g. `$FOO` and `$FOOBAR`. If you
// replace $FOO first, and aren't careful, you replace the first
// characters of `$FOOBAR`)
'g' // 'g' => Global replacement (i.e. not just the first instance)
);
text = text.replace(regex, `${value}$1`);
});
return text;
}
export function findFoamVariables(templateText: string): string[] {
const regex = /\$(FOAM_[_a-zA-Z0-9]*)|\${(FOAM_[[_a-zA-Z0-9]*)}/g;
var matches = [];
const output: string[] = [];
while ((matches = regex.exec(templateText))) {
output.push(matches[1] || matches[2]);
}
const uniqVariables = [...new Set(output)];
const knownVariables = uniqVariables.filter(x => knownFoamVariables.has(x));
return knownVariables;
}
export class Resolver {
promises = new Map<string, Thenable<string>>();
export class Resolver implements VariableResolver {
private promises = new Map<string, Promise<string | undefined>>();
/**
* Create a resolver
*
* @param givenValues the map of variable name to value
* @param foamDate the date used to fill FOAM_DATE_* variables
* @param extraVariablesToResolve other variables to always resolve, even if not present in text
*/
constructor(
private givenValues: Map<string, string>,
private foamDate: Date,
private extraVariablesToResolve: Set<string> = new Set()
private foamDate: Date
) {}
/**
@@ -86,31 +56,38 @@ export class Resolver {
* @returns an array, where the first element is the resolution map,
* and the second is the processed text
*/
async resolveText(text: string): Promise<[Map<string, string>, string]> {
const variablesInTemplate = findFoamVariables(text.toString());
const variables = variablesInTemplate.concat(
...this.extraVariablesToResolve
async resolveText(text: string): Promise<string> {
let snippet = new SnippetParser().parse(text, false, false);
let foamVariablesInTemplate = new Set(
snippet
.variables()
.map(v => v.name)
.filter(name => knownFoamVariables.has(name))
);
const uniqVariables = [...new Set(variables)];
const resolvedValues = await this.resolveAll(uniqVariables);
// Add FOAM_SELECTED_TEXT to the template text if required
// and re-parse the template text.
if (
resolvedValues.get('FOAM_SELECTED_TEXT') &&
!variablesInTemplate.includes('FOAM_SELECTED_TEXT')
this.givenValues.has('FOAM_SELECTED_TEXT') &&
!foamVariablesInTemplate.has('FOAM_SELECTED_TEXT')
) {
text = text.endsWith('\n')
? `${text}\${FOAM_SELECTED_TEXT}\n`
: `${text}\n\${FOAM_SELECTED_TEXT}`;
variablesInTemplate.push('FOAM_SELECTED_TEXT');
variables.push('FOAM_SELECTED_TEXT');
uniqVariables.push('FOAM_SELECTED_TEXT');
const token = '$FOAM_SELECTED_TEXT';
if (text.endsWith('\n')) {
text = `${text}${token}\n`;
} else {
text = `${text}\n${token}`;
}
snippet = new SnippetParser().parse(text, false, false);
foamVariablesInTemplate = new Set(
snippet
.variables()
.map(v => v.name)
.filter(name => knownFoamVariables.has(name))
);
}
const subbedText = substituteVariables(text.toString(), resolvedValues);
return [resolvedValues, subbedText];
await snippet.resolveVariables(this, foamVariablesInTemplate);
return snippet.snippetTextWithVariablesSubstituted(foamVariablesInTemplate);
}
/**
@@ -119,19 +96,16 @@ export class Resolver {
* @param variables a list of variables to resolve
* @returns a Map of variable name to its value
*/
async resolveAll(variables: string[]): Promise<Map<string, string>> {
const promises = variables.map(async variable =>
Promise.resolve([variable, await this.resolve(variable)])
);
async resolveAll(variables: Variable[]): Promise<Map<string, string>> {
await Promise.all(variables.map(variable => variable.resolve(this)));
const results = await Promise.all(promises);
const valueByName = new Map<string, string>();
results.forEach(([variable, value]) => {
valueByName.set(variable, value);
const resolvedValues = new Map<string, string>();
variables.forEach(variable => {
if (variable.children.length > 0) {
resolvedValues.set(variable.name, variable.toString());
}
});
return valueByName;
return resolvedValues;
}
/**
@@ -140,7 +114,15 @@ export class Resolver {
* @param name the variable name
* @returns the resolved value, or the name of the variable if nothing is found
*/
resolve(name: string): Thenable<string> {
async resolveFromName(name: string): Promise<string> {
const variable = new Variable(name);
await variable.resolve(this);
return (variable.children[0] ?? name).toString();
}
async resolve(variable: Variable): Promise<string | undefined> {
const name = variable.name;
if (this.givenValues.has(name)) {
this.promises.set(name, Promise.resolve(this.givenValues.get(name)));
} else if (!this.promises.has(name)) {
@@ -148,6 +130,14 @@ export class Resolver {
case 'FOAM_TITLE':
this.promises.set(name, resolveFoamTitle());
break;
case 'FOAM_SLUG':
this.promises.set(
name,
Promise.resolve(
toSlug(await this.resolve(new Variable('FOAM_TITLE')))
)
);
break;
case 'FOAM_SELECTED_TEXT':
this.promises.set(name, Promise.resolve(resolveFoamSelectedText()));
break;
@@ -263,7 +253,7 @@ export class Resolver {
);
break;
default:
this.promises.set(name, Promise.resolve(name));
this.promises.set(name, Promise.resolve(undefined));
break;
}
}

View File

@@ -15,12 +15,14 @@
process.env.FORCE_COLOR = '1';
process.env.NODE_ENV = 'test';
import path from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import { runCLI } from '@jest/core';
import path from 'path';
const rootDir = path.join(__dirname, '..', '..');
export function runUnit(): Promise<void> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
try {
const { results } = await runCLI(

View File

@@ -15,9 +15,10 @@
process.env.FORCE_COLOR = '1';
process.env.NODE_ENV = 'test';
import path from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import { runCLI } from '@jest/core';
import { cleanWorkspace } from './test-utils-vscode';
import path from 'path';
const rootDir = path.join(__dirname, '../..');
@@ -38,6 +39,7 @@ export function run(): Promise<void> {
// throw err;
// });
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
await cleanWorkspace();
try {

View File

@@ -65,7 +65,7 @@ export const createFile = async (content: string, filepath: string[] = []) => {
};
export const createNote = (r: Resource) => {
let content = `# ${r.title}
const content = `# ${r.title}
some content and ${r.links
.map(l =>

View File

@@ -6,7 +6,7 @@ import { Range } from '../core/model/range';
import { URI } from '../core/model/uri';
import { FoamWorkspace } from '../core/model/workspace';
import { Matcher } from '../core/services/datastore';
import { MarkdownResourceProvider } from '../core/markdown-provider';
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
import { NoteLinkDefinition, Resource } from '../core/model/note';
Logger.setLevel('error');
@@ -88,6 +88,7 @@ export const createTestNote = (params: {
target: link.to,
label: 'link text',
range: range,
rawText: 'link text',
};
})
: [],

View File

@@ -26,7 +26,7 @@ export const mdDocSelector = [
export function loadDocConfig() {
// Load workspace config
let activeEditor = window.activeTextEditor;
const activeEditor = window.activeTextEditor;
if (!activeEditor) {
Logger.debug('Failed to load config, no active editor');
return;
@@ -34,8 +34,8 @@ export function loadDocConfig() {
docConfig.eol = activeEditor.document.eol === EndOfLine.CRLF ? '\r\n' : '\n';
let tabSize = Number(activeEditor.options.tabSize);
let insertSpaces = activeEditor.options.insertSpaces;
const tabSize = Number(activeEditor.options.tabSize);
const insertSpaces = activeEditor.options.insertSpaces;
if (insertSpaces) {
docConfig.tab = ' '.repeat(tabSize);
} else {
@@ -133,7 +133,7 @@ export function isSome<T>(
value: T | null | undefined | void
): value is NonNullable<T> {
//
return value != null; // eslint-disable-line
return value != null;
}
/**
@@ -144,7 +144,7 @@ export function isSome<T>(
export function isNone<T>(
value: T | null | undefined | void
): value is null | undefined | void {
return value == null; // eslint-disable-line
return value == null;
}
export async function focusNote(

View File

@@ -0,0 +1,3 @@
import slugger from 'github-slugger';
export const toSlug = (s: string) => slugger.slug(s);

View File

@@ -5,7 +5,7 @@
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-87-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-92-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Discord Chat](https://img.shields.io/discord/729975036148056075?color=748AD9&label=discord%20chat&style=flat-square)](https://foambubble.github.io/join-discord/g)
@@ -302,6 +302,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/AndreiD049"><img src="https://avatars.githubusercontent.com/u/52671223?v=4?s=60" width="60px;" alt=""/><br /><sub><b>AndreiD049</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=AndreiD049" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iam-yan"><img src="https://avatars.githubusercontent.com/u/48427014?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yan</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=iam-yan" title="Documentation">📖</a></td>
<td align="center"><a href="https://WikiEducator.org/User:JimTittsler"><img src="https://avatars.githubusercontent.com/u/180326?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jim Tittsler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jimt" title="Documentation">📖</a></td>
<td align="center"><a href="http://malcolmmielle.wordpress.com/"><img src="https://avatars.githubusercontent.com/u/4457840?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Malcolm Mielle</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MalcolmMielle" title="Documentation">📖</a></td>
<td align="center"><a href="https://snippets.page/"><img src="https://avatars.githubusercontent.com/u/74916913?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Veesar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=veesar" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/bentongxyz"><img src="https://avatars.githubusercontent.com/u/60358804?v=4?s=60" width="60px;" alt=""/><br /><sub><b>bentongxyz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bentongxyz" title="Code">💻</a></td>
<td align="center"><a href="https://brianjdevries.com"><img src="https://avatars.githubusercontent.com/u/42778030?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brian DeVries</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=techCarpenter" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://Cliffordfajardo.com"><img src="https://avatars.githubusercontent.com/u/6743796?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Clifford Fajardo </b></sub></a><br /><a href="#tool-cliffordfajardo" title="Tools">🔧</a></td>
</tr>
</table>

335
yarn.lock
View File

@@ -14,7 +14,7 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.11.tgz#9c8fe523c206979c9a81b1e12fe50c1254f1aa35"
integrity sha512-BwKEkO+2a67DcFeS3RLl0Z3Gs2OvdXewuWjc1Hfokhb5eQWP9YRYH1/+VrVZvql2CfjOiNGqSAFOYt4lsqTHzg==
"@babel/core@^7.1.0", "@babel/core@^7.11.0", "@babel/core@^7.4.4", "@babel/core@^7.7.5":
"@babel/core@^7.1.0", "@babel/core@^7.4.4", "@babel/core@^7.7.5":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559"
integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==
@@ -465,13 +465,6 @@
dependencies:
"@babel/helper-plugin-utils" "^7.12.13"
"@babel/plugin-syntax-typescript@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz#9dff111ca64154cef0f4dc52cf843d9f12ce4474"
integrity sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==
dependencies:
"@babel/helper-plugin-utils" "^7.12.13"
"@babel/plugin-transform-arrow-functions@^7.13.0":
version "7.13.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz#10a59bebad52d637a027afa692e8d5ceff5e3dae"
@@ -669,7 +662,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.12.13"
"@babel/plugin-transform-runtime@^7.10.4", "@babel/plugin-transform-runtime@^7.6.0":
"@babel/plugin-transform-runtime@^7.6.0":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz#a1e40d22e2bf570c591c9c7e5ab42d6bf1e419e1"
integrity sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA==
@@ -717,15 +710,6 @@
dependencies:
"@babel/helper-plugin-utils" "^7.12.13"
"@babel/plugin-transform-typescript@^7.13.0":
version "7.13.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.13.0.tgz#4a498e1f3600342d2a9e61f60131018f55774853"
integrity sha512-elQEwluzaU8R8dbVuW2Q2Y8Nznf7hnjM7+DSCd14Lo5fF63C9qNLbwZYbmZrtV9/ySpSUpkRpQXvJb6xyu4hCQ==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.13.0"
"@babel/helper-plugin-utils" "^7.13.0"
"@babel/plugin-syntax-typescript" "^7.12.13"
"@babel/plugin-transform-unicode-escapes@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz#840ced3b816d3b5127dd1d12dcedc5dead1a5e74"
@@ -749,7 +733,7 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.4"
"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.4.4":
"@babel/preset-env@^7.4.4":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.13.10.tgz#b5cde31d5fe77ab2a6ab3d453b59041a1b3a5252"
integrity sha512-nOsTScuoRghRtUsRr/c69d042ysfPHcu+KOB4A9aAO9eJYqrkat+LF8G1yp1HD18QiwixT2CisZTr/0b3YZPXQ==
@@ -834,15 +818,6 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
"@babel/preset-typescript@^7.10.4":
version "7.13.0"
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz#ab107e5f050609d806fbb039bec553b33462c60a"
integrity sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==
dependencies:
"@babel/helper-plugin-utils" "^7.13.0"
"@babel/helper-validator-option" "^7.12.17"
"@babel/plugin-transform-typescript" "^7.13.0"
"@babel/runtime-corejs3@^7.10.2":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz#14c3f4c85de22ba88e8e86685d13e8861a82fe86"
@@ -2006,11 +1981,32 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.stat@^1.1.2":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@octokit/auth-token@^2.4.0":
version "2.4.5"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3"
@@ -2309,6 +2305,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
"@types/json-schema@^7.0.9":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@@ -2456,6 +2457,18 @@
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/experimental-utils@^5.0.0":
version "5.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.8.0.tgz#0916ffe98d34b3c95e3652efa0cace61a7b25728"
integrity sha512-KN5FvNH71bhZ8fKtL+lhW7bjm7cxs1nt+hrDZWIqb6ViCffQcWyLunGrgvISgkRojIDcXIsH+xlFfI4RCDA0xA==
dependencies:
"@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "5.8.0"
"@typescript-eslint/types" "5.8.0"
"@typescript-eslint/typescript-estree" "5.8.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/parser@^2.12.0", "@typescript-eslint/parser@^2.30.0":
version "2.34.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8"
@@ -2466,6 +2479,19 @@
"@typescript-eslint/typescript-estree" "2.34.0"
eslint-visitor-keys "^1.1.0"
"@typescript-eslint/scope-manager@5.8.0":
version "5.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.8.0.tgz#2371095b4fa4c7be6a80b380f4e1b49c715e16f4"
integrity sha512-x82CYJsLOjPCDuFFEbS6e7K1QEWj7u5Wk1alw8A+gnJiYwNnDJk0ib6PCegbaPMjrfBvFKa7SxE3EOnnIQz2Gg==
dependencies:
"@typescript-eslint/types" "5.8.0"
"@typescript-eslint/visitor-keys" "5.8.0"
"@typescript-eslint/types@5.8.0":
version "5.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.8.0.tgz#e7fa74ec35d9dbe3560d039d3d8734986c3971e0"
integrity sha512-LdCYOqeqZWqCMOmwFnum6YfW9F3nKuxJiR84CdIRN5nfHJ7gyvGpXWqL/AaW0k3Po0+wm93ARAsOdzlZDPCcXg==
"@typescript-eslint/typescript-estree@2.34.0":
version "2.34.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5"
@@ -2479,6 +2505,27 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@5.8.0":
version "5.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.8.0.tgz#900469ba9d5a37f4482b014ecce4a5dbb86cb4dd"
integrity sha512-srfeZ3URdEcUsSLbkOFqS7WoxOqn8JNil2NSLO9O+I2/Uyc85+UlfpEvQHIpj5dVts7KKOZnftoJD/Fdv0L7nQ==
dependencies:
"@typescript-eslint/types" "5.8.0"
"@typescript-eslint/visitor-keys" "5.8.0"
debug "^4.3.2"
globby "^11.0.4"
is-glob "^4.0.3"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@5.8.0":
version "5.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.8.0.tgz#22d4ed96fe2451135299239feedb9fe1dcec780c"
integrity sha512-+HDIGOEMnqbxdAHegxvnOqESUH6RWFRR2b8qxP1W9CZnnYh4Usz6MBL+2KMAgPk/P0o9c1HqnYtwzVH6GTIqug==
dependencies:
"@typescript-eslint/types" "5.8.0"
eslint-visitor-keys "^3.0.0"
"@zkochan/cmd-shim@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz#2ab8ed81f5bb5452a85f25758eb9b8681982fd2e"
@@ -2790,6 +2837,11 @@ array-union@^1.0.2:
dependencies:
array-uniq "^1.0.1"
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
array-uniq@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
@@ -3146,9 +3198,9 @@ bail@^1.0.0:
integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base@^0.11.1:
version "0.11.2"
@@ -4049,6 +4101,13 @@ debug@^3.1.0, debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@^4.3.1, debug@^4.3.2:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
dependencies:
ms "2.1.2"
debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
@@ -4188,6 +4247,13 @@ dir-glob@^2.2.2:
dependencies:
path-type "^3.0.0"
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
doctrine@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@@ -4479,6 +4545,17 @@ eslint-import-resolver-node@^0.3.6:
debug "^3.2.7"
resolve "^1.20.0"
eslint-import-resolver-typescript@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz#07661966b272d14ba97f597b51e1a588f9722f0a"
integrity sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ==
dependencies:
debug "^4.3.1"
glob "^7.1.7"
is-glob "^4.0.1"
resolve "^1.20.0"
tsconfig-paths "^3.9.0"
eslint-module-utils@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
@@ -4542,6 +4619,13 @@ eslint-plugin-import@^2.24.2:
resolve "^1.20.0"
tsconfig-paths "^3.11.0"
eslint-plugin-jest@^25.3.0:
version "25.3.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-25.3.0.tgz#6c04bbf13624a75684a05391a825b58e2e291950"
integrity sha512-79WQtuBsTN1S8Y9+7euBYwxIOia/k7ykkl9OCBHL3xuww5ecursHy/D8GCIlvzHVWv85gOkS5Kv6Sh7RxOgK1Q==
dependencies:
"@typescript-eslint/experimental-utils" "^5.0.0"
eslint-plugin-jsx-a11y@^6.2.3:
version "6.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd"
@@ -4588,7 +4672,7 @@ eslint-plugin-react@^7.14.3:
resolve "^1.18.1"
string.prototype.matchall "^4.0.2"
eslint-scope@^5.0.0:
eslint-scope@^5.0.0, eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -4610,11 +4694,28 @@ eslint-utils@^2.0.0:
dependencies:
eslint-visitor-keys "^1.1.0"
eslint-utils@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
dependencies:
eslint-visitor-keys "^2.0.0"
eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
eslint-visitor-keys@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint-visitor-keys@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2"
integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==
eslint@^6.1.0, eslint@^6.8.0:
version "6.8.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
@@ -4887,6 +4988,17 @@ fast-glob@^2.2.6:
merge2 "^1.2.3"
micromatch "^3.1.10"
fast-glob@^3.1.1:
version "3.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -4897,6 +5009,13 @@ fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
dependencies:
reusify "^1.0.4"
fault@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
@@ -5302,6 +5421,11 @@ gitconfiglocal@^1.0.0:
dependencies:
ini "^1.3.2"
github-slugger@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e"
integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -5310,7 +5434,7 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
glob-parent@^5.0.0:
glob-parent@^5.0.0, glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -5322,10 +5446,10 @@ glob-to-regexp@^0.3.0:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@@ -5356,6 +5480,18 @@ globalyzer@0.1.0:
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465"
integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==
globby@^11.0.4:
version "11.0.4"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.1.1"
ignore "^5.1.4"
merge2 "^1.3.0"
slash "^3.0.0"
globby@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
@@ -5644,6 +5780,11 @@ ignore@^4.0.3, ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.4:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
@@ -5884,6 +6025,13 @@ is-core-module@^2.6.0:
dependencies:
has "^1.0.3"
is-core-module@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
dependencies:
has "^1.0.3"
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -5994,6 +6142,13 @@ is-glob@^4.0.0, is-glob@^4.0.1:
dependencies:
is-extglob "^2.1.1"
is-glob@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-hexadecimal@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
@@ -7627,9 +7782,9 @@ markdown-it-regex@^0.2.0:
integrity sha512-111UnMGJSt37gy+DlgcpQNwEfS2jvscOFSztzGhuXUHk7K1J5eAEj6C3jifmKb0cWtTuxdpHgIt4PyGQ+DtDjw==
markdown-it@^12.0.4:
version "12.0.4"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33"
integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==
version "12.3.2"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90"
integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==
dependencies:
argparse "^2.0.1"
entities "~2.1.0"
@@ -7695,7 +7850,7 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
merge2@^1.2.3:
merge2@^1.2.3, merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
@@ -7727,6 +7882,14 @@ micromatch@^4.0.2:
braces "^3.0.1"
picomatch "^2.0.5"
micromatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
dependencies:
braces "^3.0.1"
picomatch "^2.2.3"
mime-db@1.46.0:
version "1.46.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.46.0.tgz#6267748a7f799594de3cbc8cde91def349661cee"
@@ -7961,9 +8124,11 @@ node-fetch-npm@^2.0.2:
safe-buffer "^5.1.1"
node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
node-gyp@^5.0.2:
version "5.1.1"
@@ -8600,6 +8765,11 @@ path-parse@^1.0.6:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@@ -8643,6 +8813,11 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.2:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
picomatch@^2.2.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -8913,6 +9088,11 @@ query-string@^6.13.8:
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quick-lru@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
@@ -9367,7 +9547,7 @@ resolve@1.15.1:
dependencies:
path-parse "^1.0.6"
resolve@1.x, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.20.0:
resolve@1.x, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -9375,6 +9555,15 @@ resolve@1.x, resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0,
is-core-module "^2.2.0"
path-parse "^1.0.6"
resolve@^1.1.6:
version "1.21.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f"
integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==
dependencies:
is-core-module "^2.8.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@@ -9401,6 +9590,11 @@ retry@^0.10.0:
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rimraf@2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@@ -9486,6 +9680,13 @@ run-async@^2.2.0, run-async@^2.4.0:
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
run-queue@^1.0.0, run-queue@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
@@ -9596,6 +9797,13 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
serialize-javascript@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
@@ -9655,9 +9863,9 @@ shebang-regex@^3.0.0:
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shelljs@^0.8.3:
version "0.8.4"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
version "0.8.5"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
dependencies:
glob "^7.0.0"
interpret "^1.0.0"
@@ -10178,6 +10386,11 @@ supports-hyperlinks@^2.0.0:
has-flag "^4.0.0"
supports-color "^7.0.0"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
symbol-tree@^3.2.2, symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -10421,6 +10634,11 @@ tr46@^2.0.2:
dependencies:
punycode "^2.1.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
"traverse@>=0.3.0 <0.4":
version "0.3.9"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
@@ -10442,9 +10660,9 @@ trim-newlines@^3.0.0:
integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==
trim-off-newlines@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
version "1.0.3"
resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.3.tgz#8df24847fcb821b0ab27d58ab6efec9f2fe961a1"
integrity sha512-kh6Tu6GbeSNMGfrrZh6Bb/4ZEHV1QlB4xNDBeog8Y9/QwFlKTRyWvY3Fs9tRDAMZliVUwieMgEdIeL/FtqjkJg==
trim-trailing-lines@^1.0.0:
version "1.1.4"
@@ -10595,7 +10813,7 @@ tslib@^2.0.0, tslib@^2.0.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tsutils@^3.17.1:
tsutils@^3.17.1, tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
@@ -11025,6 +11243,11 @@ wcwidth@^1.0.0, wcwidth@^1.0.1:
dependencies:
defaults "^1.0.3"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -11052,6 +11275,14 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^6.4.1:
version "6.5.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"