@singapore-editor/markdown 0.1.0
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/README.md +43 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/replacements.d.ts +4 -0
- package/dist/replacements.d.ts.map +1 -0
- package/dist/replacements.js +133 -0
- package/dist/replacements.js.map +1 -0
- package/dist/style.css +27 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @singapore-editor/markdown
|
|
2
|
+
|
|
3
|
+
Markdown live preview for the Singapore editor. Markdown text stays the document — this renders it as
|
|
4
|
+
formatted text without ever converting it into another model.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { createMarkdownPreviewPlugin } from '@singapore-editor/markdown'
|
|
8
|
+
import '@singapore-editor/markdown/style.css'
|
|
9
|
+
|
|
10
|
+
new Editor(container, {
|
|
11
|
+
plugins: [markdown(), createMarkdownPreviewPlugin()],
|
|
12
|
+
})
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The plugin's presence is the switch. It needs a markdown language plugin alongside it (for example
|
|
16
|
+
`markdown()` from `@singapore-editor/tree-sitter-languages`), because it reads that grammar's captures.
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
| Source | Rendered |
|
|
21
|
+
| ----------------------- | --------------- |
|
|
22
|
+
| `# Title` | `Title` |
|
|
23
|
+
| `a **bold** b` | `a bold b` |
|
|
24
|
+
| `an _em_ word` | `an em word` |
|
|
25
|
+
| ``use `code` here`` | `use code here` |
|
|
26
|
+
| `[docs](https://x.dev)` | `docs` |
|
|
27
|
+
| `` | `alt` |
|
|
28
|
+
| `- item` | `• item` |
|
|
29
|
+
|
|
30
|
+
Ordered lists, block quotes, escapes, and fenced code blocks are deliberately left as written.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
There is no second document model. The buffer holds markdown text and the editor's inline display
|
|
35
|
+
transform paints something else over parts of it — see [Display: Transforms](../../docs/display/transforms.md).
|
|
36
|
+
Because the text never changes, undo, selections, folds, find, and anchors all keep working on the
|
|
37
|
+
markdown itself, and there is no round-trip to lose fidelity to.
|
|
38
|
+
|
|
39
|
+
Constructs reveal as you reach them: put the caret anywhere inside `**bold**` and both fences come
|
|
40
|
+
back, so it stays editable as plain text.
|
|
41
|
+
|
|
42
|
+
`markdownInlineReplacements(text, captures)` is exported on its own if you want the derivation
|
|
43
|
+
without the plugin.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EditorPlugin } from '@singapore-editor/core/extensions';
|
|
2
|
+
import './style.css';
|
|
3
|
+
export { markdownInlineReplacements } from './replacements';
|
|
4
|
+
export type MarkdownPreviewPluginOptions = {
|
|
5
|
+
/** Language ids this applies to. Defaults to markdown only, so other files render as source. */
|
|
6
|
+
readonly languageIds?: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Renders markdown as formatted text while the buffer keeps holding markdown source: fences hide,
|
|
10
|
+
* headings drop their `#`, links collapse to their label, and the source under the caret comes back
|
|
11
|
+
* so it stays editable as text. Include the plugin to turn it on, remove it to turn it off.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createMarkdownPreviewPlugin(options?: MarkdownPreviewPluginOptions): EditorPlugin;
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EAEb,MAAM,mCAAmC,CAAA;AAG1C,OAAO,aAAa,CAAA;AAEpB,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA;AAE3D,MAAM,MAAM,4BAA4B,GAAG;IACzC,gGAAgG;IAChG,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC,CAAA;AAID;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,4BAAiC,GACzC,YAAY,CAWd"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { markdownInlineReplacements } from "./replacements.js";
|
|
2
|
+
import "./style.css";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
var DEFAULT_LANGUAGE_IDS = ["markdown"];
|
|
5
|
+
/**
|
|
6
|
+
* Renders markdown as formatted text while the buffer keeps holding markdown source: fences hide,
|
|
7
|
+
* headings drop their `#`, links collapse to their label, and the source under the caret comes back
|
|
8
|
+
* so it stays editable as text. Include the plugin to turn it on, remove it to turn it off.
|
|
9
|
+
*/
|
|
10
|
+
function createMarkdownPreviewPlugin(options = {}) {
|
|
11
|
+
const languageIds = new Set(options.languageIds ?? DEFAULT_LANGUAGE_IDS);
|
|
12
|
+
return {
|
|
13
|
+
name: "markdown-preview",
|
|
14
|
+
activate: (context) => context.registerInlineReplacementProvider((replacementContext) => replacementsForContext(replacementContext, languageIds), {
|
|
15
|
+
trigger: "edit",
|
|
16
|
+
requiresSyntax: true
|
|
17
|
+
})
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
var replacementsForContext = (context, languageIds) => {
|
|
21
|
+
if (context.languageId === null) return [];
|
|
22
|
+
if (!languageIds.has(context.languageId)) return [];
|
|
23
|
+
if (context.records?.languageId !== context.languageId) return [];
|
|
24
|
+
return markdownInlineReplacements(context.textSnapshot, context.records.data);
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { createMarkdownPreviewPlugin, markdownInlineReplacements };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type {\n EditorPlugin,\n EditorInlineReplacementContext,\n} from '@singapore-editor/core/extensions'\nimport type { InlineReplacementSpec } from '@singapore-editor/core/rendering'\nimport { markdownInlineReplacements } from './replacements'\nimport './style.css'\n\nexport { markdownInlineReplacements } from './replacements'\n\nexport type MarkdownPreviewPluginOptions = {\n /** Language ids this applies to. Defaults to markdown only, so other files render as source. */\n readonly languageIds?: readonly string[]\n}\n\nconst DEFAULT_LANGUAGE_IDS = ['markdown']\n\n/**\n * Renders markdown as formatted text while the buffer keeps holding markdown source: fences hide,\n * headings drop their `#`, links collapse to their label, and the source under the caret comes back\n * so it stays editable as text. Include the plugin to turn it on, remove it to turn it off.\n */\nexport function createMarkdownPreviewPlugin(\n options: MarkdownPreviewPluginOptions = {},\n): EditorPlugin {\n const languageIds = new Set(options.languageIds ?? DEFAULT_LANGUAGE_IDS)\n\n return {\n name: 'markdown-preview',\n activate: (context) =>\n context.registerInlineReplacementProvider(\n (replacementContext) => replacementsForContext(replacementContext, languageIds),\n { trigger: 'edit', requiresSyntax: true },\n ),\n }\n}\n\nconst replacementsForContext = (\n context: EditorInlineReplacementContext,\n languageIds: ReadonlySet<string>,\n): readonly InlineReplacementSpec[] => {\n if (context.languageId === null) return []\n if (!languageIds.has(context.languageId)) return []\n if (context.records?.languageId !== context.languageId) return []\n return markdownInlineReplacements(context.textSnapshot, context.records.data)\n}\n"],"mappings":";;;AAeA,IAAM,uBAAuB,CAAC,UAAU;;;;;;AAOxC,SAAgB,4BACd,UAAwC,CAAC,GAC3B;CACd,MAAM,cAAc,IAAI,IAAI,QAAQ,eAAe,oBAAoB;CAEvE,OAAO;EACL,MAAM;EACN,WAAW,YACT,QAAQ,mCACL,uBAAuB,uBAAuB,oBAAoB,WAAW,GAC9E;GAAE,SAAS;GAAQ,gBAAgB;EAAK,CAC1C;CACJ;AACF;AAEA,IAAM,0BACJ,SACA,gBACqC;CACrC,IAAI,QAAQ,eAAe,MAAM,OAAO,CAAC;CACzC,IAAI,CAAC,YAAY,IAAI,QAAQ,UAAU,GAAG,OAAO,CAAC;CAClD,IAAI,QAAQ,SAAS,eAAe,QAAQ,YAAY,OAAO,CAAC;CAChE,OAAO,2BAA2B,QAAQ,cAAc,QAAQ,QAAQ,IAAI;AAC9E"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TextReadSnapshot } from '@singapore-editor/core/document';
|
|
2
|
+
import type { InlineReplacementSpec } from '@singapore-editor/core/rendering';
|
|
3
|
+
export declare function markdownInlineReplacements(text: TextReadSnapshot, records: Uint32Array): readonly InlineReplacementSpec[];
|
|
4
|
+
//# sourceMappingURL=replacements.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replacements.d.ts","sourceRoot":"","sources":["../src/replacements.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AAK7E,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,WAAW,GACnB,SAAS,qBAAqB,EAAE,CAqBlC"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Kind } from "tree-sitter-md";
|
|
2
|
+
//#region src/replacements.ts
|
|
3
|
+
function markdownInlineReplacements(text, records) {
|
|
4
|
+
const specs = [];
|
|
5
|
+
const links = [];
|
|
6
|
+
const containers = containerSpans(records);
|
|
7
|
+
for (let index = 0; index < records.length; index += 4) {
|
|
8
|
+
const start = records[index], end = records[index + 1];
|
|
9
|
+
const kind = records[index + 2], extra = records[index + 3];
|
|
10
|
+
if (end <= start) continue;
|
|
11
|
+
if (kind === Kind.Link || kind === Kind.Image) links.push({
|
|
12
|
+
start,
|
|
13
|
+
end
|
|
14
|
+
});
|
|
15
|
+
if (kind === Kind.LinkText) {
|
|
16
|
+
while (links.length && (links.at(-1).start > start || links.at(-1).end < end)) links.pop();
|
|
17
|
+
appendLink(specs, text, links.pop(), {
|
|
18
|
+
start,
|
|
19
|
+
end
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const before = specs.length;
|
|
23
|
+
appendMarker(specs, text, {
|
|
24
|
+
start,
|
|
25
|
+
end
|
|
26
|
+
}, kind, extra);
|
|
27
|
+
const owner = markerOwner(containers, {
|
|
28
|
+
start,
|
|
29
|
+
end
|
|
30
|
+
}, kind);
|
|
31
|
+
if (owner) applyRevealRange(specs, before, owner);
|
|
32
|
+
}
|
|
33
|
+
return specs;
|
|
34
|
+
}
|
|
35
|
+
function appendMarker(specs, text, span, kind, extra) {
|
|
36
|
+
const group = `${kind}:${span.start}:${span.end}`;
|
|
37
|
+
if (kind === Kind.Emphasis || kind === Kind.Strong || kind === Kind.Strikethrough || kind === Kind.CodeSpan) {
|
|
38
|
+
const count = delimiterLength(text, span, kind);
|
|
39
|
+
specs.push(replacement(span.start, span.start + count, "", "marker", group));
|
|
40
|
+
specs.push(replacement(span.end - count, span.end, "", "marker", group));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (kind === Kind.HeadingMark) {
|
|
44
|
+
const end = span.end + leadingSpaces(text, span.end);
|
|
45
|
+
specs.push(replacement(span.start, end, "", `heading-marker-${extra}`, group));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (kind === Kind.FenceMark || kind === Kind.CodeInfo) {
|
|
49
|
+
specs.push(replacement(span.start, span.end, "", "fence-marker", group));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (kind === Kind.Task) {
|
|
53
|
+
specs.push(replacement(span.start, span.end, extra ? "☑" : "☐", "task-marker", group));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (kind !== Kind.ListMark && kind !== Kind.QuoteMark) return;
|
|
57
|
+
const source = text.readRange(span.start, span.end);
|
|
58
|
+
if (kind === Kind.ListMark && /^[-*+]/.test(source)) specs.push(replacement(span.start, span.end, `•${source.slice(1)}`, "list-marker", group));
|
|
59
|
+
if (kind === Kind.QuoteMark) specs.push(replacement(span.start, span.end, `│${source.slice(1)}`, "quote-marker", group));
|
|
60
|
+
}
|
|
61
|
+
function delimiterLength(text, span, kind) {
|
|
62
|
+
if (kind === Kind.Strong) return 2;
|
|
63
|
+
if (kind === Kind.Emphasis) return 1;
|
|
64
|
+
if (kind === Kind.Strikethrough) return text.readRange(span.start, span.start + 2) === "~~" ? 2 : 1;
|
|
65
|
+
let count = 0;
|
|
66
|
+
while (span.start + count < span.end && text.readRange(span.start + count, span.start + count + 1) === "`") count++;
|
|
67
|
+
return count;
|
|
68
|
+
}
|
|
69
|
+
function appendLink(specs, text, link, label) {
|
|
70
|
+
if (!link) return;
|
|
71
|
+
const group = `link:${link.start}:${link.end}`;
|
|
72
|
+
appendHiddenLines(specs, text, link.start, label.start, "link-marker", group, link);
|
|
73
|
+
appendHiddenLines(specs, text, label.end, link.end, "link-target", group, link);
|
|
74
|
+
}
|
|
75
|
+
function appendHiddenLines(specs, text, start, end, kind, group, owner) {
|
|
76
|
+
const source = text.readRange(start, end);
|
|
77
|
+
let offset = start;
|
|
78
|
+
for (const line of source.split(/(\r?\n)/)) {
|
|
79
|
+
if (line && !line.includes("\n")) specs.push({
|
|
80
|
+
...replacement(offset, offset + line.length, "", kind, group),
|
|
81
|
+
revealRange: owner
|
|
82
|
+
});
|
|
83
|
+
offset += line.length;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function replacement(startIndex, endIndex, text, kind, groupId) {
|
|
87
|
+
return {
|
|
88
|
+
id: `${kind}:${startIndex}:${endIndex}`,
|
|
89
|
+
startIndex,
|
|
90
|
+
endIndex,
|
|
91
|
+
text,
|
|
92
|
+
kind,
|
|
93
|
+
groupId
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function leadingSpaces(text, offset) {
|
|
97
|
+
let count = 0;
|
|
98
|
+
while (offset + count < text.length && text.readRange(offset + count, offset + count + 1) === " ") count++;
|
|
99
|
+
return count;
|
|
100
|
+
}
|
|
101
|
+
function containerSpans(records) {
|
|
102
|
+
const containers = [];
|
|
103
|
+
for (let i = 0; i < records.length; i += 4) {
|
|
104
|
+
const kind = records[i + 2];
|
|
105
|
+
if (kind === Kind.Heading || kind === Kind.CodeBlock || kind === Kind.BlockQuote || kind === Kind.ListItem) containers.push({
|
|
106
|
+
start: records[i],
|
|
107
|
+
end: records[i + 1],
|
|
108
|
+
kind
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return containers;
|
|
112
|
+
}
|
|
113
|
+
function markerOwner(containers, span, kind) {
|
|
114
|
+
const parent = markerParentKind(kind);
|
|
115
|
+
if (!parent) return void 0;
|
|
116
|
+
return containers.findLast((container) => container.kind === parent && container.start <= span.start && container.end >= span.end);
|
|
117
|
+
}
|
|
118
|
+
function markerParentKind(kind) {
|
|
119
|
+
if (kind === Kind.HeadingMark) return Kind.Heading;
|
|
120
|
+
if (kind === Kind.FenceMark || kind === Kind.CodeInfo) return Kind.CodeBlock;
|
|
121
|
+
if (kind === Kind.QuoteMark) return Kind.BlockQuote;
|
|
122
|
+
if (kind === Kind.ListMark || kind === Kind.Task) return Kind.ListItem;
|
|
123
|
+
}
|
|
124
|
+
function applyRevealRange(specs, from, owner) {
|
|
125
|
+
for (let i = from; i < specs.length; i++) specs[i] = {
|
|
126
|
+
...specs[i],
|
|
127
|
+
revealRange: owner
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { markdownInlineReplacements };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=replacements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replacements.js","names":[],"sources":["../src/replacements.ts"],"sourcesContent":["import type { TextReadSnapshot } from '@singapore-editor/core/document'\nimport type { InlineReplacementSpec } from '@singapore-editor/core/rendering'\nimport { Kind } from 'tree-sitter-md'\n\ntype Span = { readonly start: number; readonly end: number }\n\nexport function markdownInlineReplacements(\n text: TextReadSnapshot,\n records: Uint32Array,\n): readonly InlineReplacementSpec[] {\n const specs: InlineReplacementSpec[] = []\n const links: Span[] = []\n const containers = containerSpans(records)\n for (let index = 0; index < records.length; index += 4) {\n const start = records[index]!,\n end = records[index + 1]!\n const kind = records[index + 2]!,\n extra = records[index + 3]!\n if (end <= start) continue\n if (kind === Kind.Link || kind === Kind.Image) links.push({ start, end })\n if (kind === Kind.LinkText) {\n while (links.length && (links.at(-1)!.start > start || links.at(-1)!.end < end)) links.pop()\n appendLink(specs, text, links.pop(), { start, end })\n }\n const before = specs.length\n appendMarker(specs, text, { start, end }, kind, extra)\n const owner = markerOwner(containers, { start, end }, kind)\n if (owner) applyRevealRange(specs, before, owner)\n }\n return specs\n}\n\nfunction appendMarker(\n specs: InlineReplacementSpec[],\n text: TextReadSnapshot,\n span: Span,\n kind: number,\n extra: number,\n): void {\n const group = `${kind}:${span.start}:${span.end}`\n if (\n kind === Kind.Emphasis ||\n kind === Kind.Strong ||\n kind === Kind.Strikethrough ||\n kind === Kind.CodeSpan\n ) {\n const count = delimiterLength(text, span, kind)\n specs.push(replacement(span.start, span.start + count, '', 'marker', group))\n specs.push(replacement(span.end - count, span.end, '', 'marker', group))\n return\n }\n if (kind === Kind.HeadingMark) {\n const end = span.end + leadingSpaces(text, span.end)\n specs.push(replacement(span.start, end, '', `heading-marker-${extra}`, group))\n return\n }\n if (kind === Kind.FenceMark || kind === Kind.CodeInfo) {\n specs.push(replacement(span.start, span.end, '', 'fence-marker', group))\n return\n }\n if (kind === Kind.Task) {\n specs.push(replacement(span.start, span.end, extra ? '☑' : '☐', 'task-marker', group))\n return\n }\n if (kind !== Kind.ListMark && kind !== Kind.QuoteMark) return\n const source = text.readRange(span.start, span.end)\n if (kind === Kind.ListMark && /^[-*+]/.test(source)) {\n specs.push(replacement(span.start, span.end, `•${source.slice(1)}`, 'list-marker', group))\n }\n if (kind === Kind.QuoteMark) {\n specs.push(replacement(span.start, span.end, `│${source.slice(1)}`, 'quote-marker', group))\n }\n}\n\nfunction delimiterLength(text: TextReadSnapshot, span: Span, kind: number): number {\n if (kind === Kind.Strong) return 2\n if (kind === Kind.Emphasis) return 1\n if (kind === Kind.Strikethrough)\n return text.readRange(span.start, span.start + 2) === '~~' ? 2 : 1\n let count = 0\n while (\n span.start + count < span.end &&\n text.readRange(span.start + count, span.start + count + 1) === '`'\n )\n count++\n return count\n}\n\nfunction appendLink(\n specs: InlineReplacementSpec[],\n text: TextReadSnapshot,\n link: Span | undefined,\n label: Span,\n): void {\n if (!link) return\n const group = `link:${link.start}:${link.end}`\n appendHiddenLines(specs, text, link.start, label.start, 'link-marker', group, link)\n appendHiddenLines(specs, text, label.end, link.end, 'link-target', group, link)\n}\n\nfunction appendHiddenLines(\n specs: InlineReplacementSpec[],\n text: TextReadSnapshot,\n start: number,\n end: number,\n kind: string,\n group: string,\n owner: Span,\n): void {\n const source = text.readRange(start, end)\n let offset = start\n for (const line of source.split(/(\\r?\\n)/)) {\n if (line && !line.includes('\\n'))\n specs.push({\n ...replacement(offset, offset + line.length, '', kind, group),\n revealRange: owner,\n })\n offset += line.length\n }\n}\n\nfunction replacement(\n startIndex: number,\n endIndex: number,\n text: string,\n kind: string,\n groupId: string,\n): InlineReplacementSpec {\n return { id: `${kind}:${startIndex}:${endIndex}`, startIndex, endIndex, text, kind, groupId }\n}\n\nfunction leadingSpaces(text: TextReadSnapshot, offset: number): number {\n let count = 0\n while (offset + count < text.length && text.readRange(offset + count, offset + count + 1) === ' ')\n count++\n return count\n}\n\ntype Container = Span & { readonly kind: number }\n\nfunction containerSpans(records: Uint32Array): readonly Container[] {\n const containers: Container[] = []\n for (let i = 0; i < records.length; i += 4) {\n const kind = records[i + 2]!\n if (\n kind === Kind.Heading ||\n kind === Kind.CodeBlock ||\n kind === Kind.BlockQuote ||\n kind === Kind.ListItem\n ) {\n containers.push({ start: records[i]!, end: records[i + 1]!, kind })\n }\n }\n return containers\n}\n\nfunction markerOwner(containers: readonly Container[], span: Span, kind: number): Span | undefined {\n const parent = markerParentKind(kind)\n if (!parent) return undefined\n return containers.findLast(\n (container) =>\n container.kind === parent && container.start <= span.start && container.end >= span.end,\n )\n}\n\nfunction markerParentKind(kind: number): number | undefined {\n if (kind === Kind.HeadingMark) return Kind.Heading\n if (kind === Kind.FenceMark || kind === Kind.CodeInfo) return Kind.CodeBlock\n if (kind === Kind.QuoteMark) return Kind.BlockQuote\n if (kind === Kind.ListMark || kind === Kind.Task) return Kind.ListItem\n return undefined\n}\n\nfunction applyRevealRange(specs: InlineReplacementSpec[], from: number, owner: Span): void {\n for (let i = from; i < specs.length; i++) {\n specs[i] = { ...specs[i]!, revealRange: owner }\n }\n}\n"],"mappings":"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"}
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Markdown live preview. The buffer still holds markdown source, so these rules only style what the
|
|
3
|
+
* inline transform already decided to show — they never hide anything themselves.
|
|
4
|
+
*
|
|
5
|
+
* The editor mirrors each row's replacement kinds as `editor-inline-<kind>` classes and routes
|
|
6
|
+
* restyled rows through DOM-measured geometry, so a heading row may change font metrics without
|
|
7
|
+
* breaking caret math. Row height stays fixed: sizes below must fit within --editor-row-height.
|
|
8
|
+
*/
|
|
9
|
+
.editor-virtualized {
|
|
10
|
+
--editor-markdown-heading-scale: 1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.editor-virtualized-row[class*='editor-inline-heading-marker-'] {
|
|
14
|
+
font-weight: 700;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.editor-virtualized-row.editor-inline-heading-marker-1 {
|
|
18
|
+
font-size: calc(1em * 1.35 * var(--editor-markdown-heading-scale));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.editor-virtualized-row.editor-inline-heading-marker-2 {
|
|
22
|
+
font-size: calc(1em * 1.2 * var(--editor-markdown-heading-scale));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.editor-virtualized-row.editor-inline-heading-marker-3 {
|
|
26
|
+
font-size: calc(1em * 1.1 * var(--editor-markdown-heading-scale));
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@singapore-editor/markdown",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"README.md",
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"sideEffects": [
|
|
10
|
+
"./dist/style.css"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./style.css": "./dist/style.css"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org/"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bun ../../scripts/build-package.ts",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"typecheck": "tsgo --noEmit",
|
|
28
|
+
"lint": "oxlint .",
|
|
29
|
+
"format": "oxfmt --write .",
|
|
30
|
+
"format:check": "oxfmt --check ."
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@singapore-editor/core": "0.1.2",
|
|
34
|
+
"tree-sitter-md": "0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.6.3",
|
|
38
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
39
|
+
"happy-dom": "^20.14.5",
|
|
40
|
+
"oxfmt": "0.70.0",
|
|
41
|
+
"oxlint": "1.85.0",
|
|
42
|
+
"typescript": "~6.0.3",
|
|
43
|
+
"vitest": "^5.0.2"
|
|
44
|
+
}
|
|
45
|
+
}
|