Compare commits

...

29 Commits

Author SHA1 Message Date
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
86 changed files with 2129 additions and 960 deletions

View File

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

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

@@ -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,90 @@ 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>
</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

@@ -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]]
@@ -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.9.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": [
@@ -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.9.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.9.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.9.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.9.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.9.0",
"ora": "^4.0.4",
"tslib": "^1"
},

View File

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

@@ -66,7 +66,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);
@@ -100,7 +100,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.9.0",
"license": "MIT",
"files": [
"dist"

View File

@@ -1,4 +1,4 @@
import { createGraph } from './note-graph';
import { createGraph } from './model/note-graph';
import { createMarkdownParser } from './markdown-provider';
import { FoamConfig, Foam, Services } from './index';
import { loadPlugins } from './plugins';
@@ -36,9 +36,8 @@ export const bootstrap = async (config: FoamConfig, services: Services) => {
const content = await services.dataStore.read(uri);
graph.setNote(await parser.parse(uri, content));
});
services.dataStore.onDidDelete(async uri => {
const note = graph.getNoteByURI(uri);
note && graph.deleteNote(note.id);
services.dataStore.onDidDelete(uri => {
graph.deleteNote(uri);
});
return {

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,6 +1,6 @@
import { Note, NoteLink } from './types';
import { Note, NoteLink } from './model/note';
import { URI } from './common/uri';
import { NoteGraph, NoteGraphAPI } from './note-graph';
import { NoteGraph, NoteGraphAPI } from './model/note-graph';
import { FoamConfig } from './config';
import { IDataStore, FileDataStore } from './services/datastore';
import { ILogger } from './utils/log';
@@ -24,6 +24,8 @@ export {
generateHeading,
generateLinkReferences,
getKebabCaseFileName,
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER,
} from './janitor';
export { applyTextEdit } from './janitor/apply-text-edit';
@@ -34,11 +36,6 @@ export { bootstrap } from './bootstrap';
export { NoteGraph, NoteGraphAPI, Note, NoteLink, URI };
export {
LINK_REFERENCE_DEFINITION_HEADER,
LINK_REFERENCE_DEFINITION_FOOTER,
} from './definitions';
export interface Services {
dataStore: IDataStore;
}

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 { NoteGraphAPI } from '../model/note-graph';
import { Note } from '../model/note';
import {
createMarkdownReferences,
stringifyMarkdownLinkReferenceDefinition,
} from '../markdown-provider';
import { getHeadingFromFileName } from '../utils';
import { getHeadingFromFileName, uriToSlug } from '../utils';
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,7 +19,7 @@ export interface TextEdit {
}
export const generateLinkReferences = (
note: GraphNote,
note: Note,
ng: NoteGraphAPI,
includeExtensions: boolean
): TextEdit | null => {
@@ -30,7 +29,7 @@ export const generateLinkReferences = (
const markdownReferences = createMarkdownReferences(
ng,
note.id,
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,15 +8,35 @@ 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 { NoteGraphAPI } from './model/note-graph';
import { NoteLinkDefinition, Note, NoteParser } from './model/note';
import { dropExtension, extractHashtags, extractTagsFromProp } from './utils';
import { uriToSlug, computeRelativePath, getBasename } from './utils/uri';
import { ID } from './types';
import {
uriToSlug,
computeRelativePath,
getBasename,
parseUri,
} from './utils/uri';
import { ParserPlugin } from './plugins';
import { Logger } from './utils/log';
import { URI } from './common/uri';
/**
* 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',
onWillVisitTree: (tree, note) => {
@@ -41,7 +62,7 @@ const titlePlugin: ParserPlugin = {
},
onDidVisitTree: (tree, note) => {
if (note.title == null) {
note.title = getBasename(note.source.uri);
note.title = getBasename(note.uri);
}
},
};
@@ -56,6 +77,19 @@ const wikilinkPlugin: ParserPlugin = {
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 +160,13 @@ export function createMarkdownParser(extraPlugins: ParserPlugin[]): NoteParser {
const eol = detectNewline(markdown) || os.EOL;
var note: Note = {
slug: uriToSlug(uri),
uri: uri,
properties: {},
title: null,
tags: new Set(),
links: [],
definitions: [],
source: {
uri: uri,
text: markdown,
contentStart: tree.position!.start,
end: tree.position!.end,
@@ -236,23 +269,26 @@ export function stringifyMarkdownLinkReferenceDefinition(
}
export function createMarkdownReferences(
graph: NoteGraphAPI,
noteId: ID,
noteUri: URI,
includeExtension: boolean
): NoteLinkDefinition[] {
const source = graph.getNote(noteId);
const source = graph.getNote(noteUri);
// Should never occur since we're already in a file,
// but better safe than sorry.
if (!source) {
console.warn(
`Note ${noteId} was not added to NoteGraph before attempting to generate markdown reference list`
`Note ${noteUri} was not added to NoteGraph before attempting to generate markdown reference list`
);
return [];
}
return graph
.getForwardLinks(noteId)
.getForwardLinks(noteUri)
.map(link => {
if (link.link.type !== 'wikilink') {
return null;
}
let target = graph.getNote(link.to);
// if we don't find the target by ID we search the graph by slug
if (!target) {
@@ -268,15 +304,12 @@ export function createMarkdownReferences(
// 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.`
`Warning: Link '${link.to}' in '${noteUri}' points to a non-existing note.`
);
return null;
}
const relativePath = computeRelativePath(
source.source.uri,
target.source.uri
);
const relativePath = computeRelativePath(source.uri, target.uri);
const pathToNote = includeExtension
? relativePath
@@ -286,7 +319,7 @@ export function createMarkdownReferences(
return {
label: link.link.slug,
url: pathToNote,
title: target.title || target.slug,
title: target.title || uriToSlug(target.uri),
};
})
.filter(Boolean)

View File

@@ -0,0 +1,171 @@
import { Graph } from 'graphlib';
import { URI } from '../common/uri';
import { Note, NoteLink } from '../model/note';
import {
computeRelativeURI,
nameToSlug,
isSome,
uriToSlug,
parseUri,
} from '../utils';
import { Event, Emitter } from '../common/event';
export interface GraphConnection {
from: URI;
to: URI;
link: NoteLink;
}
export type NoteGraphEventHandler = (e: { note: Note }) => void;
export type NotesQuery = { slug: string } | { title: string };
export interface NoteGraphAPI {
setNote(note: Note): Note;
deleteNote(noteUri: URI): Note | null;
getNotes(query?: NotesQuery): Note[];
getNote(noteUri: URI): Note | null;
getAllLinks(noteUri: URI): GraphConnection[];
getForwardLinks(noteUri: URI): GraphConnection[];
getBacklinks(noteUri: URI): GraphConnection[];
onDidAddNote: Event<Note>;
onDidUpdateNote: Event<Note>;
onDidDeleteNote: Event<Note>;
}
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);
};
const uriToId = (uri: URI) => uri.path;
export class NoteGraph implements NoteGraphAPI {
onDidAddNote: Event<Note>;
onDidUpdateNote: Event<Note>;
onDidDeleteNote: Event<Note>;
private graph: Graph;
private onDidAddNoteEmitter = new Emitter<Note>();
private onDidUpdateNoteEmitter = new Emitter<Note>();
private onDidDeleteEmitter = new Emitter<Note>();
constructor() {
this.graph = new Graph();
this.onDidAddNote = this.onDidAddNoteEmitter.event;
this.onDidUpdateNote = this.onDidUpdateNoteEmitter.event;
this.onDidDeleteNote = this.onDidDeleteEmitter.event;
}
public setNote(note: Note): Note {
const oldNote = this.getNote(note.uri);
if (isSome(oldNote)) {
this.removeForwardLinks(note.uri);
}
this.graph.setNode(uriToId(note.uri), note);
note.links.forEach(link => {
let targetUri = null;
if (link.type === 'wikilink') {
const definitionUri = note.definitions.find(
def => def.label === link.slug
)?.url;
targetUri = computeRelativeURI(note.uri, definitionUri ?? link.slug);
} else {
targetUri = parseUri(note.uri, link.target);
}
const connection: GraphConnection = {
from: note.uri,
to: targetUri,
link: link,
};
this.graph.setEdge(uriToId(note.uri), uriToId(targetUri), connection);
});
isSome(oldNote)
? this.onDidUpdateNoteEmitter.fire(note)
: this.onDidAddNoteEmitter.fire(note);
return note;
}
public deleteNote(noteUri: URI): Note | null {
return this.doDelete(noteUri, true);
}
private doDelete(noteUri: URI, fireEvent: boolean): Note | null {
const note = this.getNote(noteUri);
if (isSome(note)) {
if (this.getBacklinks(noteUri).length >= 1) {
this.graph.setNode(uriToId(noteUri), null); // Changes node to the "no file" style
} else {
this.graph.removeNode(uriToId(noteUri));
}
fireEvent && this.onDidDeleteEmitter.fire(note);
}
return note;
}
public getNotes(query?: NotesQuery): Note[] {
// prettier-ignore
const filterFn =
query == null ? (note: Note | null) => note != null
: 'slug' in query ? (note: Note | null) => note && [nameToSlug(query.slug), query.slug].includes(uriToSlug(note.uri))
: '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(noteUri: URI): Note | null {
return this.graph.node(uriToId(noteUri)) ?? null;
}
public getAllLinks(noteUri: URI): GraphConnection[] {
return (this.graph.nodeEdges(uriToId(noteUri)) || []).map(edge =>
this.graph.edge(edge.v, edge.w)
);
}
public getForwardLinks(noteUri: URI): GraphConnection[] {
return (this.graph.outEdges(uriToId(noteUri)) || []).map(edge =>
this.graph.edge(edge.v, edge.w)
);
}
public removeForwardLinks(noteUri: URI) {
(this.graph.outEdges(uriToId(noteUri)) || []).forEach(edge => {
this.graph.removeEdge(edge);
});
}
public getBacklinks(noteUri: URI): GraphConnection[] {
return (this.graph.inEdges(uriToId(noteUri)) || []).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,
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

@@ -1,13 +1,8 @@
// 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';
import { URI } from '../common/uri';
export { Position, Point };
export type ID = string;
export interface NoteSource {
uri: URI;
text: string;
contentStart: Point;
end: Point;
@@ -20,8 +15,13 @@ export interface WikiLink {
position: Position;
}
// at the moment we only model wikilink
export type NoteLink = WikiLink;
export interface DirectLink {
type: 'link';
label: string;
target: string;
}
export type NoteLink = WikiLink | DirectLink;
export interface NoteLinkDefinition {
label: string;
@@ -31,8 +31,8 @@ export interface NoteLinkDefinition {
}
export interface Note {
uri: URI;
title: string | null;
slug: string; // note: this slug is not necessarily unique
properties: any;
// sections: NoteSection[]
tags: Set<string>;

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,8 @@ 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 { Middleware } from '../model/note-graph';
import { Note } from '../model/note';
import unified from 'unified';
import { FoamConfig } from '../config';
import { Logger } from '../utils/log';

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

View File

@@ -1,5 +1,5 @@
import { NoteGraph, createGraph } from '../src/note-graph';
import { NoteLinkDefinition, Note } from '../src/types';
import { NoteGraph, createGraph } from '../src/model/note-graph';
import { NoteLinkDefinition, Note } from '../src/model/note';
import { uriToSlug } from '../src/utils';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
@@ -15,32 +15,46 @@ 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 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),
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,
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 ?? '',
},
};
@@ -56,7 +70,7 @@ describe('Note graph', () => {
expect(
graph
.getNotes()
.map(n => n.slug)
.map(n => uriToSlug(n.uri))
.sort()
).toEqual(['page-a', 'page-b', 'page-c']);
});
@@ -73,7 +87,10 @@ describe('Note graph', () => {
graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)!.slug)
graph
.getForwardLinks(noteB.uri)
.map(link => graph.getNote(link.to)!.uri)
.map(uriToSlug)
).toEqual(['page-a']);
});
@@ -89,14 +106,50 @@ describe('Note graph', () => {
graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)!.slug)
graph
.getBacklinks(noteA.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b']);
});
it('Detects backlinks of direct links', () => {
const graph = new NoteGraph();
const noteA = createTestNote({
uri: '/path/to/page-a.md',
});
// connected via absolute path
const noteB = createTestNote({
uri: '/page-b.md',
links: [{ to: noteA.uri.path }],
});
// connected via relative path
const noteC = createTestNote({
uri: '/path/docs/page-c.md',
links: [{ to: '../to/page-a.md' }],
});
// not connected - wrong path
const noteD = createTestNote({
uri: '/path/docs/page-d.md',
links: [{ to: '../to/another/page-a.md' }],
});
graph.setNote(noteA);
graph.setNote(noteB);
graph.setNote(noteC);
graph.setNote(noteD);
expect(
graph
.getBacklinks(noteA.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b', 'page-c']);
});
it('Returns null when accessing non-existing node', () => {
const graph = new NoteGraph();
graph.setNote(createTestNote({ uri: 'page-a' }));
expect(graph.getNote('non-existing')).toBeNull();
expect(graph.getNote(strToUri('non-existing'))).toBeNull();
});
it('Allows adding edges to non-existing documents', () => {
@@ -108,7 +161,7 @@ describe('Note graph', () => {
})
);
expect(graph.getNote('non-existing')).toBeNull();
expect(graph.getNote(strToUri('non-existing'))).toBeNull();
});
it('Updates links when modifying note', () => {
@@ -123,13 +176,22 @@ describe('Note graph', () => {
const noteC = graph.setNote(createTestNote({ uri: '/page-c.md' }));
expect(
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)?.slug)
graph
.getForwardLinks(noteB.uri)
.map(link => graph.getNote(link.to)!.uri)
.map(uriToSlug)
).toEqual(['page-a']);
expect(
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteA.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b']);
expect(
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteC.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual([]);
graph.setNote(
@@ -140,19 +202,31 @@ describe('Note graph', () => {
);
expect(
graph.getForwardLinks(noteB.id).map(link => graph.getNote(link.to)?.slug)
graph
.getForwardLinks(noteB.uri)
.map(link => graph.getNote(link.to)!.uri)
.map(uriToSlug)
).toEqual(['page-c']);
expect(
graph.getBacklinks(noteA.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteA.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual([]);
expect(
graph.getBacklinks(noteC.id).map(link => graph.getNote(link.from)?.slug)
graph
.getBacklinks(noteC.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).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)
graph
.getBacklinks(noteC.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-b']);
});
@@ -176,17 +250,22 @@ describe('Note graph', () => {
})
);
graph.deleteNote(noteA.id);
graph.deleteNote(noteA.uri);
expect(
graph.getForwardLinks(noteB.id).map(link => link?.link?.slug)
graph.getForwardLinks(noteB.uri).map(link => (link as any)?.link?.slug)
).toEqual(['page-a']);
expect(graph.getNote(noteA.id)).toBeNull();
expect(graph.getNote(noteA.uri)).toBeNull();
graph.deleteNote(noteC.id);
graph.deleteNote(noteC.uri);
expect(
graph.getForwardLinks(noteC.id).map(link => link?.link?.slug)
graph.getForwardLinks(noteC.uri).map(link => (link as any)?.link?.slug)
).toEqual([]);
expect(graph.getNotes().map(note => note.slug)).toEqual(['page-b']);
expect(
graph
.getNotes()
.map(note => note.uri)
.map(uriToSlug)
).toEqual(['page-b']);
});
});
@@ -201,7 +280,7 @@ describe('Graph querying', () => {
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);
expect(graph.getNotes({ slug: uriToSlug(note.uri) }).length).toEqual(1);
});
it('finds a note by slug when there is more than one', () => {
@@ -262,7 +341,7 @@ describe('graph events', () => {
const note = graph.setNote(
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
);
graph.deleteNote(note.id);
graph.deleteNote(note.uri);
expect(callback).toHaveBeenCalledTimes(1);
listener.dispose();
});
@@ -270,10 +349,10 @@ describe('graph events', () => {
const graph = new NoteGraph();
const callback = jest.fn();
const listener = graph.onDidDeleteNote(callback);
const note = graph.setNote(
graph.setNote(
createTestNote({ uri: '/dir1/page-a.md', title: 'My Title' })
);
graph.deleteNote('non-existing-note');
graph.deleteNote(strToUri('non-existing-note'));
expect(callback).toHaveBeenCalledTimes(0);
listener.dispose();
});
@@ -309,7 +388,7 @@ describe('graph events', () => {
expect(updateCallback).toHaveBeenCalledTimes(2);
expect(deleteCallback).toHaveBeenCalledTimes(0);
graph.deleteNote(note.id);
graph.deleteNote(note.uri);
expect(addCallback).toHaveBeenCalledTimes(1);
expect(updateCallback).toHaveBeenCalledTimes(2);
expect(deleteCallback).toHaveBeenCalledTimes(1);
@@ -319,7 +398,7 @@ describe('graph events', () => {
});
describe('graph middleware', () => {
it('can intercept calls to the graph', async () => {
it('can intercept calls to the graph', () => {
const graph = createGraph([
next => ({
setNote: note => {

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { NoteGraphAPI } from '../../src/note-graph';
import { NoteGraphAPI } from '../../src/model/note-graph';
import { generateHeading } from '../../src/janitor';
import { bootstrap } from '../../src/bootstrap';
import { createConfigFromFolders } from '../../src/config';

View File

@@ -1,9 +1,8 @@
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, NoteGraphAPI } from '../../src';
import { FileDataStore } from '../../src/services/datastore';
import { Logger } from '../../src/utils/log';
import { URI } from '../../src/common/uri';
@@ -136,7 +135,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 +148,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 { NoteGraph } from '../src/model/note-graph';
import { ParserPlugin } from '../src/plugins';
import { URI } from '../src/common/uri';
import { Logger } from '../src/utils/log';
import { uriToSlug } from '../src/utils';
Logger.setLevel('error');
@@ -51,11 +53,56 @@ describe('Markdown loader', () => {
expect(
graph
.getNotes()
.map(n => n.slug)
.map(n => n.uri)
.map(uriToSlug)
.sort()
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
});
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);
});
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 graph = new NoteGraph();
const noteA = graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
@@ -65,10 +112,16 @@ describe('Markdown loader', () => {
graph.setNote(createNoteFromMarkdown('/page e.md', pageE));
expect(
graph.getBacklinks(noteB.id).map(link => graph.getNote(link.from)!.slug)
graph
.getBacklinks(noteB.uri)
.map(link => graph.getNote(link.from)!.uri)
.map(uriToSlug)
).toEqual(['page-a']);
expect(
graph.getForwardLinks(noteA.id).map(link => graph.getNote(link.to)!.slug)
graph
.getForwardLinks(noteA.uri)
.map(link => graph.getNote(link.to)!.uri)
.map(uriToSlug)
).toEqual(['page-b', 'page-c', 'page-d', 'page-e']);
});
});
@@ -78,7 +131,7 @@ describe('Note Title', () => {
const graph = new NoteGraph();
const note = graph.setNote(createNoteFromMarkdown('/page-a.md', pageA));
const pageANoteTitle = graph.getNote(note.id)!.title;
const pageANoteTitle = graph.getNote(note.uri)!.title;
expect(pageANoteTitle).toBe('Page A');
});
@@ -93,7 +146,7 @@ This file has no heading.
)
);
const pageANoteTitle = graph.getNote(note.id)!.title;
const pageANoteTitle = graph.getNote(note.uri)!.title;
expect(pageANoteTitle).toEqual('page-d');
});
@@ -113,7 +166,7 @@ date: 20-12-12
)
);
const pageENoteTitle = graph.getNote(note.id)!.title;
const pageENoteTitle = graph.getNote(note.uri)!.title;
expect(pageENoteTitle).toBe('Note Title');
});
@@ -151,7 +204,7 @@ date: 20-12-12
date: '20-12-12',
};
const actual: any = graph.getNote(note.id)!.properties;
const actual: any = graph.getNote(note.uri)!.properties;
expect(actual.title).toBe(expected.title);
expect(actual.date).toBe(expected.date);
@@ -173,7 +226,7 @@ date: 20-12-12
const expected = {};
const actual = graph.getNote(note.id)!.properties;
const actual = graph.getNote(note.uri)!.properties;
expect(actual).toEqual(expected);
});
@@ -196,7 +249,7 @@ title: - one
const expected = {};
const actual = graph.getNote(note.id)!.properties;
const actual = graph.getNote(note.uri)!.properties;
expect(actual).toEqual(expected);
});
@@ -211,7 +264,7 @@ describe('wikilinks definitions', () => {
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB));
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC));
const noExtRefs = createMarkdownReferences(graph, noteA.id, false);
const noExtRefs = createMarkdownReferences(graph, noteA.uri, false);
expect(noExtRefs.map(r => r.url)).toEqual(['page-b', 'page-c']);
});
@@ -223,7 +276,7 @@ describe('wikilinks definitions', () => {
graph.setNote(createNoteFromMarkdown('/dir1/page-b.md', pageB));
graph.setNote(createNoteFromMarkdown('/dir1/page-c.md', pageC));
const extRefs = createMarkdownReferences(graph, noteA.id, true);
const extRefs = createMarkdownReferences(graph, noteA.uri, true);
expect(extRefs.map(r => r.url)).toEqual(['page-b.md', 'page-c.md']);
});
@@ -235,7 +288,7 @@ describe('wikilinks definitions', () => {
graph.setNote(createNoteFromMarkdown('/dir2/page-b.md', pageB));
graph.setNote(createNoteFromMarkdown('/dir3/page-c.md', pageC));
const extRefs = createMarkdownReferences(graph, noteA.id, true);
const extRefs = createMarkdownReferences(graph, noteA.uri, true);
expect(extRefs.map(r => r.url)).toEqual([
'../dir2/page-b.md',
'../dir3/page-c.md',
@@ -298,7 +351,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,7 @@
import path from 'path';
import { loadPlugins } from '../src/plugins';
import { createMarkdownParser } from '../src/markdown-provider';
import { createGraph } from '../src/note-graph';
import { createGraph } from '../src/model/note-graph';
import { createTestNote } from './core.test';
import { FoamConfig, createConfigFromObject } from '../src/config';
import { URI } from '../src/common/uri';

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

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

View File

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

View File

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

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,13 +2,12 @@
"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.9.0",
"license": "MIT",
"publisher": "foam",
"engines": {
@@ -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",
@@ -196,12 +280,16 @@
"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.9.0",
"micromatch": "^4.0.2"
}
}

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);
@@ -41,7 +38,7 @@ export async function activate(context: ExtensionContext) {
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,18 @@
import { env, window, Uri, Position, Selection, commands } from 'vscode';
import * as vscode from 'vscode';
describe('copyWithoutBrackets', () => {
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,125 @@
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 } 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.notes.onDidAddNote(onFoamChanged);
const noteUpdatedListener = foam.notes.onDidUpdateNote(onFoamChanged);
const noteDeletedListener = foam.notes.onDidDeleteNote(onFoamChanged);
panel.onDidDispose(() => {
noteAddedListener.dispose();
noteUpdatedListener.dispose();
noteDeletedListener.dispose();
panel = undefined;
});
vscode.window.onDidChangeActiveTextEditor(e => {
if (e.document.uri.scheme === 'file') {
const note = foam.notes.getNote(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)
const links = foam.notes.getForwardLinks(n.uri);
graph.nodes[n.uri.path] = {
id: n.uri.path,
type: n.properties.type ?? 'note',
uri: n.uri,
title: cutTitle(n.title),
};
links.forEach(link => {
if (!(link.to in graph.nodes)) {
graph.nodes[link.to] = {
if (!(link.to.path in graph.nodes)) {
graph.nodes[link.to.path] = {
id: link.to,
type: "nonExistingNote",
type: 'placeholder',
uri: `virtual:${link.to}`,
title: cutTitle(link.link.slug)
title:
'slug' in link.link
? cutTitle(link.link.slug)
: cutTitle(link.link.label),
};
}
graph.edges.add({
source: link.from,
target: link.to
source: link.from.path,
target: link.to.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 +128,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.notes.getNote(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 +166,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,31 +4,31 @@ 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,
} from 'foam-core';
import {
getWikilinkDefinitionSetting,
LinkReferenceDefinitionsSetting
} from "../settings";
import { astPositionToVsCodePosition } from "../utils";
LinkReferenceDefinitionsSetting,
} from '../settings';
import { astPositionToVsCodePosition } 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) {
@@ -44,7 +44,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)
);
@@ -75,8 +75,8 @@ async function runJanitor(foam: Foam) {
const dirtyTextDocuments = workspace.textDocuments.filter(
textDocument =>
(textDocument.languageId === "markdown" ||
textDocument.languageId === "mdx") &&
(textDocument.languageId === 'markdown' ||
textDocument.languageId === 'mdx') &&
textDocument.isDirty
);
@@ -85,11 +85,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();
@@ -125,7 +125,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 +135,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
@@ -151,6 +151,7 @@ async function runJanitor(foam: Foam) {
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 +170,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.notes.getNotes();
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,123 @@
import { OrphansProvider, Directory, OrphansProviderConfig } from './orphans';
import { OrphansConfigGroupBy } from '../settings';
describe('orphans', () => {
// Rough mocks of NoteGraphAPI
const orphanA = {
uri: { fsPath: '/path/orphan-a.md', path: '/path/orphan-a.md' },
title: 'Orphan A',
links: [],
};
const orphanB = {
uri: { fsPath: '/path-bis/orphan-b.md', path: '/path-bis/orphan-b.md' },
title: 'Orphan B',
links: [],
};
const orphanC = {
uri: {
fsPath: '/path-exclude/orphan-c.md',
path: '/path-exclude/orphan-c.md',
},
title: 'Orphan C',
links: [],
};
const notOrphanNote = {
uri: { fsPath: '/path/not-orphan.md', path: '/path/not-orphan.md' },
title: 'Not-Orphan',
links: [{ from: '', to: '' }],
};
const notes = [orphanA, orphanB, orphanC, notOrphanNote];
const foam = {
notes: {
getNotes: () => notes,
getAllLinks: (uri: { path: string }) => {
switch (uri.path) {
case orphanA.uri.fsPath:
return orphanA.links;
case orphanB.uri.fsPath:
return orphanB.links;
case orphanC.uri.fsPath:
return orphanC.links;
default:
return notOrphanNote.links;
}
},
},
} 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(foam, 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(foam, 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(foam, 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(foam, 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,184 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { Foam, Note, URI } from 'foam-core';
import micromatch from 'micromatch';
import {
getOrphansConfig,
OrphansConfig,
OrphansConfigGroupBy,
} from '../settings';
import { FoamFeature } from '../types';
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, {
...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.notes.onDidUpdateNote(() => 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.fsPath;
constructor(private foam: Foam, 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);
}
private computeOrphans(): void {
this.orphans = this.foam.notes
.getNotes()
.filter(note => !this.foam.notes.getAllLinks(note.uri).length)
.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.fsPath.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 = this.description;
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}`;
this.tooltip = this.description;
}
iconPath = new vscode.ThemeIcon('folder');
contextValue = 'directory';
}

View File

@@ -1,6 +1,6 @@
import * as vscode from "vscode";
import { FoamFeature } from "../../types";
import { Foam, Note } from "foam-core";
import * as vscode from 'vscode';
import { FoamFeature } from '../../types';
import { Foam, Note } from 'foam-core';
const feature: FoamFeature = {
activate: async (
@@ -11,12 +11,12 @@ const feature: FoamFeature = {
const provider = new TagsProvider(foam);
context.subscriptions.push(
vscode.window.registerTreeDataProvider(
"foam-vscode.tags-explorer",
'foam-vscode.tags-explorer',
provider
)
);
foam.notes.onDidUpdateNote(() => provider.refresh());
}
},
};
export default feature;
@@ -29,7 +29,7 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
private tags: {
tag: string;
noteIds: string[];
noteUris: vscode.Uri[];
}[];
constructor(private foam: Foam) {
@@ -43,16 +43,16 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
private computeTags() {
const rawTags: {
[key: string]: string[];
[key: string]: vscode.Uri[];
} = this.foam.notes.getNotes().reduce((acc, note) => {
note.tags.forEach(tag => {
acc[tag] = acc[tag] ?? [];
acc[tag].push(note.id);
acc[tag].push(note.uri);
});
return acc;
}, {});
this.tags = Object.entries(rawTags)
.map(([tag, noteIds]) => ({ tag, noteIds }))
.map(([tag, noteUris]) => ({ tag, noteUris }))
.sort((a, b) => a.tag.localeCompare(b.tag));
}
@@ -62,18 +62,18 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
getChildren(element?: Tag): Thenable<TagTreeItem[]> {
if (element) {
const references: TagReference[] = element.noteIds.map(id => {
const references: TagReference[] = element.noteUris.map(id => {
const note = this.foam.notes.getNote(id);
return 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, noteUris }) => new Tag(tag, noteUris)
);
return Promise.resolve(tags.sort((a, b) => a.tag.localeCompare(b.tag)));
}
@@ -83,16 +83,19 @@ export class TagsProvider implements vscode.TreeDataProvider<TagTreeItem> {
type TagTreeItem = Tag | TagReference | TagSearch;
export class Tag extends vscode.TreeItem {
constructor(public readonly tag: string, public readonly noteIds: string[]) {
constructor(
public readonly tag: string,
public readonly noteUris: vscode.Uri[]
) {
super(tag, vscode.TreeItemCollapsibleState.Collapsed);
this.description = `${this.noteIds.length} reference${
this.noteIds.length !== 1 ? "s" : ""
this.description = `${this.noteUris.length} reference${
this.noteUris.length !== 1 ? 's' : ''
}`;
this.tooltip = this.description;
}
iconPath = new vscode.ThemeIcon("symbol-number");
contextValue = "tag";
iconPath = new vscode.ThemeIcon('symbol-number');
contextValue = 'tag';
}
export class TagSearch extends vscode.TreeItem {
@@ -101,21 +104,21 @@ 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 {
@@ -123,9 +126,9 @@ export class TagReference extends vscode.TreeItem {
constructor(tag: string, note: Note) {
super(note.title, vscode.TreeItemCollapsibleState.None);
this.title = note.title;
this.description = note.source.uri.path;
this.description = note.uri.path;
this.tooltip = this.description;
const resourceUri = note.source.uri;
const resourceUri = note.uri;
let selection: vscode.Range | null = null;
// TODO move search fn to core
const lines = note.source.text.split(/\r?\n/);
@@ -139,18 +142,18 @@ export class TagReference extends vscode.TreeItem {
// 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,8 +10,8 @@ import {
TextDocument,
window,
workspace,
Position
} from "vscode";
Position,
} from 'vscode';
import {
createMarkdownReferences,
@@ -19,33 +19,33 @@ import {
NoteGraphAPI,
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", () =>
commands.registerCommand('foam-vscode.update-wikilinks', () =>
updateReferenceList(foam.notes)
),
workspace.onWillSaveTextDocument(e => {
if (e.document.languageId === "markdown") {
if (e.document.languageId === 'markdown') {
updateDocumentInNoteGraph(foam, e.document);
e.waitUntil(updateReferenceList(foam.notes));
}
@@ -67,7 +67,7 @@ const feature: FoamFeature = {
updateDocumentInNoteGraph(foam, editor.document);
updateReferenceList(foam.notes);
});
}
},
};
function updateDocumentInNoteGraph(foam: Foam, document: TextDocument) {
@@ -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 => {
@@ -138,7 +138,7 @@ function generateReferenceList(
return [];
}
const note = foam.getNoteByURI(doc.uri);
const note = foam.getNote(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,
];
}
@@ -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,30 @@
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'],
});
} 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

@@ -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,10 @@ import {
TextEditor,
workspace,
Uri,
Selection
} from "vscode";
import * as fs from "fs";
import { Logger } from "foam-core";
Selection,
} from 'vscode';
import * as fs from 'fs';
import { Logger } from 'foam-core';
interface Point {
line: number;
@@ -18,34 +18,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 +61,7 @@ export function detectGeneratedCode(
if (headerLine < 0 || headerLine >= footerLine) {
return {
range: null,
lines: []
lines: [],
};
}
@@ -70,7 +70,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 +83,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 +103,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 +125,7 @@ export function removeBrackets(s: string): string {
return currentWord;
});
return modifiedWords.join(" ");
return modifiedWords.join(' ');
}
/**
@@ -135,9 +135,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(' ');
}
/**

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,20 @@
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. This values should only be set when all else failed. */
const styleFallback = {
background: '#202020',
fontSize: 12,
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 +27,21 @@ 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,
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 +50,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 +103,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 +129,41 @@ 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,
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()))
.d3Force('x', d3.forceX())
.d3Force('y', d3.forceY())
.d3Force('collide', d3.forceCollide(graph.nodeRelSize()))
.linkWidth(0.2)
.linkDirectionalParticles(1)
.linkDirectionalParticleWidth(link =>
getLinkState(link, model) === "highlighted" ? 1 : 0
getLinkState(link, model) === 'highlighted' ? 1 : 0
)
.nodeCanvasObject((node, ctx, globalScale) => {
const info = model.nodeInfo[node.id];
@@ -143,10 +173,10 @@ function initDataviz(channel) {
}
const size = sizeScale(info.neighbors.length);
const { fill, border } = getNodeColor(node.id, model);
const fontSize = style.fontSize / globalScale;
const fontSize = model.style.fontSize / globalScale;
let textColor = d3.rgb(fill);
textColor.opacity =
getNodeState(node.id, model) === "highlighted"
getNodeState(node.id, model) === 'highlighted'
? 1
: labelAlpha(globalScale);
const label = info.title;
@@ -161,16 +191,16 @@ function initDataviz(channel) {
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 +222,54 @@ 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 = style.node[info.type ?? 'note'] ?? style.node['note'];
switch (getNodeState(nodeId, model)) {
case "regular":
case 'regular':
return { fill: typeFill, border: typeFill };
case "lessened":
case 'lessened':
const darker = d3.hsl(typeFill).darker(3);
return { fill: darker, border: darker };
case "highlighted":
case 'highlighted':
return {
fill: typeFill,
border: style.highlightedForeground
border: 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":
case 'regular':
return d3.hsl(style.node.note).darker(2);
case "highlighted":
case 'highlighted':
return style.highlightedForeground;
case "lessened":
case 'lessened':
return d3.hsl(style.node.note).darker(4);
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 +283,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 +297,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 +332,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,
};

131
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-62-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,90 @@ 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>
</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!

142
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"
@@ -2756,6 +2767,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"
@@ -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==
@@ -7039,6 +7070,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 +7218,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 +7247,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 +7405,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"
@@ -8443,6 +8517,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 +8813,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 +9721,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 +9739,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 +9927,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"
@@ -10477,6 +10581,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"
@@ -11371,6 +11482,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 +12271,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 +12288,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"