mirror of
https://github.com/foambubble/foam.git
synced 2026-01-10 22:48:09 -05:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c59584d342 | ||
|
|
ea930d3d17 | ||
|
|
130b839ee9 | ||
|
|
1b4dc8a8b1 | ||
|
|
7fcbf1acf1 | ||
|
|
011706904b | ||
|
|
c14e1e6349 | ||
|
|
97629a2ce6 | ||
|
|
6b3eef43a8 | ||
|
|
d4adce6730 | ||
|
|
ae65f53540 | ||
|
|
8998204298 | ||
|
|
92f6e001e5 | ||
|
|
889f93a7e1 | ||
|
|
4db8c55969 | ||
|
|
7e740fec0f | ||
|
|
beae852c21 | ||
|
|
85e857d973 | ||
|
|
667eee0e10 | ||
|
|
b6e68b3605 | ||
|
|
b9b0f9b515 | ||
|
|
95399977ec | ||
|
|
f759e7cd6e | ||
|
|
5839455535 | ||
|
|
7e4ae82fe1 | ||
|
|
e47155424f | ||
|
|
903a191394 | ||
|
|
5c6212dc96 | ||
|
|
bca9756e2b | ||
|
|
2e435a3828 | ||
|
|
a0f83e7510 | ||
|
|
641024b01b | ||
|
|
c5a7d02656 | ||
|
|
b76196cd23 | ||
|
|
61ae468a70 | ||
|
|
6119278915 | ||
|
|
34c179123e | ||
|
|
c34394a0ea | ||
|
|
26c38a06ff | ||
|
|
d4623a2d91 | ||
|
|
2f9507dc87 | ||
|
|
4db09070a8 | ||
|
|
5f99d9d5c6 | ||
|
|
9c1480197c | ||
|
|
f1a6426046 | ||
|
|
6de8baa6b5 | ||
|
|
27ff023a26 | ||
|
|
690eb10856 | ||
|
|
1cb8174a9f | ||
|
|
2141bac24a | ||
|
|
6abef8f8e7 | ||
|
|
c797a00223 | ||
|
|
7c01fb13f0 | ||
|
|
abbc2bbb14 | ||
|
|
bb7fee24bb | ||
|
|
43ef3a3e2b | ||
|
|
da69a3057f | ||
|
|
2d06f26bbf | ||
|
|
edbe128e1e |
@@ -688,6 +688,33 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pderaaij",
|
||||
"name": "Paul de Raaij",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/495374?v=4",
|
||||
"profile": "http://www.paulderaaij.nl",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bronson",
|
||||
"name": "Scott Bronson",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1776?v=4",
|
||||
"profile": "https://github.com/bronson",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rafo",
|
||||
"name": "Rafael Riedel",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/41793?v=4",
|
||||
"profile": "http://rafaelriedel.de",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -203,14 +203,14 @@ GEM
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
mercenary (0.3.6)
|
||||
mini_portile2 (2.5.0)
|
||||
mini_portile2 (2.5.1)
|
||||
minima (2.5.1)
|
||||
jekyll (>= 3.5, < 5.0)
|
||||
jekyll-feed (~> 0.9)
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
minitest (5.14.2)
|
||||
multipart-post (2.1.1)
|
||||
nokogiri (1.11.1)
|
||||
nokogiri (1.11.5)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
octokit (4.19.0)
|
||||
|
||||
BIN
docs/assets/images/template-picker-annotated.png
Normal file
BIN
docs/assets/images/template-picker-annotated.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
@@ -56,16 +56,12 @@ Tests in `foam-vscode` live alongside the code in `src`.
|
||||
|
||||
This guide assumes you read the previous instructions and you're set up to work on Foam.
|
||||
|
||||
1. Now we'll use the launch configuration defined at [`.vscode/launch.json`](https://github.com/foambubble/foam/blob/master/.vscode/launch.json) to start a new extension host of VS Code. From the root, or the `foam-vscode` workspace, press f5.
|
||||
1. Now we'll use the launch configuration defined at [`.vscode/launch.json`](https://github.com/foambubble/foam/blob/master/.vscode/launch.json) to start a new extension host of VS Code. Open the "Run and Debug" Activity (the icon with the bug on the far left) and select "Run VSCode Extension" in the pop-up menu. Now hit F5 or click the green arrow "play" button to fire up a new copy of VS Code with your extension installed.
|
||||
|
||||
2. In the new extension host of VS Code that launched, open a Foam workspace (e.g. your personal one, or a test-specific one created from [foam-template](https://github.com/foambubble/foam-template)). This is strictly not necessary, but the extension won't auto-run unless it's in a workspace with a `.vscode/foam.json` file.
|
||||
|
||||
3. Test a command to make sure it's working as expected. Open the Command Palette (Ctrl/Cmd + Shift + P) and select "Foam: Update Markdown Reference List". If you see no errors, it's good to go!
|
||||
|
||||
For more resources related to the VS Code Extension, check out the links below:
|
||||
|
||||
- [[tutorial-adding-a-new-command-to-the-vs-code-extension]]
|
||||
|
||||
---
|
||||
|
||||
Feel free to modify and submit a PR if this guide is out-of-date or contains errors!
|
||||
|
||||
@@ -25,9 +25,9 @@ The above configuration would create a file `journal/note-2020-07-25.mdx`, with
|
||||
|
||||
## Daily Note Templates
|
||||
|
||||
In the future, Foam may provide a functionality for specifying a template for new Daily Notes and other types of documents.
|
||||
Daily notes can also make use of [templates](note-templates.md), by defining a special `.foam/templates/daily-note.md` template.
|
||||
|
||||
In the meantime, you can use [VS Code Snippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets) for defining your own Daily Note template.
|
||||
See [Note Templates](note-templates.md) for details of the features available in templates.
|
||||
|
||||
## Roam-style Automatic Daily Notes
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# Foam Local Plugins
|
||||
|
||||
Foam can use workspace plugins to provide customization for users.
|
||||
|
||||
## ATTENTION
|
||||
|
||||
This feature is experimental and its API subject to change.
|
||||
**Local plugins can execute arbitrary code on your machine** - ensure you trust the content of the repo.
|
||||
|
||||
## Goal
|
||||
|
||||
Here are some of the things that we could enable with local plugins in Foam:
|
||||
|
||||
- extend the document syntax to support roam style attributes (e.g. `stage:: seedling`)
|
||||
- automatically add tags to my notes based on the location in the repo (e.g. notes in `/areas/finance` will automatically get the `#finance` tag)
|
||||
- add a new CLI command to support some internal use case or automate import/export
|
||||
- extend the VSCode experience to support one's own workflow, e.g. weekly note, templates, extra panels, foam model derived TOC, ... all without having to write/deploy a VSCode extension
|
||||
|
||||
## How to enable local plugins
|
||||
|
||||
Plugins can execute arbitrary code on the client's machine.
|
||||
For this reason this feature is disabled by default, and needs to be explicitly enabled.
|
||||
|
||||
To enable the feature:
|
||||
|
||||
- create a `~/.foam/config.json` file
|
||||
- add the following content to the file
|
||||
|
||||
```
|
||||
{
|
||||
"experimental": {
|
||||
"localPlugins": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For security reasons this setting can only be defined in the user settings file.
|
||||
(otherwise a malicious repo could set it via its `./foam/config.json`)
|
||||
|
||||
- [[todo]] an additional security mechanism would involve having an explicit list of whitelisted repo paths where plugins are allowed. This would provide finer grain control over when to enable or disable the feature.
|
||||
|
||||
## Technical approach
|
||||
|
||||
When Foam is loaded it will check whether the experimental local plugin feature is enabled, and in such case it will:
|
||||
|
||||
- check `.foam/plugins` directory.
|
||||
- each directory in there is considered a plugin
|
||||
- the layout of each directory is
|
||||
- `index.js` contains the main info about the plugin, specifically it exports:
|
||||
- `name: string` the name of the plugin
|
||||
- `description?: string` the description of the plugin
|
||||
- `parser?: ParserPlugin` an object that interacts with the markdown parsing phase
|
||||
|
||||
Currently for simplicity we keep everything in one file. We might in the future split the plugin by domain (e.g. vscode, cli, core, ...)
|
||||
|
||||
[//begin]: # 'Autogenerated link references for markdown compatibility'
|
||||
[todo]: ../dev/todo.md 'Todo'
|
||||
[//end]: # 'Autogenerated link references'
|
||||
@@ -24,19 +24,135 @@ To create a note from a template:
|
||||
|
||||
_Theme: Ayu Light_
|
||||
|
||||
## Default template
|
||||
## Special templates
|
||||
### Default template
|
||||
|
||||
The `.foam/templates/new-note.md` template is special in that it is the template that will be used by the `Foam: Create New Note` command.
|
||||
Customize this template to contain content that you want included every time you create a note.
|
||||
|
||||
### Default daily note template
|
||||
|
||||
The `.foam/templates/daily-note.md` template is special in that it is the template that will be used when creating daily notes (e.g. by using `Foam: Open Daily Note`).
|
||||
Customize this template to contain content that you want included every time you create a daily note.
|
||||
|
||||
## Variables
|
||||
|
||||
Templates can use all the variables available in [VS Code Snippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables).
|
||||
|
||||
In addition, you can also use variables provided by Foam:
|
||||
|
||||
| Name | Description |
|
||||
| ------------ | ----------------------------------------------------------------------------------- |
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
| Name | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new note. |
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
|
||||
**Note:** neither the defaulting feature (eg. `${variable:default}`) nor the format feature (eg. `${variable/(.*)/${1:/upcase}/}`) (available to other variables) are available for these Foam-provided variables.
|
||||
|
||||
## Metadata
|
||||
|
||||
Templates can also contain metadata about the templates themselves. The metadata is defined in YAML "Frontmatter" blocks within the templates.
|
||||
|
||||
| Name | Description |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `filepath` | The filepath to use when creating the new note. If the filepath is a relative filepath, it is relative to the current workspace. |
|
||||
| `name` | A human readable name to show in the template picker. |
|
||||
| `description` | A human readable description to show in the template picker. |
|
||||
|
||||
Foam-specific variables (e.g. `$FOAM_TITLE`) can be used within template metadata. However, VS Code snippet variables are ([currently](https://github.com/foambubble/foam/pull/655)) not supported.
|
||||
|
||||
### `filepath` attribute
|
||||
|
||||
The `filepath` metadata attribute allows you to define a relative or absolute filepath to use when creating a note using the template.
|
||||
If the filepath is a relative filepath, it is relative to the current workspace.
|
||||
|
||||
**Note:** While you can make use of the `filepath` attribute in [daily note](daily-notes.md) templates (`.foam/templates/daily-note.md`), there is currently no way to have `filepath` vary based on the date. This will be improved in the future. For now, you can customize the location of daily notes using the [`foam.openDailyNote` settings](daily-notes.md).
|
||||
|
||||
#### Example of relative `filepath`
|
||||
|
||||
For example, `filepath` can be used to customize `.foam/templates/new-note.md`, overriding the default `Foam: Create New Note` behaviour of opening the file in the same directory as the active file:
|
||||
|
||||
```yaml
|
||||
---
|
||||
# This will create the note in the "journal" subdirectory of the current workspace,
|
||||
# regardless of which file is the active file.
|
||||
foam_template:
|
||||
filepath: 'journal/$FOAM_TITLE.md'
|
||||
---
|
||||
```
|
||||
|
||||
#### Example of absolute `filepath`
|
||||
|
||||
`filepath` can be an absolute filepath, so that the notes get created in the same location, regardless of which file or workspace the editor currently has open.
|
||||
The format of an absolute filepath may vary depending on the filesystem used.
|
||||
|
||||
```yaml
|
||||
---
|
||||
foam_template:
|
||||
# Unix / MacOS filesystems
|
||||
filepath: '/Users/john.smith/foam/journal/$FOAM_TITLE.md'
|
||||
|
||||
# Windows filesystems
|
||||
filepath: 'C:\Users\john.smith\Documents\foam\journal\$FOAM_TITLE.md'
|
||||
---
|
||||
```
|
||||
|
||||
### `name` and `description` attributes
|
||||
|
||||
These attributes provide a human readable name and description to be shown in the template picker (e.g. When a user uses the `Foam: Create New Note From Template` command):
|
||||
|
||||

|
||||
|
||||
### Adding template metadata to an existing YAML Frontmatter block
|
||||
|
||||
If your template already has a YAML Frontmatter block, you can add the Foam template metadata to it.
|
||||
|
||||
#### Limitations
|
||||
|
||||
Foam only supports adding the template metadata to *YAML* Frontmatter blocks. If the existing Frontmatter block uses some other format (e.g. JSON), you will have to add the template metadata to its own YAML Frontmatter block.
|
||||
|
||||
Further, the template metadata must be provided as a [YAML block mapping](https://yaml.org/spec/1.2/spec.html#id2798057), with the attributes placed on the lines immediately following the `foam_template` line:
|
||||
|
||||
```yaml
|
||||
---
|
||||
existing_frontmatter: "Existing Frontmatter block"
|
||||
foam_template: # this is a YAML "Block" mapping ("Flow" mappings aren't supported)
|
||||
name: My Note Template # Attributes must be on the lines immediately following `foam_template`
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
Due to the technical limitations of parsing the complex YAML format, unless the metadata is provided this specific form, Foam is unable to correctly remove the template metadata before creating the resulting note.
|
||||
|
||||
If this limitation proves inconvenient to you, please let us know. We may be able to extend our parsing capabilities to cover your use case. In the meantime, you can add the template metadata without this limitation by providing it in its own YAML Frontmatter block.
|
||||
|
||||
### Adding template metadata to its own YAML Frontmatter block
|
||||
|
||||
You can add the template metadata to its own YAML Frontmatter block at the start of the template:
|
||||
|
||||
```yaml
|
||||
---
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
If the note already has a Frontmatter block, a Foam-specific Frontmatter block can be added to the start of the template. The Foam-specific Frontmatter block must always be placed at the very beginning of the file, and only whitespace can separate the two Frontmatter blocks.
|
||||
|
||||
```yaml
|
||||
---
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: `journal/$FOAM_TITLE.md`
|
||||
---
|
||||
|
||||
---
|
||||
existing_frontmatter: "Existing Frontmatter block"
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
@@ -9,6 +9,8 @@ There are two ways of creating a tag:
|
||||
- adding a `#tag` anywhere in the text of the note
|
||||
- using the `tags: tag1, tag2` property in frontmatter
|
||||
|
||||
Tags can also be hierarchical, so you can have `#parent/child`.
|
||||
|
||||
## Navigating tags
|
||||
|
||||
It's possible to navigate tags via the Tag Explorer panel.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||
- [Links/Graphs/BackLinks don't work. How do I enable them?](#linksgraphsbacklinks-dont-work-how-do-i-enable-them)
|
||||
- [I don't want Foam enabled for all my workspaces](#i-dont-want-foam-enabled-for-all-my-workspaces)
|
||||
|
||||
## Links/Graphs/BackLinks don't work. How do I enable them?
|
||||
|
||||
@@ -11,6 +12,9 @@
|
||||
- Reload Visual Studio Code by running `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "reload" and run the **Developer: Reload Window** command to for the updated extensions take effect
|
||||
- Check the formatting rules for links on [[foam-file-format]], [[wiki-links]] and [[link-formatting-and-autocompletion]]
|
||||
|
||||
## I don't want Foam enabled for all my workspaces
|
||||
Any extension you install in Visual Studio Code is enabled by default. Give the philosophy of Foam it works out of the box without doing any configuration upfront. In case you want to disable Foam for a specific workspace, or disable Foam by default and enable it for specific workspaces, it is advised to follow the best practices as [documented by Visual Studio Code](https://code.visualstudio.com/docs/editor/extension-marketplace#_manage-extensions)
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
|
||||
[foam-file-format]: dev/foam-file-format.md "Foam File Format"
|
||||
|
||||
@@ -201,6 +201,9 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<td align="center"><a href="https://github.com/daniel-vera-g"><img src="https://avatars.githubusercontent.com/u/28257108?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Daniel VG</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=daniel-vera-g" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Barabazs"><img src="https://avatars.githubusercontent.com/u/31799121?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Barabas</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Barabazs" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://enginveske@gmail.com"><img src="https://avatars.githubusercontent.com/u/43685404?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Engincan VESKE</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=EngincanV" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.paulderaaij.nl"><img src="https://avatars.githubusercontent.com/u/495374?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul de Raaij</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=pderaaij" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/bronson"><img src="https://avatars.githubusercontent.com/u/1776?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Scott Bronson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bronson" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://rafaelriedel.de"><img src="https://avatars.githubusercontent.com/u/41793?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Rafael Riedel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=rafo" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ This principle may seem like it contradicts [Foam wants you to own your thoughts
|
||||
|
||||
- **Foam is a collection of ideas.** Foam was released to the public not to share the few good ideas in it, but to learn many good ideas from others. As you improve your own workflow, share your work on your own Foam blog.
|
||||
- **Foam is open for contributions.** If you use a tool or workflow that you like that fits these principles, please contribute them back to the Foam template as [[recipes]], [[recommended-extensions]] or documentation in [this workspace](https://github.com/foambubble/foam). See also: [[contribution-guide]].
|
||||
- **Foam is open source.** Feel free to fork it, improve it and remix it. Just don't sell it, as per our [license](license).
|
||||
- **Foam is open source.** Feel free to fork it, improve it and remix it. Just don't sell it, as per our [license](LICENSE.txt).
|
||||
- **Foam is not Roam.** This project was inspired by Roam Research, but we're not limited by what Roam does. No idea is too big (though if it doesn't fit with Foam's core workflow, we might make it a [[recipes]] page instead).
|
||||
|
||||
## Foam is for hackers, not only for programmers
|
||||
|
||||
@@ -15,7 +15,7 @@ These extensions are not (yet?) defined in `.vscode/extensions.json`, but have b
|
||||
|
||||
- [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)
|
||||
- [Mermaid Support for Preview](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)
|
||||
- [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)
|
||||
- [Mermaid Markdown Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=bpruitt-goddard.mermaid-markdown-syntax-highlighting)
|
||||
- [VSCode PDF Viewing](https://marketplace.visualstudio.com/items?itemName=tomoki1207.pdf)
|
||||
- [Git Lens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.13.3"
|
||||
"version": "0.14.1"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "foam-core",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.13.3",
|
||||
"version": "0.14.1",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -33,11 +33,12 @@
|
||||
"fast-array-diff": "^1.0.0",
|
||||
"github-slugger": "^1.3.0",
|
||||
"glob": "^7.1.6",
|
||||
"lodash": "^4.17.19",
|
||||
"lodash": "^4.17.21",
|
||||
"micromatch": "^4.0.2",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
"remark-parse": "^8.0.2",
|
||||
"remark-wiki-link": "^0.0.4",
|
||||
"replace-ext": "^2.0.0",
|
||||
"title-case": "^3.0.2",
|
||||
"unified": "^9.0.0",
|
||||
"unist-util-visit": "^2.0.2",
|
||||
|
||||
@@ -47,7 +47,7 @@ export { applyTextEdit } from './janitor/apply-text-edit';
|
||||
|
||||
export { createConfigFromFolders } from './config';
|
||||
|
||||
export { bootstrap } from './bootstrap';
|
||||
export { Foam, Services, bootstrap } from './model/foam';
|
||||
|
||||
export {
|
||||
Resource,
|
||||
@@ -58,16 +58,3 @@ export {
|
||||
NoteLinkDefinition,
|
||||
ResourceParser,
|
||||
};
|
||||
|
||||
export interface Services {
|
||||
dataStore: IDataStore;
|
||||
parser: ResourceParser;
|
||||
matcher: IMatcher;
|
||||
}
|
||||
|
||||
export interface Foam extends IDisposable {
|
||||
services: Services;
|
||||
workspace: FoamWorkspace;
|
||||
graph: FoamGraph;
|
||||
config: FoamConfig;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
isNone,
|
||||
isSome,
|
||||
} from './utils';
|
||||
import { ParserPlugin } from './plugins';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
@@ -32,6 +31,18 @@ import { ResourceProvider } from 'model/provider';
|
||||
import { IDataStore, FileDataStore, IMatcher } from './services/datastore';
|
||||
import { IDisposable } from 'common/lifecycle';
|
||||
|
||||
const ALIAS_DIVIDER_CHAR = '|';
|
||||
|
||||
export interface ParserPlugin {
|
||||
name?: string;
|
||||
visit?: (node: Node, note: Resource, noteSource: string) => void;
|
||||
onDidInitializeParser?: (parser: unified.Processor) => void;
|
||||
onWillParseMarkdown?: (markdown: string) => string;
|
||||
onWillVisitTree?: (tree: Node, note: Resource) => void;
|
||||
onDidVisitTree?: (tree: Node, note: Resource) => void;
|
||||
onDidFindProperties?: (properties: any, note: Resource) => void;
|
||||
}
|
||||
|
||||
export class MarkdownResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
@@ -112,7 +123,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
switch (link.type) {
|
||||
case 'wikilink':
|
||||
const definitionUri = resource.definitions.find(
|
||||
def => def.label === link.slug
|
||||
def => def.label === link.target
|
||||
)?.url;
|
||||
if (isSome(definitionUri)) {
|
||||
const definedUri = URI.resolve(definitionUri, resource.uri);
|
||||
@@ -121,8 +132,8 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
URI.placeholder(definedUri.path);
|
||||
} else {
|
||||
targetUri =
|
||||
workspace.find(link.slug, resource.uri)?.uri ??
|
||||
URI.placeholder(link.slug);
|
||||
workspace.find(link.target, resource.uri)?.uri ??
|
||||
URI.placeholder(link.target);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -177,7 +188,7 @@ const titlePlugin: ParserPlugin = {
|
||||
},
|
||||
onDidFindProperties: (props, note) => {
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
note.title = props.title ?? note.title;
|
||||
note.title = props.title?.toString() ?? note.title;
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
if (note.title === '') {
|
||||
@@ -188,12 +199,29 @@ const titlePlugin: ParserPlugin = {
|
||||
|
||||
const wikilinkPlugin: ParserPlugin = {
|
||||
name: 'wikilink',
|
||||
visit: (node, note) => {
|
||||
visit: (node, note, noteSource) => {
|
||||
if (node.type === 'wikiLink') {
|
||||
const text = node.value as string;
|
||||
const alias = node.data?.alias as string;
|
||||
const literalContent = noteSource.substring(
|
||||
node.position!.start.offset!,
|
||||
node.position!.end.offset!
|
||||
);
|
||||
|
||||
const hasAlias =
|
||||
literalContent !== text && literalContent.includes(ALIAS_DIVIDER_CHAR);
|
||||
note.links.push({
|
||||
type: 'wikilink',
|
||||
slug: node.value as string,
|
||||
target: node.value as string,
|
||||
rawText: literalContent,
|
||||
label: hasAlias
|
||||
? alias.trim()
|
||||
: literalContent.substring(2, literalContent.length - 2),
|
||||
target: hasAlias
|
||||
? literalContent
|
||||
.substring(2, literalContent.indexOf(ALIAS_DIVIDER_CHAR))
|
||||
.replace(/\\/g, '')
|
||||
.trim()
|
||||
: text.trim(),
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
@@ -250,7 +278,7 @@ export function createMarkdownParser(
|
||||
const parser = unified()
|
||||
.use(markdownParse, { gfm: true })
|
||||
.use(frontmatterPlugin, ['yaml'])
|
||||
.use(wikiLinkPlugin);
|
||||
.use(wikiLinkPlugin, { aliasDivider: ALIAS_DIVIDER_CHAR });
|
||||
|
||||
const plugins = [
|
||||
titlePlugin,
|
||||
@@ -313,8 +341,6 @@ export function createMarkdownParser(
|
||||
...note.properties,
|
||||
...yamlProperties,
|
||||
};
|
||||
// Give precendence to the title from the frontmatter if it exists
|
||||
note.title = note.properties.title ?? note.title;
|
||||
// Update the start position of the note by exluding the metadata
|
||||
note.source.contentStart = Position.create(
|
||||
node.position!.end.line! + 2,
|
||||
@@ -335,7 +361,7 @@ export function createMarkdownParser(
|
||||
|
||||
for (let i = 0, len = plugins.length; i < len; i++) {
|
||||
try {
|
||||
plugins[i].visit?.(node, note);
|
||||
plugins[i].visit?.(node, note, markdown);
|
||||
} catch (e) {
|
||||
handleError(plugins[i], 'visit', uri, e);
|
||||
}
|
||||
@@ -424,7 +450,14 @@ export function createMarkdownReferences(
|
||||
: dropExtension(relativePath);
|
||||
|
||||
// [wiki-link-text]: path/to/file.md "Page title"
|
||||
return { label: link.slug, url: pathToNote, title: target.title };
|
||||
return {
|
||||
label:
|
||||
link.rawText.indexOf('[[') > -1
|
||||
? link.rawText.substring(2, link.rawText.length - 2)
|
||||
: link.rawText || link.label,
|
||||
url: pathToNote,
|
||||
title: target.title,
|
||||
};
|
||||
})
|
||||
.filter(isSome)
|
||||
.sort();
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import { createMarkdownParser } from './markdown-provider';
|
||||
import { FoamConfig, Foam, IDataStore, FoamGraph } from './index';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import { Matcher } from './services/datastore';
|
||||
import { ResourceProvider } from 'model/provider';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { IDataStore, IMatcher, Matcher } from '../services/datastore';
|
||||
import { FoamConfig } from '../config';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { FoamGraph } from './graph';
|
||||
import { ResourceParser } from './note';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { createMarkdownParser } from '../markdown-provider';
|
||||
|
||||
export interface Services {
|
||||
dataStore: IDataStore;
|
||||
parser: ResourceParser;
|
||||
matcher: IMatcher;
|
||||
}
|
||||
|
||||
export interface Foam extends IDisposable {
|
||||
services: Services;
|
||||
workspace: FoamWorkspace;
|
||||
graph: FoamGraph;
|
||||
config: FoamConfig;
|
||||
}
|
||||
|
||||
export const bootstrap = async (
|
||||
config: FoamConfig,
|
||||
@@ -11,8 +11,9 @@ export interface NoteSource {
|
||||
|
||||
export interface WikiLink {
|
||||
type: 'wikilink';
|
||||
slug: string;
|
||||
target: string;
|
||||
label: string;
|
||||
rawText: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// Some code in this file coming from https://github.com/microsoft/vscode/
|
||||
// `URI` is mostly compatible with VSCode's `Uri`.
|
||||
// Having a Foam-specific URI object allows for easier maintenance of the API.
|
||||
// See https://github.com/foambubble/foam/pull/537 for more context.
|
||||
// Some code in this file comes from https://github.com/microsoft/vscode/main/src/vs/base/common/uri.ts
|
||||
// See LICENSE for details
|
||||
|
||||
import * as paths from 'path';
|
||||
@@ -148,6 +151,10 @@ export abstract class URI {
|
||||
return URI.file(posix.dirname(uri.path));
|
||||
}
|
||||
|
||||
static getFileNameWithoutExtension(uri: URI) {
|
||||
return URI.getBasename(uri).replace(/\.[^.]+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a placeholder URI, and a reference directory, to generate
|
||||
* the URI of the corresponding resource
|
||||
|
||||
@@ -26,7 +26,8 @@ const pathToResourceId = (pathValue: string) => {
|
||||
};
|
||||
const uriToResourceId = (uri: URI) => pathToResourceId(uri.path);
|
||||
|
||||
const pathToResourceName = (pathValue: string) => path.parse(pathValue).name;
|
||||
const pathToResourceName = (pathValue: string) =>
|
||||
path.parse(pathValue).name.toLowerCase();
|
||||
export const uriToResourceName = (uri: URI) => pathToResourceName(uri.path);
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
@@ -110,7 +111,12 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
case 'key':
|
||||
const name = pathToResourceName(resourceId as string);
|
||||
const paths = this.resourcesByName[name];
|
||||
let paths = this.resourcesByName[name];
|
||||
|
||||
if (isNone(paths) || paths.length === 0) {
|
||||
paths = this.resourcesByName[resourceId as string];
|
||||
}
|
||||
|
||||
if (isNone(paths) || paths.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -118,6 +124,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
const sortedPaths = paths.length === 1
|
||||
? paths
|
||||
: paths.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return this.resources[sortedPaths[0]];
|
||||
|
||||
case 'absolute-path':
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Node } from 'unist';
|
||||
import { isNotNull } from '../utils';
|
||||
import { Resource } from '../model/note';
|
||||
import unified from 'unified';
|
||||
import { FoamConfig } from '../config';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
export interface FoamPlugin {
|
||||
name: string;
|
||||
description?: string;
|
||||
parser?: ParserPlugin;
|
||||
}
|
||||
|
||||
export interface ParserPlugin {
|
||||
name?: string;
|
||||
visit?: (node: Node, note: Resource) => void;
|
||||
onDidInitializeParser?: (parser: unified.Processor) => void;
|
||||
onWillParseMarkdown?: (markdown: string) => string;
|
||||
onWillVisitTree?: (tree: Node, note: Resource) => void;
|
||||
onDidVisitTree?: (tree: Node, note: Resource) => void;
|
||||
onDidFindProperties?: (properties: any, note: Resource) => void;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
enabled?: boolean;
|
||||
pluginFolders?: string[];
|
||||
}
|
||||
|
||||
export const SETTINGS_PATH = 'experimental.localPlugins';
|
||||
|
||||
export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
|
||||
const pluginConfig = config.get<PluginConfig>(SETTINGS_PATH, {});
|
||||
const isFeatureEnabled = pluginConfig.enabled ?? false;
|
||||
if (!isFeatureEnabled) {
|
||||
return [];
|
||||
}
|
||||
const pluginDirs: URI[] =
|
||||
pluginConfig.pluginFolders?.map(URI.file) ??
|
||||
findPluginDirs(config.workspaceFolders);
|
||||
|
||||
const plugins = await Promise.all(
|
||||
pluginDirs
|
||||
.filter(dir => fs.statSync(URI.toFsPath(dir)).isDirectory)
|
||||
.map(async dir => {
|
||||
try {
|
||||
const pluginFile = path.join(URI.toFsPath(dir), 'index.js');
|
||||
fs.accessSync(pluginFile);
|
||||
Logger.info(`Found plugin at [${pluginFile}]. Loading..`);
|
||||
const plugin = validate(await import(pluginFile));
|
||||
return plugin;
|
||||
} catch (e) {
|
||||
Logger.error(`Error while loading plugin at [${dir}] - skipping`, e);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
return plugins.filter(isNotNull);
|
||||
}
|
||||
|
||||
function findPluginDirs(workspaceFolders: URI[]) {
|
||||
return workspaceFolders
|
||||
.map(root => URI.joinPath(root, '.foam', 'plugins'))
|
||||
.reduce((acc, pluginDir) => {
|
||||
try {
|
||||
const content = fs
|
||||
.readdirSync(URI.toFsPath(pluginDir))
|
||||
.map(dir => URI.joinPath(pluginDir, dir));
|
||||
return [
|
||||
...acc,
|
||||
...content.filter(c => fs.statSync(URI.toFsPath(c)).isDirectory()),
|
||||
];
|
||||
} catch {
|
||||
return acc;
|
||||
}
|
||||
}, [] as URI[]);
|
||||
}
|
||||
|
||||
function validate(plugin: any): FoamPlugin {
|
||||
if (!plugin.name) {
|
||||
throw new Error('Plugin must export `name` string property');
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isSome } from './core';
|
||||
const HASHTAG_REGEX = /(^|\s)#([0-9]*[\p{L}_-][\p{L}\p{N}_-]*)/gmu;
|
||||
const WORD_REGEX = /(^|\s)([0-9]*[\p{L}_-][\p{L}\p{N}_-]*)/gmu;
|
||||
const HASHTAG_REGEX = /(^|\s)#([0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/gmu;
|
||||
const WORD_REGEX = /(^|\s)([0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/gmu;
|
||||
|
||||
export const extractHashtags = (text: string): Set<string> => {
|
||||
return isSome(text)
|
||||
|
||||
@@ -67,10 +67,10 @@ export const createTestNote = (params: {
|
||||
return 'slug' in link
|
||||
? {
|
||||
type: 'wikilink',
|
||||
slug: link.slug,
|
||||
target: link.slug,
|
||||
label: link.slug,
|
||||
range: range,
|
||||
text: 'link text',
|
||||
rawText: 'link text',
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as path from 'path';
|
||||
import { generateHeading } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Resource } from '../../src/model/note';
|
||||
import { FileDataStore, Matcher } from '../../src/services/datastore';
|
||||
@@ -8,7 +7,7 @@ import { Logger } from '../../src/utils/log';
|
||||
import { FoamWorkspace } from '../../src/model/workspace';
|
||||
import { URI } from '../../src/model/uri';
|
||||
import { Range } from '../../src/model/range';
|
||||
import { MarkdownResourceProvider } from '../../src';
|
||||
import { MarkdownResourceProvider, bootstrap } from '../../src';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as path from 'path';
|
||||
import { generateLinkReferences } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { FileDataStore, Matcher } from '../../src/services/datastore';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
@@ -8,7 +7,7 @@ import { FoamWorkspace } from '../../src/model/workspace';
|
||||
import { URI } from '../../src/model/uri';
|
||||
import { Resource } from '../../src/model/note';
|
||||
import { Range } from '../../src/model/range';
|
||||
import { MarkdownResourceProvider } from '../../src';
|
||||
import { MarkdownResourceProvider, bootstrap } from '../../src';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
createMarkdownParser,
|
||||
createMarkdownReferences,
|
||||
ParserPlugin,
|
||||
} from '../src/markdown-provider';
|
||||
import { DirectLink } from '../src/model/note';
|
||||
import { ParserPlugin } from '../src/plugins';
|
||||
import { DirectLink, WikiLink } from '../src/model/note';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { uriToSlug } from '../src/utils/slug';
|
||||
import { URI } from '../src/model/uri';
|
||||
@@ -130,6 +130,24 @@ this is a [link to intro](#introduction)
|
||||
noteE.uri,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Parses backlinks with an alias', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is [[link|link alias]]. A link with spaces [[other link | spaced]]'
|
||||
);
|
||||
expect(note.links.length).toEqual(2);
|
||||
let link = note.links[0] as WikiLink;
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[link|link alias]]');
|
||||
expect(link.label).toEqual('link alias');
|
||||
expect(link.target).toEqual('link');
|
||||
link = note.links[1] as WikiLink;
|
||||
expect(link.type).toEqual('wikilink');
|
||||
expect(link.rawText).toEqual('[[other link | spaced]]');
|
||||
expect(link.label).toEqual('spaced');
|
||||
expect(link.target).toEqual('other link');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Note Title', () => {
|
||||
@@ -171,6 +189,26 @@ date: 20-12-12
|
||||
expect(note.title).toBe('Note Title');
|
||||
});
|
||||
|
||||
it('should support numbers', () => {
|
||||
const note1 = createNoteFromMarkdown('/157.md', `hello`);
|
||||
expect(note1.title).toBe('157');
|
||||
|
||||
const note2 = createNoteFromMarkdown('/157.md', `# 158`);
|
||||
expect(note2.title).toBe('158');
|
||||
|
||||
const note3 = createNoteFromMarkdown(
|
||||
'/157.md',
|
||||
`
|
||||
---
|
||||
title: 159
|
||||
---
|
||||
|
||||
# 158
|
||||
`
|
||||
);
|
||||
expect(note3.title).toBe('159');
|
||||
});
|
||||
|
||||
it('should not break on empty titles (see #276)', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/Hello Page.md',
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import path from 'path';
|
||||
import { loadPlugins } from '../src/plugins';
|
||||
import { createMarkdownParser } from '../src/markdown-provider';
|
||||
import { FoamConfig, createConfigFromObject } from '../src/config';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { Logger } from '../src/utils/log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const config: FoamConfig = createConfigFromObject([], [], [], {
|
||||
experimental: {
|
||||
localPlugins: {
|
||||
enabled: true,
|
||||
pluginFolders: [path.join(__dirname, 'test-plugin')],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('Foam plugins', () => {
|
||||
it('will not load if feature is not explicitly enabled', async () => {
|
||||
let plugins = await loadPlugins(createConfigFromObject([], [], [], {}));
|
||||
expect(plugins.length).toEqual(0);
|
||||
plugins = await loadPlugins(
|
||||
createConfigFromObject([], [], [], {
|
||||
experimental: {
|
||||
localPlugins: {},
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(plugins.length).toEqual(0);
|
||||
plugins = await loadPlugins(
|
||||
createConfigFromObject([], [], [], {
|
||||
experimental: {
|
||||
localPlugins: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(plugins.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('can load', async () => {
|
||||
const plugins = await loadPlugins(config);
|
||||
expect(plugins.length).toEqual(1);
|
||||
expect(plugins[0].name).toEqual('Test Plugin');
|
||||
});
|
||||
|
||||
it('supports parser extension', async () => {
|
||||
const plugins = await loadPlugins(config);
|
||||
const parserPlugin = plugins[0].parser;
|
||||
expect(parserPlugin).not.toBeUndefined();
|
||||
const parser = createMarkdownParser([parserPlugin!]);
|
||||
|
||||
const note = parser.parse(
|
||||
URI.file('/path/to/a'),
|
||||
`
|
||||
# This is a note with header
|
||||
and some content`
|
||||
);
|
||||
expect(note.properties.hasHeading).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
const middleware = next => ({
|
||||
setNote: note => {
|
||||
note.properties['injectedByMiddleware'] = true;
|
||||
return next.setNote(note);
|
||||
},
|
||||
});
|
||||
|
||||
const parser = {
|
||||
visit: (node, note) => {
|
||||
if (node.type === 'heading') {
|
||||
note.properties.hasHeading = true;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
name: 'Test Plugin',
|
||||
graphMiddleware: middleware,
|
||||
parser: parser,
|
||||
};
|
||||
@@ -19,6 +19,11 @@ describe('hashtag extraction', () => {
|
||||
new Set(['hello-world', 'this_planet'])
|
||||
);
|
||||
});
|
||||
it('supports nested tags', () => {
|
||||
expect(extractHashtags('#parent/child on #planet')).toEqual(
|
||||
new Set(['parent/child', 'planet'])
|
||||
);
|
||||
});
|
||||
it('ignores tags that only have numbers in text', () => {
|
||||
expect(
|
||||
extractHashtags('this #123 tag should be ignore, but not #123four')
|
||||
|
||||
@@ -232,7 +232,7 @@ describe('Wikilinks', () => {
|
||||
expect(graph.getAllConnections()[0]).toEqual({
|
||||
source: noteA.uri,
|
||||
target: noteB.uri,
|
||||
link: expect.objectContaining({ type: 'wikilink', slug: 'page-b' }),
|
||||
link: expect.objectContaining({ type: 'wikilink', label: 'page-b' }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -354,6 +354,51 @@ describe('Wikilinks', () => {
|
||||
attachmentABis.uri,
|
||||
]);
|
||||
});
|
||||
|
||||
it('Allows for dendron-style wikilinks, including a dot', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [{ slug: 'dendron.style' }],
|
||||
});
|
||||
const noteB1 = createTestNote({ uri: '/path/to/another/dendron.style.md' });
|
||||
|
||||
const ws = createTestWorkspace();
|
||||
ws.set(noteA).set(noteB1);
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(graph.getLinks(noteA.uri).map(l => l.target)).toEqual([noteB1.uri]);
|
||||
});
|
||||
|
||||
it('Handles capatalization of files and wiki links correctly', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [
|
||||
// uppercased filename, lowercased slug
|
||||
{ slug: 'page-b' },
|
||||
// lowercased filename, camelcased wikilink
|
||||
{ slug: 'Page-C' },
|
||||
// lowercased filename, lowercased wikilink
|
||||
{ slug: 'page-d' },
|
||||
],
|
||||
});
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(createTestNote({ uri: '/somewhere/PAGE-B.md' }))
|
||||
.set(createTestNote({ uri: '/path/another/page-c.md' }))
|
||||
.set(createTestNote({ uri: '/path/another/page-d.md' }));
|
||||
const graph = FoamGraph.fromWorkspace(ws);
|
||||
|
||||
expect(
|
||||
graph
|
||||
.getLinks(noteA.uri)
|
||||
.map(link => link.target.path)
|
||||
.sort()
|
||||
).toEqual([
|
||||
'/path/another/page-c.md',
|
||||
'/path/another/page-d.md',
|
||||
'/somewhere/PAGE-B.md',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markdown direct links', () => {
|
||||
|
||||
@@ -3741,9 +3741,9 @@ github-slugger@^1.3.0:
|
||||
emoji-regex ">=6.0.0 <=6.1.1"
|
||||
|
||||
glob-parent@^5.0.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
|
||||
integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
@@ -3870,9 +3870,9 @@ has@^1.0.3:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
html-encoding-sniffer@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -4944,10 +4944,10 @@ lodash.sortby@^4.7.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
|
||||
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4:
|
||||
version "4.17.19"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
log-symbols@^2.2.0:
|
||||
version "2.2.0"
|
||||
@@ -5924,6 +5924,11 @@ replace-ext@1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
|
||||
integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
|
||||
|
||||
replace-ext@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06"
|
||||
integrity sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==
|
||||
|
||||
request-promise-core@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
|
||||
@@ -7220,9 +7225,9 @@ write@1.0.3:
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
ws@^5.2.0:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
|
||||
integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d"
|
||||
integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
|
||||
|
||||
@@ -4,6 +4,73 @@ 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.14.1] - 2021-07-14
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed NPE that would cause markdown preview to render incorrectly (#718 - thanks @pderaaij)
|
||||
|
||||
## [0.14.0] - 2021-07-13
|
||||
|
||||
Features:
|
||||
|
||||
- Create new note from selection (#666 - thanks @pderaaij)
|
||||
- Use templates for daily notes (#700 - thanks @movermeyer)
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed for wikilink aliases in tables (#697 - thanks @pderaaij)
|
||||
- Fixed link definition generation in presence of aliased wikilinks (#698 - thanks @pderaaij)
|
||||
- Fixed template insertion of selected text (#701 - thanks @movermeyer)
|
||||
- Fixed preview navigation (#710 - thanks @pderaaij)
|
||||
|
||||
## [0.13.8] - 2021-07-02
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved handling of capitalization in wikilinks (#688 - thanks @pderaaij)
|
||||
- This update will make wikilinks with different capitalization, such as `[[wikilink]]` and `[[WikiLink]]` point to the same file. Please note that means that files that only differ in capitalization across the workspace would now be treated as having the same name
|
||||
- Allow dots in wikilinks (#689 - thanks @pderaaij)
|
||||
- Fixed a bug in the expansion of date snippets (thanks @syndenham-chorea)
|
||||
- Added support for wikilink alias syntax, like `[[wikilink|label]]` (#689 - thanks @pderaaij)
|
||||
|
||||
## [0.13.7] - 2021-06-05
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed #667, incorrect resolution of foam-core library
|
||||
|
||||
Internal:
|
||||
|
||||
- BREAKING CHANGE: Removed Foam local plugins
|
||||
If you were previously using the alpha feature of Foam local plugins you will soon be able to migrate the functionality to the V1 API
|
||||
|
||||
## [0.13.6] - 2021-06-05
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed #667, incorrect resolution of foam-core library
|
||||
|
||||
## [0.13.5] - 2021-06-05
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved support for nested tags (#661 - thanks @pderaaij)
|
||||
- Allow YAML metadata in templates (#655 - thanks @movermeyer)
|
||||
- Fixed template exclusion globs (#665)
|
||||
|
||||
## [0.13.4] - 2021-05-26
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Added support for nested tags (#643 - thanks @pderaaij)
|
||||
- Improved the flow of creating note from template (#645 - thanks @movermeyer)
|
||||
- Fixed handling of title property in YAML (#647 - thanks @pderaaij and #546)
|
||||
|
||||
Internal:
|
||||
|
||||
- Updated various dependencies
|
||||
|
||||
## [0.13.3] - 2021-05-09
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.13.3",
|
||||
"version": "0.14.1",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -395,7 +395,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dateformat": "^3.0.3",
|
||||
"foam-core": "^0.13.3",
|
||||
"foam-core": "^0.14.1",
|
||||
"gray-matter": "^4.0.2",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"micromatch": "^4.0.2",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Uri, workspace } from 'vscode';
|
||||
import { workspace } from 'vscode';
|
||||
import { getDailyNotePath } from './dated-notes';
|
||||
import { URI } from 'foam-core';
|
||||
import { isWindows } from './utils';
|
||||
@@ -13,7 +13,7 @@ describe('getDailyNotePath', () => {
|
||||
test('Adds the root directory to relative directories', async () => {
|
||||
const config = 'journal';
|
||||
|
||||
const expectedPath = Uri.joinPath(
|
||||
const expectedPath = URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
config,
|
||||
`${isoDate}.md`
|
||||
@@ -25,7 +25,7 @@ describe('getDailyNotePath', () => {
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
|
||||
expect(URI.toFsPath(getDailyNotePath(foamConfiguration, date))).toEqual(
|
||||
expectedPath.fsPath
|
||||
URI.toFsPath(expectedPath)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { workspace, WorkspaceConfiguration, Uri } from 'vscode';
|
||||
import { workspace, WorkspaceConfiguration } from 'vscode';
|
||||
import dateFormat from 'dateformat';
|
||||
import * as fs from 'fs';
|
||||
import { isAbsolute } from 'path';
|
||||
import { docConfig, focusNote, pathExists } from './utils';
|
||||
import { focusNote, pathExists } from './utils';
|
||||
import { URI } from 'foam-core';
|
||||
import { createNoteFromDailyNoteTemplate } from './features/create-from-template';
|
||||
|
||||
/**
|
||||
* Open the daily note file.
|
||||
*
|
||||
* In the case that the daily note file does not exist,
|
||||
* it gets created along with any folders in its path.
|
||||
*
|
||||
* @param date A given date to be formatted as filename.
|
||||
*/
|
||||
async function openDailyNoteFor(date?: Date) {
|
||||
const foamConfiguration = workspace.getConfiguration('foam');
|
||||
const currentDate = date !== undefined ? date : new Date();
|
||||
@@ -19,6 +27,19 @@ async function openDailyNoteFor(date?: Date) {
|
||||
await focusNote(dailyNotePath, isNew);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the daily note file path.
|
||||
*
|
||||
* This function first checks the `foam.openDailyNote.directory` configuration string,
|
||||
* defaulting to the current directory.
|
||||
*
|
||||
* In the case that the directory path is not absolute,
|
||||
* the resulting path will start on the current workspace top-level.
|
||||
*
|
||||
* @param configuration The current workspace configuration.
|
||||
* @param date A given date to be formatted as filename.
|
||||
* @returns The path to the daily note file.
|
||||
*/
|
||||
function getDailyNotePath(
|
||||
configuration: WorkspaceConfiguration,
|
||||
date: Date
|
||||
@@ -28,7 +49,7 @@ function getDailyNotePath(
|
||||
const dailyNoteFilename = getDailyNoteFileName(configuration, date);
|
||||
|
||||
if (isAbsolute(dailyNoteDirectory)) {
|
||||
return URI.joinPath(Uri.file(dailyNoteDirectory), dailyNoteFilename);
|
||||
return URI.joinPath(URI.file(dailyNoteDirectory), dailyNoteFilename);
|
||||
} else {
|
||||
return URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
@@ -38,6 +59,17 @@ function getDailyNotePath(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the daily note filename (basename) to use.
|
||||
*
|
||||
* Fetch the filename format and extension from
|
||||
* `foam.openDailyNote.filenameFormat` and
|
||||
* `foam.openDailyNote.fileExtension`, respectively.
|
||||
*
|
||||
* @param configuration The current workspace configuration.
|
||||
* @param date A given date to be formatted as filename.
|
||||
* @returns The daily note's filename.
|
||||
*/
|
||||
function getDailyNoteFileName(
|
||||
configuration: WorkspaceConfiguration,
|
||||
date: Date
|
||||
@@ -52,6 +84,17 @@ function getDailyNoteFileName(
|
||||
return `${dateFormat(date, filenameFormat, false)}.${fileExtension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a daily note if it does not exist.
|
||||
*
|
||||
* In the case that the folders referenced in the file path also do not exist,
|
||||
* this function will create all folders in the path.
|
||||
*
|
||||
* @param configuration The current workspace configuration.
|
||||
* @param dailyNotePath The path to daily note file.
|
||||
* @param currentDate The current date, to be used as a title.
|
||||
* @returns Wether the file was created.
|
||||
*/
|
||||
async function createDailyNoteIfNotExists(
|
||||
configuration: WorkspaceConfiguration,
|
||||
dailyNotePath: URI,
|
||||
@@ -61,32 +104,23 @@ async function createDailyNoteIfNotExists(
|
||||
return false;
|
||||
}
|
||||
|
||||
await createDailyNoteDirectoryIfNotExists(dailyNotePath);
|
||||
|
||||
const titleFormat: string =
|
||||
configuration.get('openDailyNote.titleFormat') ??
|
||||
configuration.get('openDailyNote.filenameFormat');
|
||||
|
||||
await fs.promises.writeFile(
|
||||
URI.toFsPath(dailyNotePath),
|
||||
`# ${dateFormat(currentDate, titleFormat, false)}${docConfig.eol}${
|
||||
docConfig.eol
|
||||
}`
|
||||
);
|
||||
const templateFallbackText: string = `---
|
||||
foam_template:
|
||||
name: New Daily Note
|
||||
description: Foam's default daily note template
|
||||
---
|
||||
# ${dateFormat(currentDate, titleFormat, false)}
|
||||
`;
|
||||
|
||||
await createNoteFromDailyNoteTemplate(dailyNotePath, templateFallbackText);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function createDailyNoteDirectoryIfNotExists(dailyNotePath: URI) {
|
||||
const dailyNoteDirectory = URI.getDir(dailyNotePath);
|
||||
|
||||
if (!(await pathExists(dailyNoteDirectory))) {
|
||||
await fs.promises.mkdir(URI.toFsPath(dailyNoteDirectory), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
openDailyNoteFor,
|
||||
getDailyNoteFileName,
|
||||
|
||||
@@ -125,10 +125,7 @@ export class BacklinkTreeItem extends vscode.TreeItem {
|
||||
public readonly resource: Resource,
|
||||
public readonly link: ResourceLink
|
||||
) {
|
||||
super(
|
||||
link.type === 'wikilink' ? link.slug : link.label,
|
||||
vscode.TreeItemCollapsibleState.None
|
||||
);
|
||||
super(link.label, vscode.TreeItemCollapsibleState.None);
|
||||
this.label = `${link.range.start.line}: ${this.label}`;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { window, Uri, workspace, commands } from 'vscode';
|
||||
import { URI } from 'foam-core';
|
||||
import path from 'path';
|
||||
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { commands, window, workspace } from 'vscode';
|
||||
describe('createFromTemplate', () => {
|
||||
describe('create-note-from-template', () => {
|
||||
afterEach(() => {
|
||||
@@ -66,7 +67,7 @@ describe('createFromTemplate', () => {
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-new-template');
|
||||
|
||||
const file = await workspace.fs.readFile(Uri.file(template));
|
||||
const file = await workspace.fs.readFile(toVsCodeUri(URI.file(template)));
|
||||
expect(window.showInputBox).toHaveBeenCalled();
|
||||
expect(file).toBeDefined();
|
||||
});
|
||||
@@ -86,7 +87,9 @@ describe('createFromTemplate', () => {
|
||||
await commands.executeCommand('foam-vscode.create-new-template');
|
||||
|
||||
expect(window.showInputBox).toHaveBeenCalled();
|
||||
await expect(workspace.fs.readFile(Uri.file(template))).rejects.toThrow();
|
||||
await expect(
|
||||
workspace.fs.readFile(toVsCodeUri(URI.file(template)))
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { SnippetString, window } from 'vscode';
|
||||
import { window, workspace } from 'vscode';
|
||||
import {
|
||||
resolveFoamVariables,
|
||||
resolveFoamTemplateVariables,
|
||||
substituteFoamVariables,
|
||||
determineDefaultFilepath,
|
||||
} from './create-from-template';
|
||||
import path from 'path';
|
||||
import { isWindows } from '../utils';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
describe('substituteFoamVariables', () => {
|
||||
test('Does nothing if no Foam-specific variables are used', () => {
|
||||
@@ -58,15 +62,15 @@ describe('resolveFoamVariables', () => {
|
||||
});
|
||||
|
||||
test('Resolves FOAM_TITLE', async () => {
|
||||
const foam_title = 'My note title';
|
||||
const foamTitle = 'My note title';
|
||||
const variables = ['FOAM_TITLE'];
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foam_title)));
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foam_title);
|
||||
expected.set('FOAM_TITLE', foamTitle);
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
expect(await resolveFoamVariables(variables, givenValues)).toEqual(
|
||||
@@ -75,14 +79,14 @@ describe('resolveFoamVariables', () => {
|
||||
});
|
||||
|
||||
test('Resolves FOAM_TITLE without asking the user when it is provided', async () => {
|
||||
const foam_title = 'My note title';
|
||||
const foamTitle = 'My note title';
|
||||
const variables = ['FOAM_TITLE'];
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foam_title);
|
||||
expected.set('FOAM_TITLE', foamTitle);
|
||||
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_TITLE', foam_title);
|
||||
givenValues.set('FOAM_TITLE', foamTitle);
|
||||
expect(await resolveFoamVariables(variables, givenValues)).toEqual(
|
||||
expected
|
||||
);
|
||||
@@ -100,8 +104,8 @@ describe('resolveFoamTemplateVariables', () => {
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
const expectedSnippet = new SnippetString(input);
|
||||
const expected = [expectedMap, expectedSnippet];
|
||||
const expectedString = input;
|
||||
const expected = [expectedMap, expectedString];
|
||||
|
||||
expect(await resolveFoamTemplateVariables(input)).toEqual(expected);
|
||||
});
|
||||
@@ -115,9 +119,187 @@ describe('resolveFoamTemplateVariables', () => {
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
const expectedSnippet = new SnippetString(input);
|
||||
const expected = [expectedMap, expectedSnippet];
|
||||
const expectedString = input;
|
||||
const expected = [expectedMap, expectedString];
|
||||
|
||||
expect(await resolveFoamTemplateVariables(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Allows extra variables to be provided; only resolves the unique set', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const input = `
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
# My note title
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
|
||||
expect(
|
||||
await resolveFoamTemplateVariables(input, new Set(['FOAM_TITLE']))
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Appends FOAM_SELECTED_TEXT with a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template ends in a newline', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const input = `# \${FOAM_TITLE}\n`;
|
||||
|
||||
const expectedOutput = `# My note title\nSelected text\n`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
expect(
|
||||
await resolveFoamTemplateVariables(
|
||||
input,
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT']),
|
||||
givenValues
|
||||
)
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Appends FOAM_SELECTED_TEXT with a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template ends in multiple newlines', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const input = `# \${FOAM_TITLE}\n\n`;
|
||||
|
||||
const expectedOutput = `# My note title\n\nSelected text\n`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
expect(
|
||||
await resolveFoamTemplateVariables(
|
||||
input,
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT']),
|
||||
givenValues
|
||||
)
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Appends FOAM_SELECTED_TEXT without a newline to the template if there is selected text but FOAM_SELECTED_TEXT is not referenced and the template does not end in a newline', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const input = `# \${FOAM_TITLE}`;
|
||||
|
||||
const expectedOutput = '# My note title\nSelected text';
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
expectedMap.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_SELECTED_TEXT', 'Selected text');
|
||||
expect(
|
||||
await resolveFoamTemplateVariables(
|
||||
input,
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT']),
|
||||
givenValues
|
||||
)
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Does not append FOAM_SELECTED_TEXT to a template if there is no selected text and is not referenced', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(foamTitle)));
|
||||
|
||||
const input = `
|
||||
# \${FOAM_TITLE}
|
||||
`;
|
||||
|
||||
const expectedOutput = `
|
||||
# My note title
|
||||
`;
|
||||
|
||||
const expectedMap = new Map<string, string>();
|
||||
expectedMap.set('FOAM_TITLE', foamTitle);
|
||||
expectedMap.set('FOAM_SELECTED_TEXT', '');
|
||||
|
||||
const expected = [expectedMap, expectedOutput];
|
||||
const givenValues = new Map<string, string>();
|
||||
givenValues.set('FOAM_SELECTED_TEXT', '');
|
||||
expect(
|
||||
await resolveFoamTemplateVariables(
|
||||
input,
|
||||
new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT']),
|
||||
givenValues
|
||||
)
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineDefaultFilepath', () => {
|
||||
test('Absolute filepath metadata is unchanged', () => {
|
||||
const absolutePath = isWindows
|
||||
? 'c:\\absolute_path\\journal\\My Note Title.md'
|
||||
: '/absolute_path/journal/My Note Title.md';
|
||||
|
||||
const resolvedValues = new Map<string, string>();
|
||||
const templateMetadata = new Map<string, string>();
|
||||
templateMetadata.set('filepath', absolutePath);
|
||||
|
||||
const resultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata
|
||||
);
|
||||
|
||||
expect(URI.toFsPath(resultFilepath)).toMatch(absolutePath);
|
||||
});
|
||||
|
||||
test('Relative filepath metadata is appended to current directory', () => {
|
||||
const relativePath = isWindows
|
||||
? 'journal\\My Note Title.md'
|
||||
: 'journal/My Note Title.md';
|
||||
|
||||
const resolvedValues = new Map<string, string>();
|
||||
const templateMetadata = new Map<string, string>();
|
||||
templateMetadata.set('filepath', relativePath);
|
||||
|
||||
const resultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata
|
||||
);
|
||||
|
||||
const expectedPath = path.join(
|
||||
workspace.workspaceFolders[0].uri.fsPath,
|
||||
relativePath
|
||||
);
|
||||
|
||||
expect(URI.toFsPath(resultFilepath)).toMatch(expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { URI } from 'foam-core';
|
||||
import { existsSync } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isAbsolute } from 'path';
|
||||
import { TextEncoder } from 'util';
|
||||
import {
|
||||
window,
|
||||
commands,
|
||||
ExtensionContext,
|
||||
workspace,
|
||||
QuickPickItem,
|
||||
Selection,
|
||||
SnippetString,
|
||||
Uri,
|
||||
TextDocument,
|
||||
ViewColumn,
|
||||
window,
|
||||
workspace,
|
||||
WorkspaceEdit,
|
||||
} from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { FoamFeature } from '../types';
|
||||
import { TextEncoder } from 'util';
|
||||
import { focusNote } from '../utils';
|
||||
import { existsSync } from 'fs';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
|
||||
const templatesDir = Uri.joinPath(
|
||||
const templatesDir = URI.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates'
|
||||
@@ -27,10 +35,25 @@ export class UserCancelledOperation extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const knownFoamVariables = new Set(['FOAM_TITLE']);
|
||||
interface FoamSelectionContent {
|
||||
document: TextDocument;
|
||||
selection: Selection;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const defaultTemplateDefaultText: string = '# ${FOAM_TITLE}'; // eslint-disable-line no-template-curly-in-string
|
||||
const defaultTemplateUri = Uri.joinPath(templatesDir, 'new-note.md');
|
||||
const knownFoamVariables = new Set(['FOAM_TITLE', 'FOAM_SELECTED_TEXT']);
|
||||
|
||||
const defaultTemplateDefaultText: string = `---
|
||||
foam_template:
|
||||
name: New Note
|
||||
description: Foam's default new note template
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
\${FOAM_SELECTED_TEXT}
|
||||
`;
|
||||
const defaultTemplateUri = URI.joinPath(templatesDir, 'new-note.md');
|
||||
const dailyNoteTemplateUri = URI.joinPath(templatesDir, 'daily-note.md');
|
||||
|
||||
const templateContent = `# \${1:$TM_FILENAME_BASE}
|
||||
|
||||
@@ -51,9 +74,19 @@ For a full list of features see [the VS Code snippets page](https://code.visuals
|
||||
2. create a note from this template by running the \`Foam: Create New Note From Template\` command
|
||||
`;
|
||||
|
||||
async function getTemplates(): Promise<string[]> {
|
||||
const templates = await workspace.findFiles('.foam/templates/**.md');
|
||||
return templates.map(template => path.basename(template.path));
|
||||
async function templateMetadata(
|
||||
templateUri: URI
|
||||
): Promise<Map<string, string>> {
|
||||
const contents = await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString());
|
||||
const [templateMetadata] = extractFoamTemplateFrontmatterMetadata(contents);
|
||||
return templateMetadata;
|
||||
}
|
||||
|
||||
async function getTemplates(): Promise<URI[]> {
|
||||
const templates = await workspace.findFiles('.foam/templates/**.md', null);
|
||||
return templates;
|
||||
}
|
||||
|
||||
async function offerToCreateTemplate(): Promise<void> {
|
||||
@@ -91,6 +124,11 @@ async function resolveFoamTitle() {
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
function resolveFoamSelectedText() {
|
||||
return findSelectionContent()?.content ?? '';
|
||||
}
|
||||
|
||||
class Resolver {
|
||||
promises = new Map<string, Thenable<string>>();
|
||||
|
||||
@@ -102,6 +140,9 @@ class Resolver {
|
||||
case 'FOAM_TITLE':
|
||||
this.promises.set(name, resolveFoamTitle());
|
||||
break;
|
||||
case 'FOAM_SELECTED_TEXT':
|
||||
this.promises.set(name, Promise.resolve(resolveFoamSelectedText()));
|
||||
break;
|
||||
default:
|
||||
this.promises.set(name, Promise.resolve(name));
|
||||
break;
|
||||
@@ -153,27 +194,84 @@ export function substituteFoamVariables(
|
||||
return templateText;
|
||||
}
|
||||
|
||||
function sortTemplatesMetadata(
|
||||
t1: Map<string, string>,
|
||||
t2: Map<string, string>
|
||||
) {
|
||||
// Sort by name's existence, then name, then path
|
||||
|
||||
if (t1.get('name') === undefined && t2.get('name') !== undefined) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (t1.get('name') !== undefined && t2.get('name') === undefined) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const pathSortOrder = t1
|
||||
.get('templatePath')
|
||||
.localeCompare(t2.get('templatePath'));
|
||||
|
||||
if (t1.get('name') === undefined && t2.get('name') === undefined) {
|
||||
return pathSortOrder;
|
||||
}
|
||||
|
||||
const nameSortOrder = t1.get('name').localeCompare(t2.get('name'));
|
||||
|
||||
return nameSortOrder || pathSortOrder;
|
||||
}
|
||||
|
||||
async function askUserForTemplate() {
|
||||
const templates = await getTemplates();
|
||||
if (templates.length === 0) {
|
||||
return offerToCreateTemplate();
|
||||
}
|
||||
return await window.showQuickPick(templates, {
|
||||
|
||||
const templatesMetadata = (
|
||||
await Promise.all(
|
||||
templates.map(async templateUri => {
|
||||
const metadata = await templateMetadata(templateUri);
|
||||
metadata.set('templatePath', path.basename(templateUri.path));
|
||||
return metadata;
|
||||
})
|
||||
)
|
||||
).sort(sortTemplatesMetadata);
|
||||
|
||||
const items: QuickPickItem[] = await Promise.all(
|
||||
templatesMetadata.map(metadata => {
|
||||
const label = metadata.get('name') || metadata.get('templatePath');
|
||||
const description = metadata.get('name')
|
||||
? metadata.get('templatePath')
|
||||
: null;
|
||||
const detail = metadata.get('description');
|
||||
const item = {
|
||||
label: label,
|
||||
description: description,
|
||||
detail: detail,
|
||||
};
|
||||
Object.keys(item).forEach(key => {
|
||||
if (!item[key]) {
|
||||
delete item[key];
|
||||
}
|
||||
});
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
return await window.showQuickPick(items, {
|
||||
placeHolder: 'Select a template to use.',
|
||||
});
|
||||
}
|
||||
|
||||
async function askUserForFilepathConfirmation(
|
||||
defaultFilepath: Uri,
|
||||
defaultFilepath: URI,
|
||||
defaultFilename: string
|
||||
) {
|
||||
const fsPath = URI.toFsPath(defaultFilepath);
|
||||
return await window.showInputBox({
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: defaultFilepath.fsPath,
|
||||
valueSelection: [
|
||||
defaultFilepath.fsPath.length - defaultFilename.length,
|
||||
defaultFilepath.fsPath.length - 3,
|
||||
],
|
||||
value: fsPath,
|
||||
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
|
||||
validateInput: value =>
|
||||
value.trim().length === 0
|
||||
? 'Please enter a value'
|
||||
@@ -183,24 +281,56 @@ async function askUserForFilepathConfirmation(
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveFoamTemplateVariables(
|
||||
templateText: string
|
||||
): Promise<[Map<string, string>, SnippetString]> {
|
||||
const givenValues = new Map<string, string>();
|
||||
const variables = findFoamVariables(templateText.toString());
|
||||
function appendSnippetVariableUsage(templateText: string, variable: string) {
|
||||
if (templateText.endsWith('\n')) {
|
||||
return `${templateText}\${${variable}}\n`;
|
||||
} else {
|
||||
return `${templateText}\n\${${variable}}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveFoamTemplateVariables(
|
||||
templateText: string,
|
||||
extraVariablesToResolve: Set<string> = new Set(),
|
||||
givenValues: Map<string, string> = new Map()
|
||||
): Promise<[Map<string, string>, string]> {
|
||||
const variablesInTemplate = findFoamVariables(templateText.toString());
|
||||
const variables = variablesInTemplate.concat(...extraVariablesToResolve);
|
||||
const uniqVariables = [...new Set(variables)];
|
||||
|
||||
const resolvedValues = await resolveFoamVariables(uniqVariables, givenValues);
|
||||
|
||||
if (
|
||||
resolvedValues.get('FOAM_SELECTED_TEXT') &&
|
||||
!variablesInTemplate.includes('FOAM_SELECTED_TEXT')
|
||||
) {
|
||||
templateText = appendSnippetVariableUsage(
|
||||
templateText,
|
||||
'FOAM_SELECTED_TEXT'
|
||||
);
|
||||
variablesInTemplate.push('FOAM_SELECTED_TEXT');
|
||||
variables.push('FOAM_SELECTED_TEXT');
|
||||
uniqVariables.push('FOAM_SELECTED_TEXT');
|
||||
}
|
||||
|
||||
const resolvedValues = await resolveFoamVariables(variables, givenValues);
|
||||
const subbedText = substituteFoamVariables(
|
||||
templateText.toString(),
|
||||
resolvedValues
|
||||
);
|
||||
const snippet = new SnippetString(subbedText);
|
||||
return [resolvedValues, snippet];
|
||||
|
||||
return [resolvedValues, subbedText];
|
||||
}
|
||||
|
||||
async function writeTemplate(templateSnippet: SnippetString, filepath: Uri) {
|
||||
await workspace.fs.writeFile(filepath, new TextEncoder().encode(''));
|
||||
await focusNote(filepath, true);
|
||||
async function writeTemplate(
|
||||
templateSnippet: SnippetString,
|
||||
filepath: URI,
|
||||
viewColumn: ViewColumn = ViewColumn.Active
|
||||
) {
|
||||
await workspace.fs.writeFile(
|
||||
toVsCodeUri(filepath),
|
||||
new TextEncoder().encode('')
|
||||
);
|
||||
await focusNote(filepath, true, viewColumn);
|
||||
await window.activeTextEditor.insertSnippet(templateSnippet);
|
||||
}
|
||||
|
||||
@@ -208,22 +338,124 @@ function currentDirectoryFilepath(filename: string) {
|
||||
const activeFile = window.activeTextEditor?.document?.uri.path;
|
||||
const currentDir =
|
||||
activeFile !== undefined
|
||||
? Uri.parse(path.dirname(activeFile))
|
||||
? URI.parse(path.dirname(activeFile))
|
||||
: workspace.workspaceFolders[0].uri;
|
||||
|
||||
return Uri.joinPath(currentDir, filename);
|
||||
return URI.joinPath(currentDir, filename);
|
||||
}
|
||||
|
||||
async function createNoteFromDefaultTemplate(): Promise<void> {
|
||||
const templateUri = defaultTemplateUri;
|
||||
const templateText = existsSync(templateUri.fsPath)
|
||||
? await workspace.fs.readFile(templateUri).then(bytes => bytes.toString())
|
||||
: defaultTemplateDefaultText;
|
||||
function findSelectionContent(): FoamSelectionContent | undefined {
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let resolvedValues, templateSnippet;
|
||||
const document = editor.document;
|
||||
const selection = editor.selection;
|
||||
|
||||
if (!document || selection.isEmpty) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
document,
|
||||
selection,
|
||||
content: document.getText(selection),
|
||||
};
|
||||
}
|
||||
|
||||
async function replaceSelectionWithWikiLink(
|
||||
document: TextDocument,
|
||||
newNoteFile: URI,
|
||||
selection: Selection
|
||||
) {
|
||||
const newNoteTitle = URI.getFileNameWithoutExtension(newNoteFile);
|
||||
|
||||
const originatingFileEdit = new WorkspaceEdit();
|
||||
originatingFileEdit.replace(document.uri, selection, `[[${newNoteTitle}]]`);
|
||||
|
||||
await workspace.applyEdit(originatingFileEdit);
|
||||
}
|
||||
|
||||
function resolveFilepathAttribute(filepath) {
|
||||
return isAbsolute(filepath)
|
||||
? URI.file(filepath)
|
||||
: URI.joinPath(workspace.workspaceFolders[0].uri, filepath);
|
||||
}
|
||||
|
||||
export function determineDefaultFilepath(
|
||||
resolvedValues: Map<string, string>,
|
||||
templateMetadata: Map<string, string>,
|
||||
fallbackURI: URI = undefined
|
||||
) {
|
||||
let defaultFilepath: URI;
|
||||
if (templateMetadata.get('filepath')) {
|
||||
defaultFilepath = resolveFilepathAttribute(
|
||||
templateMetadata.get('filepath')
|
||||
);
|
||||
} else if (fallbackURI) {
|
||||
return fallbackURI;
|
||||
} else {
|
||||
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
|
||||
defaultFilepath = currentDirectoryFilepath(`${defaultSlug}.md`);
|
||||
}
|
||||
return defaultFilepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a daily note from the daily note template.
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
* @param templateFallbackText the template text to use if daily-note.md template does not exist. This is configurable by the caller for backwards compatibility purposes.
|
||||
*/
|
||||
export async function createNoteFromDailyNoteTemplate(
|
||||
filepathFallbackURI: URI,
|
||||
templateFallbackText: string
|
||||
): Promise<void> {
|
||||
return await createNoteFromDefaultTemplate(
|
||||
new Map(),
|
||||
new Set(['FOAM_SELECTED_TEXT']),
|
||||
dailyNoteTemplateUri,
|
||||
filepathFallbackURI,
|
||||
templateFallbackText
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new note using the default note template.
|
||||
* @param givenValues already resolved values of Foam template variables. These are used instead of resolving the Foam template variables.
|
||||
* @param extraVariablesToResolve Foam template variables to resolve, in addition to those mentioned in the template.
|
||||
* @param templateUri the URI of the template to use/
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
* @param templateFallbackText the template text to use the default note template does not exist. This is configurable by the caller for backwards compatibility purposes.
|
||||
*/
|
||||
async function createNoteFromDefaultTemplate(
|
||||
givenValues: Map<string, string> = new Map(),
|
||||
extraVariablesToResolve: Set<string> = new Set([
|
||||
'FOAM_TITLE',
|
||||
'FOAM_SELECTED_TEXT',
|
||||
]),
|
||||
templateUri: URI = defaultTemplateUri,
|
||||
filepathFallbackURI: URI = undefined,
|
||||
templateFallbackText: string = defaultTemplateDefaultText
|
||||
): Promise<void> {
|
||||
const templateText = existsSync(URI.toFsPath(templateUri))
|
||||
? await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString())
|
||||
: templateFallbackText;
|
||||
|
||||
const selectedContent = findSelectionContent();
|
||||
|
||||
let resolvedValues: Map<string, string>,
|
||||
templateWithResolvedVariables: string;
|
||||
try {
|
||||
[resolvedValues, templateSnippet] = await resolveFoamTemplateVariables(
|
||||
templateText
|
||||
[
|
||||
resolvedValues,
|
||||
templateWithResolvedVariables,
|
||||
] = await resolveFoamTemplateVariables(
|
||||
templateText,
|
||||
extraVariablesToResolve,
|
||||
givenValues.set('FOAM_SELECTED_TEXT', selectedContent?.content ?? '')
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
@@ -233,12 +465,21 @@ async function createNoteFromDefaultTemplate(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
|
||||
const defaultFilename = `${defaultSlug}.md`;
|
||||
const defaultFilepath = currentDirectoryFilepath(defaultFilename);
|
||||
const [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
const templateSnippet = new SnippetString(templateWithFoamFrontmatterRemoved);
|
||||
|
||||
const defaultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata,
|
||||
filepathFallbackURI
|
||||
);
|
||||
const defaultFilename = path.basename(defaultFilepath.path);
|
||||
|
||||
let filepath = defaultFilepath;
|
||||
if (existsSync(filepath.fsPath)) {
|
||||
if (existsSync(URI.toFsPath(filepath))) {
|
||||
const newFilepath = await askUserForFilepathConfirmation(
|
||||
defaultFilepath,
|
||||
defaultFilename
|
||||
@@ -247,9 +488,22 @@ async function createNoteFromDefaultTemplate(): Promise<void> {
|
||||
if (newFilepath === undefined) {
|
||||
return;
|
||||
}
|
||||
filepath = Uri.file(newFilepath);
|
||||
filepath = URI.file(newFilepath);
|
||||
}
|
||||
|
||||
await writeTemplate(
|
||||
templateSnippet,
|
||||
filepath,
|
||||
selectedContent ? ViewColumn.Beside : ViewColumn.Active
|
||||
);
|
||||
|
||||
if (selectedContent !== undefined) {
|
||||
await replaceSelectionWithWikiLink(
|
||||
selectedContent.document,
|
||||
filepath,
|
||||
selectedContent.selection
|
||||
);
|
||||
}
|
||||
await writeTemplate(templateSnippet, filepath);
|
||||
}
|
||||
|
||||
async function createNoteFromTemplate(
|
||||
@@ -259,16 +513,25 @@ async function createNoteFromTemplate(
|
||||
if (selectedTemplate === undefined) {
|
||||
return;
|
||||
}
|
||||
templateFilename = selectedTemplate as string;
|
||||
const templateUri = Uri.joinPath(templatesDir, templateFilename);
|
||||
templateFilename =
|
||||
(selectedTemplate as QuickPickItem).description ||
|
||||
(selectedTemplate as QuickPickItem).label;
|
||||
const templateUri = URI.joinPath(templatesDir, templateFilename);
|
||||
const templateText = await workspace.fs
|
||||
.readFile(templateUri)
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString());
|
||||
|
||||
let resolvedValues, templateSnippet;
|
||||
const selectedContent = findSelectionContent();
|
||||
|
||||
let resolvedValues, templateWithResolvedVariables;
|
||||
try {
|
||||
[resolvedValues, templateSnippet] = await resolveFoamTemplateVariables(
|
||||
templateText
|
||||
[
|
||||
resolvedValues,
|
||||
templateWithResolvedVariables,
|
||||
] = await resolveFoamTemplateVariables(
|
||||
templateText,
|
||||
new Set(['FOAM_SELECTED_TEXT']),
|
||||
new Map().set('FOAM_SELECTED_TEXT', selectedContent?.content ?? '')
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
@@ -278,9 +541,17 @@ async function createNoteFromTemplate(
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSlug = resolvedValues.get('FOAM_TITLE') || 'New Note';
|
||||
const defaultFilename = `${defaultSlug}.md`;
|
||||
const defaultFilepath = currentDirectoryFilepath(defaultFilename);
|
||||
const [
|
||||
templateMetadata,
|
||||
templateWithFoamFrontmatterRemoved,
|
||||
] = extractFoamTemplateFrontmatterMetadata(templateWithResolvedVariables);
|
||||
const templateSnippet = new SnippetString(templateWithFoamFrontmatterRemoved);
|
||||
|
||||
const defaultFilepath = determineDefaultFilepath(
|
||||
resolvedValues,
|
||||
templateMetadata
|
||||
);
|
||||
const defaultFilename = path.basename(defaultFilepath.path);
|
||||
|
||||
const filepath = await askUserForFilepathConfirmation(
|
||||
defaultFilepath,
|
||||
@@ -290,20 +561,31 @@ async function createNoteFromTemplate(
|
||||
if (filepath === undefined) {
|
||||
return;
|
||||
}
|
||||
const filepathURI = Uri.file(filepath);
|
||||
await writeTemplate(templateSnippet, filepathURI);
|
||||
const filepathURI = URI.file(filepath);
|
||||
|
||||
await writeTemplate(
|
||||
templateSnippet,
|
||||
filepathURI,
|
||||
selectedContent ? ViewColumn.Beside : ViewColumn.Active
|
||||
);
|
||||
|
||||
if (selectedContent !== undefined) {
|
||||
await replaceSelectionWithWikiLink(
|
||||
selectedContent.document,
|
||||
filepathURI,
|
||||
selectedContent.selection
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewTemplate(): Promise<void> {
|
||||
const defaultFilename = 'new-template.md';
|
||||
const defaultTemplate = Uri.joinPath(templatesDir, defaultFilename);
|
||||
const defaultTemplate = URI.joinPath(templatesDir, defaultFilename);
|
||||
const fsPath = URI.toFsPath(defaultTemplate);
|
||||
const filename = await window.showInputBox({
|
||||
prompt: `Enter the filename for the new template`,
|
||||
value: defaultTemplate.fsPath,
|
||||
valueSelection: [
|
||||
defaultTemplate.fsPath.length - defaultFilename.length,
|
||||
defaultTemplate.fsPath.length - 3,
|
||||
],
|
||||
value: fsPath,
|
||||
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
|
||||
validateInput: value =>
|
||||
value.trim().length === 0
|
||||
? 'Please enter a value'
|
||||
@@ -315,9 +597,9 @@ async function createNewTemplate(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const filenameURI = Uri.file(filename);
|
||||
const filenameURI = URI.file(filename);
|
||||
await workspace.fs.writeFile(
|
||||
filenameURI,
|
||||
toVsCodeUri(filenameURI),
|
||||
new TextEncoder().encode(templateContent)
|
||||
);
|
||||
await focusNote(filenameURI, false);
|
||||
|
||||
@@ -100,4 +100,46 @@ describe('Document links provider', () => {
|
||||
);
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 35));
|
||||
});
|
||||
|
||||
it('should support wikilinks that have an alias', async () => {
|
||||
const fileB = await createFile("# File B that's aliased");
|
||||
const fileA = await createFile(
|
||||
`this is a link to [[${fileB.name}|alias]].`
|
||||
);
|
||||
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(noteB.uri));
|
||||
expect(links[0].range).toEqual(new vscode.Range(0, 18, 0, 33));
|
||||
});
|
||||
|
||||
it('should support wikilink aliases in tables using escape character', async () => {
|
||||
const fileB = await createFile('# File that has to be aliased');
|
||||
const fileA = await createFile(`
|
||||
| Col A | ColB |
|
||||
| --- | --- |
|
||||
| [[${fileB.name}\\|alias]] | test |
|
||||
`);
|
||||
const noteA = parser.parse(fileA.uri, fileA.content);
|
||||
const noteB = parser.parse(fileB.uri, fileB.content);
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
|
||||
const { doc } = await showInEditor(noteA.uri);
|
||||
const provider = new LinkProvider(ws, parser);
|
||||
const links = provider.provideDocumentLinks(doc);
|
||||
|
||||
expect(links.length).toEqual(1);
|
||||
expect(links[0].target).toEqual(OPEN_COMMAND.asURI(noteB.uri));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ const getDailyNoteLink = (date: Date) => {
|
||||
return `[[${name.replace(`.${foamExtension}`, '')}]]`;
|
||||
};
|
||||
|
||||
const snippets: (() => DateSnippet)[] = [
|
||||
const snippetFactories: (() => DateSnippet)[] = [
|
||||
() => ({
|
||||
detail: "Insert a link to today's daily note",
|
||||
snippet: '/day',
|
||||
@@ -169,24 +169,28 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
|
||||
];
|
||||
|
||||
const completions: CompletionItemProvider = {
|
||||
provideCompletionItems: (_document, _position, _token, _context) => {
|
||||
provideCompletionItems: (document, position, _token, _context) => {
|
||||
if (_context.triggerKind === CompletionTriggerKind.Invoke) {
|
||||
// if completion was triggered without trigger character then we return [] to fallback
|
||||
// to vscode word-based suggestions (see https://github.com/foambubble/foam/pull/417)
|
||||
return [];
|
||||
}
|
||||
|
||||
const range = document.getWordRangeAtPosition(position, /\S+/);
|
||||
const completionItems = [
|
||||
...snippets.map(item => createCompletionItem(item())),
|
||||
...generateDayOfWeekSnippets().map(item => createCompletionItem(item)),
|
||||
];
|
||||
...snippetFactories.map(snippetFactory => snippetFactory()),
|
||||
...generateDayOfWeekSnippets(),
|
||||
].map(snippet => {
|
||||
const completionItem = createCompletionItem(snippet);
|
||||
completionItem.range = range;
|
||||
return completionItem;
|
||||
});
|
||||
return completionItems;
|
||||
},
|
||||
};
|
||||
|
||||
const computedCompletions: CompletionItemProvider = {
|
||||
provideCompletionItems: (document, position, _token, _context) => {
|
||||
if (_context.triggerKind === CompletionTriggerKind.Invoke) {
|
||||
export const datesCompletionProvider: CompletionItemProvider = {
|
||||
provideCompletionItems: (document, position, _token, context) => {
|
||||
if (context.triggerKind === CompletionTriggerKind.Invoke) {
|
||||
// if completion was triggered without trigger character then we return [] to fallback
|
||||
// to vscode word-based suggestions (see https://github.com/foambubble/foam/pull/417)
|
||||
return [];
|
||||
@@ -229,7 +233,7 @@ const feature: FoamFeature = {
|
||||
languages.registerCompletionItemProvider('markdown', completions, '/');
|
||||
languages.registerCompletionItemProvider(
|
||||
'markdown',
|
||||
computedCompletions,
|
||||
datesCompletionProvider,
|
||||
'/',
|
||||
'+'
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('Link generation in preview', () => {
|
||||
expect(md.render(`[[note-a]]`)).toEqual(
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='${URI.toFsPath(
|
||||
noteA.uri
|
||||
)}'>note-a</a></p>\n`
|
||||
)}' data-href='${URI.toFsPath(noteA.uri)}'>note-a</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { Foam, FoamWorkspace, Logger, URI } from 'foam-core';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import fp from 'lodash/fp';
|
||||
import { isNone } from '../utils';
|
||||
|
||||
const ALIAS_DIVIDER_CHAR = '|';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -13,11 +15,11 @@ const feature: FoamFeature = {
|
||||
|
||||
return {
|
||||
extendMarkdownIt: (md: markdownit) => {
|
||||
const markdownItExtends = fp.compose(
|
||||
return [
|
||||
markdownItWithFoamTags,
|
||||
markdownItWithFoamLinks,
|
||||
markdownItWithFoamTags
|
||||
);
|
||||
return markdownItExtends(md, foam.workspace);
|
||||
markdownItWithRemoveLinkReferences,
|
||||
].reduce((acc, extension) => extension(acc, foam.workspace), md);
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -32,13 +34,25 @@ export const markdownItWithFoamLinks = (
|
||||
regex: /\[\[([^[\]]+?)\]\]/,
|
||||
replace: (wikilink: string) => {
|
||||
try {
|
||||
const resource = workspace.find(wikilink);
|
||||
if (resource == null) {
|
||||
return getPlaceholderLink(wikilink);
|
||||
const linkHasAlias = wikilink.includes(ALIAS_DIVIDER_CHAR);
|
||||
const resourceLink = linkHasAlias
|
||||
? wikilink.substring(0, wikilink.indexOf('|'))
|
||||
: wikilink;
|
||||
|
||||
const resource = workspace.find(resourceLink);
|
||||
if (isNone(resource)) {
|
||||
return getPlaceholderLink(resourceLink);
|
||||
}
|
||||
|
||||
const linkLabel = linkHasAlias
|
||||
? wikilink.substr(wikilink.indexOf('|') + 1)
|
||||
: wikilink;
|
||||
|
||||
return `<a class='foam-note-link' title='${
|
||||
resource.title
|
||||
}' href='${URI.toFsPath(resource.uri)}'>${wikilink}</a>`;
|
||||
}' href='${URI.toFsPath(resource.uri)}' data-href='${URI.toFsPath(
|
||||
resource.uri
|
||||
)}'>${linkLabel}</a>`;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while creating link for [[${wikilink}]] in Preview panel`,
|
||||
@@ -63,7 +77,7 @@ export const markdownItWithFoamTags = (
|
||||
replace: (tag: string) => {
|
||||
try {
|
||||
const resource = workspace.find(tag);
|
||||
if (resource == null) {
|
||||
if (isNone(resource)) {
|
||||
return getFoamTag(tag);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -80,4 +94,22 @@ export const markdownItWithFoamTags = (
|
||||
const getFoamTag = (content: string) =>
|
||||
`<span class='foam-tag'>${content}</span>`;
|
||||
|
||||
export const markdownItWithRemoveLinkReferences = (
|
||||
md: markdownit,
|
||||
workspace: FoamWorkspace
|
||||
) => {
|
||||
// Forget about reference links that contain an alias divider
|
||||
md.inline.ruler.before('link', 'clear-references', state => {
|
||||
if (state.env.references) {
|
||||
Object.keys(state.env.references).forEach(refKey => {
|
||||
if (refKey.includes(ALIAS_DIVIDER_CHAR)) {
|
||||
delete state.env.references[refKey];
|
||||
}
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return md;
|
||||
};
|
||||
|
||||
export default feature;
|
||||
|
||||
152
packages/foam-vscode/src/features/tags-tree-view/index.spec.ts
Normal file
152
packages/foam-vscode/src/features/tags-tree-view/index.spec.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createTestNote,
|
||||
} from '../../test/test-utils';
|
||||
|
||||
import { Tag, TagReference, TagsProvider } from '.';
|
||||
|
||||
import {
|
||||
bootstrap,
|
||||
createConfigFromFolders,
|
||||
Foam,
|
||||
FileDataStore,
|
||||
FoamConfig,
|
||||
MarkdownResourceProvider,
|
||||
Matcher,
|
||||
} from 'foam-core';
|
||||
|
||||
describe('Tags tree panel', () => {
|
||||
let _foam: Foam;
|
||||
let provider: TagsProvider;
|
||||
|
||||
const config: FoamConfig = createConfigFromFolders([]);
|
||||
const mdProvider = new MarkdownResourceProvider(
|
||||
new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
)
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
_foam.dispose();
|
||||
await cleanWorkspace();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
_foam = await bootstrap(config, new FileDataStore(), [mdProvider]);
|
||||
provider = new TagsProvider(_foam, _foam.workspace);
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
_foam.dispose();
|
||||
});
|
||||
|
||||
it('correctly provides a tag from a set of notes', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: new Set(['test']),
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
provider.refresh();
|
||||
|
||||
const treeItems = (await provider.getChildren()) as Tag[];
|
||||
|
||||
treeItems.map(item => expect(item.tag).toContain('test'));
|
||||
});
|
||||
|
||||
it('correctly handles a parent and child tag', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: new Set(['parent/child']),
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as Tag[];
|
||||
const parentTagItem = parentTreeItems.pop();
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(parentTagItem)) as Tag[];
|
||||
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof Tag) {
|
||||
expect(child.title).toEqual('child');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly handles a single parent and multiple child tag', async () => {
|
||||
const noteA = createTestNote({
|
||||
tags: new Set(['parent/child']),
|
||||
uri: './note-a.md',
|
||||
});
|
||||
_foam.workspace.set(noteA);
|
||||
const noteB = createTestNote({
|
||||
tags: new Set(['parent/subchild']),
|
||||
uri: './note-b.md',
|
||||
});
|
||||
_foam.workspace.set(noteB);
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as Tag[];
|
||||
const parentTagItem = parentTreeItems.filter(
|
||||
item => item instanceof Tag
|
||||
)[0];
|
||||
|
||||
expect(parentTagItem.title).toEqual('parent');
|
||||
expect(parentTreeItems).toHaveLength(1);
|
||||
|
||||
const childTreeItems = (await provider.getChildren(parentTagItem)) as Tag[];
|
||||
|
||||
childTreeItems.forEach(child => {
|
||||
if (child instanceof Tag) {
|
||||
expect(['child', 'subchild']).toContain(child.title);
|
||||
expect(child.title).not.toEqual('parent');
|
||||
}
|
||||
});
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('correctly handles a single parent and child tag in the same note', async () => {
|
||||
const noteC = createTestNote({
|
||||
tags: new Set(['main', 'main/subtopic']),
|
||||
title: 'Test note',
|
||||
uri: './note-c.md',
|
||||
});
|
||||
|
||||
_foam.workspace.set(noteC);
|
||||
|
||||
provider.refresh();
|
||||
|
||||
const parentTreeItems = (await provider.getChildren()) as Tag[];
|
||||
const parentTagItem = parentTreeItems.filter(
|
||||
item => item instanceof Tag
|
||||
)[0];
|
||||
|
||||
expect(parentTagItem.title).toEqual('main');
|
||||
|
||||
const childTreeItems = (await provider.getChildren(parentTagItem)) as Tag[];
|
||||
|
||||
childTreeItems
|
||||
.filter(item => item instanceof TagReference)
|
||||
.forEach(item => {
|
||||
expect(item.title).toEqual('Test note');
|
||||
});
|
||||
|
||||
childTreeItems
|
||||
.filter(item => item instanceof Tag)
|
||||
.forEach(item => {
|
||||
expect(['main/subtopic']).toContain(item.tag);
|
||||
expect(item.title).toEqual('subtopic');
|
||||
});
|
||||
|
||||
expect(childTreeItems).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { Foam, Resource, URI, FoamWorkspace } from 'foam-core';
|
||||
import { FoamFeature } from '../../types';
|
||||
import { getNoteTooltip, getContainsTooltip, isSome } from '../../utils';
|
||||
|
||||
const TAG_SEPARATOR = '/';
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -67,19 +68,45 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
|
||||
|
||||
getChildren(element?: Tag): Thenable<TagTreeItem[]> {
|
||||
if (element) {
|
||||
const references: TagReference[] = element.notes
|
||||
const nestedTagItems: TagTreeItem[] = this.tags
|
||||
.filter(item => item.tag.indexOf(element.title + TAG_SEPARATOR) > -1)
|
||||
.map(
|
||||
item =>
|
||||
new Tag(
|
||||
item.tag,
|
||||
item.tag.substring(item.tag.indexOf(TAG_SEPARATOR) + 1),
|
||||
item.notes
|
||||
)
|
||||
)
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
const references: TagTreeItem[] = element.notes
|
||||
.map(({ uri }) => this.foam.workspace.get(uri))
|
||||
.map(note => new TagReference(element.tag, note));
|
||||
.filter(note => note.tags.has(element.tag))
|
||||
.map(note => new TagReference(element.tag, note))
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return Promise.resolve([
|
||||
new TagSearch(element.tag),
|
||||
...references.sort((a, b) => a.title.localeCompare(b.title)),
|
||||
new TagSearch(element.title),
|
||||
...nestedTagItems,
|
||||
...references,
|
||||
]);
|
||||
}
|
||||
if (!element) {
|
||||
const tags: Tag[] = this.tags.map(
|
||||
({ tag, notes }) => new Tag(tag, notes)
|
||||
);
|
||||
const tags: Tag[] = this.tags
|
||||
.map(({ tag, notes }) => {
|
||||
const parentTag =
|
||||
tag.indexOf(TAG_SEPARATOR) > 0
|
||||
? tag.substring(0, tag.indexOf(TAG_SEPARATOR))
|
||||
: tag;
|
||||
|
||||
return new Tag(parentTag, parentTag, notes);
|
||||
})
|
||||
.filter(
|
||||
(value, index, array) =>
|
||||
array.findIndex(tag => tag.title === value.title) === index
|
||||
);
|
||||
|
||||
return Promise.resolve(tags.sort((a, b) => a.tag.localeCompare(b.tag)));
|
||||
}
|
||||
}
|
||||
@@ -102,9 +129,10 @@ type TagMetadata = { title: string; uri: URI };
|
||||
export class Tag extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly tag: string,
|
||||
public readonly title: string,
|
||||
public readonly notes: TagMetadata[]
|
||||
) {
|
||||
super(tag, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
super(title, vscode.TreeItemCollapsibleState.Collapsed);
|
||||
this.description = `${this.notes.length} reference${
|
||||
this.notes.length !== 1 ? 's' : ''
|
||||
}`;
|
||||
@@ -116,6 +144,7 @@ export class Tag extends vscode.TreeItem {
|
||||
}
|
||||
|
||||
export class TagSearch extends vscode.TreeItem {
|
||||
public readonly title: string;
|
||||
constructor(public readonly tag: string) {
|
||||
super(`Search #${tag}`, vscode.TreeItemCollapsibleState.None);
|
||||
const searchString = `#${tag}`;
|
||||
|
||||
@@ -13,6 +13,10 @@ class ExtendedVscodeEnvironment extends VscodeEnvironment {
|
||||
// And also https://github.com/microsoft/vscode-test/issues/37#issuecomment-700167820
|
||||
this.global.RegExp = RegExp;
|
||||
this.global.vscode = vscode;
|
||||
|
||||
vscode.workspace
|
||||
.getConfiguration()
|
||||
.update('foam.edit.linkReferenceDefinitions', 'off');
|
||||
}
|
||||
async teardown() {
|
||||
this.global.vscode = initialVscode;
|
||||
|
||||
@@ -47,6 +47,7 @@ export const createTestNote = (params: {
|
||||
title?: string;
|
||||
definitions?: NoteLinkDefinition[];
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
tags?: Set<string>;
|
||||
text?: string;
|
||||
root?: URI;
|
||||
}): Resource => {
|
||||
@@ -57,7 +58,7 @@ export const createTestNote = (params: {
|
||||
properties: {},
|
||||
title: params.title ?? path.parse(strToUri(params.uri).path).base,
|
||||
definitions: params.definitions ?? [],
|
||||
tags: new Set(),
|
||||
tags: params.tags ?? new Set(),
|
||||
links: params.links
|
||||
? params.links.map((link, index) => {
|
||||
const range = Range.create(
|
||||
@@ -69,10 +70,10 @@ export const createTestNote = (params: {
|
||||
return 'slug' in link
|
||||
? {
|
||||
type: 'wikilink',
|
||||
slug: link.slug,
|
||||
target: link.slug,
|
||||
label: link.slug,
|
||||
range: range,
|
||||
text: 'link text',
|
||||
rawText: 'link text',
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
@@ -138,7 +139,7 @@ export const createNote = (r: Resource) => {
|
||||
|
||||
some content and ${r.links
|
||||
.map(l =>
|
||||
l.type === 'wikilink' ? `[[${l.slug}]]` : `[${l.label}](${l.target})`
|
||||
l.type === 'wikilink' ? `[[${l.label}]]` : `[${l.label}](${l.target})`
|
||||
)
|
||||
.join(' some content between links.\n')}
|
||||
last line.
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
MarkdownString,
|
||||
version,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { Logger, URI } from 'foam-core';
|
||||
@@ -169,9 +170,13 @@ export function isNone<T>(
|
||||
return value == null; // eslint-disable-line
|
||||
}
|
||||
|
||||
export async function focusNote(notePath: URI, moveCursorToEnd: boolean) {
|
||||
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);
|
||||
const editor = await window.showTextDocument(document, viewColumn);
|
||||
|
||||
// Move the cursor to end of the file
|
||||
if (moveCursorToEnd) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FoamGraph, FoamWorkspace, URI } from 'foam-core';
|
||||
import { FoamWorkspace } from 'foam-core';
|
||||
import { OPEN_COMMAND } from '../features/utility-commands';
|
||||
import {
|
||||
GroupedResoucesConfigGroupBy,
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import {
|
||||
extractFoamTemplateFrontmatterMetadata,
|
||||
removeFoamMetadata,
|
||||
} from './template-frontmatter-parser';
|
||||
|
||||
describe('extractFoamTemplateFrontmatterMetadata', () => {
|
||||
test('Returns an empty object if there is not frontmatter', () => {
|
||||
const input = `# $FOAM_TITLE`;
|
||||
const expectedMetadata = new Map<string, string>();
|
||||
const expected = [expectedMetadata, input];
|
||||
const result = extractFoamTemplateFrontmatterMetadata(input);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Returns an empty object if `foam_template` is not used', () => {
|
||||
const input = `---
|
||||
foo: bar
|
||||
---
|
||||
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const expectedMetadata = new Map<string, string>();
|
||||
const expected = [expectedMetadata, input];
|
||||
const result = extractFoamTemplateFrontmatterMetadata(input);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Returns an empty object if foam_template is not a YAML mapping', () => {
|
||||
const input = `---json
|
||||
{
|
||||
"foo": "bar",
|
||||
"foam_template": 4
|
||||
}
|
||||
---
|
||||
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const expectedMetadata = new Map<string, string>();
|
||||
const expected = [expectedMetadata, input];
|
||||
const result = extractFoamTemplateFrontmatterMetadata(input);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Returns an empty object if frontmatter is not YAML', () => {
|
||||
const input = `---json
|
||||
{
|
||||
"foo": "bar",
|
||||
"foam_template": {
|
||||
"filepath": "journal/$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE_$FOAM_TITLE.md"
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const expectedMetadata = new Map<string, string>();
|
||||
const expected = [expectedMetadata, input];
|
||||
const result = extractFoamTemplateFrontmatterMetadata(input);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Returns the `foam_template` metadata when it is used in its own frontmatter block', () => {
|
||||
const input = `---
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
description: This is my note template
|
||||
filepath: journal/$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE_$FOAM_TITLE.md
|
||||
---
|
||||
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const output = `
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const expectedMetadata = new Map<string, string>();
|
||||
expectedMetadata.set('name', 'My Note Template');
|
||||
expectedMetadata.set('description', 'This is my note template');
|
||||
expectedMetadata.set(
|
||||
'filepath',
|
||||
'journal/$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE_$FOAM_TITLE.md'
|
||||
);
|
||||
const expected = [expectedMetadata, output];
|
||||
const result = extractFoamTemplateFrontmatterMetadata(input);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Returns the `foam_template` metadata when it is used in its own frontmatter block (and there is another frontmatter block after)', () => {
|
||||
const input = `---
|
||||
foam_template:
|
||||
filepath: journal/$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE_$FOAM_TITLE.md
|
||||
description: This is my note template
|
||||
name: My Note Template
|
||||
---
|
||||
|
||||
---
|
||||
foo: bar
|
||||
# A YAML comment
|
||||
metadata: &info
|
||||
title: The Gentlemen
|
||||
year: 2019
|
||||
more_metadata: *info
|
||||
---
|
||||
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const output = `---
|
||||
foo: bar
|
||||
# A YAML comment
|
||||
metadata: &info
|
||||
title: The Gentlemen
|
||||
year: 2019
|
||||
more_metadata: *info
|
||||
---
|
||||
|
||||
# $FOAM_TITLE
|
||||
`;
|
||||
|
||||
const expectedMetadata = new Map<string, string>();
|
||||
expectedMetadata.set('name', 'My Note Template');
|
||||
expectedMetadata.set('description', 'This is my note template');
|
||||
expectedMetadata.set(
|
||||
'filepath',
|
||||
'journal/$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE_$FOAM_TITLE.md'
|
||||
);
|
||||
const expected = [expectedMetadata, output];
|
||||
const result = extractFoamTemplateFrontmatterMetadata(input);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Returns the `foam_template` metadata when it is used in a shared frontmatter block', () => {
|
||||
const input = `---
|
||||
foo: bar
|
||||
foam_template:
|
||||
name: My Note Template
|
||||
filepath: journal/$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE_$FOAM_TITLE.md
|
||||
description: This is my note template
|
||||
# A YAML comment
|
||||
metadata: &info
|
||||
title: The Gentlemen
|
||||
year: 2019
|
||||
more_metadata: *info
|
||||
---
|
||||
|
||||
# $FOAM_TITLE`;
|
||||
|
||||
const output = `---
|
||||
foo: bar
|
||||
# A YAML comment
|
||||
metadata: &info
|
||||
title: The Gentlemen
|
||||
year: 2019
|
||||
more_metadata: *info
|
||||
---
|
||||
|
||||
# $FOAM_TITLE`;
|
||||
|
||||
const expectedMetadata = new Map<string, string>();
|
||||
expectedMetadata.set('name', 'My Note Template');
|
||||
expectedMetadata.set('description', 'This is my note template');
|
||||
expectedMetadata.set(
|
||||
'filepath',
|
||||
'journal/$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE_$FOAM_TITLE.md'
|
||||
);
|
||||
const expected = [expectedMetadata, output];
|
||||
const result = extractFoamTemplateFrontmatterMetadata(input);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeFoamMetadata', () => {
|
||||
test('Removes Foam specific frontmatter without messing up non-Foam frontmatter', () => {
|
||||
const input = `---
|
||||
foo: bar
|
||||
foam_template: &foam_template # A YAML comment
|
||||
description: This is my note template
|
||||
filepath: journal/$CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE_$FOAM_TITLE.md # A YAML comment
|
||||
name: My Note Template
|
||||
# A YAML comment
|
||||
metadata: &info
|
||||
title: The Gentlemen
|
||||
year: 2019
|
||||
more_metadata: *info
|
||||
---
|
||||
|
||||
# $FOAM_TITLE`;
|
||||
|
||||
const expected = `---
|
||||
foo: bar
|
||||
# A YAML comment
|
||||
metadata: &info
|
||||
title: The Gentlemen
|
||||
year: 2019
|
||||
more_metadata: *info
|
||||
---
|
||||
|
||||
# $FOAM_TITLE`;
|
||||
|
||||
const result = removeFoamMetadata(input);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import matter from 'gray-matter';
|
||||
|
||||
export function extractFoamTemplateFrontmatterMetadata(
|
||||
contents: string
|
||||
): [Map<string, string>, string] {
|
||||
// Need to pass in empty options object, in order to bust a cache
|
||||
// See https://github.com/jonschlinkert/gray-matter/issues/124
|
||||
const parsed = matter(contents, {});
|
||||
let metadata = new Map<string, string>();
|
||||
|
||||
if (parsed.language !== 'yaml') {
|
||||
// We might allow this in the future, once it has been tested adequately.
|
||||
// But for now we'll be safe and prevent people from using anything else.
|
||||
return [metadata, contents];
|
||||
}
|
||||
|
||||
const frontmatter = parsed.data;
|
||||
const frontmatterKeys = Object.keys(frontmatter);
|
||||
const foamMetadata = frontmatter['foam_template'];
|
||||
|
||||
if (typeof foamMetadata !== 'object') {
|
||||
return [metadata, contents];
|
||||
}
|
||||
|
||||
const containsFoam = foamMetadata !== undefined;
|
||||
const onlyFoam = containsFoam && frontmatterKeys.length === 1;
|
||||
metadata = new Map<string, string>(
|
||||
Object.entries((foamMetadata as object) || {})
|
||||
);
|
||||
|
||||
let newContents = contents;
|
||||
if (containsFoam) {
|
||||
if (onlyFoam) {
|
||||
// We'll remove the entire frontmatter block
|
||||
newContents = parsed.content;
|
||||
|
||||
// If there is another frontmatter block, we need to remove
|
||||
// the leading space left behind.
|
||||
const anotherFrontmatter = matter(newContents.trimStart()).matter !== '';
|
||||
if (anotherFrontmatter) {
|
||||
newContents = newContents.trimStart();
|
||||
}
|
||||
} else {
|
||||
// We'll remove only the Foam bits
|
||||
newContents = removeFoamMetadata(contents);
|
||||
}
|
||||
}
|
||||
|
||||
return [metadata, newContents];
|
||||
}
|
||||
|
||||
export function removeFoamMetadata(contents: string) {
|
||||
return contents.replace(
|
||||
/^\s*foam_template:.*?\n(?:\s*(?:filepath|name|description):.*\n)+/gm,
|
||||
''
|
||||
);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
5
packages/foam-vscode/static/dataviz/force-graph.1.40.5.min.js
vendored
Normal file
5
packages/foam-vscode/static/dataviz/force-graph.1.40.5.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script data-replace src="./d3.v6.min.js"></script>
|
||||
<script data-replace src="./force-graph.1.34.1.min.js"></script>
|
||||
<script data-replace src="./force-graph.1.40.5.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"outDir": "out",
|
||||
"lib": ["es6"],
|
||||
"lib": ["ES2019"],
|
||||
"sourceMap": true,
|
||||
"strict": false,
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", ".vscode-test"],
|
||||
"references": [{
|
||||
"path": "../foam-core"
|
||||
}]
|
||||
"references": [
|
||||
{
|
||||
"path": "../foam-core"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://foambubble.github.io/join-discord/g)
|
||||
@@ -157,6 +157,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/daniel-vera-g"><img src="https://avatars.githubusercontent.com/u/28257108?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Daniel VG</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=daniel-vera-g" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Barabazs"><img src="https://avatars.githubusercontent.com/u/31799121?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Barabas</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Barabazs" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://enginveske@gmail.com"><img src="https://avatars.githubusercontent.com/u/43685404?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Engincan VESKE</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=EngincanV" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.paulderaaij.nl"><img src="https://avatars.githubusercontent.com/u/495374?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul de Raaij</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=pderaaij" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/bronson"><img src="https://avatars.githubusercontent.com/u/1776?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Scott Bronson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bronson" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://rafaelriedel.de"><img src="https://avatars.githubusercontent.com/u/41793?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Rafael Riedel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=rafo" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
13
yarn.lock
13
yarn.lock
@@ -5422,9 +5422,9 @@ has@^1.0.3:
|
||||
function-bind "^1.1.1"
|
||||
|
||||
hosted-git-info@^2.1.4, hosted-git-info@^2.7.1:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
hosted-git-info@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -7356,7 +7356,7 @@ lodash.uniq@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash@4.x, lodash@^4.11.2, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.2.1:
|
||||
lodash@4.x, lodash@^4.11.2, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.2.1:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@@ -9128,6 +9128,11 @@ repeating@^2.0.0:
|
||||
dependencies:
|
||||
is-finite "^1.0.0"
|
||||
|
||||
replace-ext@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06"
|
||||
integrity sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==
|
||||
|
||||
request-promise-core@1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f"
|
||||
|
||||
Reference in New Issue
Block a user