@remigius42/morg 0.8.0 → 0.9.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
@@ -60,7 +60,10 @@ color schemes.
60
60
  Try morg without installing anything at
61
61
  [morg.binarypoetry.ch](https://morg.binarypoetry.ch). All conversion
62
62
  happens in your browser, nothing is uploaded (see [ADR
63
- 0003](docs/adr/0003-client-side-web-ui-on-github-pages.md)). The
63
+ 0003](docs/adr/0003-client-side-web-ui-on-github-pages.md)). Each side
64
+ names its format and dialect (Input: Org (Logseq), Output: Markdown);
65
+ the direction follows from the two, the same on both sides normalizes,
66
+ and ⇄ swaps them. The
64
67
  chrome-less embed page (`/embed.html`, optionally with
65
68
  `?theme=dark|light`) can be iframed into other sites. It posts its
66
69
  content height to the host on every change, so the frame can follow it
@@ -121,6 +124,9 @@ echo "# Hello" | morg --from markdown
121
124
  # Apply a dialect preset
122
125
  morg --input page.md --output page.org --preset logseq
123
126
 
127
+ # Or one per side: read Logseq org, write Vanilla Markdown (ADR 0006)
128
+ morg --input page.org --output page.md --input-preset logseq
129
+
124
130
  # Dropped constructs are reported on stderr; -s / --silent suppresses.
125
131
  # Boolean flags take an optional value, so --silent false overrides a
126
132
  # morg.toml that sets it
@@ -141,7 +147,7 @@ morg normalize --input notes.org --output notes.org
141
147
 
142
148
  Options can live in a `morg.toml` (auto-discovered in the working
143
149
  directory, or passed via `--config path`). Precedence: CLI flags >
144
- config file > defaults:
150
+ config file > defaults, per side for the presets:
145
151
 
146
152
  ```toml
147
153
  preset = "logseq"
@@ -217,10 +223,17 @@ preset })`: `preserveOrgisms` default `true`; `useHtml` (default
217
223
  `[[((uuid))][label]]`, and `^^highlight^^` markup survives verbatim
218
224
  (it would otherwise re-parse as superscripts).
219
225
  - `obsidian()`: wikilinks `[[Page]]` / `[[Page|alias]]` ↔ org fuzzy links
226
+ - `inputPreset` / `outputPreset` (on both conversions): the dialect
227
+ the input is read in and the one the output is written in (ADR
228
+ 0006); leaving one out is Vanilla. `preset` sets both, but leaves a
229
+ side Vanilla whose format the preset has no dialect for (Obsidian
230
+ writes no org); a side preset without one throws, and so does
231
+ `preset` naming another preset than a side preset.
220
232
 
221
233
  - `normalizeMarkdown(md, { preset })` / `normalizeOrg(org, { preset })`
222
234
  (CLI: `morg normalize`): one full round trip to morg's canonical
223
- form, a fixed point. Canonicalization, not styling: org-isms and
235
+ form, a fixed point, within one dialect: different presets per side
236
+ throw. Canonicalization, not styling: org-isms and
224
237
  md-isms are rewritten exactly as a conversion would rewrite them.
225
238
  Normalize with the same preset/config you will convert with, since
226
239
  convergence is per-config (ADR 0002).
@@ -246,8 +259,8 @@ Two pipelines, each with a two-phase transformation separating the
246
259
  dialect-agnostic core from dialect presets:
247
260
 
248
261
  ```text
249
- md → org: remark-parse → mdast→uniorg (core) → preset transforms → uniorg-stringify
250
- org → md: uniorg-parse → preset extraction → uniorg→mdast (core) → remark-stringify
262
+ md → org: remark-parse → mdast→uniorg (core) → read md dialect → write org dialect → uniorg-stringify
263
+ org → md: uniorg-parse → read org dialect → write md dialect → uniorg→mdast (core) → remark-stringify
251
264
  ```
252
265
 
253
266
  Formatting is controlled by shaping the AST (e.g. inserting newline text nodes),
@@ -261,7 +274,10 @@ Project vocabulary lives in [CONTEXT.md](CONTEXT.md); design decisions in
261
274
 
262
275
  The core conversion surface is feature-complete and validated against
263
276
  real-world Logseq org vaults (edge cases found there live on as
264
- anonymized fixtures, e.g. `tests/fixtures/logseq-vault.org`); the
277
+ anonymized fixtures, e.g. `tests/fixtures/logseq-vault.org`), and
278
+ the conversions between a Logseq dialect and Vanilla Org or Markdown
279
+ are checked by round trips of that vault and of public Markdown and
280
+ Org documentation from either side; the
265
281
  client-side [Web UI](https://morg.binarypoetry.ch) is deployed from
266
282
  `main`. The npm package is `@remigius42/morg`, since the bare `morg`
267
283
  name is taken, and pushing a `v*` tag publishes it. Most of the code is
@@ -7,6 +7,8 @@ export interface CliArgs {
7
7
  inputFile: string | undefined;
8
8
  outputFile: string | undefined;
9
9
  presetName: string | undefined;
10
+ inputPresetName: string | undefined;
11
+ outputPresetName: string | undefined;
10
12
  silent: boolean | undefined;
11
13
  taskCheckboxes: boolean | undefined;
12
14
  interpretHtml: boolean | undefined;
package/dist/cli/args.js CHANGED
@@ -16,6 +16,8 @@ export function parseArgs(args) {
16
16
  inputFile: undefined,
17
17
  outputFile: undefined,
18
18
  presetName: undefined,
19
+ inputPresetName: undefined,
20
+ outputPresetName: undefined,
19
21
  silent: undefined,
20
22
  taskCheckboxes: undefined,
21
23
  interpretHtml: undefined,
@@ -1,11 +1,11 @@
1
1
  import type { MorgConfig } from "../config.js";
2
2
  import type { MarkdownStyleOptions } from "../options.js";
3
- import type { Preset } from "../presets/types.js";
3
+ import type { PresetOptions } from "../presets/sides.js";
4
4
  import type { CliArgs } from "./args.js";
5
- export declare function buildConversionOptions(cli: CliArgs, config: MorgConfig, preset: Preset | undefined): {
5
+ export declare function buildConversionOptions(cli: CliArgs, config: MorgConfig, presets: PresetOptions): {
6
6
  mdToOrgOptions: import("../options.js").MarkdownToOrgOptions;
7
7
  orgToMdOptions: import("../options.js").OrgToMarkdownOptions & {
8
8
  markdownStyle: MarkdownStyleOptions;
9
9
  };
10
10
  };
11
- export declare function convert(inputContent: string, fromFormat: string, normalize: boolean, cli: CliArgs, config: MorgConfig, preset: Preset | undefined): string;
11
+ export declare function convert(inputContent: string, fromFormat: string, normalize: boolean, cli: CliArgs, config: MorgConfig, presets: PresetOptions): string;
@@ -3,7 +3,7 @@ import { convertOrgToMarkdown } from "../orgToMarkdown.js";
3
3
  import { normalizeMarkdown, normalizeOrg } from "../normalize.js";
4
4
  import { buildConversionOptions as layerOptions } from "../conversionOptions.js";
5
5
  import { CliError } from "./error.js";
6
- export function buildConversionOptions(cli, config, preset) {
6
+ export function buildConversionOptions(cli, config, presets) {
7
7
  // an explicit --silent wins over the config; dropped constructs are
8
8
  // reported on stderr unless it ends up on
9
9
  const silent = cli.silent ?? config.silent ?? false;
@@ -23,14 +23,14 @@ export function buildConversionOptions(cli, config, preset) {
23
23
  recordStyle: cli.recordStyle,
24
24
  markdownStyle
25
25
  }, config, {
26
- preset,
26
+ ...presets,
27
27
  onWarning,
28
28
  ...(config.orgismKeys && { orgismKeys: config.orgismKeys })
29
29
  });
30
30
  }
31
- export function convert(inputContent, fromFormat, normalize, cli, config, preset) {
31
+ export function convert(inputContent, fromFormat, normalize, cli, config, presets) {
32
32
  try {
33
- const { mdToOrgOptions, orgToMdOptions } = buildConversionOptions(cli, config, preset);
33
+ const { mdToOrgOptions, orgToMdOptions } = buildConversionOptions(cli, config, presets);
34
34
  if (normalize) {
35
35
  return fromFormat === "markdown"
36
36
  ? normalizeMarkdown(inputContent, {
@@ -1,4 +1,4 @@
1
- type StringOption = "fromFormat" | "toFormat" | "inputFile" | "outputFile" | "presetName" | "configPath";
1
+ type StringOption = "fromFormat" | "toFormat" | "inputFile" | "outputFile" | "presetName" | "inputPresetName" | "outputPresetName" | "configPath";
2
2
  type BooleanOption = "silent" | "taskCheckboxes" | "interpretHtml" | "recordStyle";
3
3
  type InfoOption = "help" | "version";
4
4
  export type FlagSpec = ({
package/dist/cli/flags.js CHANGED
@@ -34,10 +34,24 @@ export const FLAGS = [
34
34
  {
35
35
  names: ["--preset"],
36
36
  arg: "<name>",
37
- description: "Apply an editor preset, for example logseq",
37
+ description: "Read and write an editor's dialect, for example logseq",
38
38
  kind: "string",
39
39
  key: "presetName"
40
40
  },
41
+ {
42
+ names: ["--input-preset"],
43
+ arg: "<name>",
44
+ description: "Read the input in a preset's dialect",
45
+ kind: "string",
46
+ key: "inputPresetName"
47
+ },
48
+ {
49
+ names: ["--output-preset"],
50
+ arg: "<name>",
51
+ description: "Write the output in a preset's dialect",
52
+ kind: "string",
53
+ key: "outputPresetName"
54
+ },
41
55
  {
42
56
  names: ["--config"],
43
57
  arg: "<file>",
@@ -1,3 +1,4 @@
1
1
  import type { CliArgs } from "./args.js";
2
+ import type { Format } from "../presets/sides.js";
2
3
  export declare function inferFormats(cli: CliArgs): [fromFormat: string | undefined, toFormat: string | undefined];
3
- export declare function validateFormats(fromFormat: string | undefined, toFormat: string | undefined, normalize: boolean): asserts fromFormat is string;
4
+ export declare function validateFormats(fromFormat: string | undefined, toFormat: string | undefined, normalize: boolean): [fromFormat: Format, toFormat: Format];
@@ -57,4 +57,5 @@ export function validateFormats(fromFormat, toFormat, normalize) {
57
57
  throw new CliError("Error: normalize reads and writes the same format; " +
58
58
  `got '${fromFormat}' and '${toFormat}'.`);
59
59
  }
60
+ return [fromFormat, toFormat];
60
61
  }
@@ -1,2 +1,13 @@
1
- import type { Preset } from "../presets/types.js";
2
- export declare function resolvePreset(presetName: string | undefined): Preset | undefined;
1
+ import type { MorgConfig } from "../config.js";
2
+ import type { PresetOptions } from "../presets/sides.js";
3
+ import type { CliArgs } from "./args.js";
4
+ /**
5
+ * Resolves the presets per side, flags over the config (ADR 0006).
6
+ * @param cli The parsed arguments.
7
+ * @param config The parsed `morg.toml`.
8
+ * @param from The input's format.
9
+ * @param to The output's format.
10
+ * @returns The conversion's preset options.
11
+ * @throws {CliError} If the names conflict, are unknown or lack a dialect.
12
+ */
13
+ export declare function resolvePresets(cli: CliArgs, config: MorgConfig, from: "markdown" | "org", to: "markdown" | "org"): PresetOptions;
@@ -1,8 +1,22 @@
1
- import { createPreset } from "../presets/registry.js";
1
+ import { resolvePresetOptions } from "../conversionOptions.js";
2
2
  import { CliError } from "./error.js";
3
- export function resolvePreset(presetName) {
3
+ /**
4
+ * Resolves the presets per side, flags over the config (ADR 0006).
5
+ * @param cli The parsed arguments.
6
+ * @param config The parsed `morg.toml`.
7
+ * @param from The input's format.
8
+ * @param to The output's format.
9
+ * @returns The conversion's preset options.
10
+ * @throws {CliError} If the names conflict, are unknown or lack a dialect.
11
+ */
12
+ export function resolvePresets(cli, config, from, to) {
13
+ const flags = {
14
+ ...(cli.presetName && { preset: cli.presetName }),
15
+ ...(cli.inputPresetName && { inputPreset: cli.inputPresetName }),
16
+ ...(cli.outputPresetName && { outputPreset: cli.outputPresetName })
17
+ };
4
18
  try {
5
- return createPreset(presetName);
19
+ return resolvePresetOptions([flags, config], from, to);
6
20
  }
7
21
  catch (error) {
8
22
  throw new CliError(`Error: ${error.message}`);
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import * as fs from "node:fs";
3
3
  import { parseArgs } from "./cli/args.js";
4
4
  import { loadConfig } from "./cli/configFile.js";
5
- import { resolvePreset } from "./cli/presets.js";
5
+ import { resolvePresets } 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";
@@ -46,11 +46,10 @@ async function main() {
46
46
  return;
47
47
  }
48
48
  const config = loadConfig(cli.configPath);
49
- const preset = resolvePreset(cli.presetName ?? config.preset);
50
- const [fromFormat, toFormat] = inferFormats(cli);
51
- validateFormats(fromFormat, toFormat, cli.normalize);
49
+ const [fromFormat, toFormat] = validateFormats(...inferFormats(cli), cli.normalize);
50
+ const presets = resolvePresets(cli, config, fromFormat, toFormat);
52
51
  const inputContent = await readInput(cli.inputFile);
53
- const outputContent = convert(inputContent, fromFormat, cli.normalize, cli, config, preset);
52
+ const outputContent = convert(inputContent, fromFormat, cli.normalize, cli, config, presets);
54
53
  if (cli.outputFile) {
55
54
  fs.writeFileSync(cli.outputFile, outputContent, "utf8");
56
55
  }
package/dist/config.d.ts CHANGED
@@ -1,15 +1,19 @@
1
1
  import type { MarkdownToOrgOptions, OrgToMarkdownOptions } from "./options.js";
2
+ type PresetKey = "preset" | "inputPreset" | "outputPreset";
2
3
  /**
3
4
  * Shape of `morg.toml`. Sections mirror the library options objects;
4
- * `preset` and `silent` mirror their CLI flags; `orgismKeys` is shared
5
- * by both directions. Precedence: CLI > config > defaults.
5
+ * `preset`, `inputPreset`, `outputPreset` and `silent` mirror their CLI
6
+ * flags; `orgismKeys` is shared by both directions. Precedence: CLI >
7
+ * config > defaults, per side for the presets.
6
8
  */
7
9
  export interface MorgConfig {
8
10
  preset?: string;
11
+ inputPreset?: string;
12
+ outputPreset?: string;
9
13
  silent?: boolean;
10
14
  orgismKeys?: Record<string, string>;
11
- markdownToOrg?: Omit<MarkdownToOrgOptions, "preset" | "onWarning">;
12
- orgToMarkdown?: Omit<OrgToMarkdownOptions, "preset" | "onWarning" | "orgismKeys">;
15
+ markdownToOrg?: Omit<MarkdownToOrgOptions, PresetKey | "onWarning">;
16
+ orgToMarkdown?: Omit<OrgToMarkdownOptions, PresetKey | "onWarning" | "orgismKeys">;
13
17
  }
14
18
  /**
15
19
  * Parses a `morg.toml` source string. Unknown top-level keys are
@@ -18,3 +22,4 @@ export interface MorgConfig {
18
22
  * @returns The parsed configuration.
19
23
  */
20
24
  export declare function parseConfig(source: string): MorgConfig;
25
+ export {};
package/dist/config.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { parse as parseToml } from "smol-toml";
2
2
  const KNOWN_KEYS = new Set([
3
3
  "preset",
4
+ "inputPreset",
5
+ "outputPreset",
4
6
  "silent",
5
7
  "orgismKeys",
6
8
  "markdownToOrg",
@@ -1,6 +1,6 @@
1
1
  import type { MorgConfig } from "./config.js";
2
2
  import type { MarkdownStyleOptions, MarkdownToOrgOptions, OrgToMarkdownOptions, Toggle } from "./options.js";
3
- import type { Preset } from "./presets/types.js";
3
+ import { type Format, type PresetOptions } from "./presets/sides.js";
4
4
  /** Explicitly requested option values; `undefined` leaves it to config. */
5
5
  export interface ConversionOverrides {
6
6
  taskCheckboxes?: boolean;
@@ -10,8 +10,7 @@ export interface ConversionOverrides {
10
10
  markdownStyle?: MarkdownStyleOptions;
11
11
  }
12
12
  /** Values that apply to both directions. */
13
- export interface SharedConversionOptions {
14
- preset?: Preset;
13
+ export interface SharedConversionOptions extends PresetOptions {
15
14
  onWarning?: (message: string) => void;
16
15
  orgismKeys?: Record<string, string>;
17
16
  }
@@ -30,3 +29,20 @@ export declare function buildConversionOptions(overrides: ConversionOverrides, c
30
29
  markdownStyle: MarkdownStyleOptions;
31
30
  };
32
31
  };
32
+ /** Preset names as one layer (CLI, form or config) sets them. */
33
+ export interface PresetNames {
34
+ preset?: string;
35
+ inputPreset?: string;
36
+ outputPreset?: string;
37
+ }
38
+ /**
39
+ * Resolves the presets of a conversion over layers of names (ADR 0006):
40
+ * per side, the first layer that sets the side or `preset` wins.
41
+ * @param layers Preset names by layer, highest first.
42
+ * @param from The input's format.
43
+ * @param to The output's format; the input's for normalizing.
44
+ * @returns The conversion's preset options.
45
+ * @throws If a layer sets `preset` and another side preset, or a side
46
+ * preset has no dialect for its side's format, or normalizing names two.
47
+ */
48
+ export declare function resolvePresetOptions(layers: PresetNames[], from: Format, to: Format): PresetOptions;
@@ -1,3 +1,5 @@
1
+ import { resolveSide } from "./presets/sides.js";
2
+ import { createPreset } from "./presets/registry.js";
1
3
  /**
2
4
  * Layers explicit overrides over the config file, per direction. Used by
3
5
  * both adapters (CLI and Web UI) so the documented precedence (CLI or
@@ -32,3 +34,62 @@ export function buildConversionOptions(overrides, config, shared) {
32
34
  };
33
35
  return { mdToOrgOptions, orgToMdOptions };
34
36
  }
37
+ function sideName(layers, key) {
38
+ for (const layer of layers) {
39
+ if (layer[key]) {
40
+ return { name: layer[key], shorthand: false };
41
+ }
42
+ if (layer.preset) {
43
+ return { name: layer.preset, shorthand: true };
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+ // across layers the higher one wins; within one, two names conflict
49
+ function rejectConflict(layer) {
50
+ for (const key of ["inputPreset", "outputPreset"]) {
51
+ const side = layer[key];
52
+ if (layer.preset && side && side !== layer.preset) {
53
+ throw new Error(`preset '${layer.preset}' conflicts with ${key} '${side}'`);
54
+ }
55
+ }
56
+ }
57
+ function sidePreset(name, format, side) {
58
+ const preset = createPreset(name?.name);
59
+ return name?.shorthand
60
+ ? resolveSide(undefined, preset, format, side)
61
+ : resolveSide(preset, undefined, format, side);
62
+ }
63
+ // normalizing goes there and back within one dialect (ADR 0006), so
64
+ // both sides must name the same preset, which then sets both ways
65
+ function normalizePresetOptions(layers) {
66
+ const input = sideName(layers, "inputPreset")?.name ?? "vanilla";
67
+ const output = sideName(layers, "outputPreset")?.name ?? "vanilla";
68
+ if (input !== output) {
69
+ throw new Error(`normalize takes one preset; got inputPreset '${input}' and outputPreset '${output}'`);
70
+ }
71
+ const preset = createPreset(input);
72
+ return preset ? { preset } : {};
73
+ }
74
+ /**
75
+ * Resolves the presets of a conversion over layers of names (ADR 0006):
76
+ * per side, the first layer that sets the side or `preset` wins.
77
+ * @param layers Preset names by layer, highest first.
78
+ * @param from The input's format.
79
+ * @param to The output's format; the input's for normalizing.
80
+ * @returns The conversion's preset options.
81
+ * @throws If a layer sets `preset` and another side preset, or a side
82
+ * preset has no dialect for its side's format, or normalizing names two.
83
+ */
84
+ export function resolvePresetOptions(layers, from, to) {
85
+ layers.forEach(rejectConflict);
86
+ if (from === to) {
87
+ return normalizePresetOptions(layers);
88
+ }
89
+ const input = sidePreset(sideName(layers, "inputPreset"), from, "input");
90
+ const output = sidePreset(sideName(layers, "outputPreset"), to, "output");
91
+ return {
92
+ ...(input && { inputPreset: input }),
93
+ ...(output && { outputPreset: output })
94
+ };
95
+ }
@@ -1,5 +1,6 @@
1
1
  import type { OrgData } from "uniorg";
2
2
  import { type Scalar } from "yaml";
3
+ export declare const FRONTMATTER_BLOCK_BEGIN = "#+begin_comment morg_frontmatter";
3
4
  export interface FrontmatterNode {
4
5
  type: "morg-frontmatter";
5
6
  yaml: string;
@@ -15,6 +16,12 @@ export declare function isFrontmatterNode(node: {
15
16
  * @param uniorgAst The document.
16
17
  */
17
18
  export declare function renderFileHeader(uniorgAst: OrgData): void;
19
+ /**
20
+ * The Frontmatter Block carrying YAML through org (ADR 0005).
21
+ * @param yaml The YAML source, without its `---` fences.
22
+ * @returns The block's org text.
23
+ */
24
+ export declare function frontmatterBlock(yaml: string): string;
18
25
  /**
19
26
  * md→org: takes the top-level entries `take` maps to keywords out of
20
27
  * the frontmatter; the rest stays verbatim, comments included.
@@ -4,7 +4,7 @@ import { CST, isMap, isScalar, isSeq, Lexer, parseDocument, visit } from "yaml";
4
4
  // frontmatter travels verbatim in a comment block marked as morg's
5
5
  // (ADR 0005); uniorg keeps neither the block's parameter nor its
6
6
  // unescaped value, so the block is rendered as raw text
7
- const FRONTMATTER_BLOCK_BEGIN = "#+begin_comment morg_frontmatter";
7
+ export const FRONTMATTER_BLOCK_BEGIN = "#+begin_comment morg_frontmatter";
8
8
  export function isFrontmatterNode(node) {
9
9
  return node.type === "morg-frontmatter";
10
10
  }
@@ -51,7 +51,12 @@ function detachLeadingKeywords(uniorgAst) {
51
51
  }
52
52
  }
53
53
  }
54
- function frontmatterBlock(yaml) {
54
+ /**
55
+ * The Frontmatter Block carrying YAML through org (ADR 0005).
56
+ * @param yaml The YAML source, without its `---` fences.
57
+ * @returns The block's org text.
58
+ */
59
+ export function frontmatterBlock(yaml) {
55
60
  const body = yaml ? `${escapeBlockLines(yaml)}\n` : "";
56
61
  return `${FRONTMATTER_BLOCK_BEGIN}\n${body}#+end_comment\n`;
57
62
  }
@@ -1,5 +1,11 @@
1
1
  import type { Parent } from "unist";
2
2
  import { type Node } from "./render.js";
3
+ /**
4
+ * Whether org reads a line as anything but a plain paragraph.
5
+ * @param line The line, without its line break.
6
+ * @returns Whether it reads as org line syntax.
7
+ */
8
+ export declare function readsAsLineSyntax(line: string): boolean;
3
9
  /**
4
10
  * md→org: a paragraph line org would read as line syntax (an escaped
5
11
  * `1\.` or `\*`, or a lazy continuation line) gets a leading zero-width
@@ -9,8 +9,12 @@ import { delimiters, isInline, locate, positionParser, renderInline, tryParse }
9
9
  // `:` (`1.`, `a)`, `CLOCK:`, `_.`); any other line (one starting with a
10
10
  // link, markup or code, say) is text, and skips the parse
11
11
  const MAY_BE_LINE_SYNTAX_RE = /^(?:[-+]|\*+)(?:\s|$)|^(?:-{5}|\+-|#(?:\s|$|\+)|[|:]|\[fn:|\\begin\{|%%\()|^[\p{L}\p{N}_]+[.):]/u;
12
- // whether org reads `line` as anything but a plain paragraph
13
- function readsAsLineSyntax(line) {
12
+ /**
13
+ * Whether org reads a line as anything but a plain paragraph.
14
+ * @param line The line, without its line break.
15
+ * @returns Whether it reads as org line syntax.
16
+ */
17
+ export function readsAsLineSyntax(line) {
14
18
  if (!MAY_BE_LINE_SYNTAX_RE.test(line)) {
15
19
  return false;
16
20
  }
package/dist/index.d.ts CHANGED
@@ -8,5 +8,5 @@ export { transformMdastToUniorgAst } from "./core/mdastToUniorg/index.js";
8
8
  export { transformUniorgAstToMdast } from "./core/uniorgToMdast/index.js";
9
9
  export { logseq } from "./presets/logseq.js";
10
10
  export { obsidian } from "./presets/obsidian.js";
11
- export type { FragmentConverter, Preset } from "./presets/types.js";
11
+ export type { ConversionContext, FragmentConverter, MarkdownDialect, OrgDialect, Preset } from "./presets/types.js";
12
12
  export type { Toggle, MarkdownToOrgOptions, OrgToMarkdownOptions, MarkdownStyleOptions } from "./options.js";
@@ -15,6 +15,8 @@ import { escapeBackslashCommands } from "./core/backslashCommands.js";
15
15
  import { escapeTablePipes } from "./core/tablePipes.js";
16
16
  import { requireBracedScripts } from "./core/bracedScripts.js";
17
17
  import { keepPassthroughSource } from "./core/passthroughSource.js";
18
+ import { conversionContext, fragmentSides, readMarkdownWriteOrg, takeOver } from "./presets/hooks.js";
19
+ import { resolveSides } from "./presets/sides.js";
18
20
  import { keyValueEntries } from "./core/keyValueLines.js";
19
21
  /**
20
22
  * Converts a Markdown string to an Org-mode string.
@@ -23,19 +25,18 @@ import { keyValueEntries } from "./core/keyValueLines.js";
23
25
  * @returns The converted Org-mode string.
24
26
  */
25
27
  export function convertMarkdownToOrg(markdown, options = {}) {
26
- const convertMarkdown = options.preset?.convertMarkdown;
27
- return convertMarkdown
28
+ return convertMarkdownSides(markdown, options, resolveSides(options, "markdown", "org"));
29
+ }
30
+ function convertMarkdownSides(markdown, options, sides) {
31
+ const over = takeOver(sides, "convertMarkdown");
32
+ return over
28
33
  ? // a fragment's style is the preset's, not one to record
29
- convertMarkdown(markdown, (fragment, preset) => convertMarkdownToOrg(fragment, {
30
- ...options,
31
- preset,
32
- recordStyle: false
33
- }))
34
- : convertMarkdownDocument(markdown, options);
34
+ over.preset.convertMarkdown(markdown, (fragment, preset, carried) => convertMarkdownSides(fragment, { ...options, recordStyle: false }, fragmentSides(sides, over.side, preset, carried)), conversionContext(over, options))
35
+ : convertMarkdownDocument(markdown, options, sides);
35
36
  }
36
- function convertMarkdownDocument(markdown, options) {
37
+ function convertMarkdownDocument(markdown, options, sides) {
37
38
  // Phase 1: Parse Markdown to mdast
38
- const mdast = parseMarkdown(markdown, options.preset);
39
+ const mdast = parseMarkdown(markdown, sides.input);
39
40
  // Phase 2: Generic mdast to uniorg-ast transformation
40
41
  let uniorgAst = transformMdastToUniorgDraft(mdast, {
41
42
  ...(options.preserveMdisms !== undefined && {
@@ -63,10 +64,9 @@ function convertMarkdownDocument(markdown, options) {
63
64
  if (options.recordStyle) {
64
65
  recordStyleKeyword(uniorgAst, detectMarkdownStyle(mdast, markdown, options.onWarning));
65
66
  }
66
- // Phase 3: Apply dialect preset, if any
67
- if (options.preset?.applyToUniorg) {
68
- uniorgAst = options.preset.applyToUniorg(uniorgAst);
69
- }
67
+ // Phase 3: Apply dialect preset, if any: read the md dialect, then
68
+ // write the org dialect
69
+ uniorgAst = readMarkdownWriteOrg(uniorgAst, sides);
70
70
  // Phase 3b: literal footnote references, paragraph lines org would
71
71
  // read as line syntax, literal markers org would read as markup, and
72
72
  // markup touching a word character, need a zero-width space escape;
@@ -101,7 +101,7 @@ function parseMarkdown(markdown, preset) {
101
101
  .use(remarkMath)
102
102
  .parse(markdown);
103
103
  keepPassthroughSource(mdast, markdown);
104
- preset?.applyToMdast?.(mdast, markdown);
104
+ preset?.markdown?.read?.mdast?.(mdast, markdown);
105
105
  return mdast;
106
106
  }
107
107
  // leads the document, ahead of restored keywords and the frontmatter block
package/dist/normalize.js CHANGED
@@ -10,7 +10,8 @@ import { convertOrgToMarkdown } from "./orgToMarkdown.js";
10
10
  * @returns The canonical-form Markdown string.
11
11
  */
12
12
  export function normalizeMarkdown(markdown, options = {}) {
13
- return convertOrgToMarkdown(convertMarkdownToOrg(markdown, options), options);
13
+ const single = onePreset(options);
14
+ return convertOrgToMarkdown(convertMarkdownToOrg(markdown, single), single);
14
15
  }
15
16
  /**
16
17
  * Normalizes an Org string to morg's canonical form: one full round trip
@@ -20,5 +21,18 @@ export function normalizeMarkdown(markdown, options = {}) {
20
21
  * @returns The canonical-form Org string.
21
22
  */
22
23
  export function normalizeOrg(org, options = {}) {
23
- return convertMarkdownToOrg(convertOrgToMarkdown(org, options), options);
24
+ const single = onePreset(options);
25
+ return convertMarkdownToOrg(convertOrgToMarkdown(org, single), single);
26
+ }
27
+ // normalizing canonicalizes within one dialect (ADR 0006), so both
28
+ // sides name the same preset, which then sets both ways of the trip
29
+ function onePreset(options) {
30
+ const { preset, inputPreset, outputPreset, ...rest } = options;
31
+ const input = inputPreset ?? preset;
32
+ const output = outputPreset ?? preset;
33
+ if (input?.name !== output?.name) {
34
+ const name = (key, side) => `${key} '${side?.name ?? "vanilla"}'`;
35
+ throw new Error(`normalize takes one preset; got ${name("inputPreset", input)} and ${name("outputPreset", output)}`);
36
+ }
37
+ return input ? { ...rest, preset: input } : rest;
24
38
  }
package/dist/options.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Preset } from "./presets/types.js";
1
+ import type { PresetOptions } from "./presets/sides.js";
2
2
  /**
3
3
  * A feature toggle: `true`/`false` switches everything on/off, a record
4
4
  * toggles individual constructs by name. Unlisted keys fall back to the
@@ -6,7 +6,7 @@ import type { Preset } from "./presets/types.js";
6
6
  */
7
7
  export type Toggle = boolean | Record<string, boolean>;
8
8
  export declare function toggleEnabled(toggle: Toggle | undefined, key: string, defaultValue?: boolean): boolean;
9
- export interface MarkdownToOrgOptions {
9
+ export interface MarkdownToOrgOptions extends PresetOptions {
10
10
  /**
11
11
  * Preserve Markdown constructs without a native Org equivalent (e.g.
12
12
  * raw HTML as export blocks/snippets, see ADR 0002). Default: `true`.
@@ -38,8 +38,6 @@ export interface MarkdownToOrgOptions {
38
38
  orgismKeys?: Record<string, string>;
39
39
  /** Called for each construct dropped without an equivalent. */
40
40
  onWarning?: (message: string) => void;
41
- /** Dialect preset applied on top of the generic conversion. */
42
- preset?: Preset;
43
41
  }
44
42
  /**
45
43
  * Markdown stringifier style knobs (subset of remark-stringify options).
@@ -67,7 +65,7 @@ export interface MarkdownStyleOptions {
67
65
  */
68
66
  ruleRepetition?: number;
69
67
  }
70
- export interface OrgToMarkdownOptions {
68
+ export interface OrgToMarkdownOptions extends PresetOptions {
71
69
  /**
72
70
  * Serialize Org constructs without a native Markdown equivalent as
73
71
  * `key:: value` lines and verbatim passthroughs (see ADR 0002).
@@ -97,6 +95,4 @@ export interface OrgToMarkdownOptions {
97
95
  orgismKeys?: Record<string, string>;
98
96
  /** Called for each construct dropped without an equivalent. */
99
97
  onWarning?: (message: string) => void;
100
- /** Dialect preset extracted before the generic conversion. */
101
- preset?: Preset;
102
98
  }