@frontlit/text-editor 0.1.0-alpha.1 → 0.1.0-alpha.3

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/dist/docs.js ADDED
@@ -0,0 +1,905 @@
1
+ import { getSchema, Node } from '@tiptap/core';
2
+ import StarterKit from '@tiptap/starter-kit';
3
+ import Image from '@tiptap/extension-image';
4
+ import Link from '@tiptap/extension-link';
5
+ import { TableRow, Table, TableCell, TableHeader } from '@tiptap/extension-table';
6
+ import { parseDocument, stringify } from 'yaml';
7
+ import { unified } from 'unified';
8
+ import remarkParse from 'remark-parse';
9
+ import remarkGfm from 'remark-gfm';
10
+ import remarkStringify from 'remark-stringify';
11
+
12
+ // src/docs/diagnostics.ts
13
+ function diagnostic(code, message, location = {}) {
14
+ return {
15
+ code,
16
+ message,
17
+ severity: "error",
18
+ blocksSave: true,
19
+ blocksPublish: true,
20
+ ...location
21
+ };
22
+ }
23
+ function sortDiagnostics(items) {
24
+ return items.sort(
25
+ (a, b) => (a.file ?? a.documentId ?? "").localeCompare(b.file ?? b.documentId ?? "") || (a.line ?? 0) - (b.line ?? 0) || (a.column ?? 0) - (b.column ?? 0) || (a.fieldPath ?? "").localeCompare(b.fieldPath ?? "") || a.code.localeCompare(b.code)
26
+ );
27
+ }
28
+ var DocsContentError = class extends Error {
29
+ constructor(diagnostics) {
30
+ super("Documentation content is invalid.");
31
+ this.name = "DocsContentError";
32
+ this.diagnostics = sortDiagnostics(diagnostics);
33
+ }
34
+ };
35
+
36
+ // src/docs/urls.ts
37
+ function isSafeDocsUrl(value, image = false) {
38
+ if (typeof value !== "string" || !value || value !== value.trim()) return false;
39
+ if (
40
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Browsers ignore controls in dangerous protocols.
41
+ /[\u0000-\u0020\u007f\\]/.test(value) || /%(?:0[0-9a-f]|1[0-9a-f]|7f|5c)/i.test(value)
42
+ )
43
+ return false;
44
+ if (value.startsWith("//")) return false;
45
+ const privatePath = /\/(?:attachments|private|ticket-attachments|conversation-attachments)(?:\/|$)/i;
46
+ const signedKey = /^(?:x-amz-.+|x-goog-.+|signature|sig|token|access_token|expires)$/i;
47
+ if (/^(?:https?:|mailto:)/i.test(value)) {
48
+ try {
49
+ const url = new URL(value);
50
+ if (image && url.protocol !== "https:") return false;
51
+ if (url.username || url.password) return false;
52
+ if (url.protocol === "mailto:") return !image && Boolean(url.pathname);
53
+ if (privatePath.test(decodeURIComponent(url.pathname))) return false;
54
+ for (const key of url.searchParams.keys()) {
55
+ if (signedKey.test(key)) return false;
56
+ }
57
+ return Boolean(url.hostname);
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ if (image) return false;
63
+ if (/^[^/?#]*:/.test(value) || /%(?:2f|3a)/i.test(value)) return false;
64
+ try {
65
+ const url = new URL(value, "https://docs.invalid/");
66
+ return !privatePath.test(decodeURIComponent(url.pathname)) && ![...url.searchParams.keys()].some((key) => signedKey.test(key));
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ // src/docs/extensions.ts
73
+ function createDocsExtensions() {
74
+ return [
75
+ StarterKit.configure({
76
+ document: false,
77
+ underline: false,
78
+ link: false,
79
+ dropcursor: false,
80
+ gapcursor: false,
81
+ trailingNode: false
82
+ }),
83
+ Node.create({ name: "doc", topNode: true, content: "block*" }),
84
+ Link.extend({
85
+ addAttributes() {
86
+ return { ...this.parent?.(), title: { default: null } };
87
+ }
88
+ }).configure({ openOnClick: false, isAllowedUri: (url) => isSafeDocsUrl(url) }),
89
+ Image.configure({ allowBase64: false, resize: false }),
90
+ Table.configure({ resizable: false }),
91
+ TableRow,
92
+ TableCell.extend({ content: "paragraph" }),
93
+ TableHeader.extend({ content: "paragraph" })
94
+ ];
95
+ }
96
+ var docsSchema = getSchema(createDocsExtensions());
97
+ var DOCS_SCHEMA_VERSION = 1;
98
+
99
+ // src/docs/headings.ts
100
+ var DOCS_HEADING_POLICY_VERSION = 1;
101
+ function docsNodeText(node) {
102
+ if (typeof node.text === "string") return node.text;
103
+ if (node.type === "hardBreak") return "\n";
104
+ return (node.content ?? []).map(docsNodeText).join("");
105
+ }
106
+ function createHeadingIdGenerator() {
107
+ const used = /* @__PURE__ */ new Set();
108
+ return (text) => {
109
+ const base = text.normalize("NFKC").toLowerCase().trim().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s+/g, "-") || "section";
110
+ let id = base;
111
+ let suffix = 2;
112
+ while (used.has(id)) id = `${base}-${suffix++}`;
113
+ used.add(id);
114
+ return id;
115
+ };
116
+ }
117
+ function extractDocsHeadings(json) {
118
+ const nextId = createHeadingIdGenerator();
119
+ const headings = [];
120
+ function walk(node) {
121
+ if (node.type === "heading") {
122
+ const text = docsNodeText(node);
123
+ headings.push({ level: Number(node.attrs?.level ?? 1), text, id: nextId(text) });
124
+ }
125
+ for (const child of node.content ?? []) walk(child);
126
+ }
127
+ if (json) walk(json);
128
+ return headings;
129
+ }
130
+
131
+ // src/docs/validate.ts
132
+ var DOCS_LIMITS = {
133
+ bytes: 1024 * 1024,
134
+ nodes: 2e4,
135
+ depth: 64,
136
+ metadataBytes: 32768
137
+ };
138
+ var record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
139
+ var nullableString = (v) => v === null || typeof v === "string";
140
+ var attributes = {
141
+ heading: { level: (v) => Number.isInteger(v) && Number(v) >= 1 && Number(v) <= 6 },
142
+ orderedList: {
143
+ start: (v) => Number.isSafeInteger(v) && Number(v) >= 1,
144
+ type: (v) => v === null
145
+ },
146
+ codeBlock: {
147
+ language: (v) => v === null || typeof v === "string" && /^[\w+-]+$/.test(v)
148
+ },
149
+ image: {
150
+ src: (v) => typeof v === "string",
151
+ alt: nullableString,
152
+ title: nullableString,
153
+ width: (v) => v === null,
154
+ height: (v) => v === null
155
+ },
156
+ tableCell: {
157
+ colspan: (v) => v === 1,
158
+ rowspan: (v) => v === 1,
159
+ colwidth: (v) => v === null,
160
+ align: (v) => v === null
161
+ },
162
+ tableHeader: {
163
+ colspan: (v) => v === 1,
164
+ rowspan: (v) => v === 1,
165
+ colwidth: (v) => v === null,
166
+ align: (v) => v === null
167
+ },
168
+ link: {
169
+ href: (v) => typeof v === "string",
170
+ title: nullableString,
171
+ target: (v) => v === null || v === "_blank",
172
+ rel: (v) => v === null || v === "noopener noreferrer nofollow",
173
+ class: (v) => v === null
174
+ }
175
+ };
176
+ function validateDocsContent(input) {
177
+ const errors = [];
178
+ const add = (code, message, fieldPath) => errors.push(diagnostic(code, message, { fieldPath }));
179
+ let nodes = 0;
180
+ let textBytes = 0;
181
+ function attrs(value, type, path) {
182
+ if (value === void 0) return;
183
+ if (!record(value)) {
184
+ add("INVALID_DOCUMENT", "Attributes must be an object.", path);
185
+ return;
186
+ }
187
+ for (const [key, v] of Object.entries(value)) {
188
+ const allowed = attributes[type];
189
+ const check = allowed && Object.hasOwn(allowed, key) ? allowed[key] : void 0;
190
+ if (!check)
191
+ add(
192
+ "UNSUPPORTED_ATTRIBUTE",
193
+ `Unsupported ${type} attribute: ${key}.`,
194
+ `${path}.${key}`
195
+ );
196
+ else if (!check(v))
197
+ add("INVALID_DOCUMENT", `Invalid ${type} attribute: ${key}.`, `${path}.${key}`);
198
+ }
199
+ }
200
+ function walk(node, path, depth) {
201
+ if (++nodes > DOCS_LIMITS.nodes || depth > DOCS_LIMITS.depth) {
202
+ if (nodes === DOCS_LIMITS.nodes + 1 || depth > DOCS_LIMITS.depth)
203
+ add("INVALID_DOCUMENT", "Document exceeds structural limits.", path);
204
+ return;
205
+ }
206
+ if (!record(node) || typeof node.type !== "string") {
207
+ add("INVALID_DOCUMENT", "Expected a document node with a type.", path);
208
+ return;
209
+ }
210
+ if (!Object.hasOwn(docsSchema.nodes, node.type)) {
211
+ add("UNSUPPORTED_NODE", `Unsupported node: ${node.type}.`, `${path}.type`);
212
+ return;
213
+ }
214
+ for (const key of Object.keys(node)) {
215
+ if (!["type", "attrs", "content", "marks", "text"].includes(key))
216
+ add(
217
+ "UNSUPPORTED_ATTRIBUTE",
218
+ `Unsupported node field: ${key}.`,
219
+ `${path}.${key}`
220
+ );
221
+ }
222
+ attrs(node.attrs, node.type, `${path}.attrs`);
223
+ if (node.type === "text") {
224
+ if (typeof node.text !== "string" || !node.text.length)
225
+ add("INVALID_DOCUMENT", "Text nodes require nonempty text.", `${path}.text`);
226
+ else textBytes += new TextEncoder().encode(node.text).length;
227
+ if (node.content !== void 0 || node.attrs !== void 0)
228
+ add("INVALID_DOCUMENT", "Text cannot contain children or attributes.", path);
229
+ } else if (node.text !== void 0)
230
+ add("INVALID_DOCUMENT", "Only text nodes can contain text.", `${path}.text`);
231
+ if (node.marks !== void 0) {
232
+ if (!Array.isArray(node.marks))
233
+ add("INVALID_DOCUMENT", "Marks must be an array.", `${path}.marks`);
234
+ else {
235
+ const seen = /* @__PURE__ */ new Set();
236
+ node.marks.forEach((mark, i) => {
237
+ const mp = `${path}.marks[${i}]`;
238
+ if (!record(mark) || typeof mark.type !== "string") {
239
+ add("INVALID_DOCUMENT", "Invalid mark.", mp);
240
+ return;
241
+ }
242
+ if (!Object.hasOwn(docsSchema.marks, mark.type)) {
243
+ add("UNSUPPORTED_MARK", `Unsupported mark: ${mark.type}.`, `${mp}.type`);
244
+ return;
245
+ }
246
+ if (node.type !== "text" || seen.has(mark.type))
247
+ add("INVALID_DOCUMENT", "Marks must be unique and applied to text.", mp);
248
+ seen.add(mark.type);
249
+ for (const key of Object.keys(mark))
250
+ if (!["type", "attrs"].includes(key))
251
+ add(
252
+ "UNSUPPORTED_ATTRIBUTE",
253
+ `Unsupported mark field: ${key}.`,
254
+ `${mp}.${key}`
255
+ );
256
+ attrs(mark.attrs, mark.type, `${mp}.attrs`);
257
+ if (mark.type === "link" && (!record(mark.attrs) || !isSafeDocsUrl(mark.attrs.href)))
258
+ add("UNSAFE_URL", "Link URL is unsafe.", `${mp}.attrs.href`);
259
+ });
260
+ if (seen.has("code") && seen.size > 1)
261
+ add(
262
+ "INVALID_DOCUMENT",
263
+ "Inline code cannot carry other marks.",
264
+ `${path}.marks`
265
+ );
266
+ }
267
+ }
268
+ if (node.type === "image" && (!record(node.attrs) || !isSafeDocsUrl(node.attrs.src, true)))
269
+ add("UNSAFE_URL", "Images require a stable HTTPS URL.", `${path}.attrs.src`);
270
+ if (node.type === "heading" && (!record(node.attrs) || !Number.isInteger(node.attrs.level)))
271
+ add("INVALID_DOCUMENT", "Heading level is required.", `${path}.attrs.level`);
272
+ if (node.content !== void 0) {
273
+ if (!Array.isArray(node.content)) {
274
+ add("INVALID_DOCUMENT", "Content must be an array.", `${path}.content`);
275
+ return;
276
+ }
277
+ node.content.forEach((child, i) => {
278
+ walk(child, `${path}.content[${i}]`, depth + 1);
279
+ });
280
+ }
281
+ if (!errors.some((error) => error.fieldPath?.startsWith(path))) {
282
+ try {
283
+ docsSchema.nodeFromJSON(node).check();
284
+ } catch {
285
+ add("INVALID_DOCUMENT", "Invalid child ordering, cardinality, or marks.", path);
286
+ }
287
+ }
288
+ if (node.type === "table" && Array.isArray(node.content)) {
289
+ let width;
290
+ node.content.forEach((row, i) => {
291
+ if (!record(row) || !Array.isArray(row.content)) return;
292
+ width ?? (width = row.content.length);
293
+ if (row.content.length !== width || row.content.some(
294
+ (cell) => !record(cell) || cell.type !== (i === 0 ? "tableHeader" : "tableCell")
295
+ ))
296
+ add(
297
+ "INVALID_DOCUMENT",
298
+ "Simple tables require a rectangular header row followed by body rows.",
299
+ `${path}.content[${i}]`
300
+ );
301
+ });
302
+ }
303
+ }
304
+ if (!record(input) || input.type !== "doc")
305
+ add("INVALID_DOCUMENT", "Root must be a doc node.", "content.type");
306
+ else walk(input, "content", 0);
307
+ if (textBytes > DOCS_LIMITS.bytes)
308
+ add("INVALID_DOCUMENT", "Document exceeds the text size limit.", "content");
309
+ if (errors.length) return { diagnostics: sortDiagnostics(errors) };
310
+ const normalize = (node) => ({
311
+ ...node,
312
+ ...node.type === "image" ? { attrs: { ...node.attrs, alt: node.attrs?.alt ?? "" } } : {},
313
+ ...node.content ? { content: node.content.map(normalize) } : {}
314
+ });
315
+ const content = normalize(docsSchema.nodeFromJSON(input).toJSON());
316
+ return { content: { ...content, content: content.content ?? [] }, diagnostics: [] };
317
+ }
318
+ function requireDocsContent(input) {
319
+ const result = validateDocsContent(input);
320
+ if (!result.content) throw new DocsContentError(result.diagnostics);
321
+ return result.content;
322
+ }
323
+
324
+ // src/docs/routes.ts
325
+ function normalizeDocsSlug(value) {
326
+ return normalizePath(value, true);
327
+ }
328
+ function normalizePath(value, reserveRoutes) {
329
+ if (typeof value !== "string")
330
+ throw new DocsContentError([
331
+ diagnostic("INVALID_SLUG", "Slug must be a relative documentation path.", {
332
+ fieldPath: "slug"
333
+ })
334
+ ]);
335
+ const slug = value.toLowerCase().replace(/\/$/, "");
336
+ if (!slug || slug.length > 512 || !/^[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(slug))
337
+ throw new DocsContentError([
338
+ diagnostic(
339
+ "INVALID_SLUG",
340
+ "Use lowercase ASCII letters, digits, and hyphens in slash-separated segments.",
341
+ { fieldPath: "slug" }
342
+ )
343
+ ]);
344
+ if (reserveRoutes && ["collections", "search"].includes(slug.split("/")[0]))
345
+ throw new DocsContentError([
346
+ diagnostic(
347
+ "RESERVED_ROUTE",
348
+ "This path is reserved for documentation navigation.",
349
+ { route: `/docs/${slug}`, fieldPath: "slug" }
350
+ )
351
+ ]);
352
+ return slug;
353
+ }
354
+ function normalizeDocsSourceDirectory(value) {
355
+ const directory = value.replace(/\/$/, "");
356
+ if (!directory || directory.startsWith("/") || // biome-ignore lint/suspicious/noControlCharactersInRegex: Reject control characters in repository paths.
357
+ /[\\\u0000-\u001f]/.test(directory) || directory.split("/").some((segment) => !segment || segment === "." || segment === ".."))
358
+ throw new DocsContentError([
359
+ diagnostic("INVALID_REFERENCE", "Select a directory within the repository.", {
360
+ fieldPath: "directory"
361
+ })
362
+ ]);
363
+ return directory;
364
+ }
365
+ function resolveDocsSourcePath(path, directory, explicitSlug) {
366
+ const root = normalizeDocsSourceDirectory(directory);
367
+ if (!path.startsWith(`${root}/`))
368
+ throw new DocsContentError([
369
+ diagnostic(
370
+ "INVALID_REFERENCE",
371
+ "File is outside the configured docs directory.",
372
+ { file: path }
373
+ )
374
+ ]);
375
+ const relativePath = path.slice(root.length + 1);
376
+ const segments = relativePath.split("/");
377
+ if (!relativePath.endsWith(".md") || // biome-ignore lint/suspicious/noControlCharactersInRegex: Reject control characters in repository paths.
378
+ /[\\\u0000-\u001f]/.test(relativePath) || segments.some((segment) => !segment || segment === "." || segment === ".."))
379
+ throw new DocsContentError([
380
+ diagnostic("INVALID_REFERENCE", "Invalid Markdown source path.", { file: path })
381
+ ]);
382
+ const defaultSlug = relativePath.slice(0, -3).replace(/\/index$/, "");
383
+ const slug = normalizeDocsSlug(explicitSlug ?? defaultSlug);
384
+ return {
385
+ relativePath,
386
+ slug,
387
+ route: `/docs/${slug}`,
388
+ sectionSlug: segments.length > 1 ? normalizePath(segments[0], false) : null
389
+ };
390
+ }
391
+
392
+ // src/docs/frontmatter.ts
393
+ function requireDocsFrontmatter(value, file) {
394
+ const fail = (message, fieldPath = "frontmatter") => {
395
+ throw new DocsContentError([
396
+ diagnostic("INVALID_FRONTMATTER", message, { fieldPath, file })
397
+ ]);
398
+ };
399
+ let count = 0;
400
+ function check(v, depth, path) {
401
+ if (++count > 2e3 || depth > 16) fail("Metadata exceeds structural limits.", path);
402
+ if (v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number" && Number.isFinite(v))
403
+ return;
404
+ if (Array.isArray(v)) {
405
+ v.forEach((item, i) => {
406
+ check(item, depth + 1, `${path}[${i}]`);
407
+ });
408
+ return;
409
+ }
410
+ if (typeof v !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(v)))
411
+ fail("Metadata must contain only plain data.", path);
412
+ for (const [key, item] of Object.entries(v)) {
413
+ if (["__proto__", "prototype", "constructor"].includes(key))
414
+ fail("Reserved metadata key.", `${path}.${key}`);
415
+ check(item, depth + 1, `${path}.${key}`);
416
+ }
417
+ }
418
+ if (!value || typeof value !== "object" || Array.isArray(value))
419
+ fail("Frontmatter must be an object.");
420
+ check(value, 0, "frontmatter");
421
+ const meta = value;
422
+ if (new TextEncoder().encode(JSON.stringify(meta)).length > DOCS_LIMITS.metadataBytes)
423
+ fail("Metadata exceeds size limit.");
424
+ if (typeof meta.title !== "string" || !meta.title.trim())
425
+ fail("Title is required.", "frontmatter.title");
426
+ for (const key of ["description", "slug", "nav_title"])
427
+ if (meta[key] !== void 0 && typeof meta[key] !== "string")
428
+ fail(`${key} must be a string.`, `frontmatter.${key}`);
429
+ if (meta.slug !== void 0) {
430
+ try {
431
+ normalizeDocsSlug(meta.slug);
432
+ } catch (error) {
433
+ if (error instanceof DocsContentError)
434
+ throw new DocsContentError(
435
+ error.diagnostics.map((item) => ({
436
+ ...item,
437
+ fieldPath: "frontmatter.slug",
438
+ file
439
+ }))
440
+ );
441
+ throw error;
442
+ }
443
+ }
444
+ if (meta.nav_order !== void 0 && !Number.isSafeInteger(meta.nav_order))
445
+ fail("nav_order must be an integer.", "frontmatter.nav_order");
446
+ for (const key of ["published", "indexable"])
447
+ if (meta[key] !== void 0 && typeof meta[key] !== "boolean")
448
+ fail(`${key} must be a boolean.`, `frontmatter.${key}`);
449
+ if (meta.featured_image !== void 0) {
450
+ const image = meta.featured_image;
451
+ if (!image || typeof image !== "object" || Array.isArray(image) || Object.keys(image).some((key) => !["url", "alt"].includes(key)) || !isSafeDocsUrl(image.url, true) || typeof image.alt !== "string")
452
+ fail(
453
+ "featured_image must contain a stable HTTPS url and alt string.",
454
+ "frontmatter.featured_image"
455
+ );
456
+ }
457
+ return meta;
458
+ }
459
+ function splitDocsFrontmatter(source, file) {
460
+ const normalized = source.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
461
+ if (!normalized.startsWith("---\n"))
462
+ throw new DocsContentError([
463
+ diagnostic("INVALID_FRONTMATTER", "Markdown requires frontmatter with a title.", {
464
+ file,
465
+ line: 1,
466
+ column: 1
467
+ })
468
+ ]);
469
+ const end = /^---[ \t]*$/gm;
470
+ end.lastIndex = 4;
471
+ const delimiter = end.exec(normalized);
472
+ if (!delimiter)
473
+ throw new DocsContentError([
474
+ diagnostic("INVALID_FRONTMATTER", "Frontmatter is not closed.", {
475
+ file,
476
+ line: 1,
477
+ column: 1
478
+ })
479
+ ]);
480
+ const raw = normalized.slice(4, delimiter.index);
481
+ if (new TextEncoder().encode(raw).length > DOCS_LIMITS.metadataBytes)
482
+ throw new DocsContentError([
483
+ diagnostic("INVALID_FRONTMATTER", "Frontmatter exceeds size limit.", { file })
484
+ ]);
485
+ const doc = parseDocument(raw, {
486
+ schema: "core",
487
+ uniqueKeys: true,
488
+ strict: true,
489
+ merge: false
490
+ });
491
+ if (doc.errors.length || doc.warnings.length) {
492
+ throw new DocsContentError(
493
+ [...doc.errors, ...doc.warnings].map((error) => {
494
+ const offset = error.pos[0];
495
+ const prefix = raw.slice(0, offset);
496
+ return diagnostic("INVALID_FRONTMATTER", "Invalid YAML frontmatter.", {
497
+ file,
498
+ line: prefix.split("\n").length + 1,
499
+ column: prefix.length - prefix.lastIndexOf("\n")
500
+ });
501
+ })
502
+ );
503
+ }
504
+ let value;
505
+ try {
506
+ value = doc.toJS({ maxAliasCount: 0 });
507
+ } catch {
508
+ throw new DocsContentError([
509
+ diagnostic("INVALID_FRONTMATTER", "YAML aliases are not supported.", {
510
+ file,
511
+ line: 2,
512
+ column: 1
513
+ })
514
+ ]);
515
+ }
516
+ let frontmatter;
517
+ try {
518
+ frontmatter = requireDocsFrontmatter(value, file);
519
+ } catch (error) {
520
+ if (!(error instanceof DocsContentError)) throw error;
521
+ throw new DocsContentError(
522
+ error.diagnostics.map((item) => {
523
+ const key = item.fieldPath?.split(".")[1];
524
+ const field = key ? doc.get(key, true) : void 0;
525
+ const offset = field && typeof field === "object" && "range" in field ? field.range?.[0] ?? 0 : 0;
526
+ const prefix = raw.slice(0, offset);
527
+ return {
528
+ ...item,
529
+ file,
530
+ line: prefix.split("\n").length + 1,
531
+ column: prefix.length - prefix.lastIndexOf("\n")
532
+ };
533
+ })
534
+ );
535
+ }
536
+ const start = delimiter.index + delimiter[0].length + (normalized[delimiter.index + delimiter[0].length] === "\n" ? 1 : 0);
537
+ return {
538
+ frontmatter,
539
+ body: normalized.slice(start),
540
+ lineOffset: normalized.slice(0, start).split("\n").length - 1
541
+ };
542
+ }
543
+ function serializeDocsFrontmatter(meta) {
544
+ return `---
545
+ ${stringify(requireDocsFrontmatter(meta), { sortMapEntries: true, aliasDuplicateObjects: false, lineWidth: 0 })}---
546
+
547
+ `;
548
+ }
549
+ var DOCS_PARSER_VERSION = 1;
550
+ var parser = unified().use(remarkParse).use(remarkGfm);
551
+ var writer = unified().use(remarkGfm).use(remarkStringify, {
552
+ bullet: "-",
553
+ emphasis: "*",
554
+ strong: "*",
555
+ fences: true,
556
+ incrementListMarker: true
557
+ });
558
+ function parseDocsMarkdown(markdown, options = {}) {
559
+ if (typeof markdown !== "string" || new TextEncoder().encode(markdown).length > DOCS_LIMITS.bytes)
560
+ throw new DocsContentError([
561
+ diagnostic("INVALID_DOCUMENT", "Markdown exceeds size limit.", {
562
+ file: options.file
563
+ })
564
+ ]);
565
+ const source = options.frontmatter === false ? {
566
+ body: markdown.replace(/\r\n?/g, "\n"),
567
+ lineOffset: 0,
568
+ frontmatter: void 0
569
+ } : splitDocsFrontmatter(markdown, options.file);
570
+ const tree = parser.parse(source.body);
571
+ const definitions = /* @__PURE__ */ new Map();
572
+ function error(node, message, code = "UNSUPPORTED_NODE") {
573
+ throw new DocsContentError([
574
+ diagnostic(code, message, {
575
+ file: options.file,
576
+ line: node.position ? node.position.start.line + source.lineOffset : void 0,
577
+ column: node.position?.start.column
578
+ })
579
+ ]);
580
+ }
581
+ let nodes = 0;
582
+ function inspect(node, depth) {
583
+ if (++nodes > DOCS_LIMITS.nodes || depth > DOCS_LIMITS.depth)
584
+ error(node, "Markdown exceeds structural limits.", "INVALID_DOCUMENT");
585
+ if (node.type === "definition") definitions.set(node.identifier, node);
586
+ if ("children" in node)
587
+ for (const child of node.children) inspect(child, depth + 1);
588
+ }
589
+ inspect(tree, 0);
590
+ function url(node, value, image2 = false) {
591
+ if (image2 && !/^[a-z]+:/i.test(value))
592
+ error(
593
+ node,
594
+ "Local image binaries must be uploaded to MediaLit.",
595
+ "UNMANAGED_BINARY"
596
+ );
597
+ if (!isSafeDocsUrl(value, image2))
598
+ error(node, "URL is unsafe or expiring.", "UNSAFE_URL");
599
+ return value;
600
+ }
601
+ function inline(items, marks = []) {
602
+ return items.flatMap((node) => {
603
+ switch (node.type) {
604
+ case "text": {
605
+ const raw = source.body.slice(
606
+ node.position?.start.offset ?? 0,
607
+ node.position?.end.offset ?? 0
608
+ );
609
+ if (/(?<!\\)<\/?[A-Za-z][^>]*>/.test(raw) || /^(?:import|export)\s/m.test(raw))
610
+ error(node, "MDX/JSX and module syntax are unsupported.");
611
+ return node.value ? [{ type: "text", text: node.value, ...marks.length ? { marks } : {} }] : [];
612
+ }
613
+ case "strong":
614
+ return inline(node.children, [...marks, { type: "bold" }]);
615
+ case "emphasis":
616
+ return inline(node.children, [...marks, { type: "italic" }]);
617
+ case "delete":
618
+ return inline(node.children, [...marks, { type: "strike" }]);
619
+ case "inlineCode":
620
+ if (marks.length) error(node, "Inline code cannot carry other marks.");
621
+ return [{ type: "text", text: node.value, marks: [{ type: "code" }] }];
622
+ case "break":
623
+ if (marks.length) error(node, "Marked line breaks are unsupported.");
624
+ return [{ type: "hardBreak" }];
625
+ case "link":
626
+ return inline(node.children, [
627
+ ...marks,
628
+ {
629
+ type: "link",
630
+ attrs: { href: url(node, node.url), title: node.title ?? null }
631
+ }
632
+ ]);
633
+ case "linkReference": {
634
+ const def = definitions.get(node.identifier);
635
+ if (!def) error(node, "Missing link definition.", "BROKEN_LINK");
636
+ return inline(node.children, [
637
+ ...marks,
638
+ {
639
+ type: "link",
640
+ attrs: { href: url(node, def.url), title: def.title ?? null }
641
+ }
642
+ ]);
643
+ }
644
+ default:
645
+ return error(node, `Unsupported inline construct: ${node.type}.`);
646
+ }
647
+ });
648
+ }
649
+ function image(node) {
650
+ const def = node.type === "imageReference" ? definitions.get(node.identifier) : node;
651
+ if (!def) error(node, "Missing image definition.", "BROKEN_LINK");
652
+ return {
653
+ type: "image",
654
+ attrs: {
655
+ src: url(node, def.url, true),
656
+ alt: node.alt ?? "",
657
+ title: def.title ?? null
658
+ }
659
+ };
660
+ }
661
+ function block(node) {
662
+ switch (node.type) {
663
+ case "definition":
664
+ return [];
665
+ case "paragraph": {
666
+ if (node.children.length === 1 && ["image", "imageReference"].includes(node.children[0].type))
667
+ return [
668
+ image(
669
+ node.children[0]
670
+ )
671
+ ];
672
+ return [{ type: "paragraph", content: inline(node.children) }];
673
+ }
674
+ case "heading":
675
+ return [
676
+ {
677
+ type: "heading",
678
+ attrs: { level: node.depth },
679
+ content: inline(node.children)
680
+ }
681
+ ];
682
+ case "blockquote":
683
+ return [{ type: "blockquote", content: node.children.flatMap(block) }];
684
+ case "thematicBreak":
685
+ return [{ type: "horizontalRule" }];
686
+ case "code":
687
+ if (node.meta)
688
+ error(node, "Code fence metadata is unsupported.", "UNSUPPORTED_ATTRIBUTE");
689
+ return [
690
+ {
691
+ type: "codeBlock",
692
+ attrs: { language: node.lang ?? null },
693
+ content: node.value ? [{ type: "text", text: node.value }] : []
694
+ }
695
+ ];
696
+ case "list":
697
+ return [
698
+ {
699
+ type: node.ordered ? "orderedList" : "bulletList",
700
+ ...node.ordered ? { attrs: { start: node.start ?? 1 } } : {},
701
+ content: node.children.map((item) => {
702
+ if (item.checked !== null && item.checked !== void 0)
703
+ error(item, "Task lists are unsupported.");
704
+ const content2 = item.children.flatMap(block);
705
+ return {
706
+ type: "listItem",
707
+ content: content2.length ? content2 : [{ type: "paragraph" }]
708
+ };
709
+ })
710
+ }
711
+ ];
712
+ case "table":
713
+ if (node.align?.some((alignment) => alignment !== null))
714
+ error(node, "Table alignment is unsupported.", "UNSUPPORTED_ATTRIBUTE");
715
+ return [
716
+ {
717
+ type: "table",
718
+ content: node.children.map((row, i) => ({
719
+ type: "tableRow",
720
+ content: row.children.map((cell) => ({
721
+ type: i === 0 ? "tableHeader" : "tableCell",
722
+ content: [{ type: "paragraph", content: inline(cell.children) }]
723
+ }))
724
+ }))
725
+ }
726
+ ];
727
+ default:
728
+ return error(node, `Unsupported block construct: ${node.type}.`);
729
+ }
730
+ }
731
+ const content = requireDocsContent({
732
+ type: "doc",
733
+ content: tree.children.flatMap(block)
734
+ });
735
+ return { content, frontmatter: source.frontmatter };
736
+ }
737
+ function toInline(children) {
738
+ const rank = ["link", "bold", "italic", "strike", "code"];
739
+ const nodes = children.map((node) => ({
740
+ ...node,
741
+ marks: [...node.marks ?? []].sort(
742
+ (a, b) => rank.indexOf(a.type) - rank.indexOf(b.type)
743
+ )
744
+ }));
745
+ function group(items, depth) {
746
+ const output = [];
747
+ for (let i = 0; i < items.length; ) {
748
+ const node = items[i];
749
+ const mark = node.marks?.[depth];
750
+ if (!mark) {
751
+ if (node.type === "hardBreak") output.push({ type: "break" });
752
+ else output.push({ type: "text", value: node.text });
753
+ i++;
754
+ continue;
755
+ }
756
+ let end = i + 1;
757
+ while (end < items.length && JSON.stringify(items[end].marks?.[depth]) === JSON.stringify(mark))
758
+ end++;
759
+ const children2 = group(items.slice(i, end), depth + 1);
760
+ switch (mark.type) {
761
+ case "link":
762
+ output.push({
763
+ type: "link",
764
+ url: mark.attrs.href,
765
+ title: mark.attrs?.title ?? null,
766
+ children: children2
767
+ });
768
+ break;
769
+ case "bold":
770
+ output.push({ type: "strong", children: children2 });
771
+ break;
772
+ case "italic":
773
+ output.push({ type: "emphasis", children: children2 });
774
+ break;
775
+ case "strike":
776
+ output.push({ type: "delete", children: children2 });
777
+ break;
778
+ case "code":
779
+ output.push({
780
+ type: "inlineCode",
781
+ value: items.slice(i, end).map((item) => item.text).join("")
782
+ });
783
+ break;
784
+ }
785
+ i = end;
786
+ }
787
+ return output;
788
+ }
789
+ return group(nodes, 0);
790
+ }
791
+ function serializeDocsMarkdown(input, frontmatter) {
792
+ const content = requireDocsContent(input);
793
+ function block(node) {
794
+ const children = node.content ?? [];
795
+ switch (node.type) {
796
+ case "paragraph":
797
+ return { type: "paragraph", children: toInline(children) };
798
+ case "heading":
799
+ return {
800
+ type: "heading",
801
+ depth: node.attrs.level,
802
+ children: toInline(children)
803
+ };
804
+ case "horizontalRule":
805
+ return { type: "thematicBreak" };
806
+ case "image":
807
+ return {
808
+ type: "paragraph",
809
+ children: [
810
+ {
811
+ type: "image",
812
+ url: node.attrs.src,
813
+ alt: node.attrs?.alt ?? "",
814
+ title: node.attrs?.title ?? null
815
+ }
816
+ ]
817
+ };
818
+ case "codeBlock":
819
+ return {
820
+ type: "code",
821
+ lang: node.attrs?.language ?? null,
822
+ value: children.map((child) => child.text ?? "").join("")
823
+ };
824
+ case "blockquote":
825
+ return {
826
+ type: "blockquote",
827
+ children: children.map(block)
828
+ };
829
+ case "bulletList":
830
+ case "orderedList":
831
+ return {
832
+ type: "list",
833
+ ordered: node.type === "orderedList",
834
+ start: node.type === "orderedList" ? node.attrs?.start ?? 1 : null,
835
+ spread: true,
836
+ children: children.map((item) => ({
837
+ type: "listItem",
838
+ spread: true,
839
+ children: (item.content ?? []).map(block)
840
+ }))
841
+ };
842
+ case "table":
843
+ return {
844
+ type: "table",
845
+ children: children.map((row) => ({
846
+ type: "tableRow",
847
+ children: (row.content ?? []).map((cell) => ({
848
+ type: "tableCell",
849
+ children: toInline(cell.content[0].content ?? [])
850
+ }))
851
+ }))
852
+ };
853
+ default:
854
+ throw new DocsContentError([
855
+ diagnostic("UNSUPPORTED_NODE", `Cannot serialize ${node.type}.`)
856
+ ]);
857
+ }
858
+ }
859
+ const tree = { type: "root", children: (content.content ?? []).map(block) };
860
+ const markdown = writer.stringify(tree);
861
+ const roundtrip = parseDocsMarkdown(markdown, { frontmatter: false }).content;
862
+ const semantic = (doc) => ({
863
+ ...doc,
864
+ content: doc.content?.filter(
865
+ (node) => node.type !== "paragraph" || node.content?.length
866
+ ) ?? []
867
+ });
868
+ if (JSON.stringify(semantic(roundtrip)) !== JSON.stringify(semantic(content)))
869
+ throw new DocsContentError([
870
+ diagnostic(
871
+ "INVALID_DOCUMENT",
872
+ "Content has no lossless Markdown representation."
873
+ )
874
+ ]);
875
+ return (frontmatter ? serializeDocsFrontmatter(frontmatter) : "") + markdown;
876
+ }
877
+
878
+ // src/docs/hash.ts
879
+ function stable(value) {
880
+ if (Array.isArray(value)) return value.map(stable);
881
+ if (value && typeof value === "object")
882
+ return Object.fromEntries(
883
+ Object.keys(value).sort().map((key) => [key, stable(value[key])])
884
+ );
885
+ return value;
886
+ }
887
+ async function hashDocsContent(input) {
888
+ const canonical = stable({
889
+ content: requireDocsContent(input.content),
890
+ metadata: input.metadata,
891
+ versions: {
892
+ schema: DOCS_SCHEMA_VERSION,
893
+ parser: DOCS_PARSER_VERSION,
894
+ headings: DOCS_HEADING_POLICY_VERSION,
895
+ embeddingConfiguration: input.embeddingConfigurationVersion
896
+ }
897
+ });
898
+ const bytes = new TextEncoder().encode(JSON.stringify(canonical));
899
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
900
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
901
+ }
902
+
903
+ export { DOCS_HEADING_POLICY_VERSION, DOCS_LIMITS, DOCS_PARSER_VERSION, DOCS_SCHEMA_VERSION, DocsContentError, createDocsExtensions, createHeadingIdGenerator, diagnostic, docsNodeText, docsSchema, extractDocsHeadings, hashDocsContent, isSafeDocsUrl, normalizeDocsSlug, normalizeDocsSourceDirectory, parseDocsMarkdown, requireDocsContent, requireDocsFrontmatter, resolveDocsSourcePath, serializeDocsFrontmatter, serializeDocsMarkdown, sortDiagnostics, splitDocsFrontmatter, validateDocsContent };
904
+ //# sourceMappingURL=docs.js.map
905
+ //# sourceMappingURL=docs.js.map