@remigius42/morg 0.8.0 → 0.9.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 +22 -6
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +2 -0
- package/dist/cli/conversion.d.ts +3 -3
- package/dist/cli/conversion.js +4 -4
- package/dist/cli/flags.d.ts +1 -1
- package/dist/cli/flags.js +15 -1
- package/dist/cli/formats.d.ts +2 -1
- package/dist/cli/formats.js +1 -0
- package/dist/cli/presets.d.ts +13 -2
- package/dist/cli/presets.js +17 -3
- package/dist/cli.js +4 -5
- package/dist/config.d.ts +9 -4
- package/dist/config.js +2 -0
- package/dist/conversionOptions.d.ts +19 -3
- package/dist/conversionOptions.js +61 -0
- package/dist/core/frontmatterBlock.d.ts +7 -0
- package/dist/core/frontmatterBlock.js +7 -2
- package/dist/core/lineSyntax.d.ts +6 -0
- package/dist/core/lineSyntax.js +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/markdownToOrg.js +15 -15
- package/dist/normalize.js +16 -2
- package/dist/options.d.ts +3 -7
- package/dist/orgToMarkdown.js +13 -9
- package/dist/presets/hooks.d.ts +49 -0
- package/dist/presets/hooks.js +72 -0
- package/dist/presets/logseq.js +187 -29
- package/dist/presets/logseqOutline.d.ts +29 -4
- package/dist/presets/logseqOutline.js +268 -91
- package/dist/presets/logseqVanillaMarkdown.d.ts +9 -0
- package/dist/presets/logseqVanillaMarkdown.js +295 -0
- package/dist/presets/obsidian.js +4 -2
- package/dist/presets/registry.d.ts +1 -1
- package/dist/presets/registry.js +5 -3
- package/dist/presets/sides.d.ts +40 -0
- package/dist/presets/sides.js +42 -0
- package/dist/presets/types.d.ts +49 -16
- package/package.json +1 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { unified } from "unified";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
5
|
+
import remarkMath from "remark-math";
|
|
6
|
+
import { visit } from "unist-util-visit";
|
|
7
|
+
// Vanilla Markdown read as Logseq's outline (ADR 0006), as Logseq reads
|
|
8
|
+
// a file it did not write: a list item is a block, its nested list its
|
|
9
|
+
// children.
|
|
10
|
+
function parse(markdown) {
|
|
11
|
+
return unified()
|
|
12
|
+
.use(remarkParse)
|
|
13
|
+
.use(remarkGfm)
|
|
14
|
+
.use(remarkFrontmatter)
|
|
15
|
+
.use(remarkMath)
|
|
16
|
+
.parse(markdown);
|
|
17
|
+
}
|
|
18
|
+
// a node's source lines, 0-based start and end line
|
|
19
|
+
function span(node) {
|
|
20
|
+
return [
|
|
21
|
+
(node.position?.start.line ?? 1) - 1,
|
|
22
|
+
(node.position?.end.line ?? 1) - 1
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
const BULLET_RE = /^\s*(?:[-*+]|\d+[.)])(?: |$)/;
|
|
26
|
+
const CHECKBOX_RE = /^\[([ xX])\](?: |$)/;
|
|
27
|
+
// the markers Logseq shows unchecked other than TODO, which a task item
|
|
28
|
+
// writes after its checkbox
|
|
29
|
+
const UNCHECKED_MARKER_RE = /^(?:NOW|LATER|DOING|IN-PROGRESS|WAIT|WAITING) /;
|
|
30
|
+
const PROPERTY_RE = /^([\w.-]+)::(?: (.*))?$/;
|
|
31
|
+
const HEADING_RE = /^(#{1,6})(?: (.*))?$/;
|
|
32
|
+
// a task item's checkbox as Logseq's task marker
|
|
33
|
+
function taskTitle(item, title) {
|
|
34
|
+
const checkbox = CHECKBOX_RE.exec(title);
|
|
35
|
+
if (item.checked === null || item.checked === undefined || !checkbox) {
|
|
36
|
+
return title;
|
|
37
|
+
}
|
|
38
|
+
const text = title.slice(checkbox[0].length);
|
|
39
|
+
if (item.checked) {
|
|
40
|
+
return `DONE ${text}`;
|
|
41
|
+
}
|
|
42
|
+
return UNCHECKED_MARKER_RE.test(text) ? text : `TODO ${text}`;
|
|
43
|
+
}
|
|
44
|
+
// key:: lines: planning under its org-ism names (orgismKeys maps them),
|
|
45
|
+
// one planning line where the first of them was, and properties
|
|
46
|
+
function readMeta(lines, context) {
|
|
47
|
+
const canonical = new Map(Object.entries(context.orgismKeys ?? {}).map(([key, custom]) => [
|
|
48
|
+
custom,
|
|
49
|
+
key
|
|
50
|
+
]));
|
|
51
|
+
const meta = [];
|
|
52
|
+
let planning;
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const [, rawKey = "", value = ""] = PROPERTY_RE.exec(line) ?? [];
|
|
55
|
+
const key = canonical.get(rawKey) ?? rawKey;
|
|
56
|
+
if (key !== "scheduled" && key !== "deadline") {
|
|
57
|
+
meta.push({ key: rawKey, value });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const entry = `${key.toUpperCase()}: ${value}`;
|
|
61
|
+
if (planning) {
|
|
62
|
+
planning.lines[0] += ` ${entry}`;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
planning = { lines: [entry] };
|
|
66
|
+
meta.push(planning);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return meta;
|
|
70
|
+
}
|
|
71
|
+
// a lazy continuation line sits anywhere left of the column
|
|
72
|
+
function dedent(line, column) {
|
|
73
|
+
const indent = /^ */.exec(line)?.[0].length ?? 0;
|
|
74
|
+
return line.slice(Math.min(indent, column));
|
|
75
|
+
}
|
|
76
|
+
function leadingProperties(lines) {
|
|
77
|
+
const count = lines.findIndex(line => !PROPERTY_RE.test(line));
|
|
78
|
+
return count === -1 ? lines.length : count;
|
|
79
|
+
}
|
|
80
|
+
// an item's own lines, up to its nested list: the bullet off its first,
|
|
81
|
+
// its checkbox read, continuation lines moved left past the bullet
|
|
82
|
+
function itemSource(item, end, lines) {
|
|
83
|
+
const [first = "", ...rest] = lines.slice(span(item)[0], end + 1);
|
|
84
|
+
const column = BULLET_RE.exec(first)?.[0].length ?? 0;
|
|
85
|
+
return [
|
|
86
|
+
taskTitle(item, first.slice(column)),
|
|
87
|
+
...rest.map(line => dedent(line, column))
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
// a title as a heading's level and text
|
|
91
|
+
function readHeading(title) {
|
|
92
|
+
const match = HEADING_RE.exec(title);
|
|
93
|
+
return match ? [match[1]?.length ?? 0, match[2] ?? ""] : [0, title];
|
|
94
|
+
}
|
|
95
|
+
// key:: lines below the title, or opening the content, are its meta
|
|
96
|
+
function itemBlock(source, level, reader) {
|
|
97
|
+
const metaFirst = PROPERTY_RE.test(source[0] ?? "");
|
|
98
|
+
const body = metaFirst ? source : source.slice(1);
|
|
99
|
+
const count = leadingProperties(body);
|
|
100
|
+
const meta = readMeta(body.slice(0, count), reader.context);
|
|
101
|
+
if (metaFirst) {
|
|
102
|
+
return { level, heading: 0, metaFirst, meta, content: body.slice(count) };
|
|
103
|
+
}
|
|
104
|
+
const [heading, title] = readHeading(source[0] ?? "");
|
|
105
|
+
return {
|
|
106
|
+
level,
|
|
107
|
+
heading,
|
|
108
|
+
metaFirst,
|
|
109
|
+
meta,
|
|
110
|
+
content: [title, ...body.slice(count)]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function readItem(item, level, ordered, reader) {
|
|
114
|
+
// a list on the item's first line is its content, not its children
|
|
115
|
+
const nested = item.children.filter((child) => child.type === "list" && span(child)[0] > span(item)[0]);
|
|
116
|
+
const end = nested[0] ? span(nested[0])[0] - 1 : span(item)[1];
|
|
117
|
+
const block = itemBlock(itemSource(item, end, reader.lines), level, reader);
|
|
118
|
+
if (ordered) {
|
|
119
|
+
block.meta.push({ key: "logseq.order-list-type", value: "number" });
|
|
120
|
+
}
|
|
121
|
+
reader.blocks.push(block);
|
|
122
|
+
// what follows the first nested list: more lists, and text, which a
|
|
123
|
+
// block's content cannot hold after its children, as child blocks
|
|
124
|
+
const column = BULLET_RE.exec(reader.lines[span(item)[0]] ?? "")?.[0].length;
|
|
125
|
+
const rest = nested[0]
|
|
126
|
+
? item.children.slice(item.children.indexOf(nested[0]))
|
|
127
|
+
: [];
|
|
128
|
+
for (const child of rest) {
|
|
129
|
+
if (child.type === "list") {
|
|
130
|
+
readList(child, level + 1, reader);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const [start, end] = span(child);
|
|
134
|
+
reader.blocks.push({
|
|
135
|
+
level: level + 1,
|
|
136
|
+
heading: 0,
|
|
137
|
+
metaFirst: false,
|
|
138
|
+
meta: [],
|
|
139
|
+
content: reader.lines
|
|
140
|
+
.slice(start, end + 1)
|
|
141
|
+
.map(line => dedent(line, column ?? 0))
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function readList(list, level, reader) {
|
|
146
|
+
for (const item of list.children) {
|
|
147
|
+
readItem(item, level, Boolean(list.ordered), reader);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function headingBlock(node, headings, lines) {
|
|
151
|
+
while ((headings.at(-1)?.depth ?? 0) >= node.depth) {
|
|
152
|
+
headings.pop();
|
|
153
|
+
}
|
|
154
|
+
const level = (headings.at(-1)?.level ?? 0) + 1;
|
|
155
|
+
headings.push({ depth: node.depth, level });
|
|
156
|
+
return {
|
|
157
|
+
level,
|
|
158
|
+
heading: node.depth,
|
|
159
|
+
metaFirst: false,
|
|
160
|
+
meta: [],
|
|
161
|
+
content: [headingTitle(lines[span(node)[0]] ?? "")]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// a link destination as Markdown writes it inline
|
|
165
|
+
function destination(url) {
|
|
166
|
+
return /[\s()<>]/.test(url) ? `<${url}>` : url;
|
|
167
|
+
}
|
|
168
|
+
function offsets(node) {
|
|
169
|
+
return [node.position?.start.offset ?? 0, node.position?.end.offset ?? 0];
|
|
170
|
+
}
|
|
171
|
+
// what spans a page cannot travel in a block: a reference link becomes
|
|
172
|
+
// inline, a definition goes, and so does a footnote's definition, kept
|
|
173
|
+
// for the block of its first reference (ADR 0006)
|
|
174
|
+
function resolveReferences(markdown) {
|
|
175
|
+
// links first: a footnote's definition may hold one
|
|
176
|
+
const inline = inlineReferences(markdown);
|
|
177
|
+
const footnotes = new Map();
|
|
178
|
+
const edits = [];
|
|
179
|
+
visit(parse(inline), "footnoteDefinition", (node) => {
|
|
180
|
+
const [start, end] = offsets(node);
|
|
181
|
+
footnotes.set(node.label ?? "", inline.slice(start, end));
|
|
182
|
+
edits.push([start, end, ""]);
|
|
183
|
+
});
|
|
184
|
+
return { markdown: applyEdits(inline, edits), footnotes };
|
|
185
|
+
}
|
|
186
|
+
function inlineReferences(markdown) {
|
|
187
|
+
const tree = parse(markdown);
|
|
188
|
+
const definitions = new Map();
|
|
189
|
+
visit(tree, "definition", (node) => {
|
|
190
|
+
definitions.set(node.identifier, node);
|
|
191
|
+
});
|
|
192
|
+
const edits = [];
|
|
193
|
+
visit(tree, (node) => {
|
|
194
|
+
const [start, end] = offsets(node);
|
|
195
|
+
if (node.type === "definition") {
|
|
196
|
+
edits.push([start, end, ""]);
|
|
197
|
+
}
|
|
198
|
+
else if (node.type === "linkReference" ||
|
|
199
|
+
node.type === "imageReference") {
|
|
200
|
+
const reference = node;
|
|
201
|
+
const definition = definitions.get(reference.identifier);
|
|
202
|
+
if (definition) {
|
|
203
|
+
edits.push([start, end, inlineLink(reference, definition, markdown)]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return applyEdits(markdown, edits);
|
|
208
|
+
}
|
|
209
|
+
// edits of nodes that do not nest, last first so offsets hold
|
|
210
|
+
function applyEdits(text, edits) {
|
|
211
|
+
let result = text;
|
|
212
|
+
for (const [start, end, replacement] of edits.sort((a, b) => b[0] - a[0])) {
|
|
213
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
function inlineLink(reference, definition, markdown) {
|
|
218
|
+
const title = definition.title ? ` "${definition.title}"` : "";
|
|
219
|
+
const target = `(${destination(definition.url)}${title})`;
|
|
220
|
+
if (reference.type === "imageReference") {
|
|
221
|
+
return `![${reference.alt ?? ""}]${target}`;
|
|
222
|
+
}
|
|
223
|
+
const first = reference.children[0];
|
|
224
|
+
const last = reference.children.at(-1);
|
|
225
|
+
const text = first && last ? markdown.slice(offsets(first)[0], offsets(last)[1]) : "";
|
|
226
|
+
return `[${text}]${target}`;
|
|
227
|
+
}
|
|
228
|
+
// a footnote's definition, after the first block that refers to it
|
|
229
|
+
function placeFootnotes(blocks, footnotes) {
|
|
230
|
+
for (const [label, definition] of footnotes) {
|
|
231
|
+
const block = blocks.find(candidate => candidate.content.some(line => line.includes(`[^${label}]`)));
|
|
232
|
+
block?.content.push("", ...definition.split("\n").map(line => line.trim()));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Reads a Vanilla Markdown page into Logseq's outline.
|
|
237
|
+
* @param markdown The Markdown page.
|
|
238
|
+
* @param context The conversion's org-ism key names.
|
|
239
|
+
* @returns The outline, its blocks' content in Markdown.
|
|
240
|
+
*/
|
|
241
|
+
export function readVanillaMarkdownOutline(markdown, context) {
|
|
242
|
+
const resolved = resolveReferences(markdown);
|
|
243
|
+
markdown = resolved.markdown;
|
|
244
|
+
const reader = {
|
|
245
|
+
lines: markdown.split(/\r?\n/),
|
|
246
|
+
blocks: [],
|
|
247
|
+
context
|
|
248
|
+
};
|
|
249
|
+
const headings = [];
|
|
250
|
+
// a heading whose body is still running: what follows it, up to a
|
|
251
|
+
// list or the next heading, is its content
|
|
252
|
+
let body;
|
|
253
|
+
const page = [];
|
|
254
|
+
for (const node of parse(markdown).children) {
|
|
255
|
+
const level = (headings.at(-1)?.level ?? 0) + 1;
|
|
256
|
+
if (node.type === "yaml") {
|
|
257
|
+
const [start, end] = span(node);
|
|
258
|
+
page.push(...reader.lines.slice(start, end + 1));
|
|
259
|
+
}
|
|
260
|
+
else if (node.type === "heading") {
|
|
261
|
+
body = headingBlock(node, headings, reader.lines);
|
|
262
|
+
reader.blocks.push(body);
|
|
263
|
+
}
|
|
264
|
+
else if (node.type === "list") {
|
|
265
|
+
body = undefined;
|
|
266
|
+
readList(node, level, reader);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
readText(node, level, body, reader);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
placeFootnotes(reader.blocks, resolved.footnotes);
|
|
273
|
+
return { page, blocks: reader.blocks };
|
|
274
|
+
}
|
|
275
|
+
// text below a heading is its body; after a list, a block of its own
|
|
276
|
+
function readText(node, level, body, reader) {
|
|
277
|
+
const [start, end] = span(node);
|
|
278
|
+
const source = reader.lines.slice(start, end + 1);
|
|
279
|
+
if (body) {
|
|
280
|
+
// the body's paragraphs keep the blank lines between them
|
|
281
|
+
body.content.push(...(body.content.length > 1 ? [""] : []), ...source);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
reader.blocks.push({
|
|
285
|
+
level,
|
|
286
|
+
heading: 0,
|
|
287
|
+
metaFirst: false,
|
|
288
|
+
meta: [],
|
|
289
|
+
content: source
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
// an ATX heading's text, without its markers
|
|
293
|
+
function headingTitle(line) {
|
|
294
|
+
return line.replace(/^ {0,3}#{1,6}(?:\s+|$)/, "").replace(/\s+#+\s*$/, "");
|
|
295
|
+
}
|
package/dist/presets/obsidian.js
CHANGED
|
@@ -7,8 +7,10 @@ import { toString } from "orgast-util-to-string";
|
|
|
7
7
|
export function obsidian() {
|
|
8
8
|
return {
|
|
9
9
|
name: "obsidian",
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
markdown: {
|
|
11
|
+
read: { org: rewriteAliasedWikilinks },
|
|
12
|
+
write: fuzzyLinksToWikilinks
|
|
13
|
+
}
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
// md→org: a wikilink travels as plain text; org already reads `[[Page]]`
|
|
@@ -2,7 +2,7 @@ import type { Preset } from "./types.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Instantiates a preset by name.
|
|
4
4
|
* @param presetName Preset name, or `undefined` for no preset.
|
|
5
|
-
* @returns The preset, or `undefined`
|
|
5
|
+
* @returns The preset, or `undefined` for none or `vanilla`.
|
|
6
6
|
* @throws If the name is not a known preset.
|
|
7
7
|
*/
|
|
8
8
|
export declare function createPreset(presetName: string | undefined): Preset | undefined;
|
package/dist/presets/registry.js
CHANGED
|
@@ -6,15 +6,17 @@ const PRESETS = {
|
|
|
6
6
|
logseq: () => logseq(),
|
|
7
7
|
obsidian: () => obsidian()
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
// the Vanilla side: no preset, but a name to set a side back to it
|
|
10
|
+
const VANILLA = "vanilla";
|
|
11
|
+
const PRESET_NAMES = [VANILLA, ...Object.keys(PRESETS)];
|
|
10
12
|
/**
|
|
11
13
|
* Instantiates a preset by name.
|
|
12
14
|
* @param presetName Preset name, or `undefined` for no preset.
|
|
13
|
-
* @returns The preset, or `undefined`
|
|
15
|
+
* @returns The preset, or `undefined` for none or `vanilla`.
|
|
14
16
|
* @throws If the name is not a known preset.
|
|
15
17
|
*/
|
|
16
18
|
export function createPreset(presetName) {
|
|
17
|
-
if (!presetName) {
|
|
19
|
+
if (!presetName || presetName === VANILLA) {
|
|
18
20
|
return undefined;
|
|
19
21
|
}
|
|
20
22
|
const factory = PRESETS[presetName];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Preset } from "./types.js";
|
|
2
|
+
/** The presets of a conversion, by side (ADR 0006). */
|
|
3
|
+
export interface PresetOptions {
|
|
4
|
+
/** Sets both sides, where a side preset does not. */
|
|
5
|
+
preset?: Preset;
|
|
6
|
+
/** The dialect the input is read in. */
|
|
7
|
+
inputPreset?: Preset;
|
|
8
|
+
/** The dialect the output is written in. */
|
|
9
|
+
outputPreset?: Preset;
|
|
10
|
+
}
|
|
11
|
+
/** The preset each side resolved to; `undefined` is Vanilla. */
|
|
12
|
+
export interface Sides {
|
|
13
|
+
input: Preset | undefined;
|
|
14
|
+
output: Preset | undefined;
|
|
15
|
+
}
|
|
16
|
+
/** A document format. */
|
|
17
|
+
export type Format = "markdown" | "org";
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the preset of each side of a conversion. A side preset must
|
|
20
|
+
* have a dialect for its side's format; `preset` leaves a side it has
|
|
21
|
+
* none for Vanilla (Obsidian writes no org).
|
|
22
|
+
* @param options The conversion's preset options.
|
|
23
|
+
* @param from The input's format.
|
|
24
|
+
* @param to The output's format.
|
|
25
|
+
* @returns The preset per side.
|
|
26
|
+
* @throws If a side preset has no dialect for its side's format, or
|
|
27
|
+
* `preset` names another preset than a side preset.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveSides(options: PresetOptions, from: Format, to: Format): Sides;
|
|
30
|
+
/**
|
|
31
|
+
* Resolves one side's preset: the side preset, else `preset` where it
|
|
32
|
+
* has a dialect for the side's format.
|
|
33
|
+
* @param sidePreset The side's own preset, if any.
|
|
34
|
+
* @param preset The preset for both sides, if any.
|
|
35
|
+
* @param format The side's format.
|
|
36
|
+
* @param side Which side it is.
|
|
37
|
+
* @returns The side's preset; `undefined` is Vanilla.
|
|
38
|
+
* @throws If the side preset has no dialect for the format.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveSide(sidePreset: Preset | undefined, preset: Preset | undefined, format: Format, side: "input" | "output"): Preset | undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the preset of each side of a conversion. A side preset must
|
|
3
|
+
* have a dialect for its side's format; `preset` leaves a side it has
|
|
4
|
+
* none for Vanilla (Obsidian writes no org).
|
|
5
|
+
* @param options The conversion's preset options.
|
|
6
|
+
* @param from The input's format.
|
|
7
|
+
* @param to The output's format.
|
|
8
|
+
* @returns The preset per side.
|
|
9
|
+
* @throws If a side preset has no dialect for its side's format, or
|
|
10
|
+
* `preset` names another preset than a side preset.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveSides(options, from, to) {
|
|
13
|
+
for (const key of ["inputPreset", "outputPreset"]) {
|
|
14
|
+
const sidePreset = options[key];
|
|
15
|
+
if (options.preset &&
|
|
16
|
+
sidePreset &&
|
|
17
|
+
sidePreset.name !== options.preset.name) {
|
|
18
|
+
throw new Error(`preset '${options.preset.name}' conflicts with ${key} '${sidePreset.name}'`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
input: resolveSide(options.inputPreset, options.preset, from, "input"),
|
|
23
|
+
output: resolveSide(options.outputPreset, options.preset, to, "output")
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolves one side's preset: the side preset, else `preset` where it
|
|
28
|
+
* has a dialect for the side's format.
|
|
29
|
+
* @param sidePreset The side's own preset, if any.
|
|
30
|
+
* @param preset The preset for both sides, if any.
|
|
31
|
+
* @param format The side's format.
|
|
32
|
+
* @param side Which side it is.
|
|
33
|
+
* @returns The side's preset; `undefined` is Vanilla.
|
|
34
|
+
* @throws If the side preset has no dialect for the format.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveSide(sidePreset, preset, format, side) {
|
|
37
|
+
if (sidePreset && !sidePreset[format]) {
|
|
38
|
+
const purpose = side === "input" ? "read the input" : "write the output";
|
|
39
|
+
throw new Error(`Preset '${sidePreset.name}' has no ${format} dialect to ${purpose} in`);
|
|
40
|
+
}
|
|
41
|
+
return sidePreset ?? (preset?.[format] ? preset : undefined);
|
|
42
|
+
}
|
package/dist/presets/types.d.ts
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
import type { Root as MdastRoot } from "mdast";
|
|
2
2
|
import type { OrgData } from "uniorg";
|
|
3
3
|
/**
|
|
4
|
-
* A dialect preset: transforms applied on top of the dialect-agnostic
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* outline of blocks, each its own fragment); `convert` runs the core on
|
|
12
|
-
* a fragment, with the given preset's hooks.
|
|
4
|
+
* A dialect preset: transforms applied on top of the dialect-agnostic
|
|
5
|
+
* core, one dialect per format it knows (ADR 0006), each read in one
|
|
6
|
+
* direction and written in the other. `convertOrg` and
|
|
7
|
+
* `convertMarkdown` take a whole conversion over, for a dialect whose
|
|
8
|
+
* documents are no single org or Markdown document (an outline of
|
|
9
|
+
* blocks, each its own fragment); `convert` runs the core on a fragment,
|
|
10
|
+
* with the given preset's hooks.
|
|
13
11
|
*/
|
|
14
12
|
export interface Preset {
|
|
15
13
|
name: string;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
markdown?: MarkdownDialect;
|
|
15
|
+
org?: OrgDialect;
|
|
16
|
+
convertOrg?: (org: string, convert: FragmentConverter, context: ConversionContext) => string;
|
|
17
|
+
convertMarkdown?: (markdown: string, convert: FragmentConverter, context: ConversionContext) => string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* What a preset that takes a whole conversion over learns of it: the
|
|
21
|
+
* side or sides it is on (the other one is another preset's or
|
|
22
|
+
* Vanilla), and the conversion's warning callback and org-ism key names.
|
|
23
|
+
*/
|
|
24
|
+
export interface ConversionContext {
|
|
25
|
+
side: "both" | "input" | "output";
|
|
26
|
+
onWarning?: (message: string) => void;
|
|
27
|
+
orgismKeys?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts a fragment with the core, the given preset's AST hooks on
|
|
31
|
+
* the taking-over preset's side or sides, and the other side's preset;
|
|
32
|
+
* a Vanilla side converts with `carried`, the preset whose syntax it
|
|
33
|
+
* carries.
|
|
34
|
+
*/
|
|
35
|
+
export type FragmentConverter = (fragment: string, preset?: Preset, carried?: Preset) => string;
|
|
36
|
+
/**
|
|
37
|
+
* A preset's Markdown dialect. `read` runs in md→org: `mdast` on the
|
|
38
|
+
* parse, before the generic transform, with the source at hand; `org`
|
|
39
|
+
* after it. `write` runs in org→md before the generic transform.
|
|
40
|
+
*/
|
|
41
|
+
export interface MarkdownDialect {
|
|
42
|
+
read?: {
|
|
43
|
+
mdast?: (mdast: MdastRoot, markdown: string) => void;
|
|
44
|
+
org?: (uniorgAst: OrgData) => OrgData;
|
|
45
|
+
};
|
|
46
|
+
write?: (uniorgAst: OrgData) => OrgData;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A preset's org dialect. `read` runs in org→md before the generic
|
|
50
|
+
* transform, ahead of the Markdown dialect's `write`; `write` runs in
|
|
51
|
+
* md→org after the Markdown dialect was read.
|
|
52
|
+
*/
|
|
53
|
+
export interface OrgDialect {
|
|
54
|
+
read?: (uniorgAst: OrgData) => OrgData;
|
|
55
|
+
write?: (uniorgAst: OrgData) => OrgData;
|
|
21
56
|
}
|
|
22
|
-
/** Converts a fragment with the core and the given preset's AST hooks. */
|
|
23
|
-
export type FragmentConverter = (fragment: string, preset?: Preset) => string;
|