mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
af0c2bbaa3 | ||
|
|
700bfc1b26 | ||
|
|
a6d5c04453 | ||
|
|
b0c42cead2 | ||
|
|
6c643adb9d | ||
|
|
ca7fdefaae | ||
|
|
149d5f5a7c | ||
|
|
be80857fd1 | ||
|
|
611fa7359d | ||
|
|
08b7e7a231 | ||
|
|
0a259168c7 | ||
|
|
f3d0569c76 | ||
|
|
502129d5ac | ||
|
|
d29bf16db1 | ||
|
|
d228c7cb18 | ||
|
|
78078cf338 | ||
|
|
fe65883bc5 | ||
|
|
20b5261c5c | ||
|
|
f91cfe5d0d | ||
|
|
1ab9cc5f4a | ||
|
|
02ff681700 | ||
|
|
2d9c3be0e6 | ||
|
|
78cf602347 | ||
|
|
898c7b4387 | ||
|
|
7412d518d7 | ||
|
|
e6030ac562 | ||
|
|
d6d958bc92 | ||
|
|
fd7a24c5fc | ||
|
|
41b3c6fbfb | ||
|
|
84b2ab6e42 | ||
|
|
6cf184ba23 | ||
|
|
6ad8211f56 | ||
|
|
ac247867d9 | ||
|
|
46f0bf2830 | ||
|
|
f0d712d1ce | ||
|
|
b72bca661b | ||
|
|
ac5cd832f6 | ||
|
|
71e8f00e80 | ||
|
|
b371f0fa7d | ||
|
|
b11a206b4a | ||
|
|
c678375712 | ||
|
|
b1bdf766b1 | ||
|
|
531bdab250 | ||
|
|
5fa04c7384 | ||
|
|
1f95d0559c | ||
|
|
f00886acac | ||
|
|
4895a8b84c | ||
|
|
ea0f88475c | ||
|
|
567c87c285 | ||
|
|
4ea076b949 | ||
|
|
bf80a40ad3 | ||
|
|
85e687956f | ||
|
|
5f963fe895 | ||
|
|
947ddf0b77 | ||
|
|
f206e855a9 | ||
|
|
1b8f0cd2fd | ||
|
|
ca063d4eee | ||
|
|
734986211a | ||
|
|
54a4aec1a0 | ||
|
|
d1a28717fe | ||
|
|
30759bd1f3 | ||
|
|
852b19f177 | ||
|
|
16cad729fd | ||
|
|
ab6c046404 | ||
|
|
4b16b530b4 | ||
|
|
51ec6ddec4 | ||
|
|
ca39351407 | ||
|
|
8e48dd77a2 | ||
|
|
ade5b01316 | ||
|
|
4e661aa6b5 | ||
|
|
fa4b9d57aa | ||
|
|
a6db7815f0 | ||
|
|
e604f26544 | ||
|
|
9b12c79daf | ||
|
|
d924a8612e | ||
|
|
7aa2e0e411 | ||
|
|
a710358701 | ||
|
|
9e4124068a | ||
|
|
84e774144e | ||
|
|
ef9131ead7 | ||
|
|
18f0725779 | ||
|
|
eb2a2ed9e0 | ||
|
|
433c0c5b7e | ||
|
|
f48c74c607 | ||
|
|
596d96eaff | ||
|
|
4b65397106 | ||
|
|
a92ea7d86e | ||
|
|
69a5d8201c |
@@ -598,6 +598,105 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joeltjames",
|
||||
"name": "Joel James",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3732400?v=4",
|
||||
"profile": "https://github.com/joeltjames",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ryo33",
|
||||
"name": "Hashiguchi Ryo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8780513?v=4",
|
||||
"profile": "https://www.ryo33.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "movermeyer",
|
||||
"name": "Michael Overmeyer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1459385?v=4",
|
||||
"profile": "https://movermeyer.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "derrickqin",
|
||||
"name": "Derrick Qin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3038111?v=4",
|
||||
"profile": "https://github.com/derrickqin",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zomars",
|
||||
"name": "Omar López",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3504472?v=4",
|
||||
"profile": "https://www.linkedin.com/in/zomars/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "RobinKing",
|
||||
"name": "Robin King",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1583193?v=4",
|
||||
"profile": "http://robincn.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dheepakg",
|
||||
"name": "Dheepak ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4730170?v=4",
|
||||
"profile": "http://twitter.com/deegovee",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "daniel-vera-g",
|
||||
"name": "Daniel VG",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28257108?v=4",
|
||||
"profile": "https://github.com/daniel-vera-g",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Barabazs",
|
||||
"name": "Barabas",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/31799121?v=4",
|
||||
"profile": "https://github.com/Barabazs",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "EngincanV",
|
||||
"name": "Engincan VESKE",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/43685404?v=4",
|
||||
"profile": "http://enginveske@gmail.com",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -20,13 +20,14 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Restore Dependencies
|
||||
- name: Restore Dependencies and VS Code test instance
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
*/*/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
packages/foam-vscode/.vscode-test
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/foam-vscode/src/test/run-tests.ts') }}
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
- name: Check Lint Rules
|
||||
@@ -47,13 +48,14 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Restore Dependencies
|
||||
- name: Restore Dependencies and VS Code test instance
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
*/*/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
packages/foam-vscode/.vscode-test
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/foam-vscode/src/test/run-tests.ts') }}
|
||||
- name: Install Dependencies
|
||||
run: yarn
|
||||
- name: Build Packages
|
||||
|
||||
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@@ -4,12 +4,21 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"inputs": [
|
||||
{
|
||||
"id": "packageName",
|
||||
"type": "pickString",
|
||||
"description": "Select the package in which this test is located",
|
||||
"options": ["foam-core", "foam-vscode"],
|
||||
"default": "foam-core"
|
||||
}
|
||||
],
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"name": "vscode-jest-tests",
|
||||
"request": "launch",
|
||||
"runtimeArgs": ["workspace", "foam-core", "run", "test"], // ${yarnWorkspaceName} is what we're missing
|
||||
"runtimeArgs": ["workspace", "${input:packageName}", "run", "test"], // ${yarnWorkspaceName} is what we're missing
|
||||
"args": [
|
||||
"--runInBand"
|
||||
],
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -16,11 +16,16 @@
|
||||
"**/.vscode/**/*",
|
||||
"**/_layouts/**/*",
|
||||
"**/_site/**/*",
|
||||
"**/node_modules/**/*"
|
||||
"**/node_modules/**/*",
|
||||
"packages/**/*"
|
||||
],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.requireConfig": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"]
|
||||
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"],
|
||||
"gitdoc.enabled": false,
|
||||
"jest.autoEnable": false,
|
||||
"jest.runAllTestsFirst": false,
|
||||
"search.mode": "reuseEditor"
|
||||
}
|
||||
|
||||
17
LICENSE
17
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT Licence (MIT)
|
||||
|
||||
Copyright 2020 Jani Eväkallio <jani.evakallio@gmail.com>
|
||||
Copyright 2020 - present Jani Eväkallio <jani.evakallio@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
@@ -17,4 +17,17 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Where noted, some code uses the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
Well, that shouldn't have happened!
|
||||
|
||||
If you got here via a link from another document, please file an [issue](https://github.com/foambubble/foam/issues) on our GitHub repo including:
|
||||
|
||||
- the page you came from
|
||||
- the link you followed
|
||||
|
||||
Thanks!
|
||||
|
||||
-The Foam Team
|
||||
-The Foam Team
|
||||
|
||||
@@ -35,4 +35,3 @@ gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform?
|
||||
# kramdown v2 ships without the gfm parser by default. If you're using
|
||||
# kramdown v1, comment out this line.
|
||||
gem "kramdown-parser-gfm"
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -223,7 +223,7 @@ GEM
|
||||
rb-fsevent (0.10.4)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
rexml (3.2.4)
|
||||
rexml (3.2.5)
|
||||
rouge (3.23.0)
|
||||
ruby-enum (0.8.0)
|
||||
i18n
|
||||
|
||||
33
docs/LICENSE.txt
Normal file
33
docs/LICENSE.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
The MIT Licence (MIT)
|
||||
|
||||
Copyright 2020 - present Jani Eväkallio <jani.evakallio@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Where noted, some code uses the following license:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
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 |
@@ -12,7 +12,6 @@
|
||||
- What use cases are we working towards?
|
||||
-[[todo]] User round table
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -118,7 +118,7 @@ the community.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
@@ -126,7 +126,5 @@ enforcement ladder](https://github.com/mozilla/diversity).
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
|
||||
<https://www.contributor-covenant.org/faq>. Translations are available at
|
||||
<https://www.contributor-covenant.org/translations>.
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
tags: todo, good-first-task
|
||||
---
|
||||
# Contribution Guide
|
||||
|
||||
Foam is open to contributions of any kind, including but not limited to code, documentation, ideas, and feedback.
|
||||
This guide aims to help guide new and seasoned contributors getting around the Foam codebase.
|
||||
|
||||
## Getting Up To Speed
|
||||
|
||||
Before you start contributing we recommend that you read the following links:
|
||||
|
||||
- [[principles]] - This document describes the guiding principles behind Foam.
|
||||
- [[code-of-conduct]] - Rules we hope every contributor aims to follow, allowing everyone to participate in our community!
|
||||
|
||||
## Diving In
|
||||
|
||||
We understand that diving in an unfamiliar codebase may seem scary,
|
||||
to make it easier for new contributors we provide some resources:
|
||||
|
||||
- [[architecture]] - This document describes the architecture of Foam and how the repository is structured.
|
||||
|
||||
You can also see [existing issues](https://github.com/foambubble/foam/issues) and help out!
|
||||
@@ -41,10 +45,11 @@ You should now be ready to start working!
|
||||
|
||||
Code needs to come with tests.
|
||||
We use the following convention in Foam:
|
||||
|
||||
- *.test.ts are unit tests
|
||||
- *.spec.ts are integration tests
|
||||
|
||||
Also, note that tests in `foam-core` and `foam-cli` live in the `test` directory.
|
||||
Also, note that tests in `foam-core` live in the `test` directory.
|
||||
Tests in `foam-vscode` live alongside the code in `src`.
|
||||
|
||||
### The VS Code Extension
|
||||
|
||||
@@ -6,14 +6,16 @@ tags: architecture
|
||||
This document aims to provide a quick overview of the Foam architecture!
|
||||
|
||||
Foam code and documentation live in the monorepo at [foambubble/foam](https://github.com/foambubble/foam/).
|
||||
|
||||
- [/docs](https://github.com/foambubble/foam/tree/master/docs): documentation and [[recipes]].
|
||||
- [/packages/foam-core](https://github.com/foambubble/foam/tree/master/packages/foam-core) - Powers the core functionality in Foam across all platforms.
|
||||
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) - The core VSCode plugin.
|
||||
- [/packages/foam-cli](https://github.com/foambubble/foam/tree/master/packages/foam-cli) - The Foam CLI tool.
|
||||
|
||||
Exceptions to the monorepo are:
|
||||
|
||||
- The starter template at [foambubble/foam-template](https://github.com/foambubble/)
|
||||
- All other [[recommended-extensions]] live in their respective GitHub repos.
|
||||
- [foam-cli](https://github.com/foambubble/foam-cli) - The Foam CLI tool.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[recipes]: ../recipes/recipes.md "Recipes"
|
||||
|
||||
@@ -10,5 +10,3 @@ But there's also a bunch of roadmap items that are hard to implement this way, a
|
||||
Overall, we should strive to build big things from small things. Focused, interoperable modules are better, because they allow users to pick and mix which features work for them. A good example of why this matters is the Markdown All In One extension we rely on: While it provides many of the things we need, a few of its features are incompatible with how I would like to work, and therefore it becomes a limiter of how well I can improve my own workflow.
|
||||
|
||||
However, there becomes a point where we may benefit from implementing a centralised solution, e.g. a syntax, an extension or perhaps a VSCode language server. As much as possible, we should allow users to operate in a decentralised manner.
|
||||
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ The potential solution:
|
||||
- This would be a GitHub action (or a local script, ran via foam-cli) that outputs publish-friendly markdown format for static site generators and other publishing tools
|
||||
- This build step should be pluggable, so that other transformations could be ran during it
|
||||
- Have publish targets defined in settings, that support both turning the link reference definitions on/off and defining their format (.md or not). Example draft (including also edit-time aspect):
|
||||
|
||||
```typescript
|
||||
// settings json
|
||||
// see enumerations below for explanations on values
|
||||
@@ -120,6 +121,7 @@ The potential solution:
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
- With Foam repo, just use edit-time link reference definitions with '.md' extension - this makes the links work in the Github UI
|
||||
- Have publish target defined for Github pages, that doesn't use '.md' extension, but still has the link reference definitions. Generate the output into gh-pages branch (or separate repo) with automation.
|
||||
- This naturally requires first removing the existing link reference definitions during the build
|
||||
|
||||
@@ -11,8 +11,7 @@ The idea would be to automatically generate lists of backlinks (and optionally,
|
||||
- Make every link two-way navigable in published sites
|
||||
- Make Foam notes more portable to different apps and long-term storage
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: todo.md "Todo"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -11,7 +11,7 @@ One of Foam's big features is the ability to find all instances of a reference,
|
||||
Implementing this is on the [[roadmap]], but for the time being you can achieve similar things by:
|
||||
|
||||
- `Cmd` + `Shift` + `F` ( `Ctrl` + `Shift` + `F` on Windows ) to find all the references, e.g. "Cat food"
|
||||
- `Cmd` + `Shift` + `H` ( `Ctrl` + `Shift` + `F` on Windows ) to replace them with [[cat-food]].
|
||||
- `Cmd` + `Shift` + `H` ( `Ctrl` + `Shift` + `H` on Windows ) to replace them with [[cat-food]].
|
||||
- Click any of the references to create a new note.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
- Write out a new `[[wiki-link]]` and `Cmd` + `Click` to create a new file and enter it.
|
||||
- For keyboard navigation, use the 'Follow Definition' key `F12` (or [remap key binding](https://code.visualstudio.com/docs/getstarted/keybindings) to something more ergonomic)
|
||||
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `New Note` from [VS Code Markdown Notes](https://marketplace.visualstudio.com/items?itemName=kortina.vscode-markdown-notes) and enter a **Title Case Name** to create `title-case-name.md`
|
||||
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `Foam: Create New Note` and enter a **Title Case Name** to create `Title Case Name.md`
|
||||
- Add a keyboard binding to make creating new notes easier.
|
||||
- The [[note-templates]] used by this command can be customized.
|
||||
- You shouldn't worry too much about categorizing your notes. You can always [[search-for-notes]], and explore them using the [[graph-visualisation]].
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[search-for-notes]: ../recipes/search-for-notes.md "Search for Notes"
|
||||
[graph-visualisation]: graph-visualisation.md "Graph Visualisation"
|
||||
[note-templates]: note-templates "Note Templates"
|
||||
[search-for-notes]: ../recipes/search-for-notes "Search for Notes"
|
||||
[graph-visualisation]: graph-visualisation "Graph Visualisation"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -14,8 +14,8 @@ By default, Daily Notes will be created in a file called `yyyy-mm-dd.md` in the
|
||||
|
||||
These settings can be overridden in your workspace or global `.vscode/settings.json` file, using the [**dateformat** date masking syntax](https://github.com/felixge/node-dateformat#mask-options):
|
||||
|
||||
```json
|
||||
"foam.openDailyNote.directory": "journal",
|
||||
```jsonc
|
||||
"foam.openDailyNote.directory": "journal", // a relative directory path will get appended to the workspace root. An absolute directory path will be used unmodified.
|
||||
"foam.openDailyNote.filenameFormat": "'daily-note'-yyyy-mm-dd",
|
||||
"foam.openDailyNote.fileExtension": "mdx",
|
||||
"foam.openDailyNote.titleFormat": "'Journal Entry, ' dddd, mmmm d",
|
||||
@@ -31,24 +31,19 @@ In the meantime, you can use [VS Code Snippets](https://code.visualstudio.com/do
|
||||
|
||||
## Roam-style Automatic Daily Notes
|
||||
|
||||
In the future, Foam may provide an option for automatically opening your Daily Note when you open your Foam workspace.
|
||||
|
||||
If you want this behavior now, you can use the excellent [Auto Run Command](https://marketplace.visualstudio.com/items?itemName=gabrielgrinberg.auto-run-command#review-details) extension to run the "Open Daily Note" command upon entering a Foam workspace by specifying the following configuration in your `.vscode/settings.json`:
|
||||
Foam provides an option for automatically opening your Daily Note when you open your Foam workspace. You can enable it by specifying the following configuration in your `.vscode/settings.json`:
|
||||
|
||||
```json
|
||||
"auto-run-command.rules": [
|
||||
{
|
||||
"condition": "hasFile: .vscode/foam.json",
|
||||
"command": "foam-vscode.open-daily-note",
|
||||
"message": "Have a nice day!"
|
||||
}
|
||||
],
|
||||
{
|
||||
// ...Other configurations
|
||||
"foam.openDailyNote.onStartup": true
|
||||
}
|
||||
```
|
||||
|
||||
## Extend Functionality (Weekly, Monthly, Quarterly Notes)
|
||||
|
||||
Please see [[note-macros]]
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[note-macros]: ../recipes/note-macros.md "Custom Note Macros"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//begin]: # 'Autogenerated link references for markdown compatibility'
|
||||
[note-macros]: ../recipes/note-macros.md 'Custom Note Macros'
|
||||
[//end]: # 'Autogenerated link references'
|
||||
|
||||
@@ -28,11 +28,11 @@ To enable the feature:
|
||||
|
||||
```
|
||||
{
|
||||
"experimental": {
|
||||
"localPlugins": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
"experimental": {
|
||||
"localPlugins": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
# Foam logging in VsCode
|
||||
|
||||
## Find the Foam log
|
||||
|
||||
The Foam log can be found in the `Output` tab.
|
||||
|
||||
1. To show the tab, click on `View > Output`.
|
||||
2. In the dropdown on the right of the tab, select `Foam`.
|
||||
|
||||

|
||||
|
||||
## Change the default logging level
|
||||
|
||||
1. Open workspace settings (`cmd+,`, or execute the `Preferences: Open Workspace Settings` command)
|
||||
2. Look for the entry `Foam > Logging: Level`
|
||||
|
||||
Set to debug when reporting an issue
|
||||
|
||||
## Change the log level for the session
|
||||
|
||||
Execute the command `Foam: Set log level`.
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Foam comes with a graph visualisation of your notes. To see the graph execute the `Foam: Show Graph` command.
|
||||
|
||||
The graph will:
|
||||
|
||||
- allow you to highlight a node by hovering on it, to quickly see how it's connected to the rest of your notes
|
||||
- allow you to select one or more (by keeping `SHIFT` pressed while selecting) nodes by clicking on them, to better understand the structure of your notes
|
||||
- allow you to navigate to a note by clicking on it while pressing `CTRL` or `CMD`
|
||||
@@ -32,6 +33,7 @@ A sample configuration object is provided below:
|
||||
```
|
||||
|
||||
### Style nodes by type
|
||||
|
||||
It is possible to customize the style of a node based on the `type` property in the YAML frontmatter of the corresponding document.
|
||||
|
||||
For example the following `backlinking.md` note:
|
||||
@@ -46,6 +48,7 @@ type: feature
|
||||
```
|
||||
|
||||
And the following `settings.json`:
|
||||
|
||||
```json
|
||||
"foam.graph.style": {
|
||||
"node": {
|
||||
@@ -57,6 +60,3 @@ And the following `settings.json`:
|
||||
Will result in the following graph:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,15 +7,19 @@ When you use `[[wiki-links]]`, the [foam-vscode](https://github.com/foambubble/f
|
||||
## Example
|
||||
|
||||
The following example:
|
||||
|
||||
```md
|
||||
- [[wiki-links]]
|
||||
- [[github-pages]]
|
||||
```
|
||||
|
||||
...generates the following link reference definitions to the bottom of the file:
|
||||
|
||||
```md
|
||||
[wiki-links]: wiki-links "Wiki Links"
|
||||
[github-pages]: github-pages "Github Pages"
|
||||
```
|
||||
|
||||
You can open the [raw markdown](https://foambubble.github.io/foam/features/link-reference-definitions.md) to see them at the bottom of this file
|
||||
|
||||
## Specification
|
||||
@@ -67,7 +71,6 @@ After changing the setting in your workspace, you can run the [[workspace-janito
|
||||
|
||||
See [[link-reference-definition-improvements]] for further discussion on current problems and potential solutions.
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[workspace-janitor]: workspace-janitor.md "Janitor"
|
||||
[todo]: ../dev/todo.md "Todo"
|
||||
|
||||
@@ -1,17 +1,149 @@
|
||||
# Note Templates
|
||||
|
||||
Foam supports note templates.
|
||||
Foam supports note templates. Templates are a way to customize the starting content for your notes (instead of always starting from an empty note).
|
||||
|
||||
Note templates live in `.foam/templates`. Run the `Foam: Create New Template` command from the command palette or create a regular `.md` file there to add a template.
|
||||
Note templates are files located in the special `.foam/templates` directory.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Create a template:
|
||||
|
||||
* Run the `Foam: Create New Template` command from the command palette
|
||||
* OR manually create a regular `.md` file in the `.foam/templates` directory
|
||||
|
||||

|
||||
|
||||
_Theme: Ayu Light_
|
||||
|
||||
To create a note from a template, execute the `Foam: Create New Note From Template` command and follow the instructions. Don't worry if you've not created a template yet! You'll be prompted to create a new template if none exist.
|
||||
To create a note from a template:
|
||||
|
||||
* Run the `Foam: Create New Note From Template` command and follow the instructions. Don't worry if you've not created a template yet! You'll be prompted to create a new template if none exist.
|
||||
* OR run the `Foam: Create New Note` command, which uses the special default template (`.foam/templates/new-note.md`, if it exists)
|
||||
|
||||

|
||||
|
||||
_Theme: Ayu Light_
|
||||
|
||||
## 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.
|
||||
|
||||
## 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. |
|
||||
|
||||
**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.
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
@@ -3,15 +3,22 @@
|
||||
Foam supports tags.
|
||||
|
||||
## Creating a tag
|
||||
|
||||
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.
|
||||
In the future it will be possible to explore tags via the graph as well.
|
||||
|
||||
## Styling tags
|
||||
Inline tags can be styled using custom CSS with the selector `.foam-tag`.
|
||||
|
||||
## An alternative to tags
|
||||
|
||||
Given the power of backlinks, some people prefer to use them also as tags.
|
||||
|
||||
@@ -5,10 +5,12 @@ To store your personal knowledge graph in markdown files instead of a database,
|
||||
**Foam Janitor** (inspired by Andy Matuschak's [note-link-janitor](https://github.com/andymatuschak/note-link-janitor)) helps you migrate existing notes to Foam, and maintain your Foam's health over time.
|
||||
|
||||
Currently, Foam's Janitor helps you to:
|
||||
|
||||
- Ensure your [[link-reference-definitions]] are up to date
|
||||
- Ensure every document has a well-formatted title (required for Markdown Links, Markdown Notes, and Foam Gatsby Template compatibility)
|
||||
|
||||
In the future, Janitor can help you with
|
||||
|
||||
- Updating [[materialized-backlinks]]
|
||||
- Lint, format and structure notes
|
||||
- Rename and move notes around while keeping their references up to date.
|
||||
|
||||
@@ -5,7 +5,7 @@ Uncategorised thoughts, to be added
|
||||
- Release notes
|
||||
- Markdown Preview
|
||||
- It's possible to customise the markdown preview styling. **Maybe make it use local foam workspace styles for live preview of the site??**
|
||||
- See: https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles
|
||||
- See: <https://marketplace.visualstudio.com/items?itemName=bierner.markdown-preview-github-styles>
|
||||
- Use VS Code [CodeTour](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) for onboarding
|
||||
- Investigate other similar extensions:
|
||||
- [Unotes](https://marketplace.visualstudio.com/items?itemName=ryanmcalister.Unotes)
|
||||
@@ -16,7 +16,7 @@ Uncategorised thoughts, to be added
|
||||
- Every Foam could have a different theme even in the editor, so you'll see it like they see it
|
||||
- UI and layout design of your workspace can become a thing
|
||||
- VS Code Notebooks API
|
||||
- https://code.visualstudio.com/api/extension-guides/notebook
|
||||
- <https://code.visualstudio.com/api/extension-guides/notebook>
|
||||
- Future architecture
|
||||
- Could we do publish-related settings as a pre-push git hook, e.g. generating footnote labels
|
||||
- Running them on Github Actions to edit stuff as it comes in
|
||||
@@ -32,5 +32,3 @@ Uncategorised thoughts, to be added
|
||||
- Maps have persistent topologies. As the graph grows, you should be able to visualise where an idea belongs. Maybe a literal map? And island? A DeckGL visualisation?
|
||||
|
||||
Testing: This file is served from the /docs directory.
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Like the soapy suds it's named after, **Foam** is mostly air.
|
||||
|
||||
1. The editing experience of **Foam** is powered by VS Code, enhanced by workspace settings that glue together [[recommended-extensions]] and preferences optimised for writing and navigating information.
|
||||
2. To back up, collaborate on and share your content between devices, Foam pairs well with [GitHub](http://github.com/).
|
||||
3. To publish your content, you can set it up to publish to [GitHub Pages](https://pages.github.com/) with zero code and zero config, or to any website hosting platform like [Netlify](http://netlify.com/) or [Vercel](https://vercel.com).
|
||||
3. To publish your content, you can set it up to publish to [GitHub Pages](https://pages.github.com/), or to any website hosting platform like [Netlify](http://netlify.com/) or [Vercel](https://vercel.com).
|
||||
|
||||
> **Fun fact**: This documentation was researched, written and published using **Foam**.
|
||||
|
||||
@@ -66,7 +66,7 @@ These instructions assume you have a GitHub account, and you have Visual Studio
|
||||
|
||||
2. [Clone the repository locally](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) and open it in VS Code.
|
||||
|
||||
*Open the repository as a folder using the `File > Open...` menu item. In VS Code, "open workspace" refers to [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces).*
|
||||
*Open the repository as a folder using the `File > Open...` menu item. In VS Code, "open workspace" refers to [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces).*
|
||||
|
||||
3. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one)
|
||||
|
||||
@@ -189,6 +189,19 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://www.nitwit.se"><img src="https://avatars.githubusercontent.com/u/1382124?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mark Dixon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nitwit-se" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/joeltjames"><img src="https://avatars.githubusercontent.com/u/3732400?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joel James</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joeltjames" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.ryo33.com"><img src="https://avatars.githubusercontent.com/u/8780513?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hashiguchi Ryo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ryo33" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://movermeyer.com"><img src="https://avatars.githubusercontent.com/u/1459385?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Overmeyer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=movermeyer" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/derrickqin"><img src="https://avatars.githubusercontent.com/u/3038111?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Derrick Qin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=derrickqin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/zomars/"><img src="https://avatars.githubusercontent.com/u/3504472?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Omar López</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=zomars" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://robincn.com"><img src="https://avatars.githubusercontent.com/u/1583193?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Robin King</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=RobinKing" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://twitter.com/deegovee"><img src="https://avatars.githubusercontent.com/u/4730170?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Dheepak </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dheepakg" title="Documentation">📖</a></td>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -203,7 +216,7 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
|
||||
## License
|
||||
|
||||
Foam is licensed under the [MIT license](license).
|
||||
Foam is licensed under the [MIT license](LICENSE.txt).
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[graph-visualisation]: features/graph-visualisation.md "Graph Visualisation"
|
||||
|
||||
@@ -13,6 +13,7 @@ Present: @jevakallio, @riccardoferretti
|
||||
|
||||
- Land work to master
|
||||
- Create a foam-core package
|
||||
|
||||
-
|
||||
|
||||
### Open questions
|
||||
|
||||
@@ -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
|
||||
@@ -48,7 +48,6 @@ While Foam uses tools popular among computer programmers, Foam should be inclusi
|
||||
- **Foam is not just for programmers.** If you're a programmer, feel free to write scripts and extensions to support your own workflow, and publish them for others to use, but the out of the box Foam experience should not require you to know how to do so. You should, however, be curious and open to adopting new tools that are unfamiliar to you, and evaluate whether they could work for you.
|
||||
- **Foam is for everyone** As a foam user, you support everyone's quest for knowledge and self-improvement, not only your own, or folks' who look like you. All participants in Foam repositories, discussion forums, physical and virtual meeting spaces etc are expected to respect each other as described in our [[code-of-conduct]]. **Foam is not for toxic tech bros.**
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[recipes]: recipes/recipes.md "Recipes"
|
||||
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
|
||||
|
||||
329
docs/proposals/templates-v2.md
Normal file
329
docs/proposals/templates-v2.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# Templates v2 Proposal <!-- omit in TOC -->
|
||||
|
||||
The current capabilities of templates is limited in some important ways. This document aims to propose a design that addresses these shortcomings.
|
||||
|
||||
**IMPORTANT: This design is merely a proposal of a design that could be implemented. It DOES NOT represent a commitment by `Foam` developers to implement the features outlined in this document. This document is merely a mechanism to facilitate discussion of a possible future direction for `Foam`.**
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Limitations of current templating](#limitations-of-current-templating)
|
||||
- [Too much friction to create a new note](#too-much-friction-to-create-a-new-note)
|
||||
- [Manual note creation (Mouse + Keyboard)](#manual-note-creation-mouse--keyboard)
|
||||
- [Manual note creation (Keyboard)](#manual-note-creation-keyboard)
|
||||
- [Foam missing note creation](#foam-missing-note-creation)
|
||||
- [`Markdown Notes: New Note` (Keyboard)](#markdown-notes-new-note-keyboard)
|
||||
- [Foam template note creation (Keyboard)](#foam-template-note-creation-keyboard)
|
||||
- [Templating of daily notes](#templating-of-daily-notes)
|
||||
- [Templating of filepaths](#templating-of-filepaths)
|
||||
- [Goal / Philosophy](#goal--philosophy)
|
||||
- [Proposal](#proposal)
|
||||
- [Summary](#summary)
|
||||
- [Add a `${title}` and `${titleSlug}` template variables](#add-a-title-and-titleslug-template-variables)
|
||||
- [Add a `Foam: Create New Note` command and hotkey](#add-a-foam-create-new-note-command-and-hotkey)
|
||||
- [Case 1: `.foam/templates/new-note.md` doesn't exist](#case-1-foamtemplatesnew-notemd-doesnt-exist)
|
||||
- [Case 2: `.foam/templates/new-note.md` exists](#case-2-foamtemplatesnew-notemd-exists)
|
||||
- [Change missing wikilinks to use the default template](#change-missing-wikilinks-to-use-the-default-template)
|
||||
- [Add a metadata section to templates](#add-a-metadata-section-to-templates)
|
||||
- [Example](#example)
|
||||
- [Add a replacement for `dateFormat`](#add-a-replacement-for-dateformat)
|
||||
- [Add support for daily note templates](#add-support-for-daily-note-templates)
|
||||
- [Eliminate all `foam.openDailyNote` settings](#eliminate-all-foamopendailynote-settings)
|
||||
- [Summary: resulting behaviour](#summary-resulting-behaviour)
|
||||
- [`Foam: Create New Note`](#foam-create-new-note)
|
||||
- [`Foam: Open Daily Note`](#foam-open-daily-note)
|
||||
- [Navigating to missing wikilinks](#navigating-to-missing-wikilinks)
|
||||
- [`Foam: Create Note From Template`](#foam-create-note-from-template)
|
||||
- [Extensions](#extensions)
|
||||
- [More variables in templates](#more-variables-in-templates)
|
||||
- [`defaultFilepath`](#defaultfilepath)
|
||||
- [Arbitrary hotkey -> template mappings?](#arbitrary-hotkey---template-mappings)
|
||||
|
||||
## Introduction
|
||||
|
||||
Creating of new notes in Foam is too cumbersome and slow. Despite their power, Foam templates can currently only be used in very limited scenarios.
|
||||
|
||||
This proposal aims to address these issues by streamlining note creation and by allowing templates to be used everywhere.
|
||||
|
||||
## Limitations of current templating
|
||||
|
||||
### Too much friction to create a new note
|
||||
|
||||
Creating new notes should an incredibly streamlined operation. There should be no friction to creating new notes.
|
||||
|
||||
Unfortunately, all of the current methods for creating notes are cumbersome.
|
||||
|
||||
#### Manual note creation (Mouse + Keyboard)
|
||||
|
||||
1. Navigate to the directory where you want the note
|
||||
2. Click the new file button
|
||||
3. Provide a filename
|
||||
4. Manually enter the template contents you want
|
||||
|
||||
#### Manual note creation (Keyboard)
|
||||
|
||||
1. Navigate to the directory where you want the note
|
||||
2. `⌘N` to create a new file
|
||||
3. `⌘S` to save the file and give it a filename
|
||||
4. Manually enter the template contents you want
|
||||
|
||||
#### Foam missing note creation
|
||||
|
||||
1. Open an existing note in the directory where you want the note
|
||||
2. Use the wikilinks syntax to create a link to the title of the note you want to have
|
||||
3. Use `Ctrl+Click`/`F12` to create the new file
|
||||
4. Manually enter the template contents you want
|
||||
|
||||
#### `Markdown Notes: New Note` (Keyboard)
|
||||
|
||||
1. Navigate to the directory where you want the note
|
||||
2. `Shift+⌘P` to open the command pallette
|
||||
3. Type `New Note` until it appears in the list. Press `Enter/Return` to select it.
|
||||
4. Enter a title for the note
|
||||
5. Manually enter the template contents you want
|
||||
|
||||
#### Foam template note creation (Keyboard)
|
||||
|
||||
1. `Shift+⌘P` to open the command pallette
|
||||
2. Type `Create New Note From Template` until it appears in the list. Press `Enter/Return` to select it.
|
||||
3. Use the arrow keys (or type the template name) to select the template. Press `Enter/Return` to select it.
|
||||
4. Modify the filepath to match the desired directory + filename. Press `Enter/Return` to select it.
|
||||
|
||||
All of these steps are far too cumbersome. And only the last one allows the use of templates.
|
||||
|
||||
### Templating of daily notes
|
||||
|
||||
Currently `Open Daily Note` opens an otherwise empty note, with a title defined by the `foam.openDailyNote.titleFormat` setting.
|
||||
Daily notes should be able to be fully templated as well.
|
||||
|
||||
### Templating of filepaths
|
||||
|
||||
As discussed in ["Template the filepath in `openDailyNote`"](https://github.com/foambubble/foam/issues/523), it would be useful to be able to specify the default filepaths of templates. For example, many people include timestamps in their filepaths.
|
||||
|
||||
## Goal / Philosophy
|
||||
|
||||
In a sentence: **Creating a new note should be a single button press and should use templates.**
|
||||
|
||||
## Proposal
|
||||
|
||||
1. Add a new `Foam: Create New Note` that is the streamlined counterpart to the more flexible `Foam: Create New Note From Template`
|
||||
2. Use templates everywhere
|
||||
3. Add metadata into the actual templates themselves in order to template the filepaths themselves.
|
||||
|
||||
### Summary
|
||||
|
||||
This can be done through a series of changes to the way that templates are implemented:
|
||||
|
||||
1. Add a `${title}` and `${titleSlug}` template variables
|
||||
2. Add a `Foam: Create New Note` command and hotkey
|
||||
3. Change missing wikilinks to use the default template
|
||||
4. Add a metadata section to templates
|
||||
5. Add a replacement for `dateFormat`
|
||||
6. Add support for daily note templates
|
||||
7. Eliminate all `foam.openDailyNote` settings
|
||||
|
||||
I've broken it out into these steps to show that the overall proposal can be implemented piecemeal in independent PRs that build on one another.
|
||||
|
||||
### Add a `${title}` and `${titleSlug}` template variables
|
||||
|
||||
When you use `Markdown Notes: New Note`, and give it a title, the title is formatted as a filename and also used as the title in the resulting note.
|
||||
|
||||
**Example:**
|
||||
|
||||
Given the title `Living in a dream world` to `Markdown Notes: New Note`, the filename is `living-in-a-dream-world.md` and the file contents are:
|
||||
|
||||
```markdown
|
||||
# Living in a dream world
|
||||
```
|
||||
|
||||
When creating a note from a template in Foam, you should be able to use a `${title}` variable. If the template uses the `${title}` variable, the user will be prompted for a title when they create a note from a template.
|
||||
|
||||
Example:
|
||||
|
||||
Given this `.foam/templates/my_template.md` template that uses the `${title}` variable:
|
||||
|
||||
```markdown
|
||||
# ${title}
|
||||
```
|
||||
|
||||
When a user asks for a new note using this template (eg. `Foam: Create New Note From Template`), VSCode will first ask the user for a title then provide it to the template, producing:
|
||||
|
||||
```markdown
|
||||
# Living in a dream world
|
||||
```
|
||||
|
||||
There will also be a `${titleSlug}` variable made available, which will be the "slugified" version of the title (eg. `living-in-a-dream-world`). This will be useful in later steps where we want to template the filepath of a template.
|
||||
|
||||
### Add a `Foam: Create New Note` command and hotkey
|
||||
|
||||
Instead of using `Markdown Notes: New Note`, Foam itself will have a `Create New Note` command that creates notes using templates.
|
||||
|
||||
This would open use the template found at `.foam/templates/new-note.md` to create the new note.
|
||||
|
||||
`Foam: Create New Note` will offer the fastest workflow for creating a note when you don't need customization, while `Foam: Create New Note From Template` will remain to serve a fully customizable (but slower) workflow.
|
||||
|
||||
#### Case 1: `.foam/templates/new-note.md` doesn't exist
|
||||
|
||||
If `.foam/templates/new-note.md` doesn't exist, it behaves the same as `Markdown Notes: New Note`:
|
||||
|
||||
* it would ask for a title and create the note in the current directory. It would open a note with the note containing the title.
|
||||
|
||||
**Note:** this would use an implicit default template, making use of the `${title}` variable.
|
||||
|
||||
#### Case 2: `.foam/templates/new-note.md` exists
|
||||
|
||||
If `.foam/templates/new-note.md` exists:
|
||||
|
||||
* it asks for the note title and creates the note in the current directory
|
||||
|
||||
**Progress:** At this point, we have a faster way to create new notes from templates.
|
||||
|
||||
### Change missing wikilinks to use the default template
|
||||
|
||||
Clicking on a dangling/missing wikilink should be equivalent to calling `Foam: Create New Note` with the contents of the link as the title.
|
||||
That way, creating a note by navigating to a missing note uses the default template.
|
||||
|
||||
### Add a metadata section to templates
|
||||
|
||||
* The `Foam: New Note` command creates a new note in the current directory. This is a sensible default that makes it quick, but lacks flexibility.
|
||||
* The `Foam: Create New Note From Template` asks the user to confirm/customize the filepath. This is more flexible but slower since there are more steps involved.
|
||||
|
||||
Both commands use templates. It would be nice if we could template the filepaths as well as the template contents (See ["Template the filepath in `openDailyNote`"](https://github.com/foambubble/foam/issues/523) for a more in-depth discussion the benefits of filepath templating).
|
||||
|
||||
In order to template the filepath, there needs to be a place where metadata like this can be specified.
|
||||
I think this metadata should be stored alongside the templates themselves. That way, it can make use of all the same template variable available to the templates themselves.
|
||||
|
||||
Conceptually, adding metadata to the templates is similar to Markdown frontmatter, though the choice of exact syntax for adding this metadata will have to be done with care since the templates can contain arbitrary contents including frontmatter.
|
||||
|
||||
#### Example
|
||||
|
||||
A workable syntax is still to be determined.
|
||||
While this syntax probably doesn't work as a solution, for this example I will demonstrate the concept using a second frontmatter block:
|
||||
|
||||
```markdown
|
||||
<!-- The below front-matter block is for foam-specific template settings -->
|
||||
<!-- It is removed when the user creates a new note using this template -->
|
||||
---
|
||||
<!-- The default filepath to use when using this template -->
|
||||
<!-- Relative paths are relative to the workspace, absolute paths are absolute -->
|
||||
<!-- Note that you can include VSCode snippet variables to template the path -->
|
||||
filepath: `journal/${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}_${titleSlug}.md`
|
||||
---
|
||||
|
||||
<!-- The actual contents of the template begin after the `---` thematic break immediately below this line-->
|
||||
---
|
||||
---
|
||||
created: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}T${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}
|
||||
tags: []
|
||||
---
|
||||
|
||||
# ${title}
|
||||
```
|
||||
|
||||
In this example, using this template improves the UX:
|
||||
|
||||
In `Foam: Create New Note` workflow, having `filepath` metadata within `.foam/templates/new-note.md` allows for control over the filepath without having to introduce any more UX steps to create a new note. It's still just a hotkey away and a title.
|
||||
|
||||
As we'll see, when it comes to allowing daily notes to be templated, we don't even need to use `${title}` in our template, in which case we don't we don't even need to prompt for a title.
|
||||
|
||||
In the `Create New Note From Template` workflow, during the step where we allow the user to customize the filepath, it will already templated according to the `filepath` in the template's metadata. This means that the user has to make fewer changes to the path, especially in cases where they want to include things like datetimes in the filenames. This makes it faster (eg. don't have to remember what day it is, and don't have to type it) and less error-prone (eg. when they accidentally type the wrong date).
|
||||
|
||||
### Add a replacement for `dateFormat`
|
||||
|
||||
`foam.openDailyNote.filenameFormat` uses `dateFormat()` to put the current timestamp into the daily notes filename. This is much more flexible than what is available in VSCode Snippet variables. Before daily notes are switched over to use templates, we will have to come up with another mechanism/syntax to allow for calls to `dateFormat()` within template files.
|
||||
|
||||
This would be especially useful in the migration of users to the new daily notes templates. For example, if `.foam/templates/daily-note.md` is unset, then we could generate an implicit template for use by `Foam: Open Daily Note`. Very roughly something like:
|
||||
|
||||
```markdown
|
||||
<!-- The below front-matter block is for foam-specific template settings -->
|
||||
<!-- It is removed when the user creates a new note using this template -->
|
||||
---
|
||||
<!-- The default filepath to use when using this template -->
|
||||
<!-- Relative paths are relative to the workspace, absolute paths are absolute -->
|
||||
<!-- Note that you can include VSCode snippet variables to template the path -->
|
||||
filepath: `${foam.openDailyNote.directory}/${foam.openDailyNote.filenameFormat}.${foam.openDailyNote.fileExtension}`
|
||||
---
|
||||
|
||||
<!-- The actual contents of the template begin after the `---` thematic break immediately below this line-->
|
||||
---
|
||||
# ${foam.openDailyNote.titleFormat}
|
||||
```
|
||||
|
||||
### Add support for daily note templates
|
||||
|
||||
With the above features implemented, making daily notes use templates is simple.
|
||||
|
||||
We define a `.foam/templates/daily-note.md` filepath that the `Foam: Open Daily Note` command will always use to find its daily note template.
|
||||
If `.foam/templates/daily-note.md` does not exist, it falls back to a default, implicitly defined daily notes template (which follows the default behaviour of the current `foam.openDailyNote` settings).
|
||||
|
||||
Both `Foam: Open Daily Note` and `Foam: Create New Note` can share all of the implementation code, with the only differences being the hotkeys used and the template filepath used.
|
||||
|
||||
Example daily note template (again using the example syntax of the foam-specific frontmatter block):
|
||||
|
||||
```markdown
|
||||
<!-- The below front-matter block is for foam-specific template settings -->
|
||||
<!-- It is removed when the user creates a new note using this template -->
|
||||
---
|
||||
<!-- The default filepath to use when using this template -->
|
||||
<!-- Relative paths are relative to the workspace, absolute paths are absolute -->
|
||||
<!-- Note that you can include VSCode snippet variables to template the path -->
|
||||
filepath: `journal/${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}.md`
|
||||
---
|
||||
|
||||
<!-- The actual contents of the template begin after the `---` thematic break immediately below this line-->
|
||||
---
|
||||
# ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}
|
||||
```
|
||||
|
||||
Since there is no use of the `${title}` variable, opening the daily note behaves exactly as it does today and automatically opens the note with no further user interaction.
|
||||
|
||||
### Eliminate all `foam.openDailyNote` settings
|
||||
|
||||
Now that all of the functionality of the `foam.openDailyNote` settings have been obviated, these settings can be removed:
|
||||
|
||||
* `foam.openDailyNote.directory`, `foam.openDailyNote.filenameFormat`, and `foam.openDailyNote.fileExtension` can be specified in the `filepath` metadata of the daily note template.
|
||||
* `foam.openDailyNote.titleFormat` has been replaced by the ability to fully template the daily note, including the title.
|
||||
|
||||
## Summary: resulting behaviour
|
||||
|
||||
### `Foam: Create New Note`
|
||||
|
||||
A new command optimized for speedy creation of new notes. This will become the default way to create new notes. In its fastest form, it simply opens the new note with no further user interaction.
|
||||
|
||||
### `Foam: Open Daily Note`
|
||||
|
||||
Simplified since it no longer has its custom settings, and re-uses all the same implementation code as `Foam: Create New Note`.
|
||||
Templates can now be used with daily notes.
|
||||
|
||||
### Navigating to missing wikilinks
|
||||
|
||||
Now creates the new notes using the default template. Re-uses all the same implementation code as `Foam: Create New Note`
|
||||
Now uses the contents of the wikilink as the `${title}` parameter for the template.
|
||||
|
||||
### `Foam: Create Note From Template`
|
||||
|
||||
Almost the exact same as it is today. However, with `${title}` and `filepath` templating, users will have less changes to make in the filepath confirmation step.
|
||||
It's the slower but more powerful version of `Foam: Create New Note`, allowing you to pick any template, as well as customize the filepath.
|
||||
|
||||
## Extensions
|
||||
|
||||
In addition to the ideas of this proposal, there are ways we could imagine extending it. These are all "out of scope" for this design, but thinking about them could be useful to guide our thinking about this design.
|
||||
|
||||
### More variables in templates
|
||||
|
||||
`${title}` is necessary in this case to replace the functionality of `Markdown Notes: New Note`.
|
||||
However, one could imagine that this pattern of "Ask the user for a value for missing variable values" could be useful in other situations too.
|
||||
Perhaps users could even define their own (namespaced) template variables, and Foam would ask them for values to use for each when creating a note using a template that used those variables.
|
||||
|
||||
### `defaultFilepath`
|
||||
|
||||
By using `defaultFilepath` instead of `filepath` in the metadata section, you could have more control over the note creation without having to fall back to the full `Create New Note From Template` workflow.
|
||||
|
||||
* `filepath` will not ask the user for the file path, simply use the value provided (as described above)
|
||||
* `defaultFilepath` will ask the user for the file path, pre-populating the file path using `defaultFilepath`
|
||||
|
||||
The first allows "one-click" note creation, the second more customization.
|
||||
This might not be necessary, or this might not be the right way to solve the problem. We'll see.
|
||||
|
||||
### Arbitrary hotkey -> template mappings?
|
||||
|
||||
`Foam: Open Daily Note` and `Foam: Create New Note` only differ by their hotkey and their default template setting.
|
||||
Is there a reason/opportunity to abstract this further and allow for users to define custom `hotkey -> template` mappings?
|
||||
@@ -5,11 +5,13 @@
|
||||
You can use [foam-gatsby-template](https://github.com/mathieudutour/foam-gatsby-template) to generate a static site to host it online on Github or [Vercel](https://vercel.com).
|
||||
|
||||
### Publishing your foam to GitHub pages
|
||||
|
||||
It comes configured with Github actions to auto deploy to Github pages when changes are pushed to your main branch.
|
||||
|
||||
### Publishing your foam to Vercel
|
||||
|
||||
When you're ready to publish, run a local build.
|
||||
|
||||
```bash
|
||||
cd _layouts
|
||||
npm run build
|
||||
|
||||
@@ -11,14 +11,15 @@ The following recipe is written with the assumption that you already have an [Az
|
||||
1. Generate a Foam workspace using the [foam-template project](https://github.com/foambubble/foam-template).
|
||||
2. Change the remote to a git repository in Azure DevOps (Repos -> Import a Repository -> Add Clone URL with Authentication), or copy all the files into a new Azure DevOps git repository.
|
||||
3. Define which document will be the wiki home page. To do that, create a file called `.order` in the Foam workspace root folder, with first line being the document filename without `.md` extension. For a project created from the Foam template, the file would look like this:
|
||||
|
||||
```
|
||||
readme
|
||||
```
|
||||
|
||||
4. Push the repository to remote in Azure DevOps.
|
||||
|
||||
## Publish repository to a wiki
|
||||
|
||||
|
||||
1. Navigate to your Azure DevOps project in a web browser.
|
||||
2. Choose **Overview** > **Wiki**. If you don't have wikis for your project, choose **Publish code as a wiki** on welcome page.
|
||||
3. Choose repository with your Foam workspace, branch (usually `master` or `main`), folder (for workspace created from foam-template it is `/`), and wiki name, and press **Publish**.
|
||||
@@ -40,6 +41,7 @@ While you are pushing changes to GitHub, you won't see the wiki updated if you d
|
||||
3. You can then add the remote for your second remote repository, in this case, Azure. e.g `git remote add azure https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes`. You can get it from: Repos->Files->Clone and copy the URL.
|
||||
4. Now, you need to set up your origin remote to push to both of these. So run: `git config -e` and edit it.
|
||||
5. Add the `remote origin` section to the bottom of the file with the URLs from each remote repository you'd like to push to. You'll see something like that:
|
||||
|
||||
```bash
|
||||
[core]
|
||||
...
|
||||
@@ -58,6 +60,7 @@ While you are pushing changes to GitHub, you won't see the wiki updated if you d
|
||||
url = git@github.com:username/repo.git
|
||||
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
|
||||
```
|
||||
|
||||
6. You can then push to both repositories by: `git push origin master` or a single one using: `git push github master` or `git push azure master`
|
||||
|
||||
For more information, read the [Azure DevOps documentation](https://docs.microsoft.com/en-us/azure/devops/project/wiki/publish-repo-to-wiki).
|
||||
|
||||
@@ -1,42 +1,49 @@
|
||||
# Github Pages
|
||||
|
||||
- The [Foam template](https://github.com/foambubble/foam-template) is **GitHub Pages** ready, all you have to do is [turn it on in your repository settings](https://guides.github.com/features/pages/).
|
||||
- In VSCode workspace settings set `"foam.edit.linkReferenceDefinitions": "withoutExtensions"`
|
||||
- Execute the “Foam: Run Janitor” command from the command palette.
|
||||
- [Turn **GitHub Pages** on in your repository settings](https://guides.github.com/features/pages/).
|
||||
- The default GitHub Pages template is called [Primer](https://github.com/pages-themes/primer). See Primer docs for how to customise html layouts and templates.
|
||||
- GitHub Pages is built on [Jekyll](https://jekyllrb.com/), so it supports things like permalinks, front matter metadata etc.
|
||||
|
||||
## How to publish locally
|
||||
|
||||
If you want to test your published foam, follow the instructions:
|
||||
- https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll
|
||||
- https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/testing-your-github-pages-site-locally-with-jekyll
|
||||
|
||||
- <https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll>
|
||||
- <https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/testing-your-github-pages-site-locally-with-jekyll>
|
||||
|
||||
Assuming you have installed ruby/jekyll and the rest:
|
||||
|
||||
- `touch Gemfile`
|
||||
- open the file and paste the following:
|
||||
|
||||
```
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem "github-pages", "VERSION"
|
||||
```
|
||||
replacing `VERSION` with the latest from https://rubygems.org/gems/github-pages (e.g. `gem "github-pages", "209"`)
|
||||
|
||||
replacing `VERSION` with the latest from <https://rubygems.org/gems/github-pages> (e.g. `gem "github-pages", "209"`)
|
||||
|
||||
- `bundle`
|
||||
- `bundle exec jekyll 3.9.0 new .`
|
||||
- edit the `Gemfile` according to the instructions at [Creating Your Site](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/creating-a-github-pages-site-with-jekyll#creating-your-site) Point n.8
|
||||
- `bundle exec jekyll serve`
|
||||
|
||||
|
||||
## Other templates
|
||||
|
||||
There are many other templates which also support publish your foam workspace to github pages
|
||||
|
||||
* gatsby-digital-garden
|
||||
* [repo](https://github.com/mathieudutour/gatsby-digital-garden)
|
||||
* [demo-website](https://mathieudutour.github.io/gatsby-digital-garden/)
|
||||
* [repo](https://github.com/mathieudutour/gatsby-digital-garden)
|
||||
* [demo-website](https://mathieudutour.github.io/gatsby-digital-garden/)
|
||||
* foam-mkdocs-template
|
||||
* [repo](https://github.com/Jackiexiao/foam-mkdocs-template)
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* [repo](https://github.com/Jackiexiao/foam-mkdocs-template)
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* foam-jekyll-template
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://hikerpig.github.io/foam-jekyll-template/)
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://hikerpig.github.io/foam-jekyll-template/)
|
||||
|
||||
[[todo]] [[good-first-task]] Improve this documentation
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ The standard [foam-template](https://github.com/foambubble/foam-template) is rea
|
||||
## Enable navigation in GitHub
|
||||
|
||||
To allow navigation from within the GitHub repo, make sure to generate the link references, by setting
|
||||
|
||||
- `Foam › Edit: Link Reference Definitions` -> `withExtensions`
|
||||
|
||||
See [[link-reference-definitions]] for more information.
|
||||
|
||||
## Customising the style
|
||||
|
||||
You can edit `assets/css/style.scss` to change how published pages look.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
@@ -11,6 +11,7 @@ Generate a solution using the [Foam template].
|
||||
Change the remote to GitLab, or copy all the files into a new GitLab repo.
|
||||
|
||||
### Add a _config.yaml
|
||||
|
||||
Add another file to the root directory (the one with `readme.md` in it) called `_config.yaml` (no extension)
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
You can use [foam-eleventy-template](https://github.com/juanfrank77/foam-eleventy-template) to generate a static site with [Eleventy](https://www.11ty.dev/), and host it online on [Netlify](https://www.netlify.com/).
|
||||
|
||||
With this template you can
|
||||
|
||||
- Have control over what to publish and what to keep private
|
||||
- Customize the styling of the site to your own liking
|
||||
|
||||
@@ -12,8 +13,6 @@ When you're ready to publish, import the GitHub repository you created with **fo
|
||||
|
||||
Once that's done, all you have to do is make changes to your workspace in VS Code and push them to the main branch on GitHub. Netlify will recognize the changes, deploy them automatically and give you a link where your Foam is published.
|
||||
|
||||
|
||||
That's it!
|
||||
|
||||
You can now see it online and use that link to share it with your friends, so that they can see it too.
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
- [VSCode Extensions Packs](https://code.visualstudio.com/blogs/2017/03/07/extension-pack-roundup) [[todo]] Evaluate for deployment
|
||||
- [Dark mode](https://css-tricks.com/dark-modes-with-css/)
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -5,7 +5,7 @@ You can also easily manipulate the git history to reduce clutter.
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- [GitDoc](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc)
|
||||
- [GitDoc](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc)
|
||||
|
||||
## Instructions
|
||||
|
||||
@@ -18,9 +18,6 @@ __For Foam specific needs, you can add a comment here by following the [[contrib
|
||||
- Feedback and issues with the extension should be reported to the authors themselves
|
||||
- Feedback and issues with the integration of the extension in Foam can be reported in our [issue tracker](https://github.com/foambubble/foam/issues)
|
||||
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[contribution-guide]: ../contribution-guide.md "Contribution Guide"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -4,7 +4,7 @@ With this #recipe you can convert a link to a fully-formed Markdown link, using
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- [Markdown Link Expander](https://marketplace.visualstudio.com/items?itemName=skn0tt.markdown-link-expander) (not included in template)
|
||||
- [Markdown Link Expander](https://marketplace.visualstudio.com/items?itemName=skn0tt.markdown-link-expander) (not included in template)
|
||||
|
||||
Markdown Link Expander will scrape your URL's `<title>` tag to create a nice Markdown-style link.
|
||||
|
||||
@@ -22,4 +22,3 @@ Tip: If you paste a lot of links, give the action a custom [key binding](https:/
|
||||
## Feedback and issues
|
||||
|
||||
Have an idea for the extension? [Feel free to share! 🎉](https://github.com/Skn0tt/markdown-link-expander/issues)
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ With this #recipe you can create notes on your iOS device, which will automatica
|
||||
|
||||
## Instructions
|
||||
|
||||
|
||||
1. Setup the [`foam-capture-action`]() in your GitHub Repository, to be triggered by "Workflow dispatch" events.
|
||||
1. Setup the [`foam-capture-action`]() in your GitHub Repository, to be triggered by "Workflow dispatch" events.
|
||||
|
||||
```
|
||||
name: Manually triggered workflow
|
||||
@@ -26,15 +25,17 @@ on:
|
||||
data:
|
||||
description: 'What information to put in the knowledge base.'
|
||||
required: true
|
||||
|
||||
|
||||
jobs:
|
||||
store_data:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: anglinb/foam-capture-action@main
|
||||
with:
|
||||
with:
|
||||
{% raw %}
|
||||
capture: ${{ github.event.inputs.data }}
|
||||
{% endraw %}
|
||||
- run: |
|
||||
git config --local user.email "example@gmail.com"
|
||||
git config --local user.name "Your name"
|
||||
@@ -44,14 +45,16 @@ jobs:
|
||||
|
||||
2. In GitHub [create a Personal Access Token](https://github.com/settings/tokens) and give it `repo` scope - make a note of the token
|
||||
3. Run this command to find your `workflow-id` to be used in the Shortcut.
|
||||
|
||||
```bash
|
||||
curl \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
-H "Authorization: Bearer <GITHUB_TOKEN>" \
|
||||
https://api.github.com/repos/<owner>/<repository>/actions/workflows
|
||||
```
|
||||
|
||||
4. Copy this [Shortcut](https://www.icloud.com/shortcuts/57d2ed90c40e43a5badcc174ebfaaf1d) to your iOS devices and edit the contents of the last step, `GetContentsOfURL`
|
||||
- Make sure you update the URL of the shortcut step with the `owner`, `repository`, `workflow-id` (from the previous step)
|
||||
- Make sure you update the headers of the shortcut step, replaceing `[GITHUB_TOKEN]` with your Personal Access Token (from step 2)
|
||||
- Make sure you update the headers of the shortcut step, replaceing `[GITHUB_TOKEN]` with your Personal Access Token (from step 2)
|
||||
|
||||
5. Run the shortcut & celebrate! ✨ (You should see a GitHub Action run start and the text you entered show up in `inbox.md` in your repository.)
|
||||
|
||||
@@ -7,7 +7,6 @@ We have two alternative #recipe for displaying diagrams in markdown:
|
||||
- [Draw.io](#drawio)
|
||||
- [Using Draw.io](#using-drawio)
|
||||
|
||||
|
||||
## Mermaid
|
||||
|
||||
You can use [Mermaid](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) plugin to draw and preview diagrams in your content.
|
||||
@@ -27,7 +26,6 @@ You can use [Mermaid](https://marketplace.visualstudio.com/items?itemName=bierne
|
||||
3. Start drawing your diagram. Once you done, save it.
|
||||
4. Embed the diagram file as you embedding the image file, for example: ``
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[publish-to-github-pages]: ../publishing/publish-to-github-pages.md "Github Pages"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
This is an example of how to structure a Recipe. The first paragraph or two should explain the purpose of the recipe succinctly, including why it's useful, if that's not obvious.
|
||||
|
||||
Recipes are intended to document:
|
||||
|
||||
- How to use Foam's basic features
|
||||
- Power user pro-tips
|
||||
- Useful customisations of the default Foam environment
|
||||
@@ -10,8 +11,8 @@ Recipes are intended to document:
|
||||
|
||||
## Required Extensions
|
||||
|
||||
- **[Hacker Typer](https://marketplace.visualstudio.com/items?itemName=jevakallio.vscode-hacker-typer)** (not really required for this recipe, just an example)
|
||||
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (installed by default)
|
||||
- **[Hacker Typer](https://marketplace.visualstudio.com/items?itemName=jevakallio.vscode-hacker-typer)** (not really required for this recipe, just an example)
|
||||
- [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) (installed by default)
|
||||
|
||||
The first section should be a bulleted list of extensions required to use this recipe. At a minimum, this section should list all additional, non-standard extensions.
|
||||
|
||||
|
||||
@@ -16,11 +16,6 @@ In the future we'll want to improve this feature by
|
||||
- Make back links editable using [VS Code Search Editors](https://code.visualstudio.com/updates/v1_43#_search-editors)
|
||||
- [Suggested by @Jash on Discord](https://discordapp.com/channels/729975036148056075/729978910363746315/730999992419876956)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[materialized-backlinks]: ../dev/materialized-backlinks.md "Materialized Backlinks (stub)"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
If you're interested in working on it, please start a conversation in [GitHub issues](https://github.com/foambubble/foam/issues).
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: ../dev/todo.md "Todo"
|
||||
[roadmap]: ../dev/roadmap.md "Roadmap"
|
||||
|
||||
@@ -56,6 +56,3 @@ in `keybindings.json` (Code|File > Preferences > Keyboard Shortcuts) add binding
|
||||
If you have any issues or questions please look at the [README.md](https://github.com/kneely/note-macros#note-macros) on the [note-macros](https://github.com/kneely/note-macros) GitHub.
|
||||
|
||||
If you run into any issues that are not fixed by referring to the README or feature requests please open an [issue](https://github.com/kneely/note-macros/issues).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Real-time Collaboration
|
||||
|
||||
This #recipe is here to just tell you that VS Code Live Share will allow you to collaborate live on your notes.
|
||||
|
||||
|
||||
@@ -26,14 +26,17 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
- Clip webpages with [[web-clipper]]
|
||||
|
||||
## Discover
|
||||
|
||||
- Explore your notes using [[graph-visualisation]]
|
||||
- Discover relationships with [[backlinking]]
|
||||
- Simulating [[unlinked-references]]
|
||||
|
||||
## Organise
|
||||
|
||||
- Using [[backlinking]] for reference lists.
|
||||
|
||||
## Write
|
||||
|
||||
- Link documents with [[wiki-links]].
|
||||
- Use shortcuts for [[creating-new-notes]]
|
||||
- Instantly create and access your [[daily-notes]]
|
||||
|
||||
@@ -9,6 +9,7 @@ The most promising options are:
|
||||
### [GitJournal](https://gitjournal.io/)
|
||||
|
||||
Pros
|
||||
|
||||
- Open source
|
||||
- Already a usable solution.
|
||||
- Provides functionality to edit, create, and browser markdown files.
|
||||
@@ -19,6 +20,7 @@ Pros
|
||||
- Developer is happy to prioritize Foam compatibility
|
||||
|
||||
Cons
|
||||
|
||||
- Doesn't generate link reference lists (but this is ok, since [[workspace-janitor]] as a GitHub action can solve this)
|
||||
- Not as sleek as Apple/Google notes, some keyboard state glitching on Android, etc.
|
||||
- Lack of control over roadmap. Established product with a paid plan, so may not be open to Foam-supportive changes and additions that don't benefit most users.
|
||||
@@ -28,13 +30,15 @@ Verdict: Good. By far best effort/outcome ratio would be to help improve GitJour
|
||||
### GitHub Codespaces
|
||||
|
||||
Pros
|
||||
|
||||
- Works out of the box just like the desktop app
|
||||
|
||||
Cons
|
||||
|
||||
- not generally available quite yet
|
||||
- [Pricing](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/about-billing-for-codespaces)
|
||||
|
||||
For a quick demo, see https://www.youtube.com/watch?v=KI5m4Uy8_4I.
|
||||
For a quick demo, see <https://www.youtube.com/watch?v=KI5m4Uy8_4I>.
|
||||
|
||||
Verdict: Good. Pricing should be reasonable for taking notes on the fly. Harder to assess for people who would constantly use Foam from mobile phone.
|
||||
|
||||
@@ -54,7 +58,6 @@ If such an app was worth building, it would have to have the following features:
|
||||
|
||||
Given the effort vs reward ratio, it's a low priority for core team, but if someone wants to work on this, we can provide support! Talk to us on the #mobile-apps channel on [Foam Discord](https://foambubble.github.io/join-discord/w).
|
||||
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[build-vs-assemble]: ../dev/build-vs-assemble.md "Build vs Assemble"
|
||||
[workspace-janitor]: ../features/workspace-janitor.md "Janitor"
|
||||
|
||||
@@ -9,7 +9,6 @@ This list is subject to change. Especially the Git ones.
|
||||
- [Markdown Links](https://marketplace.visualstudio.com/items?itemName=tchayen.markdown-links)
|
||||
- [Markdown All In One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one)
|
||||
|
||||
|
||||
## Extensions For Additional Features
|
||||
|
||||
These extensions are not (yet?) defined in `.vscode/extensions.json`, but have been used by others and shown to play nice with Foam.
|
||||
@@ -22,3 +21,4 @@ These extensions are not (yet?) defined in `.vscode/extensions.json`, but have b
|
||||
- [Git Lens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens)
|
||||
- [Markdown Extended](https://marketplace.visualstudio.com/items?itemName=jebbs.markdown-extended) (with `kbd` option disabled, `kbd` turns wiki-links into non-clickable buttons)
|
||||
- [GitDoc](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc) (easy version management via git auto commits)
|
||||
- [Markdown Footnotes](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-footnotes) (Adds [^footnote] syntax support to VS Code's built-in markdown preview)
|
||||
|
||||
@@ -17,4 +17,3 @@ Also happens to sound quite a lot like Home. Funny, that.
|
||||
## Bubble
|
||||
|
||||
Individual Foam note, written in Markdown.
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.10.2"
|
||||
"version": "0.13.6"
|
||||
}
|
||||
|
||||
@@ -40,5 +40,6 @@
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/lib
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"oclif",
|
||||
"oclif-typescript"
|
||||
]
|
||||
}
|
||||
8
packages/foam-cli/.gitignore
vendored
8
packages/foam-cli/.gitignore
vendored
@@ -1,8 +0,0 @@
|
||||
*-debug.log
|
||||
*-error.log
|
||||
/.nyc_output
|
||||
/dist
|
||||
/lib
|
||||
/package-lock.json
|
||||
/tmp
|
||||
node_modules
|
||||
@@ -1,56 +0,0 @@
|
||||
foam-cli
|
||||
========
|
||||
|
||||
Foam CLI
|
||||
|
||||
[](https://oclif.io)
|
||||
[](https://npmjs.org/package/foam-cli)
|
||||
[](https://npmjs.org/package/foam-cli)
|
||||
[](https://github.com/foambubble/foam/blob/master/package.json)
|
||||
|
||||
<!-- toc -->
|
||||
* [Usage](#usage)
|
||||
* [Commands](#commands)
|
||||
<!-- tocstop -->
|
||||
# Usage
|
||||
<!-- usage -->
|
||||
```sh-session
|
||||
$ npm install -g foam-cli
|
||||
$ foam COMMAND
|
||||
running command...
|
||||
$ foam (-v|--version|version)
|
||||
foam-cli/0.10.1 darwin-x64 node-v12.18.2
|
||||
$ foam --help [COMMAND]
|
||||
USAGE
|
||||
$ foam COMMAND
|
||||
...
|
||||
```
|
||||
<!-- usagestop -->
|
||||
# Commands
|
||||
<!-- commands -->
|
||||
* [`foam help [COMMAND]`](#foam-help-command)
|
||||
|
||||
## `foam help [COMMAND]`
|
||||
|
||||
display help for foam
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ foam help [COMMAND]
|
||||
|
||||
ARGUMENTS
|
||||
COMMAND command to show help for
|
||||
|
||||
OPTIONS
|
||||
--all see all commands in CLI
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.1.0/src/commands/help.ts)_
|
||||
<!-- commandsstop -->
|
||||
|
||||
## Development
|
||||
|
||||
- Run `yarn` somewhere in workspace (ideally root, see [yarn workspace docs](https://classic.yarnpkg.com/en/docs/workspaces/)
|
||||
- This will automatically symlink all package directories so you're using the local copy
|
||||
- In `packages/foam-core`, run `yarn start` to rebuild the library on every change
|
||||
- In `packages/foam-cli`, make changes and run with `yarn run cli`. This should use latest workspace manager changes.
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', {targets: {node: 'current'}}],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('@oclif/command').run()
|
||||
.then(require('@oclif/command/flush'))
|
||||
.catch(require('@oclif/errors/handle'))
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
||||
@@ -1,188 +0,0 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/p6/5y5l8tbs1d32pq9b596lk48h0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
// coverageDirectory: undefined,
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
// coverageProvider: "babel",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
// moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
// setupFilesAfterEnv: [],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
{
|
||||
"name": "foam-cli",
|
||||
"description": "Foam CLI",
|
||||
"version": "0.10.1",
|
||||
"bin": {
|
||||
"foam": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/foambubble/foam/issues",
|
||||
"dependencies": {
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"foam-core": "^0.10.1",
|
||||
"ora": "^4.0.4",
|
||||
"tslib": "^1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.4",
|
||||
"@babel/preset-env": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@oclif/dev-cli": "^1",
|
||||
"@types/node": "^10",
|
||||
"babel-jest": "^26.1.0",
|
||||
"chai": "^4",
|
||||
"eslint": "^5.13",
|
||||
"eslint-config-oclif": "^3.1",
|
||||
"eslint-config-oclif-typescript": "^0.1",
|
||||
"globby": "^10",
|
||||
"jest": "^26.1.0",
|
||||
"mock-fs": "^4.12.0",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"foam-core": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
"/lib",
|
||||
"/npm-shrinkwrap.json",
|
||||
"/oclif.manifest.json"
|
||||
],
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"keywords": [
|
||||
"oclif"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"oclif": {
|
||||
"commands": "./lib/commands",
|
||||
"bin": "foam",
|
||||
"plugins": [
|
||||
"@oclif/plugin-help"
|
||||
]
|
||||
},
|
||||
"repository": "foambubble/foam",
|
||||
"scripts": {
|
||||
"clean": "rimraf tmp",
|
||||
"build": "tsc -b",
|
||||
"test": "jest",
|
||||
"lint": "echo Missing lint task in CLI package",
|
||||
"cli": "yarn build && ./bin/run",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
|
||||
"version": "oclif-dev readme && git add README.md"
|
||||
},
|
||||
"types": "lib/index.d.ts"
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
bootstrap,
|
||||
createConfigFromFolders,
|
||||
generateLinkReferences,
|
||||
generateHeading,
|
||||
applyTextEdit,
|
||||
Services,
|
||||
FileDataStore,
|
||||
URI,
|
||||
isNote,
|
||||
} from 'foam-core';
|
||||
import { writeFileToDisk } from '../utils/write-file-to-disk';
|
||||
import { isValidDirectory } from '../utils';
|
||||
|
||||
export default class Janitor extends Command {
|
||||
static description =
|
||||
'Updates link references and heading across all the markdown files in the given workspaces';
|
||||
|
||||
static examples = [
|
||||
`$ foam-cli janitor path-to-foam-workspace
|
||||
`,
|
||||
];
|
||||
|
||||
static flags = {
|
||||
'without-extensions': flags.boolean({
|
||||
char: 'w',
|
||||
description:
|
||||
'generate link reference definitions without extensions (for legacy support)',
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
static args = [{ name: 'workspacePath' }];
|
||||
|
||||
async run() {
|
||||
const spinner = ora('Reading Files').start();
|
||||
|
||||
const { args, flags } = this.parse(Janitor);
|
||||
|
||||
const { workspacePath = './' } = args;
|
||||
|
||||
if (isValidDirectory(workspacePath)) {
|
||||
const config = createConfigFromFolders([URI.file(workspacePath)]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
const workspace = (await bootstrap(config, services)).workspace;
|
||||
|
||||
const notes = workspace.list().filter(isNote);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = `${notes.length} files found`;
|
||||
spinner.succeed();
|
||||
|
||||
// exit early if no files found.
|
||||
if (notes.length === 0) {
|
||||
this.exit();
|
||||
}
|
||||
|
||||
spinner.text = 'Generating link definitions';
|
||||
|
||||
const fileWritePromises = notes.map(note => {
|
||||
// Get edits
|
||||
const heading = generateHeading(note);
|
||||
const definitions = generateLinkReferences(
|
||||
note,
|
||||
workspace,
|
||||
!flags['without-extensions']
|
||||
);
|
||||
|
||||
// apply Edits
|
||||
let file = note.source.text;
|
||||
file = heading ? applyTextEdit(file, heading) : file;
|
||||
file = definitions ? applyTextEdit(file, definitions) : file;
|
||||
|
||||
if (heading || definitions) {
|
||||
return writeFileToDisk(note.uri, file);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.succeed('Done!');
|
||||
} else {
|
||||
spinner.fail('Directory does not exist!');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
bootstrap,
|
||||
createConfigFromFolders,
|
||||
generateLinkReferences,
|
||||
generateHeading,
|
||||
getKebabCaseFileName,
|
||||
applyTextEdit,
|
||||
Services,
|
||||
FileDataStore,
|
||||
isNote,
|
||||
} from 'foam-core';
|
||||
import { writeFileToDisk } from '../utils/write-file-to-disk';
|
||||
import { renameFile } from '../utils/rename-file';
|
||||
import { isValidDirectory } from '../utils';
|
||||
|
||||
// @todo: Refactor 'migrate' and 'janitor' commands and avoid repeatition
|
||||
export default class Migrate extends Command {
|
||||
static description =
|
||||
'Updates file names, link references and heading across all the markdown files in the given workspaces';
|
||||
|
||||
static examples = [
|
||||
`$ foam-cli migrate path-to-foam-workspace
|
||||
Successfully generated link references and heading!
|
||||
`,
|
||||
];
|
||||
|
||||
static flags = {
|
||||
'without-extensions': flags.boolean({
|
||||
char: 'w',
|
||||
description:
|
||||
'generate link reference definitions without extensions (for legacy support)',
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
static args = [{ name: 'workspacePath' }];
|
||||
|
||||
async run() {
|
||||
const spinner = ora('Reading Files').start();
|
||||
|
||||
const { args, flags } = this.parse(Migrate);
|
||||
|
||||
const { workspacePath = './' } = args;
|
||||
const config = createConfigFromFolders([workspacePath]);
|
||||
|
||||
if (isValidDirectory(workspacePath)) {
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
let workspace = (await bootstrap(config, services)).workspace;
|
||||
|
||||
let notes = workspace.list().filter(isNote);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = `${notes.length} files found`;
|
||||
spinner.succeed();
|
||||
|
||||
// exit early if no files found.
|
||||
if (notes.length === 0) {
|
||||
this.exit();
|
||||
}
|
||||
|
||||
// Kebab case file names
|
||||
const fileRename = notes.map(note => {
|
||||
if (note.title != null) {
|
||||
const kebabCasedFileName = getKebabCaseFileName(note.title);
|
||||
if (kebabCasedFileName) {
|
||||
return renameFile(note.uri, kebabCasedFileName);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
await Promise.all(fileRename);
|
||||
|
||||
spinner.text = 'Renaming files';
|
||||
|
||||
// Reinitialize the graph after renaming files
|
||||
workspace = (await bootstrap(config, services)).workspace;
|
||||
|
||||
notes = workspace.list().filter(isNote);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.text = 'Generating link definitions';
|
||||
|
||||
const fileWritePromises = await Promise.all(
|
||||
notes.map(note => {
|
||||
// Get edits
|
||||
const heading = generateHeading(note);
|
||||
const definitions = generateLinkReferences(
|
||||
note,
|
||||
workspace,
|
||||
!flags['without-extensions']
|
||||
);
|
||||
|
||||
// apply Edits
|
||||
let file = note.source.text;
|
||||
file = heading ? applyTextEdit(file, heading) : file;
|
||||
file = definitions ? applyTextEdit(file, definitions) : file;
|
||||
|
||||
if (heading || definitions) {
|
||||
return writeFileToDisk(note.uri, file);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.succeed('Done!');
|
||||
} else {
|
||||
spinner.fail('Directory does not exist!');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export {run} from '@oclif/command'
|
||||
@@ -1,4 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
export const isValidDirectory = (path: string) =>
|
||||
fs.existsSync(path) && fs.lstatSync(path).isDirectory();
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fileUri absolute path for the file that needs to renamed
|
||||
* @param newFileName "new file name" without the extension
|
||||
*/
|
||||
export const renameFile = async (fileUri: URI, newFileName: string) => {
|
||||
const filePath = fileUri.fsPath;
|
||||
const dirName = path.dirname(filePath);
|
||||
const extension = path.extname(filePath);
|
||||
const newFileUri = path.join(dirName, `${newFileName}${extension}`);
|
||||
|
||||
return fs.promises.rename(filePath, newFileUri);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
export const writeFileToDisk = async (fileUri: URI, data: string) => {
|
||||
return fs.promises.writeFile(fileUri.fsPath, data);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { renameFile } from '../src/utils/rename-file';
|
||||
import * as fs from 'fs';
|
||||
import mockFS from 'mock-fs';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
const doesFileExist = (path: string) =>
|
||||
fs.promises
|
||||
.access(path)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
describe('renameFile', () => {
|
||||
const fileUri = URI.file('/test/oldFileName.md');
|
||||
|
||||
beforeAll(() => {
|
||||
mockFS({ [fileUri.fsPath]: '' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
it('should rename existing file', async () => {
|
||||
expect(await doesFileExist(fileUri.fsPath)).toBe(true);
|
||||
|
||||
renameFile(fileUri, 'new-file-name');
|
||||
|
||||
expect(await doesFileExist(fileUri.fsPath)).toBe(false);
|
||||
expect(await doesFileExist('/test/new-file-name.md')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
import { writeFileToDisk } from '../src/utils/write-file-to-disk';
|
||||
import * as fs from 'fs';
|
||||
import mockFS from 'mock-fs';
|
||||
import { URI } from 'foam-core';
|
||||
|
||||
describe('writeFileToDisk', () => {
|
||||
const fileUri = URI.file('./test-file.md');
|
||||
|
||||
beforeAll(() => {
|
||||
mockFS({ [fileUri.fsPath]: 'content in the existing file' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.unlinkSync(fileUri.fsPath);
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
it('should overrwrite existing file in the disk with the new data', async () => {
|
||||
const expected = `content in the new file`;
|
||||
await writeFileToDisk(fileUri, expected);
|
||||
const actual = await fs.promises.readFile(fileUri.fsPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"importHelpers": true,
|
||||
"module": "commonjs",
|
||||
"outDir": "lib",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "es2017"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"references": [{ "path": "../foam-core" }]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "foam-core",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.10.1",
|
||||
"version": "0.13.6",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -15,6 +15,10 @@
|
||||
"prepare": "tsdx build --tsconfig ./tsconfig.build.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.10.4",
|
||||
"@babel/plugin-transform-runtime": "^7.10.4",
|
||||
"@babel/preset-env": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
@@ -29,7 +33,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1,51 +1,39 @@
|
||||
import { createMarkdownParser } from './markdown-provider';
|
||||
import { FoamConfig, Foam, Services } from './index';
|
||||
import { loadPlugins } from './plugins';
|
||||
import { isSome } from './utils';
|
||||
import { isDisposable } from './common/lifecycle';
|
||||
import { Logger } from './utils/log';
|
||||
import { FoamConfig, Foam, IDataStore, FoamGraph } from './index';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import { Matcher } from './services/datastore';
|
||||
import { ResourceProvider } from 'model/provider';
|
||||
|
||||
export const bootstrap = async (config: FoamConfig, services: Services) => {
|
||||
const plugins = await loadPlugins(config);
|
||||
|
||||
const parserPlugins = plugins.map(p => p.parser).filter(isSome);
|
||||
const parser = createMarkdownParser(parserPlugins);
|
||||
|
||||
const workspace = new FoamWorkspace();
|
||||
const files = await services.dataStore.listFiles();
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.info('Found: ' + uri);
|
||||
if (uri.path.endsWith('md')) {
|
||||
const content = await services.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(parser.parse(uri, content));
|
||||
}
|
||||
}
|
||||
})
|
||||
export const bootstrap = async (
|
||||
config: FoamConfig,
|
||||
dataStore: IDataStore,
|
||||
initialProviders: ResourceProvider[]
|
||||
) => {
|
||||
const parser = createMarkdownParser([]);
|
||||
const matcher = new Matcher(
|
||||
config.workspaceFolders,
|
||||
config.includeGlobs,
|
||||
config.ignoreGlobs
|
||||
);
|
||||
workspace.resolveLinks(true);
|
||||
const workspace = new FoamWorkspace();
|
||||
await Promise.all(initialProviders.map(p => workspace.registerProvider(p)));
|
||||
|
||||
services.dataStore.onDidChange(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
workspace.set(await parser.parse(uri, content));
|
||||
});
|
||||
services.dataStore.onDidCreate(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
workspace.set(await parser.parse(uri, content));
|
||||
});
|
||||
services.dataStore.onDidDelete(uri => {
|
||||
workspace.delete(uri);
|
||||
});
|
||||
const graph = FoamGraph.fromWorkspace(workspace, true);
|
||||
|
||||
return {
|
||||
workspace: workspace,
|
||||
config: config,
|
||||
parse: parser.parse,
|
||||
services: services,
|
||||
dispose: () => {
|
||||
isDisposable(services.dataStore) && services.dataStore.dispose();
|
||||
const foam: Foam = {
|
||||
workspace,
|
||||
graph,
|
||||
config,
|
||||
services: {
|
||||
dataStore,
|
||||
parser,
|
||||
matcher,
|
||||
},
|
||||
} as Foam;
|
||||
dispose: () => {
|
||||
workspace.dispose();
|
||||
graph.dispose();
|
||||
},
|
||||
};
|
||||
|
||||
return foam;
|
||||
};
|
||||
|
||||
@@ -74,7 +74,8 @@ export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
|
||||
// Web environment
|
||||
if (typeof navigator === 'object' && !isElectronRenderer) {
|
||||
_userAgent = navigator.userAgent;
|
||||
_isWindows = _userAgent.indexOf('Windows') >= 0;
|
||||
_isWindows =
|
||||
_userAgent.indexOf('Windows') >= 0 || _userAgent.indexOf('win32') >= 0;
|
||||
_isMacintosh = _userAgent.indexOf('Macintosh') >= 0;
|
||||
_isIOS =
|
||||
(_userAgent.indexOf('Macintosh') >= 0 ||
|
||||
|
||||
@@ -1,748 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
|
||||
import { isWindows } from './platform';
|
||||
import { CharCode } from './charCode';
|
||||
import * as paths from 'path';
|
||||
|
||||
const _schemePattern = /^\w[\w\d+.-]*$/;
|
||||
const _singleSlashStart = /^\//;
|
||||
const _doubleSlashStart = /^\/\//;
|
||||
|
||||
function _validateUri(ret: URI, _strict?: boolean): void {
|
||||
// scheme, must be set
|
||||
if (!ret.scheme && _strict) {
|
||||
throw new Error(
|
||||
`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`
|
||||
);
|
||||
}
|
||||
|
||||
// scheme, https://tools.ietf.org/html/rfc3986#section-3.1
|
||||
// ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
|
||||
if (ret.scheme && !_schemePattern.test(ret.scheme)) {
|
||||
throw new Error('[UriError]: Scheme contains illegal characters.');
|
||||
}
|
||||
|
||||
// path, http://tools.ietf.org/html/rfc3986#section-3.3
|
||||
// If a URI contains an authority component, then the path component
|
||||
// must either be empty or begin with a slash ("/") character. If a URI
|
||||
// does not contain an authority component, then the path cannot begin
|
||||
// with two slash characters ("//").
|
||||
if (ret.path) {
|
||||
if (ret.authority) {
|
||||
if (!_singleSlashStart.test(ret.path)) {
|
||||
throw new Error(
|
||||
'[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (_doubleSlashStart.test(ret.path)) {
|
||||
throw new Error(
|
||||
'[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for a while we allowed uris *without* schemes and this is the migration
|
||||
// for them, e.g. an uri without scheme and without strict-mode warns and falls
|
||||
// back to the file-scheme. that should cause the least carnage and still be a
|
||||
// clear warning
|
||||
function _schemeFix(scheme: string, _strict: boolean): string {
|
||||
if (!scheme && !_strict) {
|
||||
return 'file';
|
||||
}
|
||||
return scheme;
|
||||
}
|
||||
|
||||
// implements a bit of https://tools.ietf.org/html/rfc3986#section-5
|
||||
function _referenceResolution(scheme: string, path: string): string {
|
||||
// the slash-character is our 'default base' as we don't
|
||||
// support constructing URIs relative to other URIs. This
|
||||
// also means that we alter and potentially break paths.
|
||||
// see https://tools.ietf.org/html/rfc3986#section-5.1.4
|
||||
switch (scheme) {
|
||||
case 'https':
|
||||
case 'http':
|
||||
case 'file':
|
||||
if (!path) {
|
||||
path = _slash;
|
||||
} else if (path[0] !== _slash) {
|
||||
path = _slash + path;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
const _empty = '';
|
||||
const _slash = '/';
|
||||
const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
||||
|
||||
/**
|
||||
* Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
|
||||
* This class is a simple parser which creates the basic component parts
|
||||
* (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
|
||||
* and encoding.
|
||||
*
|
||||
* ```txt
|
||||
* foo://example.com:8042/over/there?name=ferret#nose
|
||||
* \_/ \______________/\_________/ \_________/ \__/
|
||||
* | | | | |
|
||||
* scheme authority path query fragment
|
||||
* | _____________________|__
|
||||
* / \ / \
|
||||
* urn:example:animal:ferret:nose
|
||||
* ```
|
||||
*/
|
||||
export class URI implements UriComponents {
|
||||
static isUri(thing: any): thing is URI {
|
||||
if (thing instanceof URI) {
|
||||
return true;
|
||||
}
|
||||
if (!thing) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typeof (thing as URI).authority === 'string' &&
|
||||
typeof (thing as URI).fragment === 'string' &&
|
||||
typeof (thing as URI).path === 'string' &&
|
||||
typeof (thing as URI).query === 'string' &&
|
||||
typeof (thing as URI).scheme === 'string'
|
||||
// typeof (thing as URI).fsPath === 'function' &&
|
||||
// typeof (thing as URI).with === 'function' &&
|
||||
// typeof (thing as URI).toString === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'.
|
||||
* The part before the first colon.
|
||||
*/
|
||||
readonly scheme: string;
|
||||
|
||||
/**
|
||||
* authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'.
|
||||
* The part between the first double slashes and the next slash.
|
||||
*/
|
||||
readonly authority: string;
|
||||
|
||||
/**
|
||||
* path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'.
|
||||
*/
|
||||
readonly path: string;
|
||||
|
||||
/**
|
||||
* query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'.
|
||||
*/
|
||||
readonly query: string;
|
||||
|
||||
/**
|
||||
* fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'.
|
||||
*/
|
||||
readonly fragment: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected constructor(
|
||||
scheme: string,
|
||||
authority?: string,
|
||||
path?: string,
|
||||
query?: string,
|
||||
fragment?: string,
|
||||
_strict?: boolean
|
||||
);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected constructor(components: UriComponents);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected constructor(
|
||||
schemeOrData: string | UriComponents,
|
||||
authority?: string,
|
||||
path?: string,
|
||||
query?: string,
|
||||
fragment?: string,
|
||||
_strict: boolean = false
|
||||
) {
|
||||
if (typeof schemeOrData === 'object') {
|
||||
this.scheme = schemeOrData.scheme || _empty;
|
||||
this.authority = schemeOrData.authority || _empty;
|
||||
this.path = schemeOrData.path || _empty;
|
||||
this.query = schemeOrData.query || _empty;
|
||||
this.fragment = schemeOrData.fragment || _empty;
|
||||
// no validation because it's this URI
|
||||
// that creates uri components.
|
||||
// _validateUri(this);
|
||||
} else {
|
||||
this.scheme = _schemeFix(schemeOrData, _strict);
|
||||
this.authority = authority || _empty;
|
||||
this.path = _referenceResolution(this.scheme, path || _empty);
|
||||
this.query = query || _empty;
|
||||
this.fragment = fragment || _empty;
|
||||
|
||||
_validateUri(this, _strict);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- filesystem path -----------------------
|
||||
|
||||
/**
|
||||
* Returns a string representing the corresponding file system path of this URI.
|
||||
* Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the
|
||||
* platform specific path separator.
|
||||
*
|
||||
* * Will *not* validate the path for invalid characters and semantics.
|
||||
* * Will *not* look at the scheme of this URI.
|
||||
* * The result shall *not* be used for display purposes but for accessing a file on disk.
|
||||
*
|
||||
*
|
||||
* The *difference* to `URI#path` is the use of the platform specific separator and the handling
|
||||
* of UNC paths. See the below sample of a file-uri with an authority (UNC path).
|
||||
*
|
||||
* ```ts
|
||||
const u = URI.parse('file://server/c$/folder/file.txt')
|
||||
u.authority === 'server'
|
||||
u.path === '/shares/c$/file.txt'
|
||||
u.fsPath === '\\server\c$\folder\file.txt'
|
||||
```
|
||||
*
|
||||
* Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path,
|
||||
* namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working
|
||||
* with URIs that represent files on disk (`file` scheme).
|
||||
*/
|
||||
get fsPath(): string {
|
||||
// if (this.scheme !== 'file') {
|
||||
// console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`);
|
||||
// }
|
||||
return uriToFsPath(this, false);
|
||||
}
|
||||
|
||||
// ---- modify to new -------------------------
|
||||
|
||||
with(change: {
|
||||
scheme?: string;
|
||||
authority?: string | null;
|
||||
path?: string | null;
|
||||
query?: string | null;
|
||||
fragment?: string | null;
|
||||
}): URI {
|
||||
if (!change) {
|
||||
return this;
|
||||
}
|
||||
|
||||
let { scheme, authority, path, query, fragment } = change;
|
||||
if (scheme === undefined) {
|
||||
scheme = this.scheme;
|
||||
} else if (scheme === null) {
|
||||
scheme = _empty;
|
||||
}
|
||||
if (authority === undefined) {
|
||||
authority = this.authority;
|
||||
} else if (authority === null) {
|
||||
authority = _empty;
|
||||
}
|
||||
if (path === undefined) {
|
||||
path = this.path;
|
||||
} else if (path === null) {
|
||||
path = _empty;
|
||||
}
|
||||
if (query === undefined) {
|
||||
query = this.query;
|
||||
} else if (query === null) {
|
||||
query = _empty;
|
||||
}
|
||||
if (fragment === undefined) {
|
||||
fragment = this.fragment;
|
||||
} else if (fragment === null) {
|
||||
fragment = _empty;
|
||||
}
|
||||
|
||||
if (
|
||||
scheme === this.scheme &&
|
||||
authority === this.authority &&
|
||||
path === this.path &&
|
||||
query === this.query &&
|
||||
fragment === this.fragment
|
||||
) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return new Uri(scheme, authority, path, query, fragment);
|
||||
}
|
||||
|
||||
// ---- parse & validate ------------------------
|
||||
|
||||
/**
|
||||
* Creates a new URI from a string, e.g. `http://www.msft.com/some/path`,
|
||||
* `file:///usr/home`, or `scheme:with/path`.
|
||||
*
|
||||
* @param value A string which represents an URI (see `URI#toString`).
|
||||
*/
|
||||
static parse(value: string, _strict: boolean = false): URI {
|
||||
const match = _regexp.exec(value);
|
||||
if (!match) {
|
||||
return new Uri(_empty, _empty, _empty, _empty, _empty);
|
||||
}
|
||||
return new Uri(
|
||||
match[2] || _empty,
|
||||
percentDecode(match[4] || _empty),
|
||||
percentDecode(match[5] || _empty),
|
||||
percentDecode(match[7] || _empty),
|
||||
percentDecode(match[9] || _empty),
|
||||
_strict
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new URI from a file system path, e.g. `c:\my\files`,
|
||||
* `/usr/home`, or `\\server\share\some\path`.
|
||||
*
|
||||
* The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument
|
||||
* as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as**
|
||||
* `URI.parse('file://' + path)` because the path might contain characters that are
|
||||
* interpreted (# and ?). See the following sample:
|
||||
* ```ts
|
||||
const good = URI.file('/coding/c#/project1');
|
||||
good.scheme === 'file';
|
||||
good.path === '/coding/c#/project1';
|
||||
good.fragment === '';
|
||||
const bad = URI.parse('file://' + '/coding/c#/project1');
|
||||
bad.scheme === 'file';
|
||||
bad.path === '/coding/c'; // path is now broken
|
||||
bad.fragment === '/project1';
|
||||
```
|
||||
*
|
||||
* @param path A file system path (see `URI#fsPath`)
|
||||
*/
|
||||
static file(path: string): URI {
|
||||
let authority = _empty;
|
||||
|
||||
// normalize to fwd-slashes on windows,
|
||||
// on other systems bwd-slashes are valid
|
||||
// filename character, eg /f\oo/ba\r.txt
|
||||
if (isWindows) {
|
||||
path = path.replace(/\\/g, _slash);
|
||||
}
|
||||
|
||||
// check for authority as used in UNC shares
|
||||
// or use the path as given
|
||||
if (path[0] === _slash && path[1] === _slash) {
|
||||
const idx = path.indexOf(_slash, 2);
|
||||
if (idx === -1) {
|
||||
authority = path.substring(2);
|
||||
path = _slash;
|
||||
} else {
|
||||
authority = path.substring(2, idx);
|
||||
path = path.substring(idx) || _slash;
|
||||
}
|
||||
}
|
||||
|
||||
return new Uri('file', authority, path, _empty, _empty);
|
||||
}
|
||||
|
||||
static from(components: {
|
||||
scheme: string;
|
||||
authority?: string;
|
||||
path?: string;
|
||||
query?: string;
|
||||
fragment?: string;
|
||||
}): URI {
|
||||
return new Uri(
|
||||
components.scheme,
|
||||
components.authority,
|
||||
components.path,
|
||||
components.query,
|
||||
components.fragment
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a URI path with path fragments and normalizes the resulting path.
|
||||
*
|
||||
* @param uri The input URI.
|
||||
* @param pathFragment The path fragment to add to the URI path.
|
||||
* @returns The resulting URI.
|
||||
*/
|
||||
static joinPath(uri: URI, ...pathFragment: string[]): URI {
|
||||
if (!uri.path) {
|
||||
throw new Error(`[UriError]: cannot call joinPath on URI without path`);
|
||||
}
|
||||
let newPath: string;
|
||||
if (isWindows && uri.scheme === 'file') {
|
||||
newPath = URI.file(
|
||||
paths.win32.join(uriToFsPath(uri, true), ...pathFragment)
|
||||
).path;
|
||||
} else {
|
||||
newPath = paths.posix.join(uri.path, ...pathFragment);
|
||||
}
|
||||
return uri.with({ path: newPath });
|
||||
}
|
||||
|
||||
// ---- printing/externalize ---------------------------
|
||||
|
||||
/**
|
||||
* Creates a string representation for this URI. It's guaranteed that calling
|
||||
* `URI.parse` with the result of this function creates an URI which is equal
|
||||
* to this URI.
|
||||
*
|
||||
* * The result shall *not* be used for display purposes but for externalization or transport.
|
||||
* * The result will be encoded using the percentage encoding and encoding happens mostly
|
||||
* ignore the scheme-specific encoding rules.
|
||||
*
|
||||
* @param skipEncoding Do not encode the result, default is `false`
|
||||
*/
|
||||
toString(skipEncoding: boolean = false): string {
|
||||
return _asFormatted(this, skipEncoding);
|
||||
}
|
||||
|
||||
toJSON(): UriComponents {
|
||||
return this;
|
||||
}
|
||||
|
||||
static revive(data: UriComponents | URI): URI;
|
||||
static revive(data: UriComponents | URI | undefined): URI | undefined;
|
||||
static revive(data: UriComponents | URI | null): URI | null;
|
||||
static revive(
|
||||
data: UriComponents | URI | undefined | null
|
||||
): URI | undefined | null;
|
||||
static revive(
|
||||
data: UriComponents | URI | undefined | null
|
||||
): URI | undefined | null {
|
||||
if (!data) {
|
||||
return data;
|
||||
} else if (data instanceof URI) {
|
||||
return data;
|
||||
} else {
|
||||
const result = new Uri(data);
|
||||
result._formatted = (data as UriState).external;
|
||||
result._fsPath =
|
||||
(data as UriState)._sep === _pathSepMarker
|
||||
? (data as UriState).fsPath
|
||||
: null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface UriComponents {
|
||||
scheme: string;
|
||||
authority: string;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
|
||||
interface UriState extends UriComponents {
|
||||
$mid: number;
|
||||
external: string;
|
||||
fsPath: string;
|
||||
_sep: 1 | undefined;
|
||||
}
|
||||
|
||||
const _pathSepMarker = isWindows ? 1 : undefined;
|
||||
|
||||
// This class exists so that URI is compatibile with vscode.Uri (API).
|
||||
class Uri extends URI {
|
||||
_formatted: string | null = null;
|
||||
_fsPath: string | null = null;
|
||||
|
||||
get fsPath(): string {
|
||||
if (!this._fsPath) {
|
||||
this._fsPath = uriToFsPath(this, false);
|
||||
}
|
||||
return this._fsPath;
|
||||
}
|
||||
|
||||
toString(skipEncoding: boolean = false): string {
|
||||
if (!skipEncoding) {
|
||||
if (!this._formatted) {
|
||||
this._formatted = _asFormatted(this, false);
|
||||
}
|
||||
return this._formatted;
|
||||
} else {
|
||||
// we don't cache that
|
||||
return _asFormatted(this, true);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): UriComponents {
|
||||
const res = {
|
||||
$mid: 1,
|
||||
} as UriState;
|
||||
// cached state
|
||||
if (this._fsPath) {
|
||||
res.fsPath = this._fsPath;
|
||||
res._sep = _pathSepMarker;
|
||||
}
|
||||
if (this._formatted) {
|
||||
res.external = this._formatted;
|
||||
}
|
||||
// uri components
|
||||
if (this.path) {
|
||||
res.path = this.path;
|
||||
}
|
||||
if (this.scheme) {
|
||||
res.scheme = this.scheme;
|
||||
}
|
||||
if (this.authority) {
|
||||
res.authority = this.authority;
|
||||
}
|
||||
if (this.query) {
|
||||
res.query = this.query;
|
||||
}
|
||||
if (this.fragment) {
|
||||
res.fragment = this.fragment;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
// reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
const encodeTable: { [ch: number]: string } = {
|
||||
[CharCode.Colon]: '%3A', // gen-delims
|
||||
[CharCode.Slash]: '%2F',
|
||||
[CharCode.QuestionMark]: '%3F',
|
||||
[CharCode.Hash]: '%23',
|
||||
[CharCode.OpenSquareBracket]: '%5B',
|
||||
[CharCode.CloseSquareBracket]: '%5D',
|
||||
[CharCode.AtSign]: '%40',
|
||||
|
||||
[CharCode.ExclamationMark]: '%21', // sub-delims
|
||||
[CharCode.DollarSign]: '%24',
|
||||
[CharCode.Ampersand]: '%26',
|
||||
[CharCode.SingleQuote]: '%27',
|
||||
[CharCode.OpenParen]: '%28',
|
||||
[CharCode.CloseParen]: '%29',
|
||||
[CharCode.Asterisk]: '%2A',
|
||||
[CharCode.Plus]: '%2B',
|
||||
[CharCode.Comma]: '%2C',
|
||||
[CharCode.Semicolon]: '%3B',
|
||||
[CharCode.Equals]: '%3D',
|
||||
|
||||
[CharCode.Space]: '%20',
|
||||
};
|
||||
|
||||
function encodeURIComponentFast(
|
||||
uriComponent: string,
|
||||
allowSlash: boolean
|
||||
): string {
|
||||
let res: string | undefined = undefined;
|
||||
let nativeEncodePos = -1;
|
||||
|
||||
for (let pos = 0; pos < uriComponent.length; pos++) {
|
||||
const code = uriComponent.charCodeAt(pos);
|
||||
|
||||
// unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3
|
||||
if (
|
||||
(code >= CharCode.a && code <= CharCode.z) ||
|
||||
(code >= CharCode.A && code <= CharCode.Z) ||
|
||||
(code >= CharCode.Digit0 && code <= CharCode.Digit9) ||
|
||||
code === CharCode.Dash ||
|
||||
code === CharCode.Period ||
|
||||
code === CharCode.Underline ||
|
||||
code === CharCode.Tilde ||
|
||||
(allowSlash && code === CharCode.Slash)
|
||||
) {
|
||||
// check if we are delaying native encode
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
|
||||
nativeEncodePos = -1;
|
||||
}
|
||||
// check if we write into a new string (by default we try to return the param)
|
||||
if (res !== undefined) {
|
||||
res += uriComponent.charAt(pos);
|
||||
}
|
||||
} else {
|
||||
// encoding needed, we need to allocate a new string
|
||||
if (res === undefined) {
|
||||
res = uriComponent.substr(0, pos);
|
||||
}
|
||||
|
||||
// check with default table first
|
||||
const escaped = encodeTable[code];
|
||||
if (escaped !== undefined) {
|
||||
// check if we are delaying native encode
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(
|
||||
uriComponent.substring(nativeEncodePos, pos)
|
||||
);
|
||||
nativeEncodePos = -1;
|
||||
}
|
||||
|
||||
// append escaped variant to result
|
||||
res += escaped;
|
||||
} else if (nativeEncodePos === -1) {
|
||||
// use native encode only when needed
|
||||
nativeEncodePos = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(uriComponent.substring(nativeEncodePos));
|
||||
}
|
||||
|
||||
return res !== undefined ? res : uriComponent;
|
||||
}
|
||||
|
||||
function encodeURIComponentMinimal(path: string): string {
|
||||
let res: string | undefined = undefined;
|
||||
for (let pos = 0; pos < path.length; pos++) {
|
||||
const code = path.charCodeAt(pos);
|
||||
if (code === CharCode.Hash || code === CharCode.QuestionMark) {
|
||||
if (res === undefined) {
|
||||
res = path.substr(0, pos);
|
||||
}
|
||||
res += encodeTable[code];
|
||||
} else {
|
||||
if (res !== undefined) {
|
||||
res += path[pos];
|
||||
}
|
||||
}
|
||||
}
|
||||
return res !== undefined ? res : path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute `fsPath` for the given uri
|
||||
*/
|
||||
export function uriToFsPath(uri: URI, keepDriveLetterCasing: boolean): string {
|
||||
let value: string;
|
||||
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
|
||||
// unc path: file://shares/c$/far/boo
|
||||
value = `//${uri.authority}${uri.path}`;
|
||||
} else if (
|
||||
uri.path.charCodeAt(0) === CharCode.Slash &&
|
||||
((uri.path.charCodeAt(1) >= CharCode.A &&
|
||||
uri.path.charCodeAt(1) <= CharCode.Z) ||
|
||||
(uri.path.charCodeAt(1) >= CharCode.a &&
|
||||
uri.path.charCodeAt(1) <= CharCode.z)) &&
|
||||
uri.path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
if (!keepDriveLetterCasing) {
|
||||
// windows drive letter: file:///c:/far/boo
|
||||
value = uri.path[1].toLowerCase() + uri.path.substr(2);
|
||||
} else {
|
||||
value = uri.path.substr(1);
|
||||
}
|
||||
} else {
|
||||
// other path
|
||||
value = uri.path;
|
||||
}
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, '\\');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the external version of a uri
|
||||
*/
|
||||
function _asFormatted(uri: URI, skipEncoding: boolean): string {
|
||||
const encoder = !skipEncoding
|
||||
? encodeURIComponentFast
|
||||
: encodeURIComponentMinimal;
|
||||
|
||||
let res = '';
|
||||
let { scheme, authority, path, query, fragment } = uri;
|
||||
if (scheme) {
|
||||
res += scheme;
|
||||
res += ':';
|
||||
}
|
||||
if (authority || scheme === 'file') {
|
||||
res += _slash;
|
||||
res += _slash;
|
||||
}
|
||||
if (authority) {
|
||||
let idx = authority.indexOf('@');
|
||||
if (idx !== -1) {
|
||||
// <user>@<auth>
|
||||
const userinfo = authority.substr(0, idx);
|
||||
authority = authority.substr(idx + 1);
|
||||
idx = userinfo.indexOf(':');
|
||||
if (idx === -1) {
|
||||
res += encoder(userinfo, false);
|
||||
} else {
|
||||
// <user>:<pass>@<auth>
|
||||
res += encoder(userinfo.substr(0, idx), false);
|
||||
res += ':';
|
||||
res += encoder(userinfo.substr(idx + 1), false);
|
||||
}
|
||||
res += '@';
|
||||
}
|
||||
authority = authority.toLowerCase();
|
||||
idx = authority.indexOf(':');
|
||||
if (idx === -1) {
|
||||
res += encoder(authority, false);
|
||||
} else {
|
||||
// <auth>:<port>
|
||||
res += encoder(authority.substr(0, idx), false);
|
||||
res += authority.substr(idx);
|
||||
}
|
||||
}
|
||||
if (path) {
|
||||
// lower-case windows drive letters in /C:/fff or C:/fff
|
||||
if (
|
||||
path.length >= 3 &&
|
||||
path.charCodeAt(0) === CharCode.Slash &&
|
||||
path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
const code = path.charCodeAt(1);
|
||||
if (code >= CharCode.A && code <= CharCode.Z) {
|
||||
path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
|
||||
}
|
||||
} else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) {
|
||||
const code = path.charCodeAt(0);
|
||||
if (code >= CharCode.A && code <= CharCode.Z) {
|
||||
path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
|
||||
}
|
||||
}
|
||||
// encode the rest of the path
|
||||
res += encoder(path, true);
|
||||
}
|
||||
if (query) {
|
||||
res += '?';
|
||||
res += encoder(query, false);
|
||||
}
|
||||
if (fragment) {
|
||||
res += '#';
|
||||
res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// --- decode
|
||||
|
||||
function decodeURIComponentGraceful(str: string): string {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
if (str.length > 3) {
|
||||
return str.substr(0, 3) + decodeURIComponentGraceful(str.substr(3));
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _rEncodedAsHex = /(%[0-9A-Za-z][0-9A-Za-z])+/g;
|
||||
|
||||
function percentDecode(str: string): string {
|
||||
if (!str.match(_rEncodedAsHex)) {
|
||||
return str;
|
||||
}
|
||||
return str.replace(_rEncodedAsHex, match =>
|
||||
decodeURIComponentGraceful(match)
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './common/uri';
|
||||
import { URI } from './model/uri';
|
||||
|
||||
export interface FoamConfig {
|
||||
workspaceFolders: URI[];
|
||||
@@ -68,8 +68,8 @@ export const createConfigFromFolders = (
|
||||
|
||||
const parseConfig = (path: URI) => {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path.fsPath, 'utf8'));
|
||||
return JSON.parse(readFileSync(URI.toFsPath(path), 'utf8'));
|
||||
} catch {
|
||||
Logger.debug('Could not read configuration from ' + path);
|
||||
Logger.debug('Could not read configuration from ' + URI.toString(path));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
import {
|
||||
Resource,
|
||||
Attachment,
|
||||
Placeholder,
|
||||
Note,
|
||||
NoteLink,
|
||||
isNote,
|
||||
ResourceLink,
|
||||
NoteLinkDefinition,
|
||||
ResourceParser,
|
||||
} from './model/note';
|
||||
import { URI } from './common/uri';
|
||||
import { FoamConfig } from './config';
|
||||
import { IDataStore, FileDataStore } from './services/datastore';
|
||||
import {
|
||||
IDataStore,
|
||||
FileDataStore,
|
||||
Matcher,
|
||||
IMatcher,
|
||||
} from './services/datastore';
|
||||
import { ILogger } from './utils/log';
|
||||
import { IDisposable, isDisposable } from './common/lifecycle';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import { FoamGraph } from '../src/model/graph';
|
||||
import { URI } from './model/uri';
|
||||
|
||||
export { IDataStore, FileDataStore };
|
||||
export { Position } from './model/position';
|
||||
export { Range } from './model/range';
|
||||
export { IDataStore, FileDataStore, Matcher, IMatcher };
|
||||
export { ILogger };
|
||||
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
|
||||
export { Event, Emitter } from './common/event';
|
||||
export { FoamConfig };
|
||||
|
||||
export { ResourceProvider } from './model/provider';
|
||||
export { IDisposable, isDisposable };
|
||||
|
||||
export {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
createMarkdownParser,
|
||||
MarkdownResourceProvider,
|
||||
} from './markdown-provider';
|
||||
|
||||
export {
|
||||
@@ -44,23 +51,23 @@ export { bootstrap } from './bootstrap';
|
||||
|
||||
export {
|
||||
Resource,
|
||||
Attachment,
|
||||
Placeholder,
|
||||
Note,
|
||||
NoteLink,
|
||||
ResourceLink,
|
||||
URI,
|
||||
FoamWorkspace,
|
||||
FoamGraph,
|
||||
NoteLinkDefinition,
|
||||
isNote,
|
||||
ResourceParser,
|
||||
};
|
||||
|
||||
export interface Services {
|
||||
dataStore: IDataStore;
|
||||
parser: ResourceParser;
|
||||
matcher: IMatcher;
|
||||
}
|
||||
|
||||
export interface Foam extends IDisposable {
|
||||
services: Services;
|
||||
workspace: FoamWorkspace;
|
||||
graph: FoamGraph;
|
||||
config: FoamConfig;
|
||||
parse: (uri: URI, text: string, eol: string) => Note;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import os from 'os';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Position } from '../model/position';
|
||||
import { TextEdit } from '../index';
|
||||
|
||||
/**
|
||||
@@ -7,12 +10,29 @@ import { TextEdit } from '../index';
|
||||
* @returns {string} text with the applied textEdit
|
||||
*/
|
||||
export const applyTextEdit = (text: string, textEdit: TextEdit): string => {
|
||||
const eol = detectNewline(text) || os.EOL;
|
||||
const lines = text.split(eol);
|
||||
const characters = text.split('');
|
||||
const startOffset = textEdit.range.start.offset || 0;
|
||||
const endOffset = textEdit.range.end.offset || 0;
|
||||
let startOffset = getOffset(lines, textEdit.range.start, eol);
|
||||
let endOffset = getOffset(lines, textEdit.range.end, eol);
|
||||
const deleteCount = endOffset - startOffset;
|
||||
|
||||
const textToAppend = `${textEdit.newText}`;
|
||||
characters.splice(startOffset, deleteCount, textToAppend);
|
||||
return characters.join('');
|
||||
};
|
||||
|
||||
const getOffset = (
|
||||
lines: string[],
|
||||
position: Position,
|
||||
eol: string
|
||||
): number => {
|
||||
const eolLen = eol.length;
|
||||
let offset = 0;
|
||||
let i = 0;
|
||||
while (i < position.line && i < lines.length) {
|
||||
offset = offset + lines[i].length + eolLen;
|
||||
i++;
|
||||
}
|
||||
return offset + Math.min(position.character, lines[i].length);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Position } from 'unist';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { Note } from '../model/note';
|
||||
import { Resource } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../markdown-provider';
|
||||
import { getHeadingFromFileName, uriToSlug } from '../utils';
|
||||
import { getHeadingFromFileName } from '../utils';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { uriToSlug } from '../utils/slug';
|
||||
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
@@ -14,12 +15,12 @@ export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link
|
||||
const slugger = new GithubSlugger();
|
||||
|
||||
export interface TextEdit {
|
||||
range: Position;
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
export const generateLinkReferences = (
|
||||
note: Note,
|
||||
note: Resource,
|
||||
workspace: FoamWorkspace,
|
||||
includeExtensions: boolean
|
||||
): TextEdit | null => {
|
||||
@@ -48,15 +49,12 @@ export const generateLinkReferences = (
|
||||
}
|
||||
|
||||
const padding =
|
||||
note.source.end.column === 1
|
||||
note.source.end.character === 0
|
||||
? note.source.eol
|
||||
: `${note.source.eol}${note.source.eol}`;
|
||||
return {
|
||||
newText: `${padding}${newReferences}`,
|
||||
range: {
|
||||
start: note.source.end,
|
||||
end: note.source.end,
|
||||
},
|
||||
range: Range.createFromPosition(note.source.end, note.source.end),
|
||||
};
|
||||
} else {
|
||||
const first = note.definitions[0];
|
||||
@@ -72,15 +70,12 @@ export const generateLinkReferences = (
|
||||
return {
|
||||
// @todo: do we need to ensure new lines?
|
||||
newText: `${newReferences}`,
|
||||
range: {
|
||||
start: first.position!.start,
|
||||
end: last.position!.end,
|
||||
},
|
||||
range: Range.createFromPosition(first.range!.start, last.range!.end),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const generateHeading = (note: Note): TextEdit | null => {
|
||||
export const generateHeading = (note: Resource): TextEdit | null => {
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
@@ -114,10 +109,10 @@ export const generateHeading = (note: Note): TextEdit | null => {
|
||||
newText: `${paddingStart}# ${getHeadingFromFileName(
|
||||
uriToSlug(note.uri)
|
||||
)}${paddingEnd}`,
|
||||
range: {
|
||||
start: note.source.contentStart,
|
||||
end: note.source.contentStart,
|
||||
},
|
||||
range: Range.createFromPosition(
|
||||
note.source.contentStart,
|
||||
note.source.contentStart
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Node } from 'unist';
|
||||
import { Node, Position as AstPosition } from 'unist';
|
||||
import unified from 'unified';
|
||||
import markdownParse from 'remark-parse';
|
||||
import wikiLinkPlugin from 'remark-wiki-link';
|
||||
@@ -10,11 +10,13 @@ import detectNewline from 'detect-newline';
|
||||
import os from 'os';
|
||||
import {
|
||||
NoteLinkDefinition,
|
||||
Note,
|
||||
NoteParser,
|
||||
isWikilink,
|
||||
getTitle,
|
||||
Resource,
|
||||
ResourceLink,
|
||||
WikiLink,
|
||||
ResourceParser,
|
||||
} from './model/note';
|
||||
import { Position } from './model/position';
|
||||
import { Range } from './model/range';
|
||||
import {
|
||||
dropExtension,
|
||||
extractHashtags,
|
||||
@@ -22,11 +24,121 @@ import {
|
||||
isNone,
|
||||
isSome,
|
||||
} from './utils';
|
||||
import { computeRelativePath, getBasename, parseUri } from './utils/uri';
|
||||
import { ParserPlugin } from './plugins';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './common/uri';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import { ResourceProvider } from 'model/provider';
|
||||
import { IDataStore, FileDataStore, IMatcher } from './services/datastore';
|
||||
import { IDisposable } from 'common/lifecycle';
|
||||
|
||||
export class MarkdownResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly matcher: IMatcher,
|
||||
private readonly watcherInit?: (triggers: {
|
||||
onDidChange: (uri: URI) => void;
|
||||
onDidCreate: (uri: URI) => void;
|
||||
onDidDelete: (uri: URI) => void;
|
||||
}) => IDisposable[],
|
||||
private readonly parser: ResourceParser = createMarkdownParser([]),
|
||||
private readonly dataStore: IDataStore = new FileDataStore()
|
||||
) {}
|
||||
|
||||
async init(workspace: FoamWorkspace) {
|
||||
const filesByFolder = await Promise.all(
|
||||
this.matcher.include.map(glob => this.dataStore.list(glob))
|
||||
);
|
||||
const files = this.matcher
|
||||
.match(filesByFolder.flat())
|
||||
.filter(this.supports);
|
||||
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
Logger.info('Found: ' + URI.toString(uri));
|
||||
const content = await this.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(this.parser.parse(uri, content));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables =
|
||||
this.watcherInit?.({
|
||||
onDidChange: async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
const content = await this.dataStore.read(uri);
|
||||
isSome(content) &&
|
||||
workspace.set(await this.parser.parse(uri, content));
|
||||
}
|
||||
},
|
||||
onDidCreate: async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
const content = await this.dataStore.read(uri);
|
||||
isSome(content) &&
|
||||
workspace.set(await this.parser.parse(uri, content));
|
||||
}
|
||||
},
|
||||
onDidDelete: uri => {
|
||||
this.supports(uri) && workspace.delete(uri);
|
||||
},
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return URI.isMarkdownFile(uri);
|
||||
}
|
||||
|
||||
read(uri: URI): Promise<string | null> {
|
||||
return this.dataStore.read(uri);
|
||||
}
|
||||
|
||||
readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
return this.dataStore.read(uri);
|
||||
}
|
||||
|
||||
async fetch(uri: URI) {
|
||||
const content = await this.read(uri);
|
||||
return isSome(content) ? this.parser.parse(uri, content) : null;
|
||||
}
|
||||
|
||||
resolveLink(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource,
|
||||
link: ResourceLink
|
||||
) {
|
||||
let targetUri: URI | undefined;
|
||||
switch (link.type) {
|
||||
case 'wikilink':
|
||||
const definitionUri = resource.definitions.find(
|
||||
def => def.label === link.slug
|
||||
)?.url;
|
||||
if (isSome(definitionUri)) {
|
||||
const definedUri = URI.resolve(definitionUri, resource.uri);
|
||||
targetUri =
|
||||
workspace.find(definedUri, resource.uri)?.uri ??
|
||||
URI.placeholder(definedUri.path);
|
||||
} else {
|
||||
targetUri =
|
||||
workspace.find(link.slug, resource.uri)?.uri ??
|
||||
URI.placeholder(link.slug);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
targetUri =
|
||||
workspace.find(link.target, resource.uri)?.uri ??
|
||||
URI.placeholder(URI.resolve(link.target, resource.uri).path);
|
||||
break;
|
||||
}
|
||||
return targetUri;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses all the children of the given node, extracts
|
||||
@@ -58,18 +170,18 @@ const tagsPlugin: ParserPlugin = {
|
||||
const titlePlugin: ParserPlugin = {
|
||||
name: 'title',
|
||||
visit: (node, note) => {
|
||||
if (note.title == null && node.type === 'heading' && node.depth === 1) {
|
||||
if (note.title === '' && node.type === 'heading' && node.depth === 1) {
|
||||
note.title =
|
||||
((node as Parent)!.children?.[0]?.value as string) || note.title;
|
||||
}
|
||||
},
|
||||
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 == null) {
|
||||
note.title = getBasename(note.uri);
|
||||
if (note.title === '') {
|
||||
note.title = URI.getBasename(note.uri);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -82,12 +194,12 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
type: 'wikilink',
|
||||
slug: node.value as string,
|
||||
target: node.value as string,
|
||||
position: node.position!,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
if (node.type === 'link') {
|
||||
const targetUri = (node as any).url;
|
||||
const uri = parseUri(note.uri, targetUri);
|
||||
const uri = URI.resolve(targetUri, note.uri);
|
||||
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
|
||||
return;
|
||||
}
|
||||
@@ -96,6 +208,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
type: 'link',
|
||||
target: targetUri,
|
||||
label: label,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -109,7 +222,7 @@ const definitionsPlugin: ParserPlugin = {
|
||||
label: node.label as string,
|
||||
url: node.url as string,
|
||||
title: node.title as string,
|
||||
position: node.position,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -131,7 +244,9 @@ const handleError = (
|
||||
);
|
||||
};
|
||||
|
||||
export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
export function createMarkdownParser(
|
||||
extraPlugins: ParserPlugin[]
|
||||
): ResourceParser {
|
||||
const parser = unified()
|
||||
.use(markdownParse, { gfm: true })
|
||||
.use(frontmatterPlugin, ['yaml'])
|
||||
@@ -153,8 +268,8 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
}
|
||||
});
|
||||
|
||||
const foamParser: NoteParser = {
|
||||
parse: (uri: URI, markdown: string): Note => {
|
||||
const foamParser: ResourceParser = {
|
||||
parse: (uri: URI, markdown: string): Resource => {
|
||||
Logger.debug('Parsing:', uri);
|
||||
markdown = plugins.reduce((acc, plugin) => {
|
||||
try {
|
||||
@@ -167,18 +282,18 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
const tree = parser.parse(markdown);
|
||||
const eol = detectNewline(markdown) || os.EOL;
|
||||
|
||||
var note: Note = {
|
||||
var note: Resource = {
|
||||
uri: uri,
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: null,
|
||||
title: '',
|
||||
tags: new Set(),
|
||||
links: [],
|
||||
definitions: [],
|
||||
source: {
|
||||
text: markdown,
|
||||
contentStart: tree.position!.start,
|
||||
end: tree.position!.end,
|
||||
contentStart: astPointToFoamPosition(tree.position!.start),
|
||||
end: astPointToFoamPosition(tree.position!.end),
|
||||
eol: eol,
|
||||
},
|
||||
};
|
||||
@@ -198,14 +313,11 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
...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 = {
|
||||
line: node.position!.end.line! + 1,
|
||||
column: 1,
|
||||
offset: node.position!.end.offset! + 1,
|
||||
};
|
||||
note.source.contentStart = Position.create(
|
||||
node.position!.end.line! + 2,
|
||||
0
|
||||
);
|
||||
|
||||
for (let i = 0, len = plugins.length; i < len; i++) {
|
||||
try {
|
||||
@@ -243,7 +355,7 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
|
||||
function getFoamDefinitions(
|
||||
defs: NoteLinkDefinition[],
|
||||
fileEndPoint: Point
|
||||
fileEndPoint: Position
|
||||
): NoteLinkDefinition[] {
|
||||
let previousLine = fileEndPoint.line;
|
||||
let foamDefinitions = [];
|
||||
@@ -254,13 +366,13 @@ function getFoamDefinitions(
|
||||
// if this definition is more than 2 lines above the
|
||||
// previous one below it (or file end), that means we
|
||||
// have exited the trailing definition block, and should bail
|
||||
const start = def.position!.start.line;
|
||||
const start = def.range!.start.line;
|
||||
if (start < previousLine - 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
foamDefinitions.unshift(def);
|
||||
previousLine = def.position!.end.line;
|
||||
previousLine = def.range!.end.line;
|
||||
}
|
||||
|
||||
return foamDefinitions;
|
||||
@@ -304,14 +416,40 @@ export function createMarkdownReferences(
|
||||
return null;
|
||||
}
|
||||
|
||||
const relativePath = computeRelativePath(noteUri, target.uri);
|
||||
const relativePath = URI.relativePath(noteUri, target.uri);
|
||||
const pathToNote = includeExtension
|
||||
? relativePath
|
||||
: dropExtension(relativePath);
|
||||
|
||||
// [wiki-link-text]: path/to/file.md "Page title"
|
||||
return { label: link.slug, url: pathToNote, title: getTitle(target) };
|
||||
return { label: link.slug, url: pathToNote, title: target.title };
|
||||
})
|
||||
.filter(isSome)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the 1-index Point object into the VS Code 0-index Position object
|
||||
* @param point ast Point (1-indexed)
|
||||
* @returns Foam Position (0-indexed)
|
||||
*/
|
||||
const astPointToFoamPosition = (point: Point): Position => {
|
||||
return Position.create(point.line - 1, point.column - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the 1-index Position object into the VS Code 0-index Range object
|
||||
* @param position an ast Position object (1-indexed)
|
||||
* @returns Foam Range (0-indexed)
|
||||
*/
|
||||
const astPositionToFoamRange = (pos: AstPosition): Range =>
|
||||
Range.create(
|
||||
pos.start.line - 1,
|
||||
pos.start.column - 1,
|
||||
pos.end.line - 1,
|
||||
pos.end.column - 1
|
||||
);
|
||||
|
||||
const isWikilink = (link: ResourceLink): link is WikiLink => {
|
||||
return link.type === 'wikilink';
|
||||
};
|
||||
|
||||
232
packages/foam-core/src/model/graph.ts
Normal file
232
packages/foam-core/src/model/graph.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { IDisposable } from '../index';
|
||||
import { FoamWorkspace, uriToResourceName } from './workspace';
|
||||
import { Range } from './range';
|
||||
|
||||
export type Connection = {
|
||||
source: URI;
|
||||
target: URI;
|
||||
link: ResourceLink;
|
||||
};
|
||||
|
||||
const pathToPlaceholderId = (value: string) => value;
|
||||
const uriToPlaceholderId = (uri: URI) => pathToPlaceholderId(uri.path);
|
||||
|
||||
export class FoamGraph implements IDisposable {
|
||||
/**
|
||||
* Placehoders by key / slug / value
|
||||
*/
|
||||
public readonly placeholders: { [key: string]: URI } = {};
|
||||
/**
|
||||
* Maps the connections starting from a URI
|
||||
*/
|
||||
public readonly links: { [key: string]: Connection[] } = {};
|
||||
/**
|
||||
* Maps the connections arriving to a URI
|
||||
*/
|
||||
public readonly backlinks: { [key: string]: Connection[] } = {};
|
||||
|
||||
/**
|
||||
* List of disposables to destroy with the workspace
|
||||
*/
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(private readonly workspace: FoamWorkspace) {}
|
||||
|
||||
public contains(uri: URI): boolean {
|
||||
return this.getConnections(uri).length > 0;
|
||||
}
|
||||
|
||||
public getAllNodes(): URI[] {
|
||||
return [
|
||||
...Object.values(this.placeholders),
|
||||
...this.workspace.list().map(r => r.uri),
|
||||
];
|
||||
}
|
||||
|
||||
public getAllConnections(): Connection[] {
|
||||
return Object.values(this.links).flat();
|
||||
}
|
||||
|
||||
public getConnections(uri: URI): Connection[] {
|
||||
return [
|
||||
...(this.links[uri.path] || []),
|
||||
...(this.backlinks[uri.path] || []),
|
||||
];
|
||||
}
|
||||
|
||||
public getLinks(uri: URI): Connection[] {
|
||||
return this.links[uri.path] ?? [];
|
||||
}
|
||||
|
||||
public getBacklinks(uri: URI): Connection[] {
|
||||
return this.backlinks[uri.path] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes all the links in the workspace, connecting notes and
|
||||
* creating placeholders.
|
||||
*
|
||||
* @param workspace the target workspace
|
||||
* @param keepMonitoring whether to recompute the links when the workspace changes
|
||||
* @returns the FoamGraph
|
||||
*/
|
||||
public static fromWorkspace(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring: boolean = false
|
||||
): FoamGraph {
|
||||
let graph = new FoamGraph(workspace);
|
||||
|
||||
Object.values(workspace.list()).forEach(resource =>
|
||||
graph.resolveResource(resource)
|
||||
);
|
||||
if (keepMonitoring) {
|
||||
graph.disposables.push(
|
||||
workspace.onDidAdd(resource => {
|
||||
graph.updateLinksRelatedToAddedResource(resource);
|
||||
}),
|
||||
workspace.onDidUpdate(change => {
|
||||
graph.updateLinksForResource(change.old, change.new);
|
||||
}),
|
||||
workspace.onDidDelete(resource => {
|
||||
graph.updateLinksRelatedToDeletedResource(resource);
|
||||
})
|
||||
);
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
private updateLinksRelatedToAddedResource(resource: Resource) {
|
||||
// check if any existing connection can be filled by new resource
|
||||
const name = uriToResourceName(resource.uri);
|
||||
if (name in this.placeholders) {
|
||||
const placeholder = this.placeholders[name];
|
||||
delete this.placeholders[name];
|
||||
const resourcesToUpdate = this.backlinks[placeholder.path] ?? [];
|
||||
resourcesToUpdate.forEach(res =>
|
||||
this.resolveResource(this.workspace.get(res.source))
|
||||
);
|
||||
}
|
||||
|
||||
// resolve the resource
|
||||
this.resolveResource(resource);
|
||||
}
|
||||
|
||||
private updateLinksForResource(oldResource: Resource, newResource: Resource) {
|
||||
if (oldResource.uri.path !== newResource.uri.path) {
|
||||
throw new Error(
|
||||
'Unexpected State: update should only be called on same resource ' +
|
||||
{
|
||||
old: oldResource,
|
||||
new: newResource,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (oldResource.type === 'note' && newResource.type === 'note') {
|
||||
const patch = diff(oldResource.links, newResource.links, isEqual);
|
||||
patch.removed.forEach(link => {
|
||||
const target = this.workspace.resolveLink(oldResource, link);
|
||||
return this.disconnect(oldResource.uri, target, link);
|
||||
}, this);
|
||||
patch.added.forEach(link => {
|
||||
const target = this.workspace.resolveLink(newResource, link);
|
||||
return this.connect(newResource.uri, target, link);
|
||||
}, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private updateLinksRelatedToDeletedResource(resource: Resource) {
|
||||
const uri = resource.uri;
|
||||
|
||||
// remove forward links from old resource
|
||||
const resourcesPointedByDeletedNote = this.links[uri.path] ?? [];
|
||||
delete this.links[uri.path];
|
||||
resourcesPointedByDeletedNote.forEach(connection =>
|
||||
this.disconnect(uri, connection.target, connection.link)
|
||||
);
|
||||
|
||||
// recompute previous links to old resource
|
||||
const notesPointingToDeletedResource = this.backlinks[uri.path] ?? [];
|
||||
delete this.backlinks[uri.path];
|
||||
notesPointingToDeletedResource.forEach(link =>
|
||||
this.resolveResource(this.workspace.get(link.source))
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
private connect(source: URI, target: URI, link: ResourceLink) {
|
||||
const connection = { source, target, link };
|
||||
|
||||
this.links[source.path] = this.links[source.path] ?? [];
|
||||
this.links[source.path].push(connection);
|
||||
this.backlinks[target.path] = this.backlinks[target.path] ?? [];
|
||||
this.backlinks[target.path].push(connection);
|
||||
|
||||
if (URI.isPlaceholder(target)) {
|
||||
this.placeholders[uriToPlaceholderId(target)] = target;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection, or all connections, between the source and
|
||||
* target resources
|
||||
*
|
||||
* @param workspace the Foam workspace
|
||||
* @param source the source resource
|
||||
* @param target the target resource
|
||||
* @param link the link reference, or `true` to remove all links
|
||||
* @returns the updated Foam workspace
|
||||
*/
|
||||
private disconnect(source: URI, target: URI, link: ResourceLink | true) {
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!URI.isEqual(source, c.source) || !URI.isEqual(target, c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
|
||||
this.links[source.path] =
|
||||
this.links[source.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (this.links[source.path].length === 0) {
|
||||
delete this.links[source.path];
|
||||
}
|
||||
this.backlinks[target.path] =
|
||||
this.backlinks[target.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (this.backlinks[target.path].length === 0) {
|
||||
delete this.backlinks[target.path];
|
||||
if (URI.isPlaceholder(target)) {
|
||||
delete this.placeholders[uriToPlaceholderId(target)];
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public resolveResource(resource: Resource) {
|
||||
delete this.links[resource.uri.path];
|
||||
// prettier-ignore
|
||||
resource.links.forEach(link => {
|
||||
const targetUri = this.workspace.resolveLink(resource, link);
|
||||
this.connect(resource.uri, targetUri, link);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables = [];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move these utility fns to appropriate places
|
||||
|
||||
const isSameConnection = (a: Connection, b: Connection) =>
|
||||
URI.isEqual(a.source, b.source) &&
|
||||
URI.isEqual(a.target, b.target) &&
|
||||
isSameLink(a.link, b.link);
|
||||
|
||||
const isSameLink = (a: ResourceLink, b: ResourceLink) =>
|
||||
a.type === b.type && Range.isEqual(a.range, b.range);
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Position, Point } from 'unist';
|
||||
import { URI } from '../common/uri';
|
||||
import { getBasename } from '../utils';
|
||||
export { Position, Point };
|
||||
import { URI } from './uri';
|
||||
import { Position } from './position';
|
||||
import { Range } from './range';
|
||||
|
||||
export interface NoteSource {
|
||||
text: string;
|
||||
contentStart: Point;
|
||||
end: Point;
|
||||
contentStart: Position;
|
||||
end: Position;
|
||||
eol: string;
|
||||
}
|
||||
|
||||
@@ -14,63 +13,59 @@ export interface WikiLink {
|
||||
type: 'wikilink';
|
||||
slug: string;
|
||||
target: string;
|
||||
position: Position;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface DirectLink {
|
||||
type: 'link';
|
||||
label: string;
|
||||
target: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export type NoteLink = WikiLink | DirectLink;
|
||||
export type ResourceLink = WikiLink | DirectLink;
|
||||
|
||||
export interface NoteLinkDefinition {
|
||||
label: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
position?: Position;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
export interface BaseResource {
|
||||
export interface Resource {
|
||||
uri: URI;
|
||||
}
|
||||
|
||||
export interface Attachment extends BaseResource {
|
||||
type: 'attachment';
|
||||
}
|
||||
|
||||
export interface Placeholder extends BaseResource {
|
||||
type: 'placeholder';
|
||||
}
|
||||
|
||||
export interface Note extends BaseResource {
|
||||
type: 'note';
|
||||
title: string | null;
|
||||
type: string;
|
||||
title: string;
|
||||
properties: any;
|
||||
// sections: NoteSection[]
|
||||
tags: Set<string>;
|
||||
links: NoteLink[];
|
||||
links: ResourceLink[];
|
||||
|
||||
// TODO to remove
|
||||
definitions: NoteLinkDefinition[];
|
||||
source: NoteSource;
|
||||
}
|
||||
|
||||
export type Resource = Note | Attachment | Placeholder;
|
||||
|
||||
export interface NoteParser {
|
||||
parse: (uri: URI, text: string) => Note;
|
||||
export interface ResourceParser {
|
||||
parse: (uri: URI, text: string) => Resource;
|
||||
}
|
||||
|
||||
export const isWikilink = (link: NoteLink): link is WikiLink => {
|
||||
return link.type === 'wikilink';
|
||||
};
|
||||
export abstract class Resource {
|
||||
public static sortByTitle(a: Resource, b: Resource) {
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
|
||||
export const getTitle = (resource: Resource): string => {
|
||||
return resource.type === 'note'
|
||||
? resource.title ?? getBasename(resource.uri)
|
||||
: getBasename(resource.uri);
|
||||
};
|
||||
|
||||
export const isNote = (resource: Resource): resource is Note => {
|
||||
return resource.type === 'note';
|
||||
};
|
||||
public static isResource(thing: any): thing is Resource {
|
||||
if (!thing) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
URI.isUri((thing as Resource).uri) &&
|
||||
typeof (thing as Resource).title === 'string' &&
|
||||
typeof (thing as Resource).type === 'string' &&
|
||||
typeof (thing as Resource).properties === 'object' &&
|
||||
typeof (thing as Resource).tags === 'object' &&
|
||||
typeof (thing as Resource).links === 'object'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
91
packages/foam-core/src/model/position.ts
Normal file
91
packages/foam-core/src/model/position.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// Some code in this file coming from https://github.com/microsoft/vscode/
|
||||
// See LICENSE for details
|
||||
|
||||
export interface Position {
|
||||
line: number;
|
||||
character: number;
|
||||
}
|
||||
|
||||
export abstract class Position {
|
||||
static create(line: number, character: number): Position {
|
||||
return { line, character };
|
||||
}
|
||||
|
||||
static Min(...positions: Position[]): Position {
|
||||
if (positions.length === 0) {
|
||||
throw new TypeError();
|
||||
}
|
||||
let result = positions[0];
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
const p = positions[i];
|
||||
if (Position.isBefore(p, result!)) {
|
||||
result = p;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static Max(...positions: Position[]): Position {
|
||||
if (positions.length === 0) {
|
||||
throw new TypeError();
|
||||
}
|
||||
let result = positions[0];
|
||||
for (let i = 1; i < positions.length; i++) {
|
||||
const p = positions[i];
|
||||
if (Position.isAfter(p, result!)) {
|
||||
result = p;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static isBefore(p1: Position, p2: Position): boolean {
|
||||
if (p1.line < p2.line) {
|
||||
return true;
|
||||
}
|
||||
if (p2.line < p1.line) {
|
||||
return false;
|
||||
}
|
||||
return p1.character < p2.character;
|
||||
}
|
||||
|
||||
static isBeforeOrEqual(p1: Position, p2: Position): boolean {
|
||||
if (p1.line < p2.line) {
|
||||
return true;
|
||||
}
|
||||
if (p2.line < p1.line) {
|
||||
return false;
|
||||
}
|
||||
return p1.character <= p2.character;
|
||||
}
|
||||
|
||||
static isAfter(p1: Position, p2: Position): boolean {
|
||||
return !Position.isBeforeOrEqual(p1, p2);
|
||||
}
|
||||
|
||||
static isAfterOrEqual(p1: Position, p2: Position): boolean {
|
||||
return !Position.isBefore(p1, p2);
|
||||
}
|
||||
|
||||
static isEqual(p1: Position, p2: Position): boolean {
|
||||
return p1.line === p2.line && p1.character === p2.character;
|
||||
}
|
||||
|
||||
static compareTo(p1: Position, p2: Position): number {
|
||||
if (p1.line < p2.line) {
|
||||
return -1;
|
||||
} else if (p1.line > p2.line) {
|
||||
return 1;
|
||||
} else {
|
||||
// equal line
|
||||
if (p1.character < p2.character) {
|
||||
return -1;
|
||||
} else if (p1.character > p2.character) {
|
||||
return 1;
|
||||
} else {
|
||||
// equal line and character
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
packages/foam-core/src/model/provider.ts
Normal file
17
packages/foam-core/src/model/provider.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IDisposable } from 'common/lifecycle';
|
||||
import { ResourceLink, URI } from 'index';
|
||||
import { Resource } from './note';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
|
||||
export interface ResourceProvider extends IDisposable {
|
||||
init: (workspace: FoamWorkspace) => Promise<void>;
|
||||
supports: (uri: URI) => boolean;
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
readAsMarkdown: (uri: URI) => Promise<string | null>;
|
||||
fetch: (uri: URI) => Promise<Resource | null>;
|
||||
resolveLink: (
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource,
|
||||
link: ResourceLink
|
||||
) => URI;
|
||||
}
|
||||
72
packages/foam-core/src/model/range.ts
Normal file
72
packages/foam-core/src/model/range.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// Some code in this file coming from https://github.com/microsoft/vscode/
|
||||
// See LICENSE for details
|
||||
|
||||
import { Position } from './position';
|
||||
|
||||
export interface Range {
|
||||
start: Position;
|
||||
end: Position;
|
||||
}
|
||||
|
||||
export abstract class Range {
|
||||
static create(
|
||||
startLine: number,
|
||||
startChar: number,
|
||||
endLine?: number,
|
||||
endChar?: number
|
||||
): Range {
|
||||
const start: Position = {
|
||||
line: startLine,
|
||||
character: startChar,
|
||||
};
|
||||
const end: Position = {
|
||||
line: endLine ?? startLine,
|
||||
character: endChar ?? startChar,
|
||||
};
|
||||
return Range.createFromPosition(start, end);
|
||||
}
|
||||
|
||||
static createFromPosition(start: Position, end?: Position) {
|
||||
end = end ?? start;
|
||||
let first = start;
|
||||
let second = end;
|
||||
if (Position.isAfter(start, end)) {
|
||||
first = end;
|
||||
second = start;
|
||||
}
|
||||
return {
|
||||
start: {
|
||||
line: first.line,
|
||||
character: first.character,
|
||||
},
|
||||
end: {
|
||||
line: second.line,
|
||||
character: second.character,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static containsRange(range: Range, contained: Range): boolean {
|
||||
return (
|
||||
Range.containsPosition(range, contained.start) &&
|
||||
Range.containsPosition(range, contained.end)
|
||||
);
|
||||
}
|
||||
|
||||
static containsPosition(range: Range, position: Position): boolean {
|
||||
return (
|
||||
Position.isAfterOrEqual(position, range.start) &&
|
||||
Position.isBeforeOrEqual(position, range.end)
|
||||
);
|
||||
}
|
||||
|
||||
static isEqual(r1: Range, r2: Range): boolean {
|
||||
return (
|
||||
Position.isEqual(r1.start, r2.start) && Position.isEqual(r1.end, r2.end)
|
||||
);
|
||||
}
|
||||
|
||||
static isBefore(a: Range, b: Range): number {
|
||||
return a.start.line - b.start.line || a.start.character - b.start.character;
|
||||
}
|
||||
}
|
||||
461
packages/foam-core/src/model/uri.ts
Normal file
461
packages/foam-core/src/model/uri.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
// Some code in this file coming from https://github.com/microsoft/vscode/
|
||||
// See LICENSE for details
|
||||
|
||||
import * as paths from 'path';
|
||||
import { CharCode } from '../common/charCode';
|
||||
import { isWindows } from '../common/platform';
|
||||
|
||||
/**
|
||||
* Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
|
||||
* This class is a simple parser which creates the basic component parts
|
||||
* (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
|
||||
* and encoding.
|
||||
*
|
||||
* ```txt
|
||||
* foo://example.com:8042/over/there?name=ferret#nose
|
||||
* \_/ \______________/\_________/ \_________/ \__/
|
||||
* | | | | |
|
||||
* scheme authority path query fragment
|
||||
* | _____________________|__
|
||||
* / \ / \
|
||||
* urn:example:animal:ferret:nose
|
||||
* ```
|
||||
*/
|
||||
export interface URI {
|
||||
scheme: string;
|
||||
authority: string;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}
|
||||
|
||||
const { posix } = paths;
|
||||
const _empty = '';
|
||||
const _slash = '/';
|
||||
const _regexp = /^(([^:/?#]{2,}?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
||||
|
||||
export abstract class URI {
|
||||
static create(from: Partial<URI>): URI {
|
||||
return {
|
||||
scheme: from.scheme ?? _empty,
|
||||
authority: from.authority ?? _empty,
|
||||
path: from.path ?? _empty,
|
||||
query: from.query ?? _empty,
|
||||
fragment: from.fragment ?? _empty,
|
||||
};
|
||||
}
|
||||
|
||||
static parse(value: string): URI {
|
||||
const match = _regexp.exec(value);
|
||||
if (!match) {
|
||||
return URI.create({});
|
||||
}
|
||||
return URI.create({
|
||||
scheme: match[2] || 'file',
|
||||
authority: percentDecode(match[4] ?? _empty),
|
||||
path: percentDecode(match[5] ?? _empty),
|
||||
query: percentDecode(match[7] ?? _empty),
|
||||
fragment: percentDecode(match[9] ?? _empty),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a URI from value, taking into consideration possible relative paths.
|
||||
*
|
||||
* @param reference the URI to use as reference in case value is a relative path
|
||||
* @param value the value to parse for a URI
|
||||
* @returns the URI from the given value. In case of a relative path, the URI will take into account
|
||||
* the reference from which it is computed
|
||||
*/
|
||||
static resolve(value: string, reference: URI): URI {
|
||||
let uri = URI.parse(value);
|
||||
if (uri.scheme === 'file' && !value.startsWith('/')) {
|
||||
const [path, fragment] = value.split('#');
|
||||
uri =
|
||||
path.length > 0 ? URI.computeRelativeURI(reference, path) : reference;
|
||||
if (fragment) {
|
||||
uri = URI.create({
|
||||
...uri,
|
||||
fragment: fragment,
|
||||
});
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
static computeRelativeURI(reference: URI, relativeSlug: string): URI {
|
||||
// if no extension is provided, use the same extension as the source file
|
||||
const slug =
|
||||
posix.extname(relativeSlug) !== ''
|
||||
? relativeSlug
|
||||
: `${relativeSlug}${posix.extname(reference.path)}`;
|
||||
return URI.create({
|
||||
...reference,
|
||||
path: posix.join(posix.dirname(reference.path), slug),
|
||||
});
|
||||
}
|
||||
|
||||
static file(path: string): URI {
|
||||
let authority = _empty;
|
||||
|
||||
// normalize to fwd-slashes on windows,
|
||||
// on other systems bwd-slashes are valid
|
||||
// filename character, eg /f\oo/ba\r.txt
|
||||
if (isWindows) {
|
||||
if (path.startsWith(_slash)) {
|
||||
path = `${path.replace(/\\/g, _slash)}`;
|
||||
} else {
|
||||
path = `/${path.replace(/\\/g, _slash)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// check for authority as used in UNC shares
|
||||
// or use the path as given
|
||||
if (path[0] === _slash && path[1] === _slash) {
|
||||
const idx = path.indexOf(_slash, 2);
|
||||
if (idx === -1) {
|
||||
authority = path.substring(2);
|
||||
path = _slash;
|
||||
} else {
|
||||
authority = path.substring(2, idx);
|
||||
path = path.substring(idx) || _slash;
|
||||
}
|
||||
}
|
||||
|
||||
return URI.create({ scheme: 'file', authority, path });
|
||||
}
|
||||
|
||||
static placeholder(key: string): URI {
|
||||
return URI.create({
|
||||
scheme: 'placeholder',
|
||||
path: key,
|
||||
});
|
||||
}
|
||||
|
||||
static relativePath(source: URI, target: URI): string {
|
||||
const relativePath = posix.relative(
|
||||
posix.dirname(source.path),
|
||||
target.path
|
||||
);
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
static getBasename(uri: URI) {
|
||||
return posix.parse(uri.path).name;
|
||||
}
|
||||
|
||||
static getDir(uri: URI) {
|
||||
return URI.file(posix.dirname(uri.path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a placeholder URI, and a reference directory, to generate
|
||||
* the URI of the corresponding resource
|
||||
*
|
||||
* @param placeholderUri the placeholder URI
|
||||
* @param basedir the dir to be used as reference
|
||||
* @returns the target resource URI
|
||||
*/
|
||||
static createResourceUriFromPlaceholder(
|
||||
basedir: URI,
|
||||
placeholderUri: URI
|
||||
): URI {
|
||||
const tokens = placeholderUri.path.split('/');
|
||||
const path = tokens.slice(0, -1);
|
||||
const filename = tokens.slice(-1);
|
||||
return URI.joinPath(basedir, ...path, `${filename}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a URI path with path fragments and normalizes the resulting path.
|
||||
*
|
||||
* @param uri The input URI.
|
||||
* @param pathFragment The path fragment to add to the URI path.
|
||||
* @returns The resulting URI.
|
||||
*/
|
||||
static joinPath(uri: URI, ...pathFragment: string[]): URI {
|
||||
if (!uri.path) {
|
||||
throw new Error(`[UriError]: cannot call joinPath on URI without path`);
|
||||
}
|
||||
let newPath: string;
|
||||
if (isWindows && uri.scheme === 'file') {
|
||||
newPath = URI.file(paths.win32.join(URI.toFsPath(uri), ...pathFragment))
|
||||
.path;
|
||||
} else {
|
||||
newPath = paths.posix.join(uri.path, ...pathFragment);
|
||||
}
|
||||
return URI.create({ ...uri, path: newPath });
|
||||
}
|
||||
|
||||
static toFsPath(uri: URI, keepDriveLetterCasing = true): string {
|
||||
let value: string;
|
||||
if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') {
|
||||
// unc path: file://shares/c$/far/boo
|
||||
value = `//${uri.authority}${uri.path}`;
|
||||
} else if (
|
||||
uri.path.charCodeAt(0) === CharCode.Slash &&
|
||||
((uri.path.charCodeAt(1) >= CharCode.A &&
|
||||
uri.path.charCodeAt(1) <= CharCode.Z) ||
|
||||
(uri.path.charCodeAt(1) >= CharCode.a &&
|
||||
uri.path.charCodeAt(1) <= CharCode.z)) &&
|
||||
uri.path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
if (!keepDriveLetterCasing) {
|
||||
// windows drive letter: file:///c:/far/boo
|
||||
value = uri.path[1].toLowerCase() + uri.path.substr(2);
|
||||
} else {
|
||||
value = uri.path.substr(1);
|
||||
}
|
||||
} else {
|
||||
// other path
|
||||
value = uri.path;
|
||||
}
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, '\\');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static toString(uri: URI): string {
|
||||
return encode(uri, false);
|
||||
}
|
||||
|
||||
// --- utility
|
||||
|
||||
static isUri(thing: any): thing is URI {
|
||||
if (!thing) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
typeof (thing as URI).authority === 'string' &&
|
||||
typeof (thing as URI).fragment === 'string' &&
|
||||
typeof (thing as URI).path === 'string' &&
|
||||
typeof (thing as URI).query === 'string' &&
|
||||
typeof (thing as URI).scheme === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
static isPlaceholder(uri: URI): boolean {
|
||||
return uri.scheme === 'placeholder';
|
||||
}
|
||||
|
||||
static isEqual(a: URI, b: URI): boolean {
|
||||
return (
|
||||
a.authority === b.authority &&
|
||||
a.scheme === b.scheme &&
|
||||
a.path === b.path &&
|
||||
a.fragment === b.fragment &&
|
||||
a.query === b.query
|
||||
);
|
||||
}
|
||||
static isMarkdownFile(uri: URI): boolean {
|
||||
return uri.path.endsWith('.md');
|
||||
}
|
||||
}
|
||||
|
||||
// --- encode / decode
|
||||
|
||||
function decodeURIComponentGraceful(str: string): string {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
if (str.length > 3) {
|
||||
return str.substr(0, 3) + decodeURIComponentGraceful(str.substr(3));
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _rEncodedAsHex = /(%[0-9A-Za-z][0-9A-Za-z])+/g;
|
||||
|
||||
function percentDecode(str: string): string {
|
||||
if (!str.match(_rEncodedAsHex)) {
|
||||
return str;
|
||||
}
|
||||
return str.replace(_rEncodedAsHex, match =>
|
||||
decodeURIComponentGraceful(match)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the external version of a uri
|
||||
*/
|
||||
function encode(uri: URI, skipEncoding: boolean): string {
|
||||
const encoder = !skipEncoding
|
||||
? encodeURIComponentFast
|
||||
: encodeURIComponentMinimal;
|
||||
|
||||
let res = '';
|
||||
let { scheme, authority, path, query, fragment } = uri;
|
||||
if (scheme) {
|
||||
res += scheme;
|
||||
res += ':';
|
||||
}
|
||||
if (authority || scheme === 'file') {
|
||||
res += _slash;
|
||||
res += _slash;
|
||||
}
|
||||
if (authority) {
|
||||
let idx = authority.indexOf('@');
|
||||
if (idx !== -1) {
|
||||
// <user>@<auth>
|
||||
const userinfo = authority.substr(0, idx);
|
||||
authority = authority.substr(idx + 1);
|
||||
idx = userinfo.indexOf(':');
|
||||
if (idx === -1) {
|
||||
res += encoder(userinfo, false);
|
||||
} else {
|
||||
// <user>:<pass>@<auth>
|
||||
res += encoder(userinfo.substr(0, idx), false);
|
||||
res += ':';
|
||||
res += encoder(userinfo.substr(idx + 1), false);
|
||||
}
|
||||
res += '@';
|
||||
}
|
||||
authority = authority.toLowerCase();
|
||||
idx = authority.indexOf(':');
|
||||
if (idx === -1) {
|
||||
res += encoder(authority, false);
|
||||
} else {
|
||||
// <auth>:<port>
|
||||
res += encoder(authority.substr(0, idx), false);
|
||||
res += authority.substr(idx);
|
||||
}
|
||||
}
|
||||
if (path) {
|
||||
// lower-case windows drive letters in /C:/fff or C:/fff
|
||||
if (
|
||||
path.length >= 3 &&
|
||||
path.charCodeAt(0) === CharCode.Slash &&
|
||||
path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
const code = path.charCodeAt(1);
|
||||
if (code >= CharCode.A && code <= CharCode.Z) {
|
||||
path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3
|
||||
}
|
||||
} else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) {
|
||||
const code = path.charCodeAt(0);
|
||||
if (code >= CharCode.A && code <= CharCode.Z) {
|
||||
path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3
|
||||
}
|
||||
}
|
||||
// encode the rest of the path
|
||||
res += encoder(path, true);
|
||||
}
|
||||
if (query) {
|
||||
res += '?';
|
||||
res += encoder(query, false);
|
||||
}
|
||||
if (fragment) {
|
||||
res += '#';
|
||||
res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
const encodeTable: { [ch: number]: string } = {
|
||||
[CharCode.Colon]: '%3A', // gen-delims
|
||||
[CharCode.Slash]: '%2F',
|
||||
[CharCode.QuestionMark]: '%3F',
|
||||
[CharCode.Hash]: '%23',
|
||||
[CharCode.OpenSquareBracket]: '%5B',
|
||||
[CharCode.CloseSquareBracket]: '%5D',
|
||||
[CharCode.AtSign]: '%40',
|
||||
|
||||
[CharCode.ExclamationMark]: '%21', // sub-delims
|
||||
[CharCode.DollarSign]: '%24',
|
||||
[CharCode.Ampersand]: '%26',
|
||||
[CharCode.SingleQuote]: '%27',
|
||||
[CharCode.OpenParen]: '%28',
|
||||
[CharCode.CloseParen]: '%29',
|
||||
[CharCode.Asterisk]: '%2A',
|
||||
[CharCode.Plus]: '%2B',
|
||||
[CharCode.Comma]: '%2C',
|
||||
[CharCode.Semicolon]: '%3B',
|
||||
[CharCode.Equals]: '%3D',
|
||||
|
||||
[CharCode.Space]: '%20',
|
||||
};
|
||||
|
||||
function encodeURIComponentFast(
|
||||
uriComponent: string,
|
||||
allowSlash: boolean
|
||||
): string {
|
||||
let res: string | undefined = undefined;
|
||||
let nativeEncodePos = -1;
|
||||
|
||||
for (let pos = 0; pos < uriComponent.length; pos++) {
|
||||
const code = uriComponent.charCodeAt(pos);
|
||||
|
||||
// unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3
|
||||
if (
|
||||
(code >= CharCode.a && code <= CharCode.z) ||
|
||||
(code >= CharCode.A && code <= CharCode.Z) ||
|
||||
(code >= CharCode.Digit0 && code <= CharCode.Digit9) ||
|
||||
code === CharCode.Dash ||
|
||||
code === CharCode.Period ||
|
||||
code === CharCode.Underline ||
|
||||
code === CharCode.Tilde ||
|
||||
(allowSlash && code === CharCode.Slash)
|
||||
) {
|
||||
// check if we are delaying native encode
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos));
|
||||
nativeEncodePos = -1;
|
||||
}
|
||||
// check if we write into a new string (by default we try to return the param)
|
||||
if (res !== undefined) {
|
||||
res += uriComponent.charAt(pos);
|
||||
}
|
||||
} else {
|
||||
// encoding needed, we need to allocate a new string
|
||||
if (res === undefined) {
|
||||
res = uriComponent.substr(0, pos);
|
||||
}
|
||||
|
||||
// check with default table first
|
||||
const escaped = encodeTable[code];
|
||||
if (escaped !== undefined) {
|
||||
// check if we are delaying native encode
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(
|
||||
uriComponent.substring(nativeEncodePos, pos)
|
||||
);
|
||||
nativeEncodePos = -1;
|
||||
}
|
||||
|
||||
// append escaped variant to result
|
||||
res += escaped;
|
||||
} else if (nativeEncodePos === -1) {
|
||||
// use native encode only when needed
|
||||
nativeEncodePos = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeEncodePos !== -1) {
|
||||
res += encodeURIComponent(uriComponent.substring(nativeEncodePos));
|
||||
}
|
||||
|
||||
return res !== undefined ? res : uriComponent;
|
||||
}
|
||||
|
||||
function encodeURIComponentMinimal(path: string): string {
|
||||
let res: string | undefined = undefined;
|
||||
for (let pos = 0; pos < path.length; pos++) {
|
||||
const code = path.charCodeAt(pos);
|
||||
if (code === CharCode.Hash || code === CharCode.QuestionMark) {
|
||||
if (res === undefined) {
|
||||
res = path.substr(0, pos);
|
||||
}
|
||||
res += encodeTable[code];
|
||||
} else {
|
||||
if (res !== undefined) {
|
||||
res += path[pos];
|
||||
}
|
||||
}
|
||||
}
|
||||
return res !== undefined ? res : path;
|
||||
}
|
||||
@@ -1,23 +1,10 @@
|
||||
import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { URI } from '../common/uri';
|
||||
import { Resource, NoteLink, Note } from '../model/note';
|
||||
import {
|
||||
computeRelativeURI,
|
||||
isSome,
|
||||
isNone,
|
||||
parseUri,
|
||||
placeholderUri,
|
||||
isPlaceholder,
|
||||
} from '../utils';
|
||||
import { Resource, ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { isSome, isNone } from '../utils';
|
||||
import { Emitter } from '../common/event';
|
||||
import { IDisposable } from '../index';
|
||||
|
||||
export type Connection = {
|
||||
source: URI;
|
||||
target: URI;
|
||||
};
|
||||
import { ResourceProvider } from './provider';
|
||||
|
||||
export function getReferenceType(
|
||||
reference: URI | string
|
||||
@@ -40,10 +27,7 @@ const pathToResourceId = (pathValue: string) => {
|
||||
const uriToResourceId = (uri: URI) => pathToResourceId(uri.path);
|
||||
|
||||
const pathToResourceName = (pathValue: string) => path.parse(pathValue).name;
|
||||
const uriToResourceName = (uri: URI) => pathToResourceName(uri.path);
|
||||
|
||||
const pathToPlaceholderId = (value: string) => value;
|
||||
const uriToPlaceholderId = (uri: URI) => pathToPlaceholderId(uri.path);
|
||||
export const uriToResourceName = (uri: URI) => pathToResourceName(uri.path);
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
@@ -53,6 +37,8 @@ export class FoamWorkspace implements IDisposable {
|
||||
onDidUpdate = this.onDidUpdateEmitter.event;
|
||||
onDidDelete = this.onDidDeleteEmitter.event;
|
||||
|
||||
private providers: ResourceProvider[] = [];
|
||||
|
||||
/**
|
||||
* Resources by key / slug
|
||||
*/
|
||||
@@ -61,204 +47,53 @@ export class FoamWorkspace implements IDisposable {
|
||||
* Resources by URI
|
||||
*/
|
||||
private resources: { [key: string]: Resource } = {};
|
||||
/**
|
||||
* Placehoders by key / slug / value
|
||||
*/
|
||||
private placeholders: { [key: string]: Resource } = {};
|
||||
|
||||
/**
|
||||
* Maps the connections starting from a URI
|
||||
*/
|
||||
private links: { [key: string]: Connection[] } = {};
|
||||
/**
|
||||
* Maps the connections arriving to a URI
|
||||
*/
|
||||
private backlinks: { [key: string]: Connection[] } = {};
|
||||
/**
|
||||
* List of disposables to destroy with the workspace
|
||||
*/
|
||||
disposables: IDisposable[] = [];
|
||||
registerProvider(provider: ResourceProvider) {
|
||||
this.providers.push(provider);
|
||||
return provider.init(this);
|
||||
}
|
||||
|
||||
exists(uri: URI) {
|
||||
return FoamWorkspace.exists(this, uri);
|
||||
}
|
||||
list() {
|
||||
return FoamWorkspace.list(this);
|
||||
}
|
||||
get(uri: URI) {
|
||||
return FoamWorkspace.get(this, uri);
|
||||
}
|
||||
find(uri: URI) {
|
||||
return FoamWorkspace.find(this, uri);
|
||||
}
|
||||
set(resource: Resource) {
|
||||
return FoamWorkspace.set(this, resource);
|
||||
}
|
||||
delete(uri: URI) {
|
||||
return FoamWorkspace.delete(this, uri);
|
||||
}
|
||||
|
||||
resolveLink(note: Note, link: NoteLink) {
|
||||
return FoamWorkspace.resolveLink(this, note, link);
|
||||
}
|
||||
resolveLinks(keepMonitoring: boolean = false) {
|
||||
return FoamWorkspace.resolveLinks(this, keepMonitoring);
|
||||
}
|
||||
getAllConnections() {
|
||||
return FoamWorkspace.getAllConnections(this);
|
||||
}
|
||||
getConnections(uri: URI) {
|
||||
return FoamWorkspace.getConnections(this, uri);
|
||||
}
|
||||
getLinks(uri: URI) {
|
||||
return FoamWorkspace.getLinks(this, uri);
|
||||
}
|
||||
getBacklinks(uri: URI) {
|
||||
return FoamWorkspace.getBacklinks(this, uri);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.onDidAddEmitter.dispose();
|
||||
this.onDidDeleteEmitter.dispose();
|
||||
this.onDidUpdateEmitter.dispose();
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
|
||||
public static resolveLink(
|
||||
workspace: FoamWorkspace,
|
||||
note: Note,
|
||||
link: NoteLink
|
||||
): URI {
|
||||
let targetUri: URI | null = null;
|
||||
switch (link.type) {
|
||||
case 'wikilink':
|
||||
const definitionUri = note.definitions.find(
|
||||
def => def.label === link.slug
|
||||
)?.url;
|
||||
if (isSome(definitionUri)) {
|
||||
targetUri = parseUri(note.uri, definitionUri!);
|
||||
} else {
|
||||
targetUri =
|
||||
FoamWorkspace.find(workspace, link.slug, note.uri)?.uri ??
|
||||
placeholderUri(link.slug);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
targetUri =
|
||||
FoamWorkspace.find(workspace, link.target, note.uri)?.uri ??
|
||||
placeholderUri(link.target);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isPlaceholder(targetUri)) {
|
||||
// we can only add placeholders when links are being resolved
|
||||
workspace = FoamWorkspace.set(workspace, {
|
||||
type: 'placeholder',
|
||||
uri: targetUri,
|
||||
});
|
||||
}
|
||||
return targetUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes all the links in the workspace, connecting notes and
|
||||
* creating placeholders.
|
||||
*
|
||||
* @param workspace the target workspace
|
||||
* @param keepMonitoring whether to recompute the links when the workspace changes
|
||||
* @returns the resolved workspace
|
||||
*/
|
||||
public static resolveLinks(
|
||||
workspace: FoamWorkspace,
|
||||
keepMonitoring: boolean = false
|
||||
): FoamWorkspace {
|
||||
workspace.links = {};
|
||||
workspace.backlinks = {};
|
||||
workspace.placeholders = {};
|
||||
|
||||
workspace = Object.values(workspace.list()).reduce(
|
||||
(w, resource) => FoamWorkspace.resolveResource(w, resource),
|
||||
workspace
|
||||
);
|
||||
if (keepMonitoring) {
|
||||
workspace.disposables.push(
|
||||
workspace.onDidAdd(resource => {
|
||||
FoamWorkspace.updateLinksRelatedToAddedResource(workspace, resource);
|
||||
}),
|
||||
workspace.onDidUpdate(change => {
|
||||
FoamWorkspace.updateLinksForResource(
|
||||
workspace,
|
||||
change.old,
|
||||
change.new
|
||||
);
|
||||
}),
|
||||
workspace.onDidDelete(resource => {
|
||||
FoamWorkspace.updateLinksRelatedToDeletedResource(
|
||||
workspace,
|
||||
resource
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
public static getAllConnections(workspace: FoamWorkspace): Connection[] {
|
||||
return Object.values(workspace.links).flat();
|
||||
}
|
||||
|
||||
public static getConnections(
|
||||
workspace: FoamWorkspace,
|
||||
uri: URI
|
||||
): Connection[] {
|
||||
return [
|
||||
...(workspace.links[uri.path] || []),
|
||||
...(workspace.backlinks[uri.path] || []),
|
||||
];
|
||||
}
|
||||
|
||||
public static getLinks(workspace: FoamWorkspace, uri: URI): URI[] {
|
||||
return workspace.links[uri.path]?.map(c => c.target) ?? [];
|
||||
}
|
||||
|
||||
public static getBacklinks(workspace: FoamWorkspace, uri: URI): URI[] {
|
||||
return workspace.backlinks[uri.path]?.map(c => c.source) ?? [];
|
||||
}
|
||||
|
||||
public static set(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource
|
||||
): FoamWorkspace {
|
||||
if (resource.type === 'placeholder') {
|
||||
workspace.placeholders[uriToPlaceholderId(resource.uri)] = resource;
|
||||
return workspace;
|
||||
}
|
||||
const id = uriToResourceId(resource.uri);
|
||||
const old = FoamWorkspace.find(workspace, resource.uri);
|
||||
const old = this.find(resource.uri);
|
||||
const name = uriToResourceName(resource.uri);
|
||||
workspace.resources[id] = resource;
|
||||
workspace.resourcesByName[name] = workspace.resourcesByName[name] ?? [];
|
||||
workspace.resourcesByName[name].push(id);
|
||||
this.resources[id] = resource;
|
||||
this.resourcesByName[name] = this.resourcesByName[name] ?? [];
|
||||
this.resourcesByName[name].push(id);
|
||||
isSome(old)
|
||||
? workspace.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: workspace.onDidAddEmitter.fire(resource);
|
||||
return workspace;
|
||||
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: this.onDidAddEmitter.fire(resource);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static exists(workspace: FoamWorkspace, uri: URI): boolean {
|
||||
return isSome(workspace.resources[uriToResourceId(uri)]);
|
||||
delete(uri: URI) {
|
||||
const id = uriToResourceId(uri);
|
||||
const deleted = this.resources[id];
|
||||
delete this.resources[id];
|
||||
|
||||
const name = uriToResourceName(uri);
|
||||
this.resourcesByName[name] =
|
||||
this.resourcesByName[name]?.filter(resId => resId !== id) ?? [];
|
||||
if (this.resourcesByName[name].length === 0) {
|
||||
delete this.resourcesByName[name];
|
||||
}
|
||||
|
||||
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
}
|
||||
|
||||
public static list(workspace: FoamWorkspace): Resource[] {
|
||||
return [
|
||||
...Object.values(workspace.resources),
|
||||
...Object.values(workspace.placeholders),
|
||||
];
|
||||
public exists(uri: URI): boolean {
|
||||
return (
|
||||
!URI.isPlaceholder(uri) && isSome(this.resources[uriToResourceId(uri)])
|
||||
);
|
||||
}
|
||||
|
||||
public static get(workspace: FoamWorkspace, uri: URI): Resource {
|
||||
const note = FoamWorkspace.find(workspace, uri);
|
||||
public list(): Resource[] {
|
||||
return Object.values(this.resources);
|
||||
}
|
||||
|
||||
public get(uri: URI): Resource {
|
||||
const note = this.find(uri);
|
||||
if (isSome(note)) {
|
||||
return note;
|
||||
} else {
|
||||
@@ -266,199 +101,64 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public static find(
|
||||
workspace: FoamWorkspace,
|
||||
resourceId: URI | string,
|
||||
reference?: URI
|
||||
): Resource | null {
|
||||
public find(resourceId: URI | string, reference?: URI): Resource | null {
|
||||
const refType = getReferenceType(resourceId);
|
||||
switch (refType) {
|
||||
case 'uri':
|
||||
const uri = resourceId as URI;
|
||||
if (uri.scheme === 'placeholder') {
|
||||
return uri.path in workspace.placeholders
|
||||
? { type: 'placeholder', uri: uri }
|
||||
: null;
|
||||
} else {
|
||||
return FoamWorkspace.exists(workspace, uri)
|
||||
? workspace.resources[uriToResourceId(uri)]
|
||||
: null;
|
||||
}
|
||||
return this.exists(uri) ? this.resources[uriToResourceId(uri)] : null;
|
||||
|
||||
case 'key':
|
||||
const name = pathToResourceName(resourceId as string);
|
||||
const paths = workspace.resourcesByName[name];
|
||||
const paths = this.resourcesByName[name];
|
||||
if (isNone(paths) || paths.length === 0) {
|
||||
const placeholderId = pathToPlaceholderId(resourceId as string);
|
||||
return workspace.placeholders[placeholderId] ?? null;
|
||||
return null;
|
||||
}
|
||||
// prettier-ignore
|
||||
const sortedPaths = paths.length === 1
|
||||
? paths
|
||||
: paths.sort((a, b) => a.localeCompare(b));
|
||||
return workspace.resources[sortedPaths[0]];
|
||||
return this.resources[sortedPaths[0]];
|
||||
|
||||
case 'absolute-path':
|
||||
const resourceUri = URI.file(resourceId as string);
|
||||
return (
|
||||
workspace.resources[uriToResourceId(resourceUri)] ??
|
||||
workspace.placeholders[uriToPlaceholderId(resourceUri)]
|
||||
);
|
||||
return this.resources[uriToResourceId(resourceUri)] ?? null;
|
||||
|
||||
case 'relative-path':
|
||||
if (isNone(reference)) {
|
||||
throw new Error(
|
||||
'Cannot find note defined by relative path without reference note: ' +
|
||||
resourceId
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const relativePath = resourceId as string;
|
||||
const targetUri = computeRelativeURI(reference, relativePath);
|
||||
return (
|
||||
workspace.resources[uriToResourceId(targetUri)] ??
|
||||
workspace.placeholders[pathToPlaceholderId(resourceId as string)]
|
||||
);
|
||||
const targetUri = URI.computeRelativeURI(reference, relativePath);
|
||||
return this.resources[uriToResourceId(targetUri)] ?? null;
|
||||
|
||||
default:
|
||||
throw new Error('Unexpected reference type: ' + refType);
|
||||
}
|
||||
}
|
||||
|
||||
public static delete(workspace: FoamWorkspace, uri: URI): Resource | null {
|
||||
const id = uriToResourceId(uri);
|
||||
const deleted = workspace.resources[id];
|
||||
delete workspace.resources[id];
|
||||
|
||||
const name = uriToResourceName(uri);
|
||||
workspace.resourcesByName[name] = workspace.resourcesByName[name].filter(
|
||||
resId => resId !== id
|
||||
public resolveLink(resource: Resource, link: ResourceLink): URI {
|
||||
// TODO add tests
|
||||
const provider = this.providers.find(p => p.supports(resource.uri));
|
||||
return (
|
||||
provider?.resolveLink(this, resource, link) ??
|
||||
URI.placeholder(link.target)
|
||||
);
|
||||
if (workspace.resourcesByName[name].length === 0) {
|
||||
delete workspace.resourcesByName[name];
|
||||
}
|
||||
|
||||
isSome(deleted) && workspace.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
}
|
||||
|
||||
public static resolveResource(workspace: FoamWorkspace, resource: Resource) {
|
||||
if (resource.type === 'note') {
|
||||
delete workspace.links[resource.uri.path];
|
||||
// prettier-ignore
|
||||
resource.links.forEach(link => {
|
||||
const targetUri = FoamWorkspace.resolveLink(workspace, resource, link);
|
||||
workspace = FoamWorkspace.connect(workspace, resource.uri, targetUri);
|
||||
});
|
||||
}
|
||||
return workspace;
|
||||
public read(uri: URI): Promise<string | null> {
|
||||
const provider = this.providers.find(p => p.supports(uri));
|
||||
return provider?.read(uri) ?? Promise.resolve(null);
|
||||
}
|
||||
|
||||
private static updateLinksForResource(
|
||||
workspace: FoamWorkspace,
|
||||
oldResource: Resource,
|
||||
newResource: Resource
|
||||
) {
|
||||
if (oldResource.uri.path !== newResource.uri.path) {
|
||||
throw new Error(
|
||||
'Unexpected State: update should only be called on same resource ' +
|
||||
{
|
||||
old: oldResource,
|
||||
new: newResource,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (oldResource.type === 'note' && newResource.type === 'note') {
|
||||
const patch = diff(oldResource.links, newResource.links, isEqual);
|
||||
workspace = patch.removed.reduce((ws, link) => {
|
||||
const target = ws.resolveLink(oldResource, link);
|
||||
return FoamWorkspace.disconnect(ws, oldResource.uri, target);
|
||||
}, workspace);
|
||||
workspace = patch.added.reduce((ws, link) => {
|
||||
const target = ws.resolveLink(newResource, link);
|
||||
return FoamWorkspace.connect(ws, newResource.uri, target);
|
||||
}, workspace);
|
||||
}
|
||||
return workspace;
|
||||
public readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
const provider = this.providers.find(p => p.supports(uri));
|
||||
return provider?.readAsMarkdown(uri) ?? Promise.resolve(null);
|
||||
}
|
||||
|
||||
private static updateLinksRelatedToAddedResource(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource
|
||||
) {
|
||||
// check if any existing connection can be filled by new resource
|
||||
const name = uriToResourceName(resource.uri);
|
||||
if (name in workspace.placeholders) {
|
||||
const placeholder = workspace.placeholders[name];
|
||||
delete workspace.placeholders[name];
|
||||
const resourcesToUpdate = workspace.backlinks[placeholder.uri.path] ?? [];
|
||||
workspace = resourcesToUpdate.reduce(
|
||||
(ws, res) => FoamWorkspace.resolveResource(ws, ws.get(res.source)),
|
||||
workspace
|
||||
);
|
||||
}
|
||||
|
||||
// resolve the resource
|
||||
workspace = FoamWorkspace.resolveResource(workspace, resource);
|
||||
}
|
||||
|
||||
private static updateLinksRelatedToDeletedResource(
|
||||
workspace: FoamWorkspace,
|
||||
resource: Resource
|
||||
) {
|
||||
const uri = resource.uri;
|
||||
|
||||
// remove forward links from old resource
|
||||
const resourcesPointedByDeletedNote = workspace.links[uri.path] ?? [];
|
||||
delete workspace.links[uri.path];
|
||||
workspace = resourcesPointedByDeletedNote.reduce(
|
||||
(ws, link) => FoamWorkspace.disconnect(ws, uri, link.target),
|
||||
workspace
|
||||
);
|
||||
|
||||
// recompute previous links to old resource
|
||||
const notesPointingToDeletedResource = workspace.backlinks[uri.path] ?? [];
|
||||
delete workspace.backlinks[uri.path];
|
||||
workspace = notesPointingToDeletedResource.reduce(
|
||||
(ws, link) => FoamWorkspace.resolveResource(ws, ws.get(link.source)),
|
||||
workspace
|
||||
);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private static connect(workspace: FoamWorkspace, source: URI, target: URI) {
|
||||
const connection = {
|
||||
source: source,
|
||||
target: target,
|
||||
};
|
||||
|
||||
workspace.links[source.path] = workspace.links[source.path] ?? [];
|
||||
workspace.links[source.path].push(connection);
|
||||
workspace.backlinks[target.path] = workspace.backlinks[target.path] ?? [];
|
||||
workspace.backlinks[target.path].push(connection);
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
private static disconnect(
|
||||
workspace: FoamWorkspace,
|
||||
source: URI,
|
||||
target: URI
|
||||
) {
|
||||
workspace.links[source.path] = workspace.links[source.path]?.filter(
|
||||
c => c.source.path !== source.path || c.target.path !== target.path
|
||||
);
|
||||
if (workspace.links[source.path].length === 0) {
|
||||
delete workspace.links[source.path];
|
||||
}
|
||||
workspace.backlinks[target.path] = workspace.backlinks[target.path]?.filter(
|
||||
c => c.source.path !== source.path || c.target.path !== target.path
|
||||
);
|
||||
if (workspace.backlinks[target.path].length === 0) {
|
||||
delete workspace.backlinks[target.path];
|
||||
if (isPlaceholder(target)) {
|
||||
delete workspace.placeholders[uriToPlaceholderId(target)];
|
||||
}
|
||||
}
|
||||
return workspace;
|
||||
public dispose(): void {
|
||||
this.onDidAddEmitter.dispose();
|
||||
this.onDidDeleteEmitter.dispose();
|
||||
this.onDidUpdateEmitter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Node } from 'unist';
|
||||
import { isNotNull } from '../utils';
|
||||
import { Note } from '../model/note';
|
||||
import { Resource } from '../model/note';
|
||||
import unified from 'unified';
|
||||
import { FoamConfig } from '../config';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../common/uri';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
export interface FoamPlugin {
|
||||
name: string;
|
||||
@@ -16,12 +16,12 @@ export interface FoamPlugin {
|
||||
|
||||
export interface ParserPlugin {
|
||||
name?: string;
|
||||
visit?: (node: Node, note: Note) => void;
|
||||
visit?: (node: Node, note: Resource) => void;
|
||||
onDidInitializeParser?: (parser: unified.Processor) => void;
|
||||
onWillParseMarkdown?: (markdown: string) => string;
|
||||
onWillVisitTree?: (tree: Node, note: Note) => void;
|
||||
onDidVisitTree?: (tree: Node, note: Note) => void;
|
||||
onDidFindProperties?: (properties: any, note: Note) => void;
|
||||
onWillVisitTree?: (tree: Node, note: Resource) => void;
|
||||
onDidVisitTree?: (tree: Node, note: Resource) => void;
|
||||
onDidFindProperties?: (properties: any, note: Resource) => void;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
@@ -43,10 +43,10 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
|
||||
|
||||
const plugins = await Promise.all(
|
||||
pluginDirs
|
||||
.filter(dir => fs.statSync(dir.fsPath).isDirectory)
|
||||
.filter(dir => fs.statSync(URI.toFsPath(dir)).isDirectory)
|
||||
.map(async dir => {
|
||||
try {
|
||||
const pluginFile = path.join(dir.fsPath, 'index.js');
|
||||
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));
|
||||
@@ -66,11 +66,11 @@ function findPluginDirs(workspaceFolders: URI[]) {
|
||||
.reduce((acc, pluginDir) => {
|
||||
try {
|
||||
const content = fs
|
||||
.readdirSync(pluginDir.fsPath)
|
||||
.readdirSync(URI.toFsPath(pluginDir))
|
||||
.map(dir => URI.joinPath(pluginDir, dir));
|
||||
return [
|
||||
...acc,
|
||||
...content.filter(c => fs.statSync(c.fsPath).isDirectory()),
|
||||
...content.filter(c => fs.statSync(URI.toFsPath(c)).isDirectory()),
|
||||
];
|
||||
} catch {
|
||||
return acc;
|
||||
|
||||
@@ -1,139 +1,87 @@
|
||||
import glob from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import micromatch from 'micromatch';
|
||||
import fs from 'fs';
|
||||
import { Event, Emitter } from '../common/event';
|
||||
import { URI } from '../common/uri';
|
||||
import { FoamConfig } from '../config';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import { isSome } from '../utils';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import glob from 'glob';
|
||||
import { promisify } from 'util';
|
||||
import { isWindows } from '../common/platform';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
export interface IWatcher {
|
||||
export interface IMatcher {
|
||||
/**
|
||||
* An event which fires on file creation.
|
||||
* Filters the given list of URIs, keepin only the ones that
|
||||
* are matched by this Matcher
|
||||
*
|
||||
* @param files the URIs to check
|
||||
*/
|
||||
onDidCreate: Event<URI>;
|
||||
match(files: URI[]): URI[];
|
||||
|
||||
/**
|
||||
* An event which fires on file change.
|
||||
* Returns whether this URI is matched by this Matcher
|
||||
*
|
||||
* @param uri the URI to check
|
||||
*/
|
||||
onDidChange: Event<URI>;
|
||||
isMatch(uri: URI): boolean;
|
||||
|
||||
/**
|
||||
* An event which fires on file deletion.
|
||||
* The include globs
|
||||
*/
|
||||
onDidDelete: Event<URI>;
|
||||
include: string[];
|
||||
|
||||
/**
|
||||
* The exclude lobs
|
||||
*/
|
||||
exclude: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
* The matcher requires the path to be in unix format, so if we are in windows
|
||||
* we convert the fs path on the way in and out
|
||||
*/
|
||||
export interface IDataStore {
|
||||
/**
|
||||
* List the files available in the store
|
||||
*/
|
||||
listFiles: () => Promise<URI[]>;
|
||||
export const toMatcherPathFormat = isWindows
|
||||
? (uri: URI) => URI.toFsPath(uri).replace(/\\/g, '/')
|
||||
: (uri: URI) => URI.toFsPath(uri);
|
||||
|
||||
/**
|
||||
* Read the content of the file from the store
|
||||
*/
|
||||
read: (uri: URI) => Promise<string>;
|
||||
export const toFsPath = isWindows
|
||||
? (path: string): string => path.replace(/\//g, '\\')
|
||||
: (path: string): string => path;
|
||||
|
||||
/**
|
||||
* Returns whether the given URI is a match in
|
||||
* this data store
|
||||
*/
|
||||
isMatch: (uri: URI) => boolean;
|
||||
export class Matcher implements IMatcher {
|
||||
public readonly folders: string[];
|
||||
public readonly include: string[] = [];
|
||||
public readonly exclude: string[] = [];
|
||||
|
||||
/**
|
||||
* An event which fires on file creation.
|
||||
*/
|
||||
onDidCreate: Event<URI>;
|
||||
constructor(
|
||||
baseFolders: URI[],
|
||||
include: string[] = ['**/*'],
|
||||
exclude: string[] = []
|
||||
) {
|
||||
this.folders = baseFolders.map(toMatcherPathFormat);
|
||||
Logger.info('Workspace folders: ', this.folders);
|
||||
|
||||
/**
|
||||
* An event which fires on file change.
|
||||
*/
|
||||
onDidChange: Event<URI>;
|
||||
|
||||
/**
|
||||
* An event which fires on file deletion.
|
||||
*/
|
||||
onDidDelete: Event<URI>;
|
||||
}
|
||||
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore, IDisposable {
|
||||
readonly onDidChangeEmitter = new Emitter<URI>();
|
||||
readonly onDidCreateEmitter = new Emitter<URI>();
|
||||
readonly onDidDeleteEmitter = new Emitter<URI>();
|
||||
readonly onDidCreate: Event<URI> = this.onDidCreateEmitter.event;
|
||||
readonly onDidChange: Event<URI> = this.onDidChangeEmitter.event;
|
||||
readonly onDidDelete: Event<URI> = this.onDidDeleteEmitter.event;
|
||||
|
||||
private _folders: readonly string[];
|
||||
private _includeGlobs: string[] = [];
|
||||
private _ignoreGlobs: string[] = [];
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(config: FoamConfig, watcher?: IWatcher) {
|
||||
this._folders = config.workspaceFolders.map(f =>
|
||||
f.fsPath.replace(/\\/g, '/')
|
||||
);
|
||||
Logger.info('Workspace folders: ', this._folders);
|
||||
|
||||
this._folders.forEach(folder => {
|
||||
this.folders.forEach(folder => {
|
||||
const withFolder = folderPlusGlob(folder);
|
||||
this._includeGlobs.push(
|
||||
...config.includeGlobs.map(glob => {
|
||||
if (glob.endsWith('*')) {
|
||||
glob = `${glob}\\.(md|mdx|markdown)`;
|
||||
}
|
||||
this.include.push(
|
||||
...include.map(glob => {
|
||||
return withFolder(glob);
|
||||
})
|
||||
);
|
||||
this._ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
|
||||
this.exclude.push(...exclude.map(withFolder));
|
||||
});
|
||||
Logger.info('Glob patterns', {
|
||||
includeGlobs: this._includeGlobs,
|
||||
ignoreGlobs: this._ignoreGlobs,
|
||||
includeGlobs: this.include,
|
||||
ignoreGlobs: this.exclude,
|
||||
});
|
||||
|
||||
if (isSome(watcher)) {
|
||||
this._disposables.push(
|
||||
watcher.onDidCreate(uri => {
|
||||
if (this.isMatch(uri)) {
|
||||
Logger.info(`Created: ${uri.path}`);
|
||||
this.onDidCreateEmitter.fire(uri);
|
||||
}
|
||||
}),
|
||||
watcher.onDidChange(uri => {
|
||||
if (this.isMatch(uri)) {
|
||||
Logger.info(`Updated: ${uri.path}`);
|
||||
this.onDidChangeEmitter.fire(uri);
|
||||
}
|
||||
}),
|
||||
watcher.onDidDelete(uri => {
|
||||
if (this.isMatch(uri)) {
|
||||
Logger.info(`Deleted: ${uri.path}`);
|
||||
this.onDidDeleteEmitter.fire(uri);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match(files: URI[]) {
|
||||
const matches = micromatch(
|
||||
files.map(f => f.fsPath),
|
||||
this._includeGlobs,
|
||||
files.map(f => URI.toFsPath(f)),
|
||||
this.include,
|
||||
{
|
||||
ignore: this._ignoreGlobs,
|
||||
ignore: this.exclude,
|
||||
nocase: true,
|
||||
format: toFsPath,
|
||||
}
|
||||
);
|
||||
return matches.map(URI.file);
|
||||
@@ -142,34 +90,53 @@ export class FileDataStore implements IDataStore, IDisposable {
|
||||
isMatch(uri: URI) {
|
||||
return this.match([uri]).length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
async listFiles() {
|
||||
const files = (
|
||||
await Promise.all(
|
||||
this._folders.map(async folder => {
|
||||
const res = await findAllFiles(folderPlusGlob(folder)('**/*'));
|
||||
return res.map(URI.file);
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
return this.match(files);
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
*/
|
||||
export interface IDataStore {
|
||||
/**
|
||||
* List the files matching the given glob from the
|
||||
* store
|
||||
*/
|
||||
list: (glob: string) => Promise<URI[]>;
|
||||
|
||||
/**
|
||||
* Read the content of the file from the store
|
||||
*
|
||||
* Returns `null` in case of errors while reading
|
||||
*/
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore {
|
||||
async list(glob: string): Promise<URI[]> {
|
||||
const res = await findAllFiles(glob);
|
||||
return res.map(URI.file);
|
||||
}
|
||||
|
||||
async read(uri: URI) {
|
||||
return (await fs.promises.readFile(uri.fsPath)).toString();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
try {
|
||||
return (await fs.promises.readFile(URI.toFsPath(uri))).toString();
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const folderPlusGlob = (folder: string) => (glob: string): string => {
|
||||
export const folderPlusGlob = (folder: string) => (glob: string): string => {
|
||||
if (folder.substr(-1) === '/') {
|
||||
folder = folder.slice(0, -1);
|
||||
}
|
||||
if (glob.startsWith('/')) {
|
||||
glob = glob.slice(1);
|
||||
}
|
||||
return `${folder}/${glob}`;
|
||||
return folder.length > 0 ? `${folder}/${glob}` : glob;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isSome } from './core';
|
||||
const HASHTAG_REGEX = /(^|[ ])#([\w_-]*[a-zA-Z][\w_-]*\b)/gm;
|
||||
const WORD_REGEX = /(^|[ ])([\w_-]*[a-zA-Z][\w_-]*\b)/gm;
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { titleCase } from 'title-case';
|
||||
export { extractHashtags, extractTagsFromProp } from './hashtags';
|
||||
export * from './uri';
|
||||
export * from './core';
|
||||
|
||||
export function dropExtension(path: string): string {
|
||||
|
||||
5
packages/foam-core/src/utils/slug.ts
Normal file
5
packages/foam-core/src/utils/slug.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
export const uriToSlug = (uri: URI): string =>
|
||||
GithubSlugger.slug(URI.getBasename(uri));
|
||||
@@ -1,70 +0,0 @@
|
||||
import { posix } from 'path';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { hash } from './core';
|
||||
import { URI } from '../common/uri';
|
||||
|
||||
export const uriToSlug = (noteUri: URI): string => {
|
||||
return GithubSlugger.slug(posix.parse(noteUri.path).name);
|
||||
};
|
||||
|
||||
export const nameToSlug = (noteName: string): string => {
|
||||
return GithubSlugger.slug(noteName);
|
||||
};
|
||||
|
||||
export const hashURI = (uri: URI): string => {
|
||||
return hash(posix.normalize(uri.path));
|
||||
};
|
||||
|
||||
export const computeRelativePath = (source: URI, target: URI): string => {
|
||||
const relativePath = posix.relative(posix.dirname(source.path), target.path);
|
||||
return relativePath;
|
||||
};
|
||||
|
||||
export const getBasename = (uri: URI) => posix.parse(uri.path).name;
|
||||
|
||||
export const computeRelativeURI = (
|
||||
reference: URI,
|
||||
relativeSlug: string
|
||||
): URI => {
|
||||
// if no extension is provided, use the same extension as the source file
|
||||
const slug =
|
||||
posix.extname(relativeSlug) !== ''
|
||||
? relativeSlug
|
||||
: `${relativeSlug}${posix.extname(reference.path)}`;
|
||||
return reference.with({
|
||||
path: posix.join(posix.dirname(reference.path), slug),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a URI from value, taking into consideration possible relative paths.
|
||||
*
|
||||
* @param reference the URI to use as reference in case value is a relative path
|
||||
* @param value the value to parse for a URI
|
||||
* @returns the URI from the given value. In case of a relative path, the URI will take into account
|
||||
* the reference from which it is computed
|
||||
*/
|
||||
export const parseUri = (reference: URI, value: string): URI => {
|
||||
let uri = URI.parse(value);
|
||||
if (uri.scheme === 'file' && !value.startsWith('/')) {
|
||||
const [path, fragment] = value.split('#');
|
||||
uri = path.length > 0 ? computeRelativeURI(reference, path) : reference;
|
||||
if (fragment) {
|
||||
uri = uri.with({
|
||||
fragment: fragment,
|
||||
});
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
};
|
||||
|
||||
export const placeholderUri = (key: string): URI => {
|
||||
return URI.from({
|
||||
scheme: 'placeholder',
|
||||
path: key,
|
||||
});
|
||||
};
|
||||
|
||||
export const isPlaceholder = (uri: URI): boolean => {
|
||||
return uri.scheme === 'placeholder';
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
# Roam Document
|
||||
|
||||
[[Second Roam Document]]
|
||||
[[Second Roam Document]]
|
||||
|
||||
@@ -1 +1 @@
|
||||
# Second Roam Document
|
||||
# Second Roam Document
|
||||
|
||||
@@ -9,4 +9,4 @@ All the link references are correct in this file.
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[first-document]: first-document "First Document"
|
||||
[second-document]: second-document "Second Document"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createConfigFromFolders } from '../src/config';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { URI } from '../src/model/uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { NoteLinkDefinition, Note, Attachment } from '../src/model/note';
|
||||
import { URI } from '../src/common/uri';
|
||||
import path from 'path';
|
||||
import { FoamWorkspace } from '../src';
|
||||
import { NoteLinkDefinition, Resource } from '../src/model/note';
|
||||
import { IDataStore, Matcher } from '../src/services/datastore';
|
||||
import { MarkdownResourceProvider } from '../src/markdown-provider';
|
||||
import { Range } from '../src/model/range';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { Logger } from '../src/utils/log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const position = {
|
||||
start: { line: 1, column: 1 },
|
||||
end: { line: 1, column: 1 },
|
||||
};
|
||||
const position = Range.create(0, 0, 0, 100);
|
||||
|
||||
const documentStart = position.start;
|
||||
const documentEnd = position.end;
|
||||
@@ -20,11 +22,22 @@ const eol = '\n';
|
||||
*/
|
||||
export const strToUri = URI.file;
|
||||
|
||||
export const createAttachment = (params: { uri: string }): Attachment => {
|
||||
return {
|
||||
uri: strToUri(params.uri),
|
||||
type: 'attachment',
|
||||
};
|
||||
export const noOpDataStore = (): IDataStore => ({
|
||||
read: _ => Promise.resolve(''),
|
||||
list: _ => Promise.resolve([]),
|
||||
});
|
||||
|
||||
export const createTestWorkspace = () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const matcher = new Matcher([URI.file('/')], ['**/*']);
|
||||
const provider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
undefined,
|
||||
undefined,
|
||||
noOpDataStore()
|
||||
);
|
||||
workspace.registerProvider(provider);
|
||||
return workspace;
|
||||
};
|
||||
|
||||
export const createTestNote = (params: {
|
||||
@@ -33,30 +46,39 @@ export const createTestNote = (params: {
|
||||
definitions?: NoteLinkDefinition[];
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
text?: string;
|
||||
}): Note => {
|
||||
root?: URI;
|
||||
}): Resource => {
|
||||
const root = params.root ?? URI.file('/');
|
||||
return {
|
||||
uri: strToUri(params.uri),
|
||||
uri: URI.resolve(params.uri, root),
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: params.title ?? null,
|
||||
title: params.title ?? path.parse(strToUri(params.uri).path).base,
|
||||
definitions: params.definitions ?? [],
|
||||
tags: new Set(),
|
||||
links: params.links
|
||||
? params.links.map(link =>
|
||||
'slug' in link
|
||||
? params.links.map((link, index) => {
|
||||
const range = Range.create(
|
||||
position.start.line + index,
|
||||
position.start.character,
|
||||
position.start.line + index,
|
||||
position.end.character
|
||||
);
|
||||
return 'slug' in link
|
||||
? {
|
||||
type: 'wikilink',
|
||||
slug: link.slug,
|
||||
target: link.slug,
|
||||
position: position,
|
||||
range: range,
|
||||
text: 'link text',
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
target: link.to,
|
||||
label: 'link text',
|
||||
}
|
||||
)
|
||||
range: range,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
source: {
|
||||
eol: eol,
|
||||
|
||||
@@ -1,72 +1,93 @@
|
||||
import { createConfigFromObject } from '../src/config';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { URI } from '../src/common/uri';
|
||||
import { FileDataStore } from '../src';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { FileDataStore, Matcher } from '../src';
|
||||
import { toMatcherPathFormat } from '../src/services/datastore';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const testFolder = URI.joinPath(URI.file(__dirname), 'test-datastore');
|
||||
|
||||
function makeConfig(params: { include: string[]; ignore: string[] }) {
|
||||
return createConfigFromObject(
|
||||
[testFolder],
|
||||
params.include,
|
||||
params.ignore,
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
describe('Datastore', () => {
|
||||
it('defaults to including nothing and exclude nothing', async () => {
|
||||
const ds = new FileDataStore(
|
||||
makeConfig({
|
||||
include: [],
|
||||
ignore: [],
|
||||
})
|
||||
);
|
||||
expect(await ds.listFiles()).toHaveLength(0);
|
||||
describe('Matcher', () => {
|
||||
it('generates globs with the base dir provided', () => {
|
||||
const matcher = new Matcher([testFolder], ['*'], []);
|
||||
expect(matcher.folders).toEqual([toMatcherPathFormat(testFolder)]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, '*')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns only markdown files', async () => {
|
||||
const ds = new FileDataStore(
|
||||
makeConfig({
|
||||
include: ['**/*'],
|
||||
ignore: [],
|
||||
})
|
||||
);
|
||||
const res = toStringSet(await ds.listFiles());
|
||||
expect(res).toEqual(
|
||||
makeAbsolute([
|
||||
'/file-a.md',
|
||||
'/info/file-b.md',
|
||||
'/docs/file-in-nm.md',
|
||||
'/info/docs/file-in-sub-nm.md',
|
||||
])
|
||||
);
|
||||
it('defaults to including everything and excluding nothing', () => {
|
||||
const matcher = new Matcher([testFolder]);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, '**', '*')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports excludes', async () => {
|
||||
const ds = new FileDataStore(
|
||||
makeConfig({
|
||||
include: ['**/*'],
|
||||
ignore: ['**/docs/**'],
|
||||
})
|
||||
);
|
||||
const res = toStringSet(await ds.listFiles());
|
||||
expect(res).toEqual(makeAbsolute(['/file-a.md', '/info/file-b.md']));
|
||||
it('supports multiple includes', () => {
|
||||
const matcher = new Matcher([testFolder], ['g1', 'g2'], []);
|
||||
expect(matcher.exclude).toEqual([]);
|
||||
expect(matcher.include).toEqual([
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, 'g1')),
|
||||
toMatcherPathFormat(URI.joinPath(testFolder, 'g2')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a match method to filter strings', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.match(files)).toEqual([
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('has a isMatch method to see whether a file is matched or not', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], []);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(true);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
expect(matcher.isMatch(files[2])).toEqual(false);
|
||||
expect(matcher.isMatch(files[3])).toEqual(false);
|
||||
});
|
||||
|
||||
it('happy path', () => {
|
||||
const matcher = new Matcher([URI.file('/')], ['**/*'], ['**/*.pdf']);
|
||||
expect(matcher.isMatch(URI.file('/file.md'))).toBeTruthy();
|
||||
expect(matcher.isMatch(URI.file('/file.pdf'))).toBeFalsy();
|
||||
expect(matcher.isMatch(URI.file('/dir/file.md'))).toBeTruthy();
|
||||
expect(matcher.isMatch(URI.file('/dir/file.pdf'))).toBeFalsy();
|
||||
});
|
||||
|
||||
it('ignores files in the exclude list', () => {
|
||||
const matcher = new Matcher([testFolder], ['*.md'], ['file1.*']);
|
||||
const files = [
|
||||
URI.joinPath(testFolder, 'file1.md'),
|
||||
URI.joinPath(testFolder, 'file2.md'),
|
||||
URI.joinPath(testFolder, 'file3.mdx'),
|
||||
URI.joinPath(testFolder, 'sub', 'file4.md'),
|
||||
];
|
||||
expect(matcher.isMatch(files[0])).toEqual(false);
|
||||
expect(matcher.isMatch(files[1])).toEqual(true);
|
||||
expect(matcher.isMatch(files[2])).toEqual(false);
|
||||
expect(matcher.isMatch(files[3])).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
function toStringSet(uris: URI[]) {
|
||||
return new Set(uris.map(uri => uri.path.toLocaleLowerCase()));
|
||||
}
|
||||
|
||||
function makeAbsolute(files: string[]) {
|
||||
return new Set(
|
||||
files.map(f =>
|
||||
URI.joinPath(testFolder, f)
|
||||
.path.toLocaleLowerCase()
|
||||
.replace(/\\/g, '/')
|
||||
)
|
||||
);
|
||||
}
|
||||
describe('Datastore', () => {
|
||||
it('uses the matcher to get the file list', async () => {
|
||||
const matcher = new Matcher([testFolder], ['**/*.md'], []);
|
||||
const ds = new FileDataStore();
|
||||
expect((await ds.list(matcher.include[0])).length).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user