Compare commits

..

46 Commits

Author SHA1 Message Date
Riccardo Ferretti
320d3d2bc3 v0.10.0 2021-02-18 10:18:04 +01:00
Riccardo Ferretti
cc42345276 Preparation for 0.10.0 release 2021-02-18 10:17:23 +01:00
Riccardo Ferretti
46f60ae036 fixed alpha value in graph labels 2021-02-17 21:37:44 +01:00
Riccardo
32e443bbae Refactor core workspace model (#467)
* added workspace WIP

* workspace supports resources

* uri check more lenient (was causing bug by not recognizing some objects)

* added placeholder resource type

* consolidated code in FoamWorkspace and added more tests

* updated all modules to use FoamWorkspace

* fixed FoamWorkspace.getConnections function
when links or backlinks are not present it no longer adds `undefined` to the connection list

* fix in workspace handling of direct links

* consolidated id/name generation functions

* added test for wikilink resolution when several notes have same filename

* removed reference to graphMiddleware in foam-local-plugins doc
graph middleware won't be supported with the `FoamWorkspace`. Support for the markdown provider remains

* removed support for graph middleware, graphlib dependency, and old note-graph implementation
2021-02-17 21:36:29 +01:00
allcontributors[bot]
259642196a docs: add nitwit-se as a contributor (#489)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-02-17 10:21:51 +01:00
Mark Dixon
8a8c0221a2 Added line width and line color to graph settings (#479) 2021-02-17 10:19:55 +01:00
Riccardo Ferretti
585a6d61e1 workaround: force version 1.52.0 during test run
Running the tests with vscode 1.53.0 is causing issues in `suite.ts:23`, which is causing a stack overflow, possibly due to a recursive callback. Also see https://github.com/foambubble/foam/pull/479#issuecomment-774167127 .
It's unclear what's causing the issue, but forcing the version to 1.52.0 solves the problem.
To review, further investigate, and roll back this workaround.
2021-02-09 19:26:56 +01:00
Riccardo Ferretti
bc7dc61511 chore: reset task now checks yarn packages, and no longer executes tests 2021-02-09 17:01:31 +01:00
Riccardo Ferretti
f29edc22cb using services instead of context to pass dataStore to features
The destructuring of the `context` object was causing a ProposedAPI exception to be thrown
2021-02-09 17:00:51 +01:00
allcontributors[bot]
718c83f6ec docs: add njnygaard as a contributor (#478)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-02-05 09:39:16 +01:00
Nikhil Nygaard
e1438cf3eb Update documentation for orphans panel #476 2021-02-05 09:38:14 +01:00
dependabot[bot]
33b995583f Bump nokogiri from 1.10.10 to 1.11.1 in /docs (#473)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.10 to 1.11.1.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.10.10...v1.11.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-02 13:55:17 +01:00
léon h
bc071a20b4 [FEATURE] Notes preview (#468)
* [FEATURE] Notes preview

* Refactor

* datestore to context

* Remove bracket test
2021-02-02 13:38:22 +01:00
hikerpig
96f22fb0a8 docs: add foam-template-gatsby-kb as a publishing solution (#465) 2021-01-29 10:15:53 +01:00
Riccardo Ferretti
d219b400fa v0.9.1 2021-01-28 16:05:34 +01:00
Riccardo Ferretti
19ba7e8673 prepare for release 0.9.1 2021-01-28 16:04:55 +01:00
léon h
7922aa950a [FIX] Orphans listeners (#464) 2021-01-28 16:00:50 +01:00
Riccardo Ferretti
4457e83e38 v0.9.0 2021-01-27 23:19:01 +01:00
Riccardo Ferretti
fb15672e6a preparation for 0.9.0 release 2021-01-27 23:18:40 +01:00
Riccardo Ferretti
2ef2a217ee updated links to discord 2021-01-27 21:54:45 +01:00
léon h
8a73cba1f0 Welcome screen for Tag Explorer (#460)
* Welcome screen for Tag Explorer

* Front matter
2021-01-27 16:38:05 +01:00
allcontributors[bot]
b0ea08b84f docs: add leonhfr as a contributor (#458)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-26 15:57:07 +01:00
léon h
ea0edc5149 Orphaned notes (#457)
* Orphaned notes

* Refactor to panel

* Toggle groupBy option

* Hide command from palette

* Icon toggle

* Docs

* Tests

* Extract isMatch logic
2021-01-26 15:56:14 +01:00
Riccardo Ferretti
42dabfbf9d added discord badge 2021-01-22 23:42:18 +01:00
Riccardo Ferretti
85d3aef2ff updated discord link 2021-01-21 12:32:05 +01:00
Riccardo Ferretti
8bd3109325 removed paste-image extension from recommendation list as it's now part of foam-template 2021-01-19 23:06:37 +01:00
Riccardo Ferretti
6ca800b500 Focus graph documentation on Foam implementation 2021-01-15 19:31:38 +01:00
Riccardo Ferretti
3a798b520f v0.8.0 2021-01-15 13:36:00 +01:00
Riccardo Ferretti
8db4d2897f Preparation for 0.8.0 release 2021-01-15 13:24:35 +01:00
Riccardo
e0bcb6bd92 Style graph nodes by type (#449)
* added styling of graph nodes by type

* removed unused css file

* added style by type to documentation
2021-01-14 20:22:44 +01:00
Riccardo
cd92468311 Prettier format and chores (#448)
* fix #442 - link to known-issues replaced

* formatted settings + enable format on save + improved jest config

* removed editorconfig in foam-cli

* formatted foam-vscode

* removed author in package.json files

* minor changes to readme files

* fixed husky pre-commit hook
2021-01-14 17:00:03 +01:00
Riccardo Ferretti
5a44fbc26f improved foam-vscode readme 2021-01-14 15:05:40 +01:00
allcontributors[bot]
4412f860dd docs: add elswork as a contributor (#445)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-14 13:16:44 +01:00
elswork
3e3d36b954 Update unlinked-references.md (#444)
Fix small mistake
2021-01-14 13:08:51 +01:00
José Duarte
eee6b7bd3a Allow for the reuse of the graph panel (#443) 2021-01-14 13:07:59 +01:00
José Duarte
792e66b061 Add custom graph styling support (#438)
* Add graph style to VSCode settings

* Update default to an empty object

* Add function to retrieve the graph style from the settings

* Implement the graph custom styling setting

The implementation makes use of the webview communication mechanism to
switch messages between the webview and the graph.
It works as follows:

- When the webview is loaded, it now sends a single request to VSCode,
the request asks VSCode for the graph style
- When VSCode answers with the style, the graph style is updated and
the webview loading process continues as normal.

The style change does not modify the API, in fact it makes use of the
shadiest programming tatic ever, *global variables* to remain
compatible.

The style object *currently* supports four base fields:
- background: string
- fontSize: int
- highlightedForeground: string
- node: object
  - note: string
  - nonExistingNote: string
  - unknown: string

* Simplify null handling logic

* Remove debug logs

* Rename style setting

* Rename message style type

* Remove forgotten debug log

* Refactor the code to match model & action

* Add missing break

* Allow for dynamic style updates

* Fix the window loading bug

* Implement a permanent fix to the bug

* Replace `nonExistingNote` with `placeholder`

* Include the new graph style feature in the docs

* Remove unnecessary async

* Remove unused case
2021-01-13 17:55:58 +00:00
Mike Cluck
e5589f0555 Add ability to open a random note (#440)
* Add Open Random Note command
2021-01-12 22:29:03 +01:00
Riccardo
32f40fa0de fix(#473) - link now points to correct location 2021-01-10 22:33:44 +01:00
Riccardo
97e3b20112 Added support for direct links (#433)
* support direct links

* added support for labels with formatting

* added documentation and removed lint warning

* ignore external links

* improved uri parsing

* filter links pointing to same note (e.g. sections within the note)

* check that note exists before navigating to it

* fixed compilation error
2021-01-08 17:54:40 +00:00
Brian Anglin
0060ea2a3a Fix typo in link (#434) 2021-01-06 22:53:38 +01:00
allcontributors[bot]
a11dee89ba docs: add anglinb as a contributor (#431)
* docs: update docs/index.md [skip ci]

* docs: update readme.md [skip ci]

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

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-01-04 15:07:49 +01:00
Brian Anglin
62e46e87b0 Adds iOS Shortcut and GitHub Actions recipe (#430) 2021-01-04 15:04:59 +01:00
Riccardo
ef7ea8d23b End 2 end test for the extension in vscode (#428)
* added support for e2e vscode tests

* using github action that supports headless test run

* loading vscode test instance in empty dir to speed up bootstrap
2021-01-03 19:27:51 +01:00
Riccardo
36a632f218 consolidated prettier configuration in root package.json file (#429)
also fixed linting and consolidated .gitignore and moved .editorconfig
2021-01-03 18:35:33 +01:00
Riccardo
f9331ad327 refactor: better organization and simplification of core model (#406)
* using URI as note identifier

* cleanup: put definitions together with related code, moved note-graph, removed slug field from Note model
2021-01-03 13:04:05 +01:00
allcontributors[bot]
a944d993fc docs: add themaxdavitt as a contributor (#426)
for fixing recipe link in foam-template
2021-01-02 22:53:08 +01:00
95 changed files with 3333 additions and 1522 deletions

View File

@@ -544,6 +544,60 @@
"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"
]
}
],
"contributorsPerLine": 7,

View File

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

View File

@@ -59,4 +59,6 @@ jobs:
- 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
View File

@@ -4,6 +4,7 @@ node_modules
*.tsbuildinfo
*.vsix
*.log
out
dist
docs/_site
docs/.sass-cache

42
.vscode/settings.json vendored
View File

@@ -1,21 +1,25 @@
// 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,
"jest.debugCodeLens.showWhenTestStateIn": ["fail", "unknown", "pass"]
}

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -15,4 +15,4 @@
[//begin]: # "Autogenerated link references for markdown compatibility"
[todo]: dev/todo.md "Todo"
[//end]: # "Autogenerated link references"
[//end]: # "Autogenerated link references"

View File

@@ -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` and `foam-cli` 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.

View File

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

View File

@@ -6,7 +6,7 @@ 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:

View File

@@ -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'

View File

@@ -1,16 +1,59 @@
# 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.
![Graph style demo](../assets/images/graph-style.gif)
A sample configuration object is provided below:
```json
"foam.graph.style": {
"background": "#202020",
"fontSize": 12,
"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:
![Style node by type](../assets/images/style-node-by-type.png)
- Use the `Markdown Links: Show Graph` command to see the graph
![Demo of graph visualiser](../assets/images/foam-navigation-demo.gif)

View File

@@ -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

10
docs/features/orphans.md Normal file
View 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.

View File

@@ -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">
@@ -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,85 +107,94 @@ 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" 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" 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://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>
</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)

View File

@@ -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/).

View File

@@ -36,7 +36,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

View File

@@ -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.)

View File

@@ -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
@@ -108,6 +112,9 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
[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"
@@ -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"

View File

@@ -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"

View File

@@ -18,7 +18,6 @@ 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)

View File

@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.7.7"
"version": "0.10.0"
}

View File

@@ -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": {

View File

@@ -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

View File

@@ -19,7 +19,7 @@ $ npm install -g foam-cli
$ foam COMMAND
running command...
$ foam (-v|--version|version)
foam-cli/0.7.4 darwin-x64 node-v12.18.2
foam-cli/0.10.0 darwin-x64 node-v12.18.2
$ foam --help [COMMAND]
USAGE
$ foam COMMAND
@@ -65,7 +65,7 @@ EXAMPLE
$ foam-cli janitor path-to-foam-workspace
```
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.7.4/src/commands/janitor.ts)_
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.10.0/src/commands/janitor.ts)_
## `foam migrate [WORKSPACEPATH]`
@@ -84,7 +84,7 @@ EXAMPLE
Successfully generated link references and heading!
```
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.7.4/src/commands/migrate.ts)_
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.10.0/src/commands/migrate.ts)_
<!-- commandsstop -->
## Development

View File

@@ -1,8 +1,7 @@
{
"name": "foam-cli",
"description": "Foam CLI",
"version": "0.7.4",
"author": "Jani Eväkallio @jevakallio",
"version": "0.10.0",
"bin": {
"foam": "./bin/run"
},
@@ -11,7 +10,7 @@
"@oclif/command": "^1",
"@oclif/config": "^1",
"@oclif/plugin-help": "^3",
"foam-core": "^0.7.4",
"foam-core": "^0.10.0",
"ora": "^4.0.4",
"tslib": "^1"
},

View File

@@ -9,6 +9,7 @@ import {
Services,
FileDataStore,
URI,
isNote,
} from 'foam-core';
import { writeFileToDisk } from '../utils/write-file-to-disk';
import { isValidDirectory } from '../utils';
@@ -45,9 +46,9 @@ export default class Janitor extends Command {
const services: Services = {
dataStore: new FileDataStore(config),
};
const graph = (await bootstrap(config, services)).notes;
const workspace = (await bootstrap(config, services)).workspace;
const notes = graph.getNotes().filter(Boolean); // removes undefined notes
const notes = workspace.list().filter(isNote);
spinner.succeed();
spinner.text = `${notes.length} files found`;
@@ -65,7 +66,7 @@ export default class Janitor extends Command {
const heading = generateHeading(note);
const definitions = generateLinkReferences(
note,
graph,
workspace,
!flags['without-extensions']
);
@@ -75,7 +76,7 @@ export default class Janitor extends Command {
file = definitions ? applyTextEdit(file, definitions) : file;
if (heading || definitions) {
return writeFileToDisk(note.source.uri, file);
return writeFileToDisk(note.uri, file);
}
return Promise.resolve(null);

View File

@@ -9,6 +9,7 @@ import {
applyTextEdit,
Services,
FileDataStore,
isNote,
} from 'foam-core';
import { writeFileToDisk } from '../utils/write-file-to-disk';
import { renameFile } from '../utils/rename-file';
@@ -48,9 +49,9 @@ Successfully generated link references and heading!
const services: Services = {
dataStore: new FileDataStore(config),
};
let graph = (await bootstrap(config, services)).notes;
let workspace = (await bootstrap(config, services)).workspace;
let notes = graph.getNotes().filter(Boolean); // removes undefined notes
let notes = workspace.list().filter(isNote);
spinner.succeed();
spinner.text = `${notes.length} files found`;
@@ -66,7 +67,7 @@ Successfully generated link references and heading!
if (note.title != null) {
const kebabCasedFileName = getKebabCaseFileName(note.title);
if (kebabCasedFileName) {
return renameFile(note.source.uri, kebabCasedFileName);
return renameFile(note.uri, kebabCasedFileName);
}
}
return Promise.resolve(null);
@@ -77,9 +78,9 @@ Successfully generated link references and heading!
spinner.text = 'Renaming files';
// Reinitialize the graph after renaming files
graph = (await bootstrap(config, services)).notes;
workspace = (await bootstrap(config, services)).workspace;
notes = graph.getNotes().filter(Boolean); // remove undefined notes
notes = workspace.list().filter(isNote);
spinner.succeed();
spinner.text = 'Generating link definitions';
@@ -90,7 +91,7 @@ Successfully generated link references and heading!
const heading = generateHeading(note);
const definitions = generateLinkReferences(
note,
graph,
workspace,
!flags['without-extensions']
);
@@ -100,7 +101,7 @@ Successfully generated link references and heading!
file = definitions ? applyTextEdit(file, definitions) : file;
if (heading || definitions) {
return writeFileToDisk(note.source.uri, file);
return writeFileToDisk(note.uri, file);
}
return Promise.resolve(null);

View File

@@ -3,7 +3,7 @@ import * as fs from 'fs';
import mockFS from 'mock-fs';
import { URI } from 'foam-core';
const doesFileExist = path =>
const doesFileExist = (path: string) =>
fs.promises
.access(path)
.then(() => true)

View File

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

View File

@@ -1,8 +1,7 @@
{
"name": "foam-core",
"author": "Jani Eväkallio",
"repository": "https://github.com/foambubble/foam",
"version": "0.7.4",
"version": "0.10.0",
"license": "MIT",
"files": [
"dist"
@@ -17,7 +16,6 @@
},
"devDependencies": {
"@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 +26,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",

View File

@@ -1,10 +1,10 @@
import { createGraph } from './note-graph';
import { createMarkdownParser } from './markdown-provider';
import { FoamConfig, Foam, Services } from './index';
import { loadPlugins } from './plugins';
import { isSome } from './utils';
import { isDisposable } from './common/lifecycle';
import { Logger } from './utils/log';
import { FoamWorkspace } from './model/workspace';
export const bootstrap = async (config: FoamConfig, services: Services) => {
const plugins = await loadPlugins(config);
@@ -12,9 +12,7 @@ export const bootstrap = async (config: FoamConfig, services: Services) => {
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 workspace = new FoamWorkspace();
const files = await services.dataStore.listFiles();
await Promise.all(
files.map(async uri => {
@@ -22,29 +20,30 @@ export const bootstrap = async (config: FoamConfig, services: Services) => {
if (uri.path.endsWith('md')) {
const content = await services.dataStore.read(uri);
if (isSome(content)) {
graph.setNote(parser.parse(uri, 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));
workspace.set(await parser.parse(uri, content));
});
services.dataStore.onDidCreate(async uri => {
const content = await services.dataStore.read(uri);
graph.setNote(await parser.parse(uri, content));
workspace.set(await parser.parse(uri, content));
});
services.dataStore.onDidDelete(async uri => {
const note = graph.getNoteByURI(uri);
note && graph.deleteNote(note.id);
services.dataStore.onDidDelete(uri => {
workspace.delete(uri);
});
return {
notes: graph,
workspace: workspace,
config: config,
parse: parser.parse,
services: services,
dispose: () => {
isDisposable(services.dataStore) && services.dataStore.dispose();
},

View File

@@ -113,10 +113,10 @@ export class URI implements UriComponents {
typeof (thing as URI).fragment === 'string' &&
typeof (thing as URI).path === 'string' &&
typeof (thing as URI).query === 'string' &&
typeof (thing as URI).scheme === 'string' &&
typeof (thing as URI).fsPath === 'function' &&
typeof (thing as URI).with === 'function' &&
typeof (thing as URI).toString === 'function'
typeof (thing as URI).scheme === 'string'
// typeof (thing as URI).fsPath === 'function' &&
// typeof (thing as URI).with === 'function' &&
// typeof (thing as URI).toString === 'function'
);
}

View File

@@ -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"`;

View File

@@ -1,10 +1,18 @@
import { Note, NoteLink } from './types';
import {
Resource,
Attachment,
Placeholder,
Note,
NoteLink,
isNote,
NoteLinkDefinition,
} from './model/note';
import { URI } from './common/uri';
import { NoteGraph, NoteGraphAPI } from './note-graph';
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';
export { IDataStore, FileDataStore };
export { ILogger };
@@ -24,6 +32,8 @@ export {
generateHeading,
generateLinkReferences,
getKebabCaseFileName,
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER,
} from './janitor';
export { applyTextEdit } from './janitor/apply-text-edit';
@@ -32,19 +42,25 @@ 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,
isNote,
};
export interface Services {
dataStore: IDataStore;
}
export interface Foam extends IDisposable {
notes: NoteGraphAPI;
services: Services;
workspace: FoamWorkspace;
config: FoamConfig;
parse: (uri: URI, text: string, eol: string) => Note;
}

View File

@@ -1,16 +1,15 @@
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 {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from '../markdown-provider';
import { getHeadingFromFileName } from '../utils';
import { getHeadingFromFileName, uriToSlug } from '../utils';
import { FoamWorkspace } from '../model/workspace';
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();
@@ -20,8 +19,8 @@ export interface TextEdit {
}
export const generateLinkReferences = (
note: GraphNote,
ng: NoteGraphAPI,
note: Note,
workspace: FoamWorkspace,
includeExtensions: boolean
): TextEdit | null => {
if (!note) {
@@ -29,8 +28,8 @@ export const generateLinkReferences = (
}
const markdownReferences = createMarkdownReferences(
ng,
note.id,
workspace,
note.uri,
includeExtensions
);
@@ -113,7 +112,7 @@ export const generateHeading = (note: Note): TextEdit | null => {
return {
newText: `${paddingStart}# ${getHeadingFromFileName(
note.slug
uriToSlug(note.uri)
)}${paddingEnd}`,
range: {
start: note.source.contentStart,

View File

@@ -1,3 +1,4 @@
import { Node } from 'unist';
import unified from 'unified';
import markdownParse from 'remark-parse';
import wikiLinkPlugin from 'remark-wiki-link';
@@ -7,14 +8,41 @@ import visit from 'unist-util-visit';
import { Parent, Point } from 'unist';
import detectNewline from 'detect-newline';
import os from 'os';
import { NoteGraphAPI } from './note-graph';
import { NoteLinkDefinition, Note, NoteParser } from './types';
import { dropExtension, extractHashtags, extractTagsFromProp } from './utils';
import { uriToSlug, computeRelativePath, getBasename } from './utils/uri';
import { ID } from './types';
import {
NoteLinkDefinition,
Note,
NoteParser,
isWikilink,
getTitle,
} from './model/note';
import {
dropExtension,
extractHashtags,
extractTagsFromProp,
isNone,
isSome,
} from './utils';
import { computeRelativePath, getBasename, parseUri } from './utils/uri';
import { ParserPlugin } from './plugins';
import { Logger } from './utils/log';
import { URI } from './common/uri';
import { 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',
@@ -41,7 +69,7 @@ const titlePlugin: ParserPlugin = {
},
onDidVisitTree: (tree, note) => {
if (note.title == null) {
note.title = getBasename(note.source.uri);
note.title = getBasename(note.uri);
}
},
};
@@ -53,9 +81,23 @@ const wikilinkPlugin: ParserPlugin = {
note.links.push({
type: 'wikilink',
slug: node.value as string,
target: node.value as string,
position: node.position!,
});
}
if (node.type === 'link') {
const targetUri = (node as any).url;
const uri = parseUri(note.uri, targetUri);
if (uri.scheme !== 'file' || uri.path === note.uri.path) {
return;
}
const label = getTextFromChildren(node);
note.links.push({
type: 'link',
target: targetUri,
label: label,
});
}
},
};
@@ -126,14 +168,14 @@ 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,
@@ -235,60 +277,41 @@ 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 = computeRelativePath(
source.source.uri,
target.source.uri
);
const relativePath = computeRelativePath(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();
}

View File

@@ -0,0 +1,76 @@
import { Position, Point } from 'unist';
import { URI } from '../common/uri';
import { getBasename } from '../utils';
export { Position, Point };
export interface NoteSource {
text: string;
contentStart: Point;
end: Point;
eol: string;
}
export interface WikiLink {
type: 'wikilink';
slug: string;
target: string;
position: Position;
}
export interface DirectLink {
type: 'link';
label: string;
target: string;
}
export type NoteLink = WikiLink | DirectLink;
export interface NoteLinkDefinition {
label: string;
url: string;
title?: string;
position?: Position;
}
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 ?? getBasename(resource.uri)
: getBasename(resource.uri);
};
export const isNote = (resource: Resource): resource is Note => {
return resource.type === 'note';
};

View File

@@ -0,0 +1,412 @@
import { diff } from 'fast-array-diff';
import { isEqual } from 'lodash';
import * as path from 'path';
import { URI } from '../common/uri';
import { Resource, NoteLink, Note } from '../model/note';
import {
computeRelativeURI,
isSome,
isNone,
parseUri,
placeholderUri,
isPlaceholder,
} from '../utils';
import { Emitter } from '../common/event';
import { IDisposable } from '../index';
export type Connection = {
source: URI;
target: URI;
};
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) {
return FoamWorkspace.find(this, uri);
}
set(resource: Resource) {
return FoamWorkspace.set(this, resource);
}
delete(uri: URI) {
return FoamWorkspace.delete(this, uri);
}
resolveLink(note: Note, link: NoteLink) {
return FoamWorkspace.resolveLink(this, note, link);
}
resolveLinks(keepMonitoring: boolean = false) {
return FoamWorkspace.resolveLinks(this, keepMonitoring);
}
getAllConnections() {
return FoamWorkspace.getAllConnections(this);
}
getConnections(uri: URI) {
return FoamWorkspace.getConnections(this, uri);
}
getLinks(uri: URI) {
return FoamWorkspace.getLinks(this, uri);
}
getBacklinks(uri: URI) {
return FoamWorkspace.getBacklinks(this, uri);
}
dispose(): void {
this.onDidAddEmitter.dispose();
this.onDidDeleteEmitter.dispose();
this.onDidUpdateEmitter.dispose();
this.disposables.forEach(d => d.dispose());
}
public static resolveLink(
workspace: FoamWorkspace,
note: Note,
link: NoteLink
): URI {
let targetUri: URI | null = null;
switch (link.type) {
case 'wikilink':
const definitionUri = note.definitions.find(
def => def.label === link.slug
)?.url;
if (isSome(definitionUri)) {
targetUri = parseUri(note.uri, definitionUri!);
} else {
targetUri =
FoamWorkspace.find(workspace, link.slug, note.uri)?.uri ??
placeholderUri(link.slug);
}
break;
case 'link':
targetUri =
FoamWorkspace.find(workspace, link.target, note.uri)?.uri ??
placeholderUri(link.target);
break;
}
if (isPlaceholder(targetUri)) {
// we can only add placeholders when links are being resolved
workspace = FoamWorkspace.set(workspace, {
type: 'placeholder',
uri: targetUri,
});
}
return targetUri;
}
/**
* Computes all the links in the workspace, connecting notes and
* creating placeholders.
*
* @param workspace the target workspace
* @param keepMonitoring whether to recompute the links when the workspace changes
* @returns the resolved workspace
*/
public static resolveLinks(
workspace: FoamWorkspace,
keepMonitoring: boolean = false
): FoamWorkspace {
workspace.links = {};
workspace.backlinks = {};
workspace.placeholders = {};
workspace = Object.values(workspace.list()).reduce(
(w, resource) => FoamWorkspace.resolveResource(w, resource),
workspace
);
if (keepMonitoring) {
workspace.disposables.push(
workspace.onDidAdd(resource => {
FoamWorkspace.resolveResource(workspace, resource);
}),
workspace.onDidUpdate(change => {
FoamWorkspace.updateLinksForResource(
workspace,
change.old,
change.new
);
}),
workspace.onDidDelete(resource => {
FoamWorkspace.deleteLinksForResource(workspace, resource.uri);
})
);
}
return workspace;
}
public static getAllConnections(workspace: FoamWorkspace): Connection[] {
return Object.values(workspace.links).flat();
}
public static getConnections(
workspace: FoamWorkspace,
uri: URI
): Connection[] {
return [
...(workspace.links[uri.path] || []),
...(workspace.backlinks[uri.path] || []),
];
}
public static getLinks(workspace: FoamWorkspace, uri: URI): URI[] {
return workspace.links[uri.path]?.map(c => c.target) ?? [];
}
public static getBacklinks(workspace: FoamWorkspace, uri: URI): URI[] {
return workspace.backlinks[uri.path]?.map(c => c.source) ?? [];
}
public static set(
workspace: FoamWorkspace,
resource: Resource
): FoamWorkspace {
if (resource.type === 'placeholder') {
workspace.placeholders[uriToPlaceholderId(resource.uri)] = resource;
return workspace;
}
const id = uriToResourceId(resource.uri);
const old = FoamWorkspace.find(workspace, resource.uri);
const 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)) {
throw new Error(
'Cannot find note defined by relative path without reference note: ' +
resourceId
);
}
const relativePath = resourceId as string;
const targetUri = 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];
isSome(deleted) && workspace.onDidDeleteEmitter.fire(deleted);
return deleted ?? null;
}
public static resolveResource(workspace: FoamWorkspace, resource: Resource) {
// prettier-ignore
resource.type === 'note' && resource.links.forEach(link => {
const targetUri = FoamWorkspace.resolveLink(workspace, resource, link)
workspace = FoamWorkspace.connect(workspace, resource.uri, targetUri)
});
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((g, link) => {
const target = workspace.resolveLink(oldResource, link);
return FoamWorkspace.disconnect(g, oldResource.uri, target);
}, workspace);
workspace = patch.added.reduce((g, link) => {
const target = workspace.resolveLink(newResource, link);
return FoamWorkspace.connect(g, newResource.uri, target);
}, workspace);
}
return workspace;
}
private static deleteLinksForResource(workspace: FoamWorkspace, uri: URI) {
delete workspace.links[uri.path];
// we rebuild the backlinks by resolving any link that was pointing to the deleted resource
const toCheck = workspace.backlinks[uri.path];
delete workspace.backlinks[uri.path];
toCheck.forEach(link => {
const source = workspace.get(link.source);
source.type === 'note' &&
source.links.forEach(l => {
const targetUri = FoamWorkspace.resolveLink(workspace, source, l);
workspace = FoamWorkspace.connect(workspace, uri, targetUri);
});
});
}
private static connect(workspace: FoamWorkspace, source: URI, target: URI) {
const connection = {
source: source,
target: target,
};
workspace.links[source.path] = workspace.links[source.path] ?? [];
workspace.links[source.path].push(connection);
workspace.backlinks[target.path] = workspace.backlinks[target.path] ?? [];
workspace.backlinks[target.path].push(connection);
return workspace;
}
private static disconnect(
workspace: FoamWorkspace,
source: URI,
target: URI
) {
workspace.links[source.path] = workspace.links[source.path]?.filter(
c => c.source.path === source.path && c.target.path === target.path
);
workspace.backlinks[target.path] = workspace.backlinks[target.path]?.filter(
c => c.source.path === source.path && c.target.path === target.path
);
return workspace;
}
}

View File

@@ -1,175 +0,0 @@
import { Graph } from 'graphlib';
import { URI } from './common/uri';
import { 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.path;
}
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,
};
};

View File

@@ -2,8 +2,7 @@ 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';
@@ -12,7 +11,6 @@ import { URI } from '../common/uri';
export interface FoamPlugin {
name: string;
description?: string;
graphMiddleware?: Middleware;
parser?: ParserPlugin;
}

View File

@@ -105,7 +105,7 @@ export class FileDataStore implements IDataStore, IDisposable {
if (isSome(watcher)) {
this._disposables.push(
watcher.onDidCreate(async uri => {
watcher.onDidCreate(uri => {
if (this.isMatch(uri)) {
Logger.info(`Created: ${uri.path}`);
this.onDidCreateEmitter.fire(uri);

View File

@@ -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';
import { URI } from './common/uri';
export { Position, Point };
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: URI, text: string) => Note;
}

View File

@@ -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;
}

View File

@@ -1,6 +1,5 @@
import { posix } from 'path';
import GithubSlugger from 'github-slugger';
import { ID } from '../types';
import { hash } from './core';
import { URI } from '../common/uri';
@@ -12,7 +11,7 @@ export const nameToSlug = (noteName: string): string => {
return GithubSlugger.slug(noteName);
};
export const hashURI = (uri: URI): ID => {
export const hashURI = (uri: URI): string => {
return hash(posix.normalize(uri.path));
};
@@ -36,3 +35,36 @@ export const computeRelativeURI = (
path: posix.join(posix.dirname(reference.path), slug),
});
};
/**
* Parses a URI from value, taking into consideration possible relative paths.
*
* @param reference the URI to use as reference in case value is a relative path
* @param value the value to parse for a URI
* @returns the URI from the given value. In case of a relative path, the URI will take into account
* the reference from which it is computed
*/
export const parseUri = (reference: URI, value: string): URI => {
let uri = URI.parse(value);
if (uri.scheme === 'file' && !value.startsWith('/')) {
const [path, fragment] = value.split('#');
uri = path.length > 0 ? computeRelativeURI(reference, path) : reference;
if (fragment) {
uri = uri.with({
fragment: fragment,
});
}
}
return uri;
};
export const placeholderUri = (key: string): URI => {
return URI.from({
scheme: 'placeholder',
path: key,
});
};
export const isPlaceholder = (uri: URI): boolean => {
return uri.scheme === 'placeholder';
};

View File

@@ -1,5 +1,4 @@
import { NoteGraph, createGraph } from '../src/note-graph';
import { NoteLinkDefinition, Note } from '../src/types';
import { NoteLinkDefinition, Note, Attachment } from '../src/model/note';
import { uriToSlug } from '../src/utils';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
@@ -15,324 +14,60 @@ 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;
}): Note => {
return {
uri: strToUri(params.uri),
type: 'note',
properties: {},
title: params.title ?? null,
slug: uriToSlug(URI.file(params.uri)),
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 =>
'slug' in link
? {
type: 'wikilink',
slug: link.slug,
target: link.slug,
position: position,
text: 'link text',
}
: {
type: 'link',
target: link.to,
label: 'link text',
}
)
: [],
source: {
eol: eol,
end: documentEnd,
contentStart: documentStart,
uri: URI.file(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', () => {});
});

View File

@@ -1,17 +1,22 @@
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 { Services, Note } from '../../src';
import { URI } from '../../src/common/uri';
import { FileDataStore } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
import { FoamWorkspace } from '../../src/model/workspace';
import { getBasename } from '../../src/utils';
Logger.setLevel('error');
describe('generateHeadings', () => {
let _graph: NoteGraphAPI;
let _workspace: FoamWorkspace;
const findBySlug = (slug: string): Note => {
return _workspace.list().find(res => getBasename(res.uri) === slug) as Note;
};
beforeAll(async () => {
const config = createConfigFromFolders([
URI.file(path.join(__dirname, '..', '__scaffold__')),
@@ -20,11 +25,11 @@ describe('generateHeadings', () => {
dataStore: new FileDataStore(config),
};
const foam = await bootstrap(config, services);
_graph = foam.notes;
_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
@@ -51,12 +56,12 @@ 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',

View File

@@ -1,17 +1,22 @@
import * as path from 'path';
import { NoteGraphAPI, GraphNote } from '../../src/note-graph';
import { generateLinkReferences } from '../../src/janitor';
import { bootstrap } from '../../src/bootstrap';
import { createConfigFromFolders } from '../../src/config';
import { Services } from '../../src';
import { Services, Note } from '../../src';
import { FileDataStore } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
import { URI } from '../../src/common/uri';
import { FoamWorkspace } from '../../src/model/workspace';
import { Resource } from '../../src/model/note';
import { getBasename } from '../../src/utils';
Logger.setLevel('error');
describe('generateLinkReferences', () => {
let _graph: NoteGraphAPI;
let _workspace: FoamWorkspace;
const findBySlug = (slug: string): Note => {
return _workspace.list().find(res => getBasename(res.uri) === slug) as Note;
};
beforeAll(async () => {
const config = createConfigFromFolders([
@@ -20,15 +25,15 @@ describe('generateLinkReferences', () => {
const services: Services = {
dataStore: new FileDataStore(config),
};
_graph = await bootstrap(config, services).then(foam => foam.notes);
_workspace = await bootstrap(config, services).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: textForNote(
note,
@@ -53,7 +58,7 @@ describe('generateLinkReferences', () => {
},
};
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);
@@ -61,7 +66,7 @@ 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: '',
@@ -79,7 +84,7 @@ describe('generateLinkReferences', () => {
},
};
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);
@@ -87,7 +92,7 @@ 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: textForNote(
@@ -110,7 +115,7 @@ describe('generateLinkReferences', () => {
},
};
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);
@@ -118,11 +123,11 @@ 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);
});
@@ -136,7 +141,7 @@ describe('generateLinkReferences', () => {
* @param note the note we are adjusting for
* @param text starting text, using a \n line separator
*/
function textForNote(note: GraphNote, text: string): string {
function textForNote(note: Note, text: string): string {
return text.split('\n').join(note.source.eol);
}
@@ -149,7 +154,7 @@ function textForNote(note: GraphNote, text: string): string {
* @param pos starting position
*/
function pointForNote(
note: GraphNote,
note: Note,
pos: { line: number; column: number; offset: number }
) {
const rows = pos.line - 1;

View File

@@ -2,10 +2,12 @@ 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 { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
import { uriToSlug } from '../src/utils';
import { FoamWorkspace } from '../src/model/workspace';
Logger.setLevel('error');
@@ -41,68 +43,119 @@ const createNoteFromMarkdown = (path: string, content: string) =>
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)).toEqual([noteA.uri]);
expect(workspace.getLinks(noteA.uri)).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
@@ -110,11 +163,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)', () => {
@@ -132,58 +183,40 @@ 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 workspace = new FoamWorkspace();
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
@@ -191,51 +224,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',
@@ -298,7 +326,7 @@ 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(
URI.file('/path/to/a'),
`

View File

@@ -1,7 +1,6 @@
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/common/uri';
@@ -48,15 +47,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;

View File

@@ -4,6 +4,7 @@ import {
hashURI,
computeRelativeURI,
extractHashtags,
parseUri,
} from '../src/utils';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
@@ -50,6 +51,27 @@ describe('URI utils', () => {
computeRelativeURI(URI.file('/my/file.markdown'), '../hello')
).toEqual(URI.file('/hello.markdown'));
});
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', base.with({ fragment: 'section' })],
[
'../relative/file.md#section',
URI.parse('file:/path/relative/file.md#section'),
],
])('URI Parsing (%s) - %s', (input, exp) => {
const result = parseUri(base, input);
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);
});
});
});
describe('hashtag extraction', () => {

View File

@@ -0,0 +1,425 @@
import { FoamWorkspace, getReferenceType } from '../src/model/workspace';
import { Logger } from '../src/utils/log';
import { createTestNote, createAttachment } from './core.test';
import { URI } from '../src/common/uri';
import { placeholderUri } from '../src/utils';
Logger.setLevel('error');
describe('Reference types', () => {
it('Detects absolute references', () => {
expect(getReferenceType('/hello')).toEqual('absolute-path');
expect(getReferenceType('/hello/there')).toEqual('absolute-path');
});
it('Detects relative references', () => {
expect(getReferenceType('../hello')).toEqual('relative-path');
expect(getReferenceType('./hello')).toEqual('relative-path');
expect(getReferenceType('./hello/there')).toEqual('relative-path');
});
it('Detects key references', () => {
expect(getReferenceType('hello')).toEqual('key');
});
it('Detects URIs', () => {
expect(getReferenceType(URI.file('/path/to/file.md'))).toEqual('uri');
});
});
describe('Notes workspace', () => {
it('Adds notes to workspace', () => {
const ws = new FoamWorkspace();
ws.set(createTestNote({ uri: '/page-a.md' }));
ws.set(createTestNote({ uri: '/page-b.md' }));
ws.set(createTestNote({ uri: '/page-c.md' }));
expect(
ws
.list()
.map(n => n.uri.path)
.sort()
).toEqual(['/page-a.md', '/page-b.md', '/page-c.md']);
});
it('Listing resources includes notes, attachments and placeholders', () => {
const ws = new FoamWorkspace();
ws.set(createTestNote({ uri: '/page-a.md' }));
ws.set(createAttachment({ uri: '/file.pdf' }));
ws.set({ type: 'placeholder', uri: placeholderUri('place-holder') });
expect(
ws
.list()
.map(n => n.uri.path)
.sort()
).toEqual(['/file.pdf', '/page-a.md', 'place-holder']);
});
it('Detects outbound wikilinks', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [
// wikilink
{ slug: 'page-b' },
// relative path wikilink
{ slug: '../another/page-c.md' },
// absolute path wikilink
{ slug: '/absolute/path/page-d' },
// wikilink with extension
{ slug: 'page-e.md' },
// wikilink to placeholder
{ slug: 'placeholder-test' },
],
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(createTestNote({ uri: '/somewhere/page-b.md' }))
.set(createTestNote({ uri: '/path/another/page-c.md' }))
.set(createTestNote({ uri: '/absolute/path/page-d.md' }))
.set(createTestNote({ uri: '/absolute/path/page-e.md' }))
.resolveLinks();
expect(
ws
.getLinks(noteA.uri)
.map(link => link.path)
.sort()
).toEqual([
'/absolute/path/page-d.md',
'/absolute/path/page-e.md',
'/path/another/page-c.md',
'/somewhere/page-b.md',
'placeholder-test',
]);
});
it('Detects inbound wikilinks', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(
createTestNote({
uri: '/somewhere/page-b.md',
links: [{ slug: 'page-a' }],
})
)
.set(
createTestNote({
uri: '/path/another/page-c.md',
links: [{ slug: '/path/to/page-a' }],
})
)
.set(
createTestNote({
uri: '/absolute/path/page-d.md',
links: [{ slug: '../to/page-a.md' }],
})
)
.resolveLinks();
expect(
ws
.getBacklinks(noteA.uri)
.map(link => link.path)
.sort()
).toEqual(['/path/another/page-c.md', '/somewhere/page-b.md']);
});
it('Detects markdown links', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: './another/page-b.md' }, { to: 'more/page-c.md' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
links: [{ to: '../../to/page-a.md' }],
});
const noteC = createTestNote({
uri: '/path/to/more/page-c.md',
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC)
.resolveLinks();
expect(
ws
.getLinks(noteA.uri)
.map(link => link.path)
.sort()
).toEqual(['/path/to/another/page-b.md', '/path/to/more/page-c.md']);
expect(ws.getLinks(noteB.uri)).toEqual([noteA.uri]);
expect(ws.getBacklinks(noteA.uri)).toEqual([noteB.uri]);
expect(ws.getConnections(noteA.uri)).toEqual([
{ source: noteA.uri, target: noteB.uri },
{ source: noteA.uri, target: noteC.uri },
{ source: noteB.uri, target: noteA.uri },
]);
});
it('Resolves wikilink referencing more than one note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB1 = createTestNote({ uri: '/path/to/another/page-b.md' });
const noteB2 = createTestNote({ uri: '/path/to/more/page-b.md' });
const ws = new FoamWorkspace();
ws.set(noteA)
.set(noteB1)
.set(noteB2)
.resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([noteB1.uri]);
});
it('Resolves path wikilink in case of name conflict', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: './more/page-b' }, { slug: 'yet/page-b' }],
});
const noteB1 = createTestNote({ uri: '/path/to/another/page-b.md' });
const noteB2 = createTestNote({ uri: '/path/to/more/page-b.md' });
const noteB3 = createTestNote({ uri: '/path/to/yet/page-b.md' });
const ws = new FoamWorkspace();
ws.set(noteA)
.set(noteB1)
.set(noteB2)
.set(noteB3)
.resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([noteB2.uri, noteB3.uri]);
});
it('Fails if getting non-existing note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
});
const ws = new FoamWorkspace();
ws.set(noteA);
const uri = URI.file('/path/to/another/page-b.md');
expect(ws.exists(uri)).toBeFalsy();
expect(ws.find(uri)).toBeNull();
expect(() => ws.get(uri)).toThrow();
});
it('Supports attachments', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [
// wikilink with extension
{ slug: 'attachment-a.pdf' },
// wikilink without extension
{ slug: 'attachment-b' },
],
});
const attachmentA = createAttachment({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentB = createAttachment({
uri: '/path/to/more/attachment-b.pdf',
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentB)
.resolveLinks();
expect(ws.getBacklinks(attachmentA.uri)).toEqual([noteA.uri]);
expect(ws.getBacklinks(attachmentB.uri)).toEqual([noteA.uri]);
});
it('Resolves conflicts alphabetically - part 1', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'attachment-a' }],
});
const attachmentA = createAttachment({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentABis = createAttachment({
uri: '/path/to/attachment-a.pdf',
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(attachmentA)
.set(attachmentABis)
.resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([attachmentABis.uri]);
});
it('Resolves conflicts alphabetically - part 2', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'attachment-a' }],
});
const attachmentA = createAttachment({
uri: '/path/to/more/attachment-a.pdf',
});
const attachmentABis = createAttachment({
uri: '/path/to/attachment-a.pdf',
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(attachmentABis)
.set(attachmentA)
.resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([attachmentABis.uri]);
});
});
describe('Updating workspace happy path', () => {
it('Update links when modifying note', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
links: [{ slug: 'page-c' }],
});
const noteC = createTestNote({
uri: '/path/to/more/page-c.md',
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(noteB)
.set(noteC)
.resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
expect(ws.getBacklinks(noteC.uri)).toEqual([noteB.uri]);
// update the note
const noteABis = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-c' }],
});
ws.set(noteABis);
// change is not propagated immediately
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
expect(ws.getBacklinks(noteC.uri)).toEqual([noteB.uri]);
// recompute the links
ws.resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([noteC.uri]);
expect(ws.getBacklinks(noteB.uri)).toEqual([]);
expect(
ws
.getBacklinks(noteC.uri)
.map(link => link.path)
.sort()
).toEqual(['/path/to/another/page-b.md', '/path/to/page-a.md']);
});
it('Removing target note should produce placeholder for wikilinks', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(noteB)
.resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
ws.resolveLinks();
expect(() => ws.get(noteB.uri)).toThrow();
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
});
it('Adding note should replace placeholder for wikilinks', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ slug: 'page-b' }],
});
const ws = new FoamWorkspace();
ws.set(noteA).resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([placeholderUri('page-b')]);
expect(ws.get(placeholderUri('page-b')).type).toEqual('placeholder');
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
ws.resolveLinks();
expect(() => ws.get(placeholderUri('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
});
it('Removing target note should produce placeholder for direct links', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
const ws = new FoamWorkspace();
ws.set(noteA)
.set(noteB)
.resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([noteB.uri]);
expect(ws.getBacklinks(noteB.uri)).toEqual([noteA.uri]);
expect(ws.get(noteB.uri).type).toEqual('note');
// remove note-b
ws.delete(noteB.uri);
ws.resolveLinks();
expect(() => ws.get(noteB.uri)).toThrow();
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
'placeholder'
);
});
it('Adding note should replace placeholder for direct links', () => {
const noteA = createTestNote({
uri: '/path/to/page-a.md',
links: [{ to: '/path/to/another/page-b.md' }],
});
const ws = new FoamWorkspace();
ws.set(noteA).resolveLinks();
expect(ws.getLinks(noteA.uri)).toEqual([
placeholderUri('/path/to/another/page-b.md'),
]);
expect(ws.get(placeholderUri('/path/to/another/page-b.md')).type).toEqual(
'placeholder'
);
// add note-b
const noteB = createTestNote({
uri: '/path/to/another/page-b.md',
});
ws.set(noteB);
ws.resolveLinks();
expect(() => ws.get(placeholderUri('page-b'))).toThrow();
expect(ws.get(noteB.uri).type).toEqual('note');
});
});

View File

@@ -1,3 +0,0 @@
out
.vscode-test/
*.vsix

View File

@@ -1,3 +0,0 @@
{
"tabWidth": 2
}

View File

@@ -4,30 +4,71 @@ All notable changes to the "foam-vscode" extension will be documented in this fi
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [0.10.0] - 2021-02-18
Features:
- Notes preview in panels (#468 - thanks @leonhfr)
- Added more style options to graph setting (lineColor, lineWidth, particleWidth (#479 - thanks @nitwit-se)
Internal:
- Refactored data model representation of notes graph: `FoamWorkspace` (#467)
## [0.9.1] - 2021-01-28
Fixes and Improvements:
- Panel: Updating orphan panel when adding and removing notes (#464 - thanks @leonhfr)
## [0.9.0] - 2021-01-27
Features:
- Panel: Added orphan panel (#457 - thanks @leonhfr)
## [0.8.0] - 2021-01-15
Features:
- Model: Now direct links are included in the Foam model (#433)
- Commaands: Added `Open random note` command (#440 - thanks @MCluck90)
- Dataviz: Added graph style override from VsCode theme (#438 - thanks @jmg-duarte)
- Dataviz: Added graph style customization based on note type (#449)
Fixes and Improvements:
- Various improvements and fixes in documentation (thanks @anglinb, @themaxdavitt, @elswork)
## [0.7.7] - 2020-12-31
Fixes and Improvements:
- Fixed word-based-suggestions (#415 #417 - thanks @bpugh!)
- Date snippets use standard wikilink syntax (#416 - thanks @MCluck90!)
## [0.7.6] - 2020-12-20
Fixes and Improvements:
- Fixed "Janitor" command issue in Windows (#410)
- Fixed "Janitor" command issue in Windows (#410)
## [0.7.5] - 2020-12-17
Fixes and Improvements:
- Fixed "Open Daily Note" command issue in Windows (#407)
- Fixed "Open Daily Note" command issue in Windows (#407)
## [0.7.4] - 2020-12-16
Fixes and Improvements:
- Fixed a bug that was causing Foam to not work correctly in Windows (#391)
## [0.7.3] - 2020-12-13
Fixes and Improvements:
- Foam model: fix to link references on node update/deletion (#393 - thanks @AndrewNatoli)
- Dataviz: fix hover/selection (#401)
- Dataviz: improved logging
@@ -36,15 +77,18 @@ Fixes and Improvements:
## [0.7.2] - 2020-11-27
Fixes and Improvements:
- Dataviz: Sync note deletion
- Foam model: Fix to wikilink format (#386 - thanks @SanketDG)
## [0.7.1] - 2020-11-27
New Feature:
- Foam logging can now be inspected in VsCode Output panel (#377)
Fixes and Improvements:
- Foam model: Fixed bug in tags parsing (#382)
- Dataviz: Graph canvas now resizes with window (#383, #375)
- Dataviz: Limit label length for placeholder nodes (#381)
@@ -52,19 +96,23 @@ Fixes and Improvements:
## [0.7.0] - 2020-11-25
New Features:
- Foam stays in sync with changes in notes
- Dataviz: Added multiple selection in graph (shift+click on node)
Fixes and Improvements:
- Dataviz: Graph uses VSCode theme colors
- Reporting: Errors occurring during foam bootstrap are now reported for easier debugging
## [0.6.0] - 2020-11-19
New features:
- Added command to create notes from templates (#115 - Thanks @ingalless)
Fixes and Improvements:
- Foam model: Fixed bug that prevented wikilinks from being slugified (#323 - thanks @SanketDG)
- Editor: Improvements in defaults for ignored files setting (thanks @jmg-duarte)
- Dataviz: Centering of the graph on note displayed in active editor (#319)
@@ -75,9 +123,11 @@ Fixes and Improvements:
## [0.5.0] - 2020-11-09
New features:
- Added tags panel (#311)
Fixes and Improvements:
- Date snippets now support configurable completion actions (#307 - thanks @ingalless)
- Graph now show note titles when zooming in (#310)
- New `foam.files.ignore` setting to exclude globs from being processed by Foam (#304 - thanks @jmg-duarte)
@@ -106,7 +156,6 @@ New experimental features:
- Introduced [foam local plugins](https://foambubble.github.io/foam/foam-local-plugins)
## [0.3.1] - 2020-07-26
Fixes and improvements:

View File

@@ -6,15 +6,17 @@
[![Ratings](https://img.shields.io/visual-studio-marketplace/r/foam.foam-vscode)](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
This is the VS Code extension for [Foam](https://foambubble.github.io/foam).
> the extension doesn't do much on it's own. To learn how to use it, read [Foam documentation](https://foambubble.github.io/foam) and the [Getting started](https://foambubble.github.io/foam/#getting-started) guide.
> ⚠️ This is an early stage software. Use at your own peril.
> You can join the Foam Community on the [Foam Discord](https://discord.gg/EKFyMrVNZ9)
> You can join the Foam Community on the [Foam Discord](https://foambubble.github.io/join-discord/e)
## Quick getting started
[Foam](https://foambubble.github.io/foam) is a note-taking tool that lives within VsCode, which means you can pair it with your favorite extensions for a great editing experience.
Foam is open source, and allows you to create a local first, markdown based, personal knowledge base. You can also use it to publish your notes.
Foam is also meant to be extensible, so you can integrate with its internals to customize your knowledge base.
## Getting started
You really, _really_, **really** should read [Foam documentation](https://foambubble.github.io/foam), but if you can't be bothered, this is how to get started:
@@ -24,13 +26,18 @@ You really, _really_, **really** should read [Foam documentation](https://foambu
This will also install `Foam for VSCode`, but if you already have it installed, that's ok, just make sure you're up to date on the latest version.
You really, _really_, **really** should read [Foam documentation](https://foambubble.github.io/foam), but if you can't be bothered, this is how to get started:
## Features
- Connect your notes using `[[wiki-links]]`
- Create markdown references for `[[wiki-links]]` with the `Foam: Update Markdown Reference List` command, to use your notes in a non-foam workspace
- See how your notes are connected via a graph with the `Foam: Show Graph` command
- Tag your notes and navigate them with the Tag Explorer
- Connect your notes using [`[[wiki-links]]`](https://foambubble.github.io/foam/features/backlinking)
- Create markdown [references](https://foambubble.github.io/foam/features/link-reference-definitions) for `[[wiki-links]]`, to use your notes in a non-foam workspace
- See how your notes are connected via a [graph](https://foambubble.github.io/foam/features/graph-visualisation) with the `Foam: Show Graph` command
- Tag your notes and navigate them with the [Tag Explorer](https://foambubble.github.io/foam/features/tags)
- Make your notes navigable both in GitHub UI as well as GitHub Pages
- Use [custom templates](https://foambubble.github.io/foam/features/note-templates) for your notes
- Create a journal with [daily notes](https://foambubble.github.io/foam/features/daily-notes)
- Explore your knowledge base with the `Foam: Open Random Note` command
## Requirements

View File

@@ -2,17 +2,16 @@
"name": "foam-vscode",
"displayName": "Foam for VSCode (Wikilinks to Markdown)",
"description": "Generate markdown reference lists from wikilinks in a workspace",
"author": "Jani Eväkallio",
"repository": {
"url": "https://github.com/foambubble/foam",
"type": "git"
},
"homepage": "https://github.com/foambubble/foam",
"version": "0.7.7",
"version": "0.10.0",
"license": "MIT",
"publisher": "foam",
"engines": {
"vscode": "^1.45.1"
"vscode": "^1.47.1"
},
"icon": "icon/FOAM_ICON_256.png",
"categories": [
@@ -23,6 +22,7 @@
"onView:foam-vscode.tags-explorer",
"onCommand:foam-vscode.update-wikilinks",
"onCommand:foam-vscode.open-daily-note",
"onCommand:foam-vscode.open-random-note",
"onCommand:foam-vscode.janitor",
"onCommand:foam-vscode.copy-without-brackets",
"onCommand:foam-vscode.show-graph"
@@ -36,6 +36,46 @@
"name": "Tag Explorer",
"icon": "media/dep.svg",
"contextualTitle": "Tags Explorer"
},
{
"id": "foam-vscode.orphans",
"name": "Orphans",
"icon": "media/dep.svg",
"contextualTitle": "Orphans"
}
]
},
"viewsWelcome": [
{
"view": "foam-vscode.tags-explorer",
"contents": "No tags found. Notes that contain tags will show up here. You may add tags to a note with a hashtag (#tag) or by adding a tag list to the front matter (tags: tag1, tag2)."
},
{
"view": "foam-vscode.orphans",
"contents": "No orphans found. Notes that have no backlinks nor links will show up here."
}
],
"menus": {
"view/title": [
{
"command": "foam-vscode.group-orphans-by-folder",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == false",
"group": "navigation"
},
{
"command": "foam-vscode.group-orphans-off",
"when": "view == foam-vscode.orphans && foam-vscode.orphans-grouped-by-folder == true",
"group": "navigation"
}
],
"commandPalette": [
{
"command": "foam-vscode.group-orphans-by-folder",
"when": "false"
},
{
"command": "foam-vscode.group-orphans-off",
"when": "false"
}
]
},
@@ -56,6 +96,10 @@
"command": "foam-vscode.open-daily-note",
"title": "Foam: Open Daily Note"
},
{
"command": "foam-vscode.open-random-note",
"title": "Foam: Open Random Note"
},
{
"command": "foam-vscode.janitor",
"title": "Foam: Run Janitor (Experimental)"
@@ -67,6 +111,16 @@
{
"command": "foam-vscode.create-note-from-template",
"title": "Foam: Create New Note From Template"
},
{
"command": "foam-vscode.group-orphans-by-folder",
"title": "Foam: Group Orphans By Folder",
"icon": "$(list-tree)"
},
{
"command": "foam-vscode.group-orphans-off",
"title": "Foam: Don't Group Orphans",
"icon": "$(list-flat)"
}
],
"configuration": {
@@ -137,6 +191,30 @@
"default": null,
"description": "The directory into which daily notes should be created. Defaults to the workspace root."
},
"foam.orphans.exclude": {
"type": [
"array"
],
"default": [],
"markdownDescription": "Specifies the list of glob patterns that will be excluded from the orphans report. To ignore the all the content of a given folder, use `**<folderName>/**/*`",
"scope": "resource"
},
"foam.orphans.groupBy": {
"type": [
"string"
],
"enum": [
"off",
"folder"
],
"enumDescriptions": [
"Disable grouping",
"Group by folder"
],
"default": "folder",
"markdownDescription": "Group orphans report entries by.",
"scope": "resource"
},
"foam.dateSnippets.afterCompletion": {
"type": "string",
"default": "createNote",
@@ -156,6 +234,11 @@
"type": "number",
"default": 24,
"description": "The maximum title length before being abbreviated. Set to 0 or less to disable."
},
"foam.graph.style": {
"type": "object",
"description": "Custom graph styling settings. An example is present in the documentation.",
"default": {}
}
}
},
@@ -168,8 +251,9 @@
},
"scripts": {
"build": "tsc -p ./",
"test": "jest",
"lint": "eslint src --ext ts",
"pretest": "yarn build",
"test": "node ./out/test/run-tests.js",
"lint": "tsdx lint",
"clean": "rimraf out",
"watch": "tsc --build ./tsconfig.json ../foam-core/tsconfig.json --watch",
"vscode:start-debugging": "yarn clean && yarn watch",
@@ -189,19 +273,26 @@
"@types/dateformat": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/node": "^13.11.0",
"@types/vscode": "^1.45.1",
"@types/remove-markdown": "^0.1.1",
"@types/vscode": "^1.47.1",
"@typescript-eslint/eslint-plugin": "^2.30.0",
"@typescript-eslint/parser": "^2.30.0",
"babel-jest": "^26.2.2",
"eslint": "^6.8.0",
"glob": "^7.1.6",
"jest": "^26.2.2",
"jest-environment-vscode": "^1.0.0",
"jest-extended": "^0.11.5",
"rimraf": "^3.0.2",
"ts-jest": "^26.4.4",
"typescript": "^3.8.3",
"vscode-test": "^1.3.0"
},
"dependencies": {
"dateformat": "^3.0.3",
"foam-core": "^0.7.4"
"foam-core": "^0.10.0",
"gray-matter": "^4.0.2",
"micromatch": "^4.0.2",
"remove-markdown": "^0.3.0"
}
}

View File

@@ -1,17 +1,11 @@
import {
Selection,
Uri,
window,
workspace,
WorkspaceConfiguration
} from "vscode";
import { dirname, join } from "path";
import dateFormat from "dateformat";
import * as fs from "fs";
import { docConfig, focusNote, pathExists } from "./utils";
import { workspace, WorkspaceConfiguration } from 'vscode';
import { dirname, join } from 'path';
import dateFormat from 'dateformat';
import * as fs from 'fs';
import { docConfig, focusNote, pathExists } from './utils';
async function openDailyNoteFor(date?: Date) {
const foamConfiguration = workspace.getConfiguration("foam");
const foamConfiguration = workspace.getConfiguration('foam');
const currentDate = date !== undefined ? date : new Date();
const dailyNotePath = getDailyNotePath(foamConfiguration, currentDate);
@@ -26,7 +20,7 @@ async function openDailyNoteFor(date?: Date) {
function getDailyNotePath(configuration: WorkspaceConfiguration, date: Date) {
const rootDirectory = workspace.workspaceFolders[0].uri.fsPath;
const dailyNoteDirectory: string =
configuration.get("openDailyNote.directory") ?? ".";
configuration.get('openDailyNote.directory') ?? '.';
const dailyNoteFilename = getDailyNoteFileName(configuration, date);
return join(rootDirectory, dailyNoteDirectory, dailyNoteFilename);
@@ -37,10 +31,10 @@ function getDailyNoteFileName(
date: Date
): string {
const filenameFormat: string = configuration.get(
"openDailyNote.filenameFormat"
'openDailyNote.filenameFormat'
);
const fileExtension: string = configuration.get(
"openDailyNote.fileExtension"
'openDailyNote.fileExtension'
);
return `${dateFormat(date, filenameFormat, false)}.${fileExtension}`;
@@ -58,8 +52,8 @@ async function createDailyNoteIfNotExists(
await createDailyNoteDirectoryIfNotExists(dailyNotePath);
const titleFormat: string =
configuration.get("openDailyNote.titleFormat") ??
configuration.get("openDailyNote.filenameFormat");
configuration.get('openDailyNote.titleFormat') ??
configuration.get('openDailyNote.filenameFormat');
await fs.promises.writeFile(
dailyNotePath,
@@ -83,5 +77,5 @@ export {
openDailyNoteFor,
getDailyNoteFileName,
createDailyNoteIfNotExists,
getDailyNotePath
getDailyNotePath,
};

View File

@@ -1,19 +1,16 @@
"use strict";
import { workspace, ExtensionContext, window, Uri } from "vscode";
import { workspace, ExtensionContext, window } from 'vscode';
import {
bootstrap,
FoamConfig,
Foam,
Services,
Logger,
FileDataStore
} from "foam-core";
FileDataStore,
} from 'foam-core';
import { features } from "./features";
import { getConfigFromVscode } from "./services/config";
import { VsCodeOutputLogger, exposeLogger } from "./services/logging";
import { VsCodeDataStore } from "./services/datastore";
import { features } from './features';
import { getConfigFromVscode } from './services/config';
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
export async function activate(context: ExtensionContext) {
const logger = new VsCodeOutputLogger();
@@ -21,14 +18,14 @@ export async function activate(context: ExtensionContext) {
exposeLogger(context, logger);
try {
Logger.info("Starting Foam");
Logger.info('Starting Foam');
const config: FoamConfig = getConfigFromVscode();
const watcher = workspace.createFileSystemWatcher("**/*");
const watcher = workspace.createFileSystemWatcher('**/*');
const dataStore = new FileDataStore(config, watcher);
const services: Services = {
dataStore: dataStore
dataStore: dataStore,
};
const foamPromise: Promise<Foam> = bootstrap(config, services);
@@ -37,11 +34,11 @@ export async function activate(context: ExtensionContext) {
});
const foam = await foamPromise;
Logger.info(`Loaded ${foam.notes.getNotes().length} notes`);
Logger.info(`Loaded ${foam.workspace.list().length} notes`);
context.subscriptions.push(dataStore, foam, watcher);
} catch (e) {
Logger.error("An error occurred while bootstrapping Foam", e);
Logger.error('An error occurred while bootstrapping Foam', e);
window.showErrorMessage(
`An error occurred while bootstrapping Foam. ${e.stack}`
);

View File

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

View File

@@ -1,21 +1,19 @@
import {
window,
env,
ExtensionContext,
commands,
} from "vscode";
import { FoamFeature } from "../types";
import { removeBrackets } from "../utils";
import { window, env, ExtensionContext, commands } from 'vscode';
import { FoamFeature } from '../types';
import { removeBrackets } from '../utils';
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.copy-without-brackets", copyWithoutBrackets)
commands.registerCommand(
'foam-vscode.copy-without-brackets',
copyWithoutBrackets
)
);
},
};
async function copyWithoutBrackets () {
async function copyWithoutBrackets() {
// Get the active text editor
const editor = window.activeTextEditor;

View File

@@ -4,17 +4,17 @@ import {
ExtensionContext,
workspace,
Uri,
SnippetString
} from "vscode";
import * as path from "path";
import { FoamFeature } from "../types";
import { TextEncoder } from "util";
import { focusNote } from "../utils";
SnippetString,
} from 'vscode';
import * as path from 'path';
import { FoamFeature } from '../types';
import { TextEncoder } from 'util';
import { focusNote } from '../utils';
const templatesDir = `${workspace.workspaceFolders[0].uri.path}/.foam/templates`;
async function getTemplates(): Promise<string[]> {
const templates = await workspace.findFiles(".foam/templates/**.md");
const templates = await workspace.findFiles('.foam/templates/**.md');
// parse title, not whole file!
return templates.map(template => path.basename(template.path));
}
@@ -23,7 +23,7 @@ const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand(
"foam-vscode.create-note-from-template",
'foam-vscode.create-note-from-template',
async () => {
const templates = await getTemplates();
const activeFile = window.activeTextEditor?.document?.fileName;
@@ -34,14 +34,14 @@ const feature: FoamFeature = {
const selectedTemplate = await window.showQuickPick(templates);
const folder = await window.showInputBox({
prompt: `Where should the template be created?`,
value: currentDir
value: currentDir,
});
let filename = await window.showInputBox({
prompt: `Enter the filename for the new note`,
value: ``,
validateInput: value =>
value.length ? undefined : "Please enter a value!"
value.length ? undefined : 'Please enter a value!',
});
filename = path.extname(filename).length
? filename
@@ -54,14 +54,14 @@ const feature: FoamFeature = {
const snippet = new SnippetString(templateText.toString());
await workspace.fs.writeFile(
Uri.file(targetFile),
new TextEncoder().encode("")
new TextEncoder().encode('')
);
await focusNote(targetFile, true);
await window.activeTextEditor.insertSnippet(snippet);
}
)
);
}
},
};
export default feature;

View File

@@ -1,104 +1,116 @@
import * as vscode from "vscode";
import * as path from "path";
import { FoamFeature } from "../types";
import { Foam, Logger } from "foam-core";
import { TextDecoder } from "util";
import { getTitleMaxLength } from "../settings";
import { isSome } from "../utils";
import * as vscode from 'vscode';
import * as path from 'path';
import { FoamFeature } from '../types';
import { Foam, Logger, FoamWorkspace } from 'foam-core';
import { TextDecoder } from 'util';
import { getGraphStyle, getTitleMaxLength } from '../settings';
import { isSome } from '../utils';
const feature: FoamFeature = {
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
vscode.commands.registerCommand("foam-vscode.show-graph", async () => {
const foam = await foamPromise;
const panel = await createGraphPanel(foam, context);
const onFoamChanged = _ => {
updateGraph(panel, foam);
};
const noteAddedListener = foam.notes.onDidAddNote(onFoamChanged);
const noteUpdatedListener = foam.notes.onDidUpdateNote(onFoamChanged);
const noteDeletedListener = foam.notes.onDidDeleteNote(onFoamChanged);
panel.onDidDispose(() => {
noteAddedListener.dispose();
noteUpdatedListener.dispose();
noteDeletedListener.dispose();
});
vscode.window.onDidChangeActiveTextEditor(e => {
if (e.document.uri.scheme === "file") {
const note = foam.notes.getNoteByURI(e.document.uri);
if (isSome(note)) {
panel.webview.postMessage({
type: "didSelectNote",
payload: note.id
});
}
}
});
let panel: vscode.WebviewPanel | undefined = undefined;
vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('foam.graph.style')) {
const style = getGraphStyle();
panel.webview.postMessage({
type: 'didUpdateStyle',
payload: style,
});
}
});
}
vscode.commands.registerCommand('foam-vscode.show-graph', async () => {
if (panel) {
const columnToShowIn = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
panel.reveal(columnToShowIn);
} else {
const foam = await foamPromise;
panel = await createGraphPanel(foam, context);
const onFoamChanged = _ => {
updateGraph(panel, foam);
};
const noteAddedListener = foam.workspace.onDidAdd(onFoamChanged);
const noteUpdatedListener = foam.workspace.onDidUpdate(onFoamChanged);
const noteDeletedListener = foam.workspace.onDidDelete(onFoamChanged);
panel.onDidDispose(() => {
noteAddedListener.dispose();
noteUpdatedListener.dispose();
noteDeletedListener.dispose();
panel = undefined;
});
vscode.window.onDidChangeActiveTextEditor(e => {
if (e.document.uri.scheme === 'file') {
const note = foam.workspace.get(e.document.uri);
if (isSome(note)) {
panel.webview.postMessage({
type: 'didSelectNote',
payload: note.uri.path,
});
}
}
});
}
});
},
};
function updateGraph(panel: vscode.WebviewPanel, foam: Foam) {
const graph = generateGraphData(foam);
panel.webview.postMessage({
type: "didUpdateGraphData",
payload: graph
type: 'didUpdateGraphData',
payload: graph,
});
}
function generateGraphData(foam: Foam) {
const graph = {
nodes: {},
edges: new Set()
edges: new Set(),
};
foam.notes.getNotes().forEach(n => {
const links = foam.notes.getForwardLinks(n.id);
graph.nodes[n.id] = {
id: n.id,
type: "note",
uri: n.source.uri,
title: cutTitle(n.title)
foam.workspace.list().forEach(n => {
const type = n.type === 'note' ? n.properties.type ?? 'note' : n.type;
const title = n.type === 'note' ? n.title : path.basename(n.uri.path);
graph.nodes[n.uri.path] = {
id: n.uri.path,
type: type,
uri: n.uri,
title: cutTitle(title),
};
links.forEach(link => {
if (!(link.to in graph.nodes)) {
graph.nodes[link.to] = {
id: link.to,
type: "nonExistingNote",
uri: `virtual:${link.to}`,
title: cutTitle(link.link.slug)
};
}
graph.edges.add({
source: link.from,
target: link.to
});
});
foam.workspace.getAllConnections().forEach(c => {
graph.edges.add({
source: c.source.path,
target: c.target.path,
});
});
return {
nodes: graph.nodes,
links: Array.from(graph.edges)
links: Array.from(graph.edges),
};
}
function cutTitle(title: string): string {
const maxLen = getTitleMaxLength();
if (maxLen > 0 && title.length > maxLen) {
return title.substring(0, maxLen).concat("...");
return title.substring(0, maxLen).concat('...');
}
return title;
}
async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
const panel = vscode.window.createWebviewPanel(
"foam-graph",
"Foam Graph",
'foam-graph',
'Foam Graph',
vscode.ViewColumn.Two,
{
enableScripts: true,
retainContextWhenHidden: true
retainContextWhenHidden: true,
}
);
@@ -107,22 +119,29 @@ async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
panel.webview.onDidReceiveMessage(
async message => {
switch (message.type) {
case "webviewDidLoad":
case 'webviewDidLoad':
const styles = getGraphStyle();
panel.webview.postMessage({
type: 'didUpdateStyle',
payload: styles,
});
updateGraph(panel, foam);
break;
case "webviewDidSelectNode":
const noteId = message.payload;
const selectedNote = foam.notes.getNote(noteId);
case 'webviewDidSelectNode':
const noteUri = vscode.Uri.parse(message.payload);
const selectedNote = foam.workspace.get(noteUri);
const doc = await vscode.workspace.openTextDocument(
selectedNote.source.uri.path // vscode doesn't recognize the URI directly
);
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
if (isSome(selectedNote)) {
const doc = await vscode.workspace.openTextDocument(
selectedNote.uri.path // vscode doesn't recognize the URI directly
);
vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
}
break;
case "error":
Logger.error("An error occurred in the graph view", message.payload);
case 'error':
Logger.error('An error occurred in the graph view', message.payload);
break;
}
},
@@ -138,27 +157,27 @@ async function getWebviewContent(
panel: vscode.WebviewPanel
) {
const webviewPath = vscode.Uri.file(
path.join(context.extensionPath, "static", "dataviz.html")
path.join(context.extensionPath, 'static', 'dataviz.html')
);
const file = await vscode.workspace.fs.readFile(webviewPath);
const text = new TextDecoder("utf-8").decode(file);
const text = new TextDecoder('utf-8').decode(file);
const webviewUri = (fileName: string) =>
panel.webview
.asWebviewUri(
vscode.Uri.file(path.join(context.extensionPath, "static", fileName))
vscode.Uri.file(path.join(context.extensionPath, 'static', fileName))
)
.toString();
const graphDirectory = path.join("graphs", "default");
const graphDirectory = path.join('graphs', 'default');
const textWithVariables = text
.replace(
"${graphPath}",
"{{" + path.join(graphDirectory, "graph.js") + "}}"
'${graphPath}', // eslint-disable-line
'{{' + path.join(graphDirectory, 'graph.js') + '}}'
)
.replace(
"${graphStylesPath}",
"{{" + path.join(graphDirectory, "graph.css") + "}}"
'${graphStylesPath}', // eslint-disable-line
'{{' + path.join(graphDirectory, 'graph.css') + '}}'
);
// Basic templating. Will replace the script paths with the

View File

@@ -1,20 +1,24 @@
import createReferences from "./wikilink-reference-generation";
import openDailyNote from "./open-daily-note";
import janitor from "./janitor";
import dataviz from "./dataviz";
import copyWithoutBrackets from "./copy-without-brackets";
import openDatedNote from "./open-dated-note";
import tagsExplorer from "./tags-tree-view";
import createFromTemplate from "./create-from-template";
import { FoamFeature } from "../types";
import createReferences from './wikilink-reference-generation';
import openDailyNote from './open-daily-note';
import janitor from './janitor';
import dataviz from './dataviz';
import copyWithoutBrackets from './copy-without-brackets';
import openDatedNote from './open-dated-note';
import tagsExplorer from './tags-tree-view';
import createFromTemplate from './create-from-template';
import openRandomNote from './open-random-note';
import orphans from './orphans';
import { FoamFeature } from '../types';
export const features: FoamFeature[] = [
tagsExplorer,
createReferences,
openDailyNote,
openRandomNote,
janitor,
dataviz,
copyWithoutBrackets,
openDatedNote,
createFromTemplate
createFromTemplate,
orphans,
];

View File

@@ -4,36 +4,37 @@ import {
ExtensionContext,
commands,
Range,
ProgressLocation
} from "vscode";
import * as fs from "fs";
import { FoamFeature } from "../types";
ProgressLocation,
} from 'vscode';
import * as fs from 'fs';
import { FoamFeature } from '../types';
import {
applyTextEdit,
generateLinkReferences,
generateHeading,
Foam
} from "foam-core";
Foam,
Note,
} from 'foam-core';
import {
getWikilinkDefinitionSetting,
LinkReferenceDefinitionsSetting
} from "../settings";
import { astPositionToVsCodePosition } from "../utils";
LinkReferenceDefinitionsSetting,
} from '../settings';
import { astPositionToVsCodePosition, isNote } from '../utils';
const feature: FoamFeature = {
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.janitor", async () =>
commands.registerCommand('foam-vscode.janitor', async () =>
janitor(await foamPromise)
)
);
}
},
};
async function janitor(foam: Foam) {
try {
const noOfFiles = foam.notes.getNotes().filter(Boolean).length;
const noOfFiles = foam.workspace.list().filter(Boolean).length;
if (noOfFiles === 0) {
return window.showInformationMessage(
@@ -44,7 +45,7 @@ async function janitor(foam: Foam) {
const outcome = await window.withProgress(
{
location: ProgressLocation.Notification,
title: `Running Foam Janitor across ${noOfFiles} files!`
title: `Running Foam Janitor across ${noOfFiles} files!`,
},
() => runJanitor(foam)
);
@@ -68,15 +69,15 @@ async function janitor(foam: Foam) {
}
async function runJanitor(foam: Foam) {
const notes = foam.notes.getNotes().filter(Boolean);
const notes: Note[] = foam.workspace.list().filter(isNote);
let updatedHeadingCount = 0;
let updatedDefinitionListCount = 0;
const dirtyTextDocuments = workspace.textDocuments.filter(
textDocument =>
(textDocument.languageId === "markdown" ||
textDocument.languageId === "mdx") &&
(textDocument.languageId === 'markdown' ||
textDocument.languageId === 'mdx') &&
textDocument.isDirty
);
@@ -85,11 +86,11 @@ async function runJanitor(foam: Foam) {
);
const dirtyNotes = notes.filter(note =>
dirtyEditorsFileName.includes(note.source.uri.fsPath)
dirtyEditorsFileName.includes(note.uri.fsPath)
);
const nonDirtyNotes = notes.filter(
note => !dirtyEditorsFileName.includes(note.source.uri.fsPath)
note => !dirtyEditorsFileName.includes(note.uri.fsPath)
);
const wikilinkSetting = getWikilinkDefinitionSetting();
@@ -107,7 +108,7 @@ async function runJanitor(foam: Foam) {
? null
: generateLinkReferences(
note,
foam.notes,
foam.workspace,
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
);
if (definitions) {
@@ -125,7 +126,7 @@ async function runJanitor(foam: Foam) {
text = definitions ? applyTextEdit(text, definitions) : text;
text = heading ? applyTextEdit(text, heading) : text;
return fs.promises.writeFile(note.source.uri.fsPath, text);
return fs.promises.writeFile(note.uri.fsPath, text);
});
await Promise.all(fileWritePromises);
@@ -135,7 +136,7 @@ async function runJanitor(foam: Foam) {
for (const doc of dirtyTextDocuments) {
const editor = await window.showTextDocument(doc);
const note = dirtyNotes.find(
n => n.source.uri.fsPath === editor.document.uri.fsPath
n => n.uri.fsPath === editor.document.uri.fsPath
)!;
// Get edits
@@ -145,12 +146,13 @@ async function runJanitor(foam: Foam) {
? null
: generateLinkReferences(
note,
foam.notes,
foam.workspace,
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
);
if (heading || definitions) {
// Apply Edits
/* eslint-disable */
await editor.edit(editBuilder => {
// Note: The ordering matters. Definitions need to be inserted
// before heading, since inserting a heading changes line numbers below
@@ -169,13 +171,14 @@ async function runJanitor(foam: Foam) {
editBuilder.replace(start, heading.newText);
}
});
/* eslint-enable */
}
}
return {
updatedHeadingCount,
updatedDefinitionListCount,
changedAnyFiles: updatedHeadingCount + updatedDefinitionListCount
changedAnyFiles: updatedHeadingCount + updatedDefinitionListCount,
};
}

View File

@@ -1,13 +1,13 @@
import { ExtensionContext, commands } from "vscode";
import { FoamFeature } from "../types";
import { openDailyNoteFor } from "../dated-notes";
import { ExtensionContext, commands } from 'vscode';
import { FoamFeature } from '../types';
import { openDailyNoteFor } from '../dated-notes';
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.open-daily-note", openDailyNoteFor)
commands.registerCommand('foam-vscode.open-daily-note', openDailyNoteFor)
);
}
},
};
export default feature;

View File

@@ -7,15 +7,15 @@ import {
CompletionItem,
CompletionItemKind,
CompletionList,
CompletionTriggerKind
} from "vscode";
CompletionTriggerKind,
} from 'vscode';
import {
createDailyNoteIfNotExists,
getDailyNoteFileName,
openDailyNoteFor,
getDailyNotePath
} from "../dated-notes";
import { FoamFeature } from "../types";
getDailyNotePath,
} from '../dated-notes';
import { FoamFeature } from '../types';
interface DateSnippet {
snippet: string;
@@ -24,18 +24,18 @@ interface DateSnippet {
}
const daysOfWeek = [
{ day: "sunday", index: 0 },
{ day: "monday", index: 1 },
{ day: "tuesday", index: 2 },
{ day: "wednesday", index: 3 },
{ day: "thursday", index: 4 },
{ day: "friday", index: 5 },
{ day: "saturday", index: 6 }
{ day: 'sunday', index: 0 },
{ day: 'monday', index: 1 },
{ day: 'tuesday', index: 2 },
{ day: 'wednesday', index: 3 },
{ day: 'thursday', index: 4 },
{ day: 'friday', index: 5 },
{ day: 'saturday', index: 6 },
];
type AfterCompletionOptions = "noop" | "createNote" | "navigateToNote";
const foamConfig = workspace.getConfiguration("foam");
type AfterCompletionOptions = 'noop' | 'createNote' | 'navigateToNote';
const foamConfig = workspace.getConfiguration('foam');
const foamNavigateOnSelect: AfterCompletionOptions = foamConfig.get(
"dateSnippets.afterCompletion"
'dateSnippets.afterCompletion'
);
const generateDayOfWeekSnippets = (): DateSnippet[] => {
@@ -51,7 +51,7 @@ const generateDayOfWeekSnippets = (): DateSnippet[] => {
return {
date: target,
detail: `Get a daily note link for ${day}`,
snippet: `/${day}`
snippet: `/${day}`,
};
});
return snippets;
@@ -64,49 +64,57 @@ const createCompletionItem = ({ snippet, date, detail }: DateSnippet) => {
);
completionItem.insertText = getDailyNoteLink(date);
completionItem.detail = `${completionItem.insertText} - ${detail}`;
if (foamNavigateOnSelect !== "noop") {
if (foamNavigateOnSelect !== 'noop') {
completionItem.command = {
command: "foam-vscode.open-dated-note",
title: "Open a note for the given date",
arguments: [date]
command: 'foam-vscode.open-dated-note',
title: 'Open a note for the given date',
arguments: [date],
};
}
return completionItem;
};
const getDailyNoteLink = (date: Date) => {
const foamExtension = foamConfig.get("openDailyNote.fileExtension");
const foamExtension = foamConfig.get('openDailyNote.fileExtension');
const name = getDailyNoteFileName(foamConfig, date);
return `[[${name.replace(`.${foamExtension}`, "")}]]`;
return `[[${name.replace(`.${foamExtension}`, '')}]]`;
};
const snippets: (() => DateSnippet)[] = [
() => ({
detail: "Insert a link to today's daily note",
snippet: "/day",
date: new Date()
snippet: '/day',
date: new Date(),
}),
() => ({
detail: "Insert a link to today's daily note",
snippet: "/today",
date: new Date()
snippet: '/today',
date: new Date(),
}),
() => {
const today = new Date();
return {
detail: "Insert a link to tomorrow's daily note",
snippet: "/tomorrow",
date: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1)
snippet: '/tomorrow',
date: new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() + 1
),
};
},
() => {
const today = new Date();
return {
detail: "Insert a link to yesterday's daily note",
snippet: "/yesterday",
date: new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1)
snippet: '/yesterday',
date: new Date(
today.getFullYear(),
today.getMonth(),
today.getDate() - 1
),
};
}
},
];
const computedSnippets: ((number: number) => DateSnippet)[] = [
@@ -119,7 +127,7 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
today.getFullYear(),
today.getMonth(),
today.getDate() + days
)
),
};
},
(weeks: number) => {
@@ -131,7 +139,7 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
today.getFullYear(),
today.getMonth(),
today.getDate() + 7 * weeks
)
),
};
},
(months: number) => {
@@ -143,7 +151,7 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
today.getFullYear(),
today.getMonth() + months,
today.getDate()
)
),
};
},
(years: number) => {
@@ -155,9 +163,9 @@ const computedSnippets: ((number: number) => DateSnippet)[] = [
today.getFullYear() + years,
today.getMonth(),
today.getDate()
)
),
};
}
},
];
const completions: CompletionItemProvider = {
@@ -170,10 +178,10 @@ const completions: CompletionItemProvider = {
const completionItems = [
...snippets.map(item => createCompletionItem(item())),
...generateDayOfWeekSnippets().map(item => createCompletionItem(item))
...generateDayOfWeekSnippets().map(item => createCompletionItem(item)),
];
return completionItems;
}
},
};
const computedCompletions: CompletionItemProvider = {
@@ -187,7 +195,7 @@ const computedCompletions: CompletionItemProvider = {
const range = document.getWordRangeAtPosition(position, /\S+/);
const snippetString = document.getText(range);
const matches = snippetString.match(/(\d+)/);
const number: string = matches ? matches[0] : "1";
const number: string = matches ? matches[0] : '1';
const completionItems = computedSnippets.map(item => {
const completionItem = createCompletionItem(item(parseInt(number)));
completionItem.range = range;
@@ -195,14 +203,14 @@ const computedCompletions: CompletionItemProvider = {
});
// We still want the list to be treated as "incomplete", because the user may add another number
return new CompletionList(completionItems, true);
}
},
};
const datedNoteCommand = (date: Date) => {
if (foamNavigateOnSelect === "navigateToNote") {
if (foamNavigateOnSelect === 'navigateToNote') {
return openDailyNoteFor(date);
}
if (foamNavigateOnSelect === "createNote") {
if (foamNavigateOnSelect === 'createNote') {
return createDailyNoteIfNotExists(
foamConfig,
getDailyNotePath(foamConfig, date),
@@ -214,18 +222,18 @@ const datedNoteCommand = (date: Date) => {
const feature: FoamFeature = {
activate: (context: ExtensionContext) => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.open-dated-note", date =>
commands.registerCommand('foam-vscode.open-dated-note', date =>
datedNoteCommand(date)
)
);
languages.registerCompletionItemProvider("markdown", completions, "/");
languages.registerCompletionItemProvider('markdown', completions, '/');
languages.registerCompletionItemProvider(
"markdown",
'markdown',
computedCompletions,
"/",
"+"
'/',
'+'
);
}
},
};
export default feature;

View File

@@ -0,0 +1,31 @@
import { Foam } from 'foam-core';
import { ExtensionContext, commands, window } from 'vscode';
import { FoamFeature } from '../types';
import { focusNote } from '../utils';
const feature: FoamFeature = {
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => {
context.subscriptions.push(
commands.registerCommand('foam-vscode.open-random-note', async () => {
const foam = await foamPromise;
const currentFile = window.activeTextEditor?.document.uri.path;
const notes = foam.workspace.list();
if (notes.length <= 1) {
window.showInformationMessage(
'Could not find another note to open. If you believe this is a bug, please file an issue.'
);
return;
}
let randomNoteIndex = Math.floor(Math.random() * notes.length);
if (notes[randomNoteIndex].uri.path === currentFile) {
randomNoteIndex = (randomNoteIndex + 1) % notes.length;
}
focusNote(notes[randomNoteIndex].uri.path, false);
})
);
},
};
export default feature;

View File

@@ -0,0 +1,110 @@
import { OrphansProvider, Directory, OrphansProviderConfig } from './orphans';
import { OrphansConfigGroupBy } from '../settings';
import { FoamWorkspace } from 'foam-core';
import { createTestNote } from '../test/test-utils';
describe('orphans', () => {
const orphanA = createTestNote({
uri: '/path/orphan-a.md',
title: 'Orphan A',
});
const orphanB = createTestNote({
uri: '/path-bis/orphan-b.md',
title: 'Orphan B',
});
const orphanC = createTestNote({
uri: '/path-exclude/orphan-c.md',
title: 'Orphan C',
});
const workspace = new FoamWorkspace()
.set(orphanA)
.set(orphanB)
.set(orphanC)
.set(createTestNote({ uri: '/path/non-orphan-1.md' }))
.set(
createTestNote({
uri: '/path/non-orphan-2.md',
links: [{ slug: 'non-orphan-1' }],
})
)
.resolveLinks();
const dataStore = { read: () => '' } as any;
// Mock config
const config: OrphansProviderConfig = {
exclude: ['path-exclude/**/*'],
groupBy: OrphansConfigGroupBy.Folder,
workspacesFsPaths: [''],
};
it('should return the orphans as a folder tree', async () => {
const provider = new OrphansProvider(workspace, dataStore, config);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
collapsibleState: 1,
label: '/path',
description: '1 orphan',
notes: [{ title: 'Orphan A' }],
},
{
collapsibleState: 1,
label: '/path-bis',
description: '1 orphan',
notes: [{ title: 'Orphan B' }],
},
]);
});
it('should return the orphans in a directory', async () => {
const provider = new OrphansProvider(workspace, dataStore, config);
const directory = new Directory('/path', [orphanA as any]);
const result = await provider.getChildren(directory);
expect(result).toMatchObject([
{
collapsibleState: 0,
label: 'Orphan A',
description: '/path/orphan-a.md',
command: { command: 'vscode.open' },
},
]);
});
it('should return the flattened orphans', async () => {
const mockConfig = { ...config, groupBy: OrphansConfigGroupBy.Off };
const provider = new OrphansProvider(workspace, dataStore, mockConfig);
const result = await provider.getChildren();
expect(result).toMatchObject([
{
collapsibleState: 0,
label: 'Orphan A',
description: '/path/orphan-a.md',
command: { command: 'vscode.open' },
},
{
collapsibleState: 0,
label: 'Orphan B',
description: '/path-bis/orphan-b.md',
command: { command: 'vscode.open' },
},
]);
});
it('should return the orphans without exclusion', async () => {
const mockConfig = { ...config, exclude: [] };
const provider = new OrphansProvider(workspace, dataStore, mockConfig);
const result = await provider.getChildren();
expect(result).toMatchObject([
expect.anything(),
expect.anything(),
{
collapsibleState: 1,
label: '/path-exclude',
description: '1 orphan',
notes: [{ title: 'Orphan C' }],
},
]);
});
});

View File

@@ -0,0 +1,205 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { Foam, IDataStore, Note, URI, FoamWorkspace } from 'foam-core';
import micromatch from 'micromatch';
import {
getOrphansConfig,
OrphansConfig,
OrphansConfigGroupBy,
} from '../settings';
import { FoamFeature } from '../types';
import { getNoteTooltip, getContainsTooltip, isNote } from '../utils';
const feature: FoamFeature = {
activate: async (
context: vscode.ExtensionContext,
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const workspacesFsPaths = vscode.workspace.workspaceFolders.map(
dir => dir.uri.fsPath
);
const provider = new OrphansProvider(
foam.workspace,
foam.services.dataStore,
{
...getOrphansConfig(),
workspacesFsPaths,
}
);
context.subscriptions.push(
vscode.window.registerTreeDataProvider('foam-vscode.orphans', provider),
vscode.commands.registerCommand(
'foam-vscode.group-orphans-by-folder',
() => provider.setGroupBy(OrphansConfigGroupBy.Folder)
),
vscode.commands.registerCommand('foam-vscode.group-orphans-off', () =>
provider.setGroupBy(OrphansConfigGroupBy.Off)
),
foam.workspace.onDidAdd(() => provider.refresh()),
foam.workspace.onDidUpdate(() => provider.refresh()),
foam.workspace.onDidDelete(() => provider.refresh())
);
},
};
export default feature;
export class OrphansProvider
implements vscode.TreeDataProvider<OrphanTreeItem> {
// prettier-ignore
private _onDidChangeTreeData: vscode.EventEmitter<OrphanTreeItem | undefined | void> = new vscode.EventEmitter<OrphanTreeItem | undefined | void>();
// prettier-ignore
readonly onDidChangeTreeData: vscode.Event<OrphanTreeItem | undefined | void> = this._onDidChangeTreeData.event;
private groupBy: OrphansConfigGroupBy = OrphansConfigGroupBy.Folder;
private exclude: string[] = [];
private orphans: Note[] = [];
private root = vscode.workspace.workspaceFolders[0].uri.path;
constructor(
private workspace: FoamWorkspace,
private dataStore: IDataStore,
config: OrphansProviderConfig
) {
this.groupBy = config.groupBy;
this.exclude = this.getGlobs(config.workspacesFsPaths, config.exclude);
this.setContext();
this.computeOrphans();
}
setGroupBy(groupBy: OrphansConfigGroupBy): void {
this.groupBy = groupBy;
this.setContext();
this.refresh();
}
private setContext(): void {
vscode.commands.executeCommand(
'setContext',
'foam-vscode.orphans-grouped-by-folder',
this.groupBy === OrphansConfigGroupBy.Folder
);
}
refresh(): void {
this.computeOrphans();
this._onDidChangeTreeData.fire();
}
getTreeItem(item: OrphanTreeItem): vscode.TreeItem {
return item;
}
getChildren(directory?: Directory): Thenable<OrphanTreeItem[]> {
if (!directory && this.groupBy === OrphansConfigGroupBy.Folder) {
const directories = Object.entries(this.getOrphansByDirectory())
.sort(([a], [b]) => a.localeCompare(b))
.map(([dir, orphans]) => new Directory(dir, orphans));
return Promise.resolve(directories);
}
if (directory) {
const orphans = directory.notes.map(o => new Orphan(o));
return Promise.resolve(orphans);
}
const orphans = this.orphans.map(o => new Orphan(o));
return Promise.resolve(orphans);
}
async resolveTreeItem(item: OrphanTreeItem): Promise<OrphanTreeItem> {
if (item instanceof Orphan) {
const content = await this.dataStore.read(item.note.uri);
item.tooltip = getNoteTooltip(content);
}
return item;
}
private computeOrphans(): void {
this.orphans = this.workspace
.list()
.filter(isNote)
.filter(note => this.workspace.getConnections(note.uri).length === 0)
.filter(note => !this.isMatch(note.uri))
.sort((a, b) => a.title.localeCompare(b.title));
}
private isMatch(uri: URI) {
return micromatch.isMatch(uri.fsPath, this.exclude);
}
private getGlobs(fsPaths: string[], globs: string[]): string[] {
globs = globs.map(glob => (glob.startsWith('/') ? glob.slice(1) : glob));
const exclude: string[] = [];
for (const fsPath of fsPaths) {
let folder = fsPath.replace(/\\/g, '/');
if (folder.substr(-1) === '/') {
folder = folder.slice(0, -1);
}
exclude.push(...globs.map(g => `${folder}/${g}`));
}
return exclude;
}
private getOrphansByDirectory(): OrphansByDirectory {
const orphans: OrphansByDirectory = {};
for (const orphan of this.orphans) {
const p = orphan.uri.path.replace(this.root, '');
const { dir } = path.parse(p);
if (orphans[dir]) {
orphans[dir].push(orphan);
} else {
orphans[dir] = [orphan];
}
}
for (const k in orphans) {
orphans[k].sort((a, b) => a.title.localeCompare(b.title));
}
return orphans;
}
}
export interface OrphansProviderConfig extends OrphansConfig {
workspacesFsPaths: string[];
}
type OrphansByDirectory = { [key: string]: Note[] };
type OrphanTreeItem = Orphan | Directory;
class Orphan extends vscode.TreeItem {
constructor(public readonly note: Note) {
super(note.title, vscode.TreeItemCollapsibleState.None);
this.description = note.uri.path;
this.tooltip = undefined;
this.command = {
command: 'vscode.open',
title: 'Open File',
arguments: [note.uri],
};
}
iconPath = new vscode.ThemeIcon('note');
contextValue = 'orphan';
}
export class Directory extends vscode.TreeItem {
constructor(public readonly dir: string, public readonly notes: Note[]) {
super(dir, vscode.TreeItemCollapsibleState.Collapsed);
const s = this.notes.length > 1 ? 's' : '';
this.description = `${this.notes.length} orphan${s}`;
const titles = this.notes.map(n => n.title);
this.tooltip = getContainsTooltip(titles);
}
iconPath = new vscode.ThemeIcon('folder');
contextValue = 'directory';
}

View File

@@ -1,6 +1,7 @@
import * as vscode from "vscode";
import { FoamFeature } from "../../types";
import { Foam, Note } from "foam-core";
import * as vscode from 'vscode';
import { Foam, Note, IDataStore } from 'foam-core';
import { FoamFeature } from '../../types';
import { getNoteTooltip, getContainsTooltip, isNote } from '../../utils';
const feature: FoamFeature = {
activate: async (
@@ -8,15 +9,17 @@ const feature: FoamFeature = {
foamPromise: Promise<Foam>
) => {
const foam = await foamPromise;
const provider = new TagsProvider(foam);
const provider = new TagsProvider(foam, foam.services.dataStore);
context.subscriptions.push(
vscode.window.registerTreeDataProvider(
"foam-vscode.tags-explorer",
'foam-vscode.tags-explorer',
provider
)
);
foam.notes.onDidUpdateNote(() => provider.refresh());
}
foam.workspace.onDidUpdate(() => provider.refresh());
foam.workspace.onDidAdd(() => provider.refresh());
foam.workspace.onDidDelete(() => provider.refresh());
},
};
export default feature;
@@ -29,10 +32,10 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
private tags: {
tag: string;
noteIds: string[];
notes: TagMetadata[];
}[];
constructor(private foam: Foam) {
constructor(private foam: Foam, private dataStore: IDataStore) {
this.computeTags();
}
@@ -43,16 +46,19 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
private computeTags() {
const rawTags: {
[key: string]: string[];
} = this.foam.notes.getNotes().reduce((acc, note) => {
note.tags.forEach(tag => {
acc[tag] = acc[tag] ?? [];
acc[tag].push(note.id);
});
return acc;
}, {});
[key: string]: TagMetadata[];
} = this.foam.workspace
.list()
.filter(isNote)
.reduce((acc: { [key: string]: TagMetadata[] }, note) => {
note.tags.forEach(tag => {
acc[tag] = acc[tag] ?? [];
acc[tag].push({ title: note.title, uri: note.uri });
});
return acc;
}, {});
this.tags = Object.entries(rawTags)
.map(([tag, noteIds]) => ({ tag, noteIds }))
.map(([tag, notes]) => ({ tag, notes }))
.sort((a, b) => a.tag.localeCompare(b.tag));
}
@@ -62,37 +68,51 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
getChildren(element?: Tag): Thenable<TagTreeItem[]> {
if (element) {
const references: TagReference[] = element.noteIds.map(id => {
const note = this.foam.notes.getNote(id);
return new TagReference(element.tag, note);
});
const references: TagReference[] = element.notes
.map(({ uri }) => this.foam.workspace.get(uri))
.filter(isNote)
.map(note => new TagReference(element.tag, note));
return Promise.resolve([
new TagSearch(element.tag),
...references.sort((a, b) => a.title.localeCompare(b.title))
...references.sort((a, b) => a.title.localeCompare(b.title)),
]);
}
if (!element) {
const tags: Tag[] = this.tags.map(
({ tag, noteIds }) => new Tag(tag, noteIds)
({ tag, notes }) => new Tag(tag, notes)
);
return Promise.resolve(tags.sort((a, b) => a.tag.localeCompare(b.tag)));
}
}
async resolveTreeItem(item: TagTreeItem): Promise<TagTreeItem> {
if (item instanceof TagReference) {
const content = await this.dataStore.read(item.note.uri);
item.tooltip = getNoteTooltip(content);
}
return item;
}
}
type TagTreeItem = Tag | TagReference | TagSearch;
type TagMetadata = { title: string; uri: vscode.Uri };
export class Tag extends vscode.TreeItem {
constructor(public readonly tag: string, public readonly noteIds: string[]) {
constructor(
public readonly tag: string,
public readonly notes: TagMetadata[]
) {
super(tag, vscode.TreeItemCollapsibleState.Collapsed);
this.description = `${this.noteIds.length} reference${
this.noteIds.length !== 1 ? "s" : ""
this.description = `${this.notes.length} reference${
this.notes.length !== 1 ? 's' : ''
}`;
this.tooltip = this.description;
this.tooltip = getContainsTooltip(this.notes.map(n => n.title));
}
iconPath = new vscode.ThemeIcon("symbol-number");
contextValue = "tag";
iconPath = new vscode.ThemeIcon('symbol-number');
contextValue = 'tag';
}
export class TagSearch extends vscode.TreeItem {
@@ -101,31 +121,31 @@ export class TagSearch extends vscode.TreeItem {
const searchString = `#${tag}`;
this.tooltip = `Search ${searchString} in workspace`;
this.command = {
command: "workbench.action.findInFiles",
command: 'workbench.action.findInFiles',
arguments: [
{
query: searchString,
triggerSearch: true,
matchWholeWord: true,
isCaseSensitive: true
}
isCaseSensitive: true,
},
],
title: "Search"
title: 'Search',
};
}
iconPath = new vscode.ThemeIcon("search");
contextValue = "tag-search";
iconPath = new vscode.ThemeIcon('search');
contextValue = 'tag-search';
}
export class TagReference extends vscode.TreeItem {
public readonly title: string;
constructor(tag: string, note: Note) {
constructor(public readonly tag: string, public readonly note: Note) {
super(note.title, vscode.TreeItemCollapsibleState.None);
this.title = note.title;
this.description = note.source.uri.path;
this.tooltip = this.description;
const resourceUri = note.source.uri;
this.description = note.uri.path;
this.tooltip = undefined;
const resourceUri = note.uri;
let selection: vscode.Range | null = null;
// TODO move search fn to core
const lines = note.source.text.split(/\r?\n/);
@@ -136,21 +156,19 @@ export class TagReference extends vscode.TreeItem {
break;
}
}
// TODO I like about this showing the git state of the note, but I don't like the md icon
this.resourceUri = resourceUri;
this.command = {
command: "vscode.open",
command: 'vscode.open',
arguments: [
resourceUri,
{
preview: true,
selection: selection
}
selection: selection,
},
],
title: "Open File"
title: 'Open File',
};
}
iconPath = new vscode.ThemeIcon("note");
contextValue = "reference";
iconPath = new vscode.ThemeIcon('note');
contextValue = 'reference';
}

View File

@@ -1,4 +1,4 @@
import { uniq } from "lodash";
import { uniq } from 'lodash';
import {
CancellationToken,
CodeLens,
@@ -10,73 +10,73 @@ import {
TextDocument,
window,
workspace,
Position
} from "vscode";
Position,
} from 'vscode';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
NoteGraphAPI,
FoamWorkspace,
Foam,
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER
} from "foam-core";
LINK_REFERENCE_DEFINITION_FOOTER,
} from 'foam-core';
import {
hasEmptyTrailing,
docConfig,
loadDocConfig,
isMdEditor,
mdDocSelector,
getText
} from "../utils";
import { FoamFeature } from "../types";
getText,
} from '../utils';
import { FoamFeature } from '../types';
import {
getWikilinkDefinitionSetting,
LinkReferenceDefinitionsSetting
} from "../settings";
LinkReferenceDefinitionsSetting,
} from '../settings';
const feature: FoamFeature = {
activate: async (context: ExtensionContext, foamPromise: Promise<Foam>) => {
const foam = await foamPromise;
context.subscriptions.push(
commands.registerCommand("foam-vscode.update-wikilinks", () =>
updateReferenceList(foam.notes)
commands.registerCommand('foam-vscode.update-wikilinks', () =>
updateReferenceList(foam.workspace)
),
workspace.onWillSaveTextDocument(e => {
if (e.document.languageId === "markdown") {
if (e.document.languageId === 'markdown') {
updateDocumentInNoteGraph(foam, e.document);
e.waitUntil(updateReferenceList(foam.notes));
e.waitUntil(updateReferenceList(foam.workspace));
}
}),
languages.registerCodeLensProvider(
mdDocSelector,
new WikilinkReferenceCodeLensProvider(foam.notes)
new WikilinkReferenceCodeLensProvider(foam.workspace)
)
);
// when a file is created as a result of peekDefinition
// action on a wikilink, add definition update references
foam.notes.onDidAddNote(_ => {
foam.workspace.onDidAdd(_ => {
let editor = window.activeTextEditor;
if (!editor || !isMdEditor(editor)) {
return;
}
updateDocumentInNoteGraph(foam, editor.document);
updateReferenceList(foam.notes);
updateReferenceList(foam.workspace);
});
}
},
};
function updateDocumentInNoteGraph(foam: Foam, document: TextDocument) {
foam.notes.setNote(
foam.workspace.set(
foam.parse(document.uri, document.getText(), docConfig.eol)
);
}
async function createReferenceList(foam: NoteGraphAPI) {
async function createReferenceList(foam: FoamWorkspace) {
let editor = window.activeTextEditor;
if (!editor || !isMdEditor(editor)) {
@@ -100,7 +100,7 @@ async function createReferenceList(foam: NoteGraphAPI) {
}
}
async function updateReferenceList(foam: NoteGraphAPI) {
async function updateReferenceList(foam: FoamWorkspace) {
const editor = window.activeTextEditor;
if (!editor || !isMdEditor(editor)) {
@@ -119,7 +119,7 @@ async function updateReferenceList(foam: NoteGraphAPI) {
// references must always be preceded by an empty line
const spacing = doc.lineAt(range.start.line - 1).isEmptyOrWhitespace
? ""
? ''
: docConfig.eol;
await editor.edit(editBuilder => {
@@ -129,7 +129,7 @@ async function updateReferenceList(foam: NoteGraphAPI) {
}
function generateReferenceList(
foam: NoteGraphAPI,
foam: FoamWorkspace,
doc: TextDocument
): string[] {
const wikilinkSetting = getWikilinkDefinitionSetting();
@@ -138,7 +138,7 @@ function generateReferenceList(
return [];
}
const note = foam.getNoteByURI(doc.uri);
const note = foam.get(doc.uri);
// Should never happen as `doc` is usually given by `editor.document`, which
// binds to an opened note.
@@ -152,7 +152,7 @@ function generateReferenceList(
const references = uniq(
createMarkdownReferences(
foam,
note.id,
note.uri,
wikilinkSetting === LinkReferenceDefinitionsSetting.withExtensions
).map(stringifyMarkdownLinkReferenceDefinition)
);
@@ -161,7 +161,7 @@ function generateReferenceList(
return [
LINK_REFERENCE_DEFINITION_HEADER,
...references,
LINK_REFERENCE_DEFINITION_FOOTER
LINK_REFERENCE_DEFINITION_FOOTER,
];
}
@@ -199,9 +199,9 @@ function detectReferenceListRange(doc: TextDocument): Range | null {
}
class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
private foam: NoteGraphAPI;
private foam: FoamWorkspace;
constructor(foam: NoteGraphAPI) {
constructor(foam: FoamWorkspace) {
this.foam = foam;
}
@@ -220,14 +220,14 @@ class WikilinkReferenceCodeLensProvider implements CodeLensProvider {
const oldRefs = getText(range).replace(/\r?\n|\r/g, docConfig.eol);
const newRefs = refs.join(docConfig.eol);
let status = oldRefs === newRefs ? "up to date" : "out of date";
let status = oldRefs === newRefs ? 'up to date' : 'out of date';
return [
new CodeLens(range, {
arguments: [],
title: `Link references (${status})`,
command: ""
})
command: '',
}),
];
}
}

View File

@@ -1,6 +1,6 @@
import { workspace } from "vscode";
import { FoamConfig, createConfigFromFolders } from "foam-core";
import { getIgnoredFilesSetting } from "../settings";
import { workspace } from 'vscode';
import { FoamConfig, createConfigFromFolders } from 'foam-core';
import { getIgnoredFilesSetting } from '../settings';
// TODO this is still to be improved - foam config should
// not be dependent on vscode but at the moment it's convenient
@@ -10,6 +10,6 @@ export const getConfigFromVscode = (): FoamConfig => {
const excludeGlobs = getIgnoredFilesSetting();
return createConfigFromFolders(workspaceFolders, {
ignore: excludeGlobs.map(g => g.toString())
ignore: excludeGlobs.map(g => g.toString()),
});
};

View File

@@ -4,11 +4,11 @@ import {
URI,
FoamConfig,
IDisposable,
Logger
} from "foam-core";
import { workspace, FileSystemWatcher, EventEmitter } from "vscode";
import { TextDecoder } from "util";
import { isSome } from "../utils";
Logger,
} from 'foam-core';
import { workspace, FileSystemWatcher, EventEmitter } from 'vscode';
import { TextDecoder } from 'util';
import { isSome } from '../utils';
export class VsCodeDataStore implements IDataStore, IDisposable {
onDidCreateEmitter = new EventEmitter<URI>();
@@ -22,23 +22,23 @@ export class VsCodeDataStore implements IDataStore, IDisposable {
files: URI[];
constructor(private config: FoamConfig) {
this.watcher = workspace.createFileSystemWatcher("**/*");
this.watcher = workspace.createFileSystemWatcher('**/*');
this.watcher.onDidCreate(async uri => {
await this.listFiles();
if (this.isMatch(uri)) {
Logger.info("Created: ", uri);
Logger.info('Created: ', uri);
this.onDidCreateEmitter.fire(uri);
}
});
this.watcher.onDidChange(uri => {
if (this.isMatch(uri)) {
Logger.info("Updated: ", uri);
Logger.info('Updated: ', uri);
this.onDidChangeEmitter.fire(uri);
}
});
this.watcher.onDidDelete(uri => {
if (this.isMatch(uri)) {
Logger.info("Deleted: ", uri);
Logger.info('Deleted: ', uri);
this.files = this.files.filter(f => f.path !== uri.path);
this.onDidDeleteEmitter.fire(uri);
}
@@ -47,8 +47,8 @@ export class VsCodeDataStore implements IDataStore, IDisposable {
async listFiles(): Promise<URI[]> {
this.files = await workspace.findFiles(
`{${this.config.includeGlobs.join(",")}}`,
`{${this.config.ignoreGlobs.join(",")}}`
`{${this.config.includeGlobs.join(',')}}`,
`{${this.config.ignoreGlobs.join(',')}}`
);
return this.files;

View File

@@ -1,17 +1,17 @@
import { window, commands, ExtensionContext } from "vscode";
import { ILogger, IDisposable, LogLevel, BaseLogger } from "foam-core";
import { getFoamLoggerLevel } from "../settings";
import { window, commands, ExtensionContext } from 'vscode';
import { ILogger, IDisposable, LogLevel, BaseLogger } from 'foam-core';
import { getFoamLoggerLevel } from '../settings';
export interface VsCodeLogger extends ILogger, IDisposable {
show();
}
export class VsCodeOutputLogger extends BaseLogger implements VsCodeLogger {
private channel = window.createOutputChannel("Foam");
private channel = window.createOutputChannel('Foam');
constructor() {
super(getFoamLoggerLevel());
this.channel.appendLine("Foam Logging: " + getFoamLoggerLevel());
this.channel.appendLine('Foam Logging: ' + getFoamLoggerLevel());
}
log(lvl: LogLevel, msg?: any, ...extra: any[]): void {
@@ -42,12 +42,12 @@ export const exposeLogger = (
logger: VsCodeLogger
): void => {
context.subscriptions.push(
commands.registerCommand("foam-vscode.set-log-level", async () => {
const items: LogLevel[] = ["debug", "info", "warn", "error"];
commands.registerCommand('foam-vscode.set-log-level', async () => {
const items: LogLevel[] = ['debug', 'info', 'warn', 'error'];
const level = await window.showQuickPick(
items.map(item => ({
label: item,
description: item === logger.getLevel() && "Current"
description: item === logger.getLevel() && 'Current',
}))
);
logger.setLevel(level.label);

View File

@@ -1,17 +1,17 @@
import { workspace, GlobPattern } from "vscode";
import { LogLevel } from "foam-core";
import { workspace, GlobPattern } from 'vscode';
import { LogLevel } from 'foam-core';
export enum LinkReferenceDefinitionsSetting {
withExtensions = "withExtensions",
withoutExtensions = "withoutExtensions",
off = "off"
withExtensions = 'withExtensions',
withoutExtensions = 'withoutExtensions',
off = 'off',
}
export function getWikilinkDefinitionSetting(): LinkReferenceDefinitionsSetting {
return workspace
.getConfiguration("foam.edit")
.getConfiguration('foam.edit')
.get<LinkReferenceDefinitionsSetting>(
"linkReferenceDefinitions",
'linkReferenceDefinitions',
LinkReferenceDefinitionsSetting.withoutExtensions
);
}
@@ -19,16 +19,39 @@ export function getWikilinkDefinitionSetting(): LinkReferenceDefinitionsSetting
/** Retrieve the list of file ignoring globs. */
export function getIgnoredFilesSetting(): GlobPattern[] {
return [
...workspace.getConfiguration().get("foam.files.ignore", []),
...Object.keys(workspace.getConfiguration().get("files.exclude", {}))
...workspace.getConfiguration().get('foam.files.ignore', []),
...Object.keys(workspace.getConfiguration().get('files.exclude', {})),
];
}
/** Retrieves the maximum length for a Graph node title. */
export function getTitleMaxLength(): number {
return workspace.getConfiguration("foam.graph").get("titleMaxLength");
return workspace.getConfiguration('foam.graph').get('titleMaxLength');
}
/** Retrieve the graph's style object */
export function getGraphStyle(): object {
return workspace.getConfiguration('foam.graph').get('style');
}
export function getFoamLoggerLevel(): LogLevel {
return workspace.getConfiguration("foam.logging").get("level") ?? "info";
return workspace.getConfiguration('foam.logging').get('level') ?? 'info';
}
/** Retrieve the orphans configuration */
export function getOrphansConfig(): OrphansConfig {
const orphansConfig = workspace.getConfiguration('foam.orphans');
const exclude: string[] = orphansConfig.get('exclude');
const groupBy: OrphansConfigGroupBy = orphansConfig.get('groupBy');
return { exclude, groupBy };
}
export interface OrphansConfig {
exclude: string[];
groupBy: OrphansConfigGroupBy;
}
export enum OrphansConfigGroupBy {
Folder = 'folder',
Off = 'off',
}

View File

@@ -0,0 +1,36 @@
import * as path from 'path';
import fs from 'fs';
import os from 'os';
import { runTests } from 'vscode-test';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to the extension test script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite');
const tmpWorkspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'foam-'));
// Download VS Code, unzip it and run the integration test
await runTests({
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: [tmpWorkspaceDir, '--disable-extensions'],
// Running the tests with vscode 1.53.0 is causing issues in `suite.ts:23`,
// which is causing a stack overflow, possibly due to a recursive callback.
// Also see https://github.com/foambubble/foam/pull/479#issuecomment-774167127
// Forcing the version to 1.52.0 solves the problem.
// TODO: to review, further investigate, and roll back this workaround.
version: '1.52.0',
});
} catch (err) {
console.error('Failed to run tests');
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,66 @@
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/testRunner.ts
/**
* We use the following convention in Foam:
* - *.test.ts are unit tests
* they might still rely on vscode API and hence will be run in this environment, but
* are fundamentally about testing functions in isolations
* - *.spec.ts are integration tests
* they will make direct use of the vscode API to be invoked as commands, create editors,
* and so on..
*/
import path from 'path';
import { runCLI } from '@jest/core';
const rootDir = path.resolve(__dirname, '../..');
export function run(): Promise<void> {
process.stdout.write = (buffer: string) => {
console.log(buffer);
return true;
};
process.stderr.write = (buffer: string) => {
console.error(buffer);
return true;
};
process.env.FORCE_COLOR = '1';
process.env.NODE_ENV = 'test';
return new Promise(async (resolve, reject) => {
try {
const { results } = await runCLI(
{
rootDir,
roots: ['<rootDir>/src'],
transform: JSON.stringify({ '^.+\\.ts$': 'ts-jest' }),
runInBand: true,
testRegex: '\\.(test|spec)\\.ts$',
testEnvironment:
'<rootDir>/src/test/support/extended-vscode-environment.js',
setupFiles: ['<rootDir>/src/test/support/jest-setup.ts'],
setupFilesAfterEnv: ['jest-extended'],
globals: JSON.stringify({
'ts-jest': {
tsconfig: path.resolve(rootDir, './tsconfig.json'),
},
}),
testTimeout: 20000,
verbose: true,
colors: true,
} as any,
[rootDir]
);
const failures = results.testResults.reduce(
(acc, res) => (res.failureMessage ? acc + 1 : acc),
0
);
return failures === 0
? resolve()
: reject(`${failures} tests have failed!`);
} catch (error) {
return reject(error);
}
});
}

View File

@@ -0,0 +1,15 @@
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/env/ExtendedVscodeEnvironment.js
const VscodeEnvironment = require('jest-environment-vscode');
class ExtendedVscodeEnvironment extends VscodeEnvironment {
async setup() {
await super.setup();
// Expose RegExp otherwise document.getWordRangeAtPosition won't work as supposed.
// Implementation of getWordRangeAtPosition uses "instanceof RegExp" which returns false
// due to Jest running tests in the different vm context.
// See https://github.com/nodejs/node-v0.x-archive/issues/1277.
this.global.RegExp = RegExp;
}
}
module.exports = ExtendedVscodeEnvironment;

View File

@@ -0,0 +1,2 @@
// Based on https://github.com/svsool/vscode-memo/blob/master/src/test/config/jestSetup.ts
jest.mock('vscode', () => (global as any).vscode, { virtual: true });

View File

@@ -0,0 +1,67 @@
// TODO: this file has some utility functions also present in foam-core testing
// they should be consolidated
import { URI, Attachment, NoteLinkDefinition, Note } from 'foam-core';
const position = {
start: { line: 1, column: 1 },
end: { line: 1, column: 1 },
};
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?: Array<{ slug: string } | { to: string }>;
text?: string;
}): Note => {
return {
uri: strToUri(params.uri),
type: 'note',
properties: {},
title: params.title ?? null,
definitions: params.definitions ?? [],
tags: new Set(),
links: params.links
? params.links.map(link =>
'slug' in link
? {
type: 'wikilink',
slug: link.slug,
target: link.slug,
position: position,
text: 'link text',
}
: {
type: 'link',
target: link.to,
label: 'link text',
}
)
: [],
source: {
eol: eol,
end: documentEnd,
contentStart: documentStart,
text: params.text ?? '',
},
};
};

View File

@@ -1,6 +1,6 @@
import { ExtensionContext } from "vscode";
import { Foam } from "foam-core";
import { ExtensionContext } from 'vscode';
import { Foam } from 'foam-core';
export interface FoamFeature {
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => void
activate: (context: ExtensionContext, foamPromise: Promise<Foam>) => void;
}

View File

@@ -0,0 +1,65 @@
import { dropExtension, removeBrackets, toTitleCase } from './utils';
describe('dropExtension', () => {
test('returns file name without extension', () => {
expect(dropExtension('file.md')).toEqual('file');
});
});
describe('removeBrackets', () => {
it('removes the brackets', () => {
const input = 'hello world [[this-is-it]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the md file extension', () => {
const input = 'hello world [[this-is-it.md]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the mdx file extension', () => {
const input = 'hello world [[this-is-it.mdx]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets and the markdown file extension', () => {
const input = 'hello world [[this-is-it.markdown]]';
const actual = removeBrackets(input);
const expected = 'hello world This Is It';
expect(actual).toEqual(expected);
});
it('removes the brackets even with numbers', () => {
const input = 'hello world [[2020-07-21.markdown]]';
const actual = removeBrackets(input);
const expected = 'hello world 2020 07 21';
expect(actual).toEqual(expected);
});
it('removes brackets for more than one word', () => {
const input =
'I am reading this as part of the [[book-club]] put on by [[egghead]] folks (Lauro).';
const actual = removeBrackets(input);
const expected =
'I am reading this as part of the Book Club put on by Egghead folks (Lauro).';
expect(actual).toEqual(expected);
});
});
describe('toTitleCase', () => {
it('title cases a word', () => {
const input =
'look at this really long sentence but I am calling it a word';
const actual = toTitleCase(input);
const expected =
'Look At This Really Long Sentence But I Am Calling It A Word';
expect(actual).toEqual(expected);
});
it('works on one word', () => {
const input = 'word';
const actual = toTitleCase(input);
const expected = 'Word';
expect(actual).toEqual(expected);
});
});

View File

@@ -7,10 +7,14 @@ import {
TextEditor,
workspace,
Uri,
Selection
} from "vscode";
import * as fs from "fs";
import { Logger } from "foam-core";
Selection,
MarkdownString,
version,
} from 'vscode';
import * as fs from 'fs';
import { Logger, Resource, Note } from 'foam-core';
import matter from 'gray-matter';
import removeMarkdown from 'remove-markdown';
interface Point {
line: number;
@@ -18,34 +22,34 @@ interface Point {
offset?: number;
}
export const docConfig = { tab: " ", eol: "\r\n" };
export const docConfig = { tab: ' ', eol: '\r\n' };
export const mdDocSelector = [
{ language: "markdown", scheme: "file" },
{ language: "markdown", scheme: "untitled" }
{ language: 'markdown', scheme: 'file' },
{ language: 'markdown', scheme: 'untitled' },
];
export function loadDocConfig() {
// Load workspace config
let activeEditor = window.activeTextEditor;
if (!activeEditor) {
Logger.debug("Failed to load config, no active editor");
Logger.debug('Failed to load config, no active editor');
return;
}
docConfig.eol = activeEditor.document.eol === EndOfLine.CRLF ? "\r\n" : "\n";
docConfig.eol = activeEditor.document.eol === EndOfLine.CRLF ? '\r\n' : '\n';
let tabSize = Number(activeEditor.options.tabSize);
let insertSpaces = activeEditor.options.insertSpaces;
if (insertSpaces) {
docConfig.tab = " ".repeat(tabSize);
docConfig.tab = ' '.repeat(tabSize);
} else {
docConfig.tab = "\t";
docConfig.tab = '\t';
}
}
export function isMdEditor(editor: TextEditor) {
return editor && editor.document && editor.document.languageId === "markdown";
return editor && editor.document && editor.document.languageId === 'markdown';
}
export function detectGeneratedCode(
@@ -61,7 +65,7 @@ export function detectGeneratedCode(
if (headerLine < 0 || headerLine >= footerLine) {
return {
range: null,
lines: []
lines: [],
};
}
@@ -70,7 +74,7 @@ export function detectGeneratedCode(
new Position(headerLine, 0),
new Position(footerLine, lines[footerLine].length + 1)
),
lines: lines.slice(headerLine + 1, footerLine + 1)
lines: lines.slice(headerLine + 1, footerLine + 1),
};
}
@@ -83,9 +87,9 @@ export function getText(range: Range): string {
}
export function dropExtension(path: string): string {
const parts = path.split(".");
const parts = path.split('.');
parts.pop();
return parts.join(".");
return parts.join('.');
}
/**
@@ -103,17 +107,17 @@ export const astPositionToVsCodePosition = (point: Point): Position => {
*/
export function removeBrackets(s: string): string {
// take in the string, split on space
const stringSplitBySpace = s.split(" ");
const stringSplitBySpace = s.split(' ');
// loop through words
const modifiedWords = stringSplitBySpace.map(currentWord => {
if (currentWord.includes("[[")) {
if (currentWord.includes('[[')) {
// all of these transformations will turn this "[[you-are-awesome]]"
// to this "you are awesome"
let word = currentWord.replace(/(\[\[)/g, "");
word = word.replace(/(\]\])/g, "");
word = word.replace(/(.mdx|.md|.markdown)/g, "");
word = word.replace(/[-]/g, " ");
let word = currentWord.replace(/(\[\[)/g, '');
word = word.replace(/(\]\])/g, '');
word = word.replace(/(.mdx|.md|.markdown)/g, '');
word = word.replace(/[-]/g, ' ');
// then we titlecase the word so "you are awesome"
// becomes "You Are Awesome"
@@ -125,7 +129,7 @@ export function removeBrackets(s: string): string {
return currentWord;
});
return modifiedWords.join(" ");
return modifiedWords.join(' ');
}
/**
@@ -135,9 +139,9 @@ export function removeBrackets(s: string): string {
*/
export function toTitleCase(word: string): string {
return word
.split(" ")
.split(' ')
.map(word => word[0].toUpperCase() + word.substring(1))
.join(" ");
.join(' ');
}
/**
@@ -157,7 +161,9 @@ export function pathExists(path: string) {
*
* @param value The object to verify
*/
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; // eslint-disable-line
}
@@ -184,3 +190,80 @@ export async function focusNote(notePath: string, moveCursorToEnd: boolean) {
editor.selection = new Selection(range.end, range.end);
}
}
export function getContainsTooltip(titles: string[]): string {
const TITLES_LIMIT = 5;
const ellipsis = titles.length > TITLES_LIMIT ? ',...' : '';
return `Contains "${titles.slice(0, TITLES_LIMIT).join('", "')}"${ellipsis}`;
}
/**
* Depending on the current vscode version, returns a MarkdownString of the
* note content casted as string or returns a simple string
* MarkdownString is only available from 1.52.1 onwards
* https://code.visualstudio.com/updates/v1_52#_markdown-tree-tooltip-api
* @param note A Foam Note
*/
export function getNoteTooltip(content: string): string {
const STABLE_MARKDOWN_STRING_API_VERSION = '1.52.1';
const strippedContent = stripFrontMatter(stripImages(content));
if (version >= STABLE_MARKDOWN_STRING_API_VERSION) {
return formatMarkdownTooltip(strippedContent) as any;
}
return formatSimpleTooltip(strippedContent);
}
export function formatMarkdownTooltip(content: string): MarkdownString {
const LINES_LIMIT = 16;
const { excerpt, lines } = getExcerpt(content, LINES_LIMIT);
const totalLines = content.split('\n').length;
const diffLines = totalLines - lines;
const ellipsis = diffLines > 0 ? `\n\n[...] *(+ ${diffLines} lines)*` : '';
return new MarkdownString(`${excerpt}${ellipsis}`);
}
export function formatSimpleTooltip(content: string): string {
const CHARACTERS_LIMIT = 200;
const flatContent = removeMarkdown(content)
.replace(/\r?\n|\r/g, ' ')
.replace(/\s+/g, ' ');
const extract = flatContent.substr(0, CHARACTERS_LIMIT);
const ellipsis = flatContent.length > CHARACTERS_LIMIT ? '...' : '';
return `${extract}${ellipsis}`;
}
export function getExcerpt(
markdown: string,
maxLines: number
): { excerpt: string; lines: number } {
const OFFSET_LINES_LIMIT = 5;
const paragraphs = markdown.replace(/\r\n/g, '\n').split('\n\n');
const excerpt: string[] = [];
let lines = 0;
for (const paragraph of paragraphs) {
const n = paragraph.split('\n').length;
if (lines > maxLines || lines + n - maxLines > OFFSET_LINES_LIMIT) {
break;
}
excerpt.push(paragraph);
lines = lines + n + 1;
}
return { excerpt: excerpt.join('\n\n'), lines };
}
export function stripFrontMatter(markdown: string): string {
return matter(markdown).content.trim();
}
export function stripImages(markdown: string): string {
return markdown.replace(
/!\[(.*)\]\([-/\\.A-Za-z]*\)/gi,
'$1'.length ? '[Image: $1]' : ''
);
}
export const isNote = (resource: Resource): resource is Note => {
return resource.type === 'note';
};

View File

@@ -11,7 +11,10 @@
</style>
</head>
<body>
<div id="graph" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
<div
id="graph"
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;"
></div>
<!-- To test the graph locally in a broswer:
1. copy the json data object received in the message payload
2. paste the content in test-data.js

View File

@@ -1,29 +1,23 @@
const CONTAINER_ID = "graph";
const CONTAINER_ID = 'graph';
function getStyle(name, fallback) {
return (
getComputedStyle(document.documentElement).getPropertyValue(name) ||
fallback
);
}
const style = {
background: getStyle(`--vscode-panel-background`, "#202020"),
fontSize: parseInt(getStyle(`--vscode-font-size`, 12)) - 2,
highlightedForeground: getStyle(
"--vscode-list-highlightForeground",
"#f9c74f"
),
/** The style fallback. These values should only be used when all else fails. */
const styleFallback = {
background: '#202020',
fontSize: 12,
lineColor: '#277da1',
lineWidth: 0.2,
particleWidth: 1.0,
highlightedForeground: '#f9c74f',
node: {
note: getStyle("--vscode-editor-foreground", "#277da1"),
nonExistingNote: getStyle(
"--vscode-list-deemphasizedForeground",
"#545454"
),
unknown: getStyle("--vscode-editor-foreground", "#f94144")
}
note: '#277da1',
placeholder: '#545454',
},
};
function getStyle(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name);
}
const sizeScale = d3
.scaleLinear()
.domain([0, 30])
@@ -36,6 +30,24 @@ const labelAlpha = d3
.range([0, 1])
.clamp(true);
const defaultStyle = {
background: getStyle(`--vscode-panel-background`) ?? styleFallback.background,
fontSize:
parseInt(getStyle(`--vscode-font-size`) ?? styleFallback.fontSize) - 2,
lineColor: getStyle('--vscode-editor-foreground') ?? styleFallback.lineColor,
lineWidth: parseFloat(styleFallback.lineWidth),
particleWidth: parseFloat(styleFallback.particleWidth),
highlightedForeground:
getStyle('--vscode-list-highlightForeground') ??
styleFallback.highlightedForeground,
node: {
note: getStyle('--vscode-editor-foreground') ?? styleFallback.node.note,
placeholder:
getStyle('--vscode-list-deemphasizedForeground') ??
styleFallback.node.placeholder,
},
};
let model = {
selectedNodes: new Set(),
hoverNode: null,
@@ -44,8 +56,13 @@ let model = {
nodeInfo: {},
data: {
nodes: [],
links: []
}
links: [],
},
/** The style property.
* It tries to be set using VSCode values,
* in the case it fails, use the fallback style values.
*/
style: defaultStyle,
};
const graph = ForceGraph();
@@ -92,7 +109,7 @@ const Actions = {
});
remaining.forEach(nodeId => {
m.data.nodes.push({
id: nodeId
id: nodeId,
});
});
m.data.links = links; // links can be swapped out without problem
@@ -118,22 +135,47 @@ const Actions = {
highlightNode: nodeId =>
update(m => {
m.hoverNode = nodeId;
})
}),
/** Applies a new style to the graph,
* missing elements are set to their existing values.
*
* @param {*} newStyle the style to be applied
*/
updateStyle: newStyle => {
if (!newStyle) {
return;
}
model.style = {
...defaultStyle,
...newStyle,
lineColor:
newStyle.lineColor ||
(newStyle.node && newStyle.node.note) ||
defaultStyle.lineColor,
node: {
...defaultStyle.node,
...newStyle.node,
},
};
graph.backgroundColor(model.style.background);
},
};
function initDataviz(channel) {
const elem = document.getElementById(CONTAINER_ID);
graph(elem)
.graphData(model.data)
.backgroundColor(style.background)
.backgroundColor(model.style.background)
.linkHoverPrecision(8)
.d3Force("x", d3.forceX())
.d3Force("y", d3.forceY())
.d3Force("collide", d3.forceCollide(graph.nodeRelSize()))
.linkWidth(0.2)
.d3Force('x', d3.forceX())
.d3Force('y', d3.forceY())
.d3Force('collide', d3.forceCollide(graph.nodeRelSize()))
.linkWidth(() => model.style.lineWidth || styleFallback.lineWidth)
.linkDirectionalParticles(1)
.linkDirectionalParticleWidth(link =>
getLinkState(link, model) === "highlighted" ? 1 : 0
getLinkState(link, model) === 'highlighted'
? model.style.particleWidth || styleFallback.particleWidth
: 0
)
.nodeCanvasObject((node, ctx, globalScale) => {
const info = model.nodeInfo[node.id];
@@ -143,34 +185,35 @@ function initDataviz(channel) {
}
const size = sizeScale(info.neighbors.length);
const { fill, border } = getNodeColor(node.id, model);
const fontSize = style.fontSize / globalScale;
let textColor = d3.rgb(fill);
textColor.opacity =
getNodeState(node.id, model) === "highlighted"
? 1
: labelAlpha(globalScale);
const fontSize = model.style.fontSize / globalScale;
const textColor = fill.copy({
opacity:
getNodeState(node.id, model) === 'highlighted'
? 1
: labelAlpha(globalScale),
});
const label = info.title;
Draw(ctx)
.circle(node.x, node.y, size + 0.2, border)
.circle(node.x, node.y, size, fill)
.text(label, node.x, node.y + size + 1, fontSize, textColor);
.text(label, node.x, node.y + size + 1, fontSize, textColor.toString());
})
.linkColor(link => getLinkColor(link, model))
.onNodeHover(node => {
Actions.highlightNode(node?.id);
})
.onNodeClick((node, event) => {
if (event.getModifierState("Control") || event.getModifierState("Meta")) {
if (event.getModifierState('Control') || event.getModifierState('Meta')) {
channel.postMessage({
type: "webviewDidSelectNode",
payload: node.id
type: 'webviewDidSelectNode',
payload: node.id,
});
}
Actions.selectNode(node.id, event.getModifierState("Shift"));
Actions.selectNode(node.id, event.getModifierState('Shift'));
})
.onBackgroundClick(event => {
Actions.selectNode(null, event.getModifierState("Shift"));
Actions.selectNode(null, event.getModifierState('Shift'));
});
}
@@ -192,52 +235,56 @@ function augmentGraphInfo(data) {
function getNodeColor(nodeId, model) {
const info = model.nodeInfo[nodeId];
const typeFill = style.node[info.type || "unknown"];
const style = model.style;
const typeFill = d3.rgb(
style.node[info.type ?? 'note'] ?? style.node['note']
);
switch (getNodeState(nodeId, model)) {
case "regular":
case 'regular':
return { fill: typeFill, border: typeFill };
case "lessened":
const darker = d3.hsl(typeFill).darker(3);
return { fill: darker, border: darker };
case "highlighted":
case 'lessened':
const transparent = d3.rgb(typeFill).copy({ opacity: 0.5 });
return { fill: transparent, border: transparent };
case 'highlighted':
return {
fill: typeFill,
border: style.highlightedForeground
border: d3.rgb(style.highlightedForeground),
};
default:
throw new Error("Unknown type for node", nodeId);
throw new Error('Unknown type for node', nodeId);
}
}
function getLinkColor(link, model) {
const style = model.style;
switch (getLinkState(link, model)) {
case "regular":
return d3.hsl(style.node.note).darker(2);
case "highlighted":
case 'regular':
return style.lineColor;
case 'highlighted':
return style.highlightedForeground;
case "lessened":
return d3.hsl(style.node.note).darker(4);
case 'lessened':
return d3.hsl(style.lineColor).copy({ opacity: 0.5 });
default:
throw new Error("Unknown type for link", link);
throw new Error('Unknown type for link', link);
}
}
function getNodeState(nodeId, model) {
return model.selectedNodes.has(nodeId) || model.hoverNode === nodeId
? "highlighted"
? 'highlighted'
: model.focusNodes.size === 0
? "regular"
? 'regular'
: model.focusNodes.has(nodeId)
? "regular"
: "lessened";
? 'regular'
: 'lessened';
}
function getLinkState(link, model) {
return model.focusNodes.size === 0
? "regular"
? 'regular'
: model.focusLinks.has(link)
? "highlighted"
: "lessened";
? 'highlighted'
: 'lessened';
}
const Draw = ctx => ({
@@ -251,12 +298,12 @@ const Draw = ctx => ({
},
text: function(text, x, y, size, color) {
ctx.font = `${size}px Sans-Serif`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = color;
ctx.fillText(text, x, y);
return this;
}
},
});
// init the app
@@ -265,34 +312,34 @@ try {
window.onload = () => {
initDataviz(vscode);
console.log("ready");
console.log('ready');
vscode.postMessage({
type: "webviewDidLoad"
type: 'webviewDidLoad',
});
};
window.addEventListener("error", error => {
window.addEventListener('error', error => {
vscode.postMessage({
type: "error",
type: 'error',
payload: {
message: error.message,
filename: error.filename,
lineno: error.lineno,
colno: error.colno,
error: error.error
}
error: error.error,
},
});
});
window.addEventListener("message", event => {
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case "didUpdateGraphData":
case 'didUpdateGraphData':
const graphData = augmentGraphInfo(message.payload);
console.log("didUpdateGraphData", graphData);
console.log('didUpdateGraphData', graphData);
Actions.refresh(graphData);
break;
case "didSelectNote":
case 'didSelectNote':
const noteId = message.payload;
const node = graph.graphData().nodes.find(node => node.id === noteId);
if (node) {
@@ -300,24 +347,28 @@ try {
Actions.selectNode(noteId);
}
break;
case 'didUpdateStyle':
const style = message.payload;
Actions.updateStyle(style);
break;
}
});
} catch {
console.log("VsCode not detected");
console.log('VsCode not detected');
}
window.addEventListener("resize", () => {
window.addEventListener('resize', () => {
graph.width(window.innerWidth).height(window.innerHeight);
});
// For testing
if (window.data) {
console.log("Test mode");
console.log('Test mode');
window.model = model;
window.graph = graph;
window.onload = () => {
initDataviz({
postMessage: message => console.log("message", message)
postMessage: message => console.log('message', message),
});
const graphData = augmentGraphInfo(window.data);
Actions.refresh(graphData);

View File

@@ -1,64 +0,0 @@
// @note: This will fail due to utils importing 'vscode'
// which needs to be mocked in the jest test environment.
// See: https://github.com/microsoft/vscode-test/issues/37
import { dropExtension, removeBrackets, toTitleCase } from '../src/utils';
describe("dropExtension", () => {
test("returns file name without extension", () => {
expect(dropExtension('file.md')).toEqual('file');
});
});
describe("removeBrackets", () => {
it("removes the brackets", () => {
const input = "hello world [[this-is-it]]";
const actual = removeBrackets(input);
const expected = "hello world This Is It";
expect(actual).toEqual(expected);
});
it("removes the brackets and the md file extension", () => {
const input = "hello world [[this-is-it.md]]";
const actual = removeBrackets(input);
const expected = "hello world This Is It";
expect(actual).toEqual(expected);
});
it("removes the brackets and the mdx file extension", () => {
const input = "hello world [[this-is-it.mdx]]";
const actual = removeBrackets(input);
const expected = "hello world This Is It";
expect(actual).toEqual(expected);
});
it("removes the brackets and the markdown file extension", () => {
const input = "hello world [[this-is-it.markdown]]";
const actual = removeBrackets(input);
const expected = "hello world This Is It";
expect(actual).toEqual(expected);
});
it("removes the brackets even with numbers", () => {
const input = "hello world [[2020-07-21.markdown]]";
const actual = removeBrackets(input);
const expected = "hello world 2020 07 21";
expect(actual).toEqual(expected);
});
it("removes brackets for more than one word", () => {
const input = "I am reading this as part of the [[book-club]] put on by [[egghead]] folks (Lauro).";
const actual = removeBrackets(input);
const expected = "I am reading this as part of the Book Club put on by Egghead folks (Lauro).";
expect(actual).toEqual(expected);
});
});
describe("toTitleCase", () => {
it("title cases a word", () => {
const input = "look at this really long sentence but I am calling it a word";
const actual = toTitleCase(input);
const expected = "Look At This Really Long Sentence But I Am Calling It A Word";
expect(actual).toEqual(expected);
});
it("works on one word", () => {
const input = "word";
const actual = toTitleCase(input);
const expected = "Word";
expect(actual).toEqual(expected);
});
});

View File

@@ -1,6 +0,0 @@
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: true,
singleQuote: true,
};

135
readme.md
View File

@@ -1,12 +1,14 @@
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://discord.gg/rtdZKgj)! 💬*
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
# Foam
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-58-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-64-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![Discord Chat](https://img.shields.io/discord/729975036148056075?color=748AD9&label=discord%20chat&style=flat-square)](https://foambubble.github.io/join-discord/g)
**Foam** is a personal knowledge management and sharing system inspired by [Roam Research](https://roamresearch.com/), built on [Visual Studio Code](https://code.visualstudio.com/) and [GitHub](https://github.com/).
You can use **Foam** for organising your research, keeping re-discoverable notes, writing long-form content and, optionally, publishing it to the web.
@@ -38,6 +40,8 @@ Quick links to next documentation sections
- [Call To Adventure](https://foambubble.github.io/foam#call-to-adventure)
- [Thanks and attribution](https://foambubble.github.io/foam#thanks-and-attribution)
You can also browse the [docs folder](https://github.com/foambubble/foam/tree/master/docs).
## License
Foam is licensed under the [MIT license](LICENSE).
@@ -58,85 +62,94 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- 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" 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" 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://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>
</tr>
</table>
<!-- markdownlint-enable -->
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

204
yarn.lock
View File

@@ -1656,6 +1656,17 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@jest/types@^26.6.2":
version "26.6.2"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e"
integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==
dependencies:
"@types/istanbul-lib-coverage" "^2.0.0"
"@types/istanbul-reports" "^3.0.0"
"@types/node" "*"
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@lerna/add@3.21.0":
version "3.21.0"
resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b"
@@ -2731,11 +2742,6 @@
dependencies:
"@types/node" "*"
"@types/graphlib@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@types/graphlib/-/graphlib-2.1.6.tgz#5c7b515bfadc08d737f2e84fadbd151117c73207"
integrity sha512-os2Xj+pV/iwLkLX17LWuXdPooA4Jf4xg8WSdKPUi0tCSseP95oikcA1irOgVl3K2QYnoXrjJT3qVZeQ1uskB7g==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -2756,6 +2762,21 @@
"@types/istanbul-lib-coverage" "*"
"@types/istanbul-lib-report" "*"
"@types/istanbul-reports@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821"
integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@26.x":
version "26.0.19"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.19.tgz#e6fa1e3def5842ec85045bd5210e9bb8289de790"
integrity sha512-jqHoirTG61fee6v6rwbnEuKhpSKih0tuhqeFbCmMmErhtu3BYlOZaXWjffgOstMM4S/3iQD31lI5bGLTrs97yQ==
dependencies:
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/jest@^24.0.15":
version "24.9.1"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534"
@@ -2830,6 +2851,11 @@
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.2.tgz#5bb52ee68d0f8efa9cc0099920e56be6cc4e37f3"
integrity sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA==
"@types/remove-markdown@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@types/remove-markdown/-/remove-markdown-0.1.1.tgz#c79d3000df412526186b2af3808b85bee68bc907"
integrity sha512-SCYOFMHUqyiJU5M0V2gBB6UDdBhPwma34j0vYX0JgWaqp/74ila2Ops1jt5tB/C1UQXVXqK+is61884bITn3LQ==
"@types/resolve@0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
@@ -2847,10 +2873,10 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
"@types/vscode@^1.45.1":
version "1.47.0"
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.47.0.tgz#4a4051c21ecaadcf383a2c4387bea282540e96de"
integrity sha512-nJA37ykkz9FYA0ZOQUSc3OZnhuzEW2vUhUEo4MiduUo82jGwwcLfyvmgd/Q7b0WrZAAceojGhZybg319L24bTA==
"@types/vscode@^1.47.1":
version "1.52.0"
resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.52.0.tgz#61917968dd403932127fc4004a21fd8d69e4f61c"
integrity sha512-Kt3bvWzAvvF/WH9YEcrCICDp0Z7aHhJGhLJ1BxeyNP6yRjonWqWnAIh35/pXAjswAnWOABrYlF7SwXR9+1nnLA==
"@types/yargs-parser@*":
version "15.0.0"
@@ -4679,6 +4705,11 @@ diff-sequences@^26.0.0:
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.0.0.tgz#0760059a5c287637b842bd7085311db7060e88a6"
integrity sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg==
diff-sequences@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@@ -5367,7 +5398,7 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
expect@^24.9.0:
expect@^24.1.0, expect@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca"
integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==
@@ -5461,6 +5492,11 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fast-array-diff@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-array-diff/-/fast-array-diff-1.0.0.tgz#4ff6315e7cd9a8a9cbcf59a5c7e2ae2bdcee6763"
integrity sha512-ogkVOYhBglkIsl5pjxZ7dIuItFG+1Ihh1kCChVaNgPZ30gQggRcuSjm4v4lAoS8ruGu8wFnHWYMEMO8dtKNgoA==
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -6024,12 +6060,15 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
graphlib@^2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
gray-matter@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.2.tgz#9aa379e3acaf421193fce7d2a28cebd4518ac454"
integrity sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==
dependencies:
lodash "^4.17.15"
js-yaml "^3.11.0"
kind-of "^6.0.2"
section-matter "^1.0.0"
strip-bom-string "^1.0.0"
growly@^1.3.0:
version "1.3.0"
@@ -7039,6 +7078,16 @@ jest-diff@^24.3.0, jest-diff@^24.9.0:
jest-get-type "^24.9.0"
pretty-format "^24.9.0"
jest-diff@^26.0.0:
version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
dependencies:
chalk "^4.0.0"
diff-sequences "^26.6.2"
jest-get-type "^26.3.0"
pretty-format "^26.6.2"
jest-diff@^26.1.0:
version "26.1.0"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.1.0.tgz#00a549bdc936c9691eb4dc25d1fbd78bf456abb2"
@@ -7177,6 +7226,25 @@ jest-environment-node@^26.2.0:
jest-mock "^26.2.0"
jest-util "^26.2.0"
jest-environment-vscode@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/jest-environment-vscode/-/jest-environment-vscode-1.0.0.tgz#96367fe8531047e64359e0682deafc973bfaea91"
integrity sha512-VKlj5j5pNurFEwWPaDiX1kBgmhWqcJTAZsvEX1x5lh0/+5myjk+qipEs/dPJVRbBPb3XFxiR48XzGn+wOU7SSQ==
jest-extended@^0.11.5:
version "0.11.5"
resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-0.11.5.tgz#f063b3f1eaadad8d7c13a01f0dfe0f538d498ccf"
integrity sha512-3RsdFpLWKScpsLD6hJuyr/tV5iFOrw7v6YjA3tPdda9sJwoHwcMROws5gwiIZfcwhHlJRwFJB2OUvGmF3evV/Q==
dependencies:
expect "^24.1.0"
jest-get-type "^22.4.3"
jest-matcher-utils "^22.0.0"
jest-get-type@^22.4.3:
version "22.4.3"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4"
integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==
jest-get-type@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
@@ -7187,6 +7255,11 @@ jest-get-type@^26.0.0:
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.0.0.tgz#381e986a718998dbfafcd5ec05934be538db4039"
integrity sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==
jest-get-type@^26.3.0:
version "26.3.0"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
jest-haste-map@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
@@ -7340,6 +7413,15 @@ jest-leak-detector@^26.2.0:
jest-get-type "^26.0.0"
pretty-format "^26.2.0"
jest-matcher-utils@^22.0.0:
version "22.4.3"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz#4632fe428ebc73ebc194d3c7b65d37b161f710ff"
integrity sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA==
dependencies:
chalk "^2.0.1"
jest-get-type "^22.4.3"
pretty-format "^22.4.3"
jest-matcher-utils@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073"
@@ -7959,6 +8041,14 @@ js-tokens@^3.0.2:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
js-yaml@^3.11.0:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^3.13.0, js-yaml@^3.13.1:
version "3.14.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
@@ -8443,6 +8533,13 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
macos-release@^2.2.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.0.tgz#837b39fc01785c3584f103c5599e0f0c8068b49e"
@@ -8732,7 +8829,7 @@ mkdirp-promise@^5.0.1:
dependencies:
mkdirp "*"
mkdirp@*:
mkdirp@*, mkdirp@1.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
@@ -9640,6 +9737,14 @@ prettier@^1.19.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
pretty-format@^22.4.3:
version "22.4.3"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f"
integrity sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ==
dependencies:
ansi-regex "^3.0.0"
ansi-styles "^3.2.0"
pretty-format@^24.9.0:
version "24.9.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9"
@@ -9650,6 +9755,16 @@ pretty-format@^24.9.0:
ansi-styles "^3.2.0"
react-is "^16.8.4"
pretty-format@^26.0.0, pretty-format@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
dependencies:
"@jest/types" "^26.6.2"
ansi-regex "^5.0.0"
ansi-styles "^4.0.0"
react-is "^17.0.1"
pretty-format@^26.1.0:
version "26.1.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.1.0.tgz#272b9cd1f1a924ab5d443dc224899d7a65cb96ec"
@@ -9828,6 +9943,11 @@ react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^17.0.1:
version "17.0.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
read-cmd-shim@^1.0.1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16"
@@ -10134,6 +10254,11 @@ remark-wiki-link@^0.0.4:
"@babel/runtime" "^7.4.4"
unist-util-map "^1.0.3"
remove-markdown@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.3.0.tgz#5e4b667493a93579728f3d52ecc1db9ca505dc98"
integrity sha1-XktmdJOpNXlyjz1S7MHbnKUF3Jg=
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -10457,6 +10582,14 @@ saxes@^5.0.0:
dependencies:
xmlchars "^2.2.0"
section-matter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==
dependencies:
extend-shallow "^2.0.1"
kind-of "^6.0.0"
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
@@ -10477,6 +10610,13 @@ semver@7.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
semver@7.x:
version "7.3.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
dependencies:
lru-cache "^6.0.0"
semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
@@ -10949,6 +11089,11 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
strip-bom-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
integrity sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=
strip-bom@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
@@ -11371,6 +11516,23 @@ ts-jest@^24.0.2:
semver "^5.5"
yargs-parser "10.x"
ts-jest@^26.4.4:
version "26.4.4"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49"
integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg==
dependencies:
"@types/jest" "26.x"
bs-logger "0.x"
buffer-from "1.x"
fast-json-stable-stringify "2.x"
jest-util "^26.1.0"
json5 "2.x"
lodash.memoize "4.x"
make-error "1.x"
mkdirp "1.x"
semver "7.x"
yargs-parser "20.x"
ts-node@^8:
version "8.10.2"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d"
@@ -12143,6 +12305,11 @@ yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.0, yaml@^1.7.2:
version "1.10.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
@@ -12155,6 +12322,11 @@ yargs-parser@10.x:
dependencies:
camelcase "^4.1.0"
yargs-parser@20.x:
version "20.2.4"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==
yargs-parser@^13.1.2:
version "13.1.2"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"