@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 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.
@@ -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 — the same
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 — org only
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 — insignificant in org and in rendered md alike
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 — the return trip re-parses it as a descriptive list
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) {
@@ -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 — `org` is a file called org, and `.org` is a hidden file whose
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 — `org` is a file called org, and `.org` is a hidden file whose
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
@@ -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 — adjacent lists need two blank
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
@@ -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 — Convergence is per-config (see ADR
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 — org-isms and md-isms
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 — org-isms and md-isms
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 — Convergence is per-config (ADR 0002).
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 — prettier's
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 — a one-way door unless
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
  */
@@ -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) — consume it so
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
@@ -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))) — emitted unescaped via verbatim-inline
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remigius42/morg",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Bidirectional Markdown ↔ Org-mode converter with round-trip convergence",
5
5
  "keywords": [
6
6
  "org-mode",