@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/dist/presets/obsidian.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { visit } from "unist-util-visit";
|
|
2
2
|
import { toString } from "orgast-util-to-string";
|
|
3
|
+
import { maskCode } from "../core/outsideCode.js";
|
|
4
|
+
import { FUZZY_LINK_RE } from "./links.js";
|
|
3
5
|
/**
|
|
4
6
|
* Obsidian dialect preset: `[[Page]]` / `[[Page|alias]]` wikilinks map
|
|
5
7
|
* to org fuzzy links (`[[Page]]` / `[[Page][alias]]`).
|
|
@@ -9,16 +11,29 @@ export function obsidian() {
|
|
|
9
11
|
name: "obsidian",
|
|
10
12
|
markdown: {
|
|
11
13
|
read: { org: rewriteAliasedWikilinks },
|
|
12
|
-
write: fuzzyLinksToWikilinks
|
|
13
|
-
|
|
14
|
+
write: fuzzyLinksToWikilinks,
|
|
15
|
+
links: {
|
|
16
|
+
read: text => text.replace(ALIASED_PAGE_LINK_RE, "[[$1][$2]]"),
|
|
17
|
+
// a bare `|` would split a table's cell
|
|
18
|
+
write: (text, inTable) => text.replace(FUZZY_LINK_RE, inTable ? "[[$1\\|$2]]" : "[[$1|$2]]")
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
// Vanilla Markdown reads Obsidian's own syntax but for these; the
|
|
22
|
+
// way back has nothing to do
|
|
23
|
+
translateMarkdown: (markdown, context) => context.side === "input" ? toVanilla(markdown) : markdown
|
|
14
24
|
};
|
|
15
25
|
}
|
|
26
|
+
// not an embed, whose `|300` is a size
|
|
27
|
+
const ALIASED_WIKILINK_RE = /(?<!!)\[\[([^\][|]+)\|([^\][]+)\]\]/g;
|
|
28
|
+
// in Markdown text: not an embed, whose `|300` is a size, and with a
|
|
29
|
+
// table cell's escaped pipe (`[[Page\|alias]]`)
|
|
30
|
+
const ALIASED_PAGE_LINK_RE = /(?<!!)\[\[([^\][|\\]+)\\?\|([^\][]+)\]\]/g;
|
|
16
31
|
// md→org: a wikilink travels as plain text; org already reads `[[Page]]`
|
|
17
32
|
// as a fuzzy link, only the `[[Page|alias]]` form needs rewriting to
|
|
18
33
|
// org's `[[Page][alias]]` description syntax
|
|
19
34
|
function rewriteAliasedWikilinks(uniorgAst) {
|
|
20
35
|
visit(uniorgAst, "text", (node) => {
|
|
21
|
-
node.value = node.value.replace(
|
|
36
|
+
node.value = node.value.replace(ALIASED_WIKILINK_RE, "[[$1][$2]]");
|
|
22
37
|
});
|
|
23
38
|
return uniorgAst;
|
|
24
39
|
}
|
|
@@ -30,13 +45,69 @@ function fuzzyLinksToWikilinks(uniorgAst) {
|
|
|
30
45
|
return undefined;
|
|
31
46
|
}
|
|
32
47
|
const description = node.children.length ? toString(node) : "";
|
|
48
|
+
// an embed's `!` goes with it, else Markdown escapes it before `[`
|
|
49
|
+
const previous = parent.children[index - 1];
|
|
50
|
+
const embed = previous?.type === "text" && previous.value.endsWith("!");
|
|
51
|
+
if (embed) {
|
|
52
|
+
previous.value = previous.value.slice(0, -1);
|
|
53
|
+
}
|
|
33
54
|
parent.children[index] = {
|
|
34
55
|
type: "verbatim-inline",
|
|
35
|
-
value: description
|
|
56
|
+
value: `${embed ? "!" : ""}${description
|
|
36
57
|
? `[[${node.rawLink}|${description}]]`
|
|
37
|
-
: `[[${node.rawLink}]]`
|
|
58
|
+
: `[[${node.rawLink}]]`}`
|
|
38
59
|
};
|
|
39
60
|
return undefined;
|
|
40
61
|
});
|
|
41
62
|
return uniorgAst;
|
|
42
63
|
}
|
|
64
|
+
const COMMENT_RE = /%%([\s\S]*?)%%/g;
|
|
65
|
+
const FOOTNOTE_LABEL_RE = /\[\^(\d+)\]/g;
|
|
66
|
+
// Obsidian Markdown → Vanilla Markdown: a comment is an HTML comment,
|
|
67
|
+
// an inline footnote a footnote, numbered on from the page's own, its
|
|
68
|
+
// definition at the end; their delimiters count outside code only, what
|
|
69
|
+
// they hold may be code
|
|
70
|
+
function toVanilla(markdown) {
|
|
71
|
+
let masked = maskCode(markdown);
|
|
72
|
+
const edits = [];
|
|
73
|
+
for (const { index, 0: comment } of masked.matchAll(COMMENT_RE)) {
|
|
74
|
+
const end = index + comment.length;
|
|
75
|
+
edits.push([index, end, `<!--${markdown.slice(index + 2, end - 2)}-->`]);
|
|
76
|
+
// a footnote in a comment is part of it
|
|
77
|
+
masked =
|
|
78
|
+
masked.slice(0, index) + "\0".repeat(comment.length) + masked.slice(end);
|
|
79
|
+
}
|
|
80
|
+
let next = Math.max(0, ...[...markdown.matchAll(FOOTNOTE_LABEL_RE)].map(([, n]) => Number(n))) + 1;
|
|
81
|
+
const definitions = [];
|
|
82
|
+
for (const [start, end] of inlineFootnotes(masked)) {
|
|
83
|
+
definitions.push(`[^${next}]: ${markdown.slice(start + 2, end)}`);
|
|
84
|
+
edits.push([start, end + 1, `[^${next++}]`]);
|
|
85
|
+
}
|
|
86
|
+
let result = markdown;
|
|
87
|
+
for (const [start, end, text] of edits.sort((a, b) => b[0] - a[0])) {
|
|
88
|
+
result = result.slice(0, start) + text + result.slice(end);
|
|
89
|
+
}
|
|
90
|
+
return definitions.length
|
|
91
|
+
? `${result.replace(/\n*$/, "")}\n\n${definitions.join("\n")}\n`
|
|
92
|
+
: result;
|
|
93
|
+
}
|
|
94
|
+
// each `^[note]`, its brackets balanced: where it starts, and its `]`
|
|
95
|
+
function inlineFootnotes(text) {
|
|
96
|
+
const notes = [];
|
|
97
|
+
for (let start = text.indexOf("^["); start !== -1;) {
|
|
98
|
+
let depth = 0;
|
|
99
|
+
let end = start + 1;
|
|
100
|
+
for (; end < text.length; end++) {
|
|
101
|
+
depth += text[end] === "[" ? 1 : text[end] === "]" ? -1 : 0;
|
|
102
|
+
if (!depth) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (end === text.length) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
notes.push([start, end]);
|
|
110
|
+
start = text.indexOf("^[", end + 1);
|
|
111
|
+
}
|
|
112
|
+
return notes;
|
|
113
|
+
}
|
package/dist/presets/types.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ import type { OrgData } from "uniorg";
|
|
|
7
7
|
* `convertMarkdown` take a whole conversion over, for a dialect whose
|
|
8
8
|
* documents are no single org or Markdown document (an outline of
|
|
9
9
|
* blocks, each its own fragment); `convert` runs the core on a fragment,
|
|
10
|
-
* with the given preset's hooks.
|
|
10
|
+
* with the given preset's hooks. `translateOrg` and `translateMarkdown`
|
|
11
|
+
* translate a page between the preset's dialect of the format and
|
|
12
|
+
* Vanilla.
|
|
11
13
|
*/
|
|
12
14
|
export interface Preset {
|
|
13
15
|
name: string;
|
|
@@ -15,6 +17,8 @@ export interface Preset {
|
|
|
15
17
|
org?: OrgDialect;
|
|
16
18
|
convertOrg?: (org: string, convert: FragmentConverter, context: ConversionContext) => string;
|
|
17
19
|
convertMarkdown?: (markdown: string, convert: FragmentConverter, context: ConversionContext) => string;
|
|
20
|
+
translateOrg?: (org: string, context: ConversionContext) => string;
|
|
21
|
+
translateMarkdown?: (markdown: string, context: ConversionContext) => string;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* What a preset that takes a whole conversion over learns of it: the
|
|
@@ -25,6 +29,11 @@ export interface ConversionContext {
|
|
|
25
29
|
side: "both" | "input" | "output";
|
|
26
30
|
onWarning?: (message: string) => void;
|
|
27
31
|
orgismKeys?: Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* A translation's page links from the input's dialect into the
|
|
34
|
+
* output's, in a table's text or not.
|
|
35
|
+
*/
|
|
36
|
+
relink?: (text: string, inTable: boolean) => string;
|
|
28
37
|
}
|
|
29
38
|
/**
|
|
30
39
|
* Converts a fragment with the core, the given preset's AST hooks on
|
|
@@ -44,6 +53,17 @@ export interface MarkdownDialect {
|
|
|
44
53
|
org?: (uniorgAst: OrgData) => OrgData;
|
|
45
54
|
};
|
|
46
55
|
write?: (uniorgAst: OrgData) => OrgData;
|
|
56
|
+
/**
|
|
57
|
+
* Its page links in Markdown text to org's fuzzy link syntax
|
|
58
|
+
* (`[[Page][label]]`) and back, for a translation between two
|
|
59
|
+
* dialects; `write` is told if the text is in a table.
|
|
60
|
+
*/
|
|
61
|
+
links?: {
|
|
62
|
+
read: (text: string) => string;
|
|
63
|
+
write: (text: string, inTable: boolean) => string;
|
|
64
|
+
};
|
|
65
|
+
/** The bullet its outline needs, which no style may change. */
|
|
66
|
+
bullet?: "-" | "*" | "+";
|
|
47
67
|
}
|
|
48
68
|
/**
|
|
49
69
|
* A preset's org dialect. `read` runs in org→md before the generic
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { MarkdownStyleOptions } from "./options.js";
|
|
2
|
+
import { type PresetOptions } from "./presets/sides.js";
|
|
3
|
+
/**
|
|
4
|
+
* The presets of a translation, its warning callback and the org-ism
|
|
5
|
+
* key names Vanilla Markdown writes planning under.
|
|
6
|
+
*/
|
|
7
|
+
export type TranslateOptions = PresetOptions & {
|
|
8
|
+
onWarning?: (message: string) => void;
|
|
9
|
+
orgismKeys?: Record<string, string>;
|
|
10
|
+
/** The Markdown markers to write; others stay as written. */
|
|
11
|
+
markdownStyle?: MarkdownStyleOptions;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Translates an Org string from the Input Preset's dialect into the
|
|
15
|
+
* Output Preset's, changing only what the two dialects write
|
|
16
|
+
* differently (ADR 0006): a block's content is kept as written.
|
|
17
|
+
* @param org The Org string to translate.
|
|
18
|
+
* @param options The preset of each side, which must differ.
|
|
19
|
+
* @returns The Org string in the output's dialect.
|
|
20
|
+
* @throws If both sides name the same preset (that is `normalizeOrg`),
|
|
21
|
+
* or a side preset has no org dialect.
|
|
22
|
+
*/
|
|
23
|
+
export declare function translateOrg(org: string, options?: TranslateOptions): string;
|
|
24
|
+
/**
|
|
25
|
+
* Translates a Markdown string from the Input Preset's dialect into the
|
|
26
|
+
* Output Preset's, changing only what the two dialects write
|
|
27
|
+
* differently (ADR 0006): a block's content is kept as written.
|
|
28
|
+
* @param markdown The Markdown string to translate.
|
|
29
|
+
* @param options The preset of each side, which must differ.
|
|
30
|
+
* @returns The Markdown string in the output's dialect.
|
|
31
|
+
* @throws If both sides name the same preset (that is
|
|
32
|
+
* `normalizeMarkdown`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function translateMarkdown(markdown: string, options?: TranslateOptions): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { restyleMarkdown } from "./core/restyle.js";
|
|
2
|
+
import { resolveSides } from "./presets/sides.js";
|
|
3
|
+
/**
|
|
4
|
+
* Translates an Org string from the Input Preset's dialect into the
|
|
5
|
+
* Output Preset's, changing only what the two dialects write
|
|
6
|
+
* differently (ADR 0006): a block's content is kept as written.
|
|
7
|
+
* @param org The Org string to translate.
|
|
8
|
+
* @param options The preset of each side, which must differ.
|
|
9
|
+
* @returns The Org string in the output's dialect.
|
|
10
|
+
* @throws If both sides name the same preset (that is `normalizeOrg`),
|
|
11
|
+
* or a side preset has no org dialect.
|
|
12
|
+
*/
|
|
13
|
+
export function translateOrg(org, options = {}) {
|
|
14
|
+
const { input, output } = twoSides(options, "org", "normalizeOrg");
|
|
15
|
+
// Vanilla has no dialect to translate, so the other side does it
|
|
16
|
+
const side = input?.translateOrg ? "input" : "output";
|
|
17
|
+
const translate = (side === "input" ? input : output)?.translateOrg;
|
|
18
|
+
return translate
|
|
19
|
+
? translate(org, {
|
|
20
|
+
side,
|
|
21
|
+
...(options.onWarning && { onWarning: options.onWarning })
|
|
22
|
+
})
|
|
23
|
+
: org;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Translates a Markdown string from the Input Preset's dialect into the
|
|
27
|
+
* Output Preset's, changing only what the two dialects write
|
|
28
|
+
* differently (ADR 0006): a block's content is kept as written.
|
|
29
|
+
* @param markdown The Markdown string to translate.
|
|
30
|
+
* @param options The preset of each side, which must differ.
|
|
31
|
+
* @returns The Markdown string in the output's dialect.
|
|
32
|
+
* @throws If both sides name the same preset (that is
|
|
33
|
+
* `normalizeMarkdown`).
|
|
34
|
+
*/
|
|
35
|
+
export function translateMarkdown(markdown, options = {}) {
|
|
36
|
+
const { input, output } = twoSides(options, "markdown", "normalizeMarkdown");
|
|
37
|
+
const context = hookContext(options, pageLinks({ input, output }));
|
|
38
|
+
// into Vanilla from the input's dialect, then from it into the
|
|
39
|
+
// output's; a dialect does what its side needs
|
|
40
|
+
const vanilla = input?.translateMarkdown?.(markdown, { ...context, side: "input" }) ??
|
|
41
|
+
markdown;
|
|
42
|
+
const translated = output?.translateMarkdown?.(vanilla, { ...context, side: "output" }) ??
|
|
43
|
+
vanilla;
|
|
44
|
+
const style = outputStyle(options, output);
|
|
45
|
+
return style ? restyleMarkdown(translated, style) : translated;
|
|
46
|
+
}
|
|
47
|
+
// the style to write, but for a bullet the output's outline needs
|
|
48
|
+
function outputStyle({ markdownStyle, onWarning }, output) {
|
|
49
|
+
const needed = output?.markdown?.bullet;
|
|
50
|
+
if (!markdownStyle?.bullet || !needed || markdownStyle.bullet === needed) {
|
|
51
|
+
return markdownStyle;
|
|
52
|
+
}
|
|
53
|
+
onWarning?.(`${output.name} Markdown writes its blocks with '${needed}'; bullet '${markdownStyle.bullet}' not applied`);
|
|
54
|
+
const style = { ...markdownStyle };
|
|
55
|
+
delete style.bullet;
|
|
56
|
+
return style;
|
|
57
|
+
}
|
|
58
|
+
// what a dialect's translation learns of the options
|
|
59
|
+
function hookContext({ onWarning, orgismKeys }, relink) {
|
|
60
|
+
return {
|
|
61
|
+
...(onWarning && { onWarning }),
|
|
62
|
+
...(orgismKeys && { orgismKeys }),
|
|
63
|
+
...(relink && { relink })
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// page links between two dialects; a Vanilla side carries the other's
|
|
67
|
+
function pageLinks({ input, output }) {
|
|
68
|
+
const read = input?.markdown?.links?.read;
|
|
69
|
+
const write = output?.markdown?.links?.write;
|
|
70
|
+
return read && write
|
|
71
|
+
? (text, inTable) => write(read(text), inTable)
|
|
72
|
+
: undefined;
|
|
73
|
+
}
|
|
74
|
+
// a translation is between two dialects; one on both sides normalizes
|
|
75
|
+
function twoSides(options, format, normalize) {
|
|
76
|
+
const sides = resolveSides(options, format, format);
|
|
77
|
+
const name = sides.input?.name ?? "vanilla";
|
|
78
|
+
if (name === (sides.output?.name ?? "vanilla")) {
|
|
79
|
+
throw new Error(`translation takes two presets; for '${name}' on both sides, use ${normalize}`);
|
|
80
|
+
}
|
|
81
|
+
return sides;
|
|
82
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NormalizeOptions } from "./normalize.js";
|
|
2
|
+
import type { Format } from "./presets/sides.js";
|
|
3
|
+
/**
|
|
4
|
+
* Converts within one format: two presets translate between their
|
|
5
|
+
* dialects, one normalizes (ADR 0006). What both adapters do with one
|
|
6
|
+
* format on both sides.
|
|
7
|
+
* @param text The document.
|
|
8
|
+
* @param format Its format, on both sides.
|
|
9
|
+
* @param options The conversion's options, presets included.
|
|
10
|
+
* @returns The translated or normalized document.
|
|
11
|
+
*/
|
|
12
|
+
export declare function convertWithinFormat(text: string, format: Format, options?: NormalizeOptions): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { normalizeMarkdown, normalizeOrg } from "./normalize.js";
|
|
2
|
+
import { translateMarkdown, translateOrg } from "./translate.js";
|
|
3
|
+
/**
|
|
4
|
+
* Converts within one format: two presets translate between their
|
|
5
|
+
* dialects, one normalizes (ADR 0006). What both adapters do with one
|
|
6
|
+
* format on both sides.
|
|
7
|
+
* @param text The document.
|
|
8
|
+
* @param format Its format, on both sides.
|
|
9
|
+
* @param options The conversion's options, presets included.
|
|
10
|
+
* @returns The translated or normalized document.
|
|
11
|
+
*/
|
|
12
|
+
export function convertWithinFormat(text, format, options = {}) {
|
|
13
|
+
const markdown = format === "markdown";
|
|
14
|
+
if (options.inputPreset || options.outputPreset) {
|
|
15
|
+
return (markdown ? translateMarkdown : translateOrg)(text, options);
|
|
16
|
+
}
|
|
17
|
+
return (markdown ? normalizeMarkdown : normalizeOrg)(text, options);
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remigius42/morg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Bidirectional Markdown ↔ Org-mode converter with round-trip convergence",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"org-mode",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"provenance": true
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
43
|
+
"node": ">=24"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"prepare": "husky",
|