md2org 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,71 @@
1
+ # Changelog
2
+
3
+
4
+ ## [1.1.0] — 2026-10-03
5
+
6
+ ### Added
7
+ - #8 HTML Entities translated for CLI and web as an option
8
+ - LaTeX math passes through unchanged
9
+
10
+ ### Changed
11
+ - Warnings Footer improved
12
+ - iOS Shortcut deployment target transform.js reduced in size by completely
13
+ removing all HTML entity transformation logic.
14
+
15
+ ### Fixed
16
+ - Comments in code blocks were not passing through
17
+
18
+
19
+ ## [1.0.1] — 2026-09-06
20
+
21
+ ### Added
22
+ - Entities md2org adds are now reported in the warnings footer.
23
+
24
+ ### Changed
25
+ - `shortcut/transform.js` header comment now shows md2org version.
26
+
27
+ ### Fixed
28
+ - A bare relative link path such as `[foo](url)` or `[foo](a/b.md)` became a fuzzy Org link, which Org reads as a search for a headline of that name. Export failed. This affected 26 of the 652 CommonMark spec examples.
29
+ - The web page's Copy button did nothing in Chromium browsers over plain http,
30
+ where `navigator.clipboard` is undefined. It now falls back and reports failure.
31
+ - A code span containing `]]` inside a link description ended the link early,
32
+ producing invalid Org. The span now unwraps (monospace lost, characters kept).
33
+
34
+ ## [1.0.0] — 2026-09-06
35
+
36
+ First release. Markdown to Org conversion at the command line, in the browser, and
37
+ via an iOS Shortcut.
38
+
39
+ ### Added
40
+ - `md2org --version` / `-v`.
41
+ - `CHANGELOG.md`
42
+ - CI, and a CLI test tier.
43
+
44
+ ### Changed
45
+ - DESIGN.md, MAPPING.md and README.md reconciled with the code: the escaping
46
+ table now lists every escape the program performs.
47
+ - specs in test directory moved to `specs/`
48
+
49
+ ### Fixed
50
+ - The CLI ignored unknown options and then waited on stdin, so a typo hung.
51
+ Usage errors now exit 2.
52
+ - The CLI wrote no trailing newline, leaving a malformed text file.
53
+ - `npm test` needed esbuild and so could not run where esbuild has no binary. The
54
+ derived-copy check now compares behaviour instead of rebuilding; CI still
55
+ asserts byte-identity.
56
+ - `shortcut/transform.js` size reduced by removing dead code.
57
+
58
+ ### Removed
59
+ - Dead code in `src/org-render.js` left from the pre-fork design
60
+
61
+ ## Guidelines for updating this document
62
+ Only user facing changes to this project are documented here. Entries are very brief summaries.
63
+
64
+ For a converter, "the public API" is the mapping in [MAPPING.md](MAPPING.md) as much as it is the `md2org(string)` function. A change to what a given Markdown construct becomes in Org is a change users will notice in their files, so it is treated as breaking unless the previous output was invalid Org or plainly wrong.
65
+
66
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
67
+ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
68
+
69
+ [1.1.0]: https://github.com/nicholascarroll/md2org.js/releases/tag/1.1.0
70
+ [1.0.1]: https://github.com/nicholascarroll/md2org.js/releases/tag/1.0.1
71
+ [1.0.0]: https://github.com/nicholascarroll/md2org.js/releases/tag/1.0.0
package/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nicholas Carroll
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ## Third-party code
26
+
27
+ This package bundles a modified copy of commonmark.js in `src/vendor/`,
28
+ Copyright (c) 2014, John MacFarlane, used under the BSD 2-Clause License.
29
+ Its full licence text is in `src/vendor/LICENSE-commonmark`, which is
30
+ distributed with this package.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # md2org.js
2
+
3
+ md2org.js converts Markdown markup to Org markup.
4
+
5
+ - at the command line as a Unix filter 🙂
6
+ - on your phone via an iOS Shortcut 😎
7
+ - [in your browser](https://nicholascarroll.github.io/md2org.js/) with nothing uploaded anywhere 🥷
8
+
9
+
10
+ Source text that is not Markdown markup passes through unchanged. The conversion is one-way only.
11
+
12
+ ## Use
13
+
14
+ ### 1. Command line
15
+
16
+ A Unix filter. Needs [Node.js](https://nodejs.org).
17
+
18
+ ```sh
19
+ npm install -g md2org
20
+ ```
21
+ Usage:
22
+
23
+ ```sh
24
+ md2org notes.md > notes.org
25
+ cat notes.md | md2org
26
+ md2org < notes.md
27
+ md2org --help
28
+ md2org --version
29
+ md2org -e # translate HTML entities
30
+ ```
31
+
32
+ Exit status is 0 on success, 1 if the input file can't be read, 2 for a usage
33
+ error.
34
+
35
+ ### 2. iOS Shortcut
36
+
37
+ Copy Markdown, tap the md2org button in the Control Centre and your clipboard content converts to Org. See **[shortcut/README.md](shortcut/README.md)** for the two-minute setup, or get the [drop-in Shortcut definition](shortcut/)
38
+
39
+ ### 3. In the browser
40
+
41
+ Open the [live page](https://nicholascarroll.github.io/md2org.js/). Paste Markdown, copy Org. Conversion happens entirely on your device; it sends nothing over the network.
42
+
43
+ ## Warnings Footer
44
+
45
+ md2org will write any warnings to the tail of the Org mode output as comments. For example, if your source file had a line starting with `** foo`, you will see this at the bottom of your output.
46
+
47
+ ```
48
+ # md2org warnings:
49
+ # heading: line 2
50
+ ```
51
+ This is warning you that line 2 of your output was not a heading in the source but has become a heading in the output.
52
+
53
+
54
+ ## Known lossy conversions
55
+
56
+ - Intraword emphasis `a**b**c` becomes `a*b*c`.
57
+ - Link titles: `[t](/u "title")` loses the title.
58
+ - `+ item` becomes `- item` and `_em_` becomes `/em/`.
59
+ - Raw HTML. Blocks become `#+BEGIN_EXPORT html`
60
+ - inline HTML becomes `@@html:…@@`.
61
+ - Character references pass through as you wrote them. `&mdash;` and `&HilbertSpace;`, and numeric ones such as `&#65;` and `&#x41;`, all reach the Org file untouched. For CLI and web deployment targets you can enable translation to actual Unicode codepoints.
62
+
63
+
64
+ ## Correctness
65
+
66
+ md2org.js forks and modifies the reference [CommonMark](https://spec.commonmark.org/0.31.2/) parser, and passes 652 of the specification's 652 test cases when the HTML Entities option is enabled.
67
+
68
+ Tables and footnotes are [GitHub Flavoured Markdown](specs/gfm-spec-0.29.txt) extensions that CommonMark doesn't define.
69
+
70
+ If you encounter any nonconformities please log an issue or a PR even.
71
+
72
+ ### Edge cases
73
+
74
+ The cases below are all tested:
75
+
76
+ | Markdown | Naive output would mean | What md2org emits |
77
+ |---|---|---|
78
+ | a language-tagged fence containing `#+END_SRC` | block ends early, rest of the file is body text | `,#+END_SRC` (comma-quoted) |
79
+ | `` `a=b` `` | verbatim ending at the first `=` | falls back to `~a=b~` |
80
+ | `]` in a link path | the link ends early | percent-encoded by the parser |
81
+ | `]]` in a link description | the link ends early | passed through and warned |
82
+ | `\|` in a table cell | table alignment thrown out | passed through and warned |
83
+
84
+ ### Testing
85
+
86
+ ```sh
87
+ npm test
88
+ ```
89
+
90
+ `npm test` runs ten tiers, in order:
91
+ 1. **`test/spec.js`** is the behavioural contract.
92
+ 2. **`test/cli.js`** is the CLI contract, tests `bin/md2org` in the shell.
93
+ 3. **`test/conformance.js`** is CommonMark conformance, 652/652, measured against
94
+ the spec's own Markdown/HTML pairs run as data with HTML entities option
95
+ enabled.
96
+ 4. **`test/gfm.js`** is GFM table conformance, 8/8, against the GFM spec's
97
+ examples.
98
+ 5. **`test/document.js`** is document-scale inputs.
99
+ 6. **`test/invariant.js`** verifies invariant 1: every non-markup character in
100
+ the source appears in the output.
101
+ 7. **`test/warnings.js`** verifies invariant 4, the Warnings Footer.
102
+ 8. **`test/fuzz.js`** generates 5,000 documents from a fragment corpus and
103
+ asserts that every output is structurally valid Org. `npm run fuzz` runs
104
+ 100,000.
105
+ 9. **`test/size.js`** asserts `shortcut/transform.js` still fits into the
106
+ Actions app. See [MAPPING.md](MAPPING.md#bytes-budget).
107
+ 10. **`test/emacs.js`** tests using Emacs. `npm run test:emacs`.
108
+
109
+
110
+ Specifications are in [`specs/`](specs/README.md).
111
+
112
+
113
+ ## Building
114
+
115
+ `src/` is several modules; `build.js` concatenates them into the browser and
116
+ Shortcut copies. Run it after editing anything in `src/`:
117
+
118
+ ```sh
119
+ node build.js
120
+ ```
121
+
122
+ The CommonMark parser in `src/vendor/` is generated and committed. You only need
123
+ `node tools/build-vendor.js` if you want to regenerate it.
124
+
125
+ ### Size limit on the iOS Shortcut
126
+
127
+ The iOS Shortcut is extremely tightly constrained by a size limit for the md2org core code `shortcut/transform.js`: when pasting its contents into the Actions app's JavaScript code field, the Shortcuts editor crashes. There is a limit on lines and on bytes. For that reason the Shortcut copy is minified and new features are costed in bytes in [MAPPING.md](MAPPING.md).
128
+
129
+ I've tested on iOS 26.3.1(a) and 26.6.1 (iPhone 14). If you hit this issue on your device, please open an issue with your iOS version. Might not be fixable 🙁.
130
+
131
+
132
+ ## License
133
+
134
+ MIT
package/bin/md2org ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * md2org CLI: a Unix filter that reads Markdown and writes Org.
4
+ *
5
+ * cat notes.md | md2org > notes.org
6
+ * md2org notes.md > notes.org
7
+ * md2org < notes.md
8
+ *
9
+ * Uses the same core as the web page and the iOS Shortcut.
10
+ */
11
+ "use strict";
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const md2org = require(path.join(__dirname, "..", "src", "md2org.js"));
16
+
17
+ const args = process.argv.slice(2);
18
+
19
+ const USAGE =
20
+ "md2org — convert Markdown to Org mode\n\n" +
21
+ "Usage:\n" +
22
+ " md2org [file] convert file (or stdin) to stdout\n" +
23
+ " cat f.md | md2org read from a pipe\n\n" +
24
+ "Options:\n" +
25
+ " -e, --entities decode HTML entities and numeric character references\n" +
26
+ " to the characters they name (&mdash; and &#8212; to —)\n" +
27
+ " -h, --help show this help\n" +
28
+ " -v, --version show the version\n";
29
+
30
+ if (args.includes("-h") || args.includes("--help")) {
31
+ process.stdout.write(USAGE);
32
+ process.exit(0);
33
+ }
34
+
35
+ if (args.includes("-v") || args.includes("--version")) {
36
+ process.stdout.write(require("../package.json").version + "\n");
37
+ process.exit(0);
38
+ }
39
+
40
+ // Character-reference decoding, off by default (DESIGN.md, Character references
41
+ // and entities).
42
+ const entities = args.includes("-e") || args.includes("--entities");
43
+
44
+ // Reject unknown options rather than falling through to wait on stdin.
45
+ const badOpt = args.find((a) => a.startsWith("-") && a !== "-" &&
46
+ a !== "-e" && a !== "--entities");
47
+ if (badOpt) {
48
+ process.stderr.write("md2org: unknown option " + badOpt + "\n\n" + USAGE);
49
+ process.exit(2);
50
+ }
51
+
52
+ const fileArgs = args.filter((a) => !a.startsWith("-"));
53
+ if (fileArgs.length > 1) {
54
+ process.stderr.write("md2org: expected at most one file, got " +
55
+ fileArgs.length + "\n\n" + USAGE);
56
+ process.exit(2);
57
+ }
58
+
59
+ /*
60
+ * Terminates the output with a newline, as a POSIX text file requires. The core
61
+ * omits it because the browser and Shortcut place the result in a text box or on
62
+ * the clipboard.
63
+ */
64
+ function convert(input) {
65
+ const out = entities ? md2org.withEntities(input) : md2org(input);
66
+ process.stdout.write(out === "" ? out : out.replace(/\n?$/, "\n"));
67
+ }
68
+
69
+ const fileArg = fileArgs[0];
70
+
71
+ if (fileArg) {
72
+ let input;
73
+ try {
74
+ input = fs.readFileSync(fileArg, "utf8");
75
+ } catch (e) {
76
+ process.stderr.write("md2org: cannot read " + fileArg + ": " + e.message + "\n");
77
+ process.exit(1);
78
+ }
79
+ convert(input);
80
+ } else {
81
+ // Reading stdin. A terminal means no pipe was given, so report it rather than
82
+ // block.
83
+ if (process.stdin.isTTY) {
84
+ process.stderr.write("md2org: no input — give a file or pipe stdin\n\n" + USAGE);
85
+ process.exit(2);
86
+ }
87
+ const chunks = [];
88
+ process.stdin.on("data", (c) => chunks.push(c));
89
+ process.stdin.on("end", () => convert(Buffer.concat(chunks).toString("utf8")));
90
+ process.stdin.resume();
91
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "md2org",
3
+ "version": "1.1.0",
4
+ "description": "One-way Markdown to Org mode converter. CLI, browser and iOS Shortcut.",
5
+ "bin": {
6
+ "md2org": "bin/md2org"
7
+ },
8
+ "main": "src/md2org.js",
9
+ "scripts": {
10
+ "test": "node test/spec.js && node test/cli.js && node test/conformance.js && node test/gfm.js && node test/document.js && node test/invariant.js && node test/warnings.js && node test/fuzz.js 5000 && node test/size.js && node test/emacs.js",
11
+ "build": "node build.js",
12
+ "vendor": "node tools/build-vendor.js",
13
+ "fuzz": "node test/fuzz.js 100000",
14
+ "prepublishOnly": "npm test",
15
+ "test:emacs": "node test/emacs.js",
16
+ "compare": "node tools/compare.js"
17
+ },
18
+ "files": [
19
+ "src",
20
+ "bin",
21
+ "README.md",
22
+ "CHANGELOG.md",
23
+ "LICENSE"
24
+ ],
25
+ "license": "MIT",
26
+ "keywords": [
27
+ "markdown",
28
+ "org",
29
+ "org-mode",
30
+ "emacs",
31
+ "converter",
32
+ "commonmark",
33
+ "gfm",
34
+ "cli",
35
+ "ios",
36
+ "shortcuts"
37
+ ],
38
+ "devDependencies": {
39
+ "esbuild": "^0.28.2"
40
+ },
41
+ "homepage": "https://nicholascarroll.github.io/md2org.js/",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/nicholascarroll/md2org.js.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/nicholascarroll/md2org.js/issues"
48
+ },
49
+ "author": "Nicholas Carroll",
50
+ "engines": {
51
+ "node": ">=18"
52
+ }
53
+ }
package/src/md2org.js ADDED
@@ -0,0 +1,82 @@
1
+ /*
2
+ * md2org: one-way Markdown to Org mode converter.
3
+ *
4
+ * A pure function, string in and string out, with no I/O or platform APIs. The
5
+ * CLI, the web page and the iOS Shortcut run this same code, copied into place by
6
+ * build.js.
7
+ *
8
+ * Structure follows CommonMark's two-phase parsing strategy (spec Appendix):
9
+ *
10
+ * src/vendor/commonmark.js parse (forked commonmark.js, BSD-2-Clause)
11
+ * src/org-render.js AST to Org
12
+ * src/org-escape.js escaping
13
+ */
14
+
15
+
16
+ /*
17
+ * Node/CommonJS bootstrap. Outside the core markers, so build.js drops it from
18
+ * the generated bundles, which concatenate the modules into one scope.
19
+ */
20
+ if (typeof __cmark === "undefined") { var __cmark = require("./vendor/commonmark.js"); }
21
+ if (typeof renderOrg === "undefined") { var renderOrg = require("./org-render.js"); }
22
+ if (typeof codeSpan === "undefined") {
23
+ var __esc = require("./org-escape.js");
24
+ var codeSpan = __esc.codeSpan,
25
+ escapeLinkPath = __esc.escapeLinkPath,
26
+ protectBlockBody = __esc.protectBlockBody,
27
+ escapeCell = __esc.escapeCell;
28
+ }
29
+
30
+ /* --8<-- core start */
31
+
32
+ function md2org(src) {
33
+ if (typeof src !== "string") src = String(src == null ? "" : src);
34
+ if (src === "") return "";
35
+ renderOrg.warn = {};
36
+
37
+ var parser = new __cmark.Parser({ sourcepos: true });
38
+
39
+ var escapes = {
40
+ codeSpan: codeSpan,
41
+ escapeLinkPath: escapeLinkPath,
42
+ protectBlockBody: protectBlockBody,
43
+ escapeCell: escapeCell
44
+ };
45
+
46
+ var out = renderOrg(parser.parse(src), escapes);
47
+ var k = Object.keys(renderOrg.warn);
48
+ return k.length ? out + "\n\n# md2org warnings:\n" +
49
+ k.map(function (x) { return "# " + x + ": line " + renderOrg.warn[x].join(", "); }).join("\n") : out;
50
+ }
51
+
52
+ /* --8<-- core end */
53
+
54
+ /*
55
+ * Character-reference decoding, off by default (DESIGN.md, Character references
56
+ * and entities). Used by the CLI's -e option.
57
+ *
58
+ * Outside the core markers, so it costs the Shortcut nothing. It swaps in the
59
+ * parser built with upstream's entity table, which is loaded on first use. This
60
+ * works because the core reads __cmark at call time.
61
+ */
62
+ var __cmarkEntities = null;
63
+
64
+ md2org.withEntities = function (src) {
65
+ if (!__cmarkEntities) __cmarkEntities = require("./vendor/commonmark-entities.js");
66
+ var saved = __cmark;
67
+ __cmark = __cmarkEntities;
68
+ try {
69
+ return md2org(src);
70
+ } finally {
71
+ __cmark = saved;
72
+ }
73
+ };
74
+
75
+ // Universal export: CommonJS (Node/CLI), bundlers, browser global.
76
+ if (typeof module !== "undefined" && module.exports) {
77
+ module.exports = md2org;
78
+ module.exports.md2org = md2org;
79
+ }
80
+ if (typeof window !== "undefined") {
81
+ window.md2org = md2org;
82
+ }
@@ -0,0 +1,70 @@
1
+ /*
2
+ * All of md2org's escaping (DESIGN.md, Escaping).
3
+ *
4
+ * Literal text is not escaped. Each function below either applies Org's own
5
+ * quoting mechanism or protects markup that md2org generates.
6
+ */
7
+
8
+ /* --8<-- core start */
9
+
10
+ /*
11
+ * Block bodies (§Lesser Elements). A line beginning with "*" or "#+" inside a
12
+ * block is comma-quoted, or Org reads it as a headline or a block delimiter. Org
13
+ * removes the comma on read.
14
+ *
15
+ * Mirrors org-escape-code-in-string: quote only when the line, after any existing
16
+ * commas, begins with "*" or "#+". Org strips a comma only from such lines, so
17
+ * quoting ",foo" would not be reversed.
18
+ */
19
+ function protectBlockBody(text) {
20
+ return text.split("\n").map(function (line) {
21
+ return line.replace(/^([ \t]*)(,*)(\*|#\+)/, "$1$2,$3");
22
+ }).join("\n");
23
+ }
24
+
25
+ /*
26
+ * Code spans (§Text Markup). CONTENTS may not contain the MARKER, so a span
27
+ * holding "=" uses "~" and vice versa. A span holding both is returned as plain
28
+ * text, since entities are not expanded inside verbatim or code.
29
+ */
30
+ function codeSpan(text) {
31
+ if (text.indexOf("=") === -1) return "=" + text + "=";
32
+ if (text.indexOf("~") === -1) return "~" + text + "~";
33
+ return text;
34
+ }
35
+
36
+ /*
37
+ * Table cells (§Table). A bare "|" ends a cell and Org has no escape for it.
38
+ * The parser consumes the backslash of GFM's "\|"; this restores it, so the
39
+ * output shows what the author typed. The renderer warns.
40
+ */
41
+ function escapeCell(text) {
42
+ return String(text).replace(/\|/g, "\\|");
43
+ }
44
+
45
+ /*
46
+ * Link paths (§Regular Link). "[", "]" and "\" in PATHREG are backslash-escaped,
47
+ * the only escape Org provides there.
48
+ *
49
+ * A bare path such as "url" or "a/b.md" would be a FUZZY link (a headline
50
+ * search), and "(foo)" a CODEREF, so any path without an explicit type gets
51
+ * "file:", Org's type for a relative path. Unchanged: a path with its own
52
+ * LINKTYPE ("http:", "mailto:", "id:"), a FILENAME starting with "/", "~", "./"
53
+ * or "../", and a "#" CUSTOM-ID.
54
+ */
55
+ function linkType(path) {
56
+ return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(path) // LINKTYPE:...
57
+ || /^[/~#]/.test(path) // FILENAME or #CUSTOM-ID
58
+ || /^\.\.?\//.test(path); // ./ or ../ FILENAME
59
+ }
60
+
61
+ function escapeLinkPath(path) {
62
+ var p = String(path);
63
+ if (p && !linkType(p)) p = "file:" + p;
64
+ return p.replace(/([\[\]\\])/g, "\\$1");
65
+ }
66
+
67
+ /* --8<-- core end */
68
+
69
+ module.exports = { protectBlockBody: protectBlockBody, codeSpan: codeSpan,
70
+ escapeLinkPath: escapeLinkPath, escapeCell: escapeCell };