@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,32 @@
1
+ import type { MorgConfig } from "./config.js";
2
+ import type { MarkdownStyleOptions, MarkdownToOrgOptions, OrgToMarkdownOptions, Toggle } from "./options.js";
3
+ import type { Preset } from "./presets/types.js";
4
+ /** Explicitly requested option values; `undefined` leaves it to config. */
5
+ export interface ConversionOverrides {
6
+ taskCheckboxes?: boolean;
7
+ interpretHtml?: boolean;
8
+ recordStyle?: boolean;
9
+ useHtml?: Toggle;
10
+ markdownStyle?: MarkdownStyleOptions;
11
+ }
12
+ /** Values that apply to both directions. */
13
+ export interface SharedConversionOptions {
14
+ preset?: Preset;
15
+ onWarning?: (message: string) => void;
16
+ orgismKeys?: Record<string, string>;
17
+ }
18
+ /**
19
+ * Layers explicit overrides over the config file, per direction. Used by
20
+ * both adapters (CLI and Web UI) so the documented precedence — CLI or
21
+ * form > config > defaults — means the same thing in each.
22
+ * @param overrides Explicitly requested values; `undefined` defers to config.
23
+ * @param config The parsed `morg.toml`.
24
+ * @param shared Values that apply to both directions.
25
+ * @returns The options object for each direction.
26
+ */
27
+ export declare function buildConversionOptions(overrides: ConversionOverrides, config: MorgConfig, shared: SharedConversionOptions): {
28
+ mdToOrgOptions: MarkdownToOrgOptions;
29
+ orgToMdOptions: OrgToMarkdownOptions & {
30
+ markdownStyle: MarkdownStyleOptions;
31
+ };
32
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Layers explicit overrides over the config file, per direction. Used by
3
+ * both adapters (CLI and Web UI) so the documented precedence — CLI or
4
+ * form > config > defaults — means the same thing in each.
5
+ * @param overrides Explicitly requested values; `undefined` defers to config.
6
+ * @param config The parsed `morg.toml`.
7
+ * @param shared Values that apply to both directions.
8
+ * @returns The options object for each direction.
9
+ */
10
+ export function buildConversionOptions(overrides, config, shared) {
11
+ const mdToOrgOptions = {
12
+ ...config.markdownToOrg,
13
+ ...shared,
14
+ ...(overrides.interpretHtml !== undefined && {
15
+ interpretHtml: overrides.interpretHtml
16
+ }),
17
+ ...(overrides.recordStyle !== undefined && {
18
+ recordStyle: overrides.recordStyle
19
+ })
20
+ };
21
+ const orgToMdOptions = {
22
+ ...config.orgToMarkdown,
23
+ ...shared,
24
+ ...(overrides.useHtml !== undefined && { useHtml: overrides.useHtml }),
25
+ ...(overrides.taskCheckboxes !== undefined && {
26
+ taskCheckboxes: overrides.taskCheckboxes
27
+ }),
28
+ markdownStyle: {
29
+ ...config.orgToMarkdown?.markdownStyle,
30
+ ...overrides.markdownStyle
31
+ }
32
+ };
33
+ return { mdToOrgOptions, orgToMdOptions };
34
+ }
@@ -0,0 +1,27 @@
1
+ import type { Root } from "mdast";
2
+ import type { OrgData } from "uniorg";
3
+ import type { MarkdownStyleOptions } from "../options.js";
4
+ /**
5
+ * The org keyword carrying a recorded Markdown style, per ADR 0004.
6
+ * `MORG_`-prefixed for the same reason `morg_` properties are (ADR 0002):
7
+ * ownership on the return trip must be unambiguous.
8
+ */
9
+ export declare const STYLE_KEYWORD = "MORG_MARKDOWN_STYLE";
10
+ /**
11
+ * Detects the Markdown style knobs a source document was written with,
12
+ * by reading the markers back out of the source at each node's position.
13
+ * Knobs used inconsistently are omitted rather than guessed at (see
14
+ * ADR 0004): style is recorded per document, so there is no honest
15
+ * answer for a document that mixes markers.
16
+ * @param mdast The parsed Markdown AST.
17
+ * @param markdown The Markdown source the AST was parsed from.
18
+ * @returns The detected knobs; keys without consistent evidence are absent.
19
+ */
20
+ export declare function detectMarkdownStyle(mdast: Root, markdown: string, onWarning?: (message: string) => void): MarkdownStyleOptions;
21
+ /**
22
+ * Removes a recorded-style keyword from an org AST and returns what it
23
+ * held, so it reaches the stringifier instead of the document body.
24
+ * @param uniorgAst The parsed org AST; mutated when the keyword is present.
25
+ * @returns The recorded knobs, or `undefined` when nothing was recorded.
26
+ */
27
+ export declare function takeRecordedStyle(uniorgAst: OrgData): MarkdownStyleOptions | undefined;
@@ -0,0 +1,108 @@
1
+ import { visit } from "unist-util-visit";
2
+ /**
3
+ * The org keyword carrying a recorded Markdown style, per ADR 0004.
4
+ * `MORG_`-prefixed for the same reason `morg_` properties are (ADR 0002):
5
+ * ownership on the return trip must be unambiguous.
6
+ */
7
+ export const STYLE_KEYWORD = "MORG_MARKDOWN_STYLE";
8
+ /**
9
+ * Detects the Markdown style knobs a source document was written with,
10
+ * by reading the markers back out of the source at each node's position.
11
+ * Knobs used inconsistently are omitted rather than guessed at (see
12
+ * ADR 0004): style is recorded per document, so there is no honest
13
+ * answer for a document that mixes markers.
14
+ * @param mdast The parsed Markdown AST.
15
+ * @param markdown The Markdown source the AST was parsed from.
16
+ * @returns The detected knobs; keys without consistent evidence are absent.
17
+ */
18
+ export function detectMarkdownStyle(mdast, markdown, onWarning) {
19
+ const seen = {
20
+ bullet: new Set(),
21
+ emphasis: new Set(),
22
+ strong: new Set(),
23
+ fence: new Set(),
24
+ rule: new Set()
25
+ };
26
+ // the rule is the one marker whose length also matters
27
+ const rules = new Set();
28
+ visit(mdast, (node, _index, parent) => {
29
+ const offset = node.position?.start.offset;
30
+ if (offset === undefined) {
31
+ return;
32
+ }
33
+ const marker = markdown[offset];
34
+ const knob = knobOf(node.type, marker, parent);
35
+ if (knob) {
36
+ seen[knob].add(marker);
37
+ }
38
+ if (node.type === "thematicBreak") {
39
+ rules.add(markdown.slice(offset, node.position?.end.offset).replace(/\s/g, ""));
40
+ }
41
+ });
42
+ const style = {};
43
+ for (const knob of KNOBS) {
44
+ record(style, knob, seen[knob], onWarning);
45
+ }
46
+ recordRuleRepetition(style, rules);
47
+ return style;
48
+ }
49
+ const KNOBS = ["bullet", "emphasis", "strong", "fence", "rule"];
50
+ function knobOf(type, marker, parent) {
51
+ switch (type) {
52
+ case "emphasis":
53
+ return "emphasis";
54
+ case "strong":
55
+ return "strong";
56
+ case "thematicBreak":
57
+ return "rule";
58
+ case "code":
59
+ // an indented code block has no fence to read
60
+ return marker === "`" || marker === "~" ? "fence" : undefined;
61
+ case "listItem":
62
+ // an ordered item starts with its number, not with a bullet
63
+ return parent?.type === "list" && !parent.ordered ? "bullet" : undefined;
64
+ default:
65
+ return undefined;
66
+ }
67
+ }
68
+ // "---" is remark's own default repetition, so only a longer run is
69
+ // worth recording; a document mixing lengths records none
70
+ function recordRuleRepetition(style, rules) {
71
+ const lengths = new Set([...rules].map(rule => rule.length));
72
+ const [length] = [...lengths];
73
+ if (style.rule && lengths.size === 1 && length !== undefined && length > 3) {
74
+ style.ruleRepetition = length;
75
+ }
76
+ }
77
+ // a marker with no instances in the document has nothing to record and
78
+ // nothing to warn about; one used two ways has no honest single answer
79
+ function record(style, knob, markers, onWarning) {
80
+ if (markers.size === 1) {
81
+ Object.assign(style, { [knob]: [...markers][0] });
82
+ return;
83
+ }
84
+ if (markers.size > 1) {
85
+ onWarning?.(`${knob} marker is not used consistently; not recorded`);
86
+ }
87
+ }
88
+ /**
89
+ * Removes a recorded-style keyword from an org AST and returns what it
90
+ * held, so it reaches the stringifier instead of the document body.
91
+ * @param uniorgAst The parsed org AST; mutated when the keyword is present.
92
+ * @returns The recorded knobs, or `undefined` when nothing was recorded.
93
+ */
94
+ export function takeRecordedStyle(uniorgAst) {
95
+ const index = uniorgAst.children.findIndex(child => child.type === "keyword" &&
96
+ child.key === STYLE_KEYWORD);
97
+ if (index === -1) {
98
+ return undefined;
99
+ }
100
+ const [keyword] = uniorgAst.children.splice(index, 1);
101
+ const value = keyword.value;
102
+ try {
103
+ return JSON.parse(value);
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
@@ -0,0 +1,20 @@
1
+ import type { RootContent, PhrasingContent } from "mdast";
2
+ import type { ElementType } from "uniorg";
3
+ import { type TransformContext } from "./context.js";
4
+ export declare function frontmatterToKeywords(yamlValue: string): ElementType[];
5
+ export declare function transformMdastTable(ctx: TransformContext, node: Extract<RootContent, {
6
+ type: "table";
7
+ }>): ElementType;
8
+ export declare function transformMdastHtml(ctx: TransformContext, node: Extract<RootContent, {
9
+ type: "html";
10
+ }>): ElementType | null;
11
+ export declare function transformMdastCode(node: Extract<RootContent, {
12
+ type: "code";
13
+ }>): ElementType;
14
+ export declare function transformMdastMath(node: RootContent | PhrasingContent): ElementType;
15
+ export declare function transformMdastHeading(ctx: TransformContext, node: Extract<RootContent, {
16
+ type: "heading";
17
+ }>): ElementType;
18
+ export declare function keywordOnlyLines(node: {
19
+ children: PhrasingContent[];
20
+ }): string[] | null;
@@ -0,0 +1,211 @@
1
+ import { toString } from "orgast-util-to-string";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { mdismEnabled } from "./context.js";
4
+ import { transformPhrasingChildren } from "./phrasing.js";
5
+ // a keyword is a single line, so anything that is not a single-line
6
+ // scalar is JSON-encoded and restored by JSON.parse on the way back
7
+ function keywordValue(value) {
8
+ if (value !== null && typeof value === "object") {
9
+ return JSON.stringify(value);
10
+ }
11
+ const text = String(value);
12
+ return text.includes("\n") ? JSON.stringify(text) : text;
13
+ }
14
+ // frontmatter entries become #+KEY: value keywords; scalar values as-is,
15
+ // structured values JSON-encoded on a single line (see ADR 0002)
16
+ export function frontmatterToKeywords(yamlValue) {
17
+ const data = parseYaml(yamlValue);
18
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
19
+ return [];
20
+ }
21
+ return Object.entries(data).flatMap(([key, value]) => {
22
+ // a sequence becomes repeated keywords -- org's own way of carrying
23
+ // several values for one key, and what they read back as
24
+ const values = Array.isArray(value) && value.length ? value : [value];
25
+ return values.map(item => ({
26
+ type: "keyword",
27
+ affiliated: {},
28
+ key: key.toUpperCase(),
29
+ value: keywordValue(item)
30
+ }));
31
+ });
32
+ }
33
+ export function transformMdastTable(ctx, node) {
34
+ const [headerRow, ...bodyRows] = node.children;
35
+ // GFM column alignment maps to an org alignment cookie row
36
+ // (| <l> | <r> | <c> |) directly below the header rule
37
+ const cookieRow = (node.align || []).some(Boolean)
38
+ ? [
39
+ {
40
+ type: "table-row",
41
+ rowType: "standard",
42
+ children: (node.align || []).map(align => ({
43
+ type: "table-cell",
44
+ children: align
45
+ ? [{ type: "text", value: `<${align.charAt(0)}>` }]
46
+ : []
47
+ }))
48
+ }
49
+ ]
50
+ : [];
51
+ const rows = [
52
+ ...(headerRow ? [transformMdastTableRow(ctx, headerRow)] : []),
53
+ { type: "table-row", rowType: "rule", children: [] },
54
+ ...cookieRow,
55
+ ...bodyRows.map(row => transformMdastTableRow(ctx, row))
56
+ ];
57
+ return {
58
+ type: "table",
59
+ tableType: "org",
60
+ tblfm: null,
61
+ children: rows
62
+ };
63
+ }
64
+ function transformMdastTableRow(ctx, row) {
65
+ return {
66
+ type: "table-row",
67
+ rowType: "standard",
68
+ children: row.children.map(cell => ({
69
+ type: "table-cell",
70
+ children: transformPhrasingChildren(ctx, cell.children)
71
+ }))
72
+ };
73
+ }
74
+ export function transformMdastHtml(ctx, node) {
75
+ // html comments are markdown's comment idiom and map natively to
76
+ // org comments (not an md-ism)
77
+ const comment = /^<!--([\s\S]*?)-->\s*$/.exec(node.value);
78
+ if (comment) {
79
+ return {
80
+ type: "comment",
81
+ // inverse of the escaping applied when the comment was emitted
82
+ value: (comment[1] ?? "").trim().replaceAll("--&gt;", "-->")
83
+ };
84
+ }
85
+ if (ctx.options.interpretHtml) {
86
+ const descriptiveList = interpretDefinitionList(node.value);
87
+ if (descriptiveList) {
88
+ return descriptiveList;
89
+ }
90
+ }
91
+ // block raw html is a md-ism: preserved as an org export block
92
+ return mdismEnabled(ctx, "html")
93
+ ? {
94
+ type: "export-block",
95
+ backend: "html",
96
+ value: node.value
97
+ }
98
+ : null;
99
+ }
100
+ // a bare <dl> whose body is nothing but attribute-less <dt>/<dd> pairs
101
+ // (any whitespace between tags) becomes a ` :: ` list — the same
102
+ // markdown convention descriptive lists use without useHtml, so the
103
+ // org side re-parses it as a native descriptive list; anything richer
104
+ // stays a preserved md-ism
105
+ // inverse of the escaping the useHtml side applies to <dt>/<dd> text
106
+ function unescapeHtmlText(text) {
107
+ return text
108
+ .trim()
109
+ .replaceAll("&lt;", "<")
110
+ .replaceAll("&gt;", ">")
111
+ .replaceAll("&amp;", "&");
112
+ }
113
+ function interpretDefinitionList(html) {
114
+ const body = /^<dl\s*>([\s\S]*)<\/dl\s*>\s*$/i.exec(html.trim())?.[1];
115
+ if (!body) {
116
+ return null;
117
+ }
118
+ const entries = [];
119
+ const leftover = body.replace(/<dt\s*>([^<]*)<\/dt\s*>\s*<dd\s*>([^<]*)<\/dd\s*>/gi, (_match, term, definition) => {
120
+ entries.push([unescapeHtmlText(term), unescapeHtmlText(definition)]);
121
+ return "";
122
+ });
123
+ if (!entries.length || leftover.trim() !== "") {
124
+ return null;
125
+ }
126
+ return {
127
+ type: "plain-list",
128
+ listType: "unordered",
129
+ indent: 0,
130
+ affiliated: {},
131
+ children: entries.map(([term, definition]) => ({
132
+ type: "list-item",
133
+ indent: 0,
134
+ bullet: "- ",
135
+ counter: null,
136
+ checkbox: null,
137
+ children: [{ type: "text", value: `${term} :: ${definition}\n` }],
138
+ contentsBegin: 0,
139
+ contentsEnd: 0
140
+ })),
141
+ contentsBegin: 0,
142
+ contentsEnd: 0
143
+ };
144
+ }
145
+ export function transformMdastCode(node) {
146
+ // a table.el-tagged fence restores the verbatim table.el table it
147
+ // was serialized from
148
+ if (node.lang === "table.el") {
149
+ return {
150
+ type: "table",
151
+ tableType: "table.el",
152
+ tblfm: "",
153
+ value: `${node.value}\n`
154
+ };
155
+ }
156
+ return (node.lang
157
+ ? { type: "src-block", language: node.lang, value: node.value }
158
+ : {
159
+ type: "example-block",
160
+ value: node.value
161
+ });
162
+ }
163
+ // \begin… blocks restore to latex environments; plain display math
164
+ // becomes a $$…$$ fragment (org's display form)
165
+ export function transformMdastMath(node) {
166
+ const value = node.value;
167
+ if (value.startsWith("\\begin{")) {
168
+ return {
169
+ type: "latex-environment",
170
+ affiliated: {},
171
+ value
172
+ };
173
+ }
174
+ return {
175
+ type: "paragraph",
176
+ children: [
177
+ {
178
+ type: "latex-fragment",
179
+ value: `$$\n${value}\n$$`,
180
+ contents: `\n${value}\n`
181
+ }
182
+ ],
183
+ contentsBegin: 0,
184
+ contentsEnd: 0
185
+ };
186
+ }
187
+ export function transformMdastHeading(ctx, node) {
188
+ return {
189
+ type: "headline",
190
+ level: node.depth,
191
+ todoKeyword: null,
192
+ priority: null,
193
+ commented: false,
194
+ rawValue: toString(node),
195
+ tags: [],
196
+ children: transformPhrasingChildren(ctx, node.children)
197
+ };
198
+ }
199
+ const KEYWORD_LINE_RE = /^#\+\S+: /;
200
+ export function keywordOnlyLines(node) {
201
+ if (!node.children.every(child => child.type === "text")) {
202
+ return null;
203
+ }
204
+ const lines = node.children
205
+ .map(child => child.value)
206
+ .join("")
207
+ .split("\n");
208
+ return lines.length && lines.every(line => KEYWORD_LINE_RE.test(line))
209
+ ? lines
210
+ : null;
211
+ }
@@ -0,0 +1,15 @@
1
+ import { type Toggle } from "../../options.js";
2
+ export interface MdastToUniorgOptions {
3
+ preserveMdisms?: Toggle;
4
+ interpretHtml?: boolean;
5
+ onWarning?: (message: string) => void;
6
+ }
7
+ export interface TransformContext {
8
+ options: MdastToUniorgOptions;
9
+ definitions: Map<string, {
10
+ url: string;
11
+ title?: string;
12
+ }>;
13
+ }
14
+ export declare function mdismEnabled(ctx: TransformContext, key: string): boolean;
15
+ export declare function warn(ctx: TransformContext, message: string): void;
@@ -0,0 +1,7 @@
1
+ import { toggleEnabled } from "../../options.js";
2
+ export function mdismEnabled(ctx, key) {
3
+ return toggleEnabled(ctx.options.preserveMdisms, key);
4
+ }
5
+ export function warn(ctx, message) {
6
+ ctx.options.onWarning?.(message);
7
+ }
@@ -0,0 +1,13 @@
1
+ import type { Root as MdastRoot, RootContent, PhrasingContent } from "mdast";
2
+ import type { OrgData, Text, ElementType, GreaterElementType } from "uniorg";
3
+ import { type TransformContext } from "./context.js";
4
+ export type { MdastToUniorgOptions, TransformContext } from "./context.js";
5
+ import type { MdastToUniorgOptions } from "./context.js";
6
+ /**
7
+ * Transforms a mdast (Markdown AST) to a uniorg AST.
8
+ * @param mdast The mdast tree to transform.
9
+ * @param options Controls md-ism preservation (e.g. raw HTML).
10
+ * @returns The transformed uniorg AST.
11
+ */
12
+ export declare function transformMdastToUniorgAst(mdast: MdastRoot, options?: MdastToUniorgOptions): OrgData;
13
+ export declare function transformMdastNodeToUniorgNode(ctx: TransformContext, node: RootContent | PhrasingContent): GreaterElementType | ElementType | Text | null;
@@ -0,0 +1,95 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { warn } from "./context.js";
3
+ import { frontmatterToKeywords, keywordOnlyLines, transformMdastCode, transformMdastHeading, transformMdastHtml, transformMdastMath, transformMdastTable } from "./blocks.js";
4
+ import { transformMdastList } from "./lists.js";
5
+ import { transformPhrasingChildren } from "./phrasing.js";
6
+ /**
7
+ * Transforms a mdast (Markdown AST) to a uniorg AST.
8
+ * @param mdast The mdast tree to transform.
9
+ * @param options Controls md-ism preservation (e.g. raw HTML).
10
+ * @returns The transformed uniorg AST.
11
+ */
12
+ export function transformMdastToUniorgAst(mdast, options = {}) {
13
+ const ctx = { options, definitions: new Map() };
14
+ visit(mdast, "definition", (definition) => {
15
+ ctx.definitions.set(definition.identifier, {
16
+ url: definition.url,
17
+ ...(definition.title != null && { title: definition.title })
18
+ });
19
+ });
20
+ const children = mdast.children
21
+ .flatMap(child =>
22
+ // frontmatter maps to org keywords, a native construct (one mdast
23
+ // node fans out to one keyword per entry)
24
+ child.type === "yaml"
25
+ ? frontmatterToKeywords(child.value)
26
+ : [transformMdastNodeToUniorgNode(ctx, child)])
27
+ .filter(Boolean);
28
+ const orgAst = {
29
+ type: "org-data",
30
+ children: children,
31
+ contentsBegin: 0, // Placeholder
32
+ contentsEnd: 0 // Placeholder
33
+ };
34
+ return orgAst;
35
+ }
36
+ function transformBlockChildren(ctx, children) {
37
+ return children
38
+ .map(child => transformMdastNodeToUniorgNode(ctx, child))
39
+ .filter(Boolean);
40
+ }
41
+ export function transformMdastNodeToUniorgNode(ctx, node) {
42
+ switch (node.type) {
43
+ case "heading":
44
+ return transformMdastHeading(ctx, node);
45
+ case "paragraph": {
46
+ // a paragraph of only #+KEY: lines is affiliated keywords (or
47
+ // mid-file keywords) traveling verbatim; emit as raw text so they
48
+ // glue to the following element without a blank line — org only
49
+ // attaches affiliated keywords when directly above their element
50
+ const keywordLines = keywordOnlyLines(node);
51
+ if (keywordLines) {
52
+ return { type: "text", value: `${keywordLines.join("\n")}\n` };
53
+ }
54
+ return {
55
+ type: "paragraph",
56
+ children: transformPhrasingChildren(ctx, node.children),
57
+ contentsBegin: 0, // Placeholder
58
+ contentsEnd: 0 // Placeholder
59
+ };
60
+ }
61
+ case "text":
62
+ return { type: "text", value: node.value };
63
+ case "list":
64
+ return transformMdastList(ctx, node, 0);
65
+ case "table":
66
+ return transformMdastTable(ctx, node);
67
+ case "html":
68
+ return transformMdastHtml(ctx, node);
69
+ case "thematicBreak":
70
+ return { type: "horizontal-rule" };
71
+ case "footnoteDefinition":
72
+ return {
73
+ type: "footnote-definition",
74
+ label: node.identifier,
75
+ affiliated: {},
76
+ children: transformBlockChildren(ctx, node.children)
77
+ };
78
+ case "blockquote":
79
+ return {
80
+ type: "quote-block",
81
+ children: transformBlockChildren(ctx, node.children)
82
+ };
83
+ case "code":
84
+ return transformMdastCode(node);
85
+ case "math":
86
+ return transformMdastMath(node);
87
+ case "definition":
88
+ // consumed by reference-style link resolution
89
+ return null;
90
+ // remaining block types have no mapping; dropped with a warning
91
+ default:
92
+ warn(ctx, `dropped md ${node.type}`);
93
+ return null;
94
+ }
95
+ }
@@ -0,0 +1,4 @@
1
+ import type { List as MdastList } from "mdast";
2
+ import type { List } from "uniorg";
3
+ import type { TransformContext } from "./context.js";
4
+ export declare function transformMdastList(ctx: TransformContext, listNode: MdastList, indent: number): List;
@@ -0,0 +1,45 @@
1
+ import { transformPhrasingChildren } from "./phrasing.js";
2
+ // circular import with index.js is fine in ESM: both sides only export
3
+ // hoisted function declarations called after module initialization
4
+ import { transformMdastNodeToUniorgNode } from "./index.js";
5
+ // Mirrors the AST shape uniorg-parse produces for lists: ordered numbering
6
+ // lives in each item's bullet, and nested lists sit inside the parent
7
+ // item's children with indent = parent indent + bullet length.
8
+ export function transformMdastList(ctx, listNode, indent) {
9
+ const start = listNode.start ?? 1;
10
+ return {
11
+ type: "plain-list",
12
+ listType: listNode.ordered ? "ordered" : "unordered",
13
+ indent,
14
+ affiliated: {},
15
+ children: listNode.children.map((item, i) => transformMdastListItem(ctx, item, indent, listNode.ordered ? `${start + i}. ` : "- ")),
16
+ contentsBegin: 0,
17
+ contentsEnd: 0
18
+ };
19
+ }
20
+ function transformMdastListItem(ctx, item, indent, bullet) {
21
+ const children = item.children
22
+ .flatMap((child) => {
23
+ if (child.type === "list") {
24
+ return [transformMdastList(ctx, child, indent + bullet.length)];
25
+ }
26
+ if (child.type === "paragraph") {
27
+ return [
28
+ ...transformPhrasingChildren(ctx, child.children),
29
+ { type: "text", value: "\n" }
30
+ ];
31
+ }
32
+ return [transformMdastNodeToUniorgNode(ctx, child)];
33
+ })
34
+ .filter(Boolean);
35
+ return {
36
+ type: "list-item",
37
+ indent,
38
+ bullet,
39
+ counter: null,
40
+ checkbox: item.checked === true ? "on" : item.checked === false ? "off" : null,
41
+ children,
42
+ contentsBegin: 0,
43
+ contentsEnd: 0
44
+ };
45
+ }
@@ -0,0 +1,4 @@
1
+ import type { PhrasingContent } from "mdast";
2
+ import type { ObjectType } from "uniorg";
3
+ import { type TransformContext } from "./context.js";
4
+ export declare function transformPhrasingChildren(ctx: TransformContext, children: PhrasingContent[]): ObjectType[];