@mark-sorcery/vue 0.1.0 → 0.2.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/dist/index.cjs CHANGED
@@ -3,6 +3,13 @@ let vue = require("vue");
3
3
  let _mark_sorcery_markdown_parser = require("@mark-sorcery/markdown-parser");
4
4
 
5
5
  //#region src/hast-to-vnodes.ts
6
+ function createNodeKey(node, path) {
7
+ const start = node.position?.start?.offset;
8
+ const end = node.position?.end?.offset;
9
+ if (typeof start === "number" && typeof end === "number") return `${node.tagName}:${start}-${end}`;
10
+ if (typeof start === "number") return `${node.tagName}:${start}`;
11
+ return path;
12
+ }
6
13
  /**
7
14
  * Convert HAST node properties to Vue-compatible props.
8
15
  * - `className` array → `class` string
@@ -24,24 +31,24 @@ function resolveTag(node, components) {
24
31
  * Internal recursive converter. `path` is a dot-separated string identifying
25
32
  * the node's position in the tree (e.g. `"0"`, `"0.1"`, `"0.1.2"`).
26
33
  */
27
- function toVNodes(node, components, transition, path) {
28
- if (Array.isArray(node)) return node.flatMap((n, i) => toVNodes(n, components, transition, `${path}.${i}`));
34
+ function toVNodes(node, components, path) {
35
+ if (Array.isArray(node)) return node.flatMap((n, i) => toVNodes(n, components, `${path}.${i}`));
29
36
  switch (node.type) {
30
- case "root": return node.children.flatMap((child, i) => toVNodes(child, components, transition, String(i)));
37
+ case "root": return node.children.flatMap((child, i) => toVNodes(child, components, String(i)));
31
38
  case "element": {
32
39
  const { properties = {}, children } = node;
33
40
  const tag = resolveTag(node, components);
41
+ const nodeKey = createNodeKey(node, path);
34
42
  const props = convertProps(properties);
35
- const childVNodes = children.flatMap((child, i) => toVNodes(child, components, transition, `${path}.${i}`));
36
- const el = typeof tag === "string" ? (0, vue.h)(tag, props, childVNodes) : (0, vue.h)(tag, {
43
+ const childVNodes = children.flatMap((child, i) => toVNodes(child, components, `${path}.${i}`));
44
+ return [typeof tag === "string" ? (0, vue.h)(tag, {
45
+ ...props,
46
+ key: nodeKey
47
+ }, childVNodes) : (0, vue.h)(tag, {
37
48
  ...props,
49
+ key: nodeKey,
38
50
  node
39
- }, { default: () => childVNodes });
40
- if (transition !== void 0) return [(0, vue.h)(vue.Transition, {
41
- key: path,
42
- ...transition
43
- }, { default: () => el })];
44
- return [el];
51
+ }, { default: () => childVNodes })];
45
52
  }
46
53
  case "text": return [node.value];
47
54
  case "comment": return [(0, vue.createCommentVNode)(node.value)];
@@ -55,8 +62,8 @@ function toVNodes(node, components, transition, path) {
55
62
  * @param components - Custom component map or resolver function.
56
63
  * @param transition - Optional `<Transition>` config applied to every element node.
57
64
  */
58
- function hastToVNodes(node, components, transition) {
59
- return toVNodes(node, components, transition, "");
65
+ function hastToVNodes(node, components) {
66
+ return toVNodes(node, components, "");
60
67
  }
61
68
 
62
69
  //#endregion
@@ -72,27 +79,64 @@ const Markdown = (0, vue.defineComponent)({
72
79
  type: Object,
73
80
  default: void 0
74
81
  },
82
+ plugins: {
83
+ type: Array,
84
+ default: void 0
85
+ },
86
+ stream: {
87
+ type: Boolean,
88
+ default: false
89
+ },
75
90
  components: {
76
91
  type: [Object, Function],
77
92
  default: () => ({})
78
- },
79
- transition: {
80
- type: [Boolean, Object],
81
- default: false
82
93
  }
83
94
  },
84
95
  setup(props) {
85
- const hast = (0, vue.computed)(() => (0, _mark_sorcery_markdown_parser.parse)(props.markdown, props.options));
96
+ const getMarkdown = () => props.markdown ?? "";
97
+ const processor = (0, vue.computed)(() => {
98
+ const options = props.options;
99
+ const propPlugins = props.plugins ?? [];
100
+ return (0, _mark_sorcery_markdown_parser.createProcessor)({
101
+ ...options,
102
+ plugins: propPlugins
103
+ });
104
+ });
105
+ const hast = (0, vue.shallowRef)((0, _mark_sorcery_markdown_parser.parse)(processor.value, getMarkdown()));
106
+ let streamMemory;
107
+ let activeProcessor;
108
+ (0, vue.watchEffect)(() => {
109
+ const currentProcessor = processor.value;
110
+ const markdown = getMarkdown();
111
+ if (activeProcessor && activeProcessor !== currentProcessor) streamMemory = props.stream ? (0, _mark_sorcery_markdown_parser.createMemory)() : void 0;
112
+ activeProcessor = currentProcessor;
113
+ if (props.stream) {
114
+ streamMemory ??= (0, _mark_sorcery_markdown_parser.createMemory)();
115
+ hast.value = (0, _mark_sorcery_markdown_parser.parse)(currentProcessor, markdown, streamMemory);
116
+ return;
117
+ }
118
+ if (streamMemory) {
119
+ streamMemory.flush = true;
120
+ streamMemory = void 0;
121
+ return;
122
+ }
123
+ hast.value = (0, _mark_sorcery_markdown_parser.parse)(currentProcessor, markdown);
124
+ });
86
125
  return () => {
87
126
  const raw = props.components ?? {};
88
127
  const components = typeof raw === "function" ? raw : Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, typeof v === "string" || v == null ? v : (0, vue.markRaw)(v)]));
89
- const transitionConfig = props.transition === true ? {} : props.transition || void 0;
90
- return (0, vue.h)(vue.Fragment, hastToVNodes(hast.value, components, transitionConfig));
128
+ return (0, vue.h)(vue.Fragment, hastToVNodes(hast.value, components));
91
129
  };
92
130
  }
93
131
  });
94
132
 
95
133
  //#endregion
96
134
  exports.Markdown = Markdown;
135
+ Object.defineProperty(exports, 'createCorePlugin', {
136
+ enumerable: true,
137
+ get: function () {
138
+ return _mark_sorcery_markdown_parser.createCorePlugin;
139
+ }
140
+ });
97
141
  exports.hastToVNodes = hastToVNodes;
98
142
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["Transition","Fragment"],"sources":["../src/hast-to-vnodes.ts","../src/Markdown.ts"],"sourcesContent":["import { h, createCommentVNode, Transition } from 'vue';\r\nimport type { Element, Nodes } from 'hast';\r\nimport type { VNodeArrayChildren } from 'vue';\r\nimport type { ComponentResolution, Components, TransitionConfig } from './types.ts';\r\n\r\n/**\r\n * Convert HAST node properties to Vue-compatible props.\r\n * - `className` array → `class` string\r\n * - `htmlFor` → `for`\r\n * - All other properties pass through as-is\r\n */\r\nfunction convertProps(properties: Record<string, unknown>): Record<string, unknown> {\r\n const props: Record<string, unknown> = {};\r\n\r\n for (const [key, value] of Object.entries(properties)) {\r\n if (key === 'className' && Array.isArray(value)) {\r\n props['class'] = value.join(' ');\r\n } else if (key === 'htmlFor') {\r\n props['for'] = value;\r\n } else {\r\n props[key] = value;\r\n }\r\n }\r\n\r\n return props;\r\n}\r\n\r\n/** Resolve the tag/component for an element node from the Components option. */\r\nfunction resolveTag(node: Element, components: Components): NonNullable<ComponentResolution> {\r\n const resolved = typeof components === 'function'\r\n ? components(node)\r\n : components[node.tagName];\r\n return resolved ?? node.tagName;\r\n}\r\n\r\n/**\r\n * Internal recursive converter. `path` is a dot-separated string identifying\r\n * the node's position in the tree (e.g. `\"0\"`, `\"0.1\"`, `\"0.1.2\"`).\r\n */\r\nfunction toVNodes(\r\n node: Nodes | Nodes[],\r\n components: Components,\r\n transition: TransitionConfig | undefined,\r\n path: string,\r\n): VNodeArrayChildren {\r\n if (Array.isArray(node)) {\r\n return node.flatMap((n, i) => toVNodes(n, components, transition, `${path}.${i}`));\r\n }\r\n\r\n switch (node.type) {\r\n case 'root':\r\n return node.children.flatMap((child, i) =>\r\n toVNodes(child, components, transition, String(i)),\r\n );\r\n\r\n case 'element': {\r\n const { properties = {}, children } = node;\r\n const tag = resolveTag(node, components);\r\n const props = convertProps(properties as Record<string, unknown>);\r\n const childVNodes: VNodeArrayChildren = children.flatMap((child, i) =>\r\n toVNodes(child, components, transition, `${path}.${i}`),\r\n );\r\n\r\n // Build the element VNode\r\n // Custom Vue components also receive the raw HAST `node` prop so they\r\n // can access the original element (e.g. to extract text content for\r\n // syntax highlighting or diagram rendering).\r\n const el = typeof tag === 'string'\r\n ? h(tag, props, childVNodes)\r\n : h(tag, { ...props, node }, { default: () => childVNodes });\r\n\r\n // Wrap in <Transition> if requested, using the tree path as a stable key\r\n if (transition !== undefined) {\r\n return [h(Transition, { key: path, ...transition }, { default: () => el })];\r\n }\r\n return [el];\r\n }\r\n\r\n case 'text':\r\n return [node.value];\r\n\r\n case 'comment':\r\n return [createCommentVNode(node.value)];\r\n\r\n default:\r\n return [];\r\n }\r\n}\r\n\r\n/**\r\n * Recursively convert a HAST node (or array of nodes) into Vue VNodeArrayChildren.\r\n *\r\n * @param node - Root HAST node or array of nodes to convert.\r\n * @param components - Custom component map or resolver function.\r\n * @param transition - Optional `<Transition>` config applied to every element node.\r\n */\r\nexport function hastToVNodes(\r\n node: Nodes | Nodes[],\r\n components: Components,\r\n transition?: TransitionConfig,\r\n): VNodeArrayChildren {\r\n return toVNodes(node, components, transition, '');\r\n}\r\n","import { computed, defineComponent, Fragment, h, markRaw } from 'vue';\r\nimport { parse } from '@mark-sorcery/markdown-parser';\r\nimport { hastToVNodes } from './hast-to-vnodes.ts';\r\nimport type { Components, MarkdownProps, ParseOptions, TransitionConfig } from './types.ts';\r\n\r\nexport const Markdown = defineComponent({\r\n name: 'Markdown',\r\n\r\n props: {\r\n markdown: {\r\n type: String,\r\n required: true,\r\n },\r\n options: {\r\n type: Object as () => ParseOptions,\r\n default: undefined,\r\n },\r\n components: {\r\n type: [Object, Function] as unknown as () => Components,\r\n default: () => ({}),\r\n },\r\n transition: {\r\n type: [Boolean, Object] as unknown as () => boolean | TransitionConfig,\r\n default: false,\r\n },\r\n } satisfies {\r\n [K in keyof MarkdownProps]-?: unknown;\r\n },\r\n\r\n setup(props) {\r\n const hast = computed(() => parse(props.markdown, props.options));\r\n\r\n return () => {\r\n const raw = props.components ?? {};\r\n // Function resolvers pass through; record values are wrapped in markRaw\r\n // so Vue doesn't make component objects reactive (perf warning prevention)\r\n const components: Components = typeof raw === 'function'\r\n ? raw\r\n : Object.fromEntries(\r\n Object.entries(raw).map(([k, v]) =>\r\n [k, typeof v === 'string' || v == null ? v : markRaw(v)],\r\n ),\r\n );\r\n\r\n const transitionConfig: TransitionConfig | undefined =\r\n props.transition === true\r\n ? {}\r\n : props.transition || undefined;\r\n\r\n const vnodes = hastToVNodes(hast.value, components, transitionConfig);\r\n return h(Fragment, vnodes);\r\n };\r\n },\r\n});\r\n"],"mappings":";;;;;;;;;;;AAWA,SAAS,aAAa,YAA8D;CAClF,MAAM,QAAiC,EAAE;AAEzC,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,CACnD,KAAI,QAAQ,eAAe,MAAM,QAAQ,MAAM,CAC7C,OAAM,WAAW,MAAM,KAAK,IAAI;UACvB,QAAQ,UACjB,OAAM,SAAS;KAEf,OAAM,OAAO;AAIjB,QAAO;;;AAIT,SAAS,WAAW,MAAe,YAA0D;AAI3F,SAHiB,OAAO,eAAe,aACnC,WAAW,KAAK,GAChB,WAAW,KAAK,aACD,KAAK;;;;;;AAO1B,SAAS,SACP,MACA,YACA,YACA,MACoB;AACpB,KAAI,MAAM,QAAQ,KAAK,CACrB,QAAO,KAAK,SAAS,GAAG,MAAM,SAAS,GAAG,YAAY,YAAY,GAAG,KAAK,GAAG,IAAI,CAAC;AAGpF,SAAQ,KAAK,MAAb;EACE,KAAK,OACH,QAAO,KAAK,SAAS,SAAS,OAAO,MACnC,SAAS,OAAO,YAAY,YAAY,OAAO,EAAE,CAAC,CACnD;EAEH,KAAK,WAAW;GACd,MAAM,EAAE,aAAa,EAAE,EAAE,aAAa;GACtC,MAAM,MAAM,WAAW,MAAM,WAAW;GACxC,MAAM,QAAQ,aAAa,WAAsC;GACjE,MAAM,cAAkC,SAAS,SAAS,OAAO,MAC/D,SAAS,OAAO,YAAY,YAAY,GAAG,KAAK,GAAG,IAAI,CACxD;GAMD,MAAM,KAAK,OAAO,QAAQ,sBACpB,KAAK,OAAO,YAAY,cACxB,KAAK;IAAE,GAAG;IAAO;IAAM,EAAE,EAAE,eAAe,aAAa,CAAC;AAG9D,OAAI,eAAe,OACjB,QAAO,YAAGA,gBAAY;IAAE,KAAK;IAAM,GAAG;IAAY,EAAE,EAAE,eAAe,IAAI,CAAC,CAAC;AAE7E,UAAO,CAAC,GAAG;;EAGb,KAAK,OACH,QAAO,CAAC,KAAK,MAAM;EAErB,KAAK,UACH,QAAO,6BAAoB,KAAK,MAAM,CAAC;EAEzC,QACE,QAAO,EAAE;;;;;;;;;;AAWf,SAAgB,aACd,MACA,YACA,YACoB;AACpB,QAAO,SAAS,MAAM,YAAY,YAAY,GAAG;;;;;AChGnD,MAAa,oCAA2B;CACtC,MAAM;CAEN,OAAO;EACL,UAAU;GACR,MAAM;GACN,UAAU;GACX;EACD,SAAS;GACP,MAAM;GACN,SAAS;GACV;EACD,YAAY;GACV,MAAM,CAAC,QAAQ,SAAS;GACxB,gBAAgB,EAAE;GACnB;EACD,YAAY;GACV,MAAM,CAAC,SAAS,OAAO;GACvB,SAAS;GACV;EACF;CAID,MAAM,OAAO;EACX,MAAM,wEAA4B,MAAM,UAAU,MAAM,QAAQ,CAAC;AAEjE,eAAa;GACX,MAAM,MAAM,MAAM,cAAc,EAAE;GAGlC,MAAM,aAAyB,OAAO,QAAQ,aAC1C,MACA,OAAO,YACL,OAAO,QAAQ,IAAI,CAAC,KAAK,CAAC,GAAG,OAC3B,CAAC,GAAG,OAAO,MAAM,YAAY,KAAK,OAAO,qBAAY,EAAE,CAAC,CACzD,CACF;GAEL,MAAM,mBACJ,MAAM,eAAe,OACjB,EAAE,GACF,MAAM,cAAc;AAG1B,qBAASC,cADM,aAAa,KAAK,OAAO,YAAY,iBAAiB,CAC3C;;;CAG/B,CAAC"}
1
+ {"version":3,"file":"index.cjs","names":["Fragment"],"sources":["../src/hast-to-vnodes.ts","../src/Markdown.ts"],"sourcesContent":["import { h, createCommentVNode } from 'vue';\nimport type { Element, Nodes } from 'hast';\nimport type { VNodeArrayChildren } from 'vue';\nimport type { ComponentResolution, Components } from './types.ts';\n\nfunction createNodeKey(node: Element, path: string): string {\n const start = node.position?.start?.offset;\n const end = node.position?.end?.offset;\n\n if (typeof start === 'number' && typeof end === 'number') {\n return `${node.tagName}:${start}-${end}`;\n }\n\n if (typeof start === 'number') {\n return `${node.tagName}:${start}`;\n }\n\n return path;\n}\n\n/**\n * Convert HAST node properties to Vue-compatible props.\n * - `className` array → `class` string\n * - `htmlFor` → `for`\n * - All other properties pass through as-is\n */\nfunction convertProps(properties: Record<string, unknown>): Record<string, unknown> {\n const props: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(properties)) {\n if (key === 'className' && Array.isArray(value)) {\n props['class'] = value.join(' ');\n } else if (key === 'htmlFor') {\n props['for'] = value;\n } else {\n props[key] = value;\n }\n }\n\n return props;\n}\n\n/** Resolve the tag/component for an element node from the Components option. */\nfunction resolveTag(node: Element, components: Components): NonNullable<ComponentResolution> {\n const resolved = typeof components === 'function'\n ? components(node)\n : components[node.tagName];\n return resolved ?? node.tagName;\n}\n\n/**\n * Internal recursive converter. `path` is a dot-separated string identifying\n * the node's position in the tree (e.g. `\"0\"`, `\"0.1\"`, `\"0.1.2\"`).\n */\nfunction toVNodes(\n node: Nodes | Nodes[],\n components: Components,\n path: string,\n): VNodeArrayChildren {\n if (Array.isArray(node)) {\n return node.flatMap((n, i) => toVNodes(n, components, `${path}.${i}`));\n }\n\n switch (node.type) {\n case 'root':\n return node.children.flatMap((child, i) =>\n toVNodes(child, components, String(i)),\n );\n\n case 'element': {\n const { properties = {}, children } = node;\n const tag = resolveTag(node, components);\n const nodeKey = createNodeKey(node, path);\n const props = convertProps(properties as Record<string, unknown>);\n const childVNodes: VNodeArrayChildren = children.flatMap((child, i) =>\n toVNodes(child, components, `${path}.${i}`),\n );\n\n // Build the element VNode\n // Custom Vue components also receive the raw HAST `node` prop so they\n // can access the original element (e.g. to extract text content for\n // syntax highlighting or diagram rendering).\n const el = typeof tag === 'string'\n ? h(tag, { ...props, key: nodeKey }, childVNodes)\n : h(tag, { ...props, key: nodeKey, node }, { default: () => childVNodes });\n\n return [el];\n }\n\n case 'text':\n return [node.value];\n\n case 'comment':\n return [createCommentVNode(node.value)];\n\n default:\n return [];\n }\n}\n\n/**\n * Recursively convert a HAST node (or array of nodes) into Vue VNodeArrayChildren.\n *\n * @param node - Root HAST node or array of nodes to convert.\n * @param components - Custom component map or resolver function.\n * @param transition - Optional `<Transition>` config applied to every element node.\n */\nexport function hastToVNodes(\n node: Nodes | Nodes[],\n components: Components,\n): VNodeArrayChildren {\n return toVNodes(node, components, '');\n}\n","import { computed, defineComponent, Fragment, h, markRaw, shallowRef, watchEffect } from 'vue';\nimport { createMemory, createProcessor, parse } from '@mark-sorcery/markdown-parser';\nimport { hastToVNodes } from './hast-to-vnodes.ts';\nimport type {\n Components,\n MarkdownProcessor,\n MarkdownOptions,\n MarkdownProps,\n ParseMemory,\n} from './types.ts';\n\nexport const Markdown = defineComponent({\n name: 'Markdown',\n\n props: {\n markdown: {\n type: String,\n required: true,\n },\n options: {\n type: Object as () => MarkdownOptions,\n default: undefined,\n },\n plugins: {\n type: Array as () => MarkdownProps['plugins'],\n default: undefined,\n },\n stream: {\n type: Boolean,\n default: false,\n },\n components: {\n type: [Object, Function] as unknown as () => Components,\n default: () => ({}),\n },\n } satisfies {\n [K in keyof MarkdownProps]-?: unknown;\n },\n\n setup(props) {\n const getMarkdown = () => props.markdown ?? '';\n\n const processor = computed<MarkdownProcessor>(() => {\n const options = props.options;\n const propPlugins = props.plugins ?? [];\n\n return createProcessor({\n ...options,\n plugins: propPlugins,\n });\n });\n\n const hast = shallowRef(parse(processor.value, getMarkdown()));\n let streamMemory: ParseMemory | undefined;\n let activeProcessor: MarkdownProcessor | undefined;\n\n watchEffect(() => {\n const currentProcessor = processor.value;\n const markdown = getMarkdown();\n\n if (activeProcessor && activeProcessor !== currentProcessor) {\n streamMemory = props.stream ? createMemory() : undefined;\n }\n\n activeProcessor = currentProcessor;\n\n if (props.stream) {\n streamMemory ??= createMemory();\n hast.value = parse(currentProcessor, markdown, streamMemory);\n return;\n }\n\n if (streamMemory) {\n streamMemory.flush = true;\n streamMemory = undefined;\n return;\n }\n\n hast.value = parse(currentProcessor, markdown);\n });\n\n return () => {\n const raw = props.components ?? {};\n // Function resolvers pass through; record values are wrapped in markRaw\n // so Vue doesn't make component objects reactive (perf warning prevention)\n const components: Components = typeof raw === 'function'\n ? raw\n : Object.fromEntries(\n Object.entries(raw).map(([k, v]) =>\n [k, typeof v === 'string' || v == null ? v : markRaw(v)],\n ),\n );\n const vnodes = hastToVNodes(hast.value, components);\n return h(Fragment, vnodes);\n };\n },\n});\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as vue from "vue";
2
- import { BaseTransitionProps, Component, VNodeArrayChildren } from "vue";
3
- import { ParseOptions } from "@mark-sorcery/markdown-parser";
2
+ import { Component, VNodeArrayChildren } from "vue";
3
+ import * as _mark_sorcery_markdown_parser0 from "@mark-sorcery/markdown-parser";
4
+ import { CorePluginOptions, MarkdownProcessor, ParseMemory, ParseOptions, ParserPlugin, createCorePlugin } from "@mark-sorcery/markdown-parser";
4
5
 
5
6
  //#region ../../node_modules/.bun/@types+unist@3.0.3/node_modules/@types/unist/index.d.ts
6
7
  // ## Interfaces
@@ -317,36 +318,7 @@ interface Text extends Literal {
317
318
  interface TextData extends Data {}
318
319
  //#endregion
319
320
  //#region src/types.d.ts
320
- /**
321
- * Configuration forwarded to Vue's `<Transition>` component.
322
- * All props are optional; when `transition: true` is used on `<Markdown>`,
323
- * defaults to `{}` which activates Vue's default `v-enter-*` classes.
324
- *
325
- * @see https://vuejs.org/api/built-in-components.html#transition
326
- */
327
- interface TransitionConfig {
328
- name?: string;
329
- css?: boolean;
330
- appear?: boolean;
331
- mode?: 'in-out' | 'out-in' | 'default';
332
- enterFromClass?: string;
333
- enterActiveClass?: string;
334
- enterToClass?: string;
335
- leaveFromClass?: string;
336
- leaveActiveClass?: string;
337
- leaveToClass?: string;
338
- appearFromClass?: string;
339
- appearActiveClass?: string;
340
- appearToClass?: string;
341
- onBeforeEnter?: BaseTransitionProps['onBeforeEnter'];
342
- onEnter?: BaseTransitionProps['onEnter'];
343
- onAfterEnter?: BaseTransitionProps['onAfterEnter'];
344
- onEnterCancelled?: BaseTransitionProps['onEnterCancelled'];
345
- onBeforeLeave?: BaseTransitionProps['onBeforeLeave'];
346
- onLeave?: BaseTransitionProps['onLeave'];
347
- onAfterLeave?: BaseTransitionProps['onAfterLeave'];
348
- onLeaveCancelled?: BaseTransitionProps['onLeaveCancelled'];
349
- }
321
+ type MarkdownOptions = Omit<ParseOptions, 'plugins'>;
350
322
  /** A resolved component value: a Vue component, a tag string, or null/undefined to fall back to the default element. */
351
323
  type ComponentResolution = Component | string | null | undefined;
352
324
  /**
@@ -382,22 +354,14 @@ interface NodeProps {
382
354
  interface MarkdownProps {
383
355
  /** The markdown string to render. May be a partial/streaming string. */
384
356
  markdown: string;
385
- /** Options forwarded to the underlying markdown parser. */
386
- options?: ParseOptions;
357
+ /** Options forwarded to the underlying markdown processor factory, excluding plugins. */
358
+ options?: MarkdownOptions;
359
+ /** Plugins forwarded to the underlying markdown processor factory. */
360
+ plugins?: ParserPlugin[];
361
+ /** Enables parse memory so growing markdown streams preserve confirmed blocks. */
362
+ stream?: boolean;
387
363
  /** Custom Vue components or tags to use in place of default HTML elements. */
388
364
  components?: Components;
389
- /**
390
- * Optional Vue `<Transition>` configuration applied to every rendered element node.
391
- *
392
- * - `true` — enables transitions with Vue's default classes (`v-enter-*`).
393
- * - `TransitionConfig` object — forwarded as props to each `<Transition>` wrapper.
394
- * - `false` / `undefined` — no transitions (default).
395
- *
396
- * @example
397
- * // Basic fade (add CSS: .fade-enter-active { transition: opacity 0.3s } .fade-enter-from { opacity: 0 })
398
- * :transition="{ name: 'fade', appear: true }"
399
- */
400
- transition?: boolean | TransitionConfig;
401
365
  }
402
366
  //#endregion
403
367
  //#region src/Markdown.d.ts
@@ -407,17 +371,21 @@ declare const Markdown: vue.DefineComponent<vue.ExtractPropTypes<{
407
371
  required: boolean;
408
372
  };
409
373
  options: {
410
- type: () => ParseOptions;
374
+ type: () => MarkdownOptions;
375
+ default: undefined;
376
+ };
377
+ plugins: {
378
+ type: () => MarkdownProps["plugins"];
411
379
  default: undefined;
412
380
  };
381
+ stream: {
382
+ type: BooleanConstructor;
383
+ default: boolean;
384
+ };
413
385
  components: {
414
386
  type: () => Components;
415
387
  default: () => {};
416
388
  };
417
- transition: {
418
- type: () => boolean | TransitionConfig;
419
- default: boolean;
420
- };
421
389
  }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
422
390
  [key: string]: any;
423
391
  }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
@@ -426,21 +394,26 @@ declare const Markdown: vue.DefineComponent<vue.ExtractPropTypes<{
426
394
  required: boolean;
427
395
  };
428
396
  options: {
429
- type: () => ParseOptions;
397
+ type: () => MarkdownOptions;
398
+ default: undefined;
399
+ };
400
+ plugins: {
401
+ type: () => MarkdownProps["plugins"];
430
402
  default: undefined;
431
403
  };
404
+ stream: {
405
+ type: BooleanConstructor;
406
+ default: boolean;
407
+ };
432
408
  components: {
433
409
  type: () => Components;
434
410
  default: () => {};
435
411
  };
436
- transition: {
437
- type: () => boolean | TransitionConfig;
438
- default: boolean;
439
- };
440
412
  }>> & Readonly<{}>, {
441
- options: ParseOptions;
413
+ options: MarkdownOptions;
414
+ plugins: _mark_sorcery_markdown_parser0.ParserPlugin[] | undefined;
415
+ stream: boolean;
442
416
  components: Components;
443
- transition: boolean | TransitionConfig;
444
417
  }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
445
418
  //#endregion
446
419
  //#region src/hast-to-vnodes.d.ts
@@ -451,7 +424,7 @@ declare const Markdown: vue.DefineComponent<vue.ExtractPropTypes<{
451
424
  * @param components - Custom component map or resolver function.
452
425
  * @param transition - Optional `<Transition>` config applied to every element node.
453
426
  */
454
- declare function hastToVNodes(node: Nodes | Nodes[], components: Components, transition?: TransitionConfig): VNodeArrayChildren;
427
+ declare function hastToVNodes(node: Nodes | Nodes[], components: Components): VNodeArrayChildren;
455
428
  //#endregion
456
- export { type Components, Markdown, type MarkdownProps, type NodeProps, type ParseOptions, type TransitionConfig, hastToVNodes };
429
+ export { type Components, type CorePluginOptions, Markdown, type MarkdownOptions, type MarkdownProcessor, type MarkdownProps, type NodeProps, type ParseMemory, type ParseOptions, type ParserPlugin, createCorePlugin, hastToVNodes };
457
430
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as vue from "vue";
2
- import { BaseTransitionProps, Component, VNodeArrayChildren } from "vue";
3
- import { ParseOptions } from "@mark-sorcery/markdown-parser";
2
+ import { Component, VNodeArrayChildren } from "vue";
3
+ import * as _mark_sorcery_markdown_parser0 from "@mark-sorcery/markdown-parser";
4
+ import { CorePluginOptions, MarkdownProcessor, ParseMemory, ParseOptions, ParserPlugin, createCorePlugin } from "@mark-sorcery/markdown-parser";
4
5
 
5
6
  //#region ../../node_modules/.bun/@types+unist@3.0.3/node_modules/@types/unist/index.d.ts
6
7
  // ## Interfaces
@@ -317,36 +318,7 @@ interface Text extends Literal {
317
318
  interface TextData extends Data {}
318
319
  //#endregion
319
320
  //#region src/types.d.ts
320
- /**
321
- * Configuration forwarded to Vue's `<Transition>` component.
322
- * All props are optional; when `transition: true` is used on `<Markdown>`,
323
- * defaults to `{}` which activates Vue's default `v-enter-*` classes.
324
- *
325
- * @see https://vuejs.org/api/built-in-components.html#transition
326
- */
327
- interface TransitionConfig {
328
- name?: string;
329
- css?: boolean;
330
- appear?: boolean;
331
- mode?: 'in-out' | 'out-in' | 'default';
332
- enterFromClass?: string;
333
- enterActiveClass?: string;
334
- enterToClass?: string;
335
- leaveFromClass?: string;
336
- leaveActiveClass?: string;
337
- leaveToClass?: string;
338
- appearFromClass?: string;
339
- appearActiveClass?: string;
340
- appearToClass?: string;
341
- onBeforeEnter?: BaseTransitionProps['onBeforeEnter'];
342
- onEnter?: BaseTransitionProps['onEnter'];
343
- onAfterEnter?: BaseTransitionProps['onAfterEnter'];
344
- onEnterCancelled?: BaseTransitionProps['onEnterCancelled'];
345
- onBeforeLeave?: BaseTransitionProps['onBeforeLeave'];
346
- onLeave?: BaseTransitionProps['onLeave'];
347
- onAfterLeave?: BaseTransitionProps['onAfterLeave'];
348
- onLeaveCancelled?: BaseTransitionProps['onLeaveCancelled'];
349
- }
321
+ type MarkdownOptions = Omit<ParseOptions, 'plugins'>;
350
322
  /** A resolved component value: a Vue component, a tag string, or null/undefined to fall back to the default element. */
351
323
  type ComponentResolution = Component | string | null | undefined;
352
324
  /**
@@ -382,22 +354,14 @@ interface NodeProps {
382
354
  interface MarkdownProps {
383
355
  /** The markdown string to render. May be a partial/streaming string. */
384
356
  markdown: string;
385
- /** Options forwarded to the underlying markdown parser. */
386
- options?: ParseOptions;
357
+ /** Options forwarded to the underlying markdown processor factory, excluding plugins. */
358
+ options?: MarkdownOptions;
359
+ /** Plugins forwarded to the underlying markdown processor factory. */
360
+ plugins?: ParserPlugin[];
361
+ /** Enables parse memory so growing markdown streams preserve confirmed blocks. */
362
+ stream?: boolean;
387
363
  /** Custom Vue components or tags to use in place of default HTML elements. */
388
364
  components?: Components;
389
- /**
390
- * Optional Vue `<Transition>` configuration applied to every rendered element node.
391
- *
392
- * - `true` — enables transitions with Vue's default classes (`v-enter-*`).
393
- * - `TransitionConfig` object — forwarded as props to each `<Transition>` wrapper.
394
- * - `false` / `undefined` — no transitions (default).
395
- *
396
- * @example
397
- * // Basic fade (add CSS: .fade-enter-active { transition: opacity 0.3s } .fade-enter-from { opacity: 0 })
398
- * :transition="{ name: 'fade', appear: true }"
399
- */
400
- transition?: boolean | TransitionConfig;
401
365
  }
402
366
  //#endregion
403
367
  //#region src/Markdown.d.ts
@@ -407,17 +371,21 @@ declare const Markdown: vue.DefineComponent<vue.ExtractPropTypes<{
407
371
  required: boolean;
408
372
  };
409
373
  options: {
410
- type: () => ParseOptions;
374
+ type: () => MarkdownOptions;
375
+ default: undefined;
376
+ };
377
+ plugins: {
378
+ type: () => MarkdownProps["plugins"];
411
379
  default: undefined;
412
380
  };
381
+ stream: {
382
+ type: BooleanConstructor;
383
+ default: boolean;
384
+ };
413
385
  components: {
414
386
  type: () => Components;
415
387
  default: () => {};
416
388
  };
417
- transition: {
418
- type: () => boolean | TransitionConfig;
419
- default: boolean;
420
- };
421
389
  }>, () => vue.VNode<vue.RendererNode, vue.RendererElement, {
422
390
  [key: string]: any;
423
391
  }>, {}, {}, {}, vue.ComponentOptionsMixin, vue.ComponentOptionsMixin, {}, string, vue.PublicProps, Readonly<vue.ExtractPropTypes<{
@@ -426,21 +394,26 @@ declare const Markdown: vue.DefineComponent<vue.ExtractPropTypes<{
426
394
  required: boolean;
427
395
  };
428
396
  options: {
429
- type: () => ParseOptions;
397
+ type: () => MarkdownOptions;
398
+ default: undefined;
399
+ };
400
+ plugins: {
401
+ type: () => MarkdownProps["plugins"];
430
402
  default: undefined;
431
403
  };
404
+ stream: {
405
+ type: BooleanConstructor;
406
+ default: boolean;
407
+ };
432
408
  components: {
433
409
  type: () => Components;
434
410
  default: () => {};
435
411
  };
436
- transition: {
437
- type: () => boolean | TransitionConfig;
438
- default: boolean;
439
- };
440
412
  }>> & Readonly<{}>, {
441
- options: ParseOptions;
413
+ options: MarkdownOptions;
414
+ plugins: _mark_sorcery_markdown_parser0.ParserPlugin[] | undefined;
415
+ stream: boolean;
442
416
  components: Components;
443
- transition: boolean | TransitionConfig;
444
417
  }, {}, {}, {}, string, vue.ComponentProvideOptions, true, {}, any>;
445
418
  //#endregion
446
419
  //#region src/hast-to-vnodes.d.ts
@@ -451,7 +424,7 @@ declare const Markdown: vue.DefineComponent<vue.ExtractPropTypes<{
451
424
  * @param components - Custom component map or resolver function.
452
425
  * @param transition - Optional `<Transition>` config applied to every element node.
453
426
  */
454
- declare function hastToVNodes(node: Nodes | Nodes[], components: Components, transition?: TransitionConfig): VNodeArrayChildren;
427
+ declare function hastToVNodes(node: Nodes | Nodes[], components: Components): VNodeArrayChildren;
455
428
  //#endregion
456
- export { type Components, Markdown, type MarkdownProps, type NodeProps, type ParseOptions, type TransitionConfig, hastToVNodes };
429
+ export { type Components, type CorePluginOptions, Markdown, type MarkdownOptions, type MarkdownProcessor, type MarkdownProps, type NodeProps, type ParseMemory, type ParseOptions, type ParserPlugin, createCorePlugin, hastToVNodes };
457
430
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,7 +1,14 @@
1
- import { Fragment, Transition, computed, createCommentVNode, defineComponent, h, markRaw } from "vue";
2
- import { parse } from "@mark-sorcery/markdown-parser";
1
+ import { Fragment, computed, createCommentVNode, defineComponent, h, markRaw, shallowRef, watchEffect } from "vue";
2
+ import { createCorePlugin, createMemory, createProcessor, parse } from "@mark-sorcery/markdown-parser";
3
3
 
4
4
  //#region src/hast-to-vnodes.ts
5
+ function createNodeKey(node, path) {
6
+ const start = node.position?.start?.offset;
7
+ const end = node.position?.end?.offset;
8
+ if (typeof start === "number" && typeof end === "number") return `${node.tagName}:${start}-${end}`;
9
+ if (typeof start === "number") return `${node.tagName}:${start}`;
10
+ return path;
11
+ }
5
12
  /**
6
13
  * Convert HAST node properties to Vue-compatible props.
7
14
  * - `className` array → `class` string
@@ -23,24 +30,24 @@ function resolveTag(node, components) {
23
30
  * Internal recursive converter. `path` is a dot-separated string identifying
24
31
  * the node's position in the tree (e.g. `"0"`, `"0.1"`, `"0.1.2"`).
25
32
  */
26
- function toVNodes(node, components, transition, path) {
27
- if (Array.isArray(node)) return node.flatMap((n, i) => toVNodes(n, components, transition, `${path}.${i}`));
33
+ function toVNodes(node, components, path) {
34
+ if (Array.isArray(node)) return node.flatMap((n, i) => toVNodes(n, components, `${path}.${i}`));
28
35
  switch (node.type) {
29
- case "root": return node.children.flatMap((child, i) => toVNodes(child, components, transition, String(i)));
36
+ case "root": return node.children.flatMap((child, i) => toVNodes(child, components, String(i)));
30
37
  case "element": {
31
38
  const { properties = {}, children } = node;
32
39
  const tag = resolveTag(node, components);
40
+ const nodeKey = createNodeKey(node, path);
33
41
  const props = convertProps(properties);
34
- const childVNodes = children.flatMap((child, i) => toVNodes(child, components, transition, `${path}.${i}`));
35
- const el = typeof tag === "string" ? h(tag, props, childVNodes) : h(tag, {
42
+ const childVNodes = children.flatMap((child, i) => toVNodes(child, components, `${path}.${i}`));
43
+ return [typeof tag === "string" ? h(tag, {
44
+ ...props,
45
+ key: nodeKey
46
+ }, childVNodes) : h(tag, {
36
47
  ...props,
48
+ key: nodeKey,
37
49
  node
38
- }, { default: () => childVNodes });
39
- if (transition !== void 0) return [h(Transition, {
40
- key: path,
41
- ...transition
42
- }, { default: () => el })];
43
- return [el];
50
+ }, { default: () => childVNodes })];
44
51
  }
45
52
  case "text": return [node.value];
46
53
  case "comment": return [createCommentVNode(node.value)];
@@ -54,8 +61,8 @@ function toVNodes(node, components, transition, path) {
54
61
  * @param components - Custom component map or resolver function.
55
62
  * @param transition - Optional `<Transition>` config applied to every element node.
56
63
  */
57
- function hastToVNodes(node, components, transition) {
58
- return toVNodes(node, components, transition, "");
64
+ function hastToVNodes(node, components) {
65
+ return toVNodes(node, components, "");
59
66
  }
60
67
 
61
68
  //#endregion
@@ -71,26 +78,57 @@ const Markdown = defineComponent({
71
78
  type: Object,
72
79
  default: void 0
73
80
  },
81
+ plugins: {
82
+ type: Array,
83
+ default: void 0
84
+ },
85
+ stream: {
86
+ type: Boolean,
87
+ default: false
88
+ },
74
89
  components: {
75
90
  type: [Object, Function],
76
91
  default: () => ({})
77
- },
78
- transition: {
79
- type: [Boolean, Object],
80
- default: false
81
92
  }
82
93
  },
83
94
  setup(props) {
84
- const hast = computed(() => parse(props.markdown, props.options));
95
+ const getMarkdown = () => props.markdown ?? "";
96
+ const processor = computed(() => {
97
+ const options = props.options;
98
+ const propPlugins = props.plugins ?? [];
99
+ return createProcessor({
100
+ ...options,
101
+ plugins: propPlugins
102
+ });
103
+ });
104
+ const hast = shallowRef(parse(processor.value, getMarkdown()));
105
+ let streamMemory;
106
+ let activeProcessor;
107
+ watchEffect(() => {
108
+ const currentProcessor = processor.value;
109
+ const markdown = getMarkdown();
110
+ if (activeProcessor && activeProcessor !== currentProcessor) streamMemory = props.stream ? createMemory() : void 0;
111
+ activeProcessor = currentProcessor;
112
+ if (props.stream) {
113
+ streamMemory ??= createMemory();
114
+ hast.value = parse(currentProcessor, markdown, streamMemory);
115
+ return;
116
+ }
117
+ if (streamMemory) {
118
+ streamMemory.flush = true;
119
+ streamMemory = void 0;
120
+ return;
121
+ }
122
+ hast.value = parse(currentProcessor, markdown);
123
+ });
85
124
  return () => {
86
125
  const raw = props.components ?? {};
87
126
  const components = typeof raw === "function" ? raw : Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, typeof v === "string" || v == null ? v : markRaw(v)]));
88
- const transitionConfig = props.transition === true ? {} : props.transition || void 0;
89
- return h(Fragment, hastToVNodes(hast.value, components, transitionConfig));
127
+ return h(Fragment, hastToVNodes(hast.value, components));
90
128
  };
91
129
  }
92
130
  });
93
131
 
94
132
  //#endregion
95
- export { Markdown, hastToVNodes };
133
+ export { Markdown, createCorePlugin, hastToVNodes };
96
134
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/hast-to-vnodes.ts","../src/Markdown.ts"],"sourcesContent":["import { h, createCommentVNode, Transition } from 'vue';\r\nimport type { Element, Nodes } from 'hast';\r\nimport type { VNodeArrayChildren } from 'vue';\r\nimport type { ComponentResolution, Components, TransitionConfig } from './types.ts';\r\n\r\n/**\r\n * Convert HAST node properties to Vue-compatible props.\r\n * - `className` array → `class` string\r\n * - `htmlFor` → `for`\r\n * - All other properties pass through as-is\r\n */\r\nfunction convertProps(properties: Record<string, unknown>): Record<string, unknown> {\r\n const props: Record<string, unknown> = {};\r\n\r\n for (const [key, value] of Object.entries(properties)) {\r\n if (key === 'className' && Array.isArray(value)) {\r\n props['class'] = value.join(' ');\r\n } else if (key === 'htmlFor') {\r\n props['for'] = value;\r\n } else {\r\n props[key] = value;\r\n }\r\n }\r\n\r\n return props;\r\n}\r\n\r\n/** Resolve the tag/component for an element node from the Components option. */\r\nfunction resolveTag(node: Element, components: Components): NonNullable<ComponentResolution> {\r\n const resolved = typeof components === 'function'\r\n ? components(node)\r\n : components[node.tagName];\r\n return resolved ?? node.tagName;\r\n}\r\n\r\n/**\r\n * Internal recursive converter. `path` is a dot-separated string identifying\r\n * the node's position in the tree (e.g. `\"0\"`, `\"0.1\"`, `\"0.1.2\"`).\r\n */\r\nfunction toVNodes(\r\n node: Nodes | Nodes[],\r\n components: Components,\r\n transition: TransitionConfig | undefined,\r\n path: string,\r\n): VNodeArrayChildren {\r\n if (Array.isArray(node)) {\r\n return node.flatMap((n, i) => toVNodes(n, components, transition, `${path}.${i}`));\r\n }\r\n\r\n switch (node.type) {\r\n case 'root':\r\n return node.children.flatMap((child, i) =>\r\n toVNodes(child, components, transition, String(i)),\r\n );\r\n\r\n case 'element': {\r\n const { properties = {}, children } = node;\r\n const tag = resolveTag(node, components);\r\n const props = convertProps(properties as Record<string, unknown>);\r\n const childVNodes: VNodeArrayChildren = children.flatMap((child, i) =>\r\n toVNodes(child, components, transition, `${path}.${i}`),\r\n );\r\n\r\n // Build the element VNode\r\n // Custom Vue components also receive the raw HAST `node` prop so they\r\n // can access the original element (e.g. to extract text content for\r\n // syntax highlighting or diagram rendering).\r\n const el = typeof tag === 'string'\r\n ? h(tag, props, childVNodes)\r\n : h(tag, { ...props, node }, { default: () => childVNodes });\r\n\r\n // Wrap in <Transition> if requested, using the tree path as a stable key\r\n if (transition !== undefined) {\r\n return [h(Transition, { key: path, ...transition }, { default: () => el })];\r\n }\r\n return [el];\r\n }\r\n\r\n case 'text':\r\n return [node.value];\r\n\r\n case 'comment':\r\n return [createCommentVNode(node.value)];\r\n\r\n default:\r\n return [];\r\n }\r\n}\r\n\r\n/**\r\n * Recursively convert a HAST node (or array of nodes) into Vue VNodeArrayChildren.\r\n *\r\n * @param node - Root HAST node or array of nodes to convert.\r\n * @param components - Custom component map or resolver function.\r\n * @param transition - Optional `<Transition>` config applied to every element node.\r\n */\r\nexport function hastToVNodes(\r\n node: Nodes | Nodes[],\r\n components: Components,\r\n transition?: TransitionConfig,\r\n): VNodeArrayChildren {\r\n return toVNodes(node, components, transition, '');\r\n}\r\n","import { computed, defineComponent, Fragment, h, markRaw } from 'vue';\r\nimport { parse } from '@mark-sorcery/markdown-parser';\r\nimport { hastToVNodes } from './hast-to-vnodes.ts';\r\nimport type { Components, MarkdownProps, ParseOptions, TransitionConfig } from './types.ts';\r\n\r\nexport const Markdown = defineComponent({\r\n name: 'Markdown',\r\n\r\n props: {\r\n markdown: {\r\n type: String,\r\n required: true,\r\n },\r\n options: {\r\n type: Object as () => ParseOptions,\r\n default: undefined,\r\n },\r\n components: {\r\n type: [Object, Function] as unknown as () => Components,\r\n default: () => ({}),\r\n },\r\n transition: {\r\n type: [Boolean, Object] as unknown as () => boolean | TransitionConfig,\r\n default: false,\r\n },\r\n } satisfies {\r\n [K in keyof MarkdownProps]-?: unknown;\r\n },\r\n\r\n setup(props) {\r\n const hast = computed(() => parse(props.markdown, props.options));\r\n\r\n return () => {\r\n const raw = props.components ?? {};\r\n // Function resolvers pass through; record values are wrapped in markRaw\r\n // so Vue doesn't make component objects reactive (perf warning prevention)\r\n const components: Components = typeof raw === 'function'\r\n ? raw\r\n : Object.fromEntries(\r\n Object.entries(raw).map(([k, v]) =>\r\n [k, typeof v === 'string' || v == null ? v : markRaw(v)],\r\n ),\r\n );\r\n\r\n const transitionConfig: TransitionConfig | undefined =\r\n props.transition === true\r\n ? {}\r\n : props.transition || undefined;\r\n\r\n const vnodes = hastToVNodes(hast.value, components, transitionConfig);\r\n return h(Fragment, vnodes);\r\n };\r\n },\r\n});\r\n"],"mappings":";;;;;;;;;;AAWA,SAAS,aAAa,YAA8D;CAClF,MAAM,QAAiC,EAAE;AAEzC,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,CACnD,KAAI,QAAQ,eAAe,MAAM,QAAQ,MAAM,CAC7C,OAAM,WAAW,MAAM,KAAK,IAAI;UACvB,QAAQ,UACjB,OAAM,SAAS;KAEf,OAAM,OAAO;AAIjB,QAAO;;;AAIT,SAAS,WAAW,MAAe,YAA0D;AAI3F,SAHiB,OAAO,eAAe,aACnC,WAAW,KAAK,GAChB,WAAW,KAAK,aACD,KAAK;;;;;;AAO1B,SAAS,SACP,MACA,YACA,YACA,MACoB;AACpB,KAAI,MAAM,QAAQ,KAAK,CACrB,QAAO,KAAK,SAAS,GAAG,MAAM,SAAS,GAAG,YAAY,YAAY,GAAG,KAAK,GAAG,IAAI,CAAC;AAGpF,SAAQ,KAAK,MAAb;EACE,KAAK,OACH,QAAO,KAAK,SAAS,SAAS,OAAO,MACnC,SAAS,OAAO,YAAY,YAAY,OAAO,EAAE,CAAC,CACnD;EAEH,KAAK,WAAW;GACd,MAAM,EAAE,aAAa,EAAE,EAAE,aAAa;GACtC,MAAM,MAAM,WAAW,MAAM,WAAW;GACxC,MAAM,QAAQ,aAAa,WAAsC;GACjE,MAAM,cAAkC,SAAS,SAAS,OAAO,MAC/D,SAAS,OAAO,YAAY,YAAY,GAAG,KAAK,GAAG,IAAI,CACxD;GAMD,MAAM,KAAK,OAAO,QAAQ,WACtB,EAAE,KAAK,OAAO,YAAY,GAC1B,EAAE,KAAK;IAAE,GAAG;IAAO;IAAM,EAAE,EAAE,eAAe,aAAa,CAAC;AAG9D,OAAI,eAAe,OACjB,QAAO,CAAC,EAAE,YAAY;IAAE,KAAK;IAAM,GAAG;IAAY,EAAE,EAAE,eAAe,IAAI,CAAC,CAAC;AAE7E,UAAO,CAAC,GAAG;;EAGb,KAAK,OACH,QAAO,CAAC,KAAK,MAAM;EAErB,KAAK,UACH,QAAO,CAAC,mBAAmB,KAAK,MAAM,CAAC;EAEzC,QACE,QAAO,EAAE;;;;;;;;;;AAWf,SAAgB,aACd,MACA,YACA,YACoB;AACpB,QAAO,SAAS,MAAM,YAAY,YAAY,GAAG;;;;;AChGnD,MAAa,WAAW,gBAAgB;CACtC,MAAM;CAEN,OAAO;EACL,UAAU;GACR,MAAM;GACN,UAAU;GACX;EACD,SAAS;GACP,MAAM;GACN,SAAS;GACV;EACD,YAAY;GACV,MAAM,CAAC,QAAQ,SAAS;GACxB,gBAAgB,EAAE;GACnB;EACD,YAAY;GACV,MAAM,CAAC,SAAS,OAAO;GACvB,SAAS;GACV;EACF;CAID,MAAM,OAAO;EACX,MAAM,OAAO,eAAe,MAAM,MAAM,UAAU,MAAM,QAAQ,CAAC;AAEjE,eAAa;GACX,MAAM,MAAM,MAAM,cAAc,EAAE;GAGlC,MAAM,aAAyB,OAAO,QAAQ,aAC1C,MACA,OAAO,YACL,OAAO,QAAQ,IAAI,CAAC,KAAK,CAAC,GAAG,OAC3B,CAAC,GAAG,OAAO,MAAM,YAAY,KAAK,OAAO,IAAI,QAAQ,EAAE,CAAC,CACzD,CACF;GAEL,MAAM,mBACJ,MAAM,eAAe,OACjB,EAAE,GACF,MAAM,cAAc;AAG1B,UAAO,EAAE,UADM,aAAa,KAAK,OAAO,YAAY,iBAAiB,CAC3C;;;CAG/B,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/hast-to-vnodes.ts","../src/Markdown.ts"],"sourcesContent":["import { h, createCommentVNode } from 'vue';\nimport type { Element, Nodes } from 'hast';\nimport type { VNodeArrayChildren } from 'vue';\nimport type { ComponentResolution, Components } from './types.ts';\n\nfunction createNodeKey(node: Element, path: string): string {\n const start = node.position?.start?.offset;\n const end = node.position?.end?.offset;\n\n if (typeof start === 'number' && typeof end === 'number') {\n return `${node.tagName}:${start}-${end}`;\n }\n\n if (typeof start === 'number') {\n return `${node.tagName}:${start}`;\n }\n\n return path;\n}\n\n/**\n * Convert HAST node properties to Vue-compatible props.\n * - `className` array → `class` string\n * - `htmlFor` → `for`\n * - All other properties pass through as-is\n */\nfunction convertProps(properties: Record<string, unknown>): Record<string, unknown> {\n const props: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(properties)) {\n if (key === 'className' && Array.isArray(value)) {\n props['class'] = value.join(' ');\n } else if (key === 'htmlFor') {\n props['for'] = value;\n } else {\n props[key] = value;\n }\n }\n\n return props;\n}\n\n/** Resolve the tag/component for an element node from the Components option. */\nfunction resolveTag(node: Element, components: Components): NonNullable<ComponentResolution> {\n const resolved = typeof components === 'function'\n ? components(node)\n : components[node.tagName];\n return resolved ?? node.tagName;\n}\n\n/**\n * Internal recursive converter. `path` is a dot-separated string identifying\n * the node's position in the tree (e.g. `\"0\"`, `\"0.1\"`, `\"0.1.2\"`).\n */\nfunction toVNodes(\n node: Nodes | Nodes[],\n components: Components,\n path: string,\n): VNodeArrayChildren {\n if (Array.isArray(node)) {\n return node.flatMap((n, i) => toVNodes(n, components, `${path}.${i}`));\n }\n\n switch (node.type) {\n case 'root':\n return node.children.flatMap((child, i) =>\n toVNodes(child, components, String(i)),\n );\n\n case 'element': {\n const { properties = {}, children } = node;\n const tag = resolveTag(node, components);\n const nodeKey = createNodeKey(node, path);\n const props = convertProps(properties as Record<string, unknown>);\n const childVNodes: VNodeArrayChildren = children.flatMap((child, i) =>\n toVNodes(child, components, `${path}.${i}`),\n );\n\n // Build the element VNode\n // Custom Vue components also receive the raw HAST `node` prop so they\n // can access the original element (e.g. to extract text content for\n // syntax highlighting or diagram rendering).\n const el = typeof tag === 'string'\n ? h(tag, { ...props, key: nodeKey }, childVNodes)\n : h(tag, { ...props, key: nodeKey, node }, { default: () => childVNodes });\n\n return [el];\n }\n\n case 'text':\n return [node.value];\n\n case 'comment':\n return [createCommentVNode(node.value)];\n\n default:\n return [];\n }\n}\n\n/**\n * Recursively convert a HAST node (or array of nodes) into Vue VNodeArrayChildren.\n *\n * @param node - Root HAST node or array of nodes to convert.\n * @param components - Custom component map or resolver function.\n * @param transition - Optional `<Transition>` config applied to every element node.\n */\nexport function hastToVNodes(\n node: Nodes | Nodes[],\n components: Components,\n): VNodeArrayChildren {\n return toVNodes(node, components, '');\n}\n","import { computed, defineComponent, Fragment, h, markRaw, shallowRef, watchEffect } from 'vue';\nimport { createMemory, createProcessor, parse } from '@mark-sorcery/markdown-parser';\nimport { hastToVNodes } from './hast-to-vnodes.ts';\nimport type {\n Components,\n MarkdownProcessor,\n MarkdownOptions,\n MarkdownProps,\n ParseMemory,\n} from './types.ts';\n\nexport const Markdown = defineComponent({\n name: 'Markdown',\n\n props: {\n markdown: {\n type: String,\n required: true,\n },\n options: {\n type: Object as () => MarkdownOptions,\n default: undefined,\n },\n plugins: {\n type: Array as () => MarkdownProps['plugins'],\n default: undefined,\n },\n stream: {\n type: Boolean,\n default: false,\n },\n components: {\n type: [Object, Function] as unknown as () => Components,\n default: () => ({}),\n },\n } satisfies {\n [K in keyof MarkdownProps]-?: unknown;\n },\n\n setup(props) {\n const getMarkdown = () => props.markdown ?? '';\n\n const processor = computed<MarkdownProcessor>(() => {\n const options = props.options;\n const propPlugins = props.plugins ?? [];\n\n return createProcessor({\n ...options,\n plugins: propPlugins,\n });\n });\n\n const hast = shallowRef(parse(processor.value, getMarkdown()));\n let streamMemory: ParseMemory | undefined;\n let activeProcessor: MarkdownProcessor | undefined;\n\n watchEffect(() => {\n const currentProcessor = processor.value;\n const markdown = getMarkdown();\n\n if (activeProcessor && activeProcessor !== currentProcessor) {\n streamMemory = props.stream ? createMemory() : undefined;\n }\n\n activeProcessor = currentProcessor;\n\n if (props.stream) {\n streamMemory ??= createMemory();\n hast.value = parse(currentProcessor, markdown, streamMemory);\n return;\n }\n\n if (streamMemory) {\n streamMemory.flush = true;\n streamMemory = undefined;\n return;\n }\n\n hast.value = parse(currentProcessor, markdown);\n });\n\n return () => {\n const raw = props.components ?? {};\n // Function resolvers pass through; record values are wrapped in markRaw\n // so Vue doesn't make component objects reactive (perf warning prevention)\n const components: Components = typeof raw === 'function'\n ? raw\n : Object.fromEntries(\n Object.entries(raw).map(([k, v]) =>\n [k, typeof v === 'string' || v == null ? v : markRaw(v)],\n ),\n );\n const vnodes = hastToVNodes(hast.value, components);\n return h(Fragment, vnodes);\n };\n },\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mark-sorcery/vue",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Vue renderer for Mark Sorcery markdown.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -27,7 +27,7 @@
27
27
  "test": "vitest run"
28
28
  },
29
29
  "dependencies": {
30
- "@mark-sorcery/markdown-parser": "^0.1.0"
30
+ "@mark-sorcery/markdown-parser": "^0.2.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/hast": "^3.0.4",