@remigius42/morg 0.7.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 +35 -18
- 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/bracedScripts.d.ts +7 -0
- package/dist/core/bracedScripts.js +10 -0
- package/dist/core/frontmatterBlock.d.ts +7 -0
- package/dist/core/frontmatterBlock.js +7 -2
- package/dist/core/lineSyntax.d.ts +13 -0
- package/dist/core/lineSyntax.js +16 -4
- package/dist/core/mdastToUniorg/blocks.js +4 -1
- package/dist/core/passthroughSource.d.ts +21 -0
- package/dist/core/passthroughSource.js +94 -0
- package/dist/core/uniorgToMdast/elements.js +5 -0
- package/dist/core/uniorgToMdast/shared.js +6 -1
- package/dist/index.d.ts +1 -2
- package/dist/markdownToOrg.js +19 -6
- package/dist/normalize.js +16 -2
- package/dist/options.d.ts +3 -7
- package/dist/orgToMarkdown.js +14 -4
- package/dist/presets/hooks.d.ts +49 -0
- package/dist/presets/hooks.js +72 -0
- package/dist/presets/logseq.d.ts +3 -22
- package/dist/presets/logseq.js +306 -195
- package/dist/presets/logseqOutline.d.ts +47 -0
- package/dist/presets/logseqOutline.js +428 -0
- 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 -8
- package/package.json +1 -1
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { consumesBracedScripts } from "../core/bracedScripts.js";
|
|
2
|
+
import { readsAsLineSyntax } from "../core/lineSyntax.js";
|
|
3
|
+
import { ZERO_WIDTH_SPACE } from "../core/markupBoundary.js";
|
|
4
|
+
import { isDrawerStart, isOrgBlockStart, orgElementEnd } from "../core/passthroughSource.js";
|
|
5
|
+
import { readVanillaMarkdownOutline } from "./logseqVanillaMarkdown.js";
|
|
6
|
+
// Logseq stores a page as an outline of blocks, each block a content
|
|
7
|
+
// string it parses on its own: org writes a block as its level's stars,
|
|
8
|
+
// a space and the content (an empty block as the bare stars), Markdown
|
|
9
|
+
// as a `- ` bullet indented one tab per level, continuation lines two
|
|
10
|
+
// spaces further in. Both are converted block by block, so a block's
|
|
11
|
+
// content (a code block, a table, lines that run on) is one fragment.
|
|
12
|
+
const ORG_BLOCK_RE = /^(\*+)(?: (.*))?$/;
|
|
13
|
+
const MD_BLOCK_RE = /^(\t*)-(?: (.*))?$/;
|
|
14
|
+
const PLANNING_RE = /^(?:SCHEDULED|DEADLINE): /;
|
|
15
|
+
const ORG_PROPERTY_RE = /^:([^\s:]+):(?: (.*))?$/;
|
|
16
|
+
const MD_PROPERTY_RE = /^([\w.-]+)::(?: (.*))?$/;
|
|
17
|
+
// a repeated task's log line, which Logseq bullets per format
|
|
18
|
+
const STATE_LINE_RE = /^[-*] (?=State ")/;
|
|
19
|
+
const MD_HEADING_RE = /^(#{1,6})(?: (.*))?$/;
|
|
20
|
+
const FENCE_RE = /^\s*(?:```|~~~)/;
|
|
21
|
+
// md→org adds it for the text's bare `_` and `^`, which a block's
|
|
22
|
+
// content holds as Logseq writes it
|
|
23
|
+
const BRACED_SCRIPTS_LINE = "#+OPTIONS: ^:{}";
|
|
24
|
+
function logbook(lines, bullet) {
|
|
25
|
+
return lines[0] === ":LOGBOOK:"
|
|
26
|
+
? lines.map(line => line.replace(STATE_LINE_RE, `${bullet} `))
|
|
27
|
+
: lines;
|
|
28
|
+
}
|
|
29
|
+
function splitBlocks(lines, blockLevel) {
|
|
30
|
+
const page = [];
|
|
31
|
+
const blocks = [];
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const start = blockLevel(line);
|
|
34
|
+
if (start) {
|
|
35
|
+
blocks.push({ level: start[0], lines: [start[1]] });
|
|
36
|
+
}
|
|
37
|
+
else if (blocks.length) {
|
|
38
|
+
blocks[blocks.length - 1]?.lines.push(line);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
page.push(line);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { page, blocks };
|
|
45
|
+
}
|
|
46
|
+
// a block's leading planning lines and drawers, which Logseq writes
|
|
47
|
+
// below the content's first line in either format
|
|
48
|
+
function takeMeta(lines, isMeta) {
|
|
49
|
+
const meta = [];
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < lines.length) {
|
|
52
|
+
const line = lines[i] ?? "";
|
|
53
|
+
if (isDrawerStart(line)) {
|
|
54
|
+
const end = orgElementEnd(lines, i);
|
|
55
|
+
if (end === -1) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
meta.push(lines.slice(i, end + 1));
|
|
59
|
+
i = end + 1;
|
|
60
|
+
}
|
|
61
|
+
else if (PLANNING_RE.test(line) || isMeta(line)) {
|
|
62
|
+
meta.push([line]);
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { meta, body: lines.slice(i) };
|
|
70
|
+
}
|
|
71
|
+
// the one org block whose content Logseq md writes as Markdown markup
|
|
72
|
+
// (its <quote command), and org→md writes as a md quote
|
|
73
|
+
const QUOTE_BLOCK_RE = /^#\+begin_quote\b/i;
|
|
74
|
+
function convertOrgBlock(block, convert, preset) {
|
|
75
|
+
if (!QUOTE_BLOCK_RE.test(block[0] ?? "")) {
|
|
76
|
+
return block;
|
|
77
|
+
}
|
|
78
|
+
const content = convertContent(block.slice(1, -1), convert, preset);
|
|
79
|
+
return [block[0] ?? "", ...content, block.at(-1) ?? ""];
|
|
80
|
+
}
|
|
81
|
+
// md→org: a block's org blocks stay as written, as org→md writes them,
|
|
82
|
+
// but for a quote's content; the text around them converts
|
|
83
|
+
function convertMarkdownContent(lines, convert, preset) {
|
|
84
|
+
const result = [];
|
|
85
|
+
let text = [];
|
|
86
|
+
let fenced = false;
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
fenced = FENCE_RE.test(lines[i] ?? "") ? !fenced : fenced;
|
|
89
|
+
const end = fenced || !isOrgBlockStart(lines[i] ?? "") ? -1 : orgElementEnd(lines, i);
|
|
90
|
+
if (end === -1) {
|
|
91
|
+
text.push(lines[i] ?? "");
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
result.push(...convertContent(text, convert, preset), ...convertOrgBlock(lines.slice(i, end + 1), convert, preset));
|
|
95
|
+
text = [];
|
|
96
|
+
i = end;
|
|
97
|
+
}
|
|
98
|
+
return [...result, ...convertContent(text, convert, preset)];
|
|
99
|
+
}
|
|
100
|
+
function convertContent(lines, convert, preset) {
|
|
101
|
+
const content = lines.join("\n");
|
|
102
|
+
return content.trim()
|
|
103
|
+
? convert(content, preset).replace(/\n+$/, "").split("\n")
|
|
104
|
+
: [];
|
|
105
|
+
}
|
|
106
|
+
function convertPage(lines, convert, preset) {
|
|
107
|
+
// Logseq ends the page properties with a blank line
|
|
108
|
+
return lines.some(line => line.trim())
|
|
109
|
+
? [`${convert(lines.join("\n"), preset).replace(/\n+$/, "")}\n`]
|
|
110
|
+
: [];
|
|
111
|
+
}
|
|
112
|
+
// org→md reads a block's bare `_` and `^` as text, as md→org writes
|
|
113
|
+
// them without the setting that would say so
|
|
114
|
+
function withBracedScripts(lines) {
|
|
115
|
+
const braced = [BRACED_SCRIPTS_LINE, ...lines];
|
|
116
|
+
return consumesBracedScripts(braced.join("\n")) ? braced : lines;
|
|
117
|
+
}
|
|
118
|
+
// Logseq org: a block's properties drawer, as properties; a heading
|
|
119
|
+
// level is one of them, but for content that starts with the drawer
|
|
120
|
+
function readOrgBlock({ level, lines }) {
|
|
121
|
+
const [first = "", ...rest] = lines;
|
|
122
|
+
const metaFirst = isDrawerStart(first);
|
|
123
|
+
const { meta: groups, body } = takeMeta(metaFirst ? lines : rest, () => false);
|
|
124
|
+
let heading = 0;
|
|
125
|
+
const meta = groups.flatMap((group) => {
|
|
126
|
+
if (group[0] !== ":PROPERTIES:") {
|
|
127
|
+
return [{ lines: group }];
|
|
128
|
+
}
|
|
129
|
+
return group.slice(1, -1).flatMap(line => {
|
|
130
|
+
const [, key = "", value = ""] = ORG_PROPERTY_RE.exec(line) ?? [];
|
|
131
|
+
if (!metaFirst && key === "heading" && /^[1-6]$/.test(value)) {
|
|
132
|
+
heading = Number(value);
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
return [{ key, value }];
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
level,
|
|
140
|
+
heading,
|
|
141
|
+
metaFirst,
|
|
142
|
+
meta,
|
|
143
|
+
content: metaFirst ? body : [first, ...body]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function readOrgOutline(org) {
|
|
147
|
+
const { page, blocks } = splitBlocks(org.replace(/\r?\n$/, "").split(/\r?\n/), line => {
|
|
148
|
+
const match = ORG_BLOCK_RE.exec(line);
|
|
149
|
+
return match ? [match[1]?.length ?? 0, match[2] ?? ""] : null;
|
|
150
|
+
});
|
|
151
|
+
return { page, blocks: blocks.map(readOrgBlock) };
|
|
152
|
+
}
|
|
153
|
+
// a continuation line sits two spaces inside its bullet
|
|
154
|
+
function dedent(line, level) {
|
|
155
|
+
const indent = `${"\t".repeat(level - 1)} `;
|
|
156
|
+
return line.startsWith(indent)
|
|
157
|
+
? line.slice(indent.length)
|
|
158
|
+
: line.trim()
|
|
159
|
+
? line
|
|
160
|
+
: "";
|
|
161
|
+
}
|
|
162
|
+
// `## title`: the heading level and the title
|
|
163
|
+
function mdHeading(line) {
|
|
164
|
+
const match = MD_HEADING_RE.exec(line);
|
|
165
|
+
return match ? [match[1]?.length ?? 0, match[2] ?? ""] : [0, line];
|
|
166
|
+
}
|
|
167
|
+
function readMarkdownBlock({ level, lines: source }) {
|
|
168
|
+
const [first = "", ...rest] = source;
|
|
169
|
+
const lines = [first, ...rest.map(line => dedent(line, level))];
|
|
170
|
+
const metaFirst = MD_PROPERTY_RE.test(first);
|
|
171
|
+
const [heading, titleLine] = metaFirst ? [0, ""] : mdHeading(first);
|
|
172
|
+
const { meta: groups, body } = takeMeta(metaFirst ? lines : lines.slice(1), line => MD_PROPERTY_RE.test(line));
|
|
173
|
+
const meta = groups.map((group) => {
|
|
174
|
+
const property = MD_PROPERTY_RE.exec(group[0] ?? "");
|
|
175
|
+
return property
|
|
176
|
+
? { key: property[1] ?? "", value: property[2] ?? "" }
|
|
177
|
+
: { lines: group };
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
level,
|
|
181
|
+
heading,
|
|
182
|
+
metaFirst,
|
|
183
|
+
meta,
|
|
184
|
+
content: metaFirst ? body : [titleLine, ...body]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function readMarkdownOutline(markdown) {
|
|
188
|
+
const lines = markdown.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
189
|
+
// a leading frontmatter is page content, its `- ` lines yaml items
|
|
190
|
+
const frontmatter = lines.slice(0, lines[0] === "---" ? lines.indexOf("---", 1) + 1 : 0);
|
|
191
|
+
let fenced = false;
|
|
192
|
+
const { page, blocks } = splitBlocks(lines.slice(frontmatter.length), line => {
|
|
193
|
+
const match = MD_BLOCK_RE.exec(line);
|
|
194
|
+
// a bullet starts a block, which may open a fence of its own
|
|
195
|
+
fenced = (match ? false : fenced) !== FENCE_RE.test(match?.[2] ?? line);
|
|
196
|
+
if (match) {
|
|
197
|
+
return [(match[1]?.length ?? 0) + 1, match[2] ?? ""];
|
|
198
|
+
}
|
|
199
|
+
// a heading outside the bullets is a top-level block, as Logseq
|
|
200
|
+
// writes a page's first block if it is a heading
|
|
201
|
+
return !fenced && MD_HEADING_RE.test(line) ? [1, line] : null;
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
page: [...frontmatter, ...page],
|
|
205
|
+
blocks: blocks.map(readMarkdownBlock)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// a block's content's first line, its meta lines, then the rest of the
|
|
209
|
+
// content; content that starts with the meta lines keeps them first
|
|
210
|
+
function arrange(metaFirst, meta, content) {
|
|
211
|
+
return metaFirst
|
|
212
|
+
? [...meta, ...content]
|
|
213
|
+
: [content[0] ?? "", ...meta, ...content.slice(1)];
|
|
214
|
+
}
|
|
215
|
+
function propertyLine({ key, value }) {
|
|
216
|
+
return `${key}::${value ? ` ${value}` : ""}`;
|
|
217
|
+
}
|
|
218
|
+
function writeMarkdownBlock(block) {
|
|
219
|
+
const meta = block.meta.flatMap(item => "lines" in item ? logbook(item.lines, "*") : [propertyLine(item)]);
|
|
220
|
+
const [title = "", ...more] = arrange(block.metaFirst, meta, block.content);
|
|
221
|
+
const head = ["#".repeat(block.heading), title].filter(Boolean).join(" ");
|
|
222
|
+
const indent = "\t".repeat(block.level - 1);
|
|
223
|
+
return [
|
|
224
|
+
`${indent}-${head ? ` ${head}` : ""}`,
|
|
225
|
+
...more.map(line => `${indent} ${line}`)
|
|
226
|
+
].join("\n");
|
|
227
|
+
}
|
|
228
|
+
// the meta in org: properties become the property drawer, where the
|
|
229
|
+
// first of them was, or below the planning lines
|
|
230
|
+
function orgMetaLines(meta, heading) {
|
|
231
|
+
const properties = heading ? [`:heading: ${heading}`] : [];
|
|
232
|
+
const lines = [];
|
|
233
|
+
for (const item of meta) {
|
|
234
|
+
if ("lines" in item) {
|
|
235
|
+
lines.push(...logbook(item.lines, "-"));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (!lines.includes(null)) {
|
|
239
|
+
lines.push(null);
|
|
240
|
+
}
|
|
241
|
+
properties.push(`:${item.key}:${item.value ? ` ${item.value}` : ""}`);
|
|
242
|
+
}
|
|
243
|
+
if (properties.length && !lines.includes(null)) {
|
|
244
|
+
lines.splice(lines.filter(line => PLANNING_RE.test(line ?? "")).length, 0, null);
|
|
245
|
+
}
|
|
246
|
+
const drawer = [":PROPERTIES:", ...properties, ":END:"];
|
|
247
|
+
return lines.flatMap(line => (line === null ? drawer : [line]));
|
|
248
|
+
}
|
|
249
|
+
function writeOrgBlock(block) {
|
|
250
|
+
const [title = "", ...more] = arrange(block.metaFirst, orgMetaLines(block.meta, block.heading), block.content);
|
|
251
|
+
return [
|
|
252
|
+
`${"*".repeat(block.level)}${title ? ` ${title}` : ""}`,
|
|
253
|
+
...more
|
|
254
|
+
].join("\n");
|
|
255
|
+
}
|
|
256
|
+
function writeOutline({ page, blocks }, writeBlock) {
|
|
257
|
+
return [...page, ...blocks.map(writeBlock)].join("\n").concat("\n");
|
|
258
|
+
}
|
|
259
|
+
// a block's title is a headline's, which Logseq reads as inline text
|
|
260
|
+
// only; converted with the lines below it, it must not read as a list,
|
|
261
|
+
// a headline or a fixed-width line, so an escape the core drops keeps
|
|
262
|
+
// it text (ADR 0006)
|
|
263
|
+
function inlineTitle(block) {
|
|
264
|
+
const [title = "", ...rest] = block.content;
|
|
265
|
+
return !block.metaFirst && readsAsLineSyntax(title)
|
|
266
|
+
? [`${ZERO_WIDTH_SPACE}${title}`, ...rest]
|
|
267
|
+
: block.content;
|
|
268
|
+
}
|
|
269
|
+
// a title written from Markdown text keeps it text with an escape that
|
|
270
|
+
// a headline needs none of
|
|
271
|
+
function dropTitleEscape(block) {
|
|
272
|
+
const [title = "", ...rest] = block.content;
|
|
273
|
+
return block.metaFirst || !title.startsWith(ZERO_WIDTH_SPACE)
|
|
274
|
+
? block
|
|
275
|
+
: { ...block, content: [title.slice(ZERO_WIDTH_SPACE.length), ...rest] };
|
|
276
|
+
}
|
|
277
|
+
// a task marker Logseq shows as a checkbox (0.10, block-checkbox), and
|
|
278
|
+
// the text after it; TODO and DONE are the checkbox's own
|
|
279
|
+
const CHECKBOX_MARKER_RE = /^(TODO|DONE|NOW|LATER|DOING|IN-PROGRESS|WAIT|WAITING) (.*)$/;
|
|
280
|
+
// Vanilla Markdown: a task as a task item, marked as Logseq shows it,
|
|
281
|
+
// so the marker comes back; CANCELED, shown without one, stays text
|
|
282
|
+
function vanillaTitle(title) {
|
|
283
|
+
const [, marker, text] = CHECKBOX_MARKER_RE.exec(title) ?? [];
|
|
284
|
+
if (!marker) {
|
|
285
|
+
return title;
|
|
286
|
+
}
|
|
287
|
+
if (marker === "DONE") {
|
|
288
|
+
return `[x] ${text}`;
|
|
289
|
+
}
|
|
290
|
+
return marker === "TODO" ? `[ ] ${text}` : `[ ] ${marker} ${text}`;
|
|
291
|
+
}
|
|
292
|
+
const PLANNING_ENTRY_RE = /(SCHEDULED|DEADLINE): ([<[][^>\]]*[>\]])/g;
|
|
293
|
+
// Vanilla Markdown: planning and properties as key:: lines, under the
|
|
294
|
+
// org-ism names; a drawer and Logseq's view state have no form there
|
|
295
|
+
function vanillaMeta(meta, context) {
|
|
296
|
+
return meta.flatMap(item => {
|
|
297
|
+
if ("key" in item) {
|
|
298
|
+
if (item.key === "collapsed") {
|
|
299
|
+
context.onWarning?.("collapsed is Logseq's view state; dropped");
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
return [propertyLine(item)];
|
|
303
|
+
}
|
|
304
|
+
const [first = ""] = item.lines;
|
|
305
|
+
if (isDrawerStart(first)) {
|
|
306
|
+
context.onWarning?.(`a ${first.slice(1, -1)} drawer has no Vanilla Markdown form; dropped`);
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
return [...first.matchAll(PLANNING_ENTRY_RE)].map(([, key = "", value]) => {
|
|
310
|
+
const canonical = key.toLowerCase();
|
|
311
|
+
return `${context.orgismKeys?.[canonical] ?? canonical}:: ${value}`;
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
// a block Logseq shows numbered, as a run of its siblings
|
|
316
|
+
function isNumbered(item) {
|
|
317
|
+
return ("key" in item &&
|
|
318
|
+
item.key === "logseq.order-list-type" &&
|
|
319
|
+
item.value === "number");
|
|
320
|
+
}
|
|
321
|
+
function vanillaBlock(block, indent, bullet, context) {
|
|
322
|
+
const [first = "", ...more] = arrange(block.metaFirst, vanillaMeta(block.meta.filter(item => !isNumbered(item)), context), block.content);
|
|
323
|
+
const hashes = "#".repeat(block.heading);
|
|
324
|
+
// a heading no list holds is a heading of its own
|
|
325
|
+
if (block.heading && !indent) {
|
|
326
|
+
const body = more.length ? `\n\n${more.join("\n")}` : "";
|
|
327
|
+
return { heading: true, text: `${hashes} ${first}${body}` };
|
|
328
|
+
}
|
|
329
|
+
const title = [hashes, vanillaTitle(first)].filter(Boolean).join(" ");
|
|
330
|
+
return {
|
|
331
|
+
heading: false,
|
|
332
|
+
text: [
|
|
333
|
+
`${indent}${bullet}${title ? ` ${title}` : ""}`,
|
|
334
|
+
...more.map(line => line ? `${indent}${" ".repeat(bullet.length + 1)}${line}` : "")
|
|
335
|
+
].join("\n")
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// Vanilla Markdown: a block is a list item, its children nested two
|
|
339
|
+
// spaces further in; under a heading block they start a new list
|
|
340
|
+
// (ADR 0006)
|
|
341
|
+
function writeVanillaMarkdownOutline({ page, blocks }, context) {
|
|
342
|
+
// a parent's level, its children's indent and their numbered run
|
|
343
|
+
const parents = [{ level: 0, indent: "", run: 0 }];
|
|
344
|
+
const written = blocks.map(block => {
|
|
345
|
+
while (parents.length > 1 && (parents.at(-1)?.level ?? 0) >= block.level) {
|
|
346
|
+
parents.pop();
|
|
347
|
+
}
|
|
348
|
+
const parent = parents.at(-1) ?? { level: 0, indent: "", run: 0 };
|
|
349
|
+
parent.run = block.meta.some(isNumbered) ? parent.run + 1 : 0;
|
|
350
|
+
const bullet = parent.run ? `${parent.run}.` : "-";
|
|
351
|
+
const result = vanillaBlock(block, parent.indent, bullet, context);
|
|
352
|
+
parents.push({
|
|
353
|
+
level: block.level,
|
|
354
|
+
indent: result.heading
|
|
355
|
+
? ""
|
|
356
|
+
: `${parent.indent}${" ".repeat(bullet.length + 1)}`,
|
|
357
|
+
run: 0
|
|
358
|
+
});
|
|
359
|
+
return result;
|
|
360
|
+
});
|
|
361
|
+
const body = written
|
|
362
|
+
.map((block, i) => i && (block.heading || written[i - 1]?.heading)
|
|
363
|
+
? `\n${block.text}`
|
|
364
|
+
: block.text)
|
|
365
|
+
.join("\n");
|
|
366
|
+
return [
|
|
367
|
+
...page.map(text => text.replace(/\n+$/, "")),
|
|
368
|
+
...(body ? [body] : [])
|
|
369
|
+
]
|
|
370
|
+
.join("\n\n")
|
|
371
|
+
.concat("\n");
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Converts a Logseq org page to Markdown, block by block: Logseq's,
|
|
375
|
+
* or Vanilla where the preset is on the input side only.
|
|
376
|
+
* @param org The org page.
|
|
377
|
+
* @param convert The core's fragment converter.
|
|
378
|
+
* @param presets The presets for the page properties and for a block.
|
|
379
|
+
* @param context The side the preset is on.
|
|
380
|
+
* @returns The Markdown page.
|
|
381
|
+
*/
|
|
382
|
+
export function orgOutlineToMarkdown(org, convert, presets, context) {
|
|
383
|
+
const { page, blocks } = readOrgOutline(org);
|
|
384
|
+
const vanilla = context.side === "input";
|
|
385
|
+
const pageLines = vanilla ? (presets.vanillaPage?.(page) ?? page) : page;
|
|
386
|
+
const convertCarried = (fragment, preset) => convert(fragment, preset, vanilla ? presets.vanillaInline : undefined);
|
|
387
|
+
const outline = {
|
|
388
|
+
page: convertPage(pageLines, convertCarried, presets.page),
|
|
389
|
+
blocks: blocks.map(block => ({
|
|
390
|
+
...block,
|
|
391
|
+
content: convertContent(withBracedScripts(vanilla ? inlineTitle(block) : block.content), convertCarried, presets.block)
|
|
392
|
+
}))
|
|
393
|
+
};
|
|
394
|
+
return vanilla
|
|
395
|
+
? writeVanillaMarkdownOutline(outline, context)
|
|
396
|
+
: writeOutline(outline, writeMarkdownBlock);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Converts a Logseq Markdown page to Logseq org, block by block.
|
|
400
|
+
* @param markdown The Markdown page.
|
|
401
|
+
* @param convert The core's fragment converter.
|
|
402
|
+
* @param presets The presets for the page properties and for a block.
|
|
403
|
+
* @param context The side the preset is on.
|
|
404
|
+
* @returns The org page.
|
|
405
|
+
*/
|
|
406
|
+
export function markdownOutlineToOrg(markdown, convert, presets, context) {
|
|
407
|
+
const vanilla = context.side === "output";
|
|
408
|
+
const { page, blocks } = vanilla
|
|
409
|
+
? readVanillaMarkdownOutline(markdown, context)
|
|
410
|
+
: readMarkdownOutline(markdown);
|
|
411
|
+
// Vanilla Markdown carries Logseq's syntax as Logseq Markdown writes
|
|
412
|
+
// it; Vanilla org is written as Logseq org, which it is (ADR 0006)
|
|
413
|
+
const convertCarried = (fragment, preset) => convert(fragment, preset, context.side === "both" || !preset
|
|
414
|
+
? undefined
|
|
415
|
+
: vanilla
|
|
416
|
+
? (presets.vanillaReader?.(preset) ?? preset)
|
|
417
|
+
: preset);
|
|
418
|
+
return writeOutline({
|
|
419
|
+
page: convertPage(page, convertCarried, presets.page),
|
|
420
|
+
blocks: blocks.map(block => {
|
|
421
|
+
const converted = {
|
|
422
|
+
...block,
|
|
423
|
+
content: convertMarkdownContent(block.content, convertCarried, presets.block).filter(line => line !== BRACED_SCRIPTS_LINE)
|
|
424
|
+
};
|
|
425
|
+
return vanilla ? dropTitleEscape(converted) : converted;
|
|
426
|
+
})
|
|
427
|
+
}, writeOrgBlock);
|
|
428
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Outline } from "./logseqOutline.js";
|
|
2
|
+
import type { ConversionContext } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reads a Vanilla Markdown page into Logseq's outline.
|
|
5
|
+
* @param markdown The Markdown page.
|
|
6
|
+
* @param context The conversion's org-ism key names.
|
|
7
|
+
* @returns The outline, its blocks' content in Markdown.
|
|
8
|
+
*/
|
|
9
|
+
export declare function readVanillaMarkdownOutline(markdown: string, context: ConversionContext): Outline;
|