From 1b3e3db0226668d8641a848520e9b115e8953899 Mon Sep 17 00:00:00 2001 From: Eric Rykwalder Date: Sat, 5 Mar 2022 21:27:43 -0500 Subject: [PATCH] alphabetize, add export to index --- __tests__/extensions.ts | 130 ++++++------ src/extensions.ts | 436 ++++++++++++++++++++-------------------- src/index.ts | 2 + 3 files changed, 290 insertions(+), 278 deletions(-) diff --git a/__tests__/extensions.ts b/__tests__/extensions.ts index d636f63..38e0e29 100644 --- a/__tests__/extensions.ts +++ b/__tests__/extensions.ts @@ -10,6 +10,7 @@ const specParser = new SpecParser(parser, { __proto__: null as any, T: "Task", t: "TaskMarker", + /* End Copyright */ EM: "Embed", eM: "EmbedMark", H: "Hashtag", @@ -29,6 +30,10 @@ const specParser = new SpecParser(parser, { yc: "YAMLContent", }); +/* + Copyright (C) 2020 by Marijn Haverbeke and others + https://github.com/lezer-parser/markdown/blob/f49eb8c8c82cfe45aa213ca1fe2cebc95305b88b/LICENSE +*/ function test(name: string, spec: string, p = parser, only = false) { let f = it; if (only) { @@ -39,45 +44,57 @@ function test(name: string, spec: string, p = parser, only = false) { compareTree(p.parse(doc), tree); }); } +/* End Copyright */ describe("Obsidian Extension", () => { test( - "Task list (in unordered list)", + "Footnotes", ` -{BL:{LI:{l:-} {T:{t:[ ]} foo}} -{LI:{l:-} {T:{t:[x]} bar}}}` +{P:Some info{FN:{fM:[^}{fL:1}{fM:]}}} + +{P:Some more info{FN:{fM:[^}{fL:a$wacky^foot-note}{fM:]}}} + ` ); test( - "Task list (in nested list)", + "Footnote Reference (Simple)", ` -{BL:{LI:{l:-} {T:{t:[x]} foo} - {BL:{LI:{l:-} {T:{t:[ ]} bar}} - {LI:{l:-} {T:{t:[x]} baz}}}} -{LI:{l:-} {T:{t:[ ]} bim}}}` +{FR:{fM:[^}{fL:1}{fM:]:} Some basic info} +{FR:{fM:[^}{fL:2}{fM:]:} Some {St:{e:**}bold{e:**}} info} + ` ); test( - "Task list (in ordered list)", + "Footnote Reference (Multiline)", ` -{OL:{LI:{l:1.} {T:{t:[X]} Okay}}}` +{FR:{fM:[^}{fL:1}{fM:]:} Line 1 +Line 2} +{FR:{fM:[^}{fL:2}{fM:]:} Line 3 +Line 4 +Line 5} + ` ); test( - "Task list (versus setext header)", + "Footnote Reference (Interspersed with Bullets)", ` -{OL:{LI:{l:1.} {SH1:{Ln:{L:[}X{L:]}} foo - {h:===}}}}` +{FR:{fM:[^}{fL:1}{fM:]:} Line 1} +{BL:{LI:{l:-} {P:Line 2 +{FN:{fM:[^}{fL:2}{fM:]}}: Line 3}}} + +{FR:{fM:[^}{fL:2}{fM:]:} Line 5} +{BL:{LI:{l:-} {P:Line 6}}} + ` ); - /* End Copyright */ + test( - "Task list (different markers)", - ` -{BL:{LI:{l:-} {T:{t:[a]} foo}} -{LI:{l:-} {T:{t:[[]} bar}} -{LI:{l:-} {T:{t:[]]} baz}} -{LI:{l:-} {T:{t:[\\]} bim}}} + "Hashtag", ` +{P:Some text. {H:{hm:#}{hl:tag}} {H:{hm:#}{hl:other-tag9}}^not part +{H:{hm:#}{hl:ñáø√}}} + +{P:Test number #1234} + ` ); test( @@ -129,48 +146,51 @@ describe("Obsidian Extension", () => { ` ); + /* + Copyright (C) 2020 by Marijn Haverbeke and others + https://github.com/lezer-parser/markdown/blob/f49eb8c8c82cfe45aa213ca1fe2cebc95305b88b/LICENSE +*/ test( - "Footnotes", + "Task list (in unordered list)", ` -{P:Some info{FN:{fM:[^}{fL:1}{fM:]}}} - -{P:Some more info{FN:{fM:[^}{fL:a$wacky^foot-note}{fM:]}}} - ` +{BL:{LI:{l:-} {T:{t:[ ]} foo}} +{LI:{l:-} {T:{t:[x]} bar}}}` ); test( - "Footnote Reference (Simple)", + "Task list (in nested list)", ` -{FR:{fM:[^}{fL:1}{fM:]:} Some basic info} -{FR:{fM:[^}{fL:2}{fM:]:} Some {St:{e:**}bold{e:**}} info} - ` +{BL:{LI:{l:-} {T:{t:[x]} foo} + {BL:{LI:{l:-} {T:{t:[ ]} bar}} + {LI:{l:-} {T:{t:[x]} baz}}}} +{LI:{l:-} {T:{t:[ ]} bim}}}` ); test( - "Footnote Reference (Multiline)", + "Task list (in ordered list)", ` -{FR:{fM:[^}{fL:1}{fM:]:} Line 1 -Line 2} -{FR:{fM:[^}{fL:2}{fM:]:} Line 3 -Line 4 -Line 5} - ` +{OL:{LI:{l:1.} {T:{t:[X]} Okay}}}` ); test( - "Footnote Reference (Interspersed with Bullets)", + "Task list (versus setext header)", + ` +{OL:{LI:{l:1.} {SH1:{Ln:{L:[}X{L:]}} foo + {h:===}}}}` + ); + /* End Copyright */ + test( + "Task list (different markers)", + ` +{BL:{LI:{l:-} {T:{t:[a]} foo}} +{LI:{l:-} {T:{t:[[]} bar}} +{LI:{l:-} {T:{t:[]]} baz}} +{LI:{l:-} {T:{t:[\\]} bim}}} ` -{FR:{fM:[^}{fL:1}{fM:]:} Line 1} -{BL:{LI:{l:-} {P:Line 2 -{FN:{fM:[^}{fL:2}{fM:]}}: Line 3}}} - -{FR:{fM:[^}{fL:2}{fM:]:} Line 5} -{BL:{LI:{l:-} {P:Line 6}}} - ` ); test( - "Frontmatter", + "YAMLFrontMatter", ` {YF:{ym:---} {yc:tags: blah} @@ -186,7 +206,7 @@ Line 5} ); test( - "Frontmatter (trailing text)", + "YAMLFrontMatter (trailing text)", ` {YF:{ym:---} {yc:tags: blah} @@ -202,7 +222,7 @@ Line 5} ); test( - "Not Frontmatter (no close)", + "Not YAMLFrontMatter (no close)", ` {HR:---} @@ -214,7 +234,7 @@ Line 5} ); test( - "Not Frontmatter (close indented)", + "Not YAMLFrontMatter (close indented)", ` {HR:---} @@ -225,7 +245,7 @@ Line 5} ); test( - "Not Frontmatter (space after open)", + "Not YAMLFrontMatter (space after open)", ` {HR:--- } @@ -236,7 +256,7 @@ Line 5} ); test( - "Not Frontmatter (data before open)", + "Not YAMLFrontMatter (data before open)", ` {P:some text} @@ -247,14 +267,4 @@ Line 5} {HR:---} ` ); - - test( - "Hashtag", - ` -{P:Some text. {H:{hm:#}{hl:tag}} {H:{hm:#}{hl:other-tag9}}^not part -{H:{hm:#}{hl:ñáø√}}} - -{P:Test number #1234} - ` - ); }); diff --git a/src/extensions.ts b/src/extensions.ts index b300b41..0309d32 100644 --- a/src/extensions.ts +++ b/src/extensions.ts @@ -19,224 +19,6 @@ declare module "@lezer/markdown" { } } -/* - Copyright (C) 2020 by Marijn Haverbeke and others - https://github.com/lezer-parser/markdown/blob/f49eb8c8c82cfe45aa213ca1fe2cebc95305b88b/LICENSE -*/ -class TaskParser implements LeafBlockParser { - nextLine() { - return false; - } - - finish(cx: BlockContext, leaf: LeafBlock) { - cx.addLeafElement( - leaf, - cx.elt("Task", leaf.start, leaf.start + leaf.content.length, [ - cx.elt("TaskMarker", leaf.start, leaf.start + 3), - ...cx.parser.parseInline(leaf.content.slice(3), leaf.start + 3), - ]) - ); - return true; - } -} - -/// Extension providing -/// [GFM-style](https://github.github.com/gfm/#task-list-items-extension-) -/// task list items, where list items can be prefixed with `[ ]` or -/// `[x]` to add a checkbox. -/// `x` can be any character -export const TaskList: MarkdownConfig = { - defineNodes: [{ name: "Task", block: true }, "TaskMarker"], - parseBlock: [ - { - name: "TaskList", - leaf(cx, leaf) { - return /^\[.\]/.test(leaf.content) && cx.parentType().name == "ListItem" - ? new TaskParser() - : null; - }, - after: "SetextHeading", - }, - ], -}; -/* End Copyright */ - -const hashtagRE = - /^[^\u2000-\u206F\u2E00-\u2E7F'!"#$%&()*+,.:;<=>?@^`{|}~\[\]\\\s]+/; - -export const Hashtag: MarkdownConfig = { - defineNodes: ["Hashtag", "HashtagMark", "HashtagLabel"], - parseInline: [ - { - name: "Hashtag", - parse(cx, next, pos) { - if (next != 35 /* # */) { - return -1; - } - const start = pos; - pos += 1; - const match = hashtagRE.exec(cx.text.slice(pos - cx.offset)); - if (match && /\D/.test(match[0])) { - pos += match[0].length; - return cx.addElement( - cx.elt("Hashtag", start, pos, [ - cx.elt("HashtagMark", start, start + 1), - cx.elt("HashtagLabel", start + 1, pos), - ]) - ); - } - return -1; - }, - }, - ], -}; - -function parseInternalLink(cx: InlineContext, pos: number): Element | null { - if ( - cx.char(pos) != 91 /* [ */ || - cx.char(pos + 1) != 91 || - !isClosedLink(cx, pos) - ) { - return null; - } - const contents: Element[] = []; - contents.push(cx.elt("InternalMark", pos, pos + 2)); - pos = cx.skipSpace(pos + 2); - const path = parsePath(cx, pos - cx.offset, cx.offset); - if (path) { - contents.push(path); - pos = cx.skipSpace(path.to); - } - const subpath = parseSubpath(cx, pos); - if (subpath) { - contents.push(subpath); - pos = cx.skipSpace(subpath.to); - } - if (path == null && subpath == null) { - return null; - } - if (cx.char(pos) == 124 /* | */) { - contents.push(cx.elt("InternalMark", pos, pos + 1)); - pos += 1; - const display = parseDisplay(cx, pos); - if (display) { - contents.push(display); - pos = cx.skipSpace(display.to); - } - } - contents.push(cx.elt("InternalMark", pos, pos + 2)); - return cx.elt( - "InternalLink", - contents[0].from, - contents[contents.length - 1].to, - contents - ); -} - -export const InternalLink: MarkdownConfig = { - defineNodes: [ - "Embed", - "EmbedMark", - "InternalLink", - "InternalMark", - "InternalPath", - "InternalSubpath", - "InternalDisplay", - ], - parseInline: [ - { - name: "InternalLink", - parse(cx: InlineContext, _: number, pos: number) { - const el = parseInternalLink(cx, pos); - if (el) { - return cx.addElement(el); - } - return -1; - }, - before: "Link", - }, - { - name: "Embed", - parse(cx: InlineContext, next: number, pos: number): number { - if (next != 33) { - return -1; - } - const link = parseInternalLink(cx, pos + 1); - if (link) { - const embedMark = cx.elt("EmbedMark", pos, pos + 1); - return cx.addElement( - cx.elt("Embed", pos, link.to, [embedMark, link]) - ); - } - return -1; - }, - before: "Image", - }, - ], -}; - -function isClosedLink(cx: InlineContext, start: number): boolean { - for (let pos = start + 2; pos < cx.end; pos++) { - if (cx.char(pos) == 91 /* [ */ && cx.char(pos + 1) == 91) { - return false; - } else if (cx.char(pos) == 93 /* ] */ && cx.char(pos + 1) == 93) { - // return false for empty - // true otherwise - return pos > start + 2; - } - } - return false; -} - -function parsePath( - cx: InlineContext, - start: number, - offset: number -): Element | null { - // anything but: |[]#^\/ - const match = /^[^[\]|#^\\/]+/.exec(cx.text.slice(start)); - if (match) { - return cx.elt( - "InternalPath", - offset + start, - offset + start + match[0].length - ); - } - return null; -} - -function parseSubpath(cx: InlineContext, start: number): Element | null { - if (cx.char(start) != 35 /* # */) { - return null; - } - for (let pos = start + 1; pos < cx.end; pos++) { - if ( - cx.char(pos) == 124 /* | */ || - (cx.char(pos) == 93 /* ] */ && cx.char(pos + 1) == 93) - ) { - return cx.elt("InternalSubpath", start, pos); - } - } - return null; -} - -function parseDisplay(cx: InlineContext, start: number): Element | null { - for (let pos = start; pos < cx.end; pos++) { - if (cx.char(pos) == 93 /* ] */ && cx.char(pos + 1) == 93) { - if (pos == start) { - return null; - } - return cx.elt("InternalDisplay", start, pos); - } - } - return null; -} - -function isFootnoteRef(content: string): number { - const match = /^\[\^[^\s[\]]+\]:/.exec(content); - return match ? match[0].length : -1; -} - class FootnoteReferenceParser implements LeafBlockParser { constructor(private labelEnd: number) {} @@ -317,6 +99,224 @@ export const Footnote: MarkdownConfig = { ], }; +function isFootnoteRef(content: string): number { + const match = /^\[\^[^\s[\]]+\]:/.exec(content); + return match ? match[0].length : -1; +} + +const hashtagRE = + /^[^\u2000-\u206F\u2E00-\u2E7F'!"#$%&()*+,.:;<=>?@^`{|}~\[\]\\\s]+/; + +export const Hashtag: MarkdownConfig = { + defineNodes: ["Hashtag", "HashtagMark", "HashtagLabel"], + parseInline: [ + { + name: "Hashtag", + parse(cx, next, pos) { + if (next != 35 /* # */) { + return -1; + } + const start = pos; + pos += 1; + const match = hashtagRE.exec(cx.text.slice(pos - cx.offset)); + if (match && /\D/.test(match[0])) { + pos += match[0].length; + return cx.addElement( + cx.elt("Hashtag", start, pos, [ + cx.elt("HashtagMark", start, start + 1), + cx.elt("HashtagLabel", start + 1, pos), + ]) + ); + } + return -1; + }, + }, + ], +}; + +export const InternalLink: MarkdownConfig = { + defineNodes: [ + "Embed", + "EmbedMark", + "InternalLink", + "InternalMark", + "InternalPath", + "InternalSubpath", + "InternalDisplay", + ], + parseInline: [ + { + name: "InternalLink", + parse(cx: InlineContext, _: number, pos: number) { + const el = parseInternalLink(cx, pos); + if (el) { + return cx.addElement(el); + } + return -1; + }, + before: "Link", + }, + { + name: "Embed", + parse(cx: InlineContext, next: number, pos: number): number { + if (next != 33) { + return -1; + } + const link = parseInternalLink(cx, pos + 1); + if (link) { + const embedMark = cx.elt("EmbedMark", pos, pos + 1); + return cx.addElement( + cx.elt("Embed", pos, link.to, [embedMark, link]) + ); + } + return -1; + }, + before: "Image", + }, + ], +}; + +function parseInternalLink(cx: InlineContext, pos: number): Element | null { + if ( + cx.char(pos) != 91 /* [ */ || + cx.char(pos + 1) != 91 || + !isClosedLink(cx, pos) + ) { + return null; + } + const contents: Element[] = []; + contents.push(cx.elt("InternalMark", pos, pos + 2)); + pos = cx.skipSpace(pos + 2); + const path = parsePath(cx, pos - cx.offset, cx.offset); + if (path) { + contents.push(path); + pos = cx.skipSpace(path.to); + } + const subpath = parseSubpath(cx, pos); + if (subpath) { + contents.push(subpath); + pos = cx.skipSpace(subpath.to); + } + if (path == null && subpath == null) { + return null; + } + if (cx.char(pos) == 124 /* | */) { + contents.push(cx.elt("InternalMark", pos, pos + 1)); + pos += 1; + const display = parseDisplay(cx, pos); + if (display) { + contents.push(display); + pos = cx.skipSpace(display.to); + } + } + contents.push(cx.elt("InternalMark", pos, pos + 2)); + return cx.elt( + "InternalLink", + contents[0].from, + contents[contents.length - 1].to, + contents + ); +} + +function isClosedLink(cx: InlineContext, start: number): boolean { + for (let pos = start + 2; pos < cx.end; pos++) { + if (cx.char(pos) == 91 /* [ */ && cx.char(pos + 1) == 91) { + return false; + } else if (cx.char(pos) == 93 /* ] */ && cx.char(pos + 1) == 93) { + // return false for empty + // true otherwise + return pos > start + 2; + } + } + return false; +} + +function parsePath( + cx: InlineContext, + start: number, + offset: number +): Element | null { + // anything but: |[]#^\/ + const match = /^[^[\]|#^\\/]+/.exec(cx.text.slice(start)); + if (match) { + return cx.elt( + "InternalPath", + offset + start, + offset + start + match[0].length + ); + } + return null; +} + +function parseSubpath(cx: InlineContext, start: number): Element | null { + if (cx.char(start) != 35 /* # */) { + return null; + } + for (let pos = start + 1; pos < cx.end; pos++) { + if ( + cx.char(pos) == 124 /* | */ || + (cx.char(pos) == 93 /* ] */ && cx.char(pos + 1) == 93) + ) { + return cx.elt("InternalSubpath", start, pos); + } + } + return null; +} + +function parseDisplay(cx: InlineContext, start: number): Element | null { + for (let pos = start; pos < cx.end; pos++) { + if (cx.char(pos) == 93 /* ] */ && cx.char(pos + 1) == 93) { + if (pos == start) { + return null; + } + return cx.elt("InternalDisplay", start, pos); + } + } + return null; +} + +/* + Copyright (C) 2020 by Marijn Haverbeke and others + https://github.com/lezer-parser/markdown/blob/f49eb8c8c82cfe45aa213ca1fe2cebc95305b88b/LICENSE +*/ +class TaskParser implements LeafBlockParser { + nextLine() { + return false; + } + + finish(cx: BlockContext, leaf: LeafBlock) { + cx.addLeafElement( + leaf, + cx.elt("Task", leaf.start, leaf.start + leaf.content.length, [ + cx.elt("TaskMarker", leaf.start, leaf.start + 3), + ...cx.parser.parseInline(leaf.content.slice(3), leaf.start + 3), + ]) + ); + return true; + } +} + +/// Extension providing +/// [GFM-style](https://github.github.com/gfm/#task-list-items-extension-) +/// task list items, where list items can be prefixed with `[ ]` or +/// `[x]` to add a checkbox. +/// `x` can be any character +export const TaskList: MarkdownConfig = { + defineNodes: [{ name: "Task", block: true }, "TaskMarker"], + parseBlock: [ + { + name: "TaskList", + leaf(cx, leaf) { + return /^\[.\]/.test(leaf.content) && cx.parentType().name == "ListItem" + ? new TaskParser() + : null; + }, + after: "SetextHeading", + }, + ], +}; +/* End Copyright */ + export const YAMLFrontMatter: MarkdownConfig = { defineNodes: ["YAMLFrontMatter", "YAMLMarker", "YAMLContent"], parseBlock: [ diff --git a/src/index.ts b/src/index.ts index 0fe2a50..1e011bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ export { Footnote, + Hashtag, InternalLink, ObsidianMDExtensions, parser, TaskList, + YAMLFrontMatter, } from "./extensions";