@remigius42/morg 0.6.0 → 0.8.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 +23 -11
- 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 +7 -0
- package/dist/core/bracedScripts.js +10 -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 +16 -0
- package/dist/core/lineSyntax.js +25 -2
- package/dist/core/mdastToUniorg/blocks.d.ts +0 -1
- package/dist/core/mdastToUniorg/blocks.js +7 -32
- package/dist/core/mdastToUniorg/index.d.ts +6 -0
- package/dist/core/mdastToUniorg/index.js +72 -7
- package/dist/core/passthroughSource.d.ts +21 -0
- package/dist/core/passthroughSource.js +94 -0
- package/dist/core/tablePipes.d.ts +12 -3
- package/dist/core/tablePipes.js +38 -5
- package/dist/core/uniorgToMdast/elements.js +6 -1
- package/dist/core/uniorgToMdast/index.d.ts +3 -2
- package/dist/core/uniorgToMdast/index.js +100 -31
- package/dist/core/uniorgToMdast/lists.js +1 -1
- package/dist/core/uniorgToMdast/objects.js +6 -0
- package/dist/core/uniorgToMdast/shared.d.ts +5 -0
- package/dist/core/uniorgToMdast/shared.js +9 -13
- package/dist/index.d.ts +1 -2
- package/dist/markdownToOrg.js +41 -25
- package/dist/orgToMarkdown.js +18 -2
- package/dist/presets/logseq.d.ts +3 -22
- package/dist/presets/logseq.js +269 -186
- package/dist/presets/logseqOutline.d.ts +22 -0
- package/dist/presets/logseqOutline.js +251 -0
- package/dist/presets/types.d.ts +12 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
2
|
import { warn } from "./context.js";
|
|
3
|
-
import {
|
|
3
|
+
import { fitsKeywordLine, fitsPropertyLine, markModeLine, renderFileHeader, takeMorgEntry, takesMorgEntries } from "../frontmatterBlock.js";
|
|
4
|
+
import { keywordOnlyLines, transformMdastCode, transformMdastHeading, transformMdastHtml, transformMdastMath, transformMdastTable } from "./blocks.js";
|
|
4
5
|
import { transformMdastList } from "./lists.js";
|
|
5
6
|
import { transformPhrasingChildren } from "./phrasing.js";
|
|
6
7
|
/**
|
|
@@ -10,6 +11,16 @@ import { transformPhrasingChildren } from "./phrasing.js";
|
|
|
10
11
|
* @returns The transformed uniorg AST.
|
|
11
12
|
*/
|
|
12
13
|
export function transformMdastToUniorgAst(mdast, options = {}) {
|
|
14
|
+
const orgAst = transformMdastToUniorgDraft(mdast, options);
|
|
15
|
+
renderFileHeader(orgAst);
|
|
16
|
+
return orgAst;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* md→org pipeline: like `transformMdastToUniorgAst`, but the frontmatter
|
|
20
|
+
* stays a `morg-frontmatter` node a preset can still take entries out
|
|
21
|
+
* of; `renderFileHeader` turns it into the block.
|
|
22
|
+
*/
|
|
23
|
+
export function transformMdastToUniorgDraft(mdast, options = {}) {
|
|
13
24
|
const ctx = { options, definitions: new Map() };
|
|
14
25
|
visit(mdast, "definition", (definition) => {
|
|
15
26
|
ctx.definitions.set(definition.identifier, {
|
|
@@ -17,13 +28,20 @@ export function transformMdastToUniorgAst(mdast, options = {}) {
|
|
|
17
28
|
...(definition.title != null && { title: definition.title })
|
|
18
29
|
});
|
|
19
30
|
});
|
|
31
|
+
const firstBody = mdast.children.find(child => child.type !== "yaml");
|
|
20
32
|
const children = mdast.children
|
|
21
|
-
.flatMap(child =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
.flatMap(child => {
|
|
34
|
+
// frontmatter is passive data, org keywords can act; it travels
|
|
35
|
+
// inert in a marked comment block (ADR 0005)
|
|
36
|
+
if (child.type === "yaml") {
|
|
37
|
+
return transformFrontmatter(child.value);
|
|
38
|
+
}
|
|
39
|
+
const node = transformMdastNodeToUniorgNode(ctx, child);
|
|
40
|
+
if (child === firstBody) {
|
|
41
|
+
markModeLine(node);
|
|
42
|
+
}
|
|
43
|
+
return [node];
|
|
44
|
+
})
|
|
27
45
|
.filter(Boolean);
|
|
28
46
|
const orgAst = {
|
|
29
47
|
type: "org-data",
|
|
@@ -101,3 +119,50 @@ function quotedHeadingAsText(ctx, node) {
|
|
|
101
119
|
warn(ctx, "heading inside a blockquote became text");
|
|
102
120
|
return { type: "paragraph", children: node.children };
|
|
103
121
|
}
|
|
122
|
+
// morg's own entries, both or neither: one left in the YAML would keep
|
|
123
|
+
// the other from joining it again on the way back
|
|
124
|
+
function takeMorgEntries(yaml) {
|
|
125
|
+
// most frontmatter holds neither: no need to parse it
|
|
126
|
+
if (!yaml.includes("morg_")) {
|
|
127
|
+
return { keywords: [], properties: [], yaml };
|
|
128
|
+
}
|
|
129
|
+
const taken = takeMorgEntry(yaml, "morg_keywords", fitsKeywordLine);
|
|
130
|
+
const rest = takeMorgEntry(taken.yaml, "morg_properties", fitsPropertyLine);
|
|
131
|
+
return takesMorgEntries(rest.yaml)
|
|
132
|
+
? { keywords: taken.keywords, properties: rest.keywords, yaml: rest.yaml }
|
|
133
|
+
: { keywords: [], properties: [], yaml };
|
|
134
|
+
}
|
|
135
|
+
function keywordNodes(keywords) {
|
|
136
|
+
return keywords.map(([key, value]) => ({
|
|
137
|
+
type: "keyword",
|
|
138
|
+
affiliated: {},
|
|
139
|
+
key,
|
|
140
|
+
value
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
// org's own keywords go back to keywords; the rest of the frontmatter
|
|
144
|
+
// stays inert data in the block
|
|
145
|
+
function transformFrontmatter(value) {
|
|
146
|
+
// in the org file's line endings, which are LF
|
|
147
|
+
const lf = value.replaceAll("\r\n", "\n");
|
|
148
|
+
const { keywords, properties, yaml } = takeMorgEntries(lf);
|
|
149
|
+
const drawer = properties.length
|
|
150
|
+
? [
|
|
151
|
+
{
|
|
152
|
+
type: "property-drawer",
|
|
153
|
+
children: properties.map(([key, propertyValue]) => ({
|
|
154
|
+
type: "node-property",
|
|
155
|
+
key,
|
|
156
|
+
value: propertyValue
|
|
157
|
+
}))
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
: [];
|
|
161
|
+
const nodes = keywordNodes(keywords);
|
|
162
|
+
// an empty frontmatter stays a block
|
|
163
|
+
if (yaml || !(keywords.length || properties.length)) {
|
|
164
|
+
const node = { type: "morg-frontmatter", yaml };
|
|
165
|
+
nodes.push(node);
|
|
166
|
+
}
|
|
167
|
+
return [...drawer, ...nodes];
|
|
168
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
/** Whether a line opens an org block (`#+begin_name`). */
|
|
3
|
+
export declare function isOrgBlockStart(line: string): boolean;
|
|
4
|
+
/** Whether a line opens an org drawer (`:NAME:`). */
|
|
5
|
+
export declare function isDrawerStart(line: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The line that ends the org block or drawer a line opens.
|
|
8
|
+
* @param lines The lines.
|
|
9
|
+
* @param start The index of the opening line.
|
|
10
|
+
* @returns The index of the `#+end_name` or `:END:` line, or -1.
|
|
11
|
+
*/
|
|
12
|
+
export declare function orgElementEnd(lines: string[], start: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* md→org: a passthrough element's org text may hold lines Markdown reads
|
|
15
|
+
* as syntax of its own (a blank line and an indented one, a `#`, `-`
|
|
16
|
+
* or `>` line); the top-level nodes it parsed into become one paragraph
|
|
17
|
+
* of the source text again, which goes back to org as it is.
|
|
18
|
+
* @param mdast The parsed Markdown.
|
|
19
|
+
* @param markdown Its source.
|
|
20
|
+
*/
|
|
21
|
+
export declare function keepPassthroughSource(mdast: Root, markdown: string): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readsAsPassthrough } from "./lineSyntax.js";
|
|
2
|
+
// the first line of an org block or drawer
|
|
3
|
+
const BLOCK_START_RE = /^#\+begin_(\S+)/i;
|
|
4
|
+
const DRAWER_START_RE = /^:[\w-]+:$/;
|
|
5
|
+
/** Whether a line opens an org block (`#+begin_name`). */
|
|
6
|
+
export function isOrgBlockStart(line) {
|
|
7
|
+
return BLOCK_START_RE.test(line);
|
|
8
|
+
}
|
|
9
|
+
/** Whether a line opens an org drawer (`:NAME:`). */
|
|
10
|
+
export function isDrawerStart(line) {
|
|
11
|
+
return DRAWER_START_RE.test(line);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The line that ends the org block or drawer a line opens.
|
|
15
|
+
* @param lines The lines.
|
|
16
|
+
* @param start The index of the opening line.
|
|
17
|
+
* @returns The index of the `#+end_name` or `:END:` line, or -1.
|
|
18
|
+
*/
|
|
19
|
+
export function orgElementEnd(lines, start) {
|
|
20
|
+
const end = endPattern(lines[start] ?? "");
|
|
21
|
+
return end ? lines.findIndex((other, i) => i > start && end.test(other)) : -1;
|
|
22
|
+
}
|
|
23
|
+
// the pattern of the line that ends the block or drawer `line` opens
|
|
24
|
+
function endPattern(line) {
|
|
25
|
+
const block = BLOCK_START_RE.exec(line)?.[1];
|
|
26
|
+
if (block) {
|
|
27
|
+
return new RegExp(`^#\\+end_${block.replace(/\W/g, "\\$&")}\\s*$`, "i");
|
|
28
|
+
}
|
|
29
|
+
return DRAWER_START_RE.test(line) ? /^:end:\s*$/i : null;
|
|
30
|
+
}
|
|
31
|
+
// the text from `offset` through the line that ends the block or
|
|
32
|
+
// drawer opening there, read line by line rather than splitting the
|
|
33
|
+
// rest of the document (a page may hold thousands of drawers)
|
|
34
|
+
function elementSource(markdown, offset) {
|
|
35
|
+
const lineEnd = (from) => {
|
|
36
|
+
const index = markdown.indexOf("\n", from);
|
|
37
|
+
return index === -1 ? markdown.length : index;
|
|
38
|
+
};
|
|
39
|
+
let end = lineEnd(offset);
|
|
40
|
+
const pattern = endPattern(markdown.slice(offset, end));
|
|
41
|
+
while (pattern && end < markdown.length) {
|
|
42
|
+
const next = lineEnd(end + 1);
|
|
43
|
+
if (pattern.test(markdown.slice(end + 1, next))) {
|
|
44
|
+
return markdown.slice(offset, next);
|
|
45
|
+
}
|
|
46
|
+
end = next;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// the source offset where the passthrough element starting at a node
|
|
51
|
+
// ends, or -1
|
|
52
|
+
function passthroughEnd(node, markdown) {
|
|
53
|
+
const start = node.position?.start;
|
|
54
|
+
if (start?.column !== 1 || start.offset === undefined) {
|
|
55
|
+
return -1;
|
|
56
|
+
}
|
|
57
|
+
const source = elementSource(markdown, start.offset);
|
|
58
|
+
return source !== null && readsAsPassthrough(source)
|
|
59
|
+
? start.offset + source.length
|
|
60
|
+
: -1;
|
|
61
|
+
}
|
|
62
|
+
// the index of the last node ending at `end`, from `i` on, or -1
|
|
63
|
+
function lastNodeAt(nodes, i, end) {
|
|
64
|
+
let j = i;
|
|
65
|
+
while ((nodes[j]?.position?.end.offset ?? end) < end) {
|
|
66
|
+
j++;
|
|
67
|
+
}
|
|
68
|
+
return nodes[j]?.position?.end.offset === end ? j : -1;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* md→org: a passthrough element's org text may hold lines Markdown reads
|
|
72
|
+
* as syntax of its own (a blank line and an indented one, a `#`, `-`
|
|
73
|
+
* or `>` line); the top-level nodes it parsed into become one paragraph
|
|
74
|
+
* of the source text again, which goes back to org as it is.
|
|
75
|
+
* @param mdast The parsed Markdown.
|
|
76
|
+
* @param markdown Its source.
|
|
77
|
+
*/
|
|
78
|
+
export function keepPassthroughSource(mdast, markdown) {
|
|
79
|
+
const children = [];
|
|
80
|
+
const nodes = mdast.children;
|
|
81
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
82
|
+
const node = nodes[i];
|
|
83
|
+
const end = passthroughEnd(node, markdown);
|
|
84
|
+
const last = end === -1 ? -1 : lastNodeAt(nodes, i, end);
|
|
85
|
+
if (last === -1) {
|
|
86
|
+
children.push(node);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const value = markdown.slice(node.position?.start.offset, end);
|
|
90
|
+
children.push({ type: "paragraph", children: [{ type: "text", value }] });
|
|
91
|
+
i = last;
|
|
92
|
+
}
|
|
93
|
+
mdast.children = children;
|
|
94
|
+
}
|
|
@@ -2,7 +2,16 @@ import type { Parent } from "unist";
|
|
|
2
2
|
/**
|
|
3
3
|
* md→org: a `|` in a table cell's text (md `\|`) would end the cell; org
|
|
4
4
|
* has no escaped `|`, but renders the `\vert` entity as one, `{}` ending
|
|
5
|
-
* its name before any letter.
|
|
6
|
-
*
|
|
5
|
+
* its name before any letter. In code, where the entity would be
|
|
6
|
+
* literal, the lookalike stands in, which is reported. Runs after the
|
|
7
|
+
* preset, whose wikilink aliases (`[[Page|alias]]`) are no text.
|
|
8
|
+
* @param tree The document.
|
|
9
|
+
* @param onWarning Reports a lookalike written.
|
|
7
10
|
*/
|
|
8
|
-
export declare function escapeTablePipes(tree: Parent): void;
|
|
11
|
+
export declare function escapeTablePipes(tree: Parent, onWarning?: (message: string) => void): void;
|
|
12
|
+
/**
|
|
13
|
+
* org→md: the lookalike in code in a table cell back to the `|` it
|
|
14
|
+
* stands in for.
|
|
15
|
+
* @param tree The document.
|
|
16
|
+
*/
|
|
17
|
+
export declare function unescapeTablePipes(tree: Parent): void;
|
package/dist/core/tablePipes.js
CHANGED
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
|
+
// org has no escape inside ~code~ or =verbatim=: a `|` there would end
|
|
3
|
+
// the cell, so a lookalike (U+2223 DIVIDES) stands in for it
|
|
4
|
+
const LOOKALIKE = "∣";
|
|
2
5
|
/**
|
|
3
6
|
* md→org: a `|` in a table cell's text (md `\|`) would end the cell; org
|
|
4
7
|
* has no escaped `|`, but renders the `\vert` entity as one, `{}` ending
|
|
5
|
-
* its name before any letter.
|
|
6
|
-
*
|
|
8
|
+
* its name before any letter. In code, where the entity would be
|
|
9
|
+
* literal, the lookalike stands in, which is reported. Runs after the
|
|
10
|
+
* preset, whose wikilink aliases (`[[Page|alias]]`) are no text.
|
|
11
|
+
* @param tree The document.
|
|
12
|
+
* @param onWarning Reports a lookalike written.
|
|
7
13
|
*/
|
|
8
|
-
export function escapeTablePipes(tree) {
|
|
14
|
+
export function escapeTablePipes(tree, onWarning) {
|
|
15
|
+
let replaced = false;
|
|
9
16
|
visit(tree, "table-cell", (cell) => {
|
|
10
|
-
visit(cell,
|
|
11
|
-
node.
|
|
17
|
+
visit(cell, (node) => {
|
|
18
|
+
if (node.type === "text") {
|
|
19
|
+
node.value = node.value?.replaceAll("|", "\\vert{}");
|
|
20
|
+
}
|
|
21
|
+
else if (isCode(node) && node.value?.includes("|")) {
|
|
22
|
+
node.value = node.value.replaceAll("|", LOOKALIKE);
|
|
23
|
+
replaced = true;
|
|
24
|
+
}
|
|
12
25
|
});
|
|
13
26
|
});
|
|
27
|
+
if (replaced) {
|
|
28
|
+
onWarning?.("a | in code in a table cell becomes ∣ (U+2223) in org");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* org→md: the lookalike in code in a table cell back to the `|` it
|
|
33
|
+
* stands in for.
|
|
34
|
+
* @param tree The document.
|
|
35
|
+
*/
|
|
36
|
+
export function unescapeTablePipes(tree) {
|
|
37
|
+
visit(tree, "table-cell", (cell) => {
|
|
38
|
+
visit(cell, (node) => {
|
|
39
|
+
if (isCode(node)) {
|
|
40
|
+
node.value = node.value?.replaceAll(LOOKALIKE, "|");
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function isCode(node) {
|
|
46
|
+
return node.type === "code" || node.type === "verbatim";
|
|
14
47
|
}
|
|
@@ -180,7 +180,7 @@ function transformPropertyDrawer(ctx, node) {
|
|
|
180
180
|
}
|
|
181
181
|
const isms = (node.children || [])
|
|
182
182
|
.filter(child => child.type === "node-property")
|
|
183
|
-
.map(
|
|
183
|
+
.map(({ key, value }) => (value ? `${key}:: ${value}` : `${key}::`));
|
|
184
184
|
return isms.length ? keyValueParagraph(isms) : null;
|
|
185
185
|
}
|
|
186
186
|
function transformParagraph(ctx, node) {
|
|
@@ -253,9 +253,14 @@ function transformQuoteBlock(ctx, node) {
|
|
|
253
253
|
};
|
|
254
254
|
}
|
|
255
255
|
function transformSrcBlock(node) {
|
|
256
|
+
// switches and header arguments (`-n :results output`) are the
|
|
257
|
+
// fence's meta, after the language
|
|
258
|
+
const { switches, parameters } = node;
|
|
259
|
+
const meta = [switches, parameters].filter(Boolean).join(" ");
|
|
256
260
|
return {
|
|
257
261
|
type: "code",
|
|
258
262
|
lang: node.language || null,
|
|
263
|
+
meta: node.language && meta ? meta : null,
|
|
259
264
|
value: trimTrailingNewline(node.value)
|
|
260
265
|
};
|
|
261
266
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Root as MdastRoot } from "mdast";
|
|
2
2
|
import type { OrgData } from "uniorg";
|
|
3
|
-
import type
|
|
3
|
+
import { type UniorgToMdastOptions } from "./shared.js";
|
|
4
4
|
export type { UniorgToMdastOptions } from "./shared.js";
|
|
5
5
|
/**
|
|
6
6
|
* Transforms a uniorg AST to a mdast (Markdown AST).
|
|
7
7
|
* @param uniorgAst The uniorg AST to transform.
|
|
8
|
-
* @param options Controls org-ism serialization (`key:: value` lines)
|
|
8
|
+
* @param options Controls org-ism serialization (`key:: value` lines);
|
|
9
|
+
* `org`, the text a tree was parsed from, finds its frontmatter block.
|
|
9
10
|
* @returns The transformed mdast.
|
|
10
11
|
*/
|
|
11
12
|
export declare function transformUniorgAstToMdast(uniorgAst: OrgData, options?: UniorgToMdastOptions): MdastRoot;
|
|
@@ -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,7 +61,7 @@ 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
|
};
|
|
@@ -5,6 +5,12 @@ import { unescapeOrgPath } from "../orgPath.js";
|
|
|
5
5
|
const IMAGE_EXTENSION_RE = /\.(png|jpe?g|gif|svg|webp|avif|bmp|ico)$/i;
|
|
6
6
|
export function transformUniorgObjects(ctx, children) {
|
|
7
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)
|
|
8
14
|
.flatMap(child => transformUniorgObjectToMdastPhrasingContent(ctx, child))
|
|
9
15
|
.filter(Boolean);
|
|
10
16
|
}
|
|
@@ -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";
|
|
@@ -24,7 +24,12 @@ export function keyValueParagraph(lines) {
|
|
|
24
24
|
// trailing newline), for verbatim passthrough of org-only constructs
|
|
25
25
|
export function orgNodeToText(node) {
|
|
26
26
|
const orgText = unified()
|
|
27
|
-
|
|
27
|
+
// a preset's verbatim-inline text is org text already
|
|
28
|
+
.use(uniorgStringify, {
|
|
29
|
+
handlers: {
|
|
30
|
+
"verbatim-inline": (inline) => inline.value
|
|
31
|
+
}
|
|
32
|
+
})
|
|
28
33
|
.stringify({
|
|
29
34
|
type: "org-data",
|
|
30
35
|
children: [node],
|
|
@@ -41,15 +46,6 @@ export function trimTrailingNewline(value) {
|
|
|
41
46
|
// affiliated keywords (#+CAPTION:, #+NAME:, #+ATTR_*) precede their
|
|
42
47
|
// element as verbatim lines so the return trip re-attaches them natively
|
|
43
48
|
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
|
-
});
|
|
49
|
+
const affiliated = node.affiliated;
|
|
50
|
+
return affiliatedEntries(affiliated).map(([key, value]) => `#+${key}: ${value}`);
|
|
55
51
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,5 @@ export { transformMdastToUniorgAst } from "./core/mdastToUniorg/index.js";
|
|
|
8
8
|
export { transformUniorgAstToMdast } from "./core/uniorgToMdast/index.js";
|
|
9
9
|
export { logseq } from "./presets/logseq.js";
|
|
10
10
|
export { obsidian } from "./presets/obsidian.js";
|
|
11
|
-
export type { Preset } from "./presets/types.js";
|
|
12
|
-
export type { LogseqPresetOptions } from "./presets/logseq.js";
|
|
11
|
+
export type { FragmentConverter, Preset } from "./presets/types.js";
|
|
13
12
|
export type { Toggle, MarkdownToOrgOptions, OrgToMarkdownOptions, MarkdownStyleOptions } from "./options.js";
|