@remigius42/morg 0.7.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.
Files changed (47) hide show
  1. package/README.md +35 -18
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.js +2 -0
  4. package/dist/cli/conversion.d.ts +3 -3
  5. package/dist/cli/conversion.js +4 -4
  6. package/dist/cli/flags.d.ts +1 -1
  7. package/dist/cli/flags.js +15 -1
  8. package/dist/cli/formats.d.ts +2 -1
  9. package/dist/cli/formats.js +1 -0
  10. package/dist/cli/presets.d.ts +13 -2
  11. package/dist/cli/presets.js +17 -3
  12. package/dist/cli.js +4 -5
  13. package/dist/config.d.ts +9 -4
  14. package/dist/config.js +2 -0
  15. package/dist/conversionOptions.d.ts +19 -3
  16. package/dist/conversionOptions.js +61 -0
  17. package/dist/core/bracedScripts.d.ts +7 -0
  18. package/dist/core/bracedScripts.js +10 -0
  19. package/dist/core/frontmatterBlock.d.ts +7 -0
  20. package/dist/core/frontmatterBlock.js +7 -2
  21. package/dist/core/lineSyntax.d.ts +13 -0
  22. package/dist/core/lineSyntax.js +16 -4
  23. package/dist/core/mdastToUniorg/blocks.js +4 -1
  24. package/dist/core/passthroughSource.d.ts +21 -0
  25. package/dist/core/passthroughSource.js +94 -0
  26. package/dist/core/uniorgToMdast/elements.js +5 -0
  27. package/dist/core/uniorgToMdast/shared.js +6 -1
  28. package/dist/index.d.ts +1 -2
  29. package/dist/markdownToOrg.js +19 -6
  30. package/dist/normalize.js +16 -2
  31. package/dist/options.d.ts +3 -7
  32. package/dist/orgToMarkdown.js +14 -4
  33. package/dist/presets/hooks.d.ts +49 -0
  34. package/dist/presets/hooks.js +72 -0
  35. package/dist/presets/logseq.d.ts +3 -22
  36. package/dist/presets/logseq.js +306 -195
  37. package/dist/presets/logseqOutline.d.ts +47 -0
  38. package/dist/presets/logseqOutline.js +428 -0
  39. package/dist/presets/logseqVanillaMarkdown.d.ts +9 -0
  40. package/dist/presets/logseqVanillaMarkdown.js +295 -0
  41. package/dist/presets/obsidian.js +4 -2
  42. package/dist/presets/registry.d.ts +1 -1
  43. package/dist/presets/registry.js +5 -3
  44. package/dist/presets/sides.d.ts +40 -0
  45. package/dist/presets/sides.js +42 -0
  46. package/dist/presets/types.d.ts +49 -8
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -17,8 +17,8 @@ Bidirectional **Markdown ↔ Org-mode** converter, built on the
17
17
 
18
18
  morg treats Org as a canonical plain-text format and Markdown (Obsidian,
19
19
  generic) as the interop surface. Dialect conventions, such as
20
- [Logseq](https://docs.logseq.com/)'s `heading::` properties and outline
21
- nesting, are supported via presets.
20
+ [Logseq](https://docs.logseq.com/)'s outline of blocks and page
21
+ properties, are supported via presets.
22
22
 
23
23
  ## Round-trip convergence
24
24
 
@@ -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"
@@ -198,28 +204,36 @@ preset })`: `preserveOrgisms` default `true`; `useHtml` (default
198
204
  `- [x]`); headings become list items and do not restore on the
199
205
  return trip; anything with priority, tags or content keeps its
200
206
  heading and reports via `onWarning`
201
- - `logseq({ nestUnderHeadings })`: default `true`; content following a
202
- heading nests as child blocks of that heading: paragraphs become child
203
- headlines one level deeper (in Logseq org every outline block is a
204
- headline), other constructs stay in the preceding block's body. The
205
- reverse direction restores headings from `:heading:` properties and
206
- turns plain block headlines back into paragraphs. Hiccup blocks
207
- (`[:div …]`) pass through as plain text and are emitted unescaped in
208
- Markdown. Page properties map both directions: a first block of
207
+ - `logseq()`: a page is Logseq's outline of blocks, converted block by
208
+ block: a headline (stars, a space, the block's content, an empty
209
+ block as the bare stars) ↔ a `-` bullet indented one tab per level,
210
+ its lines below the first two spaces further in. A block's content
211
+ is one fragment, so a code block or table that starts on the
212
+ headline line converts as a whole. `:heading: N` ↔ `- ## …`, a
213
+ block's property drawer ↔ `key:: value` lines; planning lines and
214
+ other drawers (`:LOGBOOK:`) stay as written. A heading outside the
215
+ bullets is a top-level block, as Logseq writes a page's first one.
216
+ Page properties map both directions: a first block of
209
217
  `key:: value` lines and flat frontmatter entries ↔ leading
210
218
  `#+key: value` lines, which Logseq reads as page properties;
211
219
  frontmatter keys that act in Emacs (`todo`, `include`, …) stay inert.
212
- Logseq's own syntax maps both directions: `TODO`/`DONE`
213
- text markers and `[#A]` priorities ↔ org keywords/priorities, page
220
+ Task markers and `[#A]` priorities stay text, page
214
221
  references `[[page]]` and labeled forms `[label]([[page]])` ↔ org
215
222
  fuzzy links `[[page][label]]`, block refs `[label](((uuid)))` ↔
216
223
  `[[((uuid))][label]]`, and `^^highlight^^` markup survives verbatim
217
224
  (it would otherwise re-parse as superscripts).
218
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.
219
232
 
220
233
  - `normalizeMarkdown(md, { preset })` / `normalizeOrg(org, { preset })`
221
234
  (CLI: `morg normalize`): one full round trip to morg's canonical
222
- 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
223
237
  md-isms are rewritten exactly as a conversion would rewrite them.
224
238
  Normalize with the same preset/config you will convert with, since
225
239
  convergence is per-config (ADR 0002).
@@ -245,8 +259,8 @@ Two pipelines, each with a two-phase transformation separating the
245
259
  dialect-agnostic core from dialect presets:
246
260
 
247
261
  ```text
248
- md → org: remark-parse → mdast→uniorg (core) → preset transforms → uniorg-stringify
249
- 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
250
264
  ```
251
265
 
252
266
  Formatting is controlled by shaping the AST (e.g. inserting newline text nodes),
@@ -260,7 +274,10 @@ Project vocabulary lives in [CONTEXT.md](CONTEXT.md); design decisions in
260
274
 
261
275
  The core conversion surface is feature-complete and validated against
262
276
  real-world Logseq org vaults (edge cases found there live on as
263
- 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
264
281
  client-side [Web UI](https://morg.binarypoetry.ch) is deployed from
265
282
  `main`. The npm package is `@remigius42/morg`, since the bare `morg`
266
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
+ }
@@ -4,6 +4,13 @@ import type { OrgData } from "uniorg";
4
4
  * holds a bare underscore or caret org would read as a script.
5
5
  */
6
6
  export declare function requireBracedScripts(uniorgAst: OrgData): void;
7
+ /**
8
+ * Whether org→md would consume a `^:{}` setting at the head of `org`:
9
+ * its text reads a script org would otherwise take for one.
10
+ * @param org The org text, the setting included.
11
+ * @returns Whether the setting is taken as the one md→org adds.
12
+ */
13
+ export declare function consumesBracedScripts(org: string): boolean;
7
14
  /**
8
15
  * org→md: parses org, honoring its `^:` setting, and consuming `^:{}`
9
16
  * where the text needs it: md→org adds it only then, so anywhere else
@@ -135,6 +135,16 @@ function takeBracedScripts(uniorgAst) {
135
135
  return node.value !== "";
136
136
  });
137
137
  }
138
+ /**
139
+ * Whether org→md would consume a `^:{}` setting at the head of `org`:
140
+ * its text reads a script org would otherwise take for one.
141
+ * @param org The org text, the setting included.
142
+ * @returns Whether the setting is taken as the one md→org adds.
143
+ */
144
+ export function consumesBracedScripts(org) {
145
+ // text without a bare script candidate skips the parse
146
+ return (BARE_SCRIPT_RE.test(org) && readsBareScripts(bracedScriptsParser.parse(org)));
147
+ }
138
148
  /**
139
149
  * org→md: parses org, honoring its `^:` setting, and consuming `^:{}`
140
150
  * where the text needs it: md→org adds it only then, so anywhere else
@@ -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,11 +1,24 @@
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
6
12
  * space, or it would turn into a list item, headline, comment or table.
7
13
  */
8
14
  export declare function escapeLineSyntax(tree: Parent): void;
15
+ /**
16
+ * Whether org reads a text as just one passthrough element.
17
+ * @param text The org text.
18
+ * @returns Whether it is one block, drawer, keyword … org→md writes as
19
+ * its org text.
20
+ */
21
+ export declare function readsAsPassthrough(text: string): boolean;
9
22
  /**
10
23
  * md→org: whether a node is a paragraph org→md wrote as the org text of
11
24
  * one passthrough element, which goes back as it is.
@@ -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
  }
@@ -135,8 +139,16 @@ function isPassthrough(starts) {
135
139
  if (!first || !MAY_BE_LINE_SYNTAX_RE.test(first.line)) {
136
140
  return false;
137
141
  }
138
- const lines = starts.map(({ line }) => line);
139
- const [only, ...rest] = tryParse(`${lines.join("\n")}\n`)?.children ?? [];
142
+ return readsAsPassthrough(starts.map(({ line }) => line).join("\n"));
143
+ }
144
+ /**
145
+ * Whether org reads a text as just one passthrough element.
146
+ * @param text The org text.
147
+ * @returns Whether it is one block, drawer, keyword … org→md writes as
148
+ * its org text.
149
+ */
150
+ export function readsAsPassthrough(text) {
151
+ const [only, ...rest] = tryParse(`${text}\n`)?.children ?? [];
140
152
  return !rest.length && PASSTHROUGH_TYPES.has(only?.type ?? "");
141
153
  }
142
154
  /**
@@ -125,8 +125,11 @@ export function transformMdastCode(node) {
125
125
  value: `${node.value}\n`
126
126
  };
127
127
  }
128
+ // the fence's meta is the block's switches and header arguments,
129
+ // which uniorg-stringify writes after the language
130
+ const language = [node.lang, node.meta].filter(Boolean).join(" ");
128
131
  return (node.lang
129
- ? { type: "src-block", language: node.lang, value: node.value }
132
+ ? { type: "src-block", language, value: node.value }
130
133
  : {
131
134
  type: "example-block",
132
135
  value: node.value
@@ -0,0 +1,21 @@
1
+ import type { Root } from "mdast";
2
+ /** Whether a line opens an org block (`#+begin_name`). */
3
+ export declare function isOrgBlockStart(line: string): boolean;
4
+ /** Whether a line opens an org drawer (`:NAME:`). */
5
+ export declare function isDrawerStart(line: string): boolean;
6
+ /**
7
+ * The line that ends the org block or drawer a line opens.
8
+ * @param lines The lines.
9
+ * @param start The index of the opening line.
10
+ * @returns The index of the `#+end_name` or `:END:` line, or -1.
11
+ */
12
+ export declare function orgElementEnd(lines: string[], start: number): number;
13
+ /**
14
+ * md→org: a passthrough element's org text may hold lines Markdown reads
15
+ * as syntax of its own (a blank line and an indented one, a `#`, `-`
16
+ * or `>` line); the top-level nodes it parsed into become one paragraph
17
+ * of the source text again, which goes back to org as it is.
18
+ * @param mdast The parsed Markdown.
19
+ * @param markdown Its source.
20
+ */
21
+ export declare function keepPassthroughSource(mdast: Root, markdown: string): void;