@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
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ Bidirectional **Markdown ↔ Org-mode** converter, built on the
|
|
|
17
17
|
|
|
18
18
|
morg treats Org as a canonical plain-text format and Markdown (Obsidian,
|
|
19
19
|
generic) as the interop surface. Dialect conventions, such as
|
|
20
|
-
[Logseq](https://docs.logseq.com/)'s
|
|
21
|
-
|
|
20
|
+
[Logseq](https://docs.logseq.com/)'s outline of blocks and page
|
|
21
|
+
properties, are supported via presets.
|
|
22
22
|
|
|
23
23
|
## Round-trip convergence
|
|
24
24
|
|
|
@@ -37,6 +37,13 @@ guarantee is to be **semantically faithful and convergent** instead
|
|
|
37
37
|
org properties; `org → md` serializes Org-only constructs ("org-isms") as
|
|
38
38
|
`key:: value` conventions ([ADR
|
|
39
39
|
0002](docs/adr/0002-mdism-property-namespace.md)).
|
|
40
|
+
- Frontmatter travels verbatim and inert in a
|
|
41
|
+
`#+begin_comment morg_frontmatter` block, not as org keywords, which
|
|
42
|
+
can act in Emacs; an org file's own leading keywords travel as a
|
|
43
|
+
`morg_keywords` frontmatter entry and come back as keywords, a
|
|
44
|
+
file-level drawer (org-roam's `:ID:`) as `morg_properties`. The
|
|
45
|
+
Logseq preset instead maps page properties natively ([ADR
|
|
46
|
+
0005](docs/adr/0005-frontmatter-as-a-marked-comment-block.md)).
|
|
40
47
|
- The few constructs that cannot be carried are documented in the
|
|
41
48
|
[mapping reference](docs/mappings.md) and reported as warnings.
|
|
42
49
|
|
|
@@ -191,15 +198,20 @@ preset })`: `preserveOrgisms` default `true`; `useHtml` (default
|
|
|
191
198
|
`- [x]`); headings become list items and do not restore on the
|
|
192
199
|
return trip; anything with priority, tags or content keeps its
|
|
193
200
|
heading and reports via `onWarning`
|
|
194
|
-
- `logseq(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
- `logseq()`: a page is Logseq's outline of blocks, converted block by
|
|
202
|
+
block: a headline (stars, a space, the block's content, an empty
|
|
203
|
+
block as the bare stars) ↔ a `-` bullet indented one tab per level,
|
|
204
|
+
its lines below the first two spaces further in. A block's content
|
|
205
|
+
is one fragment, so a code block or table that starts on the
|
|
206
|
+
headline line converts as a whole. `:heading: N` ↔ `- ## …`, a
|
|
207
|
+
block's property drawer ↔ `key:: value` lines; planning lines and
|
|
208
|
+
other drawers (`:LOGBOOK:`) stay as written. A heading outside the
|
|
209
|
+
bullets is a top-level block, as Logseq writes a page's first one.
|
|
210
|
+
Page properties map both directions: a first block of
|
|
211
|
+
`key:: value` lines and flat frontmatter entries ↔ leading
|
|
212
|
+
`#+key: value` lines, which Logseq reads as page properties;
|
|
213
|
+
frontmatter keys that act in Emacs (`todo`, `include`, …) stay inert.
|
|
214
|
+
Task markers and `[#A]` priorities stay text, page
|
|
203
215
|
references `[[page]]` and labeled forms `[label]([[page]])` ↔ org
|
|
204
216
|
fuzzy links `[[page][label]]`, block refs `[label](((uuid)))` ↔
|
|
205
217
|
`[[((uuid))][label]]`, and `^^highlight^^` markup survives verbatim
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AffiliatedKeywords } from "uniorg";
|
|
2
|
+
export declare const DUAL_NAMES = "CAPTION|RESULTS";
|
|
3
|
+
/**
|
|
4
|
+
* An element's affiliated keywords as key, value pairs, the way they are
|
|
5
|
+
* written above it: several values (#+CAPTION, #+HEADER, #+ATTR_*) as
|
|
6
|
+
* several pairs, parsed ones (#+CAPTION) as org text, a dual value as
|
|
7
|
+
* part of the key (`CAPTION[short]`).
|
|
8
|
+
* @param affiliated The element's affiliated keywords.
|
|
9
|
+
* @param asOrg Parsed values as org text, links and markup kept, not as
|
|
10
|
+
* plain text (a Markdown line above a body element reads markup anew).
|
|
11
|
+
* @returns The pairs in order.
|
|
12
|
+
*/
|
|
13
|
+
export declare function affiliatedEntries(affiliated?: AffiliatedKeywords, asOrg?: boolean): [string, string][];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { toString } from "orgast-util-to-string";
|
|
2
|
+
import { renderInline } from "./render.js";
|
|
3
|
+
const MULTIPLE_RE = /^(?:CAPTION|HEADER|ATTR_.*)$/;
|
|
4
|
+
// the keywords org reads with a dual value (`#+CAPTION[short]: long`);
|
|
5
|
+
// aliases (RESULT) are not
|
|
6
|
+
export const DUAL_NAMES = "CAPTION|RESULTS";
|
|
7
|
+
const DUAL_RE = new RegExp(`^(?:${DUAL_NAMES})$`);
|
|
8
|
+
/**
|
|
9
|
+
* An element's affiliated keywords as key, value pairs, the way they are
|
|
10
|
+
* written above it: several values (#+CAPTION, #+HEADER, #+ATTR_*) as
|
|
11
|
+
* several pairs, parsed ones (#+CAPTION) as org text, a dual value as
|
|
12
|
+
* part of the key (`CAPTION[short]`).
|
|
13
|
+
* @param affiliated The element's affiliated keywords.
|
|
14
|
+
* @param asOrg Parsed values as org text, links and markup kept, not as
|
|
15
|
+
* plain text (a Markdown line above a body element reads markup anew).
|
|
16
|
+
* @returns The pairs in order.
|
|
17
|
+
*/
|
|
18
|
+
export function affiliatedEntries(affiliated = {}, asOrg = false) {
|
|
19
|
+
return Object.entries(affiliated).flatMap(([key, value]) => (MULTIPLE_RE.test(key) ? value : [value]).map(item => {
|
|
20
|
+
const [main, dual] = DUAL_RE.test(key) && isDual(item) ? item : [item];
|
|
21
|
+
return [
|
|
22
|
+
dual === undefined ? key : `${key}[${dual}]`,
|
|
23
|
+
asOrg ? orgText(main) : plainText(main)
|
|
24
|
+
];
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
function isDual(value) {
|
|
28
|
+
return (Array.isArray(value) && value.length === 2 && typeof value[1] === "string");
|
|
29
|
+
}
|
|
30
|
+
function plainText(value) {
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return Array.isArray(value)
|
|
35
|
+
? value.map(plainText).join("")
|
|
36
|
+
: toString(value);
|
|
37
|
+
}
|
|
38
|
+
function orgText(value) {
|
|
39
|
+
const items = Array.isArray(value) ? value : [value];
|
|
40
|
+
return items
|
|
41
|
+
.map(item => (typeof item === "string" ? item : renderInline(item)))
|
|
42
|
+
.join("");
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Parent } from "unist";
|
|
2
|
+
/**
|
|
3
|
+
* md→org: escapes a literal backslash org would read as a command, but
|
|
4
|
+
* not in a passthrough element's org text (`#+begin_export latex`).
|
|
5
|
+
*/
|
|
6
|
+
export declare function escapeBackslashCommands(tree: Parent): void;
|
|
7
|
+
/**
|
|
8
|
+
* org→md: drops the zero-width spaces `escapeBackslashCommands` inserts.
|
|
9
|
+
*/
|
|
10
|
+
export declare function unescapeBackslashCommands(tree: Parent): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { EXIT, SKIP, visit } from "unist-util-visit";
|
|
2
|
+
import { isPassthroughParagraph } from "./lineSyntax.js";
|
|
3
|
+
import { ZERO_WIDTH_SPACE } from "./markupBoundary.js";
|
|
4
|
+
// org reads a backslash before a letter as an entity (`\alpha`) or a
|
|
5
|
+
// LaTeX fragment (`\Users`, `\foo{x}`), before `(` or `[` as a LaTeX
|
|
6
|
+
// fragment too; a zero-width space after the backslash leaves it text
|
|
7
|
+
const COMMAND_RE = /\\(?=[A-Za-z([])/g;
|
|
8
|
+
const ESCAPED_RE = new RegExp(String.raw `\\${ZERO_WIDTH_SPACE}(?=[A-Za-z([])`, "g");
|
|
9
|
+
/**
|
|
10
|
+
* md→org: escapes a literal backslash org would read as a command, but
|
|
11
|
+
* not in a passthrough element's org text (`#+begin_export latex`).
|
|
12
|
+
*/
|
|
13
|
+
export function escapeBackslashCommands(tree) {
|
|
14
|
+
visit(tree, (node, index, parent) => {
|
|
15
|
+
if (holdsCommand(node) &&
|
|
16
|
+
isPassthroughParagraph(node, index ?? 0, parent)) {
|
|
17
|
+
return SKIP;
|
|
18
|
+
}
|
|
19
|
+
if (node.type === "text") {
|
|
20
|
+
const text = node;
|
|
21
|
+
text.value = text.value?.replace(COMMAND_RE, `\\${ZERO_WIDTH_SPACE}`);
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
// whether a text in `node` holds a backslash org would read as a
|
|
27
|
+
// command: the only paragraphs worth a passthrough check
|
|
28
|
+
function holdsCommand(node) {
|
|
29
|
+
let found = false;
|
|
30
|
+
visit(node, "text", (text) => {
|
|
31
|
+
found ||= new RegExp(COMMAND_RE.source).test(text.value ?? "");
|
|
32
|
+
return found ? EXIT : undefined;
|
|
33
|
+
});
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* org→md: drops the zero-width spaces `escapeBackslashCommands` inserts.
|
|
38
|
+
*/
|
|
39
|
+
export function unescapeBackslashCommands(tree) {
|
|
40
|
+
visit(tree, "text", (node) => {
|
|
41
|
+
node.value = node.value?.replace(ESCAPED_RE, "\\");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -4,6 +4,13 @@ import type { OrgData } from "uniorg";
|
|
|
4
4
|
* holds a bare underscore or caret org would read as a script.
|
|
5
5
|
*/
|
|
6
6
|
export declare function requireBracedScripts(uniorgAst: OrgData): void;
|
|
7
|
+
/**
|
|
8
|
+
* Whether org→md would consume a `^:{}` setting at the head of `org`:
|
|
9
|
+
* its text reads a script org would otherwise take for one.
|
|
10
|
+
* @param org The org text, the setting included.
|
|
11
|
+
* @returns Whether the setting is taken as the one md→org adds.
|
|
12
|
+
*/
|
|
13
|
+
export declare function consumesBracedScripts(org: string): boolean;
|
|
7
14
|
/**
|
|
8
15
|
* org→md: parses org, honoring its `^:` setting, and consuming `^:{}`
|
|
9
16
|
* where the text needs it: md→org adds it only then, so anywhere else
|
|
@@ -135,6 +135,16 @@ function takeBracedScripts(uniorgAst) {
|
|
|
135
135
|
return node.value !== "";
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether org→md would consume a `^:{}` setting at the head of `org`:
|
|
140
|
+
* its text reads a script org would otherwise take for one.
|
|
141
|
+
* @param org The org text, the setting included.
|
|
142
|
+
* @returns Whether the setting is taken as the one md→org adds.
|
|
143
|
+
*/
|
|
144
|
+
export function consumesBracedScripts(org) {
|
|
145
|
+
// text without a bare script candidate skips the parse
|
|
146
|
+
return (BARE_SCRIPT_RE.test(org) && readsBareScripts(bracedScriptsParser.parse(org)));
|
|
147
|
+
}
|
|
138
148
|
/**
|
|
139
149
|
* org→md: parses org, honoring its `^:` setting, and consuming `^:{}`
|
|
140
150
|
* where the text needs it: md→org adds it only then, so anywhere else
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { OrgData } from "uniorg";
|
|
2
|
+
import { type Scalar } from "yaml";
|
|
3
|
+
export interface FrontmatterNode {
|
|
4
|
+
type: "morg-frontmatter";
|
|
5
|
+
yaml: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function isFrontmatterNode(node: {
|
|
8
|
+
type: string;
|
|
9
|
+
}): node is FrontmatterNode;
|
|
10
|
+
/**
|
|
11
|
+
* md→org: puts the file's header in place, past keywords any pass put
|
|
12
|
+
* in front: an Emacs mode line and the file-level drawer lead the file,
|
|
13
|
+
* keywords org would attach to what follows stay apart, and the
|
|
14
|
+
* frontmatter node becomes the marked comment block.
|
|
15
|
+
* @param uniorgAst The document.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderFileHeader(uniorgAst: OrgData): void;
|
|
18
|
+
/**
|
|
19
|
+
* md→org: takes the top-level entries `take` maps to keywords out of
|
|
20
|
+
* the frontmatter; the rest stays verbatim, comments included.
|
|
21
|
+
* @param yaml The frontmatter's YAML text.
|
|
22
|
+
* @param take Keywords for an entry, or null to leave it in place.
|
|
23
|
+
* @returns The keywords in order, and the remaining YAML text.
|
|
24
|
+
*/
|
|
25
|
+
export declare function takeFrontmatterEntries(yaml: string, take: (key: Scalar, value: unknown, text: (scalar: Scalar) => string) => [string, string][] | null): {
|
|
26
|
+
keywords: [string, string][];
|
|
27
|
+
yaml: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const KEYWORD_NAME: string;
|
|
30
|
+
export declare function fitsKeywordLine(key: string, value: string): boolean;
|
|
31
|
+
export declare function fitsPropertyLine(key: string, value: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* org→md: whether morg's own entries (`morg_properties`,
|
|
34
|
+
* `morg_keywords`) can join the frontmatter: whether the YAML with one
|
|
35
|
+
* more top-level entry appended still parses as a single block mapping
|
|
36
|
+
* holding none of them yet. Comments alone take them; an indented
|
|
37
|
+
* mapping, a list, a flow mapping or a document ending in `...` do not.
|
|
38
|
+
* @param yaml The frontmatter's YAML text.
|
|
39
|
+
*/
|
|
40
|
+
export declare function takesMorgEntries(yaml: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* md→org: one of morg's own entries, a sequence of one-entry maps
|
|
43
|
+
* (`morg_keywords`, `morg_properties`, ADR 0005), restored only if
|
|
44
|
+
* every item fits its org line.
|
|
45
|
+
* @param yaml The frontmatter's YAML text.
|
|
46
|
+
* @param name The entry's key.
|
|
47
|
+
* @param fits Whether a key and value fit the org line they go back to.
|
|
48
|
+
* @returns The key, value pairs in order, and the remaining YAML text.
|
|
49
|
+
*/
|
|
50
|
+
export declare function takeMorgEntry(yaml: string, name: string, fits: (key: string, value: string) => boolean): {
|
|
51
|
+
keywords: [string, string][];
|
|
52
|
+
yaml: string;
|
|
53
|
+
};
|
|
54
|
+
interface FileHeader {
|
|
55
|
+
frontmatter?: string;
|
|
56
|
+
fileProperties?: [string, string][];
|
|
57
|
+
takesMorgEntries: boolean;
|
|
58
|
+
startsWithModeLine: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* org→md: takes the file's header morg carries as frontmatter: the
|
|
62
|
+
* marked block and, where its YAML can take it, a file-level drawer.
|
|
63
|
+
* @param uniorgAst The parsed document.
|
|
64
|
+
* @param org The text it was parsed from.
|
|
65
|
+
* @param onWarning Reports a marked block morg cannot take.
|
|
66
|
+
* @returns The transform options holding them.
|
|
67
|
+
*/
|
|
68
|
+
export declare function takeFileHeader(uniorgAst: OrgData, org: string, onWarning?: (message: string) => void): FileHeader;
|
|
69
|
+
/**
|
|
70
|
+
* org→md, for a tree `transformMdastToUniorgAst` built rather than one
|
|
71
|
+
* parsed from org text: its file header, the block still raw text.
|
|
72
|
+
* @param uniorgAst The document; its header nodes are removed.
|
|
73
|
+
* @returns The transform options holding the header.
|
|
74
|
+
*/
|
|
75
|
+
export declare function takeRenderedFileHeader(uniorgAst: OrgData): FileHeader;
|
|
76
|
+
export declare function isModeLine(comment: string): boolean;
|
|
77
|
+
export declare function isModeLineComment(node: {
|
|
78
|
+
type: string;
|
|
79
|
+
}): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* md→org: marks the comment made from the Markdown body's first node as
|
|
82
|
+
* the file's mode line, if it is a -*- comment: what passes do to the
|
|
83
|
+
* tree before the header is rendered cannot make another one lead.
|
|
84
|
+
* @param node The uniorg node made from the body's first node.
|
|
85
|
+
*/
|
|
86
|
+
export declare function markModeLine(node: {
|
|
87
|
+
type: string;
|
|
88
|
+
} | null): void;
|
|
89
|
+
export {};
|