@andesine/converters 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Async Markdown, MDX, Markdoc, and HTML conversion, plus plain-text output.
6
+ - Configurable title, property, and fragment mappings.
7
+ - Async exact and wildcard handlers, native AST access, and a custom format adapter interface.
8
+ - Published-content image URL helpers with explicit variant selection and async callbacks, without network access or an SDK dependency.
9
+ - Shared heading anchors, HTML heading IDs, AST metadata, and `context.anchor` for custom renderers.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vrite, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,366 @@
1
+ # @andesine/converters
2
+
3
+ MIT-licensed, async converters for Andesine ProseMirror JSON. ESM only. Use in
4
+ Node.js 22+, browsers, and workers. No editor instance, DOM, API client, or network
5
+ connection is required. The library does not save content or upload assets.
6
+
7
+ ```sh
8
+ npm install @andesine/converters
9
+ ```
10
+
11
+ See the [release guide](https://github.com/vriteio/andesine/blob/andesine/packages/public/README.md)
12
+ for builds, archive checks, versioning, and npm publishing. This package uses the
13
+ [MIT license](./LICENSE).
14
+
15
+ ## Formats
16
+
17
+ | Import | Output | Input |
18
+ | ------------------------------- | ------------ | -------------- |
19
+ | `@andesine/converters/markdown` | `toMarkdown` | `fromMarkdown` |
20
+ | `@andesine/converters/mdx` | `toMDX` | `fromMDX` |
21
+ | `@andesine/converters/markdoc` | `toMarkdoc` | `fromMarkdoc` |
22
+ | `@andesine/converters/html` | `toHTML` | `fromHTML` |
23
+ | `@andesine/converters/text` | `toText` | — |
24
+
25
+ All conversion functions return promises. Callbacks and handlers may return a
26
+ value or a promise. Formats have separate entry points; there is no root export
27
+ that loads every parser. Installation includes all format dependencies.
28
+
29
+ Markdoc's declarations reference its React renderer types, so this package also
30
+ includes `@types/react` for strict TypeScript consumers. No React runtime is
31
+ required or loaded by the converters.
32
+
33
+ ```ts
34
+ import { toMarkdown, fromMarkdown } from "@andesine/converters/markdown";
35
+
36
+ const source = await toMarkdown(document);
37
+ const importedDocument = await fromMarkdown("# Title\n\nHello **world**.");
38
+ ```
39
+
40
+ Input converters return a `doc` with a title and body content. If no title is
41
+ mapped, the title is empty. Use `{ document: "fragment" }` to return body content
42
+ without a title. The same setting prevents the first heading from becoming a
43
+ title. Use `targetFragment: "body"` to put imported body content in a named
44
+ fragment.
45
+
46
+ ## Reusable mapping options
47
+
48
+ ```ts
49
+ import { createMDXConverter } from "@andesine/converters/mdx";
50
+
51
+ const converter = createMDXConverter({
52
+ title: { as: "frontmatter", key: "title" },
53
+ properties: { as: "frontmatter" },
54
+ fragments: {
55
+ default: "unwrap",
56
+ named: {
57
+ summary: { as: "element", name: "Fragment" },
58
+ internalNotes: "omit"
59
+ }
60
+ }
61
+ });
62
+
63
+ const source = await converter.encode(document);
64
+ const restored = await converter.decode(source);
65
+ ```
66
+
67
+ Factory names are `createMarkdownConverter`, `createMDXConverter`,
68
+ `createMarkdocConverter`, `createHTMLConverter`, and `createTextConverter`.
69
+ Per-call options override saved options; handler maps and named fragment mappings
70
+ merge by key.
71
+
72
+ | Field | Default | Other mappings |
73
+ | ---------- | ------------------- | ----------------------------------------------- |
74
+ | Title | First-level heading | `"omit"`, `{ as: "frontmatter", key: "title" }` |
75
+ | Properties | Omit | `{ as: "frontmatter" }` |
76
+ | Fragments | `"unwrap"` | `"omit"`, `{ as: "element", name: "Fragment" }` |
77
+
78
+ Unwrap removes the fragment boundary and retains its content in document order.
79
+ Omit removes the content too. The number of fragments does not change the default.
80
+ MDX wrappers become components; Markdoc wrappers become tags; HTML wrappers use
81
+ `data-andesine-element` and JSON `data-andesine-props` on a `div`. Markdown needs
82
+ a custom handler for fragment wrappers. HTML and plain text reject frontmatter
83
+ output; use their AST result or your own surrounding document for metadata.
84
+
85
+ On import, matching named wrappers become fragments. Unwrapped boundaries cannot
86
+ be recovered. With heading mapping, only the first root heading, at level one,
87
+ becomes the title. With frontmatter mapping, the selected field becomes the title.
88
+
89
+ Property labels become frontmatter keys. Duplicate keys, including a collision
90
+ with the title field, cause an error. Import infers text, number, checkbox, and
91
+ multi-select properties from scalar values and string arrays. Date, URL, and
92
+ select fields import as text; select options and schema metadata are not retained.
93
+ Object-valued properties need custom handling. Unmapped frontmatter is ignored.
94
+
95
+ ## Heading anchors
96
+
97
+ `getHeadingAnchors(document)` returns `{ path, text, anchor }` for each heading in
98
+ document order. It is exported from the package root and the dependency-free
99
+ `@andesine/converters/anchors` entry point. `path` is the array of child indexes
100
+ from the document root. The same helper supplies search and answer-source anchors.
101
+
102
+ Anchors use NFC-normalized lowercase heading text. Punctuation is removed,
103
+ whitespace and underscores become hyphens, and empty results use `section`.
104
+ Letters, numbers, and combining marks are retained. Collisions receive `-1`,
105
+ `-2`, and later suffixes until the ID is unique, including collisions with names
106
+ that already contain a suffix. All fragments share one counter. Titles and
107
+ properties do not create anchors or consume IDs.
108
+
109
+ HTML headings receive the ID automatically. Markdown and MDX AST headings carry
110
+ it in `data.hProperties.id`; Markdoc AST headings carry it in `attributes.id`.
111
+ String serialization for Markdown, MDX, and Markdoc does not preserve this AST
112
+ metadata. Configure the final renderer to apply these IDs, or use a custom
113
+ heading handler. Do not assume a renderer's own slug rules match Andesine.
114
+
115
+ Custom encode handlers receive `context.anchor` for original heading nodes:
116
+
117
+ ```ts
118
+ const html = await toHTML(document, {
119
+ encode: {
120
+ nodes: {
121
+ heading: async (node, context) => ({
122
+ type: "element",
123
+ tagName: `h${node.attrs?.level || 1}`,
124
+ properties: { id: context.anchor },
125
+ children: await context.children(node)
126
+ })
127
+ }
128
+ }
129
+ });
130
+ ```
131
+
132
+ The converter calculates anchors before applying fragment mappings. Omitting a
133
+ fragment does not renumber later headings, but links to omitted headings have no
134
+ target. A renderer that reorders, replaces, or omits content is responsible for
135
+ adapting links. Anchors are stable within a content version, not across edits.
136
+ Plain text does not have link targets. Import does not retain supplied heading IDs;
137
+ anchors are derived again from the imported document.
138
+
139
+ ## Images
140
+
141
+ Published content already includes image URLs. Use them directly:
142
+
143
+ ```ts
144
+ import { publishedContentOptions } from "@andesine/converters";
145
+ import { toMarkdown } from "@andesine/converters/markdown";
146
+
147
+ const page = await client.content.get({ path: "/Docs/Getting started" });
148
+ const source = await toMarkdown(page.content, {
149
+ ...publishedContentOptions(page),
150
+ title: { as: "frontmatter" }
151
+ });
152
+ ```
153
+
154
+ `publishedContentOptions(page, options?)` returns only an `imageURL` callback.
155
+ It works with single published-content responses and full items from
156
+ `content.listEntries`. It does not change title, property, or fragment mappings.
157
+ The converter package has no SDK dependency; any object with a compatible
158
+ `assets` array works.
159
+
160
+ `createImageURLResolver(assets, options?)` returns that async callback directly.
161
+ Each asset needs `assetID`, `variant`, and `url`. The default variant is `display`.
162
+ Set `{ variant: "thumbnail" }`, or select a variant per image:
163
+
164
+ ```ts
165
+ const options = publishedContentOptions(page, {
166
+ variant: async (image) => (image.attrs?.small ? "thumbnail" : "display")
167
+ });
168
+ ```
169
+
170
+ The resolver returns the supplied URL unchanged. It does not fetch images, build
171
+ URLs, or fall back to another variant. Missing assets or variants throw
172
+ `ConversionError`. Keep assets from the same entry and snapshot as the content.
173
+ An explicit `imageURL` callback can override this helper through the normal
174
+ converter options. Import still needs your own `imageAssetID` callback.
175
+
176
+ Images need reference resolution in both directions:
177
+
178
+ ```ts
179
+ const source = await toMarkdown(document, {
180
+ imageURL: async (image) => {
181
+ const assetID = String(image.attrs?.assetID);
182
+ return assets.get(assetID)!.url;
183
+ }
184
+ });
185
+
186
+ const importedDocument = await fromMarkdown(source, {
187
+ imageAssetID: async ({ url, alt, title }) => resolveAsset(url, alt, title)
188
+ });
189
+ ```
190
+
191
+ Callbacks return a URL or asset ID. A missing resolution causes an error. The
192
+ caller may perform network work in callbacks; the library performs none itself.
193
+ Plain text uses alt text, or the caption when alt text is absent, without resolving
194
+ an image URL. Published snapshot URLs may expire; decide where exported assets
195
+ will live before resolving them.
196
+
197
+ ## Custom and wildcard handlers
198
+
199
+ Handlers can return a node, an array of nodes, or `null` to omit content. Exact
200
+ element handlers take precedence over wildcard element handlers. Node and mark
201
+ handlers also support `"*"`. Custom handlers take precedence over built-ins.
202
+
203
+ ```ts
204
+ import { createMDXConverter, mdxAdapter } from "@andesine/converters/mdx";
205
+
206
+ const converter = createMDXConverter({
207
+ encode: {
208
+ elements: {
209
+ "Callout": async (node, context) =>
210
+ mdxAdapter.encode(
211
+ {
212
+ ...node,
213
+ attrs: { ...node.attrs, name: "Notice" }
214
+ },
215
+ context
216
+ ),
217
+ "*": async (node, context) =>
218
+ mdxAdapter.encode(
219
+ {
220
+ ...node,
221
+ attrs: { ...node.attrs, name: `Components.${node.attrs?.name}` }
222
+ },
223
+ context
224
+ )
225
+ }
226
+ }
227
+ });
228
+ ```
229
+
230
+ The built-in MDX element handler already maps arbitrary Andesine element names
231
+ and static properties to MDX elements. Wildcards let you change that mapping for
232
+ all elements. Decode handlers are independent: define a matching handler when
233
+ custom output should convert back to the original element.
234
+
235
+ ```ts
236
+ import { element } from "@andesine/converters";
237
+
238
+ const converter = createMDXConverter({
239
+ decode: {
240
+ elements: {
241
+ Notice: async (node, context) => {
242
+ if (node.type !== "mdxJsxFlowElement") {
243
+ throw new Error("Notice must be a block element");
244
+ }
245
+ return element("Callout", {}, await context.children(node.children));
246
+ }
247
+ }
248
+ }
249
+ });
250
+ ```
251
+
252
+ Encoder contexts expose `encode`, `children`, `imageURL`, options, and the content
253
+ path. Decoder contexts expose `decode`, `children`, `image`, options, and the AST
254
+ path. Calling the adapter's built-in handler bypasses custom dispatch; calling
255
+ `context.encode` or `context.decode` uses custom dispatch again. Avoid passing the
256
+ same unchanged node back through dispatch from its own handler.
257
+
258
+ `encode.nodes` is keyed by Andesine node type; `encode.marks` by mark type.
259
+ `decode.nodes` is keyed by native AST type (HTML uses tag names for elements).
260
+ `decode.marks` uses Andesine mark names. `decode.elements` uses component/tag names.
261
+ A node wildcard also matches the document root; delegate roots if your wildcard
262
+ is intended to omit only unknown leaves.
263
+
264
+ ## Syntax trees and custom formats
265
+
266
+ Each bidirectional format exports `to…AST` and `from…AST`. They use
267
+ `{ tree, frontmatter }`, so metadata is separate from the format tree. The trees are
268
+ mdast for Markdown/MDX, Markdoc nodes for Markdoc, and hast for HTML. Plain text
269
+ exports `toTextAST` with a small text tree.
270
+
271
+ ```ts
272
+ const result = await converter.toAST(document);
273
+ // Inspect or change result.tree and result.frontmatter.
274
+ const restored = await converter.fromAST(result);
275
+ ```
276
+
277
+ To add a format, implement `FormatAdapter<T>` and call `createConverter(adapter)`.
278
+ The adapter supplies root creation, parsing, stringification, node/mark encoding,
279
+ decoding, and optional element/mark name lookup. The core supplies async traversal,
280
+ custom handler dispatch, title/property/fragment mapping, and image callbacks.
281
+ It does not require a Markdown tree.
282
+
283
+ For example, this small custom format stores document text as a JSON array. It is
284
+ intentionally lossy and supports only paragraphs and text:
285
+
286
+ ```ts
287
+ import { createConverter, ConversionError, type FormatAdapter } from "@andesine/converters";
288
+
289
+ interface ArrayNode {
290
+ type: "root" | "paragraph" | "text";
291
+ value?: string;
292
+ children?: ArrayNode[];
293
+ }
294
+
295
+ const adapter: FormatAdapter<ArrayNode> = {
296
+ name: "Paragraph array",
297
+ type: (node) => node.type,
298
+ root: (children) => ({ type: "root", children }),
299
+ parse: (source) => ({
300
+ type: "root",
301
+ children: (JSON.parse(source) as string[]).map((value) => ({
302
+ type: "paragraph",
303
+ children: [{ type: "text", value }]
304
+ }))
305
+ }),
306
+ stringify: (root) =>
307
+ JSON.stringify(
308
+ root.children?.map((node) => node.children?.map((child) => child.value || "").join(""))
309
+ ),
310
+ async encode(node, context) {
311
+ if (node.type === "text") return { type: "text", value: node.text };
312
+ if (node.type === "paragraph")
313
+ return { type: "paragraph", children: await context.children(node) };
314
+ throw new ConversionError(`Unsupported node: ${node.type}`);
315
+ },
316
+ mark: (_mark, children) => children,
317
+ async decode(node, context) {
318
+ if (node.type === "text") return { type: "text", text: node.value || "" };
319
+ const content = await context.children(node.children || []);
320
+ return node.type === "root" ? content : { type: "paragraph", content };
321
+ }
322
+ };
323
+
324
+ const converter = createConverter(adapter, { title: "omit", document: "fragment" });
325
+ ```
326
+
327
+ ## Preservation and limits
328
+
329
+ These are content conversions, not document backups. They preserve the supported
330
+ content subset, not exact source text. Node IDs, schema links, inherited-field
331
+ metadata, editor layout, original whitespace, and source syntax choices are not
332
+ preserved. There is no detailed loss report. Unsupported nodes, marks, or syntax
333
+ throw; explicit mappings and the losses below do not.
334
+
335
+ | Feature | Markdown / MDX | Markdoc | HTML | Plain text |
336
+ | ----------------------------------- | ----------------------------------------- | -------------------------------- | --------------------------------- | ------------------- |
337
+ | Paragraphs, headings, quotes, lists | Yes | Yes | Yes | Text and separators |
338
+ | Bold, italic, strike, code, links | Yes | Yes | Yes | Formatting removed |
339
+ | Subscript, superscript, highlight | Custom handlers | Custom handlers | Yes; highlight color omitted | Formatting removed |
340
+ | Task lists | GFM | Custom handlers | Andesine data attributes | Checkbox markers |
341
+ | Code blocks | Language retained | Language retained | Language retained | Code text |
342
+ | Images | URL, alt, caption as title | URL, alt, caption as title | Image and optional figure caption | Alt/caption text |
343
+ | Tables | GFM; single-paragraph cells, no spans | Single-paragraph cells, no spans | Cell spans and block content | Tab-separated cells |
344
+ | Custom elements | MD: custom handler; MDX: static block JSX | Static tags | Andesine data attributes | Custom handler |
345
+
346
+ Markdown-like tables normalize the first row to headers. Alignment, column widths,
347
+ link targets/titles, image size, and aspect ratio are not preserved. Empty
348
+ paragraphs can collapse in Markdown-like output. Mark ordering and adjacent text
349
+ node boundaries can change without changing displayed content.
350
+
351
+ MDX supports block components with static JSON-compatible properties. Expressions
352
+ inside property values may contain literal arrays/objects, but no calls, variable
353
+ references, spreads, imports, exports, or executable code. Inline JSX and JSX
354
+ fragments require custom handlers. Markdoc variables/functions also require custom
355
+ handlers. No source is evaluated during conversion.
356
+
357
+ Mixed bullet/task lists import as task lists with unchecked plain items. Ordered
358
+ task lists, Markdown footnotes, and raw HTML require custom handlers. HTML import supports
359
+ semantic elements, not arbitrary CSS/layout. Whitespace follows basic HTML
360
+ normalization. Scripts, styles, comments, and unknown elements require explicit
361
+ handlers. Built-in HTML output escapes text and rejects unsafe URL schemes; custom
362
+ handlers are responsible for the output they create. This is not an HTML sanitizer.
363
+
364
+ Import is not full editor-schema validation. A custom handler must return content
365
+ that is valid for its destination, including block/inline placement and allowed
366
+ fragment children. If you need a document backup, retain the original JSON.
@@ -0,0 +1,18 @@
1
+ import { r as ContentNode } from "./chunks/types-SDFWRP3z.js";
2
+ //#region src/anchors.d.ts
3
+ interface HeadingAnchor {
4
+ path: readonly number[];
5
+ text: string;
6
+ anchor: string;
7
+ }
8
+ /**
9
+ * Compute heading anchors that match Andesine search results and citations.
10
+ * @param document - The full ProseMirror document. Titles and properties are excluded.
11
+ * @returns Headings in document order with text, child-index path, and unique anchor.
12
+ * Duplicate anchors receive numeric suffixes. Anchors are stable within a content version;
13
+ * converting a fragment alone can produce different suffixes than converting the full document.
14
+ */
15
+ declare const getHeadingAnchors: (document: ContentNode) => HeadingAnchor[];
16
+ //#endregion
17
+ export { type HeadingAnchor, getHeadingAnchors };
18
+ //# sourceMappingURL=anchors.d.ts.map
@@ -0,0 +1,36 @@
1
+ //#region src/anchors.ts
2
+ const getHeadingText = (node) => node.type === "hardBreak" ? " " : node.text ?? (node.content || []).map(getHeadingText).join("");
3
+ /**
4
+ * Compute heading anchors that match Andesine search results and citations.
5
+ * @param document - The full ProseMirror document. Titles and properties are excluded.
6
+ * @returns Headings in document order with text, child-index path, and unique anchor.
7
+ * Duplicate anchors receive numeric suffixes. Anchors are stable within a content version;
8
+ * converting a fragment alone can produce different suffixes than converting the full document.
9
+ */
10
+ const getHeadingAnchors = (document) => {
11
+ const headings = [];
12
+ const used = /* @__PURE__ */ new Set();
13
+ const visit = (node, path) => {
14
+ if (node.type === "title" || node.type === "property") return;
15
+ if (node.type === "heading") {
16
+ const text = getHeadingText(node).normalize("NFC").replace(/\s+/g, " ").trim();
17
+ const base = text.toLowerCase().replace(/[^\p{L}\p{N}\p{M}\s_-]/gu, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "section";
18
+ let anchor = base;
19
+ let suffix = 1;
20
+ while (used.has(anchor)) anchor = `${base}-${suffix++}`;
21
+ used.add(anchor);
22
+ headings.push({
23
+ path,
24
+ text,
25
+ anchor
26
+ });
27
+ }
28
+ (node.content || []).forEach((child, index) => visit(child, [...path, index]));
29
+ };
30
+ visit(document, []);
31
+ return headings;
32
+ };
33
+ //#endregion
34
+ export { getHeadingAnchors };
35
+
36
+ //# sourceMappingURL=anchors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anchors.js","names":[],"sources":["../src/anchors.ts"],"sourcesContent":["import type { ContentNode } from \"./types\";\n\ninterface HeadingAnchor {\n path: readonly number[];\n text: string;\n anchor: string;\n}\n\nconst getHeadingText = (node: ContentNode): string =>\n node.type === \"hardBreak\"\n ? \" \"\n : (node.text ?? (node.content || []).map(getHeadingText).join(\"\"));\n/**\n * Compute heading anchors that match Andesine search results and citations.\n * @param document - The full ProseMirror document. Titles and properties are excluded.\n * @returns Headings in document order with text, child-index path, and unique anchor.\n * Duplicate anchors receive numeric suffixes. Anchors are stable within a content version;\n * converting a fragment alone can produce different suffixes than converting the full document.\n */\nconst getHeadingAnchors = (document: ContentNode): HeadingAnchor[] => {\n const headings: HeadingAnchor[] = [];\n const used = new Set<string>();\n const visit = (node: ContentNode, path: number[]): void => {\n if (node.type === \"title\" || node.type === \"property\") return;\n\n if (node.type === \"heading\") {\n const text = getHeadingText(node).normalize(\"NFC\").replace(/\\s+/g, \" \").trim();\n const base =\n text\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}\\p{M}\\s_-]/gu, \"\")\n .replace(/[\\s_]+/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\") || \"section\";\n\n let anchor = base;\n let suffix = 1;\n\n while (used.has(anchor)) anchor = `${base}-${suffix++}`;\n\n used.add(anchor);\n headings.push({ path, text, anchor });\n }\n\n (node.content || []).forEach((child, index) => visit(child, [...path, index]));\n };\n\n visit(document, []);\n\n return headings;\n};\n\nexport { getHeadingAnchors };\nexport type { HeadingAnchor };\n"],"mappings":";AAQA,MAAM,kBAAkB,SACtB,KAAK,SAAS,cACV,MACC,KAAK,SAAS,KAAK,WAAW,CAAC,EAAA,CAAG,IAAI,cAAc,CAAC,CAAC,KAAK,EAAE;;;;;;;;AAQpE,MAAM,qBAAqB,aAA2C;CACpE,MAAM,WAA4B,CAAC;CACnC,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAS,MAAmB,SAAyB;EACzD,IAAI,KAAK,SAAS,WAAW,KAAK,SAAS,YAAY;EAEvD,IAAI,KAAK,SAAS,WAAW;GAC3B,MAAM,OAAO,eAAe,IAAI,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,KAAK;GAC7E,MAAM,OACJ,KACG,YAAY,CAAC,CACb,QAAQ,4BAA4B,EAAE,CAAC,CACvC,QAAQ,WAAW,GAAG,CAAC,CACvB,QAAQ,OAAO,GAAG,CAAC,CACnB,QAAQ,UAAU,EAAE,KAAK;GAE9B,IAAI,SAAS;GACb,IAAI,SAAS;GAEb,OAAO,KAAK,IAAI,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG;GAE7C,KAAK,IAAI,MAAM;GACf,SAAS,KAAK;IAAE;IAAM;IAAM;GAAO,CAAC;EACtC;EAEA,CAAC,KAAK,WAAW,CAAC,EAAA,CAAG,SAAS,OAAO,UAAU,MAAM,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC,CAAC;CAC/E;CAEA,MAAM,UAAU,CAAC,CAAC;CAElB,OAAO;AACT"}
@@ -0,0 +1,177 @@
1
+ import { c as textContent, i as ConversionError } from "./core-DC64ddB7.js";
2
+ import { t as decodeMarkdown } from "./decode-B-xzTJFV.js";
3
+ import { n as normalizeMarkdown, t as markdownStringifyOptions } from "./stringify-DdPaV2y4.js";
4
+ import { unified } from "unified";
5
+ import remarkParse from "remark-parse";
6
+ import remarkStringify from "remark-stringify";
7
+ import remarkGFM from "remark-gfm";
8
+ //#region src/markdown/encode.ts
9
+ const encodeMarkdown = async (node, context) => {
10
+ const attrs = node.attrs || {};
11
+ const children = await context.children(node);
12
+ switch (node.type) {
13
+ case "text": return {
14
+ type: "text",
15
+ value: node.text || ""
16
+ };
17
+ case "paragraph": return {
18
+ type: "paragraph",
19
+ children
20
+ };
21
+ case "heading": return {
22
+ type: "heading",
23
+ ...context.anchor ? { data: { hProperties: { id: context.anchor } } } : {},
24
+ depth: Number(attrs.level || 1),
25
+ children
26
+ };
27
+ case "blockquote": return {
28
+ type: "blockquote",
29
+ children
30
+ };
31
+ case "bulletList":
32
+ case "orderedList":
33
+ case "taskList": return {
34
+ type: "list",
35
+ ordered: node.type === "orderedList",
36
+ ...node.type === "orderedList" ? { start: Number(attrs.start || 1) } : {},
37
+ spread: false,
38
+ children
39
+ };
40
+ case "listItem":
41
+ case "taskItem": return {
42
+ type: "listItem",
43
+ ...node.type === "taskItem" ? { checked: Boolean(attrs.checked) } : {},
44
+ spread: false,
45
+ children
46
+ };
47
+ case "hardBreak": return { type: "break" };
48
+ case "horizontalRule": return { type: "thematicBreak" };
49
+ case "codeBlock": return {
50
+ type: "code",
51
+ lang: typeof attrs.language === "string" ? attrs.language : null,
52
+ value: textContent(node)
53
+ };
54
+ case "image": return {
55
+ type: "image",
56
+ url: await context.imageURL(node),
57
+ alt: String(attrs.alt || ""),
58
+ title: attrs.caption ? String(attrs.caption) : null
59
+ };
60
+ case "table": return {
61
+ type: "table",
62
+ children
63
+ };
64
+ case "tableRow": return {
65
+ type: "tableRow",
66
+ children
67
+ };
68
+ case "tableCell":
69
+ case "tableHeader":
70
+ if (Number(attrs.colspan || 1) !== 1 || Number(attrs.rowspan || 1) !== 1 || children.some((child) => child.type !== "paragraph") || children.length > 1) throw new ConversionError("Markdown tables require single-paragraph cells without spans", context.path);
71
+ return {
72
+ type: "tableCell",
73
+ children: children.flatMap((child) => child.type === "paragraph" ? child.children : [])
74
+ };
75
+ default: throw new ConversionError(`Unsupported Markdown node: ${node.type}`, context.path);
76
+ }
77
+ };
78
+ const encodeMarkdownMark = (mark, children, context) => {
79
+ const content = children;
80
+ switch (mark.type) {
81
+ case "bold": return {
82
+ type: "strong",
83
+ children: content
84
+ };
85
+ case "italic": return {
86
+ type: "emphasis",
87
+ children: content
88
+ };
89
+ case "strike": return {
90
+ type: "delete",
91
+ children: content
92
+ };
93
+ case "link": return {
94
+ type: "link",
95
+ url: String(mark.attrs?.href || ""),
96
+ title: null,
97
+ children: content
98
+ };
99
+ case "code":
100
+ if (children.some((node) => node.type !== "text")) throw new ConversionError("Code marks must wrap plain text", context.path);
101
+ return {
102
+ type: "inlineCode",
103
+ value: children.map((node) => node.type === "text" ? node.value : "").join("")
104
+ };
105
+ default: throw new ConversionError(`Unsupported Markdown mark: ${mark.type}`, context.path);
106
+ }
107
+ };
108
+ //#endregion
109
+ //#region src/markdown/adapter.ts
110
+ const createMarkdownAdapter = (mdx) => {
111
+ const processor = unified().use(remarkParse).use(remarkGFM).use(remarkStringify, markdownStringifyOptions);
112
+ if (mdx) processor.use(mdx);
113
+ const marks = {
114
+ strong: "bold",
115
+ emphasis: "italic",
116
+ delete: "strike",
117
+ inlineCode: "code",
118
+ link: "link"
119
+ };
120
+ return {
121
+ name: mdx ? "MDX" : "Markdown",
122
+ frontmatter: true,
123
+ type: (node) => node.type,
124
+ markName: (node) => marks[node.type],
125
+ root: (children) => ({
126
+ type: "root",
127
+ children: children.map((node) => node.type === "image" ? {
128
+ type: "paragraph",
129
+ children: [node]
130
+ } : node)
131
+ }),
132
+ parse(source) {
133
+ const root = processor.parse(source);
134
+ const definitions = /* @__PURE__ */ new Map();
135
+ const collectDefinitions = (node) => {
136
+ if (node.type === "definition") {
137
+ const identifier = node.identifier.toUpperCase();
138
+ if (!definitions.has(identifier)) definitions.set(identifier, node);
139
+ }
140
+ if ("children" in node) node.children.forEach(collectDefinitions);
141
+ };
142
+ const resolve = (node) => {
143
+ if (node.type === "linkReference" || node.type === "imageReference") {
144
+ const definition = definitions.get(node.identifier.toUpperCase());
145
+ if (!definition) throw new ConversionError(`Missing Markdown definition: ${node.identifier}`);
146
+ if (node.type === "imageReference") return {
147
+ type: "image",
148
+ url: definition.url,
149
+ title: definition.title,
150
+ alt: node.alt
151
+ };
152
+ return {
153
+ type: "link",
154
+ url: definition.url,
155
+ title: definition.title,
156
+ children: node.children.map(resolve)
157
+ };
158
+ }
159
+ if ("children" in node) return {
160
+ ...node,
161
+ children: node.children.map(resolve)
162
+ };
163
+ return node;
164
+ };
165
+ collectDefinitions(root);
166
+ return resolve(root);
167
+ },
168
+ stringify: (root) => processor.stringify(normalizeMarkdown(root)),
169
+ encode: (node, context) => encodeMarkdown(node, context),
170
+ mark: (mark, children, context) => encodeMarkdownMark(mark, children, context),
171
+ decode: decodeMarkdown
172
+ };
173
+ };
174
+ //#endregion
175
+ export { createMarkdownAdapter as t };
176
+
177
+ //# sourceMappingURL=adapter-CeNgvE5W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-CeNgvE5W.js","names":[],"sources":["../../src/markdown/encode.ts","../../src/markdown/adapter.ts"],"sourcesContent":["import type {\n RootContent,\n PhrasingContent,\n BlockContent,\n ListContent,\n TableContent,\n RowContent\n} from \"mdast\";\nimport { ConversionError, textContent } from \"../utils\";\nimport type { ContentMark, ContentNode, EncodeContext } from \"../types\";\n\nconst encodeMarkdown = async (\n node: ContentNode,\n context: EncodeContext<RootContent>\n): Promise<RootContent> => {\n const attrs = node.attrs || {};\n const children = await context.children(node);\n\n switch (node.type) {\n case \"text\":\n return { type: \"text\", value: node.text || \"\" };\n case \"paragraph\":\n return { type: \"paragraph\", children: children as PhrasingContent[] };\n case \"heading\":\n return {\n type: \"heading\",\n ...(context.anchor ? { data: { hProperties: { id: context.anchor } } } : {}),\n depth: Number(attrs.level || 1) as 1 | 2 | 3 | 4 | 5 | 6,\n children: children as PhrasingContent[]\n };\n case \"blockquote\":\n return { type: \"blockquote\", children: children as BlockContent[] };\n case \"bulletList\":\n case \"orderedList\":\n case \"taskList\":\n return {\n type: \"list\",\n ordered: node.type === \"orderedList\",\n ...(node.type === \"orderedList\" ? { start: Number(attrs.start || 1) } : {}),\n spread: false,\n children: children as ListContent[]\n };\n case \"listItem\":\n case \"taskItem\":\n return {\n type: \"listItem\",\n ...(node.type === \"taskItem\" ? { checked: Boolean(attrs.checked) } : {}),\n spread: false,\n children: children as BlockContent[]\n };\n case \"hardBreak\":\n return { type: \"break\" };\n case \"horizontalRule\":\n return { type: \"thematicBreak\" };\n case \"codeBlock\":\n return {\n type: \"code\",\n lang: typeof attrs.language === \"string\" ? attrs.language : null,\n value: textContent(node)\n };\n case \"image\":\n return {\n type: \"image\",\n url: await context.imageURL(node),\n alt: String(attrs.alt || \"\"),\n title: attrs.caption ? String(attrs.caption) : null\n };\n case \"table\":\n return { type: \"table\", children: children as TableContent[] };\n case \"tableRow\":\n return { type: \"tableRow\", children: children as RowContent[] };\n case \"tableCell\":\n case \"tableHeader\": {\n if (\n Number(attrs.colspan || 1) !== 1 ||\n Number(attrs.rowspan || 1) !== 1 ||\n children.some((child) => child.type !== \"paragraph\") ||\n children.length > 1\n ) {\n throw new ConversionError(\n \"Markdown tables require single-paragraph cells without spans\",\n context.path\n );\n }\n return {\n type: \"tableCell\",\n children: children.flatMap((child) => (child.type === \"paragraph\" ? child.children : []))\n };\n }\n default:\n throw new ConversionError(`Unsupported Markdown node: ${node.type}`, context.path);\n }\n};\nconst encodeMarkdownMark = (\n mark: ContentMark,\n children: RootContent[],\n context: EncodeContext<RootContent>\n): RootContent => {\n const content = children as PhrasingContent[];\n\n switch (mark.type) {\n case \"bold\":\n return { type: \"strong\", children: content };\n case \"italic\":\n return { type: \"emphasis\", children: content };\n case \"strike\":\n return { type: \"delete\", children: content };\n case \"link\":\n return { type: \"link\", url: String(mark.attrs?.href || \"\"), title: null, children: content };\n case \"code\": {\n if (children.some((node) => node.type !== \"text\"))\n throw new ConversionError(\"Code marks must wrap plain text\", context.path);\n\n return {\n type: \"inlineCode\",\n value: children.map((node) => (node.type === \"text\" ? node.value : \"\")).join(\"\")\n };\n }\n default:\n throw new ConversionError(`Unsupported Markdown mark: ${mark.type}`, context.path);\n }\n};\n\nexport { encodeMarkdown, encodeMarkdownMark };\n","import { unified, type Plugin } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkStringify from \"remark-stringify\";\nimport remarkGFM from \"remark-gfm\";\nimport type { Definition, PhrasingContent, Root, RootContent } from \"mdast\";\nimport { encodeMarkdown, encodeMarkdownMark } from \"./encode\";\nimport { decodeMarkdown, type MarkdownNode } from \"./decode\";\nimport { normalizeMarkdown } from \"./normalize\";\nimport { markdownStringifyOptions } from \"./stringify\";\nimport { ConversionError } from \"../utils\";\nimport type { FormatAdapter } from \"../types\";\n\nconst createMarkdownAdapter = (mdx?: Plugin): FormatAdapter<MarkdownNode> => {\n const processor = unified()\n .use(remarkParse)\n .use(remarkGFM)\n .use(remarkStringify, markdownStringifyOptions);\n\n if (mdx) processor.use(mdx);\n\n const marks: Record<string, string> = {\n strong: \"bold\",\n emphasis: \"italic\",\n delete: \"strike\",\n inlineCode: \"code\",\n link: \"link\"\n };\n\n return {\n name: mdx ? \"MDX\" : \"Markdown\",\n frontmatter: true,\n type: (node) => node.type,\n markName: (node) => marks[node.type],\n root: (children) => ({\n type: \"root\",\n children: children.map((node) =>\n node.type === \"image\" ? { type: \"paragraph\", children: [node] } : node\n ) as RootContent[]\n }),\n parse(source) {\n const root = processor.parse(source);\n const definitions = new Map<string, Definition>();\n const collectDefinitions = (node: MarkdownNode): void => {\n if (node.type === \"definition\") {\n const identifier = node.identifier.toUpperCase();\n\n if (!definitions.has(identifier)) definitions.set(identifier, node);\n }\n\n if (\"children\" in node) node.children.forEach(collectDefinitions);\n };\n const resolve = (node: MarkdownNode): MarkdownNode => {\n if (node.type === \"linkReference\" || node.type === \"imageReference\") {\n const definition = definitions.get(node.identifier.toUpperCase());\n\n if (!definition)\n throw new ConversionError(`Missing Markdown definition: ${node.identifier}`);\n if (node.type === \"imageReference\")\n return { type: \"image\", url: definition.url, title: definition.title, alt: node.alt };\n\n return {\n type: \"link\",\n url: definition.url,\n title: definition.title,\n children: node.children.map(resolve) as PhrasingContent[]\n };\n }\n if (\"children\" in node)\n return { ...node, children: node.children.map(resolve) } as MarkdownNode;\n\n return node;\n };\n collectDefinitions(root);\n return resolve(root);\n },\n stringify: (root) => processor.stringify(normalizeMarkdown(root as Root)),\n encode: (node, context) =>\n encodeMarkdown(node, context as Parameters<typeof encodeMarkdown>[1]),\n mark: (mark, children, context) =>\n encodeMarkdownMark(\n mark,\n children as RootContent[],\n context as Parameters<typeof encodeMarkdownMark>[2]\n ),\n decode: decodeMarkdown\n };\n};\n\nexport { createMarkdownAdapter };\n"],"mappings":";;;;;;;;AAWA,MAAM,iBAAiB,OACrB,MACA,YACyB;CACzB,MAAM,QAAQ,KAAK,SAAS,CAAC;CAC7B,MAAM,WAAW,MAAM,QAAQ,SAAS,IAAI;CAE5C,QAAQ,KAAK,MAAb;EACE,KAAK,QACH,OAAO;GAAE,MAAM;GAAQ,OAAO,KAAK,QAAQ;EAAG;EAChD,KAAK,aACH,OAAO;GAAE,MAAM;GAAuB;EAA8B;EACtE,KAAK,WACH,OAAO;GACL,MAAM;GACN,GAAI,QAAQ,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,QAAQ,OAAO,EAAE,EAAE,IAAI,CAAC;GAC1E,OAAO,OAAO,MAAM,SAAS,CAAC;GACpB;EACZ;EACF,KAAK,cACH,OAAO;GAAE,MAAM;GAAwB;EAA2B;EACpE,KAAK;EACL,KAAK;EACL,KAAK,YACH,OAAO;GACL,MAAM;GACN,SAAS,KAAK,SAAS;GACvB,GAAI,KAAK,SAAS,gBAAgB,EAAE,OAAO,OAAO,MAAM,SAAS,CAAC,EAAE,IAAI,CAAC;GACzE,QAAQ;GACE;EACZ;EACF,KAAK;EACL,KAAK,YACH,OAAO;GACL,MAAM;GACN,GAAI,KAAK,SAAS,aAAa,EAAE,SAAS,QAAQ,MAAM,OAAO,EAAE,IAAI,CAAC;GACtE,QAAQ;GACE;EACZ;EACF,KAAK,aACH,OAAO,EAAE,MAAM,QAAQ;EACzB,KAAK,kBACH,OAAO,EAAE,MAAM,gBAAgB;EACjC,KAAK,aACH,OAAO;GACL,MAAM;GACN,MAAM,OAAO,MAAM,aAAa,WAAW,MAAM,WAAW;GAC5D,OAAO,YAAY,IAAI;EACzB;EACF,KAAK,SACH,OAAO;GACL,MAAM;GACN,KAAK,MAAM,QAAQ,SAAS,IAAI;GAChC,KAAK,OAAO,MAAM,OAAO,EAAE;GAC3B,OAAO,MAAM,UAAU,OAAO,MAAM,OAAO,IAAI;EACjD;EACF,KAAK,SACH,OAAO;GAAE,MAAM;GAAmB;EAA2B;EAC/D,KAAK,YACH,OAAO;GAAE,MAAM;GAAsB;EAAyB;EAChE,KAAK;EACL,KAAK;GACH,IACE,OAAO,MAAM,WAAW,CAAC,MAAM,KAC/B,OAAO,MAAM,WAAW,CAAC,MAAM,KAC/B,SAAS,MAAM,UAAU,MAAM,SAAS,WAAW,KACnD,SAAS,SAAS,GAElB,MAAM,IAAI,gBACR,gEACA,QAAQ,IACV;GAEF,OAAO;IACL,MAAM;IACN,UAAU,SAAS,SAAS,UAAW,MAAM,SAAS,cAAc,MAAM,WAAW,CAAC,CAAE;GAC1F;EAEF,SACE,MAAM,IAAI,gBAAgB,8BAA8B,KAAK,QAAQ,QAAQ,IAAI;CACrF;AACF;AACA,MAAM,sBACJ,MACA,UACA,YACgB;CAChB,MAAM,UAAU;CAEhB,QAAQ,KAAK,MAAb;EACE,KAAK,QACH,OAAO;GAAE,MAAM;GAAU,UAAU;EAAQ;EAC7C,KAAK,UACH,OAAO;GAAE,MAAM;GAAY,UAAU;EAAQ;EAC/C,KAAK,UACH,OAAO;GAAE,MAAM;GAAU,UAAU;EAAQ;EAC7C,KAAK,QACH,OAAO;GAAE,MAAM;GAAQ,KAAK,OAAO,KAAK,OAAO,QAAQ,EAAE;GAAG,OAAO;GAAM,UAAU;EAAQ;EAC7F,KAAK;GACH,IAAI,SAAS,MAAM,SAAS,KAAK,SAAS,MAAM,GAC9C,MAAM,IAAI,gBAAgB,mCAAmC,QAAQ,IAAI;GAE3E,OAAO;IACL,MAAM;IACN,OAAO,SAAS,KAAK,SAAU,KAAK,SAAS,SAAS,KAAK,QAAQ,EAAG,CAAC,CAAC,KAAK,EAAE;GACjF;EAEF,SACE,MAAM,IAAI,gBAAgB,8BAA8B,KAAK,QAAQ,QAAQ,IAAI;CACrF;AACF;;;AC7GA,MAAM,yBAAyB,QAA8C;CAC3E,MAAM,YAAY,QAAQ,CAAC,CACxB,IAAI,WAAW,CAAC,CAChB,IAAI,SAAS,CAAC,CACd,IAAI,iBAAiB,wBAAwB;CAEhD,IAAI,KAAK,UAAU,IAAI,GAAG;CAE1B,MAAM,QAAgC;EACpC,QAAQ;EACR,UAAU;EACV,QAAQ;EACR,YAAY;EACZ,MAAM;CACR;CAEA,OAAO;EACL,MAAM,MAAM,QAAQ;EACpB,aAAa;EACb,OAAO,SAAS,KAAK;EACrB,WAAW,SAAS,MAAM,KAAK;EAC/B,OAAO,cAAc;GACnB,MAAM;GACN,UAAU,SAAS,KAAK,SACtB,KAAK,SAAS,UAAU;IAAE,MAAM;IAAa,UAAU,CAAC,IAAI;GAAE,IAAI,IACpE;EACF;EACA,MAAM,QAAQ;GACZ,MAAM,OAAO,UAAU,MAAM,MAAM;GACnC,MAAM,8BAAc,IAAI,IAAwB;GAChD,MAAM,sBAAsB,SAA6B;IACvD,IAAI,KAAK,SAAS,cAAc;KAC9B,MAAM,aAAa,KAAK,WAAW,YAAY;KAE/C,IAAI,CAAC,YAAY,IAAI,UAAU,GAAG,YAAY,IAAI,YAAY,IAAI;IACpE;IAEA,IAAI,cAAc,MAAM,KAAK,SAAS,QAAQ,kBAAkB;GAClE;GACA,MAAM,WAAW,SAAqC;IACpD,IAAI,KAAK,SAAS,mBAAmB,KAAK,SAAS,kBAAkB;KACnE,MAAM,aAAa,YAAY,IAAI,KAAK,WAAW,YAAY,CAAC;KAEhE,IAAI,CAAC,YACH,MAAM,IAAI,gBAAgB,gCAAgC,KAAK,YAAY;KAC7E,IAAI,KAAK,SAAS,kBAChB,OAAO;MAAE,MAAM;MAAS,KAAK,WAAW;MAAK,OAAO,WAAW;MAAO,KAAK,KAAK;KAAI;KAEtF,OAAO;MACL,MAAM;MACN,KAAK,WAAW;MAChB,OAAO,WAAW;MAClB,UAAU,KAAK,SAAS,IAAI,OAAO;KACrC;IACF;IACA,IAAI,cAAc,MAChB,OAAO;KAAE,GAAG;KAAM,UAAU,KAAK,SAAS,IAAI,OAAO;IAAE;IAEzD,OAAO;GACT;GACA,mBAAmB,IAAI;GACvB,OAAO,QAAQ,IAAI;EACrB;EACA,YAAY,SAAS,UAAU,UAAU,kBAAkB,IAAY,CAAC;EACxE,SAAS,MAAM,YACb,eAAe,MAAM,OAA+C;EACtE,OAAO,MAAM,UAAU,YACrB,mBACE,MACA,UACA,OACF;EACF,QAAQ;CACV;AACF"}