mirror of
https://github.com/danbulant/lezer-markdown-obsidian
synced 2026-05-19 04:18:46 +00:00
initialize obsidian-markdown repository
Code has been moved from a branch of erykwalder/quoth.
This commit is contained in:
commit
8a6998c413
8 changed files with 7767 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
165
__tests__/extensions.ts
Normal file
165
__tests__/extensions.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { compareTree } from "@lezer/markdown/test/compare-tree";
|
||||
import { SpecParser } from "@lezer/markdown/test/spec";
|
||||
import { parser } from "../src";
|
||||
|
||||
/*
|
||||
Copyright (C) 2020 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||
https://github.com/lezer-parser/markdown/blob/f49eb8c8c82cfe45aa213ca1fe2cebc95305b88b/LICENSE
|
||||
*/
|
||||
const specParser = new SpecParser(parser, {
|
||||
__proto__: null as any,
|
||||
T: "Task",
|
||||
t: "TaskMarker",
|
||||
EM: "Embed",
|
||||
eM: "EmbedMark",
|
||||
IL: "InternalLink",
|
||||
iM: "InternalMark",
|
||||
iP: "InternalPath",
|
||||
iS: "InternalSubpath",
|
||||
iD: "InternalDisplay",
|
||||
FN: "Footnote",
|
||||
fM: "FootnoteMark",
|
||||
fL: "FootnoteLabel",
|
||||
FR: "FootnoteReference",
|
||||
});
|
||||
|
||||
function test(name: string, spec: string, p = parser, only = false) {
|
||||
let f = it;
|
||||
if (only) {
|
||||
f = it.only;
|
||||
}
|
||||
f(name, () => {
|
||||
const { tree, doc } = specParser.parse(spec, name);
|
||||
compareTree(p.parse(doc), tree);
|
||||
});
|
||||
}
|
||||
|
||||
describe("Obsidian Extension", () => {
|
||||
test(
|
||||
"Task list (in unordered list)",
|
||||
`
|
||||
{BL:{LI:{l:-} {T:{t:[ ]} foo}}
|
||||
{LI:{l:-} {T:{t:[x]} bar}}}`
|
||||
);
|
||||
|
||||
test(
|
||||
"Task list (in nested list)",
|
||||
`
|
||||
{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(
|
||||
"Task list (in ordered list)",
|
||||
`
|
||||
{OL:{LI:{l:1.} {T:{t:[X]} Okay}}}`
|
||||
);
|
||||
|
||||
test(
|
||||
"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}}}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Internal Link (bare link)",
|
||||
`
|
||||
{P:before {IL:{iM:[[}{iP:Some File}{iM:]]}} after}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Internal Link (heading link)",
|
||||
`
|
||||
{P:{IL:{iM:[[}{iP:Some File}{iS:#heading}{iM:]]}}}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Internal Link (block heading link)",
|
||||
`
|
||||
{P:{IL:{iM:[[}{iP:Some File}{iS:#^blockid}{iM:]]}}}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Internal Link (display text)",
|
||||
`
|
||||
{P:{IL:{iM:[[}{iP:Some File}{iM:|}{iD:something else}{iM:]]}}}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Internal Link (heading and display text)",
|
||||
`
|
||||
{P:{IL:{iM:[[}{iP:Some File}{iS:#heading}{iM:|}{iD:something else}{iM:]]}}}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Internal Embed (file)",
|
||||
`
|
||||
{P:{EM:{eM:!}{IL:{iM:[[}{iP:moon.jpg}{iM:]]}}}}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Internal Embed (file)",
|
||||
`
|
||||
{P:{EM:{eM:!}{IL:{iM:[[}{iP:markdown file}{iS:#a heading}{iM:]]}}}}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Footnotes",
|
||||
`
|
||||
{P:Some info{FN:{fM:[^}{fL:1}{fM:]}}}
|
||||
|
||||
{P:Some more info{FN:{fM:[^}{fL:a$wacky^foot-note}{fM:]}}}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Footnote Reference (Simple)",
|
||||
`
|
||||
{FR:{fM:[^}{fL:1}{fM:]:} Some basic info}
|
||||
{FR:{fM:[^}{fL:2}{fM:]:} Some {St:{e:**}bold{e:**}} info}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Footnote Reference (Multiline)",
|
||||
`
|
||||
{FR:{fM:[^}{fL:1}{fM:]:} Line 1
|
||||
Line 2}
|
||||
{FR:{fM:[^}{fL:2}{fM:]:} Line 3
|
||||
Line 4
|
||||
Line 5}
|
||||
`
|
||||
);
|
||||
|
||||
test(
|
||||
"Footnote Reference (Interspersed with Bullets)",
|
||||
`
|
||||
{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}}}
|
||||
`
|
||||
);
|
||||
});
|
||||
6
jest.config.js
Normal file
6
jest.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// eslint-disable-next-line no-undef
|
||||
export default {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
transformIgnorePatterns: ["/node_modules/(?!(@lezer/markdown/test)/)"],
|
||||
};
|
||||
7261
package-lock.json
generated
Normal file
7261
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
21
package.json
Normal file
21
package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "obsidian-markdown",
|
||||
"description": "Obsidian Markdown extensions for @lezer/markdown",
|
||||
"version": "0.0.1",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^0.15.11",
|
||||
"@lezer/markdown": "^0.15.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.4.1",
|
||||
"jest": "^27.5.1",
|
||||
"ts-jest": "^27.1.3",
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest --maxWorkers=1 --watchAll"
|
||||
}
|
||||
}
|
||||
290
src/extensions.ts
Normal file
290
src/extensions.ts
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
import {
|
||||
BlockContext,
|
||||
Element,
|
||||
InlineContext,
|
||||
LeafBlock,
|
||||
LeafBlockParser,
|
||||
Line,
|
||||
MarkdownConfig,
|
||||
parser as defParser,
|
||||
Strikethrough,
|
||||
Table,
|
||||
} from "@lezer/markdown";
|
||||
|
||||
/*
|
||||
Copyright (C) 2020 by Marijn Haverbeke <marijnh@gmail.com> 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 */
|
||||
|
||||
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) {}
|
||||
|
||||
nextLine(cx: BlockContext, line: Line, leaf: LeafBlock) {
|
||||
if (isFootnoteRef(line.text) != -1) {
|
||||
return this.complete(cx, leaf);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
finish(cx: BlockContext, leaf: LeafBlock) {
|
||||
return this.complete(cx, leaf);
|
||||
}
|
||||
|
||||
complete(cx: BlockContext, leaf: LeafBlock) {
|
||||
cx.addLeafElement(
|
||||
leaf,
|
||||
cx.elt(
|
||||
"FootnoteReference",
|
||||
leaf.start,
|
||||
leaf.start + leaf.content.length,
|
||||
[
|
||||
cx.elt("FootnoteMark", leaf.start, leaf.start + 2),
|
||||
cx.elt("FootnoteLabel", leaf.start + 2, this.labelEnd - 2),
|
||||
cx.elt("FootnoteMark", this.labelEnd - 2, this.labelEnd),
|
||||
...cx.parser.parseInline(
|
||||
leaf.content.slice(this.labelEnd - leaf.start),
|
||||
this.labelEnd
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const Footnote: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
"Footnote",
|
||||
"FootnoteLabel",
|
||||
"FootnoteMark",
|
||||
"FootnoteReference",
|
||||
],
|
||||
parseInline: [
|
||||
{
|
||||
name: "Footnote",
|
||||
parse(cx: InlineContext, _: number, pos: number) {
|
||||
// typically [^1], but inside can match any characters but
|
||||
// square brackets and spaces.
|
||||
const match = /^\[\^[^\s[\]]+\]/.exec(cx.text.slice(pos - cx.offset));
|
||||
if (match) {
|
||||
const end = pos + match[0].length;
|
||||
return cx.addElement(
|
||||
cx.elt("Footnote", pos, end, [
|
||||
cx.elt("FootnoteMark", pos, pos + 2),
|
||||
cx.elt("FootnoteLabel", pos + 2, end - 1),
|
||||
cx.elt("FootnoteMark", end - 1, end),
|
||||
])
|
||||
);
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
before: "Link",
|
||||
},
|
||||
],
|
||||
parseBlock: [
|
||||
{
|
||||
name: "FootnoteReference",
|
||||
leaf(cx: BlockContext, leaf: LeafBlock): LeafBlockParser | null {
|
||||
const ref = isFootnoteRef(leaf.content);
|
||||
if (ref != -1) {
|
||||
return new FootnoteReferenceParser(leaf.start + ref);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
before: "LinkReference",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const ObsidianMDExtensions = [
|
||||
Footnote,
|
||||
InternalLink,
|
||||
Strikethrough,
|
||||
Table,
|
||||
TaskList,
|
||||
];
|
||||
|
||||
export const parser = defParser.configure(ObsidianMDExtensions);
|
||||
7
src/index.ts
Normal file
7
src/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export {
|
||||
Footnote,
|
||||
InternalLink,
|
||||
ObsidianMDExtensions,
|
||||
parser,
|
||||
TaskList,
|
||||
} from "./extensions";
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES6",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"lib": ["dom", "es5", "scripthost", "es2015"],
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
Loading…
Reference in a new issue