mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
132 Commits
chore/cons
...
v0.13.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
9ea68e1f00 | ||
|
|
eaa80fdfd5 | ||
|
|
148b7252a8 | ||
|
|
9f0deb4000 | ||
|
|
f818e51be2 | ||
|
|
f56a6d8d0d | ||
|
|
026023dc7a | ||
|
|
e118ac2f5c | ||
|
|
320d3d2bc3 | ||
|
|
cc42345276 | ||
|
|
46f60ae036 | ||
|
|
32e443bbae | ||
|
|
259642196a | ||
|
|
8a8c0221a2 | ||
|
|
585a6d61e1 | ||
|
|
bc7dc61511 | ||
|
|
f29edc22cb | ||
|
|
718c83f6ec | ||
|
|
e1438cf3eb | ||
|
|
33b995583f | ||
|
|
bc071a20b4 | ||
|
|
96f22fb0a8 | ||
|
|
d219b400fa | ||
|
|
19ba7e8673 | ||
|
|
7922aa950a | ||
|
|
4457e83e38 | ||
|
|
fb15672e6a | ||
|
|
2ef2a217ee | ||
|
|
8a73cba1f0 | ||
|
|
b0ea08b84f | ||
|
|
ea0edc5149 | ||
|
|
42dabfbf9d | ||
|
|
85d3aef2ff | ||
|
|
8bd3109325 | ||
|
|
6ca800b500 | ||
|
|
3a798b520f | ||
|
|
8db4d2897f | ||
|
|
e0bcb6bd92 | ||
|
|
cd92468311 | ||
|
|
5a44fbc26f | ||
|
|
4412f860dd | ||
|
|
3e3d36b954 | ||
|
|
eee6b7bd3a | ||
|
|
792e66b061 | ||
|
|
e5589f0555 | ||
|
|
32f40fa0de | ||
|
|
97e3b20112 | ||
|
|
0060ea2a3a | ||
|
|
a11dee89ba | ||
|
|
62e46e87b0 | ||
|
|
ef7ea8d23b | ||
|
|
36a632f218 | ||
|
|
f9331ad327 | ||
|
|
a944d993fc | ||
|
|
adf2dfa779 | ||
|
|
c04bc347ed | ||
|
|
5f8a064af2 | ||
|
|
eaa522fe1b | ||
|
|
3868cc5a17 | ||
|
|
76d70d40f8 | ||
|
|
2e373c4624 | ||
|
|
71948062b1 | ||
|
|
94711cf11b | ||
|
|
7d0858246d | ||
|
|
d998c8d482 | ||
|
|
85e0784c54 | ||
|
|
035fb7b634 | ||
|
|
50075a384b | ||
|
|
71e3581409 | ||
|
|
5249edab38 | ||
|
|
6baeec4db6 |
@@ -526,6 +526,141 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "MCluck90",
|
||||
"name": "Mike Cluck",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1753801?v=4",
|
||||
"profile": "https://mcluck.tech",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bpugh",
|
||||
"name": "Brandon Pugh",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/684781?v=4",
|
||||
"profile": "http://brandonpugh.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "themaxdavitt",
|
||||
"name": "Max Davitt",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/27709025?v=4",
|
||||
"profile": "https://max.davitt.me",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "anglinb",
|
||||
"name": "Brian Anglin",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/2637602?v=4",
|
||||
"profile": "http://briananglin.me",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "elswork",
|
||||
"name": "elswork",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1455507?v=4",
|
||||
"profile": "http://deft.work",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "leonhfr",
|
||||
"name": "léon h",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/19996318?v=4",
|
||||
"profile": "http://leonh.fr/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "njnygaard",
|
||||
"name": "Nikhil Nygaard",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4606342?v=4",
|
||||
"profile": "https://nygaard.site",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nitwit-se",
|
||||
"name": "Mark Dixon",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1382124?v=4",
|
||||
"profile": "http://www.nitwit.se",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://discord.gg/8c4BChMfSu
|
||||
url: https://foambubble.github.io/join-discord/g
|
||||
about: Please ask and answer questions here.
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.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
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
name: Build and Test
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-10.15, ubuntu-18.04] # add windows-2019 after fixing tests for it
|
||||
os: [macos-10.15, ubuntu-18.04, windows-2019]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != 'foambubble/foam'
|
||||
env:
|
||||
@@ -47,16 +48,19 @@ 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
|
||||
run: yarn build
|
||||
- name: Run Tests
|
||||
run: yarn test
|
||||
uses: GabrielBB/xvfb-action@v1.0
|
||||
with:
|
||||
run: yarn test
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ node_modules
|
||||
*.tsbuildinfo
|
||||
*.vsix
|
||||
*.log
|
||||
out
|
||||
dist
|
||||
docs/_site
|
||||
docs/.sass-cache
|
||||
|
||||
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"
|
||||
],
|
||||
|
||||
48
.vscode/settings.json
vendored
48
.vscode/settings.json
vendored
@@ -1,21 +1,31 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
// set these to true to hide folders with the compiled JS files,
|
||||
"packages/**/out": false,
|
||||
"packages/**/dist": false
|
||||
},
|
||||
"search.exclude": {
|
||||
// set this to false to include compiled JS folders in search results
|
||||
"packages/**/out": true,
|
||||
"packages/**/dist": true
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"foam.files.ignore": [
|
||||
"**/.vscode/**/*",
|
||||
"**/_layouts/**/*",
|
||||
"**/_site/**/*",
|
||||
"**/node_modules/**/*"
|
||||
]
|
||||
}
|
||||
"files.exclude": {
|
||||
// set these to true to hide folders with the compiled JS files,
|
||||
"packages/**/out": false,
|
||||
"packages/**/dist": false
|
||||
},
|
||||
"search.exclude": {
|
||||
// set this to false to include compiled JS folders in search results
|
||||
"packages/**/out": true,
|
||||
"packages/**/dist": true
|
||||
},
|
||||
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"foam.files.ignore": [
|
||||
"**/.vscode/**/*",
|
||||
"**/_layouts/**/*",
|
||||
"**/_site/**/*",
|
||||
"**/node_modules/**/*"
|
||||
],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"prettier.requireConfig": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2,
|
||||
"jest.debugCodeLens.showWhenTestStateIn": [
|
||||
"fail",
|
||||
"unknown",
|
||||
"pass"
|
||||
],
|
||||
"gitdoc.enabled": false
|
||||
}
|
||||
|
||||
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:
|
||||
|
||||
@@ -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,21 +203,23 @@ GEM
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
mercenary (0.3.6)
|
||||
mini_portile2 (2.4.0)
|
||||
mini_portile2 (2.5.0)
|
||||
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.10.10)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogiri (1.11.1)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
octokit (4.19.0)
|
||||
faraday (>= 0.9)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
pathutil (0.16.2)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (3.1.1)
|
||||
racc (1.5.2)
|
||||
rb-fsevent (0.10.4)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
|
||||
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/create-new-note-from-template.gif
Normal file
BIN
docs/assets/images/create-new-note-from-template.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 759 KiB |
BIN
docs/assets/images/create-new-template.gif
Normal file
BIN
docs/assets/images/create-new-template.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 391 KiB |
BIN
docs/assets/images/graph-style.gif
Normal file
BIN
docs/assets/images/graph-style.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 MiB |
BIN
docs/assets/images/style-node-by-type.png
Normal file
BIN
docs/assets/images/style-node-by-type.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
@@ -15,4 +15,4 @@
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//end]: # "Autogenerated link references"
|
||||
@@ -17,7 +17,7 @@ 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!
|
||||
Finally, the easiest way to help, is to use it and provide feedback by [submitting issues](https://github.com/foambubble/foam/issues/new/choose) or participating in the [Foam Community Discord](https://discord.gg/rtdZKgj)!
|
||||
Finally, the easiest way to help, is to use it and provide feedback by [submitting issues](https://github.com/foambubble/foam/issues/new/choose) or participating in the [Foam Community Discord](https://foambubble.github.io/join-discord/g)!
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -37,6 +37,16 @@ If you're interested in contributing, this short guide will help you get things
|
||||
|
||||
You should now be ready to start working!
|
||||
|
||||
### Testing
|
||||
|
||||
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` live in the `test` directory.
|
||||
Tests in `foam-vscode` live alongside the code in `src`.
|
||||
|
||||
### The VS Code Extension
|
||||
|
||||
This guide assumes you read the previous instructions and you're set up to work on Foam.
|
||||
|
||||
@@ -9,13 +9,13 @@ Foam code and documentation live in the monorepo at [foambubble/foam](https://gi
|
||||
- [/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 "Recipes"
|
||||
[recommended-extensions]: ../recommended-extensions "Recommended Extensions"
|
||||
[recipes]: ../recipes/recipes.md "Recipes"
|
||||
[recommended-extensions]: ../recommended-extensions.md "Recommended Extensions"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -14,4 +14,5 @@ The idea would be to automatically generate lists of backlinks (and optionally,
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: todo.md "Todo"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[//end]: # "Autogenerated link references"
|
||||
@@ -6,4 +6,5 @@ If you're interested in working on it, please start a conversation in [GitHub is
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: todo.md "Todo"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -11,4 +11,5 @@ If you're interested in working on it, please start a conversation in [GitHub is
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: todo.md "Todo"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
@@ -0,0 +1,125 @@
|
||||
# Roadmap
|
||||
|
||||
Some of these items can be achieved by combining existing tools, but others may require us to build bespoke software solutions. See [[build-vs-assemble]] to understand trade-offs between these approaches. If a feature can be implemented by contributing to [[recipes]], it should.
|
||||
@@ -87,7 +86,7 @@ The community is working on a number of automated scripts to help you migrate to
|
||||
[build-vs-assemble]: build-vs-assemble.md "Build vs Assemble"
|
||||
[recipes]: ../recipes/recipes.md "Recipes"
|
||||
[contribution-guide]: ../contribution-guide.md "Contribution Guide"
|
||||
[git-integration]: ../features/git-integration.md "Git integration"
|
||||
[git-integration]: ../features/git-integration.md "Git Integration"
|
||||
[wiki-links]: ../wiki-links.md "Wiki Links"
|
||||
[foam-file-format]: foam-file-format.md "Foam File Format"
|
||||
[unlinked-references]: unlinked-references.md "Unlinked references (stub)"
|
||||
|
||||
@@ -14,5 +14,6 @@ Features belong on the [[roadmap]].
|
||||
For more things to do, check backlinks for Pages that annotate [[todo]].
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[todo]: todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -6,14 +6,15 @@ If you're interested in working on it, please start a conversation in [GitHub is
|
||||
|
||||
## Notes
|
||||
|
||||
One of Roam's big features is the ability to find all instances of a reference, create a page for it and update all the references to link to the new page.
|
||||
One of Foam's big features is the ability to find all instances of a reference, create a page for it and update all the references to link to the new page.
|
||||
|
||||
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"
|
||||
[todo]: todo.md "Todo"
|
||||
[roadmap]: roadmap.md "Roadmap"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
[//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"
|
||||
[graph-visualisation]: graph-visualisation.md "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'
|
||||
|
||||
@@ -10,6 +10,7 @@ This feature is experimental and its API subject to change.
|
||||
## Goal
|
||||
|
||||
Here are some of the things that we could enable with local plugins in Foam:
|
||||
|
||||
- extend the document syntax to support roam style attributes (e.g. `stage:: seedling`)
|
||||
- automatically add tags to my notes based on the location in the repo (e.g. notes in `/areas/finance` will automatically get the `#finance` tag)
|
||||
- add a new CLI command to support some internal use case or automate import/export
|
||||
@@ -21,8 +22,10 @@ Plugins can execute arbitrary code on the client's machine.
|
||||
For this reason this feature is disabled by default, and needs to be explicitly enabled.
|
||||
|
||||
To enable the feature:
|
||||
|
||||
- create a `~/.foam/config.json` file
|
||||
- add the following content to the file
|
||||
|
||||
```
|
||||
{
|
||||
"experimental": {
|
||||
@@ -38,21 +41,20 @@ For security reasons this setting can only be defined in the user settings file.
|
||||
|
||||
- [[todo]] an additional security mechanism would involve having an explicit list of whitelisted repo paths where plugins are allowed. This would provide finer grain control over when to enable or disable the feature.
|
||||
|
||||
|
||||
## Technical approach
|
||||
|
||||
When Foam is loaded it will check whether the experimental local plugin feature is enabled, and in such case it will:
|
||||
|
||||
- check `.foam/plugins` directory.
|
||||
- each directory in there is considered a plugin
|
||||
- the layout of each directory is
|
||||
- `index.js` contains the main info about the plugin, specifically it exports:
|
||||
- `name: string` the name of the plugin
|
||||
- `description?: string` the description of the plugin
|
||||
- `graphMiddleware?: Middleware` an object that can intercept calls to the Foam graph
|
||||
- `parser?: ParserPlugin` an object that interacts with the markdown parsing phase
|
||||
- each directory in there is considered a plugin
|
||||
- the layout of each directory is
|
||||
- `index.js` contains the main info about the plugin, specifically it exports:
|
||||
- `name: string` the name of the plugin
|
||||
- `description?: string` the description of the plugin
|
||||
- `parser?: ParserPlugin` an object that interacts with the markdown parsing phase
|
||||
|
||||
Currently for simplicity we keep everything in one file. We might in the future split the plugin by domain (e.g. vscode, cli, core, ...)
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: ../dev/todo.md "Todo"
|
||||
[//end]: # "Autogenerated link references"
|
||||
[//begin]: # 'Autogenerated link references for markdown compatibility'
|
||||
[todo]: ../dev/todo.md 'Todo'
|
||||
[//end]: # 'Autogenerated link references'
|
||||
|
||||
@@ -1,16 +1,62 @@
|
||||
# Graph Visualisation
|
||||
|
||||
Foam comes with a graph visualisation.
|
||||
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
|
||||
- to navigate to a note by clicking on it while pressing `CTRL` or `CMD`
|
||||
- allow you to navigate to a note by clicking on it while pressing `CTRL` or `CMD`
|
||||
- automatically center the graph on the currently edited note, to immediately see it's connections
|
||||
|
||||
### Markdown Links
|
||||
Another extension that provides a great graph visualisation is [Markdown Links](https://marketplace.visualstudio.com/items?itemName=tchayen.markdown-links).
|
||||
The extension doesn't use the Foam model, so discrepancies might arise, but it's a great visualisation extension nonetheless!
|
||||
## Custom Graph Styles
|
||||
|
||||
Currently, custom graph styles are supported through the `foam.graph.style` setting.
|
||||
|
||||

|
||||
|
||||
A sample configuration object is provided below:
|
||||
|
||||
```json
|
||||
"foam.graph.style": {
|
||||
"background": "#202020",
|
||||
"fontSize": 12,
|
||||
"lineColor": "#277da1",
|
||||
"lineWidth": 0.2,
|
||||
"particleWidth": 1.0,
|
||||
"highlightedForeground": "#f9c74f",
|
||||
"node": {
|
||||
"note": "#277da1",
|
||||
"placeholder": "#545454",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```
|
||||
---
|
||||
type: feature
|
||||
---
|
||||
# Backlinking
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
And the following `settings.json`:
|
||||
```json
|
||||
"foam.graph.style": {
|
||||
"node": {
|
||||
"feature": "red",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Will result in the following graph:
|
||||
|
||||

|
||||
|
||||
|
||||
- Use the `Markdown Links: Show Graph` command to see the graph
|
||||

|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ The following example:
|
||||
[wiki-links]: wiki-links "Wiki Links"
|
||||
[github-pages]: github-pages "Github Pages"
|
||||
```
|
||||
You can open the [raw markdown](https://foambubble.github.io/foam/foam-file-format.md) to see them at the bottom of this file
|
||||
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
|
||||
|
||||
|
||||
@@ -2,10 +2,26 @@
|
||||
|
||||
Foam supports note templates.
|
||||
|
||||
Note templates live in `.foam/templates`, just create regular `.md` files there to add templates.
|
||||
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.
|
||||
|
||||
To create a note from a template, execute the `Create New Note From Template` command and follow the instructions.
|
||||

|
||||
|
||||
_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.
|
||||
|
||||

|
||||
|
||||
_Theme: Ayu Light_
|
||||
|
||||
### 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.
|
||||
|
||||
10
docs/features/orphans.md
Normal file
10
docs/features/orphans.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Orphans
|
||||
|
||||
Foam helps you to find orphans: notes that have neither forward links nor backlinks.
|
||||
|
||||
Orphans can be found in the Orphans panel.
|
||||
|
||||
Two settings allows you to control the behaviour of the Orphans panel:
|
||||
|
||||
- `foam.orphans.exclude`: list of glob patterns that will be used to exclude directories. For example, a value of `["journal/**/*"]` would exclude your daily notes.
|
||||
- `foam.orphans.groupBy`: sets the default view mode of the Orphans panel: either groups by folder (by default), or lists all orphans. The view can be toggled on the fly from the panel, but it won't overwrite the setting.
|
||||
148
docs/index.md
148
docs/index.md
@@ -7,7 +7,7 @@ You can use **Foam** for organising your research, keeping re-discoverable notes
|
||||
**Foam** is free, open source, and extremely extensible to suit your personal workflow. You own the information you create with Foam, and you're free to share it, and collaborate on it with anyone you want.
|
||||
|
||||
<p class="announcement">
|
||||
<b>New!</b> Join <a href="https://discord.gg/rtdZKgj" target="_blank">Foam community Discord</a> for users and contributors!
|
||||
<b>New!</b> Join <a href="https://foambubble.github.io/join-discord/w" target="_blank">Foam community Discord</a> for users and contributors!
|
||||
</p>
|
||||
|
||||
<div class="website-only">
|
||||
@@ -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)
|
||||
|
||||
@@ -76,7 +76,7 @@ To learn more about how to use **Foam**, read the [[recipes]].
|
||||
|
||||
Getting stuck in the setup? Read the [[frequently-asked-questions]].
|
||||
|
||||
There are [[known-issues]], and I'm sure, many unknown issues! Please [report them on GitHub](http://github.com/foambubble/foam/issues)!
|
||||
Check our [issues on GitHub](http://github.com/foambubble/foam/issues) if you get stuck on something, and create a new one if something doesn't seem right!
|
||||
|
||||
## Features
|
||||
|
||||
@@ -107,81 +107,103 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="https://jevakallio.dev/"><img src="https://avatars1.githubusercontent.com/u/1203949?v=4" width="60px;" alt=""/><br /><sub><b>Jani Eväkallio</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://joeprevite.com/"><img src="https://avatars3.githubusercontent.com/u/3806031?v=4" width="60px;" alt=""/><br /><sub><b>Joe Previte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/riccardoferretti"><img src="https://avatars3.githubusercontent.com/u/457005?v=4" width="60px;" alt=""/><br /><sub><b>Riccardo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://ojanaho.com/"><img src="https://avatars0.githubusercontent.com/u/2180090?v=4" width="60px;" alt=""/><br /><sub><b>Janne Ojanaho</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://bypaulshen.com/"><img src="https://avatars3.githubusercontent.com/u/2266187?v=4" width="60px;" alt=""/><br /><sub><b>Paul Shen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=paulshen" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/coffenbacher"><img src="https://avatars0.githubusercontent.com/u/245867?v=4" width="60px;" alt=""/><br /><sub><b>coffenbacher</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=coffenbacher" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://mathieu.dutour.me/"><img src="https://avatars2.githubusercontent.com/u/3254314?v=4" width="60px;" alt=""/><br /><sub><b>Mathieu Dutour</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=mathieudutour" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://jevakallio.dev/"><img src="https://avatars1.githubusercontent.com/u/1203949?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jani Eväkallio</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jevakallio" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://joeprevite.com/"><img src="https://avatars3.githubusercontent.com/u/3806031?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joe Previte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jsjoeio" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/riccardoferretti"><img src="https://avatars3.githubusercontent.com/u/457005?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Riccardo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=riccardoferretti" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://ojanaho.com/"><img src="https://avatars0.githubusercontent.com/u/2180090?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Janne Ojanaho</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jojanaho" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://bypaulshen.com/"><img src="https://avatars3.githubusercontent.com/u/2266187?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Paul Shen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=paulshen" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/coffenbacher"><img src="https://avatars0.githubusercontent.com/u/245867?v=4?s=60" width="60px;" alt=""/><br /><sub><b>coffenbacher</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=coffenbacher" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://mathieu.dutour.me/"><img src="https://avatars2.githubusercontent.com/u/3254314?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mathieu Dutour</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=mathieudutour" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/presidentelect"><img src="https://avatars2.githubusercontent.com/u/1242300?v=4" width="60px;" alt=""/><br /><sub><b>Michael Hansen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=presidentelect" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://klickverbot.at/"><img src="https://avatars1.githubusercontent.com/u/19335?v=4" width="60px;" alt=""/><br /><sub><b>David Nadlinger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dnadlinger" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://pluckd.co/"><img src="https://avatars2.githubusercontent.com/u/20598571?v=4" width="60px;" alt=""/><br /><sub><b>Fernando</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MrCordeiro" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/jfgonzalez7"><img src="https://avatars3.githubusercontent.com/u/58857736?v=4" width="60px;" alt=""/><br /><sub><b>Juan Gonzalez</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jfgonzalez7" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.louiechristie.com/"><img src="https://avatars1.githubusercontent.com/u/6807448?v=4" width="60px;" alt=""/><br /><sub><b>Louie Christie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=louiechristie" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4" width="60px;" alt=""/><br /><sub><b>Sandro</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SuperSandro2000" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Skn0tt"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="60px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skn0tt" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/presidentelect"><img src="https://avatars2.githubusercontent.com/u/1242300?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Michael Hansen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=presidentelect" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://klickverbot.at/"><img src="https://avatars1.githubusercontent.com/u/19335?v=4?s=60" width="60px;" alt=""/><br /><sub><b>David Nadlinger</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dnadlinger" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://pluckd.co/"><img src="https://avatars2.githubusercontent.com/u/20598571?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Fernando</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MrCordeiro" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/jfgonzalez7"><img src="https://avatars3.githubusercontent.com/u/58857736?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Juan Gonzalez</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jfgonzalez7" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.louiechristie.com/"><img src="https://avatars1.githubusercontent.com/u/6807448?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Louie Christie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=louiechristie" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://supersandro.de/"><img src="https://avatars2.githubusercontent.com/u/7258858?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sandro</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SuperSandro2000" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Skn0tt"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Skn0tt" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://styfle.dev/"><img src="https://avatars1.githubusercontent.com/u/229881?v=4" width="60px;" alt=""/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=styfle" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Georift"><img src="https://avatars2.githubusercontent.com/u/859430?v=4" width="60px;" alt=""/><br /><sub><b>Tim</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Georift" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/sauravkhdoolia"><img src="https://avatars1.githubusercontent.com/u/34188267?v=4" width="60px;" alt=""/><br /><sub><b>Saurav Khdoolia</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sauravkhdoolia" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://anku.netlify.com/"><img src="https://avatars1.githubusercontent.com/u/22813027?v=4" width="60px;" alt=""/><br /><sub><b>Ankit Tiwari</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anku255" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Tests">⚠️</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ayushbaweja"><img src="https://avatars1.githubusercontent.com/u/44344063?v=4" width="60px;" alt=""/><br /><sub><b>Ayush Baweja</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ayushbaweja" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/TaiChi-IO"><img src="https://avatars3.githubusercontent.com/u/65092992?v=4" width="60px;" alt=""/><br /><sub><b>TaiChi-IO</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=TaiChi-IO" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://styfle.dev/"><img src="https://avatars1.githubusercontent.com/u/229881?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Steven</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=styfle" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Georift"><img src="https://avatars2.githubusercontent.com/u/859430?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tim</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Georift" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/sauravkhdoolia"><img src="https://avatars1.githubusercontent.com/u/34188267?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Saurav Khdoolia</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sauravkhdoolia" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://anku.netlify.com/"><img src="https://avatars1.githubusercontent.com/u/22813027?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ankit Tiwari</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anku255" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Tests">⚠️</a> <a href="https://github.com/foambubble/foam/commits?author=anku255" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ayushbaweja"><img src="https://avatars1.githubusercontent.com/u/44344063?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ayush Baweja</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ayushbaweja" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/TaiChi-IO"><img src="https://avatars3.githubusercontent.com/u/65092992?v=4?s=60" width="60px;" alt=""/><br /><sub><b>TaiChi-IO</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=TaiChi-IO" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/chirag-singhal"><img src="https://avatars3.githubusercontent.com/u/42653703?v=4" width="60px;" alt=""/><br /><sub><b>CHIRAG SINGHAL</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chirag-singhal" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/lostintangent"><img src="https://avatars3.githubusercontent.com/u/116461?v=4" width="60px;" alt=""/><br /><sub><b>Jonathan Carter</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lostintangent" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.synesthesia.co.uk"><img src="https://avatars3.githubusercontent.com/u/181399?v=4" width="60px;" alt=""/><br /><sub><b>Julian Elve</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=synesthesia" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4?s=60" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/chirag-singhal"><img src="https://avatars3.githubusercontent.com/u/42653703?v=4?s=60" width="60px;" alt=""/><br /><sub><b>CHIRAG SINGHAL</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chirag-singhal" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/lostintangent"><img src="https://avatars3.githubusercontent.com/u/116461?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Jonathan Carter</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lostintangent" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.synesthesia.co.uk"><img src="https://avatars3.githubusercontent.com/u/181399?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Julian Elve</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=synesthesia" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/thomaskoppelaar"><img src="https://avatars3.githubusercontent.com/u/36331365?v=4" width="60px;" alt=""/><br /><sub><b>Thomas Koppelaar</b></sub></a><br /><a href="#question-thomaskoppelaar" title="Answering Questions">💬</a> <a href="https://github.com/foambubble/foam/commits?author=thomaskoppelaar" title="Code">💻</a> <a href="#userTesting-thomaskoppelaar" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="http://www.akshaymehra.com"><img src="https://avatars1.githubusercontent.com/u/8671497?v=4" width="60px;" alt=""/><br /><sub><b>Akshay</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MehraAkshay" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4" width="60px;" alt=""/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4" width="60px;" alt=""/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4" width="60px;" alt=""/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4" width="60px;" alt=""/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4" width="60px;" alt=""/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/thomaskoppelaar"><img src="https://avatars3.githubusercontent.com/u/36331365?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Thomas Koppelaar</b></sub></a><br /><a href="#question-thomaskoppelaar" title="Answering Questions">💬</a> <a href="https://github.com/foambubble/foam/commits?author=thomaskoppelaar" title="Code">💻</a> <a href="#userTesting-thomaskoppelaar" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="http://www.akshaymehra.com"><img src="https://avatars1.githubusercontent.com/u/8671497?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Akshay</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MehraAkshay" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://johnlindquist.com"><img src="https://avatars0.githubusercontent.com/u/36073?v=4?s=60" width="60px;" alt=""/><br /><sub><b>John Lindquist</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=johnlindquist" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ashwin.run/"><img src="https://avatars2.githubusercontent.com/u/1689183?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Ashwin Ramaswami</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=epicfaace" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Klaudioz"><img src="https://avatars1.githubusercontent.com/u/632625?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Claudio Canales</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Klaudioz" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/vitaly-pevgonen"><img src="https://avatars0.githubusercontent.com/u/6272738?v=4?s=60" width="60px;" alt=""/><br /><sub><b>vitaly-pevgonen</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vitaly-pevgonen" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://dshemetov.github.io"><img src="https://avatars0.githubusercontent.com/u/1810426?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Dmitry Shemetov</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dshemetov" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4" width="60px;" alt=""/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4" width="60px;" alt=""/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4" width="60px;" alt=""/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4" width="60px;" alt=""/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://vhanda.in"><img src="https://avatars2.githubusercontent.com/u/426467?v=4" width="60px;" alt=""/><br /><sub><b>Vishesh Handa</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vHanda" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.linkedin.com/in/heroichitesh"><img src="https://avatars3.githubusercontent.com/u/37622734?v=4" width="60px;" alt=""/><br /><sub><b>Hitesh Kumar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=HeroicHitesh" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/hooncp"><img src="https://avatars1.githubusercontent.com/u/48883554?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hooncp</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hooncp" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://rt-canada.ca"><img src="https://avatars1.githubusercontent.com/u/13721239?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Martin Laws</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=martinlaws" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://seanksmith.me"><img src="https://avatars3.githubusercontent.com/u/2085441?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sean K Smith</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=sksmith" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/kevin-neely/"><img src="https://avatars1.githubusercontent.com/u/37545028?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Kevin Neely</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=kneely" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ariefrahmansyah.dev"><img src="https://avatars3.githubusercontent.com/u/8122852?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Arief Rahmansyah</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ariefrahmansyah" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://vhanda.in"><img src="https://avatars2.githubusercontent.com/u/426467?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Vishesh Handa</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=vHanda" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.linkedin.com/in/heroichitesh"><img src="https://avatars3.githubusercontent.com/u/37622734?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Hitesh Kumar</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=HeroicHitesh" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4?s=60" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4?s=60" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4?s=60" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4" width="60px;" alt=""/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4?s=60" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4?s=60" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://shaunagordon.com"><img src="https://avatars1.githubusercontent.com/u/579361?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Shauna Gordon</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ShaunaGordon" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://mcluck.tech"><img src="https://avatars1.githubusercontent.com/u/1753801?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Mike Cluck</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MCluck90" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://brandonpugh.com"><img src="https://avatars1.githubusercontent.com/u/684781?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brandon Pugh</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=bpugh" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://max.davitt.me"><img src="https://avatars1.githubusercontent.com/u/27709025?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Max Davitt</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=themaxdavitt" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://briananglin.me"><img src="https://avatars3.githubusercontent.com/u/2637602?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Brian Anglin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=anglinb" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://deft.work"><img src="https://avatars1.githubusercontent.com/u/1455507?v=4?s=60" width="60px;" alt=""/><br /><sub><b>elswork</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=elswork" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://leonh.fr/"><img src="https://avatars.githubusercontent.com/u/19996318?v=4?s=60" width="60px;" alt=""/><br /><sub><b>léon h</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=leonhfr" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://nygaard.site"><img src="https://avatars.githubusercontent.com/u/4606342?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Nikhil Nygaard</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=njnygaard" title="Documentation">📖</a></td>
|
||||
</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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
**Foam** was inspired by [Roam Research](https://roamresearch.com/) and the [Zettelkasten methodology](https://zettelkasten.de/posts/overview)
|
||||
@@ -190,10 +212,10 @@ 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"
|
||||
[graph-visualisation]: features/graph-visualisation.md "Graph Visualisation"
|
||||
[backlinking]: features/backlinking.md "Backlinking"
|
||||
[recommended-extensions]: recommended-extensions.md "Recommended Extensions"
|
||||
[recipes]: recipes/recipes.md "Recipes"
|
||||
|
||||
326
docs/proposals/templates-v2.md
Normal file
326
docs/proposals/templates-v2.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# 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?
|
||||
@@ -1,11 +1,13 @@
|
||||
# Generate a site using Gatsby
|
||||
|
||||
## Using foam-gatsby-template
|
||||
|
||||
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
|
||||
### 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
|
||||
### Publishing your foam to Vercel
|
||||
|
||||
When you're ready to publish, run a local build.
|
||||
```bash
|
||||
@@ -21,4 +23,6 @@ Import your project. Select `_layouts/public` as your root directory and click *
|
||||
|
||||
That's it!
|
||||
|
||||
## Using foam-template-gatsby-kb
|
||||
|
||||
You can use another template [foam-template-gatsby-kb](https://github.com/hikerpig/foam-template-gatsby-kb), and host it on [Vercel](https://vercel.com) or [Netlify](https://www.netlify.com/).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# 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.
|
||||
|
||||
@@ -36,7 +38,7 @@ There are many other templates which also support publish your foam workspace to
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* foam-jekyll-template
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://wiki.hikerpig.cn/)
|
||||
* [demo-website](https://hikerpig.github.io/foam-jekyll-template/)
|
||||
|
||||
[[todo]] [[good-first-task]] Improve this documentation
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Capture Notes With Shortcuts and GitHub Actions
|
||||
|
||||
With this #recipe you can create notes on your iOS device, which will automatically be imported into Foam.
|
||||
|
||||
## Context
|
||||
|
||||
* You use [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode) to manage your notes
|
||||
* You wish to adopt a practice such as [A writing inbox for transient and incomplete notes](https://notes.andymatuschak.org/A%20writing%20inbox%20for%20transient%20and%20incomplete%20notes)
|
||||
* You wish to use [Shorcuts](https://support.apple.com/guide/shortcuts/welcome/ios) to capture quick notes into your Foam notes from your iOS device
|
||||
|
||||
## Other tools
|
||||
|
||||
* We assume you are familiar with how to use GitHub (if you are using Foam this is implicit)
|
||||
* You have an iOS device.
|
||||
|
||||
## Instructions
|
||||
|
||||
|
||||
1. Setup the [`foam-capture-action`]() in your GitHub Repository, to be triggered by "Workflow dispatch" events.
|
||||
|
||||
```
|
||||
name: Manually triggered workflow
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
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:
|
||||
capture: ${{ github.event.inputs.data }}
|
||||
- run: |
|
||||
git config --local user.email "example@gmail.com"
|
||||
git config --local user.name "Your name"
|
||||
git commit -m "Captured from workflow trigger" -a
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
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.)
|
||||
@@ -37,6 +37,9 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
- Link documents with [[wiki-links]].
|
||||
- Use shortcuts for [[creating-new-notes]]
|
||||
- Instantly create and access your [[daily-notes]]
|
||||
- Add and explore [[tags]]
|
||||
- Create [[note-templates]]
|
||||
- Find [[orphans]]
|
||||
- Use custom [[note-macros]] to create weekly, monthly etc. notes
|
||||
- Draw [[diagrams-in-markdown]]
|
||||
- Prettify your links, [[automatically-expand-urls-to-well-titled-links]]
|
||||
@@ -66,7 +69,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
- Publish to [[publish-to-vercel]]
|
||||
- Publish using community templates
|
||||
- [[publish-to-netlify-with-eleventy]] by [@juanfrank77](https://github.com/juanfrank77)
|
||||
- [[generate-gatsby-site]] by [@mathieudutour](https://github.com/mathieudutour)
|
||||
- [[generate-gatsby-site]] by [@mathieudutour](https://github.com/mathieudutour) and [@hikerpig](https://github.com/hikerpig)
|
||||
- [foamy-nextjs](https://github.com/yenly/foamy-nextjs) by [@yenly](https://github.com/yenly)
|
||||
- Make the site your own by [[publish-to-github]].
|
||||
- Render math symbols, by either
|
||||
@@ -82,6 +85,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
## Workflow
|
||||
|
||||
- Capture notes from Drafts app on iOS [[capture-notes-with-drafts-pro]]
|
||||
- Capture notes from iOS Shortcuts [[capture-notes-with-shortcuts-and-github-actions]]
|
||||
|
||||
## Creative ideas
|
||||
|
||||
@@ -102,12 +106,15 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[how-to-write-recipes]: how-to-write-recipes.md "How to Write Recipes"
|
||||
[todo]: ../dev/todo.md "Todo"
|
||||
[web-clipper]: web-clipper.md "Web Clipper"
|
||||
[graph-visualisation]: ../features/graph-visualisation.md "Graph visualisation"
|
||||
[graph-visualisation]: ../features/graph-visualisation.md "Graph Visualisation"
|
||||
[backlinking]: ../features/backlinking.md "Backlinking"
|
||||
[unlinked-references]: ../dev/unlinked-references.md "Unlinked references (stub)"
|
||||
[wiki-links]: ../wiki-links.md "Wiki Links"
|
||||
[creating-new-notes]: ../features/creating-new-notes.md "Creating New Notes"
|
||||
[daily-notes]: ../features/daily-notes.md "Daily notes"
|
||||
[tags]: ../features/tags.md "Tags"
|
||||
[note-templates]: ../features/note-templates.md "Note Templates"
|
||||
[orphans]: ../features/orphans.md "Orphans"
|
||||
[note-macros]: note-macros.md "Custom Note Macros"
|
||||
[diagrams-in-markdown]: diagrams-in-markdown.md "Diagrams in Markdown"
|
||||
[automatically-expand-urls-to-well-titled-links]: automatically-expand-urls-to-well-titled-links.md "Automatically Expand URLs to Well-Titled Links"
|
||||
@@ -115,7 +122,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[add-images-to-notes]: add-images-to-notes.md "Add images to your notes"
|
||||
[shows-image-preview-on-hover]: shows-image-preview-on-hover.md "Shows Image Preview on Hover"
|
||||
[good-first-task]: ../dev/good-first-task.md "Good First Task"
|
||||
[git-integration]: ../features/git-integration.md "Git integration"
|
||||
[git-integration]: ../features/git-integration.md "Git Integration"
|
||||
[write-your-notes-in-github-gist]: write-your-notes-in-github-gist.md "Write your notes in GitHub Gist"
|
||||
[publish-to-github-pages]: ../publishing/publish-to-github-pages.md "Github Pages"
|
||||
[publish-to-gitlab-pages]: ../publishing/publish-to-gitlab-pages.md "GitLab Pages"
|
||||
@@ -127,4 +134,5 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[math-support-with-mathjax]: ../publishing/math-support-with-mathjax.md "Math Support"
|
||||
[math-support-with-katex]: ../publishing/math-support-with-katex.md "Katex Math Rendering"
|
||||
[capture-notes-with-drafts-pro]: capture-notes-with-drafts-pro.md "Capture Notes With Drafts Pro"
|
||||
[capture-notes-with-shortcuts-and-github-actions]: capture-notes-with-shortcuts-and-github-actions.md "Capture Notes With Shortcuts and GitHub Actions"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
@@ -52,7 +52,7 @@ If such an app was worth building, it would have to have the following features:
|
||||
- Ability to search and navigate forward links and back links (onlly in paid GitJournal version)
|
||||
- Killer feature that makes it the best note taking tool for Foam (?)
|
||||
|
||||
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://discord.gg/rtdZKgj).
|
||||
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"
|
||||
|
||||
@@ -18,8 +18,8 @@ These extensions are not (yet?) defined in `.vscode/extensions.json`, but have b
|
||||
- [Markdown Emoji](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-emoji) (adds `:smile:` syntax, works with emojisense to provide autocomplete for this syntax)
|
||||
- [Mermaid Support for Preview](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid)
|
||||
- [Mermaid Markdown Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=bpruitt-goddard.mermaid-markdown-syntax-highlighting)
|
||||
- [Paste Image](https://marketplace.visualstudio.com/items?itemName=mushan.vscode-paste-image)
|
||||
- [VSCode PDF Viewing](https://marketplace.visualstudio.com/items?itemName=tomoki1207.pdf)
|
||||
- [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)
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.7.3"
|
||||
"version": "0.13.1"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"version": "0.2.0",
|
||||
"description": "Foam",
|
||||
"repository": "git@github.com:foambubble/foam.git",
|
||||
"author": "Jani Eväkallio <jani.evakallio@gmail.com>",
|
||||
"license": "MIT",
|
||||
"private": "true",
|
||||
"workspaces": [
|
||||
@@ -17,7 +16,7 @@
|
||||
"vscode:package-extension": "yarn workspace foam-vscode package-extension",
|
||||
"vscode:install-extension": "yarn workspace foam-vscode install-extension",
|
||||
"vscode:publish-extension": "yarn workspace foam-vscode publish-extension",
|
||||
"reset": "yarn clean && yarn build && yarn test",
|
||||
"reset": "yarn && yarn clean && yarn build",
|
||||
"clean": "lerna run clean",
|
||||
"build": "lerna run build",
|
||||
"test": "lerna run test",
|
||||
@@ -33,7 +32,7 @@
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "tsdx lint"
|
||||
"pre-commit": "yarn lint"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -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,95 +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.7.3 darwin-x64 node-v12.18.2
|
||||
$ foam --help [COMMAND]
|
||||
USAGE
|
||||
$ foam COMMAND
|
||||
...
|
||||
```
|
||||
<!-- usagestop -->
|
||||
# Commands
|
||||
<!-- commands -->
|
||||
* [`foam help [COMMAND]`](#foam-help-command)
|
||||
* [`foam janitor [WORKSPACEPATH]`](#foam-janitor-workspacepath)
|
||||
* [`foam migrate [WORKSPACEPATH]`](#foam-migrate-workspacepath)
|
||||
|
||||
## `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)_
|
||||
|
||||
## `foam janitor [WORKSPACEPATH]`
|
||||
|
||||
Updates link references and heading across all the markdown files in the given workspaces
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ foam janitor [WORKSPACEPATH]
|
||||
|
||||
OPTIONS
|
||||
-h, --help show CLI help
|
||||
-w, --without-extensions generate link reference definitions without extensions (for legacy support)
|
||||
|
||||
EXAMPLE
|
||||
$ foam-cli janitor path-to-foam-workspace
|
||||
```
|
||||
|
||||
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.7.3/src/commands/janitor.ts)_
|
||||
|
||||
## `foam migrate [WORKSPACEPATH]`
|
||||
|
||||
Updates file names, link references and heading across all the markdown files in the given workspaces
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ foam migrate [WORKSPACEPATH]
|
||||
|
||||
OPTIONS
|
||||
-h, --help show CLI help
|
||||
-w, --without-extensions generate link reference definitions without extensions (for legacy support)
|
||||
|
||||
EXAMPLE
|
||||
$ foam-cli migrate path-to-foam-workspace
|
||||
Successfully generated link references and heading!
|
||||
```
|
||||
|
||||
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.7.3/src/commands/migrate.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,72 +0,0 @@
|
||||
{
|
||||
"name": "foam-cli",
|
||||
"description": "Foam CLI",
|
||||
"version": "0.7.3",
|
||||
"author": "Jani Eväkallio @jevakallio",
|
||||
"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.7.3",
|
||||
"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,91 +0,0 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
bootstrap,
|
||||
createConfigFromFolders,
|
||||
generateLinkReferences,
|
||||
generateHeading,
|
||||
applyTextEdit,
|
||||
Services,
|
||||
FileDataStore,
|
||||
} 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([workspacePath]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
const graph = (await bootstrap(config, services)).notes;
|
||||
|
||||
const notes = graph.getNotes().filter(Boolean); // removes undefined notes
|
||||
|
||||
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,
|
||||
graph,
|
||||
!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.source.uri, file);
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
|
||||
spinner.succeed();
|
||||
spinner.succeed('Done!');
|
||||
} else {
|
||||
spinner.fail('Directory does not exist!');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import ora from 'ora';
|
||||
import {
|
||||
bootstrap,
|
||||
createConfigFromFolders,
|
||||
generateLinkReferences,
|
||||
generateHeading,
|
||||
getKebabCaseFileName,
|
||||
applyTextEdit,
|
||||
Services,
|
||||
FileDataStore,
|
||||
} 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 graph = (await bootstrap(config, services)).notes;
|
||||
|
||||
let notes = graph.getNotes().filter(Boolean); // removes undefined notes
|
||||
|
||||
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.source.uri, kebabCasedFileName);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
await Promise.all(fileRename);
|
||||
|
||||
spinner.text = 'Renaming files';
|
||||
|
||||
// Reinitialize the graph after renaming files
|
||||
graph = (await bootstrap(config, services)).notes;
|
||||
|
||||
notes = graph.getNotes().filter(Boolean); // remove undefined notes
|
||||
|
||||
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,
|
||||
graph,
|
||||
!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.source.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,15 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fileUri absolute path for the file that needs to renamed
|
||||
* @param newFileName "new file name" without the extension
|
||||
*/
|
||||
export const renameFile = async (fileUri: string, newFileName: string) => {
|
||||
const dirName = path.dirname(fileUri);
|
||||
const extension = path.extname(fileUri);
|
||||
const newFileUri = path.join(dirName, `${newFileName}${extension}`);
|
||||
|
||||
return fs.promises.rename(fileUri, newFileUri);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
export const writeFileToDisk = async (fileUri: string, data: string) => {
|
||||
return fs.promises.writeFile(fileUri, data);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { renameFile } from '../src/utils/rename-file';
|
||||
import * as fs from 'fs';
|
||||
import mockFS from 'mock-fs';
|
||||
|
||||
const doesFileExist = path =>
|
||||
fs.promises
|
||||
.access(path)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
describe('renameFile', () => {
|
||||
const fileUri = './test/oldFileName.md';
|
||||
|
||||
beforeAll(() => {
|
||||
mockFS({ [fileUri]: '' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mockFS.restore();
|
||||
});
|
||||
|
||||
it('should rename existing file', async () => {
|
||||
expect(await doesFileExist(fileUri)).toBe(true);
|
||||
|
||||
renameFile(fileUri, 'new-file-name');
|
||||
|
||||
expect(await doesFileExist(fileUri)).toBe(false);
|
||||
expect(await doesFileExist('./test/new-file-name.md')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { writeFileToDisk } from '../src/utils/write-file-to-disk';
|
||||
import * as fs from 'fs';
|
||||
import mockFS from 'mock-fs';
|
||||
|
||||
describe('writeFileToDisk', () => {
|
||||
const fileUri = './test-file.md';
|
||||
|
||||
beforeAll(() => {
|
||||
mockFS({ [fileUri]: 'content in the existing file' });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fs.unlinkSync(fileUri);
|
||||
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, { 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,20 +1,21 @@
|
||||
# Foam Core
|
||||
|
||||
Repository for tooling used by the other modules
|
||||
This module contains the core functions, model, and API of Foam.
|
||||
It is used by its clients to integrate Foam in various use cases, from VsCode extension, to CLI, to CI integrations.
|
||||
|
||||
## Local Development
|
||||
|
||||
Below is a list of commands you will probably find useful.
|
||||
|
||||
### `npm start` or `yarn start`
|
||||
### `yarn watch`
|
||||
|
||||
Runs the project in development/watch mode. Your project will be rebuilt upon changes.
|
||||
|
||||
### `npm run build` or `yarn build`
|
||||
### `yarn build`
|
||||
|
||||
Bundles the package to the `dist` folder. The package is optimized and bundled with Rollup into multiple formats (CommonJS, UMD, and ES Module).
|
||||
|
||||
### `npm test` or `yarn test`
|
||||
### `yarn test`
|
||||
|
||||
Runs the test watcher (Jest) in an interactive mode.
|
||||
By default, runs tests related to files changed since the last commit.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"name": "foam-core",
|
||||
"author": "Jani Eväkallio",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.7.3",
|
||||
"version": "0.13.1",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -16,8 +15,11 @@
|
||||
"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/graphlib": "^2.1.6",
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
"@types/picomatch": "^2.2.1",
|
||||
@@ -28,9 +30,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"detect-newline": "^3.1.0",
|
||||
"fast-array-diff": "^1.0.0",
|
||||
"github-slugger": "^1.3.0",
|
||||
"glob": "^7.1.6",
|
||||
"graphlib": "^2.1.8",
|
||||
"lodash": "^4.17.19",
|
||||
"micromatch": "^4.0.2",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
|
||||
@@ -1,48 +1,56 @@
|
||||
import { createGraph } from './note-graph';
|
||||
import { createMarkdownParser } from './markdown-provider';
|
||||
import { FoamConfig, Foam, Services } from './index';
|
||||
import { FoamConfig, Foam, IDataStore } from './index';
|
||||
import { loadPlugins } from './plugins';
|
||||
import { isSome } from './utils';
|
||||
import { isDisposable } from './common/lifecycle';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
|
||||
export const bootstrap = async (config: FoamConfig, services: Services) => {
|
||||
export const bootstrap = async (config: FoamConfig, dataStore: IDataStore) => {
|
||||
const plugins = await loadPlugins(config);
|
||||
|
||||
const parserPlugins = plugins.map(p => p.parser).filter(isSome);
|
||||
const parser = createMarkdownParser(parserPlugins);
|
||||
|
||||
const graphMiddlewares = plugins.map(p => p.graphMiddleware).filter(isSome);
|
||||
const graph = createGraph(graphMiddlewares);
|
||||
|
||||
const files = await services.dataStore.listFiles();
|
||||
const workspace = new FoamWorkspace();
|
||||
const files = await dataStore.listFiles();
|
||||
await Promise.all(
|
||||
files.map(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
graph.setNote(parser.parse(uri, content));
|
||||
Logger.info('Found: ' + uri);
|
||||
if (URI.isMarkdownFile(uri)) {
|
||||
const content = await dataStore.read(uri);
|
||||
if (isSome(content)) {
|
||||
workspace.set(parser.parse(uri, content));
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
workspace.resolveLinks(true);
|
||||
|
||||
services.dataStore.onDidChange(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
graph.setNote(await parser.parse(uri, content));
|
||||
});
|
||||
services.dataStore.onDidCreate(async uri => {
|
||||
const content = await services.dataStore.read(uri);
|
||||
graph.setNote(await parser.parse(uri, content));
|
||||
});
|
||||
services.dataStore.onDidDelete(async uri => {
|
||||
const note = graph.getNoteByURI(uri);
|
||||
note && graph.deleteNote(note.id);
|
||||
});
|
||||
const listeners = [
|
||||
dataStore.onDidChange(async uri => {
|
||||
const content = await dataStore.read(uri);
|
||||
isSome(content) && workspace.set(await parser.parse(uri, content));
|
||||
}),
|
||||
dataStore.onDidCreate(async uri => {
|
||||
const content = await dataStore.read(uri);
|
||||
isSome(content) && workspace.set(await parser.parse(uri, content));
|
||||
}),
|
||||
dataStore.onDidDelete(uri => {
|
||||
workspace.delete(uri);
|
||||
}),
|
||||
];
|
||||
|
||||
return {
|
||||
notes: graph,
|
||||
workspace: workspace,
|
||||
config: config,
|
||||
parse: parser.parse,
|
||||
services: {
|
||||
dataStore,
|
||||
parser,
|
||||
},
|
||||
dispose: () => {
|
||||
isDisposable(services.dataStore) && services.dataStore.dispose();
|
||||
listeners.forEach(l => l.dispose());
|
||||
workspace.dispose();
|
||||
},
|
||||
} as Foam;
|
||||
};
|
||||
|
||||
436
packages/foam-core/src/common/charCode.ts
Normal file
436
packages/foam-core/src/common/charCode.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
|
||||
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
|
||||
|
||||
/**
|
||||
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
|
||||
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
|
||||
*/
|
||||
export const enum CharCode {
|
||||
Null = 0,
|
||||
/**
|
||||
* The `\b` character.
|
||||
*/
|
||||
Backspace = 8,
|
||||
/**
|
||||
* The `\t` character.
|
||||
*/
|
||||
Tab = 9,
|
||||
/**
|
||||
* The `\n` character.
|
||||
*/
|
||||
LineFeed = 10,
|
||||
/**
|
||||
* The `\r` character.
|
||||
*/
|
||||
CarriageReturn = 13,
|
||||
Space = 32,
|
||||
/**
|
||||
* The `!` character.
|
||||
*/
|
||||
ExclamationMark = 33,
|
||||
/**
|
||||
* The `"` character.
|
||||
*/
|
||||
DoubleQuote = 34,
|
||||
/**
|
||||
* The `#` character.
|
||||
*/
|
||||
Hash = 35,
|
||||
/**
|
||||
* The `$` character.
|
||||
*/
|
||||
DollarSign = 36,
|
||||
/**
|
||||
* The `%` character.
|
||||
*/
|
||||
PercentSign = 37,
|
||||
/**
|
||||
* The `&` character.
|
||||
*/
|
||||
Ampersand = 38,
|
||||
/**
|
||||
* The `'` character.
|
||||
*/
|
||||
SingleQuote = 39,
|
||||
/**
|
||||
* The `(` character.
|
||||
*/
|
||||
OpenParen = 40,
|
||||
/**
|
||||
* The `)` character.
|
||||
*/
|
||||
CloseParen = 41,
|
||||
/**
|
||||
* The `*` character.
|
||||
*/
|
||||
Asterisk = 42,
|
||||
/**
|
||||
* The `+` character.
|
||||
*/
|
||||
Plus = 43,
|
||||
/**
|
||||
* The `,` character.
|
||||
*/
|
||||
Comma = 44,
|
||||
/**
|
||||
* The `-` character.
|
||||
*/
|
||||
Dash = 45,
|
||||
/**
|
||||
* The `.` character.
|
||||
*/
|
||||
Period = 46,
|
||||
/**
|
||||
* The `/` character.
|
||||
*/
|
||||
Slash = 47,
|
||||
|
||||
Digit0 = 48,
|
||||
Digit1 = 49,
|
||||
Digit2 = 50,
|
||||
Digit3 = 51,
|
||||
Digit4 = 52,
|
||||
Digit5 = 53,
|
||||
Digit6 = 54,
|
||||
Digit7 = 55,
|
||||
Digit8 = 56,
|
||||
Digit9 = 57,
|
||||
|
||||
/**
|
||||
* The `:` character.
|
||||
*/
|
||||
Colon = 58,
|
||||
/**
|
||||
* The `;` character.
|
||||
*/
|
||||
Semicolon = 59,
|
||||
/**
|
||||
* The `<` character.
|
||||
*/
|
||||
LessThan = 60,
|
||||
/**
|
||||
* The `=` character.
|
||||
*/
|
||||
Equals = 61,
|
||||
/**
|
||||
* The `>` character.
|
||||
*/
|
||||
GreaterThan = 62,
|
||||
/**
|
||||
* The `?` character.
|
||||
*/
|
||||
QuestionMark = 63,
|
||||
/**
|
||||
* The `@` character.
|
||||
*/
|
||||
AtSign = 64,
|
||||
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
|
||||
/**
|
||||
* The `[` character.
|
||||
*/
|
||||
OpenSquareBracket = 91,
|
||||
/**
|
||||
* The `\` character.
|
||||
*/
|
||||
Backslash = 92,
|
||||
/**
|
||||
* The `]` character.
|
||||
*/
|
||||
CloseSquareBracket = 93,
|
||||
/**
|
||||
* The `^` character.
|
||||
*/
|
||||
Caret = 94,
|
||||
/**
|
||||
* The `_` character.
|
||||
*/
|
||||
Underline = 95,
|
||||
/**
|
||||
* The ``(`)`` character.
|
||||
*/
|
||||
BackTick = 96,
|
||||
|
||||
a = 97,
|
||||
b = 98,
|
||||
c = 99,
|
||||
d = 100,
|
||||
e = 101,
|
||||
f = 102,
|
||||
g = 103,
|
||||
h = 104,
|
||||
i = 105,
|
||||
j = 106,
|
||||
k = 107,
|
||||
l = 108,
|
||||
m = 109,
|
||||
n = 110,
|
||||
o = 111,
|
||||
p = 112,
|
||||
q = 113,
|
||||
r = 114,
|
||||
s = 115,
|
||||
t = 116,
|
||||
u = 117,
|
||||
v = 118,
|
||||
w = 119,
|
||||
x = 120,
|
||||
y = 121,
|
||||
z = 122,
|
||||
|
||||
/**
|
||||
* The `{` character.
|
||||
*/
|
||||
OpenCurlyBrace = 123,
|
||||
/**
|
||||
* The `|` character.
|
||||
*/
|
||||
Pipe = 124,
|
||||
/**
|
||||
* The `}` character.
|
||||
*/
|
||||
CloseCurlyBrace = 125,
|
||||
/**
|
||||
* The `~` character.
|
||||
*/
|
||||
Tilde = 126,
|
||||
|
||||
U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent
|
||||
U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent
|
||||
U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent
|
||||
U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde
|
||||
U_Combining_Macron = 0x0304, // U+0304 Combining Macron
|
||||
U_Combining_Overline = 0x0305, // U+0305 Combining Overline
|
||||
U_Combining_Breve = 0x0306, // U+0306 Combining Breve
|
||||
U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above
|
||||
U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis
|
||||
U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above
|
||||
U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above
|
||||
U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent
|
||||
U_Combining_Caron = 0x030c, // U+030C Combining Caron
|
||||
U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above
|
||||
U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above
|
||||
U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent
|
||||
U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu
|
||||
U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve
|
||||
U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above
|
||||
U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above
|
||||
U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above
|
||||
U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right
|
||||
U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below
|
||||
U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below
|
||||
U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below
|
||||
U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below
|
||||
U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above
|
||||
U_Combining_Horn = 0x031b, // U+031B Combining Horn
|
||||
U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below
|
||||
U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below
|
||||
U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below
|
||||
U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below
|
||||
U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below
|
||||
U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below
|
||||
U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below
|
||||
U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below
|
||||
U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below
|
||||
U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below
|
||||
U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below
|
||||
U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla
|
||||
U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek
|
||||
U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below
|
||||
U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below
|
||||
U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below
|
||||
U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below
|
||||
U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below
|
||||
U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below
|
||||
U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below
|
||||
U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below
|
||||
U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below
|
||||
U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line
|
||||
U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line
|
||||
U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay
|
||||
U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay
|
||||
U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay
|
||||
U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay
|
||||
U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay
|
||||
U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below
|
||||
U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below
|
||||
U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below
|
||||
U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below
|
||||
U_Combining_X_Above = 0x033d, // U+033D Combining X Above
|
||||
U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde
|
||||
U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline
|
||||
U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark
|
||||
U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark
|
||||
U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni
|
||||
U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis
|
||||
U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos
|
||||
U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni
|
||||
U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above
|
||||
U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below
|
||||
U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below
|
||||
U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below
|
||||
U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above
|
||||
U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above
|
||||
U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above
|
||||
U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below
|
||||
U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below
|
||||
U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner
|
||||
U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above
|
||||
U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above
|
||||
U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata
|
||||
U_Combining_X_Below = 0x0353, // U+0353 Combining X Below
|
||||
U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below
|
||||
U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below
|
||||
U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below
|
||||
U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above
|
||||
U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right
|
||||
U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below
|
||||
U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below
|
||||
U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above
|
||||
U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below
|
||||
U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve
|
||||
U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron
|
||||
U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below
|
||||
U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde
|
||||
U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve
|
||||
U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below
|
||||
U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A
|
||||
U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E
|
||||
U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I
|
||||
U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O
|
||||
U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U
|
||||
U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C
|
||||
U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D
|
||||
U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H
|
||||
U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M
|
||||
U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R
|
||||
U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T
|
||||
U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V
|
||||
U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X
|
||||
|
||||
/**
|
||||
* Unicode Character 'LINE SEPARATOR' (U+2028)
|
||||
* http://www.fileformat.info/info/unicode/char/2028/index.htm
|
||||
*/
|
||||
LINE_SEPARATOR = 0x2028,
|
||||
/**
|
||||
* Unicode Character 'PARAGRAPH SEPARATOR' (U+2029)
|
||||
* http://www.fileformat.info/info/unicode/char/2029/index.htm
|
||||
*/
|
||||
PARAGRAPH_SEPARATOR = 0x2029,
|
||||
/**
|
||||
* Unicode Character 'NEXT LINE' (U+0085)
|
||||
* http://www.fileformat.info/info/unicode/char/0085/index.htm
|
||||
*/
|
||||
NEXT_LINE = 0x0085,
|
||||
|
||||
// http://www.fileformat.info/info/unicode/category/Sk/list.htm
|
||||
U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX
|
||||
U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT
|
||||
U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS
|
||||
U_MACRON = 0x00af, // U+00AF MACRON
|
||||
U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT
|
||||
U_CEDILLA = 0x00b8, // U+00B8 CEDILLA
|
||||
U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD
|
||||
U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD
|
||||
U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD
|
||||
U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD
|
||||
U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING
|
||||
U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING
|
||||
U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK
|
||||
U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK
|
||||
U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN
|
||||
U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN
|
||||
U_BREVE = 0x02d8, // U+02D8 BREVE
|
||||
U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE
|
||||
U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE
|
||||
U_OGONEK = 0x02db, // U+02DB OGONEK
|
||||
U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE
|
||||
U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT
|
||||
U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK
|
||||
U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT
|
||||
U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR
|
||||
U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR
|
||||
U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR
|
||||
U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR
|
||||
U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR
|
||||
U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK
|
||||
U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK
|
||||
U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED
|
||||
U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD
|
||||
U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD
|
||||
U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD
|
||||
U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD
|
||||
U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING
|
||||
U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT
|
||||
U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT
|
||||
U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT
|
||||
U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE
|
||||
U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON
|
||||
U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE
|
||||
U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE
|
||||
U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE
|
||||
U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE
|
||||
U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF
|
||||
U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF
|
||||
U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW
|
||||
U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN
|
||||
U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS
|
||||
U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS
|
||||
U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS
|
||||
U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI
|
||||
U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI
|
||||
U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI
|
||||
U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA
|
||||
U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA
|
||||
U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI
|
||||
U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA
|
||||
U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA
|
||||
U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI
|
||||
U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA
|
||||
U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA
|
||||
U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA
|
||||
U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA
|
||||
U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA
|
||||
|
||||
U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE'
|
||||
|
||||
/**
|
||||
* UTF-8 BOM
|
||||
* Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
|
||||
* http://www.fileformat.info/info/unicode/char/feff/index.htm
|
||||
*/
|
||||
UTF8_BOM = 65279,
|
||||
}
|
||||
197
packages/foam-core/src/common/platform.ts
Normal file
197
packages/foam-core/src/common/platform.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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
|
||||
|
||||
const LANGUAGE_DEFAULT = 'en';
|
||||
|
||||
let _isWindows = false;
|
||||
let _isMacintosh = false;
|
||||
let _isLinux = false;
|
||||
let _isNative = false;
|
||||
let _isWeb = false;
|
||||
let _isIOS = false;
|
||||
let _locale: string | undefined = undefined;
|
||||
let _language: string = LANGUAGE_DEFAULT;
|
||||
let _translationsConfigFile: string | undefined = undefined;
|
||||
let _userAgent: string | undefined = undefined;
|
||||
|
||||
interface NLSConfig {
|
||||
locale: string;
|
||||
availableLanguages: { [key: string]: string };
|
||||
_translationsConfigFile: string;
|
||||
}
|
||||
|
||||
export interface IProcessEnvironment {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface INodeProcess {
|
||||
platform: 'win32' | 'linux' | 'darwin';
|
||||
env: IProcessEnvironment;
|
||||
nextTick: Function;
|
||||
versions?: {
|
||||
electron?: string;
|
||||
};
|
||||
sandboxed?: boolean; // Electron
|
||||
type?: string;
|
||||
cwd(): string;
|
||||
}
|
||||
declare const process: INodeProcess;
|
||||
declare const global: any;
|
||||
|
||||
interface INavigator {
|
||||
userAgent: string;
|
||||
language: string;
|
||||
maxTouchPoints?: number;
|
||||
}
|
||||
declare const navigator: INavigator;
|
||||
declare const self: any;
|
||||
|
||||
const _globals =
|
||||
typeof self === 'object'
|
||||
? self
|
||||
: typeof global === 'object'
|
||||
? global
|
||||
: ({} as any);
|
||||
|
||||
let nodeProcess: INodeProcess | undefined = undefined;
|
||||
if (typeof process !== 'undefined') {
|
||||
// Native environment (non-sandboxed)
|
||||
nodeProcess = process;
|
||||
} else if (typeof _globals.vscode !== 'undefined') {
|
||||
// Native environment (sandboxed)
|
||||
nodeProcess = _globals.vscode.process;
|
||||
}
|
||||
|
||||
const isElectronRenderer =
|
||||
typeof nodeProcess?.versions?.electron === 'string' &&
|
||||
nodeProcess.type === 'renderer';
|
||||
export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
|
||||
|
||||
// Web environment
|
||||
if (typeof navigator === 'object' && !isElectronRenderer) {
|
||||
_userAgent = navigator.userAgent;
|
||||
_isWindows = _userAgent.indexOf('Windows') >= 0;
|
||||
_isMacintosh = _userAgent.indexOf('Macintosh') >= 0;
|
||||
_isIOS =
|
||||
(_userAgent.indexOf('Macintosh') >= 0 ||
|
||||
_userAgent.indexOf('iPad') >= 0 ||
|
||||
_userAgent.indexOf('iPhone') >= 0) &&
|
||||
!!navigator.maxTouchPoints &&
|
||||
navigator.maxTouchPoints > 0;
|
||||
_isLinux = _userAgent.indexOf('Linux') >= 0;
|
||||
_isWeb = true;
|
||||
_locale = navigator.language;
|
||||
_language = _locale;
|
||||
}
|
||||
|
||||
// Native environment
|
||||
else if (typeof nodeProcess === 'object') {
|
||||
_isWindows = nodeProcess.platform === 'win32';
|
||||
_isMacintosh = nodeProcess.platform === 'darwin';
|
||||
_isLinux = nodeProcess.platform === 'linux';
|
||||
_locale = LANGUAGE_DEFAULT;
|
||||
_language = LANGUAGE_DEFAULT;
|
||||
const rawNlsConfig = nodeProcess.env['VSCODE_NLS_CONFIG'];
|
||||
if (rawNlsConfig) {
|
||||
try {
|
||||
const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig);
|
||||
const resolved = nlsConfig.availableLanguages['*'];
|
||||
_locale = nlsConfig.locale;
|
||||
// VSCode's default language is 'en'
|
||||
_language = resolved ? resolved : LANGUAGE_DEFAULT;
|
||||
_translationsConfigFile = nlsConfig._translationsConfigFile;
|
||||
} catch (e) {}
|
||||
}
|
||||
_isNative = true;
|
||||
}
|
||||
|
||||
// Unknown environment
|
||||
else {
|
||||
console.error('Unable to resolve platform.');
|
||||
}
|
||||
|
||||
export const enum Platform {
|
||||
Web,
|
||||
Mac,
|
||||
Linux,
|
||||
Windows,
|
||||
}
|
||||
export function PlatformToString(platform: Platform) {
|
||||
switch (platform) {
|
||||
case Platform.Web:
|
||||
return 'Web';
|
||||
case Platform.Mac:
|
||||
return 'Mac';
|
||||
case Platform.Linux:
|
||||
return 'Linux';
|
||||
case Platform.Windows:
|
||||
return 'Windows';
|
||||
}
|
||||
}
|
||||
|
||||
let _platform: Platform = Platform.Web;
|
||||
if (_isMacintosh) {
|
||||
_platform = Platform.Mac;
|
||||
} else if (_isWindows) {
|
||||
_platform = Platform.Windows;
|
||||
} else if (_isLinux) {
|
||||
_platform = Platform.Linux;
|
||||
}
|
||||
|
||||
export const isWindows = _isWindows;
|
||||
export const isMacintosh = _isMacintosh;
|
||||
export const isLinux = _isLinux;
|
||||
export const isNative = _isNative;
|
||||
export const isWeb = _isWeb;
|
||||
export const isIOS = _isIOS;
|
||||
export const platform = _platform;
|
||||
export const userAgent = _userAgent;
|
||||
|
||||
/**
|
||||
* The language used for the user interface. The format of
|
||||
* the string is all lower case (e.g. zh-tw for Traditional
|
||||
* Chinese)
|
||||
*/
|
||||
export const language = _language;
|
||||
|
||||
export namespace Language {
|
||||
export function value(): string {
|
||||
return language;
|
||||
}
|
||||
|
||||
export function isDefaultVariant(): boolean {
|
||||
if (language.length === 2) {
|
||||
return language === 'en';
|
||||
} else if (language.length >= 3) {
|
||||
return language[0] === 'e' && language[1] === 'n' && language[2] === '-';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDefault(): boolean {
|
||||
return language === 'en';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The OS locale or the locale specified by --locale. The format of
|
||||
* the string is all lower case (e.g. zh-tw for Traditional
|
||||
* Chinese). The UI is not necessarily shown in the provided locale.
|
||||
*/
|
||||
export const locale = _locale;
|
||||
|
||||
/**
|
||||
* The translatios that are available through language packs.
|
||||
*/
|
||||
export const translationsConfigFile = _translationsConfigFile;
|
||||
|
||||
export const globals: any = _globals;
|
||||
|
||||
interface ISetImmediate {
|
||||
(callback: (...args: any[]) => void): void;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { merge } from 'lodash';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './model/uri';
|
||||
|
||||
export interface FoamConfig {
|
||||
workspaceFolders: string[];
|
||||
workspaceFolders: URI[];
|
||||
includeGlobs: string[];
|
||||
ignoreGlobs: string[];
|
||||
get<T>(path: string): T | undefined;
|
||||
@@ -14,7 +16,7 @@ const DEFAULT_INCLUDES = ['**/*'];
|
||||
const DEFAULT_IGNORES = ['**/node_modules/**'];
|
||||
|
||||
export const createConfigFromObject = (
|
||||
workspaceFolders: string[],
|
||||
workspaceFolders: URI[],
|
||||
include: string[],
|
||||
ignore: string[],
|
||||
settings: any
|
||||
@@ -33,7 +35,7 @@ export const createConfigFromObject = (
|
||||
};
|
||||
|
||||
export const createConfigFromFolders = (
|
||||
workspaceFolders: string[] | string,
|
||||
workspaceFolders: URI[] | URI,
|
||||
options: {
|
||||
include?: string[];
|
||||
ignore?: string[];
|
||||
@@ -43,7 +45,7 @@ export const createConfigFromFolders = (
|
||||
workspaceFolders = [workspaceFolders];
|
||||
}
|
||||
const workspaceConfig: any = workspaceFolders.reduce(
|
||||
(acc, f) => merge(acc, parseConfig(`${f}/config.json`)),
|
||||
(acc, f) => merge(acc, parseConfig(URI.joinPath(f, 'config.json'))),
|
||||
{}
|
||||
);
|
||||
// For security reasons local plugins can only be
|
||||
@@ -52,7 +54,7 @@ export const createConfigFromFolders = (
|
||||
delete workspaceConfig['experimental']['localPlugins'];
|
||||
}
|
||||
|
||||
const userConfig = parseConfig(`~/.foam/config.json`);
|
||||
const userConfig = parseConfig(URI.file(`~/.foam/config.json`));
|
||||
|
||||
const settings = merge(workspaceConfig, userConfig);
|
||||
|
||||
@@ -64,10 +66,10 @@ export const createConfigFromFolders = (
|
||||
);
|
||||
};
|
||||
|
||||
const parseConfig = (path: string) => {
|
||||
const parseConfig = (path: URI) => {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf8'));
|
||||
return JSON.parse(readFileSync(URI.toFsPath(path), 'utf8'));
|
||||
} catch {
|
||||
console.warn('Could not read configuration from ' + path);
|
||||
Logger.debug('Could not read configuration from ' + path);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const LINK_REFERENCE_DEFINITION_HEADER = `[//begin]: # "Autogenerated link references for markdown compatibility"`;
|
||||
export const LINK_REFERENCE_DEFINITION_FOOTER = `[//end]: # "Autogenerated link references"`;
|
||||
@@ -1,19 +1,37 @@
|
||||
import { Note, NoteLink, URI } from './types';
|
||||
import { NoteGraph, NoteGraphAPI } from './note-graph';
|
||||
import {
|
||||
Resource,
|
||||
Attachment,
|
||||
Placeholder,
|
||||
Note,
|
||||
NoteLink,
|
||||
isNote,
|
||||
NoteLinkDefinition,
|
||||
isPlaceholder,
|
||||
isAttachment,
|
||||
getTitle,
|
||||
NoteParser,
|
||||
} from './model/note';
|
||||
import { FoamConfig } from './config';
|
||||
import { IDataStore, FileDataStore } from './services/datastore';
|
||||
import { ILogger } from './utils/log';
|
||||
import { IDisposable, isDisposable } from './common/lifecycle';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
import { URI } from './model/uri';
|
||||
|
||||
export { Position } from './model/position';
|
||||
export { Range } from './model/range';
|
||||
export { IDataStore, FileDataStore };
|
||||
export { ILogger };
|
||||
export { LogLevel, LogLevelThreshold, Logger, BaseLogger } from './utils/log';
|
||||
export { IDisposable, isDisposable } from './common/lifecycle';
|
||||
export { Event, Emitter } from './common/event';
|
||||
export { FoamConfig };
|
||||
|
||||
export { IDisposable, isDisposable };
|
||||
|
||||
export {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
createMarkdownParser,
|
||||
} from './markdown-provider';
|
||||
|
||||
export {
|
||||
@@ -21,6 +39,8 @@ export {
|
||||
generateHeading,
|
||||
generateLinkReferences,
|
||||
getKebabCaseFileName,
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
} from './janitor';
|
||||
|
||||
export { applyTextEdit } from './janitor/apply-text-edit';
|
||||
@@ -29,19 +49,29 @@ export { createConfigFromFolders } from './config';
|
||||
|
||||
export { bootstrap } from './bootstrap';
|
||||
|
||||
export { NoteGraph, NoteGraphAPI, Note, NoteLink, URI };
|
||||
|
||||
export {
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
} from './definitions';
|
||||
Resource,
|
||||
Attachment,
|
||||
Placeholder,
|
||||
Note,
|
||||
NoteLink,
|
||||
URI,
|
||||
FoamWorkspace,
|
||||
NoteLinkDefinition,
|
||||
NoteParser,
|
||||
isNote,
|
||||
isPlaceholder,
|
||||
isAttachment,
|
||||
getTitle,
|
||||
};
|
||||
|
||||
export interface Services {
|
||||
dataStore: IDataStore;
|
||||
parser: NoteParser;
|
||||
}
|
||||
|
||||
export interface Foam {
|
||||
notes: NoteGraphAPI;
|
||||
export interface Foam extends IDisposable {
|
||||
services: Services;
|
||||
workspace: FoamWorkspace;
|
||||
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,27 +1,27 @@
|
||||
import { Position } from 'unist';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { GraphNote, NoteGraphAPI } from '../note-graph';
|
||||
import { Note } from '../types';
|
||||
import {
|
||||
LINK_REFERENCE_DEFINITION_HEADER,
|
||||
LINK_REFERENCE_DEFINITION_FOOTER,
|
||||
} from '../definitions';
|
||||
import { Note } from '../model/note';
|
||||
import { Range } from '../model/range';
|
||||
import {
|
||||
createMarkdownReferences,
|
||||
stringifyMarkdownLinkReferenceDefinition,
|
||||
} from '../markdown-provider';
|
||||
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"`;
|
||||
|
||||
const slugger = new GithubSlugger();
|
||||
|
||||
export interface TextEdit {
|
||||
range: Position;
|
||||
range: Range;
|
||||
newText: string;
|
||||
}
|
||||
|
||||
export const generateLinkReferences = (
|
||||
note: GraphNote,
|
||||
ng: NoteGraphAPI,
|
||||
note: Note,
|
||||
workspace: FoamWorkspace,
|
||||
includeExtensions: boolean
|
||||
): TextEdit | null => {
|
||||
if (!note) {
|
||||
@@ -29,8 +29,8 @@ export const generateLinkReferences = (
|
||||
}
|
||||
|
||||
const markdownReferences = createMarkdownReferences(
|
||||
ng,
|
||||
note.id,
|
||||
workspace,
|
||||
note.uri,
|
||||
includeExtensions
|
||||
);
|
||||
|
||||
@@ -49,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];
|
||||
@@ -73,10 +70,7 @@ 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),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -113,12 +107,12 @@ export const generateHeading = (note: Note): TextEdit | null => {
|
||||
|
||||
return {
|
||||
newText: `${paddingStart}# ${getHeadingFromFileName(
|
||||
note.slug
|
||||
uriToSlug(note.uri)
|
||||
)}${paddingEnd}`,
|
||||
range: {
|
||||
start: note.source.contentStart,
|
||||
end: note.source.contentStart,
|
||||
},
|
||||
range: Range.createFromPosition(
|
||||
note.source.contentStart,
|
||||
note.source.contentStart
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Node, Position as AstPosition } from 'unist';
|
||||
import unified from 'unified';
|
||||
import markdownParse from 'remark-parse';
|
||||
import wikiLinkPlugin from 'remark-wiki-link';
|
||||
@@ -7,18 +8,42 @@ import visit from 'unist-util-visit';
|
||||
import { Parent, Point } from 'unist';
|
||||
import detectNewline from 'detect-newline';
|
||||
import os from 'os';
|
||||
import * as path from 'path';
|
||||
import { NoteGraphAPI } from './note-graph';
|
||||
import { NoteLinkDefinition, Note, NoteParser } from './types';
|
||||
import {
|
||||
NoteLinkDefinition,
|
||||
Note,
|
||||
NoteParser,
|
||||
isWikilink,
|
||||
getTitle,
|
||||
} from './model/note';
|
||||
import { Position } from './model/position';
|
||||
import { Range } from './model/range';
|
||||
import {
|
||||
dropExtension,
|
||||
uriToSlug,
|
||||
extractHashtags,
|
||||
extractTagsFromProp,
|
||||
isNone,
|
||||
isSome,
|
||||
} from './utils';
|
||||
import { ID } from './types';
|
||||
import { ParserPlugin } from './plugins';
|
||||
import { Logger } from './utils/log';
|
||||
import { URI } from './model/uri';
|
||||
import { FoamWorkspace } from './model/workspace';
|
||||
|
||||
/**
|
||||
* Traverses all the children of the given node, extracts
|
||||
* the text from them, and returns it concatenated.
|
||||
*
|
||||
* @param root the node from which to start collecting text
|
||||
*/
|
||||
const getTextFromChildren = (root: Node): string => {
|
||||
let text = '';
|
||||
visit(root, 'text', node => {
|
||||
if (node.type === 'text') {
|
||||
text = text + node.value;
|
||||
}
|
||||
});
|
||||
return text;
|
||||
};
|
||||
|
||||
const tagsPlugin: ParserPlugin = {
|
||||
name: 'tags',
|
||||
@@ -45,7 +70,7 @@ const titlePlugin: ParserPlugin = {
|
||||
},
|
||||
onDidVisitTree: (tree, note) => {
|
||||
if (note.title == null) {
|
||||
note.title = path.parse(note.source.uri).name;
|
||||
note.title = URI.getBasename(note.uri);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -57,7 +82,22 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
note.links.push({
|
||||
type: 'wikilink',
|
||||
slug: node.value as string,
|
||||
position: node.position!,
|
||||
target: node.value as string,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
if (node.type === 'link') {
|
||||
const targetUri = (node as any).url;
|
||||
const uri = URI.resolve(targetUri, note.uri);
|
||||
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
|
||||
return;
|
||||
}
|
||||
const label = getTextFromChildren(node);
|
||||
note.links.push({
|
||||
type: 'link',
|
||||
target: targetUri,
|
||||
label: label,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -71,7 +111,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!),
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -83,12 +123,12 @@ const definitionsPlugin: ParserPlugin = {
|
||||
const handleError = (
|
||||
plugin: ParserPlugin,
|
||||
fnName: string,
|
||||
uri: string | undefined,
|
||||
uri: URI | undefined,
|
||||
e: Error
|
||||
): void => {
|
||||
const name = plugin.name || '';
|
||||
Logger.warn(
|
||||
`Error while executing [${fnName}] in plugin [${name}] for file [${uri}]`,
|
||||
`Error while executing [${fnName}] in plugin [${name}] for file [${uri?.path}]`,
|
||||
e
|
||||
);
|
||||
};
|
||||
@@ -116,7 +156,7 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
});
|
||||
|
||||
const foamParser: NoteParser = {
|
||||
parse: (uri: string, markdown: string): Note => {
|
||||
parse: (uri: URI, markdown: string): Note => {
|
||||
Logger.debug('Parsing:', uri);
|
||||
markdown = plugins.reduce((acc, plugin) => {
|
||||
try {
|
||||
@@ -130,17 +170,17 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
const eol = detectNewline(markdown) || os.EOL;
|
||||
|
||||
var note: Note = {
|
||||
slug: uriToSlug(uri),
|
||||
uri: uri,
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: null,
|
||||
tags: new Set(),
|
||||
links: [],
|
||||
definitions: [],
|
||||
source: {
|
||||
uri: uri,
|
||||
text: markdown,
|
||||
contentStart: tree.position!.start,
|
||||
end: tree.position!.end,
|
||||
contentStart: astPointToFoamPosition(tree.position!.start),
|
||||
end: astPointToFoamPosition(tree.position!.end),
|
||||
eol: eol,
|
||||
},
|
||||
};
|
||||
@@ -163,11 +203,10 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
// 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 {
|
||||
@@ -205,7 +244,7 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
|
||||
|
||||
function getFoamDefinitions(
|
||||
defs: NoteLinkDefinition[],
|
||||
fileEndPoint: Point
|
||||
fileEndPoint: Position
|
||||
): NoteLinkDefinition[] {
|
||||
let previousLine = fileEndPoint.line;
|
||||
let foamDefinitions = [];
|
||||
@@ -216,13 +255,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;
|
||||
@@ -239,60 +278,63 @@ export function stringifyMarkdownLinkReferenceDefinition(
|
||||
return text;
|
||||
}
|
||||
export function createMarkdownReferences(
|
||||
graph: NoteGraphAPI,
|
||||
noteId: ID,
|
||||
workspace: FoamWorkspace,
|
||||
noteUri: URI,
|
||||
includeExtension: boolean
|
||||
): NoteLinkDefinition[] {
|
||||
const source = graph.getNote(noteId);
|
||||
|
||||
const source = workspace.find(noteUri);
|
||||
// Should never occur since we're already in a file,
|
||||
// but better safe than sorry.
|
||||
if (!source) {
|
||||
if (source?.type !== 'note') {
|
||||
console.warn(
|
||||
`Note ${noteId} was not added to NoteGraph before attempting to generate markdown reference list`
|
||||
`Note ${noteUri} note found in workspace when attempting to generate markdown reference list`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return graph
|
||||
.getForwardLinks(noteId)
|
||||
return source.links
|
||||
.filter(isWikilink)
|
||||
.map(link => {
|
||||
let target = graph.getNote(link.to);
|
||||
// if we don't find the target by ID we search the graph by slug
|
||||
if (!target) {
|
||||
const candidates = graph.getNotes({ slug: link.link.slug });
|
||||
if (candidates.length > 1) {
|
||||
Logger.info(
|
||||
`Warning: Slug ${link.link.slug} matches ${candidates.length} documents. Picking one.`
|
||||
);
|
||||
}
|
||||
target = candidates.length > 0 ? candidates[0] : null;
|
||||
const targetUri = workspace.resolveLink(source, link);
|
||||
const target = workspace.find(targetUri);
|
||||
if (isNone(target)) {
|
||||
Logger.warn(`Link ${targetUri} in ${noteUri} is not valid.`);
|
||||
return null;
|
||||
}
|
||||
// We are dropping links to non-existent notes here,
|
||||
// but int the future we may want to surface these too
|
||||
if (!target) {
|
||||
Logger.info(
|
||||
`Warning: Link '${link.to}' in '${noteId}' points to a non-existing note.`
|
||||
);
|
||||
if (target.type === 'placeholder') {
|
||||
// no need to create definitions for placeholders
|
||||
return null;
|
||||
}
|
||||
|
||||
const relativePath = path.relative(
|
||||
path.dirname(source.source.uri),
|
||||
target.source.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.link.slug,
|
||||
url: pathToNote,
|
||||
title: target.title || target.slug,
|
||||
};
|
||||
return { label: link.slug, url: pathToNote, title: getTitle(target) };
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort() as NoteLinkDefinition[];
|
||||
.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
|
||||
);
|
||||
|
||||
84
packages/foam-core/src/model/note.ts
Normal file
84
packages/foam-core/src/model/note.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { URI } from './uri';
|
||||
import { Position } from './position';
|
||||
import { Range } from './range';
|
||||
|
||||
export interface NoteSource {
|
||||
text: string;
|
||||
contentStart: Position;
|
||||
end: Position;
|
||||
eol: string;
|
||||
}
|
||||
|
||||
export interface WikiLink {
|
||||
type: 'wikilink';
|
||||
slug: string;
|
||||
target: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface DirectLink {
|
||||
type: 'link';
|
||||
label: string;
|
||||
target: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export type NoteLink = WikiLink | DirectLink;
|
||||
|
||||
export interface NoteLinkDefinition {
|
||||
label: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
export interface BaseResource {
|
||||
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;
|
||||
properties: any;
|
||||
// sections: NoteSection[]
|
||||
tags: Set<string>;
|
||||
links: NoteLink[];
|
||||
definitions: NoteLinkDefinition[];
|
||||
source: NoteSource;
|
||||
}
|
||||
|
||||
export type Resource = Note | Attachment | Placeholder;
|
||||
|
||||
export interface NoteParser {
|
||||
parse: (uri: URI, text: string) => Note;
|
||||
}
|
||||
|
||||
export const isWikilink = (link: NoteLink): link is WikiLink => {
|
||||
return link.type === 'wikilink';
|
||||
};
|
||||
|
||||
export const getTitle = (resource: Resource): string => {
|
||||
return resource.type === 'note'
|
||||
? resource.title ?? URI.getBasename(resource.uri)
|
||||
: URI.getBasename(resource.uri);
|
||||
};
|
||||
|
||||
export const isNote = (resource: Resource): resource is Note => {
|
||||
return resource.type === 'note';
|
||||
};
|
||||
|
||||
export const isPlaceholder = (resource: Resource): resource is Placeholder => {
|
||||
return resource.type === 'placeholder';
|
||||
};
|
||||
|
||||
export const isAttachment = (resource: Resource): resource is Attachment => {
|
||||
return resource.type === 'attachment';
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
packages/foam-core/src/model/range.ts
Normal file
68
packages/foam-core/src/model/range.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
462
packages/foam-core/src/model/uri.ts
Normal file
462
packages/foam-core/src/model/uri.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
// Some code in this file coming from https://github.com/microsoft/vscode/
|
||||
// See LICENSE for details
|
||||
|
||||
import * as paths from 'path';
|
||||
import { statSync } from 'fs';
|
||||
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') && statSync(URI.toFsPath(uri)).isFile();
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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;
|
||||
}
|
||||
485
packages/foam-core/src/model/workspace.ts
Normal file
485
packages/foam-core/src/model/workspace.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
import { diff } from 'fast-array-diff';
|
||||
import { isEqual } from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { Resource, NoteLink, Note } from './note';
|
||||
import { Range } from './range';
|
||||
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;
|
||||
link: NoteLink;
|
||||
};
|
||||
|
||||
export function getReferenceType(
|
||||
reference: URI | string
|
||||
): 'uri' | 'absolute-path' | 'relative-path' | 'key' {
|
||||
if (URI.isUri(reference)) {
|
||||
return 'uri';
|
||||
}
|
||||
const isPath = reference.split('/').length > 1;
|
||||
if (!isPath) {
|
||||
return 'key';
|
||||
}
|
||||
const isAbsPath = isPath && reference.startsWith('/');
|
||||
return isAbsPath ? 'absolute-path' : 'relative-path';
|
||||
}
|
||||
|
||||
const pathToResourceId = (pathValue: string) => {
|
||||
const { ext } = path.parse(pathValue);
|
||||
return ext.length > 0 ? pathValue : pathValue + '.md';
|
||||
};
|
||||
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 class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
private onDidUpdateEmitter = new Emitter<{ old: Resource; new: Resource }>();
|
||||
private onDidDeleteEmitter = new Emitter<Resource>();
|
||||
onDidAdd = this.onDidAddEmitter.event;
|
||||
onDidUpdate = this.onDidUpdateEmitter.event;
|
||||
onDidDelete = this.onDidDeleteEmitter.event;
|
||||
|
||||
/**
|
||||
* Resources by key / slug
|
||||
*/
|
||||
private resourcesByName: { [key: string]: string[] } = {};
|
||||
/**
|
||||
* 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[] = [];
|
||||
|
||||
exists(uri: URI) {
|
||||
return FoamWorkspace.exists(this, uri);
|
||||
}
|
||||
list() {
|
||||
return FoamWorkspace.list(this);
|
||||
}
|
||||
get(uri: URI) {
|
||||
return FoamWorkspace.get(this, uri);
|
||||
}
|
||||
find(uri: URI | string) {
|
||||
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 | undefined;
|
||||
switch (link.type) {
|
||||
case 'wikilink':
|
||||
const definitionUri = note.definitions.find(
|
||||
def => def.label === link.slug
|
||||
)?.url;
|
||||
if (isSome(definitionUri)) {
|
||||
const definedUri = URI.resolve(definitionUri, note.uri);
|
||||
targetUri =
|
||||
FoamWorkspace.find(workspace, definedUri, note.uri)?.uri ??
|
||||
URI.placeholder(definedUri.path);
|
||||
} else {
|
||||
targetUri =
|
||||
FoamWorkspace.find(workspace, link.slug, note.uri)?.uri ??
|
||||
URI.placeholder(link.slug);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'link':
|
||||
targetUri =
|
||||
FoamWorkspace.find(workspace, link.target, note.uri)?.uri ??
|
||||
URI.placeholder(URI.resolve(link.target, note.uri).path);
|
||||
break;
|
||||
}
|
||||
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): Connection[] {
|
||||
return workspace.links[uri.path] ?? [];
|
||||
}
|
||||
|
||||
public static getBacklinks(workspace: FoamWorkspace, uri: URI): Connection[] {
|
||||
return workspace.backlinks[uri.path] ?? [];
|
||||
}
|
||||
|
||||
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 name = uriToResourceName(resource.uri);
|
||||
workspace.resources[id] = resource;
|
||||
workspace.resourcesByName[name] = workspace.resourcesByName[name] ?? [];
|
||||
workspace.resourcesByName[name].push(id);
|
||||
isSome(old)
|
||||
? workspace.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: workspace.onDidAddEmitter.fire(resource);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
public static exists(workspace: FoamWorkspace, uri: URI): boolean {
|
||||
return isSome(workspace.resources[uriToResourceId(uri)]);
|
||||
}
|
||||
|
||||
public static list(workspace: FoamWorkspace): Resource[] {
|
||||
return [
|
||||
...Object.values(workspace.resources),
|
||||
...Object.values(workspace.placeholders),
|
||||
];
|
||||
}
|
||||
|
||||
public static get(workspace: FoamWorkspace, uri: URI): Resource {
|
||||
const note = FoamWorkspace.find(workspace, uri);
|
||||
if (isSome(note)) {
|
||||
return note;
|
||||
} else {
|
||||
throw new Error('Resource not found: ' + uri.path);
|
||||
}
|
||||
}
|
||||
|
||||
public static find(
|
||||
workspace: FoamWorkspace,
|
||||
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;
|
||||
}
|
||||
|
||||
case 'key':
|
||||
const name = pathToResourceName(resourceId as string);
|
||||
const paths = workspace.resourcesByName[name];
|
||||
if (isNone(paths) || paths.length === 0) {
|
||||
const placeholderId = pathToPlaceholderId(resourceId as string);
|
||||
return workspace.placeholders[placeholderId] ?? null;
|
||||
}
|
||||
// prettier-ignore
|
||||
const sortedPaths = paths.length === 1
|
||||
? paths
|
||||
: paths.sort((a, b) => a.localeCompare(b));
|
||||
return workspace.resources[sortedPaths[0]];
|
||||
|
||||
case 'absolute-path':
|
||||
const resourceUri = URI.file(resourceId as string);
|
||||
return (
|
||||
workspace.resources[uriToResourceId(resourceUri)] ??
|
||||
workspace.placeholders[uriToPlaceholderId(resourceUri)]
|
||||
);
|
||||
|
||||
case 'relative-path':
|
||||
if (isNone(reference)) {
|
||||
return null;
|
||||
}
|
||||
const relativePath = resourceId as string;
|
||||
const targetUri = URI.computeRelativeURI(reference, relativePath);
|
||||
return (
|
||||
workspace.resources[uriToResourceId(targetUri)] ??
|
||||
workspace.placeholders[pathToPlaceholderId(resourceId as string)]
|
||||
);
|
||||
|
||||
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) ?? [];
|
||||
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, link);
|
||||
});
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
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, link);
|
||||
}, workspace);
|
||||
workspace = patch.added.reduce((ws, link) => {
|
||||
const target = ws.resolveLink(newResource, link);
|
||||
return FoamWorkspace.connect(ws, newResource.uri, target, link);
|
||||
}, workspace);
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
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, connection) =>
|
||||
FoamWorkspace.disconnect(ws, uri, connection.target, connection.link),
|
||||
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,
|
||||
link: NoteLink
|
||||
) {
|
||||
if (URI.isPlaceholder(target)) {
|
||||
// we can only add placeholders when links are being resolved
|
||||
workspace = FoamWorkspace.set(workspace, {
|
||||
type: 'placeholder',
|
||||
uri: target,
|
||||
});
|
||||
}
|
||||
const connection = { source, target, link };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static disconnect(
|
||||
workspace: FoamWorkspace,
|
||||
source: URI,
|
||||
target: URI,
|
||||
link: NoteLink | true
|
||||
) {
|
||||
const connectionsToKeep =
|
||||
link === true
|
||||
? (c: Connection) =>
|
||||
!URI.isEqual(source, c.source) || !URI.isEqual(target, c.target)
|
||||
: (c: Connection) => !isSameConnection({ source, target, link }, c);
|
||||
|
||||
workspace.links[source.path] =
|
||||
workspace.links[source.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (workspace.links[source.path].length === 0) {
|
||||
delete workspace.links[source.path];
|
||||
}
|
||||
workspace.backlinks[target.path] =
|
||||
workspace.backlinks[target.path]?.filter(connectionsToKeep) ?? [];
|
||||
if (workspace.backlinks[target.path].length === 0) {
|
||||
delete workspace.backlinks[target.path];
|
||||
if (URI.isPlaceholder(target)) {
|
||||
delete workspace.placeholders[uriToPlaceholderId(target)];
|
||||
}
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
}
|
||||
|
||||
// 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: NoteLink, b: NoteLink) =>
|
||||
a.type === b.type && Range.isEqual(a.range, b.range);
|
||||
@@ -1,174 +0,0 @@
|
||||
import { Graph } from 'graphlib';
|
||||
import { URI, ID, Note, NoteLink } from './types';
|
||||
import { computeRelativeURI, nameToSlug, isSome } from './utils';
|
||||
import { Event, Emitter } from './common/event';
|
||||
|
||||
export type GraphNote = Note & {
|
||||
id: ID;
|
||||
};
|
||||
|
||||
export interface GraphConnection {
|
||||
from: ID;
|
||||
to: ID;
|
||||
link: NoteLink;
|
||||
}
|
||||
|
||||
export type NoteGraphEventHandler = (e: { note: GraphNote }) => void;
|
||||
|
||||
export type NotesQuery = { slug: string } | { title: string };
|
||||
|
||||
export interface NoteGraphAPI {
|
||||
setNote(note: Note): GraphNote;
|
||||
deleteNote(noteId: ID): GraphNote | null;
|
||||
getNotes(query?: NotesQuery): GraphNote[];
|
||||
getNote(noteId: ID): GraphNote | null;
|
||||
getNoteByURI(uri: URI): GraphNote | null;
|
||||
getAllLinks(noteId: ID): GraphConnection[];
|
||||
getForwardLinks(noteId: ID): GraphConnection[];
|
||||
getBacklinks(noteId: ID): GraphConnection[];
|
||||
onDidAddNote: Event<GraphNote>;
|
||||
onDidUpdateNote: Event<GraphNote>;
|
||||
onDidDeleteNote: Event<GraphNote>;
|
||||
}
|
||||
|
||||
export type Middleware = (next: NoteGraphAPI) => Partial<NoteGraphAPI>;
|
||||
|
||||
export const createGraph = (middlewares: Middleware[]): NoteGraphAPI => {
|
||||
const graph: NoteGraphAPI = new NoteGraph();
|
||||
return middlewares.reduce((acc, m) => backfill(acc, m), graph);
|
||||
};
|
||||
|
||||
export class NoteGraph implements NoteGraphAPI {
|
||||
onDidAddNote: Event<GraphNote>;
|
||||
onDidUpdateNote: Event<GraphNote>;
|
||||
onDidDeleteNote: Event<GraphNote>;
|
||||
|
||||
private graph: Graph;
|
||||
private createIdFromURI: (uri: URI) => ID;
|
||||
private onDidAddNoteEmitter = new Emitter<GraphNote>();
|
||||
private onDidUpdateNoteEmitter = new Emitter<GraphNote>();
|
||||
private onDidDeleteEmitter = new Emitter<GraphNote>();
|
||||
|
||||
constructor() {
|
||||
this.graph = new Graph();
|
||||
this.onDidAddNote = this.onDidAddNoteEmitter.event;
|
||||
this.onDidUpdateNote = this.onDidUpdateNoteEmitter.event;
|
||||
this.onDidDeleteNote = this.onDidDeleteEmitter.event;
|
||||
this.createIdFromURI = uri => uri;
|
||||
}
|
||||
|
||||
public setNote(note: Note): GraphNote {
|
||||
const id = this.createIdFromURI(note.source.uri);
|
||||
const oldNote = this.getNote(id);
|
||||
if (isSome(oldNote)) {
|
||||
this.removeForwardLinks(id);
|
||||
}
|
||||
const graphNote: GraphNote = {
|
||||
...note,
|
||||
id: id,
|
||||
};
|
||||
this.graph.setNode(id, graphNote);
|
||||
note.links.forEach(link => {
|
||||
const relativePath =
|
||||
note.definitions.find(def => def.label === link.slug)?.url ?? link.slug;
|
||||
const targetPath = computeRelativeURI(note.source.uri, relativePath);
|
||||
const targetId = this.createIdFromURI(targetPath);
|
||||
const connection: GraphConnection = {
|
||||
from: graphNote.id,
|
||||
to: targetId,
|
||||
link: link,
|
||||
};
|
||||
this.graph.setEdge(graphNote.id, targetId, connection);
|
||||
});
|
||||
isSome(oldNote)
|
||||
? this.onDidUpdateNoteEmitter.fire(graphNote)
|
||||
: this.onDidAddNoteEmitter.fire(graphNote);
|
||||
return graphNote;
|
||||
}
|
||||
|
||||
public deleteNote(noteId: ID): GraphNote | null {
|
||||
return this.doDelete(noteId, true);
|
||||
}
|
||||
|
||||
private doDelete(noteId: ID, fireEvent: boolean): GraphNote | null {
|
||||
const note = this.getNote(noteId);
|
||||
if (isSome(note)) {
|
||||
if (this.getBacklinks(noteId).length >= 1) {
|
||||
this.graph.setNode(noteId, null); // Changes node to the "no file" style
|
||||
} else {
|
||||
this.graph.removeNode(noteId);
|
||||
}
|
||||
fireEvent && this.onDidDeleteEmitter.fire(note);
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
public getNotes(query?: NotesQuery): GraphNote[] {
|
||||
// prettier-ignore
|
||||
const filterFn =
|
||||
query == null ? (note: Note | null) => note != null
|
||||
: 'slug' in query ? (note: Note | null) => [nameToSlug(query.slug), query.slug].includes(note?.slug as string)
|
||||
: 'title' in query ? (note: Note | null) => note?.title === query.title
|
||||
: (note: Note | null) => note != null;
|
||||
|
||||
return this.graph
|
||||
.nodes()
|
||||
.map(id => this.graph.node(id))
|
||||
.filter(filterFn);
|
||||
}
|
||||
|
||||
public getNote(noteId: ID): GraphNote | null {
|
||||
return this.graph.node(noteId) ?? null;
|
||||
}
|
||||
|
||||
public getNoteByURI(uri: URI): GraphNote | null {
|
||||
return this.getNote(this.createIdFromURI(uri));
|
||||
}
|
||||
|
||||
public getAllLinks(noteId: ID): GraphConnection[] {
|
||||
return (this.graph.nodeEdges(noteId) || []).map(edge =>
|
||||
this.graph.edge(edge.v, edge.w)
|
||||
);
|
||||
}
|
||||
|
||||
public getForwardLinks(noteId: ID): GraphConnection[] {
|
||||
return (this.graph.outEdges(noteId) || []).map(edge =>
|
||||
this.graph.edge(edge.v, edge.w)
|
||||
);
|
||||
}
|
||||
|
||||
public removeForwardLinks(noteId: ID) {
|
||||
(this.graph.outEdges(noteId) || []).forEach(edge => {
|
||||
this.graph.removeEdge(edge);
|
||||
});
|
||||
}
|
||||
|
||||
public getBacklinks(noteId: ID): GraphConnection[] {
|
||||
return (this.graph.inEdges(noteId) || []).map(edge =>
|
||||
this.graph.edge(edge.v, edge.w)
|
||||
);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.onDidAddNoteEmitter.dispose();
|
||||
this.onDidUpdateNoteEmitter.dispose();
|
||||
this.onDidDeleteEmitter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
const backfill = (next: NoteGraphAPI, middleware: Middleware): NoteGraphAPI => {
|
||||
const m = middleware(next);
|
||||
return {
|
||||
setNote: m.setNote || next.setNote,
|
||||
deleteNote: m.deleteNote || next.deleteNote,
|
||||
getNotes: m.getNotes || next.getNotes,
|
||||
getNote: m.getNote || next.getNote,
|
||||
getNoteByURI: m.getNoteByURI || next.getNoteByURI,
|
||||
getAllLinks: m.getAllLinks || next.getAllLinks,
|
||||
getForwardLinks: m.getForwardLinks || next.getForwardLinks,
|
||||
getBacklinks: m.getBacklinks || next.getBacklinks,
|
||||
onDidAddNote: next.onDidAddNote,
|
||||
onDidUpdateNote: next.onDidUpdateNote,
|
||||
onDidDeleteNote: next.onDidDeleteNote,
|
||||
};
|
||||
};
|
||||
@@ -2,16 +2,15 @@ import * as fs from 'fs';
|
||||
import path from 'path';
|
||||
import { Node } from 'unist';
|
||||
import { isNotNull } from '../utils';
|
||||
import { Middleware } from '../note-graph';
|
||||
import { Note } from '../types';
|
||||
import { Note } from '../model/note';
|
||||
import unified from 'unified';
|
||||
import { FoamConfig } from '../config';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
export interface FoamPlugin {
|
||||
name: string;
|
||||
description?: string;
|
||||
graphMiddleware?: Middleware;
|
||||
parser?: ParserPlugin;
|
||||
}
|
||||
|
||||
@@ -38,15 +37,16 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
|
||||
if (!isFeatureEnabled) {
|
||||
return [];
|
||||
}
|
||||
const pluginDirs: string[] =
|
||||
pluginConfig.pluginFolders ?? findPluginDirs(config.workspaceFolders);
|
||||
const pluginDirs: URI[] =
|
||||
pluginConfig.pluginFolders?.map(URI.file) ??
|
||||
findPluginDirs(config.workspaceFolders);
|
||||
|
||||
const plugins = await Promise.all(
|
||||
pluginDirs
|
||||
.filter(dir => fs.statSync(dir).isDirectory)
|
||||
.filter(dir => fs.statSync(URI.toFsPath(dir)).isDirectory)
|
||||
.map(async dir => {
|
||||
try {
|
||||
const pluginFile = path.join(dir, '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));
|
||||
@@ -60,19 +60,22 @@ export async function loadPlugins(config: FoamConfig): Promise<FoamPlugin[]> {
|
||||
return plugins.filter(isNotNull);
|
||||
}
|
||||
|
||||
function findPluginDirs(workspaceFolders: string[]) {
|
||||
function findPluginDirs(workspaceFolders: URI[]) {
|
||||
return workspaceFolders
|
||||
.map(root => path.join(root, '.foam', 'plugins'))
|
||||
.map(root => URI.joinPath(root, '.foam', 'plugins'))
|
||||
.reduce((acc, pluginDir) => {
|
||||
try {
|
||||
const content = fs
|
||||
.readdirSync(pluginDir)
|
||||
.map(dir => path.join(pluginDir, dir));
|
||||
return [...acc, ...content.filter(c => fs.statSync(c).isDirectory())];
|
||||
.readdirSync(URI.toFsPath(pluginDir))
|
||||
.map(dir => URI.joinPath(pluginDir, dir));
|
||||
return [
|
||||
...acc,
|
||||
...content.filter(c => fs.statSync(URI.toFsPath(c)).isDirectory()),
|
||||
];
|
||||
} catch {
|
||||
return acc;
|
||||
}
|
||||
}, [] as string[]);
|
||||
}, [] as URI[]);
|
||||
}
|
||||
|
||||
function validate(plugin: any): FoamPlugin {
|
||||
|
||||
@@ -3,12 +3,31 @@ import { promisify } from 'util';
|
||||
import micromatch from 'micromatch';
|
||||
import fs from 'fs';
|
||||
import { Event, Emitter } from '../common/event';
|
||||
import { URI } from '../types';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamConfig } from '../config';
|
||||
import { Logger } from '../utils/log';
|
||||
import { isSome } from '../utils';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
|
||||
const findAllFiles = promisify(glob);
|
||||
|
||||
export interface IWatcher {
|
||||
/**
|
||||
* An event which fires on file creation.
|
||||
*/
|
||||
onDidCreate: Event<URI>;
|
||||
|
||||
/**
|
||||
* An event which fires on file change.
|
||||
*/
|
||||
onDidChange: Event<URI>;
|
||||
|
||||
/**
|
||||
* An event which fires on file deletion.
|
||||
*/
|
||||
onDidDelete: Event<URI>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a source of files and content
|
||||
*/
|
||||
@@ -20,8 +39,10 @@ export interface IDataStore {
|
||||
|
||||
/**
|
||||
* Read the content of the file from the store
|
||||
*
|
||||
* Returns `null` in case of errors while reading
|
||||
*/
|
||||
read: (uri: URI) => Promise<string>;
|
||||
read: (uri: URI) => Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Returns whether the given URI is a match in
|
||||
@@ -29,12 +50,6 @@ export interface IDataStore {
|
||||
*/
|
||||
isMatch: (uri: URI) => boolean;
|
||||
|
||||
/**
|
||||
* Filters a list of URIs based on whether they are a match
|
||||
* in this data store
|
||||
*/
|
||||
match: (uris: URI[]) => string[];
|
||||
|
||||
/**
|
||||
* An event which fires on file creation.
|
||||
*/
|
||||
@@ -54,26 +69,28 @@ export interface IDataStore {
|
||||
/**
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore {
|
||||
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;
|
||||
readonly isMatch: (uri: URI) => boolean;
|
||||
readonly match: (uris: URI[]) => string[];
|
||||
|
||||
private _folders: readonly string[];
|
||||
private _includeGlobs: string[] = [];
|
||||
private _ignoreGlobs: string[] = [];
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
constructor(config: FoamConfig) {
|
||||
this._folders = config.workspaceFolders;
|
||||
constructor(config: FoamConfig, watcher?: IWatcher) {
|
||||
this._folders = config.workspaceFolders.map(f =>
|
||||
URI.toFsPath(f).replace(/\\/g, '/')
|
||||
);
|
||||
Logger.info('Workspace folders: ', this._folders);
|
||||
|
||||
let includeGlobs: string[] = [];
|
||||
let ignoreGlobs: string[] = [];
|
||||
config.workspaceFolders.forEach(folder => {
|
||||
this._folders.forEach(folder => {
|
||||
const withFolder = folderPlusGlob(folder);
|
||||
includeGlobs.push(
|
||||
this._includeGlobs.push(
|
||||
...config.includeGlobs.map(glob => {
|
||||
if (glob.endsWith('*')) {
|
||||
glob = `${glob}\\.(md|mdx|markdown)`;
|
||||
@@ -81,27 +98,59 @@ export class FileDataStore implements IDataStore {
|
||||
return withFolder(glob);
|
||||
})
|
||||
);
|
||||
ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
|
||||
this._ignoreGlobs.push(...config.ignoreGlobs.map(withFolder));
|
||||
});
|
||||
Logger.info('Glob patterns', {
|
||||
includeGlobs: this._includeGlobs,
|
||||
ignoreGlobs: this._ignoreGlobs,
|
||||
});
|
||||
|
||||
Logger.debug('Glob patterns', {
|
||||
includeGlobs,
|
||||
ignoreGlobs,
|
||||
});
|
||||
this.match = (files: URI[]) => {
|
||||
return micromatch(files, includeGlobs, {
|
||||
ignore: ignoreGlobs,
|
||||
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 => URI.toFsPath(f)),
|
||||
this._includeGlobs,
|
||||
{
|
||||
ignore: this._ignoreGlobs,
|
||||
nocase: true,
|
||||
});
|
||||
};
|
||||
this.isMatch = uri => this.match([uri]).length > 0;
|
||||
}
|
||||
);
|
||||
return matches.map(URI.file);
|
||||
}
|
||||
|
||||
isMatch(uri: URI) {
|
||||
return this.match([uri]).length > 0;
|
||||
}
|
||||
|
||||
async listFiles() {
|
||||
const files = (
|
||||
await Promise.all(
|
||||
this._folders.map(folder => {
|
||||
return findAllFiles(folderPlusGlob(folder)('**/*'));
|
||||
this._folders.map(async folder => {
|
||||
const res = await findAllFiles(folderPlusGlob(folder)('**/*'));
|
||||
return res.map(URI.file);
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
@@ -109,7 +158,18 @@ export class FileDataStore implements IDataStore {
|
||||
}
|
||||
|
||||
async read(uri: URI) {
|
||||
return (await fs.promises.readFile(uri)).toString();
|
||||
try {
|
||||
return (await fs.promises.readFile(URI.toFsPath(uri))).toString();
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// this file can't simply be .d.ts because the TS compiler wouldn't copy it to the dist directory
|
||||
// see https://stackoverflow.com/questions/56018167/typescript-does-not-copy-d-ts-files-to-build
|
||||
import { Position, Point } from 'unist';
|
||||
export { Position, Point };
|
||||
|
||||
export type URI = string;
|
||||
export type ID = string;
|
||||
|
||||
export interface NoteSource {
|
||||
uri: URI;
|
||||
text: string;
|
||||
contentStart: Point;
|
||||
end: Point;
|
||||
eol: string;
|
||||
}
|
||||
|
||||
export interface WikiLink {
|
||||
type: 'wikilink';
|
||||
slug: string;
|
||||
position: Position;
|
||||
}
|
||||
|
||||
// at the moment we only model wikilink
|
||||
export type NoteLink = WikiLink;
|
||||
|
||||
export interface NoteLinkDefinition {
|
||||
label: string;
|
||||
url: string;
|
||||
title?: string;
|
||||
position?: Position;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
title: string | null;
|
||||
slug: string; // note: this slug is not necessarily unique
|
||||
properties: any;
|
||||
// sections: NoteSection[]
|
||||
tags: Set<string>;
|
||||
links: NoteLink[];
|
||||
definitions: NoteLinkDefinition[];
|
||||
source: NoteSource;
|
||||
}
|
||||
|
||||
export interface NoteParser {
|
||||
parse: (uri: string, text: string) => Note;
|
||||
}
|
||||
@@ -4,7 +4,9 @@ export function isNotNull<T>(value: T | null): value is T {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
export function isSome<T>(value: T | null | undefined | void): value is T {
|
||||
export function isSome<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is NonNullable<T> {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,28 +0,0 @@
|
||||
import path from 'path';
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { URI, ID } from '../types';
|
||||
import { hash } from './core';
|
||||
|
||||
export const uriToSlug = (noteUri: URI): string => {
|
||||
return GithubSlugger.slug(path.parse(noteUri).name);
|
||||
};
|
||||
|
||||
export const nameToSlug = (noteName: string): string => {
|
||||
return GithubSlugger.slug(noteName);
|
||||
};
|
||||
|
||||
export const hashURI = (uri: URI): ID => {
|
||||
return hash(path.normalize(uri));
|
||||
};
|
||||
|
||||
export const computeRelativeURI = (
|
||||
reference: URI,
|
||||
relativeSlug: string
|
||||
): URI => {
|
||||
// if no extension is provided, use the same extension as the source file
|
||||
const slug =
|
||||
path.extname(relativeSlug) !== ''
|
||||
? relativeSlug
|
||||
: `${relativeSlug}${path.extname(reference)}`;
|
||||
return path.normalize(path.join(path.dirname(reference), slug));
|
||||
};
|
||||
@@ -1,10 +1,16 @@
|
||||
import * as path from 'path';
|
||||
import { createConfigFromFolders } from '../src/config';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { URI } from '../src/model/uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const testFolder = URI.joinPath(URI.file(__dirname), 'test-config');
|
||||
|
||||
const testFolder = path.join(__dirname, 'test-config');
|
||||
describe('Foam configuration', () => {
|
||||
it('can read settings from config.json', () => {
|
||||
const config = createConfigFromFolders([path.join(testFolder, 'folder1')]);
|
||||
const config = createConfigFromFolders([
|
||||
URI.joinPath(testFolder, 'folder1'),
|
||||
]);
|
||||
expect(config.get('feature1.setting1.value')).toBeTruthy();
|
||||
expect(config.get('feature2.value')).toEqual(12);
|
||||
|
||||
@@ -14,8 +20,8 @@ describe('Foam configuration', () => {
|
||||
|
||||
it('can merge settings from multiple foam folders', () => {
|
||||
const config = createConfigFromFolders([
|
||||
path.join(testFolder, 'folder1'),
|
||||
path.join(testFolder, 'folder2'),
|
||||
URI.joinPath(testFolder, 'folder1'),
|
||||
URI.joinPath(testFolder, 'folder2'),
|
||||
]);
|
||||
|
||||
// override value
|
||||
@@ -31,7 +37,7 @@ describe('Foam configuration', () => {
|
||||
|
||||
it('cannot activate local plugins from workspace config', () => {
|
||||
const config = createConfigFromFolders([
|
||||
path.join(testFolder, 'enable-plugins'),
|
||||
URI.joinPath(testFolder, 'enable-plugins'),
|
||||
]);
|
||||
expect(config.get('experimental.localPlugins.enabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -1,334 +1,80 @@
|
||||
import { NoteGraph, createGraph } from '../src/note-graph';
|
||||
import { NoteLinkDefinition, Note } from '../src/types';
|
||||
import { uriToSlug } from '../src/utils';
|
||||
import path from 'path';
|
||||
import { NoteLinkDefinition, Note, Attachment } from '../src/model/note';
|
||||
import { Range } from '../src/model/range';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { Logger } from '../src/utils/log';
|
||||
|
||||
const position = {
|
||||
start: { line: 1, column: 1 },
|
||||
end: { line: 1, column: 1 },
|
||||
};
|
||||
Logger.setLevel('error');
|
||||
|
||||
const position = Range.create(0, 0, 0, 100);
|
||||
|
||||
const documentStart = position.start;
|
||||
const documentEnd = position.end;
|
||||
const eol = '\n';
|
||||
|
||||
/**
|
||||
* Turns a string into a URI
|
||||
* The goal of this function is to make sure we are consistent in the
|
||||
* way we generate URIs (and therefore IDs) across the tests
|
||||
*/
|
||||
export const strToUri = URI.file;
|
||||
|
||||
export const createAttachment = (params: { uri: string }): Attachment => {
|
||||
return {
|
||||
uri: strToUri(params.uri),
|
||||
type: 'attachment',
|
||||
};
|
||||
};
|
||||
|
||||
export const createTestNote = (params: {
|
||||
uri: string;
|
||||
title?: string;
|
||||
definitions?: NoteLinkDefinition[];
|
||||
links?: { slug: string }[];
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
text?: string;
|
||||
root?: URI;
|
||||
}): Note => {
|
||||
const root = params.root ?? URI.file('/');
|
||||
return {
|
||||
uri: URI.resolve(params.uri, root),
|
||||
type: 'note',
|
||||
properties: {},
|
||||
title: params.title ?? null,
|
||||
slug: uriToSlug(params.uri),
|
||||
title: params.title ?? path.parse(strToUri(params.uri).path).base,
|
||||
definitions: params.definitions ?? [],
|
||||
tags: new Set(),
|
||||
links: params.links
|
||||
? params.links.map(link => ({
|
||||
type: 'wikilink',
|
||||
slug: link.slug,
|
||||
position: position,
|
||||
text: 'link text',
|
||||
}))
|
||||
? 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,
|
||||
range: range,
|
||||
text: 'link text',
|
||||
}
|
||||
: {
|
||||
type: 'link',
|
||||
target: link.to,
|
||||
label: 'link text',
|
||||
range: range,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
source: {
|
||||
eol: eol,
|
||||
end: documentEnd,
|
||||
contentStart: documentStart,
|
||||
uri: params.uri,
|
||||
text: params.text ?? '',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('Note graph', () => {
|
||||
it('Adds notes to graph', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createTestNote({ uri: '/page-a.md' }));
|
||||
graph.setNote(createTestNote({ uri: '/page-b.md' }));
|
||||
graph.setNote(createTestNote({ uri: '/page-c.md' }));
|
||||
|
||||
expect(
|
||||
graph
|
||||
.getNotes()
|
||||
.map(n => n.slug)
|
||||
.sort()
|
||||
).toEqual(['page-a', 'page-b', 'page-c']);
|
||||
});
|
||||
|
||||
it('Detects forward links', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createTestNote({ uri: '/page-a.md' }));
|
||||
const noteB = graph.setNote(
|
||||
createTestNote({
|
||||
uri: '/page-b.md',
|
||||
links: [{ slug: 'page-a' }],
|
||||
})
|
||||
);
|
||||
graph.setNote(createTestNote({ uri: '/page-c.md' }));
|
||||
|
||||
expect(
|
||||
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)!.slug)
|
||||
).toEqual(['page-a']);
|
||||
});
|
||||
|
||||
it('Detects backlinks', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(createTestNote({ uri: '/page-a.md' }));
|
||||
graph.setNote(
|
||||
createTestNote({
|
||||
uri: '/page-b.md',
|
||||
links: [{ slug: 'page-a' }],
|
||||
})
|
||||
);
|
||||
graph.setNote(createTestNote({ uri: '/page-c.md' }));
|
||||
|
||||
expect(
|
||||
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)!.slug)
|
||||
).toEqual(['page-b']);
|
||||
});
|
||||
|
||||
it('Returns null when accessing non-existing node', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createTestNote({ uri: 'page-a' }));
|
||||
expect(graph.getNote('non-existing')).toBeNull();
|
||||
});
|
||||
|
||||
it('Allows adding edges to non-existing documents', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(
|
||||
createTestNote({
|
||||
uri: '/page-a.md',
|
||||
links: [{ slug: 'non-existing' }],
|
||||
})
|
||||
);
|
||||
|
||||
expect(graph.getNote('non-existing')).toBeNull();
|
||||
});
|
||||
|
||||
it('Updates links when modifying note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(createTestNote({ uri: '/page-a.md' }));
|
||||
const noteB = graph.setNote(
|
||||
createTestNote({
|
||||
uri: '/page-b.md',
|
||||
links: [{ slug: 'page-a' }],
|
||||
})
|
||||
);
|
||||
const noteC = graph.setNote(createTestNote({ uri: '/page-c.md' }));
|
||||
|
||||
expect(
|
||||
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)?.slug)
|
||||
).toEqual(['page-a']);
|
||||
expect(
|
||||
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)?.slug)
|
||||
).toEqual(['page-b']);
|
||||
expect(
|
||||
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
|
||||
).toEqual([]);
|
||||
|
||||
graph.setNote(
|
||||
createTestNote({
|
||||
uri: '/page-b.md',
|
||||
links: [{ slug: 'page-c' }],
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)?.slug)
|
||||
).toEqual(['page-c']);
|
||||
expect(
|
||||
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)?.slug)
|
||||
).toEqual([]);
|
||||
expect(
|
||||
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
|
||||
).toEqual(['page-b']);
|
||||
|
||||
// Tests #393: page-a should not lose its links when updated
|
||||
graph.setNote(createTestNote({ title: 'Test-C', uri: '/page-c.md' }));
|
||||
expect(
|
||||
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
|
||||
).toEqual(['page-b']);
|
||||
});
|
||||
|
||||
it('Updates the graph properly when deleting a note', () => {
|
||||
// B should still link out to A after A is deleted. (#393)
|
||||
// C links out to A, like B, but should no longer link out once deleted.
|
||||
// Ensure B is only remaining note after A + C are deleted.
|
||||
const graph = new NoteGraph();
|
||||
|
||||
const noteA = graph.setNote(createTestNote({ uri: '/page-a.md' }));
|
||||
const noteB = graph.setNote(
|
||||
createTestNote({
|
||||
uri: '/page-b.md',
|
||||
links: [{ slug: 'page-a' }],
|
||||
})
|
||||
);
|
||||
const noteC = graph.setNote(
|
||||
createTestNote({
|
||||
uri: '/page-c.md',
|
||||
links: [{ slug: 'page-a' }],
|
||||
})
|
||||
);
|
||||
|
||||
graph.deleteNote(noteA.id);
|
||||
expect(
|
||||
graph.getForwardLinks(noteB.id).map(link => link?.link?.slug)
|
||||
).toEqual(['page-a']);
|
||||
expect(graph.getNote(noteA.id)).toBeNull();
|
||||
|
||||
graph.deleteNote(noteC.id);
|
||||
expect(
|
||||
graph.getForwardLinks(noteC.id).map(link => link?.link?.slug)
|
||||
).toEqual([]);
|
||||
expect(graph.getNotes().map(note => note.slug)).toEqual(['page-b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Graph querying', () => {
|
||||
it('returns empty set if no note is found', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createTestNote({ uri: '/page-a.md' }));
|
||||
expect(graph.getNotes({ slug: 'non-existing' })).toEqual([]);
|
||||
expect(graph.getNotes({ title: 'non-existing' })).toEqual([]);
|
||||
});
|
||||
|
||||
it('finds the note by slug', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(createTestNote({ uri: '/page-a.md' }));
|
||||
expect(graph.getNotes({ slug: note.slug }).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('finds a note by slug when there is more than one', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createTestNote({ uri: '/dir1/page-a.md' }));
|
||||
graph.setNote(createTestNote({ uri: '/dir2/page-a.md' }));
|
||||
expect(graph.getNotes({ slug: 'page-a' }).length).toEqual(2);
|
||||
});
|
||||
|
||||
it('finds a note by title', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
expect(graph.getNotes({ title: 'My Title' }).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('finds a note by title when there are several', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir3/page-b.md', title: 'My Title' })
|
||||
);
|
||||
expect(graph.getNotes({ title: 'My Title' }).length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('graph events', () => {
|
||||
it('fires "add" event when adding a new note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const callback = jest.fn();
|
||||
const listener = graph.onDidAddNote(callback);
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
listener.dispose();
|
||||
});
|
||||
it('fires "updated" event when changing an existing note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const callback = jest.fn();
|
||||
const listener = graph.onDidUpdateNote(callback);
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'Another title' })
|
||||
);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
listener.dispose();
|
||||
});
|
||||
it('fires "delete" event when removing a note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const callback = jest.fn();
|
||||
const listener = graph.onDidDeleteNote(callback);
|
||||
const note = graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
graph.deleteNote(note.id);
|
||||
expect(callback).toHaveBeenCalledTimes(1);
|
||||
listener.dispose();
|
||||
});
|
||||
it('does not fire "delete" event when removing a non-existing note', () => {
|
||||
const graph = new NoteGraph();
|
||||
const callback = jest.fn();
|
||||
const listener = graph.onDidDeleteNote(callback);
|
||||
const note = graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
graph.deleteNote('non-existing-note');
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
listener.dispose();
|
||||
});
|
||||
it('happy lifecycle', () => {
|
||||
const graph = new NoteGraph();
|
||||
const addCallback = jest.fn();
|
||||
const updateCallback = jest.fn();
|
||||
const deleteCallback = jest.fn();
|
||||
const listeners = [
|
||||
graph.onDidAddNote(addCallback),
|
||||
graph.onDidUpdateNote(updateCallback),
|
||||
graph.onDidDeleteNote(deleteCallback),
|
||||
];
|
||||
|
||||
const note = graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
|
||||
);
|
||||
expect(addCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updateCallback).toHaveBeenCalledTimes(0);
|
||||
expect(deleteCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'Another Title' })
|
||||
);
|
||||
expect(addCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updateCallback).toHaveBeenCalledTimes(1);
|
||||
expect(deleteCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
graph.setNote(
|
||||
createTestNote({ uri: '/dir1/page-a.md', title: 'Yet Another Title' })
|
||||
);
|
||||
expect(addCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updateCallback).toHaveBeenCalledTimes(2);
|
||||
expect(deleteCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
graph.deleteNote(note.id);
|
||||
expect(addCallback).toHaveBeenCalledTimes(1);
|
||||
expect(updateCallback).toHaveBeenCalledTimes(2);
|
||||
expect(deleteCallback).toHaveBeenCalledTimes(1);
|
||||
|
||||
listeners.forEach(l => l.dispose());
|
||||
});
|
||||
});
|
||||
|
||||
describe('graph middleware', () => {
|
||||
it('can intercept calls to the graph', async () => {
|
||||
const graph = createGraph([
|
||||
next => ({
|
||||
setNote: note => {
|
||||
note.properties = {
|
||||
injected: true,
|
||||
};
|
||||
return next.setNote(note);
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const note = createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' });
|
||||
expect(note.properties['injected']).toBeUndefined();
|
||||
const res = graph.setNote(note);
|
||||
expect(res.properties['injected']).toBeTruthy();
|
||||
});
|
||||
describe('Test utils', () => {
|
||||
it('are happy', () => {});
|
||||
});
|
||||
|
||||
72
packages/foam-core/test/datastore.test.ts
Normal file
72
packages/foam-core/test/datastore.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { createConfigFromObject } from '../src/config';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { FileDataStore } from '../src';
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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('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']));
|
||||
});
|
||||
});
|
||||
|
||||
function toStringSet(URI: URI[]) {
|
||||
return new Set(URI.map(uri => uri.path.toLocaleLowerCase()));
|
||||
}
|
||||
|
||||
function makeAbsolute(files: string[]) {
|
||||
return new Set(
|
||||
files.map(f =>
|
||||
URI.joinPath(testFolder, f)
|
||||
.path.toLocaleLowerCase()
|
||||
.replace(/\\/g, '/')
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
import { applyTextEdit } from '../../src/janitor/apply-text-edit';
|
||||
import { Range } from '../../src/model/range';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('applyTextEdit', () => {
|
||||
it('should return text with applied TextEdit in the end of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 4. this is fourth line`,
|
||||
range: {
|
||||
start: { line: 3, column: 1, offset: 79 },
|
||||
end: { line: 3, column: 1, offset: 79 },
|
||||
},
|
||||
newText: `4. this is fourth line`,
|
||||
range: Range.create(4, 0, 4, 0),
|
||||
};
|
||||
|
||||
const text = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
4. this is fourth line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
4. this is fourth line`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
@@ -30,23 +30,20 @@ describe('applyTextEdit', () => {
|
||||
|
||||
it('should return text with applied TextEdit at the top of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 1. this is first line`,
|
||||
range: {
|
||||
start: { line: 0, column: 0, offset: 0 },
|
||||
end: { line: 0, column: 0, offset: 0 },
|
||||
},
|
||||
newText: `1. this is first line\n`,
|
||||
range: Range.create(1, 0, 1, 0),
|
||||
};
|
||||
|
||||
const text = `
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
@@ -55,24 +52,21 @@ describe('applyTextEdit', () => {
|
||||
|
||||
it('should return text with applied TextEdit in the middle of the string', () => {
|
||||
const textEdit = {
|
||||
newText: `\n 2. this is the updated second line`,
|
||||
range: {
|
||||
start: { line: 0, column: 0, offset: 26 },
|
||||
end: { line: 0, column: 0, offset: 53 },
|
||||
},
|
||||
newText: `2. this is the updated second line`,
|
||||
range: Range.create(2, 0, 2, 100),
|
||||
};
|
||||
|
||||
const text = `
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
1. this is first line
|
||||
2. this is the updated second line
|
||||
3. this is third line
|
||||
`;
|
||||
1. this is first line
|
||||
2. this is the updated second line
|
||||
3. this is third line
|
||||
`;
|
||||
|
||||
const actual = applyTextEdit(text, textEdit);
|
||||
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
import * as path from 'path';
|
||||
import { NoteGraphAPI } from '../../src/note-graph';
|
||||
import { generateHeading } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Services } from '../../src';
|
||||
import { Note } from '../../src';
|
||||
import { FileDataStore } from '../../src/services/datastore';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
import { FoamWorkspace } from '../../src/model/workspace';
|
||||
import { URI } from '../../src/model/uri';
|
||||
import { Range } from '../../src/model/range';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('generateHeadings', () => {
|
||||
let _graph: NoteGraphAPI;
|
||||
let _workspace: FoamWorkspace;
|
||||
const findBySlug = (slug: string): Note => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => URI.getBasename(res.uri) === slug) as Note;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const config = createConfigFromFolders([
|
||||
path.join(__dirname, '../__scaffold__'),
|
||||
URI.file(path.join(__dirname, '..', '__scaffold__')),
|
||||
]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
const foam = await bootstrap(config, services);
|
||||
_graph = foam.notes;
|
||||
const foam = await bootstrap(config, new FileDataStore(config));
|
||||
_workspace = foam.workspace;
|
||||
});
|
||||
|
||||
it.skip('should add heading to a file that does not have them', () => {
|
||||
const note = _graph.getNotes({ slug: 'file-without-title' })[0];
|
||||
const note = findBySlug('file-without-title');
|
||||
const expected = {
|
||||
newText: `# File without Title
|
||||
|
||||
`,
|
||||
range: {
|
||||
start: {
|
||||
line: 1,
|
||||
column: 1,
|
||||
offset: 0,
|
||||
},
|
||||
end: {
|
||||
line: 1,
|
||||
column: 1,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
range: Range.create(0, 0, 0, 0),
|
||||
};
|
||||
|
||||
const actual = generateHeading(note);
|
||||
@@ -47,19 +44,16 @@ describe('generateHeadings', () => {
|
||||
});
|
||||
|
||||
it('should not cause any changes to a file that has a heading', () => {
|
||||
const note = _graph.getNotes({ slug: 'index' })[0];
|
||||
const note = findBySlug('index');
|
||||
expect(generateHeading(note)).toBeNull();
|
||||
});
|
||||
|
||||
it.skip('should generate heading when the file only contains frontmatter', () => {
|
||||
const note = _graph.getNotes({ slug: 'file-with-only-frontmatter' })[0];
|
||||
const note = findBySlug('file-with-only-frontmatter');
|
||||
|
||||
const expected = {
|
||||
newText: '\n# File with only Frontmatter\n\n',
|
||||
range: {
|
||||
start: { line: 4, column: 1, offset: 60 },
|
||||
end: { line: 4, column: 1, offset: 60 },
|
||||
},
|
||||
range: Range.create(3, 0, 3, 0),
|
||||
};
|
||||
|
||||
const actual = generateHeading(note);
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
import * as path from 'path';
|
||||
import { NoteGraphAPI } from '../../src/note-graph';
|
||||
import { generateLinkReferences } from '../../src/janitor';
|
||||
import { bootstrap } from '../../src/bootstrap';
|
||||
import { createConfigFromFolders } from '../../src/config';
|
||||
import { Services } from '../../src';
|
||||
import { Note, Range } from '../../src';
|
||||
import { FileDataStore } from '../../src/services/datastore';
|
||||
import { Logger } from '../../src/utils/log';
|
||||
import { FoamWorkspace } from '../../src/model/workspace';
|
||||
import { URI } from '../../src/model/uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('generateLinkReferences', () => {
|
||||
let _graph: NoteGraphAPI;
|
||||
let _workspace: FoamWorkspace;
|
||||
const findBySlug = (slug: string): Note => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => URI.getBasename(res.uri) === slug) as Note;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const config = createConfigFromFolders([
|
||||
path.join(__dirname, '../__scaffold__'),
|
||||
URI.file(path.join(__dirname, '..', '__scaffold__')),
|
||||
]);
|
||||
const services: Services = {
|
||||
dataStore: new FileDataStore(config),
|
||||
};
|
||||
_graph = await bootstrap(config, services).then(foam => foam.notes);
|
||||
_workspace = await bootstrap(config, new FileDataStore(config)).then(
|
||||
foam => foam.workspace
|
||||
);
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
expect(_graph.getNotes().length).toEqual(6);
|
||||
expect(_workspace.list().length).toEqual(6);
|
||||
});
|
||||
|
||||
it('should add link references to a file that does not have them', () => {
|
||||
const note = _graph.getNotes({ slug: 'index' })[0];
|
||||
const note = findBySlug('index');
|
||||
const expected = {
|
||||
newText: `
|
||||
newText: textForNote(
|
||||
note,
|
||||
`
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[first-document]: first-document "First Document"
|
||||
[second-document]: second-document "Second Document"
|
||||
[file-without-title]: file-without-title "file-without-title"
|
||||
[//end]: # "Autogenerated link references"`,
|
||||
range: {
|
||||
start: {
|
||||
line: 10,
|
||||
column: 1,
|
||||
offset: 140,
|
||||
},
|
||||
end: {
|
||||
line: 10,
|
||||
column: 1,
|
||||
offset: 140,
|
||||
},
|
||||
},
|
||||
[//end]: # "Autogenerated link references"`
|
||||
),
|
||||
range: Range.create(9, 0, 9, 0),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _graph, false);
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
@@ -54,25 +54,14 @@ describe('generateLinkReferences', () => {
|
||||
});
|
||||
|
||||
it('should remove link definitions from a file that has them, if no links are present', () => {
|
||||
const note = _graph.getNotes({ slug: 'second-document' })[0];
|
||||
const note = findBySlug('second-document');
|
||||
|
||||
const expected = {
|
||||
newText: '',
|
||||
range: {
|
||||
start: {
|
||||
line: 7,
|
||||
column: 1,
|
||||
offset: 105,
|
||||
},
|
||||
end: {
|
||||
line: 9,
|
||||
column: 43,
|
||||
offset: 269,
|
||||
},
|
||||
},
|
||||
range: Range.create(6, 0, 8, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _graph, false);
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
@@ -80,27 +69,19 @@ describe('generateLinkReferences', () => {
|
||||
});
|
||||
|
||||
it('should update link definitions if they are present but changed', () => {
|
||||
const note = _graph.getNotes({ slug: 'first-document' })[0];
|
||||
const note = findBySlug('first-document');
|
||||
|
||||
const expected = {
|
||||
newText: `[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
newText: textForNote(
|
||||
note,
|
||||
`[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[file-without-title]: file-without-title "file-without-title"
|
||||
[//end]: # "Autogenerated link references"`,
|
||||
range: {
|
||||
start: {
|
||||
line: 9,
|
||||
column: 1,
|
||||
offset: 145,
|
||||
},
|
||||
end: {
|
||||
line: 11,
|
||||
column: 43,
|
||||
offset: 312,
|
||||
},
|
||||
},
|
||||
[//end]: # "Autogenerated link references"`
|
||||
),
|
||||
range: Range.create(8, 0, 10, 42),
|
||||
};
|
||||
|
||||
const actual = generateLinkReferences(note, _graph, false);
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
|
||||
expect(actual!.range.start).toEqual(expected.range.start);
|
||||
expect(actual!.range.end).toEqual(expected.range.end);
|
||||
@@ -108,12 +89,24 @@ describe('generateLinkReferences', () => {
|
||||
});
|
||||
|
||||
it('should not cause any changes if link reference definitions were up to date', () => {
|
||||
const note = _graph.getNotes({ slug: 'third-document' })[0];
|
||||
const note = findBySlug('third-document');
|
||||
|
||||
const expected = null;
|
||||
|
||||
const actual = generateLinkReferences(note, _graph, false);
|
||||
const actual = generateLinkReferences(note, _workspace, false);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Will adjust a text line separator to match
|
||||
* what is used by the note
|
||||
* Necessary when running tests on windows
|
||||
*
|
||||
* @param note the note we are adjusting for
|
||||
* @param text starting text, using a \n line separator
|
||||
*/
|
||||
function textForNote(note: Note, text: string): string {
|
||||
return text.split('\n').join(note.source.eol);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@ import {
|
||||
createMarkdownParser,
|
||||
createMarkdownReferences,
|
||||
} from '../src/markdown-provider';
|
||||
import { NoteGraph } from '../src/note-graph';
|
||||
import { DirectLink } from '../src/model/note';
|
||||
import { ParserPlugin } from '../src/plugins';
|
||||
import { Logger } from '../src/utils/log';
|
||||
import { uriToSlug } from '../src/utils/slug';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { FoamWorkspace } from '../src/model/workspace';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const pageA = `
|
||||
# Page A
|
||||
@@ -32,72 +38,126 @@ const pageE = `
|
||||
# Page E
|
||||
`;
|
||||
|
||||
const createNoteFromMarkdown = createMarkdownParser([]).parse;
|
||||
const createNoteFromMarkdown = (path: string, content: string) =>
|
||||
createMarkdownParser([]).parse(URI.file(path), content);
|
||||
|
||||
describe('Markdown loader', () => {
|
||||
it('Converts markdown to notes', () => {
|
||||
const graph = new NoteGraph();
|
||||
graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
graph.setNote(createNoteFromMarkdown('/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC));
|
||||
graph.setNote(createNoteFromMarkdown('/page-d.md', pageD));
|
||||
graph.setNote(createNoteFromMarkdown('/page-e.md', pageE));
|
||||
const workspace = new FoamWorkspace();
|
||||
workspace.set(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
workspace.set(createNoteFromMarkdown('/page-b.md', pageB));
|
||||
workspace.set(createNoteFromMarkdown('/page-c.md', pageC));
|
||||
workspace.set(createNoteFromMarkdown('/page-d.md', pageD));
|
||||
workspace.set(createNoteFromMarkdown('/page-e.md', pageE));
|
||||
|
||||
expect(
|
||||
graph
|
||||
.getNotes()
|
||||
.map(n => n.slug)
|
||||
workspace
|
||||
.list()
|
||||
.map(n => n.uri)
|
||||
.map(uriToSlug)
|
||||
.sort()
|
||||
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
|
||||
});
|
||||
|
||||
it('Parses wikilinks correctly', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
const noteB = graph.setNote(createNoteFromMarkdown('/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC));
|
||||
graph.setNote(createNoteFromMarkdown('/Page D.md', pageD));
|
||||
graph.setNote(createNoteFromMarkdown('/page e.md', pageE));
|
||||
it('Ingores external links', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
`
|
||||
this is a [link to google](https://www.google.com)
|
||||
`
|
||||
);
|
||||
expect(note.links.length).toEqual(0);
|
||||
});
|
||||
|
||||
expect(
|
||||
graph.getBacklinks(noteB.id).map(link => graph.getNote(link.from)!.slug)
|
||||
).toEqual(['page-a']);
|
||||
expect(
|
||||
graph.getForwardLinks(noteA.id).map(link => graph.getNote(link.to)!.slug)
|
||||
).toEqual(['page-b', 'page-c', 'page-d', 'page-e']);
|
||||
it('Ignores references to sections in the same file', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
`
|
||||
this is a [link to intro](#introduction)
|
||||
`
|
||||
);
|
||||
expect(note.links.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('Parses internal links correctly', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is a [link to page b](../doc/page-b.md)'
|
||||
);
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0] as DirectLink;
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.label).toEqual('link to page b');
|
||||
expect(link.target).toEqual('../doc/page-b.md');
|
||||
});
|
||||
|
||||
it('Parses links that have formatting in label', () => {
|
||||
const note = createNoteFromMarkdown(
|
||||
'/path/to/page-a.md',
|
||||
'this is [**link** with __formatting__](../doc/page-b.md)'
|
||||
);
|
||||
expect(note.links.length).toEqual(1);
|
||||
const link = note.links[0] as DirectLink;
|
||||
expect(link.type).toEqual('link');
|
||||
expect(link.label).toEqual('link with formatting');
|
||||
expect(link.target).toEqual('../doc/page-b.md');
|
||||
});
|
||||
|
||||
it('Parses wikilinks correctly', () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/page-a.md', pageA);
|
||||
const noteB = createNoteFromMarkdown('/page-b.md', pageB);
|
||||
const noteC = createNoteFromMarkdown('/page-c.md', pageC);
|
||||
const noteD = createNoteFromMarkdown('/Page D.md', pageD);
|
||||
const noteE = createNoteFromMarkdown('/page e.md', pageE);
|
||||
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(noteB)
|
||||
.set(noteC)
|
||||
.set(noteD)
|
||||
.set(noteE)
|
||||
.resolveLinks();
|
||||
|
||||
expect(workspace.getBacklinks(noteB.uri).map(l => l.source)).toEqual([
|
||||
noteA.uri,
|
||||
]);
|
||||
expect(workspace.getLinks(noteA.uri).map(l => l.target)).toEqual([
|
||||
noteB.uri,
|
||||
noteC.uri,
|
||||
noteD.uri,
|
||||
noteE.uri,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Note Title', () => {
|
||||
it('should initialize note title if heading exists', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
|
||||
|
||||
const pageANoteTitle = graph.getNote(note.id)!.title;
|
||||
expect(pageANoteTitle).toBe('Page A');
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-a.md',
|
||||
`
|
||||
# Page A
|
||||
this note has a title
|
||||
`
|
||||
);
|
||||
expect(note.title).toBe('Page A');
|
||||
});
|
||||
|
||||
it('should default to file name if heading does not exist', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown(
|
||||
'/page-d.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-d.md',
|
||||
`
|
||||
This file has no heading.
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
const pageANoteTitle = graph.getNote(note.id)!.title;
|
||||
expect(pageANoteTitle).toEqual('page-d');
|
||||
expect(note.title).toEqual('page-d');
|
||||
});
|
||||
|
||||
it('should give precedence to frontmatter title over other headings', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
@@ -105,11 +165,9 @@ date: 20-12-12
|
||||
|
||||
# Other Note Title
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
const pageENoteTitle = graph.getNote(note.id)!.title;
|
||||
expect(pageENoteTitle).toBe('Note Title');
|
||||
expect(note.title).toBe('Note Title');
|
||||
});
|
||||
|
||||
it('should not break on empty titles (see #276)', () => {
|
||||
@@ -127,58 +185,39 @@ this note has an empty title line
|
||||
|
||||
describe('frontmatter', () => {
|
||||
it('should parse yaml frontmatter', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-e.md',
|
||||
`
|
||||
---
|
||||
title: Note Title
|
||||
date: 20-12-12
|
||||
---
|
||||
|
||||
# Other Note Title`
|
||||
)
|
||||
);
|
||||
|
||||
const expected = {
|
||||
title: 'Note Title',
|
||||
date: '20-12-12',
|
||||
};
|
||||
|
||||
const actual: any = graph.getNote(note.id)!.properties;
|
||||
|
||||
expect(actual.title).toBe(expected.title);
|
||||
expect(actual.date).toBe(expected.date);
|
||||
expect(note.properties.title).toBe('Note Title');
|
||||
expect(note.properties.date).toBe('20-12-12');
|
||||
});
|
||||
|
||||
it('should parse empty frontmatter', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
---
|
||||
---
|
||||
|
||||
# Empty Frontmatter
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
const expected = {};
|
||||
|
||||
const actual = graph.getNote(note.id)!.properties;
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(note.properties).toEqual({});
|
||||
});
|
||||
|
||||
it('should not fail when there are issues with parsing frontmatter', () => {
|
||||
const graph = new NoteGraph();
|
||||
const note = graph.setNote(
|
||||
createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
const note = createNoteFromMarkdown(
|
||||
'/page-f.md',
|
||||
`
|
||||
---
|
||||
title: - one
|
||||
- two
|
||||
@@ -186,51 +225,46 @@ title: - one
|
||||
---
|
||||
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
const expected = {};
|
||||
|
||||
const actual = graph.getNote(note.id)!.properties;
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(note.properties).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('wikilinks definitions', () => {
|
||||
it('can generate links without file extension when includeExtension = false', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA)
|
||||
);
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
const workspace = new FoamWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir1/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
|
||||
const noExtRefs = createMarkdownReferences(graph, noteA.id, false);
|
||||
const noExtRefs = createMarkdownReferences(workspace, noteA.uri, false);
|
||||
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
|
||||
});
|
||||
|
||||
it('can generate links with file extension when includeExtension = true', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA)
|
||||
);
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
const workspace = new FoamWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir1/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir1/page-c.md', pageC));
|
||||
|
||||
const extRefs = createMarkdownReferences(graph, noteA.id, true);
|
||||
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
|
||||
});
|
||||
|
||||
it('use relative paths', () => {
|
||||
const graph = new NoteGraph();
|
||||
const noteA = graph.setNote(
|
||||
createNoteFromMarkdown('/dir1/page-a.md', pageA)
|
||||
);
|
||||
graph.setNote(createNoteFromMarkdown('/dir2/page-b.md', pageB));
|
||||
graph.setNote(createNoteFromMarkdown('/dir3/page-c.md', pageC));
|
||||
const workspace = new FoamWorkspace();
|
||||
const noteA = createNoteFromMarkdown('/dir1/page-a.md', pageA);
|
||||
workspace
|
||||
.set(noteA)
|
||||
.set(createNoteFromMarkdown('/dir2/page-b.md', pageB))
|
||||
.set(createNoteFromMarkdown('/dir3/page-c.md', pageC));
|
||||
|
||||
const extRefs = createMarkdownReferences(graph, noteA.id, true);
|
||||
const extRefs = createMarkdownReferences(workspace, noteA.uri, true);
|
||||
expect(extRefs.map(r => r.url)).toEqual([
|
||||
'../dir2/page-b.md',
|
||||
'../dir3/page-c.md',
|
||||
@@ -293,9 +327,9 @@ describe('parser plugins', () => {
|
||||
};
|
||||
const parser = createMarkdownParser([testPlugin]);
|
||||
|
||||
it('can augment the parsing of the file', async () => {
|
||||
it('can augment the parsing of the file', () => {
|
||||
const note1 = parser.parse(
|
||||
'/path/to/a',
|
||||
URI.file('/path/to/a'),
|
||||
`
|
||||
This is a test note without headings.
|
||||
But with some content.
|
||||
@@ -304,7 +338,7 @@ But with some content.
|
||||
expect(note1.properties.hasHeading).toBeUndefined();
|
||||
|
||||
const note2 = parser.parse(
|
||||
'/path/to/a',
|
||||
URI.file('/path/to/a'),
|
||||
`
|
||||
# This is a note with header
|
||||
and some content`
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import path from 'path';
|
||||
import { loadPlugins } from '../src/plugins';
|
||||
import { createMarkdownParser } from '../src/markdown-provider';
|
||||
import { createGraph } from '../src/note-graph';
|
||||
import { createTestNote } from './core.test';
|
||||
import { FoamConfig, createConfigFromObject } from '../src/config';
|
||||
import { URI } from '../src/model/uri';
|
||||
import { Logger } from '../src/utils/log';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
const config: FoamConfig = createConfigFromObject([], [], [], {
|
||||
experimental: {
|
||||
@@ -44,15 +46,6 @@ describe('Foam plugins', () => {
|
||||
expect(plugins[0].name).toEqual('Test Plugin');
|
||||
});
|
||||
|
||||
it('supports graph middleware', async () => {
|
||||
const plugins = await loadPlugins(config);
|
||||
const middleware = plugins[0].graphMiddleware;
|
||||
expect(middleware).not.toBeUndefined();
|
||||
const graph = createGraph([middleware!]);
|
||||
const note = graph.setNote(createTestNote({ uri: '/path/to/note.md' }));
|
||||
expect(note.properties['injectedByMiddleware']).toBeTruthy();
|
||||
});
|
||||
|
||||
it('supports parser extension', async () => {
|
||||
const plugins = await loadPlugins(config);
|
||||
const parserPlugin = plugins[0].parser;
|
||||
@@ -60,7 +53,7 @@ describe('Foam plugins', () => {
|
||||
const parser = createMarkdownParser([parserPlugin!]);
|
||||
|
||||
const note = parser.parse(
|
||||
'/path/to/a',
|
||||
URI.file('/path/to/a'),
|
||||
`
|
||||
# This is a note with header
|
||||
and some content`
|
||||
|
||||
0
packages/foam-core/test/test-datastore/file-a.md
Normal file
0
packages/foam-core/test/test-datastore/file-a.md
Normal file
48
packages/foam-core/test/uri.test.ts
Normal file
48
packages/foam-core/test/uri.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { URI } from '../src/model/uri';
|
||||
import { uriToSlug } from '../src/utils/slug';
|
||||
|
||||
describe('Foam URIs', () => {
|
||||
describe('URI parsing', () => {
|
||||
const base = URI.file('/path/to/file.md');
|
||||
test.each([
|
||||
['https://www.google.com', URI.parse('https://www.google.com')],
|
||||
['/path/to/a/file.md', URI.file('/path/to/a/file.md')],
|
||||
['../relative/file.md', URI.file('/path/relative/file.md')],
|
||||
['#section', URI.create({ ...base, fragment: 'section' })],
|
||||
[
|
||||
'../relative/file.md#section',
|
||||
URI.parse('file:/path/relative/file.md#section'),
|
||||
],
|
||||
])('URI Parsing (%s) - %s', (input, exp) => {
|
||||
const result = URI.resolve(input, base);
|
||||
expect(result.scheme).toEqual(exp.scheme);
|
||||
expect(result.authority).toEqual(exp.authority);
|
||||
expect(result.path).toEqual(exp.path);
|
||||
expect(result.query).toEqual(exp.query);
|
||||
expect(result.fragment).toEqual(exp.fragment);
|
||||
});
|
||||
});
|
||||
it('supports various cases', () => {
|
||||
expect(uriToSlug(URI.file('/this/is/a/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('../a/relative/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('another/relative/path.md'))).toEqual('path');
|
||||
expect(uriToSlug(URI.file('no-directory.markdown'))).toEqual(
|
||||
'no-directory'
|
||||
);
|
||||
expect(uriToSlug(URI.file('many.dots.name.markdown'))).toEqual(
|
||||
'manydotsname'
|
||||
);
|
||||
});
|
||||
|
||||
it('computes a relative uri using a slug', () => {
|
||||
expect(
|
||||
URI.computeRelativeURI(URI.file('/my/file.md'), '../hello.md')
|
||||
).toEqual(URI.file('/hello.md'));
|
||||
expect(URI.computeRelativeURI(URI.file('/my/file.md'), '../hello')).toEqual(
|
||||
URI.file('/hello.md')
|
||||
);
|
||||
expect(
|
||||
URI.computeRelativeURI(URI.file('/my/file.markdown'), '../hello')
|
||||
).toEqual(URI.file('/hello.markdown'));
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user