Skip to content

Latest commit

 

History

History
734 lines (616 loc) · 24.8 KB

File metadata and controls

734 lines (616 loc) · 24.8 KB
<style> @import url("https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&family=Noto+Serif+SC:wght@400;600;800&display=swap"); #ultimate-test-title { position: relative; margin: 1.2em 0; padding: 0.72em 1em; overflow: hidden; color: #fffdf7; font-family: "Ma Shan Zheng", "STKaiti", cursive; font-size: clamp(2rem, 7vw, 4.8rem); letter-spacing: 0.08em; text-align: center; text-shadow: 0 3px 18px rgba(20, 28, 26, 0.42); background: radial-gradient(circle at 20% 30%, rgba(255, 226, 144, 0.92), transparent 23%), linear-gradient(120deg, #2e7567, #945f85, #d97745, #2e7567); background-size: 220% 220%; border: 1px solid rgba(255, 255, 255, 0.42); border-radius: 24px; box-shadow: 0 20px 70px rgba(46, 117, 103, 0.24); animation: ultimate-title-background 9s ease-in-out infinite; } #ultimate-test-title::after { content: ""; position: absolute; inset: -60% -20%; background: linear-gradient( 100deg, transparent 38%, rgba(255, 255, 255, 0.48), transparent 62% ); transform: translateX(-70%) rotate(8deg); animation: ultimate-title-shine 4.6s ease-in-out infinite; pointer-events: none; } @keyframes ultimate-title-background { 0%, 100% { background-position: 0% 45%; } 50% { background-position: 100% 55%; } } @keyframes ultimate-title-shine { 0%, 35% { transform: translateX(-75%) rotate(8deg); } 75%, 100% { transform: translateX(75%) rotate(8deg); } } .vdoc-inline-magic { padding: 0.08em 0.34em; color: #704227; font-weight: 800; background: linear-gradient(90deg, #ffe4a8, #ffd1df, #c9f4e8, #ffe4a8); background-size: 260% 100%; border-radius: 0.35em; box-decoration-break: clone; -webkit-box-decoration-break: clone; animation: inline-magic-flow 4s linear infinite; } @keyframes inline-magic-flow { to { background-position: 260% 0; } } .vdoc-test-signature { margin-top: 3rem; padding: 1.2rem 1.4rem; color: #f9f4e8; font-family: "Ma Shan Zheng", "STKaiti", cursive; font-size: 1.35rem; text-align: right; background: linear-gradient(135deg, #243d37, #5e4268); border-radius: 18px; } </style>

Scriptorium 混合富文档终极测试

这是一份用于验证 Markdown-first、原生 HTML、LaTeX、Mermaid、网络媒体、Anime.js、CSS 3D、动态表格与局部文字特效能否在同一份源码中稳定共存的测试文档。

岛闭合规则: 每个可编程岛从带有 data-vdoc-island 的根 <div> 开始,到与该根匹配的最后一个 </div> 结束。岛的结构、局部样式、依赖声明和执行脚本必须全部位于这个范围内,任何岛内状态都不得泄漏到后续 Markdown 正文。

1. 网络图片与普通 Markdown

下面的猫咪图片直接使用远程 src,用于测试网络资源解析、缓存、导出本地化、离线降级和图片说明语义。

一只近距离面对镜头的漂亮猫咪

图片说明: 猫咪的近距离肖像。渲染器应保留替代文字,并在网络不可用时提供可读降级。点击图片应当可以编辑链接。

这一段完全由 Markdown 编写。它包含 粗体斜体删除线行内代码 和一个 示例链接

  • 第一项用于测试无序列表。
  • 第二项包含 嵌套强调
  • 第三项提醒我们:普通正文不需要永久 DOM ID。

2. LaTeX 公式

行内公式展示质能关系:(E = mc^2)。

块级公式展示高斯积分:

$$ \int_{-\infty}^{+\infty} e^{-x^2},dx = \sqrt{\pi} $$

再测试一个矩阵:

[ A = \begin{bmatrix} 1 & 2 \ 3 & 4 \end{bmatrix}, \qquad \det(A) = -2 ]

3. Mermaid 图

flowchart LR
    A[Markdown 原始源码] --> B{混合语法仲裁器}
    B --> C[Markdown 块]
    B --> D[LaTeX 块]
    B --> E[Mermaid 块]
    B --> F[HTML 动画岛]
    C --> G[稳定 HTML]
    D --> G
    E --> G
    F --> H[受控运行时]
    G --> I[独立 HTML 导出]
    H --> I
Loading

4. 原生 Anime.js 动画岛

下面整个根元素是一个完整动画岛。外部依赖声明和初始化脚本都位于岛根内部,最后的根闭合标签是生命周期边界。

<style> [data-vdoc-island="anime-orbit-garden"] { position: relative; min-height: 310px; margin: 2rem 0; overflow: hidden; border-radius: 24px; background: radial-gradient(circle at center, #254f49 0%, #172724 62%, #0c1513 100%); box-shadow: 0 24px 80px rgba(20, 50, 44, 0.28); }
    [data-vdoc-island="anime-orbit-garden"] .anime-orbit-title {
        position: absolute;
        top: 22px;
        right: 0;
        left: 0;
        color: #fff;
        font: 700 1.1rem system-ui;
        letter-spacing: 0.16em;
        text-align: center;
    }

    [data-vdoc-island="anime-orbit-garden"] .anime-core {
        position: absolute;
        top: 50%;
        left: 50%;
        width: 70px;
        height: 70px;
        margin: -35px;
        border-radius: 50%;
        background: radial-gradient(
            circle at 32% 28%,
            #fff8c8,
            #ffb86b 36%,
            #d65d57 72%
        );
        box-shadow: 0 0 44px rgba(255, 184, 107, 0.78);
    }

    [data-vdoc-island="anime-orbit-garden"] .anime-petal {
        position: absolute;
        width: 22px;
        height: 22px;
        border-radius: 70% 20% 70% 20%;
    }

    [data-vdoc-island="anime-orbit-garden"] .anime-petal:nth-of-type(1) {
        background: #8de5c4;
    }

    [data-vdoc-island="anime-orbit-garden"] .anime-petal:nth-of-type(2) {
        width: 18px;
        height: 18px;
        background: #ffd38a;
    }

    [data-vdoc-island="anime-orbit-garden"] .anime-petal:nth-of-type(3) {
        width: 26px;
        height: 26px;
        background: #eca8d6;
    }

    [data-vdoc-island="anime-orbit-garden"] .anime-petal:nth-of-type(4) {
        width: 16px;
        height: 16px;
        background: #9ec9ff;
    }

    [data-vdoc-island="anime-orbit-garden"] .anime-replay {
        position: absolute;
        right: 18px;
        bottom: 16px;
        padding: 0.65rem 1rem;
        border: 1px solid rgba(255, 255, 255, 0.34);
        border-radius: 999px;
        color: #fff;
        background: rgba(255, 255, 255, 0.1);
        cursor: pointer;
    }
</style>

<div class="anime-orbit-title">ANIME.JS ORBIT GARDEN</div>
<div class="anime-core"></div>
<div class="anime-petal"></div>
<div class="anime-petal"></div>
<div class="anime-petal"></div>
<div class="anime-petal"></div>
<button type="button" class="anime-replay">重新绽放</button>

<script
    src="https://cdn.jsdelivr.net/npm/animejs@3.2.2/lib/anime.min.js"
    data-vdoc-library="anime"></script>

<script>
(() => {
    const island = document.querySelector(
        '[data-vdoc-island="anime-orbit-garden"]'
    );
    if (!island || island.dataset.vdocInitialized === 'true') return;
    island.dataset.vdocInitialized = 'true';

    const petals = [...island.querySelectorAll('.anime-petal')];
    const core = island.querySelector('.anime-core');

    const play = () => {
        if (!globalThis.anime) return;

        anime.remove([...petals, core]);

        petals.forEach((petal, index) => {
            const angle = index * Math.PI / 2;
            petal.style.left = '50%';
            petal.style.top = '50%';

            anime({
                targets: petal,
                translateX: [
                    Math.cos(angle) * 48,
                    Math.cos(angle + Math.PI * 2) * 112
                ],
                translateY: [
                    Math.sin(angle) * 48,
                    Math.sin(angle + Math.PI * 2) * 112
                ],
                rotate: '2turn',
                scale: [0.35, 1.18, 0.72],
                opacity: [0, 1, 0.78],
                duration: 4200 + index * 260,
                delay: index * 160,
                easing: 'easeInOutSine',
                direction: 'alternate',
                loop: true
            });
        });

        anime({
            targets: core,
            scale: [0.88, 1.12],
            boxShadow: [
                '0 0 24px rgba(255,184,107,.45)',
                '0 0 72px rgba(255,184,107,.95)'
            ],
            direction: 'alternate',
            duration: 1300,
            easing: 'easeInOutQuad',
            loop: true
        });
    };

    island.querySelector('.anime-replay')?.addEventListener('click', play);
    play();
})();
</script>

上一个动画岛已经在根闭合标签处结束。这一行必须重新由 Markdown 解析器解释,不能被视为动画岛的一部分。 点击文本动画应当被暂停,文本编辑应该映射到源码,源码变动应当不刷新动画状态,文本编辑应当改变动画文本。

5. 基于 Div 的 3D 文本块

<style> [data-vdoc-island="three-dimensional-text-card"] { display: grid; min-height: 420px; margin: 2rem 0; overflow: hidden; place-items: center; perspective: 1100px; border-radius: 24px; background: radial-gradient(circle at 72% 32%, rgba(255, 255, 255, 0.82), transparent 24%), linear-gradient(145deg, #e8f5ef, #f9e8dc); }
    [data-vdoc-island="three-dimensional-text-card"] .three-d-card {
        position: relative;
        width: min(80%, 620px);
        padding: 3rem 2rem;
        border: 1px solid rgba(255, 255, 255, 0.78);
        border-radius: 22px;
        background: linear-gradient(
            135deg,
            rgba(255, 255, 255, 0.86),
            rgba(255, 255, 255, 0.42)
        );
        box-shadow: 34px 34px 55px rgba(65, 87, 79, 0.25);
        transform: rotateX(10deg) rotateY(-14deg);
        transform-style: preserve-3d;
        transition: transform 0.22s ease;
    }

    [data-vdoc-island="three-dimensional-text-card"] .three-d-title {
        color: #254f49;
        font: 800 clamp(1.8rem, 5vw, 3.8rem) / 1.08 "Noto Serif SC", serif;
        text-shadow: 0 12px 18px rgba(37, 79, 73, 0.2);
        transform: translateZ(72px);
    }

    [data-vdoc-island="three-dimensional-text-card"] .three-d-copy {
        margin: 1rem 0 0;
        color: #624e45;
        font-size: 1.05rem;
        transform: translateZ(38px);
    }

    [data-vdoc-island="three-dimensional-text-card"] .three-d-depth {
        position: absolute;
        inset: 18px;
        border: 2px solid rgba(217, 119, 69, 0.34);
        border-radius: 18px;
        transform: translateZ(-28px);
    }

    [data-vdoc-island="three-dimensional-text-card"] .three-d-cube-stage {
        position: absolute;
        right: 12%;
        bottom: 30px;
        width: 112px;
        height: 112px;
        perspective: 700px;
    }

    [data-vdoc-island="three-dimensional-text-card"] .three-d-cube {
        position: relative;
        width: 100%;
        height: 100%;
        transform-style: preserve-3d;
        animation: three-d-cube-spin 10s linear infinite;
    }

    [data-vdoc-island="three-dimensional-text-card"] .three-d-cube-face {
        position: absolute;
        display: grid;
        width: 112px;
        height: 112px;
        place-items: center;
        padding: 0.7rem;
        box-sizing: border-box;
        border: 1px solid rgba(255, 255, 255, 0.7);
        color: #fffdf7;
        font: 700 0.9rem "Noto Serif SC", serif;
        letter-spacing: 0.08em;
        text-align: center;
        text-shadow: 0 2px 8px rgba(35, 47, 44, 0.32);
        background: linear-gradient(135deg, rgba(46, 117, 103, 0.9), rgba(148, 95, 133, 0.84));
        box-shadow: inset 0 0 24px rgba(255, 255, 255, 0.16);
        backface-visibility: visible;
    }

    [data-vdoc-island="three-dimensional-text-card"] .cube-front {
        transform: rotateY(0deg) translateZ(56px);
    }

    [data-vdoc-island="three-dimensional-text-card"] .cube-back {
        transform: rotateY(180deg) translateZ(56px);
    }

    [data-vdoc-island="three-dimensional-text-card"] .cube-right {
        transform: rotateY(90deg) translateZ(56px);
    }

    [data-vdoc-island="three-dimensional-text-card"] .cube-left {
        transform: rotateY(-90deg) translateZ(56px);
    }

    [data-vdoc-island="three-dimensional-text-card"] .cube-top {
        transform: rotateX(90deg) translateZ(56px);
    }

    [data-vdoc-island="three-dimensional-text-card"] .cube-bottom {
        transform: rotateX(-90deg) translateZ(56px);
    }

    @keyframes three-d-cube-spin {
        from { transform: rotateX(-18deg) rotateY(0deg) rotateZ(0deg); }
        to { transform: rotateX(342deg) rotateY(360deg) rotateZ(18deg); }
    }

    @media (max-width: 700px) {
        [data-vdoc-island="three-dimensional-text-card"] .three-d-cube-stage {
            right: 8%;
            bottom: 18px;
            transform: scale(0.72);
            transform-origin: bottom right;
        }
    }
</style>

<div class="three-d-card" tabindex="0">
    <div class="three-d-title">文字也可以拥有空间</div>
    <p class="three-d-copy">
        移动鼠标观察景深;离屏后它应保持静态且不产生后台 I/O。
    </p>
    <div class="three-d-depth"></div>
</div>

<div class="three-d-cube-stage" aria-label="通过 JavaScript 注入文字的旋转 3D 立方体">
    <div class="three-d-cube">
        <div class="three-d-cube-face cube-front"></div>
        <div class="three-d-cube-face cube-back"></div>
        <div class="three-d-cube-face cube-right"></div>
        <div class="three-d-cube-face cube-left"></div>
        <div class="three-d-cube-face cube-top"></div>
        <div class="three-d-cube-face cube-bottom"></div>
    </div>
</div>

<script>
(() => {
    const island = document.querySelector(
        '[data-vdoc-island="three-dimensional-text-card"]'
    );
    const card = island?.querySelector('.three-d-card');

    if (!island || !card || island.dataset.vdocInitialized === 'true') {
        return;
    }

    island.dataset.vdocInitialized = 'true';

    const cubeTexts = [
        ['cube-front', '文字'],
        ['cube-back', '空间'],
        ['cube-right', '旋转'],
        ['cube-left', '注入'],
        ['cube-top', 'CSS 3D'],
        ['cube-bottom', 'JS TEXT']
    ];

    cubeTexts.forEach(([faceClass, text]) => {
        const face = island.querySelector(`.${faceClass}`);
        if (face) face.textContent = text;
    });

    const move = (event) => {
        const rect = island.getBoundingClientRect();
        const x = (event.clientX - rect.left) / rect.width - 0.5;
        const y = (event.clientY - rect.top) / rect.height - 0.5;
        card.style.transform =
            `rotateX(${-y * 22}deg) rotateY(${x * 28}deg)`;
    };

    const reset = () => {
        card.style.transform = 'rotateX(10deg) rotateY(-14deg)';
    };

    island.addEventListener('pointermove', move);
    island.addEventListener('pointerleave', reset);
})();
</script>

3D 岛也已闭合。后续标题和正文不属于 3D 岛。 其中文本属于不同3d节点,但仍可以被光标选中,光标选中立方体应该停止旋转,编辑文本不会污染其它dom区,编辑结束立方体恢复旋转动作不丢失状态。

6. 动态高级表格

<style> [data-vdoc-island="interactive-paginated-table"] { margin: 2rem 0; padding: 1.25rem; border: 1px solid #d8d0c2; border-radius: 20px; background: #fffdf8; box-shadow: 0 18px 55px rgba(61, 50, 41, 0.12); }
    [data-vdoc-island="interactive-paginated-table"] .table-header {
        display: flex;
        gap: 1rem;
        align-items: center;
        justify-content: space-between;
        margin-bottom: 1rem;
    }

    [data-vdoc-island="interactive-paginated-table"] .table-title {
        font: 800 1.25rem "Noto Serif SC", serif;
    }

    [data-vdoc-island="interactive-paginated-table"] .table-description {
        margin-top: 0.2rem;
        color: #756a61;
        font: 13px system-ui;
    }

    [data-vdoc-island="interactive-paginated-table"] .table-status {
        padding: 0.4rem 0.7rem;
        border-radius: 999px;
        color: #27594e;
        background: #e7f3ee;
        font: 700 12px system-ui;
    }

    [data-vdoc-island="interactive-paginated-table"] .table-scroll {
        overflow: auto;
    }

    [data-vdoc-island="interactive-paginated-table"] table {
        width: 100%;
        border-collapse: collapse;
        font-family: system-ui, sans-serif;
    }

    [data-vdoc-island="interactive-paginated-table"] thead {
        color: #fff;
        background: #315f54;
    }

    [data-vdoc-island="interactive-paginated-table"] th,
    [data-vdoc-island="interactive-paginated-table"] td {
        padding: 0.75rem;
        text-align: left;
    }

    [data-vdoc-island="interactive-paginated-table"] td {
        border-bottom: 1px solid #e8dfd2;
    }

    [data-vdoc-island="interactive-paginated-table"] .table-pagination {
        display: flex;
        gap: 0.75rem;
        align-items: center;
        justify-content: center;
        margin-top: 1rem;
    }

    [data-vdoc-island="interactive-paginated-table"] button {
        padding: 0.6rem 0.9rem;
        border: 0;
        border-radius: 10px;
        color: #fff;
        background: #315f54;
        cursor: pointer;
    }

    [data-vdoc-island="interactive-paginated-table"] button:disabled {
        cursor: not-allowed;
        opacity: 0.42;
    }

    [data-vdoc-island="interactive-paginated-table"] .table-page {
        min-width: 90px;
        text-align: center;
        font: 700 13px system-ui;
    }
</style>

<div class="table-header">
    <div>
        <strong class="table-title">猫咪观测数据</strong>
        <div class="table-description">
            按钮、分页、状态和动态 DOM 的综合测试
        </div>
    </div>
    <span class="table-status" aria-live="polite"></span>
</div>

<div class="table-scroll">
    <table>
        <thead>
            <tr>
                <th>编号</th>
                <th>名字</th>
                <th>毛色</th>
                <th>状态</th>
                <th>评分</th>
            </tr>
        </thead>
        <tbody class="table-body"></tbody>
    </table>
</div>

<div class="table-pagination">
    <button type="button" data-table-action="previous">← 上一页</button>
    <span class="table-page"></span>
    <button type="button" data-table-action="next">下一页 →</button>
</div>

<script>
(() => {
    const island = document.querySelector(
        '[data-vdoc-island="interactive-paginated-table"]'
    );

    if (!island || island.dataset.vdocInitialized === 'true') return;
    island.dataset.vdocInitialized = 'true';

    const rows = [
        ['C-001', '琥珀', '橘白', '正在观察窗外', 98],
        ['C-002', '墨墨', '纯黑', '藏在纸箱中', 94],
        ['C-003', '雪球', '纯白', '正在晒太阳', 97],
        ['C-004', '花卷', '狸花', '巡视书架', 96],
        ['C-005', '芝麻', '奶牛色', '等待零食', 93],
        ['C-006', '银杏', '银渐层', '轻拍键盘', 95],
        ['C-007', '栗子', '金渐层', '趴在文档旁', 99],
        ['C-008', '云朵', '布偶色', '认真打哈欠', 92],
        ['C-009', '团子', '三花', '追逐光点', 97],
        ['C-010', '可可', '巧克力色', '守护显示器', 96]
    ];

    const pageSize = 4;
    let page = 0;

    const body = island.querySelector('.table-body');
    const status = island.querySelector('.table-status');
    const pageLabel = island.querySelector('.table-page');
    const previous = island.querySelector('[data-table-action="previous"]');
    const next = island.querySelector('[data-table-action="next"]');

    const render = () => {
        const pageCount = Math.ceil(rows.length / pageSize);
        page = Math.max(0, Math.min(pageCount - 1, page));

        const visible = rows.slice(
            page * pageSize,
            (page + 1) * pageSize
        );

        body.replaceChildren(...visible.map((row, rowIndex) => {
            const tr = document.createElement('tr');
            tr.style.background = rowIndex % 2 ? '#f7f1e7' : '#fffdf8';

            row.forEach((value) => {
                const td = document.createElement('td');
                td.textContent = String(value);
                tr.appendChild(td);
            });

            return tr;
        }));

        pageLabel.textContent = `${page + 1} / ${pageCount}`;
        status.textContent = `第 ${page + 1} 页 · 共 ${rows.length} 条`;
        previous.disabled = page === 0;
        next.disabled = page === pageCount - 1;
    };

    island.addEventListener('click', (event) => {
        const action = event.target
            .closest('[data-table-action]')
            ?.dataset.tableAction;

        if (action === 'previous') page -= 1;
        if (action === 'next') page += 1;
        if (action) render();
    });

    render();
})();
</script>

动态表格岛在上一个根闭合标签处结束。表格的按钮、脚本和动态行不得吞入下面的 Markdown 段落。

7. Markdown 正文与 HTML 文字特效混排

这是一段普通 Markdown 正文,前半部分应当保持朴素、稳定并且易于编辑;这一小段文字由 HTML 加入流动背景、粗体和局部颜色特效;随后文本重新回到 Markdown 的自然书写方式。编辑器应只修改被操作的最小源码区间,而不是把整个段落转换成冗长 HTML。

继续测试普通正文

当复杂组件离开视口时,它们的动画、计时器、媒体和主动测量应被冻结,但已经完成渲染的静态结构仍然可以供全文搜索和 AI 语义阅读使用。

AI 阅读这份文档时,应当能够理解:

  1. 猫咪图片的地址、替代文字和说明。
  2. 公式的原始 LaTeX 语义。
  3. Mermaid 图表达的处理流程。
  4. Anime.js 岛是一个绕核心运动的花瓣动画。
  5. 3D 文本块表达“文字也可以拥有空间”。
  6. 动态表格拥有十条数据和分页交互。
  7. 局部文字特效不改变周围 Markdown 的可读性。

终极测试的目标不是让所有语法进入同一解析器,而是验证每个语法域都拥有确定边界、完整闭合、局部生命周期和可读降级。

8. 最后一段 Markdown

文档的最后一段仍然是普通 Markdown。它用于验证经过多个完整闭合的 HTML 岛、局部样式和岛内脚本之后,Markdown 解析器仍能恢复到正确的正文状态。

如果这句话可以被直接选中、修改、保存、撤销、重新渲染和导出,且前面的动画岛不被无关地重新启动,那么新的源码范式就通过了最关键的一项测试。

作者:VCP Scriptorium 架构测试组
Human × AI · Markdown-first Programmable Document