diff --git a/media/fontello/css/fontello.css b/media/fontello/css/fontello.css new file mode 100644 index 00000000..7a592aa6 --- /dev/null +++ b/media/fontello/css/fontello.css @@ -0,0 +1,58 @@ +@font-face { + font-family: 'fontello'; + src: url('../font/fontello.eot?7840610'); + src: url('../font/fontello.eot?7840610#iefix') format('embedded-opentype'), + url('../font/fontello.woff2?7840610') format('woff2'), + url('../font/fontello.woff?7840610') format('woff'), + url('../font/fontello.ttf?7840610') format('truetype'), + url('../font/fontello.svg?7840610#fontello') format('svg'); + font-weight: normal; + font-style: normal; +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'fontello'; + src: url('../font/fontello.svg?7840610#fontello') format('svg'); + } +} +*/ + + [class^="icon-"]:before, [class*=" icon-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: never; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +.icon-link:before { content: '\e800'; } /* '' */ \ No newline at end of file diff --git a/media/fontello/font/fontello.eot b/media/fontello/font/fontello.eot new file mode 100644 index 00000000..32f6b243 Binary files /dev/null and b/media/fontello/font/fontello.eot differ diff --git a/media/fontello/font/fontello.svg b/media/fontello/font/fontello.svg new file mode 100644 index 00000000..4fea3f30 --- /dev/null +++ b/media/fontello/font/fontello.svg @@ -0,0 +1,12 @@ + + + +Copyright (C) 2020 by original authors @ fontello.com + + + + + + + + \ No newline at end of file diff --git a/media/fontello/font/fontello.ttf b/media/fontello/font/fontello.ttf new file mode 100644 index 00000000..7832c2e9 Binary files /dev/null and b/media/fontello/font/fontello.ttf differ diff --git a/media/fontello/font/fontello.woff b/media/fontello/font/fontello.woff new file mode 100644 index 00000000..61c311d4 Binary files /dev/null and b/media/fontello/font/fontello.woff differ diff --git a/media/fontello/font/fontello.woff2 b/media/fontello/font/fontello.woff2 new file mode 100644 index 00000000..a0de684c Binary files /dev/null and b/media/fontello/font/fontello.woff2 differ diff --git a/media/markdown.css b/media/markdown.css new file mode 100644 index 00000000..6e59cb29 --- /dev/null +++ b/media/markdown.css @@ -0,0 +1,44 @@ +.memo-markdown-embed { + border: 1px solid #ddd; + border-radius: 6px; + padding: 5px 20px 15px 20px; + margin: 0 20px; + position: relative; +} + +.memo-markdown-embed-title { + height: 36px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: 26px; + line-height: 42px; + top: 5px; + left: 0; + right: 0; + width: 100%; + text-align: center; + font-weight: 900; +} + +.memo-markdown-embed-link { + position: absolute; + top: 6px; + right: 12px; + cursor: pointer; +} + +.memo-markdown-embed-link i { + font-size: 18px; + color: #535353; +} + +.memo-markdown-embed-link:hover i { + color: #000000; +} + +.memo-markdown-embed-content { + max-height: 500px; + overflow-y: auto; + padding-right: 10px; +} diff --git a/package.json b/package.json index 54dd3d82..3544c599 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,11 @@ "name": "Backlinks" } ] - } + }, + "markdown.previewStyles": [ + "./media/fontello/css/fontello.css", + "./media/markdown.css" + ] }, "lint-staged": { "*.ts": [ diff --git a/src/extensions/completionProvider.spec.ts b/src/extensions/completionProvider.spec.ts index 6ab6a306..282438c0 100644 --- a/src/extensions/completionProvider.spec.ts +++ b/src/extensions/completionProvider.spec.ts @@ -108,7 +108,7 @@ describe('provideCompletionItems()', () => { ]); }); - it('should provide links to images', async () => { + it('should provide links to images and notes on embedding', async () => { const name0 = `a-${rndName()}`; const name1 = `b-${rndName()}`; const name2 = `c-${rndName()}`; @@ -141,6 +141,10 @@ describe('provideCompletionItems()', () => { insertText: `folder1/${name2}.png`, label: `folder1/${name2}.png`, }), + expect.objectContaining({ + insertText: name0, + label: name0, + }), ]); }); }); diff --git a/src/extensions/completionProvider.ts b/src/extensions/completionProvider.ts index c534f8c0..6d431bc7 100644 --- a/src/extensions/completionProvider.ts +++ b/src/extensions/completionProvider.ts @@ -10,7 +10,13 @@ import { import path from 'path'; import groupBy from 'lodash.groupby'; -import { getWorkspaceCache, extractLongRef, extractShortRef, sortPaths } from '../utils'; +import { + getWorkspaceCache, + extractLongRef, + extractShortRef, + sortPaths, + containsImageExt, +} from '../utils'; export const provideCompletionItems = (document: TextDocument, position: Position) => { const linePrefix = document.lineAt(position).text.substr(0, position.character); @@ -24,26 +30,31 @@ export const provideCompletionItems = (document: TextDocument, position: Positio const completionItems: CompletionItem[] = []; - const uris: Uri[] = sortPaths( - [ - ...(isResourceAutocomplete ? getWorkspaceCache().imageUris : []), - ...(!isResourceAutocomplete ? getWorkspaceCache().markdownUris : []), - ], - { pathKey: 'fsPath', shallowFirst: true }, - ); + const uris: Uri[] = [ + ...(isResourceAutocomplete + ? [...getWorkspaceCache().imageUris, ...getWorkspaceCache().markdownUris] + : []), + ...(!isResourceAutocomplete + ? sortPaths(getWorkspaceCache().markdownUris, { pathKey: 'fsPath', shallowFirst: true }) + : []), + ]; const urisByPathBasename = groupBy(uris, ({ fsPath }) => path.basename(fsPath).toLowerCase()); - for (const uri of uris) { + uris.forEach((uri) => { const workspaceFolder = workspace.getWorkspaceFolder(uri); if (!workspaceFolder) { - continue; + return; } - const longRef = extractLongRef(workspaceFolder.uri.fsPath, uri.fsPath, isResourceAutocomplete); + const longRef = extractLongRef( + workspaceFolder.uri.fsPath, + uri.fsPath, + containsImageExt(uri.fsPath), + ); - const shortRef = extractShortRef(uri.fsPath, isResourceAutocomplete); + const shortRef = extractShortRef(uri.fsPath, containsImageExt(uri.fsPath)); const urisGroup = urisByPathBasename[path.basename(uri.fsPath).toLowerCase()] || []; @@ -51,7 +62,7 @@ export const provideCompletionItems = (document: TextDocument, position: Positio urisGroup.findIndex((uriParam) => uriParam.fsPath === uri.fsPath) === 0; if (!longRef || !shortRef) { - continue; + return; } const item = new CompletionItem(longRef.ref, CompletionItemKind.File); @@ -59,7 +70,7 @@ export const provideCompletionItems = (document: TextDocument, position: Positio item.insertText = isFirstUriInGroup ? shortRef.ref : longRef.ref; completionItems.push(item); - } + }); return completionItems; }; diff --git a/src/extensions/extendMarkdownIt.spec.ts b/src/extensions/extendMarkdownIt.spec.ts index 6477b957..2d634f50 100644 --- a/src/extensions/extendMarkdownIt.spec.ts +++ b/src/extensions/extendMarkdownIt.spec.ts @@ -10,6 +10,7 @@ import { closeEditorsAndCleanWorkspace, getImgUrlForMarkdownPreview, getFileUrlForMarkdownPreview, + escapeForRegExp, } from '../test/testUtils'; describe('extendMarkdownIt contribution', () => { @@ -161,4 +162,158 @@ describe('extendMarkdownIt contribution', () => { `

${name}

\n`, ); }); + + it('should render embedded note', async () => { + const name = rndName(); + + await createFile(`${name}.md`, '# Hello world'); + + const md = extendMarkdownIt(MarkdownIt()); + + const notePath = `${path.join(getWorkspaceFolder()!, name)}.md`; + + const html = md.render(`![[${name}]]`); + + expect( + html.replace(new RegExp(escapeForRegExp(notePath), 'g'), `/note.md`).replace(name, 'note'), + ).toMatchInlineSnapshot(` + "

+
note
+
+ + + +
+
+

Hello world

+ +
+

+ " + `); + }); + + it('should render double embedded note', async () => { + const name = rndName(); + const name1 = rndName(); + + await createFile(`${name}.md`, '# Hello world'); + await createFile(`${name1}.md`, `![[${name}]]`); + + const md = extendMarkdownIt(MarkdownIt()); + + const notePath = `${path.join(getWorkspaceFolder()!, name)}.md`; + const notePath1 = `${path.join(getWorkspaceFolder()!, name1)}.md`; + + const html = md.render(`![[${name1}]]`); + + expect( + html + .replace(new RegExp(escapeForRegExp(notePath), 'g'), `/note.md`) + .replace(name, 'note') + .replace(new RegExp(escapeForRegExp(notePath1), 'g'), `/note1.md`) + .replace(name1, 'note1'), + ).toMatchInlineSnapshot(` + "

+
note1
+
+ + + +
+
+

+
note
+
+ + + +
+
+

Hello world

+ +
+

+ +
+

+ " + `); + }); + + it('should render cyclic linking detected warning', async () => { + const name = rndName(); + + await createFile(`${name}.md`, `![[${name}]]`); + + const md = extendMarkdownIt(MarkdownIt()); + + const notePath = `${path.join(getWorkspaceFolder()!, name)}.md`; + + const html = md.render(`![[${name}]]`); + + expect( + html.replace(new RegExp(escapeForRegExp(notePath), 'g'), `/note.md`).replace(name, 'note'), + ).toMatchInlineSnapshot(` + "

+
note
+
+ + + +
+
+
Cyclic linking detected 💥.
+
+

+ " + `); + }); + + it('should render cyclic linking detected warning for deep link', async () => { + const name = rndName(); + const name1 = rndName(); + + await createFile(`${name}.md`, `![[${name1}]]`); + await createFile(`${name1}.md`, `![[${name}]]`); + + const md = extendMarkdownIt(MarkdownIt()); + + const notePath = `${path.join(getWorkspaceFolder()!, name)}.md`; + const notePath1 = `${path.join(getWorkspaceFolder()!, name1)}.md`; + + const html = md.render(`![[${name}]]`); + + expect( + html + .replace(new RegExp(escapeForRegExp(notePath), 'g'), `/note.md`) + .replace(name, 'note') + .replace(new RegExp(escapeForRegExp(notePath1), 'g'), `/note1.md`) + .replace(name1, 'note1'), + ).toMatchInlineSnapshot(` + "

+
note
+
+ + + +
+
+

+
note1
+
+ + + +
+
+
Cyclic linking detected 💥.
+
+

+ +
+

+ " + `); + }); }); diff --git a/src/extensions/extendMarkdownIt.ts b/src/extensions/extendMarkdownIt.ts index b9204796..022b1886 100644 --- a/src/extensions/extendMarkdownIt.ts +++ b/src/extensions/extendMarkdownIt.ts @@ -1,5 +1,7 @@ import MarkdownIt from 'markdown-it'; import markdownItRegex from 'markdown-it-regex'; +import path from 'path'; +import fs from 'fs'; import { getWorkspaceCache, @@ -7,6 +9,7 @@ import { getFileUrlForMarkdownPreview, containsImageExt, findUriByRef, + extractEmbedRefs, } from '../utils'; const getInvalidRefAnchor = (text: string) => @@ -16,7 +19,9 @@ const getRefAnchor = (href: string, text: string) => `${text}`; const extendMarkdownIt = (md: MarkdownIt) => { - return md + const refsStack: string[] = []; + + const mdExtended = md .use(markdownItRegex, { name: 'ref-resource', regex: /!\[\[([^\[\]]+?)\]\]/, @@ -33,13 +38,46 @@ const extendMarkdownIt = (md: MarkdownIt) => { } } - const markdownUri = findUriByRef(getWorkspaceCache().markdownUris, refStr)?.fsPath; + const fsPath = findUriByRef(getWorkspaceCache().markdownUris, refStr)?.fsPath; - if (!markdownUri) { + if (!fsPath) { return getInvalidRefAnchor(label || refStr); } - return getRefAnchor(getFileUrlForMarkdownPreview(markdownUri), label || refStr); + const name = path.parse(fsPath).name; + + const content = fs.readFileSync(fsPath).toString(); + + const refs = extractEmbedRefs(content).map((ref) => ref.toLowerCase()); + + const cyclicLinkDetected = + refs.includes(refStr.toLowerCase()) || refs.some((ref) => refsStack.includes(ref)); + + if (!cyclicLinkDetected) { + refsStack.push(refStr.toLowerCase()); + } + + const html = `
+
${name}
+ +
+ ${ + !cyclicLinkDetected + ? (mdExtended as any).render(content, undefined, true) + : '
Cyclic linking detected 💥.
' + } +
+
`; + + if (!cyclicLinkDetected) { + refsStack.pop(); + } + + return html; }, }) .use(markdownItRegex, { @@ -48,15 +86,17 @@ const extendMarkdownIt = (md: MarkdownIt) => { replace: (ref: string) => { const [refStr, label = ''] = ref.split('|'); - const markdownUri = findUriByRef(getWorkspaceCache().markdownUris, refStr)?.fsPath; + const fsPath = findUriByRef(getWorkspaceCache().markdownUris, refStr)?.fsPath; - if (!markdownUri) { + if (!fsPath) { return getInvalidRefAnchor(label || refStr); } - return getRefAnchor(getFileUrlForMarkdownPreview(markdownUri), label || refStr); + return getRefAnchor(getFileUrlForMarkdownPreview(fsPath), label || refStr); }, }); + + return mdExtended; }; export default extendMarkdownIt; diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index ab78b6eb..2b26937f 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -6,9 +6,19 @@ export { default as waitForExpect } from 'wait-for-expect'; import * as utils from '../utils'; import { WorkspaceCache } from '../types'; -const { getWorkspaceFolder, getImgUrlForMarkdownPreview, getFileUrlForMarkdownPreview } = utils; - -export { getWorkspaceFolder, getImgUrlForMarkdownPreview, getFileUrlForMarkdownPreview }; +const { + getWorkspaceFolder, + getImgUrlForMarkdownPreview, + getFileUrlForMarkdownPreview, + escapeForRegExp, +} = utils; + +export { + getWorkspaceFolder, + getImgUrlForMarkdownPreview, + getFileUrlForMarkdownPreview, + escapeForRegExp, +}; export const cleanWorkspace = () => { const workspaceFolder = utils.getWorkspaceFolder(); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 89682574..903730ef 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -166,6 +166,16 @@ export const getReferenceAtPosition = ( export const escapeForRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +export const extractEmbedRefs = (content: string) => { + const matches = matchAll(new RegExp(`\\[\\[(([^\\[\\]]+?)(\\|.*)?)\\]\\]`, 'gi'), content); + + return matches.map((match) => { + const [, $1] = match; + + return $1; + }); +}; + export const findReferences = async ( ref: string, excludePaths: string[] = [],