@caretcms/core 0.1.2 → 0.3.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 +109 -13
- package/dist/contracts.d.ts +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +31 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +146 -30
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +17 -6
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +57 -14
- package/dist/loader.js.map +1 -1
- package/dist/markdown/block-context.d.ts +21 -0
- package/dist/markdown/block-context.d.ts.map +1 -0
- package/dist/markdown/block-context.js +27 -0
- package/dist/markdown/block-context.js.map +1 -0
- package/dist/markdown/canonical-body.d.ts +30 -0
- package/dist/markdown/canonical-body.d.ts.map +1 -0
- package/dist/markdown/canonical-body.js +54 -0
- package/dist/markdown/canonical-body.js.map +1 -0
- package/dist/markdown/contracts.d.ts +66 -0
- package/dist/markdown/contracts.d.ts.map +1 -0
- package/dist/markdown/contracts.js +102 -0
- package/dist/markdown/contracts.js.map +1 -0
- package/dist/markdown/html-to-snodes.d.ts +26 -0
- package/dist/markdown/html-to-snodes.d.ts.map +1 -0
- package/dist/markdown/html-to-snodes.js +118 -0
- package/dist/markdown/html-to-snodes.js.map +1 -0
- package/dist/markdown/remark.d.ts +35 -0
- package/dist/markdown/remark.d.ts.map +1 -0
- package/dist/markdown/remark.js +66 -0
- package/dist/markdown/remark.js.map +1 -0
- package/dist/markdown/satteri.d.ts +19 -0
- package/dist/markdown/satteri.d.ts.map +1 -0
- package/dist/markdown/satteri.js +144 -0
- package/dist/markdown/satteri.js.map +1 -0
- package/dist/markdown/serialize.d.ts +60 -0
- package/dist/markdown/serialize.d.ts.map +1 -0
- package/dist/markdown/serialize.js +264 -0
- package/dist/markdown/serialize.js.map +1 -0
- package/dist/markdown/splice.d.ts +32 -0
- package/dist/markdown/splice.d.ts.map +1 -0
- package/dist/markdown/splice.js +44 -0
- package/dist/markdown/splice.js.map +1 -0
- package/dist/markdown/stamp.d.ts +68 -0
- package/dist/markdown/stamp.d.ts.map +1 -0
- package/dist/markdown/stamp.js +131 -0
- package/dist/markdown/stamp.js.map +1 -0
- package/dist/runtime/auth/rate-limiter.d.ts.map +1 -1
- package/dist/runtime/auth/rate-limiter.js +5 -0
- package/dist/runtime/auth/rate-limiter.js.map +1 -1
- package/dist/runtime/auth/session.d.ts +3 -3
- package/dist/runtime/auth/session.d.ts.map +1 -1
- package/dist/runtime/auth/session.js +68 -21
- package/dist/runtime/auth/session.js.map +1 -1
- package/dist/runtime/config.d.ts +3 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +4 -1
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/content.d.ts.map +1 -1
- package/dist/runtime/content.js +4 -2
- package/dist/runtime/content.js.map +1 -1
- package/dist/runtime/i18n.d.ts +86 -0
- package/dist/runtime/i18n.d.ts.map +1 -0
- package/dist/runtime/i18n.js +168 -0
- package/dist/runtime/i18n.js.map +1 -0
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/middleware.js +61 -6
- package/dist/runtime/middleware.js.map +1 -1
- package/dist/runtime/mutations/contracts.d.ts +28 -1
- package/dist/runtime/mutations/contracts.d.ts.map +1 -1
- package/dist/runtime/mutations/contracts.js +76 -0
- package/dist/runtime/mutations/contracts.js.map +1 -1
- package/dist/runtime/mutations/engine.d.ts +6 -1
- package/dist/runtime/mutations/engine.d.ts.map +1 -1
- package/dist/runtime/mutations/engine.js +271 -51
- package/dist/runtime/mutations/engine.js.map +1 -1
- package/dist/runtime/providers.d.ts +2 -1
- package/dist/runtime/providers.d.ts.map +1 -1
- package/dist/runtime/providers.js +7 -1
- package/dist/runtime/providers.js.map +1 -1
- package/dist/runtime/publish.d.ts +13 -1
- package/dist/runtime/publish.d.ts.map +1 -1
- package/dist/runtime/publish.js +80 -4
- package/dist/runtime/publish.js.map +1 -1
- package/dist/runtime/request-context.d.ts +14 -2
- package/dist/runtime/request-context.d.ts.map +1 -1
- package/dist/runtime/request-context.js +7 -1
- package/dist/runtime/request-context.js.map +1 -1
- package/dist/runtime/rewrite.d.ts.map +1 -1
- package/dist/runtime/rewrite.js +109 -3
- package/dist/runtime/rewrite.js.map +1 -1
- package/dist/runtime/rich-allowlist.d.ts.map +1 -1
- package/dist/runtime/rich-allowlist.js +5 -1
- package/dist/runtime/rich-allowlist.js.map +1 -1
- package/dist/runtime/routes/admin-entry.d.ts.map +1 -1
- package/dist/runtime/routes/admin-entry.js +114 -18
- package/dist/runtime/routes/admin-entry.js.map +1 -1
- package/dist/runtime/routes/admin.d.ts.map +1 -1
- package/dist/runtime/routes/admin.js +33 -15
- package/dist/runtime/routes/admin.js.map +1 -1
- package/dist/runtime/routes/auth-login.d.ts.map +1 -1
- package/dist/runtime/routes/auth-login.js +4 -0
- package/dist/runtime/routes/auth-login.js.map +1 -1
- package/dist/runtime/routes/auth-logout.d.ts.map +1 -1
- package/dist/runtime/routes/auth-logout.js +7 -2
- package/dist/runtime/routes/auth-logout.js.map +1 -1
- package/dist/runtime/routes/auth-session.d.ts.map +1 -1
- package/dist/runtime/routes/auth-session.js +4 -2
- package/dist/runtime/routes/auth-session.js.map +1 -1
- package/dist/runtime/routes/collection-list.d.ts.map +1 -1
- package/dist/runtime/routes/collection-list.js +64 -41
- package/dist/runtime/routes/collection-list.js.map +1 -1
- package/dist/runtime/routes/entries.d.ts.map +1 -1
- package/dist/runtime/routes/entries.js +16 -6
- package/dist/runtime/routes/entries.js.map +1 -1
- package/dist/runtime/routes/history.d.ts.map +1 -1
- package/dist/runtime/routes/history.js +50 -22
- package/dist/runtime/routes/history.js.map +1 -1
- package/dist/runtime/routes/mutate.d.ts.map +1 -1
- package/dist/runtime/routes/mutate.js +40 -6
- package/dist/runtime/routes/mutate.js.map +1 -1
- package/dist/runtime/routes/publish.d.ts.map +1 -1
- package/dist/runtime/routes/publish.js +5 -2
- package/dist/runtime/routes/publish.js.map +1 -1
- package/dist/runtime/routes/schema.d.ts.map +1 -1
- package/dist/runtime/routes/schema.js +11 -3
- package/dist/runtime/routes/schema.js.map +1 -1
- package/dist/runtime/routes/studio-home.d.ts.map +1 -1
- package/dist/runtime/routes/studio-home.js +38 -19
- package/dist/runtime/routes/studio-home.js.map +1 -1
- package/dist/runtime/routes/theme-stylesheet.d.ts.map +1 -1
- package/dist/runtime/routes/theme-stylesheet.js +11 -1
- package/dist/runtime/routes/theme-stylesheet.js.map +1 -1
- package/dist/runtime/schema-registry.d.ts +8 -1
- package/dist/runtime/schema-registry.d.ts.map +1 -1
- package/dist/runtime/schema-registry.js +47 -3
- package/dist/runtime/schema-registry.js.map +1 -1
- package/dist/runtime/storage/atomic-write.d.ts.map +1 -1
- package/dist/runtime/storage/atomic-write.js +8 -3
- package/dist/runtime/storage/atomic-write.js.map +1 -1
- package/dist/runtime/storage/markdown-adapter.d.ts +19 -0
- package/dist/runtime/storage/markdown-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/markdown-adapter.js +41 -0
- package/dist/runtime/storage/markdown-adapter.js.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.d.ts +8 -0
- package/dist/runtime/storage/session-overlay-adapter.d.ts.map +1 -1
- package/dist/runtime/storage/session-overlay-adapter.js +24 -2
- package/dist/runtime/storage/session-overlay-adapter.js.map +1 -1
- package/dist/runtime/themes/preset-tokens.js +4 -4
- package/dist/runtime/themes/preset-tokens.js.map +1 -1
- package/dist/runtime/utils.d.ts +10 -0
- package/dist/runtime/utils.d.ts.map +1 -1
- package/dist/runtime/utils.js +16 -0
- package/dist/runtime/utils.js.map +1 -1
- package/dist/runtime/views/studio-layout.d.ts +3 -0
- package/dist/runtime/views/studio-layout.d.ts.map +1 -1
- package/dist/runtime/views/studio-layout.js +36 -13
- package/dist/runtime/views/studio-layout.js.map +1 -1
- package/dist/schema-utils.d.ts +7 -0
- package/dist/schema-utils.d.ts.map +1 -1
- package/dist/schema-utils.js +143 -0
- package/dist/schema-utils.js.map +1 -1
- package/dist/types.d.ts +83 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/cms/admin-entry.css +135 -13
- package/static/cms/admin-entry.js +509 -86
- package/static/cms/editor/guards.js +1 -1
- package/static/cms/editor/image-edit.js +42 -11
- package/static/cms/editor/image.css +7 -1
- package/static/cms/editor/md-block-edit.js +176 -0
- package/static/cms/editor/md-serialize.js +301 -0
- package/static/cms/editor/panel.css +69 -25
- package/static/cms/editor/panel.js +58 -1
- package/static/cms/editor/rich-toolbar.js +4 -3
- package/static/cms/editor/sanitize.js +2 -1
- package/static/cms/editor/sync.js +277 -56
- package/static/cms/editor/text-edit.js +68 -17
- package/static/cms/editor/text-link-interactions.js +4 -4
- package/static/cms/editor/toast.css +114 -2
- package/static/cms/editor/toast.js +121 -22
- package/static/cms/editor/tokens-and-text.css +73 -3
- package/static/cms/editor/toolbar.css +19 -6
- package/static/cms/editor/toolbar.js +64 -23
- package/static/cms/editor.css +1 -1
- package/static/cms/editor.js +14 -3
- package/static/cms/studio-tokens.css +4 -4
- package/static/cms/studio.css +24 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sätteri mdast plugin that stamps `data-caret-md` bindings (Astro 7).
|
|
3
|
+
*
|
|
4
|
+
* Registered on `markdown.processor.options.mdastPlugins` by the integration
|
|
5
|
+
* when markdown storage is active. Position tracking is opt-in, so the plugin
|
|
6
|
+
* declares `options: { position: true }`. Attributes are written through the
|
|
7
|
+
* per-node `data.hProperties` bag, which Sätteri carries into the rendered
|
|
8
|
+
* HTML like remark-rehype.
|
|
9
|
+
*
|
|
10
|
+
* Type-only imports keep core free of a `satteri` runtime dependency — the
|
|
11
|
+
* package is present in the user's Astro build, never shipped by us.
|
|
12
|
+
*/
|
|
13
|
+
import type { MdastPluginDefinition } from "satteri";
|
|
14
|
+
interface CaretSatteriOptions {
|
|
15
|
+
contentRoot: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function caretSatteriPlugin(options: CaretSatteriOptions): MdastPluginDefinition;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=satteri.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"satteri.d.ts","sourceRoot":"","sources":["../../src/markdown/satteri.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAGrD,UAAU,mBAAmB;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAmHD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,qBAAqB,CAwCtF"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sätteri mdast plugin that stamps `data-caret-md` bindings (Astro 7).
|
|
3
|
+
*
|
|
4
|
+
* Registered on `markdown.processor.options.mdastPlugins` by the integration
|
|
5
|
+
* when markdown storage is active. Position tracking is opt-in, so the plugin
|
|
6
|
+
* declares `options: { position: true }`. Attributes are written through the
|
|
7
|
+
* per-node `data.hProperties` bag, which Sätteri carries into the rendered
|
|
8
|
+
* HTML like remark-rehype.
|
|
9
|
+
*
|
|
10
|
+
* Type-only imports keep core free of a `satteri` runtime dependency — the
|
|
11
|
+
* package is present in the user's Astro build, never shipped by us.
|
|
12
|
+
*/
|
|
13
|
+
import { computeStamp, planParagraph } from "./stamp.js";
|
|
14
|
+
// Older Sätteri releases reported UTF-8 BYTE offsets while newer releases use
|
|
15
|
+
// JS string offsets. Both expose one-based line/column positions in JS string
|
|
16
|
+
// space, so prefer those and retain byte conversion as a compatibility fallback
|
|
17
|
+
// for processors that omit line/column. Memoized because a document's blocks
|
|
18
|
+
// are visited consecutively against the same `ctx.source`.
|
|
19
|
+
let byteMapCache = null;
|
|
20
|
+
let lineMapCache = null;
|
|
21
|
+
function byteToStringIndex(source, byteOffset) {
|
|
22
|
+
if (!byteMapCache || byteMapCache.source !== source) {
|
|
23
|
+
const prefix = new Uint32Array(source.length + 1);
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
for (let i = 0; i < source.length;) {
|
|
26
|
+
const cp = source.codePointAt(i);
|
|
27
|
+
const units = cp > 0xffff ? 2 : 1;
|
|
28
|
+
prefix[i] = bytes;
|
|
29
|
+
if (units === 2)
|
|
30
|
+
prefix[i + 1] = bytes; // low surrogate: mid-char
|
|
31
|
+
bytes += cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
|
|
32
|
+
i += units;
|
|
33
|
+
}
|
|
34
|
+
prefix[source.length] = bytes;
|
|
35
|
+
byteMapCache = { source, prefix };
|
|
36
|
+
}
|
|
37
|
+
const { prefix } = byteMapCache;
|
|
38
|
+
// Smallest string index whose byte-prefix reaches byteOffset.
|
|
39
|
+
let lo = 0;
|
|
40
|
+
let hi = source.length;
|
|
41
|
+
while (lo < hi) {
|
|
42
|
+
const mid = (lo + hi) >> 1;
|
|
43
|
+
if (prefix[mid] < byteOffset)
|
|
44
|
+
lo = mid + 1;
|
|
45
|
+
else
|
|
46
|
+
hi = mid;
|
|
47
|
+
}
|
|
48
|
+
return lo;
|
|
49
|
+
}
|
|
50
|
+
function positionToStringIndex(source, position) {
|
|
51
|
+
if (!position)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (Number.isInteger(position.line)
|
|
54
|
+
&& Number.isInteger(position.column)
|
|
55
|
+
&& position.line >= 1
|
|
56
|
+
&& position.column >= 1) {
|
|
57
|
+
if (!lineMapCache || lineMapCache.source !== source) {
|
|
58
|
+
const starts = [0];
|
|
59
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
60
|
+
if (source.charCodeAt(i) === 10)
|
|
61
|
+
starts.push(i + 1);
|
|
62
|
+
}
|
|
63
|
+
lineMapCache = { source, starts };
|
|
64
|
+
}
|
|
65
|
+
const lineStart = lineMapCache.starts[position.line - 1];
|
|
66
|
+
if (lineStart !== undefined) {
|
|
67
|
+
const index = lineStart + position.column - 1;
|
|
68
|
+
if (index >= lineStart && index <= source.length)
|
|
69
|
+
return index;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return position.offset == null ? undefined : byteToStringIndex(source, position.offset);
|
|
73
|
+
}
|
|
74
|
+
/** Root-to-node child indexes (`indexOf` returns `undefined` at the root). */
|
|
75
|
+
function pathOf(node, ctx) {
|
|
76
|
+
const idx = [];
|
|
77
|
+
let cur = node;
|
|
78
|
+
while (cur) {
|
|
79
|
+
const i = ctx.indexOf(cur);
|
|
80
|
+
if (i === undefined)
|
|
81
|
+
break;
|
|
82
|
+
idx.unshift(i);
|
|
83
|
+
cur = ctx.parent(cur);
|
|
84
|
+
}
|
|
85
|
+
return idx;
|
|
86
|
+
}
|
|
87
|
+
/** Ancestor node types, nearest-first, excluding the node itself. */
|
|
88
|
+
function ancestorTypes(node, ctx) {
|
|
89
|
+
const out = [];
|
|
90
|
+
let cur = ctx.parent(node);
|
|
91
|
+
while (cur) {
|
|
92
|
+
out.push(cur.type);
|
|
93
|
+
cur = ctx.parent(cur);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function stamp(target, ctx, input) {
|
|
98
|
+
const attrs = computeStamp(input);
|
|
99
|
+
if (!attrs)
|
|
100
|
+
return;
|
|
101
|
+
const data = { ...(target.data ?? {}) };
|
|
102
|
+
data.hProperties = { ...data.hProperties, ...attrs };
|
|
103
|
+
ctx.setProperty(target, "data", data);
|
|
104
|
+
}
|
|
105
|
+
export function caretSatteriPlugin(options) {
|
|
106
|
+
const { contentRoot } = options;
|
|
107
|
+
const base = (node, ctx) => {
|
|
108
|
+
return {
|
|
109
|
+
source: ctx.source,
|
|
110
|
+
fileURL: ctx.fileURL,
|
|
111
|
+
contentRoot,
|
|
112
|
+
start: positionToStringIndex(ctx.source, node.position?.start),
|
|
113
|
+
end: positionToStringIndex(ctx.source, node.position?.end),
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
name: "caretcms-stamp",
|
|
118
|
+
options: { position: true },
|
|
119
|
+
heading(node, ctx) {
|
|
120
|
+
const parentType = ctx.parent(node)?.type ?? "root";
|
|
121
|
+
stamp(node, ctx, {
|
|
122
|
+
...base(node, ctx),
|
|
123
|
+
blockPath: pathOf(node, ctx),
|
|
124
|
+
ancestorTypes: ancestorTypes(node, ctx),
|
|
125
|
+
nested: parentType !== "root",
|
|
126
|
+
blockType: "heading",
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
paragraph(node, ctx) {
|
|
130
|
+
const parent = ctx.parent(node);
|
|
131
|
+
const plan = planParagraph(parent?.type ?? "root", ctx.indexOf(node));
|
|
132
|
+
if (plan.action === "skip")
|
|
133
|
+
return;
|
|
134
|
+
const target = plan.action === "parent" ? parent : node;
|
|
135
|
+
stamp(target, ctx, {
|
|
136
|
+
...base(node, ctx), // the paragraph's (marker-free) offsets
|
|
137
|
+
blockPath: pathOf(target, ctx),
|
|
138
|
+
ancestorTypes: ancestorTypes(target, ctx),
|
|
139
|
+
nested: plan.action === "parent" ? true : plan.nested,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=satteri.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"satteri.js","sourceRoot":"","sources":["../../src/markdown/satteri.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM-free markdown block serializer.
|
|
3
|
+
*
|
|
4
|
+
* Turns the inline content of ONE editable block into markdown source. The
|
|
5
|
+
* closed set is deliberately tiny — text, `strong`/`b`, `em`/`i`, `a[href]`,
|
|
6
|
+
* `code`, `br` — so serialization is a small, total, reversible function
|
|
7
|
+
* rather than a general HTML→markdown converter. Anything outside the set is a
|
|
8
|
+
* hard error: the browser sanitizes to this set before serializing, so a throw
|
|
9
|
+
* means a bug or an attack, never normal input.
|
|
10
|
+
*
|
|
11
|
+
* Pure and DOM-free by design: it operates on a minimal {@link SNode} tree so
|
|
12
|
+
* it can be property-tested in Node without a DOM, and the browser editor
|
|
13
|
+
* (`static/cms/editor/md-serialize.js`) supplies a matching tree from real DOM
|
|
14
|
+
* nodes. A parity test holds the two implementations to identical output.
|
|
15
|
+
*
|
|
16
|
+
* Known limitations (CommonMark/GFM constraints, not corruption — text is
|
|
17
|
+
* always preserved; only emphasis boundaries or auto-linking may shift):
|
|
18
|
+
* - Abutting emphasis whose first span ends in punctuation and is followed by
|
|
19
|
+
* a word char: asterisks fuse, underscore can't close (see `emphasisMarker`).
|
|
20
|
+
* - Emphasis that starts with punctuation-leading content (e.g. a link) while
|
|
21
|
+
* immediately preceded by a word char: the opening `*`/`_` is not
|
|
22
|
+
* left-flanking, so the emphasis can't open. Unrepresentable in CommonMark.
|
|
23
|
+
* - A bare email/URL in plain text (`a@b.com`, `www.x.com`) is auto-linked by
|
|
24
|
+
* GFM on render; a second edit then serializes it as an explicit link.
|
|
25
|
+
* - Links normalize to untitled inline form: editing a block rewrites
|
|
26
|
+
* reference-style links (`[x][1]`) as inline links and drops link titles
|
|
27
|
+
* (`"…"`) — text and destination are preserved, the definition lines
|
|
28
|
+
* elsewhere in the document are untouched. Title support needs `title` in
|
|
29
|
+
* the shared rich allowlist (v0.4 candidate).
|
|
30
|
+
* The editor doesn't produce these from normal selection-based formatting, so
|
|
31
|
+
* they are documented rather than special-cased.
|
|
32
|
+
*/
|
|
33
|
+
/** A minimal, DOM-independent representation of inline content. */
|
|
34
|
+
export type SNode = {
|
|
35
|
+
type: "text";
|
|
36
|
+
value: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: "element";
|
|
39
|
+
tag: string;
|
|
40
|
+
attrs: Record<string, string>;
|
|
41
|
+
children: SNode[];
|
|
42
|
+
};
|
|
43
|
+
/** Which block wraps the inline content — determines markers and break rules. */
|
|
44
|
+
export type BlockContext = {
|
|
45
|
+
block: "heading";
|
|
46
|
+
level: number;
|
|
47
|
+
} | {
|
|
48
|
+
block: "paragraph";
|
|
49
|
+
nested: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** Thrown when the tree contains anything outside the closed inline set. */
|
|
52
|
+
export declare class SerializeError extends Error {
|
|
53
|
+
constructor(message: string);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Serialize the inline children of one block to markdown source, ready to
|
|
57
|
+
* splice into the block's source range.
|
|
58
|
+
*/
|
|
59
|
+
export declare function serializeBlock(children: readonly SNode[], ctx: BlockContext): string;
|
|
60
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/markdown/serialize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,mEAAmE;AACnE,MAAM,MAAM,KAAK,GACb;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC;AAEvF,iFAAiF;AACjF,MAAM,MAAM,YAAY,GACpB;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAE5C,4EAA4E;AAC5E,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,OAAO,EAAE,MAAM;CAI5B;AA6ND;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,KAAK,EAAE,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAepF"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM-free markdown block serializer.
|
|
3
|
+
*
|
|
4
|
+
* Turns the inline content of ONE editable block into markdown source. The
|
|
5
|
+
* closed set is deliberately tiny — text, `strong`/`b`, `em`/`i`, `a[href]`,
|
|
6
|
+
* `code`, `br` — so serialization is a small, total, reversible function
|
|
7
|
+
* rather than a general HTML→markdown converter. Anything outside the set is a
|
|
8
|
+
* hard error: the browser sanitizes to this set before serializing, so a throw
|
|
9
|
+
* means a bug or an attack, never normal input.
|
|
10
|
+
*
|
|
11
|
+
* Pure and DOM-free by design: it operates on a minimal {@link SNode} tree so
|
|
12
|
+
* it can be property-tested in Node without a DOM, and the browser editor
|
|
13
|
+
* (`static/cms/editor/md-serialize.js`) supplies a matching tree from real DOM
|
|
14
|
+
* nodes. A parity test holds the two implementations to identical output.
|
|
15
|
+
*
|
|
16
|
+
* Known limitations (CommonMark/GFM constraints, not corruption — text is
|
|
17
|
+
* always preserved; only emphasis boundaries or auto-linking may shift):
|
|
18
|
+
* - Abutting emphasis whose first span ends in punctuation and is followed by
|
|
19
|
+
* a word char: asterisks fuse, underscore can't close (see `emphasisMarker`).
|
|
20
|
+
* - Emphasis that starts with punctuation-leading content (e.g. a link) while
|
|
21
|
+
* immediately preceded by a word char: the opening `*`/`_` is not
|
|
22
|
+
* left-flanking, so the emphasis can't open. Unrepresentable in CommonMark.
|
|
23
|
+
* - A bare email/URL in plain text (`a@b.com`, `www.x.com`) is auto-linked by
|
|
24
|
+
* GFM on render; a second edit then serializes it as an explicit link.
|
|
25
|
+
* - Links normalize to untitled inline form: editing a block rewrites
|
|
26
|
+
* reference-style links (`[x][1]`) as inline links and drops link titles
|
|
27
|
+
* (`"…"`) — text and destination are preserved, the definition lines
|
|
28
|
+
* elsewhere in the document are untouched. Title support needs `title` in
|
|
29
|
+
* the shared rich allowlist (v0.4 candidate).
|
|
30
|
+
* The editor doesn't produce these from normal selection-based formatting, so
|
|
31
|
+
* they are documented rather than special-cased.
|
|
32
|
+
*/
|
|
33
|
+
/** Thrown when the tree contains anything outside the closed inline set. */
|
|
34
|
+
export class SerializeError extends Error {
|
|
35
|
+
constructor(message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "SerializeError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** ASCII punctuation that CommonMark/GFM lets a backslash escape; we escape the
|
|
41
|
+
* subset that can change inline parsing (`~` is GFM strikethrough). */
|
|
42
|
+
const INLINE_ESCAPE_RE = /[\\`*_[\]<&>~]/g;
|
|
43
|
+
function escapeText(value) {
|
|
44
|
+
// Collapse ASCII whitespace runs to a single space FIRST — this is exactly
|
|
45
|
+
// what HTML rendering does visually, and it makes `<br>` the serializer's
|
|
46
|
+
// only newline source. Without it, a raw `\n` in a text node emits a soft
|
|
47
|
+
// line break whose next line can open a block, and 4+ leading spaces form an
|
|
48
|
+
// indented code block when spliced. ` ` (nbsp) is deliberately NOT
|
|
49
|
+
// collapsed — HTML preserves it, so we do too.
|
|
50
|
+
return value.replace(/[ \t\r\n]+/g, " ").replace(INLINE_ESCAPE_RE, (c) => `\\${c}`);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Escape a leading block-marker so spliced paragraph text can't open a new
|
|
54
|
+
* block. Markers act only at line start, and a bullet / ordered marker triggers
|
|
55
|
+
* even with no following text (`+` alone is an empty list item), so the guard
|
|
56
|
+
* covers end-of-line too. Ordered markers escape the punctuation, not the digit
|
|
57
|
+
* (`\1` is not a valid escape; `1\.` is).
|
|
58
|
+
*/
|
|
59
|
+
function escapeLineStart(md) {
|
|
60
|
+
const ws = /^\s*/.exec(md)[0];
|
|
61
|
+
const rest = md.slice(ws.length);
|
|
62
|
+
const ordered = /^(\d{1,9})([.)])(\s|$)/.exec(rest);
|
|
63
|
+
if (ordered) {
|
|
64
|
+
return `${ws}${ordered[1]}\\${ordered[2]}${rest.slice(ordered[1].length + 1)}`;
|
|
65
|
+
}
|
|
66
|
+
// Dash/equals runs cover three block constructs at once: setext underlines
|
|
67
|
+
// are ONE-or-more `-`/`=` (a lone `=` after a hard break turns the paragraph
|
|
68
|
+
// into an <h1>), and thematic breaks allow interior spaces (`-- -`). `*`/`_`
|
|
69
|
+
// variants are already neutralized by escapeText.
|
|
70
|
+
const marker = /^(?:#{1,6}(?=\s|$)|>|[-+*](?=\s|$)|-(?:[ \t]*-)*[ \t]*$|=(?:[ \t]*=)*[ \t]*$|~{3,}|`{3,}|\|)/.test(rest);
|
|
71
|
+
return marker ? `${ws}\\${rest}` : md;
|
|
72
|
+
}
|
|
73
|
+
function tagOf(node) {
|
|
74
|
+
const t = node.tag.toLowerCase();
|
|
75
|
+
if (t === "b")
|
|
76
|
+
return "strong";
|
|
77
|
+
if (t === "i")
|
|
78
|
+
return "em";
|
|
79
|
+
return t;
|
|
80
|
+
}
|
|
81
|
+
/** Concatenated text of a subtree (for code spans, whose content is literal). */
|
|
82
|
+
function rawText(nodes) {
|
|
83
|
+
let out = "";
|
|
84
|
+
for (const n of nodes) {
|
|
85
|
+
if (n.type === "text")
|
|
86
|
+
out += n.value;
|
|
87
|
+
else if (n.tag.toLowerCase() === "br")
|
|
88
|
+
out += " ";
|
|
89
|
+
else
|
|
90
|
+
out += rawText(n.children);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/** Render a code span with a backtick fence long enough to hold the content. */
|
|
95
|
+
function codeSpan(content) {
|
|
96
|
+
if (content === "")
|
|
97
|
+
return "``";
|
|
98
|
+
let runs = content.match(/`+/g) ?? [];
|
|
99
|
+
let fenceLen = 1;
|
|
100
|
+
while (runs.some((r) => r.length === fenceLen))
|
|
101
|
+
fenceLen++;
|
|
102
|
+
const fence = "`".repeat(fenceLen);
|
|
103
|
+
// Pad when the content touches a backtick edge or is all spaces (CommonMark rule).
|
|
104
|
+
const needsPad = content.startsWith("`") ||
|
|
105
|
+
content.endsWith("`") ||
|
|
106
|
+
(content.startsWith(" ") && content.endsWith(" ") && content.trim() !== "");
|
|
107
|
+
return needsPad ? `${fence} ${content} ${fence}` : `${fence}${content}${fence}`;
|
|
108
|
+
}
|
|
109
|
+
/** Represent a link destination, using the `<...>` form when it isn't bare-safe. */
|
|
110
|
+
function linkDestination(href) {
|
|
111
|
+
// Control characters (incl. \n, \r) have no representable escape in either
|
|
112
|
+
// destination form — a newline inside `<...>` terminates the destination and
|
|
113
|
+
// injects raw markdown structure. Hard-fail, per this file's philosophy.
|
|
114
|
+
// eslint-disable-next-line no-control-regex
|
|
115
|
+
if (/[\u0000-\u001f\u007f]/u.test(href)) {
|
|
116
|
+
throw new SerializeError("control character in link destination");
|
|
117
|
+
}
|
|
118
|
+
if (/^[^\s<>()\\]*$/.test(href))
|
|
119
|
+
return href;
|
|
120
|
+
return `<${href.replace(/([<>\\])/g, "\\$1")}>`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Coalesce adjacent siblings that would otherwise emit colliding markdown:
|
|
124
|
+
* consecutive text nodes merge, and consecutive elements of the same
|
|
125
|
+
* (normalized) tag and attributes fuse their children. Without this,
|
|
126
|
+
* `<strong>a</strong><strong>b</strong>` serializes to `**a****b**`, whose
|
|
127
|
+
* `****` run CommonMark parses ambiguously. Merging renders identically and
|
|
128
|
+
* round-trips cleanly.
|
|
129
|
+
*/
|
|
130
|
+
function mergeAdjacent(nodes) {
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const node of nodes) {
|
|
133
|
+
const prev = out[out.length - 1];
|
|
134
|
+
if (node.type === "text" && prev?.type === "text") {
|
|
135
|
+
prev.value += node.value;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (node.type === "element" &&
|
|
139
|
+
prev?.type === "element" &&
|
|
140
|
+
tagOf(prev) === tagOf(node) &&
|
|
141
|
+
tagOf(node) !== "br" &&
|
|
142
|
+
sameAttrs(prev.attrs, node.attrs)) {
|
|
143
|
+
prev.children = [...prev.children, ...node.children];
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
out.push(node.type === "text" ? { ...node } : { ...node, children: [...node.children] });
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function sameAttrs(a, b) {
|
|
151
|
+
const ka = Object.keys(a);
|
|
152
|
+
const kb = Object.keys(b);
|
|
153
|
+
return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
|
|
154
|
+
}
|
|
155
|
+
/** First raw character of the next sibling, or "" — used to keep an underscore
|
|
156
|
+
* emphasis from abutting a word character (CommonMark's intraword rule). */
|
|
157
|
+
function nextLeadChar(nodes, i) {
|
|
158
|
+
const next = nodes[i + 1];
|
|
159
|
+
if (!next)
|
|
160
|
+
return "";
|
|
161
|
+
if (next.type === "text")
|
|
162
|
+
return next.value[0] ?? "";
|
|
163
|
+
return "<"; // an element emits a delimiter/bracket — never a word char
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Emphasis delimiter for the element at `i`. Consecutive `em`/`strong` siblings
|
|
167
|
+
* abut with no text between, so their `*` delimiters would fuse into an
|
|
168
|
+
* ambiguous run (CommonMark's rule of three). We alternate `*`↔`_` across such
|
|
169
|
+
* a run so neighbours never share a delimiter character. Underscore can't close
|
|
170
|
+
* against a following word character, so we fall back to `*` there (safe for
|
|
171
|
+
* every realistic case; see the serializer notes for the one excluded shape).
|
|
172
|
+
*/
|
|
173
|
+
function emphasisMarker(nodes, i, prevMarker) {
|
|
174
|
+
if (prevMarker === null)
|
|
175
|
+
return "*";
|
|
176
|
+
const flipped = prevMarker === "*" ? "_" : "*";
|
|
177
|
+
if (flipped === "_" && /[A-Za-z0-9_]/.test(nextLeadChar(nodes, i)))
|
|
178
|
+
return "*";
|
|
179
|
+
return flipped;
|
|
180
|
+
}
|
|
181
|
+
function serializeChildren(rawNodes, opts) {
|
|
182
|
+
const nodes = mergeAdjacent(rawNodes);
|
|
183
|
+
let out = "";
|
|
184
|
+
let prevMarker = null;
|
|
185
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
186
|
+
const node = nodes[i];
|
|
187
|
+
if (node.type === "text") {
|
|
188
|
+
out += escapeText(node.value);
|
|
189
|
+
prevMarker = null;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const tag = tagOf(node);
|
|
193
|
+
switch (tag) {
|
|
194
|
+
case "strong": {
|
|
195
|
+
const inner = serializeChildren(node.children, opts);
|
|
196
|
+
if (inner === "") {
|
|
197
|
+
prevMarker = null;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
const m = emphasisMarker(nodes, i, prevMarker);
|
|
201
|
+
out += `${m}${m}${inner}${m}${m}`;
|
|
202
|
+
prevMarker = m;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case "em": {
|
|
206
|
+
const inner = serializeChildren(node.children, opts);
|
|
207
|
+
if (inner === "") {
|
|
208
|
+
prevMarker = null;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
const m = emphasisMarker(nodes, i, prevMarker);
|
|
212
|
+
out += `${m}${inner}${m}`;
|
|
213
|
+
prevMarker = m;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case "code": {
|
|
217
|
+
prevMarker = null;
|
|
218
|
+
out += codeSpan(rawText(node.children));
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case "a": {
|
|
222
|
+
const href = node.attrs.href;
|
|
223
|
+
if (href === undefined)
|
|
224
|
+
throw new SerializeError("<a> without href");
|
|
225
|
+
prevMarker = null;
|
|
226
|
+
const inner = serializeChildren(node.children, opts);
|
|
227
|
+
out += `[${inner}](${linkDestination(href)})`;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case "br": {
|
|
231
|
+
if (!opts.allowBreak) {
|
|
232
|
+
throw new SerializeError("hard break not allowed in a nested block");
|
|
233
|
+
}
|
|
234
|
+
prevMarker = null;
|
|
235
|
+
out += "\\\n";
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
default:
|
|
239
|
+
throw new SerializeError(`unsupported inline element <${node.tag}>`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Serialize the inline children of one block to markdown source, ready to
|
|
246
|
+
* splice into the block's source range.
|
|
247
|
+
*/
|
|
248
|
+
export function serializeBlock(children, ctx) {
|
|
249
|
+
if (ctx.block === "heading") {
|
|
250
|
+
if (!Number.isInteger(ctx.level) || ctx.level < 1 || ctx.level > 6) {
|
|
251
|
+
throw new SerializeError(`invalid heading level: ${ctx.level}`);
|
|
252
|
+
}
|
|
253
|
+
const inline = serializeChildren(children, { allowBreak: false });
|
|
254
|
+
return `${"#".repeat(ctx.level)} ${inline}`;
|
|
255
|
+
}
|
|
256
|
+
const inline = serializeChildren(children, { allowBreak: !ctx.nested });
|
|
257
|
+
// Every LINE of a paragraph must be prevented from opening another block —
|
|
258
|
+
// CommonMark's block scanner runs before inline parsing, so a line after a
|
|
259
|
+
// hard break that starts with `#`/`>`/`- `/etc. would interrupt the
|
|
260
|
+
// paragraph. `escapeText` collapses raw newlines, so the hard-break sequence
|
|
261
|
+
// is the only line boundary the serializer can emit.
|
|
262
|
+
return inline.split("\\\n").map(escapeLineStart).join("\\\n");
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=serialize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../../src/markdown/serialize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish-time body splice: apply drafted block edits to a markdown source
|
|
3
|
+
* file. This is the ONLY code that rewrites user prose in place, so it is
|
|
4
|
+
* all-or-nothing by construction — every block's range is re-verified against
|
|
5
|
+
* the CURRENT file (hash + bounds + overlap) before a single byte moves, and
|
|
6
|
+
* any failure leaves the file untouched.
|
|
7
|
+
*
|
|
8
|
+
* Pure (string -> string) so it can be property-tested without a filesystem;
|
|
9
|
+
* `MarkdownAdapter.spliceBodyBlocks` wraps it with read + atomic write.
|
|
10
|
+
*/
|
|
11
|
+
import { type MdSrc } from "./contracts.js";
|
|
12
|
+
export interface BodySplice {
|
|
13
|
+
/** Server-derived markdown to put in the block's place. */
|
|
14
|
+
md: string;
|
|
15
|
+
/** The verified source range the draft was made against. */
|
|
16
|
+
src: MdSrc;
|
|
17
|
+
}
|
|
18
|
+
export type SpliceResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
content: string;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
reason: "stale" | "overlap";
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Splice `blocks` into `fileSource`. Returns the new file content, or a
|
|
27
|
+
* failure (`stale` = a range no longer hashes, the file changed since the
|
|
28
|
+
* draft; `overlap` = two drafts claim intersecting ranges, which can only
|
|
29
|
+
* happen through a corrupted draft store — never write through it).
|
|
30
|
+
*/
|
|
31
|
+
export declare function spliceBodyBlocks(fileSource: string, blocks: readonly BodySplice[]): SpliceResult;
|
|
32
|
+
//# sourceMappingURL=splice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splice.d.ts","sourceRoot":"","sources":["../../src/markdown/splice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAW,KAAK,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,GAAG,EAAE,KAAK,CAAC;CACZ;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;CAAE,CAAC;AAE/C;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,UAAU,EAAE,GAC5B,YAAY,CA0Bd"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish-time body splice: apply drafted block edits to a markdown source
|
|
3
|
+
* file. This is the ONLY code that rewrites user prose in place, so it is
|
|
4
|
+
* all-or-nothing by construction — every block's range is re-verified against
|
|
5
|
+
* the CURRENT file (hash + bounds + overlap) before a single byte moves, and
|
|
6
|
+
* any failure leaves the file untouched.
|
|
7
|
+
*
|
|
8
|
+
* Pure (string -> string) so it can be property-tested without a filesystem;
|
|
9
|
+
* `MarkdownAdapter.spliceBodyBlocks` wraps it with read + atomic write.
|
|
10
|
+
*/
|
|
11
|
+
import { canonicalBody } from "./canonical-body.js";
|
|
12
|
+
import { fnv1a32 } from "./contracts.js";
|
|
13
|
+
/**
|
|
14
|
+
* Splice `blocks` into `fileSource`. Returns the new file content, or a
|
|
15
|
+
* failure (`stale` = a range no longer hashes, the file changed since the
|
|
16
|
+
* draft; `overlap` = two drafts claim intersecting ranges, which can only
|
|
17
|
+
* happen through a corrupted draft store — never write through it).
|
|
18
|
+
*/
|
|
19
|
+
export function spliceBodyBlocks(fileSource, blocks) {
|
|
20
|
+
const { body, fileOffsetOf } = canonicalBody(fileSource);
|
|
21
|
+
for (const { src } of blocks) {
|
|
22
|
+
if (src.end > body.length)
|
|
23
|
+
return { ok: false, reason: "stale" };
|
|
24
|
+
if (fnv1a32(body.slice(src.start, src.end)) !== src.hash) {
|
|
25
|
+
return { ok: false, reason: "stale" };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const ordered = [...blocks].sort((a, b) => b.src.start - a.src.start);
|
|
29
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
30
|
+
// Descending by start: the later block in the file comes first. Overlap
|
|
31
|
+
// exists when the previous (later-in-file) block starts before this one ends.
|
|
32
|
+
if (ordered[i - 1].src.start < ordered[i].src.end) {
|
|
33
|
+
return { ok: false, reason: "overlap" };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let content = fileSource;
|
|
37
|
+
for (const { md, src } of ordered) {
|
|
38
|
+
const start = fileOffsetOf(src.start);
|
|
39
|
+
const end = fileOffsetOf(src.end);
|
|
40
|
+
content = content.slice(0, start) + md + content.slice(end);
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, content };
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=splice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"splice.js","sourceRoot":"","sources":["../../src/markdown/splice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAc,MAAM,gBAAgB,CAAC;AAarD;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAkB,EAClB,MAA6B;IAE7B,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;IAEzD,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,MAAM,EAAE,CAAC;QAC7B,IAAI,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QACjE,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,EAAE,CAAC;YACzD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QACxC,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACtE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,wEAAwE;QACxE,8EAA8E;QAC9E,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;YAClD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,IAAI,OAAO,GAAG,UAAU,CAAC;IACzB,KAAK,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,OAAO,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC"}
|