mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
27 Commits
v0.5.0-alp
...
v0.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
846908e9d2 | ||
|
|
da69cc0f5d | ||
|
|
76a9a4ac93 | ||
|
|
138217e39d | ||
|
|
b473749260 | ||
|
|
fa01cce934 | ||
|
|
44e498dddb | ||
|
|
679a2947d2 | ||
|
|
8710438a46 | ||
|
|
90f869e8d0 | ||
|
|
8a7e9bcdd4 | ||
|
|
e68b6e3023 | ||
|
|
ba84b9b496 | ||
|
|
af5a4a20e6 | ||
|
|
38181acd53 | ||
|
|
683c28e393 | ||
|
|
f9444636e2 | ||
|
|
fd0b2ef912 | ||
|
|
b911d5b7b1 | ||
|
|
7287aa62b5 | ||
|
|
0475d26f2c | ||
|
|
bf43113fac | ||
|
|
81639fd650 | ||
|
|
8d110eb04b | ||
|
|
d89b6f0285 | ||
|
|
dbdb4c30b8 | ||
|
|
fdc2c7cf4c |
@@ -212,7 +212,8 @@
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/8980971?v=4",
|
||||
"profile": "https://sanketdg.github.io",
|
||||
"contributions": [
|
||||
"doc"
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -406,6 +407,115 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ingalless",
|
||||
"name": "ingalless",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/22981941?v=4",
|
||||
"profile": "https://ingalless.com",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jmg-duarte",
|
||||
"name": "José Duarte",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/15343819?v=4",
|
||||
"profile": "http://jmg-duarte.github.io",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yenly",
|
||||
"name": "Yenly",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/6759658?v=4",
|
||||
"profile": "https://www.yenly.wtf",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hikerpig",
|
||||
"name": "hikerpig",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/2259688?v=4",
|
||||
"profile": "https://www.hikerpig.cn",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Sigfried",
|
||||
"name": "Sigfried Gold",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1586931?v=4",
|
||||
"profile": "http://sigfried.org",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tristansokol",
|
||||
"name": "Tristan Sokol",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/867661?v=4",
|
||||
"profile": "http://www.tristansokol.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "umbrellait-danil-rodin",
|
||||
"name": "Danil Rodin",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/49779373?v=4",
|
||||
"profile": "https://umbrellait.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "scott-joe",
|
||||
"name": "Scott Williams",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/2026866?v=4",
|
||||
"profile": "https://www.linkedin.com/in/scottjoewilliams/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Jackiexiao",
|
||||
"name": "jackiexiao",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/18050469?v=4",
|
||||
"profile": "https://jackiexiao.github.io/blog",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jbn",
|
||||
"name": "John B Nelson",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/78835?v=4",
|
||||
"profile": "https://generativist.substack.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "asifm",
|
||||
"name": "Asif Mehedi",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/3958387?v=4",
|
||||
"profile": "https://github.com/asifm",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "litanlitudan",
|
||||
"name": "Tan Li",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/4970420?v=4",
|
||||
"profile": "https://github.com/litanlitudan",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -15,7 +15,7 @@ There are many other templates which also support publish your foam workspace to
|
||||
* [demo-website](https://jackiexiao.github.io/foam/)
|
||||
* foam-jekyll-template
|
||||
* [repo](https://github.com/hikerpig/foam-jekyll-template)
|
||||
* [demo-website](https://blog.hikerpig.cn/wiki/)
|
||||
* [demo-website](https://wiki.hikerpig.cn/)
|
||||
|
||||
[[todo]] [[good-first-task]] Improve this documentation
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
|
||||
@@ -165,6 +165,20 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.5.0-alpha.2"
|
||||
"version": "0.6.0"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
"build:core": "yarn workspace foam-core build",
|
||||
"watch:core": "yarn workspace foam-core start",
|
||||
"test:core": "yarn workspace foam-core test",
|
||||
"vscode:package-extension": "yarn workspace foam-vscode package-extension",
|
||||
"vscode:install-extension": "yarn workspace foam-vscode install-extension",
|
||||
"vscode:publish-extension": "yarn workspace foam-vscode publish-extension",
|
||||
"clean": "lerna run clean",
|
||||
"build": "lerna run build",
|
||||
"test": "lerna run test",
|
||||
|
||||
@@ -19,7 +19,7 @@ $ npm install -g foam-cli
|
||||
$ foam COMMAND
|
||||
running command...
|
||||
$ foam (-v|--version|version)
|
||||
foam-cli/0.5.0-alpha.2 darwin-x64 node-v10.19.0
|
||||
foam-cli/0.6.0 darwin-x64 node-v10.19.0
|
||||
$ foam --help [COMMAND]
|
||||
USAGE
|
||||
$ foam COMMAND
|
||||
@@ -65,7 +65,7 @@ EXAMPLE
|
||||
$ foam-cli janitor path-to-foam-workspace
|
||||
```
|
||||
|
||||
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.5.0-alpha.2/src/commands/janitor.ts)_
|
||||
_See code: [src/commands/janitor.ts](https://github.com/foambubble/foam/blob/v0.6.0/src/commands/janitor.ts)_
|
||||
|
||||
## `foam migrate [WORKSPACEPATH]`
|
||||
|
||||
@@ -84,7 +84,7 @@ EXAMPLE
|
||||
Successfully generated link references and heading!
|
||||
```
|
||||
|
||||
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.5.0-alpha.2/src/commands/migrate.ts)_
|
||||
_See code: [src/commands/migrate.ts](https://github.com/foambubble/foam/blob/v0.6.0/src/commands/migrate.ts)_
|
||||
<!-- commandsstop -->
|
||||
|
||||
## Development
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "foam-cli",
|
||||
"description": "Foam CLI",
|
||||
"version": "0.5.0-alpha.2",
|
||||
"version": "0.6.0",
|
||||
"author": "Jani Eväkallio @jevakallio",
|
||||
"bin": {
|
||||
"foam": "./bin/run"
|
||||
@@ -11,7 +11,7 @@
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"foam-core": "^0.5.0-alpha.2",
|
||||
"foam-core": "^0.6.0",
|
||||
"ora": "^4.0.4",
|
||||
"tslib": "^1"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "foam-core",
|
||||
"author": "Jani Eväkallio",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.5.0-alpha.2",
|
||||
"version": "0.6.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
uriToSlug,
|
||||
extractHashtags,
|
||||
extractTagsFromProp,
|
||||
nameToSlug,
|
||||
} from './utils';
|
||||
import { ID } from './types';
|
||||
import { ParserPlugin } from './plugins';
|
||||
@@ -53,7 +54,7 @@ const wikilinkPlugin: ParserPlugin = {
|
||||
if (node.type === 'wikiLink') {
|
||||
note.links.push({
|
||||
type: 'wikilink',
|
||||
slug: node.value as string,
|
||||
slug: nameToSlug(node.value as string),
|
||||
position: node.position!,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ export const uriToSlug = (noteUri: URI): string => {
|
||||
return GithubSlugger.slug(path.parse(noteUri).name);
|
||||
};
|
||||
|
||||
export const nameToSlug = (noteName: string): string => {
|
||||
return GithubSlugger.slug(noteName);
|
||||
};
|
||||
|
||||
export const hashURI = (uri: URI): ID => {
|
||||
return hash(path.normalize(uri));
|
||||
};
|
||||
|
||||
@@ -11,6 +11,8 @@ const pageA = `
|
||||
## Section
|
||||
- [[page-b]]
|
||||
- [[page-c]]
|
||||
- [[Page D]]
|
||||
- [[page e]]
|
||||
`;
|
||||
|
||||
const pageB = `
|
||||
@@ -22,6 +24,14 @@ const pageC = `
|
||||
# Page C
|
||||
`;
|
||||
|
||||
const pageD = `
|
||||
# Page D
|
||||
`;
|
||||
|
||||
const pageE = `
|
||||
# Page E
|
||||
`;
|
||||
|
||||
const createNoteFromMarkdown = createMarkdownParser([]).parse;
|
||||
|
||||
describe('Markdown loader', () => {
|
||||
@@ -30,13 +40,15 @@ describe('Markdown loader', () => {
|
||||
graph.setNote(createNoteFromMarkdown('/page-a.md', pageA, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-b.md', pageB, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-d.md', pageD, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-e.md', pageE, '\n'));
|
||||
|
||||
expect(
|
||||
graph
|
||||
.getNotes()
|
||||
.map(n => n.slug)
|
||||
.sort()
|
||||
).toEqual(['page-a', 'page-b', 'page-c']);
|
||||
).toEqual(['page-a', 'page-b', 'page-c', 'page-d', 'page-e']);
|
||||
});
|
||||
|
||||
it('Parses wikilinks correctly', () => {
|
||||
@@ -48,13 +60,15 @@ describe('Markdown loader', () => {
|
||||
createNoteFromMarkdown('/page-b.md', pageB, '\n')
|
||||
);
|
||||
graph.setNote(createNoteFromMarkdown('/page-c.md', pageC, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-d.md', pageD, '\n'));
|
||||
graph.setNote(createNoteFromMarkdown('/page-e.md', pageE, '\n'));
|
||||
|
||||
expect(
|
||||
graph.getBacklinks(noteB.id).map(link => graph.getNote(link.from)!.slug)
|
||||
).toEqual(['page-a']);
|
||||
expect(
|
||||
graph.getForwardLinks(noteA.id).map(link => graph.getNote(link.to)!.slug)
|
||||
).toEqual(['page-b', 'page-c']);
|
||||
).toEqual(['page-b', 'page-c', 'page-d', 'page-e']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
uriToSlug,
|
||||
nameToSlug,
|
||||
hashURI,
|
||||
computeRelativeURI,
|
||||
extractHashtags,
|
||||
@@ -14,6 +15,13 @@ describe('URI utils', () => {
|
||||
expect(uriToSlug('many.dots.name.markdown')).toEqual('manydotsname');
|
||||
});
|
||||
|
||||
it('converts a name to a slug', () => {
|
||||
expect(nameToSlug('this.has.dots')).toEqual('thishasdots');
|
||||
expect(nameToSlug('title')).toEqual('title');
|
||||
expect(nameToSlug('this is a title')).toEqual('this-is-a-title');
|
||||
expect(nameToSlug('this is a title/slug')).toEqual('this-is-a-titleslug');
|
||||
});
|
||||
|
||||
it('normalizes URI before hashing', () => {
|
||||
expect(hashURI('/this/is/a/path.md')).toEqual(
|
||||
hashURI('/this/has/../is/a/path.md')
|
||||
|
||||
@@ -4,6 +4,19 @@ 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.6.0] - 2020-11-19
|
||||
|
||||
New features:
|
||||
- Added command to create notes from templates (#115 - Thanks @ingalless)
|
||||
|
||||
Fixes and Improvements:
|
||||
- Foam model: Fixed bug that prevented wikilinks from being slugified (#323 - thanks @SanketDG)
|
||||
- Editor: Improvements in defaults for ignored files setting (thanks @jmg-duarte)
|
||||
- Dataviz: Centering of the graph on note displayed in active editor (#319)
|
||||
- Dataviz: Improved graph styling
|
||||
- Dataviz: Added setting to cap the length of labels in the graph (thanks @jmg-duarte)
|
||||
- Misc: Fixed problem with packaging icon in extension (#350 - thanks @litanlitudan)
|
||||
|
||||
## [0.5.0] - 2020-11-09
|
||||
|
||||
New features:
|
||||
|
||||
BIN
packages/foam-vscode/icon/FOAM_ICON.png
Normal file
BIN
packages/foam-vscode/icon/FOAM_ICON.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 604 KiB |
BIN
packages/foam-vscode/icon/FOAM_ICON_256.png
Normal file
BIN
packages/foam-vscode/icon/FOAM_ICON_256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -3,13 +3,18 @@
|
||||
"displayName": "Foam for VSCode (Wikilinks to Markdown)",
|
||||
"description": "Generate markdown reference lists from wikilinks in a workspace",
|
||||
"author": "Jani Eväkallio",
|
||||
"repository": "https://github.com/foambubble/foam",
|
||||
"version": "0.5.0-alpha.2",
|
||||
"repository": {
|
||||
"url": "https://github.com/foambubble/foam",
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.6.0",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
"vscode": "^1.45.1"
|
||||
},
|
||||
"icon": "icon/FOAM_ICON_256.png",
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
@@ -54,6 +59,10 @@
|
||||
{
|
||||
"command": "foam-vscode.copy-without-brackets",
|
||||
"title": "Foam: Copy To Clipboard Without Brackets"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.create-note-from-template",
|
||||
"title": "Foam: Create New Note From Template"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
@@ -64,10 +73,11 @@
|
||||
"array"
|
||||
],
|
||||
"default": [
|
||||
".vscode/*",
|
||||
"_layouts/*"
|
||||
".vscode/**/*",
|
||||
"_layouts/**/*",
|
||||
"_site/**/*"
|
||||
],
|
||||
"description": "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph)."
|
||||
"description": "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore the all the content of a given folder, use `<folderName>/**/*`"
|
||||
},
|
||||
"foam.edit.linkReferenceDefinitions": {
|
||||
"type": "string",
|
||||
@@ -125,6 +135,11 @@
|
||||
"Navigates to the note, creating it following your daily note settings if it does not exist"
|
||||
],
|
||||
"description": "Whether or not to navigate to the target daily note when a daily note snippet is selected."
|
||||
},
|
||||
"foam.graph.titleMaxLength": {
|
||||
"type": "number",
|
||||
"default": 24,
|
||||
"description": "The maximum title length before being abbreviated. Set to 0 or less to disable."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -146,7 +161,8 @@
|
||||
"npm-install": "rimraf node_modules && npm i",
|
||||
"npm-cleanup": "rimraf package-lock.json node_modules && yarn",
|
||||
"package-extension": "npx vsce package && yarn npm-cleanup",
|
||||
"publish-extension": "npx vsce publish && yarn npm-cleanup"
|
||||
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
|
||||
"publish-extension": "npx vsce publish --packagePath foam-vscode-$npm_package_version.vsix && yarn npm-cleanup"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.0",
|
||||
@@ -168,6 +184,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dateformat": "^3.0.3",
|
||||
"foam-core": "^0.5.0-alpha.2"
|
||||
"foam-core": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { dirname, join } from "path";
|
||||
import dateFormat from "dateformat";
|
||||
import * as fs from "fs";
|
||||
import { docConfig, pathExists } from "./utils";
|
||||
import { docConfig, focusNote, pathExists } from "./utils";
|
||||
|
||||
async function openDailyNoteFor(date?: Date) {
|
||||
const foamConfiguration = workspace.getConfiguration("foam");
|
||||
@@ -79,18 +79,6 @@ async function createDailyNoteDirectoryIfNotExists(dailyNotePath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function focusNote(notePath: string, isNewNote: boolean) {
|
||||
const document = await workspace.openTextDocument(Uri.file(notePath));
|
||||
const editor = await window.showTextDocument(document);
|
||||
|
||||
// Move the cursor to end of the file
|
||||
if (isNewNote) {
|
||||
const { lineCount } = editor.document;
|
||||
const { range } = editor.document.lineAt(lineCount - 1);
|
||||
editor.selection = new Selection(range.end, range.end);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
openDailyNoteFor,
|
||||
getDailyNoteFileName,
|
||||
|
||||
@@ -46,6 +46,12 @@ function isLocalMarkdownFile(uri: Uri) {
|
||||
return uri.scheme === "file" && uri.path.match(/\.(md|mdx|markdown)/i);
|
||||
}
|
||||
|
||||
async function registerFiles(foam: Foam, localUri: Iterable<Uri>) {
|
||||
for (const uri of localUri) {
|
||||
registerFile(foam, uri);
|
||||
}
|
||||
}
|
||||
|
||||
async function registerFile(foam: Foam, localUri: Uri) {
|
||||
// read file from disk (async)
|
||||
const path = localUri.fsPath;
|
||||
@@ -62,27 +68,36 @@ async function registerFile(foam: Foam, localUri: Uri) {
|
||||
return note;
|
||||
}
|
||||
|
||||
async function filterIgnored(files: Uri[]) {
|
||||
const excludedPaths = getIgnoredFilesSetting();
|
||||
/**
|
||||
* Filter the files and register them in the Foam object.
|
||||
* Filtering is done according to:
|
||||
* 1. Extension (currently `.md`, `.mdx`, `.markdown`)
|
||||
* 2. Excluded globs set by the user in `foam.files.ignore`
|
||||
* @param foam the Foam object.
|
||||
* @param files the list of files to be filtered and registered.
|
||||
*/
|
||||
async function filterAndRegister(foam: Foam, files: Uri[]) {
|
||||
const excludedPaths: string[] = getIgnoredFilesSetting();
|
||||
const includedFiles: Map<String, Uri> = new Map();
|
||||
for (const included of files) {
|
||||
includedFiles.set(included.fsPath, included);
|
||||
if (isLocalMarkdownFile(included)) {
|
||||
includedFiles.set(included.fsPath, included);
|
||||
}
|
||||
}
|
||||
for (const excluded of excludedPaths) {
|
||||
for (const file of await workspace.findFiles(excluded)) {
|
||||
includedFiles.delete(file.fsPath);
|
||||
}
|
||||
}
|
||||
return [...includedFiles.values()];
|
||||
registerFiles(foam, includedFiles.values());
|
||||
}
|
||||
|
||||
const bootstrap = async () => {
|
||||
const files = await workspace.findFiles("**/*").then(filterIgnored);
|
||||
const config: FoamConfig = getConfig();
|
||||
const foam = await foamBootstrap(config);
|
||||
const addFile = (uri: Uri) => registerFile(foam, uri);
|
||||
|
||||
await Promise.all(files.filter(isLocalMarkdownFile).map(addFile));
|
||||
const foam: Foam = await foamBootstrap(config);
|
||||
await workspace
|
||||
.findFiles("**/*")
|
||||
.then(files => filterAndRegister(foam, files));
|
||||
|
||||
workspaceWatcher = workspace.createFileSystemWatcher(
|
||||
"**/*",
|
||||
@@ -93,7 +108,7 @@ const bootstrap = async () => {
|
||||
|
||||
workspaceWatcher.onDidCreate(uri => {
|
||||
if (isLocalMarkdownFile(uri)) {
|
||||
addFile(uri).then(() => {
|
||||
registerFile(foam, uri).then(() => {
|
||||
console.log(`Added ${uri} to workspace`);
|
||||
});
|
||||
}
|
||||
|
||||
67
packages/foam-vscode/src/features/create-from-template.ts
Normal file
67
packages/foam-vscode/src/features/create-from-template.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
window,
|
||||
commands,
|
||||
ExtensionContext,
|
||||
workspace,
|
||||
Uri,
|
||||
SnippetString
|
||||
} from "vscode";
|
||||
import * as path from "path";
|
||||
import { FoamFeature } from "../types";
|
||||
import { TextEncoder } from "util";
|
||||
import { focusNote } from "../utils";
|
||||
|
||||
const templatesDir = `${workspace.workspaceFolders[0].uri.fsPath}/.foam/templates`;
|
||||
|
||||
async function getTemplates(): Promise<string[]> {
|
||||
const templates = await workspace.findFiles(".foam/templates/**.md");
|
||||
// parse title, not whole file!
|
||||
return templates.map(template => path.basename(template.fsPath));
|
||||
}
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
"foam-vscode.create-note-from-template",
|
||||
async () => {
|
||||
const templates = await getTemplates();
|
||||
const activeFile = window.activeTextEditor?.document?.fileName;
|
||||
const currentDir =
|
||||
activeFile !== undefined
|
||||
? path.dirname(activeFile)
|
||||
: workspace.workspaceFolders[0].uri.fsPath;
|
||||
const selectedTemplate = await window.showQuickPick(templates);
|
||||
const folder = await window.showInputBox({
|
||||
prompt: `Where should the template be created?`,
|
||||
value: currentDir
|
||||
});
|
||||
|
||||
let filename = await window.showInputBox({
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: ``,
|
||||
validateInput: value =>
|
||||
value.length ? undefined : "Please enter a value!"
|
||||
});
|
||||
filename = path.extname(filename).length
|
||||
? filename
|
||||
: `${filename}.md`;
|
||||
const targetFile = path.join(folder, filename);
|
||||
|
||||
const templateText = await workspace.fs.readFile(
|
||||
Uri.file(`${templatesDir}/${selectedTemplate}`)
|
||||
);
|
||||
const snippet = new SnippetString(templateText.toString());
|
||||
await workspace.fs.writeFile(
|
||||
Uri.file(targetFile),
|
||||
new TextEncoder().encode("")
|
||||
);
|
||||
await focusNote(targetFile, true);
|
||||
await window.activeTextEditor.insertSnippet(snippet);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default feature;
|
||||
@@ -3,6 +3,8 @@ import * as path from "path";
|
||||
import { FoamFeature } from "../types";
|
||||
import { Foam } from "foam-core";
|
||||
import { TextDecoder } from "util";
|
||||
import { getTitleMaxLength } from "../settings";
|
||||
import { isSome } from "../utils";
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
@@ -19,6 +21,17 @@ const feature: FoamFeature = {
|
||||
foam.notes.unstable_removeEventListener(onNoteAdded);
|
||||
});
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(e => {
|
||||
if (e.document.uri.scheme === "file") {
|
||||
const note = foam.notes.getNoteByURI(e.document.uri.fsPath);
|
||||
if (isSome(note)) {
|
||||
panel.webview.postMessage({
|
||||
type: "selected",
|
||||
payload: note.id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
updateGraph(panel, foam);
|
||||
});
|
||||
}
|
||||
@@ -44,7 +57,7 @@ function generateGraphData(foam: Foam) {
|
||||
id: n.id,
|
||||
type: "note",
|
||||
uri: n.source.uri,
|
||||
title: n.title,
|
||||
title: cutTitle(n.title),
|
||||
nOutLinks: links.length,
|
||||
nInLinks: graph.nodes[n.id]?.nInLinks ?? 0
|
||||
};
|
||||
@@ -71,6 +84,14 @@ function generateGraphData(foam: Foam) {
|
||||
};
|
||||
}
|
||||
|
||||
function cutTitle(title: string): string {
|
||||
const maxLen = getTitleMaxLength();
|
||||
if (maxLen > 0 && title.length > maxLen) {
|
||||
return title.substring(0, maxLen).concat("...");
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
async function createGraphPanel(foam: Foam, context: vscode.ExtensionContext) {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"foam-graph",
|
||||
|
||||
@@ -5,6 +5,7 @@ import dataviz from "./dataviz";
|
||||
import copyWithoutBrackets from "./copy-without-brackets";
|
||||
import openDatedNote from "./open-dated-note";
|
||||
import tagsExplorer from "./tags-tree-view";
|
||||
import createFromTemplate from "./create-from-template";
|
||||
import { FoamFeature } from "../types";
|
||||
|
||||
export const features: FoamFeature[] = [
|
||||
@@ -14,5 +15,6 @@ export const features: FoamFeature[] = [
|
||||
janitor,
|
||||
dataviz,
|
||||
copyWithoutBrackets,
|
||||
openDatedNote
|
||||
openDatedNote,
|
||||
createFromTemplate
|
||||
];
|
||||
|
||||
@@ -15,6 +15,12 @@ export function getWikilinkDefinitionSetting(): LinkReferenceDefinitionsSetting
|
||||
);
|
||||
}
|
||||
|
||||
/** Retrieve the list of file ignoring globs. */
|
||||
export function getIgnoredFilesSetting(): string[] {
|
||||
return workspace.getConfiguration("foam.files").get("ignore")
|
||||
}
|
||||
return workspace.getConfiguration("foam.files").get("ignore");
|
||||
}
|
||||
|
||||
/** Retrieves the maximum length for a Graph node title. */
|
||||
export function getTitleMaxLength(): number {
|
||||
return workspace.getConfiguration("foam.graph").get("titleMaxLength");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
TextDocument,
|
||||
window,
|
||||
Position,
|
||||
TextEditor
|
||||
TextEditor,
|
||||
workspace,
|
||||
Uri,
|
||||
Selection
|
||||
} from "vscode";
|
||||
import * as fs from "fs";
|
||||
|
||||
@@ -162,6 +165,20 @@ export function isSome<T>(value: T | null | undefined | void): value is T {
|
||||
*
|
||||
* @param value The object to verify
|
||||
*/
|
||||
export function isNone<T>(value: T | null | undefined | void): value is null | undefined | void {
|
||||
export function isNone<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is null | undefined | void {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
export async function focusNote(notePath: string, moveCursorToEnd: boolean) {
|
||||
const document = await workspace.openTextDocument(Uri.file(notePath));
|
||||
const editor = await window.showTextDocument(document);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,18 @@ try {
|
||||
const data = message.payload;
|
||||
createWebGLGraph(data, vscode);
|
||||
break;
|
||||
case "selected":
|
||||
const noteId = message.payload;
|
||||
const node = myGraph.graphData().nodes.find(node => node.id === noteId);
|
||||
if (node) {
|
||||
myGraph.centerAt(node.x, node.y, 300).zoom(3, 300);
|
||||
model = updateModel(node, null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
console.log("VSCode not detected");
|
||||
console.log("VsCode not detected");
|
||||
}
|
||||
|
||||
const CONTAINER_ID = "graph";
|
||||
@@ -29,30 +37,31 @@ const style = {
|
||||
},
|
||||
link: {
|
||||
highlighted: "#f9c74f",
|
||||
regular: "#277da1"
|
||||
regular: "#055171"
|
||||
}
|
||||
};
|
||||
|
||||
const sizeScale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, 30])
|
||||
.range([2, 6])
|
||||
.range([1, 3])
|
||||
.clamp(true);
|
||||
|
||||
const labelAlpha = d3
|
||||
.scaleLinear()
|
||||
.domain([1.7, 4])
|
||||
.domain([1.2, 2])
|
||||
.range([0, 1])
|
||||
.clamp(true);
|
||||
|
||||
const globalFontSize = 12;
|
||||
|
||||
const myGraph = ForceGraph();
|
||||
let model = updateModel(null, null);
|
||||
|
||||
function createWebGLGraph(data, channel) {
|
||||
data = convertData(data);
|
||||
let model = updateModel(null, null);
|
||||
|
||||
const elem = document.getElementById(CONTAINER_ID);
|
||||
const myGraph = ForceGraph();
|
||||
myGraph(elem)
|
||||
.graphData(data)
|
||||
.backgroundColor(style.backgroundColor)
|
||||
@@ -60,6 +69,7 @@ function createWebGLGraph(data, channel) {
|
||||
.d3Force("x", d3.forceX())
|
||||
.d3Force("y", d3.forceY())
|
||||
.d3Force("collide", d3.forceCollide(myGraph.nodeRelSize()))
|
||||
.linkWidth(0.5)
|
||||
.linkDirectionalParticles(1)
|
||||
.linkDirectionalParticleWidth(link =>
|
||||
getLinkState(link, model) === "highlighted" ? 1 : 0
|
||||
@@ -85,7 +95,10 @@ function createWebGLGraph(data, channel) {
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "top";
|
||||
let textColor = d3.rgb(fill);
|
||||
textColor.opacity = labelAlpha(globalScale);
|
||||
textColor.opacity =
|
||||
getNodeState(node, model) === "highlighted"
|
||||
? 1
|
||||
: labelAlpha(globalScale);
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.fillText(node.name, node.x, node.y + size + 1);
|
||||
})
|
||||
|
||||
18
readme.md
18
readme.md
@@ -4,7 +4,7 @@
|
||||
# Foam
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
**Foam** is a personal knowledge management and sharing system inspired by [Roam Research](https://roamresearch.com/), built on [Visual Studio Code](https://code.visualstudio.com/) and [GitHub](https://github.com/).
|
||||
@@ -85,7 +85,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center"><a href="https://github.com/juanfrank77"><img src="https://avatars1.githubusercontent.com/u/12146882?v=4" width="60px;" alt=""/><br /><sub><b>Juan F Gonzalez </b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=juanfrank77" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://sanketdg.github.io"><img src="https://avatars3.githubusercontent.com/u/8980971?v=4" width="60px;" alt=""/><br /><sub><b>Sanket Dasgupta</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Documentation">📖</a> <a href="https://github.com/foambubble/foam/commits?author=SanketDG" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nstafie"><img src="https://avatars1.githubusercontent.com/u/10801854?v=4" width="60px;" alt=""/><br /><sub><b>Nicholas Stafie</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=nstafie" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/francishamel"><img src="https://avatars3.githubusercontent.com/u/36383308?v=4" width="60px;" alt=""/><br /><sub><b>Francis Hamel</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=francishamel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://digiguru.co.uk"><img src="https://avatars1.githubusercontent.com/u/619436?v=4" width="60px;" alt=""/><br /><sub><b>digiguru</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=digiguru" title="Documentation">📖</a></td>
|
||||
@@ -113,6 +113,20 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://spencerwoo.com"><img src="https://avatars2.githubusercontent.com/u/32114380?v=4" width="60px;" alt=""/><br /><sub><b>Spencer Woo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=spencerwooo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://ingalless.com"><img src="https://avatars3.githubusercontent.com/u/22981941?v=4" width="60px;" alt=""/><br /><sub><b>ingalless</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Code">💻</a> <a href="https://github.com/foambubble/foam/commits?author=ingalless" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://jmg-duarte.github.io"><img src="https://avatars2.githubusercontent.com/u/15343819?v=4" width="60px;" alt=""/><br /><sub><b>José Duarte</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jmg-duarte" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.yenly.wtf"><img src="https://avatars1.githubusercontent.com/u/6759658?v=4" width="60px;" alt=""/><br /><sub><b>Yenly</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=yenly" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.hikerpig.cn"><img src="https://avatars1.githubusercontent.com/u/2259688?v=4" width="60px;" alt=""/><br /><sub><b>hikerpig</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=hikerpig" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://sigfried.org"><img src="https://avatars1.githubusercontent.com/u/1586931?v=4" width="60px;" alt=""/><br /><sub><b>Sigfried Gold</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Sigfried" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://www.tristansokol.com"><img src="https://avatars3.githubusercontent.com/u/867661?v=4" width="60px;" alt=""/><br /><sub><b>Tristan Sokol</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=tristansokol" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://umbrellait.com"><img src="https://avatars0.githubusercontent.com/u/49779373?v=4" width="60px;" alt=""/><br /><sub><b>Danil Rodin</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=umbrellait-danil-rodin" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/scottjoewilliams/"><img src="https://avatars1.githubusercontent.com/u/2026866?v=4" width="60px;" alt=""/><br /><sub><b>Scott Williams</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=scott-joe" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://jackiexiao.github.io/blog"><img src="https://avatars2.githubusercontent.com/u/18050469?v=4" width="60px;" alt=""/><br /><sub><b>jackiexiao</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Jackiexiao" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://generativist.substack.com/"><img src="https://avatars3.githubusercontent.com/u/78835?v=4" width="60px;" alt=""/><br /><sub><b>John B Nelson</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=jbn" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/asifm"><img src="https://avatars2.githubusercontent.com/u/3958387?v=4" width="60px;" alt=""/><br /><sub><b>Asif Mehedi</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=asifm" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/litanlitudan"><img src="https://avatars2.githubusercontent.com/u/4970420?v=4" width="60px;" alt=""/><br /><sub><b>Tan Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=litanlitudan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user