@remigius42/morg 0.5.0 → 0.7.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/README.md +12 -1
- package/dist/core/affiliated.d.ts +13 -0
- package/dist/core/affiliated.js +43 -0
- package/dist/core/backslashCommands.d.ts +10 -0
- package/dist/core/backslashCommands.js +43 -0
- package/dist/core/bracedScripts.d.ts +12 -0
- package/dist/core/bracedScripts.js +159 -0
- package/dist/core/footnoteReferences.d.ts +10 -0
- package/dist/core/footnoteReferences.js +24 -0
- package/dist/core/frontmatterBlock.d.ts +89 -0
- package/dist/core/frontmatterBlock.js +448 -0
- package/dist/core/keyValueLines.d.ts +6 -0
- package/dist/core/keyValueLines.js +18 -0
- package/dist/core/lineSyntax.d.ts +21 -0
- package/dist/core/lineSyntax.js +221 -0
- package/dist/core/markupBoundary.d.ts +12 -0
- package/dist/core/markupBoundary.js +195 -0
- package/dist/core/mdastToUniorg/blocks.d.ts +0 -1
- package/dist/core/mdastToUniorg/blocks.js +3 -31
- package/dist/core/mdastToUniorg/index.d.ts +6 -0
- package/dist/core/mdastToUniorg/index.js +81 -8
- package/dist/core/mdastToUniorg/lists.d.ts +1 -1
- package/dist/core/mdastToUniorg/lists.js +21 -2
- package/dist/core/mdastToUniorg/phrasing.js +145 -12
- package/dist/core/orgPath.d.ts +9 -0
- package/dist/core/orgPath.js +24 -0
- package/dist/core/render.d.ts +33 -0
- package/dist/core/render.js +101 -0
- package/dist/core/tablePipes.d.ts +17 -0
- package/dist/core/tablePipes.js +47 -0
- package/dist/core/underscoreBullets.d.ts +10 -0
- package/dist/core/underscoreBullets.js +32 -0
- package/dist/core/uniorgToMdast/elements.js +1 -1
- package/dist/core/uniorgToMdast/index.d.ts +3 -2
- package/dist/core/uniorgToMdast/index.js +100 -31
- package/dist/core/uniorgToMdast/lists.js +12 -1
- package/dist/core/uniorgToMdast/objects.js +44 -6
- package/dist/core/uniorgToMdast/shared.d.ts +5 -0
- package/dist/core/uniorgToMdast/shared.js +3 -12
- package/dist/core/uniorgToMdast/tables.js +12 -6
- package/dist/markdownToOrg.js +44 -23
- package/dist/orgToMarkdown.js +39 -5
- package/dist/presets/logseq.js +130 -0
- package/dist/presets/types.d.ts +4 -1
- package/package.json +4 -1
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { stringify as stringifyYaml } from "yaml";
|
|
2
|
+
import { warn } from "./shared.js";
|
|
3
|
+
import { isModeLineComment, takeFileHeader, takeRenderedFileHeader } from "../frontmatterBlock.js";
|
|
2
4
|
import { collectFootnoteLabels } from "./footnotes.js";
|
|
3
5
|
import { transformNodes } from "./elements.js";
|
|
4
6
|
/**
|
|
5
7
|
* Transforms a uniorg AST to a mdast (Markdown AST).
|
|
6
8
|
* @param uniorgAst The uniorg AST to transform.
|
|
7
|
-
* @param options Controls org-ism serialization (`key:: value` lines)
|
|
9
|
+
* @param options Controls org-ism serialization (`key:: value` lines);
|
|
10
|
+
* `org`, the text a tree was parsed from, finds its frontmatter block.
|
|
8
11
|
* @returns The transformed mdast.
|
|
9
12
|
*/
|
|
10
13
|
export function transformUniorgAstToMdast(uniorgAst, options = {}) {
|
|
14
|
+
// called on its own, not by convertOrgToMarkdown, which takes the
|
|
15
|
+
// header off the org text: the header of the org text the tree was
|
|
16
|
+
// parsed from, or else the one transformMdastToUniorgAst built, from a
|
|
17
|
+
// copy, so the caller's tree stays as it is
|
|
18
|
+
if (options.takesMorgEntries === undefined) {
|
|
19
|
+
uniorgAst = { ...uniorgAst, children: [...uniorgAst.children] };
|
|
20
|
+
const header = options.org === undefined
|
|
21
|
+
? takeRenderedFileHeader(uniorgAst)
|
|
22
|
+
: takeFileHeader(uniorgAst, options.org, options.onWarning);
|
|
23
|
+
options = { ...options, ...header };
|
|
24
|
+
}
|
|
11
25
|
const ctx = {
|
|
12
26
|
options,
|
|
13
27
|
inlineFootnotes: [],
|
|
@@ -15,36 +29,15 @@ export function transformUniorgAstToMdast(uniorgAst, options = {}) {
|
|
|
15
29
|
};
|
|
16
30
|
collectFootnoteLabels(ctx, uniorgAst);
|
|
17
31
|
const nodes = uniorgAst.children || [];
|
|
18
|
-
|
|
19
|
-
|
|
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));
|
|
32
|
+
const { yaml, rest } = takeFrontmatter(ctx, nodes);
|
|
33
|
+
const children = transformNodes(ctx, rest);
|
|
38
34
|
// adjacent single-item task lists (one per converted TODO section)
|
|
39
35
|
// merge into one list, or the output would not be a fixed point
|
|
40
36
|
if (options.taskCheckboxes) {
|
|
41
37
|
mergeAdjacentTaskLists(children);
|
|
42
38
|
}
|
|
43
|
-
if (
|
|
44
|
-
children.unshift({
|
|
45
|
-
type: "yaml",
|
|
46
|
-
value: stringifyYaml(frontmatter).trimEnd()
|
|
47
|
-
});
|
|
39
|
+
if (yaml !== undefined) {
|
|
40
|
+
children.unshift({ type: "yaml", value: yaml });
|
|
48
41
|
}
|
|
49
42
|
for (const footnote of ctx.inlineFootnotes) {
|
|
50
43
|
children.push({
|
|
@@ -56,12 +49,88 @@ export function transformUniorgAstToMdast(uniorgAst, options = {}) {
|
|
|
56
49
|
}
|
|
57
50
|
return { type: "root", children: children };
|
|
58
51
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
// the frontmatter: the block's YAML, and the leading #+KEY: value
|
|
53
|
+
// keywords, which can act in Emacs, in order, repeats included, as
|
|
54
|
+
// morg's own entry (ADR 0005); where they cannot join it, they stay
|
|
55
|
+
// keyword lines. Returns the YAML and the nodes left
|
|
56
|
+
function takeFrontmatter(ctx, nodes) {
|
|
57
|
+
const block = ctx.options.frontmatter;
|
|
58
|
+
// set by the caller or transformUniorgAstToMdast, never undefined
|
|
59
|
+
const joins = ctx.options.takesMorgEntries === true;
|
|
60
|
+
const start = leadingKeywordsStart(nodes);
|
|
61
|
+
warnAboutFrontmatter(ctx, block, joins ? [] : nodes.slice(start));
|
|
62
|
+
const keywords = inertModeLineGuard(ctx, nodes, joins ? leadingKeywords(nodes.slice(start)) : [], start);
|
|
63
|
+
const entries = morgEntries(ctx.options.fileProperties ?? [], keywords);
|
|
64
|
+
const rest = [
|
|
65
|
+
...nodes.slice(0, start),
|
|
66
|
+
...nodes.slice(start + keywords.length)
|
|
67
|
+
];
|
|
68
|
+
// an empty block adds nothing next to the entries
|
|
69
|
+
// on a line of their own: a keep-chomped (`|+`) scalar at the end of
|
|
70
|
+
// the block holds a newline already there
|
|
71
|
+
const yaml = entries && block
|
|
72
|
+
? `${block.replace(/(?<!\n)$/, "\n")}${entries}`
|
|
73
|
+
: (entries ?? block);
|
|
74
|
+
// leading the Markdown body, with or without frontmatter above it
|
|
75
|
+
if (isInertModeLine(ctx, rest[0])) {
|
|
76
|
+
warn(ctx, "a -*- comment below the first line becomes the mode line in org");
|
|
77
|
+
}
|
|
78
|
+
return { yaml, rest };
|
|
79
|
+
}
|
|
80
|
+
// leading keywords may sit below a -*- comment, which leads the
|
|
81
|
+
// Markdown body and so becomes the mode line (warned if it was none);
|
|
82
|
+
// below another comment, they would come back above it
|
|
83
|
+
function leadingKeywordsStart(nodes) {
|
|
84
|
+
return nodes[0] && isModeLineComment(nodes[0]) ? 1 : 0;
|
|
85
|
+
}
|
|
86
|
+
// a -*- comment below the file's first line, which Emacs ignores
|
|
87
|
+
function isInertModeLine(ctx, node) {
|
|
88
|
+
return !ctx.options.startsWithModeLine && !!node && isModeLineComment(node);
|
|
89
|
+
}
|
|
90
|
+
// md→org makes a -*- comment that leads the Markdown body the file's
|
|
91
|
+
// mode line: leading keywords directly above one stay lines, so it
|
|
92
|
+
// does not lead it
|
|
93
|
+
function inertModeLineGuard(ctx, nodes, keywords, start) {
|
|
94
|
+
if (!keywords.length ||
|
|
95
|
+
start !== 0 ||
|
|
96
|
+
!isInertModeLine(ctx, nodes[keywords.length])) {
|
|
97
|
+
return keywords;
|
|
98
|
+
}
|
|
99
|
+
warn(ctx, "leading keywords stay lines: a -*- comment below them is no mode line");
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
function leadingKeywords(nodes) {
|
|
103
|
+
const keywords = [];
|
|
104
|
+
while (nodes[keywords.length]?.type === "keyword") {
|
|
105
|
+
const keyword = nodes[keywords.length];
|
|
106
|
+
keywords.push([keyword.key, keyword.value]);
|
|
107
|
+
}
|
|
108
|
+
return keywords;
|
|
109
|
+
}
|
|
110
|
+
// morg's own entries as YAML text, or undefined without any
|
|
111
|
+
function morgEntries(fileProperties, keywords) {
|
|
112
|
+
// an empty value as `KEY:`, as it reads back, not as `KEY: ""`
|
|
113
|
+
const items = (pairs) => pairs.map(([key, value]) => ({ [key]: value || null }));
|
|
114
|
+
const entries = {
|
|
115
|
+
...(fileProperties.length && { morg_properties: items(fileProperties) }),
|
|
116
|
+
...(keywords.length && { morg_keywords: items(keywords) })
|
|
117
|
+
};
|
|
118
|
+
// unfolded: a value folded over lines fits no keyword line again
|
|
119
|
+
return Object.keys(entries).length
|
|
120
|
+
? stringifyYaml(entries, { lineWidth: 0, nullStr: "" }).trimEnd()
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
123
|
+
// `left`: the leading nodes the frontmatter could not take
|
|
124
|
+
function warnAboutFrontmatter(ctx, block, left) {
|
|
125
|
+
if (block && /^---[ \t]*$/m.test(block)) {
|
|
126
|
+
warn(ctx, "frontmatter holds a --- line, which ends it early in Markdown");
|
|
127
|
+
}
|
|
128
|
+
const drawer = left[0]?.type === "property-drawer";
|
|
129
|
+
if (drawer) {
|
|
130
|
+
warn(ctx, "file-level drawer stays text: the frontmatter cannot take it");
|
|
62
131
|
}
|
|
63
|
-
|
|
64
|
-
|
|
132
|
+
if (left[drawer ? 1 : 0]?.type === "keyword") {
|
|
133
|
+
warn(ctx, "leading keywords stay lines: the frontmatter cannot take them");
|
|
65
134
|
}
|
|
66
135
|
}
|
|
67
136
|
function isTaskList(node) {
|
|
@@ -61,17 +61,28 @@ function transformUniorgList(ctx, node) {
|
|
|
61
61
|
return {
|
|
62
62
|
type: "list",
|
|
63
63
|
ordered,
|
|
64
|
-
start: ordered ? parseInt(firstBullet, 10)
|
|
64
|
+
start: ordered ? parseInt(firstBullet, 10) : null,
|
|
65
65
|
spread: false,
|
|
66
66
|
children: run.map(item => transformUniorgListItem(ctx, item))
|
|
67
67
|
};
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
+
// uniorg keeps a list item's indentation in its code blocks' values;
|
|
71
|
+
// md indents them itself. Only that much is dropped, as uniorg-stringify
|
|
72
|
+
// does, so the code keeps its own
|
|
73
|
+
function outdent(value, level) {
|
|
74
|
+
return value.replace(new RegExp(`^ {0,${level}}`, "gm"), "");
|
|
75
|
+
}
|
|
70
76
|
function transformUniorgListItem(ctx, item) {
|
|
71
77
|
// md has no descriptive lists, so keep the ` :: ` syntax literally in the
|
|
72
78
|
// item text; the return trip re-parses it as a descriptive list
|
|
73
79
|
const tag = listItemTag(item);
|
|
74
80
|
const children = transformNodes(ctx, (item.children || []).filter(child => child !== tag));
|
|
81
|
+
for (const child of children) {
|
|
82
|
+
if (child.type === "code") {
|
|
83
|
+
child.value = outdent(child.value, item.indent + item.bullet.length);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
75
86
|
if (tag) {
|
|
76
87
|
const term = {
|
|
77
88
|
type: "text",
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { toString as orgastToString } from "orgast-util-to-string";
|
|
2
2
|
import { htmlEnabled, orgNodeToText, warn } from "./shared.js";
|
|
3
3
|
import { transformFootnoteReference } from "./footnotes.js";
|
|
4
|
+
import { unescapeOrgPath } from "../orgPath.js";
|
|
4
5
|
const IMAGE_EXTENSION_RE = /\.(png|jpe?g|gif|svg|webp|avif|bmp|ico)$/i;
|
|
5
6
|
export function transformUniorgObjects(ctx, children) {
|
|
6
7
|
return (children || [])
|
|
8
|
+
.map((child, index) =>
|
|
9
|
+
// uniorg keeps the continuation line's indentation after a line
|
|
10
|
+
// break; md would render it as leading whitespace
|
|
11
|
+
child.type === "text" && children?.[index - 1]?.type === "line-break"
|
|
12
|
+
? { ...child, value: child.value.replace(/^[ \t]+/, "") }
|
|
13
|
+
: child)
|
|
7
14
|
.flatMap(child => transformUniorgObjectToMdastPhrasingContent(ctx, child))
|
|
8
15
|
.filter(Boolean);
|
|
9
16
|
}
|
|
@@ -74,12 +81,43 @@ function transformUniorgObjectToMdastPhrasingContent(ctx, node) {
|
|
|
74
81
|
return null;
|
|
75
82
|
}
|
|
76
83
|
}
|
|
84
|
+
// the inverse of md→org's decoding: % and # would be read as an escape
|
|
85
|
+
// or the anchor, and a markdown url cannot hold a bare space. Org path
|
|
86
|
+
// escapes (`[`, `]`, `:`) are undone first rather than escaped twice
|
|
87
|
+
function encodeUrlPart(part) {
|
|
88
|
+
return unescapeOrgPath(part)
|
|
89
|
+
.replaceAll("%", "%25")
|
|
90
|
+
.replaceAll("#", "%23")
|
|
91
|
+
.replaceAll(" ", "%20");
|
|
92
|
+
}
|
|
93
|
+
// a path starting like `a:` would read as a url scheme in markdown
|
|
94
|
+
function encodeUrlPath(path) {
|
|
95
|
+
const encoded = encodeUrlPart(path);
|
|
96
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(encoded)
|
|
97
|
+
? encoded.replaceAll(":", "%3A")
|
|
98
|
+
: encoded;
|
|
99
|
+
}
|
|
100
|
+
// an org file: link (or a ./ path) is a relative markdown link, the
|
|
101
|
+
// search option becoming the #anchor
|
|
102
|
+
function markdownUrl(node) {
|
|
103
|
+
if (node.linkType !== "file") {
|
|
104
|
+
return node.rawLink;
|
|
105
|
+
}
|
|
106
|
+
const target = node.rawLink.replace(/^file:/, "");
|
|
107
|
+
const search = target.indexOf("::");
|
|
108
|
+
return search === -1
|
|
109
|
+
? encodeUrlPath(target)
|
|
110
|
+
: `${encodeUrlPath(target.slice(0, search))}#${encodeUrlPart(target.slice(search + 2))}`;
|
|
111
|
+
}
|
|
77
112
|
function transformUniorgLink(ctx, node) {
|
|
78
113
|
const descriptionText = orgastToString(node);
|
|
114
|
+
const url = markdownUrl(node);
|
|
79
115
|
// org has no dedicated image syntax; the common convention is a
|
|
80
|
-
// link to an image file, so map those to markdown images
|
|
81
|
-
|
|
82
|
-
|
|
116
|
+
// link to an image file, so map those to markdown images; a file
|
|
117
|
+
// link's ::search option is no part of the file name
|
|
118
|
+
const path = node.linkType === "file" ? node.rawLink.replace(/::.*$/s, "") : node.rawLink;
|
|
119
|
+
if (IMAGE_EXTENSION_RE.test(path)) {
|
|
120
|
+
return { type: "image", url, alt: descriptionText };
|
|
83
121
|
}
|
|
84
122
|
// a description equal to the url (a common Logseq pattern) is no
|
|
85
123
|
// description: text === url makes remark-stringify emit an
|
|
@@ -100,8 +138,8 @@ function transformUniorgLink(ctx, node) {
|
|
|
100
138
|
withoutMarkers(serializedDescription) === withoutMarkers(node.rawLink)) {
|
|
101
139
|
return {
|
|
102
140
|
type: "link",
|
|
103
|
-
url
|
|
104
|
-
children: [{ type: "text", value:
|
|
141
|
+
url,
|
|
142
|
+
children: [{ type: "text", value: url }]
|
|
105
143
|
};
|
|
106
144
|
}
|
|
107
145
|
const children = transformUniorgObjects(ctx, node.children);
|
|
@@ -110,7 +148,7 @@ function transformUniorgLink(ctx, node) {
|
|
|
110
148
|
const flattened = children.some(child => child.type === "link" || child.type === "image")
|
|
111
149
|
? [{ type: "text", value: descriptionText }]
|
|
112
150
|
: children;
|
|
113
|
-
return { type: "link", url
|
|
151
|
+
return { type: "link", url, children: flattened };
|
|
114
152
|
}
|
|
115
153
|
function transformScriptMarkup(ctx, node) {
|
|
116
154
|
// markdown has no equivalents; with useHtml render as raw html
|
|
@@ -6,6 +6,11 @@ export interface UniorgToMdastOptions {
|
|
|
6
6
|
taskCheckboxes?: boolean;
|
|
7
7
|
orgismKeys?: Record<string, string>;
|
|
8
8
|
onWarning?: (message: string) => void;
|
|
9
|
+
frontmatter?: string;
|
|
10
|
+
fileProperties?: [string, string][];
|
|
11
|
+
takesMorgEntries?: boolean;
|
|
12
|
+
startsWithModeLine?: boolean;
|
|
13
|
+
org?: string;
|
|
9
14
|
}
|
|
10
15
|
export interface TransformContext {
|
|
11
16
|
options: UniorgToMdastOptions;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { affiliatedEntries } from "../affiliated.js";
|
|
2
2
|
import { unified } from "unified";
|
|
3
3
|
import { uniorgStringify } from "uniorg-stringify";
|
|
4
4
|
import { toggleEnabled } from "../../options.js";
|
|
@@ -41,15 +41,6 @@ export function trimTrailingNewline(value) {
|
|
|
41
41
|
// affiliated keywords (#+CAPTION:, #+NAME:, #+ATTR_*) precede their
|
|
42
42
|
// element as verbatim lines so the return trip re-attaches them natively
|
|
43
43
|
export function affiliatedLines(node) {
|
|
44
|
-
const affiliated = node
|
|
45
|
-
|
|
46
|
-
return Object.entries(affiliated ?? {}).flatMap(([key, value]) => {
|
|
47
|
-
const entries = Array.isArray(value) ? value : [value];
|
|
48
|
-
return entries.map(entry => {
|
|
49
|
-
const text = Array.isArray(entry)
|
|
50
|
-
? entry.map(child => orgastToString(child)).join("")
|
|
51
|
-
: String(entry);
|
|
52
|
-
return `#+${key}: ${text}`;
|
|
53
|
-
});
|
|
54
|
-
});
|
|
44
|
+
const affiliated = node.affiliated;
|
|
45
|
+
return affiliatedEntries(affiliated).map(([key, value]) => `#+${key}: ${value}`);
|
|
55
46
|
}
|
|
@@ -16,12 +16,18 @@ function isAlignmentCookieRow(row) {
|
|
|
16
16
|
cells.some(cell => cellText(cell) !== ""));
|
|
17
17
|
}
|
|
18
18
|
// org cell content keeps the aligning whitespace padding; markdown
|
|
19
|
-
// cells are re-padded by the stringifier
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// cells are re-padded by the stringifier. Only the cell's edges are
|
|
20
|
+
// padding: a space between text and markup is content
|
|
21
|
+
function trimCellPadding(children) {
|
|
22
|
+
const first = children[0];
|
|
23
|
+
if (first?.type === "text") {
|
|
24
|
+
first.value = first.value.trimStart();
|
|
23
25
|
}
|
|
24
|
-
|
|
26
|
+
const last = children.at(-1);
|
|
27
|
+
if (last?.type === "text") {
|
|
28
|
+
last.value = last.value.trimEnd();
|
|
29
|
+
}
|
|
30
|
+
return children;
|
|
25
31
|
}
|
|
26
32
|
export function transformTable(ctx, node) {
|
|
27
33
|
if (node.tableType === "table.el") {
|
|
@@ -59,7 +65,7 @@ export function transformTable(ctx, node) {
|
|
|
59
65
|
type: "tableRow",
|
|
60
66
|
children: (row.children || []).map(cell => ({
|
|
61
67
|
type: "tableCell",
|
|
62
|
-
children: transformUniorgObjects(ctx, cell.children)
|
|
68
|
+
children: trimCellPadding(transformUniorgObjects(ctx, cell.children))
|
|
63
69
|
}))
|
|
64
70
|
}))
|
|
65
71
|
};
|
package/dist/markdownToOrg.js
CHANGED
|
@@ -5,8 +5,16 @@ import remarkFrontmatter from "remark-frontmatter";
|
|
|
5
5
|
import remarkMath from "remark-math";
|
|
6
6
|
import { uniorgStringify } from "uniorg-stringify";
|
|
7
7
|
import { visit } from "unist-util-visit";
|
|
8
|
-
import {
|
|
8
|
+
import { transformMdastToUniorgDraft } from "./core/mdastToUniorg/index.js";
|
|
9
9
|
import { detectMarkdownStyle, STYLE_KEYWORD } from "./core/markdownStyle.js";
|
|
10
|
+
import { escapeOrgMarkup } from "./core/markupBoundary.js";
|
|
11
|
+
import { renderFileHeader } from "./core/frontmatterBlock.js";
|
|
12
|
+
import { escapeLineSyntax } from "./core/lineSyntax.js";
|
|
13
|
+
import { escapeFootnoteReferences } from "./core/footnoteReferences.js";
|
|
14
|
+
import { escapeBackslashCommands } from "./core/backslashCommands.js";
|
|
15
|
+
import { escapeTablePipes } from "./core/tablePipes.js";
|
|
16
|
+
import { requireBracedScripts } from "./core/bracedScripts.js";
|
|
17
|
+
import { keyValueEntries } from "./core/keyValueLines.js";
|
|
10
18
|
/**
|
|
11
19
|
* Converts a Markdown string to an Org-mode string.
|
|
12
20
|
* @param markdown The Markdown string to convert.
|
|
@@ -15,14 +23,9 @@ import { detectMarkdownStyle, STYLE_KEYWORD } from "./core/markdownStyle.js";
|
|
|
15
23
|
*/
|
|
16
24
|
export function convertMarkdownToOrg(markdown, options = {}) {
|
|
17
25
|
// Phase 1: Parse Markdown to mdast
|
|
18
|
-
const mdast =
|
|
19
|
-
.use(remarkParse)
|
|
20
|
-
.use(remarkGfm)
|
|
21
|
-
.use(remarkFrontmatter)
|
|
22
|
-
.use(remarkMath)
|
|
23
|
-
.parse(markdown);
|
|
26
|
+
const mdast = parseMarkdown(markdown, options.preset);
|
|
24
27
|
// Phase 2: Generic mdast to uniorg-ast transformation
|
|
25
|
-
let uniorgAst =
|
|
28
|
+
let uniorgAst = transformMdastToUniorgDraft(mdast, {
|
|
26
29
|
...(options.preserveMdisms !== undefined && {
|
|
27
30
|
preserveMdisms: options.preserveMdisms
|
|
28
31
|
}),
|
|
@@ -52,12 +55,43 @@ export function convertMarkdownToOrg(markdown, options = {}) {
|
|
|
52
55
|
if (options.preset?.applyToUniorg) {
|
|
53
56
|
uniorgAst = options.preset.applyToUniorg(uniorgAst);
|
|
54
57
|
}
|
|
58
|
+
// Phase 3b: literal footnote references, paragraph lines org would
|
|
59
|
+
// read as line syntax, literal markers org would read as markup, and
|
|
60
|
+
// markup touching a word character, need a zero-width space escape;
|
|
61
|
+
// a pipe in a table cell an entity; a literal backslash before a
|
|
62
|
+
// letter first, the entity being none
|
|
63
|
+
escapeBackslashCommands(uniorgAst);
|
|
64
|
+
escapeTablePipes(uniorgAst, options.onWarning);
|
|
65
|
+
escapeFootnoteReferences(uniorgAst);
|
|
66
|
+
escapeLineSyntax(uniorgAst);
|
|
67
|
+
escapeOrgMarkup(uniorgAst);
|
|
68
|
+
// Phase 3c: md text has no sub/superscripts; keep org from reading
|
|
69
|
+
// bare underscores and carets as such. Checked on the text as
|
|
70
|
+
// rendered: after the preset, whose text rewrites (wikilink aliases)
|
|
71
|
+
// org parses too, and after the escapes, next to which org reads them
|
|
72
|
+
requireBracedScripts(uniorgAst);
|
|
73
|
+
// Phase 3d: the file's header (ADR 0005): mode line and file-level
|
|
74
|
+
// drawer first, keywords apart from what follows, the frontmatter as
|
|
75
|
+
// a marked comment block; raw text, past every pass that rewrites text
|
|
76
|
+
renderFileHeader(uniorgAst);
|
|
55
77
|
// Phase 4: Render uniorg-ast to Org-mode string
|
|
56
78
|
const processor = unified().use(uniorgStringify);
|
|
57
79
|
const orgContent = processor.stringify(uniorgAst);
|
|
58
80
|
return orgContent;
|
|
59
81
|
}
|
|
60
|
-
//
|
|
82
|
+
// a preset's dialect conventions that need the Markdown source apply
|
|
83
|
+
// to the parse, before the generic transform
|
|
84
|
+
function parseMarkdown(markdown, preset) {
|
|
85
|
+
const mdast = unified()
|
|
86
|
+
.use(remarkParse)
|
|
87
|
+
.use(remarkGfm)
|
|
88
|
+
.use(remarkFrontmatter)
|
|
89
|
+
.use(remarkMath)
|
|
90
|
+
.parse(markdown);
|
|
91
|
+
preset?.applyToMdast?.(mdast, markdown);
|
|
92
|
+
return mdast;
|
|
93
|
+
}
|
|
94
|
+
// leads the document, ahead of restored keywords and the frontmatter block
|
|
61
95
|
function recordStyleKeyword(uniorgAst, style) {
|
|
62
96
|
if (!Object.keys(style).length) {
|
|
63
97
|
return;
|
|
@@ -68,7 +102,6 @@ function recordStyleKeyword(uniorgAst, style) {
|
|
|
68
102
|
value: JSON.stringify(style)
|
|
69
103
|
});
|
|
70
104
|
}
|
|
71
|
-
const KEY_VALUE_LINE_RE = /^([\w-]+):: (.*)$/;
|
|
72
105
|
function parseKeyValueParagraph(node) {
|
|
73
106
|
if (node?.type !== "paragraph" || !node.children?.length) {
|
|
74
107
|
return null;
|
|
@@ -76,19 +109,7 @@ function parseKeyValueParagraph(node) {
|
|
|
76
109
|
if (!node.children.every(child => child.type === "text")) {
|
|
77
110
|
return null;
|
|
78
111
|
}
|
|
79
|
-
|
|
80
|
-
.map(child => child.value ?? "")
|
|
81
|
-
.join("")
|
|
82
|
-
.split("\n");
|
|
83
|
-
const entries = [];
|
|
84
|
-
for (const line of lines) {
|
|
85
|
-
const match = KEY_VALUE_LINE_RE.exec(line);
|
|
86
|
-
if (!match) {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
entries.push([match[1], match[2]]);
|
|
90
|
-
}
|
|
91
|
-
return entries;
|
|
112
|
+
return keyValueEntries(node.children.map(child => child.value ?? "").join(""));
|
|
92
113
|
}
|
|
93
114
|
function makeTimestamp(rawValue) {
|
|
94
115
|
return { type: "timestamp", rawValue };
|
package/dist/orgToMarkdown.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import { unified } from "unified";
|
|
2
|
-
import uniorgParse from "uniorg-parse";
|
|
3
2
|
import remarkStringify from "remark-stringify";
|
|
4
3
|
import remarkGfm from "remark-gfm";
|
|
5
4
|
import remarkFrontmatter from "remark-frontmatter";
|
|
6
5
|
import remarkMath from "remark-math";
|
|
7
6
|
import { transformUniorgAstToMdast } from "./core/uniorgToMdast/index.js";
|
|
8
7
|
import { takeRecordedStyle } from "./core/markdownStyle.js";
|
|
8
|
+
import { takeFileHeader } from "./core/frontmatterBlock.js";
|
|
9
|
+
import { unescapeOrgMarkup } from "./core/markupBoundary.js";
|
|
10
|
+
import { unescapeLineSyntax } from "./core/lineSyntax.js";
|
|
11
|
+
import { unescapeFootnoteReferences } from "./core/footnoteReferences.js";
|
|
12
|
+
import { unescapeBackslashCommands } from "./core/backslashCommands.js";
|
|
13
|
+
import { unescapeTablePipes } from "./core/tablePipes.js";
|
|
14
|
+
import { parseOrg } from "./core/bracedScripts.js";
|
|
15
|
+
import { dropUnderscoreBulletGuards, guardUnderscoreBullets } from "./core/underscoreBullets.js";
|
|
16
|
+
// a list item's paragraph after its nested list needs a blank line, or
|
|
17
|
+
// md reads it as a lazy continuation of the nested list's last item
|
|
18
|
+
function separateTextAfterNestedList(left, right, parent) {
|
|
19
|
+
return parent.type === "listItem" &&
|
|
20
|
+
left.type === "list" &&
|
|
21
|
+
right.type === "paragraph"
|
|
22
|
+
? 1
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
9
25
|
/**
|
|
10
26
|
* Converts an Org-mode string to a Markdown string.
|
|
11
27
|
* @param org The Org-mode string to convert.
|
|
@@ -14,10 +30,23 @@ import { takeRecordedStyle } from "./core/markdownStyle.js";
|
|
|
14
30
|
*/
|
|
15
31
|
export function convertOrgToMarkdown(org, options = {}) {
|
|
16
32
|
// Phase 1: Parse Org-mode to uniorg-ast
|
|
17
|
-
|
|
33
|
+
// md text has no scripts, so ^:{} is implied there and consumed here
|
|
34
|
+
// (see markdownToOrg); uniorg misreads `_.` lines (see underscoreBullets)
|
|
35
|
+
const guarded = guardUnderscoreBullets(org);
|
|
36
|
+
let uniorgAst = parseOrg(guarded);
|
|
37
|
+
dropUnderscoreBulletGuards(uniorgAst);
|
|
18
38
|
// Phase 1b: a recorded style is morg's own (ADR 0004), so consume it so
|
|
19
39
|
// it does not travel on as frontmatter; explicit options still win
|
|
20
40
|
const recordedStyle = takeRecordedStyle(uniorgAst);
|
|
41
|
+
// frontmatter travels in a block marked as morg's (ADR 0005)
|
|
42
|
+
// and so does a file-level drawer (org-roam's :ID:)
|
|
43
|
+
const fileHeader = takeFileHeader(uniorgAst, guarded, options.onWarning);
|
|
44
|
+
// Phase 1c: markdown needs no zero-width space escapes (inverse of md→org)
|
|
45
|
+
unescapeOrgMarkup(uniorgAst);
|
|
46
|
+
unescapeLineSyntax(uniorgAst);
|
|
47
|
+
unescapeFootnoteReferences(uniorgAst);
|
|
48
|
+
unescapeBackslashCommands(uniorgAst);
|
|
49
|
+
unescapeTablePipes(uniorgAst);
|
|
21
50
|
// Phase 2: Extract dialect preset conventions, if any
|
|
22
51
|
if (options.preset?.extractFromUniorg) {
|
|
23
52
|
uniorgAst = options.preset.extractFromUniorg(uniorgAst);
|
|
@@ -34,7 +63,8 @@ export function convertOrgToMarkdown(org, options = {}) {
|
|
|
34
63
|
...(options.orgismKeys !== undefined && {
|
|
35
64
|
orgismKeys: options.orgismKeys
|
|
36
65
|
}),
|
|
37
|
-
...(options.onWarning !== undefined && { onWarning: options.onWarning })
|
|
66
|
+
...(options.onWarning !== undefined && { onWarning: options.onWarning }),
|
|
67
|
+
...fileHeader
|
|
38
68
|
});
|
|
39
69
|
// Phase 4: Render mdast to Markdown string
|
|
40
70
|
// bullet and rule "-" (not remark's default "*") are morg's canonical
|
|
@@ -46,11 +76,15 @@ export function convertOrgToMarkdown(org, options = {}) {
|
|
|
46
76
|
rule: "-",
|
|
47
77
|
...recordedStyle,
|
|
48
78
|
...options.markdownStyle,
|
|
79
|
+
join: [separateTextAfterNestedList],
|
|
49
80
|
handlers: {
|
|
50
81
|
// key:: value blocks and preset inline passthroughs (e.g.
|
|
51
|
-
// wikilinks) are emitted verbatim, unescaped
|
|
82
|
+
// wikilinks) are emitted verbatim, unescaped; only a pipe inside
|
|
83
|
+
// a table cell is escaped, or it would split the cell
|
|
52
84
|
keyValue: (node) => node.value,
|
|
53
|
-
verbatimInline: (node) =>
|
|
85
|
+
verbatimInline: (node, _parent, state) => state.stack.includes("tableCell")
|
|
86
|
+
? node.value.replaceAll("|", "\\|")
|
|
87
|
+
: node.value
|
|
54
88
|
}
|
|
55
89
|
})
|
|
56
90
|
.use(remarkGfm)
|