@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.
Files changed (47) hide show
  1. package/README.md +35 -18
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.js +2 -0
  4. package/dist/cli/conversion.d.ts +3 -3
  5. package/dist/cli/conversion.js +4 -4
  6. package/dist/cli/flags.d.ts +1 -1
  7. package/dist/cli/flags.js +15 -1
  8. package/dist/cli/formats.d.ts +2 -1
  9. package/dist/cli/formats.js +1 -0
  10. package/dist/cli/presets.d.ts +13 -2
  11. package/dist/cli/presets.js +17 -3
  12. package/dist/cli.js +4 -5
  13. package/dist/config.d.ts +9 -4
  14. package/dist/config.js +2 -0
  15. package/dist/conversionOptions.d.ts +19 -3
  16. package/dist/conversionOptions.js +61 -0
  17. package/dist/core/bracedScripts.d.ts +7 -0
  18. package/dist/core/bracedScripts.js +10 -0
  19. package/dist/core/frontmatterBlock.d.ts +7 -0
  20. package/dist/core/frontmatterBlock.js +7 -2
  21. package/dist/core/lineSyntax.d.ts +13 -0
  22. package/dist/core/lineSyntax.js +16 -4
  23. package/dist/core/mdastToUniorg/blocks.js +4 -1
  24. package/dist/core/passthroughSource.d.ts +21 -0
  25. package/dist/core/passthroughSource.js +94 -0
  26. package/dist/core/uniorgToMdast/elements.js +5 -0
  27. package/dist/core/uniorgToMdast/shared.js +6 -1
  28. package/dist/index.d.ts +1 -2
  29. package/dist/markdownToOrg.js +19 -6
  30. package/dist/normalize.js +16 -2
  31. package/dist/options.d.ts +3 -7
  32. package/dist/orgToMarkdown.js +14 -4
  33. package/dist/presets/hooks.d.ts +49 -0
  34. package/dist/presets/hooks.js +72 -0
  35. package/dist/presets/logseq.d.ts +3 -22
  36. package/dist/presets/logseq.js +306 -195
  37. package/dist/presets/logseqOutline.d.ts +47 -0
  38. package/dist/presets/logseqOutline.js +428 -0
  39. package/dist/presets/logseqVanillaMarkdown.d.ts +9 -0
  40. package/dist/presets/logseqVanillaMarkdown.js +295 -0
  41. package/dist/presets/obsidian.js +4 -2
  42. package/dist/presets/registry.d.ts +1 -1
  43. package/dist/presets/registry.js +5 -3
  44. package/dist/presets/sides.d.ts +40 -0
  45. package/dist/presets/sides.js +42 -0
  46. package/dist/presets/types.d.ts +49 -8
  47. package/package.json +1 -1
@@ -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: `heading::` properties and outline nesting.
10
+ * Logseq dialect preset: the outline of blocks, page properties, page
11
+ * and block references, highlights and hiccup.
8
12
  */
9
- export function logseq(options = {}) {
10
- const nestUnderHeadings = options.nestUnderHeadings ?? true;
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
- name: "logseq",
13
- applyToMdast: keepPagePropertySource,
14
- applyToUniorg: uniorgAst => applyLogseqSpecificsToUniorgAst(uniorgAst, nestUnderHeadings),
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
- function headingProperty(level) {
19
- return { type: "node-property", key: "heading", value: String(level) };
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
- function headingDrawer(level) {
50
+ // the hooks for a block's content
51
+ function blockPreset() {
52
+ const bareUrls = new Map();
22
53
  return {
23
- type: "property-drawer",
24
- children: [headingProperty(level)],
25
- contentsBegin: 0,
26
- contentsEnd: 0
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
- // org fixes the order below a headline: the planning line first, then a
30
- // single property drawer. :heading: therefore has to let the planning
31
- // line pass and join an existing drawer rather than displace either.
32
- // Returns whether the property has been placed on `node`.
33
- function placeHeadingProperty(node, level) {
34
- if (node.type === "planning") {
35
- return false;
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
- if (node.type !== "property-drawer") {
38
- return false;
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
- node.children.unshift(headingProperty(level));
42
- return true;
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 md keeps TODO/DONE as leading text markers and priorities as
268
- // [#A] text; in org they are the headline's TODO keyword and priority
269
- // (other Logseq markers like DOING are not org keywords and simply
270
- // stay in the title text)
271
- function takeTaskMarker(headline) {
272
- const first = headline.children[0];
273
- if (first?.type !== "text") {
274
- return;
275
- }
276
- const marker = /^(TODO|DONE) (?:\[#([A-Z])\] )?/.exec(first.value);
277
- if (!marker) {
278
- return;
279
- }
280
- headline.todoKeyword = marker[1];
281
- if (marker[2]) {
282
- headline.priority = marker[2];
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
- first.value = first.value.slice((marker[0] ?? "").length);
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
- * Extracts Logseq-specific conventions from a uniorg AST: headlines with
288
- * a `:heading:` property become plain headings of that level, headlines
289
- * without one are outline blocks and become paragraphs.
290
- * @param uniorgAst The Logseq-flavored uniorg AST to transform.
291
- * @returns The generic uniorg AST.
292
- */
293
- function extractLogseqSpecificsFromUniorgAst(uniorgAst) {
294
- pageProperties(uniorgAst);
295
- extractInParent(uniorgAst);
296
- repairHighlights(uniorgAst);
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 {};