diff --git a/package-lock.json b/package-lock.json index c9cbc54..3dc1ee3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,11 @@ "gettext-parser": "^8.0.0", "glob": "^13.0.6", "inquirer": "^12.9.6", + "mdast-util-from-markdown": "^2.0.3", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-gfm": "^3.1.0", + "micromark-extension-frontmatter": "^2.0.0", + "micromark-extension-gfm": "^3.0.0", "nanoid": "^5.1.16", "yaml": "^2.8.3" }, @@ -3125,6 +3130,15 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3184,6 +3198,21 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.18.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", @@ -3211,6 +3240,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -3976,6 +4011,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -3998,6 +4043,16 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chardet": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", @@ -4204,7 +4259,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4218,6 +4272,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dedent": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", @@ -4250,6 +4317,15 @@ "node": ">=0.10.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4260,6 +4336,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -4815,6 +4904,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4890,6 +4992,14 @@ "dev": true, "license": "ISC" }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6726,6 +6836,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -6782,6 +6902,247 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6799,6 +7160,585 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6862,7 +7802,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mute-stream": { @@ -8166,6 +9105,61 @@ "node": ">=4" } }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -8396,6 +9390,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 968545c..f624010 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,11 @@ "gettext-parser": "^8.0.0", "glob": "^13.0.6", "inquirer": "^12.9.6", + "mdast-util-from-markdown": "^2.0.3", + "mdast-util-frontmatter": "^2.0.1", + "mdast-util-gfm": "^3.1.0", + "micromark-extension-frontmatter": "^2.0.0", + "micromark-extension-gfm": "^3.0.0", "nanoid": "^5.1.16", "yaml": "^2.8.3" }, diff --git a/src/document-formats/markdown.ts b/src/document-formats/markdown.ts new file mode 100644 index 0000000..30a38fc --- /dev/null +++ b/src/document-formats/markdown.ts @@ -0,0 +1,735 @@ +import { createHash } from 'node:crypto'; +import { fromMarkdown } from 'mdast-util-from-markdown'; +import { frontmatter } from 'micromark-extension-frontmatter'; +import { frontmatterFromMarkdown } from 'mdast-util-frontmatter'; +import { gfm } from 'micromark-extension-gfm'; +import { gfmFromMarkdown } from 'mdast-util-gfm'; +import yaml from 'yaml'; +import { + DocumentExtraction, + DocumentFormatAdapter, + DocumentFormatError, + DocumentManifest, + DocumentTranslation, + DocumentUnit, + DocumentUnitContext, + DocumentUnitRole +} from './types.js'; + +const FORMAT = 'markdown'; +const FORMAT_VERSION = 1; +const RESERVED_PLACEHOLDER_PREFIX = '__LOCALHERO_MD_'; +const RESERVED_PLACEHOLDER_PATTERN = /__LOCALHERO_MD_U\d{4}_P\d{4}_[0-9A-F]{8}__/g; + +export interface MarkdownExtractOptions { + translateFrontmatterFields?: readonly string[]; +} + +interface Point { + offset?: number; +} + +interface Position { + start: Point; + end: Point; +} + +interface SourceRange { + start: number; + end: number; +} + +interface ProtectedFragment { + token: string; + value: string; +} + +interface MarkdownPatchPlan { + unitId: string; + range: SourceRange; + original: string; + protectedFragments: ProtectedFragment[]; + serialization: 'markdown' | 'yaml-scalar'; + yamlScalarType?: string | null; +} + +interface MarkdownApplyPlan { + sourceSha256: string; + patches: MarkdownPatchPlan[]; +} + +export type MarkdownExtraction = DocumentExtraction; + +interface MarkdownNode { + type: string; + position?: Position; + children?: MarkdownNode[]; + value?: string; + depth?: number; + url?: string; + title?: string | null; + identifier?: string; + label?: string; + lang?: string | null; + meta?: string | null; + ordered?: boolean | null; + start?: number | null; + checked?: boolean | null; + align?: Array; +} + +interface ParsedMarkdown { + root: MarkdownNode; + offsetBase: number; +} + +interface ProtectedRange extends SourceRange { + kind: string; +} + +interface DraftUnit { + role: DocumentUnitRole; + range: SourceRange; + source: string; + context: DocumentUnitContext; + placeholders: Array<{ token: string; kind: string }>; + protectedFragments: ProtectedFragment[]; + serialization: MarkdownPatchPlan['serialization']; + yamlScalarType?: string | null; +} + +function sha256(value: string): string { + return createHash('sha256').update(value, 'utf8').digest('hex'); +} + +function parseMarkdown(source: string): ParsedMarkdown { + const offsetBase = source.charCodeAt(0) === 0xfeff ? 1 : 0; + const root = fromMarkdown(source.slice(offsetBase), { + extensions: [gfm(), frontmatter(['yaml'])], + mdastExtensions: [gfmFromMarkdown(), frontmatterFromMarkdown(['yaml'])] + }) as MarkdownNode; + + return { root, offsetBase }; +} + +function nodeRange(node: MarkdownNode, offsetBase: number): SourceRange | null { + const start = node.position?.start.offset; + const end = node.position?.end.offset; + if (start === undefined || end === undefined) return null; + return { start: start + offsetBase, end: end + offsetBase }; +} + +function childContentRange(node: MarkdownNode, offsetBase: number): SourceRange | null { + const positionedChildren = (node.children ?? []) + .map(child => nodeRange(child, offsetBase)) + .filter((range): range is SourceRange => range !== null); + if (positionedChildren.length === 0) return null; + return { + start: positionedChildren[0].start, + end: positionedChildren[positionedChildren.length - 1].end + }; +} + +function walk(node: MarkdownNode, visit: (child: MarkdownNode) => void): void { + visit(node); + for (const child of node.children ?? []) { + walk(child, visit); + } +} + +function plainText(node: MarkdownNode): string { + const values: string[] = []; + walk(node, child => { + if ((child.type === 'text' || child.type === 'inlineCode') && child.value) { + values.push(child.value); + } + }); + return values.join('').trim(); +} + +function collectSemanticNodes(root: MarkdownNode, offsetBase: number): MarkdownNode[] { + const nodes: MarkdownNode[] = []; + + const collect = (node: MarkdownNode): void => { + if (node.type === 'heading' || node.type === 'paragraph' || node.type === 'tableCell') { + if (childContentRange(node, offsetBase)) nodes.push(node); + return; + } + + if (node.type === 'code' || node.type === 'html' || node.type === 'yaml') return; + for (const child of node.children ?? []) collect(child); + }; + + collect(root); + return nodes.sort((left, right) => { + const leftStart = nodeRange(left, offsetBase)?.start ?? 0; + const rightStart = nodeRange(right, offsetBase)?.start ?? 0; + return leftStart - rightStart; + }); +} + +function roleForNode(node: MarkdownNode): DocumentUnitRole { + if (node.type === 'heading') return 'heading'; + if (node.type === 'tableCell') return 'table-cell'; + return 'paragraph'; +} + +function addWholeNodeRange( + ranges: ProtectedRange[], + node: MarkdownNode, + offsetBase: number, + unitRange: SourceRange, + kind: string +): void { + const range = nodeRange(node, offsetBase); + if (!range || range.start < unitRange.start || range.end > unitRange.end) return; + ranges.push({ ...range, kind }); +} + +function addWrapperRanges( + ranges: ProtectedRange[], + node: MarkdownNode, + offsetBase: number, + unitRange: SourceRange, + kind: string +): void { + const range = nodeRange(node, offsetBase); + const contents = childContentRange(node, offsetBase); + if (!range || !contents || range.start < unitRange.start || range.end > unitRange.end) return; + if (range.start < contents.start) { + ranges.push({ start: range.start, end: contents.start, kind: `${kind}-open` }); + } + if (contents.end < range.end) { + ranges.push({ start: contents.end, end: range.end, kind: `${kind}-close` }); + } +} + +function isAutolink(node: MarkdownNode, source: string, offsetBase: number): boolean { + if (node.type !== 'link' || !node.url) return false; + const range = nodeRange(node, offsetBase); + if (!range) return false; + const raw = source.slice(range.start, range.end); + const anchor = plainText(node); + return anchor === node.url && (raw === node.url || raw === `<${node.url}>`); +} + +function normalizeProtectedRanges(ranges: ProtectedRange[]): ProtectedRange[] { + const sorted = [...ranges].sort((left, right) => + left.start - right.start || right.end - left.end + ); + const normalized: ProtectedRange[] = []; + + for (const range of sorted) { + if (range.start === range.end) continue; + const previous = normalized[normalized.length - 1]; + if (!previous || range.start >= previous.end) { + normalized.push(range); + continue; + } + if (range.end <= previous.end) continue; + throw new DocumentFormatError( + 'overlapping_protected_ranges', + 'Markdown parser produced partially overlapping protected ranges.' + ); + } + + return normalized; +} + +function protectedRangesForNode( + node: MarkdownNode, + source: string, + offsetBase: number, + unitRange: SourceRange +): ProtectedRange[] { + const ranges: ProtectedRange[] = []; + const atomicTypes = new Set([ + 'inlineCode', + 'html', + 'image', + 'imageReference', + 'footnoteReference', + 'break' + ]); + const wrapperTypes = new Set(['emphasis', 'strong', 'delete', 'linkReference']); + + walk(node, child => { + if (atomicTypes.has(child.type)) { + addWholeNodeRange(ranges, child, offsetBase, unitRange, child.type); + return; + } + if (child.type === 'link') { + if (isAutolink(child, source, offsetBase)) { + addWholeNodeRange(ranges, child, offsetBase, unitRange, 'autolink'); + } else { + addWrapperRanges(ranges, child, offsetBase, unitRange, 'link'); + } + return; + } + if (wrapperTypes.has(child.type)) { + addWrapperRanges(ranges, child, offsetBase, unitRange, child.type); + } + }); + + const original = source.slice(unitRange.start, unitRange.end); + const newlinePattern = /(?:\r\n|\r|\n)[ \t]*(?:(?:>[ \t]?)+)?/g; + for (const match of original.matchAll(newlinePattern)) { + const relativeStart = match.index; + ranges.push({ + start: unitRange.start + relativeStart, + end: unitRange.start + relativeStart + match[0].length, + kind: 'line-break' + }); + } + + return normalizeProtectedRanges(ranges); +} + +function makePlaceholder(unitIndex: number, fragmentIndex: number, value: string): string { + const digest = sha256(value).slice(0, 8).toUpperCase(); + return `${RESERVED_PLACEHOLDER_PREFIX}U${String(unitIndex + 1).padStart(4, '0')}_P${String(fragmentIndex + 1).padStart(4, '0')}_${digest}__`; +} + +function templateWithPlaceholders( + source: string, + unitRange: SourceRange, + protectedRanges: ProtectedRange[], + unitIndex: number +): { + template: string; + placeholders: Array<{ token: string; kind: string }>; + fragments: ProtectedFragment[]; +} { + let template = source.slice(unitRange.start, unitRange.end); + const placeholders: Array<{ token: string; kind: string }> = []; + const fragments: ProtectedFragment[] = []; + + protectedRanges.forEach((range, fragmentIndex) => { + const value = source.slice(range.start, range.end); + const token = makePlaceholder(unitIndex, fragmentIndex, value); + placeholders.push({ token, kind: range.kind }); + fragments.push({ token, value }); + }); + + for (let index = protectedRanges.length - 1; index >= 0; index--) { + const range = protectedRanges[index]; + const relativeStart = range.start - unitRange.start; + const relativeEnd = range.end - unitRange.start; + template = template.slice(0, relativeStart) + placeholders[index].token + template.slice(relativeEnd); + } + + return { template, placeholders, fragments }; +} + +function hasTranslatableText(template: string): boolean { + const withoutPlaceholders = template.replace(RESERVED_PLACEHOLDER_PATTERN, ''); + return /[\p{L}\p{N}]/u.test(withoutPlaceholders); +} + +function yamlInnerRange(source: string, range: SourceRange): SourceRange { + const raw = source.slice(range.start, range.end); + const opening = /^---[ \t]*(?:\r\n|\n|\r)/.exec(raw); + const closing = /(?:\r\n|\n|\r)---[ \t]*$/.exec(raw); + if (!opening || !closing || closing.index < opening[0].length) { + throw new DocumentFormatError('invalid_frontmatter', 'Could not locate YAML frontmatter fences.'); + } + return { + start: range.start + opening[0].length, + end: range.start + closing.index + }; +} + +function rangeFromYamlNode(node: unknown): [number, number, number] | null { + if (!node || typeof node !== 'object' || !('range' in node)) return null; + const range = (node as { range?: [number, number, number] }).range; + return Array.isArray(range) && range.length === 3 ? range : null; +} + +function frontmatterDrafts( + source: string, + parsed: ParsedMarkdown, + fields: readonly string[] +): DraftUnit[] { + if (fields.length === 0) return []; + const yamlNode = parsed.root.children?.find(child => child.type === 'yaml'); + if (!yamlNode) return []; + const frontmatterRange = nodeRange(yamlNode, parsed.offsetBase); + if (!frontmatterRange) return []; + + const innerRange = yamlInnerRange(source, frontmatterRange); + const inner = source.slice(innerRange.start, innerRange.end); + const document = yaml.parseDocument(inner, { keepSourceTokens: true, strict: true }); + if (document.errors.length > 0) { + throw new DocumentFormatError( + 'invalid_frontmatter', + `YAML frontmatter could not be parsed: ${document.errors[0].message}` + ); + } + if (!document.contents || !yaml.isMap(document.contents)) { + throw new DocumentFormatError('invalid_frontmatter', 'YAML frontmatter must be a mapping.'); + } + + const drafts: DraftUnit[] = []; + for (const field of new Set(fields)) { + const valueNode = document.get(field, true); + if (valueNode === undefined) continue; + if (!yaml.isScalar(valueNode) || typeof valueNode.value !== 'string') { + throw new DocumentFormatError( + 'unsupported_frontmatter_value', + `Configured frontmatter field "${field}" must be a string scalar.` + ); + } + if (valueNode.tag) { + throw new DocumentFormatError( + 'unsupported_frontmatter_value', + `Configured frontmatter field "${field}" may not use a YAML tag.` + ); + } + if (valueNode.type === 'BLOCK_FOLDED' || valueNode.type === 'BLOCK_LITERAL') { + throw new DocumentFormatError( + 'unsupported_frontmatter_style', + `Configured frontmatter field "${field}" uses a block scalar, which is not supported yet.` + ); + } + if (valueNode.value.includes('\n') || valueNode.value.includes('\r')) { + throw new DocumentFormatError( + 'unsupported_frontmatter_style', + `Configured frontmatter field "${field}" contains a multiline value, which is not supported yet.` + ); + } + const scalarRange = rangeFromYamlNode(valueNode); + if (!scalarRange) { + throw new DocumentFormatError( + 'invalid_frontmatter', + `Configured frontmatter field "${field}" has no source range.` + ); + } + const absoluteRange = { + start: innerRange.start + scalarRange[0], + end: innerRange.start + scalarRange[1] + }; + drafts.push({ + role: 'frontmatter', + range: absoluteRange, + source: valueNode.value, + context: { headingPath: [], frontmatterField: field }, + placeholders: [], + protectedFragments: [], + serialization: 'yaml-scalar', + yamlScalarType: valueNode.type + }); + } + + return drafts; +} + +function markdownDrafts(source: string, parsed: ParsedMarkdown, unitOffset: number): DraftUnit[] { + const drafts: DraftUnit[] = []; + const headingStack: Array<{ depth: number; title: string }> = []; + + for (const node of collectSemanticNodes(parsed.root, parsed.offsetBase)) { + const range = childContentRange(node, parsed.offsetBase); + if (!range) continue; + const role = roleForNode(node); + const ranges = protectedRangesForNode(node, source, parsed.offsetBase, range); + const templated = templateWithPlaceholders(source, range, ranges, unitOffset + drafts.length); + + if (hasTranslatableText(templated.template)) { + drafts.push({ + role, + range, + source: templated.template, + context: { headingPath: headingStack.map(heading => heading.title) }, + placeholders: templated.placeholders, + protectedFragments: templated.fragments, + serialization: 'markdown' + }); + } + + if (node.type === 'heading' && node.depth) { + const title = plainText(node); + const depth = node.depth; + while (headingStack.length > 0 && headingStack.at(-1)!.depth >= depth) headingStack.pop(); + headingStack.push({ depth, title }); + } + } + + return drafts; +} + +function serializeYamlScalar(value: string, existingType: string | null | undefined): string { + if (value.includes('\n') || value.includes('\r')) { + throw new DocumentFormatError( + 'unsupported_frontmatter_translation', + 'Multiline frontmatter translations are not supported yet.' + ); + } + const document = new yaml.Document(); + document.contents = document.createNode({}) as yaml.YAMLMap; + const root = document.contents as yaml.YAMLMap; + const key = '__localhero_scalar__'; + + if (!value.includes('\n') && (existingType === 'QUOTE_SINGLE' || existingType === 'QUOTE_DOUBLE')) { + const scalar = new yaml.Scalar(value); + scalar.type = existingType; + root.set(key, scalar); + } else { + root.set(key, document.createNode(value)); + } + + const emitted = document.toString({ indent: 2, lineWidth: 0 }); + const prefix = `${key}: `; + const prefixIndex = emitted.indexOf(prefix); + if (prefixIndex < 0) { + throw new DocumentFormatError('yaml_serialization_failed', 'Could not serialize a frontmatter value.'); + } + return emitted.slice(prefixIndex + prefix.length).replace(/\n$/, ''); +} + +function countOccurrences(value: string, needle: string): number { + let count = 0; + let offset = 0; + while ((offset = value.indexOf(needle, offset)) !== -1) { + count++; + offset += needle.length; + } + return count; +} + +function restoreMarkdownTranslation( + translation: string, + patch: MarkdownPatchPlan, + unitId: string +): string { + const expectedTokens = new Set(patch.protectedFragments.map(fragment => fragment.token)); + const seenTokens = translation.match(RESERVED_PLACEHOLDER_PATTERN) ?? []; + for (const token of seenTokens) { + if (!expectedTokens.has(token)) { + throw new DocumentFormatError( + 'unknown_placeholder', + `Translation for ${unitId} contains an unknown Localhero placeholder.`, + unitId + ); + } + } + for (const fragment of patch.protectedFragments) { + if (countOccurrences(translation, fragment.token) !== 1) { + throw new DocumentFormatError( + 'invalid_placeholder_count', + `Translation for ${unitId} must contain placeholder ${fragment.token} exactly once.`, + unitId + ); + } + } + if (/[\r\n]/.test(translation)) { + throw new DocumentFormatError( + 'unexpected_line_break', + `Translation for ${unitId} introduced an unprotected line break.`, + unitId + ); + } + + let restored = translation; + for (const fragment of patch.protectedFragments) { + restored = restored.replace(fragment.token, fragment.value); + } + return restored; +} + +function structuralSignature(source: string): { shape: string[]; protectedValues: string[] } { + const { root } = parseMarkdown(source); + const shape: string[] = []; + const protectedValues: string[] = []; + walk(root, node => { + if (node.type === 'root' || node.type === 'text' || node.type === 'yaml') return; + const properties: Record = { type: node.type }; + if (node.depth !== undefined) properties.depth = node.depth; + if (node.lang !== undefined) properties.lang = node.lang; + if (node.meta !== undefined) properties.meta = node.meta; + if (node.ordered !== undefined) properties.ordered = node.ordered; + if (node.start !== undefined) properties.start = node.start; + if (node.checked !== undefined) properties.checked = node.checked; + if (node.align !== undefined) properties.align = node.align; + shape.push(JSON.stringify(properties)); + + if ( + node.value !== undefined || + node.url !== undefined || + node.title !== undefined || + node.identifier !== undefined || + node.label !== undefined + ) { + protectedValues.push(JSON.stringify({ + type: node.type, + value: node.type === 'code' || node.type === 'inlineCode' || node.type === 'html' + ? node.value + : undefined, + url: node.url, + title: node.title, + identifier: node.identifier, + label: node.label + })); + } + }); + return { shape, protectedValues: protectedValues.sort() }; +} + +function assertSameStructure(source: string, output: string): void { + const before = structuralSignature(source); + const after = structuralSignature(output); + if (JSON.stringify(before) !== JSON.stringify(after)) { + throw new DocumentFormatError( + 'markdown_structure_changed', + 'Translated Markdown changed protected document structure.' + ); + } +} + +function assertNonOverlappingPatches(patches: readonly MarkdownPatchPlan[]): void { + const sorted = [...patches].sort((left, right) => left.range.start - right.range.start); + for (let index = 1; index < sorted.length; index++) { + if (sorted[index].range.start < sorted[index - 1].range.end) { + throw new DocumentFormatError('overlapping_patches', 'Document apply plan contains overlapping patches.'); + } + } +} + +export class MarkdownDocumentAdapter implements DocumentFormatAdapter { + readonly format = FORMAT; + readonly version = FORMAT_VERSION; + + extract(source: string, options: MarkdownExtractOptions = {}): MarkdownExtraction { + if (source.includes(RESERVED_PLACEHOLDER_PREFIX)) { + throw new DocumentFormatError( + 'reserved_placeholder_collision', + `Source contains the reserved placeholder prefix ${RESERVED_PLACEHOLDER_PREFIX}.` + ); + } + + const parsed = parseMarkdown(source); + const frontmatter = frontmatterDrafts( + source, + parsed, + options.translateFrontmatterFields ?? [] + ); + const markdown = markdownDrafts(source, parsed, frontmatter.length); + const drafts = [...frontmatter, ...markdown].sort((left, right) => left.range.start - right.range.start); + const sourceSha256 = sha256(source); + + const units: DocumentUnit[] = []; + const patches: MarkdownPatchPlan[] = []; + drafts.forEach((draft, index) => { + const id = `u${String(index + 1).padStart(4, '0')}`; + units.push({ + id, + role: draft.role, + source: draft.source, + sourceHash: sha256(draft.source), + context: draft.context, + placeholders: draft.placeholders + }); + patches.push({ + unitId: id, + range: draft.range, + original: source.slice(draft.range.start, draft.range.end), + protectedFragments: draft.protectedFragments, + serialization: draft.serialization, + yamlScalarType: draft.yamlScalarType + }); + }); + + const manifest: DocumentManifest = { + format: this.format, + formatVersion: this.version, + sourceSha256, + units + }; + return { + manifest, + plan: { sourceSha256, patches } + }; + } + + apply( + source: string, + extraction: MarkdownExtraction, + translations: readonly DocumentTranslation[] + ): string { + const actualSha = sha256(source); + if (actualSha !== extraction.manifest.sourceSha256 || actualSha !== extraction.plan.sourceSha256) { + throw new DocumentFormatError('stale_source', 'Source changed after document units were extracted.'); + } + if (extraction.manifest.format !== this.format || extraction.manifest.formatVersion !== this.version) { + throw new DocumentFormatError('unsupported_contract', 'Extraction uses an unsupported document contract.'); + } + + assertNonOverlappingPatches(extraction.plan.patches); + const unitsById = new Map(extraction.manifest.units.map(unit => [unit.id, unit])); + const patchesById = new Map(extraction.plan.patches.map(patch => [patch.unitId, patch])); + if (unitsById.size !== extraction.manifest.units.length || patchesById.size !== extraction.plan.patches.length) { + throw new DocumentFormatError('invalid_extraction', 'Extraction contains duplicate unit IDs.'); + } + if (unitsById.size !== patchesById.size || [...unitsById.keys()].some(id => !patchesById.has(id))) { + throw new DocumentFormatError('invalid_extraction', 'Manifest units do not match the private apply plan.'); + } + + const translationsById = new Map(); + for (const translation of translations) { + if (translationsById.has(translation.id)) { + throw new DocumentFormatError( + 'duplicate_translation', + `Translation ID ${translation.id} appears more than once.`, + translation.id + ); + } + if (!unitsById.has(translation.id)) { + throw new DocumentFormatError( + 'unknown_translation', + `Translation ID ${translation.id} is not part of this extraction.`, + translation.id + ); + } + translationsById.set(translation.id, translation.text); + } + for (const unit of extraction.manifest.units) { + if (!translationsById.has(unit.id)) { + throw new DocumentFormatError( + 'missing_translation', + `Translation is missing for unit ${unit.id}.`, + unit.id + ); + } + if (sha256(unit.source) !== unit.sourceHash) { + throw new DocumentFormatError('invalid_extraction', `Source hash is invalid for unit ${unit.id}.`, unit.id); + } + } + + const replacements: Array<{ range: SourceRange; value: string }> = []; + for (const unit of extraction.manifest.units) { + const patch = patchesById.get(unit.id)!; + if (source.slice(patch.range.start, patch.range.end) !== patch.original) { + throw new DocumentFormatError('stale_source', `Source range changed for unit ${unit.id}.`, unit.id); + } + const translated = translationsById.get(unit.id)!; + if (translated === unit.source) continue; + + const value = patch.serialization === 'yaml-scalar' + ? serializeYamlScalar(translated, patch.yamlScalarType) + : restoreMarkdownTranslation(translated, patch, unit.id); + replacements.push({ range: patch.range, value }); + } + + if (replacements.length === 0) return source; + let output = source; + replacements + .sort((left, right) => right.range.start - left.range.start) + .forEach(replacement => { + output = output.slice(0, replacement.range.start) + replacement.value + output.slice(replacement.range.end); + }); + + assertSameStructure(source, output); + return output; + } +} + +export const markdownDocumentAdapter = new MarkdownDocumentAdapter(); diff --git a/src/document-formats/types.ts b/src/document-formats/types.ts new file mode 100644 index 0000000..0396402 --- /dev/null +++ b/src/document-formats/types.ts @@ -0,0 +1,63 @@ +export type DocumentUnitRole = 'heading' | 'paragraph' | 'table-cell' | 'frontmatter'; + +export interface DocumentPlaceholder { + token: string; + kind: string; +} + +export interface DocumentUnitContext { + headingPath: string[]; + frontmatterField?: string; +} + +/** + * The format-neutral payload that may be sent to the Localhero API. + * Parser offsets and original protected fragments intentionally do not belong here. + */ +export interface DocumentUnit { + id: string; + role: DocumentUnitRole; + source: string; + sourceHash: string; + context: DocumentUnitContext; + placeholders: DocumentPlaceholder[]; +} + +export interface DocumentManifest { + format: string; + formatVersion: number; + sourceSha256: string; + units: DocumentUnit[]; +} + +export interface DocumentTranslation { + id: string; + text: string; +} + +export interface DocumentExtraction { + manifest: DocumentManifest; + plan: ApplyPlan; +} + +export interface DocumentFormatAdapter { + readonly format: string; + readonly version: number; + extract(source: string, options: Options): DocumentExtraction; + apply( + source: string, + extraction: DocumentExtraction, + translations: readonly DocumentTranslation[] + ): string; +} + +export class DocumentFormatError extends Error { + constructor( + public readonly code: string, + message: string, + public readonly unitId?: string + ) { + super(message); + this.name = 'DocumentFormatError'; + } +} diff --git a/tests/document-formats/markdown.test.ts b/tests/document-formats/markdown.test.ts new file mode 100644 index 0000000..8508bb1 --- /dev/null +++ b/tests/document-formats/markdown.test.ts @@ -0,0 +1,364 @@ +import { describe, expect, it } from '@jest/globals'; +import yaml from 'yaml'; +import { + MarkdownDocumentAdapter, + MarkdownExtractOptions +} from '../../src/document-formats/markdown.js'; +import { + DocumentFormatError, + DocumentTranslation +} from '../../src/document-formats/types.js'; +import type { MarkdownExtraction } from '../../src/document-formats/markdown.js'; + +const adapter = new MarkdownDocumentAdapter(); + +function extract(source: string, options: MarkdownExtractOptions = {}): MarkdownExtraction { + return adapter.extract(source, options); +} + +function identityTranslations(extraction: MarkdownExtraction): DocumentTranslation[] { + return extraction.manifest.units.map(unit => ({ id: unit.id, text: unit.source })); +} + +function translationsWith( + extraction: MarkdownExtraction, + replacements: Record +): DocumentTranslation[] { + return extraction.manifest.units.map(unit => ({ + id: unit.id, + text: replacements[unit.id] ?? unit.source + })); +} + +function errorCode(callback: () => unknown): string | undefined { + try { + callback(); + return undefined; + } catch (error) { + return error instanceof DocumentFormatError ? error.code : undefined; + } +} + +describe('MarkdownDocumentAdapter', () => { + it('extracts ordered semantic units and round-trips byte-identically', () => { + const source = [ + '---', + 'title: "A guide: #1"', + 'description: A useful guide', + 'author: "Arvid"', + '---', + '# Getting started', + '', + 'Read the **short guide** and visit [the docs](https://example.com/docs?q=1 "Docs").', + '', + '- First item', + '- Second item', + '', + '| Feature | Status |', + '| --- | --- |', + '| Markdown | Ready |', + '' + ].join('\n'); + + const extraction = extract(source, { + translateFrontmatterFields: ['title', 'description'] + }); + + expect(extraction.manifest).toMatchObject({ + format: 'markdown', + formatVersion: 1 + }); + expect(extraction.manifest.units.map(unit => unit.role)).toEqual([ + 'frontmatter', + 'frontmatter', + 'heading', + 'paragraph', + 'paragraph', + 'paragraph', + 'table-cell', + 'table-cell', + 'table-cell', + 'table-cell' + ]); + expect(extraction.manifest.units[2].context.headingPath).toEqual([]); + expect(extraction.manifest.units[3].context.headingPath).toEqual(['Getting started']); + expect(JSON.stringify(extraction.manifest)).not.toContain('https://example.com'); + expect(extraction.manifest.units.every(unit => !('range' in unit) && !('original' in unit))).toBe(true); + expect(adapter.apply(source, extraction, identityTranslations(extraction))).toBe(source); + }); + + it('changes selected prose while preserving Markdown syntax and opaque regions', () => { + const source = [ + '# Guide with `npm test`', + '', + 'Use **carefully** with [the docs](https://example.com/a_(b) "A title"), ![logo](./logo.png), and Enter.', + '', + '> Two lines of', + '> quoted prose.', + '', + '````md', + '```js', + 'const untranslated = "Hello";', + '```', + '````', + '', + '~~~txt', + 'also untranslated', + '~~~', + '' + ].join('\n'); + const extraction = extract(source); + const heading = extraction.manifest.units.find(unit => unit.role === 'heading')!; + const prose = extraction.manifest.units.find(unit => + unit.role === 'paragraph' && unit.source.includes('carefully') + )!; + const quote = extraction.manifest.units.find(unit => unit.source.includes('quoted prose'))!; + + const output = adapter.apply(source, extraction, translationsWith(extraction, { + [heading.id]: heading.source.replace('Guide with', 'Guide avec'), + [prose.id]: prose.source + .replace('Use', 'Utilisez') + .replace('carefully', 'avec prudence') + .replace('the docs', 'la documentation') + .replace('and', 'et') + .replace('Enter', 'Entrée'), + [quote.id]: quote.source.replace('Two lines of', 'Deux lignes de').replace('quoted prose', 'texte cité') + })); + + expect(output).toContain('# Guide avec `npm test`'); + expect(output).toContain('**avec prudence**'); + expect(output).toContain('[la documentation](https://example.com/a_(b) "A title")'); + expect(output).toContain('![logo](./logo.png)'); + expect(output).toContain('Entrée'); + expect(output).toContain('> Deux lignes de\n> texte cité.'); + expect(output).toContain('````md\n```js\nconst untranslated = "Hello";\n```\n````'); + expect(output).toContain('~~~txt\nalso untranslated\n~~~'); + }); + + it('translates only explicitly configured top-level frontmatter strings', () => { + const source = [ + '---', + 'title: "Hello: #world"', + "description: 'A quoted value'", + 'locale: en', + 'draft: false', + '---', + '# Hello', + '' + ].join('\n'); + const extraction = extract(source, { translateFrontmatterFields: ['title', 'description'] }); + const title = extraction.manifest.units.find(unit => unit.context.frontmatterField === 'title')!; + const description = extraction.manifest.units.find(unit => + unit.context.frontmatterField === 'description' + )!; + + const output = adapter.apply(source, extraction, translationsWith(extraction, { + [title.id]: 'Bonjour : #monde', + [description.id]: "Une valeur avec l'apostrophe" + })); + const parsed = yaml.parse(output.split('---')[1]) as Record; + + expect(output).toContain('title: "Bonjour : #monde"'); + expect(output).toContain("description: 'Une valeur avec l''apostrophe'"); + expect(output).toContain('locale: en\ndraft: false'); + expect(parsed).toMatchObject({ + title: 'Bonjour : #monde', + description: "Une valeur avec l'apostrophe", + locale: 'en', + draft: false + }); + }); + + it('does not infer locale or translate frontmatter without an allowlist', () => { + const source = '---\ntitle: Hello\nlocale: en\n---\n# Hello\n'; + const extraction = extract(source); + + expect(extraction.manifest.units.filter(unit => unit.role === 'frontmatter')).toEqual([]); + expect(extraction.manifest).not.toHaveProperty('locale'); + }); + + it('rejects configured non-string and block-scalar frontmatter values', () => { + expect(errorCode(() => extract('---\ndraft: false\n---\n', { + translateFrontmatterFields: ['draft'] + }))).toBe('unsupported_frontmatter_value'); + + const block = '---\ndescription: |-\n First line\n Second line\n---\n'; + expect(errorCode(() => extract(block, { + translateFrontmatterFields: ['description'] + }))).toBe('unsupported_frontmatter_style'); + expect(() => extract(block)).not.toThrow(); + }); + + it('rejects multiline frontmatter translations instead of emitting ambiguous YAML', () => { + const source = '---\ndescription: "One line"\n---\n'; + const extraction = extract(source, { translateFrontmatterFields: ['description'] }); + const description = extraction.manifest.units[0]; + + expect(errorCode(() => adapter.apply(source, extraction, [{ + id: description.id, + text: 'First line\nSecond line' + }]))).toBe('unsupported_frontmatter_translation'); + }); + + it('rejects stale sources before applying translations', () => { + const source = '# Hello\n'; + const extraction = extract(source); + + expect(errorCode(() => adapter.apply( + '# Hello!\n', + extraction, + identityTranslations(extraction) + ))).toBe('stale_source'); + }); + + it('rejects missing, duplicate, and unknown translation IDs', () => { + const source = '# Hello\n\nWorld.\n'; + const extraction = extract(source); + const translations = identityTranslations(extraction); + + expect(errorCode(() => adapter.apply(source, extraction, translations.slice(1)))) + .toBe('missing_translation'); + expect(errorCode(() => adapter.apply(source, extraction, [...translations, translations[0]]))) + .toBe('duplicate_translation'); + expect(errorCode(() => adapter.apply(source, extraction, [ + ...translations, + { id: 'not-a-unit', text: 'No' } + ]))).toBe('unknown_translation'); + }); + + it('rejects lost, duplicated, changed, and injected placeholders', () => { + const source = 'Use `npm test` and [the docs](https://example.com).\n'; + const extraction = extract(source); + const unit = extraction.manifest.units[0]; + const token = unit.placeholders[0].token; + + for (const text of [ + unit.source.replace(token, ''), + `${unit.source}${token}`, + unit.source.replace(token, token.slice(0, -1)), + `${unit.source} __LOCALHERO_MD_UNKNOWN__` + ]) { + expect(() => adapter.apply(source, extraction, [{ id: unit.id, text }])).toThrow(DocumentFormatError); + } + }); + + it('allows protected inline constructs to be reordered without changing them', () => { + const source = 'Run `first` before `second`.\n'; + const extraction = extract(source); + const unit = extraction.manifest.units[0]; + const [first, second] = unit.placeholders.map(placeholder => placeholder.token); + const translated = unit.source + .replace(first, '__SWAP__') + .replace(second, first) + .replace('__SWAP__', second); + + expect(adapter.apply(source, extraction, [{ id: unit.id, text: translated }])) + .toBe('Run `second` before `first`.\n'); + }); + + it('preserves BOM, CRLF, Unicode offsets, and the final-newline shape', () => { + const source = '\uFEFF# 👋 Hello\r\n\r\nA café paragraph.\r\n'; + const extraction = extract(source); + const paragraph = extraction.manifest.units.find(unit => unit.role === 'paragraph')!; + const output = adapter.apply(source, extraction, translationsWith(extraction, { + [paragraph.id]: 'Un paragraphe café.' + })); + + expect(output).toBe('\uFEFF# 👋 Hello\r\n\r\nUn paragraphe café.\r\n'); + expect(adapter.apply(source, extraction, identityTranslations(extraction))).toBe(source); + }); + + it('preserves ordered-list, task-list, and table delimiters', () => { + const source = [ + '1. First step', + '2. [x] Finished step', + '', + '| Name | State |', + '| :--- | ---: |', + '| Build | Ready |' + ].join('\n'); + const extraction = extract(source); + const replacements = Object.fromEntries(extraction.manifest.units.map(unit => [ + unit.id, + unit.source + .replace('First step', 'Première étape') + .replace('Finished step', 'Étape terminée') + .replace('Name', 'Nom') + .replace('State', 'État') + .replace('Build', 'Version') + .replace('Ready', 'Prêt') + ])); + + expect(adapter.apply(source, extraction, translationsWith(extraction, replacements))).toBe([ + '1. Première étape', + '2. [x] Étape terminée', + '', + '| Nom | État |', + '| :--- | ---: |', + '| Version | Prêt |' + ].join('\n')); + }); + + it('does not add a final newline when the source has none', () => { + const source = '# Hello\n\nNo final newline'; + const extraction = extract(source); + const paragraph = extraction.manifest.units.find(unit => unit.role === 'paragraph')!; + + expect(adapter.apply(source, extraction, translationsWith(extraction, { + [paragraph.id]: 'Toujours sans saut final' + }))).toBe('# Hello\n\nToujours sans saut final'); + }); + + it('builds compact heading context when a document starts below H1', () => { + const source = '## Setup\n\nIntro.\n\n### Install\n\nRun it.\n'; + const extraction = extract(source); + const paragraphs = extraction.manifest.units.filter(unit => unit.role === 'paragraph'); + + expect(paragraphs.map(unit => unit.context.headingPath)).toEqual([ + ['Setup'], + ['Setup', 'Install'] + ]); + }); + + it('tokenizes adjacent placeholders separately when no text sits between them', () => { + const source = 'Status **`check`** and **bold** here.\n'; + const extraction = extract(source); + const unit = extraction.manifest.units[0]; + + expect(unit.placeholders.map(placeholder => placeholder.kind)).toEqual([ + 'strong-open', + 'inlineCode', + 'strong-close', + 'strong-open', + 'strong-close' + ]); + for (const placeholder of unit.placeholders) { + expect(unit.source).toContain(placeholder.token); + } + expect(adapter.apply(source, extraction, identityTranslations(extraction))).toBe(source); + }); + + it('keeps image paths, autolinks, reference destinations, and raw HTML opaque', () => { + const source = [ + 'See , [reference][docs], and ![logo](./logo.svg).', + '', + '
', + 'Raw HTML body', + '
', + '', + '[docs]: https://example.com/reference "Reference"', + '' + ].join('\n'); + const extraction = extract(source); + const prose = extraction.manifest.units[0]; + const output = adapter.apply(source, extraction, translationsWith(extraction, { + [prose.id]: prose.source.replace('See', 'Voir').replace('reference', 'référence') + })); + + expect(output).toContain(''); + expect(output).toContain('[référence][docs]'); + expect(output).toContain('![logo](./logo.svg)'); + expect(output).toContain('
\nRaw HTML body\n
'); + expect(output).toContain('[docs]: https://example.com/reference "Reference"'); + }); +});