@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.
Files changed (37) hide show
  1. package/README.md +23 -11
  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 +7 -0
  7. package/dist/core/bracedScripts.js +10 -0
  8. package/dist/core/frontmatterBlock.d.ts +89 -0
  9. package/dist/core/frontmatterBlock.js +448 -0
  10. package/dist/core/keyValueLines.d.ts +6 -0
  11. package/dist/core/keyValueLines.js +18 -0
  12. package/dist/core/lineSyntax.d.ts +16 -0
  13. package/dist/core/lineSyntax.js +25 -2
  14. package/dist/core/mdastToUniorg/blocks.d.ts +0 -1
  15. package/dist/core/mdastToUniorg/blocks.js +7 -32
  16. package/dist/core/mdastToUniorg/index.d.ts +6 -0
  17. package/dist/core/mdastToUniorg/index.js +72 -7
  18. package/dist/core/passthroughSource.d.ts +21 -0
  19. package/dist/core/passthroughSource.js +94 -0
  20. package/dist/core/tablePipes.d.ts +12 -3
  21. package/dist/core/tablePipes.js +38 -5
  22. package/dist/core/uniorgToMdast/elements.js +6 -1
  23. package/dist/core/uniorgToMdast/index.d.ts +3 -2
  24. package/dist/core/uniorgToMdast/index.js +100 -31
  25. package/dist/core/uniorgToMdast/lists.js +1 -1
  26. package/dist/core/uniorgToMdast/objects.js +6 -0
  27. package/dist/core/uniorgToMdast/shared.d.ts +5 -0
  28. package/dist/core/uniorgToMdast/shared.js +9 -13
  29. package/dist/index.d.ts +1 -2
  30. package/dist/markdownToOrg.js +41 -25
  31. package/dist/orgToMarkdown.js +18 -2
  32. package/dist/presets/logseq.d.ts +3 -22
  33. package/dist/presets/logseq.js +269 -186
  34. package/dist/presets/logseqOutline.d.ts +22 -0
  35. package/dist/presets/logseqOutline.js +251 -0
  36. package/dist/presets/types.d.ts +12 -1
  37. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { visit } from "unist-util-visit";
2
2
  import { warn } from "./context.js";
3
- import { frontmatterToKeywords, keywordOnlyLines, transformMdastCode, transformMdastHeading, transformMdastHtml, transformMdastMath, transformMdastTable } from "./blocks.js";
3
+ import { fitsKeywordLine, fitsPropertyLine, markModeLine, renderFileHeader, takeMorgEntry, takesMorgEntries } from "../frontmatterBlock.js";
4
+ import { keywordOnlyLines, transformMdastCode, transformMdastHeading, transformMdastHtml, transformMdastMath, transformMdastTable } from "./blocks.js";
4
5
  import { transformMdastList } from "./lists.js";
5
6
  import { transformPhrasingChildren } from "./phrasing.js";
6
7
  /**
@@ -10,6 +11,16 @@ import { transformPhrasingChildren } from "./phrasing.js";
10
11
  * @returns The transformed uniorg AST.
11
12
  */
12
13
  export function transformMdastToUniorgAst(mdast, options = {}) {
14
+ const orgAst = transformMdastToUniorgDraft(mdast, options);
15
+ renderFileHeader(orgAst);
16
+ return orgAst;
17
+ }
18
+ /**
19
+ * md→org pipeline: like `transformMdastToUniorgAst`, but the frontmatter
20
+ * stays a `morg-frontmatter` node a preset can still take entries out
21
+ * of; `renderFileHeader` turns it into the block.
22
+ */
23
+ export function transformMdastToUniorgDraft(mdast, options = {}) {
13
24
  const ctx = { options, definitions: new Map() };
14
25
  visit(mdast, "definition", (definition) => {
15
26
  ctx.definitions.set(definition.identifier, {
@@ -17,13 +28,20 @@ export function transformMdastToUniorgAst(mdast, options = {}) {
17
28
  ...(definition.title != null && { title: definition.title })
18
29
  });
19
30
  });
31
+ const firstBody = mdast.children.find(child => child.type !== "yaml");
20
32
  const children = mdast.children
21
- .flatMap(child =>
22
- // frontmatter maps to org keywords, a native construct (one mdast
23
- // node fans out to one keyword per entry)
24
- child.type === "yaml"
25
- ? frontmatterToKeywords(child.value)
26
- : [transformMdastNodeToUniorgNode(ctx, child)])
33
+ .flatMap(child => {
34
+ // frontmatter is passive data, org keywords can act; it travels
35
+ // inert in a marked comment block (ADR 0005)
36
+ if (child.type === "yaml") {
37
+ return transformFrontmatter(child.value);
38
+ }
39
+ const node = transformMdastNodeToUniorgNode(ctx, child);
40
+ if (child === firstBody) {
41
+ markModeLine(node);
42
+ }
43
+ return [node];
44
+ })
27
45
  .filter(Boolean);
28
46
  const orgAst = {
29
47
  type: "org-data",
@@ -101,3 +119,50 @@ function quotedHeadingAsText(ctx, node) {
101
119
  warn(ctx, "heading inside a blockquote became text");
102
120
  return { type: "paragraph", children: node.children };
103
121
  }
122
+ // morg's own entries, both or neither: one left in the YAML would keep
123
+ // the other from joining it again on the way back
124
+ function takeMorgEntries(yaml) {
125
+ // most frontmatter holds neither: no need to parse it
126
+ if (!yaml.includes("morg_")) {
127
+ return { keywords: [], properties: [], yaml };
128
+ }
129
+ const taken = takeMorgEntry(yaml, "morg_keywords", fitsKeywordLine);
130
+ const rest = takeMorgEntry(taken.yaml, "morg_properties", fitsPropertyLine);
131
+ return takesMorgEntries(rest.yaml)
132
+ ? { keywords: taken.keywords, properties: rest.keywords, yaml: rest.yaml }
133
+ : { keywords: [], properties: [], yaml };
134
+ }
135
+ function keywordNodes(keywords) {
136
+ return keywords.map(([key, value]) => ({
137
+ type: "keyword",
138
+ affiliated: {},
139
+ key,
140
+ value
141
+ }));
142
+ }
143
+ // org's own keywords go back to keywords; the rest of the frontmatter
144
+ // stays inert data in the block
145
+ function transformFrontmatter(value) {
146
+ // in the org file's line endings, which are LF
147
+ const lf = value.replaceAll("\r\n", "\n");
148
+ const { keywords, properties, yaml } = takeMorgEntries(lf);
149
+ const drawer = properties.length
150
+ ? [
151
+ {
152
+ type: "property-drawer",
153
+ children: properties.map(([key, propertyValue]) => ({
154
+ type: "node-property",
155
+ key,
156
+ value: propertyValue
157
+ }))
158
+ }
159
+ ]
160
+ : [];
161
+ const nodes = keywordNodes(keywords);
162
+ // an empty frontmatter stays a block
163
+ if (yaml || !(keywords.length || properties.length)) {
164
+ const node = { type: "morg-frontmatter", yaml };
165
+ nodes.push(node);
166
+ }
167
+ return [...drawer, ...nodes];
168
+ }
@@ -0,0 +1,21 @@
1
+ import type { Root } from "mdast";
2
+ /** Whether a line opens an org block (`#+begin_name`). */
3
+ export declare function isOrgBlockStart(line: string): boolean;
4
+ /** Whether a line opens an org drawer (`:NAME:`). */
5
+ export declare function isDrawerStart(line: string): boolean;
6
+ /**
7
+ * The line that ends the org block or drawer a line opens.
8
+ * @param lines The lines.
9
+ * @param start The index of the opening line.
10
+ * @returns The index of the `#+end_name` or `:END:` line, or -1.
11
+ */
12
+ export declare function orgElementEnd(lines: string[], start: number): number;
13
+ /**
14
+ * md→org: a passthrough element's org text may hold lines Markdown reads
15
+ * as syntax of its own (a blank line and an indented one, a `#`, `-`
16
+ * or `>` line); the top-level nodes it parsed into become one paragraph
17
+ * of the source text again, which goes back to org as it is.
18
+ * @param mdast The parsed Markdown.
19
+ * @param markdown Its source.
20
+ */
21
+ export declare function keepPassthroughSource(mdast: Root, markdown: string): void;
@@ -0,0 +1,94 @@
1
+ import { readsAsPassthrough } from "./lineSyntax.js";
2
+ // the first line of an org block or drawer
3
+ const BLOCK_START_RE = /^#\+begin_(\S+)/i;
4
+ const DRAWER_START_RE = /^:[\w-]+:$/;
5
+ /** Whether a line opens an org block (`#+begin_name`). */
6
+ export function isOrgBlockStart(line) {
7
+ return BLOCK_START_RE.test(line);
8
+ }
9
+ /** Whether a line opens an org drawer (`:NAME:`). */
10
+ export function isDrawerStart(line) {
11
+ return DRAWER_START_RE.test(line);
12
+ }
13
+ /**
14
+ * The line that ends the org block or drawer a line opens.
15
+ * @param lines The lines.
16
+ * @param start The index of the opening line.
17
+ * @returns The index of the `#+end_name` or `:END:` line, or -1.
18
+ */
19
+ export function orgElementEnd(lines, start) {
20
+ const end = endPattern(lines[start] ?? "");
21
+ return end ? lines.findIndex((other, i) => i > start && end.test(other)) : -1;
22
+ }
23
+ // the pattern of the line that ends the block or drawer `line` opens
24
+ function endPattern(line) {
25
+ const block = BLOCK_START_RE.exec(line)?.[1];
26
+ if (block) {
27
+ return new RegExp(`^#\\+end_${block.replace(/\W/g, "\\$&")}\\s*$`, "i");
28
+ }
29
+ return DRAWER_START_RE.test(line) ? /^:end:\s*$/i : null;
30
+ }
31
+ // the text from `offset` through the line that ends the block or
32
+ // drawer opening there, read line by line rather than splitting the
33
+ // rest of the document (a page may hold thousands of drawers)
34
+ function elementSource(markdown, offset) {
35
+ const lineEnd = (from) => {
36
+ const index = markdown.indexOf("\n", from);
37
+ return index === -1 ? markdown.length : index;
38
+ };
39
+ let end = lineEnd(offset);
40
+ const pattern = endPattern(markdown.slice(offset, end));
41
+ while (pattern && end < markdown.length) {
42
+ const next = lineEnd(end + 1);
43
+ if (pattern.test(markdown.slice(end + 1, next))) {
44
+ return markdown.slice(offset, next);
45
+ }
46
+ end = next;
47
+ }
48
+ return null;
49
+ }
50
+ // the source offset where the passthrough element starting at a node
51
+ // ends, or -1
52
+ function passthroughEnd(node, markdown) {
53
+ const start = node.position?.start;
54
+ if (start?.column !== 1 || start.offset === undefined) {
55
+ return -1;
56
+ }
57
+ const source = elementSource(markdown, start.offset);
58
+ return source !== null && readsAsPassthrough(source)
59
+ ? start.offset + source.length
60
+ : -1;
61
+ }
62
+ // the index of the last node ending at `end`, from `i` on, or -1
63
+ function lastNodeAt(nodes, i, end) {
64
+ let j = i;
65
+ while ((nodes[j]?.position?.end.offset ?? end) < end) {
66
+ j++;
67
+ }
68
+ return nodes[j]?.position?.end.offset === end ? j : -1;
69
+ }
70
+ /**
71
+ * md→org: a passthrough element's org text may hold lines Markdown reads
72
+ * as syntax of its own (a blank line and an indented one, a `#`, `-`
73
+ * or `>` line); the top-level nodes it parsed into become one paragraph
74
+ * of the source text again, which goes back to org as it is.
75
+ * @param mdast The parsed Markdown.
76
+ * @param markdown Its source.
77
+ */
78
+ export function keepPassthroughSource(mdast, markdown) {
79
+ const children = [];
80
+ const nodes = mdast.children;
81
+ for (let i = 0; i < nodes.length; i++) {
82
+ const node = nodes[i];
83
+ const end = passthroughEnd(node, markdown);
84
+ const last = end === -1 ? -1 : lastNodeAt(nodes, i, end);
85
+ if (last === -1) {
86
+ children.push(node);
87
+ continue;
88
+ }
89
+ const value = markdown.slice(node.position?.start.offset, end);
90
+ children.push({ type: "paragraph", children: [{ type: "text", value }] });
91
+ i = last;
92
+ }
93
+ mdast.children = children;
94
+ }
@@ -2,7 +2,16 @@ import type { Parent } from "unist";
2
2
  /**
3
3
  * md→org: a `|` in a table cell's text (md `\|`) would end the cell; org
4
4
  * has no escaped `|`, but renders the `\vert` entity as one, `{}` ending
5
- * its name before any letter. Runs after the preset, whose wikilink
6
- * aliases (`[[Page|alias]]`) are no text.
5
+ * its name before any letter. In code, where the entity would be
6
+ * literal, the lookalike stands in, which is reported. Runs after the
7
+ * preset, whose wikilink aliases (`[[Page|alias]]`) are no text.
8
+ * @param tree The document.
9
+ * @param onWarning Reports a lookalike written.
7
10
  */
8
- export declare function escapeTablePipes(tree: Parent): void;
11
+ export declare function escapeTablePipes(tree: Parent, onWarning?: (message: string) => void): void;
12
+ /**
13
+ * org→md: the lookalike in code in a table cell back to the `|` it
14
+ * stands in for.
15
+ * @param tree The document.
16
+ */
17
+ export declare function unescapeTablePipes(tree: Parent): void;
@@ -1,14 +1,47 @@
1
1
  import { visit } from "unist-util-visit";
2
+ // org has no escape inside ~code~ or =verbatim=: a `|` there would end
3
+ // the cell, so a lookalike (U+2223 DIVIDES) stands in for it
4
+ const LOOKALIKE = "∣";
2
5
  /**
3
6
  * md→org: a `|` in a table cell's text (md `\|`) would end the cell; org
4
7
  * has no escaped `|`, but renders the `\vert` entity as one, `{}` ending
5
- * its name before any letter. Runs after the preset, whose wikilink
6
- * aliases (`[[Page|alias]]`) are no text.
8
+ * its name before any letter. In code, where the entity would be
9
+ * literal, the lookalike stands in, which is reported. Runs after the
10
+ * preset, whose wikilink aliases (`[[Page|alias]]`) are no text.
11
+ * @param tree The document.
12
+ * @param onWarning Reports a lookalike written.
7
13
  */
8
- export function escapeTablePipes(tree) {
14
+ export function escapeTablePipes(tree, onWarning) {
15
+ let replaced = false;
9
16
  visit(tree, "table-cell", (cell) => {
10
- visit(cell, "text", (node) => {
11
- node.value = node.value?.replaceAll("|", "\\vert{}");
17
+ visit(cell, (node) => {
18
+ if (node.type === "text") {
19
+ node.value = node.value?.replaceAll("|", "\\vert{}");
20
+ }
21
+ else if (isCode(node) && node.value?.includes("|")) {
22
+ node.value = node.value.replaceAll("|", LOOKALIKE);
23
+ replaced = true;
24
+ }
12
25
  });
13
26
  });
27
+ if (replaced) {
28
+ onWarning?.("a | in code in a table cell becomes ∣ (U+2223) in org");
29
+ }
30
+ }
31
+ /**
32
+ * org→md: the lookalike in code in a table cell back to the `|` it
33
+ * stands in for.
34
+ * @param tree The document.
35
+ */
36
+ export function unescapeTablePipes(tree) {
37
+ visit(tree, "table-cell", (cell) => {
38
+ visit(cell, (node) => {
39
+ if (isCode(node)) {
40
+ node.value = node.value?.replaceAll(LOOKALIKE, "|");
41
+ }
42
+ });
43
+ });
44
+ }
45
+ function isCode(node) {
46
+ return node.type === "code" || node.type === "verbatim";
14
47
  }
@@ -180,7 +180,7 @@ function transformPropertyDrawer(ctx, node) {
180
180
  }
181
181
  const isms = (node.children || [])
182
182
  .filter(child => child.type === "node-property")
183
- .map(property => `${property.key}:: ${property.value}`);
183
+ .map(({ key, value }) => (value ? `${key}:: ${value}` : `${key}::`));
184
184
  return isms.length ? keyValueParagraph(isms) : null;
185
185
  }
186
186
  function transformParagraph(ctx, node) {
@@ -253,9 +253,14 @@ function transformQuoteBlock(ctx, node) {
253
253
  };
254
254
  }
255
255
  function transformSrcBlock(node) {
256
+ // switches and header arguments (`-n :results output`) are the
257
+ // fence's meta, after the language
258
+ const { switches, parameters } = node;
259
+ const meta = [switches, parameters].filter(Boolean).join(" ");
256
260
  return {
257
261
  type: "code",
258
262
  lang: node.language || null,
263
+ meta: node.language && meta ? meta : null,
259
264
  value: trimTrailingNewline(node.value)
260
265
  };
261
266
  }
@@ -1,11 +1,12 @@
1
1
  import type { Root as MdastRoot } from "mdast";
2
2
  import type { OrgData } from "uniorg";
3
- import type { UniorgToMdastOptions } from "./shared.js";
3
+ import { type UniorgToMdastOptions } from "./shared.js";
4
4
  export type { UniorgToMdastOptions } from "./shared.js";
5
5
  /**
6
6
  * Transforms a uniorg AST to a mdast (Markdown AST).
7
7
  * @param uniorgAst The uniorg AST to transform.
8
- * @param options Controls org-ism serialization (`key:: value` lines).
8
+ * @param options Controls org-ism serialization (`key:: value` lines);
9
+ * `org`, the text a tree was parsed from, finds its frontmatter block.
9
10
  * @returns The transformed mdast.
10
11
  */
11
12
  export declare function transformUniorgAstToMdast(uniorgAst: OrgData, options?: UniorgToMdastOptions): MdastRoot;
@@ -1,13 +1,27 @@
1
1
  import { stringify as stringifyYaml } from "yaml";
2
+ import { warn } from "./shared.js";
3
+ import { isModeLineComment, takeFileHeader, takeRenderedFileHeader } from "../frontmatterBlock.js";
2
4
  import { collectFootnoteLabels } from "./footnotes.js";
3
5
  import { transformNodes } from "./elements.js";
4
6
  /**
5
7
  * Transforms a uniorg AST to a mdast (Markdown AST).
6
8
  * @param uniorgAst The uniorg AST to transform.
7
- * @param options Controls org-ism serialization (`key:: value` lines).
9
+ * @param options Controls org-ism serialization (`key:: value` lines);
10
+ * `org`, the text a tree was parsed from, finds its frontmatter block.
8
11
  * @returns The transformed mdast.
9
12
  */
10
13
  export function transformUniorgAstToMdast(uniorgAst, options = {}) {
14
+ // called on its own, not by convertOrgToMarkdown, which takes the
15
+ // header off the org text: the header of the org text the tree was
16
+ // parsed from, or else the one transformMdastToUniorgAst built, from a
17
+ // copy, so the caller's tree stays as it is
18
+ if (options.takesMorgEntries === undefined) {
19
+ uniorgAst = { ...uniorgAst, children: [...uniorgAst.children] };
20
+ const header = options.org === undefined
21
+ ? takeRenderedFileHeader(uniorgAst)
22
+ : takeFileHeader(uniorgAst, options.org, options.onWarning);
23
+ options = { ...options, ...header };
24
+ }
11
25
  const ctx = {
12
26
  options,
13
27
  inlineFootnotes: [],
@@ -15,36 +29,15 @@ export function transformUniorgAstToMdast(uniorgAst, options = {}) {
15
29
  };
16
30
  collectFootnoteLabels(ctx, uniorgAst);
17
31
  const nodes = uniorgAst.children || [];
18
- // leading #+KEY: value keywords map to md frontmatter, a native
19
- // construct; JSON-encoded values restore their structure (ADR 0002)
20
- const collected = new Map();
21
- let first = 0;
22
- while (nodes[first]?.type === "keyword") {
23
- const keyword = nodes[first];
24
- const key = keyword.key.toLowerCase();
25
- // a keyword may legally repeat; collect the values instead of
26
- // letting the last one win
27
- collected.set(key, [
28
- ...(collected.get(key) ?? []),
29
- parseKeywordValue(keyword.value)
30
- ]);
31
- first++;
32
- }
33
- const frontmatter = Object.fromEntries([...collected].map(([key, values]) => [
34
- key,
35
- values.length === 1 ? values[0] : values
36
- ]));
37
- const children = transformNodes(ctx, nodes.slice(first));
32
+ const { yaml, rest } = takeFrontmatter(ctx, nodes);
33
+ const children = transformNodes(ctx, rest);
38
34
  // adjacent single-item task lists (one per converted TODO section)
39
35
  // merge into one list, or the output would not be a fixed point
40
36
  if (options.taskCheckboxes) {
41
37
  mergeAdjacentTaskLists(children);
42
38
  }
43
- if (first > 0) {
44
- children.unshift({
45
- type: "yaml",
46
- value: stringifyYaml(frontmatter).trimEnd()
47
- });
39
+ if (yaml !== undefined) {
40
+ children.unshift({ type: "yaml", value: yaml });
48
41
  }
49
42
  for (const footnote of ctx.inlineFootnotes) {
50
43
  children.push({
@@ -56,12 +49,88 @@ export function transformUniorgAstToMdast(uniorgAst, options = {}) {
56
49
  }
57
50
  return { type: "root", children: children };
58
51
  }
59
- function parseKeywordValue(value) {
60
- try {
61
- return JSON.parse(value);
52
+ // the frontmatter: the block's YAML, and the leading #+KEY: value
53
+ // keywords, which can act in Emacs, in order, repeats included, as
54
+ // morg's own entry (ADR 0005); where they cannot join it, they stay
55
+ // keyword lines. Returns the YAML and the nodes left
56
+ function takeFrontmatter(ctx, nodes) {
57
+ const block = ctx.options.frontmatter;
58
+ // set by the caller or transformUniorgAstToMdast, never undefined
59
+ const joins = ctx.options.takesMorgEntries === true;
60
+ const start = leadingKeywordsStart(nodes);
61
+ warnAboutFrontmatter(ctx, block, joins ? [] : nodes.slice(start));
62
+ const keywords = inertModeLineGuard(ctx, nodes, joins ? leadingKeywords(nodes.slice(start)) : [], start);
63
+ const entries = morgEntries(ctx.options.fileProperties ?? [], keywords);
64
+ const rest = [
65
+ ...nodes.slice(0, start),
66
+ ...nodes.slice(start + keywords.length)
67
+ ];
68
+ // an empty block adds nothing next to the entries
69
+ // on a line of their own: a keep-chomped (`|+`) scalar at the end of
70
+ // the block holds a newline already there
71
+ const yaml = entries && block
72
+ ? `${block.replace(/(?<!\n)$/, "\n")}${entries}`
73
+ : (entries ?? block);
74
+ // leading the Markdown body, with or without frontmatter above it
75
+ if (isInertModeLine(ctx, rest[0])) {
76
+ warn(ctx, "a -*- comment below the first line becomes the mode line in org");
77
+ }
78
+ return { yaml, rest };
79
+ }
80
+ // leading keywords may sit below a -*- comment, which leads the
81
+ // Markdown body and so becomes the mode line (warned if it was none);
82
+ // below another comment, they would come back above it
83
+ function leadingKeywordsStart(nodes) {
84
+ return nodes[0] && isModeLineComment(nodes[0]) ? 1 : 0;
85
+ }
86
+ // a -*- comment below the file's first line, which Emacs ignores
87
+ function isInertModeLine(ctx, node) {
88
+ return !ctx.options.startsWithModeLine && !!node && isModeLineComment(node);
89
+ }
90
+ // md→org makes a -*- comment that leads the Markdown body the file's
91
+ // mode line: leading keywords directly above one stay lines, so it
92
+ // does not lead it
93
+ function inertModeLineGuard(ctx, nodes, keywords, start) {
94
+ if (!keywords.length ||
95
+ start !== 0 ||
96
+ !isInertModeLine(ctx, nodes[keywords.length])) {
97
+ return keywords;
98
+ }
99
+ warn(ctx, "leading keywords stay lines: a -*- comment below them is no mode line");
100
+ return [];
101
+ }
102
+ function leadingKeywords(nodes) {
103
+ const keywords = [];
104
+ while (nodes[keywords.length]?.type === "keyword") {
105
+ const keyword = nodes[keywords.length];
106
+ keywords.push([keyword.key, keyword.value]);
107
+ }
108
+ return keywords;
109
+ }
110
+ // morg's own entries as YAML text, or undefined without any
111
+ function morgEntries(fileProperties, keywords) {
112
+ // an empty value as `KEY:`, as it reads back, not as `KEY: ""`
113
+ const items = (pairs) => pairs.map(([key, value]) => ({ [key]: value || null }));
114
+ const entries = {
115
+ ...(fileProperties.length && { morg_properties: items(fileProperties) }),
116
+ ...(keywords.length && { morg_keywords: items(keywords) })
117
+ };
118
+ // unfolded: a value folded over lines fits no keyword line again
119
+ return Object.keys(entries).length
120
+ ? stringifyYaml(entries, { lineWidth: 0, nullStr: "" }).trimEnd()
121
+ : undefined;
122
+ }
123
+ // `left`: the leading nodes the frontmatter could not take
124
+ function warnAboutFrontmatter(ctx, block, left) {
125
+ if (block && /^---[ \t]*$/m.test(block)) {
126
+ warn(ctx, "frontmatter holds a --- line, which ends it early in Markdown");
127
+ }
128
+ const drawer = left[0]?.type === "property-drawer";
129
+ if (drawer) {
130
+ warn(ctx, "file-level drawer stays text: the frontmatter cannot take it");
62
131
  }
63
- catch {
64
- return value;
132
+ if (left[drawer ? 1 : 0]?.type === "keyword") {
133
+ warn(ctx, "leading keywords stay lines: the frontmatter cannot take them");
65
134
  }
66
135
  }
67
136
  function isTaskList(node) {
@@ -61,7 +61,7 @@ function transformUniorgList(ctx, node) {
61
61
  return {
62
62
  type: "list",
63
63
  ordered,
64
- start: ordered ? parseInt(firstBullet, 10) || 1 : null,
64
+ start: ordered ? parseInt(firstBullet, 10) : null,
65
65
  spread: false,
66
66
  children: run.map(item => transformUniorgListItem(ctx, item))
67
67
  };
@@ -5,6 +5,12 @@ import { unescapeOrgPath } from "../orgPath.js";
5
5
  const IMAGE_EXTENSION_RE = /\.(png|jpe?g|gif|svg|webp|avif|bmp|ico)$/i;
6
6
  export function transformUniorgObjects(ctx, children) {
7
7
  return (children || [])
8
+ .map((child, index) =>
9
+ // uniorg keeps the continuation line's indentation after a line
10
+ // break; md would render it as leading whitespace
11
+ child.type === "text" && children?.[index - 1]?.type === "line-break"
12
+ ? { ...child, value: child.value.replace(/^[ \t]+/, "") }
13
+ : child)
8
14
  .flatMap(child => transformUniorgObjectToMdastPhrasingContent(ctx, child))
9
15
  .filter(Boolean);
10
16
  }
@@ -6,6 +6,11 @@ export interface UniorgToMdastOptions {
6
6
  taskCheckboxes?: boolean;
7
7
  orgismKeys?: Record<string, string>;
8
8
  onWarning?: (message: string) => void;
9
+ frontmatter?: string;
10
+ fileProperties?: [string, string][];
11
+ takesMorgEntries?: boolean;
12
+ startsWithModeLine?: boolean;
13
+ org?: string;
9
14
  }
10
15
  export interface TransformContext {
11
16
  options: UniorgToMdastOptions;
@@ -1,4 +1,4 @@
1
- import { toString as orgastToString } from "orgast-util-to-string";
1
+ import { affiliatedEntries } from "../affiliated.js";
2
2
  import { unified } from "unified";
3
3
  import { uniorgStringify } from "uniorg-stringify";
4
4
  import { toggleEnabled } from "../../options.js";
@@ -24,7 +24,12 @@ export function keyValueParagraph(lines) {
24
24
  // trailing newline), for verbatim passthrough of org-only constructs
25
25
  export function orgNodeToText(node) {
26
26
  const orgText = unified()
27
- .use(uniorgStringify)
27
+ // a preset's verbatim-inline text is org text already
28
+ .use(uniorgStringify, {
29
+ handlers: {
30
+ "verbatim-inline": (inline) => inline.value
31
+ }
32
+ })
28
33
  .stringify({
29
34
  type: "org-data",
30
35
  children: [node],
@@ -41,15 +46,6 @@ export function trimTrailingNewline(value) {
41
46
  // affiliated keywords (#+CAPTION:, #+NAME:, #+ATTR_*) precede their
42
47
  // element as verbatim lines so the return trip re-attaches them natively
43
48
  export function affiliatedLines(node) {
44
- const affiliated = node
45
- .affiliated;
46
- return Object.entries(affiliated ?? {}).flatMap(([key, value]) => {
47
- const entries = Array.isArray(value) ? value : [value];
48
- return entries.map(entry => {
49
- const text = Array.isArray(entry)
50
- ? entry.map(child => orgastToString(child)).join("")
51
- : String(entry);
52
- return `#+${key}: ${text}`;
53
- });
54
- });
49
+ const affiliated = node.affiliated;
50
+ return affiliatedEntries(affiliated).map(([key, value]) => `#+${key}: ${value}`);
55
51
  }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,5 @@ export { transformMdastToUniorgAst } from "./core/mdastToUniorg/index.js";
8
8
  export { transformUniorgAstToMdast } from "./core/uniorgToMdast/index.js";
9
9
  export { logseq } from "./presets/logseq.js";
10
10
  export { obsidian } from "./presets/obsidian.js";
11
- export type { Preset } from "./presets/types.js";
12
- export type { LogseqPresetOptions } from "./presets/logseq.js";
11
+ export type { FragmentConverter, Preset } from "./presets/types.js";
13
12
  export type { Toggle, MarkdownToOrgOptions, OrgToMarkdownOptions, MarkdownStyleOptions } from "./options.js";