@remigius42/morg 0.6.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.
@@ -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,19 @@
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
+ * 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;
8
17
  /**
9
18
  * org→md: drops the line-start zero-width spaces `escapeLineSyntax`
10
19
  * inserts.
@@ -139,6 +139,21 @@ function isPassthrough(starts) {
139
139
  const [only, ...rest] = tryParse(`${lines.join("\n")}\n`)?.children ?? [];
140
140
  return !rest.length && PASSTHROUGH_TYPES.has(only?.type ?? "");
141
141
  }
142
+ /**
143
+ * md→org: whether a node is a paragraph org→md wrote as the org text of
144
+ * one passthrough element, which goes back as it is.
145
+ * @param node The node.
146
+ * @param index Its index in its parent.
147
+ * @param parent Its parent.
148
+ */
149
+ export function isPassthroughParagraph(node, index, parent) {
150
+ if (node.type !== "paragraph") {
151
+ return false;
152
+ }
153
+ const context = contextOf(node, index, parent);
154
+ const children = node.children;
155
+ return (!context.afterBullet && isPassthrough(lineStarts(children, context).starts));
156
+ }
142
157
  // line syntax spanning lines (`#+begin_src`…`#+end_src`, a drawer) or
143
158
  // depending on context (planning below a headline) shows only in the
144
159
  // 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
- // a keyword is a single line, so anything that is not a single-line
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
@@ -196,7 +168,7 @@ export function transformMdastHeading(ctx, node) {
196
168
  children: transformPhrasingChildren(ctx, node.children)
197
169
  };
198
170
  }
199
- const KEYWORD_LINE_RE = /^#\+\S+: /;
171
+ const KEYWORD_LINE_RE = new RegExp(String.raw `^#\+${KEYWORD_NAME}: `);
200
172
  export function keywordOnlyLines(node) {
201
173
  if (!node.children.every(child => child.type === "text")) {
202
174
  return null;
@@ -204,7 +176,7 @@ export function keywordOnlyLines(node) {
204
176
  const lines = node.children
205
177
  .map(child => child.value)
206
178
  .join("")
207
- .split("\n");
179
+ .split(/\r?\n/);
208
180
  return lines.length && lines.every(line => KEYWORD_LINE_RE.test(line))
209
181
  ? lines
210
182
  : 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;
@@ -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
+ }
@@ -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) {
@@ -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";
@@ -41,15 +41,6 @@ export function trimTrailingNewline(value) {
41
41
  // affiliated keywords (#+CAPTION:, #+NAME:, #+ATTR_*) precede their
42
42
  // element as verbatim lines so the return trip re-attaches them natively
43
43
  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
- });
44
+ const affiliated = node.affiliated;
45
+ return affiliatedEntries(affiliated).map(([key, value]) => `#+${key}: ${value}`);
55
46
  }