@remigius42/morg 0.8.0 → 0.9.1
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 +22 -6
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +2 -0
- package/dist/cli/conversion.d.ts +3 -3
- package/dist/cli/conversion.js +4 -4
- package/dist/cli/flags.d.ts +1 -1
- package/dist/cli/flags.js +15 -1
- package/dist/cli/formats.d.ts +2 -1
- package/dist/cli/formats.js +1 -0
- package/dist/cli/presets.d.ts +13 -2
- package/dist/cli/presets.js +17 -3
- package/dist/cli.js +4 -5
- package/dist/config.d.ts +9 -4
- package/dist/config.js +2 -0
- package/dist/conversionOptions.d.ts +19 -3
- package/dist/conversionOptions.js +61 -0
- package/dist/core/frontmatterBlock.d.ts +7 -0
- package/dist/core/frontmatterBlock.js +7 -2
- package/dist/core/lineSyntax.d.ts +13 -0
- package/dist/core/lineSyntax.js +15 -2
- package/dist/core/mdastToUniorg/blocks.js +15 -1
- package/dist/index.d.ts +1 -1
- package/dist/markdownToOrg.js +36 -18
- package/dist/normalize.js +16 -2
- package/dist/options.d.ts +3 -7
- package/dist/orgToMarkdown.js +13 -9
- package/dist/presets/hooks.d.ts +49 -0
- package/dist/presets/hooks.js +72 -0
- package/dist/presets/logseq.js +187 -29
- package/dist/presets/logseqOutline.d.ts +29 -4
- package/dist/presets/logseqOutline.js +308 -92
- package/dist/presets/logseqVanillaMarkdown.d.ts +9 -0
- package/dist/presets/logseqVanillaMarkdown.js +295 -0
- package/dist/presets/obsidian.js +4 -2
- package/dist/presets/registry.d.ts +1 -1
- package/dist/presets/registry.js +5 -3
- package/dist/presets/sides.d.ts +40 -0
- package/dist/presets/sides.js +42 -0
- package/dist/presets/types.d.ts +49 -16
- package/package.json +2 -2
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)).
|
|
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
|
|
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) →
|
|
250
|
-
org → md: uniorg-parse →
|
|
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`)
|
|
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
|
package/dist/cli/args.d.ts
CHANGED
|
@@ -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
package/dist/cli/conversion.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { MorgConfig } from "../config.js";
|
|
2
2
|
import type { MarkdownStyleOptions } from "../options.js";
|
|
3
|
-
import type {
|
|
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,
|
|
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,
|
|
11
|
+
export declare function convert(inputContent: string, fromFormat: string, normalize: boolean, cli: CliArgs, config: MorgConfig, presets: PresetOptions): string;
|
package/dist/cli/conversion.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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,
|
|
31
|
+
export function convert(inputContent, fromFormat, normalize, cli, config, presets) {
|
|
32
32
|
try {
|
|
33
|
-
const { mdToOrgOptions, orgToMdOptions } = buildConversionOptions(cli, config,
|
|
33
|
+
const { mdToOrgOptions, orgToMdOptions } = buildConversionOptions(cli, config, presets);
|
|
34
34
|
if (normalize) {
|
|
35
35
|
return fromFormat === "markdown"
|
|
36
36
|
? normalizeMarkdown(inputContent, {
|
package/dist/cli/flags.d.ts
CHANGED
|
@@ -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: "
|
|
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>",
|
package/dist/cli/formats.d.ts
CHANGED
|
@@ -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):
|
|
4
|
+
export declare function validateFormats(fromFormat: string | undefined, toFormat: string | undefined, normalize: boolean): [fromFormat: Format, toFormat: Format];
|
package/dist/cli/formats.js
CHANGED
package/dist/cli/presets.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
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;
|
package/dist/cli/presets.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolvePresetOptions } from "../conversionOptions.js";
|
|
2
2
|
import { CliError } from "./error.js";
|
|
3
|
-
|
|
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
|
|
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 {
|
|
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
|
|
50
|
-
const
|
|
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,
|
|
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
|
|
5
|
-
* by both directions. Precedence: CLI >
|
|
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,
|
|
12
|
-
orgToMarkdown?: Omit<OrgToMarkdownOptions,
|
|
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,6 @@
|
|
|
1
1
|
import type { MorgConfig } from "./config.js";
|
|
2
2
|
import type { MarkdownStyleOptions, MarkdownToOrgOptions, OrgToMarkdownOptions, Toggle } from "./options.js";
|
|
3
|
-
import type
|
|
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
|
-
|
|
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,18 @@
|
|
|
1
1
|
import type { Parent } from "unist";
|
|
2
2
|
import { type Node } from "./render.js";
|
|
3
|
+
/**
|
|
4
|
+
* Whether a line, indented or not, may start org line syntax: false
|
|
5
|
+
* where org surely reads it as text.
|
|
6
|
+
* @param line The line, without its line break.
|
|
7
|
+
* @returns Whether it may start an element other than a paragraph.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mayBeLineSyntax(line: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether org reads a line as anything but a plain paragraph.
|
|
12
|
+
* @param line The line, without its line break.
|
|
13
|
+
* @returns Whether it reads as org line syntax.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readsAsLineSyntax(line: string): boolean;
|
|
3
16
|
/**
|
|
4
17
|
* md→org: a paragraph line org would read as line syntax (an escaped
|
|
5
18
|
* `1\.` or `\*`, or a lazy continuation line) gets a leading zero-width
|
package/dist/core/lineSyntax.js
CHANGED
|
@@ -9,8 +9,21 @@ 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
|
-
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Whether a line, indented or not, may start org line syntax: false
|
|
14
|
+
* where org surely reads it as text.
|
|
15
|
+
* @param line The line, without its line break.
|
|
16
|
+
* @returns Whether it may start an element other than a paragraph.
|
|
17
|
+
*/
|
|
18
|
+
export function mayBeLineSyntax(line) {
|
|
19
|
+
return MAY_BE_LINE_SYNTAX_RE.test(line.trimStart());
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether org reads a line as anything but a plain paragraph.
|
|
23
|
+
* @param line The line, without its line break.
|
|
24
|
+
* @returns Whether it reads as org line syntax.
|
|
25
|
+
*/
|
|
26
|
+
export function readsAsLineSyntax(line) {
|
|
14
27
|
if (!MAY_BE_LINE_SYNTAX_RE.test(line)) {
|
|
15
28
|
return false;
|
|
16
29
|
}
|
|
@@ -43,15 +43,29 @@ function transformMdastTableRow(ctx, row) {
|
|
|
43
43
|
}))
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
// lines without the indentation all non-blank ones share
|
|
47
|
+
function dedentLines(text) {
|
|
48
|
+
const lines = text.split("\n");
|
|
49
|
+
const indent = Math.min(...lines
|
|
50
|
+
.filter(line => line.trim())
|
|
51
|
+
.map(line => /^[ \t]*/.exec(line)?.[0].length ?? 0));
|
|
52
|
+
return Number.isFinite(indent)
|
|
53
|
+
? lines.map(line => line.slice(indent)).join("\n")
|
|
54
|
+
: text;
|
|
55
|
+
}
|
|
46
56
|
export function transformMdastHtml(ctx, node) {
|
|
47
57
|
// html comments are markdown's comment idiom and map natively to
|
|
48
58
|
// org comments (not an md-ism)
|
|
49
59
|
const comment = /^<!--([\s\S]*?)-->\s*$/.exec(node.value);
|
|
50
60
|
if (comment) {
|
|
61
|
+
const body = comment[1] ?? "";
|
|
62
|
+
// a multi-line comment's own line breaks frame its lines, so an
|
|
63
|
+
// empty first or last line is the comment's, not padding
|
|
64
|
+
const lines = /^[ \t]*\n([\s\S]*)\n[ \t]*$/.exec(body);
|
|
51
65
|
return {
|
|
52
66
|
type: "comment",
|
|
53
67
|
// inverse of the escaping applied when the comment was emitted
|
|
54
|
-
value: (
|
|
68
|
+
value: (lines ? dedentLines(lines[1] ?? "") : body.trim()).replaceAll("-->", "-->")
|
|
55
69
|
};
|
|
56
70
|
}
|
|
57
71
|
if (ctx.options.interpretHtml) {
|
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";
|
package/dist/markdownToOrg.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
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) =>
|
|
30
|
-
|
|
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,
|
|
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
|
-
|
|
68
|
-
|
|
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;
|
|
@@ -87,10 +87,28 @@ function convertMarkdownDocument(markdown, options) {
|
|
|
87
87
|
// a marked comment block; raw text, past every pass that rewrites text
|
|
88
88
|
renderFileHeader(uniorgAst);
|
|
89
89
|
// Phase 4: Render uniorg-ast to Org-mode string
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const orgContent = String(stringifier.stringify(uniorgAst));
|
|
91
|
+
return orgContent.replaceAll(SPACE_KEEPER, "");
|
|
92
|
+
}
|
|
93
|
+
// uniorg-stringify trims a headline's line, and the document's end, but
|
|
94
|
+
// Emacs reads stars without a space after them as text; the space ends
|
|
95
|
+
// in a character no trim takes, removed after. null leaves the rest to
|
|
96
|
+
// uniorg-stringify
|
|
97
|
+
const SPACE_KEEPER = "\u0000";
|
|
98
|
+
function emptyHeadline(node) {
|
|
99
|
+
const empty = !node.children.length &&
|
|
100
|
+
!node.todoKeyword &&
|
|
101
|
+
!node.priority &&
|
|
102
|
+
!node.commented &&
|
|
103
|
+
!node.tags.length;
|
|
104
|
+
return empty ? `${"*".repeat(node.level)} ${SPACE_KEEPER}\n` : null;
|
|
93
105
|
}
|
|
106
|
+
// built once: a preset converts a page block by block
|
|
107
|
+
const stringifier = unified()
|
|
108
|
+
.use(uniorgStringify, {
|
|
109
|
+
handlers: { headline: emptyHeadline }
|
|
110
|
+
})
|
|
111
|
+
.freeze();
|
|
94
112
|
// a preset's dialect conventions that need the Markdown source apply
|
|
95
113
|
// to the parse, before the generic transform
|
|
96
114
|
function parseMarkdown(markdown, preset) {
|
|
@@ -101,7 +119,7 @@ function parseMarkdown(markdown, preset) {
|
|
|
101
119
|
.use(remarkMath)
|
|
102
120
|
.parse(markdown);
|
|
103
121
|
keepPassthroughSource(mdast, markdown);
|
|
104
|
-
preset?.
|
|
122
|
+
preset?.markdown?.read?.mdast?.(mdast, markdown);
|
|
105
123
|
return mdast;
|
|
106
124
|
}
|
|
107
125
|
// leads the document, ahead of restored keywords and the frontmatter block
|