Compare commits

...

63 Commits

Author SHA1 Message Date
Riccardo Ferretti
4a410d1f5c v0.25.12 2024-07-13 14:14:16 +02:00
Riccardo Ferretti
ccb92ad5ee Preparation for next release 2024-07-13 14:13:04 +02:00
Riccardo Ferretti
e6512cffa8 Fixed imports 2024-07-13 14:09:32 +02:00
Riccardo Ferretti
1fa4f37d96 Moved around settings functions 2024-07-13 13:51:23 +02:00
Riccardo Ferretti
27b9b451ad Refactored utils.ts 2024-07-13 13:51:23 +02:00
allcontributors[bot]
362d6f8e09 add hereistheusername as a contributor for code (#1367)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-07-10 20:39:31 +02:00
Xinglan Liu
cef8d2a532 generate copy without wikilinks (#1365)
* add generate standalone note command

* fix embeded wikilinks

* refactor convertLinksFormat function & add 4 user command interfaces

* change user interface

* modify createUpdateLinkEdit to accomplish convert

* only images can be embedded

* keey filename when using in page anchor

* give a default value to alias in link format combination branch

* add tests to createUpdateLinkEdit about changint links' type and isEmbed

* get target from getIdentifier

---------

Co-authored-by: Riccardo <code@riccardoferretti.com>
2024-07-10 20:38:51 +02:00
Riccardo Ferretti
22b837f252 Adding redirect to code-of-conduct 2024-06-03 23:43:54 +02:00
allcontributors[bot]
07e02c2d69 add Walshkev as a contributor for doc (#1359)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-05-31 13:15:28 +02:00
Kevin Walsh
931ad7a5b6 [doc] documentation additions (#1358)
* Update readme.md to include code of conduct 

lack of code of conduct that was in the previous version of foam _1

* added the Contribution Guide 

this has a link to their Contribution Guide that is already on the foams website.

* Update readme.md presentation 

added See the before the contribution guide and code of conduct links to make it more professional

* Update readme.md

---------

Co-authored-by: Riccardo <code@riccardoferretti.com>
2024-05-31 13:15:13 +02:00
Riccardo Ferretti
db7eb9775f Improved YAML regex delimiter
Fixes #1347
2024-03-22 09:23:57 +01:00
Riccardo Ferretti
b25152d115 v0.25.11 2024-03-18 13:17:27 +01:00
Riccardo Ferretti
1545079c62 Prepare for release 2024-03-18 13:17:04 +01:00
Riccardo Ferretti
4835164902 Fire onDidUpdate even only after full graph recomputed 2024-03-18 13:16:26 +01:00
Riccardo Ferretti
06efdc2865 v0.25.10 2024-03-18 10:09:31 +01:00
Riccardo Ferretti
b68fd7e138 Prepare for release 2024-03-18 10:09:04 +01:00
Riccardo Ferretti
d8baa2fd36 Fixed graph computation issue 2024-03-18 10:06:18 +01:00
Riccardo Ferretti
7f587095e8 v0.25.9 2024-03-17 20:51:24 +01:00
Riccardo Ferretti
77ad245319 Prepare next release 2024-03-17 20:51:16 +01:00
Riccardo
b892c783da Rename placeholder on note creation so it can update it if necessary (#1344)
* Introduced Location
* Passing a reference to the source link to the create-note command

Also
* Added withTiming fn for performance logging
* Added extra test to check incoming wikilink with sections
* Tweaked creation of vscode URI to also support raw objects
2024-03-17 20:49:11 +01:00
Andrew Thiesen
e4f6259104 Update recipes.md (#1341)
REM foamy-js. https://github.com/yenly/foamy-nextjs has been Archived and is no longer being maintained.
2024-03-10 15:18:37 +01:00
Riccardo Ferretti
aa197239fc v0.25.8 2024-02-21 09:53:40 +01:00
Riccardo Ferretti
8f3c23dd60 Prepare for release 2024-02-21 09:47:23 +01:00
Richard C Yeh
9a027c08ba Bump dateformat from 3.0.3 to 4.5.1. Closes #1324. (#1326) 2024-02-19 10:15:34 +01:00
dependabot[bot]
959d0f1ea1 Bump axios from 1.3.2 to 1.6.1 (#1306)
Bumps [axios](https://github.com/axios/axios) from 1.3.2 to 1.6.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.3.2...v1.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 19:48:34 +01:00
dependabot[bot]
57e32c4349 Bump @babel/traverse from 7.20.13 to 7.23.2 (#1297)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.20.13 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 19:48:10 +01:00
dependabot[bot]
f168f66368 Bump follow-redirects from 1.15.2 to 1.15.4 (#1318)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 19:47:46 +01:00
Riccardo Ferretti
103ff12b2d v0.25.7 2024-01-16 14:47:01 +01:00
Riccardo Ferretti
96a3afa132 Prepare for release 2024-01-16 14:46:11 +01:00
Riccardo Ferretti
d586e63104 Removed "use" from quick fix list elements 2024-01-16 14:29:54 +01:00
Miguel Angel Bruni Montero
2fba6e9008 Modifies url encoding to target only the filename and skip spaces (#1322) 2024-01-15 20:22:04 +01:00
Riccardo
cdbb965661 Update commands.md 2023-12-15 22:31:54 +01:00
Riccardo Ferretti
0c958e31f6 v0.25.6 2023-12-13 20:01:32 +01:00
Riccardo Ferretti
fd84fcfd74 Preparing next release 2023-12-13 20:01:00 +01:00
allcontributors[bot]
becf495edc add MABruni as a contributor for code (#1313)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-12-13 19:59:11 +01:00
Miguel Angel Bruni Montero
4d99883c03 Fix #1298. Wikilink definitions links not working. (#1311)
* Added encoding to handle special characters when wikilink definitions are generated

* Added tests for wikilinks encoding and updated previous tests to support encoded links
2023-12-13 19:58:36 +01:00
Riccardo Ferretti
8bd679c751 v0.25.5 2023-11-30 19:19:26 +01:00
Riccardo Ferretti
8986ee286c Preparation for next release 2023-11-30 19:19:03 +01:00
Riccardo
51b1af1981 Using note title in preview panel (#1309)
See conversation in https://github.com/foambubble/foam/pull/1150
2023-11-30 19:17:50 +01:00
Daniel Carosone
4276e8043f extension descriptions now reflect .vscode dir (#1302)
* extension descriptions now reflect .vscode dir

including that image pasting is now supported natively

* Markdown AiO is still recommended

---------

Co-authored-by: Daniel Carosone <dan@geek.com.au>
2023-10-26 10:24:51 +02:00
Riccardo Ferretti
8d1e9b15ce Readding markdown-all-in-one in extension list and fixing script 2023-10-26 10:23:45 +02:00
jonathan berger
bfcfad32e8 Add more detail to Support for sections. (#1296)
* Add more detail to Support for `sections`.

* Removed extra quote

---------

Co-authored-by: Riccardo <code@riccardoferretti.com>
2023-10-25 12:53:00 +02:00
allcontributors[bot]
abe18cc961 add dcarosone as a contributor for doc (#1301)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-10-25 11:43:29 +02:00
Riccardo Ferretti
9490aa2dad Fixed typo 2023-10-25 11:36:34 +02:00
Riccardo Ferretti
6c3f4588b3 Explicitly create .vscode folder in sync script 2023-10-25 11:26:07 +02:00
Riccardo Ferretti
21b8c5b827 Updated default extensions and settings for foam docs and template
Also monitoring the folder to trigger the appropriate sync job
2023-10-25 11:22:46 +02:00
Daniel Carosone
c66ed74aea replicate .vscode dir to template (#1300)
fixes #1299

Co-authored-by: Daniel Carosone <dan@geek.com.au>
2023-10-25 11:17:43 +02:00
Riccardo Ferretti
3876811fb6 v0.25.4 2023-10-19 17:41:13 +02:00
Riccardo Ferretti
d9299ee9d4 Prepare for next release 2023-10-19 17:40:29 +02:00
Joe Taber
23e21a62f3 Copy assets to foam-template repo (#1238) 2023-10-12 17:22:38 +02:00
Riccardo
e7c8d5a4eb Added support for linking to sections in current file (#1289)
And improved support for links to sections that need to be slugified
2023-10-12 17:01:13 +02:00
Daniel Wang
3ef1b69b2e Fix embedded notes not generating proper reference links (#1286) 2023-09-25 10:09:13 +02:00
Riccardo Ferretti
5859b2a9c6 v0.25.3 2023-09-07 13:07:26 +02:00
Riccardo Ferretti
54086fdd7e Preparation for next release 2023-09-07 13:07:06 +02:00
Daniel Wang
a308dfd109 Handle a case where the embedded note does not exist (#1283) 2023-09-07 09:03:25 +02:00
Daniel Wang
e327115673 Feat #879: Note Embedding Syntax (#1281)
* Removed deprecated preview.embedNoteInContainer

* Integrate explicit type modifiers to wikilink syntax

* Add to documentation
2023-09-06 00:08:56 +02:00
Riccardo Ferretti
c019767476 Fix #1242 - attachments are not considered in the context of orphan notes
A note that only has outbound links to attachments is now still considered an orphan.
2023-09-03 11:46:31 +02:00
Riccardo Ferretti
5e8b817a82 v0.25.2 2023-09-02 19:02:14 +02:00
Riccardo Ferretti
e0acc0ba8c Updated changelog 2023-09-02 19:01:49 +02:00
Riccardo
df4efc5138 Expand all button in tree views (#1276)
* Added expand all util function

* Added expand-all command for notes explorer

* Added expand-all command for placeholder tree view

* Added expand-all in tags explorer

* Added id field to tree items to make `reveal` work for all items
2023-08-30 16:19:14 +02:00
allcontributors[bot]
1e2b3b1bc3 add thara as a contributor for doc (#1280)
* update docs/index.md [skip ci]

* update readme.md [skip ci]

* update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-08-30 16:17:45 +02:00
Daniel Wang
eee364de50 Feat #879: Content embedding style (#1279)
* Add content extractor and enable it as an option for note embed type

* Fix content extractor edgecase where note title has lines above it
2023-08-30 16:17:25 +02:00
Tomochika Hara
3ed2bcd37b Fix deadlink (#1278) 2023-08-30 16:02:53 +02:00
77 changed files with 2171 additions and 659 deletions

View File

@@ -1049,6 +1049,51 @@
"contributions": [
"code"
]
},
{
"login": "thara",
"name": "Tomochika Hara",
"avatar_url": "https://avatars.githubusercontent.com/u/1532891?v=4",
"profile": "https://thara.dev",
"contributions": [
"doc"
]
},
{
"login": "dcarosone",
"name": "Daniel Carosone",
"avatar_url": "https://avatars.githubusercontent.com/u/11495017?v=4",
"profile": "https://github.com/dcarosone",
"contributions": [
"doc"
]
},
{
"login": "MABruni",
"name": "Miguel Angel Bruni Montero",
"avatar_url": "https://avatars.githubusercontent.com/u/100445384?v=4",
"profile": "https://github.com/MABruni",
"contributions": [
"code"
]
},
{
"login": "Walshkev",
"name": "Kevin Walsh ",
"avatar_url": "https://avatars.githubusercontent.com/u/77123083?v=4",
"profile": "https://github.com/Walshkev",
"contributions": [
"doc"
]
},
{
"login": "hereistheusername",
"name": "Xinglan Liu",
"avatar_url": "https://avatars.githubusercontent.com/u/33437051?v=4",
"profile": "http://hereistheusername.github.io/",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -6,6 +6,8 @@ on:
- master
paths:
- docs/user/**/*
- docs/.vscode/**/*
- docs/assets/**/*
workflow_dispatch:
jobs:
@@ -20,11 +22,15 @@ jobs:
- uses: actions/checkout@v3
with:
path: foam
- name: Copy and fixup user docs files
- name: Copy and fixup user docs files
id: copy
run: |
rm -r foam-template/docs
rm -r foam-template/assets
rm -r foam-template/.vscode
cp -r foam/docs/user foam-template/docs
cp -r foam/docs/assets foam-template/assets
cp -r foam/docs/.vscode foam-template/.vscode
# Strip autogenerated wikileaks references because
# they are not an appropriate default user experience.

View File

@@ -5,17 +5,11 @@
// Foam's own extension
"foam.foam-vscode",
// Prettier for auto formatting code
"esbenp.prettier-vscode",
// GitLens for seeing version history inline
"eamodio.gitlens",
// Tons of markdown goodies (lists, tables of content, so much more)
"yzhang.markdown-all-in-one",
// Graph visualizer
"tchayen.markdown-links",
// Prettier for auto formatting code
"esbenp.prettier-vscode",
// Understated grayscale theme (light and dark variants)
"philipbe.theme-gray-matter"

View File

@@ -1,4 +0,0 @@
{
"purpose": "this file exists to tell the foam-vscode plugin that it's currently in a foam workspace",
"future": "we may use this for custom configuration"
}

View File

@@ -3,6 +3,6 @@
[
{
"key": "cmd+shift+n",
"command": "vscodeMarkdownNotes.newNote"
"command": "foam-vscode.create-note"
}
]

View File

@@ -5,7 +5,6 @@
"editor.overviewRulerBorder": false,
"editor.lineHeight": 24,
"foam.edit.linkReferenceDefinitions": "withExtensions",
"vscodeMarkdownNotes.noteCompletionConvention": "noExtension",
"[markdown]": {
"editor.quickSuggestions": {
"other": true,
@@ -13,21 +12,11 @@
"strings": false
}
},
"cSpell.language": "en-GB",
"git.enableSmartCommit": true,
"git.postCommitCommand": "sync",
"spellright.language": [
"en"
],
"spellright.documentTypes": [
"markdown",
"plaintext"
],
"files.exclude": {
"_site/**": true
},
"files.insertFinalNewline": true,
"markdown.styles": [
".vscode/custom-tag-style.css"
]
"markdown.styles": [".vscode/custom-tag-style.css"]
}

View File

@@ -1 +0,0 @@
Backlinking

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

View File

@@ -1,3 +1,8 @@
---
redirect_from:
- /code-of-conduct
---
# Code of Conduct
We follow the [Contributor Covenant](https://www.contributor-covenant.org/) code of conduct.

View File

@@ -93,7 +93,7 @@ After you have made your changes to your copy of the project, it is time to try
1. Return to the project's [home repository page](https://github.com/foambubble/foam).
2. Github should show you an button called "Compare & pull request" linking your forked repository to the community repository.
3. Click that button and confirm that your repository is going to be merged into the community repository. See [this guide](https://sqldbawithabeard.com/2019/11/29/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/) for more specifics.
3. Click that button and confirm that your repository is going to be merged into the community repository. See [this guide](https://blog.robsewell.com/blog/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/) for more specifics.
4. Add as many relevant details to the PR message to make it clear to the project maintainers and other members of the community what you have accomplished with your new changes. Link to any issues the changes are related to.
5. Your PR will then need to be reviewed and accepted by the other members of the community. Any discussion about the changes will occur in your PR thread.
6. Once reviewed and accept you can complete the merge request!

View File

@@ -254,6 +254,11 @@ If that sounds like something you're interested in, I'd love to have you along o
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://amnesiak.org/me"><img src="https://avatars.githubusercontent.com/u/952059?v=4?s=60" width="60px;" alt="Tony Cheneau"/><br /><sub><b>Tony Cheneau</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tcheneau" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicholas-l"><img src="https://avatars.githubusercontent.com/u/12977174?v=4?s=60" width="60px;" alt="Nicholas Latham"/><br /><sub><b>Nicholas Latham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nicholas-l" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://thara.dev"><img src="https://avatars.githubusercontent.com/u/1532891?v=4?s=60" width="60px;" alt="Tomochika Hara"/><br /><sub><b>Tomochika Hara</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=thara" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dcarosone"><img src="https://avatars.githubusercontent.com/u/11495017?v=4?s=60" width="60px;" alt="Daniel Carosone"/><br /><sub><b>Daniel Carosone</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dcarosone" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MABruni"><img src="https://avatars.githubusercontent.com/u/100445384?v=4?s=60" width="60px;" alt="Miguel Angel Bruni Montero"/><br /><sub><b>Miguel Angel Bruni Montero</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MABruni" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Walshkev"><img src="https://avatars.githubusercontent.com/u/77123083?v=4?s=60" width="60px;" alt="Kevin Walsh "/><br /><sub><b>Kevin Walsh </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Walshkev" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hereistheusername.github.io/"><img src="https://avatars.githubusercontent.com/u/33437051?v=4?s=60" width="60px;" alt="Xinglan Liu"/><br /><sub><b>Xinglan Liu</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hereistheusername" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,29 @@
# Built-In Note Embedding Types
When embedding a note, there are a few ways to modify the scope of the content as well as its display style. The following are Foam keywords that are used to describe note embedding.
Note, this only applies to note embedding, not embedding of attachments or images.
![Note Embed Types GIF](../../assets/images/note-embed-type-demo.gif)
## Scope
- `full` - the entire note in the case of `![[note]]` or the entire section in the case of `![[note#section1]]`
- `content` - everything excluding the title of the section. So the entire note minus the title for `![[note]]`, or the entire section minus the section header for `![[note#section1]]`
## Style
- `card` - outlines the embedded note with a border
- `inline` - adds the note continuously as if the text were part of the calling note
## Default Setting
Foam expresses note display type as `<scope>-<style>`.
By default, Foam configures note embedding to be `full-card`. That is, whenever the standard embedding syntax is used, `![[note]]`, the note will have `full` scope and `card` style display. This setting is stored under `foam.preview.embedNoteStyle` and can be modified.
## Explicit Modifiers
Prepend the wikilink with one of the scope or style keywords, or a combination of the two to explicitly modify a note embedding if you would like to override the default setting.
For example, given your `foam.embedNoteStyle` is set to `content-card`, embedding a note with standard syntax `![[note-a]]` would show a bordered note without its title. Say, for a specific `note-b` you would like to display the title. You can simply use one of the above keywords to override your default setting like so: `full![[note-b]]`. In this case, `full` overrides the default `content` scope and because a style is not specified, it falls back to the default style setting, `card`. If you would like it to be inline, override that as well: `full-inline![[note-b]]`.

View File

@@ -10,13 +10,13 @@ This command creates a note.
Although it works fine on its own, it can be customized to achieve various use cases.
Here are the settings available for the command:
- notePath: The path of the note to create. If relative it will be resolved against the workspace root.
- templatePath: The path of the template to use. If relative it will be resolved against the workspace root.
- title: The title of the note (that is, the `FOAM_TITLE` variable)
- text: The text to use for the note. If also a template is provided, the template has precedence
- variables: Variables to use in the text or template
- date: The date used to resolve the FOAM*DATE*\* variables. in `YYYY-MM-DD` format
- onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel': What to do in case the target file already exists
- `notePath`: The path of the note to create. If relative it will be resolved against the workspace root.
- `templatePath`: The path of the template to use. If relative it will be resolved against the workspace root.
- `title`: The title of the note (that is, the `FOAM_TITLE` variable)
- `text`: The text to use for the note. If also a template is provided, the template has precedence
- `variables`: Variables to use in the text or template
- `date`: The date used to resolve the FOAM*DATE*\* variables. in `YYYY-MM-DD` format
- `onFileExists?: 'overwrite' | 'open' | 'ask' | 'cancel'`: What to do in case the target file already exists
To customize a command and associate a key binding to it, open the key binding settings and add the appropriate configuration, here are some examples:

View File

@@ -8,13 +8,16 @@ Including a note can be done by adding an `!` before a wikilink definition. For
## Custom styling
Displaying the inclusion of notes allows for some custom styling, see [[custom-markdown-preview-styles]]
To modify how an embedded note looks and the scope of its content, see [[built-in-note-embedding-types]]
For more fine-grained custom styling, see [[custom-markdown-preview-styles]]
## Future possibilities
Work on this feature is evolving and progressing. See the [[inclusion-of-notes]] proposal for the current discussion.
[//begin]: # "Autogenerated link references for markdown compatibility"
[custom-markdown-preview-styles]: custom-markdown-preview-styles.md "Custom Markdown Preview Styles"
[inclusion-of-notes]: ../../dev/proposals/inclusion-of-notes.md "Inclusion of notes Proposal "
[//end]: # "Autogenerated link references"
[//begin]: # 'Autogenerated link references for markdown compatibility'
[built-in-note-embedding-types]: built-in-note-embedding-types.md 'Built-In Note Embedding Types'
[custom-markdown-preview-styles]: custom-markdown-preview-styles.md 'Custom Markdown Preview Styles'
[inclusion-of-notes]: ../../dev/proposals/inclusion-of-notes.md 'Inclusion of notes Proposal '
[//end]: # 'Autogenerated link references'

View File

@@ -20,7 +20,10 @@ Remember, with `CTRL/CMD+click` on a wikilink you can navigate to the note, or c
## Support for sections
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
Foam supports autocompletion, navigation, embedding and diagnostics for note sections. Just use the standard wiki syntax of `[[resource#Section Title]]`.
- If it's an external file, `[your link will need the filename](other-file.md#that-section-I-want-to-link-to)`, but
- if it's an anchor within the same document, `[you just need an octothorpe and the section name](#that-section-above)`.
- Doesn't matter what heading-level the anchor is; whether you're linking to an `H1` like `# MEN WALK ON MOON` or an `H2` like `## Astronauts Land on Plain`, the link syntax uses a single octothorpe: `[Walk!](#men-walk-on-moon)` and `[Land!](#astronauts-land-on-plain-collect-rocks-plant-flag)`. Autocomplete is your friend here.
## Markdown compatibility

View File

@@ -6,11 +6,11 @@ This list is subject to change.
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (alpha)
- [Markdown All In One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)
- [Paste Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## Extensions For Additional Features
These extensions are not (yet?) defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
These extensions are not defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
- [Emojisense](https://marketplace.visualstudio.com/items?itemName=bierner.emojisense)
- [Markdown Emoji](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-emoji) (adds `:smile:` syntax, works with emojisense to provide autocomplete for this syntax)

View File

@@ -2,8 +2,12 @@
This #recipe allows you to paste images on to your notes.
You can directly link and paste images that are copied to the clipboard using either the [Paste
Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
extension, or the [Markdown Image](https://marketplace.visualstudio.com/items?itemName=hancel.markdown-image) extension.
VScode (since
[1.79](https://code.visualstudio.com/updates/v1_79#_copy-external-media-files-into-workspace-on-drop-or-paste-for-markdown))
now has the ability to paste images from the clipboard, or drag-and-drop image
files, directly into markdown documents. The file will be created in the
workspace, and a link generated in Markdown format.
The former does not have MDX support (yet), the latter does.
VSCode settings under `Markdown > Copy Files` and `Markdown > Editor > Drop` can
be used to configure where the files get placed in your workspace, how they're
named, how conflicts with existing files are handled, and more.

View File

@@ -75,7 +75,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
- Publish using community templates
- [[publish-to-netlify-with-eleventy]] by [@juanfrank77](https://github.com/juanfrank77)
- [[generate-gatsby-site]] by [@mathieudutour](https://github.com/mathieudutour) and [@hikerpig](https://github.com/hikerpig)
- [foamy-nextjs](https://github.com/yenly/foamy-nextjs) by [@yenly](https://github.com/yenly)
- Make the site your own by [[publish-to-github]].
- Render math symbols, by either
- adding client-side [[math-support-with-mathjax]] to the default [[publish-to-github-pages]] site

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.25.1"
"version": "0.25.12"
}

View File

@@ -4,6 +4,79 @@ 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.25.12] - 2024-07-13
Fixes and Improvements:
- Improved YAML support (#1367)
- Added convesion of wikilinks to markdown links (#1365 - thanks @hereistheusername)
- Refactored util and settings code
## [0.25.11] - 2024-03-18
Fixes and Improvements:
- Actually fixed bug in graph computation (#1345)
## [0.25.10] - 2024-03-18
Fixes and Improvements:
- Fixed bug in graph computation (#1345)
## [0.25.9] - 2024-03-17
Fixes and Improvements:
- Improved note creation from placeholder (#1344)
## [0.25.8] - 2024-02-21
Fixes and Improvements:
- Upgraded dataformat to improve support for daily note naming (#1326 - thanks @rcyeh)
## [0.25.7] - 2024-01-16
Fixes and Improvements:
- Modifies url encoding to target only the filename and skip spaces (#1322 - thanks @MABruni)
- Minor tweak to quick action menu with suggestions for section name
## [0.25.6] - 2023-12-13
Fixes and Improvements:
- Fixed wikilink definition encoding (#1311 - thanks @MABruni)
## [0.25.5] - 2023-11-30
Fixes and Improvements:
- Using note title in preview (#1309)
## [0.25.4] - 2023-09-19
Fixes and Improvements:
- Added support for linking sections within same document (#1289)
- Fixed note embedding bug (#1286 - thanks @badsketch)
## [0.25.3] - 2023-09-07
Fixes and Improvements:
- Fixed incorrect handling of embedding of non-existing notes (#1283 - thanks @badsketch)
- Introduced Note Embedding Sytanx (#1281 - thanks @badsketch)
- Attachments are not considered when computing orphan notes (#1242)
## [0.25.2] - 2023-09-02
Fixes and Improvements:
- Added content-only embed styles (#1279 - thanks @badsketch)
- Added expand-all button to tree views (#1276)
## [0.25.1] - 2023-08-23
Fixes and Improvements:

View File

@@ -8,7 +8,7 @@
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.25.1",
"version": "0.25.12",
"license": "MIT",
"publisher": "foam",
"engines": {
@@ -152,6 +152,11 @@
"when": "view == foam-vscode.tags-explorer && foam-vscode.views.tags-explorer.group-by == 'folder'",
"group": "navigation"
},
{
"command": "foam-vscode.views.tags-explorer.expand-all",
"when": "view == foam-vscode.tags-explorer",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.show:for-current-file",
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.show == 'all'",
@@ -172,6 +177,11 @@
"when": "view == foam-vscode.placeholders && foam-vscode.views.placeholders.group-by == 'folder'",
"group": "navigation"
},
{
"command": "foam-vscode.views.placeholders.expand-all",
"when": "view == foam-vscode.placeholders",
"group": "navigation"
},
{
"command": "foam-vscode.views.notes-explorer.show:notes",
"when": "view == foam-vscode.notes-explorer && foam-vscode.views.notes-explorer.show == 'all'",
@@ -181,6 +191,11 @@
"command": "foam-vscode.views.notes-explorer.show:all",
"when": "view == foam-vscode.notes-explorer && foam-vscode.views.notes-explorer.show == 'notes-only'",
"group": "navigation"
},
{
"command": "foam-vscode.views.notes-explorer.expand-all",
"when": "view == foam-vscode.notes-explorer",
"group": "navigation"
}
],
"commandPalette": [
@@ -228,6 +243,10 @@
"command": "foam-vscode.views.tags-explorer.group-by:off",
"when": "false"
},
{
"command": "foam-vscode.views.tags-explorer.expand-all",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.show:for-current-file",
"when": "false"
@@ -244,6 +263,10 @@
"command": "foam-vscode.views.placeholders.group-by:off",
"when": "false"
},
{
"command": "foam-vscode.views.placeholders.expand-all",
"when": "false"
},
{
"command": "foam-vscode.views.notes-explorer.show:all",
"when": "false"
@@ -252,6 +275,10 @@
"command": "foam-vscode.views.notes-explorer.show:notes",
"when": "false"
},
{
"command": "foam-vscode.views.notes-explorer.expand-all",
"when": "false"
},
{
"command": "foam-vscode.open-resource",
"when": "false"
@@ -319,6 +346,14 @@
"command": "foam-vscode.open-resource",
"title": "Foam: Open Resource"
},
{
"command": "foam-vscode.convert-link-style-inplace",
"title": "Foam: convert link style in place"
},
{
"command": "foam-vscode.convert-link-style-incopy",
"title": "Foam: convert link format in copy"
},
{
"command": "foam-vscode.views.orphans.group-by:folder",
"title": "Group By Folder",
@@ -364,6 +399,11 @@
"title": "Flat list",
"icon": "$(list-flat)"
},
{
"command": "foam-vscode.views.tags-explorer.expand-all",
"title": "Expand all",
"icon": "$(expand-all)"
},
{
"command": "foam-vscode.views.placeholders.show:for-current-file",
"title": "Show placeholders in current file",
@@ -384,11 +424,21 @@
"title": "Flat list",
"icon": "$(list-flat)"
},
{
"command": "foam-vscode.views.placeholders.expand-all",
"title": "Expand all",
"icon": "$(expand-all)"
},
{
"command": "foam-vscode.views.notes-explorer.show:all",
"title": "Show all resources",
"icon": "$(files)"
},
{
"command": "foam-vscode.views.notes-explorer.expand-all",
"title": "Expand all",
"icon": "$(expand-all)"
},
{
"command": "foam-vscode.views.notes-explorer.show:notes",
"title": "Show only notes",
@@ -567,22 +617,20 @@
],
"description": "Whether or not to navigate to the target daily note when a daily note snippet is selected."
},
"foam.preview.embedNoteInContainer": {
"type": "boolean",
"default": true,
"description": "Wrap embedded notes in a container when displayed in preview panel",
"deprecationMessage": "*DEPRECATED* use foam.preview.embedNoteType instead."
},
"foam.preview.embedNoteType": {
"type": "string",
"default": "full-card",
"enum": [
"full-inline",
"full-card"
"full-card",
"content-inline",
"content-card"
],
"enumDescriptions": [
"Include the section with title and style inline",
"Include the section with title and style it within a container"
"Include the section with title and style it within a container",
"Include the section without title and style inline",
"Include the section without title and style it within a container"
]
},
"foam.graph.titleMaxLength": {
@@ -659,7 +707,7 @@
"wait-for-expect": "^3.0.2"
},
"dependencies": {
"dateformat": "^3.0.3",
"dateformat": "4.5.1",
"detect-newline": "^3.1.0",
"github-slugger": "^1.4.0",
"gray-matter": "^4.0.2",

View File

@@ -0,0 +1,71 @@
import { convertLinkFormat } from '.';
import { TEST_DATA_DIR } from '../../test/test-utils';
import { MarkdownResourceProvider } from '../services/markdown-provider';
import { Resource } from '../model/note';
import { FoamWorkspace } from '../model/workspace';
import { Logger } from '../utils/log';
import fs from 'fs';
import { URI } from '../model/uri';
import { createMarkdownParser } from '../services/markdown-parser';
import { FileDataStore } from '../../test/test-datastore';
Logger.setLevel('error');
describe('generateStdMdLink', () => {
let _workspace: FoamWorkspace;
// TODO slug must be reserved for actual slugs, not file names
const findBySlug = (slug: string): Resource => {
return _workspace
.list()
.find(res => res.uri.getName() === slug) as Resource;
};
beforeAll(async () => {
/** Use fs for reading files in units where vscode.workspace is unavailable */
const readFile = async (uri: URI) =>
(await fs.promises.readFile(uri.toFsPath())).toString();
const dataStore = new FileDataStore(
readFile,
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
);
const parser = createMarkdownParser();
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
});
it('initialised test graph correctly', () => {
expect(_workspace.list().length).toEqual(11);
});
it('can generate markdown links correctly', async () => {
const note = findBySlug('file-with-different-link-formats');
const actual = note.links
.filter(link => link.type === 'wikilink')
.map(link => convertLinkFormat(link, 'link', _workspace, note));
const expected: string[] = [
'[first-document](first-document.md)',
'[second-document](second-document.md)',
'[[non-exist-file]]',
'[#one section](<file-with-different-link-formats.md#one section>)',
'[another name](<file-with-different-link-formats.md#one section>)',
'[an alias](first-document.md)',
'[first-document](first-document.md)',
];
expect(actual.length).toEqual(expected.length);
const _ = actual.map((LinkReplace, index) => {
expect(LinkReplace.newText).toEqual(expected[index]);
});
});
it('can generate wikilinks correctly', async () => {
const note = findBySlug('file-with-different-link-formats');
const actual = note.links
.filter(link => link.type === 'link')
.map(link => convertLinkFormat(link, 'wikilink', _workspace, note));
const expected: string[] = ['[[first-document|file]]'];
expect(actual.length).toEqual(expected.length);
const _ = actual.map((LinkReplace, index) => {
expect(LinkReplace.newText).toEqual(expected[index]);
});
});
});

View File

@@ -0,0 +1,83 @@
import { Resource, ResourceLink } from '../model/note';
import { URI } from '../model/uri';
import { Range } from '../model/range';
import { FoamWorkspace } from '../model/workspace';
import { isNone } from '../utils';
import { MarkdownLink } from '../services/markdown-link';
export interface LinkReplace {
newText: string;
range: Range /* old range */;
}
/**
* convert a link based on its workspace and the note containing it.
* According to targetFormat parameter to decide output format. If link.type === targetFormat, then it simply copy
* the rawText into LinkReplace. Therefore, it's recommended to filter before conversion.
* If targetFormat isn't supported, or the target resource pointed by link cannot be found, the function will throw
* exception.
* @param link
* @param targetFormat 'wikilink' | 'link'
* @param workspace
* @param note
* @returns LinkReplace { newText: string; range: Range; }
*/
export function convertLinkFormat(
link: ResourceLink,
targetFormat: 'wikilink' | 'link',
workspace: FoamWorkspace,
note: Resource | URI
): LinkReplace {
const resource = note instanceof URI ? workspace.find(note) : note;
const targetUri = workspace.resolveLink(resource, link);
/* If it's already the target format or a placeholder, no transformation happens */
if (link.type === targetFormat || targetUri.scheme === 'placeholder') {
return {
newText: link.rawText,
range: link.range,
};
}
let { target, section, alias } = MarkdownLink.analyzeLink(link);
let sectionDivider = section ? '#' : '';
if (isNone(targetUri)) {
throw new Error(
`Unexpected state: link to: "${link.rawText}" is not resolvable`
);
}
const targetRes = workspace.find(targetUri);
let relativeUri = targetRes.uri.relativeTo(resource.uri.getDirectory());
if (targetFormat === 'wikilink') {
return MarkdownLink.createUpdateLinkEdit(link, {
target: workspace.getIdentifier(relativeUri),
type: 'wikilink',
});
}
if (targetFormat === 'link') {
/* if alias is empty, construct one as target#section */
if (alias === '') {
/* in page anchor have no filename */
if (relativeUri.getBasename() === resource.uri.getBasename()) {
target = '';
}
alias = `${target}${sectionDivider}${section}`;
}
/* if it's originally an embedded note, the markdown link shouldn't be embedded */
const isEmbed = targetRes.type === 'image' ? link.isEmbed : false;
return MarkdownLink.createUpdateLinkEdit(link, {
alias: alias,
target: relativeUri.path,
isEmbed: isEmbed,
type: 'link',
});
}
throw new Error(
`Unexpected state: targetFormat: ${targetFormat} is not supported`
);
}

View File

@@ -36,7 +36,7 @@ describe('generateLinkReferences', () => {
});
it('initialised test graph correctly', () => {
expect(_workspace.list().length).toEqual(10);
expect(_workspace.list().length).toEqual(11);
});
it('should add link references to a file that does not have them', async () => {

View File

@@ -1,2 +1,3 @@
export { generateLinkReferences } from './generate-link-references';
export { generateHeading } from './generate-headings';
export { convertLinkFormat } from './convert-links-format';

View File

@@ -5,7 +5,7 @@ import { FoamGraph } from './graph';
import { ResourceParser } from './note';
import { ResourceProvider } from './provider';
import { FoamTags } from './tags';
import { Logger } from '../utils/log';
import { Logger, withTiming, withTimingAsync } from '../utils/log';
export interface Services {
dataStore: IDataStore;
@@ -28,24 +28,25 @@ export const bootstrap = async (
initialProviders: ResourceProvider[],
defaultExtension: string = '.md'
) => {
const tsStart = Date.now();
const workspace = await FoamWorkspace.fromProviders(
initialProviders,
dataStore,
defaultExtension
const workspace = await withTimingAsync(
() =>
FoamWorkspace.fromProviders(
initialProviders,
dataStore,
defaultExtension
),
ms => Logger.info(`Workspace loaded in ${ms}ms`)
);
const tsWsDone = Date.now();
Logger.info(`Workspace loaded in ${tsWsDone - tsStart}ms`);
const graph = withTiming(
() => FoamGraph.fromWorkspace(workspace, true),
ms => Logger.info(`Graph loaded in ${ms}ms`)
);
const graph = FoamGraph.fromWorkspace(workspace, true);
const tsGraphDone = Date.now();
Logger.info(`Graph loaded in ${tsGraphDone - tsWsDone}ms`);
const tags = FoamTags.fromWorkspace(workspace, true);
const tsTagsEnd = Date.now();
Logger.info(`Tags loaded in ${tsTagsEnd - tsGraphDone}ms`);
const tags = withTiming(
() => FoamTags.fromWorkspace(workspace, true),
ms => Logger.info(`Tags loaded in ${ms}ms`)
);
watcher?.onDidChange(async uri => {
if (matcher.isMatch(uri)) {

View File

@@ -139,6 +139,21 @@ describe('Graph', () => {
).toEqual(['/path/another/page-c.md', '/somewhere/page-b.md']);
});
it('should create inbound connections when targeting a section', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b#section 2' }],
});
const noteB = createTestNote({
uri: '/somewhere/page-b.md',
text: '## Section 1\n\n## Section 2',
});
const ws = createTestWorkspace().set(noteA).set(noteB);
const graph = FoamGraph.fromWorkspace(ws);
expect(graph.getBacklinks(noteB.uri).length).toEqual(1);
});
it('should support attachments', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',

View File

@@ -3,7 +3,7 @@ import { ResourceLink } from './note';
import { URI } from './uri';
import { FoamWorkspace } from './workspace';
import { IDisposable } from '../common/lifecycle';
import { Logger } from '../utils/log';
import { Logger, withTiming } from '../utils/log';
import { Emitter } from '../common/event';
export type Connection = {

View File

@@ -0,0 +1,35 @@
import { Range } from './range';
import { URI } from './uri';
import { ResourceLink } from './note';
/**
* Represents a location inside a resource, such as a line
* inside a text file.
*/
export interface Location<T> {
/**
* The resource identifier of this location.
*/
uri: URI;
/**
* The document range of this locations.
*/
range: Range;
/**
* The data associated to this location.
*/
data: T;
}
export abstract class Location<T> {
static create<T>(uri: URI, range: Range, data: T): Location<T> {
return { uri, range, data };
}
static forObjectWithRange<T extends { range: Range }>(
uri: URI,
obj: T
): Location<T> {
return Location.create(uri, obj.range, obj);
}
}

View File

@@ -4,7 +4,14 @@ import { FoamWorkspace } from '../model/workspace';
import { IDisposable } from '../common/lifecycle';
import { ResourceProvider } from '../model/provider';
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'];
export const imageExtensions = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.svg',
'.webp',
];
const asResource = (uri: URI): Resource => {
const type = imageExtensions.includes(uri.getExtension())

View File

@@ -254,4 +254,185 @@ describe('MarkdownLink', () => {
expect(edit.range).toEqual(link.range);
});
});
describe('convert wikilink to link', () => {
it('should generate default alias if no one', () => {
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'link',
});
expect(wikilinkEdit.newText).toEqual(`[wikilink](wikilink)`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
const wikilinkWithSection = parser.parse(
getRandomURI(),
`[[wikilink#section]]`
).links[0];
const wikilinkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
wikilinkWithSection,
{
type: 'link',
}
);
expect(wikilinkWithSectionEdit.newText).toEqual(
`[wikilink#section](wikilink#section)`
);
expect(wikilinkWithSectionEdit.range).toEqual(wikilinkWithSection.range);
});
it('should use alias in the wikilik the if there has one', () => {
const wikilink = parser.parse(
getRandomURI(),
`[[wikilink#section|alias]]`
).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'link',
});
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
});
describe('convert link to wikilink', () => {
it('should reorganize target, section, and alias in wikilink manner', () => {
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
type: 'wikilink',
});
expect(linkEdit.newText).toEqual(`[[to/path.md|link]]`);
expect(linkEdit.range).toEqual(link.range);
const linkWithSection = parser.parse(
getRandomURI(),
`[link](to/path.md#section)`
).links[0];
const linkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
linkWithSection,
{
type: 'wikilink',
}
);
expect(linkWithSectionEdit.newText).toEqual(
`[[to/path.md#section|link]]`
);
expect(linkWithSectionEdit.range).toEqual(linkWithSection.range);
});
it('should use alias in the wikilik the if there has one', () => {
const wikilink = parser.parse(
getRandomURI(),
`[[wikilink#section|alias]]`
).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'link',
});
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
});
describe('convert to its original type', () => {
it('should remain unchanged', () => {
const link = parser.parse(getRandomURI(), `[link](to/path.md#section)`)
.links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
type: 'link',
});
expect(linkEdit.newText).toEqual(`[link](to/path.md#section)`);
expect(linkEdit.range).toEqual(link.range);
const wikilink = parser.parse(
getRandomURI(),
`[[wikilink#section|alias]]`
).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'wikilink',
});
expect(wikilinkEdit.newText).toEqual(`[[wikilink#section|alias]]`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
});
describe('change isEmbed property', () => {
it('should change isEmbed only', () => {
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
isEmbed: true,
});
expect(wikilinkEdit.newText).toEqual(`![[wikilink]]`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
const link = parser.parse(getRandomURI(), `![link](to/path.md)`).links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
isEmbed: false,
});
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
expect(linkEdit.range).toEqual(link.range);
});
it('should be unchanged if the update value is the same as the original one', () => {
const embeddedWikilink = parser.parse(getRandomURI(), `![[wikilink]]`)
.links[0];
const embeddedWikilinkEdit = MarkdownLink.createUpdateLinkEdit(
embeddedWikilink,
{
isEmbed: true,
}
);
expect(embeddedWikilinkEdit.newText).toEqual(`![[wikilink]]`);
expect(embeddedWikilinkEdit.range).toEqual(embeddedWikilink.range);
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
isEmbed: false,
});
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
expect(linkEdit.range).toEqual(link.range);
});
});
describe('insert angles', () => {
it('should insert angles when meeting space in links', () => {
const link = parser.parse(getRandomURI(), `![link](to/path.md)`).links[0];
const linkAddSection = MarkdownLink.createUpdateLinkEdit(link, {
section: 'one section',
});
expect(linkAddSection.newText).toEqual(
`![link](<to/path.md#one section>)`
);
expect(linkAddSection.range).toEqual(link.range);
const linkChangingTarget = parser.parse(
getRandomURI(),
`[link](to/path.md#one-section)`
).links[0];
const linkEdit = MarkdownLink.createUpdateLinkEdit(linkChangingTarget, {
target: 'to/another path.md',
});
expect(linkEdit.newText).toEqual(
`[link](<to/another path.md#one-section>)`
);
expect(linkEdit.range).toEqual(linkChangingTarget.range);
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
.links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
type: 'link',
});
expect(wikilinkEdit.newText).toEqual(
`[wikilink#one section](<wikilink#one section>)`
);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
it('should not insert angles in wikilink', () => {
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
.links[0];
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
target: 'another wikilink',
});
expect(wikilinkEdit.newText).toEqual(`[[another wikilink#one section]]`);
expect(wikilinkEdit.range).toEqual(wikilink.range);
});
});
});

View File

@@ -38,7 +38,13 @@ export abstract class MarkdownLink {
public static createUpdateLinkEdit(
link: ResourceLink,
delta: { target?: string; section?: string; alias?: string }
delta: {
target?: string;
section?: string;
alias?: string;
type?: 'wikilink' | 'link';
isEmbed?: boolean;
}
) {
const { target, section, alias } = MarkdownLink.analyzeLink(link);
const newTarget = delta.target ?? target;
@@ -46,21 +52,27 @@ export abstract class MarkdownLink {
const newAlias = delta.alias ?? alias ?? '';
const sectionDivider = newSection ? '#' : '';
const aliasDivider = newAlias ? '|' : '';
const embed = link.isEmbed ? '!' : '';
if (link.type === 'wikilink') {
const embed = delta.isEmbed ?? link.isEmbed ? '!' : '';
const type = delta.type ?? link.type;
if (type === 'wikilink') {
return {
newText: `${embed}[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
range: link.range,
};
}
if (link.type === 'link') {
if (type === 'link') {
const defaultAlias = () => {
return `${newTarget}${sectionDivider}${newSection}`;
};
const useAngles =
newTarget.indexOf(' ') > 0 || newSection.indexOf(' ') > 0;
return {
newText: `${embed}[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
newText: `${embed}[${newAlias ? newAlias : defaultAlias()}](${
useAngles ? '<' : ''
}${newTarget}${sectionDivider}${newSection}${useAngles ? '>' : ''})`,
range: link.range,
};
}
throw new Error(
`Unexpected state: link of type ${link.type} is not supported`
);
throw new Error(`Unexpected state: link of type ${type} is not supported`);
}
}

View File

@@ -166,6 +166,65 @@ describe('Link resolution', () => {
noteA.uri.withFragment('section')
);
});
it('should resolve wikilinks with special characters', () => {
const ws = createTestWorkspace();
const noteA = createNoteFromMarkdown(
`Link to [[page: a]] and [[page %b%]] and [[page? c]] and [[[page] d]] and
[[page ^e^]] and [[page \`f\`]] and [[page {g}]] and [[page ~i]] and
[[page /j]]`
);
const noteB = createNoteFromMarkdown(
'Note containing :',
'/dir1/page: a.md'
);
const noteC = createNoteFromMarkdown(
'Note containing %',
'/dir1/page %b%.md'
);
const noteD = createNoteFromMarkdown(
'Note containing ?',
'/dir1/page? c.md'
);
const noteE = createNoteFromMarkdown(
'Note containing ]',
'/dir1/[page] d.md'
);
const noteF = createNoteFromMarkdown(
'Note containing ^',
'/dir1/page ^e^.md'
);
const noteG = createNoteFromMarkdown(
'Note containing `',
'/dir1/page `f`.md'
);
const noteH = createNoteFromMarkdown(
'Note containing { and }',
'/dir1/page {g}.md'
);
const noteI = createNoteFromMarkdown(
'Note containing ~',
'/dir1/page ~i.md'
);
ws.set(noteA)
.set(noteB)
.set(noteC)
.set(noteD)
.set(noteE)
.set(noteF)
.set(noteG)
.set(noteH)
.set(noteI);
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);
expect(ws.resolveLink(noteA, noteA.links[3])).toEqual(noteE.uri);
expect(ws.resolveLink(noteA, noteA.links[4])).toEqual(noteF.uri);
expect(ws.resolveLink(noteA, noteA.links[5])).toEqual(noteG.uri);
expect(ws.resolveLink(noteA, noteA.links[6])).toEqual(noteH.uri);
expect(ws.resolveLink(noteA, noteA.links[7])).toEqual(noteI.uri);
});
});
describe('Markdown direct links', () => {
@@ -311,9 +370,78 @@ describe('Generation of markdown references', () => {
.set(createNoteFromMarkdown('Content of note C', '/dir3/page-c.md'));
const references = createMarkdownReferences(workspace, noteA.uri, true);
expect(references.map(r => r.url)).toEqual([
expect(references.map(r => decodeURIComponent(r.url))).toEqual([
'../dir2/page-b.md',
'../dir3/page-c.md',
]);
});
it('should generate links for embedded notes that are formatted properly', () => {
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 => [decodeURIComponent(r.url), r.label])).toEqual([
['../dir2/page-b.md', 'page-b'],
['../dir3/page-c.md', 'page-c'],
]);
});
it('should not generate links for placeholders', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
'Link to ![[page-b]] and [[page-c]] and [[does-not-exist]] and ![[does-not-exist-either]]',
'/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 => decodeURIComponent(r.url))).toEqual([
'../dir2/page-b.md',
'../dir3/page-c.md',
]);
});
it('should encode special characters in links', () => {
const workspace = createTestWorkspace();
const noteA = createNoteFromMarkdown(
`Link to [[page: a]] and [[page %b%]] and [[page? c]] and [[[page] d]] and
[[page ^e^]] and [[page \`f\`]] and [[page {g}]] and [[page ~i]] and
[[page /j]]`
);
workspace
.set(noteA)
.set(createNoteFromMarkdown('Note containing :', '/dir1/page: a.md'))
.set(createNoteFromMarkdown('Note containing %', '/dir1/page %b%.md'))
.set(createNoteFromMarkdown('Note containing ?', '/dir1/page? c.md'))
.set(createNoteFromMarkdown('Note containing ]', '/dir1/[page] d.md'))
.set(createNoteFromMarkdown('Note containing ^', '/dir1/page ^e^.md'))
.set(createNoteFromMarkdown('Note containing `', '/dir1/page `f`.md'))
.set(
createNoteFromMarkdown('Note containing { and }', '/dir1/page {g}.md')
)
.set(createNoteFromMarkdown('Note containing ~', '/dir1/page ~i.md'));
const references = createMarkdownReferences(workspace, noteA.uri, true);
expect(references.map(r => decodeURIComponent(r.url))).toEqual([
'../dir1/page: a.md',
'../dir1/page %b%.md',
'../dir1/page? c.md',
'../dir1/[page] d.md',
'../dir1/page ^e^.md',
'../dir1/page `f`.md',
'../dir1/page {g}.md',
'../dir1/page ~i.md',
]);
});
});

View File

@@ -137,13 +137,21 @@ export function createMarkdownReferences(
relativeUri = relativeUri.changeExtension('*', '');
}
// Extract base path and link name separately.
const basePath = relativeUri.path.split('/').slice(0, -1).join('/');
const linkName = relativeUri.path.split('/').pop();
const encodedURL = encodeURIComponent(linkName).replace(/%20/g, ' ');
// [wikilink-text]: path/to/file.md "Page title"
return {
label:
link.rawText.indexOf('[[') > -1
? link.rawText.substring(2, link.rawText.length - 2)
: link.rawText,
url: relativeUri.path,
// embedded looks like ![[note-a]]
// regular note looks like [[note-a]]
label: link.rawText.substring(
link.isEmbed ? 3 : 2,
link.rawText.length - 2
),
url: `${basePath ? basePath + '/' : ''}${encodedURL}`,
title: target.title,
};
})

View File

@@ -89,3 +89,25 @@ export class Logger {
Logger.defaultLogger = logger;
}
}
export const withTiming = <T>(
fn: () => T,
onDidComplete: (elapsed: number) => void
): T => {
const tsStart = Date.now();
const res = fn();
const tsEnd = Date.now();
onDidComplete(tsEnd - tsStart);
return res;
};
export const withTimingAsync = async <T>(
fn: () => Promise<T>,
onDidComplete: (elapsed: number) => void
): Promise<T> => {
const tsStart = Date.now();
const res = await fn();
const tsEnd = Date.now();
onDidComplete(tsEnd - tsStart);
return res;
};

View File

@@ -1,67 +1,4 @@
import {
isInFrontMatter,
isOnYAMLKeywordLine,
removeBrackets,
toTitleCase,
} from './utils';
describe('removeBrackets', () => {
it('removes the brackets', () => {
const input = 'hello world [[this-is-it]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the md file extension', () => {
const input = 'hello world [[this-is-it.md]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the mdx file extension', () => {
const input = 'hello world [[this-is-it.mdx]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the markdown file extension', () => {
const input = 'hello world [[this-is-it.markdown]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets even with numbers', () => {
const input = 'hello world [[2020-07-21.markdown]]';
const actual = removeBrackets(input);
const expected = 'hello world 2020 07 21';
expect(actual).toEqual(expected);
});
it('removes brackets for more than one word', () => {
const input =
'I am reading this as part of the [[book-club]] put on by [[egghead]] folks (Lauro).';
const actual = removeBrackets(input);
const expected =
'I am reading this as part of the Book Club put on by Egghead folks (Lauro).';
expect(actual).toEqual(expected);
});
});
describe('toTitleCase', () => {
it('title cases a word', () => {
const input =
'look at this really long sentence but I am calling it a word';
const actual = toTitleCase(input);
const expected =
'Look At This Really Long Sentence But I Am Calling It A Word';
expect(actual).toEqual(expected);
});
it('works on one word', () => {
const input = 'word';
const actual = toTitleCase(input);
const expected = 'Word';
expect(actual).toEqual(expected);
});
});
import { isInFrontMatter, isOnYAMLKeywordLine } from './md';
describe('isInFrontMatter', () => {
it('is true for started front matter', () => {
@@ -81,6 +18,12 @@ describe('isInFrontMatter', () => {
const actual = isInFrontMatter(content, 1);
expect(actual).toBeTruthy();
});
it('is false for non valid front matter delimiter #1347', () => {
const content = '---\ntitle: A title\n-..\n\n\n---\ntest\n';
expect(isInFrontMatter(content, 1)).toBeTruthy();
expect(isInFrontMatter(content, 4)).toBeTruthy();
expect(isInFrontMatter(content, 6)).toBeFalsy();
});
it('is false for outside completed front matter', () => {
const content = '---\ntitle: A title\n---\ncontent\nmore content\n';
const actual = isInFrontMatter(content, 3);

View File

@@ -0,0 +1,70 @@
import matter from 'gray-matter';
export function getExcerpt(
markdown: string,
maxLines: number
): { excerpt: string; lines: number } {
const OFFSET_LINES_LIMIT = 5;
const paragraphs = markdown.replace(/\r\n/g, '\n').split('\n\n');
const excerpt: string[] = [];
let lines = 0;
for (const paragraph of paragraphs) {
const n = paragraph.split('\n').length;
if (lines > maxLines || lines + n - maxLines > OFFSET_LINES_LIMIT) {
break;
}
excerpt.push(paragraph);
lines = lines + n + 1;
}
return { excerpt: excerpt.join('\n\n'), lines };
}
export function stripFrontMatter(markdown: string): string {
return matter(markdown).content.trim();
}
export function stripImages(markdown: string): string {
return markdown.replace(
/!\[(.*)\]\([-/\\.A-Za-z]*\)/gi,
'$1'.length ? '[Image: $1]' : ''
);
}
/**
* Returns if the given line is inside a front matter block
* @param content the string to check
* @param lineNumber the line number within the string, 0-based
* @returns true if the line is inside a frontmatter block in content
*/
export function isInFrontMatter(content: string, lineNumber: number): boolean {
const FIRST_DELIMITER_MATCH = /^---\s*?$/m;
const LAST_DELIMITER_MATCH = /^(-{3}|\.{3})/;
// if we're on the first line, we're not _yet_ in the front matter
if (lineNumber === 0) {
return false;
}
// look for --- at start, and a second --- or ... to end
if (content.match(FIRST_DELIMITER_MATCH) === null) {
return false;
}
const lines = content.split('\n');
lines.shift();
const endLineNumber = lines.findIndex(l => l.match(LAST_DELIMITER_MATCH));
return endLineNumber === -1 || endLineNumber >= lineNumber;
}
export function isOnYAMLKeywordLine(content: string, keyword: string): boolean {
const keywordMatch = /^\s*(\w+):/gm;
if (content.match(keywordMatch) === null) {
return false;
}
const matches = Array.from(content.matchAll(keywordMatch));
const lastMatch = matches[matches.length - 1];
return lastMatch[1] === keyword;
}

View File

@@ -1,9 +1,8 @@
import dateFormat from 'dateformat';
import { focusNote } from './utils';
import { URI } from './core/model/uri';
import { NoteFactory } from './services/templates';
import { getFoamVsCodeConfig } from './services/config';
import { asAbsoluteWorkspaceUri } from './services/editor';
import { asAbsoluteWorkspaceUri, focusNote } from './services/editor';
/**
* Open the daily note file.

View File

@@ -73,7 +73,9 @@ export async function activate(context: ExtensionContext) {
);
// Load the features
const resPromises = features.map(feature => feature(context, foamPromise));
const featuresPromises = features.map(feature =>
feature(context, foamPromise)
);
const foam = await foamPromise;
Logger.info(`Loaded ${foam.workspace.list().length} resources`);
@@ -102,14 +104,15 @@ export async function activate(context: ExtensionContext) {
})
);
const res = (await Promise.all(resPromises)).filter(r => r != null);
const feats = (await Promise.all(featuresPromises)).filter(r => r != null);
return {
extendMarkdownIt: (md: markdownit) => {
return res.reduce((acc: markdownit, r: any) => {
return feats.reduce((acc: markdownit, r: any) => {
return r.extendMarkdownIt ? r.extendMarkdownIt(acc) : acc;
}, md);
},
foam,
};
} catch (e) {
Logger.error('An error occurred while bootstrapping Foam', e);

View File

@@ -0,0 +1,188 @@
import { commands, ExtensionContext, window, workspace, Uri } from 'vscode';
import { Foam } from '../../core/model/foam';
import { FoamWorkspace } from '../../core/model/workspace';
import { fromVsCodeUri, toVsCodeRange } from '../../utils/vsc-utils';
import { ResourceParser } from '../../core/model/note';
import { IMatcher } from '../../core/services/datastore';
import { convertLinkFormat } from '../../core/janitor';
import { isMdEditor } from '../../services/editor';
type LinkFormat = 'wikilink' | 'link';
enum ConvertOption {
Wikilink2MDlink,
MDlink2Wikilink,
}
interface IConfig {
from: string;
to: string;
}
const Config: { [key in ConvertOption]: IConfig } = {
[ConvertOption.Wikilink2MDlink]: {
from: 'wikilink',
to: 'link',
},
[ConvertOption.MDlink2Wikilink]: {
from: 'link',
to: 'wikilink',
},
};
export default async function activate(
context: ExtensionContext,
foamPromise: Promise<Foam>
) {
const foam = await foamPromise;
/*
commands:
foam-vscode.convert-link-style-inplace
foam-vscode.convert-link-style-incopy
*/
context.subscriptions.push(
commands.registerCommand('foam-vscode.convert-link-style-inplace', () => {
return convertLinkAdapter(
foam.workspace,
foam.services.parser,
foam.services.matcher,
true
);
}),
commands.registerCommand('foam-vscode.convert-link-style-incopy', () => {
return convertLinkAdapter(
foam.workspace,
foam.services.parser,
foam.services.matcher,
false
);
})
);
}
async function convertLinkAdapter(
fWorkspace: FoamWorkspace,
fParser: ResourceParser,
fMatcher: IMatcher,
isInPlace: boolean
) {
const convertOption = await pickConvertStrategy();
if (!convertOption) {
window.showInformationMessage('Convert canceled');
return;
}
if (isInPlace) {
await convertLinkInPlace(fWorkspace, fParser, fMatcher, convertOption);
} else {
await convertLinkInCopy(fWorkspace, fParser, fMatcher, convertOption);
}
}
async function pickConvertStrategy(): Promise<IConfig | undefined> {
const options = {
'to wikilink': ConvertOption.MDlink2Wikilink,
'to markdown link': ConvertOption.Wikilink2MDlink,
};
return window.showQuickPick(Object.keys(options)).then(name => {
if (name) {
return Config[options[name]];
} else {
return undefined;
}
});
}
/**
* convert links based on its workspace and the note containing it.
* Changes happen in-place
* @param fWorkspace
* @param fParser
* @param fMatcher
* @param convertOption
* @returns void
*/
async function convertLinkInPlace(
fWorkspace: FoamWorkspace,
fParser: ResourceParser,
fMatcher: IMatcher,
convertOption: IConfig
) {
const editor = window.activeTextEditor;
const doc = editor.document;
if (!isMdEditor(editor) || !fMatcher.isMatch(fromVsCodeUri(doc.uri))) {
return;
}
// const eol = getEditorEOL();
let text = doc.getText();
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
const textReplaceArr = resource.links
.filter(link => link.type === convertOption.from)
.map(link =>
convertLinkFormat(
link,
convertOption.to as LinkFormat,
fWorkspace,
resource
)
)
/* transform .range property into vscode range */
.map(linkReplace => ({
...linkReplace,
range: toVsCodeRange(linkReplace.range),
}));
/* reorder the array such that the later range comes first */
textReplaceArr.sort((a, b) => b.range.start.compareTo(a.range.start));
await editor.edit(editorBuilder => {
textReplaceArr.forEach(edit => {
editorBuilder.replace(edit.range, edit.newText);
});
});
}
/**
* convert links based on its workspace and the note containing it.
* Changes happen in a copy
* 1. prepare a copy file, and makt it the activeTextEditor
* 2. call to convertLinkInPlace
* @param fWorkspace
* @param fParser
* @param fMatcher
* @param convertOption
* @returns void
*/
async function convertLinkInCopy(
fWorkspace: FoamWorkspace,
fParser: ResourceParser,
fMatcher: IMatcher,
convertOption: IConfig
) {
const editor = window.activeTextEditor;
const doc = editor.document;
if (!isMdEditor(editor) || !fMatcher.isMatch(fromVsCodeUri(doc.uri))) {
return;
}
// const eol = getEditorEOL();
let text = doc.getText();
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
const basePath = doc.uri.path.split('/').slice(0, -1).join('/');
const fileUri = Uri.file(
`${
basePath ? basePath + '/' : ''
}${resource.uri.getName()}.copy${resource.uri.getExtension()}`
);
const encoder = new TextEncoder();
await workspace.fs.writeFile(fileUri, encoder.encode(text));
await window.showTextDocument(fileUri);
await convertLinkInPlace(fWorkspace, fParser, fMatcher, convertOption);
}

View File

@@ -1,5 +1,6 @@
import { env, Position, Selection, commands } from 'vscode';
import { createFile, showInEditor } from '../../test/test-utils-vscode';
import { removeBrackets, toTitleCase } from './copy-without-brackets';
describe('copy-without-brackets command', () => {
it('should get the input from the active editor selection', async () => {
@@ -11,3 +12,61 @@ describe('copy-without-brackets command', () => {
expect(value).toEqual('This is my Test Content.');
});
});
describe('removeBrackets', () => {
it('removes the brackets', () => {
const input = 'hello world [[this-is-it]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the md file extension', () => {
const input = 'hello world [[this-is-it.md]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the mdx file extension', () => {
const input = 'hello world [[this-is-it.mdx]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the markdown file extension', () => {
const input = 'hello world [[this-is-it.markdown]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets even with numbers', () => {
const input = 'hello world [[2020-07-21.markdown]]';
const actual = removeBrackets(input);
const expected = 'hello world 2020 07 21';
expect(actual).toEqual(expected);
});
it('removes brackets for more than one word', () => {
const input =
'I am reading this as part of the [[book-club]] put on by [[egghead]] folks (Lauro).';
const actual = removeBrackets(input);
const expected =
'I am reading this as part of the Book Club put on by Egghead folks (Lauro).';
expect(actual).toEqual(expected);
});
});
describe('toTitleCase', () => {
it('title cases a word', () => {
const input =
'look at this really long sentence but I am calling it a word';
const actual = toTitleCase(input);
const expected =
'Look At This Really Long Sentence But I Am Calling It A Word';
expect(actual).toEqual(expected);
});
it('works on one word', () => {
const input = 'word';
const actual = toTitleCase(input);
const expected = 'Word';
expect(actual).toEqual(expected);
});
});

View File

@@ -1,5 +1,4 @@
import { window, env, ExtensionContext, commands } from 'vscode';
import { removeBrackets } from '../../utils';
export default async function activate(context: ExtensionContext) {
context.subscriptions.push(
@@ -31,3 +30,46 @@ async function copyWithoutBrackets() {
window.showInformationMessage('Successfully copied to clipboard!');
}
}
/**
* Used for the "Copy to Clipboard Without Brackets" command
*
*/
export function removeBrackets(s: string): string {
// take in the string, split on space
const stringSplitBySpace = s.split(' ');
// loop through words
const modifiedWords = stringSplitBySpace.map(currentWord => {
if (currentWord.includes('[[')) {
// all of these transformations will turn this "[[you-are-awesome]]"
// to this "you are awesome"
let word = currentWord.replace(/(\[\[)/g, '');
word = word.replace(/(\]\])/g, '');
word = word.replace(/(.mdx|.md|.markdown)/g, '');
word = word.replace(/[-]/g, ' ');
// then we titlecase the word so "you are awesome"
// becomes "You Are Awesome"
const titleCasedWord = toTitleCase(word);
return titleCasedWord;
}
return currentWord;
});
return modifiedWords.join(' ');
}
/**
* Takes in a string and returns it titlecased
*
* @example toTitleCase("hello world") -> "Hello World"
*/
export function toTitleCase(word: string): string {
return word
.split(' ')
.map(word => word[0].toUpperCase() + word.substring(1))
.join(' ');
}

View File

@@ -10,7 +10,12 @@ import {
showInEditor,
} from '../../test/test-utils-vscode';
import { fromVsCodeUri } from '../../utils/vsc-utils';
import { CREATE_NOTE_COMMAND } from './create-note';
import { CREATE_NOTE_COMMAND, createNote } from './create-note';
import { Location } from '../../core/model/location';
import { Range } from '../../core/model/range';
import { ResourceLink } from '../../core/model/note';
import { MarkdownResourceProvider } from '../../core/services/markdown-provider';
import { createMarkdownParser } from '../../core/services/markdown-parser';
describe('create-note command', () => {
afterEach(() => {
@@ -194,8 +199,14 @@ describe('factories', () => {
describe('forPlaceholder', () => {
it('adds the .md extension to notes created for placeholders', async () => {
await closeEditors();
const link: ResourceLink = {
type: 'wikilink',
rawText: '[[my-placeholder]]',
range: Range.create(0, 0, 0, 0),
isEmbed: false,
};
const command = CREATE_NOTE_COMMAND.forPlaceholder(
'my-placeholder',
Location.forObjectWithRange(URI.file(''), link),
'.md'
);
await commands.executeCommand(command.name, command.params);
@@ -204,5 +215,41 @@ describe('factories', () => {
expect(doc.uri.path).toMatch(/my-placeholder.md$/);
expect(doc.getText()).toMatch(/^# my-placeholder/);
});
it('replaces the original placeholder based on the new note identifier (#1327)', async () => {
await closeEditors();
const templateA = await createFile(
`---
foam_template:
name: 'Example Template'
description: 'An example for reproducing a bug'
filepath: '$FOAM_SLUG-world.md'
---`,
['.foam', 'templates', 'template-a.md']
);
const noteA = await createFile(`this is my [[hello]]`);
const parser = createMarkdownParser();
const res = parser.parse(noteA.uri, noteA.content);
const command = CREATE_NOTE_COMMAND.forPlaceholder(
Location.forObjectWithRange(noteA.uri, res.links[0]),
'.md',
{
templatePath: templateA.uri.path,
}
);
const results: Awaited<ReturnType<typeof createNote>> =
await commands.executeCommand(command.name, command.params);
expect(results.didCreateFile).toBeTruthy();
expect(results.uri.path.endsWith('hello-world.md')).toBeTruthy();
const newNoteDoc = window.activeTextEditor.document;
expect(newNoteDoc.uri.path).toMatch(/hello-world.md$/);
const { doc } = await showInEditor(noteA.uri);
expect(doc.getText()).toEqual(`this is my [[hello-world]]`);
});
});
});

View File

@@ -10,10 +10,21 @@ import { Resolver } from '../../services/variable-resolver';
import { asAbsoluteWorkspaceUri, fileExists } from '../../services/editor';
import { isSome } from '../../core/utils';
import { CommandDescriptor } from '../../utils/commands';
import { Foam } from '../../core/model/foam';
import { Location } from '../../core/model/location';
import { MarkdownLink } from '../../core/services/markdown-link';
import { ResourceLink } from '../../core/model/note';
import { toVsCodeRange, toVsCodeUri } from '../../utils/vsc-utils';
export default async function activate(context: vscode.ExtensionContext) {
export default async function activate(
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) {
const foam = await foamPromise;
context.subscriptions.push(
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, createNote)
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, args =>
createNote(args, foam)
)
);
}
@@ -48,6 +59,11 @@ interface CreateNoteArgs {
* The title of the note (translates into the FOAM_TITLE variable)
*/
title?: string;
/**
* The source link that triggered the creation of the note.
* It will be updated with the appropriate identifier to the note, if necessary.
*/
sourceLink?: Location<ResourceLink>;
/**
* What to do in case the target file already exists
*/
@@ -66,7 +82,7 @@ const DEFAULT_NEW_NOTE_TEXT = `# \${FOAM_TITLE}
\${FOAM_SELECTED_TEXT}`;
async function createNote(args: CreateNoteArgs) {
export async function createNote(args: CreateNoteArgs, foam: Foam) {
args = args ?? {};
const date = isSome(args.date) ? new Date(Date.parse(args.date)) : new Date();
const resolver = new Resolver(
@@ -92,23 +108,39 @@ async function createNote(args: CreateNoteArgs) {
: getDefaultTemplateUri();
}
if (await fileExists(templateUri)) {
return NoteFactory.createFromTemplate(
templateUri,
resolver,
noteUri,
text,
args.onFileExists
);
} else {
return NoteFactory.createNote(
noteUri ?? (await getPathFromTitle(resolver)),
text,
resolver,
args.onFileExists,
args.onRelativeNotePath
);
const createdNote = (await fileExists(templateUri))
? await NoteFactory.createFromTemplate(
templateUri,
resolver,
noteUri,
text,
args.onFileExists
)
: await NoteFactory.createNote(
noteUri ?? (await getPathFromTitle(resolver)),
text,
resolver,
args.onFileExists,
args.onRelativeNotePath
);
if (args.sourceLink) {
const identifier = foam.workspace.getIdentifier(createdNote.uri);
const edit = MarkdownLink.createUpdateLinkEdit(args.sourceLink.data, {
target: identifier,
});
if (edit.newText != args.sourceLink.data.rawText) {
const updateLink = new vscode.WorkspaceEdit();
const uri = toVsCodeUri(args.sourceLink.uri);
updateLink.replace(
uri,
toVsCodeRange(args.sourceLink.range),
edit.newText
);
await vscode.workspace.applyEdit(updateLink);
}
}
return createdNote;
}
export const CREATE_NOTE_COMMAND = {
@@ -123,12 +155,12 @@ export const CREATE_NOTE_COMMAND = {
* @returns the command descriptor
*/
forPlaceholder: (
placeholder: string,
sourceLink: Location<ResourceLink>,
defaultExtension: string,
extra: Partial<CreateNoteArgs> = {}
): CommandDescriptor<CreateNoteArgs> => {
const endsWithDefaultExtension = new RegExp(defaultExtension + '$');
const { target: placeholder } = MarkdownLink.analyzeLink(sourceLink.data);
const title = placeholder.endsWith(defaultExtension)
? placeholder.replace(endsWithDefaultExtension, '')
: placeholder;
@@ -140,6 +172,7 @@ export const CREATE_NOTE_COMMAND = {
params: {
title,
notePath,
sourceLink,
...extra,
},
};

View File

@@ -10,3 +10,4 @@ export { default as openResource } from './open-resource';
export { default as updateGraphCommand } from './update-graph';
export { default as updateWikilinksCommand } from './update-wikilinks';
export { default as createNote } from './create-note';
export { default as generateStandaloneNote } from './convert-links-format-in-note';

View File

@@ -5,18 +5,18 @@ import {
commands,
ProgressLocation,
} from 'vscode';
import { getWikilinkDefinitionSetting } from '../../settings';
import detectNewline from 'detect-newline';
import { Foam } from '../../core/model/foam';
import { Resource } from '../../core/model/note';
import { generateHeading, generateLinkReferences } from '../../core/janitor';
import { Range } from '../../core/model/range';
import { TextEdit } from '../../core/services/text-edit';
import {
toVsCodePosition,
toVsCodeRange,
toVsCodeUri,
} from '../../utils/vsc-utils';
import { Foam } from '../../core/model/foam';
import { Resource } from '../../core/model/note';
import { generateHeading, generateLinkReferences } from '../../core/janitor';
import { Range } from '../../core/model/range';
import detectNewline from 'detect-newline';
import { TextEdit } from '../../core/services/text-edit';
import { getWikilinkDefinitionSetting } from './update-wikilinks';
export default async function activate(
context: ExtensionContext,

View File

@@ -1,6 +1,6 @@
import { ExtensionContext, commands, window } from 'vscode';
import { focusNote } from '../../utils';
import { Foam } from '../../core/model/foam';
import { focusNote } from '../../services/editor';
export default async function activate(
context: ExtensionContext,

View File

@@ -11,7 +11,7 @@ import {
workspace,
Position,
} from 'vscode';
import { isMdEditor, mdDocSelector } from '../../utils';
import { isMdEditor, mdDocSelector } from '../../services/editor';
import { Foam } from '../../core/model/foam';
import { FoamWorkspace } from '../../core/model/workspace';
import {
@@ -22,7 +22,6 @@ import {
import { fromVsCodeUri, toVsCodeRange } from '../../utils/vsc-utils';
import { getEditorEOL } from '../../services/editor';
import { ResourceParser } from '../../core/model/note';
import { getWikilinkDefinitionSetting } from '../../settings';
import { IMatcher } from '../../core/services/datastore';
export default async function activate(
@@ -58,6 +57,15 @@ export default async function activate(
);
}
export function getWikilinkDefinitionSetting():
| 'withExtensions'
| 'withoutExtensions'
| 'off' {
return workspace
.getConfiguration('foam.edit')
.get('linkReferenceDefinitions', 'withoutExtensions');
}
async function updateWikilinkDefinitions(
fWorkspace: FoamWorkspace,
fParser: ResourceParser,

View File

@@ -1,6 +1,5 @@
import { uniqWith } from 'lodash';
import * as vscode from 'vscode';
import { getNoteTooltip, mdDocSelector, isSome } from '../utils';
import { fromVsCodeUri, toVsCodeRange } from '../utils/vsc-utils';
import {
ConfigurationMonitor,
@@ -14,6 +13,9 @@ import { FoamGraph } from '../core/model/graph';
import { OPEN_COMMAND } from './commands/open-resource';
import { CREATE_NOTE_COMMAND } from './commands/create-note';
import { commandAsURI } from '../utils/commands';
import { Location } from '../core/model/location';
import { getNoteTooltip, mdDocSelector } from '../services/editor';
import { isSome } from '../core/utils';
export const CONFIG_KEY = 'links.hover.enable';
@@ -107,7 +109,7 @@ export class HoverProvider implements vscode.HoverProvider {
}
const command = CREATE_NOTE_COMMAND.forPlaceholder(
targetUri.path,
Location.forObjectWithRange(documentUri, targetLink),
this.workspace.defaultExtension,
{
askForTemplate: true,

View File

@@ -5,8 +5,8 @@ import { Resource } from '../core/model/note';
import { URI } from '../core/model/uri';
import { FoamWorkspace } from '../core/model/workspace';
import { getFoamVsCodeConfig } from '../services/config';
import { getNoteTooltip, mdDocSelector } from '../utils';
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
import { getNoteTooltip, mdDocSelector } from '../services/editor';
export const aliasCommitCharacters = ['#'];
export const linkCommitCharacters = ['#', '|'];

View File

@@ -12,6 +12,10 @@ import { createMarkdownParser } from '../core/services/markdown-parser';
import { FoamGraph } from '../core/model/graph';
import { commandAsURI } from '../utils/commands';
import { CREATE_NOTE_COMMAND } from './commands/create-note';
import { Location } from '../core/model/location';
import { URI } from '../core/model/uri';
import { Range } from '../core/model/range';
import { ResourceLink } from '../core/model/note';
describe('Document navigation', () => {
const parser = createMarkdownParser([]);
@@ -71,9 +75,8 @@ describe('Document navigation', () => {
it('should create links for placeholders', async () => {
const fileA = await createFile(`this is a link to [[a placeholder]].`);
const ws = createTestWorkspace().set(
parser.parse(fileA.uri, fileA.content)
);
const noteA = parser.parse(fileA.uri, fileA.content);
const ws = createTestWorkspace().set(noteA);
const graph = FoamGraph.fromWorkspace(ws);
const { doc } = await showInEditor(fileA.uri);
@@ -83,9 +86,13 @@ describe('Document navigation', () => {
expect(links.length).toEqual(1);
expect(links[0].target).toEqual(
commandAsURI(
CREATE_NOTE_COMMAND.forPlaceholder('a placeholder', '.md', {
onFileExists: 'open',
})
CREATE_NOTE_COMMAND.forPlaceholder(
Location.forObjectWithRange(noteA.uri, noteA.links[0]),
'.md',
{
onFileExists: 'open',
}
)
)
);
expect(links[0].range).toEqual(new vscode.Range(0, 20, 0, 33));

View File

@@ -1,5 +1,4 @@
import * as vscode from 'vscode';
import { mdDocSelector } from '../utils';
import { toVsCodeRange, toVsCodeUri, fromVsCodeUri } from '../utils/vsc-utils';
import { Foam } from '../core/model/foam';
import { FoamWorkspace } from '../core/model/workspace';
@@ -10,6 +9,8 @@ import { FoamGraph } from '../core/model/graph';
import { Position } from '../core/model/position';
import { CREATE_NOTE_COMMAND } from './commands/create-note';
import { commandAsURI } from '../utils/commands';
import { Location } from '../core/model/location';
import { mdDocSelector } from '../services/editor';
export default async function activate(
context: vscode.ExtensionContext,
@@ -146,10 +147,8 @@ export class NavigationProvider
public provideDocumentLinks(
document: vscode.TextDocument
): vscode.DocumentLink[] {
const resource = this.parser.parse(
fromVsCodeUri(document.uri),
document.getText()
);
const documentUri = fromVsCodeUri(document.uri);
const resource = this.parser.parse(documentUri, document.getText());
const targets: { link: ResourceLink; target: URI }[] = resource.links.map(
link => ({
@@ -162,7 +161,7 @@ export class NavigationProvider
.filter(o => o.target.isPlaceholder()) // links to resources are managed by the definition provider
.map(o => {
const command = CREATE_NOTE_COMMAND.forPlaceholder(
o.target.path,
Location.forObjectWithRange(documentUri, o.link),
this.workspace.defaultExtension,
{
onFileExists: 'open',

View File

@@ -1,6 +1,5 @@
import * as vscode from 'vscode';
import { URI } from '../../core/model/uri';
import { isNone } from '../../utils';
import { Foam } from '../../core/model/foam';
import { FoamWorkspace } from '../../core/model/workspace';
import { Connection, FoamGraph } from '../../core/model/graph';
@@ -14,6 +13,7 @@ import {
createConnectionItemsForResource,
} from './utils/tree-view-utils';
import { BaseTreeProvider } from './utils/base-tree-provider';
import { isNone } from '../../core/utils';
export default async function activate(
context: vscode.ExtensionContext,

View File

@@ -1,10 +1,9 @@
import * as vscode from 'vscode';
import { TextDecoder } from 'util';
import { getGraphStyle, getTitleMaxLength } from '../../settings';
import { isSome } from '../../utils';
import { Foam } from '../../core/model/foam';
import { Logger } from '../../core/utils/log';
import { fromVsCodeUri } from '../../utils/vsc-utils';
import { isSome } from '../../core/utils';
export default async function activate(
context: vscode.ExtensionContext,
@@ -104,7 +103,9 @@ function generateGraphData(foam: Foam) {
}
function cutTitle(title: string): string {
const maxLen = getTitleMaxLength();
const maxLen = vscode.workspace
.getConfiguration('foam.graph')
.get('titleMaxLength', 24);
if (maxLen > 0 && title.length > maxLen) {
return title.substring(0, maxLen).concat('...');
}
@@ -191,3 +192,7 @@ async function getWebviewContent(
return filled;
}
function getGraphStyle(): object {
return vscode.workspace.getConfiguration('foam.graph').get('style');
}

View File

@@ -5,6 +5,7 @@ import {
ResourceRangeTreeItem,
ResourceTreeItem,
createBacklinkItemsForResource as createBacklinkTreeItemsForResource,
expandAll,
} from './utils/tree-view-utils';
import { Resource } from '../../core/model/note';
import { FoamGraph } from '../../core/model/graph';
@@ -60,6 +61,11 @@ export default async function activate(
foam.graph.onDidUpdate(() => {
provider.refresh();
}),
vscode.commands.registerCommand(
`foam-vscode.views.notes-explorer.expand-all`,
(...args) =>
expandAll(treeView, provider, node => node.contextValue === 'folder')
),
vscode.window.onDidChangeActiveTextEditor(revealTextEditorItem),
treeView.onDidChangeVisibility(revealTextEditorItem)
);
@@ -138,18 +144,19 @@ export class NotesProvider extends FolderTreeProvider<
value: Resource,
parent: FolderTreeItem<Resource>
): NotesTreeItems {
const res = new ResourceTreeItem(value, this.workspace, {
const item = new ResourceTreeItem(value, this.workspace, {
parent,
collapsibleState:
this.graph.getBacklinks(value.uri).length > 0
? vscode.TreeItemCollapsibleState.Collapsed
: vscode.TreeItemCollapsibleState.None,
});
res.getChildren = async () => {
item.id = value.uri.toString();
item.getChildren = async () => {
const backlinks = await createBacklinkTreeItemsForResource(
this.workspace,
this.graph,
res.uri
item.uri
);
backlinks.forEach(item => {
item.description = item.label;
@@ -157,11 +164,11 @@ export class NotesProvider extends FolderTreeProvider<
});
return backlinks;
};
res.description =
item.description =
value.uri.getName().toLocaleLowerCase() ===
value.title.toLocaleLowerCase()
? undefined
: value.uri.getBasename();
return res;
return item;
}
}

View File

@@ -1,12 +1,17 @@
import * as vscode from 'vscode';
import { Foam } from '../../core/model/foam';
import { createMatcherAndDataStore } from '../../services/editor';
import { getOrphansConfig } from '../../settings';
import { GroupedResourcesTreeDataProvider } from './utils/grouped-resources-tree-data-provider';
import { getAttachmentsExtensions } from '../../settings';
import {
GroupedResourcesConfig,
GroupedResourcesTreeDataProvider,
} from './utils/grouped-resources-tree-data-provider';
import { ResourceTreeItem, UriTreeItem } from './utils/tree-view-utils';
import { IMatcher } from '../../core/services/datastore';
import { FoamWorkspace } from '../../core/model/workspace';
import { FoamGraph } from '../../core/model/graph';
import { URI } from '../../core/model/uri';
import { imageExtensions } from '../../core/services/attachment-provider';
const EXCLUDE_TYPES = ['image', 'attachment'];
export default async function activate(
@@ -44,6 +49,13 @@ export default async function activate(
);
}
/** Retrieve the orphans configuration */
export function getOrphansConfig(): GroupedResourcesConfig {
const orphansConfig = vscode.workspace.getConfiguration('foam.orphans');
const exclude: string[] = orphansConfig.get('exclude');
return { exclude };
}
export class OrphanTreeView extends GroupedResourcesTreeDataProvider {
constructor(
state: vscode.Memento,
@@ -66,6 +78,13 @@ export class OrphanTreeView extends GroupedResourcesTreeDataProvider {
.filter(
uri =>
!EXCLUDE_TYPES.includes(this.workspace.find(uri)?.type) &&
this.graph.getConnections(uri).length === 0
this.graph.getBacklinks(uri).length === 0 &&
this.graph.getLinks(uri).filter(c => !isAttachment(c.target))
.length === 0
);
}
function isAttachment(uri: URI) {
const ext = [...getAttachmentsExtensions(), ...imageExtensions];
return ext.includes(uri.getExtension());
}

View File

@@ -1,11 +1,14 @@
import * as vscode from 'vscode';
import { Foam } from '../../core/model/foam';
import { createMatcherAndDataStore } from '../../services/editor';
import { getPlaceholdersConfig } from '../../settings';
import { GroupedResourcesTreeDataProvider } from './utils/grouped-resources-tree-data-provider';
import {
GroupedResourcesConfig,
GroupedResourcesTreeDataProvider,
} from './utils/grouped-resources-tree-data-provider';
import {
UriTreeItem,
createBacklinkItemsForResource,
expandAll,
groupRangesByResource,
} from './utils/tree-view-utils';
import { IMatcher } from '../../core/services/datastore';
@@ -15,6 +18,13 @@ import { URI } from '../../core/model/uri';
import { FoamWorkspace } from '../../core/model/workspace';
import { FolderTreeItem } from './utils/folder-tree-provider';
/** Retrieve the placeholders configuration */
export function getPlaceholdersConfig(): GroupedResourcesConfig {
const placeholderCfg = vscode.workspace.getConfiguration('foam.placeholders');
const exclude: string[] = placeholderCfg.get('exclude');
return { exclude };
}
export default async function activate(
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
@@ -47,6 +57,17 @@ export default async function activate(
provider.onDidChangeTreeData(() => {
treeView.title = baseTitle + ` (${provider.nValues})`;
}),
vscode.commands.registerCommand(
`foam-vscode.views.placeholders.expand-all`,
() =>
expandAll(
treeView,
provider,
node =>
node.contextValue === 'placeholder' ||
node.contextValue === 'folder'
)
),
vscode.window.onDidChangeActiveTextEditor(() => {
if (provider.show.get() === 'for-current-file') {
provider.refresh();
@@ -92,6 +113,8 @@ export class PlaceholderTreeView extends GroupedResourcesTreeDataProvider {
parent,
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
});
item.contextValue = 'placeholder';
item.id = uri.toString();
item.getChildren = async () => {
return groupRangesByResource(
this.workspace,

View File

@@ -6,6 +6,7 @@ import { FoamTags } from '../../core/model/tags';
import {
ResourceRangeTreeItem,
ResourceTreeItem,
expandAll,
groupRangesByResource,
} from './utils/tree-view-utils';
import {
@@ -44,12 +45,21 @@ export default async function activate(
if (provider.show.get() === 'for-current-file') {
provider.refresh();
}
})
}),
vscode.commands.registerCommand(
`foam-vscode.views.${provider.providerId}.expand-all`,
() =>
expandAll(
treeView,
provider,
node => node.contextValue === 'tag' || node.contextValue === 'folder'
)
)
);
}
export class TagsProvider extends FolderTreeProvider<TagTreeItem, string> {
private providerId = 'tags-explorer';
public providerId = 'tags-explorer';
public show = new ContextMemento<'all' | 'for-current-file'>(
new MapBasedMemento(),
`foam-vscode.views.${this.providerId}.show`,
@@ -178,7 +188,12 @@ export class TagsProvider extends FolderTreeProvider<TagTreeItem, string> {
);
return [...acc, ...items];
}, []);
const resources = await groupRangesByResource(this.workspace, resourceTags);
const resources = (
await groupRangesByResource(this.workspace, resourceTags)
).map(item => {
item.id = element.tag + ' / ' + item.uri.toString();
return item;
});
return [...subtags, ...resources];
}
@@ -197,6 +212,7 @@ export class TagItem extends FolderTreeItem<string> {
) {
super(node, node.path.slice(-1)[0], parentElement);
this.tag = node.path.join(TAG_SEPARATOR);
this.id = this.tag;
this.description = `${nResourcesInSubtree} reference${
nResourcesInSubtree !== 1 ? 's' : ''
}`;

View File

@@ -10,6 +10,10 @@ import {
Folder,
} from './folder-tree-provider';
export interface GroupedResourcesConfig {
exclude: string[];
}
type GroupedResourceTreeItem = UriTreeItem | FolderTreeItem<URI>;
/**

View File

@@ -5,10 +5,11 @@ import { toVsCodeUri } from '../../../utils/vsc-utils';
import { Range } from '../../../core/model/range';
import { URI } from '../../../core/model/uri';
import { FoamWorkspace } from '../../../core/model/workspace';
import { getNoteTooltip } from '../../../utils';
import { isSome } from '../../../core/utils';
import { getBlockFor } from '../../../core/services/markdown-parser';
import { Connection, FoamGraph } from '../../../core/model/graph';
import { Logger } from '../../../core/utils/log';
import { getNoteTooltip } from '../../../services/editor';
export class BaseTreeItem extends vscode.TreeItem {
resolveTreeItem(): Promise<vscode.TreeItem> {
@@ -227,3 +228,57 @@ export function createConnectionItemsForResource(
});
return Promise.all(backlinkItems);
}
/**
* Expands a node and its children in a tree view that match a given predicate
*
* @param treeView - The tree view to expand nodes in
* @param provider - The tree data provider for the view
* @param element - The element to expand
* @param when - A function that returns true if the node should be expanded
*/
export async function expandNode<T>(
treeView: vscode.TreeView<any>,
provider: vscode.TreeDataProvider<T>,
element: T,
when: (element: T) => boolean
) {
try {
if (when(element)) {
await treeView.reveal(element, {
select: false,
focus: false,
expand: true,
});
}
} catch (e) {
const obj = element as any;
const label = obj.label ?? obj.toString();
Logger.warn(
`Could not expand element: ${label}. Try setting the ID property of the TreeItem`
);
}
const children = await provider.getChildren(element);
for (const child of children) {
await expandNode(treeView, provider, child, when);
}
}
/**
* Expands all items in a tree view that match a given predicate
*
* @param treeView - The tree view to expand items in
* @param provider - The tree data provider for the view
* @param when - A function that returns true if the node should be expanded
*/
export async function expandAll<T>(
treeView: vscode.TreeView<T>,
provider: vscode.TreeDataProvider<T>,
when: (element: T) => boolean = () => true
) {
const elements = await provider.getChildren(undefined);
for (const element of elements) {
await expandNode(treeView, provider, element, when);
}
}

View File

@@ -1,9 +1,9 @@
/*global markdownit:readonly*/
import markdownItRegex from 'markdown-it-regex';
import { isNone } from '../../utils';
import { FoamWorkspace } from '../../core/model/workspace';
import { Logger } from '../../core/utils/log';
import { isNone } from '../../core/utils';
export const markdownItFoamTags = (
md: markdownit,

View File

@@ -9,45 +9,38 @@ import {
import {
default as markdownItWikilinkEmbed,
CONFIG_EMBED_NOTE_TYPE,
CONFIG_EMBED_NOTE_IN_CONTAINER,
} from './wikilink-embed';
const parser = createMarkdownParser();
describe('Displaying included notes in preview', () => {
it('should render an included note in flat mode', async () => {
it('should render an included note in full inline mode', async () => {
const note = await createFile('This is the text of note A', [
'preview',
'note-a.md',
]);
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_IN_CONTAINER,
null,
async () => {
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'full-inline',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
CONFIG_EMBED_NOTE_TYPE,
'full-inline',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
expect(
md.render(`This is the root node.
expect(
md.render(`This is the root node.
![[note-a]]`)
).toMatch(
`<p>This is the root node.</p>
).toMatch(
`<p>This is the root node.</p>
<p><p>This is the text of note A</p>
</p>`
);
}
);
}
);
await deleteFile(note);
});
it('should render an included note in container mode', async () => {
it('should render an included note in full card mode', async () => {
const note = await createFile('This is the text of note A', [
'preview',
'note-a.md',
@@ -55,27 +48,21 @@ describe('Displaying included notes in preview', () => {
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_IN_CONTAINER,
null,
async () => {
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'full-card',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
CONFIG_EMBED_NOTE_TYPE,
'full-card',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
const res = md.render(`This is the root node. ![[note-a]]`);
expect(res).toContain('This is the root node');
expect(res).toContain('embed-container-note');
expect(res).toContain('This is the text of note A');
}
);
const res = md.render(`This is the root node. ![[note-a]]`);
expect(res).toContain('This is the root node');
expect(res).toContain('embed-container-note');
expect(res).toContain('This is the text of note A');
}
);
await deleteFile(note);
});
it('should render an included section', async () => {
it('should render an included section in full inline mode', async () => {
// here we use createFile as the test note doesn't fill in
// all the metadata we need
const note = await createFile(
@@ -96,24 +83,18 @@ This is the third section of note E
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_IN_CONTAINER,
null,
async () => {
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'full-inline',
() => {
expect(
md.render(`This is the root node.
CONFIG_EMBED_NOTE_TYPE,
'full-inline',
() => {
expect(
md.render(`This is the root node.
![[note-e#Section 2]]`)
).toMatch(
`<p>This is the root node.</p>
).toMatch(
`<p>This is the root node.</p>
<p><h1>Section 2</h1>
<p>This is the second section of note E</p>
</p>`
);
}
);
}
);
@@ -121,7 +102,7 @@ This is the third section of note E
await deleteFile(note);
});
it('should render an included section in container mode', async () => {
it('should render an included section in full card mode', async () => {
const note = await createFile(
`
# Section 1
@@ -139,24 +120,232 @@ This is the third section of note E
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_IN_CONTAINER,
null,
async () => {
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'full-card',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
CONFIG_EMBED_NOTE_TYPE,
'full-card',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
const res = md.render(
`This is the root node. ![[note-e-container#Section 3]]`
);
expect(res).toContain('This is the root node');
expect(res).toContain('embed-container-note');
expect(res).toContain('Section 3');
expect(res).toContain('This is the third section of note E');
}
const res = md.render(
`This is the root node. ![[note-e-container#Section 3]]`
);
expect(res).toContain('This is the root node');
expect(res).toContain('embed-container-note');
expect(res).toContain('Section 3');
expect(res).toContain('This is the third section of note E');
}
);
await deleteFile(note);
});
it('should not render the title of a note in content inline mode', async () => {
const note = await createFile(
`
# Title
## Section 1
This is the first section of note E`,
['note-e.md']
);
const parser = createMarkdownParser([]);
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'content-inline',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
expect(
md.render(`This is the root node.
![[note-e]]`)
).toMatch(
`<p>This is the root node.</p>
<p><h2>Section 1</h2>
<p>This is the first section of note E</p>
</p>`
);
}
);
await deleteFile(note);
});
it('should not render the title of a note in content card mode', async () => {
const note = await createFile(
`# Title
## Section 1
This is the first section of note E
`,
['note-e.md']
);
const parser = createMarkdownParser([]);
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'content-card',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
const res = md.render(`This is the root node. ![[note-e.md]]`);
expect(res).toContain('This is the root node');
expect(res).toContain('embed-container-note');
expect(res).toContain('Section 1');
expect(res).toContain('This is the first section of note E');
expect(res).not.toContain('Title');
}
);
await deleteFile(note);
});
it('should not render the section title, but still render subsection titles in content inline mode', async () => {
const note = await createFile(
`# Title
## Section 1
This is the first section of note E
### Subsection a
This is the first subsection of note E
`,
['note-e.md']
);
const parser = createMarkdownParser([]);
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'content-inline',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
expect(
md.render(`This is the root node.
![[note-e#Section 1]]`)
).toMatch(
`<p>This is the root node.</p>
<p><p>This is the first section of note E</p>
<h3>Subsection a</h3>
<p>This is the first subsection of note E</p>
</p>`
);
}
);
await deleteFile(note);
});
it('should not render the subsection title in content mode if you link to it and regardless of its level', async () => {
const note = await createFile(
`# Title
## Section 1
This is the first section of note E
### Subsection a
This is the first subsection of note E`,
['note-e.md']
);
const parser = createMarkdownParser([]);
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'content-inline',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
expect(
md.render(`This is the root node.
![[note-e#Subsection a]]`)
).toMatch(
`<p>This is the root node.</p>
<p><p>This is the first subsection of note E</p>
</p>`
);
}
);
await deleteFile(note);
});
it('should allow a note embedding type to be overridden if a modifier is passed in', async () => {
const note = await createFile(
`
# Section 1
This is the first section of note E
# Section 2
This is the second section of note E
# Section 3
This is the third section of note E
`,
['note-e.md']
);
const parser = createMarkdownParser([]);
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'full-inline',
() => {
expect(
md.render(`This is the root node.
content![[note-e#Section 2]]
full![[note-e#Section 3]]`)
).toMatch(
`<p>This is the root node.</p>
<p><p>This is the second section of note E</p>
</p>
<p><h1>Section 3</h1>
<p>This is the third section of note E</p>
</p>
`
);
}
);
await deleteFile(note);
});
it('should allow a note embedding type to be overridden if two modifiers are passed in', async () => {
const note = await createFile(
`
# Section 1
This is the first section of note E
# Section 2
This is the second section of note E
`,
['note-e.md']
);
const parser = createMarkdownParser([]);
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'full-inline',
() => {
const res = md.render(`This is the root node.
content-card![[note-e#Section 2]]`);
expect(res).toContain('This is the root node');
expect(res).toContain('embed-container-note');
expect(res).toContain('This is the second section of note E');
expect(res).not.toContain('Section 2');
}
);
@@ -175,6 +364,27 @@ This is the third section of note E
);
});
it('should render the bare text for an embedded note that is embedding a note that is not found', async () => {
const note = await createFile(
'This is the text of note A which includes ![[does-not-exist]]',
['note.md']
);
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'full-inline',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
expect(md.render(`This is the root node. ![[note]]`)).toMatch(
`<p>This is the root node. <p>This is the text of note A which includes ![[does-not-exist]]</p>
</p>`
);
}
);
});
it('should display a warning in case of cyclical inclusions', async () => {
const noteA = await createFile(
'This is the text of note A which includes ![[note-b]]',
@@ -187,14 +397,21 @@ This is the third section of note E
const ws = new FoamWorkspace()
.set(parser.parse(noteA.uri, noteA.content))
.set(parser.parse(noteB.uri, noteB.content));
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
const res = md.render(noteBText);
expect(res).toContain('This is the text of note B which includes');
expect(res).toContain('This is the text of note A which includes');
expect(res).toContain('Cyclic link detected for wikilink');
await withModifiedFoamConfiguration(
CONFIG_EMBED_NOTE_TYPE,
'full-card',
() => {
const md = markdownItWikilinkEmbed(MarkdownIt(), ws, parser);
const res = md.render(noteBText);
deleteFile(noteA);
deleteFile(noteB);
expect(res).toContain('This is the text of note B which includes');
expect(res).toContain('This is the text of note A which includes');
expect(res).toContain('Cyclic link detected for wikilink');
}
);
await deleteFile(noteA);
await deleteFile(noteB);
});
});

View File

@@ -1,4 +1,8 @@
import { retrieveNoteConfig } from './wikilink-embed';
import {
WIKILINK_EMBED_REGEX,
WIKILINK_EMBED_REGEX_GROUPS,
retrieveNoteConfig,
} from './wikilink-embed';
import * as config from '../../services/config';
describe('Wikilink Note Embedding', () => {
@@ -6,25 +10,81 @@ describe('Wikilink Note Embedding', () => {
jest.clearAllMocks();
});
describe('Wikilink Parsing', () => {
it('should match a wikilink item including a modifier and wikilink', () => {
// no configuration
expect('![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
// one of the configurations
expect('full![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
expect('content![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
expect('inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
expect('card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
// any combination of configurations
expect('full-inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
expect('full-card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
expect('content-inline![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
expect('content-card![[note-a]]').toMatch(WIKILINK_EMBED_REGEX);
});
it('should only match the wikilink if there are unrecognized keywords', () => {
const match1 = 'random-word![[note-a]]'.match(WIKILINK_EMBED_REGEX);
expect(match1[0]).toEqual('![[note-a]]');
expect(match1[1]).toEqual('![[note-a]]');
const match2 = 'foo![[note-a#section 1]]'.match(WIKILINK_EMBED_REGEX);
expect(match2[0]).toEqual('![[note-a#section 1]]');
expect(match2[1]).toEqual('![[note-a#section 1]]');
});
it('should group the wikilink into modifier and wikilink', () => {
const match1 = 'content![[note-a]]'.match(WIKILINK_EMBED_REGEX_GROUPS);
expect(match1[0]).toEqual('content![[note-a]]');
expect(match1[1]).toEqual('content');
expect(match1[2]).toEqual('note-a');
const match2 = 'full-inline![[note-a#section 1]]'.match(
WIKILINK_EMBED_REGEX_GROUPS
);
expect(match2[0]).toEqual('full-inline![[note-a#section 1]]');
expect(match2[1]).toEqual('full-inline');
expect(match2[2]).toEqual('note-a#section 1');
const match3 = '![[note-a#section 1]]'.match(WIKILINK_EMBED_REGEX_GROUPS);
expect(match3[0]).toEqual('![[note-a#section 1]]');
expect(match3[1]).toEqual(undefined);
expect(match3[2]).toEqual('note-a#section 1');
});
});
describe('Config Parsing', () => {
it('should use preview.embedNoteType if deprecated preview.embedNoteInContainer not used', () => {
it('should use preview.embedNoteType if an explicit modifier is not passed in', () => {
jest
.spyOn(config, 'getFoamVsCodeConfig')
.mockReturnValueOnce('full-card')
.mockReturnValueOnce(false);
.mockReturnValueOnce('full-card');
const { noteScope, noteStyle } = retrieveNoteConfig();
const { noteScope, noteStyle } = retrieveNoteConfig(undefined);
expect(noteScope).toEqual('full');
expect(noteStyle).toEqual('card');
});
it('should use preview.embedNoteInContainer if set', () => {
it('should use explicit modifier over user settings if passed in', () => {
jest
.spyOn(config, 'getFoamVsCodeConfig')
.mockReturnValueOnce('full-inline')
.mockReturnValueOnce(true);
.mockReturnValueOnce('full-inline')
.mockReturnValueOnce('full-inline');
const { noteScope, noteStyle } = retrieveNoteConfig();
let { noteScope, noteStyle } = retrieveNoteConfig('content-card');
expect(noteScope).toEqual('content');
expect(noteStyle).toEqual('card');
({ noteScope, noteStyle } = retrieveNoteConfig('content'));
expect(noteScope).toEqual('content');
expect(noteStyle).toEqual('inline');
({ noteScope, noteStyle } = retrieveNoteConfig('card'));
expect(noteScope).toEqual('full');
expect(noteStyle).toEqual('card');
});

View File

@@ -4,7 +4,6 @@
import { readFileSync } from 'fs';
import { workspace as vsWorkspace } from 'vscode';
import markdownItRegex from 'markdown-it-regex';
import { isSome } from '../../utils';
import { FoamWorkspace } from '../../core/model/workspace';
import { Logger } from '../../core/utils/log';
import { Resource, ResourceParser } from '../../core/model/note';
@@ -13,8 +12,15 @@ import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
import { MarkdownLink } from '../../core/services/markdown-link';
import { Position } from '../../core/model/position';
import { TextEdit } from '../../core/services/text-edit';
import { isNone, isSome } from '../../core/utils';
export const CONFIG_EMBED_NOTE_IN_CONTAINER = 'preview.embedNoteInContainer';
export const WIKILINK_EMBED_REGEX =
/((?:(?:full|content)-(?:inline|card)|full|content|inline|card)?!\[\[[^[\]]+?\]\])/;
// we need another regex because md.use(regex, replace) only permits capturing one group
// so we capture the entire possible wikilink item (ex. content-card![[note]]) using WIKILINK_EMBED_REGEX and then
// use WIKILINK_EMBED_REGEX_GROUPER to parse it into the modifier(content-card) and the wikilink(note)
export const WIKILINK_EMBED_REGEX_GROUPS =
/((?:\w+)|(?:(?:\w+)-(?:\w+)))?!\[\[([^[\]]+?)\]\]/;
export const CONFIG_EMBED_NOTE_TYPE = 'preview.embedNoteType';
const refsStack: string[] = [];
@@ -25,9 +31,13 @@ export const markdownItWikilinkEmbed = (
) => {
return md.use(markdownItRegex, {
name: 'embed-wikilinks',
regex: /!\[\[([^[\]]+?)\]\]/,
replace: (wikilink: string) => {
regex: WIKILINK_EMBED_REGEX,
replace: (wikilinkItem: string) => {
try {
const [_, noteEmbedModifier, wikilink] = wikilinkItem.match(
WIKILINK_EMBED_REGEX_GROUPS
);
const includedNote = workspace.find(wikilink);
if (!includedNote) {
@@ -50,9 +60,15 @@ export const markdownItWikilinkEmbed = (
switch (includedNote.type) {
case 'note': {
const { noteScope: _, noteStyle } = retrieveNoteConfig();
const { noteScope, noteStyle } =
retrieveNoteConfig(noteEmbedModifier);
const extractor: EmbedNoteExtractor = fullExtractor;
const extractor: EmbedNoteExtractor =
noteScope === 'full'
? fullExtractor
: noteScope === 'content'
? contentExtractor
: fullExtractor;
const formatter: EmbedNoteFormatter =
noteStyle === 'card'
@@ -84,7 +100,7 @@ Embed for attachments is not supported
return html;
} catch (e) {
Logger.error(
`Error while including [[${wikilink}]] into the current document of the Preview panel`,
`Error while including ${wikilinkItem} into the current document of the Preview panel`,
e
);
return '';
@@ -106,6 +122,11 @@ function withLinksRelativeToWorkspaceRoot(
.map(link => {
const info = MarkdownLink.analyzeLink(link);
const resource = workspace.find(info.target);
// embedded notes that aren't created are still collected
// return null so it can be filtered in the next step
if (isNone(resource)) {
return null;
}
const pathFromRoot = vsWorkspace.asRelativePath(
toVsCodeUri(resource.uri)
);
@@ -113,6 +134,7 @@ function withLinksRelativeToWorkspaceRoot(
target: pathFromRoot,
});
})
.filter(linkEdits => !isNone(linkEdits))
.sort((a, b) => Position.compareTo(b.range.start, a.range.start));
const text = edits.reduce(
(text, edit) => TextEdit.apply(text, edit),
@@ -121,17 +143,22 @@ function withLinksRelativeToWorkspaceRoot(
return text;
}
export function retrieveNoteConfig(): {
export function retrieveNoteConfig(explicitModifier: string | undefined): {
noteScope: string;
noteStyle: string;
} {
let config = getFoamVsCodeConfig<string>(CONFIG_EMBED_NOTE_TYPE); // ex. full-inline
let [noteScope, noteStyle] = config.split('-');
// **DEPRECATED** setting to be removed
// for now it overrides the above to preserve user settings if they have it set
if (getFoamVsCodeConfig<boolean>(CONFIG_EMBED_NOTE_IN_CONTAINER, false)) {
noteStyle = 'card';
// an explicit modifier will always override corresponding user setting
if (explicitModifier !== undefined) {
if (['full', 'content'].includes(explicitModifier)) {
noteScope = explicitModifier;
} else if (['card', 'inline'].includes(explicitModifier)) {
noteStyle = explicitModifier;
} else {
[noteScope, noteStyle] = explicitModifier.split('-');
}
}
return { noteScope, noteStyle };
}
@@ -162,6 +189,32 @@ function fullExtractor(
return noteText;
}
function contentExtractor(
note: Resource,
parser: ResourceParser,
workspace: FoamWorkspace
): string {
let noteText = readFileSync(note.uri.toFsPath()).toString();
let section = Resource.findSection(note, note.uri.fragment);
if (!note.uri.fragment) {
// if there's no fragment(section), the wikilink is linking to the entire note,
// in which case we need to remove the title. We could just use rows.shift()
// but should the note start with blank lines, it will only remove the first blank line
// leaving the title
// A better way is to find where the actual title starts by assuming it's at section[0]
// then we treat it as the same case as link to a section
section = note.sections.length ? note.sections[0] : null;
}
let rows = noteText.split('\n');
if (isSome(section)) {
rows = rows.slice(section.range.start.line, section.range.end.line);
}
rows.shift();
noteText = rows.join('\n');
noteText = withLinksRelativeToWorkspaceRoot(noteText, parser, workspace);
return noteText;
}
/**
* A type of function that renders note content with the desired style in html
*/

View File

@@ -26,9 +26,9 @@ describe('Link generation in preview', () => {
markdownItRemoveLinkReferences,
].reduce((acc, extension) => extension(acc, ws), MarkdownIt());
it('generates a link to a note', () => {
it('generates a link to a note using the note title as link', () => {
expect(md.render(`[[note-a]]`)).toEqual(
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>note-a</a></p>\n`
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>${noteA.title}</a></p>\n`
);
});
@@ -48,13 +48,22 @@ describe('Link generation in preview', () => {
const note = `[[note-a]]
[note-a]: <note-a.md> "Note A"`;
expect(md.render(note)).toEqual(
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>note-a</a>\n[note-a]: &lt;note-a.md&gt; &quot;Note A&quot;</p>\n`
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>${noteA.title}</a>\n[note-a]: &lt;note-a.md&gt; &quot;Note A&quot;</p>\n`
);
});
it('generates a link to a section within the note', () => {
expect(md.render(`[[#sec]]`)).toEqual(
`<p><a class='foam-note-link' title='sec' href='#sec' data-href='#sec'>#sec</a></p>\n`
);
expect(md.render(`[[#Section Name]]`)).toEqual(
`<p><a class='foam-note-link' title='Section Name' href='#section-name' data-href='#section-name'>#Section Name</a></p>\n`
);
});
it('generates a link to a note with a specific section', () => {
expect(md.render(`[[note-b#sec2]]`)).toEqual(
`<p><a class='foam-note-link' title='My second note#sec2' href='/path2/to/note-b.md#sec2' data-href='/path2/to/note-b.md#sec2'>note-b#sec2</a></p>\n`
`<p><a class='foam-note-link' title='My second note#sec2' href='/path2/to/note-b.md#sec2' data-href='/path2/to/note-b.md#sec2'>${noteB.title}#sec2</a></p>\n`
);
});
@@ -66,7 +75,7 @@ describe('Link generation in preview', () => {
it('generates a link to a note if the note exists, but the section does not exist', () => {
expect(md.render(`[[note-b#nonexistentsec]]`)).toEqual(
`<p><a class='foam-note-link' title='My second note#nonexistentsec' href='/path2/to/note-b.md#nonexistentsec' data-href='/path2/to/note-b.md#nonexistentsec'>note-b#nonexistentsec</a></p>\n`
`<p><a class='foam-note-link' title='My second note#nonexistentsec' href='/path2/to/note-b.md#nonexistentsec' data-href='/path2/to/note-b.md#nonexistentsec'>${noteB.title}#nonexistentsec</a></p>\n`
);
});

View File

@@ -2,13 +2,14 @@
import markdownItRegex from 'markdown-it-regex';
import * as vscode from 'vscode';
import { isNone } from '../../utils';
import { FoamWorkspace } from '../../core/model/workspace';
import { Logger } from '../../core/utils/log';
import { toVsCodeUri } from '../../utils/vsc-utils';
import { MarkdownLink } from '../../core/services/markdown-link';
import { Range } from '../../core/model/range';
import { isEmpty } from 'lodash';
import { toSlug } from '../../utils/slug';
import { isNone } from '../../core/utils';
export const markdownItWikilinkNavigation = (
md: markdownit,
@@ -26,18 +27,32 @@ export const markdownItWikilinkNavigation = (
isEmbed: false,
});
const formattedSection = section ? `#${section}` : '';
const linkSection = section ? `#${toSlug(section)}` : '';
const label = isEmpty(alias) ? `${target}${formattedSection}` : alias;
// [[#section]] links
if (target.length === 0) {
// we don't have a good way to check if the section exists within the
// open file, so we just create a regular link for it
return getResourceLink(section, linkSection, label);
}
const resource = workspace.find(target);
if (isNone(resource)) {
return getPlaceholderLink(label);
}
const link = `${vscode.workspace.asRelativePath(
const resourceLabel = isEmpty(alias)
? `${resource.title}${formattedSection}`
: alias;
const resourceLink = `/${vscode.workspace.asRelativePath(
toVsCodeUri(resource.uri)
)}${formattedSection}`;
const title = `${resource.title}${formattedSection}`;
return `<a class='foam-note-link' title='${title}' href='/${link}' data-href='/${link}'>${label}</a>`;
)}`;
return getResourceLink(
`${resource.title}${formattedSection}`,
`${resourceLink}${linkSection}`,
resourceLabel
);
} catch (e) {
Logger.error(
`Error while creating link for [[${wikilink}]] in Preview panel`,
@@ -52,4 +67,7 @@ export const markdownItWikilinkNavigation = (
const getPlaceholderLink = (content: string) =>
`<a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">${content}</a>`;
const getResourceLink = (title: string, link: string, label: string) =>
`<a class='foam-note-link' title='${title}' href='${link}' data-href='${link}'>${label}</a>`;
export default markdownItWikilinkNavigation;

View File

@@ -1,7 +1,8 @@
import * as vscode from 'vscode';
import { Foam } from '../core/model/foam';
import { FoamTags } from '../core/model/tags';
import { isInFrontMatter, isOnYAMLKeywordLine, mdDocSelector } from '../utils';
import { isInFrontMatter, isOnYAMLKeywordLine } from '../core/utils/md';
import { mdDocSelector } from '../services/editor';
// this regex is different from HASHTAG_REGEX in that it does not look for a
// #+character. It uses a negative look-ahead for `# `

View File

@@ -5,13 +5,13 @@ import { Resource, ResourceParser } from '../core/model/note';
import { Range } from '../core/model/range';
import { FoamWorkspace } from '../core/model/workspace';
import { MarkdownLink } from '../core/services/markdown-link';
import { isNone } from '../utils';
import {
fromVsCodeUri,
toVsCodePosition,
toVsCodeRange,
toVsCodeUri,
} from '../utils/vsc-utils';
import { isNone } from '../core/utils';
const AMBIGUOUS_IDENTIFIER_CODE = 'ambiguous-identifier';
const UNKNOWN_SECTION_CODE = 'unknown-section';
@@ -240,12 +240,12 @@ const createReplaceSectionCommand = (
section: string
): vscode.CodeAction => {
const action = new vscode.CodeAction(
`Use ${section}`,
`${section}`,
vscode.CodeActionKind.QuickFix
);
action.command = {
command: REPLACE_TEXT_COMMAND.name,
title: `Use section ${section}`,
title: `Use section "${section}"`,
arguments: [
{
value: section,
@@ -269,7 +269,7 @@ const createFindIdentifierCommand = (
possibleTargets: vscode.Uri[]
): vscode.CodeAction => {
const action = new vscode.CodeAction(
`Use ${vscode.workspace.asRelativePath(target)}`,
`${vscode.workspace.asRelativePath(target)}`,
vscode.CodeActionKind.QuickFix
);
action.command = {

View File

@@ -1,6 +1,5 @@
import { isEmpty } from 'lodash';
import { asAbsoluteUri, URI } from '../core/model/uri';
import { TextEncoder } from 'util';
import { isEmpty } from 'lodash';
import {
EndOfLine,
FileType,
@@ -8,15 +7,18 @@ import {
Selection,
SnippetString,
TextDocument,
TextEditor,
Uri,
ViewColumn,
window,
workspace,
WorkspaceEdit,
MarkdownString,
} from 'vscode';
import { focusNote } from '../utils';
import { getExcerpt, stripFrontMatter, stripImages } from '../core/utils/md';
import { isSome } from '../core/utils/core';
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
import { isSome } from '../core/utils';
import { asAbsoluteUri, URI } from '../core/model/uri';
import {
AlwaysIncludeMatcher,
FileListBasedMatcher,
@@ -31,6 +33,35 @@ interface SelectionInfo {
content: string;
}
/**
* Returns a MarkdownString of the note content
* @param note A Foam Note
*/
export function getNoteTooltip(content: string): string {
const strippedContent = stripFrontMatter(stripImages(content));
return formatMarkdownTooltip(strippedContent) as any;
}
export function formatMarkdownTooltip(content: string): MarkdownString {
const LINES_LIMIT = 16;
const { excerpt, lines } = getExcerpt(content, LINES_LIMIT);
const totalLines = content.split('\n').length;
const diffLines = totalLines - lines;
const ellipsis = diffLines > 0 ? `\n\n[...] *(+ ${diffLines} lines)*` : '';
const md = new MarkdownString(`${excerpt}${ellipsis}`);
md.isTrusted = true;
return md;
}
export const mdDocSelector = [
{ language: 'markdown', scheme: 'file' },
{ language: 'markdown', scheme: 'untitled' },
];
export function isMdEditor(editor: TextEditor) {
return editor && editor.document && editor.document.languageId === 'markdown';
}
export function findSelectionContent(): SelectionInfo | undefined {
const editor = window.activeTextEditor;
if (editor === undefined) {
@@ -51,6 +82,24 @@ export function findSelectionContent(): SelectionInfo | undefined {
};
}
export async function focusNote(
notePath: URI,
moveCursorToEnd: boolean,
viewColumn: ViewColumn = ViewColumn.Active
) {
const document = await workspace.openTextDocument(toVsCodeUri(notePath));
const editor = await window.showTextDocument(document, viewColumn);
// Move the cursor to end of the file
if (moveCursorToEnd) {
const { lineCount } = editor.document;
const { range } = editor.document.lineAt(lineCount - 1);
editor.selection = new Selection(range.end, range.end);
}
return { document, editor };
}
export async function createDocAndFocus(
text: SnippetString,
filepath: URI,

View File

@@ -1,7 +1,10 @@
import { window, commands, ExtensionContext } from 'vscode';
import { workspace, window, commands, ExtensionContext } from 'vscode';
import { IDisposable } from '../core/common/lifecycle';
import { BaseLogger, ILogger, LogLevel } from '../core/utils/log';
import { getFoamLoggerLevel } from '../settings';
function getFoamLoggerLevel(): LogLevel {
return workspace.getConfiguration('foam.logging').get('level') ?? 'info';
}
export interface VsCodeLogger extends ILogger, IDisposable {
show();

View File

@@ -8,7 +8,6 @@ import {
window,
workspace,
} from 'vscode';
import { focusNote, isNone } from '../utils';
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
import { UserCancelledOperation } from './errors';
@@ -18,6 +17,7 @@ import {
deleteFile,
fileExists,
findSelectionContent,
focusNote,
getCurrentEditorDirectory,
readFile,
replaceSelection,
@@ -25,6 +25,7 @@ import {
import { Resolver } from './variable-resolver';
import dateFormat from 'dateformat';
import { getFoamVsCodeConfig } from './config';
import { isNone } from '../core/utils';
/**
* The templates directory

View File

@@ -1,6 +1,5 @@
import { workspace, GlobPattern } from 'vscode';
import { uniq } from 'lodash';
import { LogLevel } from './core/utils/log';
import { getFoamVsCodeConfig } from './services/config';
/**
@@ -39,15 +38,6 @@ export function getAttachmentsExtensions() {
.map(ext => '.' + ext.trim());
}
export function getWikilinkDefinitionSetting():
| 'withExtensions'
| 'withoutExtensions'
| 'off' {
return workspace
.getConfiguration('foam.edit')
.get('linkReferenceDefinitions', 'withoutExtensions');
}
/** Retrieve the list of file ignoring globs. */
export function getIgnoredFilesSetting(): GlobPattern[] {
return [
@@ -56,35 +46,3 @@ export function getIgnoredFilesSetting(): GlobPattern[] {
...Object.keys(workspace.getConfiguration().get('files.exclude', {})),
];
}
/** Retrieves the maximum length for a Graph node title. */
export function getTitleMaxLength(): number {
return workspace.getConfiguration('foam.graph').get('titleMaxLength');
}
/** Retrieve the graph's style object */
export function getGraphStyle(): object {
return workspace.getConfiguration('foam.graph').get('style');
}
export function getFoamLoggerLevel(): LogLevel {
return workspace.getConfiguration('foam.logging').get('level') ?? 'info';
}
/** Retrieve the orphans configuration */
export function getOrphansConfig(): GroupedResourcesConfig {
const orphansConfig = workspace.getConfiguration('foam.orphans');
const exclude: string[] = orphansConfig.get('exclude');
return { exclude };
}
/** Retrieve the placeholders configuration */
export function getPlaceholdersConfig(): GroupedResourcesConfig {
const placeholderCfg = workspace.getConfiguration('foam.placeholders');
const exclude: string[] = placeholderCfg.get('exclude');
return { exclude };
}
export interface GroupedResourcesConfig {
exclude: string[];
}

View File

@@ -1,235 +0,0 @@
import {
Range,
TextDocument,
window,
Position,
TextEditor,
workspace,
Selection,
MarkdownString,
ViewColumn,
} from 'vscode';
import matter from 'gray-matter';
import { toVsCodeUri } from './utils/vsc-utils';
import { URI } from './core/model/uri';
import { getEditorEOL } from './services/editor';
export const mdDocSelector = [
{ language: 'markdown', scheme: 'file' },
{ language: 'markdown', scheme: 'untitled' },
];
export function isMdEditor(editor: TextEditor) {
return editor && editor.document && editor.document.languageId === 'markdown';
}
export function detectGeneratedCode(
fullText: string,
header: string,
footer: string
): { range: Range | null; lines: string[] } {
const lines = fullText.split(getEditorEOL());
const headerLine = lines.findIndex(line => line === header);
const footerLine = lines.findIndex(line => line === footer);
if (headerLine < 0 || headerLine >= footerLine) {
return {
range: null,
lines: [],
};
}
return {
range: new Range(
new Position(headerLine, 0),
new Position(footerLine, lines[footerLine].length + 1)
),
lines: lines.slice(headerLine + 1, footerLine + 1),
};
}
export function hasEmptyTrailing(doc: TextDocument): boolean {
return doc.lineAt(doc.lineCount - 1).isEmptyOrWhitespace;
}
export function getText(range: Range): string {
return window.activeTextEditor.document.getText(range);
}
/**
* Used for the "Copy to Clipboard Without Brackets" command
*
*/
export function removeBrackets(s: string): string {
// take in the string, split on space
const stringSplitBySpace = s.split(' ');
// loop through words
const modifiedWords = stringSplitBySpace.map(currentWord => {
if (currentWord.includes('[[')) {
// all of these transformations will turn this "[[you-are-awesome]]"
// to this "you are awesome"
let word = currentWord.replace(/(\[\[)/g, '');
word = word.replace(/(\]\])/g, '');
word = word.replace(/(.mdx|.md|.markdown)/g, '');
word = word.replace(/[-]/g, ' ');
// then we titlecase the word so "you are awesome"
// becomes "You Are Awesome"
const titleCasedWord = toTitleCase(word);
return titleCasedWord;
}
return currentWord;
});
return modifiedWords.join(' ');
}
/**
* Takes in a string and returns it titlecased
*
* @example toTitleCase("hello world") -> "Hello World"
*/
export function toTitleCase(word: string): string {
return word
.split(' ')
.map(word => word[0].toUpperCase() + word.substring(1))
.join(' ');
}
/**
* Verify the given object is defined
*
* @param value The object to verify
*/
export function isSome<T>(
value: T | null | undefined | void
): value is NonNullable<T> {
//
return value != null;
}
/**
* Verify the given object is not defined
*
* @param value The object to verify
*/
export function isNone<T>(
value: T | null | undefined | void
): value is null | undefined | void {
return value == null;
}
export async function focusNote(
notePath: URI,
moveCursorToEnd: boolean,
viewColumn: ViewColumn = ViewColumn.Active
) {
const document = await workspace.openTextDocument(toVsCodeUri(notePath));
const editor = await window.showTextDocument(document, viewColumn);
// Move the cursor to end of the file
if (moveCursorToEnd) {
const { lineCount } = editor.document;
const { range } = editor.document.lineAt(lineCount - 1);
editor.selection = new Selection(range.end, range.end);
}
return { document, editor };
}
export function getContainsTooltip(titles: string[]): string {
const TITLES_LIMIT = 5;
const ellipsis = titles.length > TITLES_LIMIT ? ',...' : '';
return `Contains "${titles.slice(0, TITLES_LIMIT).join('", "')}"${ellipsis}`;
}
/**
* Depending on the current vscode version, returns a MarkdownString of the
* note content casted as string or returns a simple string
* MarkdownString is only available from 1.52.1 onwards
* https://code.visualstudio.com/updates/v1_52#_markdown-tree-tooltip-api
* @param note A Foam Note
*/
export function getNoteTooltip(content: string): string {
const strippedContent = stripFrontMatter(stripImages(content));
return formatMarkdownTooltip(strippedContent) as any;
}
export function formatMarkdownTooltip(content: string): MarkdownString {
const LINES_LIMIT = 16;
const { excerpt, lines } = getExcerpt(content, LINES_LIMIT);
const totalLines = content.split('\n').length;
const diffLines = totalLines - lines;
const ellipsis = diffLines > 0 ? `\n\n[...] *(+ ${diffLines} lines)*` : '';
const md = new MarkdownString(`${excerpt}${ellipsis}`);
md.isTrusted = true;
return md;
}
export function getExcerpt(
markdown: string,
maxLines: number
): { excerpt: string; lines: number } {
const OFFSET_LINES_LIMIT = 5;
const paragraphs = markdown.replace(/\r\n/g, '\n').split('\n\n');
const excerpt: string[] = [];
let lines = 0;
for (const paragraph of paragraphs) {
const n = paragraph.split('\n').length;
if (lines > maxLines || lines + n - maxLines > OFFSET_LINES_LIMIT) {
break;
}
excerpt.push(paragraph);
lines = lines + n + 1;
}
return { excerpt: excerpt.join('\n\n'), lines };
}
export function stripFrontMatter(markdown: string): string {
return matter(markdown).content.trim();
}
export function stripImages(markdown: string): string {
return markdown.replace(
/!\[(.*)\]\([-/\\.A-Za-z]*\)/gi,
'$1'.length ? '[Image: $1]' : ''
);
}
export function isInFrontMatter(content: string, lineNumber: number): Boolean {
const FIRST_DELIMITER_MATCH = /^---\s*?$/gm;
const LAST_DELIMITER_MATCH = /^[-.]{3}\s*?$/g;
// if we're on the first line, we're not _yet_ in the front matter
if (lineNumber === 0) {
return false;
}
// look for --- at start, and a second --- or ... to end
if (content.match(FIRST_DELIMITER_MATCH) === null) {
return false;
}
const lines = content.split('\n');
lines.shift();
const endLineMatches = (l: string) => l.match(LAST_DELIMITER_MATCH);
const endLineNumber = lines.findIndex(endLineMatches);
return endLineNumber === -1 || endLineNumber >= lineNumber;
}
export function isOnYAMLKeywordLine(content: string, keyword: string): Boolean {
const keywordMatch = /^\s*(\w+):/gm;
if (content.match(keywordMatch) === null) {
return false;
}
const matches = Array.from(content.matchAll(keywordMatch));
const lastMatch = matches[matches.length - 1];
return lastMatch[1] === keyword;
}

View File

@@ -9,7 +9,7 @@ export const toVsCodePosition = (p: FoamPosition): Position =>
export const toVsCodeRange = (r: FoamRange): Range =>
new Range(r.start.line, r.start.character, r.end.line, r.end.character);
export const toVsCodeUri = (u: FoamURI): Uri => Uri.parse(u.toString());
export const toVsCodeUri = (u: FoamURI): Uri => Uri.from(u);
export const fromVsCodeUri = (u: Uri): FoamURI => FoamURI.parse(u.toString());

View File

@@ -0,0 +1,21 @@
# File with different link formats
markdown link [home page](https://foambubble.github.io/)
wikilink to file [[first-document]].
markdown format link to local [file](first-document.md)
embedded wikilink to file ![[second-document]].
wikilink to placeholder [[non-exist-file]]
in-note anchor [[file-with-different-link-formats#one section]]
alias to anchor [[file-with-different-link-formats#one section|another name]]
alias [[first-document|an alias]]
dupilcated wikilink to file [[first-document]]
# one section

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-114-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-119-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/foam.foam-vscode?label=VS%20Code%20Installs)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
@@ -190,6 +190,16 @@ Foam is licensed under the [MIT license](LICENSE).
[Backlinking]: docs/user/features/backlinking.md "Backlinking"
[//end]: # "Autogenerated link references"
## Contribution Guide
See the [Contribution Guide](https://foambubble.github.io/foam/dev/contribution-guide)
## Code of conduct
See the [Code of Conduct](https://foambubble.github.io/foam/dev/code-of-conduct)
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -346,6 +356,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://amnesiak.org/me"><img src="https://avatars.githubusercontent.com/u/952059?v=4?s=60" width="60px;" alt="Tony Cheneau"/><br /><sub><b>Tony Cheneau</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tcheneau" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nicholas-l"><img src="https://avatars.githubusercontent.com/u/12977174?v=4?s=60" width="60px;" alt="Nicholas Latham"/><br /><sub><b>Nicholas Latham</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nicholas-l" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://thara.dev"><img src="https://avatars.githubusercontent.com/u/1532891?v=4?s=60" width="60px;" alt="Tomochika Hara"/><br /><sub><b>Tomochika Hara</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=thara" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dcarosone"><img src="https://avatars.githubusercontent.com/u/11495017?v=4?s=60" width="60px;" alt="Daniel Carosone"/><br /><sub><b>Daniel Carosone</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dcarosone" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MABruni"><img src="https://avatars.githubusercontent.com/u/100445384?v=4?s=60" width="60px;" alt="Miguel Angel Bruni Montero"/><br /><sub><b>Miguel Angel Bruni Montero</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MABruni" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Walshkev"><img src="https://avatars.githubusercontent.com/u/77123083?v=4?s=60" width="60px;" alt="Kevin Walsh "/><br /><sub><b>Kevin Walsh </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Walshkev" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://hereistheusername.github.io/"><img src="https://avatars.githubusercontent.com/u/33437051?v=4?s=60" width="60px;" alt="Xinglan Liu"/><br /><sub><b>Xinglan Liu</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hereistheusername" title="Code">💻</a></td>
</tr>
</tbody>
</table>

129
yarn.lock
View File

@@ -33,6 +33,14 @@
"@babel/highlight" "^7.22.10"
chalk "^2.4.2"
"@babel/code-frame@^7.22.13":
version "7.22.13"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
dependencies:
"@babel/highlight" "^7.22.13"
chalk "^2.4.2"
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5":
version "7.20.14"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8"
@@ -104,6 +112,16 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/generator@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
dependencies:
"@babel/types" "^7.23.0"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
@@ -180,6 +198,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
"@babel/helper-environment-visitor@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
"@babel/helper-environment-visitor@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98"
@@ -200,13 +223,13 @@
"@babel/template" "^7.18.10"
"@babel/types" "^7.19.0"
"@babel/helper-function-name@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be"
integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==
"@babel/helper-function-name@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
dependencies:
"@babel/template" "^7.22.5"
"@babel/types" "^7.22.5"
"@babel/template" "^7.22.15"
"@babel/types" "^7.23.0"
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
@@ -357,6 +380,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/helper-validator-identifier@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
"@babel/helper-validator-identifier@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
@@ -418,7 +446,16 @@
chalk "^2.4.2"
js-tokens "^4.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.13", "@babel/parser@^7.20.5", "@babel/parser@^7.20.7":
"@babel/highlight@^7.22.13":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
dependencies:
"@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2"
js-tokens "^4.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.5", "@babel/parser@^7.20.7":
version "7.20.15"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89"
integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==
@@ -428,6 +465,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55"
integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==
"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
@@ -1070,6 +1112,15 @@
"@babel/parser" "^7.20.7"
"@babel/types" "^7.20.7"
"@babel/template@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
dependencies:
"@babel/code-frame" "^7.22.13"
"@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15"
"@babel/template@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
@@ -1079,35 +1130,19 @@
"@babel/parser" "^7.22.5"
"@babel/types" "^7.22.5"
"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.7.2":
version "7.20.13"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473"
integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==
"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.22.10", "@babel/traverse@^7.7.2":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
dependencies:
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.20.7"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.19.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.20.13"
"@babel/types" "^7.20.7"
debug "^4.1.0"
globals "^11.1.0"
"@babel/traverse@^7.22.10":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa"
integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==
dependencies:
"@babel/code-frame" "^7.22.10"
"@babel/generator" "^7.22.10"
"@babel/helper-environment-visitor" "^7.22.5"
"@babel/helper-function-name" "^7.22.5"
"@babel/code-frame" "^7.22.13"
"@babel/generator" "^7.23.0"
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-function-name" "^7.23.0"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/parser" "^7.22.10"
"@babel/types" "^7.22.10"
"@babel/parser" "^7.23.0"
"@babel/types" "^7.23.0"
debug "^4.1.0"
globals "^11.1.0"
@@ -1129,6 +1164,15 @@
"@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0"
"@babel/types@^7.22.15", "@babel/types@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
dependencies:
"@babel/helper-string-parser" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -3542,9 +3586,9 @@ axe-core@^4.6.2:
integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==
axios@^1.0.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.2.tgz#7ac517f0fa3ec46e0e636223fd973713a09c72b3"
integrity sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==
version "1.6.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.1.tgz#76550d644bf0a2d469a01f9244db6753208397d7"
integrity sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
@@ -4381,7 +4425,12 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
dateformat@^3.0.0, dateformat@^3.0.3:
dateformat@4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.5.1.tgz#c20e7a9ca77d147906b6dc2261a8be0a5bd2173c"
integrity sha512-OD0TZ+B7yP7ZgpJf5K2DIbj3FZvFvxgFUuaqA/V5zTjAtAAXZ1E8bktHxmAGs4x5b7PflqA9LeQ84Og7wYtF7Q==
dateformat@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
@@ -5441,9 +5490,9 @@ flatted@^3.1.0:
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
version "1.15.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
for-each@^0.3.3:
version "0.3.3"