Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ea681ff
chore: ignore .staged-delivery evidence dir
jamliaoo Jul 12, 2026
ebd5375
feat(vue): add default components mirroring the HTML adapter set
jamliaoo Jul 12, 2026
dc5540d
feat(vue): add markup-to-VNode converter with on* neutralization
jamliaoo Jul 12, 2026
76113f6
feat(vue): add createMarkdownToVueParser pipeline factory
jamliaoo Jul 12, 2026
17554ce
test: extend cross-adapter parity to a three-way React/HTML/Vue contract
jamliaoo Jul 12, 2026
881b5f7
feat(vue): add SeeMark convenience component
jamliaoo Jul 12, 2026
a5c419a
test(vue): add SSR + hydration round-trip coverage
jamliaoo Jul 12, 2026
01d23f6
feat(vue): expose @coseeing/see-mark/vue package entry
jamliaoo Jul 12, 2026
a0fd03f
chore: add vue-playground example as a live consumer for manual verif…
jamliaoo Jul 12, 2026
3bf5792
fix: load MathJax AsciiMath lazily so ESM-strict bundlers can import …
jamliaoo Jul 12, 2026
8201a00
chore(playground): vite config for symlinked CJS dep; disable AsciiMa…
jamliaoo Jul 12, 2026
e586be4
fix(vue): ship an ESM build to avoid the dual-Vue-runtime hazard
jamliaoo Jul 12, 2026
3d343ab
fix(playground): dedupe vue across the file:-linked layout
jamliaoo Jul 12, 2026
1147083
docs: document the Vue adapter, custom components, SSR, bundlers and …
jamliaoo Jul 12, 2026
07565c0
fix(vue): neutralize script execution, ref/key hijack, and SVG attr d…
jamliaoo Jul 12, 2026
3294009
revert(vue): drop the ESM build, keep /vue CommonJS-only like React/HTML
jamliaoo Jul 12, 2026
15f40b2
chore: pull vue-only playground out of the library PR
jamliaoo Jul 14, 2026
7ea34aa
docs: tighten Vue adapter comments to rationale over mechanism
jamliaoo Jul 14, 2026
b5723ac
fix(vue): default enableAsciimath to false for the Vue entry
jamliaoo Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,139 @@ interface Position {
}
```

## Vue

The `@coseeing/see-mark/vue` entry renders the same markdown pipeline to Vue 3
VNodes. Vue 3.2+ is required (declared as an optional peer dependency — React
users are unaffected).

### Usage

```js
import { createApp, defineComponent, h, ref } from 'vue';
import { SeeMark, createMarkdownToVueParser } from '@coseeing/see-mark/vue';

// Option 1: the <SeeMark> component (idiomatic for most apps)
const App = defineComponent({
setup() {
const source = ref('# Hello \\(a^2 + b^2 = c^2\\)');
return () => h(SeeMark, { source: source.value, options: OPTIONS });
},
});

// Option 2: the parser factory (mirrors createMarkdownToReactParser)
const parse = createMarkdownToVueParser({ options: OPTIONS });
const MyDoc = defineComponent({
props: { markdown: String },
setup(props) {
return () => h('article', parse(props.markdown));
},
});
```

`options` accepts the same table as the React parser (see above), with one
changed default: **`enableAsciimath` defaults to `false`** for the Vue entry
(React/HTML default it to `true`). AsciiMath's backtick delimiter otherwise
turns ordinary inline code into math, and its MathJax shim can't run under
Vite/esbuild (see Bundler compatibility). Pass `enableAsciimath: true` to opt
in where it works.
`createMarkdownToVueParser` returns a function producing a fresh VNode array —
call it inside a render function. `<SeeMark>` re-parses when `source` changes
and rebuilds its parser when `options`/`components` change; pass stable object
references for `options`/`components` (an inline literal re-creates the parser
on every parent render).

### Custom components

A custom component is an ordinary Vue 3 component (functional, `defineComponent`
or SFC). Payload arrives as props; children arrive through the **default slot**.
Declare the payload props you use (plus `position`) — undeclared payload keys
would otherwise fall through onto the root element as DOM attributes.

```js
const Alert = defineComponent({
props: {
variant: { type: String, default: '' },
title: { type: String, default: '' },
internalLinkId: { type: String, default: '' },
position: { type: Object, default: undefined },
},
setup(props, { slots }) {
return () =>
h('div', { class: `alert alert-${props.variant}` }, [
props.title ? h('strong', null, props.title) : null,
slots.default?.(),
]);
},
});

h(SeeMark, { source, options: OPTIONS, components: { alert: Alert } });
```

Errors thrown by a custom component surface at mount/render time and follow
Vue's normal error path (`app.config.errorHandler`), not at parse time.

### SSR

The Vue adapter is SSR-safe (`@vue/server-renderer` / Nuxt): MathJax runs in
the parsing stage, not in components. One caveat: MathJax assigns
globally-incrementing element IDs, so a server parse and a client parse of the
same document can disagree on `MJX-*` attribute values, which may surface as
attribute-level hydration warnings in dev builds. Structure and content
hydrate correctly.

### Bundler compatibility

Like the React and HTML entries, `/vue` ships a CommonJS bundle. Bundlers
(Vite, webpack) pre-bundle it to ESM automatically and share your app's single
`vue` copy; no extra config is needed for a normal registry install. Two
notes:

- **AsciiMath under ESM-strict bundlers (Vite, esbuild) is unavailable.**
MathJax implements AsciiMath through a MathJax-v2 legacy shim that cannot
run under strict mode, and Vite's dependency pre-bundling converts even
CommonJS deps to always-strict ESM. SeeMark loads it lazily, so importing
SeeMark and rendering LaTeX/Nemeth work everywhere; the Vue entry also
**defaults `enableAsciimath` to `false`**, so ordinary backtick content
(inline code) renders as code spans out of the box. Explicitly setting
`enableAsciimath: true` under such a bundler throws a descriptive error when
it hits AsciiMath. Server-side (Node/webpack) AsciiMath is unaffected — a
plain esbuild *build* (not Vite's dep pre-bundle) does not trip it either.
- **Linked-package development**: if you consume SeeMark via `npm link` /
`file:` during development, add `optimizeDeps.include: ['@coseeing/see-mark/vue']`
and `resolve.dedupe: ['vue']` to your Vite config — Vite skips CJS→ESM
pre-bundling for symlinked packages, and the linked repo carries its own
`node_modules/vue` (two Vue runtimes on one page silently break reactivity
across the component boundary). Registry installs need neither. In the
browser you may also need a `global` → `globalThis` define, since
mathjax-full references the Node.js `global` at module scope.

## Security / trust model

SeeMark adapters are **not sanitizers**. Raw HTML in the markdown source
passes through to the output — including `javascript:` URLs — matching the
React adapter's behavior. If you render untrusted markdown, sanitize it at the
source, or sanitize the HTML adapter's string output with DOMPurify via its
`sanitize` hook.

To keep the Vue adapter no more dangerous than the React one, raw passthrough
neutralizes three Vue-specific execution vectors — none of which affect your
own custom components (`@click`/`v-on`, `ref`, `key` on components all work
normally):

- **string `on*` attributes** (e.g. `onclick="..."`) are dropped. Vue would
otherwise attach them as live inline handlers; React ignores string
handlers.
- **raw `<script>` elements are dropped.** A `<script>` built as a VNode is
not parser-inserted and executes on mount; React's script elements are
inert and the HTML adapter's string output is inert under `innerHTML`.
- **the Vue-reserved props `ref`, `key`, `ref_for`, `ref_key`** are stripped,
so untrusted markup cannot register on the host component's `$refs` or
corrupt keyed diffing.

Everything else — including `javascript:` URLs and `<style>` — still passes
through verbatim; sanitize untrusted input at the source.

## Table of Contents

### Usage
Expand Down
Loading