@remigius42/morg 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +12 -1
  2. package/dist/core/affiliated.d.ts +13 -0
  3. package/dist/core/affiliated.js +43 -0
  4. package/dist/core/backslashCommands.d.ts +10 -0
  5. package/dist/core/backslashCommands.js +43 -0
  6. package/dist/core/bracedScripts.d.ts +12 -0
  7. package/dist/core/bracedScripts.js +159 -0
  8. package/dist/core/footnoteReferences.d.ts +10 -0
  9. package/dist/core/footnoteReferences.js +24 -0
  10. package/dist/core/frontmatterBlock.d.ts +89 -0
  11. package/dist/core/frontmatterBlock.js +448 -0
  12. package/dist/core/keyValueLines.d.ts +6 -0
  13. package/dist/core/keyValueLines.js +18 -0
  14. package/dist/core/lineSyntax.d.ts +21 -0
  15. package/dist/core/lineSyntax.js +221 -0
  16. package/dist/core/markupBoundary.d.ts +12 -0
  17. package/dist/core/markupBoundary.js +195 -0
  18. package/dist/core/mdastToUniorg/blocks.d.ts +0 -1
  19. package/dist/core/mdastToUniorg/blocks.js +3 -31
  20. package/dist/core/mdastToUniorg/index.d.ts +6 -0
  21. package/dist/core/mdastToUniorg/index.js +81 -8
  22. package/dist/core/mdastToUniorg/lists.d.ts +1 -1
  23. package/dist/core/mdastToUniorg/lists.js +21 -2
  24. package/dist/core/mdastToUniorg/phrasing.js +145 -12
  25. package/dist/core/orgPath.d.ts +9 -0
  26. package/dist/core/orgPath.js +24 -0
  27. package/dist/core/render.d.ts +33 -0
  28. package/dist/core/render.js +101 -0
  29. package/dist/core/tablePipes.d.ts +17 -0
  30. package/dist/core/tablePipes.js +47 -0
  31. package/dist/core/underscoreBullets.d.ts +10 -0
  32. package/dist/core/underscoreBullets.js +32 -0
  33. package/dist/core/uniorgToMdast/elements.js +1 -1
  34. package/dist/core/uniorgToMdast/index.d.ts +3 -2
  35. package/dist/core/uniorgToMdast/index.js +100 -31
  36. package/dist/core/uniorgToMdast/lists.js +12 -1
  37. package/dist/core/uniorgToMdast/objects.js +44 -6
  38. package/dist/core/uniorgToMdast/shared.d.ts +5 -0
  39. package/dist/core/uniorgToMdast/shared.js +3 -12
  40. package/dist/core/uniorgToMdast/tables.js +12 -6
  41. package/dist/markdownToOrg.js +44 -23
  42. package/dist/orgToMarkdown.js +39 -5
  43. package/dist/presets/logseq.js +130 -0
  44. package/dist/presets/types.d.ts +4 -1
  45. package/package.json +4 -1
@@ -0,0 +1,448 @@
1
+ import { affiliatedEntries, DUAL_NAMES } from "./affiliated.js";
2
+ import { positionParser } from "./render.js";
3
+ import { CST, isMap, isScalar, isSeq, Lexer, parseDocument, visit } from "yaml";
4
+ // frontmatter travels verbatim in a comment block marked as morg's
5
+ // (ADR 0005); uniorg keeps neither the block's parameter nor its
6
+ // unescaped value, so the block is rendered as raw text
7
+ const FRONTMATTER_BLOCK_BEGIN = "#+begin_comment morg_frontmatter";
8
+ export function isFrontmatterNode(node) {
9
+ return node.type === "morg-frontmatter";
10
+ }
11
+ /**
12
+ * md→org: puts the file's header in place, past keywords any pass put
13
+ * in front: an Emacs mode line and the file-level drawer lead the file,
14
+ * keywords org would attach to what follows stay apart, and the
15
+ * frontmatter node becomes the marked comment block.
16
+ * @param uniorgAst The document.
17
+ */
18
+ export function renderFileHeader(uniorgAst) {
19
+ leadWithFileHeader(uniorgAst);
20
+ detachLeadingKeywords(uniorgAst);
21
+ renderFrontmatterBlock(uniorgAst);
22
+ }
23
+ // the frontmatter node as a marked org comment block, raw text
24
+ // uniorg-stringify writes as is
25
+ function renderFrontmatterBlock(uniorgAst) {
26
+ const children = uniorgAst.children;
27
+ const index = children.findIndex(isFrontmatterNode);
28
+ const node = children[index];
29
+ if (node) {
30
+ children[index] = { type: "text", value: frontmatterBlock(node.yaml) };
31
+ }
32
+ }
33
+ // org reads these directly above an element as its affiliated keywords
34
+ // (uniorg-parse's list, aliases included), not as document keywords;
35
+ // `#+NAME:: x` too, which uniorg reads as the key `NAME:` elsewhere
36
+ const AFFILIATED_RE = new RegExp(String.raw `^(?:(?:${DUAL_NAMES})(?:\[.*\])?|DATA|HEADERS?|LABEL|NAME|PLOT|RESNAME|RESULT|SOURCE|SRCNAME|TBLNAME|ATTR_[-\w]+):*$`, "i");
37
+ function isAffiliatedKey(key) {
38
+ return AFFILIATED_RE.test(key);
39
+ }
40
+ /**
41
+ * md→org: a blank line after each leading keyword org would otherwise
42
+ * attach to the element below it, a keyword or the block included.
43
+ * @param uniorgAst The document.
44
+ */
45
+ function detachLeadingKeywords(uniorgAst) {
46
+ const children = uniorgAst.children;
47
+ const first = children.findIndex(node => node.type !== "comment" && node.type !== "property-drawer");
48
+ for (let i = first; children[i]?.type === "keyword"; i++) {
49
+ if (children[i + 1] && isAffiliatedKey(children[i]?.key ?? "")) {
50
+ children.splice(++i, 0, { type: "text", value: "\n" });
51
+ }
52
+ }
53
+ }
54
+ function frontmatterBlock(yaml) {
55
+ const body = yaml ? `${escapeBlockLines(yaml)}\n` : "";
56
+ return `${FRONTMATTER_BLOCK_BEGIN}\n${body}#+end_comment\n`;
57
+ }
58
+ /**
59
+ * md→org: takes the top-level entries `take` maps to keywords out of
60
+ * the frontmatter; the rest stays verbatim, comments included.
61
+ * @param yaml The frontmatter's YAML text.
62
+ * @param take Keywords for an entry, or null to leave it in place.
63
+ * @returns The keywords in order, and the remaining YAML text.
64
+ */
65
+ export function takeFrontmatterEntries(yaml, take) {
66
+ const document = parseDocument(yaml);
67
+ const keywords = [];
68
+ let rest = "";
69
+ let from = 0;
70
+ let comments;
71
+ for (const { key, value } of cuttableEntries(document)) {
72
+ const entries = isScalar(key)
73
+ ? take(key, value, scalar => scalarText(scalar, yaml))
74
+ : null;
75
+ const span = entrySpan(key, value, yaml);
76
+ if (!entries || !span) {
77
+ continue;
78
+ }
79
+ keywords.push(...entries);
80
+ comments ??= yamlComments(yaml);
81
+ rest += yaml.slice(from, span[0]) + commentLines(comments, span);
82
+ from = span[1];
83
+ }
84
+ const left = rest + yaml.slice(from);
85
+ return keywords.length
86
+ ? { keywords, yaml: from === yaml.length ? dropCutNewline(left) : left }
87
+ : { keywords, yaml };
88
+ }
89
+ // the newline before a cut at the end, unless a keep-chomped (`|+`)
90
+ // scalar above it holds it as a line of its own
91
+ function dropCutNewline(yaml) {
92
+ const dropped = yaml.replace(/\n$/, "");
93
+ const value = (text) => JSON.stringify(parseDocument(text).toJS());
94
+ return value(dropped) === value(yaml) ? dropped : yaml;
95
+ }
96
+ // entries can be cut out of a block mapping, one per line, unless an
97
+ // alias would lose its anchor or the YAML has errors, which make the
98
+ // ranges unreliable
99
+ function cuttableEntries(document) {
100
+ const contents = document.contents;
101
+ let aliased = false;
102
+ visit(document, {
103
+ Alias() {
104
+ aliased = true;
105
+ return visit.BREAK;
106
+ }
107
+ });
108
+ return isMap(contents) &&
109
+ !contents.flow &&
110
+ !aliased &&
111
+ !document.errors.length
112
+ ? contents.items
113
+ : [];
114
+ }
115
+ // a plain scalar as written: its parsed value would rewrite `1.10` as
116
+ // `1.1`, `01234` as `1234` and an empty value as `null`; a quoted one
117
+ // as its string
118
+ function scalarText(scalar, yaml) {
119
+ const [start, end] = scalar.range ?? [0, 0];
120
+ return scalar.type === "PLAIN" ? yaml.slice(start, end) : String(scalar.value);
121
+ }
122
+ // an entry's source text, from the start of its key's line (an explicit
123
+ // `? `, an anchor or a tag precede the key itself) to the end of its value
124
+ function entrySpan(key, value, yaml) {
125
+ const start = isScalar(key) ? key.range?.[0] : undefined;
126
+ const end = value?.range?.[2];
127
+ return start === undefined || end === undefined
128
+ ? null
129
+ : [yaml.lastIndexOf("\n", start - 1) + 1, end];
130
+ }
131
+ const CONTROL_TOKENS = [CST.DOCUMENT, CST.FLOW_END, CST.SCALAR];
132
+ // each comment and its offset
133
+ function yamlComments(yaml) {
134
+ const comments = [];
135
+ let offset = 0;
136
+ for (const token of new Lexer().lex(yaml)) {
137
+ if (token.startsWith("#")) {
138
+ comments.push([offset, token]);
139
+ }
140
+ // control tokens mark structure, but hold no source
141
+ offset += CONTROL_TOKENS.includes(token) ? 0 : token.length;
142
+ }
143
+ return comments;
144
+ }
145
+ // the comments within a span, each on a line of its own
146
+ function commentLines(comments, [start, end]) {
147
+ return comments
148
+ .filter(([offset]) => offset >= start && offset < end)
149
+ .map(([, comment]) => `${comment}\n`)
150
+ .join("");
151
+ }
152
+ // a keyword name as uniorg reads it in `#+NAME: value`: no whitespace
153
+ // outside a dual keyword's brackets (`#+FOO:: bar` as the key `FOO:`)
154
+ export const KEYWORD_NAME = String.raw `(?:\[[^\]\r\n]*\]|\S)+`;
155
+ // what a `#+KEY: value` line can hold, read back as the same key: not a
156
+ // block's begin line, and no line break, which would end it and inject
157
+ // org structure
158
+ const KEYWORD_KEY_RE = new RegExp(String.raw `^(?!begin_)${KEYWORD_NAME}$`, "i");
159
+ export function fitsKeywordLine(key, value) {
160
+ return KEYWORD_KEY_RE.test(key) && !/[\r\n]/.test(value);
161
+ }
162
+ // what a `:KEY: value` drawer line can hold, read back as the same key
163
+ // (`:header-args:python:` included): `:END:` would close the drawer
164
+ export function fitsPropertyLine(key, value) {
165
+ return /^\S+$/.test(key) && !/^end$/i.test(key) && !/[\r\n]/.test(value);
166
+ }
167
+ /**
168
+ * org→md: whether morg's own entries (`morg_properties`,
169
+ * `morg_keywords`) can join the frontmatter: whether the YAML with one
170
+ * more top-level entry appended still parses as a single block mapping
171
+ * holding none of them yet. Comments alone take them; an indented
172
+ * mapping, a list, a flow mapping or a document ending in `...` do not.
173
+ * @param yaml The frontmatter's YAML text.
174
+ */
175
+ export function takesMorgEntries(yaml) {
176
+ if (!yaml.trim()) {
177
+ return true;
178
+ }
179
+ const document = parseDocument(`${yaml}\nmorg_probe: x`);
180
+ const contents = document.contents;
181
+ return (!document.errors.length &&
182
+ isMap(contents) &&
183
+ !contents.flow &&
184
+ !contents.has("morg_keywords") &&
185
+ !contents.has("morg_properties"));
186
+ }
187
+ /**
188
+ * md→org: one of morg's own entries, a sequence of one-entry maps
189
+ * (`morg_keywords`, `morg_properties`, ADR 0005), restored only if
190
+ * every item fits its org line.
191
+ * @param yaml The frontmatter's YAML text.
192
+ * @param name The entry's key.
193
+ * @param fits Whether a key and value fit the org line they go back to.
194
+ * @returns The key, value pairs in order, and the remaining YAML text.
195
+ */
196
+ export function takeMorgEntry(yaml, name, fits) {
197
+ return takeFrontmatterEntries(yaml, (key, value, text) => key.value === name && isSeq(value)
198
+ ? morgEntryItems(value.items, text, fits)
199
+ : null);
200
+ }
201
+ // each item a single `KEY: value` pair, or the entry is not morg's to
202
+ // restore and stays in the frontmatter as written
203
+ function morgEntryItems(items, text, fits) {
204
+ const entries = [];
205
+ for (const item of items) {
206
+ const pair = isMap(item) && item.items.length === 1 ? item.items[0] : null;
207
+ if (!isScalar(pair?.key) ||
208
+ !(pair.value === null || isScalar(pair.value))) {
209
+ return null;
210
+ }
211
+ const entry = [
212
+ text(pair.key),
213
+ pair.value ? text(pair.value) : ""
214
+ ];
215
+ if (!fits(...entry)) {
216
+ return null;
217
+ }
218
+ entries.push(entry);
219
+ }
220
+ return entries;
221
+ }
222
+ /**
223
+ * org→md: takes the file's header morg carries as frontmatter: the
224
+ * marked block and, where its YAML can take it, a file-level drawer.
225
+ * @param uniorgAst The parsed document.
226
+ * @param org The text it was parsed from.
227
+ * @param onWarning Reports a marked block morg cannot take.
228
+ * @returns The transform options holding them.
229
+ */
230
+ export function takeFileHeader(uniorgAst, org, onWarning) {
231
+ // uniorg drops leading blank lines: a -*- comment below one is inert
232
+ const modeLine = startsWithModeLine(uniorgAst) && isModeLine(org);
233
+ detachKeywordsOfKeywords(uniorgAst);
234
+ return headerOptions(uniorgAst, takeFrontmatterBlock(uniorgAst, org, onWarning), modeLine);
235
+ }
236
+ // `#+NAME: n` directly above `#+TITLE: t`: uniorg attaches it to the
237
+ // keyword below, where nothing reads it; it becomes a keyword of its own
238
+ function detachKeywordsOfKeywords(uniorgAst) {
239
+ const children = uniorgAst.children;
240
+ for (let i = zerothSection(children).length - 1; i >= 0; i--) {
241
+ const node = children[i];
242
+ if (node?.type === "keyword") {
243
+ children.splice(i, 0, ...affiliatedKeywords(node.affiliated));
244
+ node.affiliated = {};
245
+ }
246
+ }
247
+ }
248
+ // affiliated keywords as keyword nodes, upper-cased as org reads them
249
+ // apart from their element: a short caption too
250
+ function affiliatedKeywords(affiliated) {
251
+ return affiliatedEntries(affiliated, true).map(([key, value]) => ({
252
+ type: "keyword",
253
+ key: key.toUpperCase(),
254
+ value
255
+ }));
256
+ }
257
+ /**
258
+ * org→md, for a tree `transformMdastToUniorgAst` built rather than one
259
+ * parsed from org text: its file header, the block still raw text.
260
+ * @param uniorgAst The document; its header nodes are removed.
261
+ * @returns The transform options holding the header.
262
+ */
263
+ export function takeRenderedFileHeader(uniorgAst) {
264
+ const modeLine = startsWithModeLine(uniorgAst);
265
+ const header = zerothSection(uniorgAst.children);
266
+ const block = header.find(node => renderedBlockYaml(node) !== undefined);
267
+ const frontmatter = renderedBlockYaml(block);
268
+ // the block, and the separators renderFileHeader put below keywords
269
+ uniorgAst.children = uniorgAst.children.filter((node, i, children) => node !== block &&
270
+ !(i < header.length &&
271
+ isSeparator(node) &&
272
+ children[i - 1]?.type === "keyword"));
273
+ return headerOptions(uniorgAst, frontmatter, modeLine);
274
+ }
275
+ // the transform options for a header whose block is taken: the
276
+ // file-level drawer joins it where its YAML can take it
277
+ function headerOptions(uniorgAst, frontmatter, modeLine) {
278
+ const takes = takesMorgEntries(frontmatter ?? "");
279
+ const fileProperties = takes ? takeFileDrawer(uniorgAst) : undefined;
280
+ return {
281
+ ...(frontmatter !== undefined && { frontmatter }),
282
+ ...(fileProperties && { fileProperties }),
283
+ takesMorgEntries: takes,
284
+ startsWithModeLine: modeLine
285
+ };
286
+ }
287
+ // before the header is taken, which may leave a comment first
288
+ function startsWithModeLine(uniorgAst) {
289
+ const first = uniorgAst.children[0];
290
+ return !!first && isModeLineComment(first);
291
+ }
292
+ function zerothSection(children) {
293
+ const headline = children.findIndex(node => node.type === "headline");
294
+ return headline === -1 ? children : children.slice(0, headline);
295
+ }
296
+ function isSeparator(node) {
297
+ return node.type === "text" && node.value === "\n";
298
+ }
299
+ // the YAML of a block `frontmatterBlock` rendered, or undefined
300
+ function renderedBlockYaml(node) {
301
+ const text = node?.type === "text" ? (node.value ?? "") : "";
302
+ const begin = `${FRONTMATTER_BLOCK_BEGIN}\n`;
303
+ const end = "#+end_comment\n";
304
+ if (!text.startsWith(begin) || !text.endsWith(end)) {
305
+ return undefined;
306
+ }
307
+ return unescapeBlockLines(text.slice(begin.length, -end.length)).replace(/\n$/, "");
308
+ }
309
+ /**
310
+ * org→md: removes a file-level property drawer (org-roam's `:ID:`),
311
+ * which org reads as the file's only where nothing but comments
312
+ * precede it.
313
+ * @param uniorgAst The parsed document.
314
+ * @returns Its properties in order, or undefined without one.
315
+ */
316
+ function takeFileDrawer(uniorgAst) {
317
+ const children = uniorgAst.children;
318
+ const index = children.findIndex(node => node.type !== "comment");
319
+ const drawer = children[index];
320
+ if (drawer?.type !== "property-drawer") {
321
+ return undefined;
322
+ }
323
+ children.splice(index, 1);
324
+ return drawer.children.map(property => [property.key, property.value]);
325
+ }
326
+ // Emacs reads file variables from a `-*- … -*-` comment on the first line
327
+ const MODE_LINE_RE = /^[^\n]*-\*-.*-\*-/;
328
+ export function isModeLine(comment) {
329
+ return MODE_LINE_RE.test(comment);
330
+ }
331
+ export function isModeLineComment(node) {
332
+ return (node.type === "comment" &&
333
+ isModeLine(node.value ?? ""));
334
+ }
335
+ /**
336
+ * md→org: marks the comment made from the Markdown body's first node as
337
+ * the file's mode line, if it is a -*- comment: what passes do to the
338
+ * tree before the header is rendered cannot make another one lead.
339
+ * @param node The uniorg node made from the body's first node.
340
+ */
341
+ export function markModeLine(node) {
342
+ if (node && isModeLineComment(node)) {
343
+ ;
344
+ node.modeLine = true;
345
+ }
346
+ }
347
+ /**
348
+ * md→org: the file's header leads it, past keywords any pass put in
349
+ * front: the marked Emacs mode line on the first line, then the
350
+ * file-level property drawer, which org reads as the file's only where
351
+ * nothing but comments precede it.
352
+ * @param uniorgAst The document.
353
+ */
354
+ function leadWithFileHeader(uniorgAst) {
355
+ const children = uniorgAst.children;
356
+ const body = children.findIndex(node => node.modeLine);
357
+ const modeLine = children[body];
358
+ if (modeLine) {
359
+ children.splice(body, 1);
360
+ children.unshift(modeLine);
361
+ }
362
+ const index = zerothSection(children).findIndex(node => node.type === "property-drawer");
363
+ if (index === -1) {
364
+ return;
365
+ }
366
+ const [drawer] = children.splice(index, 1);
367
+ const at = children.findIndex(node => node.type !== "comment");
368
+ children.splice(at === -1 ? children.length : at, 0, drawer);
369
+ }
370
+ // Org's own block escaping (org-escape-code-in-string): a line org would
371
+ // read as a headline or a keyword, or one already escaped, gets a comma
372
+ function escapeBlockLines(text) {
373
+ return text.replace(/^([ \t]*)(,*(?:\*|#\+))/gm, "$1,$2");
374
+ }
375
+ // org-unescape-code-in-string: drops one comma of an escaped line
376
+ function unescapeBlockLines(text) {
377
+ return text.replace(/^([ \t]*,*),(\*|#\+)/gm, "$1$2");
378
+ }
379
+ /**
380
+ * org→md: removes the marked frontmatter block, the first one before
381
+ * the first headline, recognized by its own begin line in the source.
382
+ * Keywords org attached to it stay, as keywords in its place: they
383
+ * come back apart from it, as nothing reads them on a comment block.
384
+ * @param uniorgAst The parsed document.
385
+ * @param org The text it was parsed from.
386
+ * @param onWarning Reports keywords attached to the block, and any
387
+ * other marked block, which stays a comment block: uniorg drops its
388
+ * marker.
389
+ * @returns The frontmatter's YAML text, or undefined without a block.
390
+ */
391
+ function takeFrontmatterBlock(uniorgAst, org, onWarning) {
392
+ const children = uniorgAst.children;
393
+ const blocks = zerothSectionCommentBlocks(uniorgAst);
394
+ if (!blocks.length) {
395
+ return undefined;
396
+ }
397
+ const [index, ...others] = markedBlockIndices(org, blocks.length);
398
+ if (others.length) {
399
+ onWarning?.("second frontmatter block stays a comment block, without its marker");
400
+ }
401
+ const node = index === undefined ? undefined : blocks[index];
402
+ if (!node) {
403
+ return undefined;
404
+ }
405
+ if (Object.keys(node.affiliated).length) {
406
+ onWarning?.("keywords on the frontmatter block come back apart from it");
407
+ }
408
+ children.splice(children.indexOf(node), 1, ...affiliatedKeywords(node.affiliated));
409
+ return unescapeBlockLines(node.value).replace(/\n$/, "");
410
+ }
411
+ function zerothSectionCommentBlocks(uniorgAst) {
412
+ const blocks = [];
413
+ for (const node of uniorgAst.children) {
414
+ if (node.type === "headline" || node.type === "section") {
415
+ break;
416
+ }
417
+ if (node.type === "comment-block") {
418
+ blocks.push(node);
419
+ }
420
+ }
421
+ return blocks;
422
+ }
423
+ // uniorg drops the parameter after #+begin_comment; a parse that keeps
424
+ // positions finds each block's own begin line (after any keywords org
425
+ // attached to it)
426
+ const BEGIN_COMMENT_RE = /^[ \t]*#\+begin_comment(.*)$/im;
427
+ // org reads a line of stars and a space or tab as a headline anywhere,
428
+ // which is why blocks comma-escape theirs
429
+ const HEADLINE_RE = /^\*+[ \t]/m;
430
+ // `count`: how many comment blocks the full parse found there
431
+ function markedBlockIndices(org, count) {
432
+ // only the text before the first headline: its blocks are all we need,
433
+ // unless a star line inside a block cut it short
434
+ const headline = HEADLINE_RE.exec(org)?.index ?? org.length;
435
+ let zeroth = org.slice(0, headline);
436
+ let blocks = zerothSectionCommentBlocks(positionParser.parse(zeroth));
437
+ if (blocks.length !== count) {
438
+ zeroth = org;
439
+ blocks = zerothSectionCommentBlocks(positionParser.parse(org));
440
+ }
441
+ return blocks.flatMap((block, i) => {
442
+ const start = block.position?.start.offset ?? 0;
443
+ return BEGIN_COMMENT_RE.exec(zeroth.slice(start))?.[1]?.trim() ===
444
+ "morg_frontmatter"
445
+ ? [i]
446
+ : [];
447
+ });
448
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The key, value pairs of a block of `key:: value` lines.
3
+ * @param text The block's text.
4
+ * @returns The pairs in order, or null if a line is none.
5
+ */
6
+ export declare function keyValueEntries(text: string): [string, string][] | null;
@@ -0,0 +1,18 @@
1
+ // a `key:: value` line, Logseq's property syntax; an empty `key::` too
2
+ const KEY_VALUE_LINE_RE = /^([\w-]+)::(?: (.*))?$/;
3
+ /**
4
+ * The key, value pairs of a block of `key:: value` lines.
5
+ * @param text The block's text.
6
+ * @returns The pairs in order, or null if a line is none.
7
+ */
8
+ export function keyValueEntries(text) {
9
+ const entries = [];
10
+ for (const line of text.replace(/\r?\n$/, "").split(/\r?\n/)) {
11
+ const match = KEY_VALUE_LINE_RE.exec(line);
12
+ if (!match) {
13
+ return null;
14
+ }
15
+ entries.push([match[1] ?? "", match[2] ?? ""]);
16
+ }
17
+ return entries;
18
+ }
@@ -0,0 +1,21 @@
1
+ import type { Parent } from "unist";
2
+ import { type Node } from "./render.js";
3
+ /**
4
+ * md→org: a paragraph line org would read as line syntax (an escaped
5
+ * `1\.` or `\*`, or a lazy continuation line) gets a leading zero-width
6
+ * space, or it would turn into a list item, headline, comment or table.
7
+ */
8
+ export declare function escapeLineSyntax(tree: Parent): void;
9
+ /**
10
+ * md→org: whether a node is a paragraph org→md wrote as the org text of
11
+ * one passthrough element, which goes back as it is.
12
+ * @param node The node.
13
+ * @param index Its index in its parent.
14
+ * @param parent Its parent.
15
+ */
16
+ export declare function isPassthroughParagraph(node: Node | Parent, index: number, parent: Parent | undefined): boolean;
17
+ /**
18
+ * org→md: drops the line-start zero-width spaces `escapeLineSyntax`
19
+ * inserts.
20
+ */
21
+ export declare function unescapeLineSyntax(tree: Parent): void;