en Vue 3 Rendering - chiba233/yumeDSL GitHub Wiki
Error Handling | Deprecated API
Drop-in recursive Vue 3 component for rendering TextToken[] trees.
Full example with:
- createSimpleInlineHandlers for bold, thin, underline, strike, code, center
- link handler with parsePipeArgs
- titledHandler factory for info/warning (inline+block+raw)
- collapse (block+raw only)
- raw-code with parsePipeTextArgs
- date and fromNow inline handlers
import {
createParser, createSimpleInlineHandlers,
parsePipeArgs, parsePipeTextArgs, createToken, materializeTextTokens,
type TagHandler, type TokenDraft,
} from "yume-dsl-rich-text";
const titledHandler = (type: string, defaultTitle: string): TagHandler => ({
inline: (tokens, ctx): TokenDraft => {
const args = parsePipeArgs(tokens, ctx);
if (args.parts.length <= 1) return {type, title: defaultTitle, value: args.materializedTokens(0)};
return {type, title: args.text(0), value: args.materializedTailTokens(1)};
},
block: (arg, tokens, ctx): TokenDraft => ({type, title: arg || defaultTitle, value: tokens}),
raw: (arg, content, ctx): TokenDraft => ({
type, title: arg || defaultTitle,
value: [createToken({type: "text", value: content}, undefined, ctx)],
}),
});
const collapseHandler: TagHandler = {
block: (arg, tokens, ctx): TokenDraft => ({type: "collapse", title: arg || "Details", value: tokens}),
raw: (arg, content, ctx): TokenDraft => ({
type: "collapse", title: arg || "Details",
value: [createToken({type: "text", value: content}, undefined, ctx)],
}),
};
const rawCodeHandler: TagHandler = {
raw: (arg, content, ctx): TokenDraft => {
const args = parsePipeTextArgs(arg || "", ctx);
return {type: "raw-code", lang: args.text(0) || "text", value: content};
},
};
const dateHandler: TagHandler = {
inline: (tokens, ctx): TokenDraft => {
const args = parsePipeArgs(tokens, ctx);
return {type: "date", value: args.text(0)};
},
};
const fromNowHandler: TagHandler = {
inline: (tokens, ctx): TokenDraft => {
const args = parsePipeArgs(tokens, ctx);
return {type: "fromNow", value: args.text(0)};
},
};
export const dsl = createParser({
handlers: {
...createSimpleInlineHandlers(["bold", "thin", "underline", "strike", "code", "center"]),
link: {
inline: (tokens, ctx): TokenDraft => {
const args = parsePipeArgs(tokens, ctx);
return {
type: "link",
url: args.text(0),
value: args.materializedTailTokens(1),
};
},
},
info: titledHandler("info", "Info"),
warning: titledHandler("warning", "Warning"),
collapse: collapseHandler,
"raw-code": rawCodeHandler,
date: dateHandler,
fromNow: fromNowHandler,
},
});<!-- src/components/RichTextRenderer.vue -->
<script setup lang="ts">
import type { TextToken } from "yume-dsl-rich-text";
import type { Component } from "vue";
defineProps<{
tokens: TextToken[];
}>();
/**
* Map DSL tag names to Vue components or HTML elements.
* Extend this object when you add new tags.
*/
const tagMap: Record<string, string | Component> = {
bold: "strong",
thin: "span",
underline: "u",
strike: "s",
code: "code",
center: "div",
link: "a",
info: "div",
warning: "div",
collapse: "details",
"raw-code": "pre",
date: "time",
fromNow: "time",
};
/** Return extra props for specific tags. */
function getComponentProps(token: TextToken): Record<string, unknown> {
if (token.type === "link") {
const href = normalizeUrl(String(token.url ?? "#"));
return {href, target: "_blank", rel: "noopener noreferrer"};
}
if (token.type === "info" || token.type === "warning") {
return {title: token.title};
}
if (token.type === "collapse") {
return {};
}
if (token.type === "date" || token.type === "fromNow") {
return {datetime: typeof token.value === "string" ? token.value : ""};
}
return {};
}
/** Return CSS class(es) for specific tags. */
function getComponentClass(token: TextToken): string | undefined {
if (token.type === "info") return "alert alert-info";
if (token.type === "warning") return "alert alert-warning";
if (token.type === "raw-code") return "code-block";
if (token.type === "center") return "text-center";
return undefined;
}
/** Basic URL sanitization -- only allow http/https. */
function normalizeUrl(raw: string): string {
try {
const url = new URL(raw, "https://example.com");
if (url.protocol === "http:" || url.protocol === "https:") return url.href;
} catch {
// invalid URL
}
return "#";
}
</script>
<template>
<template v-for="(token, idx) in tokens" :key="token.id ?? idx">
<!-- Plain text node -->
<span v-if="token.type === 'text' && typeof token.value === 'string'">{{ token.value }}</span>
<!-- Raw code: string value, no recursion -->
<component
v-else-if="token.type === 'raw-code' && typeof token.value === 'string'"
:is="tagMap['raw-code']"
:class="getComponentClass(token)"
v-bind="getComponentProps(token)"
>{{ token.value }}</component>
<!-- Everything else: recursive children -->
<component
v-else-if="tagMap[token.type]"
:is="tagMap[token.type]"
:class="getComponentClass(token)"
v-bind="getComponentProps(token)"
>
<RichTextRenderer
v-if="Array.isArray(token.value) && token.value.length"
:tokens="token.value"
/>
</component>
</template>
</template><script setup lang="ts">
import { dsl } from "@/dsl";
import RichTextRenderer from "@/components/RichTextRenderer.vue";
const source = "Hello $$bold(world)$$! Visit $$link(https://example.com | here)$$.";
const tokens = dsl.parse(source);
</script>
<template>
<RichTextRenderer :tokens="tokens" />
</template>Map tags to Vue components from libraries like Naive UI:
import { NAlert } from "naive-ui";
import CodeBlock from "@/components/CodeBlock.vue";
import CollapseWrapper from "@/components/CollapseWrapper.vue";
const tagMap: Record<string, string | Component> = {
// ... base mappings ...
info: NAlert, // renders <NAlert type="info">
warning: NAlert, // renders <NAlert type="warning">
"raw-code": CodeBlock, // syntax-highlighted code block
collapse: CollapseWrapper, // animated collapse component
};For tags that need dynamic rendering logic, use functional components:
import { h, type FunctionalComponent } from "vue";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
const DateTag: FunctionalComponent<{datetime: string}> = (props) => {
const d = dayjs(props.datetime);
return h("time", {datetime: props.datetime}, d.format("YYYY-MM-DD"));
};
const FromNowTag: FunctionalComponent<{datetime: string}> = (props) => {
return h("time", {datetime: props.datetime}, dayjs(props.datetime).fromNow());
};
const tagMap = {
date: DateTag,
fromNow: FromNowTag,
};