@liminis/editor 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +34 -0
- package/README.md +376 -0
- package/dist/annotations/anchor-align.d.ts +50 -0
- package/dist/annotations/anchor-align.js +74 -0
- package/dist/annotations/anchor-model.d.ts +81 -0
- package/dist/annotations/anchor-model.js +80 -0
- package/dist/annotations/anchor-resolver.d.ts +92 -0
- package/dist/annotations/anchor-resolver.js +231 -0
- package/dist/annotations/block-structure.d.ts +65 -0
- package/dist/annotations/block-structure.js +113 -0
- package/dist/annotations/types.d.ts +159 -0
- package/dist/annotations/types.js +50 -0
- package/dist/annotations.d.ts +25 -0
- package/dist/annotations.js +27 -0
- package/dist/app/App.d.ts +64 -0
- package/dist/app/App.js +180 -0
- package/dist/app/editor/AmbientCorrectionPlugin.d.ts +18 -0
- package/dist/app/editor/AmbientCorrectionPlugin.js +185 -0
- package/dist/app/editor/AnchorScrollPlugin.d.ts +16 -0
- package/dist/app/editor/AnchorScrollPlugin.js +134 -0
- package/dist/app/editor/AnnotationMarkerPlugin.d.ts +24 -0
- package/dist/app/editor/AnnotationMarkerPlugin.js +224 -0
- package/dist/app/editor/AnnotationPlugin.d.ts +42 -0
- package/dist/app/editor/AnnotationPlugin.js +135 -0
- package/dist/app/editor/AnnotationSurface.d.ts +34 -0
- package/dist/app/editor/AnnotationSurface.js +115 -0
- package/dist/app/editor/AssetContext.d.ts +20 -0
- package/dist/app/editor/AssetContext.js +81 -0
- package/dist/app/editor/BlockClickPlugin.d.ts +8 -0
- package/dist/app/editor/BlockClickPlugin.js +126 -0
- package/dist/app/editor/CodeBlockPlugin.d.ts +1 -0
- package/dist/app/editor/CodeBlockPlugin.js +219 -0
- package/dist/app/editor/CorrectionPanelPlugin.d.ts +14 -0
- package/dist/app/editor/CorrectionPanelPlugin.js +333 -0
- package/dist/app/editor/DragHandlePlugin.d.ts +1 -0
- package/dist/app/editor/DragHandlePlugin.js +300 -0
- package/dist/app/editor/Editor.d.ts +84 -0
- package/dist/app/editor/Editor.js +539 -0
- package/dist/app/editor/FrontmatterPlugin.d.ts +19 -0
- package/dist/app/editor/FrontmatterPlugin.js +301 -0
- package/dist/app/editor/ImageModal.d.ts +13 -0
- package/dist/app/editor/ImageModal.js +267 -0
- package/dist/app/editor/ImagePlugin.d.ts +3 -0
- package/dist/app/editor/ImagePlugin.js +187 -0
- package/dist/app/editor/LinkClickPlugin.d.ts +23 -0
- package/dist/app/editor/LinkClickPlugin.js +90 -0
- package/dist/app/editor/MarkdownShortcutsPlugin.d.ts +49 -0
- package/dist/app/editor/MarkdownShortcutsPlugin.js +373 -0
- package/dist/app/editor/OrderedTaskListPlugin.d.ts +21 -0
- package/dist/app/editor/OrderedTaskListPlugin.js +78 -0
- package/dist/app/editor/SearchPlugin.d.ts +1 -0
- package/dist/app/editor/SearchPlugin.js +379 -0
- package/dist/app/editor/SelectionContextMenuPlugin.d.ts +29 -0
- package/dist/app/editor/SelectionContextMenuPlugin.js +160 -0
- package/dist/app/editor/SlashMenu.d.ts +11 -0
- package/dist/app/editor/SlashMenu.js +494 -0
- package/dist/app/editor/SlashMenuPlugin.d.ts +1 -0
- package/dist/app/editor/SlashMenuPlugin.js +168 -0
- package/dist/app/editor/TableActionsPlugin.d.ts +1 -0
- package/dist/app/editor/TableActionsPlugin.js +398 -0
- package/dist/app/editor/TogglePlugin.d.ts +1 -0
- package/dist/app/editor/TogglePlugin.js +159 -0
- package/dist/app/editor/Toolbar.d.ts +10 -0
- package/dist/app/editor/Toolbar.js +263 -0
- package/dist/app/editor/WikiLinkExistencePlugin.d.ts +17 -0
- package/dist/app/editor/WikiLinkExistencePlugin.js +104 -0
- package/dist/app/editor/WikiLinkFormatPlugin.d.ts +17 -0
- package/dist/app/editor/WikiLinkFormatPlugin.js +203 -0
- package/dist/app/editor/annotation-marks.d.ts +177 -0
- package/dist/app/editor/annotation-marks.js +750 -0
- package/dist/app/editor/annotationCommands.d.ts +13 -0
- package/dist/app/editor/annotationCommands.js +11 -0
- package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +35 -0
- package/dist/app/editor/c4/C4InteractiveRenderer.js +299 -0
- package/dist/app/editor/c4/edge-clipping.d.ts +24 -0
- package/dist/app/editor/c4/edge-clipping.js +139 -0
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +38 -0
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +112 -0
- package/dist/app/editor/c4/layout.d.ts +25 -0
- package/dist/app/editor/c4/layout.js +839 -0
- package/dist/app/editor/c4/parser.d.ts +19 -0
- package/dist/app/editor/c4/parser.js +410 -0
- package/dist/app/editor/c4/render-to-string.d.ts +24 -0
- package/dist/app/editor/c4/render-to-string.js +34 -0
- package/dist/app/editor/c4/renderer.d.ts +64 -0
- package/dist/app/editor/c4/renderer.js +569 -0
- package/dist/app/editor/c4/types.d.ts +203 -0
- package/dist/app/editor/c4/types.js +43 -0
- package/dist/app/editor/correction-yaml.d.ts +10 -0
- package/dist/app/editor/correction-yaml.js +40 -0
- package/dist/app/editor/editorNodes.d.ts +9 -0
- package/dist/app/editor/editorNodes.js +36 -0
- package/dist/app/editor/index.d.ts +1 -0
- package/dist/app/editor/index.js +1 -0
- package/dist/app/editor/mark-overlap-resolver.d.ts +6 -0
- package/dist/app/editor/mark-overlap-resolver.js +45 -0
- package/dist/app/editor/nodes/C4Component.d.ts +14 -0
- package/dist/app/editor/nodes/C4Component.js +284 -0
- package/dist/app/editor/nodes/C4Node.d.ts +32 -0
- package/dist/app/editor/nodes/C4Node.js +112 -0
- package/dist/app/editor/nodes/CalloutNode.d.ts +23 -0
- package/dist/app/editor/nodes/CalloutNode.js +94 -0
- package/dist/app/editor/nodes/CustomLinkNode.d.ts +57 -0
- package/dist/app/editor/nodes/CustomLinkNode.js +209 -0
- package/dist/app/editor/nodes/CustomListItemNode.d.ts +46 -0
- package/dist/app/editor/nodes/CustomListItemNode.js +120 -0
- package/dist/app/editor/nodes/CustomListNode.d.ts +27 -0
- package/dist/app/editor/nodes/CustomListNode.js +54 -0
- package/dist/app/editor/nodes/DefinitionListNode.d.ts +44 -0
- package/dist/app/editor/nodes/DefinitionListNode.js +151 -0
- package/dist/app/editor/nodes/DiagramContextMenu.d.ts +45 -0
- package/dist/app/editor/nodes/DiagramContextMenu.js +116 -0
- package/dist/app/editor/nodes/EquationComponent.d.ts +17 -0
- package/dist/app/editor/nodes/EquationComponent.js +130 -0
- package/dist/app/editor/nodes/EquationNode.d.ts +43 -0
- package/dist/app/editor/nodes/EquationNode.js +179 -0
- package/dist/app/editor/nodes/FootnoteNode.d.ts +40 -0
- package/dist/app/editor/nodes/FootnoteNode.js +151 -0
- package/dist/app/editor/nodes/FrontmatterNode.d.ts +24 -0
- package/dist/app/editor/nodes/FrontmatterNode.js +88 -0
- package/dist/app/editor/nodes/HorizontalRuleNode.d.ts +17 -0
- package/dist/app/editor/nodes/HorizontalRuleNode.js +57 -0
- package/dist/app/editor/nodes/HtmlNode.d.ts +32 -0
- package/dist/app/editor/nodes/HtmlNode.js +121 -0
- package/dist/app/editor/nodes/ImageComponent.d.ts +11 -0
- package/dist/app/editor/nodes/ImageComponent.js +228 -0
- package/dist/app/editor/nodes/ImageNode.d.ts +53 -0
- package/dist/app/editor/nodes/ImageNode.js +183 -0
- package/dist/app/editor/nodes/ListItemParagraphBreakNode.d.ts +42 -0
- package/dist/app/editor/nodes/ListItemParagraphBreakNode.js +95 -0
- package/dist/app/editor/nodes/MermaidComponent.d.ts +11 -0
- package/dist/app/editor/nodes/MermaidComponent.js +216 -0
- package/dist/app/editor/nodes/MermaidNode.d.ts +27 -0
- package/dist/app/editor/nodes/MermaidNode.js +96 -0
- package/dist/app/editor/nodes/ToggleNode.d.ts +56 -0
- package/dist/app/editor/nodes/ToggleNode.js +237 -0
- package/dist/app/editor/nodes/diagram-context-menu.d.ts +11 -0
- package/dist/app/editor/nodes/diagram-context-menu.js +70 -0
- package/dist/app/editor/nodes/diagram-utils.d.ts +13 -0
- package/dist/app/editor/nodes/diagram-utils.js +29 -0
- package/dist/app/editor/nodes/index.d.ts +30 -0
- package/dist/app/editor/nodes/index.js +17 -0
- package/dist/app/editor/utils.d.ts +2 -0
- package/dist/app/editor/utils.js +17 -0
- package/dist/app/mapper/index.d.ts +3 -0
- package/dist/app/mapper/index.js +2 -0
- package/dist/app/mapper/lexicalToMdast.d.ts +62 -0
- package/dist/app/mapper/lexicalToMdast.js +1937 -0
- package/dist/app/mapper/mdastToLexical.d.ts +31 -0
- package/dist/app/mapper/mdastToLexical.js +1075 -0
- package/dist/contract.d.ts +8 -0
- package/dist/contract.js +8 -0
- package/dist/headless.d.ts +40 -0
- package/dist/headless.js +45 -0
- package/dist/host/context.d.ts +20 -0
- package/dist/host/context.js +24 -0
- package/dist/host/defaults.d.ts +21 -0
- package/dist/host/defaults.js +47 -0
- package/dist/host/messages.d.ts +26 -0
- package/dist/host/messages.js +47 -0
- package/dist/host/types.d.ts +75 -0
- package/dist/host/types.js +13 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +51 -0
- package/dist/markdown/annotate-sentinels.d.ts +42 -0
- package/dist/markdown/annotate-sentinels.js +86 -0
- package/dist/markdown/parse.d.ts +56 -0
- package/dist/markdown/parse.js +516 -0
- package/dist/markdown/stringify.d.ts +7 -0
- package/dist/markdown/stringify.js +439 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/LICENSE +21 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/README.md +41 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +72 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +127 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/index.d.ts +8 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/index.js +6 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +17 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +33 -0
- package/dist/markdown.d.ts +28 -0
- package/dist/markdown.js +44 -0
- package/dist/mathjax-config.d.ts +89 -0
- package/dist/mathjax-config.js +133 -0
- package/dist/nodes.d.ts +34 -0
- package/dist/nodes.js +34 -0
- package/dist/stores/correctionStore.d.ts +15 -0
- package/dist/stores/correctionStore.js +12 -0
- package/dist/stores/editorStore.d.ts +16 -0
- package/dist/stores/editorStore.js +27 -0
- package/dist/styles.css +2477 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.js +91 -0
- package/dist/utils/file-types.d.ts +10 -0
- package/dist/utils/file-types.js +46 -0
- package/docs/annotations.md +134 -0
- package/docs/decisions/README.md +72 -0
- package/docs/decisions/adr-002.md +27 -0
- package/docs/decisions/adr-007.md +27 -0
- package/docs/decisions/adr-010.md +26 -0
- package/docs/decisions/adr-024.md +124 -0
- package/docs/decisions/adr-024b.md +0 -0
- package/docs/decisions/adr-025.md +103 -0
- package/docs/decisions/adr-027.md +66 -0
- package/docs/decisions/adr-057.md +67 -0
- package/docs/decisions/adr-070.md +131 -0
- package/docs/decisions/adr-075.md +410 -0
- package/docs/decisions/adr-076.md +183 -0
- package/docs/decisions/adr-077.md +410 -0
- package/docs/decisions/adr-078.md +243 -0
- package/docs/decisions/adr-079.md +167 -0
- package/docs/decisions/adr-080.md +325 -0
- package/docs/decisions/adr-081.md +108 -0
- package/docs/decisions/adr-082.md +123 -0
- package/docs/decisions/adr-083.md +204 -0
- package/docs/decisions/adr-084.md +179 -0
- package/docs/editor-api.md +106 -0
- package/docs/markdown-pipeline.md +282 -0
- package/docs/provenance.md +106 -0
- package/docs/zusammen-editor-capability-map.md +232 -0
- package/package.json +175 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Root, Content, PhrasingContent } from 'mdast';
|
|
2
|
+
export interface ParseOptions {
|
|
3
|
+
mathEnabled?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface ParseResult {
|
|
6
|
+
root: Root;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseMarkdown(text: string, _options?: ParseOptions): ParseResult;
|
|
9
|
+
export declare function isParagraph(node: Content): node is Extract<Content, {
|
|
10
|
+
type: 'paragraph';
|
|
11
|
+
}>;
|
|
12
|
+
export declare function isHeading(node: Content): node is Extract<Content, {
|
|
13
|
+
type: 'heading';
|
|
14
|
+
}>;
|
|
15
|
+
export declare function isList(node: Content): node is Extract<Content, {
|
|
16
|
+
type: 'list';
|
|
17
|
+
}>;
|
|
18
|
+
export declare function isListItem(node: Content): node is Extract<Content, {
|
|
19
|
+
type: 'listItem';
|
|
20
|
+
}>;
|
|
21
|
+
export declare function isBlockquote(node: Content): node is Extract<Content, {
|
|
22
|
+
type: 'blockquote';
|
|
23
|
+
}>;
|
|
24
|
+
export declare function isCode(node: Content): node is Extract<Content, {
|
|
25
|
+
type: 'code';
|
|
26
|
+
}>;
|
|
27
|
+
export declare function isThematicBreak(node: Content): node is Extract<Content, {
|
|
28
|
+
type: 'thematicBreak';
|
|
29
|
+
}>;
|
|
30
|
+
export declare function isTable(node: Content): node is Extract<Content, {
|
|
31
|
+
type: 'table';
|
|
32
|
+
}>;
|
|
33
|
+
export declare function isImage(node: Content | PhrasingContent): node is Extract<Content, {
|
|
34
|
+
type: 'image';
|
|
35
|
+
}>;
|
|
36
|
+
export declare function isLink(node: Content | PhrasingContent): node is Extract<Content, {
|
|
37
|
+
type: 'link';
|
|
38
|
+
}>;
|
|
39
|
+
export declare function isHtml(node: Content): node is Extract<Content, {
|
|
40
|
+
type: 'html';
|
|
41
|
+
}>;
|
|
42
|
+
export declare function isText(node: Content | PhrasingContent): node is Extract<PhrasingContent, {
|
|
43
|
+
type: 'text';
|
|
44
|
+
}>;
|
|
45
|
+
export declare function isStrong(node: Content | PhrasingContent): node is Extract<PhrasingContent, {
|
|
46
|
+
type: 'strong';
|
|
47
|
+
}>;
|
|
48
|
+
export declare function isEmphasis(node: Content | PhrasingContent): node is Extract<PhrasingContent, {
|
|
49
|
+
type: 'emphasis';
|
|
50
|
+
}>;
|
|
51
|
+
export declare function isInlineCode(node: Content | PhrasingContent): node is Extract<PhrasingContent, {
|
|
52
|
+
type: 'inlineCode';
|
|
53
|
+
}>;
|
|
54
|
+
export declare function isDelete(node: Content | PhrasingContent): node is Extract<PhrasingContent, {
|
|
55
|
+
type: 'delete';
|
|
56
|
+
}>;
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
2
|
+
import { gfm } from 'micromark-extension-gfm';
|
|
3
|
+
import { gfmFromMarkdown } from 'mdast-util-gfm';
|
|
4
|
+
import { math } from 'micromark-extension-math';
|
|
5
|
+
import { mathFromMarkdown } from 'mdast-util-math';
|
|
6
|
+
import { frontmatter } from 'micromark-extension-frontmatter';
|
|
7
|
+
import { frontmatterFromMarkdown } from 'mdast-util-frontmatter';
|
|
8
|
+
import { defList } from 'micromark-extension-definition-list';
|
|
9
|
+
import { defListFromMarkdown } from 'mdast-util-definition-list';
|
|
10
|
+
import { gfmFootnote } from 'micromark-extension-gfm-footnote';
|
|
11
|
+
import { gfmFootnoteFromMarkdown } from 'mdast-util-gfm-footnote';
|
|
12
|
+
import { syntax as wikiLinkSyntax } from 'micromark-extension-wiki-link';
|
|
13
|
+
import * as wikiLinkMdast from './vendor/mdast-util-wiki-link/index.js';
|
|
14
|
+
// Wiki-link options: use | as the alias divider (Obsidian/Foam style)
|
|
15
|
+
const wikiLinkOptions = { aliasDivider: '|' };
|
|
16
|
+
const EMPTY_ALIAS_SENTINEL = '__EMPTY_ALIAS__';
|
|
17
|
+
/**
|
|
18
|
+
* Escape pipes inside wiki-links: [[target|alias]] → [[target\|alias]]
|
|
19
|
+
* This prevents GFM table parsing from splitting wiki-links at the pipe.
|
|
20
|
+
* The vendored mdast-util-wiki-link strips the backslash during conversion.
|
|
21
|
+
*
|
|
22
|
+
* NOTE: Only escapes the FIRST pipe inside each wiki-link (the alias divider).
|
|
23
|
+
* Additional pipes in the alias text are valid and should remain unescaped.
|
|
24
|
+
*/
|
|
25
|
+
function escapeWikiLinkPipes(text) {
|
|
26
|
+
const replacements = [];
|
|
27
|
+
// Match [[ followed by non-] characters, then | (alias divider), then more content until ]]
|
|
28
|
+
// Use non-greedy matching to handle multiple wiki-links
|
|
29
|
+
const pattern = /\[\[([^\]|]+)\|([^\]]*)\]\]/g;
|
|
30
|
+
let result = '';
|
|
31
|
+
let cursor = 0;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
34
|
+
const originalStart = match.index;
|
|
35
|
+
const originalEnd = match.index + match[0].length;
|
|
36
|
+
const target = match[1];
|
|
37
|
+
const alias = match[2];
|
|
38
|
+
// Skip if pipe is already escaped (rare edge case where source has \|)
|
|
39
|
+
// This would show up as target ending with \
|
|
40
|
+
if (target.endsWith('\\')) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const replacement = `[[${target}\\|${alias}]]`;
|
|
44
|
+
result += text.slice(cursor, originalStart);
|
|
45
|
+
const normalizedStart = result.length;
|
|
46
|
+
result += replacement;
|
|
47
|
+
const normalizedEnd = result.length;
|
|
48
|
+
replacements.push({
|
|
49
|
+
normalizedStart,
|
|
50
|
+
normalizedEnd,
|
|
51
|
+
delta: replacement.length - match[0].length, // +1 for the backslash
|
|
52
|
+
originalStart,
|
|
53
|
+
originalEnd,
|
|
54
|
+
});
|
|
55
|
+
cursor = originalEnd;
|
|
56
|
+
}
|
|
57
|
+
result += text.slice(cursor);
|
|
58
|
+
return { text: result, replacements };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Combine two sets of replacements from sequential preprocessing steps.
|
|
62
|
+
* The second set of replacements refers to positions in text AFTER the first set was applied.
|
|
63
|
+
* We need to adjust the second set's original positions to refer to the true original text.
|
|
64
|
+
*/
|
|
65
|
+
function combineReplacements(first, second) {
|
|
66
|
+
if (first.length === 0)
|
|
67
|
+
return second;
|
|
68
|
+
if (second.length === 0)
|
|
69
|
+
return first;
|
|
70
|
+
// Adjust second replacements' original positions based on first replacements' deltas.
|
|
71
|
+
// Uses a single-pass O(N+M) merge since both arrays are sorted by position.
|
|
72
|
+
//
|
|
73
|
+
// NOTE: This does not adjust the first set's normalizedStart/normalizedEnd for shifts
|
|
74
|
+
// introduced by the second step. In practice this is fine because mapNormalizedOffsetToOriginal
|
|
75
|
+
// is only used for emphasis/strong marker detection, and emphasis markers won't appear
|
|
76
|
+
// inside wiki-links. If additional preprocessing steps or offset uses are added in the
|
|
77
|
+
// future, this assumption should be revisited.
|
|
78
|
+
const adjustedSecond = [];
|
|
79
|
+
let deltaAdjustment = 0;
|
|
80
|
+
let firstIndex = 0;
|
|
81
|
+
for (const rep of second) {
|
|
82
|
+
// Advance through `first` to accumulate deltas from replacements before this position
|
|
83
|
+
while (firstIndex < first.length && first[firstIndex].normalizedEnd <= rep.originalStart) {
|
|
84
|
+
deltaAdjustment += first[firstIndex].delta;
|
|
85
|
+
firstIndex++;
|
|
86
|
+
}
|
|
87
|
+
adjustedSecond.push({
|
|
88
|
+
...rep,
|
|
89
|
+
originalStart: rep.originalStart - deltaAdjustment,
|
|
90
|
+
originalEnd: rep.originalEnd - deltaAdjustment,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Merge and sort by normalizedStart
|
|
94
|
+
return [...first, ...adjustedSecond].sort((a, b) => a.normalizedStart - b.normalizedStart);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Normalize wiki-links with empty aliases: [[target|]] → [[target|__EMPTY_ALIAS__]]
|
|
98
|
+
* We preserve the intent by using a sentinel, then strip it after parsing.
|
|
99
|
+
* Returns the normalized text plus offset mapping data.
|
|
100
|
+
*/
|
|
101
|
+
function normalizeWikiLinks(text) {
|
|
102
|
+
const replacements = [];
|
|
103
|
+
const pattern = /\[\[([^\]|]+)\|\]\]/g;
|
|
104
|
+
let result = '';
|
|
105
|
+
let cursor = 0;
|
|
106
|
+
let match;
|
|
107
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
108
|
+
const originalStart = match.index;
|
|
109
|
+
const originalEnd = match.index + match[0].length;
|
|
110
|
+
const replacement = `[[${match[1]}|${EMPTY_ALIAS_SENTINEL}]]`;
|
|
111
|
+
result += text.slice(cursor, originalStart);
|
|
112
|
+
const normalizedStart = result.length;
|
|
113
|
+
result += replacement;
|
|
114
|
+
const normalizedEnd = result.length;
|
|
115
|
+
replacements.push({
|
|
116
|
+
normalizedStart,
|
|
117
|
+
normalizedEnd,
|
|
118
|
+
delta: replacement.length - match[0].length,
|
|
119
|
+
originalStart,
|
|
120
|
+
originalEnd,
|
|
121
|
+
});
|
|
122
|
+
cursor = originalEnd;
|
|
123
|
+
}
|
|
124
|
+
result += text.slice(cursor);
|
|
125
|
+
return { text: result, replacements };
|
|
126
|
+
}
|
|
127
|
+
function mapNormalizedOffsetToOriginal(offset, replacements) {
|
|
128
|
+
let delta = 0;
|
|
129
|
+
for (const rep of replacements) {
|
|
130
|
+
if (offset >= rep.normalizedEnd) {
|
|
131
|
+
delta += rep.delta;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (offset >= rep.normalizedStart) {
|
|
135
|
+
// Offset falls within a replacement; map to the start of the original span.
|
|
136
|
+
return rep.originalStart;
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
return offset - delta;
|
|
141
|
+
}
|
|
142
|
+
function annotateEmphasisMarkers(root, originalText, replacements) {
|
|
143
|
+
function walk(node) {
|
|
144
|
+
if (!node || typeof node !== 'object')
|
|
145
|
+
return node;
|
|
146
|
+
if ((node.type === 'emphasis' || node.type === 'strong') && node.position?.start?.offset != null) {
|
|
147
|
+
const normalizedOffset = node.position.start.offset;
|
|
148
|
+
const originalOffset = mapNormalizedOffsetToOriginal(normalizedOffset, replacements);
|
|
149
|
+
const data = node.data && typeof node.data === 'object' ? { ...node.data } : {};
|
|
150
|
+
if (node.type === 'emphasis') {
|
|
151
|
+
const marker = originalText[originalOffset];
|
|
152
|
+
if (marker === '_' || marker === '*') {
|
|
153
|
+
data._emphasisMarker = marker;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (node.type === 'strong') {
|
|
157
|
+
const two = originalText.slice(originalOffset, originalOffset + 2);
|
|
158
|
+
if (two === '__' || two === '**') {
|
|
159
|
+
data._strongMarker = two[0];
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const marker = originalText[originalOffset];
|
|
163
|
+
if (marker === '_' || marker === '*') {
|
|
164
|
+
data._strongMarker = marker;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
node.data = data;
|
|
169
|
+
}
|
|
170
|
+
if (node.children && Array.isArray(node.children)) {
|
|
171
|
+
node.children = node.children.map(walk);
|
|
172
|
+
}
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
return walk(root);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Mark wiki-links that had an empty alias in the source.
|
|
179
|
+
* Sets data._emptyAlias = true and clears alias to empty string.
|
|
180
|
+
*/
|
|
181
|
+
function markEmptyAliasWikiLinks(root) {
|
|
182
|
+
function walk(node) {
|
|
183
|
+
if (!node || typeof node !== 'object')
|
|
184
|
+
return node;
|
|
185
|
+
if (node.type === 'wikiLink') {
|
|
186
|
+
const alias = node.data?.alias;
|
|
187
|
+
if (alias === EMPTY_ALIAS_SENTINEL) {
|
|
188
|
+
return {
|
|
189
|
+
...node,
|
|
190
|
+
data: { ...(node.data || {}), alias: '', _emptyAlias: true },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
return node;
|
|
194
|
+
}
|
|
195
|
+
if (node.children && Array.isArray(node.children)) {
|
|
196
|
+
return {
|
|
197
|
+
...node,
|
|
198
|
+
children: node.children.map(walk),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return node;
|
|
202
|
+
}
|
|
203
|
+
return walk(root);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Strip trailing backslash from wiki-link values when they have an alias.
|
|
207
|
+
* This handles the escaped pipe (\|) that was added by escapeWikiLinkPipes().
|
|
208
|
+
*
|
|
209
|
+
* NOTE: The vendored `mdast-util-wiki-link` (src/markdown/vendor/) already
|
|
210
|
+
* strips this backslash during fromMarkdown() processing. This function serves
|
|
211
|
+
* as defense-in-depth in case that strip is ever removed or a caller supplies a
|
|
212
|
+
* different wiki-link mdast extension. In normal operation the condition
|
|
213
|
+
* `node.value?.endsWith('\\')` will be false (already stripped upstream of here).
|
|
214
|
+
*/
|
|
215
|
+
function stripEscapedPipeFromWikiLinks(root) {
|
|
216
|
+
function walk(node) {
|
|
217
|
+
if (!node || typeof node !== 'object')
|
|
218
|
+
return node;
|
|
219
|
+
if (node.type === 'wikiLink') {
|
|
220
|
+
// Only strip if there's an alias (the backslash was only added when there's a pipe/alias)
|
|
221
|
+
if (node.data?.alias && node.value?.endsWith('\\')) {
|
|
222
|
+
return {
|
|
223
|
+
...node,
|
|
224
|
+
value: node.value.slice(0, -1),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return node;
|
|
228
|
+
}
|
|
229
|
+
if (node.children && Array.isArray(node.children)) {
|
|
230
|
+
return {
|
|
231
|
+
...node,
|
|
232
|
+
children: node.children.map(walk),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return node;
|
|
236
|
+
}
|
|
237
|
+
return walk(root);
|
|
238
|
+
}
|
|
239
|
+
// CommonMark's escapable ASCII punctuation set: a backslash followed by one
|
|
240
|
+
// of these is a genuine escape (consumed to the literal character); a
|
|
241
|
+
// backslash followed by anything else is a literal backslash.
|
|
242
|
+
const ESCAPABLE_PUNCTUATION = new Set('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split(''));
|
|
243
|
+
// The subset of escapable punctuation this issue (#17) covers: characters
|
|
244
|
+
// whose escape must survive the round trip so it isn't reinterpreted on the
|
|
245
|
+
// next parse (see spec's "Assumptions" for why this list stops at seven).
|
|
246
|
+
const FORCE_ESCAPE_CHARS = new Set(['*', '_', '`', '[', ']', '#', '\\']);
|
|
247
|
+
/**
|
|
248
|
+
* Replay CommonMark's own backslash-escape decoding over a text node's raw
|
|
249
|
+
* source span, character by character, so each decoded character can be
|
|
250
|
+
* traced back to the exact source offset(s) it came from.
|
|
251
|
+
*/
|
|
252
|
+
function replayDecodeEscapes(source) {
|
|
253
|
+
const parts = [];
|
|
254
|
+
let decoded = '';
|
|
255
|
+
let i = 0;
|
|
256
|
+
while (i < source.length) {
|
|
257
|
+
const ch = source[i];
|
|
258
|
+
if (ch === '\\' && i + 1 < source.length && ESCAPABLE_PUNCTUATION.has(source[i + 1])) {
|
|
259
|
+
const escaped = source[i + 1];
|
|
260
|
+
parts.push({ char: escaped, srcStart: i, srcEnd: i + 2, escaped: true });
|
|
261
|
+
decoded += escaped;
|
|
262
|
+
i += 2;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
parts.push({ char: ch, srcStart: i, srcEnd: i + 1, escaped: false });
|
|
266
|
+
decoded += ch;
|
|
267
|
+
i += 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { decoded, parts };
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Split a single `text` node into siblings wherever it contains a
|
|
274
|
+
* backslash-escaped instance of one of the seven target characters, tagging
|
|
275
|
+
* only that single-character node with `data._forceEscape = true`.
|
|
276
|
+
*
|
|
277
|
+
* Returns `[node]` unchanged whenever nothing needs protecting, or whenever
|
|
278
|
+
* the source span's replayed decoding doesn't exactly reproduce `node.value`
|
|
279
|
+
* (e.g. a character reference like `&` in the span) — the latter is a
|
|
280
|
+
* conservative bail-out, mirroring `recordOffsetSpan`'s own width-mismatch
|
|
281
|
+
* guard in `mdastToLexical.ts`: a missed protection reproduces today's
|
|
282
|
+
* existing defect, but a wrong split would corrupt text. Because
|
|
283
|
+
* `fromMarkdown` merges a character reference and any surrounding literal
|
|
284
|
+
* text (including an escaped target character) into one text node, a
|
|
285
|
+
* character reference anywhere in the same run also suppresses protection
|
|
286
|
+
* for an escape elsewhere in that run — a narrow, pre-existing-defect-shaped
|
|
287
|
+
* limitation, not a new one.
|
|
288
|
+
*/
|
|
289
|
+
function splitTextNodeEscapes(node, normalizedText) {
|
|
290
|
+
const start = node.position?.start?.offset;
|
|
291
|
+
const end = node.position?.end?.offset;
|
|
292
|
+
if (start == null || end == null) {
|
|
293
|
+
return [node];
|
|
294
|
+
}
|
|
295
|
+
const source = normalizedText.slice(start, end);
|
|
296
|
+
const { decoded, parts } = replayDecodeEscapes(source);
|
|
297
|
+
if (decoded !== node.value) {
|
|
298
|
+
return [node];
|
|
299
|
+
}
|
|
300
|
+
if (!parts.some((p) => p.escaped && FORCE_ESCAPE_CHARS.has(p.char))) {
|
|
301
|
+
return [node];
|
|
302
|
+
}
|
|
303
|
+
const result = [];
|
|
304
|
+
const startPos = node.position.start;
|
|
305
|
+
// Per-offset line/column within `source`, walked once from the original
|
|
306
|
+
// node's start position — `makeTextNode` below looks up the correct point
|
|
307
|
+
// for each split node's start/end instead of copying the original node's
|
|
308
|
+
// start position onto every sibling (which would leave every split node's
|
|
309
|
+
// `end` reporting the same line/column as the original node's `start`).
|
|
310
|
+
const positionAt = new Array(source.length + 1);
|
|
311
|
+
{
|
|
312
|
+
let line = startPos.line;
|
|
313
|
+
let column = startPos.column;
|
|
314
|
+
positionAt[0] = { line, column };
|
|
315
|
+
for (let i = 0; i < source.length; i++) {
|
|
316
|
+
if (source[i] === '\n') {
|
|
317
|
+
line += 1;
|
|
318
|
+
column = 1;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
column += 1;
|
|
322
|
+
}
|
|
323
|
+
positionAt[i + 1] = { line, column };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const makeTextNode = (value, srcStart, srcEnd, forceEscape) => ({
|
|
327
|
+
type: 'text',
|
|
328
|
+
value,
|
|
329
|
+
...(forceEscape ? { data: { _forceEscape: true } } : {}),
|
|
330
|
+
position: {
|
|
331
|
+
start: { ...positionAt[srcStart - start], offset: srcStart },
|
|
332
|
+
end: { ...positionAt[srcEnd - start], offset: srcEnd },
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
let runStart = 0;
|
|
336
|
+
const flushRun = (from, to) => {
|
|
337
|
+
if (to <= from)
|
|
338
|
+
return;
|
|
339
|
+
const value = parts.slice(from, to).map((p) => p.char).join('');
|
|
340
|
+
result.push(makeTextNode(value, start + parts[from].srcStart, start + parts[to - 1].srcEnd, false));
|
|
341
|
+
};
|
|
342
|
+
for (let i = 0; i < parts.length; i++) {
|
|
343
|
+
if (parts[i].escaped && FORCE_ESCAPE_CHARS.has(parts[i].char)) {
|
|
344
|
+
flushRun(runStart, i);
|
|
345
|
+
result.push(makeTextNode(parts[i].char, start + parts[i].srcStart, start + parts[i].srcEnd, true));
|
|
346
|
+
runStart = i + 1;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
flushRun(runStart, parts.length);
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Walk the tree and split every `text` node containing a force-escaped
|
|
354
|
+
* character (see `splitTextNodeEscapes`) into siblings. Run last, after all
|
|
355
|
+
* other parse post-processing, so nothing downstream needs to reason about
|
|
356
|
+
* multi-node text runs that used to be a single node.
|
|
357
|
+
*/
|
|
358
|
+
function splitEscapedPunctuation(root, normalizedText) {
|
|
359
|
+
function walk(node) {
|
|
360
|
+
if (!node || typeof node !== 'object')
|
|
361
|
+
return node;
|
|
362
|
+
if (node.children && Array.isArray(node.children)) {
|
|
363
|
+
const children = [];
|
|
364
|
+
for (const child of node.children) {
|
|
365
|
+
if (child?.type === 'text') {
|
|
366
|
+
children.push(...splitTextNodeEscapes(child, normalizedText));
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
children.push(walk(child));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return { ...node, children };
|
|
373
|
+
}
|
|
374
|
+
return node;
|
|
375
|
+
}
|
|
376
|
+
return walk(root);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Post-process mdast tree to support checkboxes in ordered lists.
|
|
380
|
+
*
|
|
381
|
+
* GFM task lists only work with unordered lists (- [ ]), but Foam/Obsidian
|
|
382
|
+
* commonly use them in ordered lists too (1. [ ]). The GFM parser leaves
|
|
383
|
+
* the checkbox text as regular text in ordered lists.
|
|
384
|
+
*
|
|
385
|
+
* This function finds ordered list items that start with [ ] or [x] text
|
|
386
|
+
* and converts them to proper task list items by:
|
|
387
|
+
* 1. Setting the checked property on the list item
|
|
388
|
+
* 2. Removing the checkbox text from the content
|
|
389
|
+
*/
|
|
390
|
+
function addCheckboxesToOrderedLists(root) {
|
|
391
|
+
function processNode(node) {
|
|
392
|
+
if (node.type === 'list' && node.ordered === true) {
|
|
393
|
+
// Process children (list items)
|
|
394
|
+
return {
|
|
395
|
+
...node,
|
|
396
|
+
children: node.children.map((listItem) => processOrderedListItem(listItem)),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
// Recursively process children for other node types
|
|
400
|
+
if (node.children && Array.isArray(node.children)) {
|
|
401
|
+
return {
|
|
402
|
+
...node,
|
|
403
|
+
children: node.children.map(processNode),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return node;
|
|
407
|
+
}
|
|
408
|
+
function processOrderedListItem(listItem) {
|
|
409
|
+
// List item should have checked: null for non-task items
|
|
410
|
+
// Check if the first paragraph starts with [ ] or [x]
|
|
411
|
+
if (listItem.children && listItem.children.length > 0) {
|
|
412
|
+
const firstChild = listItem.children[0];
|
|
413
|
+
if (firstChild.type === 'paragraph' && firstChild.children && firstChild.children.length > 0) {
|
|
414
|
+
const firstInline = firstChild.children[0];
|
|
415
|
+
if (firstInline.type === 'text') {
|
|
416
|
+
const text = firstInline.value;
|
|
417
|
+
// Match [ ] or [x] or [X] at the start
|
|
418
|
+
const checkboxMatch = text.match(/^\[( |x|X)\]\s*/);
|
|
419
|
+
if (checkboxMatch) {
|
|
420
|
+
const isChecked = checkboxMatch[1].toLowerCase() === 'x';
|
|
421
|
+
// Mark as checked but keep the original text intact.
|
|
422
|
+
// This preserves "[ ]" in the editor and lets export detect it later.
|
|
423
|
+
return {
|
|
424
|
+
...listItem,
|
|
425
|
+
checked: isChecked,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return listItem;
|
|
432
|
+
}
|
|
433
|
+
return processNode(root);
|
|
434
|
+
}
|
|
435
|
+
export function parseMarkdown(text, _options = {}) {
|
|
436
|
+
// Pre-process to handle edge cases
|
|
437
|
+
// Step 1: Escape pipes inside wiki-links to protect from GFM table parsing
|
|
438
|
+
const { text: pipesEscaped, replacements: pipeReplacements } = escapeWikiLinkPipes(text);
|
|
439
|
+
// Step 2: Normalize empty aliases (existing logic)
|
|
440
|
+
const { text: normalizedText, replacements: aliasReplacements } = normalizeWikiLinks(pipesEscaped);
|
|
441
|
+
// Combine replacements for offset mapping (pipeReplacements first, then adjust aliasReplacements)
|
|
442
|
+
const replacements = combineReplacements(pipeReplacements, aliasReplacements);
|
|
443
|
+
let root = fromMarkdown(normalizedText, {
|
|
444
|
+
extensions: [gfm(), math(), defList, gfmFootnote(), frontmatter(['yaml']), wikiLinkSyntax(wikiLinkOptions)],
|
|
445
|
+
mdastExtensions: [
|
|
446
|
+
gfmFromMarkdown(),
|
|
447
|
+
mathFromMarkdown(),
|
|
448
|
+
defListFromMarkdown,
|
|
449
|
+
gfmFootnoteFromMarkdown(),
|
|
450
|
+
frontmatterFromMarkdown(['yaml']),
|
|
451
|
+
wikiLinkMdast.fromMarkdown(wikiLinkOptions),
|
|
452
|
+
],
|
|
453
|
+
});
|
|
454
|
+
// Post-process: add checkbox support for ordered lists
|
|
455
|
+
// GFM only supports task lists in unordered lists, but Foam/Obsidian use them in ordered lists too
|
|
456
|
+
root = addCheckboxesToOrderedLists(root);
|
|
457
|
+
// Post-process: strip escaped pipe backslash from wiki-link targets
|
|
458
|
+
root = stripEscapedPipeFromWikiLinks(root);
|
|
459
|
+
// Post-process: mark wiki-links that had empty aliases in the source
|
|
460
|
+
root = markEmptyAliasWikiLinks(root);
|
|
461
|
+
// Post-process: annotate emphasis/strong marker characters from original source
|
|
462
|
+
root = annotateEmphasisMarkers(root, text, replacements);
|
|
463
|
+
// Post-process: split out backslash-escaped punctuation so its escape can
|
|
464
|
+
// be carried through Lexical and restored at stringify time (#17)
|
|
465
|
+
root = splitEscapedPunctuation(root, normalizedText);
|
|
466
|
+
return { root };
|
|
467
|
+
}
|
|
468
|
+
// Type guards for mdast nodes
|
|
469
|
+
export function isParagraph(node) {
|
|
470
|
+
return node.type === 'paragraph';
|
|
471
|
+
}
|
|
472
|
+
export function isHeading(node) {
|
|
473
|
+
return node.type === 'heading';
|
|
474
|
+
}
|
|
475
|
+
export function isList(node) {
|
|
476
|
+
return node.type === 'list';
|
|
477
|
+
}
|
|
478
|
+
export function isListItem(node) {
|
|
479
|
+
return node.type === 'listItem';
|
|
480
|
+
}
|
|
481
|
+
export function isBlockquote(node) {
|
|
482
|
+
return node.type === 'blockquote';
|
|
483
|
+
}
|
|
484
|
+
export function isCode(node) {
|
|
485
|
+
return node.type === 'code';
|
|
486
|
+
}
|
|
487
|
+
export function isThematicBreak(node) {
|
|
488
|
+
return node.type === 'thematicBreak';
|
|
489
|
+
}
|
|
490
|
+
export function isTable(node) {
|
|
491
|
+
return node.type === 'table';
|
|
492
|
+
}
|
|
493
|
+
export function isImage(node) {
|
|
494
|
+
return node.type === 'image';
|
|
495
|
+
}
|
|
496
|
+
export function isLink(node) {
|
|
497
|
+
return node.type === 'link';
|
|
498
|
+
}
|
|
499
|
+
export function isHtml(node) {
|
|
500
|
+
return node.type === 'html';
|
|
501
|
+
}
|
|
502
|
+
export function isText(node) {
|
|
503
|
+
return node.type === 'text';
|
|
504
|
+
}
|
|
505
|
+
export function isStrong(node) {
|
|
506
|
+
return node.type === 'strong';
|
|
507
|
+
}
|
|
508
|
+
export function isEmphasis(node) {
|
|
509
|
+
return node.type === 'emphasis';
|
|
510
|
+
}
|
|
511
|
+
export function isInlineCode(node) {
|
|
512
|
+
return node.type === 'inlineCode';
|
|
513
|
+
}
|
|
514
|
+
export function isDelete(node) {
|
|
515
|
+
return node.type === 'delete';
|
|
516
|
+
}
|