@markmap/parse 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Gerald
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @markmap/parse
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Parse Markdown into a Markmap tree.
4
+
5
+ See the [Markmap README](https://github.com/markmap/markmap#readme) for how the packages fit together.
@@ -0,0 +1,19 @@
1
+ import type { Root } from 'mdast';
2
+ export interface HtmlImage {
3
+ src: string;
4
+ alt: string;
5
+ /** In px. Missing when not given, or not in px (such as a percentage). */
6
+ width?: number;
7
+ height?: number;
8
+ }
9
+ /**
10
+ * Read one GitHub-style `<img>` tag: exactly one tag, nothing around it, well-formed
11
+ * attributes. Only `src`, `alt`, `width` and `height` are kept; other attributes are
12
+ * ignored. Returns `undefined` for anything else, which callers then drop.
13
+ */
14
+ export declare function parseHtmlImage(html: string): HtmlImage | undefined;
15
+ /**
16
+ * Turn raw `<img>` tags into `image` nodes, inline and on their own line. All other raw
17
+ * HTML is left alone for later steps to drop.
18
+ */
19
+ export declare function remarkHtmlImages(): (tree: Root) => void;
@@ -0,0 +1,4 @@
1
+ export * from './parse';
2
+ export * from './to-tree';
3
+ export * from './plugins';
4
+ export * from './html-image';
package/dist/index.js ADDED
@@ -0,0 +1,385 @@
1
+ import remarkFrontmatter from "remark-frontmatter";
2
+ import remarkGfm from "remark-gfm";
3
+ import remarkParse from "remark-parse";
4
+ import { unified } from "unified";
5
+ import { parse as parse$1 } from "yaml";
6
+ import { assignIds } from "@markmap/core";
7
+ // src/html-image.ts
8
+ var TAG = /^<img((?:\s+[a-z][a-z0-9-]*(?:\s*=\s*(?:"[^"<>]*"|'[^'<>]*'|[^\s"'=<>`]+))?)*)\s*\/?>$/i;
9
+ var ATTR = /([a-z][a-z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/gi;
10
+ var PIXELS = /^(\d+(?:\.\d+)?)(?:px)?$/;
11
+ var ENTITIES = {
12
+ "&amp;": "&",
13
+ "&quot;": "\"",
14
+ "&#39;": "'",
15
+ "&lt;": "<",
16
+ "&gt;": ">"
17
+ };
18
+ var decode = (value) => value.replace(/&(?:amp|quot|#39|lt|gt);/g, (e) => ENTITIES[e]);
19
+ function pixels(value) {
20
+ const match = value && PIXELS.exec(value.trim());
21
+ const n = match ? Number(match[1]) : 0;
22
+ return n > 0 ? n : void 0;
23
+ }
24
+ /**
25
+ * Read one GitHub-style `<img>` tag: exactly one tag, nothing around it, well-formed
26
+ * attributes. Only `src`, `alt`, `width` and `height` are kept; other attributes are
27
+ * ignored. Returns `undefined` for anything else, which callers then drop.
28
+ */
29
+ function parseHtmlImage(html) {
30
+ const match = TAG.exec(html.trim());
31
+ if (!match) return void 0;
32
+ const attrs = /* @__PURE__ */ new Map();
33
+ for (const [, name, ...values] of match[1].matchAll(ATTR)) {
34
+ const key = name.toLowerCase();
35
+ if (!attrs.has(key)) attrs.set(key, decode(values.find((v) => v != null) ?? ""));
36
+ }
37
+ const src = attrs.get("src")?.trim();
38
+ if (!src) return void 0;
39
+ return {
40
+ src,
41
+ alt: attrs.get("alt") ?? "",
42
+ width: pixels(attrs.get("width")),
43
+ height: pixels(attrs.get("height"))
44
+ };
45
+ }
46
+ function toImage(html) {
47
+ const parsed = parseHtmlImage(html.value);
48
+ if (!parsed) return void 0;
49
+ const size = {
50
+ ...parsed.width && { width: parsed.width },
51
+ ...parsed.height && { height: parsed.height }
52
+ };
53
+ return {
54
+ type: "image",
55
+ url: parsed.src,
56
+ alt: parsed.alt,
57
+ data: {
58
+ ...size,
59
+ hProperties: size
60
+ }
61
+ };
62
+ }
63
+ /** Parents whose children are blocks, where an `<img>` line becomes its own paragraph. */
64
+ var FLOW = /* @__PURE__ */ new Set([
65
+ "root",
66
+ "listItem",
67
+ "blockquote",
68
+ "footnoteDefinition"
69
+ ]);
70
+ /**
71
+ * Turn raw `<img>` tags into `image` nodes, inline and on their own line. All other raw
72
+ * HTML is left alone for later steps to drop.
73
+ */
74
+ function remarkHtmlImages() {
75
+ const visit = (node) => {
76
+ if (!("children" in node)) return;
77
+ const parent = node;
78
+ parent.children = parent.children.map((child) => {
79
+ if (child.type !== "html") {
80
+ visit(child);
81
+ return child;
82
+ }
83
+ const image = toImage(child);
84
+ if (!image) return child;
85
+ if (!FLOW.has(parent.type)) return image;
86
+ return {
87
+ type: "paragraph",
88
+ children: [image]
89
+ };
90
+ });
91
+ };
92
+ return (tree) => visit(tree);
93
+ }
94
+ // src/plugins.ts
95
+ function visitParents(node, fn) {
96
+ if (!("children" in node)) return;
97
+ for (const child of node.children) visitParents(child, fn);
98
+ fn(node);
99
+ }
100
+ /**
101
+ * Turn `==text==` into `mark` nodes. Like `**`, the opening `==` must be followed by
102
+ * a non-space and the closing one preceded by a non-space. Delimiters can wrap other
103
+ * inline nodes (`==a **b**==`) but not cross block boundaries.
104
+ */
105
+ function remarkMark() {
106
+ return (tree) => visitParents(tree, (parent) => {
107
+ if (parent.children.some((child) => child.type === "text" && child.value.includes("=="))) parent.children = markChildren(parent.children);
108
+ });
109
+ }
110
+ function markChildren(children) {
111
+ const tokens = [];
112
+ for (const child of children) {
113
+ if (child.type !== "text") {
114
+ tokens.push({
115
+ delim: false,
116
+ node: child
117
+ });
118
+ continue;
119
+ }
120
+ child.value.split(/(==)/).forEach((part) => {
121
+ if (part === "==") tokens.push({
122
+ delim: true,
123
+ canOpen: false,
124
+ canClose: false
125
+ });
126
+ else if (part) tokens.push({
127
+ delim: false,
128
+ node: {
129
+ type: "text",
130
+ value: part
131
+ }
132
+ });
133
+ });
134
+ }
135
+ const edgeChar = (token, end) => {
136
+ if (!token) return "";
137
+ if (token.delim) return "=";
138
+ if (token.node.type === "text") return end ? token.node.value.at(-1) : token.node.value[0];
139
+ return "x";
140
+ };
141
+ tokens.forEach((token, i) => {
142
+ if (!token.delim) return;
143
+ const next = edgeChar(tokens[i + 1], false);
144
+ const prev = edgeChar(tokens[i - 1], true);
145
+ token.canOpen = !!next && !/\s/.test(next);
146
+ token.canClose = !!prev && !/\s/.test(prev);
147
+ });
148
+ const pairs = /* @__PURE__ */ new Map();
149
+ let open = -1;
150
+ tokens.forEach((token, i) => {
151
+ if (!token.delim) return;
152
+ if (open >= 0 && token.canClose && i > open + 1) {
153
+ pairs.set(open, i);
154
+ open = -1;
155
+ } else if (open < 0 && token.canOpen) open = i;
156
+ });
157
+ const build = (from, to) => {
158
+ const result = [];
159
+ const pushText = (value) => {
160
+ const last = result.at(-1);
161
+ if (last?.type === "text") last.value += value;
162
+ else result.push({
163
+ type: "text",
164
+ value
165
+ });
166
+ };
167
+ for (let i = from; i < to; i += 1) {
168
+ const token = tokens[i];
169
+ const close = pairs.get(i);
170
+ if (close != null) {
171
+ result.push({
172
+ type: "mark",
173
+ children: build(i + 1, close)
174
+ });
175
+ i = close;
176
+ } else if (token.delim) pushText("==");
177
+ else if (token.node.type === "text") pushText(token.node.value);
178
+ else result.push(token.node);
179
+ }
180
+ return result;
181
+ };
182
+ return build(0, tokens.length);
183
+ }
184
+ /** Replace `[text][ref]` and `![alt][ref]` with plain links and images, and drop definitions. */
185
+ function remarkResolveReferences() {
186
+ return (tree) => {
187
+ const definitions = /* @__PURE__ */ new Map();
188
+ visitParents(tree, (parent) => {
189
+ for (const child of parent.children) if (child.type === "definition") definitions.set(child.identifier, child);
190
+ });
191
+ visitParents(tree, (parent) => {
192
+ parent.children = parent.children.flatMap((child) => {
193
+ if (child.type === "linkReference") {
194
+ const def = definitions.get(child.identifier);
195
+ return def ? [{
196
+ type: "link",
197
+ url: def.url,
198
+ title: def.title,
199
+ children: child.children
200
+ }] : child.children;
201
+ }
202
+ if (child.type === "imageReference") {
203
+ const def = definitions.get(child.identifier);
204
+ return def ? [{
205
+ type: "image",
206
+ url: def.url,
207
+ title: def.title,
208
+ alt: child.alt
209
+ }] : [{
210
+ type: "text",
211
+ value: child.alt ?? ""
212
+ }];
213
+ }
214
+ return [child];
215
+ });
216
+ });
217
+ };
218
+ }
219
+ // src/to-tree.ts
220
+ /**
221
+ * Convert an mdast tree into a Markmap tree.
222
+ *
223
+ * - Headings nest by depth; list items nest under the heading or item that contains them.
224
+ * - Other blocks become notes of the node that owns them. Blocks before the first
225
+ * heading attach to the root.
226
+ * - A lone top-level H1 becomes the root, unless `title` is given.
227
+ * - Raw HTML, frontmatter and link definitions are dropped. A `<!-- markmap: … -->`
228
+ * comment in a label sets node options: `fold` folds the node, `foldAll` folds it
229
+ * and everything below.
230
+ */
231
+ function toTree(mdast, options = {}) {
232
+ let root = createNode("root", options.title ? [{
233
+ type: "text",
234
+ value: options.title
235
+ }] : []);
236
+ const stack = [{
237
+ depth: 0,
238
+ node: root
239
+ }];
240
+ for (const block of mdast.children) if (block.type === "heading") {
241
+ while (stack.length > 1 && stack.at(-1).depth >= block.depth) stack.pop();
242
+ const node = createNode("heading", block.children, block);
243
+ node.data.depth = block.depth;
244
+ stack.at(-1).node.children.push(node);
245
+ stack.push({
246
+ depth: block.depth,
247
+ node
248
+ });
249
+ } else addBlock(stack.at(-1).node, block);
250
+ if (!options.title) root = promoteSingleH1(root);
251
+ return assignIds(root);
252
+ }
253
+ function createNode(type, label, source) {
254
+ const { content, directives } = readLabel(label);
255
+ const node = {
256
+ id: "",
257
+ type,
258
+ content,
259
+ notes: [],
260
+ data: {},
261
+ children: []
262
+ };
263
+ if (directives.has("fold")) node.data.fold = true;
264
+ if (directives.has("foldAll")) node.data.foldAll = true;
265
+ const start = source?.position?.start.offset;
266
+ const end = source?.position?.end.offset;
267
+ if (start != null && end != null) node.position = {
268
+ start,
269
+ end
270
+ };
271
+ return node;
272
+ }
273
+ function addBlock(parent, block) {
274
+ switch (block.type) {
275
+ case "list":
276
+ addList(parent, block);
277
+ return;
278
+ case "html":
279
+ case "yaml":
280
+ case "definition": return;
281
+ default: attach(parent, block);
282
+ }
283
+ }
284
+ function addList(parent, list) {
285
+ for (const item of list.children) parent.children.push(fromListItem(item, list));
286
+ }
287
+ function fromListItem(item, list) {
288
+ const [first, ...rest] = item.children;
289
+ const hasLabel = first?.type === "paragraph" || first?.type === "heading";
290
+ const node = createNode("listItem", hasLabel ? first.children : [], item);
291
+ if (list.ordered) node.data.ordered = true;
292
+ if (item.checked != null) node.data.checked = item.checked;
293
+ for (const block of hasLabel ? rest : item.children) addBlock(node, block);
294
+ return node;
295
+ }
296
+ function attach(node, block) {
297
+ const note = {
298
+ block: stripPositions(block),
299
+ after: node.children.length
300
+ };
301
+ node.notes.push(note);
302
+ }
303
+ function promoteSingleH1(root) {
304
+ const [only] = root.children;
305
+ if (root.children.length !== 1 || only.type !== "heading" || only.data.depth !== 1) return root;
306
+ only.notes = [...root.notes.map((a) => ({
307
+ ...a,
308
+ after: 0
309
+ })), ...only.notes];
310
+ return only;
311
+ }
312
+ /** Drop mdast `position` fields so the tree stays small and serializable. */
313
+ function stripPositions(value) {
314
+ if (Array.isArray(value)) return value.map(stripPositions);
315
+ if (value && typeof value === "object") {
316
+ const result = {};
317
+ for (const [key, child] of Object.entries(value)) if (key !== "position") result[key] = stripPositions(child);
318
+ return result;
319
+ }
320
+ return value;
321
+ }
322
+ var DIRECTIVE = /^<!--\s*markmap:\s*([\s\S]*?)\s*-->$/;
323
+ /** Drop raw HTML from a label, collecting `<!-- markmap: … -->` directives on the way. */
324
+ function readLabel(label) {
325
+ const directives = /* @__PURE__ */ new Set();
326
+ const clean = (nodes) => nodes.flatMap((node) => {
327
+ if (node.type === "html") {
328
+ const match = node.value.trim().match(DIRECTIVE);
329
+ for (const word of match?.[1].split(/\s+/) ?? []) if (word) directives.add(word);
330
+ return [];
331
+ }
332
+ if ("children" in node) return [{
333
+ ...node,
334
+ children: clean(node.children)
335
+ }];
336
+ return [node];
337
+ });
338
+ const content = stripPositions(clean(label));
339
+ const last = content.at(-1);
340
+ if (last?.type === "text") {
341
+ last.value = last.value.trimEnd();
342
+ if (!last.value) content.pop();
343
+ }
344
+ return {
345
+ content,
346
+ directives
347
+ };
348
+ }
349
+ // src/parse.ts
350
+ function parse(markdown, options = {}) {
351
+ const { mdast, gfm } = parseMdast(markdown, options.plugins ?? []);
352
+ const frontmatter = readFrontmatter(mdast);
353
+ return {
354
+ root: toTree(mdast, { title: typeof frontmatter.title === "string" ? frontmatter.title : void 0 }),
355
+ frontmatter,
356
+ gfm
357
+ };
358
+ }
359
+ /**
360
+ * GFM depends on the most third-party code, and old browsers reject syntax it may use, like
361
+ * regex lookbehind before Safari 16.4. A map without GFM is better than no map at all.
362
+ */
363
+ function parseMdast(markdown, plugins, gfm = true) {
364
+ const processor = unified().use(remarkParse).use(gfm ? [remarkGfm] : []).use(remarkFrontmatter, ["yaml"]).use(remarkHtmlImages).use(remarkMark).use(remarkResolveReferences).use(plugins);
365
+ try {
366
+ return {
367
+ mdast: processor.runSync(processor.parse(markdown)),
368
+ gfm
369
+ };
370
+ } catch (error) {
371
+ if (!gfm || !(error instanceof SyntaxError)) throw error;
372
+ return parseMdast(markdown, plugins, false);
373
+ }
374
+ }
375
+ function readFrontmatter(mdast) {
376
+ const node = mdast.children.find((child) => child.type === "yaml");
377
+ if (!node) return {};
378
+ try {
379
+ const data = parse$1(node.value);
380
+ return data && typeof data === "object" && !Array.isArray(data) ? data : {};
381
+ } catch {
382
+ return {};
383
+ }
384
+ }
385
+ export { parse, parseHtmlImage, remarkHtmlImages, remarkMark, remarkResolveReferences, toTree };
@@ -0,0 +1,14 @@
1
+ import type { MarkmapNode } from '@markmap/core';
2
+ import { type PluggableList } from 'unified';
3
+ export interface ParseOptions {
4
+ /** Extra unified plugins, applied after the built-in ones (gfm, frontmatter, highlight, references). */
5
+ plugins?: PluggableList;
6
+ }
7
+ export interface ParseResult {
8
+ root: MarkmapNode;
9
+ /** Parsed YAML frontmatter. Markmap options live under `markmap`; validate them with the theme in use. */
10
+ frontmatter: Record<string, unknown>;
11
+ /** Whether GFM was applied. It is skipped when the browser cannot run it, see `parseMdast`. */
12
+ gfm: boolean;
13
+ }
14
+ export declare function parse(markdown: string, options?: ParseOptions): ParseResult;
@@ -0,0 +1,9 @@
1
+ import type { Root } from 'mdast';
2
+ /**
3
+ * Turn `==text==` into `mark` nodes. Like `**`, the opening `==` must be followed by
4
+ * a non-space and the closing one preceded by a non-space. Delimiters can wrap other
5
+ * inline nodes (`==a **b**==`) but not cross block boundaries.
6
+ */
7
+ export declare function remarkMark(): (tree: Root) => void;
8
+ /** Replace `[text][ref]` and `![alt][ref]` with plain links and images, and drop definitions. */
9
+ export declare function remarkResolveReferences(): (tree: Root) => void;
@@ -0,0 +1,18 @@
1
+ import { type MarkmapNode } from '@markmap/core';
2
+ import type { Root } from 'mdast';
3
+ export interface ToTreeOptions {
4
+ /** Title for a synthesized root, usually from frontmatter. */
5
+ title?: string;
6
+ }
7
+ /**
8
+ * Convert an mdast tree into a Markmap tree.
9
+ *
10
+ * - Headings nest by depth; list items nest under the heading or item that contains them.
11
+ * - Other blocks become notes of the node that owns them. Blocks before the first
12
+ * heading attach to the root.
13
+ * - A lone top-level H1 becomes the root, unless `title` is given.
14
+ * - Raw HTML, frontmatter and link definitions are dropped. A `<!-- markmap: … -->`
15
+ * comment in a label sets node options: `fold` folds the node, `foldAll` folds it
16
+ * and everything below.
17
+ */
18
+ export declare function toTree(mdast: Root, options?: ToTreeOptions): MarkmapNode;
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@markmap/parse",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Parse Markdown into a Markmap tree",
5
+ "homepage": "https://github.com/markmap/markmap#readme",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/markmap/markmap.git",
10
+ "directory": "packages/parse"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "dependencies": {
27
+ "@markmap/core": "1.0.0",
28
+ "remark-frontmatter": "^5.0.0",
29
+ "remark-gfm": "^4.0.1",
30
+ "remark-parse": "^11.0.0",
31
+ "unified": "^11.0.5",
32
+ "yaml": "^2.9.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/mdast": "^4.0.4"
36
+ },
37
+ "scripts": {
38
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
39
+ "build:types": "tsc -b",
40
+ "build:js": "vite build"
41
+ }
6
42
  }