mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 15:08:01 -05:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa311b2688 | ||
|
|
0fca141a7b | ||
|
|
6a4bd341ab | ||
|
|
764750f591 | ||
|
|
2686b9a365 | ||
|
|
5a6ef644bd | ||
|
|
d7c92f8284 | ||
|
|
c2e5e4bf2a | ||
|
|
9d0ba879d2 | ||
|
|
9606dcc64c | ||
|
|
d70e441790 | ||
|
|
dde11f8c6f | ||
|
|
cd9ee4d556 | ||
|
|
a8296c2c88 | ||
|
|
13a340eb1d | ||
|
|
d2dd979e70 | ||
|
|
4989796cb0 | ||
|
|
d24814d065 | ||
|
|
4a410d1f5c | ||
|
|
ccb92ad5ee | ||
|
|
e6512cffa8 | ||
|
|
1fa4f37d96 | ||
|
|
27b9b451ad | ||
|
|
362d6f8e09 | ||
|
|
cef8d2a532 | ||
|
|
22b837f252 | ||
|
|
07e02c2d69 | ||
|
|
931ad7a5b6 | ||
|
|
db7eb9775f |
@@ -1076,6 +1076,42 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Walshkev",
|
||||
"name": "Kevin Walsh ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/77123083?v=4",
|
||||
"profile": "https://github.com/Walshkev",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hereistheusername",
|
||||
"name": "Xinglan Liu",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/33437051?v=4",
|
||||
"profile": "http://hereistheusername.github.io/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Hegghammer",
|
||||
"name": "Thomas Hegghammer",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/64712218?v=4",
|
||||
"profile": "http://www.hegghammer.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PiotrAleksander",
|
||||
"name": "Piotr Mrzygłosz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6314591?v=4",
|
||||
"profile": "https://github.com/PiotrAleksander",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.vscode-test/
|
||||
.vscode-test-web/
|
||||
*.tsbuildinfo
|
||||
*.vsix
|
||||
*.log
|
||||
|
||||
BIN
docs/assets/images/pdf_output.png
Normal file
BIN
docs/assets/images/pdf_output.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -1,3 +1,8 @@
|
||||
---
|
||||
redirect_from:
|
||||
- /code-of-conduct
|
||||
---
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
We follow the [Contributor Covenant](https://www.contributor-covenant.org/) code of conduct.
|
||||
|
||||
@@ -257,6 +257,12 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://thara.dev"><img src="https://avatars.githubusercontent.com/u/1532891?v=4?s=60" width="60px;" alt="Tomochika Hara"/><br /><sub><b>Tomochika Hara</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=thara" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dcarosone"><img src="https://avatars.githubusercontent.com/u/11495017?v=4?s=60" width="60px;" alt="Daniel Carosone"/><br /><sub><b>Daniel Carosone</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dcarosone" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MABruni"><img src="https://avatars.githubusercontent.com/u/100445384?v=4?s=60" width="60px;" alt="Miguel Angel Bruni Montero"/><br /><sub><b>Miguel Angel Bruni Montero</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MABruni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Walshkev"><img src="https://avatars.githubusercontent.com/u/77123083?v=4?s=60" width="60px;" alt="Kevin Walsh "/><br /><sub><b>Kevin Walsh </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Walshkev" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://hereistheusername.github.io/"><img src="https://avatars.githubusercontent.com/u/33437051?v=4?s=60" width="60px;" alt="Xinglan Liu"/><br /><sub><b>Xinglan Liu</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hereistheusername" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.hegghammer.com"><img src="https://avatars.githubusercontent.com/u/64712218?v=4?s=60" width="60px;" alt="Thomas Hegghammer"/><br /><sub><b>Thomas Hegghammer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Hegghammer" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PiotrAleksander"><img src="https://avatars.githubusercontent.com/u/6314591?v=4?s=60" width="60px;" alt="Piotr Mrzygłosz"/><br /><sub><b>Piotr Mrzygłosz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=PiotrAleksander" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Daily Notes
|
||||
|
||||
Daily notes allow you to quickly create and access a new notes file for each day. This is a surpisingly effective and increasingly common strategy to organize notes and manage events.
|
||||
Daily notes allow you to quickly create and access a new notes file for each day. This is a surprisingly effective and increasingly common strategy to organize notes and manage events.
|
||||
|
||||
View today's note file by running the `Foam: Open Daily Note` command, by using the shortcut `alt+d` (note: shortcuts can be [overridden](https://code.visualstudio.com/docs/getstarted/keybindings)), or by using [#snippets](#Snippets). The name, location, and title of daily notes files is [#configurable](#Configuration).
|
||||
View today's note file by running the `Foam: Open Daily Note` command, by using the shortcut `alt+d` (note: shortcuts can be [overridden](https://code.visualstudio.com/docs/getstarted/keybindings)), or by using [#snippets](#Snippets). The name, location, and title of daily notes files are [#configurable](#Configuration).
|
||||
|
||||
## Roam-style Automatic Daily Notes
|
||||
|
||||
@@ -29,11 +29,11 @@ Create a link to a recent daily note using [snippets](https://code.visualstudio.
|
||||
|
||||
## Configuration
|
||||
|
||||
By default, Daily Notes will be created in a file called `yyyy-mm-dd.md` in the workspace's `journals` folder, with a heading `yyyy-mm-dd`.
|
||||
By default, Daily Notes will be created in a file called `yyyy-mm-dd.md` in the workspace's `journals` folder, with the heading `yyyy-mm-dd`.
|
||||
|
||||
These settings can be overridden in your workspace or global `.vscode/settings.json` file, using the [**dateformat** date masking syntax](https://github.com/felixge/node-dateformat#mask-options):
|
||||
|
||||
It's possible to customize path and heading of your daily notes, by following the [dateformat masking syntax](https://github.com/felixge/node-dateformat#mask-options).
|
||||
It's possible to customize the path and heading of your daily notes, by following the [dateformat masking syntax](https://github.com/felixge/node-dateformat#mask-options).
|
||||
The following properties can be used:
|
||||
|
||||
```json
|
||||
@@ -45,7 +45,7 @@ The following properties can be used:
|
||||
|
||||
The above configuration would create a file `journal/daily-note-2020-07-25.mdx`, with the heading `Journal Entry, Sunday, July 25`.
|
||||
|
||||
> NOTE: It is possible to set the filepath of a daily note according to the date using the special [[note-properties]] configurable for [[Note Templates]]. Specifically see [[note-templates#Example of date-based|Example of date-based filepath]]. Using the template property will override any setting configured through `.vscode/settings.json`.
|
||||
> NOTE: It is possible to set the filepath of a daily note according to the date using the special [[note-properties]] configurable for [[Note Templates]]. Specifically, see [[note-templates#Example of date-based|Example of date-based filepath]]. Using the template property will override any setting configured through `.vscode/settings.json`.
|
||||
|
||||
## Extend Functionality (Weekly, Monthly, Quarterly Notes)
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ on:
|
||||
jobs:
|
||||
store_data:
|
||||
runs-on: ubuntu-latest
|
||||
# If you encounter a 403 error from a workflow run, try uncommenting the following 2 lines (taken from: https://stackoverflow.com/questions/75880266/cant-make-push-on-a-repo-with-github-actions accepted answer)
|
||||
# permissions:
|
||||
# contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: anglinb/foam-capture-action@main
|
||||
|
||||
53
docs/user/recipes/export-to-pdf.md
Normal file
53
docs/user/recipes/export-to-pdf.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Export to PDF
|
||||
|
||||
This #recipe shows how to export a note to PDF.
|
||||
|
||||
## Required extensions
|
||||
|
||||
- **[vscode-pandoc](https://marketplace.visualstudio.com/items?itemName=chrischinchilla.vscode-pandoc)**
|
||||
|
||||
## Required third-party tools
|
||||
|
||||
- [Pandoc](https://pandoc.org/installing.html)
|
||||
- A [LaTeX distribution](https://www.latex-project.org/get/) such as TeXLive (Linux), MacTeX (MacOS), or MikTeX (Windows)
|
||||
|
||||
Check that Pandoc is installed by opening a terminal and running `pandoc --version`.
|
||||
|
||||
Check that Pandoc can produce PDFs with LaTeX by running the following in the terminal.
|
||||
|
||||
```
|
||||
echo It is working > test.md
|
||||
pandoc test.md -o test.pdf
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Create a folder in your workspace named `.pandoc`. Take note of the full path to this directory. The rest of this recipe will refer to this path as `$WORKSPACE/.pandoc`.
|
||||
|
||||
2. Download the template file [`foam.latex`](https://raw.githubusercontent.com/Hegghammer/foam-templates/main/foam.latex) from [Hegghammer/foam-templates](https://github.com/Hegghammer/foam-templates) and place it in `$WORKSPACE/.pandoc`.
|
||||
|
||||
3. In VSCode, open `settings.json` for your user (or just for your workspace if you prefer), and add the following line:
|
||||
|
||||
```
|
||||
"pandoc.pdfOptString": "--from=markdown+wikilinks_title_after_pipe --resource-path $WORKSPACE/.pandoc --template foam --listings",
|
||||
```
|
||||
|
||||
Make sure to replace `$WORKSPACE/.pandoc` with the real full path to the `.pandoc` directory you created earlier.
|
||||
|
||||
4. Open a Foam note in VSCode.
|
||||
|
||||
5. Press `Ctrl` + `k`, `p`. Choose "pdf", and press `Enter`.
|
||||
|
||||
The PDF should look something like this:
|
||||
|
||||

|
||||
|
||||
## Options
|
||||
|
||||
If you include a name in the `author` parameter in the YAML of the Foam note, that name will feature in the PDF header on the top left.
|
||||
|
||||
If you don't want syntax highlighting and frames around the codeblocks, remove `--listings` from the `pandoc.pdfOptString` parameter in `settings.json`.
|
||||
|
||||
## Further customization
|
||||
|
||||
If you know some LaTeX, you can [tweak](https://bookdown.org/yihui/rmarkdown-cookbook/latex-template.html) the `foam.latex` template to your needs. Alternatively, you can supply another ready-made template such as [Eisvogel](https://github.com/Wandmalfarbe/pandoc-latex-template); just place the `TEMPLATE_NAME.latex` file in `$WORKSPACE/.pandoc`. You can also use all of Pandoc's [other functionalities](https://learnbyexample.github.io/customizing-pandoc/) by tweaking the `pandoc.pdfOptString` parameter in `settings.json`.
|
||||
@@ -1,19 +1,21 @@
|
||||
<!-- omit in toc -->
|
||||
|
||||
# Recipes
|
||||
|
||||
A #recipe is a guide, tip or strategy for getting the most out of your Foam workspace!
|
||||
|
||||
- [Contribute](#contribute)
|
||||
- [Take smart notes](#take-smart-notes)
|
||||
- [Discover](#discover)
|
||||
- [Organise](#organise)
|
||||
- [Write](#write)
|
||||
- [Version control](#version-control)
|
||||
- [Publish](#publish)
|
||||
- [Collaborate](#collaborate)
|
||||
- [Workflow](#workflow)
|
||||
- [Creative ideas](#creative-ideas)
|
||||
- [Other](#other)
|
||||
- [Recipes](#recipes)
|
||||
- [Contribute](#contribute)
|
||||
- [Take smart notes](#take-smart-notes)
|
||||
- [Discover](#discover)
|
||||
- [Organise](#organise)
|
||||
- [Write](#write)
|
||||
- [Version control](#version-control)
|
||||
- [Publish](#publish)
|
||||
- [Collaborate](#collaborate)
|
||||
- [Workflow](#workflow)
|
||||
- [Creative ideas](#creative-ideas)
|
||||
- [Other](#other)
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -75,11 +77,11 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
- Publish using community templates
|
||||
- [[publish-to-netlify-with-eleventy]] by [@juanfrank77](https://github.com/juanfrank77)
|
||||
- [[generate-gatsby-site]] by [@mathieudutour](https://github.com/mathieudutour) and [@hikerpig](https://github.com/hikerpig)
|
||||
|
||||
- Make the site your own by [[publish-to-github]].
|
||||
- Render math symbols, by either
|
||||
- adding client-side [[math-support-with-mathjax]] to the default [[publish-to-github-pages]] site
|
||||
- adding a custom Jekyll plugin to support [[math-support-with-katex]]
|
||||
- Export note to PDF [[export-to-pdf]]
|
||||
|
||||
## Collaborate
|
||||
|
||||
@@ -140,6 +142,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[publish-to-github]: ../publishing/publish-to-github.md "Publish to GitHub"
|
||||
[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"
|
||||
[export-to-pdf]: export-to-pdf.md "Export to PDF"
|
||||
[real-time-collaboration]: real-time-collaboration.md "Real-time Collaboration"
|
||||
[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"
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.25.11"
|
||||
"version": "0.26.3"
|
||||
}
|
||||
|
||||
31816
package-lock.json
generated
Normal file
31816
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ out/**/*.spec.*
|
||||
test-data/**
|
||||
src/**
|
||||
jest.config.js
|
||||
esbuild.js
|
||||
.test-workspace
|
||||
.gitignore
|
||||
vsc-extension-quickstart.md
|
||||
|
||||
@@ -4,6 +4,38 @@ 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.26.3] - 2024-11-12
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Finetuned use of triemap (#1411 - thanks @pderaaij)
|
||||
|
||||
## [0.26.2] - 2024-11-06
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Performance improvements (#1406 - thanks @pderaaij)
|
||||
|
||||
## [0.26.1] - 2024-10-09
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed issue with Buffer in web extension (#1401 - thanks @pderaaij)
|
||||
|
||||
## [0.26.0] - 2024-10-01
|
||||
|
||||
Features:
|
||||
|
||||
- Foam is now a web extension! (#1395 - many thanks @pderaaij)
|
||||
|
||||
## [0.25.12] - 2024-07-13
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved YAML support (#1367)
|
||||
- Added convesion of wikilinks to markdown links (#1365 - thanks @hereistheusername)
|
||||
- Refactored util and settings code
|
||||
|
||||
## [0.25.11] - 2024-03-18
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
111
packages/foam-vscode/esbuild.js
Normal file
111
packages/foam-vscode/esbuild.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// also see https://code.visualstudio.com/api/working-with-extensions/bundling-extension
|
||||
const assert = require('assert');
|
||||
const esbuild = require('esbuild');
|
||||
const polyfillPlugin = require('esbuild-plugin-polyfill-node');
|
||||
|
||||
// pass the platform to esbuild as an argument
|
||||
|
||||
function getPlatform() {
|
||||
const args = process.argv.slice(2);
|
||||
const pArg = args.find(arg => arg.startsWith('--platform='));
|
||||
if (pArg) {
|
||||
return pArg.split('=')[1];
|
||||
}
|
||||
throw new Error('No platform specified. Pass --platform <web|node>.');
|
||||
}
|
||||
|
||||
const platform = getPlatform();
|
||||
assert(['web', 'node'].includes(platform), 'Platform must be "web" or "node".');
|
||||
|
||||
const production = process.argv.includes('--production');
|
||||
const watch = process.argv.includes('--watch');
|
||||
|
||||
const config = {
|
||||
web: {
|
||||
platform: 'browser',
|
||||
format: 'cjs',
|
||||
outfile: `out/bundles/extension-web.js`,
|
||||
plugins: [
|
||||
polyfillPlugin.polyfillNode({
|
||||
// Options (optional)
|
||||
}),
|
||||
{
|
||||
name: 'path-browserify',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^path$/ }, args => {
|
||||
return { path: require.resolve('path-browserify') };
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'wikilink-embed',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /wikilink-embed/ }, args => {
|
||||
return {
|
||||
path: require.resolve(
|
||||
args.resolveDir + '/wikilink-embed-web-extension.ts'
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
node: {
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
outfile: `out/bundles/extension-node.js`,
|
||||
plugins: [],
|
||||
},
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const ctx = await esbuild.context({
|
||||
...config[platform],
|
||||
entryPoints: ['src/extension.ts'],
|
||||
bundle: true,
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
external: ['vscode'],
|
||||
logLevel: 'silent',
|
||||
plugins: [
|
||||
...config[platform].plugins,
|
||||
/* add to the end of plugins array */
|
||||
esbuildProblemMatcherPlugin,
|
||||
],
|
||||
});
|
||||
if (watch) {
|
||||
await ctx.watch();
|
||||
} else {
|
||||
await ctx.rebuild();
|
||||
await ctx.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
*/
|
||||
const esbuildProblemMatcherPlugin = {
|
||||
name: 'esbuild-problem-matcher',
|
||||
|
||||
setup(build) {
|
||||
build.onStart(() => {
|
||||
console.log('[watch] build started');
|
||||
});
|
||||
build.onEnd(result => {
|
||||
result.errors.forEach(({ text, location }) => {
|
||||
console.error(`✘ [ERROR] ${text}`);
|
||||
console.error(
|
||||
` ${location.file}:${location.line}:${location.column}:`
|
||||
);
|
||||
});
|
||||
console.log('[watch] build finished');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.25.11",
|
||||
"version": "0.26.3",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -21,7 +21,8 @@
|
||||
"activationEvents": [
|
||||
"workspaceContains:.vscode/foam.json"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"main": "./out/bundles/extension-node.js",
|
||||
"browser": "./out/bundles/extension-web.js",
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
"supported": "limited",
|
||||
@@ -346,6 +347,14 @@
|
||||
"command": "foam-vscode.open-resource",
|
||||
"title": "Foam: Open Resource"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.convert-link-style-inplace",
|
||||
"title": "Foam: convert link style in place"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.convert-link-style-incopy",
|
||||
"title": "Foam: convert link format in copy"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.orphans.group-by:folder",
|
||||
"title": "Group By Folder",
|
||||
@@ -649,21 +658,23 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p ./",
|
||||
"pretest": "yarn build",
|
||||
"test": "node ./out/test/run-tests.js",
|
||||
"pretest:unit": "yarn build",
|
||||
"test:unit": "node ./out/test/run-tests.js --unit",
|
||||
"pretest:e2e": "yarn build",
|
||||
"test:e2e": "node ./out/test/run-tests.js --e2e",
|
||||
"build:node": "node esbuild.js --platform=node",
|
||||
"build:web": "node esbuild.js --platform=web",
|
||||
"build": "yarn build:node && yarn build:web",
|
||||
"vscode:prepublish": "yarn clean && yarn build:node --production && yarn build:web --production",
|
||||
"compile": "tsc -p ./",
|
||||
"test-reset-workspace": "rm -rf .test-workspace && mkdir .test-workspace && touch .test-workspace/.keep",
|
||||
"test-setup": "yarn compile && yarn build && yarn test-reset-workspace",
|
||||
"test": "yarn test-setup && node ./out/test/run-tests.js",
|
||||
"test:unit": "yarn test-setup && node ./out/test/run-tests.js --unit",
|
||||
"test:e2e": "yarn test-setup && node ./out/test/run-tests.js --e2e",
|
||||
"lint": "dts lint src",
|
||||
"clean": "rimraf out",
|
||||
"watch": "tsc --build ./tsconfig.json --watch",
|
||||
"vscode:start-debugging": "yarn clean && yarn watch",
|
||||
"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
|
||||
"vscode:prepublish": "yarn run esbuild-base -- --minify",
|
||||
"package-extension": "npx vsce package --yarn",
|
||||
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
|
||||
"open-in-browser": "vscode-test-web --quality=stable --browser=chromium --extensionDevelopmentPath=. ",
|
||||
"publish-extension-openvsx": "npx ovsx publish foam-vscode-$npm_package_version.vsix -p $OPENVSX_TOKEN",
|
||||
"publish-extension-vscode": "npx vsce publish --packagePath foam-vscode-$npm_package_version.vsix",
|
||||
"publish-extension": "yarn publish-extension-vscode && yarn publish-extension-openvsx"
|
||||
@@ -680,8 +691,10 @@
|
||||
"@types/vscode": "^1.70.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
||||
"@typescript-eslint/parser": "^5.51.0",
|
||||
"@vscode/test-web": "^0.0.62",
|
||||
"dts-cli": "^1.6.3",
|
||||
"esbuild": "^0.17.7",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
@@ -703,9 +716,12 @@
|
||||
"detect-newline": "^3.1.0",
|
||||
"github-slugger": "^1.4.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
"js-sha1": "^0.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"mnemonist": "^0.39.8",
|
||||
"path-browserify": "^1.0.1",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
"remark-parse": "^8.0.2",
|
||||
"remark-wiki-link": "^0.0.4",
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { convertLinkFormat } from '.';
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { MarkdownResourceProvider } from '../services/markdown-provider';
|
||||
import { Resource } from '../model/note';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { Logger } from '../utils/log';
|
||||
import fs from 'fs';
|
||||
import { URI } from '../model/uri';
|
||||
import { createMarkdownParser } from '../services/markdown-parser';
|
||||
import { FileDataStore } from '../../test/test-datastore';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
describe('generateStdMdLink', () => {
|
||||
let _workspace: FoamWorkspace;
|
||||
// TODO slug must be reserved for actual slugs, not file names
|
||||
const findBySlug = (slug: string): Resource => {
|
||||
return _workspace
|
||||
.list()
|
||||
.find(res => res.uri.getName() === slug) as Resource;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
/** Use fs for reading files in units where vscode.workspace is unavailable */
|
||||
const readFile = async (uri: URI) =>
|
||||
(await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
const dataStore = new FileDataStore(
|
||||
readFile,
|
||||
TEST_DATA_DIR.joinPath('__scaffold__').toFsPath()
|
||||
);
|
||||
const parser = createMarkdownParser();
|
||||
const mdProvider = new MarkdownResourceProvider(dataStore, parser);
|
||||
_workspace = await FoamWorkspace.fromProviders([mdProvider], dataStore);
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
expect(_workspace.list().length).toEqual(11);
|
||||
});
|
||||
|
||||
it('can generate markdown links correctly', async () => {
|
||||
const note = findBySlug('file-with-different-link-formats');
|
||||
const actual = note.links
|
||||
.filter(link => link.type === 'wikilink')
|
||||
.map(link => convertLinkFormat(link, 'link', _workspace, note));
|
||||
const expected: string[] = [
|
||||
'[first-document](first-document.md)',
|
||||
'[second-document](second-document.md)',
|
||||
'[[non-exist-file]]',
|
||||
'[#one section](<file-with-different-link-formats.md#one section>)',
|
||||
'[another name](<file-with-different-link-formats.md#one section>)',
|
||||
'[an alias](first-document.md)',
|
||||
'[first-document](first-document.md)',
|
||||
];
|
||||
expect(actual.length).toEqual(expected.length);
|
||||
actual.forEach((LinkReplace, index) => {
|
||||
expect(LinkReplace.newText).toEqual(expected[index]);
|
||||
});
|
||||
});
|
||||
|
||||
it('can generate wikilinks correctly', async () => {
|
||||
const note = findBySlug('file-with-different-link-formats');
|
||||
const actual = note.links
|
||||
.filter(link => link.type === 'link')
|
||||
.map(link => convertLinkFormat(link, 'wikilink', _workspace, note));
|
||||
const expected: string[] = ['[[first-document|file]]'];
|
||||
expect(actual.length).toEqual(expected.length);
|
||||
actual.forEach((LinkReplace, index) => {
|
||||
expect(LinkReplace.newText).toEqual(expected[index]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Resource, ResourceLink } from '../model/note';
|
||||
import { URI } from '../model/uri';
|
||||
import { Range } from '../model/range';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { isNone } from '../utils';
|
||||
import { MarkdownLink } from '../services/markdown-link';
|
||||
|
||||
export interface LinkReplace {
|
||||
newText: string;
|
||||
range: Range /* old range */;
|
||||
}
|
||||
|
||||
/**
|
||||
* convert a link based on its workspace and the note containing it.
|
||||
* According to targetFormat parameter to decide output format. If link.type === targetFormat, then it simply copy
|
||||
* the rawText into LinkReplace. Therefore, it's recommended to filter before conversion.
|
||||
* If targetFormat isn't supported, or the target resource pointed by link cannot be found, the function will throw
|
||||
* exception.
|
||||
* @param link
|
||||
* @param targetFormat 'wikilink' | 'link'
|
||||
* @param workspace
|
||||
* @param note
|
||||
* @returns LinkReplace { newText: string; range: Range; }
|
||||
*/
|
||||
export function convertLinkFormat(
|
||||
link: ResourceLink,
|
||||
targetFormat: 'wikilink' | 'link',
|
||||
workspace: FoamWorkspace,
|
||||
note: Resource | URI
|
||||
): LinkReplace {
|
||||
const resource = note instanceof URI ? workspace.find(note) : note;
|
||||
const targetUri = workspace.resolveLink(resource, link);
|
||||
/* If it's already the target format or a placeholder, no transformation happens */
|
||||
if (link.type === targetFormat || targetUri.scheme === 'placeholder') {
|
||||
return {
|
||||
newText: link.rawText,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
|
||||
let { target, section, alias } = MarkdownLink.analyzeLink(link);
|
||||
let sectionDivider = section ? '#' : '';
|
||||
|
||||
if (isNone(targetUri)) {
|
||||
throw new Error(
|
||||
`Unexpected state: link to: "${link.rawText}" is not resolvable`
|
||||
);
|
||||
}
|
||||
|
||||
const targetRes = workspace.find(targetUri);
|
||||
let relativeUri = targetRes.uri.relativeTo(resource.uri.getDirectory());
|
||||
|
||||
if (targetFormat === 'wikilink') {
|
||||
return MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: workspace.getIdentifier(relativeUri),
|
||||
type: 'wikilink',
|
||||
});
|
||||
}
|
||||
|
||||
if (targetFormat === 'link') {
|
||||
/* if alias is empty, construct one as target#section */
|
||||
if (alias === '') {
|
||||
/* in page anchor have no filename */
|
||||
if (relativeUri.getBasename() === resource.uri.getBasename()) {
|
||||
target = '';
|
||||
}
|
||||
alias = `${target}${sectionDivider}${section}`;
|
||||
}
|
||||
|
||||
/* if it's originally an embedded note, the markdown link shouldn't be embedded */
|
||||
const isEmbed = targetRes.type === 'image' ? link.isEmbed : false;
|
||||
|
||||
return MarkdownLink.createUpdateLinkEdit(link, {
|
||||
alias: alias,
|
||||
target: relativeUri.path,
|
||||
isEmbed: isEmbed,
|
||||
type: 'link',
|
||||
});
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected state: targetFormat: ${targetFormat} is not supported`
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ describe('generateLinkReferences', () => {
|
||||
});
|
||||
|
||||
it('initialised test graph correctly', () => {
|
||||
expect(_workspace.list().length).toEqual(10);
|
||||
expect(_workspace.list().length).toEqual(11);
|
||||
});
|
||||
|
||||
it('should add link references to a file that does not have them', async () => {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { generateLinkReferences } from './generate-link-references';
|
||||
export { generateHeading } from './generate-headings';
|
||||
export { convertLinkFormat } from './convert-links-format';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ResourceLink } from './note';
|
||||
import { URI } from './uri';
|
||||
import { FoamWorkspace } from './workspace';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { Logger, withTiming } from '../utils/log';
|
||||
import { Logger } from '../utils/log';
|
||||
import { Emitter } from '../common/event';
|
||||
|
||||
export type Connection = {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Range } from './range';
|
||||
import { URI } from './uri';
|
||||
import { ResourceLink } from './note';
|
||||
|
||||
/**
|
||||
* Represents a location inside a resource, such as a line
|
||||
|
||||
@@ -195,7 +195,6 @@ function encode(uri: URI, skipEncoding: boolean): string {
|
||||
: encodeURIComponentMinimal;
|
||||
|
||||
let res = '';
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { scheme, authority, path, query, fragment } = uri;
|
||||
if (scheme) {
|
||||
res += scheme;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Emitter } from '../common/event';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { IDataStore } from '../services/datastore';
|
||||
import TrieMap from 'mnemonist/trie-map';
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
@@ -20,7 +21,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
/**
|
||||
* Resources by path
|
||||
*/
|
||||
private _resources: Map<string, Resource> = new Map();
|
||||
private _resources: TrieMap<string, Resource> = new TrieMap();
|
||||
|
||||
/**
|
||||
* @param defaultExtension: The default extension for notes in this workspace (e.g. `.md`)
|
||||
@@ -33,7 +34,10 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
set(resource: Resource) {
|
||||
const old = this.find(resource.uri);
|
||||
this._resources.set(normalize(resource.uri.path), resource);
|
||||
|
||||
// store resource
|
||||
this._resources.set(this.getTrieIdentifier(resource.uri.path), resource);
|
||||
|
||||
isSome(old)
|
||||
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: this.onDidAddEmitter.fire(resource);
|
||||
@@ -41,8 +45,8 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
delete(uri: URI) {
|
||||
const deleted = this._resources.get(normalize(uri.path));
|
||||
this._resources.delete(normalize(uri.path));
|
||||
const deleted = this._resources.get(this.getTrieIdentifier(uri));
|
||||
this._resources.delete(this.getTrieIdentifier(uri));
|
||||
|
||||
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
@@ -57,7 +61,11 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public resources(): IterableIterator<Resource> {
|
||||
return this._resources.values();
|
||||
const resources: Array<Resource> = Array.from(
|
||||
this._resources.values()
|
||||
).sort(Resource.sortByPath);
|
||||
|
||||
return resources.values();
|
||||
}
|
||||
|
||||
public get(uri: URI): Resource {
|
||||
@@ -70,17 +78,21 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public listByIdentifier(identifier: string): Resource[] {
|
||||
const needle = normalize('/' + identifier);
|
||||
let needle = this.getTrieIdentifier(identifier);
|
||||
|
||||
const mdNeedle =
|
||||
getExtension(needle) !== this.defaultExtension
|
||||
? needle + this.defaultExtension
|
||||
getExtension(normalize(identifier)) !== this.defaultExtension
|
||||
? this.getTrieIdentifier(identifier + this.defaultExtension)
|
||||
: undefined;
|
||||
|
||||
const resources: Resource[] = [];
|
||||
for (const key of this._resources.keys()) {
|
||||
if (key.endsWith(mdNeedle) || key.endsWith(needle)) {
|
||||
resources.push(this._resources.get(normalize(key)));
|
||||
}
|
||||
|
||||
this._resources.find(needle).forEach(elm => resources.push(elm[1]));
|
||||
|
||||
if (mdNeedle) {
|
||||
this._resources.find(mdNeedle).forEach(elm => resources.push(elm[1]));
|
||||
}
|
||||
|
||||
return resources.sort(Resource.sortByPath);
|
||||
}
|
||||
|
||||
@@ -92,21 +104,19 @@ export class FoamWorkspace implements IDisposable {
|
||||
public getIdentifier(forResource: URI, exclude?: URI[]): string {
|
||||
const amongst = [];
|
||||
const basename = forResource.getBasename();
|
||||
for (const res of this._resources.values()) {
|
||||
// skip elements that cannot possibly match
|
||||
if (!res.uri.path.endsWith(basename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.listByIdentifier(basename).map(res => {
|
||||
// skip self
|
||||
if (res.uri.isEqual(forResource)) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// skip exclude list
|
||||
if (exclude && exclude.find(ex => ex.isEqual(res.uri))) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
amongst.push(res.uri);
|
||||
}
|
||||
});
|
||||
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
forResource.path,
|
||||
@@ -119,9 +129,32 @@ export class FoamWorkspace implements IDisposable {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a note identifier in reversed order. Used to optimise the storage of notes in
|
||||
* the workspace to optimise retrieval of notes.
|
||||
*
|
||||
* @param reference the URI path to reverse
|
||||
*/
|
||||
private getTrieIdentifier(reference: URI | string): string {
|
||||
let path: string;
|
||||
if (reference instanceof URI) {
|
||||
path = (reference as URI).path;
|
||||
} else {
|
||||
path = reference as string;
|
||||
}
|
||||
|
||||
let reversedPath = normalize(path).split('/').reverse().join('/');
|
||||
|
||||
if (reversedPath.indexOf('/') < 0) {
|
||||
reversedPath = reversedPath + '/';
|
||||
}
|
||||
|
||||
return reversedPath;
|
||||
}
|
||||
|
||||
public find(reference: URI | string, baseUri?: URI): Resource | null {
|
||||
if (reference instanceof URI) {
|
||||
return this._resources.get(normalize((reference as URI).path)) ?? null;
|
||||
return this._resources.get(this.getTrieIdentifier(reference)) ?? null;
|
||||
}
|
||||
let resource: Resource | null = null;
|
||||
const [path, fragment] = (reference as string).split('#');
|
||||
@@ -135,7 +168,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
: isSome(baseUri)
|
||||
? baseUri.resolve(candidate).path
|
||||
: null;
|
||||
resource = this._resources.get(normalize(searchKey));
|
||||
resource = this._resources.get(this.getTrieIdentifier(searchKey));
|
||||
if (resource) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -254,4 +254,185 @@ describe('MarkdownLink', () => {
|
||||
expect(edit.range).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert wikilink to link', () => {
|
||||
it('should generate default alias if no one', () => {
|
||||
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[wikilink](wikilink)`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
|
||||
const wikilinkWithSection = parser.parse(
|
||||
getRandomURI(),
|
||||
`[[wikilink#section]]`
|
||||
).links[0];
|
||||
const wikilinkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
|
||||
wikilinkWithSection,
|
||||
{
|
||||
type: 'link',
|
||||
}
|
||||
);
|
||||
expect(wikilinkWithSectionEdit.newText).toEqual(
|
||||
`[wikilink#section](wikilink#section)`
|
||||
);
|
||||
expect(wikilinkWithSectionEdit.range).toEqual(wikilinkWithSection.range);
|
||||
});
|
||||
|
||||
it('should use alias in the wikilik the if there has one', () => {
|
||||
const wikilink = parser.parse(
|
||||
getRandomURI(),
|
||||
`[[wikilink#section|alias]]`
|
||||
).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert link to wikilink', () => {
|
||||
it('should reorganize target, section, and alias in wikilink manner', () => {
|
||||
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
type: 'wikilink',
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(`[[to/path.md|link]]`);
|
||||
expect(linkEdit.range).toEqual(link.range);
|
||||
|
||||
const linkWithSection = parser.parse(
|
||||
getRandomURI(),
|
||||
`[link](to/path.md#section)`
|
||||
).links[0];
|
||||
const linkWithSectionEdit = MarkdownLink.createUpdateLinkEdit(
|
||||
linkWithSection,
|
||||
{
|
||||
type: 'wikilink',
|
||||
}
|
||||
);
|
||||
expect(linkWithSectionEdit.newText).toEqual(
|
||||
`[[to/path.md#section|link]]`
|
||||
);
|
||||
expect(linkWithSectionEdit.range).toEqual(linkWithSection.range);
|
||||
});
|
||||
|
||||
it('should use alias in the wikilik the if there has one', () => {
|
||||
const wikilink = parser.parse(
|
||||
getRandomURI(),
|
||||
`[[wikilink#section|alias]]`
|
||||
).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[alias](wikilink#section)`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convert to its original type', () => {
|
||||
it('should remain unchanged', () => {
|
||||
const link = parser.parse(getRandomURI(), `[link](to/path.md#section)`)
|
||||
.links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(`[link](to/path.md#section)`);
|
||||
expect(linkEdit.range).toEqual(link.range);
|
||||
|
||||
const wikilink = parser.parse(
|
||||
getRandomURI(),
|
||||
`[[wikilink#section|alias]]`
|
||||
).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'wikilink',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[[wikilink#section|alias]]`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change isEmbed property', () => {
|
||||
it('should change isEmbed only', () => {
|
||||
const wikilink = parser.parse(getRandomURI(), `[[wikilink]]`).links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
isEmbed: true,
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`![[wikilink]]`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
|
||||
const link = parser.parse(getRandomURI(), ``).links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
isEmbed: false,
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
|
||||
expect(linkEdit.range).toEqual(link.range);
|
||||
});
|
||||
|
||||
it('should be unchanged if the update value is the same as the original one', () => {
|
||||
const embeddedWikilink = parser.parse(getRandomURI(), `![[wikilink]]`)
|
||||
.links[0];
|
||||
const embeddedWikilinkEdit = MarkdownLink.createUpdateLinkEdit(
|
||||
embeddedWikilink,
|
||||
{
|
||||
isEmbed: true,
|
||||
}
|
||||
);
|
||||
expect(embeddedWikilinkEdit.newText).toEqual(`![[wikilink]]`);
|
||||
expect(embeddedWikilinkEdit.range).toEqual(embeddedWikilink.range);
|
||||
|
||||
const link = parser.parse(getRandomURI(), `[link](to/path.md)`).links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
isEmbed: false,
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(`[link](to/path.md)`);
|
||||
expect(linkEdit.range).toEqual(link.range);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert angles', () => {
|
||||
it('should insert angles when meeting space in links', () => {
|
||||
const link = parser.parse(getRandomURI(), ``).links[0];
|
||||
const linkAddSection = MarkdownLink.createUpdateLinkEdit(link, {
|
||||
section: 'one section',
|
||||
});
|
||||
expect(linkAddSection.newText).toEqual(
|
||||
``
|
||||
);
|
||||
expect(linkAddSection.range).toEqual(link.range);
|
||||
|
||||
const linkChangingTarget = parser.parse(
|
||||
getRandomURI(),
|
||||
`[link](to/path.md#one-section)`
|
||||
).links[0];
|
||||
const linkEdit = MarkdownLink.createUpdateLinkEdit(linkChangingTarget, {
|
||||
target: 'to/another path.md',
|
||||
});
|
||||
expect(linkEdit.newText).toEqual(
|
||||
`[link](<to/another path.md#one-section>)`
|
||||
);
|
||||
expect(linkEdit.range).toEqual(linkChangingTarget.range);
|
||||
|
||||
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
|
||||
.links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
type: 'link',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(
|
||||
`[wikilink#one section](<wikilink#one section>)`
|
||||
);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
|
||||
it('should not insert angles in wikilink', () => {
|
||||
const wikilink = parser.parse(getRandomURI(), `[[wikilink#one section]]`)
|
||||
.links[0];
|
||||
const wikilinkEdit = MarkdownLink.createUpdateLinkEdit(wikilink, {
|
||||
target: 'another wikilink',
|
||||
});
|
||||
expect(wikilinkEdit.newText).toEqual(`[[another wikilink#one section]]`);
|
||||
expect(wikilinkEdit.range).toEqual(wikilink.range);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,13 @@ export abstract class MarkdownLink {
|
||||
|
||||
public static createUpdateLinkEdit(
|
||||
link: ResourceLink,
|
||||
delta: { target?: string; section?: string; alias?: string }
|
||||
delta: {
|
||||
target?: string;
|
||||
section?: string;
|
||||
alias?: string;
|
||||
type?: 'wikilink' | 'link';
|
||||
isEmbed?: boolean;
|
||||
}
|
||||
) {
|
||||
const { target, section, alias } = MarkdownLink.analyzeLink(link);
|
||||
const newTarget = delta.target ?? target;
|
||||
@@ -46,21 +52,27 @@ export abstract class MarkdownLink {
|
||||
const newAlias = delta.alias ?? alias ?? '';
|
||||
const sectionDivider = newSection ? '#' : '';
|
||||
const aliasDivider = newAlias ? '|' : '';
|
||||
const embed = link.isEmbed ? '!' : '';
|
||||
if (link.type === 'wikilink') {
|
||||
const embed = delta.isEmbed ?? link.isEmbed ? '!' : '';
|
||||
const type = delta.type ?? link.type;
|
||||
if (type === 'wikilink') {
|
||||
return {
|
||||
newText: `${embed}[[${newTarget}${sectionDivider}${newSection}${aliasDivider}${newAlias}]]`,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
if (link.type === 'link') {
|
||||
if (type === 'link') {
|
||||
const defaultAlias = () => {
|
||||
return `${newTarget}${sectionDivider}${newSection}`;
|
||||
};
|
||||
const useAngles =
|
||||
newTarget.indexOf(' ') > 0 || newSection.indexOf(' ') > 0;
|
||||
return {
|
||||
newText: `${embed}[${newAlias}](${newTarget}${sectionDivider}${newSection})`,
|
||||
newText: `${embed}[${newAlias ? newAlias : defaultAlias()}](${
|
||||
useAngles ? '<' : ''
|
||||
}${newTarget}${sectionDivider}${newSection}${useAngles ? '>' : ''})`,
|
||||
range: link.range,
|
||||
};
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected state: link of type ${link.type} is not supported`
|
||||
);
|
||||
throw new Error(`Unexpected state: link of type ${type} is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,9 +459,9 @@ export const getBlockFor = (
|
||||
}
|
||||
});
|
||||
|
||||
let nLines = startLine == -1 ? 1 : endLine - startLine;
|
||||
let nLines = startLine === -1 ? 1 : endLine - startLine;
|
||||
let block =
|
||||
startLine == -1
|
||||
startLine === -1
|
||||
? lines[searchLine] ?? ''
|
||||
: lines.slice(startLine, endLine).join('\n');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import crypto from 'crypto';
|
||||
import sha1 from 'js-sha1';
|
||||
|
||||
export function isNotNull<T>(value: T | null): value is T {
|
||||
return value != null;
|
||||
@@ -20,5 +20,4 @@ export function isNumeric(value: string): boolean {
|
||||
return /-?\d+$/.test(value);
|
||||
}
|
||||
|
||||
export const hash = (text: string) =>
|
||||
crypto.createHash('sha1').update(text).digest('hex');
|
||||
export const hash = (text: string) => sha1.sha1(text);
|
||||
|
||||
@@ -1,67 +1,4 @@
|
||||
import {
|
||||
isInFrontMatter,
|
||||
isOnYAMLKeywordLine,
|
||||
removeBrackets,
|
||||
toTitleCase,
|
||||
} from './utils';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
import { isInFrontMatter, isOnYAMLKeywordLine } from './md';
|
||||
|
||||
describe('isInFrontMatter', () => {
|
||||
it('is true for started front matter', () => {
|
||||
@@ -81,6 +18,12 @@ describe('isInFrontMatter', () => {
|
||||
const actual = isInFrontMatter(content, 1);
|
||||
expect(actual).toBeTruthy();
|
||||
});
|
||||
it('is false for non valid front matter delimiter #1347', () => {
|
||||
const content = '---\ntitle: A title\n-..\n\n\n---\ntest\n';
|
||||
expect(isInFrontMatter(content, 1)).toBeTruthy();
|
||||
expect(isInFrontMatter(content, 4)).toBeTruthy();
|
||||
expect(isInFrontMatter(content, 6)).toBeFalsy();
|
||||
});
|
||||
it('is false for outside completed front matter', () => {
|
||||
const content = '---\ntitle: A title\n---\ncontent\nmore content\n';
|
||||
const actual = isInFrontMatter(content, 3);
|
||||
70
packages/foam-vscode/src/core/utils/md.ts
Normal file
70
packages/foam-vscode/src/core/utils/md.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import matter from 'gray-matter';
|
||||
|
||||
export function getExcerpt(
|
||||
markdown: string,
|
||||
maxLines: number
|
||||
): { excerpt: string; lines: number } {
|
||||
const OFFSET_LINES_LIMIT = 5;
|
||||
const paragraphs = markdown.replace(/\r\n/g, '\n').split('\n\n');
|
||||
const excerpt: string[] = [];
|
||||
let lines = 0;
|
||||
for (const paragraph of paragraphs) {
|
||||
const n = paragraph.split('\n').length;
|
||||
if (lines > maxLines || lines + n - maxLines > OFFSET_LINES_LIMIT) {
|
||||
break;
|
||||
}
|
||||
excerpt.push(paragraph);
|
||||
lines = lines + n + 1;
|
||||
}
|
||||
return { excerpt: excerpt.join('\n\n'), lines };
|
||||
}
|
||||
|
||||
export function stripFrontMatter(markdown: string): string {
|
||||
return matter(markdown).content.trim();
|
||||
}
|
||||
|
||||
export function stripImages(markdown: string): string {
|
||||
return markdown.replace(
|
||||
/!\[(.*)\]\([-/\\.A-Za-z]*\)/gi,
|
||||
'$1'.length ? '[Image: $1]' : ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the given line is inside a front matter block
|
||||
* @param content the string to check
|
||||
* @param lineNumber the line number within the string, 0-based
|
||||
* @returns true if the line is inside a frontmatter block in content
|
||||
*/
|
||||
export function isInFrontMatter(content: string, lineNumber: number): boolean {
|
||||
const FIRST_DELIMITER_MATCH = /^---\s*?$/m;
|
||||
const LAST_DELIMITER_MATCH = /^(-{3}|\.{3})/;
|
||||
|
||||
// if we're on the first line, we're not _yet_ in the front matter
|
||||
if (lineNumber === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// look for --- at start, and a second --- or ... to end
|
||||
if (content.match(FIRST_DELIMITER_MATCH) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
lines.shift();
|
||||
const endLineNumber = lines.findIndex(l => l.match(LAST_DELIMITER_MATCH));
|
||||
|
||||
return endLineNumber === -1 || endLineNumber >= lineNumber;
|
||||
}
|
||||
|
||||
export function isOnYAMLKeywordLine(content: string, keyword: string): boolean {
|
||||
const keywordMatch = /^\s*(\w+):/gm;
|
||||
|
||||
if (content.match(keywordMatch) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matches = Array.from(content.matchAll(keywordMatch));
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
return lastMatch[1] === keyword;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import dateFormat from 'dateformat';
|
||||
import { focusNote } from './utils';
|
||||
import { URI } from './core/model/uri';
|
||||
import { NoteFactory } from './services/templates';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
import { asAbsoluteWorkspaceUri } from './services/editor';
|
||||
import { asAbsoluteWorkspaceUri, focusNote } from './services/editor';
|
||||
|
||||
/**
|
||||
* Open the daily note file.
|
||||
|
||||
@@ -17,7 +17,6 @@ import { VsCodeWatcher } from './services/watcher';
|
||||
import { createMarkdownParser } from './core/services/markdown-parser';
|
||||
import VsCodeBasedParserCache from './services/cache';
|
||||
import { createMatcherAndDataStore } from './services/editor';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { commands, ExtensionContext, window, workspace, Uri } from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../../utils/vsc-utils';
|
||||
import { ResourceParser } from '../../core/model/note';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
import { convertLinkFormat } from '../../core/janitor';
|
||||
import { isMdEditor } from '../../services/editor';
|
||||
|
||||
type LinkFormat = 'wikilink' | 'link';
|
||||
|
||||
enum ConvertOption {
|
||||
Wikilink2MDlink,
|
||||
MDlink2Wikilink,
|
||||
}
|
||||
|
||||
interface IConfig {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
const Config: { [key in ConvertOption]: IConfig } = {
|
||||
[ConvertOption.Wikilink2MDlink]: {
|
||||
from: 'wikilink',
|
||||
to: 'link',
|
||||
},
|
||||
[ConvertOption.MDlink2Wikilink]: {
|
||||
from: 'link',
|
||||
to: 'wikilink',
|
||||
},
|
||||
};
|
||||
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
|
||||
/*
|
||||
commands:
|
||||
foam-vscode.convert-link-style-inplace
|
||||
foam-vscode.convert-link-style-incopy
|
||||
*/
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.convert-link-style-inplace', () => {
|
||||
return convertLinkAdapter(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
foam.services.matcher,
|
||||
true
|
||||
);
|
||||
}),
|
||||
commands.registerCommand('foam-vscode.convert-link-style-incopy', () => {
|
||||
return convertLinkAdapter(
|
||||
foam.workspace,
|
||||
foam.services.parser,
|
||||
foam.services.matcher,
|
||||
false
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function convertLinkAdapter(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
fMatcher: IMatcher,
|
||||
isInPlace: boolean
|
||||
) {
|
||||
const convertOption = await pickConvertStrategy();
|
||||
if (!convertOption) {
|
||||
window.showInformationMessage('Convert canceled');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInPlace) {
|
||||
await convertLinkInPlace(fWorkspace, fParser, fMatcher, convertOption);
|
||||
} else {
|
||||
await convertLinkInCopy(fWorkspace, fParser, fMatcher, convertOption);
|
||||
}
|
||||
}
|
||||
|
||||
async function pickConvertStrategy(): Promise<IConfig | undefined> {
|
||||
const options = {
|
||||
'to wikilink': ConvertOption.MDlink2Wikilink,
|
||||
'to markdown link': ConvertOption.Wikilink2MDlink,
|
||||
};
|
||||
return window.showQuickPick(Object.keys(options)).then(name => {
|
||||
if (name) {
|
||||
return Config[options[name]];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* convert links based on its workspace and the note containing it.
|
||||
* Changes happen in-place
|
||||
* @param fWorkspace
|
||||
* @param fParser
|
||||
* @param fMatcher
|
||||
* @param convertOption
|
||||
* @returns void
|
||||
*/
|
||||
async function convertLinkInPlace(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
fMatcher: IMatcher,
|
||||
convertOption: IConfig
|
||||
) {
|
||||
const editor = window.activeTextEditor;
|
||||
const doc = editor.document;
|
||||
|
||||
if (!isMdEditor(editor) || !fMatcher.isMatch(fromVsCodeUri(doc.uri))) {
|
||||
return;
|
||||
}
|
||||
// const eol = getEditorEOL();
|
||||
let text = doc.getText();
|
||||
|
||||
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
|
||||
|
||||
const textReplaceArr = resource.links
|
||||
.filter(link => link.type === convertOption.from)
|
||||
.map(link =>
|
||||
convertLinkFormat(
|
||||
link,
|
||||
convertOption.to as LinkFormat,
|
||||
fWorkspace,
|
||||
resource
|
||||
)
|
||||
)
|
||||
/* transform .range property into vscode range */
|
||||
.map(linkReplace => ({
|
||||
...linkReplace,
|
||||
range: toVsCodeRange(linkReplace.range),
|
||||
}));
|
||||
|
||||
/* reorder the array such that the later range comes first */
|
||||
textReplaceArr.sort((a, b) => b.range.start.compareTo(a.range.start));
|
||||
|
||||
await editor.edit(editorBuilder => {
|
||||
textReplaceArr.forEach(edit => {
|
||||
editorBuilder.replace(edit.range, edit.newText);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* convert links based on its workspace and the note containing it.
|
||||
* Changes happen in a copy
|
||||
* 1. prepare a copy file, and makt it the activeTextEditor
|
||||
* 2. call to convertLinkInPlace
|
||||
* @param fWorkspace
|
||||
* @param fParser
|
||||
* @param fMatcher
|
||||
* @param convertOption
|
||||
* @returns void
|
||||
*/
|
||||
async function convertLinkInCopy(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
fMatcher: IMatcher,
|
||||
convertOption: IConfig
|
||||
) {
|
||||
const editor = window.activeTextEditor;
|
||||
const doc = editor.document;
|
||||
|
||||
if (!isMdEditor(editor) || !fMatcher.isMatch(fromVsCodeUri(doc.uri))) {
|
||||
return;
|
||||
}
|
||||
// const eol = getEditorEOL();
|
||||
let text = doc.getText();
|
||||
|
||||
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
|
||||
const basePath = doc.uri.path.split('/').slice(0, -1).join('/');
|
||||
|
||||
const fileUri = Uri.file(
|
||||
`${
|
||||
basePath ? basePath + '/' : ''
|
||||
}${resource.uri.getName()}.copy${resource.uri.getExtension()}`
|
||||
);
|
||||
const encoder = new TextEncoder();
|
||||
await workspace.fs.writeFile(fileUri, encoder.encode(text));
|
||||
await window.showTextDocument(fileUri);
|
||||
|
||||
await convertLinkInPlace(fWorkspace, fParser, fMatcher, convertOption);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { env, Position, Selection, commands } from 'vscode';
|
||||
import { createFile, showInEditor } from '../../test/test-utils-vscode';
|
||||
import { removeBrackets, toTitleCase } from './copy-without-brackets';
|
||||
|
||||
describe('copy-without-brackets command', () => {
|
||||
it('should get the input from the active editor selection', async () => {
|
||||
@@ -11,3 +12,61 @@ describe('copy-without-brackets command', () => {
|
||||
expect(value).toEqual('This is my Test Content.');
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { window, env, ExtensionContext, commands } from 'vscode';
|
||||
import { removeBrackets } from '../../utils';
|
||||
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
@@ -31,3 +30,46 @@ async function copyWithoutBrackets() {
|
||||
window.showInformationMessage('Successfully copied to clipboard!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for the "Copy to Clipboard Without Brackets" command
|
||||
*
|
||||
*/
|
||||
export function removeBrackets(s: string): string {
|
||||
// take in the string, split on space
|
||||
const stringSplitBySpace = s.split(' ');
|
||||
|
||||
// loop through words
|
||||
const modifiedWords = stringSplitBySpace.map(currentWord => {
|
||||
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, ' ');
|
||||
|
||||
// then we titlecase the word so "you are awesome"
|
||||
// becomes "You Are Awesome"
|
||||
const titleCasedWord = toTitleCase(word);
|
||||
|
||||
return titleCasedWord;
|
||||
}
|
||||
|
||||
return currentWord;
|
||||
});
|
||||
|
||||
return modifiedWords.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in a string and returns it titlecased
|
||||
*
|
||||
* @example toTitleCase("hello world") -> "Hello World"
|
||||
*/
|
||||
export function toTitleCase(word: string): string {
|
||||
return word
|
||||
.split(' ')
|
||||
.map(word => word[0].toUpperCase() + word.substring(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { CREATE_NOTE_COMMAND, createNote } from './create-note';
|
||||
import { Location } from '../../core/model/location';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { ResourceLink } from '../../core/model/note';
|
||||
import { MarkdownResourceProvider } from '../../core/services/markdown-provider';
|
||||
import { createMarkdownParser } from '../../core/services/markdown-parser';
|
||||
|
||||
describe('create-note command', () => {
|
||||
|
||||
@@ -129,7 +129,7 @@ export async function createNote(args: CreateNoteArgs, foam: Foam) {
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(args.sourceLink.data, {
|
||||
target: identifier,
|
||||
});
|
||||
if (edit.newText != args.sourceLink.data.rawText) {
|
||||
if (edit.newText !== args.sourceLink.data.rawText) {
|
||||
const updateLink = new vscode.WorkspaceEdit();
|
||||
const uri = toVsCodeUri(args.sourceLink.uri);
|
||||
updateLink.replace(
|
||||
|
||||
@@ -10,3 +10,4 @@ export { default as openResource } from './open-resource';
|
||||
export { default as updateGraphCommand } from './update-graph';
|
||||
export { default as updateWikilinksCommand } from './update-wikilinks';
|
||||
export { default as createNote } from './create-note';
|
||||
export { default as generateStandaloneNote } from './convert-links-format-in-note';
|
||||
|
||||
@@ -5,18 +5,18 @@ import {
|
||||
commands,
|
||||
ProgressLocation,
|
||||
} from 'vscode';
|
||||
import { getWikilinkDefinitionSetting } from '../../settings';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { generateHeading, generateLinkReferences } from '../../core/janitor';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
import {
|
||||
toVsCodePosition,
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../../utils/vsc-utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Resource } from '../../core/model/note';
|
||||
import { generateHeading, generateLinkReferences } from '../../core/janitor';
|
||||
import { Range } from '../../core/model/range';
|
||||
import detectNewline from 'detect-newline';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
import { getWikilinkDefinitionSetting } from './update-wikilinks';
|
||||
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExtensionContext, commands, window } from 'vscode';
|
||||
import { focusNote } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { focusNote } from '../../services/editor';
|
||||
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
workspace,
|
||||
Position,
|
||||
} from 'vscode';
|
||||
import { isMdEditor, mdDocSelector } from '../../utils';
|
||||
import { isMdEditor, mdDocSelector } from '../../services/editor';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import {
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../../utils/vsc-utils';
|
||||
import { getEditorEOL } from '../../services/editor';
|
||||
import { ResourceParser } from '../../core/model/note';
|
||||
import { getWikilinkDefinitionSetting } from '../../settings';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
|
||||
export default async function activate(
|
||||
@@ -58,6 +57,15 @@ export default async function activate(
|
||||
);
|
||||
}
|
||||
|
||||
export function getWikilinkDefinitionSetting():
|
||||
| 'withExtensions'
|
||||
| 'withoutExtensions'
|
||||
| 'off' {
|
||||
return workspace
|
||||
.getConfiguration('foam.edit')
|
||||
.get('linkReferenceDefinitions', 'withoutExtensions');
|
||||
}
|
||||
|
||||
async function updateWikilinkDefinitions(
|
||||
fWorkspace: FoamWorkspace,
|
||||
fParser: ResourceParser,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { uniqWith } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { getNoteTooltip, mdDocSelector, isSome } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeRange } from '../utils/vsc-utils';
|
||||
import {
|
||||
ConfigurationMonitor,
|
||||
@@ -15,6 +14,8 @@ import { OPEN_COMMAND } from './commands/open-resource';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { Location } from '../core/model/location';
|
||||
import { getNoteTooltip, mdDocSelector } from '../services/editor';
|
||||
import { isSome } from '../core/utils';
|
||||
|
||||
export const CONFIG_KEY = 'links.hover.enable';
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { Resource } from '../core/model/note';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { getNoteTooltip, mdDocSelector } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { getNoteTooltip, mdDocSelector } from '../services/editor';
|
||||
|
||||
export const aliasCommitCharacters = ['#'];
|
||||
export const linkCommitCharacters = ['#', '|'];
|
||||
|
||||
@@ -13,9 +13,6 @@ import { FoamGraph } from '../core/model/graph';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { Location } from '../core/model/location';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Range } from '../core/model/range';
|
||||
import { ResourceLink } from '../core/model/note';
|
||||
|
||||
describe('Document navigation', () => {
|
||||
const parser = createMarkdownParser([]);
|
||||
@@ -234,6 +231,10 @@ describe('Document navigation', () => {
|
||||
doc,
|
||||
new vscode.Position(0, 26)
|
||||
);
|
||||
|
||||
// Make sure the references are sorted by position, so we match the right expectation
|
||||
refs.sort((a, b) => a.range.start.character - b.range.start.character);
|
||||
|
||||
expect(refs.length).toEqual(2);
|
||||
expect(refs[0]).toEqual({
|
||||
uri: toVsCodeUri(fileB.uri),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { mdDocSelector } from '../utils';
|
||||
import { toVsCodeRange, toVsCodeUri, fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
@@ -11,6 +10,7 @@ import { Position } from '../core/model/position';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { Location } from '../core/model/location';
|
||||
import { mdDocSelector } from '../services/editor';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { isNone } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Connection, FoamGraph } from '../../core/model/graph';
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
createConnectionItemsForResource,
|
||||
} from './utils/tree-view-utils';
|
||||
import { BaseTreeProvider } from './utils/base-tree-provider';
|
||||
import { isNone } from '../../core/utils';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -30,7 +30,6 @@ export default async function activate(
|
||||
treeDataProvider: provider,
|
||||
showCollapseAll: true,
|
||||
});
|
||||
const baseTitle = treeView.title;
|
||||
|
||||
const updateTreeView = async () => {
|
||||
provider.target = vscode.window.activeTextEditor
|
||||
@@ -53,12 +52,7 @@ export default async function activate(
|
||||
}
|
||||
|
||||
export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeItem> {
|
||||
public show = new ContextMemento<'all links' | 'backlinks' | 'forward links'>(
|
||||
this.state,
|
||||
`foam-vscode.views.connections.show`,
|
||||
'all links',
|
||||
true
|
||||
);
|
||||
public show: ContextMemento<'all links' | 'backlinks' | 'forward links'>;
|
||||
public target?: URI = undefined;
|
||||
public nValues = 0;
|
||||
private connectionItems: ResourceRangeTreeItem[] = [];
|
||||
@@ -70,6 +64,12 @@ export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeIte
|
||||
registerCommands = true // for testing. don't love it, but will do for now
|
||||
) {
|
||||
super();
|
||||
this.show = new ContextMemento<'all links' | 'backlinks' | 'forward links'>(
|
||||
this.state,
|
||||
`foam-vscode.views.connections.show`,
|
||||
'all links',
|
||||
true
|
||||
);
|
||||
if (!registerCommands) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { TextDecoder } from 'util';
|
||||
import { getGraphStyle, getTitleMaxLength } from '../../settings';
|
||||
import { isSome } from '../../utils';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { fromVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { isSome } from '../../core/utils';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -104,7 +102,9 @@ function generateGraphData(foam: Foam) {
|
||||
}
|
||||
|
||||
function cutTitle(title: string): string {
|
||||
const maxLen = getTitleMaxLength();
|
||||
const maxLen = vscode.workspace
|
||||
.getConfiguration('foam.graph')
|
||||
.get('titleMaxLength', 24);
|
||||
if (maxLen > 0 && title.length > maxLen) {
|
||||
return title.substring(0, maxLen).concat('...');
|
||||
}
|
||||
@@ -166,28 +166,37 @@ async function getWebviewContent(
|
||||
context: vscode.ExtensionContext,
|
||||
panel: vscode.WebviewPanel
|
||||
) {
|
||||
const datavizPath = vscode.Uri.joinPath(
|
||||
vscode.Uri.file(context.extensionPath),
|
||||
const datavizUri = vscode.Uri.joinPath(
|
||||
context.extensionUri,
|
||||
'static',
|
||||
'dataviz'
|
||||
);
|
||||
|
||||
const getWebviewUri = (fileName: string) =>
|
||||
panel.webview.asWebviewUri(vscode.Uri.joinPath(datavizPath, fileName));
|
||||
panel.webview.asWebviewUri(vscode.Uri.joinPath(datavizUri, fileName));
|
||||
|
||||
const indexHtml = await vscode.workspace.fs.readFile(
|
||||
vscode.Uri.joinPath(datavizPath, 'index.html')
|
||||
);
|
||||
const indexHtml =
|
||||
vscode.env.uiKind === vscode.UIKind.Desktop
|
||||
? new TextDecoder('utf-8').decode(
|
||||
await vscode.workspace.fs.readFile(
|
||||
vscode.Uri.joinPath(datavizUri, 'index.html')
|
||||
)
|
||||
)
|
||||
: await fetch(getWebviewUri('index.html').toString()).then(r => r.text());
|
||||
|
||||
// Replace the script paths with the appropriate webview URI.
|
||||
const filled = new TextDecoder('utf-8')
|
||||
.decode(indexHtml)
|
||||
.replace(/data-replace (src|href)="[^"]+"/g, match => {
|
||||
const filled = indexHtml.replace(
|
||||
/data-replace (src|href)="[^"]+"/g,
|
||||
match => {
|
||||
const i = match.indexOf(' ');
|
||||
const j = match.indexOf('=');
|
||||
const uri = getWebviewUri(match.slice(j + 2, -1).trim());
|
||||
return match.slice(i + 1, j) + '="' + uri.toString() + '"';
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return filled;
|
||||
}
|
||||
|
||||
function getGraphStyle(): object {
|
||||
return vscode.workspace.getConfiguration('foam.graph').get('style');
|
||||
}
|
||||
|
||||
@@ -91,11 +91,7 @@ export class NotesProvider extends FolderTreeProvider<
|
||||
NotesTreeItems,
|
||||
Resource
|
||||
> {
|
||||
public show = new ContextMemento<'all' | 'notes-only'>(
|
||||
this.state,
|
||||
`foam-vscode.views.notes-explorer.show`,
|
||||
'all'
|
||||
);
|
||||
public show: ContextMemento<'all' | 'notes-only'>;
|
||||
|
||||
constructor(
|
||||
private workspace: FoamWorkspace,
|
||||
@@ -103,6 +99,12 @@ export class NotesProvider extends FolderTreeProvider<
|
||||
private state: vscode.Memento
|
||||
) {
|
||||
super();
|
||||
this.show = new ContextMemento<'all' | 'notes-only'>(
|
||||
this.state,
|
||||
`foam-vscode.views.notes-explorer.show`,
|
||||
'all'
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.notes-explorer.show:all`,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getAttachmentsExtensions, getOrphansConfig } from '../../settings';
|
||||
import { GroupedResourcesTreeDataProvider } from './utils/grouped-resources-tree-data-provider';
|
||||
import { getAttachmentsExtensions } from '../../settings';
|
||||
import {
|
||||
GroupedResourcesConfig,
|
||||
GroupedResourcesTreeDataProvider,
|
||||
} from './utils/grouped-resources-tree-data-provider';
|
||||
import { ResourceTreeItem, UriTreeItem } from './utils/tree-view-utils';
|
||||
import { IMatcher } from '../../core/services/datastore';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
@@ -46,6 +49,13 @@ export default async function activate(
|
||||
);
|
||||
}
|
||||
|
||||
/** Retrieve the orphans configuration */
|
||||
export function getOrphansConfig(): GroupedResourcesConfig {
|
||||
const orphansConfig = vscode.workspace.getConfiguration('foam.orphans');
|
||||
const exclude: string[] = orphansConfig.get('exclude');
|
||||
return { exclude };
|
||||
}
|
||||
|
||||
export class OrphanTreeView extends GroupedResourcesTreeDataProvider {
|
||||
constructor(
|
||||
state: vscode.Memento,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { createMatcherAndDataStore } from '../../services/editor';
|
||||
import { getPlaceholdersConfig } from '../../settings';
|
||||
import { GroupedResourcesTreeDataProvider } from './utils/grouped-resources-tree-data-provider';
|
||||
import {
|
||||
GroupedResourcesConfig,
|
||||
GroupedResourcesTreeDataProvider,
|
||||
} from './utils/grouped-resources-tree-data-provider';
|
||||
import {
|
||||
UriTreeItem,
|
||||
createBacklinkItemsForResource,
|
||||
@@ -16,6 +18,13 @@ import { URI } from '../../core/model/uri';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { FolderTreeItem } from './utils/folder-tree-provider';
|
||||
|
||||
/** Retrieve the placeholders configuration */
|
||||
export function getPlaceholdersConfig(): GroupedResourcesConfig {
|
||||
const placeholderCfg = vscode.workspace.getConfiguration('foam.placeholders');
|
||||
const exclude: string[] = placeholderCfg.get('exclude');
|
||||
return { exclude };
|
||||
}
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
Folder,
|
||||
} from './folder-tree-provider';
|
||||
|
||||
export interface GroupedResourcesConfig {
|
||||
exclude: string[];
|
||||
}
|
||||
|
||||
type GroupedResourceTreeItem = UriTreeItem | FolderTreeItem<URI>;
|
||||
|
||||
/**
|
||||
@@ -31,11 +35,7 @@ export abstract class GroupedResourcesTreeDataProvider extends FolderTreeProvide
|
||||
GroupedResourceTreeItem,
|
||||
URI
|
||||
> {
|
||||
public groupBy = new ContextMemento<'off' | 'folder'>(
|
||||
this.state,
|
||||
`foam-vscode.views.${this.providerId}.group-by`,
|
||||
'folder'
|
||||
);
|
||||
public groupBy: ContextMemento<'off' | 'folder'>;
|
||||
|
||||
/**
|
||||
* Creates an instance of GroupedResourcesTreeDataProvider.
|
||||
@@ -57,6 +57,12 @@ export abstract class GroupedResourcesTreeDataProvider extends FolderTreeProvide
|
||||
private matcher: IMatcher
|
||||
) {
|
||||
super();
|
||||
this.groupBy = new ContextMemento<'off' | 'folder'>(
|
||||
this.state,
|
||||
`foam-vscode.views.${this.providerId}.group-by`,
|
||||
'folder'
|
||||
);
|
||||
|
||||
this.disposables.push(
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${this.providerId}.group-by:folder`,
|
||||
|
||||
@@ -5,11 +5,11 @@ import { toVsCodeUri } from '../../../utils/vsc-utils';
|
||||
import { Range } from '../../../core/model/range';
|
||||
import { URI } from '../../../core/model/uri';
|
||||
import { FoamWorkspace } from '../../../core/model/workspace';
|
||||
import { getNoteTooltip } from '../../../utils';
|
||||
import { isSome } from '../../../core/utils';
|
||||
import { getBlockFor } from '../../../core/services/markdown-parser';
|
||||
import { Connection, FoamGraph } from '../../../core/model/graph';
|
||||
import { Logger } from '../../../core/utils/log';
|
||||
import { getNoteTooltip } from '../../../services/editor';
|
||||
|
||||
export class BaseTreeItem extends vscode.TreeItem {
|
||||
resolveTreeItem(): Promise<vscode.TreeItem> {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { isNone } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { isNone } from '../../core/utils';
|
||||
|
||||
export const markdownItFoamTags = (
|
||||
md: markdownit,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*global markdownit:readonly*/
|
||||
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { ResourceParser } from '../../core/model/note';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
|
||||
export const WIKILINK_EMBED_REGEX =
|
||||
/((?:(?:full|content)-(?:inline|card)|full|content|inline|card)?!\[\[[^[\]]+?\]\])/;
|
||||
|
||||
export const markdownItWikilinkEmbed = (
|
||||
md: markdownit,
|
||||
workspace: FoamWorkspace,
|
||||
parser: ResourceParser
|
||||
) => {
|
||||
return md.use(markdownItRegex, {
|
||||
name: 'embed-wikilinks',
|
||||
regex: WIKILINK_EMBED_REGEX,
|
||||
replace: (wikilinkItem: string) => {
|
||||
return `
|
||||
<div style="padding: 0.25em; margin: 1.5em 0; text-align: center; border: 1px solid var(--vscode-editorLineNumber-foreground);">
|
||||
Embeds are not supported in web extension: <br/> ${wikilinkItem}
|
||||
</div>`;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default markdownItWikilinkEmbed;
|
||||
@@ -4,7 +4,6 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { workspace as vsWorkspace } from 'vscode';
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import { isSome, isNone } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { Resource, ResourceParser } from '../../core/model/note';
|
||||
@@ -13,6 +12,7 @@ import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { Position } from '../../core/model/position';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
import { isNone, isSome } from '../../core/utils';
|
||||
|
||||
export const WIKILINK_EMBED_REGEX =
|
||||
/((?:(?:full|content)-(?:inline|card)|full|content|inline|card)?!\[\[[^[\]]+?\]\])/;
|
||||
@@ -34,7 +34,7 @@ export const markdownItWikilinkEmbed = (
|
||||
regex: WIKILINK_EMBED_REGEX,
|
||||
replace: (wikilinkItem: string) => {
|
||||
try {
|
||||
const [_, noteEmbedModifier, wikilink] = wikilinkItem.match(
|
||||
const [, noteEmbedModifier, wikilink] = wikilinkItem.match(
|
||||
WIKILINK_EMBED_REGEX_GROUPS
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import markdownItRegex from 'markdown-it-regex';
|
||||
import * as vscode from 'vscode';
|
||||
import { isNone } from '../../utils';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
@@ -10,6 +9,7 @@ import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { Range } from '../../core/model/range';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { toSlug } from '../../utils/slug';
|
||||
import { isNone } from '../../core/utils';
|
||||
|
||||
export const markdownItWikilinkNavigation = (
|
||||
md: markdownit,
|
||||
@@ -46,7 +46,8 @@ export const markdownItWikilinkNavigation = (
|
||||
? `${resource.title}${formattedSection}`
|
||||
: alias;
|
||||
const resourceLink = `/${vscode.workspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
toVsCodeUri(resource.uri),
|
||||
false
|
||||
)}`;
|
||||
return getResourceLink(
|
||||
`${resource.title}${formattedSection}`,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { isInFrontMatter, isOnYAMLKeywordLine, mdDocSelector } from '../utils';
|
||||
import { isInFrontMatter, isOnYAMLKeywordLine } from '../core/utils/md';
|
||||
import { mdDocSelector } from '../services/editor';
|
||||
|
||||
// this regex is different from HASHTAG_REGEX in that it does not look for a
|
||||
// #+character. It uses a negative look-ahead for `# `
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Resource, ResourceParser } from '../core/model/note';
|
||||
import { Range } from '../core/model/range';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { MarkdownLink } from '../core/services/markdown-link';
|
||||
import { isNone } from '../utils';
|
||||
import {
|
||||
fromVsCodeUri,
|
||||
toVsCodePosition,
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../utils/vsc-utils';
|
||||
import { isNone } from '../core/utils';
|
||||
|
||||
const AMBIGUOUS_IDENTIFIER_CODE = 'ambiguous-identifier';
|
||||
const UNKNOWN_SECTION_CODE = 'unknown-section';
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import { TextEncoder } from 'util';
|
||||
import {
|
||||
EndOfLine,
|
||||
FileType,
|
||||
@@ -8,15 +6,18 @@ import {
|
||||
Selection,
|
||||
SnippetString,
|
||||
TextDocument,
|
||||
TextEditor,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
window,
|
||||
workspace,
|
||||
WorkspaceEdit,
|
||||
MarkdownString,
|
||||
} from 'vscode';
|
||||
import { focusNote } from '../utils';
|
||||
import { getExcerpt, stripFrontMatter, stripImages } from '../core/utils/md';
|
||||
import { isSome } from '../core/utils/core';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { isSome } from '../core/utils';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import {
|
||||
AlwaysIncludeMatcher,
|
||||
FileListBasedMatcher,
|
||||
@@ -31,6 +32,35 @@ interface SelectionInfo {
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MarkdownString of the note content
|
||||
* @param note A Foam Note
|
||||
*/
|
||||
export function getNoteTooltip(content: string): string {
|
||||
const strippedContent = stripFrontMatter(stripImages(content));
|
||||
return formatMarkdownTooltip(strippedContent) as any;
|
||||
}
|
||||
|
||||
export function formatMarkdownTooltip(content: string): MarkdownString {
|
||||
const LINES_LIMIT = 16;
|
||||
const { excerpt, lines } = getExcerpt(content, LINES_LIMIT);
|
||||
const totalLines = content.split('\n').length;
|
||||
const diffLines = totalLines - lines;
|
||||
const ellipsis = diffLines > 0 ? `\n\n[...] *(+ ${diffLines} lines)*` : '';
|
||||
const md = new MarkdownString(`${excerpt}${ellipsis}`);
|
||||
md.isTrusted = true;
|
||||
return md;
|
||||
}
|
||||
|
||||
export const mdDocSelector = [
|
||||
{ language: 'markdown', scheme: 'file' },
|
||||
{ language: 'markdown', scheme: 'untitled' },
|
||||
];
|
||||
|
||||
export function isMdEditor(editor: TextEditor) {
|
||||
return editor && editor.document && editor.document.languageId === 'markdown';
|
||||
}
|
||||
|
||||
export function findSelectionContent(): SelectionInfo | undefined {
|
||||
const editor = window.activeTextEditor;
|
||||
if (editor === undefined) {
|
||||
@@ -51,6 +81,24 @@ export function findSelectionContent(): SelectionInfo | undefined {
|
||||
};
|
||||
}
|
||||
|
||||
export async function focusNote(
|
||||
notePath: URI,
|
||||
moveCursorToEnd: boolean,
|
||||
viewColumn: ViewColumn = ViewColumn.Active
|
||||
) {
|
||||
const document = await workspace.openTextDocument(toVsCodeUri(notePath));
|
||||
const editor = await window.showTextDocument(document, viewColumn);
|
||||
|
||||
// Move the cursor to end of the file
|
||||
if (moveCursorToEnd) {
|
||||
const { lineCount } = editor.document;
|
||||
const { range } = editor.document.lineAt(lineCount - 1);
|
||||
editor.selection = new Selection(range.end, range.end);
|
||||
}
|
||||
|
||||
return { document, editor };
|
||||
}
|
||||
|
||||
export async function createDocAndFocus(
|
||||
text: SnippetString,
|
||||
filepath: URI,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { window, commands, ExtensionContext } from 'vscode';
|
||||
import { workspace, window, commands, ExtensionContext } from 'vscode';
|
||||
import { IDisposable } from '../core/common/lifecycle';
|
||||
import { BaseLogger, ILogger, LogLevel } from '../core/utils/log';
|
||||
import { getFoamLoggerLevel } from '../settings';
|
||||
|
||||
function getFoamLoggerLevel(): LogLevel {
|
||||
return workspace.getConfiguration('foam.logging').get('level') ?? 'info';
|
||||
}
|
||||
|
||||
export interface VsCodeLogger extends ILogger, IDisposable {
|
||||
show();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { TextEncoder } from 'util';
|
||||
import {
|
||||
SnippetString,
|
||||
ViewColumn,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
window,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import { focusNote, isNone } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
import { UserCancelledOperation } from './errors';
|
||||
@@ -18,6 +16,7 @@ import {
|
||||
deleteFile,
|
||||
fileExists,
|
||||
findSelectionContent,
|
||||
focusNote,
|
||||
getCurrentEditorDirectory,
|
||||
readFile,
|
||||
replaceSelection,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
import { Resolver } from './variable-resolver';
|
||||
import dateFormat from 'dateformat';
|
||||
import { getFoamVsCodeConfig } from './config';
|
||||
import { isNone } from '../core/utils';
|
||||
|
||||
/**
|
||||
* The templates directory
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { workspace, GlobPattern } from 'vscode';
|
||||
import { uniq } from 'lodash';
|
||||
import { LogLevel } from './core/utils/log';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
|
||||
/**
|
||||
@@ -39,15 +38,6 @@ export function getAttachmentsExtensions() {
|
||||
.map(ext => '.' + ext.trim());
|
||||
}
|
||||
|
||||
export function getWikilinkDefinitionSetting():
|
||||
| 'withExtensions'
|
||||
| 'withoutExtensions'
|
||||
| 'off' {
|
||||
return workspace
|
||||
.getConfiguration('foam.edit')
|
||||
.get('linkReferenceDefinitions', 'withoutExtensions');
|
||||
}
|
||||
|
||||
/** Retrieve the list of file ignoring globs. */
|
||||
export function getIgnoredFilesSetting(): GlobPattern[] {
|
||||
return [
|
||||
@@ -56,35 +46,3 @@ export function getIgnoredFilesSetting(): GlobPattern[] {
|
||||
...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');
|
||||
}
|
||||
|
||||
/** 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';
|
||||
}
|
||||
|
||||
/** Retrieve the orphans configuration */
|
||||
export function getOrphansConfig(): GroupedResourcesConfig {
|
||||
const orphansConfig = workspace.getConfiguration('foam.orphans');
|
||||
const exclude: string[] = orphansConfig.get('exclude');
|
||||
return { exclude };
|
||||
}
|
||||
|
||||
/** Retrieve the placeholders configuration */
|
||||
export function getPlaceholdersConfig(): GroupedResourcesConfig {
|
||||
const placeholderCfg = workspace.getConfiguration('foam.placeholders');
|
||||
const exclude: string[] = placeholderCfg.get('exclude');
|
||||
return { exclude };
|
||||
}
|
||||
|
||||
export interface GroupedResourcesConfig {
|
||||
exclude: string[];
|
||||
}
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
import {
|
||||
Range,
|
||||
TextDocument,
|
||||
window,
|
||||
Position,
|
||||
TextEditor,
|
||||
workspace,
|
||||
Selection,
|
||||
MarkdownString,
|
||||
ViewColumn,
|
||||
} from 'vscode';
|
||||
import matter from 'gray-matter';
|
||||
import { toVsCodeUri } from './utils/vsc-utils';
|
||||
import { URI } from './core/model/uri';
|
||||
import { getEditorEOL } from './services/editor';
|
||||
|
||||
export const mdDocSelector = [
|
||||
{ language: 'markdown', scheme: 'file' },
|
||||
{ language: 'markdown', scheme: 'untitled' },
|
||||
];
|
||||
|
||||
export function isMdEditor(editor: TextEditor) {
|
||||
return editor && editor.document && editor.document.languageId === 'markdown';
|
||||
}
|
||||
|
||||
export function detectGeneratedCode(
|
||||
fullText: string,
|
||||
header: string,
|
||||
footer: string
|
||||
): { range: Range | null; lines: string[] } {
|
||||
const lines = fullText.split(getEditorEOL());
|
||||
|
||||
const headerLine = lines.findIndex(line => line === header);
|
||||
const footerLine = lines.findIndex(line => line === footer);
|
||||
|
||||
if (headerLine < 0 || headerLine >= footerLine) {
|
||||
return {
|
||||
range: null,
|
||||
lines: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
range: new Range(
|
||||
new Position(headerLine, 0),
|
||||
new Position(footerLine, lines[footerLine].length + 1)
|
||||
),
|
||||
lines: lines.slice(headerLine + 1, footerLine + 1),
|
||||
};
|
||||
}
|
||||
|
||||
export function hasEmptyTrailing(doc: TextDocument): boolean {
|
||||
return doc.lineAt(doc.lineCount - 1).isEmptyOrWhitespace;
|
||||
}
|
||||
|
||||
export function getText(range: Range): string {
|
||||
return window.activeTextEditor.document.getText(range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for the "Copy to Clipboard Without Brackets" command
|
||||
*
|
||||
*/
|
||||
export function removeBrackets(s: string): string {
|
||||
// take in the string, split on space
|
||||
const stringSplitBySpace = s.split(' ');
|
||||
|
||||
// loop through words
|
||||
const modifiedWords = stringSplitBySpace.map(currentWord => {
|
||||
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, ' ');
|
||||
|
||||
// then we titlecase the word so "you are awesome"
|
||||
// becomes "You Are Awesome"
|
||||
const titleCasedWord = toTitleCase(word);
|
||||
|
||||
return titleCasedWord;
|
||||
}
|
||||
|
||||
return currentWord;
|
||||
});
|
||||
|
||||
return modifiedWords.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in a string and returns it titlecased
|
||||
*
|
||||
* @example toTitleCase("hello world") -> "Hello World"
|
||||
*/
|
||||
export function toTitleCase(word: string): string {
|
||||
return word
|
||||
.split(' ')
|
||||
.map(word => word[0].toUpperCase() + word.substring(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given object is defined
|
||||
*
|
||||
* @param value The object to verify
|
||||
*/
|
||||
export function isSome<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is NonNullable<T> {
|
||||
//
|
||||
return value != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the given object is not defined
|
||||
*
|
||||
* @param value The object to verify
|
||||
*/
|
||||
export function isNone<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is null | undefined | void {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
export async function focusNote(
|
||||
notePath: URI,
|
||||
moveCursorToEnd: boolean,
|
||||
viewColumn: ViewColumn = ViewColumn.Active
|
||||
) {
|
||||
const document = await workspace.openTextDocument(toVsCodeUri(notePath));
|
||||
const editor = await window.showTextDocument(document, viewColumn);
|
||||
|
||||
// Move the cursor to end of the file
|
||||
if (moveCursorToEnd) {
|
||||
const { lineCount } = editor.document;
|
||||
const { range } = editor.document.lineAt(lineCount - 1);
|
||||
editor.selection = new Selection(range.end, range.end);
|
||||
}
|
||||
|
||||
return { document, editor };
|
||||
}
|
||||
|
||||
export function getContainsTooltip(titles: string[]): string {
|
||||
const TITLES_LIMIT = 5;
|
||||
const ellipsis = titles.length > TITLES_LIMIT ? ',...' : '';
|
||||
return `Contains "${titles.slice(0, TITLES_LIMIT).join('", "')}"${ellipsis}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Depending on the current vscode version, returns a MarkdownString of the
|
||||
* note content casted as string or returns a simple string
|
||||
* MarkdownString is only available from 1.52.1 onwards
|
||||
* https://code.visualstudio.com/updates/v1_52#_markdown-tree-tooltip-api
|
||||
* @param note A Foam Note
|
||||
*/
|
||||
export function getNoteTooltip(content: string): string {
|
||||
const strippedContent = stripFrontMatter(stripImages(content));
|
||||
return formatMarkdownTooltip(strippedContent) as any;
|
||||
}
|
||||
|
||||
export function formatMarkdownTooltip(content: string): MarkdownString {
|
||||
const LINES_LIMIT = 16;
|
||||
const { excerpt, lines } = getExcerpt(content, LINES_LIMIT);
|
||||
const totalLines = content.split('\n').length;
|
||||
const diffLines = totalLines - lines;
|
||||
const ellipsis = diffLines > 0 ? `\n\n[...] *(+ ${diffLines} lines)*` : '';
|
||||
const md = new MarkdownString(`${excerpt}${ellipsis}`);
|
||||
md.isTrusted = true;
|
||||
return md;
|
||||
}
|
||||
|
||||
export function getExcerpt(
|
||||
markdown: string,
|
||||
maxLines: number
|
||||
): { excerpt: string; lines: number } {
|
||||
const OFFSET_LINES_LIMIT = 5;
|
||||
const paragraphs = markdown.replace(/\r\n/g, '\n').split('\n\n');
|
||||
const excerpt: string[] = [];
|
||||
let lines = 0;
|
||||
for (const paragraph of paragraphs) {
|
||||
const n = paragraph.split('\n').length;
|
||||
if (lines > maxLines || lines + n - maxLines > OFFSET_LINES_LIMIT) {
|
||||
break;
|
||||
}
|
||||
excerpt.push(paragraph);
|
||||
lines = lines + n + 1;
|
||||
}
|
||||
return { excerpt: excerpt.join('\n\n'), lines };
|
||||
}
|
||||
|
||||
export function stripFrontMatter(markdown: string): string {
|
||||
return matter(markdown).content.trim();
|
||||
}
|
||||
|
||||
export function stripImages(markdown: string): string {
|
||||
return markdown.replace(
|
||||
/!\[(.*)\]\([-/\\.A-Za-z]*\)/gi,
|
||||
'$1'.length ? '[Image: $1]' : ''
|
||||
);
|
||||
}
|
||||
|
||||
export function isInFrontMatter(content: string, lineNumber: number): Boolean {
|
||||
const FIRST_DELIMITER_MATCH = /^---\s*?$/gm;
|
||||
const LAST_DELIMITER_MATCH = /^[-.]{3}\s*?$/g;
|
||||
|
||||
// if we're on the first line, we're not _yet_ in the front matter
|
||||
if (lineNumber === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// look for --- at start, and a second --- or ... to end
|
||||
if (content.match(FIRST_DELIMITER_MATCH) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = content.split('\n');
|
||||
lines.shift();
|
||||
const endLineMatches = (l: string) => l.match(LAST_DELIMITER_MATCH);
|
||||
const endLineNumber = lines.findIndex(endLineMatches);
|
||||
|
||||
return endLineNumber === -1 || endLineNumber >= lineNumber;
|
||||
}
|
||||
|
||||
export function isOnYAMLKeywordLine(content: string, keyword: string): Boolean {
|
||||
const keywordMatch = /^\s*(\w+):/gm;
|
||||
|
||||
if (content.match(keywordMatch) === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matches = Array.from(content.matchAll(keywordMatch));
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
return lastMatch[1] === keyword;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# File with different link formats
|
||||
|
||||
markdown link [home page](https://foambubble.github.io/)
|
||||
|
||||
wikilink to file [[first-document]].
|
||||
|
||||
markdown format link to local [file](first-document.md)
|
||||
|
||||
embedded wikilink to file ![[second-document]].
|
||||
|
||||
wikilink to placeholder [[non-exist-file]]
|
||||
|
||||
in-note anchor [[file-with-different-link-formats#one section]]
|
||||
|
||||
alias to anchor [[file-with-different-link-formats#one section|another name]]
|
||||
|
||||
alias [[first-document|an alias]]
|
||||
|
||||
dupilcated wikilink to file [[first-document]]
|
||||
|
||||
# one section
|
||||
18
readme.md
18
readme.md
@@ -5,7 +5,7 @@
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://marketplace.visualstudio.com/items?itemName=foam.foam-vscode)
|
||||
@@ -190,6 +190,16 @@ Foam is licensed under the [MIT license](LICENSE).
|
||||
[Backlinking]: docs/user/features/backlinking.md "Backlinking"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
## Contribution Guide
|
||||
|
||||
See the [Contribution Guide](https://foambubble.github.io/foam/dev/contribution-guide)
|
||||
|
||||
## Code of conduct
|
||||
|
||||
See the [Code of Conduct](https://foambubble.github.io/foam/dev/code-of-conduct)
|
||||
|
||||
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
@@ -349,6 +359,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://thara.dev"><img src="https://avatars.githubusercontent.com/u/1532891?v=4?s=60" width="60px;" alt="Tomochika Hara"/><br /><sub><b>Tomochika Hara</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=thara" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dcarosone"><img src="https://avatars.githubusercontent.com/u/11495017?v=4?s=60" width="60px;" alt="Daniel Carosone"/><br /><sub><b>Daniel Carosone</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=dcarosone" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MABruni"><img src="https://avatars.githubusercontent.com/u/100445384?v=4?s=60" width="60px;" alt="Miguel Angel Bruni Montero"/><br /><sub><b>Miguel Angel Bruni Montero</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=MABruni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Walshkev"><img src="https://avatars.githubusercontent.com/u/77123083?v=4?s=60" width="60px;" alt="Kevin Walsh "/><br /><sub><b>Kevin Walsh </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Walshkev" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://hereistheusername.github.io/"><img src="https://avatars.githubusercontent.com/u/33437051?v=4?s=60" width="60px;" alt="Xinglan Liu"/><br /><sub><b>Xinglan Liu</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hereistheusername" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.hegghammer.com"><img src="https://avatars.githubusercontent.com/u/64712218?v=4?s=60" width="60px;" alt="Thomas Hegghammer"/><br /><sub><b>Thomas Hegghammer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Hegghammer" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PiotrAleksander"><img src="https://avatars.githubusercontent.com/u/6314591?v=4?s=60" width="60px;" alt="Piotr Mrzygłosz"/><br /><sub><b>Piotr Mrzygłosz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=PiotrAleksander" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user