@remigius42/morg 0.4.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.
Files changed (67) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +267 -0
  3. package/dist/cli/args.d.ts +15 -0
  4. package/dist/cli/args.js +105 -0
  5. package/dist/cli/configFile.d.ts +2 -0
  6. package/dist/cli/configFile.js +18 -0
  7. package/dist/cli/conversion.d.ts +11 -0
  8. package/dist/cli/conversion.js +50 -0
  9. package/dist/cli/error.d.ts +2 -0
  10. package/dist/cli/error.js +4 -0
  11. package/dist/cli/formats.d.ts +3 -0
  12. package/dist/cli/formats.js +46 -0
  13. package/dist/cli/presets.d.ts +2 -0
  14. package/dist/cli/presets.js +10 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +54 -0
  17. package/dist/config.d.ts +20 -0
  18. package/dist/config.js +22 -0
  19. package/dist/conversionOptions.d.ts +32 -0
  20. package/dist/conversionOptions.js +34 -0
  21. package/dist/core/markdownStyle.d.ts +27 -0
  22. package/dist/core/markdownStyle.js +108 -0
  23. package/dist/core/mdastToUniorg/blocks.d.ts +20 -0
  24. package/dist/core/mdastToUniorg/blocks.js +211 -0
  25. package/dist/core/mdastToUniorg/context.d.ts +15 -0
  26. package/dist/core/mdastToUniorg/context.js +7 -0
  27. package/dist/core/mdastToUniorg/index.d.ts +13 -0
  28. package/dist/core/mdastToUniorg/index.js +95 -0
  29. package/dist/core/mdastToUniorg/lists.d.ts +4 -0
  30. package/dist/core/mdastToUniorg/lists.js +45 -0
  31. package/dist/core/mdastToUniorg/phrasing.d.ts +4 -0
  32. package/dist/core/mdastToUniorg/phrasing.js +176 -0
  33. package/dist/core/uniorgToMdast/elements.d.ts +4 -0
  34. package/dist/core/uniorgToMdast/elements.js +278 -0
  35. package/dist/core/uniorgToMdast/footnotes.d.ts +10 -0
  36. package/dist/core/uniorgToMdast/footnotes.js +54 -0
  37. package/dist/core/uniorgToMdast/index.d.ts +11 -0
  38. package/dist/core/uniorgToMdast/index.js +84 -0
  39. package/dist/core/uniorgToMdast/lists.d.ts +4 -0
  40. package/dist/core/uniorgToMdast/lists.js +94 -0
  41. package/dist/core/uniorgToMdast/objects.d.ts +4 -0
  42. package/dist/core/uniorgToMdast/objects.js +152 -0
  43. package/dist/core/uniorgToMdast/shared.d.ts +25 -0
  44. package/dist/core/uniorgToMdast/shared.js +55 -0
  45. package/dist/core/uniorgToMdast/tables.d.ts +6 -0
  46. package/dist/core/uniorgToMdast/tables.js +66 -0
  47. package/dist/fileNames.d.ts +16 -0
  48. package/dist/fileNames.js +34 -0
  49. package/dist/index.d.ts +13 -0
  50. package/dist/index.js +8 -0
  51. package/dist/markdownToOrg.d.ts +8 -0
  52. package/dist/markdownToOrg.js +194 -0
  53. package/dist/normalize.d.ts +26 -0
  54. package/dist/normalize.js +24 -0
  55. package/dist/options.d.ts +102 -0
  56. package/dist/options.js +9 -0
  57. package/dist/orgToMarkdown.d.ts +8 -0
  58. package/dist/orgToMarkdown.js +61 -0
  59. package/dist/presets/logseq.d.ts +25 -0
  60. package/dist/presets/logseq.js +298 -0
  61. package/dist/presets/obsidian.d.ts +6 -0
  62. package/dist/presets/obsidian.js +40 -0
  63. package/dist/presets/registry.d.ts +8 -0
  64. package/dist/presets/registry.js +25 -0
  65. package/dist/presets/types.d.ts +12 -0
  66. package/dist/presets/types.js +1 -0
  67. package/package.json +112 -0
@@ -0,0 +1,298 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { toString } from "orgast-util-to-string";
3
+ /**
4
+ * Logseq dialect preset: `heading::` properties and outline nesting.
5
+ */
6
+ export function logseq(options = {}) {
7
+ const nestUnderHeadings = options.nestUnderHeadings ?? true;
8
+ return {
9
+ name: "logseq",
10
+ applyToUniorg: uniorgAst => applyLogseqSpecificsToUniorgAst(uniorgAst, nestUnderHeadings),
11
+ extractFromUniorg: extractLogseqSpecificsFromUniorgAst
12
+ };
13
+ }
14
+ function headingProperty(level) {
15
+ return { type: "node-property", key: "heading", value: String(level) };
16
+ }
17
+ function headingDrawer(level) {
18
+ return {
19
+ type: "property-drawer",
20
+ children: [headingProperty(level)],
21
+ contentsBegin: 0,
22
+ contentsEnd: 0
23
+ };
24
+ }
25
+ // org fixes the order below a headline: the planning line first, then a
26
+ // single property drawer. :heading: therefore has to let the planning
27
+ // line pass and join an existing drawer rather than displace either.
28
+ // Returns whether the property has been placed on `node`.
29
+ function placeHeadingProperty(node, level) {
30
+ if (node.type === "planning") {
31
+ return false;
32
+ }
33
+ if (node.type !== "property-drawer") {
34
+ return false;
35
+ }
36
+ ;
37
+ node.children.unshift(headingProperty(level));
38
+ return true;
39
+ }
40
+ // the page name of a [[page]] link destination, or null. The core
41
+ // transform percent-encodes brackets in urls (an org link path cannot
42
+ // hold them), so the destination arrives here already encoded.
43
+ function pageRefUrlTarget(rawLink) {
44
+ const decoded = rawLink.replaceAll("%5B", "[").replaceAll("%5D", "]");
45
+ return /^\[\[(.+)\]\]$/.exec(decoded)?.[1] ?? null;
46
+ }
47
+ // Logseq md labeled page refs ([label]([[page]])) become org's
48
+ // [[page][label]] description syntax. A page name containing a space is
49
+ // not a valid CommonMark destination, so the ref travels as plain text;
50
+ // without one remark parses it as a real link instead.
51
+ function rewriteLabeledPageRefs(uniorgAst) {
52
+ visit(uniorgAst, "text", (node) => {
53
+ node.value = node.value.replace(/\[([^\]]+)\]\(\[\[([^\]]+)\]\]\)/g, "[[$2][$1]]");
54
+ });
55
+ visit(uniorgAst, "link", (node) => {
56
+ const page = pageRefUrlTarget(node.rawLink);
57
+ if (page === null) {
58
+ return;
59
+ }
60
+ node.linkType = "fuzzy";
61
+ node.rawLink = page;
62
+ node.path = page;
63
+ });
64
+ }
65
+ function toBlockHeadline(node, level) {
66
+ const blockHeadline = {
67
+ type: "headline",
68
+ level,
69
+ todoKeyword: null,
70
+ priority: null,
71
+ commented: false,
72
+ rawValue: "",
73
+ tags: [],
74
+ children: node.children
75
+ };
76
+ takeTaskMarker(blockHeadline);
77
+ return blockHeadline;
78
+ }
79
+ /**
80
+ * Applies Logseq-specific conventions to a uniorg AST: every heading gets
81
+ * a `:heading:` property drawer, and with `nestUnderHeadings` following
82
+ * paragraphs become child block headlines (in Logseq's org format every
83
+ * outline block is a headline).
84
+ * @param uniorgAst The uniorg AST to transform.
85
+ * @param nestUnderHeadings Nest content as child blocks. Default: `true`.
86
+ * @returns The Logseq-flavored uniorg AST.
87
+ */
88
+ export function applyLogseqSpecificsToUniorgAst(uniorgAst, nestUnderHeadings = true) {
89
+ rewriteLabeledPageRefs(uniorgAst);
90
+ const children = uniorgAst.children;
91
+ const result = [];
92
+ let currentLevel = 0;
93
+ // level of a headline whose :heading: property still needs a home
94
+ let pending = null;
95
+ const separate = () => {
96
+ if (!nestUnderHeadings) {
97
+ result.push({ type: "text", value: "\n" });
98
+ }
99
+ };
100
+ // no drawer took the property: give it one of its own
101
+ const settle = () => {
102
+ if (pending === null) {
103
+ return;
104
+ }
105
+ result.push(headingDrawer(pending));
106
+ pending = null;
107
+ separate();
108
+ };
109
+ for (const node of children) {
110
+ if (node.type === "headline") {
111
+ settle();
112
+ const headline = node;
113
+ currentLevel = headline.level;
114
+ takeTaskMarker(headline);
115
+ result.push(node);
116
+ pending = headline.level;
117
+ continue;
118
+ }
119
+ if (pending !== null) {
120
+ if (placeHeadingProperty(node, pending)) {
121
+ pending = null;
122
+ result.push(node);
123
+ separate();
124
+ continue;
125
+ }
126
+ if (node.type !== "planning") {
127
+ settle();
128
+ }
129
+ }
130
+ result.push(nestUnderHeadings && node.type === "paragraph"
131
+ ? toBlockHeadline(node, currentLevel + 1)
132
+ : node);
133
+ }
134
+ settle();
135
+ uniorgAst.children = result;
136
+ return uniorgAst;
137
+ }
138
+ // Logseq md keeps TODO/DONE as leading text markers and priorities as
139
+ // [#A] text; in org they are the headline's TODO keyword and priority
140
+ // (other Logseq markers like DOING are not org keywords and simply
141
+ // stay in the title text)
142
+ function takeTaskMarker(headline) {
143
+ const first = headline.children[0];
144
+ if (first?.type !== "text") {
145
+ return;
146
+ }
147
+ const marker = /^(TODO|DONE) (?:\[#([A-Z])\] )?/.exec(first.value);
148
+ if (!marker) {
149
+ return;
150
+ }
151
+ headline.todoKeyword = marker[1];
152
+ if (marker[2]) {
153
+ headline.priority = marker[2];
154
+ }
155
+ first.value = first.value.slice((marker[0] ?? "").length);
156
+ }
157
+ /**
158
+ * Extracts Logseq-specific conventions from a uniorg AST: headlines with
159
+ * a `:heading:` property become plain headings of that level, headlines
160
+ * without one are outline blocks and become paragraphs.
161
+ * @param uniorgAst The Logseq-flavored uniorg AST to transform.
162
+ * @returns The generic uniorg AST.
163
+ */
164
+ function extractLogseqSpecificsFromUniorgAst(uniorgAst) {
165
+ extractInParent(uniorgAst);
166
+ repairHighlights(uniorgAst);
167
+ fuzzyLinksToPageRefs(uniorgAst);
168
+ markHiccupParagraphs(uniorgAst);
169
+ return uniorgAst;
170
+ }
171
+ // Logseq highlight markup (^^words^^) re-parses as a caret plus a
172
+ // superscript; merge the pieces back into literal text
173
+ function repairHighlights(uniorgAst) {
174
+ visit(uniorgAst, node => {
175
+ const children = node.children;
176
+ if (!children) {
177
+ return;
178
+ }
179
+ for (let i = 0; i + 1 < children.length; i++) {
180
+ const current = children[i];
181
+ const next = children[i + 1];
182
+ if (current?.type === "text" &&
183
+ current.value?.endsWith("^") &&
184
+ next?.type === "superscript") {
185
+ current.value += `^${toString(next)}`;
186
+ children.splice(i + 1, 1);
187
+ i--;
188
+ }
189
+ }
190
+ });
191
+ }
192
+ // Logseq page and block references: [[page]] stays a wikilink,
193
+ // [[page][label]] becomes [label]([[page]]), [[((uuid))][label]]
194
+ // becomes [label](((uuid))) — emitted unescaped via verbatim-inline
195
+ const BLOCK_REF_RE = /^\(\(.*\)\)$/;
196
+ function pageRefValue(label, target) {
197
+ if (!label) {
198
+ return `[[${target}]]`;
199
+ }
200
+ if (BLOCK_REF_RE.test(target)) {
201
+ return `[${label}](${target})`;
202
+ }
203
+ return `[${label}]([[${target}]])`;
204
+ }
205
+ function fuzzyLinksToPageRefs(uniorgAst) {
206
+ visit(uniorgAst, "link", (node, index, parent) => {
207
+ const target = node.rawLink ?? "";
208
+ const isBlockRef = BLOCK_REF_RE.test(target);
209
+ if ((node.linkType !== "fuzzy" && !isBlockRef) || !parent) {
210
+ return undefined;
211
+ }
212
+ const label = node.children.length ? toString(node) : "";
213
+ const value = pageRefValue(label, target);
214
+ parent.children[index] = {
215
+ type: "verbatim-inline",
216
+ value
217
+ };
218
+ return undefined;
219
+ });
220
+ }
221
+ // hiccup blocks ([:tag …]) are Logseq markup, not links or footnotes; a
222
+ // verbatim-inline node keeps the brackets unescaped in Markdown output
223
+ function markHiccupParagraphs(uniorgAst) {
224
+ visit(uniorgAst, "paragraph", (node) => {
225
+ const text = toString(node);
226
+ if (text.startsWith("[:")) {
227
+ node.children = [
228
+ { type: "verbatim-inline", value: text.replace(/\n$/, "") }
229
+ ];
230
+ }
231
+ });
232
+ }
233
+ function extractInParent(parent) {
234
+ const children = parent.children;
235
+ for (let i = 0; i < children.length; i++) {
236
+ const node = children[i];
237
+ if (!node) {
238
+ continue;
239
+ }
240
+ if (node.type === "section") {
241
+ extractInParent(node);
242
+ continue;
243
+ }
244
+ if (node.type !== "headline") {
245
+ continue;
246
+ }
247
+ const headline = node;
248
+ if (headline.todoKeyword) {
249
+ // back to Logseq md's text conventions (TODO [#A] Ship it);
250
+ // verbatim-inline keeps the [#A] brackets unescaped
251
+ const priority = headline.priority ? `[#${headline.priority}] ` : "";
252
+ headline.children.unshift({
253
+ type: "verbatim-inline",
254
+ value: `${headline.todoKeyword} ${priority}`
255
+ });
256
+ headline.todoKeyword = null;
257
+ headline.priority = null;
258
+ }
259
+ // a planning line sits between the headline and its drawer
260
+ const drawerIndex = children[i + 1]?.type === "planning" ? i + 2 : i + 1;
261
+ const heading = takeHeadingProperty(children, drawerIndex);
262
+ if (heading !== null) {
263
+ headline.level = heading;
264
+ }
265
+ else {
266
+ // a block headline (no :heading:) is outline structure only; its
267
+ // title is the block's content
268
+ children[i] = {
269
+ type: "paragraph",
270
+ children: headline.children,
271
+ contentsBegin: 0,
272
+ contentsEnd: 0
273
+ };
274
+ }
275
+ }
276
+ }
277
+ // removes the heading property from a drawer at `index` (and the drawer
278
+ // itself if that empties it); returns the heading level or null
279
+ function takeHeadingProperty(children, index) {
280
+ const drawer = children[index];
281
+ if (drawer?.type !== "property-drawer") {
282
+ return null;
283
+ }
284
+ const properties = drawer.children;
285
+ const heading = properties.find(property => property.key === "heading");
286
+ if (!heading) {
287
+ return null;
288
+ }
289
+ const remaining = properties.filter(property => property !== heading);
290
+ if (remaining.length) {
291
+ ;
292
+ drawer.children = remaining;
293
+ }
294
+ else {
295
+ children.splice(index, 1);
296
+ }
297
+ return parseInt(heading.value, 10) || null;
298
+ }
@@ -0,0 +1,6 @@
1
+ import type { Preset } from "./types.js";
2
+ /**
3
+ * Obsidian dialect preset: `[[Page]]` / `[[Page|alias]]` wikilinks map
4
+ * to org fuzzy links (`[[Page]]` / `[[Page][alias]]`).
5
+ */
6
+ export declare function obsidian(): Preset;
@@ -0,0 +1,40 @@
1
+ import { visit } from "unist-util-visit";
2
+ import { toString } from "orgast-util-to-string";
3
+ /**
4
+ * Obsidian dialect preset: `[[Page]]` / `[[Page|alias]]` wikilinks map
5
+ * to org fuzzy links (`[[Page]]` / `[[Page][alias]]`).
6
+ */
7
+ export function obsidian() {
8
+ return {
9
+ name: "obsidian",
10
+ applyToUniorg: rewriteAliasedWikilinks,
11
+ extractFromUniorg: fuzzyLinksToWikilinks
12
+ };
13
+ }
14
+ // md→org: a wikilink travels as plain text; org already reads `[[Page]]`
15
+ // as a fuzzy link, only the `[[Page|alias]]` form needs rewriting to
16
+ // org's `[[Page][alias]]` description syntax
17
+ function rewriteAliasedWikilinks(uniorgAst) {
18
+ visit(uniorgAst, "text", (node) => {
19
+ node.value = node.value.replace(/\[\[([^\][|]+)\|([^\][]+)\]\]/g, "[[$1][$2]]");
20
+ });
21
+ return uniorgAst;
22
+ }
23
+ // org→md: fuzzy links become wikilink text; a verbatim-inline node keeps
24
+ // the brackets unescaped in the Markdown output
25
+ function fuzzyLinksToWikilinks(uniorgAst) {
26
+ visit(uniorgAst, "link", (node, index, parent) => {
27
+ if (node.linkType !== "fuzzy") {
28
+ return undefined;
29
+ }
30
+ const description = node.children.length ? toString(node) : "";
31
+ parent.children[index] = {
32
+ type: "verbatim-inline",
33
+ value: description
34
+ ? `[[${node.rawLink}|${description}]]`
35
+ : `[[${node.rawLink}]]`
36
+ };
37
+ return undefined;
38
+ });
39
+ return uniorgAst;
40
+ }
@@ -0,0 +1,8 @@
1
+ import type { Preset } from "./types.js";
2
+ /**
3
+ * Instantiates a preset by name.
4
+ * @param presetName Preset name, or `undefined` for no preset.
5
+ * @returns The preset, or `undefined` when no name was given.
6
+ * @throws If the name is not a known preset.
7
+ */
8
+ export declare function createPreset(presetName: string | undefined): Preset | undefined;
@@ -0,0 +1,25 @@
1
+ import { logseq } from "./logseq.js";
2
+ import { obsidian } from "./obsidian.js";
3
+ // the one place a dialect preset is registered; both adapters (CLI and
4
+ // Web UI) resolve names through here so they cannot drift apart
5
+ const PRESETS = {
6
+ logseq: () => logseq(),
7
+ obsidian: () => obsidian()
8
+ };
9
+ const PRESET_NAMES = Object.keys(PRESETS);
10
+ /**
11
+ * Instantiates a preset by name.
12
+ * @param presetName Preset name, or `undefined` for no preset.
13
+ * @returns The preset, or `undefined` when no name was given.
14
+ * @throws If the name is not a known preset.
15
+ */
16
+ export function createPreset(presetName) {
17
+ if (!presetName) {
18
+ return undefined;
19
+ }
20
+ const factory = PRESETS[presetName];
21
+ if (!factory) {
22
+ throw new Error(`Unknown preset '${presetName}'. Available presets: ${PRESET_NAMES.join(", ")}`);
23
+ }
24
+ return factory();
25
+ }
@@ -0,0 +1,12 @@
1
+ import type { OrgData } from "uniorg";
2
+ /**
3
+ * A dialect preset: transforms applied on top of the dialect-agnostic core.
4
+ * `applyToUniorg` runs in the md→org pipeline after the generic transform;
5
+ * `extractFromUniorg` runs in the org→md pipeline before the generic
6
+ * transform, normalizing dialect conventions back to generic uniorg.
7
+ */
8
+ export interface Preset {
9
+ name: string;
10
+ applyToUniorg?: (uniorgAst: OrgData) => OrgData;
11
+ extractFromUniorg?: (uniorgAst: OrgData) => OrgData;
12
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,112 @@
1
+ {
2
+ "name": "@remigius42/morg",
3
+ "version": "0.4.0",
4
+ "description": "Bidirectional Markdown ↔ Org-mode converter with round-trip convergence",
5
+ "keywords": [
6
+ "org-mode",
7
+ "markdown",
8
+ "converter",
9
+ "unified",
10
+ "remark",
11
+ "uniorg",
12
+ "logseq",
13
+ "obsidian"
14
+ ],
15
+ "license": "GPL-3.0-or-later",
16
+ "author": "Andreas Remigius Schmidt",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/remigius42/morg.git"
20
+ },
21
+ "bugs": "https://github.com/remigius42/morg/issues",
22
+ "homepage": "https://github.com/remigius42/morg#readme",
23
+ "type": "module",
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "bin": {
33
+ "morg": "./dist/cli.js"
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "provenance": true
41
+ },
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "scripts": {
46
+ "prepare": "husky",
47
+ "prepack": "npm run build",
48
+ "version": "node scripts/release-changelog.mjs && git add CHANGELOG.md",
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "build:web": "vite build web",
51
+ "dev:web": "vite web",
52
+ "preview:web": "vite preview web --port 4173 --strictPort",
53
+ "typecheck": "tsc --noEmit -p tsconfig.json",
54
+ "lint": "npm run lint:prettier && npm run lint:cspell && npm run lint:markdown && npm run lint:eslint && npm run lint:knip && npm run typecheck",
55
+ "lint:knip": "knip",
56
+ "lint:packaging": "attw --pack . --ignore-rules cjs-resolves-to-esm",
57
+ "lint:prettier": "prettier --no-error-on-unmatched-pattern --ignore-unknown --list-different '**/*'",
58
+ "lint:cspell": "cspell --dot --no-must-find-files --no-progress '**/*'",
59
+ "lint:markdown": "markdownlint-cli2 '**/*.md' '!node_modules/**'",
60
+ "lint:eslint": "eslint --max-warnings=0 '**/*.{ts,mts,js,mjs}'",
61
+ "lint:fix": "npm run lint:fix:prettier && npm run lint:fix:eslint",
62
+ "lint:fix:prettier": "prettier --write '**/*'",
63
+ "lint:fix:eslint": "eslint --fix '**/*.{ts,mts,js}'",
64
+ "test:unit": "vitest",
65
+ "test:unit:ci": "vitest run --reporter default --coverage",
66
+ "test:coverage": "vitest run --coverage",
67
+ "test:e2e": "playwright test",
68
+ "test:e2e:chromium": "playwright test --project=chromium",
69
+ "test:e2e:report": "playwright show-report tests/e2e/reports/html"
70
+ },
71
+ "dependencies": {
72
+ "@types/mdast": "^4.0.4",
73
+ "orgast-util-to-string": "^1.1.0",
74
+ "remark-frontmatter": "^5.0.0",
75
+ "remark-gfm": "^4.0.1",
76
+ "remark-math": "^6.0.0",
77
+ "remark-parse": "^11.0.0",
78
+ "remark-stringify": "^11.0.0",
79
+ "smol-toml": "^1.8.0",
80
+ "unified": "^11.0.5",
81
+ "uniorg": "^1.4.0",
82
+ "uniorg-parse": "^3.2.2",
83
+ "uniorg-stringify": "^1.4.0",
84
+ "unist-util-visit": "^5.0.0",
85
+ "yaml": "^2.9.0"
86
+ },
87
+ "devDependencies": {
88
+ "@arethetypeswrong/cli": "^0.18.5",
89
+ "@axe-core/playwright": "^4.13.0",
90
+ "@commitlint/cli": "^21.2.2",
91
+ "@commitlint/config-conventional": "^21.2.2",
92
+ "@eslint/js": "^10.0.1",
93
+ "@picocss/pico": "^2.1.1",
94
+ "@playwright/test": "^1.63.0",
95
+ "@types/node": "^24.3.0",
96
+ "@types/unist": "^3.0.3",
97
+ "@vitest/coverage-v8": "^5.0.0",
98
+ "cspell": "^10.2.2",
99
+ "eslint": "^10.10.0",
100
+ "happy-dom": "^20.14.3",
101
+ "husky": "^9.1.7",
102
+ "knip": "^6.35.1",
103
+ "lint-staged": "^17.5.1",
104
+ "markdownlint-cli2": "^0.23.2",
105
+ "prettier": "^3.6.2",
106
+ "tsx": "^4.23.13",
107
+ "typescript": "^5.9.2",
108
+ "typescript-eslint": "^8.42.0",
109
+ "vite": "^8.2.2",
110
+ "vitest": "^5.0.0"
111
+ }
112
+ }