@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,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,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
|
+
}
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import type { Parent } from "unist";
|
|
2
|
+
import { type Node } from "./render.js";
|
|
2
3
|
/**
|
|
3
4
|
* md→org: a paragraph line org would read as line syntax (an escaped
|
|
4
5
|
* `1\.` or `\*`, or a lazy continuation line) gets a leading zero-width
|
|
5
6
|
* space, or it would turn into a list item, headline, comment or table.
|
|
6
7
|
*/
|
|
7
8
|
export declare function escapeLineSyntax(tree: Parent): void;
|
|
9
|
+
/**
|
|
10
|
+
* Whether org reads a text as just one passthrough element.
|
|
11
|
+
* @param text The org text.
|
|
12
|
+
* @returns Whether it is one block, drawer, keyword … org→md writes as
|
|
13
|
+
* its org text.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readsAsPassthrough(text: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* md→org: whether a node is a paragraph org→md wrote as the org text of
|
|
18
|
+
* one passthrough element, which goes back as it is.
|
|
19
|
+
* @param node The node.
|
|
20
|
+
* @param index Its index in its parent.
|
|
21
|
+
* @param parent Its parent.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isPassthroughParagraph(node: Node | Parent, index: number, parent: Parent | undefined): boolean;
|
|
8
24
|
/**
|
|
9
25
|
* org→md: drops the line-start zero-width spaces `escapeLineSyntax`
|
|
10
26
|
* inserts.
|
package/dist/core/lineSyntax.js
CHANGED
|
@@ -135,10 +135,33 @@ function isPassthrough(starts) {
|
|
|
135
135
|
if (!first || !MAY_BE_LINE_SYNTAX_RE.test(first.line)) {
|
|
136
136
|
return false;
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
return readsAsPassthrough(starts.map(({ line }) => line).join("\n"));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Whether org reads a text as just one passthrough element.
|
|
142
|
+
* @param text The org text.
|
|
143
|
+
* @returns Whether it is one block, drawer, keyword … org→md writes as
|
|
144
|
+
* its org text.
|
|
145
|
+
*/
|
|
146
|
+
export function readsAsPassthrough(text) {
|
|
147
|
+
const [only, ...rest] = tryParse(`${text}\n`)?.children ?? [];
|
|
140
148
|
return !rest.length && PASSTHROUGH_TYPES.has(only?.type ?? "");
|
|
141
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* md→org: whether a node is a paragraph org→md wrote as the org text of
|
|
152
|
+
* one passthrough element, which goes back as it is.
|
|
153
|
+
* @param node The node.
|
|
154
|
+
* @param index Its index in its parent.
|
|
155
|
+
* @param parent Its parent.
|
|
156
|
+
*/
|
|
157
|
+
export function isPassthroughParagraph(node, index, parent) {
|
|
158
|
+
if (node.type !== "paragraph") {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const context = contextOf(node, index, parent);
|
|
162
|
+
const children = node.children;
|
|
163
|
+
return (!context.afterBullet && isPassthrough(lineStarts(children, context).starts));
|
|
164
|
+
}
|
|
142
165
|
// line syntax spanning lines (`#+begin_src`…`#+end_src`, a drawer) or
|
|
143
166
|
// depending on context (planning below a headline) shows only in the
|
|
144
167
|
// whole content as org reads it; each round escapes the first line of
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { RootContent, PhrasingContent } from "mdast";
|
|
2
2
|
import type { ElementType } from "uniorg";
|
|
3
3
|
import { type TransformContext } from "./context.js";
|
|
4
|
-
export declare function frontmatterToKeywords(yamlValue: string): ElementType[];
|
|
5
4
|
export declare function transformMdastTable(ctx: TransformContext, node: Extract<RootContent, {
|
|
6
5
|
type: "table";
|
|
7
6
|
}>): ElementType;
|
|
@@ -1,35 +1,7 @@
|
|
|
1
1
|
import { toString } from "orgast-util-to-string";
|
|
2
|
-
import { parse as parseYaml } from "yaml";
|
|
3
2
|
import { mdismEnabled } from "./context.js";
|
|
4
3
|
import { transformPhrasingChildren } from "./phrasing.js";
|
|
5
|
-
|
|
6
|
-
// scalar is JSON-encoded and restored by JSON.parse on the way back
|
|
7
|
-
function keywordValue(value) {
|
|
8
|
-
if (value !== null && typeof value === "object") {
|
|
9
|
-
return JSON.stringify(value);
|
|
10
|
-
}
|
|
11
|
-
const text = String(value);
|
|
12
|
-
return text.includes("\n") ? JSON.stringify(text) : text;
|
|
13
|
-
}
|
|
14
|
-
// frontmatter entries become #+KEY: value keywords; scalar values as-is,
|
|
15
|
-
// structured values JSON-encoded on a single line (see ADR 0002)
|
|
16
|
-
export function frontmatterToKeywords(yamlValue) {
|
|
17
|
-
const data = parseYaml(yamlValue);
|
|
18
|
-
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
return Object.entries(data).flatMap(([key, value]) => {
|
|
22
|
-
// a sequence becomes repeated keywords -- org's own way of carrying
|
|
23
|
-
// several values for one key, and what they read back as
|
|
24
|
-
const values = Array.isArray(value) && value.length ? value : [value];
|
|
25
|
-
return values.map(item => ({
|
|
26
|
-
type: "keyword",
|
|
27
|
-
affiliated: {},
|
|
28
|
-
key: key.toUpperCase(),
|
|
29
|
-
value: keywordValue(item)
|
|
30
|
-
}));
|
|
31
|
-
});
|
|
32
|
-
}
|
|
4
|
+
import { KEYWORD_NAME } from "../frontmatterBlock.js";
|
|
33
5
|
export function transformMdastTable(ctx, node) {
|
|
34
6
|
const [headerRow, ...bodyRows] = node.children;
|
|
35
7
|
// GFM column alignment maps to an org alignment cookie row
|
|
@@ -153,8 +125,11 @@ export function transformMdastCode(node) {
|
|
|
153
125
|
value: `${node.value}\n`
|
|
154
126
|
};
|
|
155
127
|
}
|
|
128
|
+
// the fence's meta is the block's switches and header arguments,
|
|
129
|
+
// which uniorg-stringify writes after the language
|
|
130
|
+
const language = [node.lang, node.meta].filter(Boolean).join(" ");
|
|
156
131
|
return (node.lang
|
|
157
|
-
? { type: "src-block", language
|
|
132
|
+
? { type: "src-block", language, value: node.value }
|
|
158
133
|
: {
|
|
159
134
|
type: "example-block",
|
|
160
135
|
value: node.value
|
|
@@ -196,7 +171,7 @@ export function transformMdastHeading(ctx, node) {
|
|
|
196
171
|
children: transformPhrasingChildren(ctx, node.children)
|
|
197
172
|
};
|
|
198
173
|
}
|
|
199
|
-
const KEYWORD_LINE_RE =
|
|
174
|
+
const KEYWORD_LINE_RE = new RegExp(String.raw `^#\+${KEYWORD_NAME}: `);
|
|
200
175
|
export function keywordOnlyLines(node) {
|
|
201
176
|
if (!node.children.every(child => child.type === "text")) {
|
|
202
177
|
return null;
|
|
@@ -204,7 +179,7 @@ export function keywordOnlyLines(node) {
|
|
|
204
179
|
const lines = node.children
|
|
205
180
|
.map(child => child.value)
|
|
206
181
|
.join("")
|
|
207
|
-
.split(
|
|
182
|
+
.split(/\r?\n/);
|
|
208
183
|
return lines.length && lines.every(line => KEYWORD_LINE_RE.test(line))
|
|
209
184
|
? lines
|
|
210
185
|
: null;
|
|
@@ -10,4 +10,10 @@ import type { MdastToUniorgOptions } from "./context.js";
|
|
|
10
10
|
* @returns The transformed uniorg AST.
|
|
11
11
|
*/
|
|
12
12
|
export declare function transformMdastToUniorgAst(mdast: MdastRoot, options?: MdastToUniorgOptions): OrgData;
|
|
13
|
+
/**
|
|
14
|
+
* md→org pipeline: like `transformMdastToUniorgAst`, but the frontmatter
|
|
15
|
+
* stays a `morg-frontmatter` node a preset can still take entries out
|
|
16
|
+
* of; `renderFileHeader` turns it into the block.
|
|
17
|
+
*/
|
|
18
|
+
export declare function transformMdastToUniorgDraft(mdast: MdastRoot, options?: MdastToUniorgOptions): OrgData;
|
|
13
19
|
export declare function transformMdastNodeToUniorgNode(ctx: TransformContext, node: RootContent | PhrasingContent): GreaterElementType | ElementType | Text | null;
|