@remigius42/morg 0.5.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.
Files changed (45) hide show
  1. package/README.md +12 -1
  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 +12 -0
  7. package/dist/core/bracedScripts.js +159 -0
  8. package/dist/core/footnoteReferences.d.ts +10 -0
  9. package/dist/core/footnoteReferences.js +24 -0
  10. package/dist/core/frontmatterBlock.d.ts +89 -0
  11. package/dist/core/frontmatterBlock.js +448 -0
  12. package/dist/core/keyValueLines.d.ts +6 -0
  13. package/dist/core/keyValueLines.js +18 -0
  14. package/dist/core/lineSyntax.d.ts +21 -0
  15. package/dist/core/lineSyntax.js +221 -0
  16. package/dist/core/markupBoundary.d.ts +12 -0
  17. package/dist/core/markupBoundary.js +195 -0
  18. package/dist/core/mdastToUniorg/blocks.d.ts +0 -1
  19. package/dist/core/mdastToUniorg/blocks.js +3 -31
  20. package/dist/core/mdastToUniorg/index.d.ts +6 -0
  21. package/dist/core/mdastToUniorg/index.js +81 -8
  22. package/dist/core/mdastToUniorg/lists.d.ts +1 -1
  23. package/dist/core/mdastToUniorg/lists.js +21 -2
  24. package/dist/core/mdastToUniorg/phrasing.js +145 -12
  25. package/dist/core/orgPath.d.ts +9 -0
  26. package/dist/core/orgPath.js +24 -0
  27. package/dist/core/render.d.ts +33 -0
  28. package/dist/core/render.js +101 -0
  29. package/dist/core/tablePipes.d.ts +17 -0
  30. package/dist/core/tablePipes.js +47 -0
  31. package/dist/core/underscoreBullets.d.ts +10 -0
  32. package/dist/core/underscoreBullets.js +32 -0
  33. package/dist/core/uniorgToMdast/elements.js +1 -1
  34. package/dist/core/uniorgToMdast/index.d.ts +3 -2
  35. package/dist/core/uniorgToMdast/index.js +100 -31
  36. package/dist/core/uniorgToMdast/lists.js +12 -1
  37. package/dist/core/uniorgToMdast/objects.js +44 -6
  38. package/dist/core/uniorgToMdast/shared.d.ts +5 -0
  39. package/dist/core/uniorgToMdast/shared.js +3 -12
  40. package/dist/core/uniorgToMdast/tables.js +12 -6
  41. package/dist/markdownToOrg.js +44 -23
  42. package/dist/orgToMarkdown.js +39 -5
  43. package/dist/presets/logseq.js +130 -0
  44. package/dist/presets/types.d.ts +4 -1
  45. package/package.json +4 -1
@@ -1,4 +1,7 @@
1
1
  import { mdismEnabled, warn } from "./context.js";
2
+ import { visit } from "unist-util-visit";
3
+ import { escapeOrgPath } from "../orgPath.js";
4
+ import { orgParser, renderInline } from "../render.js";
2
5
  // html tags morg itself emits under useHtml; with interpretHtml a bare
3
6
  // open/close pair becomes the corresponding native org object
4
7
  const INLINE_HTML_ORG_TYPES = {
@@ -30,20 +33,87 @@ export function transformPhrasingChildren(ctx, children) {
30
33
  ? matchInlineHtmlPair(children, i)
31
34
  : null;
32
35
  if (pair) {
33
- result.push({
36
+ result.push(...hoistEdgeWhitespace(joinLines({
34
37
  type: pair.orgType,
35
38
  children: transformPhrasingChildren(ctx, children.slice(i + 1, pair.end))
36
- });
39
+ })));
37
40
  i = pair.end;
38
41
  continue;
39
42
  }
43
+ if (node.type === "inlineCode") {
44
+ result.push(...transformMdastInlineCode(ctx, node.value));
45
+ continue;
46
+ }
40
47
  const transformed = transformMdastPhrasingContentToUniorgObject(ctx, node);
41
48
  if (transformed) {
42
- result.push(transformed);
49
+ result.push(...hoistEdgeWhitespace(joinLines(transformed)));
43
50
  }
44
51
  }
45
52
  return result;
46
53
  }
54
+ const CONTAINER_MARKUP = new Set([
55
+ "bold",
56
+ "italic",
57
+ "strike-through",
58
+ "underline"
59
+ ]);
60
+ // org markup spans at most two lines; beyond that its line endings
61
+ // become spaces, as CommonMark renders them anyway (see
62
+ // transformMdastInlineCode)
63
+ function joinLines(node) {
64
+ if (!CONTAINER_MARKUP.has(node.type)) {
65
+ return node;
66
+ }
67
+ const texts = [];
68
+ visit(node, "text", (text) => {
69
+ texts.push(text);
70
+ });
71
+ const lineEndings = texts
72
+ .map(text => text.value)
73
+ .join("")
74
+ .split("\n").length - 1;
75
+ if (lineEndings > 1) {
76
+ for (const text of texts) {
77
+ text.value = text.value.replaceAll("\n", " ");
78
+ }
79
+ }
80
+ return node;
81
+ }
82
+ // strips the whitespace at the edges of `children`, returning it
83
+ function takeEdgeWhitespace(children) {
84
+ const first = children[0];
85
+ const last = children.at(-1);
86
+ let before = "";
87
+ let after = "";
88
+ if (first?.type === "text") {
89
+ before = /^\s*/.exec(first.value)?.[0] ?? "";
90
+ first.value = first.value.slice(before.length);
91
+ }
92
+ if (last?.type === "text") {
93
+ after = /\s*$/.exec(last.value)?.[0] ?? "";
94
+ last.value = last.value.slice(0, last.value.length - after.length);
95
+ }
96
+ return { before, after };
97
+ }
98
+ // org markup may not start or end with whitespace, which a code span's
99
+ // moved-out edge whitespace (see transformMdastInlineCode) can leave
100
+ // inside it; it moves out further, to the markup's siblings
101
+ function hoistEdgeWhitespace(node) {
102
+ if (!CONTAINER_MARKUP.has(node.type) || !("children" in node)) {
103
+ return [node];
104
+ }
105
+ const { before, after } = takeEdgeWhitespace(node.children);
106
+ if (!before && !after) {
107
+ return [node];
108
+ }
109
+ const content = node.children.filter(child => child.type !== "text" || child.value !== "");
110
+ const text = (value) => value ? [{ type: "text", value }] : [];
111
+ return [
112
+ ...text(before),
113
+ ...(content.length ? [{ ...node, children: content }] : []),
114
+ ...text(after)
115
+ ];
116
+ }
47
117
  // an org bracket-link path cannot contain [ or ]; percent-encoding keeps
48
118
  // the url equivalent and, unlike org's backslash escaping, survives being
49
119
  // parsed back (uniorg does not decode `\[`). A silent normalization, not
@@ -51,6 +121,36 @@ export function transformPhrasingChildren(ctx, children) {
51
121
  function orgSafeUrl(url) {
52
122
  return url.replaceAll("[", "%5B").replaceAll("]", "%5D");
53
123
  }
124
+ // a url without a scheme is a relative path in markdown, but a bare org
125
+ // path is a fuzzy link (a heading search); org's file: type keeps it a
126
+ // file. A #anchor becomes org's search option, which finds a
127
+ // <<target>> or a headline of that name. `[[page]]` / `((uuid))` urls
128
+ // are dialect references (logseq), not paths
129
+ const SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
130
+ function isRelativePath(url) {
131
+ return url !== "" && !SCHEME_RE.test(url) && !/^(#|\/\/|\[|\()/.test(url);
132
+ }
133
+ // md urls are percent-encoded, org paths are not; a malformed escape
134
+ // stays as written
135
+ function decodeUrlPart(part) {
136
+ try {
137
+ return decodeURIComponent(part);
138
+ }
139
+ catch {
140
+ return part;
141
+ }
142
+ }
143
+ function orgLinkTarget(url) {
144
+ if (!isRelativePath(url)) {
145
+ return { rawLink: orgSafeUrl(url), linkType: "url" };
146
+ }
147
+ const hash = url.indexOf("#");
148
+ const path = escapeOrgPath(decodeUrlPart(hash === -1 ? url : url.slice(0, hash)));
149
+ const search = hash === -1
150
+ ? ""
151
+ : `::${escapeOrgPath(decodeUrlPart(url.slice(hash + 1)), true)}`;
152
+ return { rawLink: `file:${path}${search}`, linkType: "file" };
153
+ }
54
154
  function transformMdastLink(ctx, linkNode) {
55
155
  const [only] = linkNode.children;
56
156
  // text equal to the url (autolinks) is no description; a plain
@@ -61,13 +161,13 @@ function transformMdastLink(ctx, linkNode) {
61
161
  ? []
62
162
  : transformPhrasingChildren(ctx, linkNode.children);
63
163
  // rawLink should just be the URL, uniorg-stringify adds the brackets
64
- const url = orgSafeUrl(linkNode.url);
164
+ const { rawLink, linkType } = orgLinkTarget(linkNode.url);
65
165
  return {
66
166
  type: "link",
67
167
  format: "bracket", // Assuming bracket format for Markdown links
68
- linkType: "url",
69
- rawLink: url,
70
- path: url,
168
+ linkType,
169
+ rawLink,
170
+ path: rawLink,
71
171
  children: linkChildren
72
172
  };
73
173
  }
@@ -101,16 +201,51 @@ function transformMdastImage(ctx, node) {
101
201
  if (node.title) {
102
202
  warn(ctx, `dropped image title "${node.title}" (${node.url})`);
103
203
  }
104
- const url = orgSafeUrl(node.url);
204
+ const { rawLink } = orgLinkTarget(node.url);
105
205
  return {
106
206
  type: "link",
107
207
  format: "bracket",
108
208
  linkType: "file",
109
- rawLink: url,
110
- path: url,
209
+ rawLink,
210
+ path: rawLink,
111
211
  children: node.alt ? [{ type: "text", value: node.alt }] : []
112
212
  };
113
213
  }
214
+ // org markup spans at most two lines; CommonMark renders a line ending
215
+ // inside a code span as a space, so nothing is lost. Org markup may not
216
+ // start or end with whitespace either: edge whitespace moves outside
217
+ // the markers, a one-space shift in the rendered output
218
+ function transformMdastInlineCode(ctx, value) {
219
+ const [, before = "", code = "", after = ""] = /^(\s*)(.*?)(\s*)$/s.exec(value.replace(/\r\n?|\n/g, " ")) ?? [];
220
+ if (!code) {
221
+ // org has no empty code markup
222
+ warn(ctx, "whitespace-only inline code kept as text");
223
+ return [{ type: "text", value: before }];
224
+ }
225
+ const type = codeType(code);
226
+ if (!type) {
227
+ warn(ctx, "inline code holding both ~ and = kept as text");
228
+ return [{ type: "text", value: before + code + after }];
229
+ }
230
+ return [
231
+ ...(before ? [{ type: "text", value: before }] : []),
232
+ { type, value: code },
233
+ ...(after ? [{ type: "text", value: after }] : [])
234
+ ];
235
+ }
236
+ // org ends code at the first `~` it may close on (`~a~ b~`); verbatim
237
+ // keeps such code whole, and comes back as md code too, unless a `=`
238
+ // ends it early in turn
239
+ function codeType(code) {
240
+ return ["code", "verbatim"].find(type => !code.includes(type === "code" ? "~" : "=") ||
241
+ readsWhole({ type, value: code }));
242
+ }
243
+ // whether org reads `node`, rendered, back as the same node
244
+ function readsWhole(node) {
245
+ const [paragraph] = orgParser.parse(renderInline(node)).children;
246
+ const [first, ...rest] = paragraph && "children" in paragraph ? paragraph.children : [];
247
+ return !rest.length && first?.type === node.type && first.value === node.value;
248
+ }
114
249
  function transformMdastInlineMath(node) {
115
250
  const value = node.value;
116
251
  return {
@@ -144,8 +279,6 @@ function transformMdastPhrasingContentToUniorgObject(ctx, node) {
144
279
  return transformMdastLinkReference(ctx, node);
145
280
  case "imageReference":
146
281
  return transformMdastImageReference(ctx, node);
147
- case "inlineCode":
148
- return { type: "code", value: node.value };
149
282
  case "inlineMath":
150
283
  return transformMdastInlineMath(node);
151
284
  case "break":
@@ -0,0 +1,9 @@
1
+ /**
2
+ * md→org: a decoded file link path, or its search option, as org link
3
+ * text.
4
+ */
5
+ export declare function escapeOrgPath(part: string, isSearch?: boolean): string;
6
+ /**
7
+ * org→md: the inverse of `escapeOrgPath`.
8
+ */
9
+ export declare function unescapeOrgPath(part: string): string;
@@ -0,0 +1,24 @@
1
+ // an org bracket-link path cannot contain [ or ], and `::` starts its
2
+ // search option. morg percent-encodes those (uniorg does not decode org's
3
+ // own backslash escaping); a literal % in front of such an escape gets
4
+ // one more `25`, so the encoding stays reversible (`%5B` ↔ `%255B`)
5
+ const ESCAPE_RE = /%((?:25)*)(5B|5D|3A)/g;
6
+ const ESCAPED = { "5B": "[", "5D": "]", "3A": ":" };
7
+ /**
8
+ * md→org: a decoded file link path, or its search option, as org link
9
+ * text.
10
+ */
11
+ export function escapeOrgPath(part, isSearch = false) {
12
+ const escaped = part
13
+ .replace(ESCAPE_RE, "%25$1$2")
14
+ .replaceAll("[", "%5B")
15
+ .replaceAll("]", "%5D");
16
+ // a colon next to another, or before the `::` separator
17
+ return isSearch ? escaped : escaped.replace(/:(?=:|$)/g, "%3A");
18
+ }
19
+ /**
20
+ * org→md: the inverse of `escapeOrgPath`.
21
+ */
22
+ export function unescapeOrgPath(part) {
23
+ return part.replace(ESCAPE_RE, (_, more, code) => more ? `%${more.slice(2)}${code}` : (ESCAPED[code] ?? code));
24
+ }
@@ -0,0 +1,33 @@
1
+ import type { Parent } from "unist";
2
+ export type Node = Parent["children"][number] & {
3
+ value?: string;
4
+ };
5
+ export declare const orgParser: import("unified").Processor<import("uniorg").OrgData, undefined, undefined, undefined, undefined>;
6
+ export declare const positionParser: import("unified").Processor<import("uniorg").OrgData, undefined, undefined, undefined, undefined>;
7
+ /**
8
+ * Parses `text` as an org document, or returns undefined where uniorg
9
+ * throws: it takes a line starting `_.` or `_)` for a bullet, then
10
+ * fails to read it (org has no such bullet; see underscoreBullets).
11
+ */
12
+ export declare function tryParse(text: string, parser?: {
13
+ parse(text: string): unknown;
14
+ }): Parent | undefined;
15
+ export declare function isInline(node: Node | Parent): boolean;
16
+ /**
17
+ * How org renders an inline node within its line.
18
+ */
19
+ export declare function renderInline(node: Node): string;
20
+ /**
21
+ * An inline node's delimiters around its children, as org renders them
22
+ * (`*` and `*`, `[[url][` and `]]`).
23
+ */
24
+ export declare function delimiters(node: Node): [string, string];
25
+ /**
26
+ * The org rendering of each child; a block element only ends a line.
27
+ */
28
+ export declare function renderChildren(children: Node[]): string[];
29
+ /**
30
+ * The index of the rendered child `offset` (into the joined rendering)
31
+ * lies in, and the offset within that child.
32
+ */
33
+ export declare function locate(rendered: string[], offset: number): [number, number];
@@ -0,0 +1,101 @@
1
+ import { unified } from "unified";
2
+ import uniorgParse from "uniorg-parse";
3
+ import { uniorgStringify } from "uniorg-stringify";
4
+ // built once: constructing a processor per parse dominates the cost
5
+ export const orgParser = unified().use(uniorgParse).freeze();
6
+ export const positionParser = unified()
7
+ .use(uniorgParse, { trackPosition: true })
8
+ .freeze();
9
+ const stringifier = unified().use(uniorgStringify).freeze();
10
+ /**
11
+ * Parses `text` as an org document, or returns undefined where uniorg
12
+ * throws: it takes a line starting `_.` or `_)` for a bullet, then
13
+ * fails to read it (org has no such bullet; see underscoreBullets).
14
+ */
15
+ export function tryParse(text, parser = orgParser) {
16
+ try {
17
+ return parser.parse(text);
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ // uniorg's inline node types; anything else is a block element (in a
24
+ // list item's flattened content: a nested list or code block)
25
+ const INLINE_TYPES = new Set([
26
+ "text",
27
+ "bold",
28
+ "italic",
29
+ "underline",
30
+ "strike-through",
31
+ "code",
32
+ "verbatim",
33
+ "link",
34
+ "footnote-reference",
35
+ "latex-fragment",
36
+ "entity",
37
+ "timestamp",
38
+ "subscript",
39
+ "superscript",
40
+ "export-snippet",
41
+ "statistics-cookie",
42
+ "citation",
43
+ "line-break"
44
+ ]);
45
+ export function isInline(node) {
46
+ return INLINE_TYPES.has(node.type);
47
+ }
48
+ // ends the rendering, so the paragraph's own trailing newline and
49
+ // whitespace trimming stay out of it
50
+ const SENTINEL = "\u0000";
51
+ /**
52
+ * How org renders an inline node within its line.
53
+ */
54
+ export function renderInline(node) {
55
+ if (node.type === "text") {
56
+ return node.value ?? "";
57
+ }
58
+ const rendered = String(stringifier.stringify({
59
+ type: "org-data",
60
+ children: [
61
+ {
62
+ type: "paragraph",
63
+ children: [node, { type: "text", value: SENTINEL }]
64
+ }
65
+ ]
66
+ }));
67
+ return rendered.slice(0, rendered.lastIndexOf(SENTINEL));
68
+ }
69
+ // stands in for an inline node's content
70
+ const CONTENT = "\u0001";
71
+ /**
72
+ * An inline node's delimiters around its children, as org renders them
73
+ * (`*` and `*`, `[[url][` and `]]`).
74
+ */
75
+ export function delimiters(node) {
76
+ const [open = "", close = ""] = renderInline({
77
+ ...node,
78
+ children: [{ type: "text", value: CONTENT }]
79
+ }).split(CONTENT);
80
+ return [open, close];
81
+ }
82
+ /**
83
+ * The org rendering of each child; a block element only ends a line.
84
+ */
85
+ export function renderChildren(children) {
86
+ return children.map(child => (isInline(child) ? renderInline(child) : "\n"));
87
+ }
88
+ /**
89
+ * The index of the rendered child `offset` (into the joined rendering)
90
+ * lies in, and the offset within that child.
91
+ */
92
+ export function locate(rendered, offset) {
93
+ let start = 0;
94
+ for (const [index, part] of rendered.entries()) {
95
+ if (offset < start + part.length) {
96
+ return [index, offset - start];
97
+ }
98
+ start += part.length;
99
+ }
100
+ return [-1, 0];
101
+ }
@@ -0,0 +1,17 @@
1
+ import type { Parent } from "unist";
2
+ /**
3
+ * md→org: a `|` in a table cell's text (md `\|`) would end the cell; org
4
+ * has no escaped `|`, but renders the `\vert` entity as one, `{}` ending
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.
10
+ */
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;
@@ -0,0 +1,47 @@
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 = "∣";
5
+ /**
6
+ * md→org: a `|` in a table cell's text (md `\|`) would end the cell; org
7
+ * has no escaped `|`, but renders the `\vert` entity as one, `{}` ending
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.
13
+ */
14
+ export function escapeTablePipes(tree, onWarning) {
15
+ let replaced = false;
16
+ visit(tree, "table-cell", (cell) => {
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
+ }
25
+ });
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";
47
+ }
@@ -0,0 +1,10 @@
1
+ import type { Parent } from "unist";
2
+ /**
3
+ * org→md: guards the lines uniorg misreads before parsing.
4
+ */
5
+ export declare function guardUnderscoreBullets(org: string): string;
6
+ /**
7
+ * org→md: drops the guards again, also where uniorg reads no text (a
8
+ * src block's code). A zero-width space the author put there goes too.
9
+ */
10
+ export declare function dropUnderscoreBulletGuards(tree: Parent): void;
@@ -0,0 +1,32 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { ZERO_WIDTH_SPACE } from "./markupBoundary.js";
3
+ // ====================================================================
4
+ // WORKAROUND for a bug in uniorg-parse 3.2.2 (upstream issue: not filed
5
+ // yet, see TODO.md). Drop this module once a fixed version is in; the
6
+ // canary in tests/uniorgWorkarounds.spec.ts fails then.
7
+ // ====================================================================
8
+ // Its `listItemRe` takes a line starting `_.` or `_)` for a list item
9
+ // (`\w` includes `_`), but its `fullListItemRe`, like org itself, has
10
+ // no such bullet. `parseListStructure` then throws (`match error`), or,
11
+ // when a real bullet line follows, reads that one instead and silently
12
+ // drops the line. Org reads the line as text, and so does uniorg with a
13
+ // zero-width space in front (morg's line-start escape, see lineSyntax)
14
+ const UNDERSCORE_BULLET_RE = /^([ \t]*)(?=_[.)](?:[ \t]|$))/gm;
15
+ /**
16
+ * org→md: guards the lines uniorg misreads before parsing.
17
+ */
18
+ export function guardUnderscoreBullets(org) {
19
+ return org.replace(UNDERSCORE_BULLET_RE, `$1${ZERO_WIDTH_SPACE}`);
20
+ }
21
+ const GUARD_RE = new RegExp(`(^|\\n)([ \\t]*)${ZERO_WIDTH_SPACE}(?=_[.)](?:[ \\t\\n]|$))`, "g");
22
+ /**
23
+ * org→md: drops the guards again, also where uniorg reads no text (a
24
+ * src block's code). A zero-width space the author put there goes too.
25
+ */
26
+ export function dropUnderscoreBulletGuards(tree) {
27
+ visit(tree, (node) => {
28
+ if (typeof node.value === "string") {
29
+ node.value = node.value.replace(GUARD_RE, "$1$2");
30
+ }
31
+ });
32
+ }
@@ -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;