@remigius42/morg 0.6.0 → 0.8.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 +23 -11
- package/dist/core/affiliated.d.ts +13 -0
- package/dist/core/affiliated.js +43 -0
- package/dist/core/backslashCommands.d.ts +10 -0
- package/dist/core/backslashCommands.js +43 -0
- package/dist/core/bracedScripts.d.ts +7 -0
- package/dist/core/bracedScripts.js +10 -0
- package/dist/core/frontmatterBlock.d.ts +89 -0
- package/dist/core/frontmatterBlock.js +448 -0
- package/dist/core/keyValueLines.d.ts +6 -0
- package/dist/core/keyValueLines.js +18 -0
- package/dist/core/lineSyntax.d.ts +16 -0
- package/dist/core/lineSyntax.js +25 -2
- package/dist/core/mdastToUniorg/blocks.d.ts +0 -1
- package/dist/core/mdastToUniorg/blocks.js +7 -32
- package/dist/core/mdastToUniorg/index.d.ts +6 -0
- package/dist/core/mdastToUniorg/index.js +72 -7
- package/dist/core/passthroughSource.d.ts +21 -0
- package/dist/core/passthroughSource.js +94 -0
- package/dist/core/tablePipes.d.ts +12 -3
- package/dist/core/tablePipes.js +38 -5
- package/dist/core/uniorgToMdast/elements.js +6 -1
- package/dist/core/uniorgToMdast/index.d.ts +3 -2
- package/dist/core/uniorgToMdast/index.js +100 -31
- package/dist/core/uniorgToMdast/lists.js +1 -1
- package/dist/core/uniorgToMdast/objects.js +6 -0
- package/dist/core/uniorgToMdast/shared.d.ts +5 -0
- package/dist/core/uniorgToMdast/shared.js +9 -13
- package/dist/index.d.ts +1 -2
- package/dist/markdownToOrg.js +41 -25
- package/dist/orgToMarkdown.js +18 -2
- package/dist/presets/logseq.d.ts +3 -22
- package/dist/presets/logseq.js +269 -186
- package/dist/presets/logseqOutline.d.ts +22 -0
- package/dist/presets/logseqOutline.js +251 -0
- package/dist/presets/types.d.ts +12 -1
- package/package.json +1 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { consumesBracedScripts } from "../core/bracedScripts.js";
|
|
2
|
+
import { isDrawerStart, isOrgBlockStart, orgElementEnd } from "../core/passthroughSource.js";
|
|
3
|
+
// Logseq stores a page as an outline of blocks, each block a content
|
|
4
|
+
// string it parses on its own: org writes a block as its level's stars,
|
|
5
|
+
// a space and the content (an empty block as the bare stars), Markdown
|
|
6
|
+
// as a `- ` bullet indented one tab per level, continuation lines two
|
|
7
|
+
// spaces further in. Both are converted block by block, so a block's
|
|
8
|
+
// content (a code block, a table, lines that run on) is one fragment.
|
|
9
|
+
const ORG_BLOCK_RE = /^(\*+)(?: (.*))?$/;
|
|
10
|
+
const MD_BLOCK_RE = /^(\t*)-(?: (.*))?$/;
|
|
11
|
+
const PLANNING_RE = /^(?:SCHEDULED|DEADLINE): /;
|
|
12
|
+
const ORG_PROPERTY_RE = /^:([^\s:]+):(?: (.*))?$/;
|
|
13
|
+
const MD_PROPERTY_RE = /^([\w.-]+)::(?: (.*))?$/;
|
|
14
|
+
// a repeated task's log line, which Logseq bullets per format
|
|
15
|
+
const STATE_LINE_RE = /^[-*] (?=State ")/;
|
|
16
|
+
const MD_HEADING_RE = /^(#{1,6})(?: (.*))?$/;
|
|
17
|
+
const FENCE_RE = /^\s*(?:```|~~~)/;
|
|
18
|
+
// md→org adds it for the text's bare `_` and `^`, which a block's
|
|
19
|
+
// content holds as Logseq writes it
|
|
20
|
+
const BRACED_SCRIPTS_LINE = "#+OPTIONS: ^:{}";
|
|
21
|
+
function logbook(lines, bullet) {
|
|
22
|
+
return lines[0] === ":LOGBOOK:"
|
|
23
|
+
? lines.map(line => line.replace(STATE_LINE_RE, `${bullet} `))
|
|
24
|
+
: lines;
|
|
25
|
+
}
|
|
26
|
+
function splitBlocks(lines, blockLevel) {
|
|
27
|
+
const page = [];
|
|
28
|
+
const blocks = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const start = blockLevel(line);
|
|
31
|
+
if (start) {
|
|
32
|
+
blocks.push({ level: start[0], lines: [start[1]] });
|
|
33
|
+
}
|
|
34
|
+
else if (blocks.length) {
|
|
35
|
+
blocks[blocks.length - 1]?.lines.push(line);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
page.push(line);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { page, blocks };
|
|
42
|
+
}
|
|
43
|
+
// a block's leading planning lines and drawers, which Logseq writes
|
|
44
|
+
// below the content's first line in either format
|
|
45
|
+
function takeMeta(lines, isMeta) {
|
|
46
|
+
const meta = [];
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < lines.length) {
|
|
49
|
+
const line = lines[i] ?? "";
|
|
50
|
+
if (isDrawerStart(line)) {
|
|
51
|
+
const end = orgElementEnd(lines, i);
|
|
52
|
+
if (end === -1) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
meta.push(lines.slice(i, end + 1));
|
|
56
|
+
i = end + 1;
|
|
57
|
+
}
|
|
58
|
+
else if (PLANNING_RE.test(line) || isMeta(line)) {
|
|
59
|
+
meta.push([line]);
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { meta, body: lines.slice(i) };
|
|
67
|
+
}
|
|
68
|
+
// the one org block whose content Logseq md writes as Markdown markup
|
|
69
|
+
// (its <quote command), and org→md writes as a md quote
|
|
70
|
+
const QUOTE_BLOCK_RE = /^#\+begin_quote\b/i;
|
|
71
|
+
function convertOrgBlock(block, convert, preset) {
|
|
72
|
+
if (!QUOTE_BLOCK_RE.test(block[0] ?? "")) {
|
|
73
|
+
return block;
|
|
74
|
+
}
|
|
75
|
+
const content = convertContent(block.slice(1, -1), convert, preset);
|
|
76
|
+
return [block[0] ?? "", ...content, block.at(-1) ?? ""];
|
|
77
|
+
}
|
|
78
|
+
// md→org: a block's org blocks stay as written, as org→md writes them,
|
|
79
|
+
// but for a quote's content; the text around them converts
|
|
80
|
+
function convertMarkdownContent(lines, convert, preset) {
|
|
81
|
+
const result = [];
|
|
82
|
+
let text = [];
|
|
83
|
+
let fenced = false;
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
fenced = FENCE_RE.test(lines[i] ?? "") ? !fenced : fenced;
|
|
86
|
+
const end = fenced || !isOrgBlockStart(lines[i] ?? "") ? -1 : orgElementEnd(lines, i);
|
|
87
|
+
if (end === -1) {
|
|
88
|
+
text.push(lines[i] ?? "");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
result.push(...convertContent(text, convert, preset), ...convertOrgBlock(lines.slice(i, end + 1), convert, preset));
|
|
92
|
+
text = [];
|
|
93
|
+
i = end;
|
|
94
|
+
}
|
|
95
|
+
return [...result, ...convertContent(text, convert, preset)];
|
|
96
|
+
}
|
|
97
|
+
function convertContent(lines, convert, preset) {
|
|
98
|
+
const content = lines.join("\n");
|
|
99
|
+
return content.trim()
|
|
100
|
+
? convert(content, preset).replace(/\n+$/, "").split("\n")
|
|
101
|
+
: [];
|
|
102
|
+
}
|
|
103
|
+
function convertPage(lines, convert, preset) {
|
|
104
|
+
// Logseq ends the page properties with a blank line
|
|
105
|
+
return lines.some(line => line.trim())
|
|
106
|
+
? [`${convert(lines.join("\n"), preset).replace(/\n+$/, "")}\n`]
|
|
107
|
+
: [];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Converts a Logseq org page to Logseq Markdown, block by block.
|
|
111
|
+
* @param org The org page.
|
|
112
|
+
* @param convert The core's fragment converter.
|
|
113
|
+
* @param presets The presets for the page properties and for a block.
|
|
114
|
+
* @returns The Markdown page.
|
|
115
|
+
*/
|
|
116
|
+
export function orgOutlineToMarkdown(org, convert, presets) {
|
|
117
|
+
const { page, blocks } = splitBlocks(org.replace(/\r?\n$/, "").split(/\r?\n/), line => {
|
|
118
|
+
const match = ORG_BLOCK_RE.exec(line);
|
|
119
|
+
return match ? [match[1]?.length ?? 0, match[2] ?? ""] : null;
|
|
120
|
+
});
|
|
121
|
+
return [
|
|
122
|
+
...convertPage(page, convert, presets.page),
|
|
123
|
+
...blocks.map(block => orgBlockToMarkdown(block, convert, presets.block))
|
|
124
|
+
]
|
|
125
|
+
.join("\n")
|
|
126
|
+
.concat("\n");
|
|
127
|
+
}
|
|
128
|
+
// org→md reads a block's bare `_` and `^` as text, as md→org writes
|
|
129
|
+
// them without the setting that would say so
|
|
130
|
+
function withBracedScripts(lines) {
|
|
131
|
+
const braced = [BRACED_SCRIPTS_LINE, ...lines];
|
|
132
|
+
return consumesBracedScripts(braced.join("\n")) ? braced : lines;
|
|
133
|
+
}
|
|
134
|
+
function orgBlockToMarkdown(block, convert, preset) {
|
|
135
|
+
const [first = "", ...rest] = block.lines;
|
|
136
|
+
// content that starts with the block's properties puts their drawer
|
|
137
|
+
// on the headline line; its heading level is then a property too
|
|
138
|
+
const metaFirst = isDrawerStart(first);
|
|
139
|
+
const { meta, body } = takeMeta(metaFirst ? block.lines : rest, () => false);
|
|
140
|
+
let heading = "";
|
|
141
|
+
const metaLines = meta.flatMap(lines => {
|
|
142
|
+
if (lines[0] !== ":PROPERTIES:") {
|
|
143
|
+
return logbook(lines, "*");
|
|
144
|
+
}
|
|
145
|
+
return lines.slice(1, -1).flatMap(line => {
|
|
146
|
+
const [, key = "", value = ""] = ORG_PROPERTY_RE.exec(line) ?? [];
|
|
147
|
+
if (!metaFirst && key === "heading" && /^[1-6]$/.test(value)) {
|
|
148
|
+
heading = "#".repeat(Number(value));
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
return [`${key}::${value ? ` ${value}` : ""}`];
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
const content = convertContent(withBracedScripts(metaFirst ? body : [first, ...body]), convert, preset);
|
|
155
|
+
const [title = "", ...more] = arrange(metaFirst, metaLines, content);
|
|
156
|
+
const head = [heading, title].filter(Boolean).join(" ");
|
|
157
|
+
const indent = "\t".repeat(block.level - 1);
|
|
158
|
+
return [
|
|
159
|
+
`${indent}-${head ? ` ${head}` : ""}`,
|
|
160
|
+
...more.map(line => `${indent} ${line}`)
|
|
161
|
+
].join("\n");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Converts a Logseq Markdown page to Logseq org, block by block.
|
|
165
|
+
* @param markdown The Markdown page.
|
|
166
|
+
* @param convert The core's fragment converter.
|
|
167
|
+
* @param presets The presets for the page properties and for a block.
|
|
168
|
+
* @returns The org page.
|
|
169
|
+
*/
|
|
170
|
+
export function markdownOutlineToOrg(markdown, convert, presets) {
|
|
171
|
+
const lines = markdown.replace(/\r?\n$/, "").split(/\r?\n/);
|
|
172
|
+
// a leading frontmatter is page content, its `- ` lines yaml items
|
|
173
|
+
const frontmatter = lines.slice(0, lines[0] === "---" ? lines.indexOf("---", 1) + 1 : 0);
|
|
174
|
+
let fenced = false;
|
|
175
|
+
const { page, blocks } = splitBlocks(lines.slice(frontmatter.length), line => {
|
|
176
|
+
const match = MD_BLOCK_RE.exec(line);
|
|
177
|
+
// a bullet starts a block, which may open a fence of its own
|
|
178
|
+
fenced = (match ? false : fenced) !== FENCE_RE.test(match?.[2] ?? line);
|
|
179
|
+
if (match) {
|
|
180
|
+
return [(match[1]?.length ?? 0) + 1, match[2] ?? ""];
|
|
181
|
+
}
|
|
182
|
+
// a heading outside the bullets is a top-level block, as Logseq
|
|
183
|
+
// writes a page's first block if it is a heading
|
|
184
|
+
return !fenced && MD_HEADING_RE.test(line) ? [1, line] : null;
|
|
185
|
+
});
|
|
186
|
+
page.unshift(...frontmatter);
|
|
187
|
+
return [
|
|
188
|
+
...convertPage(page, convert, presets.page),
|
|
189
|
+
...blocks.map(block => mdBlockToOrg(block, convert, presets.block))
|
|
190
|
+
]
|
|
191
|
+
.join("\n")
|
|
192
|
+
.concat("\n");
|
|
193
|
+
}
|
|
194
|
+
// a continuation line sits two spaces inside its bullet
|
|
195
|
+
function dedent(line, level) {
|
|
196
|
+
const indent = `${"\t".repeat(level - 1)} `;
|
|
197
|
+
return line.startsWith(indent)
|
|
198
|
+
? line.slice(indent.length)
|
|
199
|
+
: line.trim()
|
|
200
|
+
? line
|
|
201
|
+
: "";
|
|
202
|
+
}
|
|
203
|
+
// the md meta lines in org: `key::` lines become the property drawer,
|
|
204
|
+
// where the first of them was, or below the planning lines
|
|
205
|
+
function orgMetaLines(meta, heading) {
|
|
206
|
+
const properties = heading ? [`:heading: ${heading}`] : [];
|
|
207
|
+
const lines = [];
|
|
208
|
+
for (const group of meta) {
|
|
209
|
+
const property = MD_PROPERTY_RE.exec(group[0] ?? "");
|
|
210
|
+
if (!property) {
|
|
211
|
+
lines.push(...logbook(group, "-"));
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!lines.includes(null)) {
|
|
215
|
+
lines.push(null);
|
|
216
|
+
}
|
|
217
|
+
properties.push(`:${property[1]}:${property[2] ? ` ${property[2]}` : ""}`);
|
|
218
|
+
}
|
|
219
|
+
if (properties.length && !lines.includes(null)) {
|
|
220
|
+
lines.splice(lines.filter(line => PLANNING_RE.test(line ?? "")).length, 0, null);
|
|
221
|
+
}
|
|
222
|
+
const drawer = [":PROPERTIES:", ...properties, ":END:"];
|
|
223
|
+
return lines.flatMap(line => (line === null ? drawer : [line]));
|
|
224
|
+
}
|
|
225
|
+
// a block's content's first line, its meta lines, then the rest of the
|
|
226
|
+
// content; content that starts with the meta lines keeps them first
|
|
227
|
+
function arrange(metaFirst, meta, content) {
|
|
228
|
+
return metaFirst
|
|
229
|
+
? [...meta, ...content]
|
|
230
|
+
: [content[0] ?? "", ...meta, ...content.slice(1)];
|
|
231
|
+
}
|
|
232
|
+
// `## title`: the heading level and the title
|
|
233
|
+
function mdHeading(line) {
|
|
234
|
+
const match = MD_HEADING_RE.exec(line);
|
|
235
|
+
return match ? [match[1]?.length ?? 0, match[2] ?? ""] : [0, line];
|
|
236
|
+
}
|
|
237
|
+
function mdBlockToOrg(block, convert, preset) {
|
|
238
|
+
const [first = "", ...rest] = block.lines;
|
|
239
|
+
const lines = [first, ...rest.map(line => dedent(line, block.level))];
|
|
240
|
+
// content that starts with properties: their drawer opens on the
|
|
241
|
+
// headline line
|
|
242
|
+
const metaFirst = MD_PROPERTY_RE.test(first);
|
|
243
|
+
const [heading, titleLine] = metaFirst ? [0, ""] : mdHeading(first);
|
|
244
|
+
const { meta, body } = takeMeta(metaFirst ? lines : lines.slice(1), line => MD_PROPERTY_RE.test(line));
|
|
245
|
+
const content = convertMarkdownContent(metaFirst ? body : [titleLine, ...body], convert, preset).filter(line => line !== BRACED_SCRIPTS_LINE);
|
|
246
|
+
const [title = "", ...more] = arrange(metaFirst, orgMetaLines(meta, heading), content);
|
|
247
|
+
return [
|
|
248
|
+
`${"*".repeat(block.level)}${title ? ` ${title}` : ""}`,
|
|
249
|
+
...more
|
|
250
|
+
].join("\n");
|
|
251
|
+
}
|
package/dist/presets/types.d.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
import type { Root as MdastRoot } from "mdast";
|
|
1
2
|
import type { OrgData } from "uniorg";
|
|
2
3
|
/**
|
|
3
4
|
* A dialect preset: transforms applied on top of the dialect-agnostic core.
|
|
4
|
-
* `
|
|
5
|
+
* `applyToMdast` runs in the md→org pipeline before the generic
|
|
6
|
+
* transform, with the Markdown source at hand; `applyToUniorg` runs in the md→org pipeline after the generic transform;
|
|
5
7
|
* `extractFromUniorg` runs in the org→md pipeline before the generic
|
|
6
8
|
* transform, normalizing dialect conventions back to generic uniorg.
|
|
9
|
+
* `convertOrg` and `convertMarkdown` take a whole conversion over, for a
|
|
10
|
+
* dialect whose documents are no single org or Markdown document (an
|
|
11
|
+
* outline of blocks, each its own fragment); `convert` runs the core on
|
|
12
|
+
* a fragment, with the given preset's hooks.
|
|
7
13
|
*/
|
|
8
14
|
export interface Preset {
|
|
9
15
|
name: string;
|
|
16
|
+
applyToMdast?: (mdast: MdastRoot, markdown: string) => void;
|
|
10
17
|
applyToUniorg?: (uniorgAst: OrgData) => OrgData;
|
|
11
18
|
extractFromUniorg?: (uniorgAst: OrgData) => OrgData;
|
|
19
|
+
convertOrg?: (org: string, convert: FragmentConverter) => string;
|
|
20
|
+
convertMarkdown?: (markdown: string, convert: FragmentConverter) => string;
|
|
12
21
|
}
|
|
22
|
+
/** Converts a fragment with the core and the given preset's AST hooks. */
|
|
23
|
+
export type FragmentConverter = (fragment: string, preset?: Preset) => string;
|