@remigius42/morg 0.6.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/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 +9 -0
- package/dist/core/lineSyntax.js +15 -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 +72 -7
- package/dist/core/tablePipes.d.ts +12 -3
- package/dist/core/tablePipes.js +38 -5
- 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 +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 +3 -12
- package/dist/markdownToOrg.js +28 -25
- package/dist/orgToMarkdown.js +12 -2
- package/dist/presets/logseq.js +130 -0
- package/dist/presets/types.d.ts +4 -1
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// a `key:: value` line, Logseq's property syntax; an empty `key::` too
|
|
2
|
+
const KEY_VALUE_LINE_RE = /^([\w-]+)::(?: (.*))?$/;
|
|
3
|
+
/**
|
|
4
|
+
* The key, value pairs of a block of `key:: value` lines.
|
|
5
|
+
* @param text The block's text.
|
|
6
|
+
* @returns The pairs in order, or null if a line is none.
|
|
7
|
+
*/
|
|
8
|
+
export function keyValueEntries(text) {
|
|
9
|
+
const entries = [];
|
|
10
|
+
for (const line of text.replace(/\r?\n$/, "").split(/\r?\n/)) {
|
|
11
|
+
const match = KEY_VALUE_LINE_RE.exec(line);
|
|
12
|
+
if (!match) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
entries.push([match[1] ?? "", match[2] ?? ""]);
|
|
16
|
+
}
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { Parent } from "unist";
|
|
2
|
+
import { type Node } from "./render.js";
|
|
2
3
|
/**
|
|
3
4
|
* md→org: a paragraph line org would read as line syntax (an escaped
|
|
4
5
|
* `1\.` or `\*`, or a lazy continuation line) gets a leading zero-width
|
|
5
6
|
* space, or it would turn into a list item, headline, comment or table.
|
|
6
7
|
*/
|
|
7
8
|
export declare function escapeLineSyntax(tree: Parent): void;
|
|
9
|
+
/**
|
|
10
|
+
* md→org: whether a node is a paragraph org→md wrote as the org text of
|
|
11
|
+
* one passthrough element, which goes back as it is.
|
|
12
|
+
* @param node The node.
|
|
13
|
+
* @param index Its index in its parent.
|
|
14
|
+
* @param parent Its parent.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPassthroughParagraph(node: Node | Parent, index: number, parent: Parent | undefined): boolean;
|
|
8
17
|
/**
|
|
9
18
|
* org→md: drops the line-start zero-width spaces `escapeLineSyntax`
|
|
10
19
|
* inserts.
|
package/dist/core/lineSyntax.js
CHANGED
|
@@ -139,6 +139,21 @@ function isPassthrough(starts) {
|
|
|
139
139
|
const [only, ...rest] = tryParse(`${lines.join("\n")}\n`)?.children ?? [];
|
|
140
140
|
return !rest.length && PASSTHROUGH_TYPES.has(only?.type ?? "");
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* md→org: whether a node is a paragraph org→md wrote as the org text of
|
|
144
|
+
* one passthrough element, which goes back as it is.
|
|
145
|
+
* @param node The node.
|
|
146
|
+
* @param index Its index in its parent.
|
|
147
|
+
* @param parent Its parent.
|
|
148
|
+
*/
|
|
149
|
+
export function isPassthroughParagraph(node, index, parent) {
|
|
150
|
+
if (node.type !== "paragraph") {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const context = contextOf(node, index, parent);
|
|
154
|
+
const children = node.children;
|
|
155
|
+
return (!context.afterBullet && isPassthrough(lineStarts(children, context).starts));
|
|
156
|
+
}
|
|
142
157
|
// line syntax spanning lines (`#+begin_src`…`#+end_src`, a drawer) or
|
|
143
158
|
// depending on context (planning below a headline) shows only in the
|
|
144
159
|
// whole content as org reads it; each round escapes the first line of
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { RootContent, PhrasingContent } from "mdast";
|
|
2
2
|
import type { ElementType } from "uniorg";
|
|
3
3
|
import { type TransformContext } from "./context.js";
|
|
4
|
-
export declare function frontmatterToKeywords(yamlValue: string): ElementType[];
|
|
5
4
|
export declare function transformMdastTable(ctx: TransformContext, node: Extract<RootContent, {
|
|
6
5
|
type: "table";
|
|
7
6
|
}>): ElementType;
|
|
@@ -1,35 +1,7 @@
|
|
|
1
1
|
import { toString } from "orgast-util-to-string";
|
|
2
|
-
import { parse as parseYaml } from "yaml";
|
|
3
2
|
import { mdismEnabled } from "./context.js";
|
|
4
3
|
import { transformPhrasingChildren } from "./phrasing.js";
|
|
5
|
-
|
|
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
|
-
}
|
|
4
|
+
import { KEYWORD_NAME } from "../frontmatterBlock.js";
|
|
33
5
|
export function transformMdastTable(ctx, node) {
|
|
34
6
|
const [headerRow, ...bodyRows] = node.children;
|
|
35
7
|
// GFM column alignment maps to an org alignment cookie row
|
|
@@ -196,7 +168,7 @@ export function transformMdastHeading(ctx, node) {
|
|
|
196
168
|
children: transformPhrasingChildren(ctx, node.children)
|
|
197
169
|
};
|
|
198
170
|
}
|
|
199
|
-
const KEYWORD_LINE_RE =
|
|
171
|
+
const KEYWORD_LINE_RE = new RegExp(String.raw `^#\+${KEYWORD_NAME}: `);
|
|
200
172
|
export function keywordOnlyLines(node) {
|
|
201
173
|
if (!node.children.every(child => child.type === "text")) {
|
|
202
174
|
return null;
|
|
@@ -204,7 +176,7 @@ export function keywordOnlyLines(node) {
|
|
|
204
176
|
const lines = node.children
|
|
205
177
|
.map(child => child.value)
|
|
206
178
|
.join("")
|
|
207
|
-
.split(
|
|
179
|
+
.split(/\r?\n/);
|
|
208
180
|
return lines.length && lines.every(line => KEYWORD_LINE_RE.test(line))
|
|
209
181
|
? lines
|
|
210
182
|
: null;
|
|
@@ -10,4 +10,10 @@ import type { MdastToUniorgOptions } from "./context.js";
|
|
|
10
10
|
* @returns The transformed uniorg AST.
|
|
11
11
|
*/
|
|
12
12
|
export declare function transformMdastToUniorgAst(mdast: MdastRoot, options?: MdastToUniorgOptions): OrgData;
|
|
13
|
+
/**
|
|
14
|
+
* md→org pipeline: like `transformMdastToUniorgAst`, but the frontmatter
|
|
15
|
+
* stays a `morg-frontmatter` node a preset can still take entries out
|
|
16
|
+
* of; `renderFileHeader` turns it into the block.
|
|
17
|
+
*/
|
|
18
|
+
export declare function transformMdastToUniorgDraft(mdast: MdastRoot, options?: MdastToUniorgOptions): OrgData;
|
|
13
19
|
export declare function transformMdastNodeToUniorgNode(ctx: TransformContext, node: RootContent | PhrasingContent): GreaterElementType | ElementType | Text | null;
|
|
@@ -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
|
+
}
|
|
@@ -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) {
|
|
@@ -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";
|
|
@@ -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
|
}
|