zh CN Vue 3 渲染 - chiba233/yumeDSL GitHub Wiki

Vue 3 渲染

错误处理 | 待弃用 API

用于渲染 TextToken[] 树的即插即用递归 Vue 3 组件。

1. 配置解析器 (dsl.ts)

完整示例,包含:

  • 使用 createSimpleInlineHandlers 处理 bold、thin、underline、strike、code、center
  • 使用 parsePipeArgs 的 link handler
  • titledHandler 工厂用于 info/warning(inline+block+raw)
  • collapse(仅 block+raw)
  • 使用 parsePipeTextArgs 的 raw-code
  • date 和 fromNow inline handler
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,
    },
});

2. RichTextRenderer.vue

<!-- 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>

3. 使用方法

<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>

与 UI 库集成扩展

将标签映射到来自 Naive UI 等库的 Vue 组件:

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
};

对于需要动态渲染逻辑的标签,使用函数式组件:

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,
};
⚠️ **GitHub.com Fallback** ⚠️