@remigius42/morg 0.9.0 → 0.9.2
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.
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Parent } from "unist";
|
|
2
2
|
import { type Node } from "./render.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether a line, indented or not, may start org line syntax: false
|
|
5
|
+
* where org surely reads it as text.
|
|
6
|
+
* @param line The line, without its line break.
|
|
7
|
+
* @returns Whether it may start an element other than a paragraph.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mayBeLineSyntax(line: string): boolean;
|
|
3
10
|
/**
|
|
4
11
|
* Whether org reads a line as anything but a plain paragraph.
|
|
5
12
|
* @param line The line, without its line break.
|
package/dist/core/lineSyntax.js
CHANGED
|
@@ -9,6 +9,15 @@ import { delimiters, isInline, locate, positionParser, renderInline, tryParse }
|
|
|
9
9
|
// `:` (`1.`, `a)`, `CLOCK:`, `_.`); any other line (one starting with a
|
|
10
10
|
// link, markup or code, say) is text, and skips the parse
|
|
11
11
|
const MAY_BE_LINE_SYNTAX_RE = /^(?:[-+]|\*+)(?:\s|$)|^(?:-{5}|\+-|#(?:\s|$|\+)|[|:]|\[fn:|\\begin\{|%%\()|^[\p{L}\p{N}_]+[.):]/u;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a line, indented or not, may start org line syntax: false
|
|
14
|
+
* where org surely reads it as text.
|
|
15
|
+
* @param line The line, without its line break.
|
|
16
|
+
* @returns Whether it may start an element other than a paragraph.
|
|
17
|
+
*/
|
|
18
|
+
export function mayBeLineSyntax(line) {
|
|
19
|
+
return MAY_BE_LINE_SYNTAX_RE.test(line.trimStart());
|
|
20
|
+
}
|
|
12
21
|
/**
|
|
13
22
|
* Whether org reads a line as anything but a plain paragraph.
|
|
14
23
|
* @param line The line, without its line break.
|
|
@@ -43,15 +43,29 @@ function transformMdastTableRow(ctx, row) {
|
|
|
43
43
|
}))
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
// lines without the indentation all non-blank ones share
|
|
47
|
+
function dedentLines(text) {
|
|
48
|
+
const lines = text.split("\n");
|
|
49
|
+
const indent = Math.min(...lines
|
|
50
|
+
.filter(line => line.trim())
|
|
51
|
+
.map(line => /^[ \t]*/.exec(line)?.[0].length ?? 0));
|
|
52
|
+
return Number.isFinite(indent)
|
|
53
|
+
? lines.map(line => line.slice(indent)).join("\n")
|
|
54
|
+
: text;
|
|
55
|
+
}
|
|
46
56
|
export function transformMdastHtml(ctx, node) {
|
|
47
57
|
// html comments are markdown's comment idiom and map natively to
|
|
48
58
|
// org comments (not an md-ism)
|
|
49
59
|
const comment = /^<!--([\s\S]*?)-->\s*$/.exec(node.value);
|
|
50
60
|
if (comment) {
|
|
61
|
+
const body = comment[1] ?? "";
|
|
62
|
+
// a multi-line comment's own line breaks frame its lines, so an
|
|
63
|
+
// empty first or last line is the comment's, not padding
|
|
64
|
+
const lines = /^[ \t]*\n([\s\S]*)\n[ \t]*$/.exec(body);
|
|
51
65
|
return {
|
|
52
66
|
type: "comment",
|
|
53
67
|
// inverse of the escaping applied when the comment was emitted
|
|
54
|
-
value: (
|
|
68
|
+
value: (lines ? dedentLines(lines[1] ?? "") : body.trim()).replaceAll("-->", "-->")
|
|
55
69
|
};
|
|
56
70
|
}
|
|
57
71
|
if (ctx.options.interpretHtml) {
|
package/dist/markdownToOrg.js
CHANGED
|
@@ -87,10 +87,28 @@ function convertMarkdownDocument(markdown, options, sides) {
|
|
|
87
87
|
// a marked comment block; raw text, past every pass that rewrites text
|
|
88
88
|
renderFileHeader(uniorgAst);
|
|
89
89
|
// Phase 4: Render uniorg-ast to Org-mode string
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
return orgContent;
|
|
90
|
+
const orgContent = String(stringifier.stringify(uniorgAst));
|
|
91
|
+
return orgContent.replaceAll(SPACE_KEEPER, "");
|
|
93
92
|
}
|
|
93
|
+
// uniorg-stringify trims a headline's line, and the document's end, but
|
|
94
|
+
// Emacs reads stars without a space after them as text; the space ends
|
|
95
|
+
// in a character no trim takes, removed after. null leaves the rest to
|
|
96
|
+
// uniorg-stringify
|
|
97
|
+
const SPACE_KEEPER = "\u0000";
|
|
98
|
+
function emptyHeadline(node) {
|
|
99
|
+
const empty = !node.children.length &&
|
|
100
|
+
!node.todoKeyword &&
|
|
101
|
+
!node.priority &&
|
|
102
|
+
!node.commented &&
|
|
103
|
+
!node.tags.length;
|
|
104
|
+
return empty ? `${"*".repeat(node.level)} ${SPACE_KEEPER}\n` : null;
|
|
105
|
+
}
|
|
106
|
+
// built once: a preset converts a page block by block
|
|
107
|
+
const stringifier = unified()
|
|
108
|
+
.use(uniorgStringify, {
|
|
109
|
+
handlers: { headline: emptyHeadline }
|
|
110
|
+
})
|
|
111
|
+
.freeze();
|
|
94
112
|
// a preset's dialect conventions that need the Markdown source apply
|
|
95
113
|
// to the parse, before the generic transform
|
|
96
114
|
function parseMarkdown(markdown, preset) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { consumesBracedScripts } from "../core/bracedScripts.js";
|
|
2
|
-
import { readsAsLineSyntax } from "../core/lineSyntax.js";
|
|
2
|
+
import { mayBeLineSyntax, readsAsLineSyntax } from "../core/lineSyntax.js";
|
|
3
|
+
import { positionParser, tryParse } from "../core/render.js";
|
|
3
4
|
import { ZERO_WIDTH_SPACE } from "../core/markupBoundary.js";
|
|
4
5
|
import { isDrawerStart, isOrgBlockStart, orgElementEnd } from "../core/passthroughSource.js";
|
|
5
6
|
import { readVanillaMarkdownOutline } from "./logseqVanillaMarkdown.js";
|
|
@@ -17,6 +18,7 @@ const MD_PROPERTY_RE = /^([\w.-]+)::(?: (.*))?$/;
|
|
|
17
18
|
// a repeated task's log line, which Logseq bullets per format
|
|
18
19
|
const STATE_LINE_RE = /^[-*] (?=State ")/;
|
|
19
20
|
const MD_HEADING_RE = /^(#{1,6})(?: (.*))?$/;
|
|
21
|
+
const HEADLINE_RE = /^\*+ /;
|
|
20
22
|
const FENCE_RE = /^\s*(?:```|~~~)/;
|
|
21
23
|
// md→org adds it for the text's bare `_` and `^`, which a block's
|
|
22
24
|
// content holds as Logseq writes it
|
|
@@ -116,8 +118,10 @@ function withBracedScripts(lines) {
|
|
|
116
118
|
return consumesBracedScripts(braced.join("\n")) ? braced : lines;
|
|
117
119
|
}
|
|
118
120
|
// 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
|
-
|
|
121
|
+
// level is one of them, but for content that starts with the drawer,
|
|
122
|
+
// unless written below an empty title (Vanilla org), which both look
|
|
123
|
+
// alike in
|
|
124
|
+
function readOrgBlock({ level, lines }, joined = false) {
|
|
121
125
|
const [first = "", ...rest] = lines;
|
|
122
126
|
const metaFirst = isDrawerStart(first);
|
|
123
127
|
const { meta: groups, body } = takeMeta(metaFirst ? lines : rest, () => false);
|
|
@@ -128,7 +132,9 @@ function readOrgBlock({ level, lines }) {
|
|
|
128
132
|
}
|
|
129
133
|
return group.slice(1, -1).flatMap(line => {
|
|
130
134
|
const [, key = "", value = ""] = ORG_PROPERTY_RE.exec(line) ?? [];
|
|
131
|
-
if (!metaFirst
|
|
135
|
+
if ((!metaFirst || joined) &&
|
|
136
|
+
key === "heading" &&
|
|
137
|
+
/^[1-6]$/.test(value)) {
|
|
132
138
|
heading = Number(value);
|
|
133
139
|
return [];
|
|
134
140
|
}
|
|
@@ -143,12 +149,21 @@ function readOrgBlock({ level, lines }) {
|
|
|
143
149
|
content: metaFirst ? body : [first, ...body]
|
|
144
150
|
};
|
|
145
151
|
}
|
|
146
|
-
function readOrgOutline(org) {
|
|
152
|
+
function readOrgOutline(org, vanilla) {
|
|
147
153
|
const { page, blocks } = splitBlocks(org.replace(/\r?\n$/, "").split(/\r?\n/), line => {
|
|
148
154
|
const match = ORG_BLOCK_RE.exec(line);
|
|
149
155
|
return match ? [match[1]?.length ?? 0, match[2] ?? ""] : null;
|
|
150
156
|
});
|
|
151
|
-
return {
|
|
157
|
+
return {
|
|
158
|
+
page,
|
|
159
|
+
blocks: blocks.map(({ level, lines }) => {
|
|
160
|
+
// Vanilla org: a first line written below an empty title, as it
|
|
161
|
+
// starts an element, is the block's first line again
|
|
162
|
+
const [title, ...rest] = lines;
|
|
163
|
+
const joined = vanilla && title === "" && startsElement(rest);
|
|
164
|
+
return readOrgBlock({ level, lines: joined ? rest : lines }, joined);
|
|
165
|
+
})
|
|
166
|
+
};
|
|
152
167
|
}
|
|
153
168
|
// a continuation line sits two spaces inside its bullet
|
|
154
169
|
function dedent(line, level) {
|
|
@@ -246,23 +261,51 @@ function orgMetaLines(meta, heading) {
|
|
|
246
261
|
const drawer = [":PROPERTIES:", ...properties, ":END:"];
|
|
247
262
|
return lines.flatMap(line => (line === null ? drawer : [line]));
|
|
248
263
|
}
|
|
249
|
-
|
|
250
|
-
|
|
264
|
+
// Logseq org writes a block's first line on its stars' line, where
|
|
265
|
+
// Emacs reads it as the title; whether the line starts an element there
|
|
266
|
+
// that runs on below it (a block, a list, a table, a drawer), which the
|
|
267
|
+
// title would cut off; one line of a list or a table is a fine title,
|
|
268
|
+
// as is a headline's own
|
|
269
|
+
function startsElement(lines) {
|
|
270
|
+
const [first = ""] = lines;
|
|
271
|
+
if (!mayBeLineSyntax(first) || HEADLINE_RE.test(first)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const text = `${lines.join("\n")}\n`;
|
|
275
|
+
const [element] = tryParse(text, positionParser)?.children ?? [];
|
|
276
|
+
// its end takes in the blank lines after it
|
|
277
|
+
return (!!element &&
|
|
278
|
+
element.type !== "paragraph" &&
|
|
279
|
+
text
|
|
280
|
+
.slice(0, element.position?.end.offset ?? 0)
|
|
281
|
+
.trimEnd()
|
|
282
|
+
.includes("\n"));
|
|
283
|
+
}
|
|
284
|
+
// Vanilla org: Emacs reads stars without a space after them as text, so
|
|
285
|
+
// an empty title keeps the space Logseq org leaves out, and a first line
|
|
286
|
+
// that starts an element goes below the stars (ADR 0006)
|
|
287
|
+
function writeOrgBlock(block, vanilla = false) {
|
|
288
|
+
const lines = arrange(block.metaFirst, orgMetaLines(block.meta, block.heading), block.content);
|
|
289
|
+
const [title = "", ...more] = vanilla && startsElement(lines) ? ["", ...lines] : lines;
|
|
251
290
|
return [
|
|
252
|
-
`${"*".repeat(block.level)}${title ? ` ${title}` : ""}`,
|
|
291
|
+
`${"*".repeat(block.level)}${title || vanilla ? ` ${title}` : ""}`,
|
|
253
292
|
...more
|
|
254
293
|
].join("\n");
|
|
255
294
|
}
|
|
256
295
|
function writeOutline({ page, blocks }, writeBlock) {
|
|
257
296
|
return [...page, ...blocks.map(writeBlock)].join("\n").concat("\n");
|
|
258
297
|
}
|
|
259
|
-
// a
|
|
260
|
-
|
|
298
|
+
// a table or a rule, which Logseq reads on a headline's line
|
|
299
|
+
const TITLE_ELEMENT_RE = /^(\||-{5,}\s*$)/;
|
|
300
|
+
// a block's title is a headline's, inline text but for a table or a
|
|
301
|
+
// rule; converted with the lines below it, it must not read as a list,
|
|
261
302
|
// a headline or a fixed-width line, so an escape the core drops keeps
|
|
262
303
|
// it text (ADR 0006)
|
|
263
304
|
function inlineTitle(block) {
|
|
264
305
|
const [title = "", ...rest] = block.content;
|
|
265
|
-
return !block.metaFirst &&
|
|
306
|
+
return !block.metaFirst &&
|
|
307
|
+
!TITLE_ELEMENT_RE.test(title) &&
|
|
308
|
+
readsAsLineSyntax(title)
|
|
266
309
|
? [`${ZERO_WIDTH_SPACE}${title}`, ...rest]
|
|
267
310
|
: block.content;
|
|
268
311
|
}
|
|
@@ -380,15 +423,17 @@ function writeVanillaMarkdownOutline({ page, blocks }, context) {
|
|
|
380
423
|
* @returns The Markdown page.
|
|
381
424
|
*/
|
|
382
425
|
export function orgOutlineToMarkdown(org, convert, presets, context) {
|
|
383
|
-
const { page, blocks } = readOrgOutline(org);
|
|
426
|
+
const { page, blocks } = readOrgOutline(org, context.side === "output");
|
|
384
427
|
const vanilla = context.side === "input";
|
|
428
|
+
// read as Logseq org, a block's title is inline text (ADR 0006)
|
|
429
|
+
const inline = context.side !== "output";
|
|
385
430
|
const pageLines = vanilla ? (presets.vanillaPage?.(page) ?? page) : page;
|
|
386
431
|
const convertCarried = (fragment, preset) => convert(fragment, preset, vanilla ? presets.vanillaInline : undefined);
|
|
387
432
|
const outline = {
|
|
388
433
|
page: convertPage(pageLines, convertCarried, presets.page),
|
|
389
434
|
blocks: blocks.map(block => ({
|
|
390
435
|
...block,
|
|
391
|
-
content: convertContent(withBracedScripts(
|
|
436
|
+
content: convertContent(withBracedScripts(inline ? inlineTitle(block) : block.content), convertCarried, presets.block)
|
|
392
437
|
}))
|
|
393
438
|
};
|
|
394
439
|
return vanilla
|
|
@@ -422,7 +467,7 @@ export function markdownOutlineToOrg(markdown, convert, presets, context) {
|
|
|
422
467
|
...block,
|
|
423
468
|
content: convertMarkdownContent(block.content, convertCarried, presets.block).filter(line => line !== BRACED_SCRIPTS_LINE)
|
|
424
469
|
};
|
|
425
|
-
return
|
|
470
|
+
return context.side === "input" ? converted : dropTitleEscape(converted);
|
|
426
471
|
})
|
|
427
|
-
}, writeOrgBlock);
|
|
472
|
+
}, block => writeOrgBlock(block, context.side === "input"));
|
|
428
473
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remigius42/morg",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Bidirectional Markdown ↔ Org-mode converter with round-trip convergence",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"org-mode",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"bin": {
|
|
33
|
-
"morg": "
|
|
33
|
+
"morg": "dist/cli.js"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist"
|