@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.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 = 1;
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, "&amp;").replace(/"/g, "&quot;")}"`;
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 writer = unified().use(remarkGfm).use(remarkStringify, {
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 = parser.parse(source.body);
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
- if (marks.length) error(node, "Inline code cannot carry other marks.");
621
- return [{ type: "text", text: node.value, marks: [{ type: "code" }] }];
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 block(node) {
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(block) }];
1165
+ return [{ type: "blockquote", content: node.children.flatMap(block2) }];
684
1166
  case "thematicBreak":
685
1167
  return [{ type: "horizontalRule" }];
686
- case "code":
687
- if (node.meta)
688
- error(node, "Code fence metadata is unsupported.", "UNSUPPORTED_ATTRIBUTE");
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: { language: node.lang ?? null },
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(block);
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(block)
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
- function block(node) {
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: node.attrs?.language ?? null,
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(block)
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(block)
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(block) };
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 = (doc) => ({
863
- ...doc,
864
- content: doc.content?.filter(
865
- (node) => node.type !== "paragraph" || node.content?.length
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(