@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
package/dist/presets/logseq.js
CHANGED
|
@@ -1,45 +1,173 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
2
|
import { toString } from "orgast-util-to-string";
|
|
3
|
-
import { isScalar, isSeq } from "yaml";
|
|
4
|
-
import { fitsKeywordLine, isFrontmatterNode, isModeLine, isModeLineComment, takeFrontmatterEntries } from "../core/frontmatterBlock.js";
|
|
3
|
+
import { isScalar, isSeq, stringify as stringifyYaml } from "yaml";
|
|
4
|
+
import { fitsKeywordLine, FRONTMATTER_BLOCK_BEGIN, frontmatterBlock, isFrontmatterNode, isModeLine, isModeLineComment, takeFrontmatterEntries } from "../core/frontmatterBlock.js";
|
|
5
5
|
import { keyValueEntries } from "../core/keyValueLines.js";
|
|
6
|
+
import { tryParse } from "../core/render.js";
|
|
7
|
+
import { orgNodeToText } from "../core/uniorgToMdast/shared.js";
|
|
8
|
+
import { markdownOutlineToOrg, orgOutlineToMarkdown } from "./logseqOutline.js";
|
|
6
9
|
/**
|
|
7
|
-
* Logseq dialect preset:
|
|
10
|
+
* Logseq dialect preset: the outline of blocks, page properties, page
|
|
11
|
+
* and block references, highlights and hiccup.
|
|
8
12
|
*/
|
|
9
|
-
export function logseq(
|
|
10
|
-
const
|
|
13
|
+
export function logseq() {
|
|
14
|
+
const page = pagePreset();
|
|
15
|
+
const block = blockPreset();
|
|
16
|
+
const presets = {
|
|
17
|
+
page,
|
|
18
|
+
block,
|
|
19
|
+
vanillaReader,
|
|
20
|
+
vanillaPage: pagePropertiesToFrontmatter,
|
|
21
|
+
vanillaInline
|
|
22
|
+
};
|
|
11
23
|
return {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
extractFromUniorg: extractLogseqSpecificsFromUniorgAst
|
|
24
|
+
...page,
|
|
25
|
+
convertOrg: (org, convert, context) => orgOutlineToMarkdown(org, convert, presets, context),
|
|
26
|
+
convertMarkdown: (markdown, convert, context) => markdownOutlineToOrg(markdown, convert, presets, context)
|
|
16
27
|
};
|
|
17
28
|
}
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
// the hooks for a page's properties
|
|
30
|
+
function pagePreset() {
|
|
31
|
+
return {
|
|
32
|
+
name: "logseq",
|
|
33
|
+
markdown: {
|
|
34
|
+
read: {
|
|
35
|
+
mdast: keepPagePropertySource,
|
|
36
|
+
org: rewriteLabeledPageRefs
|
|
37
|
+
},
|
|
38
|
+
write: uniorgAst => {
|
|
39
|
+
codeToQueryBlocks(uniorgAst);
|
|
40
|
+
pageProperties(uniorgAst);
|
|
41
|
+
return extractInlineSpecifics(uniorgAst);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
org: {
|
|
45
|
+
read: readOrgInline,
|
|
46
|
+
write: takePageProperties
|
|
47
|
+
}
|
|
48
|
+
};
|
|
20
49
|
}
|
|
21
|
-
|
|
50
|
+
// the hooks for a block's content
|
|
51
|
+
function blockPreset() {
|
|
52
|
+
const bareUrls = new Map();
|
|
22
53
|
return {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
54
|
+
name: "logseq",
|
|
55
|
+
markdown: {
|
|
56
|
+
read: {
|
|
57
|
+
mdast: mdast => {
|
|
58
|
+
countBareUrls(mdast, bareUrls);
|
|
59
|
+
emailLinksToText(mdast);
|
|
60
|
+
},
|
|
61
|
+
org: rewriteLabeledPageRefs
|
|
62
|
+
},
|
|
63
|
+
write: uniorgAst => {
|
|
64
|
+
codeToQueryBlocks(uniorgAst);
|
|
65
|
+
bareUrlsToText(uniorgAst);
|
|
66
|
+
return extractInlineSpecifics(uniorgAst);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
org: {
|
|
70
|
+
read: uniorgAst => {
|
|
71
|
+
bareUrlsToText(uniorgAst);
|
|
72
|
+
return readOrgInline(uniorgAst);
|
|
73
|
+
},
|
|
74
|
+
write: uniorgAst => restoreBareUrls(uniorgAst, bareUrls)
|
|
75
|
+
}
|
|
27
76
|
};
|
|
28
77
|
}
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
78
|
+
// Logseq md writes a url bare, as org writes a plain link; the core
|
|
79
|
+
// carries a md link as a [[url]] bracket link, so md→org counts the
|
|
80
|
+
// links that were bare in the source and turns as many back to plain.
|
|
81
|
+
// Where org's plain link ends short of md's autolink on trailing
|
|
82
|
+
// punctuation (a macro's `}}`), that tail splits off as text
|
|
83
|
+
function countBareUrls(mdast, counts) {
|
|
84
|
+
counts.clear();
|
|
85
|
+
visit(mdast, "link", (node, index, parent) => {
|
|
86
|
+
const url = bareUrl(node);
|
|
87
|
+
if (url === null) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
counts.set(url, (counts.get(url) ?? 0) + 1);
|
|
91
|
+
const tail = node.url.slice(url.length);
|
|
92
|
+
if (tail && parent && index !== undefined) {
|
|
93
|
+
node.url = url;
|
|
94
|
+
node.children = [{ type: "text", value: url }];
|
|
95
|
+
parent.children.splice(index + 1, 0, { type: "text", value: tail });
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// the plain link org reads for a link written bare, if the rest is
|
|
101
|
+
// trailing punctuation
|
|
102
|
+
function bareUrl(node) {
|
|
103
|
+
const start = node.position?.start.offset;
|
|
104
|
+
const end = node.position?.end.offset;
|
|
105
|
+
if (start === undefined ||
|
|
106
|
+
end === undefined ||
|
|
107
|
+
end - start !== node.url.length) {
|
|
108
|
+
return null;
|
|
36
109
|
}
|
|
37
|
-
|
|
38
|
-
|
|
110
|
+
const url = orgPlainLink(node.url);
|
|
111
|
+
return url !== null && /^[^\w(]*$/.test(node.url.slice(url.length))
|
|
112
|
+
? url
|
|
113
|
+
: null;
|
|
114
|
+
}
|
|
115
|
+
// the url of the plain link org reads at the start of `url`, if any;
|
|
116
|
+
// it ends early at a `(` (`…/Bandwidth_(signal)`) or a macro's `}}`
|
|
117
|
+
function orgPlainLink(url) {
|
|
118
|
+
if (!/^https?:\/\//.test(url)) {
|
|
119
|
+
return null;
|
|
39
120
|
}
|
|
40
|
-
;
|
|
41
|
-
|
|
42
|
-
return
|
|
121
|
+
const [paragraph] = tryParse(`${url}\n`)?.children ?? [];
|
|
122
|
+
const [link] = paragraph?.children ?? [];
|
|
123
|
+
return link?.format === "plain"
|
|
124
|
+
? link.rawLink
|
|
125
|
+
: null;
|
|
126
|
+
}
|
|
127
|
+
function restoreBareUrls(uniorgAst, counts) {
|
|
128
|
+
visit(uniorgAst, "link", (node) => {
|
|
129
|
+
const count = counts.get(node.rawLink) ?? 0;
|
|
130
|
+
if (node.format === "bracket" && !node.children.length && count) {
|
|
131
|
+
node.format = "plain";
|
|
132
|
+
counts.set(node.rawLink, count - 1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
return uniorgAst;
|
|
136
|
+
}
|
|
137
|
+
// org→md: a plain http(s) link stays a bare url, unescaped
|
|
138
|
+
// an email address is text in Logseq org and written bare in Logseq md,
|
|
139
|
+
// where GFM links it: md→org takes such a link back to its text, and
|
|
140
|
+
// org→md keeps the address unescaped (keepVerbatimText)
|
|
141
|
+
const EMAIL_RE = /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/;
|
|
142
|
+
function emailLinksToText(mdast) {
|
|
143
|
+
visit(mdast, "link", (node, index, parent) => {
|
|
144
|
+
const start = node.position?.start.offset;
|
|
145
|
+
const end = node.position?.end.offset;
|
|
146
|
+
const text = node.url.replace(/^mailto:/, "");
|
|
147
|
+
if (node.url.startsWith("mailto:") &&
|
|
148
|
+
start !== undefined &&
|
|
149
|
+
end !== undefined &&
|
|
150
|
+
end - start === text.length &&
|
|
151
|
+
parent &&
|
|
152
|
+
index !== undefined) {
|
|
153
|
+
parent.children[index] = { type: "text", value: text };
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function bareUrlsToText(uniorgAst) {
|
|
158
|
+
visit(uniorgAst, "link", (node, index, parent) => {
|
|
159
|
+
if (node.format !== "plain" ||
|
|
160
|
+
!/^https?$/.test(node.linkType) ||
|
|
161
|
+
!parent ||
|
|
162
|
+
index === undefined) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
parent.children[index] = {
|
|
166
|
+
type: "verbatim-inline",
|
|
167
|
+
value: node.rawLink
|
|
168
|
+
};
|
|
169
|
+
return undefined;
|
|
170
|
+
});
|
|
43
171
|
}
|
|
44
172
|
// the page name of a [[page]] link destination, or null. The core
|
|
45
173
|
// transform percent-encodes brackets in urls (an org link path cannot
|
|
@@ -65,79 +193,6 @@ function rewriteLabeledPageRefs(uniorgAst) {
|
|
|
65
193
|
node.rawLink = page;
|
|
66
194
|
node.path = page;
|
|
67
195
|
});
|
|
68
|
-
}
|
|
69
|
-
function toBlockHeadline(node, level) {
|
|
70
|
-
const blockHeadline = {
|
|
71
|
-
type: "headline",
|
|
72
|
-
level,
|
|
73
|
-
todoKeyword: null,
|
|
74
|
-
priority: null,
|
|
75
|
-
commented: false,
|
|
76
|
-
rawValue: "",
|
|
77
|
-
tags: [],
|
|
78
|
-
children: node.children
|
|
79
|
-
};
|
|
80
|
-
takeTaskMarker(blockHeadline);
|
|
81
|
-
return blockHeadline;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Applies Logseq-specific conventions to a uniorg AST: every heading gets
|
|
85
|
-
* a `:heading:` property drawer, and with `nestUnderHeadings` following
|
|
86
|
-
* paragraphs become child block headlines (in Logseq's org format every
|
|
87
|
-
* outline block is a headline).
|
|
88
|
-
* @param uniorgAst The uniorg AST to transform.
|
|
89
|
-
* @param nestUnderHeadings Nest content as child blocks. Default: `true`.
|
|
90
|
-
* @returns The Logseq-flavored uniorg AST.
|
|
91
|
-
*/
|
|
92
|
-
export function applyLogseqSpecificsToUniorgAst(uniorgAst, nestUnderHeadings = true) {
|
|
93
|
-
rewriteLabeledPageRefs(uniorgAst);
|
|
94
|
-
takePageProperties(uniorgAst);
|
|
95
|
-
const children = uniorgAst.children;
|
|
96
|
-
const result = [];
|
|
97
|
-
let currentLevel = 0;
|
|
98
|
-
// level of a headline whose :heading: property still needs a home
|
|
99
|
-
let pending = null;
|
|
100
|
-
const separate = () => {
|
|
101
|
-
if (!nestUnderHeadings) {
|
|
102
|
-
result.push({ type: "text", value: "\n" });
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
// no drawer took the property: give it one of its own
|
|
106
|
-
const settle = () => {
|
|
107
|
-
if (pending === null) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
result.push(headingDrawer(pending));
|
|
111
|
-
pending = null;
|
|
112
|
-
separate();
|
|
113
|
-
};
|
|
114
|
-
for (const node of children) {
|
|
115
|
-
if (node.type === "headline") {
|
|
116
|
-
settle();
|
|
117
|
-
const headline = node;
|
|
118
|
-
currentLevel = headline.level;
|
|
119
|
-
takeTaskMarker(headline);
|
|
120
|
-
result.push(node);
|
|
121
|
-
pending = headline.level;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (pending !== null) {
|
|
125
|
-
if (placeHeadingProperty(node, pending)) {
|
|
126
|
-
pending = null;
|
|
127
|
-
result.push(node);
|
|
128
|
-
separate();
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (node.type !== "planning") {
|
|
132
|
-
settle();
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
result.push(nestUnderHeadings && node.type === "paragraph"
|
|
136
|
-
? toBlockHeadline(node, currentLevel + 1)
|
|
137
|
-
: node);
|
|
138
|
-
}
|
|
139
|
-
settle();
|
|
140
|
-
uniorgAst.children = result;
|
|
141
196
|
return uniorgAst;
|
|
142
197
|
}
|
|
143
198
|
// Logseq reads a page's first block of `key:: value` lines (md) and its
|
|
@@ -180,6 +235,7 @@ function takePageProperties(uniorgAst) {
|
|
|
180
235
|
// ahead of the block, so all of them lead the page
|
|
181
236
|
const at = children.findIndex(node => node.type !== "keyword");
|
|
182
237
|
children.splice(at === -1 ? children.length : at, 0, ...keywords.map(([key, value]) => ({ type: "keyword", key, value })));
|
|
238
|
+
return uniorgAst;
|
|
183
239
|
}
|
|
184
240
|
function takeFrontmatterKeywords(children) {
|
|
185
241
|
const frontmatter = children.find(isFrontmatterNode);
|
|
@@ -240,6 +296,43 @@ function flatValue(value, text) {
|
|
|
240
296
|
? items.join(", ")
|
|
241
297
|
: null;
|
|
242
298
|
}
|
|
299
|
+
const PAGE_PROPERTY_LINE_RE = /^#\+([a-z0-9_-]+):(?: (.*))?$/;
|
|
300
|
+
// Vanilla Markdown: a page's properties, its leading lower-case keywords
|
|
301
|
+
// as Logseq writes them, become plain frontmatter, as Markdown tools
|
|
302
|
+
// read metadata (ADR 0006); the way back takes them as page properties
|
|
303
|
+
// (takeFlatEntries). A keyword that acts in Emacs stays one
|
|
304
|
+
function pagePropertiesToFrontmatter(lines) {
|
|
305
|
+
const begin = lines.findIndex(line => line.startsWith(FRONTMATTER_BLOCK_BEGIN));
|
|
306
|
+
const end = begin === -1 ? -1 : lines.indexOf("#+end_comment", begin);
|
|
307
|
+
// a key the frontmatter block has already stays a keyword
|
|
308
|
+
const taken = new Set(lines
|
|
309
|
+
.slice(begin + 1, begin === -1 ? 0 : end)
|
|
310
|
+
.map(line => /^([^\s:#][^:]*):/.exec(line)?.[1]));
|
|
311
|
+
const properties = {};
|
|
312
|
+
const rest = lines.filter((line, i) => {
|
|
313
|
+
const [, key, value = ""] = PAGE_PROPERTY_LINE_RE.exec(line) ?? [];
|
|
314
|
+
if (!key ||
|
|
315
|
+
(i > begin && i < end) ||
|
|
316
|
+
ACTING_KEYWORD_RE.test(key) ||
|
|
317
|
+
taken.has(key) ||
|
|
318
|
+
key in properties) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
properties[key] = value;
|
|
322
|
+
return false;
|
|
323
|
+
});
|
|
324
|
+
if (!Object.keys(properties).length) {
|
|
325
|
+
return lines;
|
|
326
|
+
}
|
|
327
|
+
// unfolded, or a long value would come back as no flat one
|
|
328
|
+
const yaml = stringifyYaml(properties, { lineWidth: 0 }).replace(/\n$/, "");
|
|
329
|
+
const block = frontmatterBlock(yaml).replace(/\n$/, "").split("\n");
|
|
330
|
+
const at = rest.findIndex(line => line.startsWith(FRONTMATTER_BLOCK_BEGIN));
|
|
331
|
+
// ahead of what the block holds, so the page properties lead
|
|
332
|
+
return at === -1
|
|
333
|
+
? [...block, ...rest]
|
|
334
|
+
: [...rest.slice(0, at + 1), ...block.slice(1, -1), ...rest.slice(at + 1)];
|
|
335
|
+
}
|
|
243
336
|
// the reverse: leading keywords become the first block, verbatim, keys
|
|
244
337
|
// lower-cased (uniorg upper-cases them, Logseq reads only lower case)
|
|
245
338
|
function pageProperties(uniorgAst) {
|
|
@@ -264,40 +357,123 @@ function pageProperties(uniorgAst) {
|
|
|
264
357
|
contentsEnd: 0
|
|
265
358
|
});
|
|
266
359
|
}
|
|
267
|
-
// Logseq
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
360
|
+
// Logseq org's own inline syntax, carried in its Markdown form, which
|
|
361
|
+
// Vanilla Markdown keeps as text (ADR 0006)
|
|
362
|
+
function readOrgInline(uniorgAst) {
|
|
363
|
+
// first: a query's body is Logseq's query language, not org text
|
|
364
|
+
queryBlocksToCode(uniorgAst);
|
|
365
|
+
keepVerbatimText(uniorgAst);
|
|
366
|
+
repairHighlights(uniorgAst);
|
|
367
|
+
return uniorgAst;
|
|
368
|
+
}
|
|
369
|
+
// reading Vanilla Markdown as carrying Logseq Markdown's syntax: a
|
|
370
|
+
// `query` code block is a query block (ADR 0006), which Logseq Markdown
|
|
371
|
+
// writes as the block itself, so its code blocks stay code
|
|
372
|
+
function vanillaReader(preset) {
|
|
373
|
+
const read = preset.markdown?.read;
|
|
374
|
+
return {
|
|
375
|
+
...preset,
|
|
376
|
+
markdown: {
|
|
377
|
+
...preset.markdown,
|
|
378
|
+
read: {
|
|
379
|
+
...read,
|
|
380
|
+
org: uniorgAst => {
|
|
381
|
+
const tree = read?.org?.(uniorgAst) ?? uniorgAst;
|
|
382
|
+
codeToQueryBlocks(tree, "QUERY");
|
|
383
|
+
return tree;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
// a page ref stays a link for the output's Markdown dialect to write
|
|
390
|
+
// (Obsidian's wikilink); Vanilla Markdown carries Logseq's syntax. A
|
|
391
|
+
// hiccup paragraph is taken whole after its links are written, or a
|
|
392
|
+
// page ref in it would be lost
|
|
393
|
+
const vanillaInline = {
|
|
394
|
+
name: "logseq",
|
|
395
|
+
markdown: {
|
|
396
|
+
write: uniorgAst => {
|
|
397
|
+
fuzzyLinksToPageRefs(uniorgAst);
|
|
398
|
+
markHiccupParagraphs(uniorgAst);
|
|
399
|
+
return uniorgAst;
|
|
400
|
+
}
|
|
283
401
|
}
|
|
284
|
-
|
|
402
|
+
};
|
|
403
|
+
// a query block: Vanilla Markdown has none, so a code block in the
|
|
404
|
+
// query language carries it (ADR 0006); Logseq Markdown writes the
|
|
405
|
+
// block itself, as Logseq does
|
|
406
|
+
const QUERY_LANGUAGE = "query";
|
|
407
|
+
function queryBlocksToCode(uniorgAst) {
|
|
408
|
+
visit(uniorgAst, "special-block", (node, index, parent) => {
|
|
409
|
+
if (node.blockType.toUpperCase() !== "QUERY" ||
|
|
410
|
+
!parent ||
|
|
411
|
+
index === undefined) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
const lines = orgNodeToText(node).split("\n");
|
|
415
|
+
parent.children[index] = {
|
|
416
|
+
type: "src-block",
|
|
417
|
+
affiliated: node.affiliated,
|
|
418
|
+
language: QUERY_LANGUAGE,
|
|
419
|
+
// the block as written, for Logseq Markdown to write it back
|
|
420
|
+
blockType: node.blockType,
|
|
421
|
+
switches: null,
|
|
422
|
+
parameters: null,
|
|
423
|
+
value: `${lines.slice(1, -1).join("\n")}\n`
|
|
424
|
+
};
|
|
425
|
+
return undefined;
|
|
426
|
+
});
|
|
285
427
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
428
|
+
// a query block read as a code block (queryBlocksToCode) goes back as
|
|
429
|
+
// it was written; with a fallback type, so does any `query` code block,
|
|
430
|
+
// as Vanilla Markdown carries a query block
|
|
431
|
+
function codeToQueryBlocks(uniorgAst, fallback) {
|
|
432
|
+
visit(uniorgAst, "src-block", (node, index, parent) => {
|
|
433
|
+
const type = node.blockType ?? fallback;
|
|
434
|
+
if (node.language !== QUERY_LANGUAGE ||
|
|
435
|
+
!type ||
|
|
436
|
+
!parent ||
|
|
437
|
+
index === undefined) {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
// a Markdown code block's value ends short of its last line break
|
|
441
|
+
const body = node.value.replace(/\n?$/, "\n");
|
|
442
|
+
const [block] = tryParse(`#+begin_${type}\n${body}#+end_${type}\n`)?.children ?? [];
|
|
443
|
+
if (block) {
|
|
444
|
+
parent.children[index] = block;
|
|
445
|
+
}
|
|
446
|
+
return undefined;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
function extractInlineSpecifics(uniorgAst) {
|
|
450
|
+
keepVerbatimText(uniorgAst);
|
|
297
451
|
fuzzyLinksToPageRefs(uniorgAst);
|
|
298
452
|
markHiccupParagraphs(uniorgAst);
|
|
299
453
|
return uniorgAst;
|
|
300
454
|
}
|
|
455
|
+
// text Logseq md writes as it is, which remark would escape: a task's
|
|
456
|
+
// priority ([#A]), an email address, and a tag's # (`#tag` starting a
|
|
457
|
+
// line, which mldoc reads as escaped plain text once written `\#`);
|
|
458
|
+
// verbatim-inline keeps it
|
|
459
|
+
const VERBATIM_TEXT_RE = new RegExp(`(\\[#[A-Z]\\]|${EMAIL_RE.source}|(?<!\\S)#(?=[^\\s#]|$))`);
|
|
460
|
+
function keepVerbatimText(uniorgAst) {
|
|
461
|
+
visit(uniorgAst, "text", (node, index, parent) => {
|
|
462
|
+
const parts = node.value.split(VERBATIM_TEXT_RE);
|
|
463
|
+
if (parts.length === 1 || !parent || index === undefined) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
// split's captures sit at the odd indices
|
|
467
|
+
const nodes = parts
|
|
468
|
+
.map((value, i) => ({
|
|
469
|
+
type: i % 2 ? "verbatim-inline" : "text",
|
|
470
|
+
value
|
|
471
|
+
}))
|
|
472
|
+
.filter(part => part.value);
|
|
473
|
+
parent.children.splice(index, 1, ...nodes);
|
|
474
|
+
return index + nodes.length;
|
|
475
|
+
});
|
|
476
|
+
}
|
|
301
477
|
// Logseq highlight markup (^^words^^) re-parses as a caret plus a
|
|
302
478
|
// superscript; merge the pieces back into literal text
|
|
303
479
|
function repairHighlights(uniorgAst) {
|
|
@@ -318,6 +494,7 @@ function repairHighlights(uniorgAst) {
|
|
|
318
494
|
}
|
|
319
495
|
}
|
|
320
496
|
});
|
|
497
|
+
return uniorgAst;
|
|
321
498
|
}
|
|
322
499
|
// Logseq page and block references: [[page]] stays a wikilink,
|
|
323
500
|
// [[page][label]] becomes [label]([[page]]), [[((uuid))][label]]
|
|
@@ -360,69 +537,3 @@ function markHiccupParagraphs(uniorgAst) {
|
|
|
360
537
|
}
|
|
361
538
|
});
|
|
362
539
|
}
|
|
363
|
-
function extractInParent(parent) {
|
|
364
|
-
const children = parent.children;
|
|
365
|
-
for (let i = 0; i < children.length; i++) {
|
|
366
|
-
const node = children[i];
|
|
367
|
-
if (!node) {
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
if (node.type === "section") {
|
|
371
|
-
extractInParent(node);
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
if (node.type !== "headline") {
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
const headline = node;
|
|
378
|
-
if (headline.todoKeyword) {
|
|
379
|
-
// back to Logseq md's text conventions (TODO [#A] Ship it);
|
|
380
|
-
// verbatim-inline keeps the [#A] brackets unescaped
|
|
381
|
-
const priority = headline.priority ? `[#${headline.priority}] ` : "";
|
|
382
|
-
headline.children.unshift({
|
|
383
|
-
type: "verbatim-inline",
|
|
384
|
-
value: `${headline.todoKeyword} ${priority}`
|
|
385
|
-
});
|
|
386
|
-
headline.todoKeyword = null;
|
|
387
|
-
headline.priority = null;
|
|
388
|
-
}
|
|
389
|
-
// a planning line sits between the headline and its drawer
|
|
390
|
-
const drawerIndex = children[i + 1]?.type === "planning" ? i + 2 : i + 1;
|
|
391
|
-
const heading = takeHeadingProperty(children, drawerIndex);
|
|
392
|
-
if (heading !== null) {
|
|
393
|
-
headline.level = heading;
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
// a block headline (no :heading:) is outline structure only; its
|
|
397
|
-
// title is the block's content
|
|
398
|
-
children[i] = {
|
|
399
|
-
type: "paragraph",
|
|
400
|
-
children: headline.children,
|
|
401
|
-
contentsBegin: 0,
|
|
402
|
-
contentsEnd: 0
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
// removes the heading property from a drawer at `index` (and the drawer
|
|
408
|
-
// itself if that empties it); returns the heading level or null
|
|
409
|
-
function takeHeadingProperty(children, index) {
|
|
410
|
-
const drawer = children[index];
|
|
411
|
-
if (drawer?.type !== "property-drawer") {
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
const properties = drawer.children;
|
|
415
|
-
const heading = properties.find(property => property.key === "heading");
|
|
416
|
-
if (!heading) {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
const remaining = properties.filter(property => property !== heading);
|
|
420
|
-
if (remaining.length) {
|
|
421
|
-
;
|
|
422
|
-
drawer.children = remaining;
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
children.splice(index, 1);
|
|
426
|
-
}
|
|
427
|
-
return parseInt(heading.value, 10) || null;
|
|
428
|
-
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ConversionContext, FragmentConverter, Preset } from "./types.js";
|
|
2
|
+
export type Meta = {
|
|
3
|
+
lines: string[];
|
|
4
|
+
} | {
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
8
|
+
/** A block of the outline, read from either format. */
|
|
9
|
+
export interface Block {
|
|
10
|
+
level: number;
|
|
11
|
+
heading: number;
|
|
12
|
+
metaFirst: boolean;
|
|
13
|
+
meta: Meta[];
|
|
14
|
+
content: string[];
|
|
15
|
+
}
|
|
16
|
+
/** A page: its properties' source lines, then its blocks. */
|
|
17
|
+
export interface Outline {
|
|
18
|
+
page: string[];
|
|
19
|
+
blocks: Block[];
|
|
20
|
+
}
|
|
21
|
+
interface Presets {
|
|
22
|
+
page: Preset;
|
|
23
|
+
block: Preset;
|
|
24
|
+
vanillaPage?: (lines: string[]) => string[];
|
|
25
|
+
vanillaReader?: (preset: Preset) => Preset;
|
|
26
|
+
vanillaInline?: Preset;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Converts a Logseq org page to Markdown, block by block: Logseq's,
|
|
30
|
+
* or Vanilla where the preset is on the input side only.
|
|
31
|
+
* @param org The org page.
|
|
32
|
+
* @param convert The core's fragment converter.
|
|
33
|
+
* @param presets The presets for the page properties and for a block.
|
|
34
|
+
* @param context The side the preset is on.
|
|
35
|
+
* @returns The Markdown page.
|
|
36
|
+
*/
|
|
37
|
+
export declare function orgOutlineToMarkdown(org: string, convert: FragmentConverter, presets: Presets, context: ConversionContext): string;
|
|
38
|
+
/**
|
|
39
|
+
* Converts a Logseq Markdown page to Logseq org, block by block.
|
|
40
|
+
* @param markdown The Markdown page.
|
|
41
|
+
* @param convert The core's fragment converter.
|
|
42
|
+
* @param presets The presets for the page properties and for a block.
|
|
43
|
+
* @param context The side the preset is on.
|
|
44
|
+
* @returns The org page.
|
|
45
|
+
*/
|
|
46
|
+
export declare function markdownOutlineToOrg(markdown: string, convert: FragmentConverter, presets: Presets, context: ConversionContext): string;
|
|
47
|
+
export {};
|