@remigius42/morg 0.9.2 → 0.10.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 +35 -11
- package/dist/cli/args.d.ts +0 -1
- package/dist/cli/args.js +3 -6
- package/dist/cli/conversion.d.ts +2 -2
- package/dist/cli/conversion.js +7 -9
- package/dist/cli/formats.d.ts +1 -1
- package/dist/cli/formats.js +4 -17
- package/dist/cli/help.js +5 -6
- package/dist/cli.js +2 -2
- package/dist/conversionOptions.d.ts +2 -2
- package/dist/conversionOptions.js +9 -7
- package/dist/core/outsideCode.d.ts +17 -0
- package/dist/core/outsideCode.js +98 -0
- package/dist/core/restyle.d.ts +9 -0
- package/dist/core/restyle.js +110 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/presets/links.d.ts +6 -0
- package/dist/presets/links.js +6 -0
- package/dist/presets/logseq.js +76 -2
- package/dist/presets/logseqOutline.d.ts +30 -0
- package/dist/presets/logseqOutline.js +299 -11
- package/dist/presets/logseqVanillaMarkdown.d.ts +2 -0
- package/dist/presets/logseqVanillaMarkdown.js +36 -10
- package/dist/presets/obsidian.js +76 -5
- package/dist/presets/types.d.ts +21 -1
- package/dist/translate.d.ts +34 -0
- package/dist/translate.js +82 -0
- package/dist/withinFormat.d.ts +12 -0
- package/dist/withinFormat.js +18 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Copyright 2026 [Andreas Remigius Schmidt](https://github.com/remigius42)
|
|
|
6
6
|
[](https://github.com/remigius42/morg/blob/main/CHANGELOG.md)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://github.com/remigius42/morg/actions/workflows/ci.yml)
|
|
9
|
-

|
|
10
10
|
[](https://app.codacy.com/gh/remigius42/morg/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
11
11
|
[](https://app.codacy.com/gh/remigius42/morg/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
|
|
12
12
|
|
|
@@ -18,7 +18,9 @@ Bidirectional **Markdown ↔ Org-mode** converter, built on the
|
|
|
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
20
|
[Logseq](https://docs.logseq.com/)'s outline of blocks and page
|
|
21
|
-
properties, are supported via presets.
|
|
21
|
+
properties, are supported via presets. Within one format, morg
|
|
22
|
+
translates between two dialects (Logseq Markdown ↔ Markdown, Obsidian
|
|
23
|
+
↔ Logseq), changing only what they write differently.
|
|
22
24
|
|
|
23
25
|
## Round-trip convergence
|
|
24
26
|
|
|
@@ -63,6 +65,7 @@ happens in your browser, nothing is uploaded (see [ADR
|
|
|
63
65
|
0003](docs/adr/0003-client-side-web-ui-on-github-pages.md)). Each side
|
|
64
66
|
names its format and dialect (Input: Org (Logseq), Output: Markdown);
|
|
65
67
|
the direction follows from the two, the same on both sides normalizes,
|
|
68
|
+
two dialects of one format translate (Markdown (Logseq) → Markdown),
|
|
66
69
|
and ⇄ swaps them. The
|
|
67
70
|
chrome-less embed page (`/embed.html`, optionally with
|
|
68
71
|
`?theme=dark|light`) can be iframed into other sites. It posts its
|
|
@@ -90,7 +93,8 @@ both at once and each goes where it belongs; an overlay names what is
|
|
|
90
93
|
accepted while a drag is in flight, and anything that turns out not to
|
|
91
94
|
be text is named in the warning list rather than loaded. The result can
|
|
92
95
|
be copied or saved with the Copy and Download buttons; a normalized file
|
|
93
|
-
is saved as `notes.normalized.org`,
|
|
96
|
+
is saved as `notes.normalized.org`, a translated one under its dialect
|
|
97
|
+
(`page.vanilla.md`), and switching to a direction that no
|
|
94
98
|
longer reads the opened file falls back to a generic name, so neither
|
|
95
99
|
lands on top of its own source. Files are read and written by the
|
|
96
100
|
browser itself; this is not an upload.
|
|
@@ -137,10 +141,14 @@ morg --input notes.md --output notes.org --silent
|
|
|
137
141
|
# canonicalizing it; markers used inconsistently warn and are skipped
|
|
138
142
|
morg --input notes.md --output notes.org --record-style
|
|
139
143
|
|
|
140
|
-
#
|
|
144
|
+
# Translate between two dialects of one format, changing only what
|
|
145
|
+
# they write differently (a block's content stays as written)
|
|
146
|
+
morg --input-preset logseq --input page.md --output page.vanilla.md
|
|
147
|
+
|
|
148
|
+
# Normalize to canonical form (same format and preset in and out); this
|
|
141
149
|
# canonicalizes (the one-time reformat a first conversion would apply
|
|
142
150
|
# anyway, ADR 0001); it is not a style formatter like prettier
|
|
143
|
-
morg
|
|
151
|
+
morg --input notes.org --output notes.org
|
|
144
152
|
```
|
|
145
153
|
|
|
146
154
|
### Configuration file
|
|
@@ -223,6 +231,9 @@ preset })`: `preserveOrgisms` default `true`; `useHtml` (default
|
|
|
223
231
|
`[[((uuid))][label]]`, and `^^highlight^^` markup survives verbatim
|
|
224
232
|
(it would otherwise re-parse as superscripts).
|
|
225
233
|
- `obsidian()`: wikilinks `[[Page]]` / `[[Page|alias]]` ↔ org fuzzy links
|
|
234
|
+
(`[[Page\|alias]]` in a table cell). Translated to Vanilla or Logseq
|
|
235
|
+
Markdown, a `%%comment%%` becomes an HTML comment and an inline
|
|
236
|
+
footnote `^[note]` a footnote.
|
|
226
237
|
- `inputPreset` / `outputPreset` (on both conversions): the dialect
|
|
227
238
|
the input is read in and the one the output is written in (ADR
|
|
228
239
|
0006); leaving one out is Vanilla. `preset` sets both, but leaves a
|
|
@@ -231,15 +242,27 @@ preset })`: `preserveOrgisms` default `true`; `useHtml` (default
|
|
|
231
242
|
`preset` naming another preset than a side preset.
|
|
232
243
|
|
|
233
244
|
- `normalizeMarkdown(md, { preset })` / `normalizeOrg(org, { preset })`
|
|
234
|
-
(CLI:
|
|
245
|
+
(CLI: one format and preset on both sides): one full round trip to morg's canonical
|
|
235
246
|
form, a fixed point, within one dialect: different presets per side
|
|
236
247
|
throw. Canonicalization, not styling: org-isms and
|
|
237
248
|
md-isms are rewritten exactly as a conversion would rewrite them.
|
|
238
249
|
Normalize with the same preset/config you will convert with, since
|
|
239
250
|
convergence is per-config (ADR 0002).
|
|
240
251
|
|
|
252
|
+
- `translateOrg(org, { inputPreset, outputPreset })`: translates
|
|
253
|
+
between two org dialects (Logseq org ↔ Vanilla org), changing only
|
|
254
|
+
what they write differently: a block's content stays as written.
|
|
255
|
+
The same preset on both sides throws; that is `normalizeOrg`.
|
|
256
|
+
`translateMarkdown(md, { inputPreset, outputPreset })` does the same
|
|
257
|
+
within Markdown (Logseq md, Obsidian md and Vanilla md, any two);
|
|
258
|
+
`orgismKeys` names the
|
|
259
|
+
`key::` lines Vanilla md writes planning under, as in a conversion,
|
|
260
|
+
and `markdownStyle` rewrites the markers it names, leaving the rest
|
|
261
|
+
as written.
|
|
262
|
+
|
|
241
263
|
- `markdownStyle: { bullet, emphasis, strong, fence, rule, ruleRepetition }`
|
|
242
|
-
(on `convertOrgToMarkdown
|
|
264
|
+
(on `convertOrgToMarkdown`, `normalizeMarkdown` and
|
|
265
|
+
`translateMarkdown`; CLI `--bullet`,
|
|
243
266
|
`--emphasis`, `--strong`, `--fence`, `--rule`, `--rule-repetition`)
|
|
244
267
|
are Markdown output style knobs. Defaults match prettier except
|
|
245
268
|
emphasis (`*italic*`); `--emphasis _` aligns fully with prettier.
|
|
@@ -248,7 +271,7 @@ preset })`: `preserveOrgisms` default `true`; `useHtml` (default
|
|
|
248
271
|
CommonMark/GFM prescribe no style; these defaults are morg's
|
|
249
272
|
canonical choices, not a standard.
|
|
250
273
|
|
|
251
|
-
|
|
274
|
+
The convert and translate functions also accept `onWarning: message => …`, called for
|
|
252
275
|
each construct dropped without an equivalent (e.g. image titles, LaTeX
|
|
253
276
|
fragments). The CLI wires this to stderr unless `-s` / `--silent` is
|
|
254
277
|
given; the library is silent unless a callback is passed.
|
|
@@ -275,9 +298,10 @@ Project vocabulary lives in [CONTEXT.md](CONTEXT.md); design decisions in
|
|
|
275
298
|
The core conversion surface is feature-complete and validated against
|
|
276
299
|
real-world Logseq org vaults (edge cases found there live on as
|
|
277
300
|
anonymized fixtures, e.g. `tests/fixtures/logseq-vault.org`), and
|
|
278
|
-
the conversions between a Logseq dialect and Vanilla Org or Markdown
|
|
279
|
-
|
|
280
|
-
|
|
301
|
+
the conversions between a Logseq dialect and Vanilla Org or Markdown,
|
|
302
|
+
and the translations between two dialects of one format, are checked
|
|
303
|
+
by round trips of that vault and of public Markdown and Org
|
|
304
|
+
documentation from either side; the
|
|
281
305
|
client-side [Web UI](https://morg.binarypoetry.ch) is deployed from
|
|
282
306
|
`main`. The npm package is `@remigius42/morg`, since the bare `morg`
|
|
283
307
|
name is taken, and pushing a `v*` tag publishes it. Most of the code is
|
package/dist/cli/args.d.ts
CHANGED
package/dist/cli/args.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { CliError } from "./error.js";
|
|
2
2
|
import { FLAGS_BY_NAME } from "./flags.js";
|
|
3
3
|
export function parseArgs(args) {
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (normalize) {
|
|
8
|
-
args.shift();
|
|
4
|
+
// gone in 0.10.0: one format on both sides normalizes (ADR 0006)
|
|
5
|
+
if (args[0] === "normalize") {
|
|
6
|
+
throw new CliError("'morg normalize' is gone: the same format and preset on both sides normalizes (--from org --to org)");
|
|
9
7
|
}
|
|
10
8
|
const parsed = {
|
|
11
|
-
normalize,
|
|
12
9
|
help: false,
|
|
13
10
|
version: false,
|
|
14
11
|
fromFormat: undefined,
|
package/dist/cli/conversion.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MorgConfig } from "../config.js";
|
|
2
2
|
import type { MarkdownStyleOptions } from "../options.js";
|
|
3
|
-
import type { PresetOptions } from "../presets/sides.js";
|
|
3
|
+
import type { Format, PresetOptions } from "../presets/sides.js";
|
|
4
4
|
import type { CliArgs } from "./args.js";
|
|
5
5
|
export declare function buildConversionOptions(cli: CliArgs, config: MorgConfig, presets: PresetOptions): {
|
|
6
6
|
mdToOrgOptions: import("../options.js").MarkdownToOrgOptions;
|
|
@@ -8,4 +8,4 @@ export declare function buildConversionOptions(cli: CliArgs, config: MorgConfig,
|
|
|
8
8
|
markdownStyle: MarkdownStyleOptions;
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
|
-
export declare function convert(inputContent: string, fromFormat:
|
|
11
|
+
export declare function convert(inputContent: string, fromFormat: Format, toFormat: Format, cli: CliArgs, config: MorgConfig, presets: PresetOptions): string;
|
package/dist/cli/conversion.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { convertMarkdownToOrg } from "../markdownToOrg.js";
|
|
2
2
|
import { convertOrgToMarkdown } from "../orgToMarkdown.js";
|
|
3
|
-
import {
|
|
3
|
+
import { convertWithinFormat } from "../withinFormat.js";
|
|
4
4
|
import { buildConversionOptions as layerOptions } from "../conversionOptions.js";
|
|
5
5
|
import { CliError } from "./error.js";
|
|
6
6
|
export function buildConversionOptions(cli, config, presets) {
|
|
@@ -28,16 +28,14 @@ export function buildConversionOptions(cli, config, presets) {
|
|
|
28
28
|
...(config.orgismKeys && { orgismKeys: config.orgismKeys })
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
export function convert(inputContent, fromFormat,
|
|
31
|
+
export function convert(inputContent, fromFormat, toFormat, cli, config, presets) {
|
|
32
32
|
try {
|
|
33
33
|
const { mdToOrgOptions, orgToMdOptions } = buildConversionOptions(cli, config, presets);
|
|
34
|
-
if (
|
|
35
|
-
return fromFormat
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
: normalizeOrg(inputContent, { ...mdToOrgOptions, ...orgToMdOptions });
|
|
34
|
+
if (fromFormat === toFormat) {
|
|
35
|
+
return convertWithinFormat(inputContent, fromFormat, {
|
|
36
|
+
...mdToOrgOptions,
|
|
37
|
+
...orgToMdOptions
|
|
38
|
+
});
|
|
41
39
|
}
|
|
42
40
|
if (fromFormat === "markdown") {
|
|
43
41
|
return convertMarkdownToOrg(inputContent, mdToOrgOptions);
|
package/dist/cli/formats.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { CliArgs } from "./args.js";
|
|
2
2
|
import type { Format } from "../presets/sides.js";
|
|
3
3
|
export declare function inferFormats(cli: CliArgs): [fromFormat: string | undefined, toFormat: string | undefined];
|
|
4
|
-
export declare function validateFormats(fromFormat: string | undefined, toFormat: string | undefined
|
|
4
|
+
export declare function validateFormats(fromFormat: string | undefined, toFormat: string | undefined): [fromFormat: Format, toFormat: Format];
|
package/dist/cli/formats.js
CHANGED
|
@@ -9,17 +9,11 @@ export function inferFormats(cli) {
|
|
|
9
9
|
if (cli.outputFile && !toFormat) {
|
|
10
10
|
toFormat = formatFromFileName(cli.outputFile);
|
|
11
11
|
}
|
|
12
|
-
return inferMissingFormat(
|
|
12
|
+
return inferMissingFormat(fromFormat, toFormat);
|
|
13
13
|
}
|
|
14
14
|
// Infer missing format based on the other
|
|
15
|
-
function inferMissingFormat(
|
|
16
|
-
if (
|
|
17
|
-
// mirror whichever side is known; a conflict between the two is left
|
|
18
|
-
// intact for validateFormats to reject rather than silently overwritten
|
|
19
|
-
fromFormat = fromFormat ?? toFormat;
|
|
20
|
-
toFormat = toFormat ?? fromFormat;
|
|
21
|
-
}
|
|
22
|
-
else if (fromFormat && !toFormat) {
|
|
15
|
+
function inferMissingFormat(fromFormat, toFormat) {
|
|
16
|
+
if (fromFormat && !toFormat) {
|
|
23
17
|
toFormat = fromFormat === "markdown" ? "org" : "markdown";
|
|
24
18
|
}
|
|
25
19
|
else if (toFormat && !fromFormat) {
|
|
@@ -36,7 +30,7 @@ function fileNameHint(flag, value) {
|
|
|
36
30
|
const fileFlag = flag === "--from" ? "--input" : "--output";
|
|
37
31
|
return `\n${flag} takes a format name; for a file use ${fileFlag} ${value}.`;
|
|
38
32
|
}
|
|
39
|
-
export function validateFormats(fromFormat, toFormat
|
|
33
|
+
export function validateFormats(fromFormat, toFormat) {
|
|
40
34
|
if (!fromFormat || !toFormat) {
|
|
41
35
|
throw new CliError("Error: Could not determine conversion formats.\n" +
|
|
42
36
|
"Please specify --from and --to, or provide input/output files with .md or .org extensions.");
|
|
@@ -50,12 +44,5 @@ export function validateFormats(fromFormat, toFormat, normalize) {
|
|
|
50
44
|
`'markdown' and 'org'.${fileNameHint(flag, value)}`);
|
|
51
45
|
}
|
|
52
46
|
}
|
|
53
|
-
if (!normalize && fromFormat === toFormat) {
|
|
54
|
-
throw new CliError("Error: Source and target formats cannot be the same.");
|
|
55
|
-
}
|
|
56
|
-
if (normalize && fromFormat !== toFormat) {
|
|
57
|
-
throw new CliError("Error: normalize reads and writes the same format; " +
|
|
58
|
-
`got '${fromFormat}' and '${toFormat}'.`);
|
|
59
|
-
}
|
|
60
47
|
return [fromFormat, toFormat];
|
|
61
48
|
}
|
package/dist/cli/help.js
CHANGED
|
@@ -5,15 +5,13 @@ const DESCRIPTION_COLUMN = Math.max(...FLAGS.map(spec => signature(spec).length)
|
|
|
5
5
|
const section = (specs) => specs
|
|
6
6
|
.map(spec => ` ${signature(spec).padEnd(DESCRIPTION_COLUMN)}${spec.description}`)
|
|
7
7
|
.join("\n");
|
|
8
|
-
export const HELP_TEXT = `Usage: morg [
|
|
8
|
+
export const HELP_TEXT = `Usage: morg [options]
|
|
9
9
|
|
|
10
10
|
Convert between Markdown and Org-mode. Reads stdin and writes stdout
|
|
11
11
|
unless --input/--output are given; formats are inferred from the .md and
|
|
12
12
|
.org file extensions, so --from/--to are only needed for stdin or stdout.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
${"normalize".padEnd(DESCRIPTION_COLUMN)}Round-trip a document through the other format
|
|
16
|
-
${"".padEnd(DESCRIPTION_COLUMN)}and back, canonicalizing it in its own format
|
|
13
|
+
One format on both sides translates between two presets' dialects, and
|
|
14
|
+
with one preset normalizes: a round trip through the other format.
|
|
17
15
|
|
|
18
16
|
Options:
|
|
19
17
|
${section(FLAGS.filter(spec => spec.kind !== "style"))}
|
|
@@ -24,5 +22,6 @@ ${section(FLAGS.filter(spec => spec.kind === "style"))}
|
|
|
24
22
|
Examples:
|
|
25
23
|
morg --input notes.md --output notes.org
|
|
26
24
|
morg --from markdown < notes.md > notes.org
|
|
27
|
-
morg
|
|
25
|
+
morg --input-preset logseq --input page.md --output page.vanilla.md
|
|
26
|
+
morg --input notes.org --output notes.org
|
|
28
27
|
`;
|
package/dist/cli.js
CHANGED
|
@@ -46,10 +46,10 @@ async function main() {
|
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
const config = loadConfig(cli.configPath);
|
|
49
|
-
const [fromFormat, toFormat] = validateFormats(...inferFormats(cli)
|
|
49
|
+
const [fromFormat, toFormat] = validateFormats(...inferFormats(cli));
|
|
50
50
|
const presets = resolvePresets(cli, config, fromFormat, toFormat);
|
|
51
51
|
const inputContent = await readInput(cli.inputFile);
|
|
52
|
-
const outputContent = convert(inputContent, fromFormat,
|
|
52
|
+
const outputContent = convert(inputContent, fromFormat, toFormat, cli, config, presets);
|
|
53
53
|
if (cli.outputFile) {
|
|
54
54
|
fs.writeFileSync(cli.outputFile, outputContent, "utf8");
|
|
55
55
|
}
|
|
@@ -40,9 +40,9 @@ export interface PresetNames {
|
|
|
40
40
|
* per side, the first layer that sets the side or `preset` wins.
|
|
41
41
|
* @param layers Preset names by layer, highest first.
|
|
42
42
|
* @param from The input's format.
|
|
43
|
-
* @param to The output's format
|
|
43
|
+
* @param to The output's format.
|
|
44
44
|
* @returns The conversion's preset options.
|
|
45
45
|
* @throws If a layer sets `preset` and another side preset, or a side
|
|
46
|
-
* preset has no dialect for its side's format
|
|
46
|
+
* preset has no dialect for its side's format.
|
|
47
47
|
*/
|
|
48
48
|
export declare function resolvePresetOptions(layers: PresetNames[], from: Format, to: Format): PresetOptions;
|
|
@@ -60,13 +60,14 @@ function sidePreset(name, format, side) {
|
|
|
60
60
|
? resolveSide(undefined, preset, format, side)
|
|
61
61
|
: resolveSide(preset, undefined, format, side);
|
|
62
62
|
}
|
|
63
|
-
//
|
|
64
|
-
//
|
|
63
|
+
// one preset on both sides of one format normalizes, going there and
|
|
64
|
+
// back within its dialect, so it sets both ways of the trip; two
|
|
65
|
+
// translate (ADR 0006)
|
|
65
66
|
function normalizePresetOptions(layers) {
|
|
66
67
|
const input = sideName(layers, "inputPreset")?.name ?? "vanilla";
|
|
67
68
|
const output = sideName(layers, "outputPreset")?.name ?? "vanilla";
|
|
68
69
|
if (input !== output) {
|
|
69
|
-
|
|
70
|
+
return undefined;
|
|
70
71
|
}
|
|
71
72
|
const preset = createPreset(input);
|
|
72
73
|
return preset ? { preset } : {};
|
|
@@ -76,15 +77,16 @@ function normalizePresetOptions(layers) {
|
|
|
76
77
|
* per side, the first layer that sets the side or `preset` wins.
|
|
77
78
|
* @param layers Preset names by layer, highest first.
|
|
78
79
|
* @param from The input's format.
|
|
79
|
-
* @param to The output's format
|
|
80
|
+
* @param to The output's format.
|
|
80
81
|
* @returns The conversion's preset options.
|
|
81
82
|
* @throws If a layer sets `preset` and another side preset, or a side
|
|
82
|
-
* preset has no dialect for its side's format
|
|
83
|
+
* preset has no dialect for its side's format.
|
|
83
84
|
*/
|
|
84
85
|
export function resolvePresetOptions(layers, from, to) {
|
|
85
86
|
layers.forEach(rejectConflict);
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
const normalize = from === to ? normalizePresetOptions(layers) : undefined;
|
|
88
|
+
if (normalize) {
|
|
89
|
+
return normalize;
|
|
88
90
|
}
|
|
89
91
|
const input = sidePreset(sideName(layers, "inputPreset"), from, "input");
|
|
90
92
|
const output = sidePreset(sideName(layers, "outputPreset"), to, "output");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Markdown string with its code, math, frontmatter and raw HTML
|
|
3
|
+
* blanked out (NUL characters, one per character), so a scan for syntax
|
|
4
|
+
* finds it outside them only, at the offsets it has in the string.
|
|
5
|
+
* @param markdown The Markdown string.
|
|
6
|
+
* @returns The masked string, as long as the input.
|
|
7
|
+
*/
|
|
8
|
+
export declare function maskCode(markdown: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Maps a Markdown string's text, each stretch between code, math,
|
|
11
|
+
* frontmatter and raw HTML on its own, leaving those as written; a
|
|
12
|
+
* stretch ends where a table starts or ends too.
|
|
13
|
+
* @param markdown The Markdown string.
|
|
14
|
+
* @param map What a stretch of text becomes, told if it is in a table.
|
|
15
|
+
* @returns The mapped Markdown string.
|
|
16
|
+
*/
|
|
17
|
+
export declare function mapOutsideCode(markdown: string, map: (text: string, inTable: boolean) => string): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { unified } from "unified";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
5
|
+
import remarkMath from "remark-math";
|
|
6
|
+
// what holds no Markdown text: code, math, frontmatter and raw HTML
|
|
7
|
+
const VERBATIM = new Set([
|
|
8
|
+
"code",
|
|
9
|
+
"inlineCode",
|
|
10
|
+
"math",
|
|
11
|
+
"inlineMath",
|
|
12
|
+
"yaml",
|
|
13
|
+
"html"
|
|
14
|
+
]);
|
|
15
|
+
function verbatimRanges(node, ranges) {
|
|
16
|
+
if (VERBATIM.has(node.type)) {
|
|
17
|
+
ranges.push([
|
|
18
|
+
node.position?.start.offset ?? 0,
|
|
19
|
+
node.position?.end.offset ?? 0
|
|
20
|
+
]);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const child of node.children ?? []) {
|
|
24
|
+
verbatimRanges(child, ranges);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function parse(markdown) {
|
|
28
|
+
return unified()
|
|
29
|
+
.use(remarkParse)
|
|
30
|
+
.use(remarkGfm)
|
|
31
|
+
.use(remarkFrontmatter)
|
|
32
|
+
.use(remarkMath)
|
|
33
|
+
.parse(markdown);
|
|
34
|
+
}
|
|
35
|
+
function mask(markdown, tree) {
|
|
36
|
+
const ranges = [];
|
|
37
|
+
verbatimRanges(tree, ranges);
|
|
38
|
+
let masked = "";
|
|
39
|
+
let from = 0;
|
|
40
|
+
for (const [start, end] of ranges) {
|
|
41
|
+
masked += markdown.slice(from, start) + "\0".repeat(end - start);
|
|
42
|
+
from = end;
|
|
43
|
+
}
|
|
44
|
+
return masked + markdown.slice(from);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A Markdown string with its code, math, frontmatter and raw HTML
|
|
48
|
+
* blanked out (NUL characters, one per character), so a scan for syntax
|
|
49
|
+
* finds it outside them only, at the offsets it has in the string.
|
|
50
|
+
* @param markdown The Markdown string.
|
|
51
|
+
* @returns The masked string, as long as the input.
|
|
52
|
+
*/
|
|
53
|
+
export function maskCode(markdown) {
|
|
54
|
+
return mask(markdown, parse(markdown));
|
|
55
|
+
}
|
|
56
|
+
function tableRanges(node, ranges) {
|
|
57
|
+
if (node.type === "table") {
|
|
58
|
+
ranges.push([
|
|
59
|
+
node.position?.start.offset ?? 0,
|
|
60
|
+
node.position?.end.offset ?? 0
|
|
61
|
+
]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const child of node.children ?? []) {
|
|
65
|
+
tableRanges(child, ranges);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Maps a Markdown string's text, each stretch between code, math,
|
|
70
|
+
* frontmatter and raw HTML on its own, leaving those as written; a
|
|
71
|
+
* stretch ends where a table starts or ends too.
|
|
72
|
+
* @param markdown The Markdown string.
|
|
73
|
+
* @param map What a stretch of text becomes, told if it is in a table.
|
|
74
|
+
* @returns The mapped Markdown string.
|
|
75
|
+
*/
|
|
76
|
+
export function mapOutsideCode(markdown, map) {
|
|
77
|
+
const tree = parse(markdown);
|
|
78
|
+
const tables = [];
|
|
79
|
+
tableRanges(tree, tables);
|
|
80
|
+
const bounds = tables.flat();
|
|
81
|
+
return [...mask(markdown, tree).matchAll(/\0+|[^\0]+/g)]
|
|
82
|
+
.map(({ 0: run, index }) => {
|
|
83
|
+
const end = index + run.length;
|
|
84
|
+
if (run.startsWith("\0")) {
|
|
85
|
+
return markdown.slice(index, end);
|
|
86
|
+
}
|
|
87
|
+
const cuts = [index, ...bounds.filter(b => b > index && b < end), end];
|
|
88
|
+
return cuts
|
|
89
|
+
.slice(1)
|
|
90
|
+
.map((to, i) => {
|
|
91
|
+
const from = cuts[i] ?? index;
|
|
92
|
+
const inTable = tables.some(([s, e]) => from >= s && from < e);
|
|
93
|
+
return map(markdown.slice(from, to), inTable);
|
|
94
|
+
})
|
|
95
|
+
.join("");
|
|
96
|
+
})
|
|
97
|
+
.join("");
|
|
98
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MarkdownStyleOptions } from "../options.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rewrites the markers a style names, and nothing else: the rest of the
|
|
4
|
+
* Markdown stays as written, where a stringifier would canonicalize it.
|
|
5
|
+
* @param markdown The Markdown string.
|
|
6
|
+
* @param style The markers to write.
|
|
7
|
+
* @returns The Markdown string with those markers.
|
|
8
|
+
*/
|
|
9
|
+
export declare function restyleMarkdown(markdown: string, style: MarkdownStyleOptions): string;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { unified } from "unified";
|
|
2
|
+
import remarkParse from "remark-parse";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
5
|
+
import remarkMath from "remark-math";
|
|
6
|
+
import { visit } from "unist-util-visit";
|
|
7
|
+
function offsets(node) {
|
|
8
|
+
return [node.position?.start.offset ?? 0, node.position?.end.offset ?? 0];
|
|
9
|
+
}
|
|
10
|
+
function bulletEdits(tree, bullet) {
|
|
11
|
+
const edits = [];
|
|
12
|
+
visit(tree, "list", (list) => {
|
|
13
|
+
if (list.ordered) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
for (const item of list.children) {
|
|
17
|
+
const [start] = offsets(item);
|
|
18
|
+
edits.push([start, start + 1, bullet]);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return edits;
|
|
22
|
+
}
|
|
23
|
+
// `_` closes no emphasis within a word
|
|
24
|
+
function inWord(markdown, start, end) {
|
|
25
|
+
return /\w/.test(markdown[start - 1] ?? "") || /\w/.test(markdown[end] ?? "");
|
|
26
|
+
}
|
|
27
|
+
function delimiterEdits(tree, markdown, type, marker) {
|
|
28
|
+
const width = type === "strong" ? 2 : 1;
|
|
29
|
+
const edits = [];
|
|
30
|
+
visit(tree, type, (node) => {
|
|
31
|
+
const [start, end] = offsets(node);
|
|
32
|
+
if (markdown[start] === marker ||
|
|
33
|
+
(marker === "_" && inWord(markdown, start, end))) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const markers = marker.repeat(width);
|
|
37
|
+
edits.push([start, start + width, markers], [end - width, end, markers]);
|
|
38
|
+
});
|
|
39
|
+
return edits;
|
|
40
|
+
}
|
|
41
|
+
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
42
|
+
// a fenced block's opening and closing fence, unless its code holds a
|
|
43
|
+
// run of the new marker, which could close it
|
|
44
|
+
function fenceEdits(tree, markdown, fence) {
|
|
45
|
+
const edits = [];
|
|
46
|
+
visit(tree, "code", (node) => {
|
|
47
|
+
const [start, end] = offsets(node);
|
|
48
|
+
const source = markdown.slice(start, end);
|
|
49
|
+
const run = FENCE_RE.exec(source)?.[1];
|
|
50
|
+
if (!run || run[0] === fence || node.value.includes(fence.repeat(3))) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const markers = fence.repeat(run.length);
|
|
54
|
+
const open = start + source.indexOf(run);
|
|
55
|
+
edits.push([open, open + run.length, markers]);
|
|
56
|
+
// the closing fence may be longer, and missing at the document's end
|
|
57
|
+
const last = source.lastIndexOf("\n") + 1;
|
|
58
|
+
const closing = /^ *([`~]+) *$/.exec(source.slice(last));
|
|
59
|
+
if (last && closing?.[1]) {
|
|
60
|
+
const at = start + last + source.slice(last).indexOf(closing[1]);
|
|
61
|
+
edits.push([at, at + closing[1].length, fence.repeat(closing[1].length)]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return edits;
|
|
65
|
+
}
|
|
66
|
+
// a thematic break, as long as it was unless the style says how long
|
|
67
|
+
function ruleEdits(tree, markdown, { rule, ruleRepetition }) {
|
|
68
|
+
const edits = [];
|
|
69
|
+
visit(tree, "thematicBreak", (node) => {
|
|
70
|
+
const [start, end] = offsets(node);
|
|
71
|
+
const source = markdown.slice(start, end).trim();
|
|
72
|
+
const marker = rule ?? source[0] ?? "-";
|
|
73
|
+
const count = ruleRepetition ?? source.replace(/\s/g, "").length;
|
|
74
|
+
edits.push([start, end, marker.repeat(Math.max(3, count))]);
|
|
75
|
+
});
|
|
76
|
+
return edits;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Rewrites the markers a style names, and nothing else: the rest of the
|
|
80
|
+
* Markdown stays as written, where a stringifier would canonicalize it.
|
|
81
|
+
* @param markdown The Markdown string.
|
|
82
|
+
* @param style The markers to write.
|
|
83
|
+
* @returns The Markdown string with those markers.
|
|
84
|
+
*/
|
|
85
|
+
export function restyleMarkdown(markdown, style) {
|
|
86
|
+
const tree = unified()
|
|
87
|
+
.use(remarkParse)
|
|
88
|
+
.use(remarkGfm)
|
|
89
|
+
.use(remarkFrontmatter)
|
|
90
|
+
.use(remarkMath)
|
|
91
|
+
.parse(markdown);
|
|
92
|
+
const edits = [
|
|
93
|
+
...(style.bullet ? bulletEdits(tree, style.bullet) : []),
|
|
94
|
+
...(style.emphasis
|
|
95
|
+
? delimiterEdits(tree, markdown, "emphasis", style.emphasis)
|
|
96
|
+
: []),
|
|
97
|
+
...(style.strong
|
|
98
|
+
? delimiterEdits(tree, markdown, "strong", style.strong)
|
|
99
|
+
: []),
|
|
100
|
+
...(style.fence ? fenceEdits(tree, markdown, style.fence) : []),
|
|
101
|
+
...(style.rule || style.ruleRepetition
|
|
102
|
+
? ruleEdits(tree, markdown, style)
|
|
103
|
+
: [])
|
|
104
|
+
];
|
|
105
|
+
let result = markdown;
|
|
106
|
+
for (const [start, end, text] of edits.sort((a, b) => b[0] - a[0])) {
|
|
107
|
+
result = result.slice(0, start) + text + result.slice(end);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { convertMarkdownToOrg } from "./markdownToOrg.js";
|
|
|
2
2
|
export { convertOrgToMarkdown } from "./orgToMarkdown.js";
|
|
3
3
|
export { normalizeMarkdown, normalizeOrg } from "./normalize.js";
|
|
4
4
|
export type { NormalizeOptions } from "./normalize.js";
|
|
5
|
+
export { translateMarkdown, translateOrg } from "./translate.js";
|
|
6
|
+
export type { TranslateOptions } from "./translate.js";
|
|
5
7
|
export { parseConfig } from "./config.js";
|
|
6
8
|
export type { MorgConfig } from "./config.js";
|
|
7
9
|
export { transformMdastToUniorgAst } from "./core/mdastToUniorg/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { convertMarkdownToOrg } from "./markdownToOrg.js";
|
|
2
2
|
export { convertOrgToMarkdown } from "./orgToMarkdown.js";
|
|
3
3
|
export { normalizeMarkdown, normalizeOrg } from "./normalize.js";
|
|
4
|
+
export { translateMarkdown, translateOrg } from "./translate.js";
|
|
4
5
|
export { parseConfig } from "./config.js";
|
|
5
6
|
export { transformMdastToUniorgAst } from "./core/mdastToUniorg/index.js";
|
|
6
7
|
export { transformUniorgAstToMdast } from "./core/uniorgToMdast/index.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A labeled org fuzzy link in text (`[[Page][label]]`): the form a
|
|
3
|
+
* translation's page links pass through between two Markdown dialects
|
|
4
|
+
* (`MarkdownDialect.links`), as a conversion's pass through org.
|
|
5
|
+
*/
|
|
6
|
+
export const FUZZY_LINK_RE = /\[\[([^\][]+)\]\[([^\][]+)\]\]/g;
|