@remigius42/morg 0.4.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.
Files changed (67) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +267 -0
  3. package/dist/cli/args.d.ts +15 -0
  4. package/dist/cli/args.js +105 -0
  5. package/dist/cli/configFile.d.ts +2 -0
  6. package/dist/cli/configFile.js +18 -0
  7. package/dist/cli/conversion.d.ts +11 -0
  8. package/dist/cli/conversion.js +50 -0
  9. package/dist/cli/error.d.ts +2 -0
  10. package/dist/cli/error.js +4 -0
  11. package/dist/cli/formats.d.ts +3 -0
  12. package/dist/cli/formats.js +46 -0
  13. package/dist/cli/presets.d.ts +2 -0
  14. package/dist/cli/presets.js +10 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +54 -0
  17. package/dist/config.d.ts +20 -0
  18. package/dist/config.js +22 -0
  19. package/dist/conversionOptions.d.ts +32 -0
  20. package/dist/conversionOptions.js +34 -0
  21. package/dist/core/markdownStyle.d.ts +27 -0
  22. package/dist/core/markdownStyle.js +108 -0
  23. package/dist/core/mdastToUniorg/blocks.d.ts +20 -0
  24. package/dist/core/mdastToUniorg/blocks.js +211 -0
  25. package/dist/core/mdastToUniorg/context.d.ts +15 -0
  26. package/dist/core/mdastToUniorg/context.js +7 -0
  27. package/dist/core/mdastToUniorg/index.d.ts +13 -0
  28. package/dist/core/mdastToUniorg/index.js +95 -0
  29. package/dist/core/mdastToUniorg/lists.d.ts +4 -0
  30. package/dist/core/mdastToUniorg/lists.js +45 -0
  31. package/dist/core/mdastToUniorg/phrasing.d.ts +4 -0
  32. package/dist/core/mdastToUniorg/phrasing.js +176 -0
  33. package/dist/core/uniorgToMdast/elements.d.ts +4 -0
  34. package/dist/core/uniorgToMdast/elements.js +278 -0
  35. package/dist/core/uniorgToMdast/footnotes.d.ts +10 -0
  36. package/dist/core/uniorgToMdast/footnotes.js +54 -0
  37. package/dist/core/uniorgToMdast/index.d.ts +11 -0
  38. package/dist/core/uniorgToMdast/index.js +84 -0
  39. package/dist/core/uniorgToMdast/lists.d.ts +4 -0
  40. package/dist/core/uniorgToMdast/lists.js +94 -0
  41. package/dist/core/uniorgToMdast/objects.d.ts +4 -0
  42. package/dist/core/uniorgToMdast/objects.js +152 -0
  43. package/dist/core/uniorgToMdast/shared.d.ts +25 -0
  44. package/dist/core/uniorgToMdast/shared.js +55 -0
  45. package/dist/core/uniorgToMdast/tables.d.ts +6 -0
  46. package/dist/core/uniorgToMdast/tables.js +66 -0
  47. package/dist/fileNames.d.ts +16 -0
  48. package/dist/fileNames.js +34 -0
  49. package/dist/index.d.ts +13 -0
  50. package/dist/index.js +8 -0
  51. package/dist/markdownToOrg.d.ts +8 -0
  52. package/dist/markdownToOrg.js +194 -0
  53. package/dist/normalize.d.ts +26 -0
  54. package/dist/normalize.js +24 -0
  55. package/dist/options.d.ts +102 -0
  56. package/dist/options.js +9 -0
  57. package/dist/orgToMarkdown.d.ts +8 -0
  58. package/dist/orgToMarkdown.js +61 -0
  59. package/dist/presets/logseq.d.ts +25 -0
  60. package/dist/presets/logseq.js +298 -0
  61. package/dist/presets/obsidian.d.ts +6 -0
  62. package/dist/presets/obsidian.js +40 -0
  63. package/dist/presets/registry.d.ts +8 -0
  64. package/dist/presets/registry.js +25 -0
  65. package/dist/presets/types.d.ts +12 -0
  66. package/dist/presets/types.js +1 -0
  67. package/package.json +112 -0
@@ -0,0 +1,176 @@
1
+ import { mdismEnabled, warn } from "./context.js";
2
+ // html tags morg itself emits under useHtml; with interpretHtml a bare
3
+ // open/close pair becomes the corresponding native org object
4
+ const INLINE_HTML_ORG_TYPES = {
5
+ u: "underline",
6
+ sup: "superscript",
7
+ sub: "subscript"
8
+ };
9
+ // tag names are case-insensitive and may have whitespace before
10
+ // the closing > (valid html); attributes disqualify the tag
11
+ function matchInlineHtmlPair(children, i) {
12
+ const node = children[i];
13
+ if (node.type !== "html") {
14
+ return null;
15
+ }
16
+ const tag = /^<(u|sup|sub)\s*>$/i.exec(node.value)?.[1]?.toLowerCase();
17
+ const orgType = tag ? INLINE_HTML_ORG_TYPES[tag] : undefined;
18
+ if (!orgType) {
19
+ return null;
20
+ }
21
+ const closeTag = new RegExp(`^</${tag}\\s*>$`, "i");
22
+ const end = children.findIndex((child, j) => j > i && child.type === "html" && closeTag.test(child.value));
23
+ return end === -1 ? null : { orgType, end };
24
+ }
25
+ export function transformPhrasingChildren(ctx, children) {
26
+ const result = [];
27
+ for (let i = 0; i < children.length; i++) {
28
+ const node = children[i];
29
+ const pair = ctx.options.interpretHtml
30
+ ? matchInlineHtmlPair(children, i)
31
+ : null;
32
+ if (pair) {
33
+ result.push({
34
+ type: pair.orgType,
35
+ children: transformPhrasingChildren(ctx, children.slice(i + 1, pair.end))
36
+ });
37
+ i = pair.end;
38
+ continue;
39
+ }
40
+ const transformed = transformMdastPhrasingContentToUniorgObject(ctx, node);
41
+ if (transformed) {
42
+ result.push(transformed);
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ // an org bracket-link path cannot contain [ or ]; percent-encoding keeps
48
+ // the url equivalent and, unlike org's backslash escaping, survives being
49
+ // parsed back (uniorg does not decode `\[`). A silent normalization, not
50
+ // a drop: dialect wikilink destinations take this path routinely.
51
+ function orgSafeUrl(url) {
52
+ return url.replaceAll("[", "%5B").replaceAll("]", "%5D");
53
+ }
54
+ function transformMdastLink(ctx, linkNode) {
55
+ const [only] = linkNode.children;
56
+ // text equal to the url (autolinks) is no description; a plain
57
+ // [[url]] keeps the org side canonical
58
+ const linkChildren = linkNode.children.length === 1 &&
59
+ only?.type === "text" &&
60
+ only.value === linkNode.url
61
+ ? []
62
+ : transformPhrasingChildren(ctx, linkNode.children);
63
+ // rawLink should just be the URL, uniorg-stringify adds the brackets
64
+ const url = orgSafeUrl(linkNode.url);
65
+ return {
66
+ type: "link",
67
+ format: "bracket", // Assuming bracket format for Markdown links
68
+ linkType: "url",
69
+ rawLink: url,
70
+ path: url,
71
+ children: linkChildren
72
+ };
73
+ }
74
+ function transformMdastLinkReference(ctx, node) {
75
+ // org has no reference-style links: resolve to an inline link
76
+ const definition = ctx.definitions.get(node.identifier);
77
+ if (!definition) {
78
+ return null;
79
+ }
80
+ return transformMdastPhrasingContentToUniorgObject(ctx, {
81
+ type: "link",
82
+ url: definition.url,
83
+ children: node.children
84
+ });
85
+ }
86
+ function transformMdastImageReference(ctx, node) {
87
+ const definition = ctx.definitions.get(node.identifier);
88
+ if (!definition) {
89
+ return null;
90
+ }
91
+ return transformMdastPhrasingContentToUniorgObject(ctx, {
92
+ type: "image",
93
+ url: definition.url,
94
+ alt: node.alt ?? null
95
+ });
96
+ }
97
+ function transformMdastImage(ctx, node) {
98
+ // org has no dedicated image syntax: a plain file link renders
99
+ // inline, alt text becomes the link description; the title
100
+ // attribute has no org slot and is dropped (see README)
101
+ if (node.title) {
102
+ warn(ctx, `dropped image title "${node.title}" (${node.url})`);
103
+ }
104
+ const url = orgSafeUrl(node.url);
105
+ return {
106
+ type: "link",
107
+ format: "bracket",
108
+ linkType: "file",
109
+ rawLink: url,
110
+ path: url,
111
+ children: node.alt ? [{ type: "text", value: node.alt }] : []
112
+ };
113
+ }
114
+ function transformMdastInlineMath(node) {
115
+ const value = node.value;
116
+ return {
117
+ type: "latex-fragment",
118
+ value: `$${value}$`,
119
+ contents: value
120
+ };
121
+ }
122
+ function transformMdastPhrasingContentToUniorgObject(ctx, node) {
123
+ switch (node.type) {
124
+ case "text":
125
+ return { type: "text", value: node.value };
126
+ case "emphasis":
127
+ return {
128
+ type: "italic",
129
+ children: transformPhrasingChildren(ctx, node.children)
130
+ };
131
+ case "strong":
132
+ return {
133
+ type: "bold",
134
+ children: transformPhrasingChildren(ctx, node.children)
135
+ };
136
+ case "delete":
137
+ return {
138
+ type: "strike-through",
139
+ children: transformPhrasingChildren(ctx, node.children)
140
+ };
141
+ case "link":
142
+ return transformMdastLink(ctx, node);
143
+ case "linkReference":
144
+ return transformMdastLinkReference(ctx, node);
145
+ case "imageReference":
146
+ return transformMdastImageReference(ctx, node);
147
+ case "inlineCode":
148
+ return { type: "code", value: node.value };
149
+ case "inlineMath":
150
+ return transformMdastInlineMath(node);
151
+ case "break":
152
+ return { type: "line-break" };
153
+ case "footnoteReference":
154
+ return {
155
+ type: "footnote-reference",
156
+ label: node.identifier,
157
+ footnoteType: "standard",
158
+ children: []
159
+ };
160
+ case "html":
161
+ // inline raw html is a md-ism: preserved as an org export snippet
162
+ return mdismEnabled(ctx, "html")
163
+ ? {
164
+ type: "export-snippet",
165
+ backEnd: "html",
166
+ value: node.value
167
+ }
168
+ : null;
169
+ case "image":
170
+ return transformMdastImage(ctx, node);
171
+ // remaining phrasing types have no mapping; dropped with a warning
172
+ default:
173
+ warn(ctx, `dropped md ${node.type}`);
174
+ return null;
175
+ }
176
+ }
@@ -0,0 +1,4 @@
1
+ import type { RootContent } from "mdast";
2
+ import type { ElementType, GreaterElementType, Text } from "uniorg";
3
+ import { type TransformContext } from "./shared.js";
4
+ export declare function transformNodes(ctx: TransformContext, nodes: (GreaterElementType | ElementType | Text)[]): RootContent[];
@@ -0,0 +1,278 @@
1
+ import { toString as orgastToString } from "orgast-util-to-string";
2
+ import { affiliatedLines, keyName, keyValueParagraph, orgismEnabled, orgNodeToText, trimTrailingNewline, warn } from "./shared.js";
3
+ import { transformUniorgObjects } from "./objects.js";
4
+ import { transformFootnoteDefinition } from "./footnotes.js";
5
+ import { transformTable } from "./tables.js";
6
+ import { transformPlainList } from "./lists.js";
7
+ export function transformNodes(ctx, nodes) {
8
+ return nodes
9
+ .flatMap(node => transformUniorgNodeToMdastNode(ctx, node))
10
+ .filter(Boolean);
11
+ }
12
+ function transformUniorgNodeToMdastNode(ctx, node) {
13
+ const result = transformUniorgElement(ctx, node);
14
+ const lines = affiliatedLines(node);
15
+ if (!result ||
16
+ !lines.length ||
17
+ (!Array.isArray(result) && result.type === "keyValue")) {
18
+ // verbatim passthroughs (keyValue) already carry their affiliated
19
+ // lines via orgNodeToText
20
+ return result;
21
+ }
22
+ return [
23
+ keyValueParagraph(lines),
24
+ ...(Array.isArray(result) ? result : [result])
25
+ ];
26
+ }
27
+ function transformUniorgElement(ctx, node) {
28
+ switch (node.type) {
29
+ case "section":
30
+ return transformSection(ctx, node);
31
+ case "headline":
32
+ return transformHeadline(ctx, node);
33
+ case "planning":
34
+ return transformPlanning(ctx, node);
35
+ case "drawer":
36
+ return transformDrawer(ctx, node);
37
+ case "special-block":
38
+ case "center-block":
39
+ case "verse-block":
40
+ case "comment-block":
41
+ case "fixed-width":
42
+ case "keyword":
43
+ case "babel-call":
44
+ case "diary-sexp":
45
+ case "clock":
46
+ // org-only blocks with no md equivalent travel the same way as
47
+ // drawers: verbatim org text, re-parsed natively on the return trip
48
+ // (keywords here are mid-file ones; leading ones became frontmatter)
49
+ return keyValueParagraph([orgNodeToText(node)]);
50
+ case "property-drawer":
51
+ return transformPropertyDrawer(ctx, node);
52
+ case "paragraph":
53
+ return transformParagraph(ctx, node);
54
+ case "text":
55
+ // Whitespace-only text at block level is a formatting artifact.
56
+ if (node.value.trim() === "") {
57
+ return null;
58
+ }
59
+ return { type: "text", value: node.value };
60
+ case "plain-list":
61
+ return transformPlainList(ctx, node);
62
+ case "table":
63
+ return transformTable(ctx, node);
64
+ case "horizontal-rule":
65
+ return { type: "thematicBreak" };
66
+ case "footnote-definition":
67
+ return transformFootnoteDefinition(ctx, node);
68
+ case "export-block":
69
+ return transformExportBlock(node);
70
+ case "quote-block":
71
+ return transformQuoteBlock(ctx, node);
72
+ case "src-block":
73
+ return transformSrcBlock(node);
74
+ case "example-block":
75
+ return transformExampleBlock(node);
76
+ case "comment":
77
+ return transformComment(node);
78
+ case "latex-environment":
79
+ return { type: "math", value: node.value };
80
+ // remaining element types have no mapping; dropped with a warning
81
+ default:
82
+ warn(ctx, `dropped org ${node.type}`);
83
+ return null;
84
+ }
85
+ }
86
+ function transformSection(ctx, node) {
87
+ if (ctx.options.taskCheckboxes) {
88
+ const task = sectionAsTaskItem(ctx, node.children || []);
89
+ if (task) {
90
+ return task;
91
+ }
92
+ }
93
+ return transformNodes(ctx, node.children || []);
94
+ }
95
+ // with taskCheckboxes, a section holding just a bare TODO/DONE headline
96
+ // (no priority, tags or content) becomes a GFM task item; anything
97
+ // richer keeps the heading (with a warning) so no metadata is lost
98
+ function sectionAsTaskItem(ctx, children) {
99
+ const headline = children[0];
100
+ if (headline?.type !== "headline" || !headline.todoKeyword) {
101
+ return null;
102
+ }
103
+ const rest = children.slice(1);
104
+ const reason = headline.todoKeyword !== "TODO" && headline.todoKeyword !== "DONE"
105
+ ? `keyword ${headline.todoKeyword}`
106
+ : headline.priority
107
+ ? "has priority"
108
+ : headline.tags.length
109
+ ? "has tags"
110
+ : rest.some(child => !(child.type === "text" && child.value.trim() === ""))
111
+ ? "has content"
112
+ : null;
113
+ if (reason) {
114
+ warn(ctx, `taskCheckboxes: kept heading "${orgastToString(headline).trim()}" (${reason})`);
115
+ return null;
116
+ }
117
+ return {
118
+ type: "list",
119
+ ordered: false,
120
+ spread: false,
121
+ children: [
122
+ {
123
+ type: "listItem",
124
+ spread: false,
125
+ checked: headline.todoKeyword === "DONE",
126
+ children: [
127
+ {
128
+ type: "paragraph",
129
+ children: transformUniorgObjects(ctx, headline.children)
130
+ }
131
+ ]
132
+ }
133
+ ]
134
+ };
135
+ }
136
+ function transformHeadline(ctx, node) {
137
+ const heading = {
138
+ type: "heading",
139
+ depth: node.level,
140
+ children: transformUniorgObjects(ctx, node.children)
141
+ };
142
+ // org-isms serialize as key:: value lines directly below the heading
143
+ const isms = [];
144
+ if (node.todoKeyword && orgismEnabled(ctx, "todo")) {
145
+ isms.push(`${keyName(ctx, "todo")}:: ${node.todoKeyword}`);
146
+ }
147
+ if (node.priority && orgismEnabled(ctx, "priority")) {
148
+ isms.push(`${keyName(ctx, "priority")}:: ${node.priority}`);
149
+ }
150
+ if (node.tags.length && orgismEnabled(ctx, "tags")) {
151
+ isms.push(`${keyName(ctx, "tags")}:: ${node.tags.join(", ")}`);
152
+ }
153
+ return isms.length ? [heading, keyValueParagraph(isms)] : heading;
154
+ }
155
+ function transformPlanning(ctx, node) {
156
+ const isms = [];
157
+ if (node.scheduled && orgismEnabled(ctx, "scheduled")) {
158
+ isms.push(`${keyName(ctx, "scheduled")}:: ${node.scheduled.rawValue}`);
159
+ }
160
+ if (node.deadline && orgismEnabled(ctx, "deadline")) {
161
+ isms.push(`${keyName(ctx, "deadline")}:: ${node.deadline.rawValue}`);
162
+ }
163
+ if (node.closed && orgismEnabled(ctx, "closed")) {
164
+ isms.push(`${keyName(ctx, "closed")}:: ${node.closed.rawValue}`);
165
+ }
166
+ return isms.length ? keyValueParagraph(isms) : null;
167
+ }
168
+ function transformDrawer(ctx, node) {
169
+ if (!orgismEnabled(ctx, "drawers")) {
170
+ return null;
171
+ }
172
+ // generic drawers (:LOGBOOK: …) have no md equivalent; keep their
173
+ // org text verbatim (unescaped) so the return trip re-parses the
174
+ // drawer natively
175
+ return keyValueParagraph([orgNodeToText(node)]);
176
+ }
177
+ function transformPropertyDrawer(ctx, node) {
178
+ if (!orgismEnabled(ctx, "properties")) {
179
+ return null;
180
+ }
181
+ const isms = (node.children || [])
182
+ .filter(child => child.type === "node-property")
183
+ .map(property => `${property.key}:: ${property.value}`);
184
+ return isms.length ? keyValueParagraph(isms) : null;
185
+ }
186
+ function transformParagraph(ctx, node) {
187
+ const math = paragraphAsDisplayMath(node);
188
+ if (math) {
189
+ return math;
190
+ }
191
+ const children = transformUniorgObjects(ctx, node.children);
192
+ // md gives leading whitespace structural meaning (list
193
+ // continuation, code); collapse per-line indentation inside
194
+ // paragraphs — insignificant in org and in rendered md alike
195
+ children.forEach((child, index) => {
196
+ if (child.type === "text") {
197
+ child.value = child.value.replace(/\n[ \t]+/g, "\n");
198
+ if (index === 0) {
199
+ child.value = child.value.replace(/^[ \t]+/, "");
200
+ }
201
+ }
202
+ });
203
+ trimParagraphEdges(children);
204
+ // a paragraph emptied by the cleanup (whitespace-only) would
205
+ // stringify as stray blank lines
206
+ return children.length ? { type: "paragraph", children } : null;
207
+ }
208
+ // a paragraph holding only a display fragment ($$…$$ or \[…\]) is
209
+ // display math and becomes a math block
210
+ function paragraphAsDisplayMath(node) {
211
+ const substantial = (node.children || []).filter(child => !(child.type === "text" && child.value.trim() === ""));
212
+ const only = substantial[0];
213
+ if (substantial.length === 1 &&
214
+ only?.type === "latex-fragment" &&
215
+ (only.value.startsWith("$$") || only.value.startsWith("\\["))) {
216
+ return {
217
+ type: "math",
218
+ value: only.contents.replace(/^\n/, "").replace(/\n$/, "")
219
+ };
220
+ }
221
+ return null;
222
+ }
223
+ // uniorg keeps surrounding blank lines inside the paragraph node;
224
+ // strip them so remark-stringify produces canonical spacing.
225
+ function trimParagraphEdges(children) {
226
+ const last = children[children.length - 1];
227
+ if (last?.type === "text") {
228
+ last.value = last.value.replace(/\n+$/, "");
229
+ if (last.value === "") {
230
+ children.pop();
231
+ }
232
+ }
233
+ const head = children[0];
234
+ if (head?.type === "text") {
235
+ head.value = head.value.replace(/^\n+/, "");
236
+ if (head.value === "") {
237
+ children.shift();
238
+ }
239
+ }
240
+ }
241
+ function transformExportBlock(node) {
242
+ if (node.backend !== "html") {
243
+ // other backends have no md meaning; verbatim like the org-only
244
+ // blocks so the return trip restores them
245
+ return keyValueParagraph([orgNodeToText(node)]);
246
+ }
247
+ return { type: "html", value: trimTrailingNewline(node.value) };
248
+ }
249
+ function transformQuoteBlock(ctx, node) {
250
+ return {
251
+ type: "blockquote",
252
+ children: transformNodes(ctx, node.children || [])
253
+ };
254
+ }
255
+ function transformSrcBlock(node) {
256
+ return {
257
+ type: "code",
258
+ lang: node.language || null,
259
+ value: trimTrailingNewline(node.value)
260
+ };
261
+ }
262
+ function transformExampleBlock(node) {
263
+ return {
264
+ type: "code",
265
+ lang: null,
266
+ value: trimTrailingNewline(node.value)
267
+ };
268
+ }
269
+ function transformComment(node) {
270
+ // html comments are markdown's comment idiom (hidden by every
271
+ // renderer) and restore to org comments on the return trip; an
272
+ // embedded `-->` would close the comment early, so escape it
273
+ const value = node.value.replaceAll("-->", "--&gt;");
274
+ return {
275
+ type: "html",
276
+ value: value.includes("\n") ? `<!--\n${value}\n-->` : `<!-- ${value} -->`
277
+ };
278
+ }
@@ -0,0 +1,10 @@
1
+ import type { PhrasingContent, RootContent } from "mdast";
2
+ import type { GreaterElementType, ObjectType } from "uniorg";
3
+ import { type TransformContext } from "./shared.js";
4
+ export declare function collectFootnoteLabels(ctx: TransformContext, node: unknown): void;
5
+ export declare function transformFootnoteReference(ctx: TransformContext, node: Extract<ObjectType, {
6
+ type: "footnote-reference";
7
+ }>): PhrasingContent;
8
+ export declare function transformFootnoteDefinition(ctx: TransformContext, node: Extract<GreaterElementType, {
9
+ type: "footnote-definition";
10
+ }>): RootContent;
@@ -0,0 +1,54 @@
1
+ import {} from "./shared.js";
2
+ import { transformUniorgObjects } from "./objects.js";
3
+ import { transformNodes } from "./elements.js";
4
+ function nextFreeFootnoteLabel(ctx) {
5
+ let candidate = 1;
6
+ while (ctx.usedFootnoteLabels.has(String(candidate))) {
7
+ candidate++;
8
+ }
9
+ const label = String(candidate);
10
+ ctx.usedFootnoteLabels.add(label);
11
+ return label;
12
+ }
13
+ export function collectFootnoteLabels(ctx, node) {
14
+ if (!node || typeof node !== "object") {
15
+ return;
16
+ }
17
+ const candidate = node;
18
+ if ((candidate.type === "footnote-reference" ||
19
+ candidate.type === "footnote-definition") &&
20
+ candidate.label) {
21
+ ctx.usedFootnoteLabels.add(candidate.label);
22
+ }
23
+ for (const child of candidate.children || []) {
24
+ collectFootnoteLabels(ctx, child);
25
+ }
26
+ }
27
+ export function transformFootnoteReference(ctx, node) {
28
+ if (node.footnoteType === "inline") {
29
+ return transformInlineFootnoteReference(ctx, node);
30
+ }
31
+ return {
32
+ type: "footnoteReference",
33
+ identifier: node.label,
34
+ label: node.label
35
+ };
36
+ }
37
+ function transformInlineFootnoteReference(ctx, node) {
38
+ const label = node.label || nextFreeFootnoteLabel(ctx);
39
+ const content = transformUniorgObjects(ctx, node.children);
40
+ const firstChild = content[0];
41
+ if (firstChild?.type === "text") {
42
+ firstChild.value = firstChild.value.trimStart();
43
+ }
44
+ ctx.inlineFootnotes.push({ label, children: content });
45
+ return { type: "footnoteReference", identifier: label, label };
46
+ }
47
+ export function transformFootnoteDefinition(ctx, node) {
48
+ return {
49
+ type: "footnoteDefinition",
50
+ identifier: node.label,
51
+ label: node.label,
52
+ children: transformNodes(ctx, node.children || [])
53
+ };
54
+ }
@@ -0,0 +1,11 @@
1
+ import type { Root as MdastRoot } from "mdast";
2
+ import type { OrgData } from "uniorg";
3
+ import type { UniorgToMdastOptions } from "./shared.js";
4
+ export type { UniorgToMdastOptions } from "./shared.js";
5
+ /**
6
+ * Transforms a uniorg AST to a mdast (Markdown AST).
7
+ * @param uniorgAst The uniorg AST to transform.
8
+ * @param options Controls org-ism serialization (`key:: value` lines).
9
+ * @returns The transformed mdast.
10
+ */
11
+ export declare function transformUniorgAstToMdast(uniorgAst: OrgData, options?: UniorgToMdastOptions): MdastRoot;
@@ -0,0 +1,84 @@
1
+ import { stringify as stringifyYaml } from "yaml";
2
+ import { collectFootnoteLabels } from "./footnotes.js";
3
+ import { transformNodes } from "./elements.js";
4
+ /**
5
+ * Transforms a uniorg AST to a mdast (Markdown AST).
6
+ * @param uniorgAst The uniorg AST to transform.
7
+ * @param options Controls org-ism serialization (`key:: value` lines).
8
+ * @returns The transformed mdast.
9
+ */
10
+ export function transformUniorgAstToMdast(uniorgAst, options = {}) {
11
+ const ctx = {
12
+ options,
13
+ inlineFootnotes: [],
14
+ usedFootnoteLabels: new Set()
15
+ };
16
+ collectFootnoteLabels(ctx, uniorgAst);
17
+ const nodes = uniorgAst.children || [];
18
+ // leading #+KEY: value keywords map to md frontmatter, a native
19
+ // construct; JSON-encoded values restore their structure (ADR 0002)
20
+ const collected = new Map();
21
+ let first = 0;
22
+ while (nodes[first]?.type === "keyword") {
23
+ const keyword = nodes[first];
24
+ const key = keyword.key.toLowerCase();
25
+ // a keyword may legally repeat; collect the values instead of
26
+ // letting the last one win
27
+ collected.set(key, [
28
+ ...(collected.get(key) ?? []),
29
+ parseKeywordValue(keyword.value)
30
+ ]);
31
+ first++;
32
+ }
33
+ const frontmatter = Object.fromEntries([...collected].map(([key, values]) => [
34
+ key,
35
+ values.length === 1 ? values[0] : values
36
+ ]));
37
+ const children = transformNodes(ctx, nodes.slice(first));
38
+ // adjacent single-item task lists (one per converted TODO section)
39
+ // merge into one list, or the output would not be a fixed point
40
+ if (options.taskCheckboxes) {
41
+ mergeAdjacentTaskLists(children);
42
+ }
43
+ if (first > 0) {
44
+ children.unshift({
45
+ type: "yaml",
46
+ value: stringifyYaml(frontmatter).trimEnd()
47
+ });
48
+ }
49
+ for (const footnote of ctx.inlineFootnotes) {
50
+ children.push({
51
+ type: "footnoteDefinition",
52
+ identifier: footnote.label,
53
+ label: footnote.label,
54
+ children: [{ type: "paragraph", children: footnote.children }]
55
+ });
56
+ }
57
+ return { type: "root", children: children };
58
+ }
59
+ function parseKeywordValue(value) {
60
+ try {
61
+ return JSON.parse(value);
62
+ }
63
+ catch {
64
+ return value;
65
+ }
66
+ }
67
+ function isTaskList(node) {
68
+ return (node?.type === "list" &&
69
+ !node.ordered &&
70
+ node.children.every(item => item.checked !== null));
71
+ }
72
+ function mergeAdjacentTaskLists(children) {
73
+ for (let i = 0; i < children.length - 1;) {
74
+ const current = children[i];
75
+ const next = children[i + 1];
76
+ if (isTaskList(current) && isTaskList(next)) {
77
+ current.children.push(...next.children);
78
+ children.splice(i + 1, 1);
79
+ }
80
+ else {
81
+ i++;
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,4 @@
1
+ import type { RootContent } from "mdast";
2
+ import type { List } from "uniorg";
3
+ import { type TransformContext } from "./shared.js";
4
+ export declare function transformPlainList(ctx: TransformContext, node: List): RootContent | RootContent[];