@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,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("-->", "-->")
|
|
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("<", "<")
|
|
110
|
+
.replaceAll(">", ">")
|
|
111
|
+
.replaceAll("&", "&");
|
|
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,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,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
|
+
}
|