@remigius42/morg 0.4.0 → 0.5.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 +36 -30
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +22 -60
- package/dist/cli/error.js +1 -1
- package/dist/cli/flags.d.ts +27 -0
- package/dist/cli/flags.js +127 -0
- package/dist/cli/formats.js +18 -4
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +28 -0
- package/dist/cli.js +24 -1
- package/dist/conversionOptions.d.ts +2 -2
- package/dist/conversionOptions.js +2 -2
- package/dist/core/mdastToUniorg/blocks.js +1 -1
- package/dist/core/mdastToUniorg/index.js +1 -1
- package/dist/core/uniorgToMdast/elements.js +1 -1
- package/dist/core/uniorgToMdast/lists.js +1 -1
- package/dist/fileNames.d.ts +1 -1
- package/dist/fileNames.js +1 -1
- package/dist/markdownToOrg.js +1 -1
- package/dist/normalize.d.ts +2 -2
- package/dist/normalize.js +1 -1
- package/dist/options.d.ts +3 -3
- package/dist/orgToMarkdown.js +1 -1
- package/dist/presets/logseq.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Copyright 2026 [Andreas Remigius Schmidt](https://github.com/remigius42)
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@remigius42/morg)
|
|
6
|
+
[](https://github.com/remigius42/morg/blob/main/CHANGELOG.md)
|
|
6
7
|
[](LICENSE)
|
|
7
8
|
[](https://github.com/remigius42/morg/actions/workflows/ci.yml)
|
|
8
9
|

|
|
@@ -15,9 +16,9 @@ Bidirectional **Markdown ↔ Org-mode** converter, built on the
|
|
|
15
16
|
[uniorg](https://github.com/rasendubi/uniorg) for Org).
|
|
16
17
|
|
|
17
18
|
morg treats Org as a canonical plain-text format and Markdown (Obsidian,
|
|
18
|
-
generic) as the interop surface. Dialect conventions
|
|
19
|
-
[Logseq](https://docs.logseq.com/)'s `heading::` properties and outline
|
|
20
|
-
|
|
19
|
+
generic) as the interop surface. Dialect conventions, such as
|
|
20
|
+
[Logseq](https://docs.logseq.com/)'s `heading::` properties and outline
|
|
21
|
+
nesting, are supported via presets.
|
|
21
22
|
|
|
22
23
|
## Round-trip convergence
|
|
23
24
|
|
|
@@ -50,7 +51,7 @@ color schemes.
|
|
|
50
51
|
### Web UI
|
|
51
52
|
|
|
52
53
|
Try morg without installing anything at
|
|
53
|
-
[morg.binarypoetry.ch](https://morg.binarypoetry.ch)
|
|
54
|
+
[morg.binarypoetry.ch](https://morg.binarypoetry.ch). All conversion
|
|
54
55
|
happens in your browser, nothing is uploaded (see [ADR
|
|
55
56
|
0003](docs/adr/0003-client-side-web-ui-on-github-pages.md)). The
|
|
56
57
|
chrome-less embed page (`/embed.html`, optionally with
|
|
@@ -67,13 +68,13 @@ addEventListener("message", event => {
|
|
|
67
68
|
})
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
Give the frame at least 768px of width if you can
|
|
71
|
+
Give the frame at least 768px of width if you can; below that the
|
|
71
72
|
input and output stack, which doubles its height. `allow="clipboard-write"`
|
|
72
73
|
lets the Copy button use the clipboard rather than falling back to
|
|
73
74
|
selecting the output.
|
|
74
75
|
|
|
75
76
|
Besides pasting, a file can be opened with the picker or dropped
|
|
76
|
-
anywhere on the page
|
|
77
|
+
anywhere on the page: a `.toml` lands in the config panel, a document
|
|
77
78
|
in the input, and the conversion direction follows the extension. Drop
|
|
78
79
|
both at once and each goes where it belongs; an overlay names what is
|
|
79
80
|
accepted while a drag is in flight, and anything that turns out not to
|
|
@@ -100,7 +101,11 @@ npm install --global @remigius42/morg
|
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
```bash
|
|
103
|
-
#
|
|
104
|
+
# Every flag, with examples; also shown for a bare `morg`
|
|
105
|
+
morg --help
|
|
106
|
+
|
|
107
|
+
# Formats inferred from file extensions; --from and --to take a format
|
|
108
|
+
# name (markdown or org), not a path
|
|
104
109
|
morg --input notes.md --output notes.org
|
|
105
110
|
|
|
106
111
|
# stdin/stdout with explicit format
|
|
@@ -120,8 +125,8 @@ morg --input notes.md --output notes.org --silent
|
|
|
120
125
|
morg --input notes.md --output notes.org --record-style
|
|
121
126
|
|
|
122
127
|
# Normalize to canonical form (same format in and out); this
|
|
123
|
-
# canonicalizes
|
|
124
|
-
#
|
|
128
|
+
# canonicalizes (the one-time reformat a first conversion would apply
|
|
129
|
+
# anyway, ADR 0001); it is not a style formatter like prettier
|
|
125
130
|
morg normalize --input notes.org --output notes.org
|
|
126
131
|
```
|
|
127
132
|
|
|
@@ -138,8 +143,8 @@ preset = "logseq"
|
|
|
138
143
|
emphasis = "_" # align with prettier
|
|
139
144
|
```
|
|
140
145
|
|
|
141
|
-
The full reference
|
|
142
|
-
prettier and mdformat
|
|
146
|
+
The full reference, covering all sections and compatibility snippets
|
|
147
|
+
for prettier and mdformat, is in
|
|
143
148
|
[docs/CONFIGURATION.md](docs/CONFIGURATION.md).
|
|
144
149
|
|
|
145
150
|
### Library
|
|
@@ -167,26 +172,26 @@ const logseqOrg = convertMarkdownToOrg(markdown, { preset: logseq() })
|
|
|
167
172
|
Options (flags accept `boolean` or a per-construct `Record<string, boolean>`):
|
|
168
173
|
|
|
169
174
|
- `convertMarkdownToOrg(md, { preserveMdisms, interpretHtml, recordStyle,
|
|
170
|
-
preset })
|
|
175
|
+
preset })`:
|
|
171
176
|
`preserveMdisms` default `true`; `interpretHtml` (default `false`,
|
|
172
177
|
CLI `--interpret-html`) interprets the HTML vocabulary morg itself
|
|
173
178
|
emits under `useHtml` (bare `<u>`, `<sup>`, `<sub>`, `<dl>`) as
|
|
174
|
-
native Org constructs
|
|
179
|
+
native Org constructs, the inverse of `useHtml`: with both enabled
|
|
175
180
|
the round trip is lossless, with `interpretHtml` alone it converges
|
|
176
181
|
away from HTML (cleanup mode); other HTML preserves as usual;
|
|
177
182
|
`recordStyle` (default `false`, CLI `--record-style`) records the
|
|
178
183
|
document-level markdown style as a `#+MORG_MARKDOWN_STYLE:` keyword so the
|
|
179
184
|
round trip restores it (ADR 0004)
|
|
180
185
|
- `convertOrgToMarkdown(org, { preserveOrgisms, useHtml, taskCheckboxes,
|
|
181
|
-
preset })
|
|
186
|
+
preset })`: `preserveOrgisms` default `true`; `useHtml` (default
|
|
182
187
|
`false`) renders org-only markup as raw HTML (`<u>`, `<sup>`, `<sub>`,
|
|
183
188
|
`<dl>`) instead of keeping it verbatim; `taskCheckboxes` (default
|
|
184
189
|
`false`, CLI `--task-checkboxes`) is a lossy export mode that maps
|
|
185
190
|
bare `TODO`/`DONE` leaf headlines to GFM task items (`- [ ]` /
|
|
186
|
-
`- [x]`)
|
|
191
|
+
`- [x]`); headings become list items and do not restore on the
|
|
187
192
|
return trip; anything with priority, tags or content keeps its
|
|
188
193
|
heading and reports via `onWarning`
|
|
189
|
-
- `logseq({ nestUnderHeadings })
|
|
194
|
+
- `logseq({ nestUnderHeadings })`: default `true`; content following a
|
|
190
195
|
heading nests as child blocks of that heading: paragraphs become child
|
|
191
196
|
headlines one level deeper (in Logseq org every outline block is a
|
|
192
197
|
headline), other constructs stay in the preceding block's body. The
|
|
@@ -199,22 +204,23 @@ preset })` — `preserveOrgisms` default `true`; `useHtml` (default
|
|
|
199
204
|
fuzzy links `[[page][label]]`, block refs `[label](((uuid)))` ↔
|
|
200
205
|
`[[((uuid))][label]]`, and `^^highlight^^` markup survives verbatim
|
|
201
206
|
(it would otherwise re-parse as superscripts).
|
|
202
|
-
- `obsidian()
|
|
207
|
+
- `obsidian()`: wikilinks `[[Page]]` / `[[Page|alias]]` ↔ org fuzzy links
|
|
203
208
|
|
|
204
209
|
- `normalizeMarkdown(md, { preset })` / `normalizeOrg(org, { preset })`
|
|
205
|
-
(CLI: `morg normalize`)
|
|
210
|
+
(CLI: `morg normalize`): one full round trip to morg's canonical
|
|
206
211
|
form, a fixed point. Canonicalization, not styling: org-isms and
|
|
207
212
|
md-isms are rewritten exactly as a conversion would rewrite them.
|
|
208
|
-
Normalize with the same preset/config you will convert with
|
|
213
|
+
Normalize with the same preset/config you will convert with, since
|
|
209
214
|
convergence is per-config (ADR 0002).
|
|
210
215
|
|
|
211
216
|
- `markdownStyle: { bullet, emphasis, strong, fence, rule, ruleRepetition }`
|
|
212
217
|
(on `convertOrgToMarkdown` and `normalizeMarkdown`; CLI `--bullet`,
|
|
213
218
|
`--emphasis`, `--strong`, `--fence`, `--rule`, `--rule-repetition`)
|
|
214
|
-
|
|
215
|
-
`--emphasis _` aligns fully with prettier.
|
|
219
|
+
are Markdown output style knobs. Defaults match prettier except
|
|
220
|
+
emphasis (`*italic*`); `--emphasis _` aligns fully with prettier.
|
|
221
|
+
Canonical form is
|
|
216
222
|
per-config (ADR 0001): round trips must use the same style. Note
|
|
217
|
-
CommonMark/GFM prescribe no style
|
|
223
|
+
CommonMark/GFM prescribe no style; these defaults are morg's
|
|
218
224
|
canonical choices, not a standard.
|
|
219
225
|
|
|
220
226
|
Both convert functions also accept `onWarning: message => …`, called for
|
|
@@ -233,8 +239,8 @@ org → md: uniorg-parse → preset extraction → uniorg→mdast (core) → re
|
|
|
233
239
|
```
|
|
234
240
|
|
|
235
241
|
Formatting is controlled by shaping the AST (e.g. inserting newline text nodes),
|
|
236
|
-
not by custom stringifier handlers
|
|
237
|
-
the rendering.
|
|
242
|
+
not by custom stringifier handlers. The default, battle-tested
|
|
243
|
+
stringifiers do the rendering.
|
|
238
244
|
|
|
239
245
|
Project vocabulary lives in [CONTEXT.md](CONTEXT.md); design decisions in
|
|
240
246
|
[docs/adr/](docs/adr/).
|
|
@@ -245,14 +251,14 @@ The core conversion surface is feature-complete and validated against
|
|
|
245
251
|
real-world Logseq org vaults (edge cases found there live on as
|
|
246
252
|
anonymized fixtures, e.g. `tests/fixtures/logseq-vault.org`); the
|
|
247
253
|
client-side [Web UI](https://morg.binarypoetry.ch) is deployed from
|
|
248
|
-
`main`. The npm package is `@remigius42/morg
|
|
249
|
-
taken
|
|
254
|
+
`main`. The npm package is `@remigius42/morg`, since the bare `morg`
|
|
255
|
+
name is taken, and pushing a `v*` tag publishes it. Most of the code is
|
|
250
256
|
written with an AI coding agent under human direction, test-first and
|
|
251
|
-
CI-gated
|
|
257
|
+
CI-gated. See
|
|
252
258
|
the [contributing guide](CONTRIBUTING.md#development-process).
|
|
253
259
|
|
|
254
|
-
How each construct maps
|
|
255
|
-
documented drops
|
|
260
|
+
How each construct maps, including deliberate normalizations and
|
|
261
|
+
documented drops, is covered in the
|
|
256
262
|
[mapping reference](docs/mappings.md). Notable changes are tracked in
|
|
257
263
|
the [changelog](CHANGELOG.md).
|
|
258
264
|
|
package/dist/cli/args.d.ts
CHANGED
package/dist/cli/args.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "./error.js";
|
|
2
|
+
import { FLAGS_BY_NAME } from "./flags.js";
|
|
2
3
|
export function parseArgs(args) {
|
|
3
4
|
// `morg normalize` canonicalizes in place of converting: same format
|
|
4
5
|
// in and out, one full round trip (see ADR 0001)
|
|
@@ -8,6 +9,8 @@ export function parseArgs(args) {
|
|
|
8
9
|
}
|
|
9
10
|
const parsed = {
|
|
10
11
|
normalize,
|
|
12
|
+
help: false,
|
|
13
|
+
version: false,
|
|
11
14
|
fromFormat: undefined,
|
|
12
15
|
toFormat: undefined,
|
|
13
16
|
inputFile: undefined,
|
|
@@ -23,35 +26,12 @@ export function parseArgs(args) {
|
|
|
23
26
|
parseFlags(parsed, args);
|
|
24
27
|
return parsed;
|
|
25
28
|
}
|
|
26
|
-
const VALUE_FLAGS = new Set([
|
|
27
|
-
"--config",
|
|
28
|
-
"--bullet",
|
|
29
|
-
"--emphasis",
|
|
30
|
-
"--strong",
|
|
31
|
-
"--fence",
|
|
32
|
-
"--rule",
|
|
33
|
-
"--rule-repetition",
|
|
34
|
-
"--from",
|
|
35
|
-
"--to",
|
|
36
|
-
"--input",
|
|
37
|
-
"--output",
|
|
38
|
-
"--preset"
|
|
39
|
-
]);
|
|
40
|
-
const BOOLEAN_FLAGS = new Map([
|
|
41
|
-
["-s", "silent"],
|
|
42
|
-
["--silent", "silent"],
|
|
43
|
-
["--task-checkboxes", "taskCheckboxes"],
|
|
44
|
-
["--interpret-html", "interpretHtml"],
|
|
45
|
-
["--record-style", "recordStyle"]
|
|
46
|
-
]);
|
|
47
29
|
// a following flag means the value was forgotten; a lone `-` is a legitimate
|
|
48
30
|
// bullet or rule character, so only recognized flag tokens disqualify
|
|
49
31
|
function takeValue(args, index) {
|
|
50
32
|
const flag = args[index];
|
|
51
33
|
const value = args[index + 1];
|
|
52
|
-
if (value === undefined ||
|
|
53
|
-
VALUE_FLAGS.has(value) ||
|
|
54
|
-
BOOLEAN_FLAGS.has(value)) {
|
|
34
|
+
if (value === undefined || FLAGS_BY_NAME.has(value)) {
|
|
55
35
|
throw new CliError(`${flag} requires a value`);
|
|
56
36
|
}
|
|
57
37
|
return value;
|
|
@@ -59,47 +39,29 @@ function takeValue(args, index) {
|
|
|
59
39
|
function parseFlags(parsed, args) {
|
|
60
40
|
for (let i = 0; i < args.length; i++) {
|
|
61
41
|
const arg = args[i] ?? "";
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const value = args[i + 1];
|
|
66
|
-
if (value === "true" || value === "false") {
|
|
67
|
-
i++;
|
|
68
|
-
}
|
|
69
|
-
parsed[option] = value !== "false";
|
|
70
|
-
continue;
|
|
42
|
+
const spec = FLAGS_BY_NAME.get(arg);
|
|
43
|
+
if (!spec) {
|
|
44
|
+
throw new CliError(`Unknown argument: ${arg}`);
|
|
71
45
|
}
|
|
72
|
-
switch (
|
|
73
|
-
case "
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
case "--rule":
|
|
81
|
-
parsed.markdownStyle[arg.slice(2)] = takeValue(args, i++);
|
|
82
|
-
break;
|
|
83
|
-
case "--rule-repetition":
|
|
84
|
-
parsed.markdownStyle.ruleRepetition = takeValue(args, i++);
|
|
46
|
+
switch (spec.kind) {
|
|
47
|
+
case "boolean": {
|
|
48
|
+
// boolean flags take an optional `true`/`false`; bare means true
|
|
49
|
+
const value = args[i + 1];
|
|
50
|
+
if (value === "true" || value === "false") {
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
parsed[spec.key] = value !== "false";
|
|
85
54
|
break;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
case "--to":
|
|
90
|
-
parsed.toFormat = takeValue(args, i++);
|
|
91
|
-
break;
|
|
92
|
-
case "--input":
|
|
93
|
-
parsed.inputFile = takeValue(args, i++);
|
|
55
|
+
}
|
|
56
|
+
case "info":
|
|
57
|
+
parsed[spec.key] = true;
|
|
94
58
|
break;
|
|
95
|
-
case "
|
|
96
|
-
parsed.
|
|
59
|
+
case "string":
|
|
60
|
+
parsed[spec.key] = takeValue(args, i++);
|
|
97
61
|
break;
|
|
98
|
-
case "
|
|
99
|
-
parsed.
|
|
62
|
+
case "style":
|
|
63
|
+
parsed.markdownStyle[spec.key] = takeValue(args, i++);
|
|
100
64
|
break;
|
|
101
|
-
default:
|
|
102
|
-
throw new CliError(`Unknown argument: ${arg}`);
|
|
103
65
|
}
|
|
104
66
|
}
|
|
105
67
|
}
|
package/dist/cli/error.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type StringOption = "fromFormat" | "toFormat" | "inputFile" | "outputFile" | "presetName" | "configPath";
|
|
2
|
+
type BooleanOption = "silent" | "taskCheckboxes" | "interpretHtml" | "recordStyle";
|
|
3
|
+
type InfoOption = "help" | "version";
|
|
4
|
+
export type FlagSpec = ({
|
|
5
|
+
names: string[];
|
|
6
|
+
arg: string;
|
|
7
|
+
description: string;
|
|
8
|
+
} & ({
|
|
9
|
+
kind: "string";
|
|
10
|
+
key: StringOption;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "style";
|
|
13
|
+
key: string;
|
|
14
|
+
})) | ({
|
|
15
|
+
names: string[];
|
|
16
|
+
arg?: undefined;
|
|
17
|
+
description: string;
|
|
18
|
+
} & ({
|
|
19
|
+
kind: "boolean";
|
|
20
|
+
key: BooleanOption;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "info";
|
|
23
|
+
key: InfoOption;
|
|
24
|
+
}));
|
|
25
|
+
export declare const FLAGS: FlagSpec[];
|
|
26
|
+
export declare const FLAGS_BY_NAME: Map<string, FlagSpec>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// the single source of truth for the CLI surface: the parser dispatches on
|
|
2
|
+
// these entries and the help text is rendered from them, so a flag cannot
|
|
3
|
+
// exist undocumented or be documented without existing
|
|
4
|
+
// declaration order is the order the help text lists them in
|
|
5
|
+
export const FLAGS = [
|
|
6
|
+
{
|
|
7
|
+
names: ["--from"],
|
|
8
|
+
arg: "<format>",
|
|
9
|
+
description: "Source format: markdown or org",
|
|
10
|
+
kind: "string",
|
|
11
|
+
key: "fromFormat"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
names: ["--to"],
|
|
15
|
+
arg: "<format>",
|
|
16
|
+
description: "Target format: markdown or org",
|
|
17
|
+
kind: "string",
|
|
18
|
+
key: "toFormat"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
names: ["--input"],
|
|
22
|
+
arg: "<file>",
|
|
23
|
+
description: "Read from a file instead of stdin",
|
|
24
|
+
kind: "string",
|
|
25
|
+
key: "inputFile"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
names: ["--output"],
|
|
29
|
+
arg: "<file>",
|
|
30
|
+
description: "Write to a file instead of stdout",
|
|
31
|
+
kind: "string",
|
|
32
|
+
key: "outputFile"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
names: ["--preset"],
|
|
36
|
+
arg: "<name>",
|
|
37
|
+
description: "Apply an editor preset, for example logseq",
|
|
38
|
+
kind: "string",
|
|
39
|
+
key: "presetName"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
names: ["--config"],
|
|
43
|
+
arg: "<file>",
|
|
44
|
+
description: "Read settings from a TOML file",
|
|
45
|
+
kind: "string",
|
|
46
|
+
key: "configPath"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
names: ["-s", "--silent"],
|
|
50
|
+
description: "Suppress warnings about dropped constructs",
|
|
51
|
+
kind: "boolean",
|
|
52
|
+
key: "silent"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
names: ["--task-checkboxes"],
|
|
56
|
+
description: "Render Org TODO keywords as Markdown checkboxes",
|
|
57
|
+
kind: "boolean",
|
|
58
|
+
key: "taskCheckboxes"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
names: ["--interpret-html"],
|
|
62
|
+
description: "Convert inline HTML instead of passing it through",
|
|
63
|
+
kind: "boolean",
|
|
64
|
+
key: "interpretHtml"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
names: ["--record-style"],
|
|
68
|
+
description: "Record the detected Markdown style as front matter",
|
|
69
|
+
kind: "boolean",
|
|
70
|
+
key: "recordStyle"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
names: ["-h", "--help"],
|
|
74
|
+
description: "Show this help",
|
|
75
|
+
kind: "info",
|
|
76
|
+
key: "help"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
names: ["--version"],
|
|
80
|
+
description: "Show the version",
|
|
81
|
+
kind: "info",
|
|
82
|
+
key: "version"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
names: ["--bullet"],
|
|
86
|
+
arg: "<char>",
|
|
87
|
+
description: "List bullet character",
|
|
88
|
+
kind: "style",
|
|
89
|
+
key: "bullet"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
names: ["--emphasis"],
|
|
93
|
+
arg: "<char>",
|
|
94
|
+
description: "Emphasis marker",
|
|
95
|
+
kind: "style",
|
|
96
|
+
key: "emphasis"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
names: ["--strong"],
|
|
100
|
+
arg: "<char>",
|
|
101
|
+
description: "Strong emphasis marker",
|
|
102
|
+
kind: "style",
|
|
103
|
+
key: "strong"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
names: ["--fence"],
|
|
107
|
+
arg: "<char>",
|
|
108
|
+
description: "Code fence character",
|
|
109
|
+
kind: "style",
|
|
110
|
+
key: "fence"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
names: ["--rule"],
|
|
114
|
+
arg: "<char>",
|
|
115
|
+
description: "Thematic break character",
|
|
116
|
+
kind: "style",
|
|
117
|
+
key: "rule"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
names: ["--rule-repetition"],
|
|
121
|
+
arg: "<n>",
|
|
122
|
+
description: "Thematic break character count",
|
|
123
|
+
kind: "style",
|
|
124
|
+
key: "ruleRepetition"
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
export const FLAGS_BY_NAME = new Map(FLAGS.flatMap(spec => spec.names.map(name => [name, spec])));
|
package/dist/cli/formats.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "./error.js";
|
|
2
|
-
import { formatFromFileName } from "../fileNames.js";
|
|
2
|
+
import { formatFromFileName, splitFileName } from "../fileNames.js";
|
|
3
3
|
export function inferFormats(cli) {
|
|
4
4
|
let { fromFormat, toFormat } = cli;
|
|
5
5
|
// Infer formats from file extensions first
|
|
@@ -27,14 +27,28 @@ function inferMissingFormat(normalize, fromFormat, toFormat) {
|
|
|
27
27
|
}
|
|
28
28
|
return [fromFormat, toFormat];
|
|
29
29
|
}
|
|
30
|
+
// --from and --to read like the file pair they usually accompany, so a file
|
|
31
|
+
// name given to one is the likely mistake behind an unsupported format
|
|
32
|
+
function fileNameHint(flag, value) {
|
|
33
|
+
if (splitFileName(value).extension === "") {
|
|
34
|
+
return "";
|
|
35
|
+
}
|
|
36
|
+
const fileFlag = flag === "--from" ? "--input" : "--output";
|
|
37
|
+
return `\n${flag} takes a format name; for a file use ${fileFlag} ${value}.`;
|
|
38
|
+
}
|
|
30
39
|
export function validateFormats(fromFormat, toFormat, normalize) {
|
|
31
40
|
if (!fromFormat || !toFormat) {
|
|
32
41
|
throw new CliError("Error: Could not determine conversion formats.\n" +
|
|
33
42
|
"Please specify --from and --to, or provide input/output files with .md or .org extensions.");
|
|
34
43
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
for (const [flag, value] of [
|
|
45
|
+
["--from", fromFormat],
|
|
46
|
+
["--to", toFormat]
|
|
47
|
+
]) {
|
|
48
|
+
if (!["markdown", "org"].includes(value)) {
|
|
49
|
+
throw new CliError(`Error: Unsupported format '${value}'. Supported formats are ` +
|
|
50
|
+
`'markdown' and 'org'.${fileNameHint(flag, value)}`);
|
|
51
|
+
}
|
|
38
52
|
}
|
|
39
53
|
if (!normalize && fromFormat === toFormat) {
|
|
40
54
|
throw new CliError("Error: Source and target formats cannot be the same.");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HELP_TEXT: string;
|
package/dist/cli/help.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FLAGS } from "./flags.js";
|
|
2
|
+
const signature = (spec) => spec.arg ? `${spec.names.join(", ")} ${spec.arg}` : spec.names.join(", ");
|
|
3
|
+
// one column width across both sections, so the descriptions line up
|
|
4
|
+
const DESCRIPTION_COLUMN = Math.max(...FLAGS.map(spec => signature(spec).length)) + 2;
|
|
5
|
+
const section = (specs) => specs
|
|
6
|
+
.map(spec => ` ${signature(spec).padEnd(DESCRIPTION_COLUMN)}${spec.description}`)
|
|
7
|
+
.join("\n");
|
|
8
|
+
export const HELP_TEXT = `Usage: morg [normalize] [options]
|
|
9
|
+
|
|
10
|
+
Convert between Markdown and Org-mode. Reads stdin and writes stdout
|
|
11
|
+
unless --input/--output are given; formats are inferred from the .md and
|
|
12
|
+
.org file extensions, so --from/--to are only needed for stdin or stdout.
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
${"normalize".padEnd(DESCRIPTION_COLUMN)}Round-trip a document through the other format
|
|
16
|
+
${"".padEnd(DESCRIPTION_COLUMN)}and back, canonicalizing it in its own format
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
${section(FLAGS.filter(spec => spec.kind !== "style"))}
|
|
20
|
+
|
|
21
|
+
Markdown style:
|
|
22
|
+
${section(FLAGS.filter(spec => spec.kind === "style"))}
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
morg --input notes.md --output notes.org
|
|
26
|
+
morg --from markdown < notes.md > notes.org
|
|
27
|
+
morg normalize --input notes.org --output notes.org
|
|
28
|
+
`;
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,13 @@ import { resolvePreset } from "./cli/presets.js";
|
|
|
6
6
|
import { inferFormats, validateFormats } from "./cli/formats.js";
|
|
7
7
|
import { convert } from "./cli/conversion.js";
|
|
8
8
|
import { CliError } from "./cli/error.js";
|
|
9
|
+
import { HELP_TEXT } from "./cli/help.js";
|
|
10
|
+
// both src/cli.ts and the built dist/cli.js sit one level below the package
|
|
11
|
+
// root, so the manifest is at the same relative path either way
|
|
12
|
+
function packageVersion() {
|
|
13
|
+
const manifest = fs.readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
14
|
+
return JSON.parse(manifest).version;
|
|
15
|
+
}
|
|
9
16
|
async function readInput(inputFile) {
|
|
10
17
|
if (inputFile) {
|
|
11
18
|
return fs.readFileSync(inputFile, "utf8");
|
|
@@ -23,7 +30,21 @@ async function readInput(inputFile) {
|
|
|
23
30
|
});
|
|
24
31
|
}
|
|
25
32
|
async function main() {
|
|
26
|
-
const
|
|
33
|
+
const argv = process.argv.slice(2);
|
|
34
|
+
// a bare invocation has nothing to convert, so it asks for help
|
|
35
|
+
if (argv.length === 0) {
|
|
36
|
+
console.log(HELP_TEXT);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const cli = parseArgs(argv);
|
|
40
|
+
if (cli.help) {
|
|
41
|
+
console.log(HELP_TEXT);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (cli.version) {
|
|
45
|
+
console.log(packageVersion());
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
27
48
|
const config = loadConfig(cli.configPath);
|
|
28
49
|
const preset = resolvePreset(cli.presetName ?? config.preset);
|
|
29
50
|
const [fromFormat, toFormat] = inferFormats(cli);
|
|
@@ -41,10 +62,12 @@ main().catch((error) => {
|
|
|
41
62
|
if (error instanceof CliError) {
|
|
42
63
|
// helpers throw instead of exiting; this is the only exit point
|
|
43
64
|
if (error.cause !== undefined) {
|
|
65
|
+
// a cause means the input or environment failed, not the invocation
|
|
44
66
|
console.error(error.message, error.cause);
|
|
45
67
|
}
|
|
46
68
|
else {
|
|
47
69
|
console.error(error.message);
|
|
70
|
+
console.error("Run 'morg --help' for usage.");
|
|
48
71
|
}
|
|
49
72
|
}
|
|
50
73
|
else {
|
|
@@ -17,8 +17,8 @@ export interface SharedConversionOptions {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Layers explicit overrides over the config file, per direction. Used by
|
|
20
|
-
* both adapters (CLI and Web UI) so the documented precedence
|
|
21
|
-
* form > config > defaults
|
|
20
|
+
* both adapters (CLI and Web UI) so the documented precedence (CLI or
|
|
21
|
+
* form > config > defaults) means the same thing in each.
|
|
22
22
|
* @param overrides Explicitly requested values; `undefined` defers to config.
|
|
23
23
|
* @param config The parsed `morg.toml`.
|
|
24
24
|
* @param shared Values that apply to both directions.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Layers explicit overrides over the config file, per direction. Used by
|
|
3
|
-
* both adapters (CLI and Web UI) so the documented precedence
|
|
4
|
-
* form > config > defaults
|
|
3
|
+
* both adapters (CLI and Web UI) so the documented precedence (CLI or
|
|
4
|
+
* form > config > defaults) means the same thing in each.
|
|
5
5
|
* @param overrides Explicitly requested values; `undefined` defers to config.
|
|
6
6
|
* @param config The parsed `morg.toml`.
|
|
7
7
|
* @param shared Values that apply to both directions.
|
|
@@ -98,7 +98,7 @@ export function transformMdastHtml(ctx, node) {
|
|
|
98
98
|
: null;
|
|
99
99
|
}
|
|
100
100
|
// a bare <dl> whose body is nothing but attribute-less <dt>/<dd> pairs
|
|
101
|
-
// (any whitespace between tags) becomes a ` :: ` list
|
|
101
|
+
// (any whitespace between tags) becomes a ` :: ` list, the same
|
|
102
102
|
// markdown convention descriptive lists use without useHtml, so the
|
|
103
103
|
// org side re-parses it as a native descriptive list; anything richer
|
|
104
104
|
// stays a preserved md-ism
|
|
@@ -45,7 +45,7 @@ export function transformMdastNodeToUniorgNode(ctx, node) {
|
|
|
45
45
|
case "paragraph": {
|
|
46
46
|
// a paragraph of only #+KEY: lines is affiliated keywords (or
|
|
47
47
|
// mid-file keywords) traveling verbatim; emit as raw text so they
|
|
48
|
-
// glue to the following element without a blank line
|
|
48
|
+
// glue to the following element without a blank line: org only
|
|
49
49
|
// attaches affiliated keywords when directly above their element
|
|
50
50
|
const keywordLines = keywordOnlyLines(node);
|
|
51
51
|
if (keywordLines) {
|
|
@@ -191,7 +191,7 @@ function transformParagraph(ctx, node) {
|
|
|
191
191
|
const children = transformUniorgObjects(ctx, node.children);
|
|
192
192
|
// md gives leading whitespace structural meaning (list
|
|
193
193
|
// continuation, code); collapse per-line indentation inside
|
|
194
|
-
// paragraphs
|
|
194
|
+
// paragraphs, insignificant in org and in rendered md alike
|
|
195
195
|
children.forEach((child, index) => {
|
|
196
196
|
if (child.type === "text") {
|
|
197
197
|
child.value = child.value.replace(/\n[ \t]+/g, "\n");
|
|
@@ -69,7 +69,7 @@ function transformUniorgList(ctx, node) {
|
|
|
69
69
|
}
|
|
70
70
|
function transformUniorgListItem(ctx, item) {
|
|
71
71
|
// md has no descriptive lists, so keep the ` :: ` syntax literally in the
|
|
72
|
-
// item text
|
|
72
|
+
// item text; the return trip re-parses it as a descriptive list
|
|
73
73
|
const tag = listItemTag(item);
|
|
74
74
|
const children = transformNodes(ctx, (item.children || []).filter(child => child !== tag));
|
|
75
75
|
if (tag) {
|
package/dist/fileNames.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Splits a file name into the part a derived name keeps and the extension
|
|
3
3
|
* that carries its format. A dotless name and a dotfile both have no
|
|
4
|
-
* extension
|
|
4
|
+
* extension: `org` is a file called org, and `.org` is a hidden file whose
|
|
5
5
|
* name happens to start with a dot.
|
|
6
6
|
*
|
|
7
7
|
* The extension comes from the last path segment, since the CLI is handed
|
package/dist/fileNames.js
CHANGED
|
@@ -6,7 +6,7 @@ const DOCUMENT_FORMATS = new Map([
|
|
|
6
6
|
/**
|
|
7
7
|
* Splits a file name into the part a derived name keeps and the extension
|
|
8
8
|
* that carries its format. A dotless name and a dotfile both have no
|
|
9
|
-
* extension
|
|
9
|
+
* extension: `org` is a file called org, and `.org` is a hidden file whose
|
|
10
10
|
* name happens to start with a dot.
|
|
11
11
|
*
|
|
12
12
|
* The extension comes from the last path segment, since the CLI is handed
|
package/dist/markdownToOrg.js
CHANGED
|
@@ -40,7 +40,7 @@ export function convertMarkdownToOrg(markdown, options = {}) {
|
|
|
40
40
|
canonical
|
|
41
41
|
]));
|
|
42
42
|
restoreOrgisms(uniorgAst, canonicalKeys);
|
|
43
|
-
// Phase 2c: formatting-as-structure
|
|
43
|
+
// Phase 2c: formatting-as-structure. Adjacent lists need two blank
|
|
44
44
|
// lines between them, or org's parser merges them into one list.
|
|
45
45
|
separateAdjacentLists(uniorgAst);
|
|
46
46
|
// Phase 2d: record the source's own style markers, so the return trip
|
package/dist/normalize.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MarkdownToOrgOptions, OrgToMarkdownOptions } from "./options.js";
|
|
2
2
|
/**
|
|
3
3
|
* The full option set of both directions: normalization uses the same
|
|
4
|
-
* configuration as conversion
|
|
4
|
+
* configuration as conversion, because Convergence is per-config (ADR
|
|
5
5
|
* 0002), so a file must be normalized with the exact config (preset,
|
|
6
6
|
* style, key names, toggles) it will be converted with.
|
|
7
7
|
*/
|
|
@@ -9,7 +9,7 @@ export type NormalizeOptions = MarkdownToOrgOptions & OrgToMarkdownOptions;
|
|
|
9
9
|
/**
|
|
10
10
|
* Normalizes a Markdown string to morg's canonical form: one full round
|
|
11
11
|
* trip (`md → org → md`), whose output is a fixed point (see ADR 0001).
|
|
12
|
-
* This canonicalizes, it does not just re-style
|
|
12
|
+
* This canonicalizes, it does not just re-style: org-isms and md-isms
|
|
13
13
|
* are rewritten the same way a conversion would rewrite them.
|
|
14
14
|
* @param markdown The Markdown string to normalize.
|
|
15
15
|
* @param options Warning callback and dialect preset.
|
package/dist/normalize.js
CHANGED
|
@@ -3,7 +3,7 @@ import { convertOrgToMarkdown } from "./orgToMarkdown.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Normalizes a Markdown string to morg's canonical form: one full round
|
|
5
5
|
* trip (`md → org → md`), whose output is a fixed point (see ADR 0001).
|
|
6
|
-
* This canonicalizes, it does not just re-style
|
|
6
|
+
* This canonicalizes, it does not just re-style: org-isms and md-isms
|
|
7
7
|
* are rewritten the same way a conversion would rewrite them.
|
|
8
8
|
* @param markdown The Markdown string to normalize.
|
|
9
9
|
* @param options Warning callback and dialect preset.
|
package/dist/options.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface MarkdownToOrgOptions {
|
|
|
33
33
|
/**
|
|
34
34
|
* Custom names for org-ism `key::` lines, canonical → custom (e.g.
|
|
35
35
|
* `{ todo: "state" }`). Must match the mapping the file was written
|
|
36
|
-
* with
|
|
36
|
+
* with, since Convergence is per-config (ADR 0002).
|
|
37
37
|
*/
|
|
38
38
|
orgismKeys?: Record<string, string>;
|
|
39
39
|
/** Called for each construct dropped without an equivalent. */
|
|
@@ -46,7 +46,7 @@ export interface MarkdownToOrgOptions {
|
|
|
46
46
|
* Canonical form is parameterized by these (see ADR 0001): round trips
|
|
47
47
|
* must use the same style, and files formatted under one style are not
|
|
48
48
|
* a fixed point under another. Defaults: `-` bullet, `*` emphasis /
|
|
49
|
-
* `*` strong (i.e. `**bold**`), backtick fences, `-` rule
|
|
49
|
+
* `*` strong (i.e. `**bold**`), backtick fences, `-` rule: prettier's
|
|
50
50
|
* choices except emphasis (`emphasis: "_"` aligns with prettier).
|
|
51
51
|
*/
|
|
52
52
|
export interface MarkdownStyleOptions {
|
|
@@ -78,7 +78,7 @@ export interface OrgToMarkdownOptions {
|
|
|
78
78
|
* Render Org constructs without a Markdown equivalent as raw HTML
|
|
79
79
|
* (`<u>`, `<sup>`, `<sub>`, `<dl>`) instead of keeping their org markup
|
|
80
80
|
* verbatim. HTML round-trips as a preserved md-ism (export blocks and
|
|
81
|
-
* snippets), not back to native org markup
|
|
81
|
+
* snippets), not back to native org markup, a one-way door unless
|
|
82
82
|
* the return trip enables its inverse, `interpretHtml`.
|
|
83
83
|
* Default: `false`.
|
|
84
84
|
*/
|
package/dist/orgToMarkdown.js
CHANGED
|
@@ -15,7 +15,7 @@ import { takeRecordedStyle } from "./core/markdownStyle.js";
|
|
|
15
15
|
export function convertOrgToMarkdown(org, options = {}) {
|
|
16
16
|
// Phase 1: Parse Org-mode to uniorg-ast
|
|
17
17
|
let uniorgAst = unified().use(uniorgParse).parse(org);
|
|
18
|
-
// Phase 1b: a recorded style is morg's own (ADR 0004)
|
|
18
|
+
// Phase 1b: a recorded style is morg's own (ADR 0004), so consume it so
|
|
19
19
|
// it does not travel on as frontmatter; explicit options still win
|
|
20
20
|
const recordedStyle = takeRecordedStyle(uniorgAst);
|
|
21
21
|
// Phase 2: Extract dialect preset conventions, if any
|
package/dist/presets/logseq.js
CHANGED
|
@@ -191,7 +191,7 @@ function repairHighlights(uniorgAst) {
|
|
|
191
191
|
}
|
|
192
192
|
// Logseq page and block references: [[page]] stays a wikilink,
|
|
193
193
|
// [[page][label]] becomes [label]([[page]]), [[((uuid))][label]]
|
|
194
|
-
// becomes [label](((uuid)))
|
|
194
|
+
// becomes [label](((uuid))), emitted unescaped via verbatim-inline
|
|
195
195
|
const BLOCK_REF_RE = /^\(\(.*\)\)$/;
|
|
196
196
|
function pageRefValue(label, target) {
|
|
197
197
|
if (!label) {
|