@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.
- package/LICENSE +674 -0
- package/README.md +267 -0
- package/dist/cli/args.d.ts +15 -0
- package/dist/cli/args.js +105 -0
- package/dist/cli/configFile.d.ts +2 -0
- package/dist/cli/configFile.js +18 -0
- package/dist/cli/conversion.d.ts +11 -0
- package/dist/cli/conversion.js +50 -0
- package/dist/cli/error.d.ts +2 -0
- package/dist/cli/error.js +4 -0
- package/dist/cli/formats.d.ts +3 -0
- package/dist/cli/formats.js +46 -0
- package/dist/cli/presets.d.ts +2 -0
- package/dist/cli/presets.js +10 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +54 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +22 -0
- package/dist/conversionOptions.d.ts +32 -0
- package/dist/conversionOptions.js +34 -0
- package/dist/core/markdownStyle.d.ts +27 -0
- package/dist/core/markdownStyle.js +108 -0
- package/dist/core/mdastToUniorg/blocks.d.ts +20 -0
- package/dist/core/mdastToUniorg/blocks.js +211 -0
- package/dist/core/mdastToUniorg/context.d.ts +15 -0
- package/dist/core/mdastToUniorg/context.js +7 -0
- package/dist/core/mdastToUniorg/index.d.ts +13 -0
- package/dist/core/mdastToUniorg/index.js +95 -0
- package/dist/core/mdastToUniorg/lists.d.ts +4 -0
- package/dist/core/mdastToUniorg/lists.js +45 -0
- package/dist/core/mdastToUniorg/phrasing.d.ts +4 -0
- package/dist/core/mdastToUniorg/phrasing.js +176 -0
- package/dist/core/uniorgToMdast/elements.d.ts +4 -0
- package/dist/core/uniorgToMdast/elements.js +278 -0
- package/dist/core/uniorgToMdast/footnotes.d.ts +10 -0
- package/dist/core/uniorgToMdast/footnotes.js +54 -0
- package/dist/core/uniorgToMdast/index.d.ts +11 -0
- package/dist/core/uniorgToMdast/index.js +84 -0
- package/dist/core/uniorgToMdast/lists.d.ts +4 -0
- package/dist/core/uniorgToMdast/lists.js +94 -0
- package/dist/core/uniorgToMdast/objects.d.ts +4 -0
- package/dist/core/uniorgToMdast/objects.js +152 -0
- package/dist/core/uniorgToMdast/shared.d.ts +25 -0
- package/dist/core/uniorgToMdast/shared.js +55 -0
- package/dist/core/uniorgToMdast/tables.d.ts +6 -0
- package/dist/core/uniorgToMdast/tables.js +66 -0
- package/dist/fileNames.d.ts +16 -0
- package/dist/fileNames.js +34 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/markdownToOrg.d.ts +8 -0
- package/dist/markdownToOrg.js +194 -0
- package/dist/normalize.d.ts +26 -0
- package/dist/normalize.js +24 -0
- package/dist/options.d.ts +102 -0
- package/dist/options.js +9 -0
- package/dist/orgToMarkdown.d.ts +8 -0
- package/dist/orgToMarkdown.js +61 -0
- package/dist/presets/logseq.d.ts +25 -0
- package/dist/presets/logseq.js +298 -0
- package/dist/presets/obsidian.d.ts +6 -0
- package/dist/presets/obsidian.js +40 -0
- package/dist/presets/registry.d.ts +8 -0
- package/dist/presets/registry.js +25 -0
- package/dist/presets/types.d.ts +12 -0
- package/dist/presets/types.js +1 -0
- 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("-->", "-->");
|
|
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
|
+
}
|