@frontlit/text-editor 0.1.0-alpha.3 → 0.1.0-alpha.4
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.d.ts +1 -1
- package/dist/docs.js +621 -35
- package/dist/docs.js.map +1 -1
- package/dist/index.js +683 -18
- package/dist/index.js.map +1 -1
- package/dist/prose.css +305 -0
- package/dist/renderer/index.js +218 -14
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/islands/docs-tabs.js +57 -0
- package/dist/renderer/islands/docs-tabs.js.map +1 -0
- package/dist/styles.css +299 -0
- package/package.json +7 -2
package/dist/docs.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { getSchema, Node } from '@tiptap/core';
|
|
1
|
+
import { Extension, getSchema, Node } from '@tiptap/core';
|
|
2
2
|
import StarterKit from '@tiptap/starter-kit';
|
|
3
|
+
import CodeBlock from '@tiptap/extension-code-block';
|
|
3
4
|
import Image from '@tiptap/extension-image';
|
|
4
5
|
import Link from '@tiptap/extension-link';
|
|
5
6
|
import { TableRow, Table, TableCell, TableHeader } from '@tiptap/extension-table';
|
|
7
|
+
import Code from '@tiptap/extension-code';
|
|
6
8
|
import { parseDocument, stringify } from 'yaml';
|
|
7
9
|
import { unified } from 'unified';
|
|
8
10
|
import remarkParse from 'remark-parse';
|
|
9
11
|
import remarkGfm from 'remark-gfm';
|
|
10
12
|
import remarkStringify from 'remark-stringify';
|
|
13
|
+
import { mdxJsx } from 'micromark-extension-mdx-jsx';
|
|
14
|
+
import { mdxJsxFromMarkdown, mdxJsxToMarkdown } from 'mdast-util-mdx-jsx';
|
|
15
|
+
import { parseExpressionAt } from 'acorn';
|
|
11
16
|
|
|
12
17
|
// src/docs/diagnostics.ts
|
|
13
18
|
function diagnostic(code, message, location = {}) {
|
|
@@ -68,11 +73,112 @@ function isSafeDocsUrl(value, image = false) {
|
|
|
68
73
|
return false;
|
|
69
74
|
}
|
|
70
75
|
}
|
|
76
|
+
var CALLOUT_TYPES = ["note", "tip", "info", "warning", "danger"];
|
|
77
|
+
function attr(name, fallback = null) {
|
|
78
|
+
return {
|
|
79
|
+
[name]: {
|
|
80
|
+
default: fallback,
|
|
81
|
+
parseHTML: (element) => {
|
|
82
|
+
const value = element.getAttribute(`data-${name}`);
|
|
83
|
+
if (value === null) return fallback;
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(value);
|
|
86
|
+
} catch {
|
|
87
|
+
return fallback;
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
renderHTML: (attributes2) => attributes2[name] === null || attributes2[name] === void 0 ? {} : { [`data-${name}`]: JSON.stringify(attributes2[name]) }
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function block(name, spec) {
|
|
95
|
+
const tag = `div[data-docs-block="${name}"]`;
|
|
96
|
+
return Node.create({
|
|
97
|
+
name,
|
|
98
|
+
group: spec.group,
|
|
99
|
+
content: spec.content,
|
|
100
|
+
atom: spec.atom,
|
|
101
|
+
defining: true,
|
|
102
|
+
isolating: true,
|
|
103
|
+
addAttributes() {
|
|
104
|
+
return Object.assign(
|
|
105
|
+
{},
|
|
106
|
+
...Object.entries(spec.attrs ?? {}).map(([key, value]) => attr(key, value))
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
parseHTML() {
|
|
110
|
+
return [{ tag }];
|
|
111
|
+
},
|
|
112
|
+
renderHTML({ HTMLAttributes }) {
|
|
113
|
+
return spec.atom ? ["div", { ...HTMLAttributes, "data-docs-block": name }] : ["div", { ...HTMLAttributes, "data-docs-block": name }, 0];
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
var DocsCode = Code.extend({ excludes: "" });
|
|
118
|
+
function createDocsBlockExtensions() {
|
|
119
|
+
return [
|
|
120
|
+
DocsCode,
|
|
121
|
+
block("callout", {
|
|
122
|
+
group: "block",
|
|
123
|
+
content: "block+",
|
|
124
|
+
attrs: { type: null, title: null }
|
|
125
|
+
}),
|
|
126
|
+
block("tabs", { group: "block", content: "tab+" }),
|
|
127
|
+
block("tab", { content: "block+", attrs: { label: "" } }),
|
|
128
|
+
block("steps", { group: "block", content: "step+" }),
|
|
129
|
+
block("step", { content: "block+" }),
|
|
130
|
+
block("cards", { group: "block", content: "card+" }),
|
|
131
|
+
block("card", {
|
|
132
|
+
content: "block*",
|
|
133
|
+
attrs: { title: "", href: null, icon: null }
|
|
134
|
+
}),
|
|
135
|
+
block("accordions", {
|
|
136
|
+
group: "block",
|
|
137
|
+
content: "accordion+",
|
|
138
|
+
attrs: { type: null }
|
|
139
|
+
}),
|
|
140
|
+
block("accordion", { content: "block+", attrs: { title: "" } }),
|
|
141
|
+
block("codeGroup", { group: "block", content: "codeBlock+" }),
|
|
142
|
+
block("typeTable", { group: "block", atom: true, attrs: { rows: [] } })
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
var DOCS_BLOCK_COMPONENTS = {
|
|
146
|
+
Callout: "callout",
|
|
147
|
+
Tabs: "tabs",
|
|
148
|
+
Tab: "tab",
|
|
149
|
+
Steps: "steps",
|
|
150
|
+
Step: "step",
|
|
151
|
+
Cards: "cards",
|
|
152
|
+
Card: "card",
|
|
153
|
+
Accordions: "accordions",
|
|
154
|
+
Accordion: "accordion",
|
|
155
|
+
CodeGroup: "codeGroup",
|
|
156
|
+
TypeTable: "typeTable"
|
|
157
|
+
};
|
|
158
|
+
var CodeBlockTitle = Extension.create({
|
|
159
|
+
name: "codeBlockTitle",
|
|
160
|
+
addGlobalAttributes() {
|
|
161
|
+
return [
|
|
162
|
+
{
|
|
163
|
+
types: ["codeBlock"],
|
|
164
|
+
attributes: {
|
|
165
|
+
title: {
|
|
166
|
+
default: null,
|
|
167
|
+
parseHTML: (element) => element.getAttribute("data-title"),
|
|
168
|
+
renderHTML: (attributes2) => attributes2.title ? { "data-title": attributes2.title } : {}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
});
|
|
71
175
|
|
|
72
176
|
// src/docs/extensions.ts
|
|
73
177
|
function createDocsExtensions() {
|
|
74
178
|
return [
|
|
75
179
|
StarterKit.configure({
|
|
180
|
+
code: false,
|
|
181
|
+
codeBlock: false,
|
|
76
182
|
document: false,
|
|
77
183
|
underline: false,
|
|
78
184
|
link: false,
|
|
@@ -90,7 +196,10 @@ function createDocsExtensions() {
|
|
|
90
196
|
Table.configure({ resizable: false }),
|
|
91
197
|
TableRow,
|
|
92
198
|
TableCell.extend({ content: "paragraph" }),
|
|
93
|
-
TableHeader.extend({ content: "paragraph" })
|
|
199
|
+
TableHeader.extend({ content: "paragraph" }),
|
|
200
|
+
CodeBlock,
|
|
201
|
+
CodeBlockTitle,
|
|
202
|
+
...createDocsBlockExtensions()
|
|
94
203
|
];
|
|
95
204
|
}
|
|
96
205
|
var docsSchema = getSchema(createDocsExtensions());
|
|
@@ -137,6 +246,11 @@ var DOCS_LIMITS = {
|
|
|
137
246
|
};
|
|
138
247
|
var record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
139
248
|
var nullableString = (v) => v === null || typeof v === "string";
|
|
249
|
+
var label = (v) => typeof v === "string";
|
|
250
|
+
var nullableLabel = nullableString;
|
|
251
|
+
var typeTableRow = (v) => record(v) && Object.keys(v).every(
|
|
252
|
+
(key) => ["name", "type", "default", "description", "required"].includes(key)
|
|
253
|
+
) && label(v.name) && typeof v.type === "string" && nullableString(v.default) && nullableString(v.description) && typeof v.required === "boolean";
|
|
140
254
|
var attributes = {
|
|
141
255
|
heading: { level: (v) => Number.isInteger(v) && Number(v) >= 1 && Number(v) <= 6 },
|
|
142
256
|
orderedList: {
|
|
@@ -144,7 +258,23 @@ var attributes = {
|
|
|
144
258
|
type: (v) => v === null
|
|
145
259
|
},
|
|
146
260
|
codeBlock: {
|
|
147
|
-
language: (v) => v === null || typeof v === "string" && /^[\w+-]+$/.test(v)
|
|
261
|
+
language: (v) => v === null || typeof v === "string" && /^[\w+-]+$/.test(v),
|
|
262
|
+
title: nullableLabel
|
|
263
|
+
},
|
|
264
|
+
callout: {
|
|
265
|
+
type: (v) => v === null || CALLOUT_TYPES.includes(v),
|
|
266
|
+
title: nullableLabel
|
|
267
|
+
},
|
|
268
|
+
tab: { label },
|
|
269
|
+
card: {
|
|
270
|
+
title: label,
|
|
271
|
+
href: nullableString,
|
|
272
|
+
icon: (v) => v === null || typeof v === "string" && /^[A-Za-z][\w-]*$/.test(v)
|
|
273
|
+
},
|
|
274
|
+
accordions: { type: (v) => v === null || v === "single" || v === "multiple" },
|
|
275
|
+
accordion: { title: label },
|
|
276
|
+
typeTable: {
|
|
277
|
+
rows: (v) => Array.isArray(v) && v.every(typeTableRow) && new Set(v.map((row) => row.name)).size === v.length
|
|
148
278
|
},
|
|
149
279
|
image: {
|
|
150
280
|
src: (v) => typeof v === "string",
|
|
@@ -257,16 +387,12 @@ function validateDocsContent(input) {
|
|
|
257
387
|
if (mark.type === "link" && (!record(mark.attrs) || !isSafeDocsUrl(mark.attrs.href)))
|
|
258
388
|
add("UNSAFE_URL", "Link URL is unsafe.", `${mp}.attrs.href`);
|
|
259
389
|
});
|
|
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
390
|
}
|
|
267
391
|
}
|
|
268
392
|
if (node.type === "image" && (!record(node.attrs) || !isSafeDocsUrl(node.attrs.src, true)))
|
|
269
393
|
add("UNSAFE_URL", "Images require a stable HTTPS URL.", `${path}.attrs.src`);
|
|
394
|
+
if (node.type === "card" && record(node.attrs) && typeof node.attrs.href === "string" && !isSafeDocsUrl(node.attrs.href))
|
|
395
|
+
add("UNSAFE_URL", "Card link URL is unsafe.", `${path}.attrs.href`);
|
|
270
396
|
if (node.type === "heading" && (!record(node.attrs) || !Number.isInteger(node.attrs.level)))
|
|
271
397
|
add("INVALID_DOCUMENT", "Heading level is required.", `${path}.attrs.level`);
|
|
272
398
|
if (node.content !== void 0) {
|
|
@@ -310,6 +436,18 @@ function validateDocsContent(input) {
|
|
|
310
436
|
const normalize = (node) => ({
|
|
311
437
|
...node,
|
|
312
438
|
...node.type === "image" ? { attrs: { ...node.attrs, alt: node.attrs?.alt ?? "" } } : {},
|
|
439
|
+
// Type table rows are one attribute value; give them a fixed key order.
|
|
440
|
+
...node.type === "typeTable" ? {
|
|
441
|
+
attrs: {
|
|
442
|
+
rows: (node.attrs?.rows ?? []).map((row) => ({
|
|
443
|
+
name: row.name,
|
|
444
|
+
type: row.type,
|
|
445
|
+
default: row.default,
|
|
446
|
+
description: row.description,
|
|
447
|
+
required: row.required
|
|
448
|
+
}))
|
|
449
|
+
}
|
|
450
|
+
} : {},
|
|
313
451
|
...node.content ? { content: node.content.map(normalize) } : {}
|
|
314
452
|
});
|
|
315
453
|
const content = normalize(docsSchema.nodeFromJSON(input).toJSON());
|
|
@@ -546,15 +684,160 @@ ${stringify(requireDocsFrontmatter(meta), { sortMapEntries: true, aliasDuplicate
|
|
|
546
684
|
|
|
547
685
|
`;
|
|
548
686
|
}
|
|
549
|
-
var DOCS_PARSER_VERSION =
|
|
687
|
+
var DOCS_PARSER_VERSION = 2;
|
|
688
|
+
function jsxTags() {
|
|
689
|
+
const data = this.data();
|
|
690
|
+
data.micromarkExtensions = [...data.micromarkExtensions ?? [], mdxJsx()];
|
|
691
|
+
data.fromMarkdownExtensions = [
|
|
692
|
+
...data.fromMarkdownExtensions ?? [],
|
|
693
|
+
mdxJsxFromMarkdown()
|
|
694
|
+
];
|
|
695
|
+
data.toMarkdownExtensions = [
|
|
696
|
+
...data.toMarkdownExtensions ?? [],
|
|
697
|
+
mdxJsxToMarkdown(),
|
|
698
|
+
{
|
|
699
|
+
// The stock handler indents children, which turns nested fences into indented code.
|
|
700
|
+
handlers: { mdxJsxFlowElement: writeComponent }
|
|
701
|
+
}
|
|
702
|
+
];
|
|
703
|
+
}
|
|
704
|
+
function writeComponent(node, _parent, state, info) {
|
|
705
|
+
const props = node.attributes.map((attribute) => {
|
|
706
|
+
const { name, value } = attribute;
|
|
707
|
+
if (value === null || value === void 0) return name;
|
|
708
|
+
if (typeof value === "string")
|
|
709
|
+
return `${name}="${value.replace(/&/g, "&").replace(/"/g, """)}"`;
|
|
710
|
+
return `${name}={${value.value}}`;
|
|
711
|
+
});
|
|
712
|
+
const multiline = props.some((item) => item.includes("\n"));
|
|
713
|
+
const open = multiline ? `<${node.name}
|
|
714
|
+
${props.map((item) => ` ${item}`).join("\n")}
|
|
715
|
+
` : `<${node.name}${props.map((item) => ` ${item}`).join("")}`;
|
|
716
|
+
if (!node.children.length) return `${open}${multiline ? "" : " "}/>`;
|
|
717
|
+
const body = state.containerFlow(node, info);
|
|
718
|
+
return `${open}>
|
|
719
|
+
|
|
720
|
+
${body}
|
|
721
|
+
|
|
722
|
+
</${node.name}>`;
|
|
723
|
+
}
|
|
724
|
+
function noIndentedCode() {
|
|
725
|
+
const data = this.data();
|
|
726
|
+
data.micromarkExtensions = [
|
|
727
|
+
...data.micromarkExtensions ?? [],
|
|
728
|
+
{ disable: { null: ["codeIndented"] } }
|
|
729
|
+
];
|
|
730
|
+
}
|
|
550
731
|
var parser = unified().use(remarkParse).use(remarkGfm);
|
|
551
|
-
var
|
|
732
|
+
var componentParser = unified().use(remarkParse).use(remarkGfm).use(jsxTags).use(noIndentedCode);
|
|
733
|
+
var writer = unified().use(remarkGfm).use(jsxTags).use(remarkStringify, {
|
|
552
734
|
bullet: "-",
|
|
553
735
|
emphasis: "*",
|
|
554
736
|
strong: "*",
|
|
555
737
|
fences: true,
|
|
556
|
-
incrementListMarker: true
|
|
738
|
+
incrementListMarker: true,
|
|
739
|
+
resourceLink: true
|
|
557
740
|
});
|
|
741
|
+
var COMPONENT_TAG = new RegExp(
|
|
742
|
+
`<\\/?(?:${Object.keys(DOCS_BLOCK_COMPONENTS).join("|")})\\b`
|
|
743
|
+
);
|
|
744
|
+
var CODE_TITLE = /^title=(?:"([^"\n]+)"|'([^'\n]+)')$/;
|
|
745
|
+
var IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
746
|
+
var ICON = /^[A-Za-z][\w-]*$/;
|
|
747
|
+
var PROPS = {
|
|
748
|
+
Callout: ["type", "title"],
|
|
749
|
+
Tabs: ["items"],
|
|
750
|
+
Tab: ["value"],
|
|
751
|
+
Steps: [],
|
|
752
|
+
Step: [],
|
|
753
|
+
Cards: [],
|
|
754
|
+
Card: ["title", "href", "icon"],
|
|
755
|
+
Accordions: ["type"],
|
|
756
|
+
Accordion: ["title"],
|
|
757
|
+
CodeGroup: [],
|
|
758
|
+
TypeTable: ["type"]
|
|
759
|
+
};
|
|
760
|
+
var CHILD = {
|
|
761
|
+
Tabs: "Tab",
|
|
762
|
+
Steps: "Step",
|
|
763
|
+
Cards: "Card",
|
|
764
|
+
Accordions: "Accordion"
|
|
765
|
+
};
|
|
766
|
+
var TYPE_TABLE_FIELDS = ["type", "default", "description", "required"];
|
|
767
|
+
function parseTree(body, fail) {
|
|
768
|
+
if (!COMPONENT_TAG.test(body)) return parser.parse(body);
|
|
769
|
+
try {
|
|
770
|
+
return componentParser.parse(body);
|
|
771
|
+
} catch (cause) {
|
|
772
|
+
const {
|
|
773
|
+
line,
|
|
774
|
+
column,
|
|
775
|
+
reason = String(cause)
|
|
776
|
+
} = cause;
|
|
777
|
+
const place = /\((\d+):(\d+)-/.exec(reason);
|
|
778
|
+
return fail(
|
|
779
|
+
line ? { line, column: column ?? 1 } : place ? { line: Number(place[1]), column: Number(place[2]) } : void 0,
|
|
780
|
+
`Invalid component syntax: ${reason}`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function literal(source) {
|
|
785
|
+
const expression = parseExpressionAt(source, 0, { ecmaVersion: "latest" });
|
|
786
|
+
if (source.slice(expression.end).trim()) throw new Error("Unexpected input.");
|
|
787
|
+
return evaluate(expression);
|
|
788
|
+
}
|
|
789
|
+
function evaluate(node) {
|
|
790
|
+
switch (node.type) {
|
|
791
|
+
case "Literal":
|
|
792
|
+
if (node.value === null || ["string", "number", "boolean"].includes(typeof node.value))
|
|
793
|
+
return node.value;
|
|
794
|
+
break;
|
|
795
|
+
case "TemplateLiteral":
|
|
796
|
+
if (!node.expressions.length) return node.quasis[0].value.cooked;
|
|
797
|
+
break;
|
|
798
|
+
case "UnaryExpression":
|
|
799
|
+
if (node.operator === "-" && node.argument.type === "Literal" && typeof node.argument.value === "number")
|
|
800
|
+
return -node.argument.value;
|
|
801
|
+
break;
|
|
802
|
+
case "ArrayExpression":
|
|
803
|
+
return node.elements.map((element) => {
|
|
804
|
+
if (!element || element.type === "SpreadElement")
|
|
805
|
+
throw new Error("Not literal.");
|
|
806
|
+
return evaluate(element);
|
|
807
|
+
});
|
|
808
|
+
case "ObjectExpression": {
|
|
809
|
+
const output = {};
|
|
810
|
+
for (const property of node.properties) {
|
|
811
|
+
if (property.type !== "Property" || property.kind !== "init" || property.computed || property.method || property.shorthand)
|
|
812
|
+
throw new Error("Not literal.");
|
|
813
|
+
const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" && typeof property.key.value === "string" ? property.key.value : void 0;
|
|
814
|
+
if (key === void 0 || key === "__proto__" || Object.hasOwn(output, key))
|
|
815
|
+
throw new Error("Not literal.");
|
|
816
|
+
output[key] = evaluate(property.value);
|
|
817
|
+
}
|
|
818
|
+
return output;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
throw new Error("Not literal.");
|
|
822
|
+
}
|
|
823
|
+
function quote(value) {
|
|
824
|
+
return value.includes('"') && !/['\\\n]/.test(value) ? `'${value}'` : JSON.stringify(value);
|
|
825
|
+
}
|
|
826
|
+
function formatRows(rows) {
|
|
827
|
+
const lines = rows.map((row) => {
|
|
828
|
+
const fields = [
|
|
829
|
+
`type: ${quote(row.type)}`,
|
|
830
|
+
...row.default === null ? [] : [`default: ${quote(row.default)}`],
|
|
831
|
+
...row.description === null ? [] : [`description: ${quote(row.description)}`],
|
|
832
|
+
...row.required ? ["required: true"] : []
|
|
833
|
+
];
|
|
834
|
+
const key = IDENTIFIER.test(row.name) ? row.name : JSON.stringify(row.name);
|
|
835
|
+
return ` ${key}: { ${fields.join(", ")} },`;
|
|
836
|
+
});
|
|
837
|
+
return `{
|
|
838
|
+
${lines.join("\n")}
|
|
839
|
+
}`;
|
|
840
|
+
}
|
|
558
841
|
function parseDocsMarkdown(markdown, options = {}) {
|
|
559
842
|
if (typeof markdown !== "string" || new TextEncoder().encode(markdown).length > DOCS_LIMITS.bytes)
|
|
560
843
|
throw new DocsContentError([
|
|
@@ -567,7 +850,15 @@ function parseDocsMarkdown(markdown, options = {}) {
|
|
|
567
850
|
lineOffset: 0,
|
|
568
851
|
frontmatter: void 0
|
|
569
852
|
} : splitDocsFrontmatter(markdown, options.file);
|
|
570
|
-
const tree =
|
|
853
|
+
const tree = parseTree(source.body, (place, message) => {
|
|
854
|
+
throw new DocsContentError([
|
|
855
|
+
diagnostic("UNSUPPORTED_NODE", message, {
|
|
856
|
+
file: options.file,
|
|
857
|
+
line: place ? place.line + source.lineOffset : void 0,
|
|
858
|
+
column: place?.column
|
|
859
|
+
})
|
|
860
|
+
]);
|
|
861
|
+
});
|
|
571
862
|
const definitions = /* @__PURE__ */ new Map();
|
|
572
863
|
function error(node, message, code = "UNSUPPORTED_NODE") {
|
|
573
864
|
throw new DocsContentError([
|
|
@@ -617,8 +908,9 @@ function parseDocsMarkdown(markdown, options = {}) {
|
|
|
617
908
|
case "delete":
|
|
618
909
|
return inline(node.children, [...marks, { type: "strike" }]);
|
|
619
910
|
case "inlineCode":
|
|
620
|
-
|
|
621
|
-
|
|
911
|
+
return [
|
|
912
|
+
{ type: "text", text: node.value, marks: [...marks, { type: "code" }] }
|
|
913
|
+
];
|
|
622
914
|
case "break":
|
|
623
915
|
if (marks.length) error(node, "Marked line breaks are unsupported.");
|
|
624
916
|
return [{ type: "hardBreak" }];
|
|
@@ -641,6 +933,11 @@ function parseDocsMarkdown(markdown, options = {}) {
|
|
|
641
933
|
}
|
|
642
934
|
]);
|
|
643
935
|
}
|
|
936
|
+
case "mdxJsxTextElement":
|
|
937
|
+
return error(
|
|
938
|
+
node,
|
|
939
|
+
`Inline <${node.name ?? ""}> is unsupported. Put the component on its own lines.`
|
|
940
|
+
);
|
|
644
941
|
default:
|
|
645
942
|
return error(node, `Unsupported inline construct: ${node.type}.`);
|
|
646
943
|
}
|
|
@@ -658,11 +955,196 @@ function parseDocsMarkdown(markdown, options = {}) {
|
|
|
658
955
|
}
|
|
659
956
|
};
|
|
660
957
|
}
|
|
661
|
-
function
|
|
958
|
+
function props(node) {
|
|
959
|
+
const name = node.name ?? "";
|
|
960
|
+
const output = {};
|
|
961
|
+
for (const attribute of node.attributes) {
|
|
962
|
+
if (attribute.type === "mdxJsxExpressionAttribute")
|
|
963
|
+
error(node, "Spread props are unsupported.", "UNSUPPORTED_ATTRIBUTE");
|
|
964
|
+
if (!PROPS[name].includes(attribute.name))
|
|
965
|
+
error(
|
|
966
|
+
node,
|
|
967
|
+
`Unsupported <${name}> prop: ${attribute.name}.`,
|
|
968
|
+
"UNSUPPORTED_ATTRIBUTE"
|
|
969
|
+
);
|
|
970
|
+
const value = attribute.value;
|
|
971
|
+
const icon = name === "Card" && attribute.name === "icon" && typeof value === "object" && value ? /^\s*<([A-Za-z][\w-]*)\s*\/>\s*$/.exec(value.value) : null;
|
|
972
|
+
if (value === null || value === void 0) output[attribute.name] = true;
|
|
973
|
+
else if (typeof value === "string") output[attribute.name] = value;
|
|
974
|
+
else if (icon) output.icon = icon[1];
|
|
975
|
+
else
|
|
976
|
+
try {
|
|
977
|
+
output[attribute.name] = literal(value.value);
|
|
978
|
+
} catch {
|
|
979
|
+
error(
|
|
980
|
+
node,
|
|
981
|
+
`<${name}> prop ${attribute.name} must be a literal value.`,
|
|
982
|
+
"UNSUPPORTED_ATTRIBUTE"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return output;
|
|
987
|
+
}
|
|
988
|
+
function component(node, children, parent) {
|
|
989
|
+
const name = node.name ?? "";
|
|
990
|
+
if (!Object.hasOwn(PROPS, name))
|
|
991
|
+
error(
|
|
992
|
+
node,
|
|
993
|
+
`Unknown component <${name}>. Use a supported block or plain Markdown.`
|
|
994
|
+
);
|
|
995
|
+
const container = Object.keys(CHILD).find((key) => CHILD[key] === name);
|
|
996
|
+
if (container && parent !== container)
|
|
997
|
+
error(node, `<${name}> must be inside <${container}>.`);
|
|
998
|
+
const p = props(node);
|
|
999
|
+
const label2 = (prop, required) => {
|
|
1000
|
+
const value = p[prop];
|
|
1001
|
+
if (value === void 0 && !required) return null;
|
|
1002
|
+
if (typeof value !== "string")
|
|
1003
|
+
error(node, `<${name}> needs a text ${prop} prop.`, "INVALID_DOCUMENT");
|
|
1004
|
+
return value;
|
|
1005
|
+
};
|
|
1006
|
+
const content2 = (optional = false) => {
|
|
1007
|
+
const output = children.flatMap(block2);
|
|
1008
|
+
return output.length || optional ? output : [{ type: "paragraph" }];
|
|
1009
|
+
};
|
|
1010
|
+
const items = () => {
|
|
1011
|
+
const output = children.filter((child) => child.type !== "definition").map((child) => {
|
|
1012
|
+
if (child.type !== "mdxJsxFlowElement" || child.name !== CHILD[name])
|
|
1013
|
+
error(child, `<${name}> can only contain <${CHILD[name]}>.`);
|
|
1014
|
+
return child;
|
|
1015
|
+
});
|
|
1016
|
+
if (!output.length)
|
|
1017
|
+
error(
|
|
1018
|
+
node,
|
|
1019
|
+
`<${name}> needs at least one <${CHILD[name]}>.`,
|
|
1020
|
+
"INVALID_DOCUMENT"
|
|
1021
|
+
);
|
|
1022
|
+
return output;
|
|
1023
|
+
};
|
|
1024
|
+
const nested = () => items().map((child) => component(child, child.children, name));
|
|
1025
|
+
switch (name) {
|
|
1026
|
+
case "Callout": {
|
|
1027
|
+
const type = p.type ?? null;
|
|
1028
|
+
if (type !== null && !CALLOUT_TYPES.includes(type))
|
|
1029
|
+
error(
|
|
1030
|
+
node,
|
|
1031
|
+
`Callout type must be one of: ${CALLOUT_TYPES.join(", ")}.`,
|
|
1032
|
+
"UNSUPPORTED_ATTRIBUTE"
|
|
1033
|
+
);
|
|
1034
|
+
return {
|
|
1035
|
+
type: "callout",
|
|
1036
|
+
attrs: { type, title: label2("title", false) },
|
|
1037
|
+
content: content2()
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
case "Tabs": {
|
|
1041
|
+
const listed = p.items;
|
|
1042
|
+
if (listed !== void 0 && !(Array.isArray(listed) && listed.every((item) => typeof item === "string")))
|
|
1043
|
+
error(
|
|
1044
|
+
node,
|
|
1045
|
+
"<Tabs> items must be a list of strings.",
|
|
1046
|
+
"UNSUPPORTED_ATTRIBUTE"
|
|
1047
|
+
);
|
|
1048
|
+
const tabs = nested().map((tab, i) => {
|
|
1049
|
+
const value = tab.attrs.label ?? listed?.[i] ?? "";
|
|
1050
|
+
if (listed && !listed.includes(value))
|
|
1051
|
+
error(
|
|
1052
|
+
node,
|
|
1053
|
+
`Tab value "${value}" is not listed in items.`,
|
|
1054
|
+
"INVALID_DOCUMENT"
|
|
1055
|
+
);
|
|
1056
|
+
return { ...tab, attrs: { label: value } };
|
|
1057
|
+
});
|
|
1058
|
+
return { type: "tabs", content: tabs };
|
|
1059
|
+
}
|
|
1060
|
+
case "Tab":
|
|
1061
|
+
return {
|
|
1062
|
+
type: "tab",
|
|
1063
|
+
attrs: { label: label2("value", false) },
|
|
1064
|
+
content: content2()
|
|
1065
|
+
};
|
|
1066
|
+
case "Steps":
|
|
1067
|
+
return { type: "steps", content: nested() };
|
|
1068
|
+
case "Step":
|
|
1069
|
+
return { type: "step", content: content2() };
|
|
1070
|
+
case "Cards":
|
|
1071
|
+
return { type: "cards", content: nested() };
|
|
1072
|
+
case "Card": {
|
|
1073
|
+
const href = label2("href", false);
|
|
1074
|
+
if (href !== null && !isSafeDocsUrl(href))
|
|
1075
|
+
error(node, "Card link URL is unsafe.", "UNSAFE_URL");
|
|
1076
|
+
const icon = label2("icon", false);
|
|
1077
|
+
if (icon !== null && !ICON.test(icon))
|
|
1078
|
+
error(node, "Card icon must be an icon name.", "UNSUPPORTED_ATTRIBUTE");
|
|
1079
|
+
const body = content2(true);
|
|
1080
|
+
return {
|
|
1081
|
+
type: "card",
|
|
1082
|
+
attrs: { title: label2("title", true), href, icon },
|
|
1083
|
+
...body.length ? { content: body } : {}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
case "Accordions": {
|
|
1087
|
+
const type = p.type ?? null;
|
|
1088
|
+
if (type !== null && type !== "single" && type !== "multiple")
|
|
1089
|
+
error(
|
|
1090
|
+
node,
|
|
1091
|
+
'Accordions type must be "single" or "multiple".',
|
|
1092
|
+
"UNSUPPORTED_ATTRIBUTE"
|
|
1093
|
+
);
|
|
1094
|
+
return { type: "accordions", attrs: { type }, content: nested() };
|
|
1095
|
+
}
|
|
1096
|
+
case "Accordion":
|
|
1097
|
+
return {
|
|
1098
|
+
type: "accordion",
|
|
1099
|
+
attrs: { title: label2("title", true) },
|
|
1100
|
+
content: content2()
|
|
1101
|
+
};
|
|
1102
|
+
case "CodeGroup": {
|
|
1103
|
+
for (const child of children)
|
|
1104
|
+
if (child.type !== "code")
|
|
1105
|
+
error(child, "<CodeGroup> can only contain code blocks.");
|
|
1106
|
+
return { type: "codeGroup", content: content2() };
|
|
1107
|
+
}
|
|
1108
|
+
default: {
|
|
1109
|
+
if (children.length) error(node, "<TypeTable> cannot have children.");
|
|
1110
|
+
const type = p.type;
|
|
1111
|
+
if (!type || typeof type !== "object" || Array.isArray(type))
|
|
1112
|
+
error(node, "<TypeTable> needs a type={{ \u2026 }} prop.", "INVALID_DOCUMENT");
|
|
1113
|
+
const rows = Object.entries(type).map(
|
|
1114
|
+
([field, entry]) => {
|
|
1115
|
+
const value = entry;
|
|
1116
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.type !== "string" || Object.keys(value).some((key) => !TYPE_TABLE_FIELDS.includes(key)) || !["string", "undefined"].includes(typeof value.default) || !["string", "undefined"].includes(typeof value.description) || !["boolean", "undefined"].includes(typeof value.required))
|
|
1117
|
+
error(
|
|
1118
|
+
node,
|
|
1119
|
+
`<TypeTable> field ${field} needs a string type and only string default/description and boolean required.`,
|
|
1120
|
+
"UNSUPPORTED_ATTRIBUTE"
|
|
1121
|
+
);
|
|
1122
|
+
return {
|
|
1123
|
+
name: field,
|
|
1124
|
+
type: value.type,
|
|
1125
|
+
default: value.default ?? null,
|
|
1126
|
+
description: value.description ?? null,
|
|
1127
|
+
required: value.required ?? false
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
);
|
|
1131
|
+
return { type: "typeTable", attrs: { rows } };
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function block2(node) {
|
|
662
1136
|
switch (node.type) {
|
|
663
1137
|
case "definition":
|
|
664
1138
|
return [];
|
|
665
1139
|
case "paragraph": {
|
|
1140
|
+
const only = node.children.length === 1 ? node.children[0] : void 0;
|
|
1141
|
+
if (only?.type === "mdxJsxTextElement")
|
|
1142
|
+
return [
|
|
1143
|
+
component(
|
|
1144
|
+
only,
|
|
1145
|
+
only.children.length ? [{ type: "paragraph", children: only.children }] : []
|
|
1146
|
+
)
|
|
1147
|
+
];
|
|
666
1148
|
if (node.children.length === 1 && ["image", "imageReference"].includes(node.children[0].type))
|
|
667
1149
|
return [
|
|
668
1150
|
image(
|
|
@@ -680,19 +1162,40 @@ function parseDocsMarkdown(markdown, options = {}) {
|
|
|
680
1162
|
}
|
|
681
1163
|
];
|
|
682
1164
|
case "blockquote":
|
|
683
|
-
return [{ type: "blockquote", content: node.children.flatMap(
|
|
1165
|
+
return [{ type: "blockquote", content: node.children.flatMap(block2) }];
|
|
684
1166
|
case "thematicBreak":
|
|
685
1167
|
return [{ type: "horizontalRule" }];
|
|
686
|
-
case "code":
|
|
687
|
-
|
|
688
|
-
|
|
1168
|
+
case "code": {
|
|
1169
|
+
const untitled = node.lang?.startsWith("title=");
|
|
1170
|
+
const meta = untitled ? [node.lang, node.meta].filter(Boolean).join(" ") : node.meta;
|
|
1171
|
+
const lang = untitled ? null : node.lang;
|
|
1172
|
+
const title = meta ? CODE_TITLE.exec(meta.trim()) : null;
|
|
1173
|
+
if (meta && !title)
|
|
1174
|
+
error(
|
|
1175
|
+
node,
|
|
1176
|
+
'Code fence metadata other than title="\u2026" is unsupported.',
|
|
1177
|
+
"UNSUPPORTED_ATTRIBUTE"
|
|
1178
|
+
);
|
|
689
1179
|
return [
|
|
690
1180
|
{
|
|
691
1181
|
type: "codeBlock",
|
|
692
|
-
attrs: {
|
|
1182
|
+
attrs: {
|
|
1183
|
+
language: lang ?? null,
|
|
1184
|
+
...title ? { title: title[1] ?? title[2] } : {}
|
|
1185
|
+
},
|
|
693
1186
|
content: node.value ? [{ type: "text", text: node.value }] : []
|
|
694
1187
|
}
|
|
695
1188
|
];
|
|
1189
|
+
}
|
|
1190
|
+
case "mdxJsxFlowElement":
|
|
1191
|
+
return [component(node, node.children)];
|
|
1192
|
+
case "html": {
|
|
1193
|
+
const tag = /^<([A-Z][\w.]*)/.exec(node.value);
|
|
1194
|
+
return error(
|
|
1195
|
+
node,
|
|
1196
|
+
tag ? `Unknown component <${tag[1]}>. Use a supported block or plain Markdown.` : "Raw HTML is unsupported."
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
696
1199
|
case "list":
|
|
697
1200
|
return [
|
|
698
1201
|
{
|
|
@@ -701,7 +1204,7 @@ function parseDocsMarkdown(markdown, options = {}) {
|
|
|
701
1204
|
content: node.children.map((item) => {
|
|
702
1205
|
if (item.checked !== null && item.checked !== void 0)
|
|
703
1206
|
error(item, "Task lists are unsupported.");
|
|
704
|
-
const content2 = item.children.flatMap(
|
|
1207
|
+
const content2 = item.children.flatMap(block2);
|
|
705
1208
|
return {
|
|
706
1209
|
type: "listItem",
|
|
707
1210
|
content: content2.length ? content2 : [{ type: "paragraph" }]
|
|
@@ -730,7 +1233,7 @@ function parseDocsMarkdown(markdown, options = {}) {
|
|
|
730
1233
|
}
|
|
731
1234
|
const content = requireDocsContent({
|
|
732
1235
|
type: "doc",
|
|
733
|
-
content: tree.children.flatMap(
|
|
1236
|
+
content: tree.children.flatMap(block2)
|
|
734
1237
|
});
|
|
735
1238
|
return { content, frontmatter: source.frontmatter };
|
|
736
1239
|
}
|
|
@@ -790,7 +1293,20 @@ function toInline(children) {
|
|
|
790
1293
|
}
|
|
791
1294
|
function serializeDocsMarkdown(input, frontmatter) {
|
|
792
1295
|
const content = requireDocsContent(input);
|
|
793
|
-
|
|
1296
|
+
const prop = (name, value, expression = false) => value === null || value === void 0 ? void 0 : {
|
|
1297
|
+
type: "mdxJsxAttribute",
|
|
1298
|
+
name,
|
|
1299
|
+
value: expression ? { type: "mdxJsxAttributeValueExpression", value } : value
|
|
1300
|
+
};
|
|
1301
|
+
function element(name, attributes2, content2, nested) {
|
|
1302
|
+
return {
|
|
1303
|
+
type: "mdxJsxFlowElement",
|
|
1304
|
+
name,
|
|
1305
|
+
attributes: attributes2.filter((item) => item !== void 0),
|
|
1306
|
+
children: nested ? nested() : content2.map(block2)
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function block2(node) {
|
|
794
1310
|
const children = node.content ?? [];
|
|
795
1311
|
switch (node.type) {
|
|
796
1312
|
case "paragraph":
|
|
@@ -815,16 +1331,83 @@ function serializeDocsMarkdown(input, frontmatter) {
|
|
|
815
1331
|
}
|
|
816
1332
|
]
|
|
817
1333
|
};
|
|
818
|
-
case "codeBlock":
|
|
1334
|
+
case "codeBlock": {
|
|
1335
|
+
const title = node.attrs?.title ?? null;
|
|
1336
|
+
const language = node.attrs?.language ?? null;
|
|
1337
|
+
const meta = title ? title.includes('"') ? `title='${title}'` : `title="${title}"` : null;
|
|
819
1338
|
return {
|
|
820
1339
|
type: "code",
|
|
821
|
-
lang:
|
|
1340
|
+
lang: language ?? meta,
|
|
1341
|
+
meta: language ? meta : null,
|
|
822
1342
|
value: children.map((child) => child.text ?? "").join("")
|
|
823
1343
|
};
|
|
1344
|
+
}
|
|
1345
|
+
case "callout":
|
|
1346
|
+
return element(
|
|
1347
|
+
"Callout",
|
|
1348
|
+
[prop("type", node.attrs?.type), prop("title", node.attrs?.title)],
|
|
1349
|
+
children
|
|
1350
|
+
);
|
|
1351
|
+
case "tabs": {
|
|
1352
|
+
const labels = children.map((tab) => quote(tab.attrs.label));
|
|
1353
|
+
return element(
|
|
1354
|
+
"Tabs",
|
|
1355
|
+
[prop("items", `[${labels.join(", ")}]`, true)],
|
|
1356
|
+
[],
|
|
1357
|
+
() => children.map(
|
|
1358
|
+
(tab) => element("Tab", [prop("value", tab.attrs.label)], tab.content ?? [])
|
|
1359
|
+
)
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
case "steps":
|
|
1363
|
+
return element(
|
|
1364
|
+
"Steps",
|
|
1365
|
+
[],
|
|
1366
|
+
[],
|
|
1367
|
+
() => children.map((step) => element("Step", [], step.content ?? []))
|
|
1368
|
+
);
|
|
1369
|
+
case "cards":
|
|
1370
|
+
return element(
|
|
1371
|
+
"Cards",
|
|
1372
|
+
[],
|
|
1373
|
+
[],
|
|
1374
|
+
() => children.map(
|
|
1375
|
+
(card) => element(
|
|
1376
|
+
"Card",
|
|
1377
|
+
[
|
|
1378
|
+
prop("title", card.attrs.title),
|
|
1379
|
+
prop("href", card.attrs?.href),
|
|
1380
|
+
prop("icon", card.attrs?.icon ? `<${card.attrs.icon} />` : null, true)
|
|
1381
|
+
],
|
|
1382
|
+
card.content ?? []
|
|
1383
|
+
)
|
|
1384
|
+
)
|
|
1385
|
+
);
|
|
1386
|
+
case "accordions":
|
|
1387
|
+
return element(
|
|
1388
|
+
"Accordions",
|
|
1389
|
+
[prop("type", node.attrs?.type)],
|
|
1390
|
+
[],
|
|
1391
|
+
() => children.map(
|
|
1392
|
+
(item) => element(
|
|
1393
|
+
"Accordion",
|
|
1394
|
+
[prop("title", item.attrs.title)],
|
|
1395
|
+
item.content ?? []
|
|
1396
|
+
)
|
|
1397
|
+
)
|
|
1398
|
+
);
|
|
1399
|
+
case "codeGroup":
|
|
1400
|
+
return element("CodeGroup", [], children);
|
|
1401
|
+
case "typeTable":
|
|
1402
|
+
return element(
|
|
1403
|
+
"TypeTable",
|
|
1404
|
+
[prop("type", formatRows(node.attrs?.rows ?? []), true)],
|
|
1405
|
+
[]
|
|
1406
|
+
);
|
|
824
1407
|
case "blockquote":
|
|
825
1408
|
return {
|
|
826
1409
|
type: "blockquote",
|
|
827
|
-
children: children.map(
|
|
1410
|
+
children: children.map(block2)
|
|
828
1411
|
};
|
|
829
1412
|
case "bulletList":
|
|
830
1413
|
case "orderedList":
|
|
@@ -836,7 +1419,7 @@ function serializeDocsMarkdown(input, frontmatter) {
|
|
|
836
1419
|
children: children.map((item) => ({
|
|
837
1420
|
type: "listItem",
|
|
838
1421
|
spread: true,
|
|
839
|
-
children: (item.content ?? []).map(
|
|
1422
|
+
children: (item.content ?? []).map(block2)
|
|
840
1423
|
}))
|
|
841
1424
|
};
|
|
842
1425
|
case "table":
|
|
@@ -856,15 +1439,18 @@ function serializeDocsMarkdown(input, frontmatter) {
|
|
|
856
1439
|
]);
|
|
857
1440
|
}
|
|
858
1441
|
}
|
|
859
|
-
const tree = { type: "root", children: (content.content ?? []).map(
|
|
1442
|
+
const tree = { type: "root", children: (content.content ?? []).map(block2) };
|
|
860
1443
|
const markdown = writer.stringify(tree);
|
|
861
1444
|
const roundtrip = parseDocsMarkdown(markdown, { frontmatter: false }).content;
|
|
862
|
-
const semantic = (
|
|
863
|
-
|
|
864
|
-
content
|
|
865
|
-
|
|
866
|
-
)
|
|
867
|
-
|
|
1445
|
+
const semantic = (node) => {
|
|
1446
|
+
if (!node.content) return node;
|
|
1447
|
+
const children = node.content.filter((child) => child.type !== "paragraph" || child.content?.length).map(semantic);
|
|
1448
|
+
const required = node.type !== "doc" && !docsSchema.nodes[node.type].contentMatch.validEnd;
|
|
1449
|
+
if (children.length) return { ...node, content: children };
|
|
1450
|
+
if (required) return { ...node, content: [{ type: "paragraph" }] };
|
|
1451
|
+
const { content: _empty, ...rest } = node;
|
|
1452
|
+
return node.type === "doc" ? { ...rest, content: [] } : rest;
|
|
1453
|
+
};
|
|
868
1454
|
if (JSON.stringify(semantic(roundtrip)) !== JSON.stringify(semantic(content)))
|
|
869
1455
|
throw new DocsContentError([
|
|
870
1456
|
diagnostic(
|