@remigius42/morg 0.4.0 → 0.6.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 (46) hide show
  1. package/README.md +36 -30
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.js +22 -60
  4. package/dist/cli/error.js +1 -1
  5. package/dist/cli/flags.d.ts +27 -0
  6. package/dist/cli/flags.js +127 -0
  7. package/dist/cli/formats.js +18 -4
  8. package/dist/cli/help.d.ts +1 -0
  9. package/dist/cli/help.js +28 -0
  10. package/dist/cli.js +24 -1
  11. package/dist/conversionOptions.d.ts +2 -2
  12. package/dist/conversionOptions.js +2 -2
  13. package/dist/core/bracedScripts.d.ts +12 -0
  14. package/dist/core/bracedScripts.js +159 -0
  15. package/dist/core/footnoteReferences.d.ts +10 -0
  16. package/dist/core/footnoteReferences.js +24 -0
  17. package/dist/core/lineSyntax.d.ts +12 -0
  18. package/dist/core/lineSyntax.js +206 -0
  19. package/dist/core/markupBoundary.d.ts +12 -0
  20. package/dist/core/markupBoundary.js +195 -0
  21. package/dist/core/mdastToUniorg/blocks.js +1 -1
  22. package/dist/core/mdastToUniorg/index.js +10 -2
  23. package/dist/core/mdastToUniorg/lists.d.ts +1 -1
  24. package/dist/core/mdastToUniorg/lists.js +21 -2
  25. package/dist/core/mdastToUniorg/phrasing.js +145 -12
  26. package/dist/core/orgPath.d.ts +9 -0
  27. package/dist/core/orgPath.js +24 -0
  28. package/dist/core/render.d.ts +33 -0
  29. package/dist/core/render.js +101 -0
  30. package/dist/core/tablePipes.d.ts +8 -0
  31. package/dist/core/tablePipes.js +14 -0
  32. package/dist/core/underscoreBullets.d.ts +10 -0
  33. package/dist/core/underscoreBullets.js +32 -0
  34. package/dist/core/uniorgToMdast/elements.js +1 -1
  35. package/dist/core/uniorgToMdast/lists.js +12 -1
  36. package/dist/core/uniorgToMdast/objects.js +38 -6
  37. package/dist/core/uniorgToMdast/tables.js +12 -6
  38. package/dist/fileNames.d.ts +1 -1
  39. package/dist/fileNames.js +1 -1
  40. package/dist/markdownToOrg.js +19 -1
  41. package/dist/normalize.d.ts +2 -2
  42. package/dist/normalize.js +1 -1
  43. package/dist/options.d.ts +3 -3
  44. package/dist/orgToMarkdown.js +29 -5
  45. package/dist/presets/logseq.js +1 -1
  46. package/package.json +4 -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
- [![Version](https://img.shields.io/github/v/tag/remigius42/morg?label=version)](https://github.com/remigius42/morg/blob/main/CHANGELOG.md)
5
+ [![npm](https://img.shields.io/npm/v/%40remigius42%2Fmorg?label=npm)](https://www.npmjs.com/package/@remigius42/morg)
6
+ [![Changelog](https://img.shields.io/github/v/tag/remigius42/morg?label=changelog)](https://github.com/remigius42/morg/blob/main/CHANGELOG.md)
6
7
  [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE)
7
8
  [![CI](https://github.com/remigius42/morg/actions/workflows/ci.yml/badge.svg)](https://github.com/remigius42/morg/actions/workflows/ci.yml)
8
9
  ![Node](https://img.shields.io/badge/node-%3E%3D20-lightgrey.svg)
@@ -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 — such as
19
- [Logseq](https://docs.logseq.com/)'s `heading::` properties and outline nesting
20
- — are supported via presets.
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) — all conversion
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 — below that the
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 — a `.toml` lands in the config panel, a document
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
- # Formats inferred from file extensions
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 — the one-time reformat a first conversion would
124
- # apply anyway (ADR 0001) — it is not a style formatter like prettier
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 — all sections and compatibility snippets for
142
- prettier and mdformat — is in
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 — the inverse of `useHtml`: with both enabled
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 })` — `preserveOrgisms` default `true`; `useHtml` (default
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]`) — headings become list items and do not restore on the
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 })` — default `true`; content following a
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()` — wikilinks `[[Page]]` / `[[Page|alias]]` ↔ org fuzzy links
207
+ - `obsidian()`: wikilinks `[[Page]]` / `[[Page|alias]]` ↔ org fuzzy links
203
208
 
204
209
  - `normalizeMarkdown(md, { preset })` / `normalizeOrg(org, { preset })`
205
- (CLI: `morg normalize`) — one full round trip to morg's canonical
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
- — Markdown output style knobs. Defaults match prettier except emphasis (`*italic*`);
215
- `--emphasis _` aligns fully with prettier. Canonical form is
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 — these defaults are morg's
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 — the default, battle-tested stringifiers do
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` — the bare `morg` name is
249
- taken — and pushing a `v*` tag publishes it. Most of the code is
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 — see
257
+ CI-gated. See
252
258
  the [contributing guide](CONTRIBUTING.md#development-process).
253
259
 
254
- How each construct maps — including deliberate normalizations and
255
- documented drops — is covered in the
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
 
@@ -1,5 +1,7 @@
1
1
  export interface CliArgs {
2
2
  normalize: boolean;
3
+ help: boolean;
4
+ version: boolean;
3
5
  fromFormat: string | undefined;
4
6
  toFormat: string | undefined;
5
7
  inputFile: string | undefined;
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
- // boolean flags take an optional `true`/`false`; bare means true
63
- const option = BOOLEAN_FLAGS.get(arg);
64
- if (option) {
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 (arg) {
73
- case "--config":
74
- parsed.configPath = takeValue(args, i++);
75
- break;
76
- case "--bullet":
77
- case "--emphasis":
78
- case "--strong":
79
- case "--fence":
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
- case "--from":
87
- parsed.fromFormat = takeValue(args, i++);
88
- break;
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 "--output":
96
- parsed.outputFile = takeValue(args, i++);
59
+ case "string":
60
+ parsed[spec.key] = takeValue(args, i++);
97
61
  break;
98
- case "--preset":
99
- parsed.presetName = takeValue(args, i++);
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
@@ -1,4 +1,4 @@
1
1
  // user-facing CLI failure; the executable entry maps it to stderr and
2
- // exit code 1 — helpers throw instead of exiting so they stay testable
2
+ // exit code 1; helpers throw instead of exiting so they stay testable
3
3
  export class CliError extends Error {
4
4
  }
@@ -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])));
@@ -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
- if (!["markdown", "org"].includes(fromFormat) ||
36
- !["markdown", "org"].includes(toFormat)) {
37
- throw new CliError(`Error: Unsupported format. Supported formats are 'markdown' and 'org'.`);
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;
@@ -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 cli = parseArgs(process.argv.slice(2));
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 — CLI or
21
- * form > config > defaults — means the same thing in each.
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 — CLI or
4
- * form > config > defaults — means the same thing in each.
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.
@@ -0,0 +1,12 @@
1
+ import type { OrgData } from "uniorg";
2
+ /**
3
+ * md→org: adds `^:{}` to the document's `#+OPTIONS:` when its text
4
+ * holds a bare underscore or caret org would read as a script.
5
+ */
6
+ export declare function requireBracedScripts(uniorgAst: OrgData): void;
7
+ /**
8
+ * org→md: parses org, honoring its `^:` setting, and consuming `^:{}`
9
+ * where the text needs it: md→org adds it only then, so anywhere else
10
+ * it is the author's own setting.
11
+ */
12
+ export declare function parseOrg(org: string): OrgData;