feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
@@ -0,0 +1,75 @@
|
|||||||
|
name: 🐛 提交 BUG
|
||||||
|
description: 报告一个 Bug 以帮助我们改进
|
||||||
|
labels: ["bug"]
|
||||||
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: 操作系统
|
||||||
|
options:
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: arch
|
||||||
|
attributes:
|
||||||
|
label: 系统架构
|
||||||
|
options:
|
||||||
|
- x86_64 (Intel/AMD)
|
||||||
|
- aarch64 (ARM/Apple Silicon)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: BoosApi 版本
|
||||||
|
placeholder: "例如:v1.0.0,可在设置中查看"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Bug 描述
|
||||||
|
placeholder: 请清楚地描述遇到的问题
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: 复现步骤
|
||||||
|
placeholder: |
|
||||||
|
1. 第一步
|
||||||
|
2. 第二步
|
||||||
|
3. ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: 期望行为
|
||||||
|
placeholder: 描述你期望看到的正确行为
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: 实际行为
|
||||||
|
placeholder: 描述实际发生的错误行为
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: 截图 / 日志
|
||||||
|
placeholder: 可选:附上截图或相关错误日志
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: 💡 提交建议
|
||||||
|
description: 提出你的功能建议或改进想法
|
||||||
|
labels: ["enhancement"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: 建议描述
|
||||||
|
placeholder: 请清楚地描述你希望添加或改进的功能
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: use-case
|
||||||
|
attributes:
|
||||||
|
label: 使用场景
|
||||||
|
placeholder: 描述在什么情况下你需要这个功能
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: extra
|
||||||
|
attributes:
|
||||||
|
label: 补充说明
|
||||||
|
placeholder: 可选:附加截图、参考链接或其他说明
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
After Width: | Height: | Size: 443 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 457 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 436 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 650 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 344 KiB |
@@ -0,0 +1,67 @@
|
|||||||
|
name: Deploy Website to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- "website/**"
|
||||||
|
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: website/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
working-directory: website
|
||||||
|
|
||||||
|
- name: Build VitePress site
|
||||||
|
run: pnpm build
|
||||||
|
working-directory: website
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: website/docs/.vitepress/dist
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v5
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
permissions: write-all
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: '${{ github.workflow }}-${{ github.ref }}'
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ── Validate tag matches package.json version ──────────
|
||||||
|
check-version:
|
||||||
|
name: Check Version
|
||||||
|
if: ${{ !contains(github.ref_name, '-test') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate tag vs package.json
|
||||||
|
run: |
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
PKG_VERSION="v$(jq -r .version package.json)"
|
||||||
|
if [[ "$TAG" != "$PKG_VERSION" ]]; then
|
||||||
|
echo "❌ Tag $TAG does not match package.json $PKG_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Version $TAG is consistent"
|
||||||
|
|
||||||
|
# ── Generate release notes via git-cliff ─────────────
|
||||||
|
draft-release-notes:
|
||||||
|
name: Generate Release Notes
|
||||||
|
needs: [check-version]
|
||||||
|
if: ${{ !contains(github.ref_name, '-test') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
outputs:
|
||||||
|
body: ${{ steps.cliff.outputs.content }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Generate changelog
|
||||||
|
uses: orhun/git-cliff-action@v4
|
||||||
|
id: cliff
|
||||||
|
with:
|
||||||
|
config: cliff.toml
|
||||||
|
args: --latest --strip header
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_REPO: ${{ github.repository }}
|
||||||
|
CLIFF_REPO: ${{ github.repository }}
|
||||||
|
|
||||||
|
- name: Create draft release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: 'BoosApi ${{ github.ref_name }}'
|
||||||
|
body: ${{ steps.cliff.outputs.content }}
|
||||||
|
draft: true
|
||||||
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# ── Build for macOS / Windows / Linux x86_64 ───────────
|
||||||
|
release:
|
||||||
|
name: Build (${{ matrix.target }})
|
||||||
|
needs: [check-version, draft-release-notes]
|
||||||
|
if: ${{ !contains(github.ref_name, '-test') }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# macOS
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
- os: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
# Windows
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
- os: windows-latest
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
# Linux x86_64
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: 'v1'
|
||||||
|
key: '${{ matrix.os }}-${{ matrix.target }}'
|
||||||
|
workspaces: src-tauri -> target
|
||||||
|
|
||||||
|
- name: Install Linux dependencies
|
||||||
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
patchelf
|
||||||
|
|
||||||
|
- name: Install x86_64 OpenSSL (macOS Intel cross-compile)
|
||||||
|
if: matrix.target == 'x86_64-apple-darwin'
|
||||||
|
run: |
|
||||||
|
arch -x86_64 brew install openssl@3
|
||||||
|
echo "OPENSSL_DIR=$(arch -x86_64 brew --prefix openssl@3)" >> $GITHUB_ENV
|
||||||
|
echo "OPENSSL_INCLUDE_DIR=$(arch -x86_64 brew --prefix openssl@3)/include" >> $GITHUB_ENV
|
||||||
|
echo "OPENSSL_LIB_DIR=$(arch -x86_64 brew --prefix openssl@3)/lib" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install Protoc
|
||||||
|
uses: arduino/setup-protoc@v3
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get linked dependency versions
|
||||||
|
id: dep-versions
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
SM_SHA=$(git -C libs/stream-monaco rev-parse HEAD)
|
||||||
|
MS_SHA=$(git -C libs/markstream-vue rev-parse HEAD)
|
||||||
|
echo "cache-key=${{ runner.os }}-${{ matrix.target }}-linked-deps-${SM_SHA:0:12}-${MS_SHA:0:12}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache linked dependencies
|
||||||
|
id: cache-linked-deps
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
libs/stream-monaco/node_modules
|
||||||
|
libs/stream-monaco/dist
|
||||||
|
libs/markstream-vue/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/dist
|
||||||
|
libs/markstream-vue/packages/markstream-react/node_modules
|
||||||
|
libs/markstream-vue/packages/markstream-react/dist
|
||||||
|
key: ${{ steps.dep-versions.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Build linked dependencies
|
||||||
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
||||||
|
pnpm --dir libs/stream-monaco build
|
||||||
|
|
||||||
|
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
||||||
|
pnpm --dir libs/markstream-vue run build:parser
|
||||||
|
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build with Tauri
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ matrix.os == 'macos-latest' && '-' || '' }}
|
||||||
|
with:
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: 'BoosApi ${{ github.ref_name }}'
|
||||||
|
releaseBody: ${{ needs.draft-release-notes.outputs.body }}
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
|
tauriScript: pnpm tauri
|
||||||
|
args: --target ${{ matrix.target }}
|
||||||
|
includeUpdaterJson: true
|
||||||
|
|
||||||
|
- name: Package Windows portable (zip)
|
||||||
|
if: contains(matrix.target, 'windows')
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$exe = "src-tauri/target/${{ matrix.target }}/release/BoosApi.exe"
|
||||||
|
$arch = if ("${{ matrix.target }}" -match "aarch64") { "arm64" } else { "x64" }
|
||||||
|
$zipName = "BoosApi_${{ github.ref_name }}_windows-${arch}-portable.zip"
|
||||||
|
Compress-Archive -Path $exe -DestinationPath $zipName
|
||||||
|
|
||||||
|
- name: Upload Windows portable to release
|
||||||
|
if: contains(matrix.target, 'windows')
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
draft: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: BoosApi_${{ github.ref_name }}_windows-*-portable.zip
|
||||||
|
|
||||||
|
# ── Build for Linux ARM64 (cross-compile) ──────────────
|
||||||
|
release-linux-arm:
|
||||||
|
name: Build (aarch64-unknown-linux-gnu)
|
||||||
|
needs: [check-version, draft-release-notes]
|
||||||
|
if: ${{ !contains(github.ref_name, '-test') }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: 'v1'
|
||||||
|
key: 'ubuntu-22.04-aarch64-unknown-linux-gnu'
|
||||||
|
workspaces: src-tauri -> target
|
||||||
|
|
||||||
|
- name: Setup cross-compilation sources
|
||||||
|
run: |
|
||||||
|
cat > /tmp/sources.list << 'EOF'
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
|
||||||
|
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-updates main multiverse universe restricted
|
||||||
|
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main multiverse universe restricted
|
||||||
|
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main multiverse universe restricted
|
||||||
|
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main multiverse universe restricted
|
||||||
|
EOF
|
||||||
|
sudo mv /etc/apt/sources.list /etc/apt/sources.list.bak
|
||||||
|
sudo mv /tmp/sources.list /etc/apt/sources.list
|
||||||
|
sudo dpkg --add-architecture arm64
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
- name: Install ARM64 dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
g++-aarch64-linux-gnu \
|
||||||
|
libwebkit2gtk-4.1-dev:arm64 \
|
||||||
|
libayatana-appindicator3-dev:arm64 \
|
||||||
|
librsvg2-dev:arm64 \
|
||||||
|
libssl-dev:arm64 \
|
||||||
|
patchelf:arm64
|
||||||
|
|
||||||
|
- name: Install Protoc
|
||||||
|
uses: arduino/setup-protoc@v3
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get linked dependency versions
|
||||||
|
id: dep-versions
|
||||||
|
run: |
|
||||||
|
SM_SHA=$(git -C libs/stream-monaco rev-parse HEAD)
|
||||||
|
MS_SHA=$(git -C libs/markstream-vue rev-parse HEAD)
|
||||||
|
echo "cache-key=${{ runner.os }}-aarch64-unknown-linux-gnu-linked-deps-${SM_SHA:0:12}-${MS_SHA:0:12}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache linked dependencies
|
||||||
|
id: cache-linked-deps
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
libs/stream-monaco/node_modules
|
||||||
|
libs/stream-monaco/dist
|
||||||
|
libs/markstream-vue/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/dist
|
||||||
|
libs/markstream-vue/packages/markstream-react/node_modules
|
||||||
|
libs/markstream-vue/packages/markstream-react/dist
|
||||||
|
key: ${{ steps.dep-versions.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Build linked dependencies
|
||||||
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
||||||
|
pnpm --dir libs/stream-monaco build
|
||||||
|
|
||||||
|
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
||||||
|
pnpm --dir libs/markstream-vue run build:parser
|
||||||
|
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build for Linux ARM64
|
||||||
|
run: |
|
||||||
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
|
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
||||||
|
pnpm tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
||||||
|
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
|
||||||
|
CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++
|
||||||
|
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Upload Linux ARM64 artifacts
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: 'BoosApi ${{ github.ref_name }}'
|
||||||
|
draft: true
|
||||||
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: |
|
||||||
|
src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||||
|
src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/rpm/*.rpm
|
||||||
|
|
||||||
|
# ── Publish draft release as latest ────────────────────
|
||||||
|
publish-release:
|
||||||
|
name: Publish Release
|
||||||
|
needs: [release, release-linux-arm]
|
||||||
|
if: ${{ !contains(github.ref_name, '-test') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Publish draft release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
|
make_latest: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# ── Deploy website after release ────────────────────────
|
||||||
|
deploy-website:
|
||||||
|
name: Deploy Website
|
||||||
|
needs: [publish-release]
|
||||||
|
if: ${{ !contains(github.ref_name, '-test') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Trigger website deployment
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
await github.rest.actions.createWorkflowDispatch({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
workflow_id: 'deploy-website.yml',
|
||||||
|
ref: 'main',
|
||||||
|
});
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
name: Test Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'test-v*.*.*'
|
||||||
|
|
||||||
|
permissions: write-all
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: '${{ github.workflow }}-${{ github.ref }}'
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-build:
|
||||||
|
name: Test Build (aarch64-apple-darwin)
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: 'v1'
|
||||||
|
key: 'macos-latest-aarch64-apple-darwin'
|
||||||
|
workspaces: src-tauri -> target
|
||||||
|
|
||||||
|
- name: Install Protoc
|
||||||
|
uses: arduino/setup-protoc@v3
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get linked dependency versions
|
||||||
|
id: dep-versions
|
||||||
|
run: |
|
||||||
|
SM_SHA=$(git -C libs/stream-monaco rev-parse HEAD)
|
||||||
|
MS_SHA=$(git -C libs/markstream-vue rev-parse HEAD)
|
||||||
|
echo "cache-key=${{ runner.os }}-aarch64-apple-darwin-linked-deps-${SM_SHA:0:12}-${MS_SHA:0:12}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache linked dependencies
|
||||||
|
id: cache-linked-deps
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
libs/stream-monaco/node_modules
|
||||||
|
libs/stream-monaco/dist
|
||||||
|
libs/markstream-vue/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/dist
|
||||||
|
libs/markstream-vue/packages/markstream-react/node_modules
|
||||||
|
libs/markstream-vue/packages/markstream-react/dist
|
||||||
|
key: ${{ steps.dep-versions.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Build linked dependencies
|
||||||
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
||||||
|
pnpm --dir libs/stream-monaco build
|
||||||
|
|
||||||
|
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
||||||
|
pnpm --dir libs/markstream-vue run build:parser
|
||||||
|
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build with Tauri
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: '-'
|
||||||
|
with:
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: 'BoosApi ${{ github.ref_name }} (Test)'
|
||||||
|
releaseBody: 'Internal test build for scrollbar fix verification.'
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: true
|
||||||
|
tauriScript: pnpm tauri
|
||||||
|
args: --target aarch64-apple-darwin
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
name: Test Linux Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*-test*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: '${{ github.workflow }}-${{ github.ref }}'
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-linux:
|
||||||
|
name: Build Linux test package (x86_64)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: 'v1'
|
||||||
|
key: 'ubuntu-22.04-x86_64-unknown-linux-gnu-test'
|
||||||
|
workspaces: src-tauri -> target
|
||||||
|
|
||||||
|
- name: Install Linux dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
patchelf
|
||||||
|
|
||||||
|
- name: Install Protoc
|
||||||
|
uses: arduino/setup-protoc@v3
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get linked dependency versions
|
||||||
|
id: dep-versions
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
SM_SHA=$(git -C libs/stream-monaco rev-parse HEAD)
|
||||||
|
MS_SHA=$(git -C libs/markstream-vue rev-parse HEAD)
|
||||||
|
echo "cache-key=${{ runner.os }}-x86_64-unknown-linux-gnu-test-linked-deps-${SM_SHA:0:12}-${MS_SHA:0:12}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Cache linked dependencies
|
||||||
|
id: cache-linked-deps
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
libs/stream-monaco/node_modules
|
||||||
|
libs/stream-monaco/dist
|
||||||
|
libs/markstream-vue/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/dist
|
||||||
|
libs/markstream-vue/packages/markstream-react/node_modules
|
||||||
|
libs/markstream-vue/packages/markstream-react/dist
|
||||||
|
key: ${{ steps.dep-versions.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Build linked dependencies
|
||||||
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
||||||
|
pnpm --dir libs/stream-monaco build
|
||||||
|
|
||||||
|
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
||||||
|
pnpm --dir libs/markstream-vue run build:parser
|
||||||
|
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build Linux bundles
|
||||||
|
run: pnpm tauri build --target x86_64-unknown-linux-gnu --bundles appimage,rpm,deb
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
|
BOOSAPI_TEST_BUILD: '1'
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Publish Linux test release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: 'BoosApi ${{ github.ref_name }}'
|
||||||
|
body: |
|
||||||
|
Linux test build for issue #83 diagnostics.
|
||||||
|
|
||||||
|
If BoosApi still opens as a blank window, start it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BOOSAPI_LOG_FILE="$HOME/boosapi-${{ github.ref_name }}.log" RUST_LOG=debug BOOSAPI_ENABLE_DEVTOOLS=1 ./BoosApi_0.0.76_amd64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
For the RPM install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BOOSAPI_LOG_FILE="$HOME/boosapi-${{ github.ref_name }}-rpm.log" RUST_LOG=debug BOOSAPI_ENABLE_DEVTOOLS=1 BoosApi
|
||||||
|
```
|
||||||
|
|
||||||
|
If the log still stops around `builder.build(context)`, run the minimal-plugin diagnostic path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BOOSAPI_LOG_FILE="$HOME/boosapi-${{ github.ref_name }}-minimal.log" RUST_LOG=debug BOOSAPI_ENABLE_DEVTOOLS=1 BOOSAPI_LINUX_MINIMAL_PLUGINS=1 ./BoosApi_0.0.76_amd64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Please attach the generated log file back to issue #83.
|
||||||
|
draft: false
|
||||||
|
prerelease: true
|
||||||
|
make_latest: false
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: |
|
||||||
|
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage
|
||||||
|
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.sig
|
||||||
|
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm
|
||||||
|
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm.sig
|
||||||
|
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb
|
||||||
|
src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
name: Test Windows Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'test-windows-v*.*.*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: '${{ github.workflow }}-${{ github.ref }}'
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-windows:
|
||||||
|
name: Build (x86_64-pc-windows-msvc)
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: 'v1'
|
||||||
|
key: 'windows-latest-x86_64-pc-windows-msvc'
|
||||||
|
workspaces: src-tauri -> target
|
||||||
|
|
||||||
|
- name: Install Protoc
|
||||||
|
uses: arduino/setup-protoc@v3
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get linked dependency versions
|
||||||
|
id: dep-versions
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
SM_SHA=$(git -C libs/stream-monaco rev-parse HEAD)
|
||||||
|
MS_SHA=$(git -C libs/markstream-vue rev-parse HEAD)
|
||||||
|
echo "cache-key=${{ runner.os }}-x86_64-pc-windows-msvc-linked-deps-${SM_SHA:0:12}-${MS_SHA:0:12}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache linked dependencies
|
||||||
|
id: cache-linked-deps
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
libs/stream-monaco/node_modules
|
||||||
|
libs/stream-monaco/dist
|
||||||
|
libs/markstream-vue/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/node_modules
|
||||||
|
libs/markstream-vue/packages/markdown-parser/dist
|
||||||
|
libs/markstream-vue/packages/markstream-react/node_modules
|
||||||
|
libs/markstream-vue/packages/markstream-react/dist
|
||||||
|
key: ${{ steps.dep-versions.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Build linked dependencies
|
||||||
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
||||||
|
pnpm --dir libs/stream-monaco build
|
||||||
|
|
||||||
|
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
||||||
|
pnpm --dir libs/markstream-vue run build:parser
|
||||||
|
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build with Tauri
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: 'BoosApi Test ${{ github.ref_name }}'
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: true
|
||||||
|
tauriScript: pnpm tauri
|
||||||
|
args: --target x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Package Windows portable (zip)
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$exe = "src-tauri/target/x86_64-pc-windows-msvc/release/BoosApi.exe"
|
||||||
|
$zipName = "BoosApi_${{ github.ref_name }}_windows-x64-portable.zip"
|
||||||
|
Compress-Archive -Path $exe -DestinationPath $zipName
|
||||||
|
|
||||||
|
- name: Upload Windows portable to release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
draft: true
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
files: BoosApi_${{ github.ref_name }}_windows-x64-portable.zip
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.superpowers
|
||||||
|
.vscode
|
||||||
|
.agents
|
||||||
|
.mcp.json
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
*.key
|
||||||
|
|
||||||
|
# Planning files
|
||||||
|
task_plan.md
|
||||||
|
findings.md
|
||||||
|
progress.md
|
||||||
|
docs/superpowers/*
|
||||||
|
|
||||||
|
# Local git worktrees
|
||||||
|
.worktrees/
|
||||||
|
|
||||||
|
# Website (VitePress)
|
||||||
|
website/node_modules/
|
||||||
|
website/docs/.vitepress/cache/
|
||||||
|
website/docs/.vitepress/dist/
|
||||||
|
fix/
|
||||||
|
autoresearch-*.json
|
||||||
|
research-*.tsv
|
||||||
|
.autoresearch/
|
||||||
|
|
||||||
|
debug/*
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
# Contributor Agreement
|
||||||
|
|
||||||
|
This Contributor Agreement ("Agreement") applies to contributions submitted to the BoosApi project. By submitting a pull request, commit, patch, code snippet in an issue, or any other material intended to be incorporated into this project, you confirm that you have read, understood, and agreed to this Agreement.
|
||||||
|
|
||||||
|
This Agreement confirms that each contribution is properly authorized, that the project may continue to be distributed under AGPLv3, and that the Project Owner may use the contribution without restriction for its own purposes.
|
||||||
|
|
||||||
|
## 1. Scope
|
||||||
|
|
||||||
|
This Agreement applies to all contributions you submit to this project, including but not limited to:
|
||||||
|
|
||||||
|
- source code, scripts, configuration, tests, and build files;
|
||||||
|
- documentation, examples, comments, and translations;
|
||||||
|
- UI assets, icons, images, design materials, and data files;
|
||||||
|
- content submitted through pull requests, commits, patches, issues, discussions, or other GitHub features with the intent that it may be incorporated into the project.
|
||||||
|
|
||||||
|
Content submitted only as a problem report, runtime log, screenshot, or reproduction note is not treated as a contribution under this Agreement unless it is clearly intended to be merged into the project.
|
||||||
|
|
||||||
|
## 2. Authority and Rights
|
||||||
|
|
||||||
|
You represent that:
|
||||||
|
|
||||||
|
1. you are the author or lawful rights holder of the contribution, or you have obtained sufficient rights to submit and license it under this Agreement;
|
||||||
|
2. if your employer, client, school, organization, or another third party owns or may own rights in the contribution, you have obtained all required authorization from that party;
|
||||||
|
3. to the best of your knowledge, the contribution does not infringe any third-party copyright, patent, trademark, trade secret, privacy right, or other legal right;
|
||||||
|
4. the contribution does not contain confidential information, secrets, credentials, proprietary code, or restricted data that you are not authorized to disclose or license to this project;
|
||||||
|
5. if the contribution includes third-party material, you have clearly identified its source, copyright notice, license, and any applicable restrictions, and you have confirmed that the material may be used by this project under this Agreement.
|
||||||
|
|
||||||
|
If you cannot make these representations, do not submit the contribution.
|
||||||
|
|
||||||
|
## 3. License to the Project Under AGPLv3
|
||||||
|
|
||||||
|
You grant this project and its users a perpetual, worldwide, irrevocable, non-exclusive, royalty-free license to use, reproduce, modify, merge, publish, distribute, and sublicense your contribution as part of the project under the GNU Affero General Public License version 3 only (SPDX: `AGPL-3.0-only`).
|
||||||
|
|
||||||
|
You understand and agree that, once accepted and merged, your contribution may be distributed to the public as part of this project under `AGPL-3.0-only` and will be subject to that license.
|
||||||
|
|
||||||
|
## 4. Unrestricted Use by the Project Owner
|
||||||
|
|
||||||
|
In addition to the AGPLv3 grant above, you grant the Project Owner, including the current repository owner and its lawful successors, assigns, and transferees, a perpetual, worldwide, irrevocable, non-exclusive, royalty-free, sublicensable, and transferable license to use your contribution without restriction for any lawful purpose.
|
||||||
|
|
||||||
|
This grant includes, without limitation, the right to:
|
||||||
|
|
||||||
|
- use, reproduce, modify, adapt, merge, compile, run, test, and deploy the contribution;
|
||||||
|
- create, maintain, publish, distribute, sell, host, or commercialize works or services that include the contribution;
|
||||||
|
- use the contribution in open-source versions, commercial versions, private versions, hosted services, internal tools, or any other project version;
|
||||||
|
- sublicense or dual-license the contribution, or use it under other licensing models, without additional permission from or payment to you.
|
||||||
|
|
||||||
|
This grant does not require you to transfer copyright ownership. Unless separately agreed in writing, you retain any rights you lawfully hold in your contribution. However, you may not revoke this license or impose additional restrictions or fees on the Project Owner's use of the contribution under this Agreement.
|
||||||
|
|
||||||
|
## 5. Patent License
|
||||||
|
|
||||||
|
To the extent you own or have the right to license patent claims, you grant this project, the Project Owner, and project users a perpetual, worldwide, irrevocable, non-exclusive, royalty-free patent license to use, make, reproduce, modify, publish, distribute, run, deploy, and sublicense your contribution and project versions that include it.
|
||||||
|
|
||||||
|
This patent license covers only patent claims that are necessarily infringed by your contribution itself or by the intended combination of your contribution with this project.
|
||||||
|
|
||||||
|
## 6. Third-Party Materials
|
||||||
|
|
||||||
|
Do not copy code or assets from sources whose license or authorization cannot be confirmed. If a contribution includes third-party material, you must:
|
||||||
|
|
||||||
|
1. identify the source, copyright owner, and license;
|
||||||
|
2. confirm that the license is compatible with `AGPL-3.0-only`;
|
||||||
|
3. confirm that the Project Owner may use the material without restriction as described in Section 4;
|
||||||
|
4. provide any required license text, notice, or attribution.
|
||||||
|
|
||||||
|
Maintainers may reject, modify, or remove contributions with unclear authorization, incompatible licensing, or unacceptable rights risk.
|
||||||
|
|
||||||
|
## 7. Submission and Acceptance
|
||||||
|
|
||||||
|
By submitting a contribution through GitHub or another method accepted by the maintainers, you accept this Agreement. Acceptance or merger of a contribution does not require the maintainers to accept future contributions and does not limit the Project Owner's control over project licensing, releases, product form, or commercial arrangements.
|
||||||
|
|
||||||
|
If you submit a contribution on behalf of an organization, your account, commit author information, or pull request description should reflect the proper authorization relationship. Maintainers may request additional confirmation when needed.
|
||||||
|
|
||||||
|
## 8. No Warranty
|
||||||
|
|
||||||
|
Except for the representations about rights and authority stated in this Agreement, your contribution is provided "as is". The Project Owner and maintainers may decide whether to accept, modify, publish, or remove any contribution.
|
||||||
|
|
||||||
|
## 9. Relationship to Other Files
|
||||||
|
|
||||||
|
This project is publicly distributed under `AGPL-3.0-only`; see [LICENSE](LICENSE) for the full license text. If this Agreement conflicts with other contribution workflow documents, the contribution licensing terms in this Agreement control. If any part of this Agreement conflicts with mandatory applicable law, it should be interpreted and enforced to the maximum extent permitted by law.
|
||||||
|
|
||||||
|
If you have questions about the legal effect of this Agreement, consult your legal counsel before submitting a contribution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 贡献者协议
|
||||||
|
|
||||||
|
本文件是 BoosApi 项目的贡献者协议(Contributor License Agreement, CLA)。你向本仓库提交 Pull Request、commit、patch、issue 附件中的代码片段,或以其他方式提交可合并到本项目中的内容,即表示你已阅读、理解并同意本协议。
|
||||||
|
|
||||||
|
本协议用于确认贡献内容具有合法授权来源,并确保本项目可以继续按照 AGPLv3 发布,同时允许项目所有者为自身目的不受限制地使用贡献内容。
|
||||||
|
|
||||||
|
## 1. 适用范围
|
||||||
|
|
||||||
|
本协议适用于你提交给本项目的所有贡献内容,包括但不限于:
|
||||||
|
|
||||||
|
- 源代码、脚本、配置、测试、构建文件;
|
||||||
|
- 文档、示例、注释、翻译;
|
||||||
|
- UI 资源、图标、图片、设计稿、数据文件;
|
||||||
|
- 通过 Pull Request、commit、patch、issue、讨论区或其他 GitHub 功能提交,并明确供本项目采用的内容。
|
||||||
|
|
||||||
|
仅用于问题描述、运行日志、截图或复现说明,且未明确要求合并进项目的内容,不视为本协议下的贡献内容。
|
||||||
|
|
||||||
|
## 2. 贡献资格与权利保证
|
||||||
|
|
||||||
|
你声明并保证:
|
||||||
|
|
||||||
|
1. 你是贡献内容的作者或合法权利人,或者已经取得足够授权,可以按照本协议提交并授权该贡献内容;
|
||||||
|
2. 如果贡献内容由你的雇主、客户、学校、组织或其他第三方拥有或可能拥有权利,你已经取得其必要授权;
|
||||||
|
3. 就你所知,贡献内容不会侵犯任何第三方的著作权、专利权、商标权、商业秘密、隐私权或其他合法权益;
|
||||||
|
4. 贡献内容不包含你无权公开或无权授权给本项目使用的保密信息、密钥、凭证、专有代码或受限制数据;
|
||||||
|
5. 如果贡献内容包含第三方材料,你已清楚标明来源、版权声明、许可证及任何使用限制,并确认这些材料允许本项目按本协议使用。
|
||||||
|
|
||||||
|
如果你无法作出上述声明和保证,请不要提交该贡献内容。
|
||||||
|
|
||||||
|
## 3. 授权本项目以 AGPLv3 发布
|
||||||
|
|
||||||
|
你授予本项目及其用户一项永久、全球范围、不可撤销、非独占、免版税的许可,允许本项目将你的贡献内容作为项目的一部分,按照 GNU Affero General Public License version 3 only(SPDX: `AGPL-3.0-only`)进行使用、复制、修改、合并、发布、分发和再许可。
|
||||||
|
|
||||||
|
你理解并同意:一旦贡献内容被项目接收并合并,该贡献内容可以随本项目以 `AGPL-3.0-only` 许可证向公众发布,并适用该许可证的全部条款。
|
||||||
|
|
||||||
|
## 4. 授权项目所有者不受限制地自身使用
|
||||||
|
|
||||||
|
除第 3 条的 AGPLv3 开源发布授权外,你还授予项目所有者(包括本仓库当前所有者及其合法继受者、受让人和转让对象)一项永久、全球范围、不可撤销、非独占、免版税、可转授权、可转让的许可,允许其为任何合法目的不受限制地使用你的贡献内容。
|
||||||
|
|
||||||
|
该授权包括但不限于:
|
||||||
|
|
||||||
|
- 使用、复制、修改、改编、合并、编译、运行、测试和部署贡献内容;
|
||||||
|
- 创建、维护、发布、分发、销售、托管或商业化包含贡献内容的作品或服务;
|
||||||
|
- 将贡献内容用于开源版本、商业版本、私有版本、托管服务、内部工具或其他任何项目版本;
|
||||||
|
- 在不额外征得你许可或支付费用的情况下,对贡献内容进行再许可、双重许可,或与其他许可模式并行使用。
|
||||||
|
|
||||||
|
该授权不要求你转让贡献内容的著作权。除非另有书面约定,你仍保留你依法拥有的权利。但你不得撤销本条授权,也不得因项目所有者依据本协议使用贡献内容而主张额外限制或费用。
|
||||||
|
|
||||||
|
## 5. 专利授权
|
||||||
|
|
||||||
|
在你拥有或有权授权的范围内,你授予本项目、项目所有者及项目用户一项永久、全球范围、不可撤销、非独占、免版税的专利许可,允许其使用、制作、复制、修改、发布、分发、运行、部署和再许可你的贡献内容,以及包含该贡献内容的项目版本。
|
||||||
|
|
||||||
|
该专利许可仅覆盖因你的贡献内容本身,或因你的贡献内容与本项目的预期组合,而必然被实施的专利权利要求。
|
||||||
|
|
||||||
|
## 6. 第三方材料
|
||||||
|
|
||||||
|
请不要直接复制无法确认授权来源的代码或资源。若贡献内容包含第三方材料,你需要:
|
||||||
|
|
||||||
|
1. 明确说明材料来源、版权归属和许可证;
|
||||||
|
2. 确认其许可证兼容 `AGPL-3.0-only`;
|
||||||
|
3. 确认项目所有者可以按第 4 条不受限制地使用该材料;
|
||||||
|
4. 在需要时补充对应的许可证文本、NOTICE 或归属说明。
|
||||||
|
|
||||||
|
项目维护者可以拒绝、修改或移除授权来源不清、许可证不兼容或权利风险不可接受的贡献内容。
|
||||||
|
|
||||||
|
## 7. 提交与接受
|
||||||
|
|
||||||
|
你通过 GitHub 或其他项目维护者认可的方式提交贡献内容,即表示你接受本协议。项目维护者合并贡献内容,并不表示其承诺采纳你未来的贡献,也不表示项目所有者放弃对项目许可、发布方式、产品形态或商业安排的决定权。
|
||||||
|
|
||||||
|
如果你代表组织提交贡献,应确保提交账号、commit 作者信息或 Pull Request 说明能够反映适当的授权关系;项目维护者可以在必要时要求你提供额外确认。
|
||||||
|
|
||||||
|
## 8. 无担保
|
||||||
|
|
||||||
|
除本协议中关于权利来源和授权能力的声明与保证外,你的贡献内容按“现状”提交。项目所有者和维护者可以自行决定是否接收、修改、发布或移除贡献内容。
|
||||||
|
|
||||||
|
## 9. 与其他文件的关系
|
||||||
|
|
||||||
|
本项目当前使用 `AGPL-3.0-only` 作为公开发布许可证,完整文本见 [LICENSE](LICENSE)。若本协议与项目贡献流程文档存在不一致,以本协议中关于贡献授权的条款为准;若本协议与适用法律强制规定冲突,以适用法律允许的最大范围解释和执行。
|
||||||
|
|
||||||
|
如你对本协议的法律效果存在疑问,请在提交贡献前咨询你的法律顾问。
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# BoosAPI Desktop
|
||||||
|
|
||||||
|
**English** | [简体中文](./README.zh-CN.md)
|
||||||
|
|
||||||
|
A local-first AI desktop client for the Boos ecosystem — multi-provider chat, AI agents, knowledge base, MCP, and a built-in API gateway, in one app.
|
||||||
|
|
||||||
|
[](https://github.com/Boos4721/boos-chat/releases)
|
||||||
|
[](#platform-support)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## What is BoosAPI Desktop
|
||||||
|
|
||||||
|
BoosAPI Desktop is a desktop AI workspace that runs on your own machine. It ships with **BoosAPI** (`https://api.boos.lat`) wired in as the default provider, so you can start chatting immediately, while staying free to connect any other OpenAI-compatible endpoint. Your conversations, keys, and files live locally.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Chat & models
|
||||||
|
- **Multi-provider chat** — Connect BoosAPI, OpenAI, Claude, Gemini, DeepSeek, Qwen, and any OpenAI-compatible endpoint, with custom base URL, API path, headers, and proxy rules.
|
||||||
|
- **Provider onboarding** — Import provider profiles via `boosapi://` deep links and CC Switch config import, applied after your confirmation.
|
||||||
|
- **Model management** — Sync remote model lists, manage groups, test latency, and configure capability tags, context length, sampling defaults, reasoning profiles, and per-model `extra_body`.
|
||||||
|
- **Conversation workflow** — Streaming replies, reasoning blocks, message versions, conversation branching, title generation, long-conversation compression, and multi-model parallel answers.
|
||||||
|
|
||||||
|
### AI agent
|
||||||
|
- **Agent mode** — Let models read/edit files, run commands, and analyze code inside a controlled desktop workflow.
|
||||||
|
- **Permission control** — Choose standard review, auto-accept edits, or full-access mode, with working-directory sandbox checks.
|
||||||
|
- **Approval & cost panel** — Watch tool calls live, remember allow decisions, and track tokens and cost per agent session.
|
||||||
|
|
||||||
|
### Content rendering
|
||||||
|
- **Markdown & math** — Render Markdown, code highlighting, tables, task lists, and LaTeX inline during streaming.
|
||||||
|
- **Code, diagrams & artifacts** — Built-in Monaco code blocks, Mermaid, D2 diagrams, and an artifact panel for code, notes, reports, and previews.
|
||||||
|
- **HTML fragment rendering** — Safely preview model-generated HTML fragments with streaming-stability fixes.
|
||||||
|
|
||||||
|
### Search & knowledge
|
||||||
|
- **Web search** — Plug in Tavily, Zhipu WebSearch, Bocha, and more, with source citations and search-query generation.
|
||||||
|
- **Local knowledge base** — Index private documents with sqlite-vec, configure retrieval/rerank options, and inspect retrieval feedback.
|
||||||
|
- **Context management** — Attach files, search results, knowledge snippets, memory, and tool output to conversation context.
|
||||||
|
|
||||||
|
### Tools & extensions
|
||||||
|
- **MCP protocol** — Run Model Context Protocol servers over stdio, SSE, or StreamableHTTP transports.
|
||||||
|
- **Built-in tools** — Use built-in MCP tools like fetch and file search without installing separate servers.
|
||||||
|
- **Tool-loop limits** — Configure a max tool-call loop count and recover interrupted or stuck tool sessions.
|
||||||
|
|
||||||
|
### API gateway
|
||||||
|
- **Local gateway** — Expose OpenAI Chat Completions, OpenAI Responses, Claude-native, and Gemini-native APIs from the desktop app.
|
||||||
|
- **Access & observability** — Manage gateway keys, SSL/TLS certificates, request logs, and usage stats locally.
|
||||||
|
- **Client templates** — Ready-made config templates for Claude Code, Codex CLI, OpenCode, Gemini CLI, and custom clients.
|
||||||
|
|
||||||
|
### Data import & backup
|
||||||
|
- **Third-party import** — Import ChatGPT exports, Cherry Studio backups, and Kelivo backups, with preview stats, warnings, and duplicate handling.
|
||||||
|
- **Provider & file migration** — Cherry Studio / Kelivo import can optionally migrate linked providers, API keys, and file attachments.
|
||||||
|
- **Backup** — Back up and restore via a local directory, WebDAV, or S3-compatible storage.
|
||||||
|
|
||||||
|
### Desktop experience & security
|
||||||
|
- **Local encryption** — App state lives in `~/.boosapi/`, user files in `~/Documents/BoosAPI Desktop/`, and API keys are protected with AES-256 and a local master key.
|
||||||
|
- **Desktop integration** — Tray, always-on-top, global shortcuts, autostart, proxy settings, and update checks.
|
||||||
|
- **11 UI languages** — Switch between Simplified Chinese, Traditional Chinese, English, Japanese, Korean, French, German, Spanish, Russian, Hindi, and Arabic.
|
||||||
|
|
||||||
|
## Platform support
|
||||||
|
|
||||||
|
| Platform | Architecture |
|
||||||
|
|----------|--------------|
|
||||||
|
| macOS | Apple Silicon (arm64), Intel (x86_64) |
|
||||||
|
| Windows 10/11 | x86_64, arm64 |
|
||||||
|
| Linux | x86_64 (AppImage/deb/rpm), arm64 (AppImage/deb/rpm) |
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Download the build for your platform from the [Releases](https://github.com/Boos4721/boos-chat/releases) page. On first launch, BoosAPI is preconfigured as the default provider — add your key in Settings and start chatting.
|
||||||
|
|
||||||
|
## Build from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frontend deps
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Build the bundled renderer packages (first time only)
|
||||||
|
pnpm --dir libs/stream-monaco install && pnpm --dir libs/stream-monaco build
|
||||||
|
pnpm --dir libs/markstream-vue install && pnpm --dir libs/markstream-vue run build:parser
|
||||||
|
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
||||||
|
|
||||||
|
# Run in dev / produce a bundle
|
||||||
|
pnpm tauri dev
|
||||||
|
pnpm tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Rust (stable), Node 20+, pnpm, and the Tauri Linux system dependencies (`webkit2gtk-4.1`, `libsoup-3.0`) on Linux.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### macOS says the app is "damaged" or "from an unidentified developer"
|
||||||
|
|
||||||
|
The app is not signed by Apple, so macOS may warn you. Remove the quarantine attribute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo xattr -dr com.apple.quarantine "/Applications/BoosAPI Desktop.app"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open it from **System Settings → Privacy & Security** and click **Open Anyway** if prompted.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
BoosAPI Desktop is a fork of [AQBot](https://github.com/AQBot-Desktop/AQBot), used and relicensed with the author's permission. It bundles the MIT-licensed open-agent-sdk. See [LICENSE](LICENSE).
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
# BoosAPI Desktop
|
||||||
|
|
||||||
|
[English](./README.md) | **简体中文**
|
||||||
|
|
||||||
|
面向 Boos 生态的本地优先 AI 桌面客户端 —— 多服务商对话、AI Agent、知识库、MCP 与内置 API 网关,集于一身。
|
||||||
|
|
||||||
|
[](https://github.com/Boos4721/boos-chat/releases)
|
||||||
|
[](#平台支持)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## BoosAPI Desktop 是什么
|
||||||
|
|
||||||
|
BoosAPI Desktop 是一款运行在你本机的 AI 桌面工作台。它默认接入 **BoosAPI**(`https://api.boos.lat`)作为服务商,开箱即用,同时也能连接任意 OpenAI 兼容端点。对话、密钥与文件都保存在本地。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 对话与模型
|
||||||
|
- **多服务商对话** —— 连接 BoosAPI、OpenAI、Claude、Gemini、DeepSeek、Qwen 以及任意 OpenAI 兼容端点,支持自定义 Base URL、API Path、请求头和代理规则。
|
||||||
|
- **服务商快速导入** —— 通过 `boosapi://` 服务商链接和 CC Switch 配置导入,在用户确认后把服务商资料带入应用。
|
||||||
|
- **模型管理** —— 同步远程模型列表、管理分组、测试延迟,并配置能力标签、上下文长度、采样默认值、推理档位和模型级 `extra_body`。
|
||||||
|
- **对话工作流** —— 流式回复、思考块、消息多版本、对话分支、标题生成状态、长对话压缩和多模型并行回答。
|
||||||
|
|
||||||
|
### AI Agent
|
||||||
|
- **Agent 模式** —— 让模型在受控桌面工作流中读取/编辑文件、执行命令并分析代码。
|
||||||
|
- **权限控制** —— 可选择标准审核、自动接受编辑或完全访问模式,同时保留工作目录沙箱检查。
|
||||||
|
- **审批与成本面板** —— 实时查看工具调用、记住允许决策,并跟踪每个 Agent 会话的 token 与成本。
|
||||||
|
|
||||||
|
### 内容渲染
|
||||||
|
- **Markdown 与数学公式** —— 在流式对话中渲染 Markdown、代码高亮、表格、任务列表和 LaTeX 公式。
|
||||||
|
- **代码、图表与 Artifact** —— 内置 Monaco 代码块、Mermaid、D2 图表和 Artifact 面板,用于代码、笔记、报告和预览。
|
||||||
|
- **HTML 片段渲染** —— 安全预览模型生成的 HTML 片段,并包含流式渲染稳定性修复。
|
||||||
|
|
||||||
|
### 搜索与知识
|
||||||
|
- **联网搜索** —— 接入 Tavily、智谱 WebSearch、Bocha 等搜索服务,支持引用来源和搜索查询生成。
|
||||||
|
- **本地知识库** —— 用 sqlite-vec 索引私有文档,配置检索/重排选项并查看检索反馈。
|
||||||
|
- **上下文管理** —— 把文件、搜索结果、知识片段、记忆和工具输出附加到对话上下文。
|
||||||
|
|
||||||
|
### 工具与扩展
|
||||||
|
- **MCP 协议** —— 运行 stdio、SSE 或 StreamableHTTP 传输的 Model Context Protocol 服务器。
|
||||||
|
- **内置工具** —— 直接使用 fetch、文件搜索等内置 MCP 工具,无需额外安装独立服务器。
|
||||||
|
- **工具循环上限** —— 可配置 MCP 工具调用最大循环次数,并更好地恢复中断或卡住的工具会话。
|
||||||
|
|
||||||
|
### API 网关
|
||||||
|
- **本地网关** —— 从桌面应用暴露 OpenAI Chat Completions、OpenAI Responses、Claude 原生和 Gemini 原生接口。
|
||||||
|
- **访问与观测** —— 本地管理网关密钥、SSL/TLS 证书、请求日志和用量统计。
|
||||||
|
- **客户端模板** —— 提供 Claude Code、Codex CLI、OpenCode、Gemini CLI 和自定义客户端的配置模板。
|
||||||
|
|
||||||
|
### 数据导入与备份
|
||||||
|
- **第三方导入** —— 导入 ChatGPT 官方导出、Cherry Studio 备份和 Kelivo 备份,带预览统计、警告和重复处理。
|
||||||
|
- **服务商与文件迁移** —— Cherry Studio/Kelivo 导入可选择迁移关联服务商、API Key 和文件附件。
|
||||||
|
- **备份** —— 通过本地目录、WebDAV 或 S3 兼容存储进行备份与恢复。
|
||||||
|
|
||||||
|
### 桌面体验与安全
|
||||||
|
- **本地加密** —— 应用状态位于 `~/.boosapi/`,用户文件位于 `~/Documents/BoosAPI Desktop/`,API Key 使用 AES-256 和本地主密钥保护。
|
||||||
|
- **桌面集成** —— 支持托盘、窗口置顶、全局快捷键、开机自启、代理设置和自动更新检查。
|
||||||
|
- **11 种界面语言** —— 可在简体中文、繁体中文、英语、日语、韩语、法语、德语、西班牙语、俄语、印地语和阿拉伯语之间切换。
|
||||||
|
|
||||||
|
## 平台支持
|
||||||
|
|
||||||
|
| 平台 | 架构 |
|
||||||
|
|------|------|
|
||||||
|
| macOS | Apple Silicon (arm64)、Intel (x86_64) |
|
||||||
|
| Windows 10/11 | x86_64、arm64 |
|
||||||
|
| Linux | x86_64 (AppImage/deb/rpm)、arm64 (AppImage/deb/rpm) |
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
前往 [Releases](https://github.com/Boos4721/boos-chat/releases) 页面下载适合你平台的安装包。首次启动时 BoosAPI 已预置为默认服务商 —— 在设置中填入你的密钥即可开始对话。
|
||||||
|
|
||||||
|
## 从源码构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 前端依赖
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# 构建内置渲染包(仅首次)
|
||||||
|
pnpm --dir libs/stream-monaco install && pnpm --dir libs/stream-monaco build
|
||||||
|
pnpm --dir libs/markstream-vue install && pnpm --dir libs/markstream-vue run build:parser
|
||||||
|
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
||||||
|
|
||||||
|
# 开发运行 / 打包
|
||||||
|
pnpm tauri dev
|
||||||
|
pnpm tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
需要 Rust (stable)、Node 20+、pnpm,Linux 下还需 Tauri 系统依赖(`webkit2gtk-4.1`、`libsoup-3.0`)。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### macOS 提示"已损坏"或"无法验证开发者"
|
||||||
|
|
||||||
|
应用未经 Apple 签名,macOS 可能弹出提示。移除隔离属性即可:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo xattr -dr com.apple.quarantine "/Applications/BoosAPI Desktop.app"
|
||||||
|
```
|
||||||
|
|
||||||
|
随后在 **「系统设置 → 隐私与安全性」** 点击 **「仍要打开」**。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
BoosAPI Desktop 是 [AQBot](https://github.com/AQBot-Desktop/AQBot) 的 fork,经作者授权使用并重新许可,内含 MIT 许可的 open-agent-sdk。详见 [LICENSE](LICENSE)。
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# BoosApi Storage Policy
|
||||||
|
|
||||||
|
## Dual-Root Directory Model
|
||||||
|
|
||||||
|
BoosApi uses two directory roots with distinct responsibilities:
|
||||||
|
|
||||||
|
| Root | Platform path | Purpose |
|
||||||
|
|------|---------------|---------|
|
||||||
|
| **Config home** | macOS/Linux: `~/.boosapi/`<br>Windows: `%USERPROFILE%\.boosapi\` | Application state, database, encryption keys, SSL, vector DB |
|
||||||
|
| **Documents root** | macOS/Linux: `~/Documents/boosapi/`<br>Windows: `%USERPROFILE%\Documents\boosapi\` | User-visible files: images, documents, backups |
|
||||||
|
|
||||||
|
Both directories are created automatically on first launch.
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
|
||||||
|
### Config home (`~/.boosapi/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.boosapi/
|
||||||
|
├── boosapi.db # SQLite database (all app state, settings, keys, …)
|
||||||
|
├── master.key # 32-byte AES-256 master encryption key (mode 0600 on Unix)
|
||||||
|
├── vector_db/ # sqlite-vec vector store for knowledge-base embeddings
|
||||||
|
└── ssl/ # Self-signed TLS certificate and private key for the gateway
|
||||||
|
├── cert.pem
|
||||||
|
└── key.pem # mode 0600 on Unix
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documents root (`~/Documents/boosapi/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
~/Documents/boosapi/
|
||||||
|
├── images/ # Image attachments (chat uploads, avatars, AI-generated)
|
||||||
|
├── files/ # Non-image file attachments (documents, code, archives)
|
||||||
|
└── backups/ # Default location for auto- and manual backups
|
||||||
|
```
|
||||||
|
|
||||||
|
All paths stored in the database (e.g. `messages.attachments`, `stored_files.storage_path`)
|
||||||
|
use **relative paths** under the documents root (e.g. `images/abc123_photo.jpg`).
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### Dual-Root vs. Single Home
|
||||||
|
|
||||||
|
User-created files (images, documents, backups) belong in a user-visible
|
||||||
|
location under `~/Documents/` so users can browse, back up, and share them
|
||||||
|
with standard OS tools. Application internals (database, encryption keys,
|
||||||
|
vector indices) stay hidden in `~/.boosapi/` to avoid clutter and accidental
|
||||||
|
modification.
|
||||||
|
|
||||||
|
### Single Home vs. Tauri `app_data_dir`
|
||||||
|
|
||||||
|
Tauri's `app_data_dir()` resolves to platform-specific, version-locked paths
|
||||||
|
(e.g. `~/Library/Application Support/top.boosapi.app/` on macOS). Using a
|
||||||
|
user-visible `~/.boosapi/` makes backups, debugging, and cross-version upgrades
|
||||||
|
predictable and independent of the Tauri bundle identifier.
|
||||||
|
|
||||||
|
### `boosapi.db` + `master.key` — Atomic Migration
|
||||||
|
|
||||||
|
The database and its master encryption key are always migrated as a matched
|
||||||
|
pair. During the one-time migration from the legacy `app_data_dir`:
|
||||||
|
|
||||||
|
1. Both files are copied to `~/.boosapi/*.migrating` staging names.
|
||||||
|
2. Both staging files are renamed to their final names in a single pass.
|
||||||
|
3. If either step fails the staging files are cleaned up and the old location
|
||||||
|
is left intact — no data is ever left in a half-migrated state.
|
||||||
|
|
||||||
|
### Other Subdirectories
|
||||||
|
|
||||||
|
`vector_db/` and `ssl/` are migrated best-effort (rename, falling back to
|
||||||
|
copy+delete for cross-device moves). A failure to migrate a subdirectory is
|
||||||
|
logged as a warning; the application continues normally and the new
|
||||||
|
subdirectory will be created empty on next use.
|
||||||
|
|
||||||
|
### Backup Defaults
|
||||||
|
|
||||||
|
`resolve_backup_dir(None)` returns `~/Documents/boosapi/backups/`.
|
||||||
|
Users may override this via Settings → Backup → Backup Directory; an absolute
|
||||||
|
path stored there takes precedence.
|
||||||
|
|
||||||
|
### SSL Certificate Storage
|
||||||
|
|
||||||
|
`generate_self_signed_cert` writes `cert.pem` and `key.pem` to
|
||||||
|
`~/.boosapi/ssl/`. The private key is written atomically (temp-file + rename)
|
||||||
|
with mode `0600` on Unix.
|
||||||
|
|
||||||
|
## Agent / Automation Guidance
|
||||||
|
|
||||||
|
- Application state (database, keys, vector DB): read/write under `~/.boosapi/`
|
||||||
|
- User files (images, documents, backups): read/write under `~/Documents/boosapi/`
|
||||||
|
- Database paths for files must be **relative** to the documents root
|
||||||
|
- Do **not** hard-code paths derived from `app_data_dir`, bundle identifier,
|
||||||
|
or application version strings
|
||||||
|
- All directory names are **lowercase** with no spaces
|
||||||
|
|
||||||
|
## UI Conventions
|
||||||
|
|
||||||
|
### Image Preview & Modal Rules
|
||||||
|
|
||||||
|
All antd `<Image>` components **must** use blur-mask preview:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Image preview={{ mask: { blur: true }, scaleStep: 0.5 }} />
|
||||||
|
```
|
||||||
|
|
||||||
|
- `mask: { blur: true }` — hover shows a blurred overlay (never `mask: false` or plain text mask)
|
||||||
|
- `scaleStep: 0.5` — consistent zoom step across the app
|
||||||
|
- These settings apply to **all** image previews: chat attachments, file list thumbnails, avatar previews, etc.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# git-cliff configuration
|
||||||
|
# https://git-cliff.org/docs/configuration
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
header = ""
|
||||||
|
body = """
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | upper_first }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | split(pat="\n") | first | trim }}{% if commit.remote.pr_number %} (#{{ commit.remote.pr_number }}){% endif %} ([#{{ commit.id | truncate(length=7, end="") }}](https://github.com/BoosApi-Desktop/BoosApi/commit/{{ commit.id }}))
|
||||||
|
{%- endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
"""
|
||||||
|
footer = ""
|
||||||
|
trim = true
|
||||||
|
|
||||||
|
[git]
|
||||||
|
conventional_commits = true
|
||||||
|
filter_unconventional = false
|
||||||
|
split_commits = false
|
||||||
|
commit_preprocessors = [
|
||||||
|
{ pattern = '\(#([0-9]+)\)', replace = "" },
|
||||||
|
]
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "🚀 新功能" },
|
||||||
|
{ message = "^fix", group = "🐛 Bug 修复" },
|
||||||
|
{ message = "^perf", group = "⚡ 性能提升" },
|
||||||
|
{ message = "^refactor", group = "🔨 重构" },
|
||||||
|
{ message = "^style", group = "🎨 样式" },
|
||||||
|
{ message = "^docs", group = "📝 文档" },
|
||||||
|
{ message = "^ci", group = "🔧 CI/CD" },
|
||||||
|
{ message = "^chore\\(version\\)", skip = true },
|
||||||
|
{ message = "^chore", group = "📦 杂项" },
|
||||||
|
{ message = "^test", group = "🧪 测试" },
|
||||||
|
{ message = "^build", group = "🏗️ 构建" },
|
||||||
|
{ body = ".*security", group = "🔒 安全" },
|
||||||
|
]
|
||||||
|
filter_commits = false
|
||||||
|
tag_pattern = "v[0-9].*"
|
||||||
|
sort_commits = "newest"
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>BoosAPI Desktop</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
<script>
|
||||||
|
// Match initial background to theme to avoid white flash
|
||||||
|
try {
|
||||||
|
var m = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
if (m && m.matches) document.body.style.backgroundColor = '#141414';
|
||||||
|
} catch(e) {}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.ts linguist-detectable=false
|
||||||
|
*.css linguist-detectable=false
|
||||||
|
*.scss linguist-detectable=false
|
||||||
|
*.js linguist-detectable=true
|
||||||
|
*.vue linguist-detectable=true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github: Simon-He95
|
||||||
|
custom: ['https://github.com/Simon-He95/sponsor']
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Report an issue with markstream-vue (renderer or parser)
|
||||||
|
labels: [pending triage]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- Please attach a shareable repro link from the test page when possible: https://markstream-vue.simonhe.me/test
|
||||||
|
- Or provide a minimal StackBlitz/GitHub repo with the failing case.
|
||||||
|
- Raw Markdown samples help us reproduce quickly.
|
||||||
|
- type: textarea
|
||||||
|
id: bug-description
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug
|
||||||
|
description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks!
|
||||||
|
placeholder: I am doing ... What I expect is ... What actually happening is ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Reproduction link
|
||||||
|
description: Shareable link from the test page (preferred) or a minimal StackBlitz/repo. Reports without a repro may be closed after 3 days.
|
||||||
|
placeholder: https://markstream-vue.simonhe.me/test#...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: markdown
|
||||||
|
attributes:
|
||||||
|
label: Markdown / content sample
|
||||||
|
description: Provide the exact Markdown (or nodes JSON) that triggers the issue.
|
||||||
|
placeholder: |
|
||||||
|
## Example
|
||||||
|
- [ ] task
|
||||||
|
```js
|
||||||
|
console.log('hello')
|
||||||
|
```
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: actual
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: package-manager
|
||||||
|
attributes:
|
||||||
|
label: Used Package Manager
|
||||||
|
description: Select the used package manager
|
||||||
|
options:
|
||||||
|
- npm
|
||||||
|
- yarn
|
||||||
|
- pnpm
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: versions
|
||||||
|
attributes:
|
||||||
|
label: Versions
|
||||||
|
description: markstream-vue version, Vue version, and browser/runtime (if applicable).
|
||||||
|
placeholder: markstream-vue 0.0.x, Vue 3.x, Chrome 131 / SSR
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs
|
||||||
|
description: |
|
||||||
|
Optional if provided reproduction. Please avoid screenshots; copy/paste text instead.
|
||||||
|
|
||||||
|
- If using the playground/nuxt playground, run with `pnpm dev` / `pnpm play:nuxt` and capture console output.
|
||||||
|
- For build issues, run `pnpm build` with `DEBUG=*` and include the stack trace.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 📚 Documentation
|
||||||
|
url: https://markstream-vue-docs.simonhe.me/guide/
|
||||||
|
about: Full docs, troubleshooting, and API usage.
|
||||||
|
- name: 🧪 Test page (shareable repro)
|
||||||
|
url: https://markstream-vue.simonhe.me/test
|
||||||
|
about: Reproduce rendering issues and generate a shareable link for your issue.
|
||||||
|
- name: 🕹️ Playground
|
||||||
|
url: https://markstream-vue.simonhe.me/
|
||||||
|
about: Try the renderer live with sample content.
|
||||||
|
- name: 📖 Support guide
|
||||||
|
url: https://github.com/Simon-He95/markstream-vue/blob/main/SUPPORT.md
|
||||||
|
about: Where to get help, ask questions, or report issues (docs, test page, discussions, discord).
|
||||||
|
- name: 🛡️ Security policy
|
||||||
|
url: https://github.com/Simon-He95/markstream-vue/blob/main/SECURITY.md
|
||||||
|
about: Report potential vulnerabilities privately via email.
|
||||||
|
- name: 💬 Discussions
|
||||||
|
url: https://github.com/Simon-He95/markstream-vue/discussions
|
||||||
|
about: Ask questions, share ideas, or discuss integrations.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: 🚀 Feature proposal
|
||||||
|
description: Propose a feature or improvement for markstream-vue
|
||||||
|
labels: ['enhancement: pending triage']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for your interest in the project and taking the time to fill out this feature report!
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: 'What problem does this solve? Please describe the use case (AI/chat/SSE, docs, diff viewer, custom node, etc.). If you intend to submit a PR for this issue, tell us in the description. Thanks!'
|
||||||
|
placeholder: As a developer using markstream-vue I want [goal] so that [benefit].
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: current-config
|
||||||
|
attributes:
|
||||||
|
label: Current config (if applicable)
|
||||||
|
description: 'Renderer props or parser options you are using (e.g., max-live-nodes, batchRendering, renderBatchSize, enableMermaid/Katex/Monaco, custom components).'
|
||||||
|
- type: textarea
|
||||||
|
id: suggested-solution
|
||||||
|
attributes:
|
||||||
|
label: Suggested solution / API shape
|
||||||
|
description: What would the API/prop/config look like?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternative
|
||||||
|
attributes:
|
||||||
|
label: Alternatives
|
||||||
|
description: Clear and concise description of any alternative solutions or workarounds you've considered.
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Any other context or screenshots about the feature request here (include sample Markdown or mockups if relevant).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## Summary
|
||||||
|
|
||||||
|
Please explain the goal of this change in 2–3 lines.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Screenshots / Demo (if UI/behavior changes)
|
||||||
|
|
||||||
|
- [ ] Added GIF/screenshot
|
||||||
|
- [ ] Shareable repro link from https://markstream-vue.simonhe.me/test (recommended)
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] Lint: `pnpm lint`
|
||||||
|
- [ ] Typecheck: `pnpm typecheck`
|
||||||
|
- [ ] Tests: `pnpm test` (or `pnpm test:update` if snapshots changed)
|
||||||
|
- [ ] Build: `pnpm build` (library + CSS)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Lint, Typecheck & Tests
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node_version: ['lts/*']
|
||||||
|
include:
|
||||||
|
- os: macos-latest
|
||||||
|
node_version: 'lts/*'
|
||||||
|
- os: windows-latest
|
||||||
|
node_version: 'lts/*'
|
||||||
|
fail-fast: false
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
steps:
|
||||||
|
- name: Set git to use LF
|
||||||
|
run: |
|
||||||
|
git config --global core.autocrlf false
|
||||||
|
git config --global core.eol lf
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
|
||||||
|
- name: Set node ${{ matrix.node_version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node_version }}
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: corepack enable
|
||||||
|
|
||||||
|
- name: Setup
|
||||||
|
run: npm i -g @antfu/ni
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: nci
|
||||||
|
|
||||||
|
- name: Lint (Ubuntu only)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: nr typecheck
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: nr test -- --run
|
||||||
|
|
||||||
|
- name: Run SSR check
|
||||||
|
run: nr check:ssr
|
||||||
|
|
||||||
|
- name: Build (Ubuntu only)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Check package size budget (Ubuntu only)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: pnpm size:check
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Docs check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: pnpm
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
- name: Build docs
|
||||||
|
run: pnpm docs:build:ci
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: Docs parity check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs-parity:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: pnpm
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
- name: Check docs parity
|
||||||
|
run: pnpm docs:check-zh
|
||||||
|
- name: Build docs
|
||||||
|
run: pnpm build:parser && pnpm build && pnpm docs:build
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Types Declaration Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dts-and-typecheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: pnpm
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
- name: Run typecheck
|
||||||
|
run: pnpm run typecheck
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
name: Deploy Docs to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: github-pages
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Configure GitHub Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build docs for GitHub Pages
|
||||||
|
run: pnpm docs:build:gh:ci
|
||||||
|
|
||||||
|
- name: Disable Jekyll on Pages artifact
|
||||||
|
run: touch docs/.vitepress/dist/.nojekyll
|
||||||
|
|
||||||
|
- name: Upload Pages artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: docs/.vitepress/dist
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: pkg.pr.new
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
# pkg.pr.new requires its GitHub App to be installed on the repository.
|
||||||
|
# Keep previews opt-in so PR CI does not fail before that external setup exists.
|
||||||
|
if: ${{ vars.PKG_PR_NEW_ENABLED == 'true' }}
|
||||||
|
name: Publish preview packages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
|
||||||
|
- name: Set node lts/*
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: corepack enable
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: |
|
||||||
|
pnpm -r --filter "./packages/**" build
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
- name: Publish preview packages
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: pnpm dlx pkg-pr-new publish . ./packages/markdown-parser ./packages/markstream-react ./packages/markstream-vue2
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
name: Release (Nightly)
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# 02:15 UTC daily
|
||||||
|
- cron: '15 2 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
all:
|
||||||
|
description: Tag all packages even if unchanged
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tag:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
created: ${{ steps.nightly.outputs.created }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Configure git identity (annotated tags)
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Create nightly tags (if needed)
|
||||||
|
id: nightly
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ARGS=(--push --remote origin)
|
||||||
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.all }}" == "true" ]]; then
|
||||||
|
ARGS+=(--all)
|
||||||
|
fi
|
||||||
|
|
||||||
|
OUT="$(node scripts/create-nightly-tags.mjs "${ARGS[@]}")"
|
||||||
|
echo "${OUT}"
|
||||||
|
CREATED_JSON="$(node -e 'const s=process.argv[1]; const j=JSON.parse(s); console.log(JSON.stringify(j.created||[]))' "${OUT}")"
|
||||||
|
echo "created=${CREATED_JSON}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: tag
|
||||||
|
if: ${{ needs.tag.outputs.created != '[]' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
tag: ${{ fromJson(needs.tag.outputs.created) }}
|
||||||
|
steps:
|
||||||
|
- name: Parse tag
|
||||||
|
id: meta
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${{ matrix.tag }}"
|
||||||
|
PKG="${TAG%@*}"
|
||||||
|
ID="${TAG##*@}"
|
||||||
|
echo "pkg=${PKG}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "name=[${PKG}] ${ID}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Checkout (for changelog body)
|
||||||
|
if: ${{ steps.meta.outputs.pkg == 'markstream-vue' }}
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Build changelog body (best effort)
|
||||||
|
id: changelog
|
||||||
|
if: ${{ steps.meta.outputs.pkg == 'markstream-vue' }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="$(grep -m1 '"version"' package.json | sed -E 's/.*"version": "([^"]+)".*/\1/')"
|
||||||
|
BODY_FILE="$(mktemp)"
|
||||||
|
FOUND=0
|
||||||
|
|
||||||
|
if [[ -n "${VERSION}" ]]; then
|
||||||
|
awk -v version="${VERSION}" '
|
||||||
|
BEGIN { capture = 0; found = 0 }
|
||||||
|
/^## \[/ {
|
||||||
|
if (capture == 1)
|
||||||
|
exit
|
||||||
|
if ($0 ~ "^## \\[" version "\\]") {
|
||||||
|
capture = 1
|
||||||
|
found = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
capture == 1 { print }
|
||||||
|
END {
|
||||||
|
if (found == 0)
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
' CHANGELOG.md > "${BODY_FILE}" && FOUND=1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${FOUND}" == "1" && -s "${BODY_FILE}" ]]; then
|
||||||
|
echo "has_body=true" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "body_path=${BODY_FILE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
else
|
||||||
|
echo "has_body=false" >> "${GITHUB_OUTPUT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create GitHub Pre-release (markstream-vue with changelog)
|
||||||
|
if: ${{ steps.meta.outputs.pkg == 'markstream-vue' && steps.changelog.outputs.has_body == 'true' }}
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ matrix.tag }}
|
||||||
|
name: ${{ steps.meta.outputs.name }}
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: true
|
||||||
|
body_path: ${{ steps.changelog.outputs.body_path }}
|
||||||
|
append_body: true
|
||||||
|
|
||||||
|
- name: Create GitHub Pre-release (fallback)
|
||||||
|
if: ${{ steps.meta.outputs.pkg != 'markstream-vue' || steps.changelog.outputs.has_body != 'true' }}
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ matrix.tag }}
|
||||||
|
name: ${{ steps.meta.outputs.name }}
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: true
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
name: Release (Stable)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
# Stable package tags: <pkg>@<semver>
|
||||||
|
# e.g. markstream-vue@0.0.4, markstream-react@0.0.3-beta.2
|
||||||
|
- '*@[0-9]*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Parse tag
|
||||||
|
id: meta
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
PKG="${TAG%@*}"
|
||||||
|
VERSION="${TAG##*@}"
|
||||||
|
|
||||||
|
if [[ -z "${PKG}" || -z "${VERSION}" ]]; then
|
||||||
|
echo "Invalid tag: ${TAG}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PRERELEASE=false
|
||||||
|
if [[ "${VERSION}" == *-* ]]; then
|
||||||
|
PRERELEASE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "pkg=${PKG}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "prerelease=${PRERELEASE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "name=[${PKG}] ${VERSION}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Checkout (for changelog body)
|
||||||
|
if: ${{ steps.meta.outputs.pkg == 'markstream-vue' }}
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Build changelog body
|
||||||
|
id: changelog
|
||||||
|
if: ${{ steps.meta.outputs.pkg == 'markstream-vue' }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
BODY_FILE="$(mktemp)"
|
||||||
|
|
||||||
|
awk -v version="${VERSION}" '
|
||||||
|
BEGIN { capture = 0; found = 0 }
|
||||||
|
/^## \[/ {
|
||||||
|
if (capture == 1)
|
||||||
|
exit
|
||||||
|
if ($0 ~ "^## \\[" version "\\]") {
|
||||||
|
capture = 1
|
||||||
|
found = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
capture == 1 { print }
|
||||||
|
END {
|
||||||
|
if (found == 0)
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
' CHANGELOG.md > "${BODY_FILE}" || {
|
||||||
|
echo "Unable to find CHANGELOG.md section for version ${VERSION}" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -s "${BODY_FILE}" ]]; then
|
||||||
|
echo "Found empty CHANGELOG.md section for version ${VERSION}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "body_path=${BODY_FILE}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Create GitHub Release (markstream-vue)
|
||||||
|
if: ${{ steps.meta.outputs.pkg == 'markstream-vue' }}
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: ${{ steps.meta.outputs.name }}
|
||||||
|
prerelease: ${{ steps.meta.outputs.prerelease }}
|
||||||
|
generate_release_notes: true
|
||||||
|
body_path: ${{ steps.changelog.outputs.body_path }}
|
||||||
|
append_body: true
|
||||||
|
|
||||||
|
- name: Create GitHub Release (other packages)
|
||||||
|
if: ${{ steps.meta.outputs.pkg != 'markstream-vue' }}
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: ${{ steps.meta.outputs.name }}
|
||||||
|
prerelease: ${{ steps.meta.outputs.prerelease }}
|
||||||
|
generate_release_notes: true
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
dist
|
||||||
|
dist-tw
|
||||||
|
node_modules
|
||||||
|
.nuxt
|
||||||
|
.output
|
||||||
|
.eslintcache
|
||||||
|
.history
|
||||||
|
.DS_Store
|
||||||
|
.tmp
|
||||||
|
.next
|
||||||
|
# vite press
|
||||||
|
cache
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Disable GitHub Pages Jekyll processing for repository source files.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
shamefully-hoist=false
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
shell-emulator=true
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
- `src/` is the Vue 3 library source.
|
||||||
|
- `src/components/*/` contains SFC components (PascalCase folders, `*.vue` files) and `index.ts` barrel exports.
|
||||||
|
- `src/utils/`, `src/types/`, `src/composables/` contain shared helpers, types, and composables.
|
||||||
|
- Library entry points and styles are exported via `src/exports.ts` and `src/index.css`.
|
||||||
|
- `test/` contains Vitest specs (e.g. `test/index.test.ts`, `test/plugins/*.test.ts`).
|
||||||
|
- `playground/` and `playground-nuxt/` are local demo apps for manual testing.
|
||||||
|
- `docs/` is the VitePress documentation site.
|
||||||
|
- `scripts/` contains maintenance utilities (DTS build, peer-deps checks, doc sync scripts).
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
Use `pnpm` (see `package.json#packageManager`). Key commands:
|
||||||
|
- `pnpm dev` / `pnpm play`: run the Vite playground.
|
||||||
|
- `pnpm build`: build the library bundles and generate `.d.ts`.
|
||||||
|
- `pnpm test` / `pnpm test:ui`: run tests (CLI/UI).
|
||||||
|
- `pnpm typecheck`: run `vue-tsc --noEmit`.
|
||||||
|
- `pnpm lint` / `pnpm lint:fix`: ESLint checks/fixes.
|
||||||
|
- `pnpm docs:dev` / `pnpm docs:build`: run/build VitePress.
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
- TypeScript-first, Vue 3 SFCs.
|
||||||
|
- Indentation: 2 spaces, LF endings (see `.editorconfig`).
|
||||||
|
- Components: `PascalCase` folder + file names (e.g. `src/components/CodeBlockNode/CodeBlockNode.vue`).
|
||||||
|
- Helpers: `camelCase` functions; follow existing file naming in `src/utils/`.
|
||||||
|
- Linting uses `@antfu/eslint-config` via `eslint.config.mjs`.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
- Framework: Vitest (`pnpm test`).
|
||||||
|
- Prefer small, focused unit tests for parsers/components; name files `*.test.ts` under `test/`.
|
||||||
|
- Update snapshots with `pnpm test:update`.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
- Commit messages follow Conventional Commits (e.g. `feat:`, `fix:`, `docs:`, `chore:`).
|
||||||
|
- Link issues using GitHub keywords when applicable (e.g. `close: #123`).
|
||||||
|
- PRs should include: clear description/rationale, linked issues, and screenshots/GIFs for UI-visible changes.
|
||||||
|
- Before requesting review: run `pnpm lint`, `pnpm typecheck`, and `pnpm test`, then sanity-check the playground.
|
||||||
|
|
||||||
|
## Security & Configuration Tips
|
||||||
|
- Keep `peerDependencies` accurate; large integrations (e.g. `mermaid`, `katex`) are peers and may be optional.
|
||||||
|
- Verify local peer installs with `pnpm run check:peer-deps`.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||||
|
- Focusing on what is best for the community, not just for ourselves
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at [simon_he@outlook.com](mailto:simon_he@outlook.com). All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Contributing to markstream-vue
|
||||||
|
|
||||||
|
Thanks for helping improve the project! This guide keeps contributions fast and consistent.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
1) Fork and clone the repo.
|
||||||
|
2) Install deps with `pnpm install` (repo uses `pnpm@10.x`).
|
||||||
|
3) Run the playground while you work: `pnpm dev` (or `pnpm play` for the alias).
|
||||||
|
4) Make changes under `src/` (library), `packages/` (parser), or `playground/` (demo).
|
||||||
|
|
||||||
|
## Checks to run
|
||||||
|
|
||||||
|
- `pnpm lint` — ESLint (Vue + TS).
|
||||||
|
- `pnpm typecheck` — vue-tsc.
|
||||||
|
- `pnpm test` — Vitest suite; use `pnpm test:update` to refresh snapshots.
|
||||||
|
- `pnpm build` — library + CSS build (ensures workers/CSS are emitted).
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
- Follow Conventional Commits (`feat:`, `fix:`, `chore:`, etc.).
|
||||||
|
- Add/extend tests for parser changes or rendering behaviors when possible.
|
||||||
|
- Include screenshots/GIFs for UI-visible changes (playground/demo).
|
||||||
|
- Note any prop/API changes in the PR description; update README or docs if needed.
|
||||||
|
- Be kind and follow the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
- Use the provided issue templates for bugs/feature requests.
|
||||||
|
- For rendering bugs, the interactive test page helps generate a shareable repro: https://markstream-vue.simonhe.me/test
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
- Vue 3 + TypeScript-first, 2-space indent, LF endings (see `.editorconfig`).
|
||||||
|
- Components in `src/components` use PascalCase directories/files; utils are camel/kebab as existing.
|
||||||
|
|
||||||
|
## Releases & tags (monorepo)
|
||||||
|
|
||||||
|
This repository publishes multiple packages (`markstream-vue`, `markstream-vue2`, `markstream-react`, `stream-markdown-parser`). To keep tags unambiguous, we use **namespaced tags**:
|
||||||
|
|
||||||
|
- `markstream-vue@<version>`
|
||||||
|
- `markstream-vue2@<version>`
|
||||||
|
- `markstream-react@<version>`
|
||||||
|
- `stream-markdown-parser@<version>`
|
||||||
|
|
||||||
|
Avoid creating bare `v<version>` tags (they mix different package versions in a monorepo).
|
||||||
|
|
||||||
|
### Stable vs Nightly
|
||||||
|
|
||||||
|
- **Stable**: tags are semver-based (`<pkg>@<version>`) and should match npm releases.
|
||||||
|
- **Nightly**: tags are commit snapshots (`<pkg>@nightly-YYYYMMDD-<sha>`) and are published as GitHub **Pre-releases**.
|
||||||
|
|
||||||
|
Nightly tags can be **dependency-driven**: when `stream-markdown-parser` changes, nightly tags are also created for `markstream-vue`, `markstream-vue2`, and `markstream-react` so consumers can test the whole stack against the latest parser.
|
||||||
|
|
||||||
|
### Common commands
|
||||||
|
|
||||||
|
- Tag current version of a package: `pnpm tag:vue3` / `pnpm tag:vue2` / `pnpm tag:react` / `pnpm tag:parser`
|
||||||
|
- Tag + push to remote: `pnpm tag:vue3:push` / `pnpm tag:vue2:push` / `pnpm tag:react:push` / `pnpm tag:parser:push`
|
||||||
|
- Backfill namespaced tags from existing legacy `v*` tags (dry run): `pnpm tag:backfill:dry`
|
||||||
|
- Apply backfill locally: `pnpm tag:backfill` (add `-- --push` to also push tags)
|
||||||
@@ -0,0 +1,526 @@
|
|||||||
|
# markstream-vue
|
||||||
|
|
||||||
|
> Fast, streaming-friendly Markdown rendering for Vue 3 — progressive Mermaid, streaming diff code blocks, and real-time previews optimized for large documents.
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/markstream-vue)
|
||||||
|
[](README.zh-CN.md)
|
||||||
|
[](https://markstream-vue-docs.simonhe.me/)
|
||||||
|
[](https://markstream-vue.simonhe.me/)
|
||||||
|
[](https://markstream-vue.simonhe.me/test)
|
||||||
|
[](https://www.npmjs.com/package/markstream-vue)
|
||||||
|
[](https://bundlephobia.com/package/markstream-vue)
|
||||||
|
[](https://github.com/Simon-He95/markstream-vue/releases)
|
||||||
|
[](https://github.com/Simon-He95/markstream-vue/discussions)
|
||||||
|
[](https://discord.gg/vkzdkjeRCW)
|
||||||
|
[](./SUPPORT.md)
|
||||||
|
[](./SECURITY.md)
|
||||||
|
[](https://github.com/Simon-He95/markstream-vue/actions/workflows/ci.yml)
|
||||||
|
[](./license)
|
||||||
|
|
||||||
|
Looking for other frameworks?
|
||||||
|
|
||||||
|
- Vue 2.6: use [`markstream-vue2`](./packages/markstream-vue2/README.md) (a baseline port with fewer advanced features)
|
||||||
|
- React: see `packages/markstream-react` at [`packages/markstream-react/README.md`](./packages/markstream-react/README.md) (first-pass port)
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [TL;DR Highlights](#tldr-highlights)
|
||||||
|
- [Choose Your Path](#choose-your-path)
|
||||||
|
- [Try It Now](#-try-it-now)
|
||||||
|
- [Community & support](#-community--support)
|
||||||
|
- [Quick Start](#-quick-start)
|
||||||
|
- [Common commands](#-common-commands)
|
||||||
|
- [Streaming in 30 seconds](#-streaming-in-30-seconds)
|
||||||
|
- [Performance presets](#-performance-presets)
|
||||||
|
- [Key props & options](#-key-props--options-cheatsheet)
|
||||||
|
- [Where it shines](#-where-it-shines)
|
||||||
|
- [FAQ](#-faq-quick-answers)
|
||||||
|
- [Why markstream-vue](#-why-markstream-vue-over-a-typical-markdown-renderer)
|
||||||
|
- [Roadmap](#-roadmap-snapshot)
|
||||||
|
- [Releases](#-releases)
|
||||||
|
- [Showcase](#-showcase--examples)
|
||||||
|
- [Introduction Video](#-introduction-video)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Contributing & community](#-contributing--community)
|
||||||
|
- [Troubleshooting](#troubleshooting--common-issues)
|
||||||
|
- [Thanks](#thanks)
|
||||||
|
- [Star History](#star-history)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
> 📖 Detailed docs, API, and advanced usage: https://markstream-vue-docs.simonhe.me/guide/
|
||||||
|
|
||||||
|
## TL;DR Highlights
|
||||||
|
|
||||||
|
- Purpose-built for **streaming Markdown** (AI/chat/SSE) with zero flicker and predictable memory.
|
||||||
|
- **Two render modes**: virtual window for long docs, incremental batching for “typing” effects.
|
||||||
|
- **Progressive diagrams** (Mermaid) and **streaming code blocks** (Monaco/Shiki) that keep up with diffs.
|
||||||
|
- Works with **raw Markdown strings or pre-parsed nodes**, supports **custom Vue components** inline.
|
||||||
|
- TypeScript-first, ship-ready defaults — import CSS and render.
|
||||||
|
|
||||||
|
## Choose Your Path
|
||||||
|
|
||||||
|
| If you want to... | Start here | Then go to |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| get the first render on screen | [Quick Start](#-quick-start) | [Installation guide](https://markstream-vue-docs.simonhe.me/guide/installation) |
|
||||||
|
| integrate it into a docs site or VitePress theme | [Docs Site & VitePress](https://markstream-vue-docs.simonhe.me/guide/vitepress-docs-integration) | [Custom Tags & Advanced Components](https://markstream-vue-docs.simonhe.me/guide/custom-components) |
|
||||||
|
| build an AI chat UI or SSE stream | [AI Chat & Streaming](https://markstream-vue-docs.simonhe.me/guide/ai-chat-streaming) | [Performance](https://markstream-vue-docs.simonhe.me/guide/performance) |
|
||||||
|
| replace one built-in renderer | [Override Built-in Components](https://markstream-vue-docs.simonhe.me/guide/component-overrides) | [Renderer & Node Components](https://markstream-vue-docs.simonhe.me/guide/components) |
|
||||||
|
| add trusted tags such as `thinking` | [Custom Tags & Advanced Components](https://markstream-vue-docs.simonhe.me/guide/custom-components) | [API Reference](https://markstream-vue-docs.simonhe.me/guide/api) |
|
||||||
|
| debug a broken integration but do not know why yet | [Troubleshooting by Symptom](https://markstream-vue-docs.simonhe.me/guide/troubleshooting-path) | [Troubleshooting](https://markstream-vue-docs.simonhe.me/guide/troubleshooting) |
|
||||||
|
|
||||||
|
## 🚀 Try It Now
|
||||||
|
|
||||||
|
- Playground (interactive demo): https://markstream-vue.simonhe.me/
|
||||||
|
- Interactive test page (shareable links, easy reproduction): https://markstream-vue.simonhe.me/test
|
||||||
|
- Docs: https://markstream-vue-docs.simonhe.me/guide/
|
||||||
|
- AI/LLM context (project map): https://markstream-vue-docs.simonhe.me/llms
|
||||||
|
- AI/LLM context (中文): https://markstream-vue-docs.simonhe.me/llms.zh-CN
|
||||||
|
- One-click StackBlitz demo: https://stackblitz.com/github/Simon-He95/markstream-vue?file=playground/src/App.vue
|
||||||
|
- Changelog: [CHANGELOG.md](./CHANGELOG.md)
|
||||||
|
- Nuxt playground: `pnpm play:nuxt`
|
||||||
|
- Discord: https://discord.gg/vkzdkjeRCW
|
||||||
|
|
||||||
|
## CLI helpers for skills and prompts
|
||||||
|
|
||||||
|
If you want the packaged AI assets without cloning the repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills add Simon-He95/markstream-vue
|
||||||
|
npx markstream-vue skills list
|
||||||
|
npx markstream-vue skills install
|
||||||
|
npx markstream-vue prompts list
|
||||||
|
npx markstream-vue prompts show install-markstream
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended usage:
|
||||||
|
|
||||||
|
- `npx skills add Simon-He95/markstream-vue` is the primary path for Codex-compatible skill discovery because it reads `.agents/skills` directly from the GitHub repository
|
||||||
|
- `skills install` installs the bundled skills into your agent skill directory (default: `~/.agents/skills`)
|
||||||
|
- `prompts list` and `prompts show` to discover and copy maintained prompt templates
|
||||||
|
|
||||||
|
Other `npx skills add` forms also work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full GitHub URL
|
||||||
|
npx skills add https://github.com/Simon-He95/markstream-vue
|
||||||
|
|
||||||
|
# Direct path to one skill in this repo
|
||||||
|
npx skills add https://github.com/Simon-He95/markstream-vue/tree/main/.agents/skills/markstream-install
|
||||||
|
|
||||||
|
# Any git URL
|
||||||
|
npx skills add git@github.com:Simon-He95/markstream-vue.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💬 Community & support
|
||||||
|
|
||||||
|
- Discussions: https://github.com/Simon-He95/markstream-vue/discussions
|
||||||
|
- Discord: https://discord.gg/vkzdkjeRCW
|
||||||
|
- Issues: please use templates and attach a repro link (https://markstream-vue.simonhe.me/test)
|
||||||
|
|
||||||
|
The test page gives you an editor + live preview plus “generate share link” that encodes the input in the URL (with a fallback to open directly or pre-fill a GitHub Issue for long payloads).
|
||||||
|
|
||||||
|
## Support the project
|
||||||
|
|
||||||
|
If markstream-vue helps your work, you can support ongoing maintenance with one of these QR codes.
|
||||||
|
|
||||||
|
| Alipay | WeChat Pay |
|
||||||
|
| --- | --- |
|
||||||
|
| <img src="https://raw.githubusercontent.com/Simon-He95/markstream-vue/main/docs/public/sponsor/zhifubao.jpg" alt="Alipay QR code" width="240" /> | <img src="https://raw.githubusercontent.com/Simon-He95/markstream-vue/main/docs/public/sponsor/weixin.jpg" alt="WeChat Pay QR code" width="240" /> |
|
||||||
|
|
||||||
|
## ⚡ Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add markstream-vue
|
||||||
|
# npm install markstream-vue
|
||||||
|
# yarn add markstream-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
// main.ts
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import 'markstream-vue/index.css'
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
components: { MarkdownRender },
|
||||||
|
template: '<MarkdownRender custom-id="docs" :content="doc" />',
|
||||||
|
setup() {
|
||||||
|
const doc = '# Hello from markstream-vue\\n\\nSupports **streaming** nodes.'
|
||||||
|
return { doc }
|
||||||
|
},
|
||||||
|
}).mount('#app')
|
||||||
|
```
|
||||||
|
|
||||||
|
Import `markstream-vue/index.css` after your reset (e.g., Tailwind `@layer components`) so renderer styles win over utility classes. Install optional peers such as `stream-monaco`, `shiki`, `stream-markdown`, `mermaid`, and `katex` only when you need Monaco code blocks, Shiki highlighting, diagrams, or math.
|
||||||
|
If your app intentionally scales root font size on mobile, use `markstream-vue/index.px.css` to avoid `rem`-based global scaling side effects.
|
||||||
|
|
||||||
|
Renderer CSS is scoped under an internal `.markstream-vue` container to minimize global style conflicts. If you render exported node components outside of `MarkdownRender`, wrap them in an element with class `markstream-vue`.
|
||||||
|
|
||||||
|
For dark theme variables, either add a `.dark` class on an ancestor, or pass `:is-dark="true"` to `MarkdownRender` to scope dark mode to the renderer.
|
||||||
|
|
||||||
|
Prefer the unified code-block `theme` prop for new integrations. When you render through `MarkdownRender`, pass it via `code-block-props`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MarkdownRender
|
||||||
|
:is-dark="isDark"
|
||||||
|
:code-block-props="{ theme: { light: 'vitesse-light', dark: 'vitesse-dark' } }"
|
||||||
|
:content="doc"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Language icons use the built-in `material` theme by default. Advanced integrations can inspect or switch icon themes with the exported helpers, or set an initial theme with `app.use(VueRendererMarkdown, { iconTheme })`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getRegisteredThemes, setIconTheme } from 'markstream-vue'
|
||||||
|
|
||||||
|
console.log(getRegisteredThemes()) // ['material']
|
||||||
|
setIconTheme('material')
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `registerIconTheme()` if you want to add your own icon pack.
|
||||||
|
|
||||||
|
Enable heavy peers only when needed:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { enableKatex, enableMermaid } from 'markstream-vue'
|
||||||
|
import 'markstream-vue/index.css'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
|
// after you install `mermaid` / `katex` peers
|
||||||
|
enableMermaid()
|
||||||
|
enableKatex()
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Optional: CDN-backed workers (KaTeX / Mermaid)</summary>
|
||||||
|
|
||||||
|
If you load KaTeX via CDN and want KaTeX rendering in a Web Worker (no bundler / optional peer not installed), inject a CDN-backed worker:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createKaTeXWorkerFromCDN, setKaTeXWorker } from 'markstream-vue'
|
||||||
|
|
||||||
|
const { worker } = createKaTeXWorkerFromCDN({
|
||||||
|
mode: 'classic',
|
||||||
|
// UMD builds used by importScripts() inside the worker
|
||||||
|
katexUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js',
|
||||||
|
mhchemUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/mhchem.min.js',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (worker)
|
||||||
|
setKaTeXWorker(worker)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you load Mermaid via CDN and want off-main-thread parsing (used by progressive Mermaid rendering), inject a Mermaid parser worker:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createMermaidWorkerFromCDN, setMermaidWorker } from 'markstream-vue'
|
||||||
|
|
||||||
|
const { worker } = createMermaidWorkerFromCDN({
|
||||||
|
// Mermaid CDN builds are commonly ESM; module worker is recommended.
|
||||||
|
mode: 'module',
|
||||||
|
workerOptions: { type: 'module' },
|
||||||
|
mermaidUrl: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (worker)
|
||||||
|
setMermaidWorker(worker)
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Nuxt quick drop-in</summary>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// plugins/markstream-vue.client.ts
|
||||||
|
import { defineNuxtPlugin } from '#app'
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
import 'markstream-vue/index.css'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
nuxtApp.vueApp.component('MarkdownRender', MarkdownRender)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use `<MarkdownRender :content="md" />` in your pages.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 🛠️ Common commands
|
||||||
|
|
||||||
|
- `pnpm dev` — playground dev server
|
||||||
|
- `pnpm play:nuxt` — Nuxt playground dev
|
||||||
|
- `pnpm build` — library + CSS build
|
||||||
|
- `pnpm build:analyze` — build with bundle visualizer reports (`bundle-visualizer.html`, `bundle-visualizer-tailwind.html`)
|
||||||
|
- `pnpm size:check` — run dist + npm package size budget checks (same guard used in CI)
|
||||||
|
- `pnpm test` — Vitest suite (`pnpm test:update` for snapshots)
|
||||||
|
- `pnpm typecheck` / `pnpm lint` — type and lint checks
|
||||||
|
|
||||||
|
## ⏱️ Streaming in 30 seconds
|
||||||
|
|
||||||
|
Render streamed Markdown (SSE/websocket) with incremental updates:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ParsedNode } from 'markstream-vue'
|
||||||
|
import MarkdownRender, { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const nodes = ref<ParsedNode[]>([])
|
||||||
|
const buffer = ref('')
|
||||||
|
const md = getMarkdown()
|
||||||
|
|
||||||
|
function addChunk(chunk: string) {
|
||||||
|
buffer.value += chunk
|
||||||
|
nodes.value = parseMarkdownToStructure(buffer.value, md)
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g., inside your SSE/onmessage handler
|
||||||
|
eventSource.onmessage = event => addChunk(event.data)
|
||||||
|
|
||||||
|
// template
|
||||||
|
// <MarkdownRender
|
||||||
|
// :nodes="nodes"
|
||||||
|
// :max-live-nodes="0"
|
||||||
|
// :batch-rendering="{
|
||||||
|
// renderBatchSize: 16,
|
||||||
|
// renderBatchDelay: 8,
|
||||||
|
// }"
|
||||||
|
// />
|
||||||
|
```
|
||||||
|
|
||||||
|
Switch rendering style per surface:
|
||||||
|
|
||||||
|
- Virtualized window (default): steady scrolling and memory usage for long docs.
|
||||||
|
- Incremental batches: set `:max-live-nodes="0"` for AI-like “typing” with lightweight placeholders.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Advanced: SSR / worker / streaming handoff</summary>
|
||||||
|
|
||||||
|
### SSR / Worker usage (deterministic output)
|
||||||
|
|
||||||
|
Pre-parse Markdown on the server or in a worker and render typed nodes on the client:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// server or worker
|
||||||
|
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||||
|
|
||||||
|
const md = getMarkdown()
|
||||||
|
const nodes = parseMarkdownToStructure('# Hello\n\nThis is parsed once', md)
|
||||||
|
// send `nodes` JSON to the client
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- client -->
|
||||||
|
<MarkdownRender :nodes="nodesFromServer" />
|
||||||
|
```
|
||||||
|
|
||||||
|
This avoids client-side parsing and keeps SSR/hydration deterministic.
|
||||||
|
|
||||||
|
### Hybrid: SSR + streaming handoff
|
||||||
|
|
||||||
|
- Server: parse the first Markdown batch to nodes and serialize `initialNodes` (and the raw `initialMarkdown` if you also stream later chunks).
|
||||||
|
- Client: hydrate with the same parser options, then keep streaming:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ParsedNode } from 'markstream-vue'
|
||||||
|
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const nodes = ref<ParsedNode[]>(initialNodes)
|
||||||
|
const buffer = ref(initialMarkdown)
|
||||||
|
const md = getMarkdown() // match server setup
|
||||||
|
|
||||||
|
function addChunk(chunk: string) {
|
||||||
|
buffer.value += chunk
|
||||||
|
nodes.value = parseMarkdownToStructure(buffer.value, md)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This avoids re-parsing SSR content while letting later SSE/WebSocket chunks continue the stream.
|
||||||
|
|
||||||
|
> Tip: when you know the stream has ended (the message is complete), use `parseMarkdownToStructure(buffer.value, md, { final: true })` or pass `:final="true"` to the component. This disables mid-state (`loading`) parsing so trailing delimiters (like `$$` or an unclosed code fence) won’t get stuck showing perpetual loading.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## ⚙️ Performance presets
|
||||||
|
|
||||||
|
- **Virtual window (default)** – keep `max-live-nodes` at its default `320` to enable virtualization. Nodes render immediately and the renderer keeps a sliding window of elements mounted so long docs remain responsive without showing skeleton placeholders.
|
||||||
|
- **Incremental stream** – set `:max-live-nodes="0"` when you want a true typewriter effect. This disables virtualization and turns on incremental batching governed by `batchRendering`, `initialRenderBatchSize`, `renderBatchSize`, `renderBatchDelay`, and `renderBatchBudgetMs`, so new content flows in small slices with lightweight placeholders.
|
||||||
|
|
||||||
|
Pick one mode per surface: virtualization for best scrollback and steady memory usage, or incremental batching for AI-style “typing” previews.
|
||||||
|
|
||||||
|
> Tip: In chats, combine `max-live-nodes="0"` with small `renderBatchSize` (e.g., `16`) and a tiny `renderBatchDelay` (e.g., `8ms`) to keep the “typing” feel smooth without jumping large chunks. Tune `renderBatchBudgetMs` down if you need to cap CPU per frame.
|
||||||
|
|
||||||
|
## 🧰 Key props & options (cheatsheet)
|
||||||
|
|
||||||
|
- `content` vs `nodes`: pass raw Markdown or pre-parsed nodes (from `parseMarkdownToStructure`).
|
||||||
|
- `max-live-nodes`: `320` (default virtualization) or `0` (incremental batches).
|
||||||
|
- `batchRendering`: fine-tune batches with `initialRenderBatchSize`, `renderBatchSize`, `renderBatchDelay`, `renderBatchBudgetMs`.
|
||||||
|
- `enableMermaid` / `enableKatex`: (re)enable heavy peers or custom loaders when needed (pairs with `disableMermaid` / `disableKatex`).
|
||||||
|
- `parse-options`: reuse parser hooks (e.g., `preTransformTokens`, `requireClosingStrong`) on the component.
|
||||||
|
- `final`: marks end-of-stream; disables mid-state loading parsing and forces unfinished constructs to settle.
|
||||||
|
- `custom-html-tags`: extend streaming HTML allowlist for custom tags and emit them as custom nodes for `setCustomComponents` (e.g., `['thinking']`).
|
||||||
|
- `setCustomComponents(customId?, mapping)`: register inline Vue components for custom tags/markers (scoped by `custom-id` when provided).
|
||||||
|
|
||||||
|
Example: map Markdown placeholders to Vue components (scoped)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
CALLOUT: () => import('./components/Callout.vue'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Markdown: [[CALLOUT:warning title="Heads up" body="Details here"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the same `custom-id` on the renderer:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MarkdownRender
|
||||||
|
:content="doc"
|
||||||
|
custom-id="docs"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse hooks example (match server + client):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MarkdownRender
|
||||||
|
:content="doc"
|
||||||
|
:parse-options="{
|
||||||
|
requireClosingStrong: true,
|
||||||
|
preTransformTokens: (tokens) => tokens,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔥 Where it shines
|
||||||
|
|
||||||
|
- AI/chat UIs with long-form answers and Markdown tokens arriving over SSE/websocket.
|
||||||
|
- Docs, changelogs, and knowledge bases that need instant load but stay responsive as they grow.
|
||||||
|
- Streaming diffs and code review panes that benefit from Monaco live updates.
|
||||||
|
- Diagram-heavy content that should render progressively (Mermaid) without blocking.
|
||||||
|
- Embedding Vue components in Markdown-driven surfaces (callouts, widgets, CTA buttons).
|
||||||
|
|
||||||
|
## ❓ FAQ (quick answers)
|
||||||
|
|
||||||
|
- Mermaid/KaTeX not rendering? Install the peer (`mermaid` / `katex`) and pass `:enable-mermaid="true"` / `:enable-katex="true"` or call the loader setters. If you load them via CDN script tags, the library will also pick up `window.mermaid` / `window.katex`.
|
||||||
|
- CDN + KaTeX worker: if you don't bundle `katex` but still want off-main-thread rendering, create and inject a worker that loads KaTeX via CDN (UMD) using `createKaTeXWorkerFromCDN()` + `setKaTeXWorker()`.
|
||||||
|
- Bundle size: peers are optional and not bundled; import only `markstream-vue/index.css` once; use Shiki (`MarkdownCodeBlockNode`) when Monaco is too heavy. Infrequent language icons are split into an async chunk and load on demand; call `preloadExtendedLanguageIcons()` during app idle if you want to avoid first-hit icon fallback.
|
||||||
|
- Custom UI: register components via `setCustomComponents` (global or scoped), then emit markers/placeholders in Markdown and map them to Vue components.
|
||||||
|
|
||||||
|
## 🆚 Why markstream-vue over a typical Markdown renderer?
|
||||||
|
|
||||||
|
| Needs | Typical Markdown preview | markstream-vue |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Streaming input | Re-renders whole tree, flashes | Incremental batches with virtual windowing |
|
||||||
|
| Large code blocks | Slow re-highlight | Monaco streaming updates + Shiki option |
|
||||||
|
| Diagrams | Blocks while parsing | Progressive Mermaid with graceful fallback |
|
||||||
|
| Custom UI | Limited slots | Inline Vue components & typed nodes |
|
||||||
|
| Long docs | Memory spikes | Configurable live-node cap for steady usage |
|
||||||
|
|
||||||
|
## 🗺️ Roadmap (snapshot)
|
||||||
|
|
||||||
|
- More “instant start” templates (Vite + Nuxt + Tailwind) and updated StackBlitz.
|
||||||
|
- Additional codeblock presets (diff-friendly Shiki themes, Monaco decoration helpers).
|
||||||
|
- Cookbook docs for AI/chat patterns (SSE/WebSocket, retry/resume, markdown mid-states).
|
||||||
|
- More showcase examples for embedding Vue components inside Markdown surfaces.
|
||||||
|
|
||||||
|
## 📦 Releases
|
||||||
|
|
||||||
|
- Latest: [Releases](https://github.com/Simon-He95/markstream-vue/releases) — see highlights and upgrade notes.
|
||||||
|
- Full history: [CHANGELOG.md](./CHANGELOG.md)
|
||||||
|
- Recent highlights (0.0.3-beta.1/beta.0):
|
||||||
|
- Parser bumped to `stream-markdown-parser@0.0.36` for parsing fixes.
|
||||||
|
- Monaco upgrades with more languages/themes + diff-friendly code block tweaks.
|
||||||
|
- HTML/SVG preview dialog and AST debug view in the playground.
|
||||||
|
|
||||||
|
## 🧭 Showcase & examples
|
||||||
|
|
||||||
|
Build something with markstream-vue? Open a PR to add it here (include a link + 1 screenshot/GIF). Ideal fits: AI/chat UIs, streaming docs, diff/code-review panes, or Markdown-driven pages with embedded Vue components.
|
||||||
|
|
||||||
|
- **FlowNote** — streaming Markdown note app demo (SSE + virtual window) — https://markstream-vue.simonhe.me/
|
||||||
|
- **AI Chat surface** — playground “test” page showing incremental batches + share links — https://markstream-vue.simonhe.me/test
|
||||||
|
|
||||||
|
## 📺 Introduction Video
|
||||||
|
|
||||||
|
A short video introduces the key features and usage of markstream-vue:
|
||||||
|
|
||||||
|
[](https://www.bilibili.com/video/BV17Z4qzpE9c/)
|
||||||
|
|
||||||
|
Watch on Bilibili: [Open in Bilibili](https://www.bilibili.com/video/BV17Z4qzpE9c/)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ⚡ Extreme performance: minimal re-rendering and efficient DOM updates for streaming scenarios
|
||||||
|
- 🌊 Streaming-first: native support for incomplete or frequently updated tokenized Markdown
|
||||||
|
- 🧠 Monaco streaming updates: high-performance Monaco integration for smooth incremental updates of large code blocks
|
||||||
|
- 🪄 Progressive Mermaid: charts render instantly when syntax is available, and improve with later updates
|
||||||
|
- 🧩 Custom components: embed custom Vue components in Markdown content
|
||||||
|
- 📝 Full Markdown support: tables, formulas, emoji, checkboxes, code blocks, etc.
|
||||||
|
- 🔄 Real-time updates: supports incremental content without breaking formatting
|
||||||
|
- 📦 TypeScript-first: complete type definitions and IntelliSense
|
||||||
|
- 🔌 Zero config: works out of the box in Vue 3 projects
|
||||||
|
- 🎨 Flexible code block rendering: choose Monaco editor (`CodeBlockNode`) or lightweight Shiki highlighting (`MarkdownCodeBlockNode`)
|
||||||
|
- 🧰 Parser toolkit: [`stream-markdown-parser`](./packages/markdown-parser) now documents how to reuse the parser in workers/SSE streams and feed `<MarkdownRender :nodes>` directly, plus APIs for registering global plugins and custom math helpers.
|
||||||
|
|
||||||
|
## 🙌 Contributing & community
|
||||||
|
|
||||||
|
- Read the contributor guide: [CONTRIBUTING.md](./CONTRIBUTING.md) and follow the PR template.
|
||||||
|
- Be kind and follow the [Code of Conduct](./CODE_OF_CONDUCT.md).
|
||||||
|
- Issues: use templates for bugs/requests; attach a repro from https://markstream-vue.simonhe.me/test when possible.
|
||||||
|
- Questions? Start a discussion: https://github.com/Simon-He95/markstream-vue/discussions
|
||||||
|
- Chat live: Discord https://discord.gg/vkzdkjeRCW
|
||||||
|
- Looking to contribute? Start with [good first issues](https://github.com/Simon-He95/markstream-vue/labels/good%20first%20issue).
|
||||||
|
- Support guide: [SUPPORT.md](./SUPPORT.md)
|
||||||
|
- PRs: follow Conventional Commits, add tests for parser/render changes, and include screenshots/GIFs for UI tweaks.
|
||||||
|
- If the project helps you, consider starring and sharing the repo — it keeps the work sustainable.
|
||||||
|
- Security: see [SECURITY.md](./SECURITY.md) to report vulnerabilities privately.
|
||||||
|
|
||||||
|
### Quick ways to help
|
||||||
|
|
||||||
|
- Add repro links/screenshots to existing issues.
|
||||||
|
- Improve docs/examples (especially streaming + SSR/worker patterns).
|
||||||
|
- Share playground/test links that showcase performance edge cases.
|
||||||
|
|
||||||
|
## Troubleshooting & Common Issues
|
||||||
|
|
||||||
|
Troubleshooting has moved into the docs:
|
||||||
|
https://markstream-vue-docs.simonhe.me/guide/troubleshooting
|
||||||
|
|
||||||
|
If you can't find a solution there, open a GitHub issue:
|
||||||
|
https://github.com/Simon-He95/markstream-vue/issues
|
||||||
|
|
||||||
|
### Report an issue quickly
|
||||||
|
|
||||||
|
1. Reproduce in the test page and click “generate share link”: https://markstream-vue.simonhe.me/test
|
||||||
|
2. Open a bug report with the link and a screenshot: https://github.com/Simon-He95/markstream-vue/issues/new?template=bug_report.yml
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
Thanks to all the people who have contributed to this project!
|
||||||
|
|
||||||
|
[](https://github.com/Simon-He95/markstream-vue/graphs/contributors)
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
This project uses and benefits from:
|
||||||
|
- [stream-monaco](https://github.com/Simon-He95/stream-monaco)
|
||||||
|
- [stream-markdown](https://github.com/Simon-He95/stream-markdown)
|
||||||
|
- [mermaid](https://mermaid-js.github.io/mermaid)
|
||||||
|
- [katex](https://katex.org/)
|
||||||
|
- [shiki](https://github.com/shikijs/shiki)
|
||||||
|
- [markdown-it-ts](https://github.com/Simon-He95/markdown-it-ts)
|
||||||
|
|
||||||
|
Thanks to the authors and contributors of these projects!
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#Simon-He95/markstream-vue&Date)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./license) © [Simon He](https://github.com/Simon-He95)
|
||||||
@@ -0,0 +1,528 @@
|
|||||||
|
# markstream-vue
|
||||||
|
|
||||||
|
> 针对 Vue 3 的高性能、流式友好型 Markdown 渲染组件 — 支持渐进式 Mermaid、流式 diff 代码块以及为大文档优化的实时预览。
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/markstream-vue)
|
||||||
|
[](README.md)
|
||||||
|
[](https://markstream-vue-docs.simonhe.me/zh/guide/)
|
||||||
|
[](https://markstream-vue.simonhe.me/)
|
||||||
|
[](https://markstream-vue.simonhe.me/test)
|
||||||
|
[](https://www.npmjs.com/package/markstream-vue)
|
||||||
|
[](https://bundlephobia.com/package/markstream-vue)
|
||||||
|
[](https://github.com/Simon-He95/markstream-vue/releases)
|
||||||
|
[](https://github.com/Simon-He95/markstream-vue/discussions)
|
||||||
|
[](https://discord.gg/vkzdkjeRCW)
|
||||||
|
[](./SUPPORT.md)
|
||||||
|
[](./SECURITY.md)
|
||||||
|
[](https://github.com/Simon-He95/markstream-vue/actions/workflows/ci.yml)
|
||||||
|
[](./license)
|
||||||
|
|
||||||
|
需要其他框架/版本?
|
||||||
|
|
||||||
|
- Vue 2.6:请使用 [`markstream-vue2`](./packages/markstream-vue2/README.md)(基础移植版,功能更精简)
|
||||||
|
- React:请参考 `packages/markstream-react` 的说明 [`packages/markstream-react/README.md`](./packages/markstream-react/README.md)(初步移植版)
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [速览](#速览)
|
||||||
|
- [按场景选择入口](#按场景选择入口)
|
||||||
|
- [立即试用](#-立即试用)
|
||||||
|
- [社区与支持](#-社区与支持)
|
||||||
|
- [快速上手](#-快速上手)
|
||||||
|
- [常用命令](#-常用命令)
|
||||||
|
- [30 秒流式接入](#-30-秒流式接入)
|
||||||
|
- [性能模式](#-性能模式)
|
||||||
|
- [关键属性速览](#-关键属性速览)
|
||||||
|
- [适用场景](#-适用场景)
|
||||||
|
- [快问快答](#-快问快答)
|
||||||
|
- [为什么选择 markstream-vue](#-为什么选择-markstream-vue而不是普通-markdown-渲染器)
|
||||||
|
- [Roadmap](#-roadmap快照)
|
||||||
|
- [发布](#-发布)
|
||||||
|
- [案例与展示](#-案例与展示)
|
||||||
|
- [介绍视频](#介绍视频)
|
||||||
|
- [核心特性](#核心特性)
|
||||||
|
- [贡献与社区](#-贡献与社区)
|
||||||
|
- [故障排查](#故障排查--常见问题)
|
||||||
|
- [鸣谢](#鸣谢)
|
||||||
|
- [Star 历史](#star-历史)
|
||||||
|
- [许可](#许可)
|
||||||
|
|
||||||
|
> 📖 所有详细文档、API、示例和高级用法已迁移至 VitePress 中文文档站点:
|
||||||
|
> https://markstream-vue-docs.simonhe.me/zh/guide/
|
||||||
|
|
||||||
|
## 速览
|
||||||
|
|
||||||
|
- 为 **流式 Markdown**(AI/聊天/SSE)打造,避免闪烁,内存可预期。
|
||||||
|
- **双渲染模式**:长文档虚拟化窗口,或“打字机”式增量批次。
|
||||||
|
- **渐进式图表**(Mermaid)与 **流式代码块**(Monaco/Shiki),跟上 diff/增量输出。
|
||||||
|
- 同时支持 **Markdown 字符串或预解析节点**,可在 Markdown 中嵌入 **自定义 Vue 组件**。
|
||||||
|
- TypeScript 优先,开箱默认即可上线(导入 CSS 即用)。
|
||||||
|
|
||||||
|
## 按场景选择入口
|
||||||
|
|
||||||
|
| 如果你现在想做的是... | 先看这里 | 然后看 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 先把第一段渲染跑起来 | [快速上手](#-快速上手) | [安装指南](https://markstream-vue-docs.simonhe.me/zh/guide/installation) |
|
||||||
|
| 接到文档站或 VitePress 主题里 | [文档站与 VitePress 集成](https://markstream-vue-docs.simonhe.me/zh/guide/vitepress-docs-integration) | [自定义标签与高级组件](https://markstream-vue-docs.simonhe.me/zh/guide/custom-components) |
|
||||||
|
| 做 AI 聊天界面或 SSE 流式输出 | [AI 聊天与流式输出](https://markstream-vue-docs.simonhe.me/zh/guide/ai-chat-streaming) | [性能](https://markstream-vue-docs.simonhe.me/zh/guide/performance) |
|
||||||
|
| 替换一个内置节点渲染器 | [覆盖内置组件](https://markstream-vue-docs.simonhe.me/zh/guide/component-overrides) | [渲染器与节点组件](https://markstream-vue-docs.simonhe.me/zh/guide/components) |
|
||||||
|
| 增加 `thinking` 这类可信标签 | [自定义标签与高级组件](https://markstream-vue-docs.simonhe.me/zh/guide/custom-components) | [API 参考](https://markstream-vue-docs.simonhe.me/zh/guide/api) |
|
||||||
|
| 接入坏了但还不知道原因 | [按症状排查](https://markstream-vue-docs.simonhe.me/zh/guide/troubleshooting-path) | [排查问题](https://markstream-vue-docs.simonhe.me/zh/guide/troubleshooting) |
|
||||||
|
|
||||||
|
## 🚀 立即试用
|
||||||
|
|
||||||
|
- Playground(交互演示): https://markstream-vue.simonhe.me/
|
||||||
|
- 交互测试页(可分享链接,便于复现): https://markstream-vue.simonhe.me/test
|
||||||
|
- 文档: https://markstream-vue-docs.simonhe.me/zh/guide/
|
||||||
|
- AI/LLM 项目索引(中文): https://markstream-vue-docs.simonhe.me/llms.zh-CN
|
||||||
|
- AI/LLM 项目索引(英文): https://markstream-vue-docs.simonhe.me/llms
|
||||||
|
- 一键 StackBlitz 体验: https://stackblitz.com/github/Simon-He95/markstream-vue?file=playground/src/App.vue
|
||||||
|
- 更新日志: [CHANGELOG.md](./CHANGELOG.md)
|
||||||
|
- Nuxt playground:`pnpm play:nuxt`
|
||||||
|
- Discord: https://discord.gg/vkzdkjeRCW
|
||||||
|
|
||||||
|
## skills 和 prompts 的 CLI
|
||||||
|
|
||||||
|
如果你想直接拿到打包后的 AI 资产,而不是先克隆仓库:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills add Simon-He95/markstream-vue
|
||||||
|
npx markstream-vue skills list
|
||||||
|
npx markstream-vue skills install
|
||||||
|
npx markstream-vue prompts list
|
||||||
|
npx markstream-vue prompts show install-markstream
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐这样理解:
|
||||||
|
|
||||||
|
- `npx skills add Simon-He95/markstream-vue` 是最推荐的安装方式,因为它会直接读取 GitHub 仓库里的 `.agents/skills`
|
||||||
|
- `skills install` 会把打包好的 skills 安装到你的 agent skills 目录,默认是 `~/.agents/skills`
|
||||||
|
- `prompts list` / `prompts show` 用来发现并直接复制官方维护的 prompt 模板
|
||||||
|
|
||||||
|
`npx skills add` 也支持这些来源:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 完整 GitHub URL
|
||||||
|
npx skills add https://github.com/Simon-He95/markstream-vue
|
||||||
|
|
||||||
|
# 仓库里的单个 skill 直链
|
||||||
|
npx skills add https://github.com/Simon-He95/markstream-vue/tree/main/.agents/skills/markstream-install
|
||||||
|
|
||||||
|
# 任意 git URL
|
||||||
|
npx skills add git@github.com:Simon-He95/markstream-vue.git
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💬 社区与支持
|
||||||
|
|
||||||
|
- Discussions:https://github.com/Simon-He95/markstream-vue/discussions
|
||||||
|
- Discord:https://discord.gg/vkzdkjeRCW
|
||||||
|
- Issues:请使用模板并附上复现链接(https://markstream-vue.simonhe.me/test)
|
||||||
|
|
||||||
|
测试页内置编辑器 + 实时预览,并提供“生成分享链接”功能(过长内容会回退为直接打开或预填 GitHub Issue)。
|
||||||
|
|
||||||
|
## 支持项目
|
||||||
|
|
||||||
|
如果 markstream-vue 对你的工作有帮助,欢迎通过下面的收款码支持项目的持续维护。
|
||||||
|
|
||||||
|
| 支付宝 | 微信收款 |
|
||||||
|
| --- | --- |
|
||||||
|
| <img src="https://raw.githubusercontent.com/Simon-He95/markstream-vue/main/docs/public/sponsor/zhifubao.jpg" alt="支付宝收款码" width="240" /> | <img src="https://raw.githubusercontent.com/Simon-He95/markstream-vue/main/docs/public/sponsor/weixin.jpg" alt="微信收款码" width="240" /> |
|
||||||
|
|
||||||
|
## ⚡ 快速上手
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add markstream-vue
|
||||||
|
# npm install markstream-vue
|
||||||
|
# yarn add markstream-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
// main.ts
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import 'markstream-vue/index.css'
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
components: { MarkdownRender },
|
||||||
|
template: '<MarkdownRender custom-id="docs" :content="doc" />',
|
||||||
|
setup() {
|
||||||
|
const doc = '# Hello markstream-vue\\n\\n支持 **流式** 节点。'
|
||||||
|
return { doc }
|
||||||
|
},
|
||||||
|
}).mount('#app')
|
||||||
|
```
|
||||||
|
|
||||||
|
确保在 CSS reset(如 `@tailwind base` 或 `@unocss/reset`)之后导入 `markstream-vue/index.css`,最好放在 `@layer components` 中以避免 Tailwind/UnoCSS 覆盖组件样式。根据需求再按需安装可选 peer 依赖:`stream-monaco`(Monaco 代码块)、`shiki` + `stream-markdown`(Shiki 高亮)、`mermaid`(Mermaid 图表)、`katex`(数学公式)。
|
||||||
|
如果你的移动端会主动调大根字号(`html`/`body`),建议改用 `markstream-vue/index.px.css`,避免 `rem` 跟随根字号导致整体放大。
|
||||||
|
|
||||||
|
渲染器的 CSS 会作用于内部 `.markstream-vue` 容器下,以尽量降低对全局的影响;如果你脱离 `MarkdownRender` 单独使用导出的节点组件,请在外层包一层带 `markstream-vue` 类名的容器。
|
||||||
|
|
||||||
|
暗色变量可以通过给祖先节点加 `.dark`,或直接给 `MarkdownRender` 传入 `:is-dark="true"`(仅对渲染器生效)。
|
||||||
|
|
||||||
|
新的接入建议优先使用统一代码块 `theme` prop。如果你是通过 `MarkdownRender` 统一下发,可以放到 `code-block-props`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MarkdownRender
|
||||||
|
:is-dark="isDark"
|
||||||
|
:code-block-props="{ theme: { light: 'vitesse-light', dark: 'vitesse-dark' } }"
|
||||||
|
:content="doc"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
语言图标默认使用内置的 `material` theme。进阶接入可以用导出的 helper 查看或切换 icon theme,也可以在 `app.use(VueRendererMarkdown, { iconTheme })` 时指定初始主题:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getRegisteredThemes, setIconTheme } from 'markstream-vue'
|
||||||
|
|
||||||
|
console.log(getRegisteredThemes()) // ['material']
|
||||||
|
setIconTheme('material')
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你需要接自己的图标包,可以继续使用 `registerIconTheme()` 注册。
|
||||||
|
|
||||||
|
按需启用重型依赖:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { enableKatex, enableMermaid } from 'markstream-vue'
|
||||||
|
import 'markstream-vue/index.css'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
|
// 安装对应 peer 后再启用
|
||||||
|
enableMermaid()
|
||||||
|
enableKatex()
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>可选:CDN Worker(KaTeX / Mermaid)</summary>
|
||||||
|
|
||||||
|
如果你是用 CDN 引入 KaTeX,并且希望公式在 Web Worker 中渲染(不打包 / 不安装可选 peer),可以注入一个“CDN 加载 KaTeX”的 worker:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createKaTeXWorkerFromCDN, setKaTeXWorker } from 'markstream-vue'
|
||||||
|
|
||||||
|
const { worker } = createKaTeXWorkerFromCDN({
|
||||||
|
mode: 'classic',
|
||||||
|
// worker 内通过 importScripts() 加载的 UMD 构建
|
||||||
|
katexUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js',
|
||||||
|
mhchemUrl: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/mhchem.min.js',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (worker)
|
||||||
|
setKaTeXWorker(worker)
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你是用 CDN 引入 Mermaid,并且希望 Mermaid 的解析在 worker 中进行(用于渐进式 Mermaid 渲染的后台解析),可以注入 Mermaid parser worker:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createMermaidWorkerFromCDN, setMermaidWorker } from 'markstream-vue'
|
||||||
|
|
||||||
|
const { worker } = createMermaidWorkerFromCDN({
|
||||||
|
// Mermaid CDN 构建通常是 ESM,推荐 module worker。
|
||||||
|
mode: 'module',
|
||||||
|
workerOptions: { type: 'module' },
|
||||||
|
mermaidUrl: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (worker)
|
||||||
|
setMermaidWorker(worker)
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Nuxt 快速接入</summary>
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// plugins/markstream-vue.client.ts
|
||||||
|
import { defineNuxtPlugin } from '#app'
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
import 'markstream-vue/index.css'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
nuxtApp.vueApp.component('MarkdownRender', MarkdownRender)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在页面中直接使用 `<MarkdownRender :content=\"md\" />`。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 🛠️ 常用命令
|
||||||
|
|
||||||
|
- `pnpm dev` — playground 开发
|
||||||
|
- `pnpm play:nuxt` — Nuxt playground 开发
|
||||||
|
- `pnpm build` — 构建库与 CSS
|
||||||
|
- `pnpm build:analyze` — 带可视化报告的构建(输出 `bundle-visualizer.html`、`bundle-visualizer-tailwind.html`)
|
||||||
|
- `pnpm size:check` — 本地执行 dist + npm 包体积预算检查(与 CI 一致)
|
||||||
|
- `pnpm test` — Vitest 测试(快照用 `pnpm test:update`)
|
||||||
|
- `pnpm typecheck` / `pnpm lint` — 类型检查与 Lint
|
||||||
|
|
||||||
|
## ⏱️ 30 秒流式接入
|
||||||
|
|
||||||
|
用 SSE / WebSocket 增量渲染 Markdown:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ParsedNode } from 'markstream-vue'
|
||||||
|
import MarkdownRender, { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const nodes = ref<ParsedNode[]>([])
|
||||||
|
const buffer = ref('')
|
||||||
|
const md = getMarkdown()
|
||||||
|
|
||||||
|
function addChunk(chunk: string) {
|
||||||
|
buffer.value += chunk
|
||||||
|
nodes.value = parseMarkdownToStructure(buffer.value, md)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 例如在 SSE / onmessage 处理器中
|
||||||
|
eventSource.onmessage = event => addChunk(event.data)
|
||||||
|
|
||||||
|
// template
|
||||||
|
// <MarkdownRender
|
||||||
|
// :nodes="nodes"
|
||||||
|
// :max-live-nodes="0"
|
||||||
|
// :batch-rendering="{
|
||||||
|
// renderBatchSize: 16,
|
||||||
|
// renderBatchDelay: 8,
|
||||||
|
// }"
|
||||||
|
// />
|
||||||
|
```
|
||||||
|
|
||||||
|
按页面需要切换渲染风格:
|
||||||
|
|
||||||
|
- 虚拟化窗口(默认):长文档滚动平稳、内存稳定。
|
||||||
|
- 增量批次:将 `:max-live-nodes="0"`,获得更明显的“打字机”体验与轻量占位。
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>进阶:SSR / Worker / 流式续写</summary>
|
||||||
|
|
||||||
|
### SSR / Worker(确定性输出)
|
||||||
|
|
||||||
|
在服务端或 Worker 预解析 Markdown,前端直接渲染节点:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// server or worker
|
||||||
|
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||||
|
|
||||||
|
const md = getMarkdown()
|
||||||
|
const nodes = parseMarkdownToStructure('# Hello\n\n服务端解析一次', md)
|
||||||
|
// 将 nodes JSON 下发到客户端
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- client -->
|
||||||
|
<MarkdownRender :nodes="nodesFromServer" />
|
||||||
|
```
|
||||||
|
|
||||||
|
这样可以避免前端解析,保持 SSR/水合的一致性。
|
||||||
|
|
||||||
|
### 混合模式:SSR + 流式续写
|
||||||
|
|
||||||
|
- 服务端:解析首批 Markdown,序列化 `initialNodes`(以及 `initialMarkdown`,便于后续流式追加)。
|
||||||
|
- 客户端:用相同的解析配置水合,然后继续流式追加:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { ParsedNode } from 'markstream-vue'
|
||||||
|
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const nodes = ref<ParsedNode[]>(initialNodes)
|
||||||
|
const buffer = ref(initialMarkdown)
|
||||||
|
const md = getMarkdown() // 与服务端保持一致
|
||||||
|
|
||||||
|
function addChunk(chunk: string) {
|
||||||
|
buffer.value += chunk
|
||||||
|
nodes.value = parseMarkdownToStructure(buffer.value, md)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这样无需重新解析 SSR 内容,同时还能通过 SSE/WebSocket 持续追加后续片段。
|
||||||
|
|
||||||
|
> 提示:当你明确知道流已结束(消息已完整)时,建议用 `parseMarkdownToStructure(buffer.value, md, { final: true })` 或在组件上设置 `:final="true"`,以关闭解析器的中间态(loading)策略,避免末尾残留分隔符(如 `$$`、未闭合 code fence)导致永久 loading。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## ⚙️ 性能模式
|
||||||
|
|
||||||
|
- **默认虚拟化窗口**:保持 `max-live-nodes` 默认值(`320`),渲染器会立即渲染当前窗口的节点,同时只保留有限数量的 DOM 节点,实现平滑滚动与可控内存,占位骨架极少。
|
||||||
|
- **增量流式模式**:当需要更明显的“打字机”体验时,将 `:max-live-nodes="0"`。这会关闭虚拟化并启用 `batchRendering` 系列参数控制的增量渲染,新的节点会以小批次加上占位骨架的形式进入视图。
|
||||||
|
|
||||||
|
可根据页面类型选择最合适的模式:虚拟化适合长文档/回溯需求,增量流式适合聊天或 AI 输出面板。
|
||||||
|
|
||||||
|
> 小贴士:聊天场景可使用 `max-live-nodes="0"`,并将 `renderBatchSize` 调小(如 `16`),`renderBatchDelay` 设为较小值(如 `8ms`),获得平滑的“打字”节奏且避免大段跳变。如需限制单帧 CPU,可适当调低 `renderBatchBudgetMs`。
|
||||||
|
|
||||||
|
## 🧰 关键属性速览
|
||||||
|
|
||||||
|
- `content` 与 `nodes`:传原始 Markdown 或预解析节点(来自 `parseMarkdownToStructure`)。
|
||||||
|
- `max-live-nodes`:`320`(默认虚拟化)或 `0`(增量批次)。
|
||||||
|
- `batchRendering`:用 `initialRenderBatchSize`、`renderBatchSize`、`renderBatchDelay`、`renderBatchBudgetMs` 微调批次。
|
||||||
|
- `enableMermaid` / `enableKatex`:用于(重新)启用重型依赖或自定义 loader(可与 `disableMermaid` / `disableKatex` 配合)。
|
||||||
|
- `parse-options`:在组件上复用解析钩子(如 `preTransformTokens`、`requireClosingStrong`)。
|
||||||
|
- `final`:标记“最终态/流结束”,关闭中间态 loading 解析并强制收敛未闭合结构。
|
||||||
|
- `custom-html-tags`:扩展流式 HTML 白名单并将这些标签输出为自定义节点,便于 `setCustomComponents` 直接映射(如 `['thinking']`)。
|
||||||
|
- `setCustomComponents(customId?, mapping)`:为自定义标签/标记注册内嵌 Vue 组件(传 `custom-id` 可限定作用域)。
|
||||||
|
|
||||||
|
示例:将 Markdown 占位符映射到 Vue 组件(作用域)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
CALLOUT: () => import('./components/Callout.vue'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Markdown: [[CALLOUT:warning title="提示" body="具体内容"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
渲染时使用同一个 `custom-id`:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MarkdownRender
|
||||||
|
:content="doc"
|
||||||
|
custom-id="docs"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
解析钩子示例(服务端/客户端保持一致):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MarkdownRender
|
||||||
|
:content="doc"
|
||||||
|
:parse-options="{
|
||||||
|
requireClosingStrong: true,
|
||||||
|
preTransformTokens: (tokens) => tokens,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔥 适用场景
|
||||||
|
|
||||||
|
- AI / 聊天界面:Markdown token 通过 SSE/WebSocket 持续抵达,要求无闪烁与稳定内存。
|
||||||
|
- 文档、变更日志、知识库:需要即时加载,同时保持长内容滚动的流畅性。
|
||||||
|
- 流式 diff / 代码审查:Monaco 增量更新让大代码块也能跟上变更。
|
||||||
|
- 图表与示意:Mermaid 渐进式渲染,避免阻塞主渲染。
|
||||||
|
- Markdown 驱动的界面中嵌入 Vue 组件(callout、交互式挂件、CTA 等)。
|
||||||
|
|
||||||
|
## ❓ 快问快答
|
||||||
|
|
||||||
|
- Mermaid / KaTeX 不显示?安装对应 peer(`mermaid` / `katex`),并传入 `:enable-mermaid="true"` / `:enable-katex="true"` 或调用 loader 设置函数。如果你是用 CDN `<script>` 引入,库也会自动读取 `window.mermaid` / `window.katex`。
|
||||||
|
- CDN + KaTeX worker:如果你不打包 `katex` 但仍希望公式在 worker 中渲染(不占主线程),可以用 `createKaTeXWorkerFromCDN()` 创建一个“CDN 加载 KaTeX”的 worker,然后通过 `setKaTeXWorker()` 注入。
|
||||||
|
- 体积问题:可选 peer 不会被打包,CSS 只需导入一次;对代码块可用 Shiki(`MarkdownCodeBlockNode`)替代 Monaco。低频语言图标已拆分为异步 chunk 并按需加载;如果希望首屏就避免图标回退,可在空闲时调用 `preloadExtendedLanguageIcons()` 预热。
|
||||||
|
- 自定义 UI:通过 `setCustomComponents`(全局或作用域)注册组件,在 Markdown 中放置占位标记并映射到 Vue 组件。
|
||||||
|
|
||||||
|
## 🆚 为什么选择 markstream-vue,而不是普通 Markdown 渲染器?
|
||||||
|
|
||||||
|
| 需求 | 普通 Markdown 预览 | markstream-vue |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 流式输入 | 全量重渲染、易闪烁 | 虚拟窗口 + 增量批次 |
|
||||||
|
| 大代码块 | 重新高亮速度慢 | Monaco 流式更新 + 可选 Shiki |
|
||||||
|
| 图表 | 解析/渲染阻塞 | Mermaid 渐进式渲染与回退 |
|
||||||
|
| 自定义 UI | 插槽有限 | Markdown 内嵌 Vue 组件与类型化节点 |
|
||||||
|
| 长文档 | 内存峰值高 | 可配置 live-node 上限,滚动稳定 |
|
||||||
|
|
||||||
|
## 🗺️ Roadmap(快照)
|
||||||
|
|
||||||
|
- 更多「即开即用」模板(Vite / Nuxt / Tailwind)与 StackBlitz 更新。
|
||||||
|
- 代码块预设扩展(适合 diff 的 Shiki 主题、Monaco 装饰/标注辅助)。
|
||||||
|
- AI / 聊天场景的 Cookbook(SSE/WebSocket、重试与续传、Markdown 中间态处理)。
|
||||||
|
- 展示更多在 Markdown 中嵌入 Vue 组件的示例与实践。
|
||||||
|
|
||||||
|
## 📦 发布
|
||||||
|
|
||||||
|
- 最新版本与升级提示:[Releases](https://github.com/Simon-He95/markstream-vue/releases)
|
||||||
|
- 完整历史:[CHANGELOG.md](./CHANGELOG.md)
|
||||||
|
- 最新亮点(0.0.3-beta.1/beta.0):
|
||||||
|
- 解析器升级到 `stream-markdown-parser@0.0.36`,修复多项解析问题。
|
||||||
|
- Monaco 升级,更多语言/主题,代码块对 diff 更友好。
|
||||||
|
- Playground 增加 HTML/SVG 预览对话框与 AST 调试视图。
|
||||||
|
|
||||||
|
## 🧭 案例与展示
|
||||||
|
|
||||||
|
用 markstream-vue 做了什么?欢迎提 PR 把你的项目放到这里(附链接 + 截图/GIF)。理想场景:AI/聊天界面、流式文档、diff/代码审查、或在 Markdown 驱动页面中嵌入 Vue 组件。
|
||||||
|
|
||||||
|
- **FlowNote** —— 流式 Markdown 笔记示例(SSE + 虚拟化窗口):https://markstream-vue.simonhe.me/
|
||||||
|
- **AI Chat 场景** —— playground “test” 页展示增量批次与分享链接:https://markstream-vue.simonhe.me/test
|
||||||
|
|
||||||
|
## 介绍视频
|
||||||
|
|
||||||
|
一段短视频介绍了 markstream-vue 的关键特性与使用方式。
|
||||||
|
|
||||||
|
[](https://www.bilibili.com/video/BV17Z4qzpE9c/)
|
||||||
|
|
||||||
|
在 Bilibili 上观看: [Open in Bilibili](https://www.bilibili.com/video/BV17Z4qzpE9c/)
|
||||||
|
|
||||||
|
## 核心特性
|
||||||
|
|
||||||
|
- ⚡ 极致性能:为流式场景设计的最小化重渲染和高效 DOM 更新
|
||||||
|
- 🌊 流式优先:原生支持不完整或频繁更新的 token 化 Markdown 内容
|
||||||
|
- 🧠 Monaco 流式更新:高性能的 Monaco 集成,支持大代码块的平滑增量更新
|
||||||
|
- 🪄 渐进式 Mermaid:图表在语法可用时即时渲染,并在后续更新中完善
|
||||||
|
- 🧩 自定义组件:允许在 Markdown 内容中嵌入自定义 Vue 组件
|
||||||
|
- 📝 完整 Markdown 支持:表格、公式、Emoji、复选框、代码块等
|
||||||
|
- 🔄 实时更新:支持增量内容而不破坏格式
|
||||||
|
- 📦 TypeScript 优先:提供完善的类型定义与智能提示
|
||||||
|
- 🔌 零配置:开箱即可在 Vue 3 项目中使用
|
||||||
|
- 🎨 灵活的代码块渲染:可选 Monaco 编辑器 (`CodeBlockNode`) 或轻量的 Shiki 高亮 (`MarkdownCodeBlockNode`)
|
||||||
|
- 🧰 解析工具集:[`stream-markdown-parser`](./packages/markdown-parser) 文档现已覆盖如何在 Worker/SSE 流中复用解析器、直接向 `<MarkdownRender :nodes>` 输送 AST、以及注册全局插件/数学辅助函数的方式。
|
||||||
|
|
||||||
|
## 🙌 贡献与社区
|
||||||
|
|
||||||
|
- 阅读贡献指南与 PR 模板:[CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||||
|
- 遵守 [行为准则](./CODE_OF_CONDUCT.md)。
|
||||||
|
- 提交 Issue 时使用模板;渲染问题尽量附上测试页复现链接:https://markstream-vue.simonhe.me/test
|
||||||
|
- 有问题先讨论:https://github.com/Simon-He95/markstream-vue/discussions
|
||||||
|
- 实时交流:Discord https://discord.gg/vkzdkjeRCW
|
||||||
|
- 新手贡献入口:[good first issues](https://github.com/Simon-He95/markstream-vue/labels/good%20first%20issue)
|
||||||
|
- 支持与求助入口:[SUPPORT.md](./SUPPORT.md)
|
||||||
|
- 提交 PR 时遵循 Conventional Commits,渲染/解析改动补充测试,UI 改动附上截图/GIF。
|
||||||
|
- 如果本项目对你有帮助,欢迎点 Star、分享给需要的人,助力项目持续演进。
|
||||||
|
- 安全披露:请通过 [SECURITY.md](./SECURITY.md) 中的邮件私下报告潜在漏洞。
|
||||||
|
|
||||||
|
### 快速参与的方式
|
||||||
|
|
||||||
|
- 在现有 issue 中补充复现链接/截图。
|
||||||
|
- 完善文档或示例(尤其是流式 + SSR/Worker 场景)。
|
||||||
|
- 分享 playground/test 链接,展示性能边界或有趣用法。
|
||||||
|
|
||||||
|
## 故障排查 & 常见问题
|
||||||
|
|
||||||
|
详细故障排查与常见问题已迁移至文档站点:
|
||||||
|
https://markstream-vue-docs.simonhe.me/zh/guide/troubleshooting
|
||||||
|
|
||||||
|
如需更多帮助,请到 GitHub Issues 创建问题:
|
||||||
|
https://github.com/Simon-He95/markstream-vue/issues
|
||||||
|
|
||||||
|
### 快速提交问题
|
||||||
|
|
||||||
|
1. 在测试页复现并点击“生成分享链接”:https://markstream-vue.simonhe.me/test
|
||||||
|
2. 打开 Bug 模板并附上链接与截图:https://github.com/Simon-He95/markstream-vue/issues/new?template=bug_report.yml
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
### 贡献者
|
||||||
|
|
||||||
|
感谢所有为项目做出贡献的开发者!
|
||||||
|
|
||||||
|
[](https://github.com/Simon-He95/markstream-vue/graphs/contributors)
|
||||||
|
|
||||||
|
### 依赖项目
|
||||||
|
|
||||||
|
本项目使用并受益于:
|
||||||
|
|
||||||
|
- [stream-monaco](https://github.com/Simon-He95/stream-monaco)
|
||||||
|
- [stream-markdown](https://github.com/Simon-He95/stream-markdown)
|
||||||
|
- [mermaid](https://mermaid-js.github.io/mermaid)
|
||||||
|
- [katex](https://katex.org/)
|
||||||
|
- [shiki](https://github.com/shikijs/shiki)
|
||||||
|
- [markdown-it-ts](https://github.com/Simon-He95/markdown-it-ts)
|
||||||
|
|
||||||
|
感谢这些项目的作者与贡献者!
|
||||||
|
|
||||||
|
## Star 历史
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#Simon-He95/markstream-vue&Date)
|
||||||
|
|
||||||
|
## 许可
|
||||||
|
|
||||||
|
[MIT](./license) © [Simon He](https://github.com/Simon-He95)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
We support the latest published version and the `main` branch. Please update to the newest release before reporting issues.
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
- Email: [simon_he@outlook.com](mailto:simon_he@outlook.com)
|
||||||
|
- Please include reproduction steps, affected version, and impact. If possible, attach a minimal repro (the test page can generate shareable links: https://markstream-vue.simonhe.me/test).
|
||||||
|
- Do not open public issues for potential vulnerabilities until we coordinate a fix and disclosure timeline.
|
||||||
|
|
||||||
|
## Response expectations
|
||||||
|
|
||||||
|
You should receive an acknowledgement within 72 hours. Once confirmed, we will work with you on a fix and coordinated disclosure.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Support
|
||||||
|
|
||||||
|
Need help with markstream-vue? Start here.
|
||||||
|
|
||||||
|
## Self-serve
|
||||||
|
|
||||||
|
- Docs: https://markstream-vue-docs.simonhe.me/guide/
|
||||||
|
- 中文文档: https://markstream-vue-docs.simonhe.me/zh/guide/
|
||||||
|
- Troubleshooting: https://markstream-vue-docs.simonhe.me/guide/troubleshooting
|
||||||
|
- Playground (interactive demo): https://markstream-vue.simonhe.me/
|
||||||
|
- Test page (shareable repro): https://markstream-vue.simonhe.me/test
|
||||||
|
|
||||||
|
## Ask a question
|
||||||
|
|
||||||
|
- GitHub Discussions: https://github.com/Simon-He95/markstream-vue/discussions
|
||||||
|
- Discord: https://discord.gg/vkzdkjeRCW
|
||||||
|
|
||||||
|
## File a bug
|
||||||
|
|
||||||
|
- Use the bug template and attach a repro link from the test page: https://github.com/Simon-He95/markstream-vue/issues/new?template=bug_report.yml
|
||||||
|
- Include the exact Markdown/content and versions (markstream-vue, Vue, browser/runtime).
|
||||||
|
|
||||||
|
## Feature requests
|
||||||
|
|
||||||
|
- Use the feature template and describe the use case (AI/chat/SSE, docs, diff viewer, custom nodes): https://github.com/Simon-He95/markstream-vue/issues/new?template=feature_request.yml
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Please report privately via [SECURITY.md](./SECURITY.md) (email: simon_he@outlook.com).
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
// Generated by 'unplugin-auto-import'
|
||||||
|
// We suggest you to commit this file into source control
|
||||||
|
declare global {
|
||||||
|
const $: typeof import('vue/macros')['$']
|
||||||
|
const $$: typeof import('vue/macros')['$$']
|
||||||
|
const $computed: typeof import('vue/macros')['$computed']
|
||||||
|
const $customRef: typeof import('vue/macros')['$customRef']
|
||||||
|
const $ref: typeof import('vue/macros')['$ref']
|
||||||
|
const $shallowRef: typeof import('vue/macros')['$shallowRef']
|
||||||
|
const $toRef: typeof import('vue/macros')['$toRef']
|
||||||
|
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||||
|
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||||
|
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||||
|
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||||
|
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||||
|
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||||
|
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||||
|
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||||
|
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||||
|
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||||
|
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||||
|
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||||
|
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
||||||
|
const logicNot: typeof import('@vueuse/core')['logicNot']
|
||||||
|
const logicOr: typeof import('@vueuse/core')['logicOr']
|
||||||
|
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||||
|
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactify: typeof import('@vueuse/core')['reactify']
|
||||||
|
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||||
|
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||||
|
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||||
|
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||||
|
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||||
|
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||||
|
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||||
|
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||||
|
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||||
|
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||||
|
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||||
|
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||||
|
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||||
|
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||||
|
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||||
|
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||||
|
const until: typeof import('@vueuse/core')['until']
|
||||||
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
|
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||||
|
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||||
|
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||||
|
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||||
|
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||||
|
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||||
|
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||||
|
const useCached: typeof import('@vueuse/core')['useCached']
|
||||||
|
const useClamp: typeof import('@vueuse/core')['useClamp']
|
||||||
|
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||||
|
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||||
|
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||||
|
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||||
|
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||||
|
const useDark: typeof import('@vueuse/core')['useDark']
|
||||||
|
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||||
|
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||||
|
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||||
|
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||||
|
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||||
|
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||||
|
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||||
|
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||||
|
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||||
|
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||||
|
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||||
|
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||||
|
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||||
|
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||||
|
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||||
|
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||||
|
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||||
|
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||||
|
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||||
|
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||||
|
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||||
|
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||||
|
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||||
|
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||||
|
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||||
|
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||||
|
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||||
|
const useFps: typeof import('@vueuse/core')['useFps']
|
||||||
|
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||||
|
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||||
|
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||||
|
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||||
|
const useImage: typeof import('@vueuse/core')['useImage']
|
||||||
|
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||||
|
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||||
|
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||||
|
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||||
|
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||||
|
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||||
|
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||||
|
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||||
|
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||||
|
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||||
|
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||||
|
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||||
|
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||||
|
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||||
|
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||||
|
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||||
|
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||||
|
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||||
|
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||||
|
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||||
|
const useNow: typeof import('@vueuse/core')['useNow']
|
||||||
|
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||||
|
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||||
|
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||||
|
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||||
|
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||||
|
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||||
|
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||||
|
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||||
|
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||||
|
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||||
|
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||||
|
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||||
|
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||||
|
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||||
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||||
|
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||||
|
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||||
|
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||||
|
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||||
|
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||||
|
const useShare: typeof import('@vueuse/core')['useShare']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||||
|
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||||
|
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||||
|
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||||
|
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||||
|
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||||
|
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||||
|
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||||
|
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||||
|
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||||
|
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||||
|
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||||
|
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||||
|
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||||
|
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||||
|
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||||
|
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||||
|
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||||
|
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||||
|
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||||
|
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||||
|
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||||
|
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||||
|
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||||
|
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||||
|
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||||
|
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||||
|
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||||
|
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||||
|
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||||
|
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||||
|
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||||
|
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||||
|
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||||
|
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||||
|
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||||
|
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||||
|
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||||
|
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||||
|
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||||
|
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||||
|
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||||
|
const whenever: typeof import('@vueuse/core')['whenever']
|
||||||
|
}
|
||||||
|
export {}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import process from 'node:process'
|
||||||
|
import { defaultPromptsRoot, listPromptNames, readPrompt } from './prompts-lib.mjs'
|
||||||
|
import {
|
||||||
|
listSkillNames,
|
||||||
|
parseInstallArgs,
|
||||||
|
printInstallHelp,
|
||||||
|
printInstallSummary,
|
||||||
|
runSkillsInstall,
|
||||||
|
} from './skills-lib.mjs'
|
||||||
|
|
||||||
|
function printCliHelp() {
|
||||||
|
console.log(`Usage: markstream-vue <command>
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
skills list List bundled skills
|
||||||
|
skills install [options] Install bundled skills
|
||||||
|
prompts list List bundled prompt templates
|
||||||
|
prompts dir Print bundled prompt directory
|
||||||
|
prompts show <name> Print a bundled prompt template
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
markstream-vue skills list
|
||||||
|
markstream-vue skills install
|
||||||
|
markstream-vue skills install --target codex
|
||||||
|
markstream-vue skills install --mode copy --force
|
||||||
|
markstream-vue prompts list
|
||||||
|
markstream-vue prompts show install-markstream
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const argv = process.argv.slice(2)
|
||||||
|
const [namespace, action, ...rest] = argv
|
||||||
|
|
||||||
|
if (!namespace || namespace === '--help' || namespace === '-h' || namespace === 'help') {
|
||||||
|
printCliHelp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namespace === 'prompts') {
|
||||||
|
if (!action || action === 'help') {
|
||||||
|
console.log(`Usage: markstream-vue prompts <list|dir|show> [name]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
markstream-vue prompts list
|
||||||
|
markstream-vue prompts dir
|
||||||
|
markstream-vue prompts show install-markstream
|
||||||
|
`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'list') {
|
||||||
|
const promptNames = await listPromptNames()
|
||||||
|
console.log(`Bundled prompts (${promptNames.length}):`)
|
||||||
|
for (const promptName of promptNames)
|
||||||
|
console.log(`- ${promptName}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'dir') {
|
||||||
|
console.log(defaultPromptsRoot)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'show') {
|
||||||
|
const [promptName] = rest
|
||||||
|
const { content } = await readPrompt(promptName)
|
||||||
|
process.stdout.write(content)
|
||||||
|
if (!content.endsWith('\n'))
|
||||||
|
process.stdout.write('\n')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown prompts action: ${action}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (namespace !== 'skills') {
|
||||||
|
throw new Error(`Unknown command namespace: ${namespace}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!action || action === 'help') {
|
||||||
|
printInstallHelp('markstream-vue skills install', 'copy')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'list') {
|
||||||
|
const skillNames = await listSkillNames()
|
||||||
|
console.log(`Bundled skills (${skillNames.length}):`)
|
||||||
|
for (const skillName of skillNames)
|
||||||
|
console.log(`- ${skillName}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'install') {
|
||||||
|
const args = parseInstallArgs(rest, {
|
||||||
|
target: 'agents',
|
||||||
|
mode: 'copy',
|
||||||
|
})
|
||||||
|
if (args.help) {
|
||||||
|
printInstallHelp('markstream-vue skills install', 'copy')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = await runSkillsInstall(args)
|
||||||
|
printInstallSummary(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown skills action: ${action}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error instanceof Error ? error.message : error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
export const packageRoot = path.resolve(__dirname, '..')
|
||||||
|
export const defaultPromptsRoot = path.join(packageRoot, 'prompts')
|
||||||
|
|
||||||
|
export async function listPromptFiles(promptsRoot = defaultPromptsRoot) {
|
||||||
|
const dirents = await fs.readdir(promptsRoot, { withFileTypes: true })
|
||||||
|
return dirents
|
||||||
|
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.md'))
|
||||||
|
.map(dirent => dirent.name)
|
||||||
|
.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listPromptNames(promptsRoot = defaultPromptsRoot) {
|
||||||
|
return (await listPromptFiles(promptsRoot)).map(file => file.replace(/\.md$/i, ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolvePromptPath(name, promptsRoot = defaultPromptsRoot) {
|
||||||
|
const normalized = String(name ?? '').trim()
|
||||||
|
if (!normalized)
|
||||||
|
throw new Error('Prompt name is required')
|
||||||
|
|
||||||
|
const candidates = normalized.endsWith('.md')
|
||||||
|
? [normalized]
|
||||||
|
: [`${normalized}.md`, normalized]
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const fullPath = path.join(promptsRoot, candidate)
|
||||||
|
try {
|
||||||
|
await fs.access(fullPath)
|
||||||
|
return fullPath
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const available = await listPromptNames(promptsRoot)
|
||||||
|
throw new Error(`Unknown prompt: ${normalized}. Available prompts: ${available.join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readPrompt(name, promptsRoot = defaultPromptsRoot) {
|
||||||
|
const fullPath = await resolvePromptPath(name, promptsRoot)
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8')
|
||||||
|
return { fullPath, content }
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
export const packageRoot = path.resolve(__dirname, '..')
|
||||||
|
export const defaultSourceRoot = path.join(packageRoot, '.agents', 'skills')
|
||||||
|
|
||||||
|
export function printInstallHelp(command = 'markstream-vue skills install', defaultMode = 'copy') {
|
||||||
|
console.log(`Usage: ${command} [options]
|
||||||
|
|
||||||
|
Install bundled Markstream skills from .agents/skills into a local agent skill directory.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--target <agents|codex|path> Target directory preset or custom path.
|
||||||
|
Default: agents
|
||||||
|
--mode <symlink|copy> Install mode. Default: ${defaultMode}
|
||||||
|
--force Replace existing non-symlink targets
|
||||||
|
--dry-run Print actions without writing anything
|
||||||
|
--help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
${command}
|
||||||
|
${command} --mode copy
|
||||||
|
${command} --target codex
|
||||||
|
${command} --target ./tmp/skills-test --mode copy --force
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseInstallArgs(argv, defaults = {}) {
|
||||||
|
const args = {
|
||||||
|
target: defaults.target ?? 'agents',
|
||||||
|
mode: defaults.mode ?? 'copy',
|
||||||
|
force: false,
|
||||||
|
dryRun: false,
|
||||||
|
help: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const arg = argv[i]
|
||||||
|
if (arg === '--')
|
||||||
|
continue
|
||||||
|
if (arg === '--target') {
|
||||||
|
args.target = argv[++i] ?? ''
|
||||||
|
}
|
||||||
|
else if (arg === '--mode') {
|
||||||
|
args.mode = argv[++i] ?? ''
|
||||||
|
}
|
||||||
|
else if (arg === '--force') {
|
||||||
|
args.force = true
|
||||||
|
}
|
||||||
|
else if (arg === '--dry-run') {
|
||||||
|
args.dryRun = true
|
||||||
|
}
|
||||||
|
else if (arg === '--help' || arg === '-h') {
|
||||||
|
args.help = true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Unknown argument: ${arg}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['symlink', 'copy'].includes(args.mode))
|
||||||
|
throw new Error(`Invalid --mode value: ${args.mode}`)
|
||||||
|
|
||||||
|
if (!args.target)
|
||||||
|
throw new Error('Missing --target value')
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTargetRoot(target) {
|
||||||
|
const home = os.homedir()
|
||||||
|
if (target === 'agents')
|
||||||
|
return path.join(home, '.agents', 'skills')
|
||||||
|
if (target === 'codex') {
|
||||||
|
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex')
|
||||||
|
return path.join(codexHome, 'skills')
|
||||||
|
}
|
||||||
|
return path.resolve(process.cwd(), target)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(filePath) {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listSkillDirs(sourceRoot = defaultSourceRoot) {
|
||||||
|
const dirents = await fs.readdir(sourceRoot, { withFileTypes: true })
|
||||||
|
const skillDirs = []
|
||||||
|
for (const dirent of dirents) {
|
||||||
|
if (!dirent.isDirectory())
|
||||||
|
continue
|
||||||
|
const fullPath = path.join(sourceRoot, dirent.name)
|
||||||
|
if (await exists(path.join(fullPath, 'SKILL.md')))
|
||||||
|
skillDirs.push(fullPath)
|
||||||
|
}
|
||||||
|
return skillDirs.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listSkillNames(sourceRoot = defaultSourceRoot) {
|
||||||
|
return (await listSkillDirs(sourceRoot)).map(dir => path.basename(dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureDir(dirPath, dryRun) {
|
||||||
|
if (dryRun)
|
||||||
|
return
|
||||||
|
await fs.mkdir(dirPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLinkedTarget(destPath) {
|
||||||
|
try {
|
||||||
|
const rawTarget = await fs.readlink(destPath)
|
||||||
|
return path.resolve(path.dirname(destPath), rawTarget)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeExisting(destPath, force, dryRun) {
|
||||||
|
const stat = await fs.lstat(destPath)
|
||||||
|
if (stat.isSymbolicLink()) {
|
||||||
|
if (!dryRun)
|
||||||
|
await fs.rm(destPath, { recursive: true, force: true })
|
||||||
|
return 'replaced-symlink'
|
||||||
|
}
|
||||||
|
if (!force)
|
||||||
|
throw new Error(`Target already exists and is not a symlink: ${destPath}. Re-run with --force to replace it.`)
|
||||||
|
if (!dryRun)
|
||||||
|
await fs.rm(destPath, { recursive: true, force: true })
|
||||||
|
return 'replaced-existing'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installSkill({ srcPath, destPath, mode, force, dryRun }) {
|
||||||
|
if (await exists(destPath)) {
|
||||||
|
if (mode === 'symlink') {
|
||||||
|
const linkedTarget = await readLinkedTarget(destPath)
|
||||||
|
if (linkedTarget === srcPath)
|
||||||
|
return { status: 'skipped', detail: 'already-linked' }
|
||||||
|
}
|
||||||
|
const detail = await removeExisting(destPath, force, dryRun)
|
||||||
|
if (mode === 'copy') {
|
||||||
|
if (!dryRun)
|
||||||
|
await fs.cp(srcPath, destPath, { recursive: true })
|
||||||
|
return { status: 'installed', detail }
|
||||||
|
}
|
||||||
|
if (!dryRun)
|
||||||
|
await fs.symlink(srcPath, destPath, process.platform === 'win32' ? 'junction' : 'dir')
|
||||||
|
return { status: 'installed', detail }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
if (mode === 'copy')
|
||||||
|
await fs.cp(srcPath, destPath, { recursive: true })
|
||||||
|
else
|
||||||
|
await fs.symlink(srcPath, destPath, process.platform === 'win32' ? 'junction' : 'dir')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 'installed', detail: 'new' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runSkillsInstall({ sourceRoot = defaultSourceRoot, target, mode, force = false, dryRun = false }) {
|
||||||
|
const targetRoot = resolveTargetRoot(target)
|
||||||
|
const skillDirs = await listSkillDirs(sourceRoot)
|
||||||
|
|
||||||
|
if (skillDirs.length === 0)
|
||||||
|
throw new Error(`No skill directories with SKILL.md found under ${sourceRoot}`)
|
||||||
|
|
||||||
|
await ensureDir(targetRoot, dryRun)
|
||||||
|
|
||||||
|
const summary = []
|
||||||
|
for (const srcPath of skillDirs) {
|
||||||
|
const skillName = path.basename(srcPath)
|
||||||
|
const destPath = path.join(targetRoot, skillName)
|
||||||
|
const result = await installSkill({
|
||||||
|
srcPath,
|
||||||
|
destPath,
|
||||||
|
mode,
|
||||||
|
force,
|
||||||
|
dryRun,
|
||||||
|
})
|
||||||
|
summary.push({ skillName, destPath, ...result })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sourceRoot,
|
||||||
|
targetRoot,
|
||||||
|
mode,
|
||||||
|
dryRun,
|
||||||
|
summary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printInstallSummary(result) {
|
||||||
|
const label = result.dryRun ? 'Dry run complete' : 'Skill install complete'
|
||||||
|
console.log(`${label}: ${result.summary.length} skill(s) processed.`)
|
||||||
|
console.log(`Source: ${result.sourceRoot}`)
|
||||||
|
console.log(`Target: ${result.targetRoot}`)
|
||||||
|
console.log(`Mode: ${result.mode}`)
|
||||||
|
|
||||||
|
for (const item of result.summary)
|
||||||
|
console.log(`- ${item.skillName}: ${item.status} (${item.detail}) -> ${item.destPath}`)
|
||||||
|
|
||||||
|
if (!result.dryRun && result.mode === 'symlink')
|
||||||
|
console.log('These installs are symlinks, so editing the source skills updates the installed versions too.')
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
AdmonitionNode: typeof import('./src/components/AdmonitionNode/AdmonitionNode.vue')['default']
|
||||||
|
BlockquoteNode: typeof import('./src/components/BlockquoteNode/BlockquoteNode.vue')['default']
|
||||||
|
Button: typeof import('./src/components/button/Button.vue')['default']
|
||||||
|
CheckboxNode: typeof import('./src/components/CheckboxNode/CheckboxNode.vue')['default']
|
||||||
|
CodeBlockNode: typeof import('./src/components/CodeBlockNode/CodeBlockNode.vue')['default']
|
||||||
|
CodeBlockShell: typeof import('./src/components/CodeBlockNode/CodeBlockShell.vue')['default']
|
||||||
|
Counter: typeof import('./src/components/Counter.vue')['default']
|
||||||
|
D2BlockNode: typeof import('./src/components/D2BlockNode/D2BlockNode.vue')['default']
|
||||||
|
DefinitionListNode: typeof import('./src/components/DefinitionListNode/DefinitionListNode.vue')['default']
|
||||||
|
EmojiNode: typeof import('./src/components/EmojiNode/EmojiNode.vue')['default']
|
||||||
|
EmphasisNode: typeof import('./src/components/EmphasisNode/EmphasisNode.vue')['default']
|
||||||
|
FallbackComponent: typeof import('./src/components/NodeRenderer/FallbackComponent.vue')['default']
|
||||||
|
FootnoteAnchorNode: typeof import('./src/components/FootnoteAnchorNode/FootnoteAnchorNode.vue')['default']
|
||||||
|
FootnoteNode: typeof import('./src/components/FootnoteNode/FootnoteNode.vue')['default']
|
||||||
|
FootnoteReferenceNode: typeof import('./src/components/FootnoteReferenceNode/FootnoteReferenceNode.vue')['default']
|
||||||
|
HardBreakNode: typeof import('./src/components/HardBreakNode/HardBreakNode.vue')['default']
|
||||||
|
HeadingNode: typeof import('./src/components/HeadingNode/HeadingNode.vue')['default']
|
||||||
|
HighlightNode: typeof import('./src/components/HighlightNode/HighlightNode.vue')['default']
|
||||||
|
HtmlBlockNode: typeof import('./src/components/HtmlBlockNode/HtmlBlockNode.vue')['default']
|
||||||
|
HtmlInlineNode: typeof import('./src/components/HtmlInlineNode/HtmlInlineNode.vue')['default']
|
||||||
|
HtmlPreviewFrame: typeof import('./src/components/CodeBlockNode/HtmlPreviewFrame.vue')['default']
|
||||||
|
ImageNode: typeof import('./src/components/ImageNode/ImageNode.vue')['default']
|
||||||
|
InfographicBlockNode: typeof import('./src/components/InfographicBlockNode/InfographicBlockNode.vue')['default']
|
||||||
|
InlineCodeNode: typeof import('./src/components/InlineCodeNode/InlineCodeNode.vue')['default']
|
||||||
|
InsertNode: typeof import('./src/components/InsertNode/InsertNode.vue')['default']
|
||||||
|
LinkNode: typeof import('./src/components/LinkNode/LinkNode.vue')['default']
|
||||||
|
ListItemNode: typeof import('./src/components/ListItemNode/ListItemNode.vue')['default']
|
||||||
|
ListNode: typeof import('./src/components/ListNode/ListNode.vue')['default']
|
||||||
|
MarkdownCodeBlockNode: typeof import('./src/components/MarkdownCodeBlockNode/MarkdownCodeBlockNode.vue')['default']
|
||||||
|
MathBlockNode: typeof import('./src/components/MathBlockNode/MathBlockNode.vue')['default']
|
||||||
|
MathInlineNode: typeof import('./src/components/MathInlineNode/MathInlineNode.vue')['default']
|
||||||
|
MermaidBlockNode: typeof import('./src/components/MermaidBlockNode/MermaidBlockNode.vue')['default']
|
||||||
|
NodeRenderer: typeof import('./src/components/NodeRenderer/NodeRenderer.vue')['default']
|
||||||
|
ParagraphNode: typeof import('./src/components/ParagraphNode/ParagraphNode.vue')['default']
|
||||||
|
PreCodeNode: typeof import('./src/components/PreCodeNode/PreCodeNode.vue')['default']
|
||||||
|
ReferenceNode: typeof import('./src/components/ReferenceNode/ReferenceNode.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
StrikethroughNode: typeof import('./src/components/StrikethroughNode/StrikethroughNode.vue')['default']
|
||||||
|
StrongNode: typeof import('./src/components/StrongNode/StrongNode.vue')['default']
|
||||||
|
SubscriptNode: typeof import('./src/components/SubscriptNode/SubscriptNode.vue')['default']
|
||||||
|
SuperscriptNode: typeof import('./src/components/SuperscriptNode/SuperscriptNode.vue')['default']
|
||||||
|
TableNode: typeof import('./src/components/TableNode/TableNode.vue')['default']
|
||||||
|
TextNode: typeof import('./src/components/TextNode/TextNode.vue')['default']
|
||||||
|
ThematicBreakNode: typeof import('./src/components/ThematicBreakNode/ThematicBreakNode.vue')['default']
|
||||||
|
Tooltip: typeof import('./src/components/Tooltip/Tooltip.vue')['default']
|
||||||
|
VmrContainerNode: typeof import('./src/components/VmrContainerNode/VmrContainerNode.vue')['default']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# Animations & Transitions
|
||||||
|
|
||||||
|
> 所有动画、过渡效果与运动设计清单。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. CSS 变量控制的动画
|
||||||
|
|
||||||
|
| 变量 | 默认值 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--typewriter-fade-duration` | `900ms` | 流式打字机淡入时长 |
|
||||||
|
| `--typewriter-fade-ease` | `ease-out` | 流式打字机缓动 |
|
||||||
|
| `--stream-update-fade-duration` | 同上回退 | 流式内容更新淡入时长 |
|
||||||
|
| `--stream-update-fade-ease` | 同上回退 | 流式内容更新缓动 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Keyframe 动画
|
||||||
|
|
||||||
|
### 2.1 流式内容淡入
|
||||||
|
|
||||||
|
**组件**:InlineCodeNode
|
||||||
|
|
||||||
|
```css
|
||||||
|
@keyframes inline-code-stream-update-fade-a { from { opacity: 0 } to { opacity: 1 } }
|
||||||
|
@keyframes inline-code-stream-update-fade-b { from { opacity: 0 } to { opacity: 1 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
- 使用 a/b 交替触发避免重复动画不生效
|
||||||
|
- Duration:`var(--stream-update-fade-duration, var(--typewriter-fade-duration, 900ms))`
|
||||||
|
- Easing:`var(--stream-update-fade-ease, var(--typewriter-fade-ease, ease-out))`
|
||||||
|
- `will-change: opacity`
|
||||||
|
|
||||||
|
### 2.2 骨架屏 Shimmer
|
||||||
|
|
||||||
|
**组件**:CodeBlockNode, TableNode
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* CodeBlockNode */
|
||||||
|
@keyframes code-skeleton-shimmer {
|
||||||
|
0% { background-position: -200% 0 }
|
||||||
|
100% { background-position: 200% 0 }
|
||||||
|
}
|
||||||
|
/* background: linear-gradient(90deg, rgba(0,0,0,0.04) 25%, rgba(0,0,0,0.08) 50%, rgba(0,0,0,0.04) 75%) */
|
||||||
|
|
||||||
|
/* TableNode */
|
||||||
|
@keyframes table-node-shimmer {
|
||||||
|
0% { background-position: -200% 0 }
|
||||||
|
100% { background-position: 200% 0 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Spinner 旋转
|
||||||
|
|
||||||
|
**组件**:MathBlockNode, TableNode, ImageNode
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* MathBlockNode */
|
||||||
|
@keyframes math-spin {
|
||||||
|
to { transform: rotate(360deg) }
|
||||||
|
}
|
||||||
|
/* 0.8s linear infinite */
|
||||||
|
|
||||||
|
/* TableNode */
|
||||||
|
/* Tailwind animate-spin */
|
||||||
|
|
||||||
|
/* ImageNode */
|
||||||
|
/* Tailwind animate-spin */
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 链接脉冲下划线
|
||||||
|
|
||||||
|
**组件**:LinkNode
|
||||||
|
|
||||||
|
```css
|
||||||
|
@keyframes underlinePulse {
|
||||||
|
0%, 100% { opacity: var(--underline-rest-opacity, 0.18) }
|
||||||
|
50% { opacity: var(--underline-opacity, 0.35) }
|
||||||
|
}
|
||||||
|
/* Duration: var(--underline-duration, 1.6s) */
|
||||||
|
/* Timing: var(--underline-timing, ease-in-out) */
|
||||||
|
/* Iteration: var(--underline-iteration, infinite) */
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. CSS Transitions
|
||||||
|
|
||||||
|
### 3.1 通用过渡模式
|
||||||
|
|
||||||
|
| 组件 | 属性 | 时长 | 缓动 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| CodeBlockNode | `height, max-height` | `180ms` | `ease` |
|
||||||
|
| MathBlockNode 渲染中 | `opacity` | `200ms` | `ease` |
|
||||||
|
| MermaidBlockNode 容器 | `height` | `150ms` | `ease-out` |
|
||||||
|
| Tooltip 位移 | `transform, box-shadow` | `220ms` | `cubic-bezier(.16,1,.3,1)` |
|
||||||
|
|
||||||
|
### 3.2 Vue Transition 组件
|
||||||
|
|
||||||
|
#### ImageNode — 图片切换
|
||||||
|
|
||||||
|
```
|
||||||
|
名称:img-switch
|
||||||
|
enter: opacity 220ms ease + translateY(6px → 0)
|
||||||
|
leave: opacity 220ms ease + translateY(0 → 6px)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MathBlockNode — 公式切换
|
||||||
|
|
||||||
|
```
|
||||||
|
名称:math-fade
|
||||||
|
enter: opacity 0.3s ease (0 → 1)
|
||||||
|
leave: opacity 0.3s ease (1 → 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TableNode — 表格淡入
|
||||||
|
|
||||||
|
```
|
||||||
|
名称:table-node-fade
|
||||||
|
enter: opacity 0.18s ease
|
||||||
|
leave: opacity 0.18s ease
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tooltip — 浮层出入
|
||||||
|
|
||||||
|
```
|
||||||
|
名称:tooltip
|
||||||
|
enter-from: opacity: 0, translateY(-6px) scale(0.98)
|
||||||
|
enter-to: opacity: 1, translateY(0) scale(1)
|
||||||
|
enter-active: opacity 120ms linear
|
||||||
|
leave-active: opacity 120ms linear
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MermaidBlockNode — 弹窗
|
||||||
|
|
||||||
|
```
|
||||||
|
名称:mermaid-dialog
|
||||||
|
enter: opacity 200ms ease + scale(0.98 → 1.0)
|
||||||
|
leave: opacity 200ms ease + scale(1.0 → 0.98)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 交互反馈
|
||||||
|
|
||||||
|
### 4.1 按钮点击缩放
|
||||||
|
|
||||||
|
| 组件 | 选择器 | 效果 |
|
||||||
|
|---|---|---|
|
||||||
|
| CodeBlockNode | `.code-action-btn:active` | `transform: scale(0.98)` |
|
||||||
|
| MermaidBlockNode | `.mermaid-action-btn:active` | `transform: scale(0.98)` |
|
||||||
|
| AdmonitionNode | `.admonition-toggle` | `focus: outline 2px solid rgba(0,0,0,0.08)` |
|
||||||
|
|
||||||
|
### 4.2 禁用状态
|
||||||
|
|
||||||
|
| 组件 | 效果 |
|
||||||
|
|---|---|
|
||||||
|
| MermaidBlockNode 按钮 | `opacity: 0.5`, `cursor: not-allowed` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 性能优化
|
||||||
|
|
||||||
|
### 5.1 `will-change` 使用
|
||||||
|
|
||||||
|
| 组件 | 元素 | `will-change` 值 |
|
||||||
|
|---|---|---|
|
||||||
|
| InlineCodeNode | `.inline-code-stream-delta` | `opacity` |
|
||||||
|
| LinkNode | `.link-loading-indicator` | `opacity` |
|
||||||
|
|
||||||
|
### 5.2 减少动效偏好
|
||||||
|
|
||||||
|
支持 `prefers-reduced-motion`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.inline-code-stream-delta { animation: none !important }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**组件**:InlineCodeNode, LinkNode
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 动画时长速查表
|
||||||
|
|
||||||
|
| 速度等级 | 时长 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| 极快 | `120ms` | Tooltip 出入 |
|
||||||
|
| 快速 | `150ms` | Mermaid 容器高度 |
|
||||||
|
| 快速 | `180ms` | 代码块高度、表格淡入 |
|
||||||
|
| 标准 | `200ms` | Mermaid 弹窗、数学公式渲染 |
|
||||||
|
| 标准 | `220ms` | 图片切换、Tooltip 位移 |
|
||||||
|
| 慢速 | `300ms` | 数学公式切换 |
|
||||||
|
| 流式 | `900ms` | 打字机/流式更新淡入 |
|
||||||
|
| 脉冲 | `1600ms` | 链接加载指示器 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 缓动函数速查
|
||||||
|
|
||||||
|
| 缓动 | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `ease` | 代码块高度、数学公式、图片 |
|
||||||
|
| `ease-out` | Mermaid 容器、打字机淡入 |
|
||||||
|
| `ease-in-out` | 链接脉冲 |
|
||||||
|
| `linear` | Spinner 旋转、Tooltip 透明度 |
|
||||||
|
| `cubic-bezier(.16,1,.3,1)` | Tooltip 位移(弹性过冲) |
|
||||||
@@ -0,0 +1,521 @@
|
|||||||
|
# 样式架构方案
|
||||||
|
|
||||||
|
> 核心决策:**带默认样式 + CSS 变量全量可定制**,非 headless。
|
||||||
|
> shadcn 兼容:继承宿主变量,不覆盖;无宿主时回退到内置默认值。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
1. **开箱即用** — 引入即有完整合理的 Markdown 渲染样式
|
||||||
|
2. **变量换肤** — 用户只需覆盖 CSS 变量即可定制全部视觉表现
|
||||||
|
3. **零硬编码** — 组件内不允许出现硬编码的色值、间距等魔法数字
|
||||||
|
4. **Light/Dark 内建** — 两套主题开关切换,暗色模式完整覆盖所有组件
|
||||||
|
5. **不污染宿主** — 所有样式限定在 `.markstream-vue` 作用域内
|
||||||
|
6. **不覆盖宿主** — 不重定义 shadcn 同名变量,通过继承 + 回退兼容
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 变量不冲突策略
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
|
||||||
|
如果我们在 `.markstream-vue` 上直接定义 `--background`,会覆盖宿主从 `:root` 继承下来的值:
|
||||||
|
|
||||||
|
```
|
||||||
|
宿主 :root { --background: 210 20% 98% } ← 用户自定义的暖灰
|
||||||
|
我们 .markstream-vue { --background: 0 0% 100% } ← 覆盖成了纯白 ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
### 解决方案:Bridge Tokens
|
||||||
|
|
||||||
|
引入 `--ms-*` 前缀的桥接层,用 `var()` 继承宿主变量,未定义时回退到默认值:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
--ms-background: var(--background, 0 0% 100%);
|
||||||
|
/* ↑ 宿主有则用宿主的 ↑ 没有则用回退值 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**shadcn 宿主**:
|
||||||
|
|
||||||
|
```
|
||||||
|
:root { --background: 210 20% 98% } ← 宿主定义
|
||||||
|
└─ .markstream-vue 继承 --background = 210 20% 98%
|
||||||
|
└─ --ms-background: var(--background, 0 0% 100%)
|
||||||
|
= 210 20% 98% ✅ 宿主主题穿透生效
|
||||||
|
```
|
||||||
|
|
||||||
|
**独立使用(无 shadcn)**:
|
||||||
|
|
||||||
|
```
|
||||||
|
:root 未定义 --background
|
||||||
|
└─ .markstream-vue 未继承到 --background
|
||||||
|
└─ --ms-background: var(--background, 0 0% 100%)
|
||||||
|
= 0 0% 100% ✅ 回退值生效,开箱即用
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分层架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ Layer 4: Dark Theme │ 仅覆盖 Layer 1 bridge + ext 的回退值
|
||||||
|
│ .dark .markstream-vue { --ms-bg: var(...) } │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ Layer 3: Component Styles │ 引用 Layer 2 变量,不允许硬编码
|
||||||
|
│ .heading { color: var(--blockquote-fg) } │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ Layer 2: Semantic Tokens │ 组件级变量,引用 Layer 1
|
||||||
|
│ --code-bg: hsl(var(--ms-background)); │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ Layer 1: Base Tokens │
|
||||||
|
│ ├─ Bridge (--ms-*): 继承 shadcn + 回退 │
|
||||||
|
│ └─ Extension (--*): 本项目独有 │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer 1: Base Tokens
|
||||||
|
|
||||||
|
### 1.1 Bridge Tokens(`--ms-*`,继承 shadcn 同名变量)
|
||||||
|
|
||||||
|
> 内部桥接用。用户定制时直接覆盖 shadcn 变量名(如 `--background`)或覆盖 semantic token 即可。
|
||||||
|
|
||||||
|
#### Light(默认)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
|
||||||
|
/* ── 背景 / 前景 ── */
|
||||||
|
--ms-background: var(--background, 0 0% 100%);
|
||||||
|
--ms-foreground: var(--foreground, 222.2 84% 4.9%);
|
||||||
|
|
||||||
|
/* ── 弱化 ── */
|
||||||
|
--ms-muted: var(--muted, 210 40% 96.1%);
|
||||||
|
--ms-muted-foreground: var(--muted-foreground, 215.4 16.3% 46.9%);
|
||||||
|
|
||||||
|
/* ── 次要 ── */
|
||||||
|
--ms-secondary: var(--secondary, 210 40% 96.1%);
|
||||||
|
--ms-secondary-foreground: var(--secondary-foreground, 222.2 47.4% 11.2%);
|
||||||
|
|
||||||
|
/* ── 强调 ── */
|
||||||
|
--ms-accent: var(--accent, 210 40% 96.1%);
|
||||||
|
--ms-accent-foreground: var(--accent-foreground, 222.2 47.4% 11.2%);
|
||||||
|
|
||||||
|
/* ── 主要操作 ── */
|
||||||
|
--ms-primary: var(--primary, 222.2 47.4% 11.2%);
|
||||||
|
--ms-primary-foreground: var(--primary-foreground, 210 40% 98%);
|
||||||
|
|
||||||
|
/* ── 危险 ── */
|
||||||
|
--ms-destructive: var(--destructive, 0 84.2% 60.2%);
|
||||||
|
--ms-destructive-foreground: var(--destructive-foreground, 210 40% 98%);
|
||||||
|
|
||||||
|
/* ── 边框 / Focus ── */
|
||||||
|
--ms-border: var(--border, 214.3 31.8% 91.4%);
|
||||||
|
--ms-ring: var(--ring, 222.2 84% 4.9%);
|
||||||
|
|
||||||
|
/* ── 弹出层 ── */
|
||||||
|
--ms-popover: var(--popover, 0 0% 100%);
|
||||||
|
--ms-popover-foreground: var(--popover-foreground, 222.2 84% 4.9%);
|
||||||
|
|
||||||
|
/* ── 圆角 ── */
|
||||||
|
--ms-radius: var(--radius, 0.5rem);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dark
|
||||||
|
|
||||||
|
```css
|
||||||
|
.dark .markstream-vue,
|
||||||
|
.markstream-vue.dark {
|
||||||
|
|
||||||
|
--ms-background: var(--background, 222.2 84% 4.9%);
|
||||||
|
--ms-foreground: var(--foreground, 210 40% 98%);
|
||||||
|
|
||||||
|
--ms-muted: var(--muted, 217.2 32.6% 17.5%);
|
||||||
|
--ms-muted-foreground: var(--muted-foreground, 215 20.2% 65.1%);
|
||||||
|
|
||||||
|
--ms-secondary: var(--secondary, 217.2 32.6% 17.5%);
|
||||||
|
--ms-secondary-foreground: var(--secondary-foreground, 210 40% 98%);
|
||||||
|
|
||||||
|
--ms-accent: var(--accent, 217.2 32.6% 17.5%);
|
||||||
|
--ms-accent-foreground: var(--accent-foreground, 210 40% 98%);
|
||||||
|
|
||||||
|
--ms-primary: var(--primary, 210 40% 98%);
|
||||||
|
--ms-primary-foreground: var(--primary-foreground, 222.2 47.4% 11.2%);
|
||||||
|
|
||||||
|
--ms-destructive: var(--destructive, 0 62.8% 30.6%);
|
||||||
|
--ms-destructive-foreground: var(--destructive-foreground, 210 40% 98%);
|
||||||
|
|
||||||
|
--ms-border: var(--border, 217.2 32.6% 17.5%);
|
||||||
|
--ms-ring: var(--ring, 212.7 26.8% 83.9%);
|
||||||
|
|
||||||
|
--ms-popover: var(--popover, 222.2 84% 4.9%);
|
||||||
|
--ms-popover-foreground: var(--popover-foreground, 210 40% 98%);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作原理**:
|
||||||
|
|
||||||
|
- shadcn 宿主有 `.dark { --background: X }` → `var(--background, fallback)` 取 X → 宿主暗色穿透
|
||||||
|
- 独立使用,无 `--background` → 取 fallback 中的暗色值 → 内置暗色生效
|
||||||
|
|
||||||
|
### 1.2 Extension Tokens(本项目独有,无冲突风险)
|
||||||
|
|
||||||
|
> 这些变量名 shadcn 不存在,直接定义即可。
|
||||||
|
|
||||||
|
#### Light
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
|
||||||
|
/* ── 语义色 ── */
|
||||||
|
--ms-info: 217 91% 60%; /* 信息/备注 (#448aff) */
|
||||||
|
--ms-info-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ms-success: 168 100% 37.5%; /* 成功/提示 (#00bfa5) */
|
||||||
|
--ms-success-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ms-warning: 34 100% 50%; /* 警告 (#ff9100) */
|
||||||
|
--ms-warning-foreground: 0 0% 100%;
|
||||||
|
|
||||||
|
/* ── Diff ── */
|
||||||
|
--ms-diff-added: 174 60% 51%; /* 新增 (#14b8a6) */
|
||||||
|
--ms-diff-removed: 350 100% 60%; /* 删除 (#ff3658) */
|
||||||
|
|
||||||
|
/* ── 其他 ── */
|
||||||
|
--ms-highlight: 54 100% 62%; /* 文本高亮 */
|
||||||
|
--ms-highlight-foreground: 0 0% 0%;
|
||||||
|
|
||||||
|
--ms-link: 212 100% 42%; /* 链接色 (#0366d6) */
|
||||||
|
|
||||||
|
/* ── 排版 ── */
|
||||||
|
--ms-font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||||
|
--ms-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dark
|
||||||
|
|
||||||
|
```css
|
||||||
|
.dark .markstream-vue,
|
||||||
|
.markstream-vue.dark {
|
||||||
|
|
||||||
|
--ms-diff-added: 174 72% 70%; /* 暗色更亮 (#5eead4) */
|
||||||
|
--ms-diff-removed: 0 92% 82%; /* 暗色更柔 (#fda4af) */
|
||||||
|
|
||||||
|
--ms-highlight: 54 80% 42%; /* 暗色降低亮度 */
|
||||||
|
--ms-highlight-foreground: 0 0% 100%;
|
||||||
|
|
||||||
|
--ms-link: 213 94% 68%; /* 暗色更亮链接 */
|
||||||
|
|
||||||
|
/* info / success / warning 在暗色下保持不变,仍然醒目 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 圆角推算规则
|
||||||
|
|
||||||
|
```
|
||||||
|
sm = calc(var(--ms-radius) - 2px) → 6px
|
||||||
|
base = var(--ms-radius) → 8px (0.5rem)
|
||||||
|
md = calc(var(--ms-radius) + 2px) → 10px
|
||||||
|
lg = calc(var(--ms-radius) + 4px) → 12px
|
||||||
|
full = 9999px
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer 2: Semantic Tokens
|
||||||
|
|
||||||
|
全部引用 Layer 1 的 `--ms-*` 变量。Dark 模式下 Layer 1 变化 → Layer 2 自动跟随,无需重复定义。
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
|
||||||
|
/* ── 行内代码 ── */
|
||||||
|
--inline-code-bg: hsl(var(--ms-secondary));
|
||||||
|
--inline-code-fg: hsl(var(--ms-foreground));
|
||||||
|
--inline-code-border: hsl(var(--ms-border) / 0.9);
|
||||||
|
|
||||||
|
/* ── 代码块 ── */
|
||||||
|
--code-bg: hsl(var(--ms-background));
|
||||||
|
--code-fg: hsl(var(--ms-foreground));
|
||||||
|
--code-border: hsl(var(--ms-border));
|
||||||
|
--code-header-bg: hsl(var(--ms-muted));
|
||||||
|
--code-selection-bg: hsl(var(--ms-accent) / 0.3);
|
||||||
|
--code-line-number: hsl(var(--ms-muted-foreground));
|
||||||
|
|
||||||
|
/* ── 代码块操作栏 ── */
|
||||||
|
--code-action-fg: hsl(var(--ms-muted-foreground));
|
||||||
|
--code-action-hover-bg: hsl(var(--ms-accent));
|
||||||
|
--code-action-hover-fg: hsl(var(--ms-accent-foreground));
|
||||||
|
--code-action-active-bg: hsl(var(--ms-primary));
|
||||||
|
--code-action-active-fg: hsl(var(--ms-primary-foreground));
|
||||||
|
|
||||||
|
/* ── Diff ── */
|
||||||
|
--diff-added-fg: hsl(var(--ms-diff-added));
|
||||||
|
--diff-removed-fg: hsl(var(--ms-diff-removed));
|
||||||
|
--diff-added-bg: hsl(var(--ms-diff-added) / 0.1);
|
||||||
|
--diff-added-inline-bg: hsl(var(--ms-diff-added) / 0.2);
|
||||||
|
--diff-removed-bg: hsl(var(--ms-diff-removed) / 0.1);
|
||||||
|
--diff-removed-inline-bg: hsl(var(--ms-diff-removed) / 0.2);
|
||||||
|
|
||||||
|
/* ── 引用块 ── */
|
||||||
|
--blockquote-border: hsl(var(--ms-border));
|
||||||
|
--blockquote-fg: hsl(var(--ms-foreground));
|
||||||
|
|
||||||
|
/* ── 提示框 (Admonition) ── */
|
||||||
|
--admonition-bg: hsl(var(--ms-muted));
|
||||||
|
--admonition-border: hsl(var(--ms-border));
|
||||||
|
--admonition-fg: hsl(var(--ms-foreground));
|
||||||
|
--admonition-muted: hsl(var(--ms-muted-foreground));
|
||||||
|
--admonition-header-bg: hsl(var(--ms-muted) / 0.5);
|
||||||
|
--admonition-note: hsl(var(--ms-info));
|
||||||
|
--admonition-tip: hsl(var(--ms-success));
|
||||||
|
--admonition-warning: hsl(var(--ms-warning));
|
||||||
|
--admonition-danger: hsl(var(--ms-destructive));
|
||||||
|
|
||||||
|
/* ── 表格 ── */
|
||||||
|
--table-border: hsl(var(--ms-border));
|
||||||
|
--table-header-bg: hsl(var(--ms-muted));
|
||||||
|
--table-header-fg: hsl(var(--ms-foreground));
|
||||||
|
--table-row-hover-bg: hsl(var(--ms-accent) / 0.5);
|
||||||
|
|
||||||
|
/* ── 链接 ── */
|
||||||
|
--link-color: hsl(var(--ms-link));
|
||||||
|
|
||||||
|
/* ── 列表 ── */
|
||||||
|
--list-marker: hsl(var(--ms-border));
|
||||||
|
--list-counter-marker: hsl(var(--ms-muted-foreground));
|
||||||
|
|
||||||
|
/* ── 分割线 ── */
|
||||||
|
--hr-border: hsl(var(--ms-border));
|
||||||
|
|
||||||
|
/* ── 高亮 ── */
|
||||||
|
--highlight-bg: hsl(var(--ms-highlight));
|
||||||
|
--highlight-fg: hsl(var(--ms-highlight-foreground));
|
||||||
|
|
||||||
|
/* ── 脚注 ── */
|
||||||
|
--footnote-border: hsl(var(--ms-border));
|
||||||
|
|
||||||
|
/* ── Tooltip ── */
|
||||||
|
--tooltip-bg: hsl(var(--ms-popover));
|
||||||
|
--tooltip-fg: hsl(var(--ms-popover-foreground));
|
||||||
|
--tooltip-border: hsl(var(--ms-border));
|
||||||
|
|
||||||
|
/* ── Modal ── */
|
||||||
|
--modal-overlay: hsl(0 0% 0% / 0.7);
|
||||||
|
--modal-bg: hsl(var(--ms-popover));
|
||||||
|
--modal-fg: hsl(var(--ms-popover-foreground));
|
||||||
|
|
||||||
|
/* ── 图表容器 (Mermaid / D2 / Infographic) ── */
|
||||||
|
--diagram-bg: hsl(var(--ms-muted));
|
||||||
|
--diagram-border: hsl(var(--ms-border));
|
||||||
|
--diagram-header-bg: hsl(var(--ms-muted));
|
||||||
|
|
||||||
|
/* ── 数学公式 ── */
|
||||||
|
--math-fg: hsl(var(--ms-foreground));
|
||||||
|
|
||||||
|
/* ── 加载态 ── */
|
||||||
|
--loading-spinner: hsl(var(--ms-muted-foreground));
|
||||||
|
--loading-shimmer: hsl(var(--ms-muted) / 0.5);
|
||||||
|
|
||||||
|
/* ── 图片 ── */
|
||||||
|
--image-placeholder-bg: hsl(var(--ms-muted));
|
||||||
|
|
||||||
|
/* ── Focus ring ── */
|
||||||
|
--focus-ring: hsl(var(--ms-ring));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dark 模式下无需重复定义 Layer 2** — 因为 Layer 1 的 `--ms-*` 值变了,这里自动跟随。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer 3: Component Styles
|
||||||
|
|
||||||
|
组件中只引用 Layer 2 语义变量:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* ❌ 之前 */
|
||||||
|
.blockquote { border-left: 0.25rem solid #e2e8f0; }
|
||||||
|
.admonition-note { border-left-color: #448aff; }
|
||||||
|
.highlight-node { background-color: #ffff00; }
|
||||||
|
|
||||||
|
/* ✅ 之后 */
|
||||||
|
.blockquote { border-left: 0.25rem solid var(--blockquote-border); }
|
||||||
|
.admonition-note { border-left-color: var(--admonition-note); }
|
||||||
|
.highlight-node { background: var(--highlight-bg); }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 暗色模式统一方案
|
||||||
|
|
||||||
|
**去掉组件级 `.is-dark` CSS**,统一为:
|
||||||
|
|
||||||
|
```
|
||||||
|
之前:isDark prop → 每个组件 .is-dark → 30+ 组件各自覆盖 CSS 变量
|
||||||
|
之后:.dark 类 → Layer 1 bridge 回退值切换 → Layer 2 自动跟随 → 全部组件适配
|
||||||
|
```
|
||||||
|
|
||||||
|
**保留 `isDark` prop** 仅用于 JS 逻辑(不用于 CSS):
|
||||||
|
- Monaco Editor 主题名切换
|
||||||
|
- Mermaid 主题名切换
|
||||||
|
- `prefers-color-scheme` 检测兜底
|
||||||
|
|
||||||
|
### CodeBlockNode `.is-dark` 例外
|
||||||
|
|
||||||
|
CodeBlockNode 保留 `.is-dark` CSS 块。此处 `.is-dark` 的语义是**"编辑器表面为暗色"**
|
||||||
|
(由 Monaco 主题检测 `resolvedSurfaceIsDark` 决定),与页面级 `.dark` 无关。
|
||||||
|
|
||||||
|
- 一个亮色页面上可以出现暗色主题的代码块
|
||||||
|
- `.is-dark` 块包含 ~15 个 diff 视图的阴影/透明度/渐变差异值
|
||||||
|
- 这些值已全部引用 `--ms-*` 语义令牌,只是 opacity 系数不同
|
||||||
|
|
||||||
|
### Admonition header-bg 例外
|
||||||
|
|
||||||
|
提示框标题背景需要在 dark 下使用更高透明度(6% → 12%),因此
|
||||||
|
`--admonition-*-header-bg` 在 `.dark` 区块中重新定义。这是 Layer 2 中
|
||||||
|
唯一需要 dark 覆盖的语义令牌。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## shadcn 兼容矩阵
|
||||||
|
|
||||||
|
| 变量来源 | 我们的处理 | 用户如何定制 |
|
||||||
|
|---|---|---|
|
||||||
|
| shadcn 同名(`--background` 等 14 个) | `--ms-*` 桥接继承 + 回退 | 在 `:root` 设置 shadcn 变量,自动穿透 |
|
||||||
|
| 本项目独有(`--ms-info` 等 8 个) | 直接定义在 `.markstream-vue` | 在 `.markstream-vue` 上覆盖 |
|
||||||
|
| 组件语义(`--code-bg` 等 ~35 个) | 引用 `--ms-*`,定义一次 | 在 `.markstream-vue` 上覆盖 |
|
||||||
|
|
||||||
|
### shadcn 未采用(本项目不需要)
|
||||||
|
|
||||||
|
| Token | 原因 |
|
||||||
|
|---|---|
|
||||||
|
| `--card` / `--card-foreground` | 无 card 组件 |
|
||||||
|
| `--input` | 无输入框 |
|
||||||
|
| `--sidebar-*` | 无侧边栏 |
|
||||||
|
| `--chart-*` | Mermaid 自带主题 |
|
||||||
|
|
||||||
|
### 本项目扩展(shadcn 没有)
|
||||||
|
|
||||||
|
| Token | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `--ms-info` / `--ms-success` / `--ms-warning` | Admonition 语义色 |
|
||||||
|
| `--ms-diff-added` / `--ms-diff-removed` | Diff 视图 |
|
||||||
|
| `--ms-highlight` / `--ms-highlight-foreground` | `==高亮==` 语法 |
|
||||||
|
| `--ms-link` | 链接色 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前控件清单
|
||||||
|
|
||||||
|
| 控件 | 出现组件 | 涉及 tokens |
|
||||||
|
|---|---|---|
|
||||||
|
| 操作按钮(复制/折叠/全屏/导出) | CodeBlock, Mermaid, D2, Infographic, Admonition | `--code-action-*`, `--focus-ring` |
|
||||||
|
| 字号调节按钮组 | CodeBlock, MarkdownCodeBlock | `--code-action-*` |
|
||||||
|
| 模式切换(Preview/Source) | CodeBlock, Mermaid, D2, Infographic | `--code-action-active-*` |
|
||||||
|
| 缩放控制 | Mermaid, Infographic | `--code-action-*` |
|
||||||
|
| Tooltip | 全局 | `--tooltip-*` |
|
||||||
|
| Modal | CodeBlock, Mermaid, Infographic | `--modal-*` |
|
||||||
|
| Checkbox(只读) | CheckboxNode | `--ms-foreground` |
|
||||||
|
| Spinner | Image, Table, MathBlock | `--loading-spinner` |
|
||||||
|
| Shimmer 骨架屏 | CodeBlock, Table | `--loading-shimmer` |
|
||||||
|
| 错误态 | ImageNode | `--ms-destructive` |
|
||||||
|
| 可点击引用 | FootnoteRef, ReferenceNode | `--link-color` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 用户定制示例
|
||||||
|
|
||||||
|
### 场景 1:shadcn 项目(零配置接入)
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 只需引入样式,shadcn 变量自动穿透
|
||||||
|
import 'markstream-vue/dist/index.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
宿主 `:root` 上的 `--background`、`--foreground` 等自动继承进 `.markstream-vue`。
|
||||||
|
|
||||||
|
如需补充 Markdown 扩展色:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
--ms-info: 217 91% 60%;
|
||||||
|
--ms-success: 142 71% 45%;
|
||||||
|
--ms-link: 221 83% 53%;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 2:独立项目,换整体配色
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 方式 A:覆盖 bridge 识别的 shadcn 变量名 */
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 98%;
|
||||||
|
--foreground: 240 10% 10%;
|
||||||
|
--border: 240 5% 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 方式 B:直接覆盖 --ms-* bridge token */
|
||||||
|
.markstream-vue {
|
||||||
|
--ms-background: 0 0% 98%;
|
||||||
|
--ms-foreground: 240 10% 10%;
|
||||||
|
--ms-border: 240 5% 85%;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:只改代码块(覆盖 semantic token)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
--code-bg: #1e1e2e;
|
||||||
|
--code-fg: #cdd6f4;
|
||||||
|
--code-border: transparent;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 4:只改提示框色彩
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
--ms-info: 221 83% 53%;
|
||||||
|
--ms-success: 142 71% 45%;
|
||||||
|
--ms-warning: 38 92% 50%;
|
||||||
|
--ms-destructive: 0 72% 51%;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已知边界情况
|
||||||
|
|
||||||
|
**shadcn 宿主 + 仅对 markstream-vue 启用 dark**:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 宿主全局 light,但 markstream-vue 想用 dark -->
|
||||||
|
<div class="markstream-vue dark">...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
此时宿主的 light `--background` 仍会继承进来,bridge 的 dark 回退值不会生效。
|
||||||
|
**解决**:shadcn 宿主应在文档级切换 `.dark`,或通过覆盖 semantic token 单独控制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 迁移策略
|
||||||
|
|
||||||
|
分步进行,每步可独立验证:
|
||||||
|
|
||||||
|
| 阶段 | 内容 | 影响范围 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 定义 Bridge Tokens + Extension Tokens(Layer 1) | 仅新增变量,不破坏现有 |
|
||||||
|
| 2 | 定义 Semantic Tokens(Layer 2),引用 `--ms-*` | 仅新增变量,不破坏现有 |
|
||||||
|
| 3 | 逐组件替换硬编码值为 Semantic Token 引用 | 逐组件改,可逐个验证 |
|
||||||
|
| 4 | 补全 Dark 的 Bridge Token 回退值 | 暗色模式完整 |
|
||||||
|
| 5 | 移除组件内 `.is-dark` CSS 覆盖 | 简化暗色逻辑 |
|
||||||
|
| 6 | 清理:删除废弃变量、统一动画变量 | 最终打磨 |
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
# CodeBlock 主题架构
|
||||||
|
|
||||||
|
> 核心决策:Shell(外壳)与内容渲染器**严格分离**。Shell 走 token,渲染器各自管理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 整体架构
|
||||||
|
|
||||||
|
### Shell + Renderer 分离
|
||||||
|
|
||||||
|
```
|
||||||
|
内置模式(自动套 Shell):
|
||||||
|
|
||||||
|
┌─ CodeBlockShell ──────────────────────────┐
|
||||||
|
│ header: 标题、语言标签、操作按钮 │ ← token 驱动
|
||||||
|
│ border / shadow / 圆角 │
|
||||||
|
│ 折叠/展开 │
|
||||||
|
│ 骨架屏 │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Renderer(可替换) ─────────────────┐ │
|
||||||
|
│ │ Monaco / Shiki / <pre> │ │ ← 渲染器自治
|
||||||
|
│ └─────────────────────────────────────┘ │
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
自定义模式(无 Shell,用户全权控制):
|
||||||
|
|
||||||
|
┌─ UserComponent ────────────────────────────┐
|
||||||
|
│ 用户自己实现全部 UI │
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 三档内置渲染器
|
||||||
|
|
||||||
|
| 模式 | 渲染器 | Shell | 特点 | 选择方式 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Monaco | CodeBlockNode | **有** | 完整编辑器,语法高亮,diff | 默认 |
|
||||||
|
| Shiki | MarkdownCodeBlockNode | **有** | 轻量高亮,无编辑器,适合大量只读展示 | `setCustomComponents` 注册 |
|
||||||
|
| Pre | PreCodeNode | **无** | 纯 `<pre><code>`,无高亮,超轻量 | `renderCodeBlocksAsPre: true` |
|
||||||
|
|
||||||
|
CodeBlockNode 和 MarkdownCodeBlockNode 共享 `CodeBlockShell`(header、操作按钮、骨架屏、折叠)。
|
||||||
|
PreCodeNode 有意不套 Shell — 它是超轻量选项,不需要 header/按钮。需要这些功能应使用 Shiki 或 Monaco 模式。
|
||||||
|
|
||||||
|
### 自定义组件
|
||||||
|
|
||||||
|
通过 `setCustomComponents({ code_block: UserComponent })` 注册的组件:
|
||||||
|
- **不套 Shell**,直接渲染
|
||||||
|
- 用户全权控制 UI,库不干预
|
||||||
|
- 可选择通过 slot 或 composable 自行引入 Shell 能力(如果需要)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shell 与 Monaco 渲染器的边界
|
||||||
|
|
||||||
|
以下仅针对 Monaco 渲染器(CodeBlockNode)的内部架构:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ Shell(token 驱动)─────────────────────────┐
|
||||||
|
│ header / 操作按钮 / 骨架屏 / diff 外框 │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Monaco 渲染器(Monaco 主题自治)────────┐ │
|
||||||
|
│ │ 编辑器背景/前景、语法高亮 │ │
|
||||||
|
│ │ 行号、选区背景 │ │
|
||||||
|
│ │ diff 行背景、内联高亮、gutter 标记 │ │
|
||||||
|
│ └──────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 边界规则(每个元素只归一个区域)
|
||||||
|
|
||||||
|
### Shell 区域 — 页面主题 token
|
||||||
|
|
||||||
|
| 元素 | token | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 容器边框 | `var(--code-border)` | |
|
||||||
|
| **Header 背景** | `var(--code-header-bg)` | **改动:不再使用 `--vscode-editor-background`** |
|
||||||
|
| **Header 文字** | `var(--code-fg)` | **改动:不再使用 `--vscode-editor-foreground`** |
|
||||||
|
| 操作按钮默认色 | `var(--code-action-fg)` | |
|
||||||
|
| 操作按钮 hover | `var(--code-action-hover-bg/fg)` | **改动:不再使用 `--vscode-editor-selectionBackground`** |
|
||||||
|
| 操作按钮 active | `var(--code-action-active-bg/fg)` | 模式切换活跃态 |
|
||||||
|
| 骨架屏 | `var(--loading-shimmer)` | **改动:不再受 `.is-dark` 影响** |
|
||||||
|
| Diff 外框/阴影 | `var(--code-border)` 等 | |
|
||||||
|
| Diff 标题栏 | `var(--code-header-bg)` | |
|
||||||
|
|
||||||
|
**规则**:Shell 元素只用 `var(--code-*)` 和 `var(--ms-*)` token。不引用任何 `--vscode-*` 变量。
|
||||||
|
跟随页面 `.dark` 自动切换,无需 `.is-dark`。
|
||||||
|
|
||||||
|
### Monaco 区域 — Monaco 主题自治
|
||||||
|
|
||||||
|
| 元素 | 取值来源 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 编辑器背景/前景 | Monaco theme | `--vscode-editor-background/foreground` |
|
||||||
|
| 语法高亮色 | Monaco theme | 完全由 Monaco token colors 决定 |
|
||||||
|
| 行号色 | Monaco theme | `--vscode-editorLineNumber-foreground` |
|
||||||
|
| 选区背景 | Monaco theme | `--vscode-editor-selectionBackground` |
|
||||||
|
| Diff 行背景 | 基于 Monaco 明暗 | `.is-dark` 切换 opacity |
|
||||||
|
| Diff 内联高亮 | 基于 Monaco 明暗 | `.is-dark` 切换 opacity |
|
||||||
|
| Gutter 标记 | 基于 Monaco 明暗 | `.is-dark` 切换 opacity |
|
||||||
|
|
||||||
|
**规则**:Monaco 区域由 Monaco 主题 + `.is-dark` class 驱动。diff 基色(色相)仍来自全局
|
||||||
|
`--ms-diff-added` / `--ms-diff-removed`,但 opacity 和阴影深度由 `.is-dark` 控制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monaco 主题 Props 设计
|
||||||
|
|
||||||
|
### 统一为单个 `theme` prop
|
||||||
|
|
||||||
|
取代现有的 `darkTheme` + `lightTheme` + 新增的 `forceTheme` 三 prop 设计,
|
||||||
|
合并为一个灵活的 `theme` prop:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CodeBlockTheme = string | MonacoThemeObject
|
||||||
|
|
||||||
|
interface CodeBlockProps {
|
||||||
|
/**
|
||||||
|
* Monaco 主题配置。
|
||||||
|
*
|
||||||
|
* - 字符串 / 对象:固定主题,不随页面明暗切换
|
||||||
|
* theme="monokai"
|
||||||
|
* theme={...customThemeObject}
|
||||||
|
*
|
||||||
|
* - { light, dark } 配对:随页面明暗自动切换
|
||||||
|
* theme={{ light: 'github-light', dark: 'one-dark-pro' }}
|
||||||
|
*
|
||||||
|
* - 未指定:使用内置默认 light/dark 主题
|
||||||
|
*/
|
||||||
|
theme?: CodeBlockTheme | { light: CodeBlockTheme, dark: CodeBlockTheme }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 切换逻辑
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const activeTheme = computed(() => {
|
||||||
|
const t = props.theme
|
||||||
|
if (!t) {
|
||||||
|
// 未指定:内置默认
|
||||||
|
return isDark.value ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME
|
||||||
|
}
|
||||||
|
if (typeof t === 'string' || isThemeObject(t)) {
|
||||||
|
// 单主题:固定使用,不切换
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
// 配对:按页面明暗切换
|
||||||
|
return isDark.value ? t.dark : t.light
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 用法示例
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 自动切换(大多数场景) -->
|
||||||
|
<CodeBlock :theme="{ light: 'github-light', dark: 'one-dark-pro' }" />
|
||||||
|
|
||||||
|
<!-- 固定主题(不随页面切换) -->
|
||||||
|
<CodeBlock theme="monokai" />
|
||||||
|
|
||||||
|
<!-- 默认(内置 light/dark) -->
|
||||||
|
<CodeBlock />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 向后兼容
|
||||||
|
|
||||||
|
保留 `darkTheme` / `lightTheme` prop 作为 deprecated alias:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const resolvedThemeProp = computed(() => {
|
||||||
|
if (props.theme)
|
||||||
|
return props.theme
|
||||||
|
// 向后兼容
|
||||||
|
if (props.darkTheme || props.lightTheme) {
|
||||||
|
return { light: props.lightTheme ?? DEFAULT_LIGHT, dark: props.darkTheme ?? DEFAULT_DARK }
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `.is-dark` 的角色
|
||||||
|
|
||||||
|
### 触发规则
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const editorSurfaceIsDark = computed(() => {
|
||||||
|
const t = activeTheme.value
|
||||||
|
// 配对模式:跟随页面
|
||||||
|
if (!props.theme || isPairedTheme(props.theme))
|
||||||
|
return isDark.value
|
||||||
|
// 单主题模式:检测主题名/亮度
|
||||||
|
return detectThemeDarkness(t)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检测主题明暗
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function detectThemeDarkness(theme: CodeBlockTheme): boolean {
|
||||||
|
if (typeof theme === 'string')
|
||||||
|
return themeLooksDark(theme)
|
||||||
|
// 主题对象:检测 background 色的亮度
|
||||||
|
if (theme.colors?.['editor.background']) {
|
||||||
|
return getLuminance(theme.colors['editor.background']) < 128
|
||||||
|
}
|
||||||
|
// 兜底
|
||||||
|
return isDark.value
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `.is-dark` 只作用于 Monaco 区域
|
||||||
|
|
||||||
|
`.is-dark` class **只影响**编辑器内部和 diff 行/gutter 的 opacity 系数。
|
||||||
|
Shell 元素(header、按钮、边框、骨架屏)**完全不受** `.is-dark` 影响。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diff stage 背景层
|
||||||
|
|
||||||
|
**决策:移除 `--markstream-diff-stage-bg` 渐变,直接使用编辑器背景。**
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 之前 */
|
||||||
|
.code-block-container.is-diff .code-editor-layer {
|
||||||
|
background: var(--markstream-diff-stage-bg); /* 复杂渐变 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 之后 */
|
||||||
|
.code-block-container.is-diff .code-editor-layer {
|
||||||
|
background: transparent; /* Monaco 自己控制背景 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
简化层级,避免 Shell token 渐变和 Monaco 背景色叠加不协调。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `--markstream-diff-*` 变量归属
|
||||||
|
|
||||||
|
### 归 Shell(迁移到 token / 删除)
|
||||||
|
|
||||||
|
| 变量 | 处理 |
|
||||||
|
|---|---|
|
||||||
|
| `--markstream-diff-frame-border` | → `var(--code-border)` |
|
||||||
|
| `--markstream-diff-frame-shadow` | → 基于 `hsl(var(--ms-foreground) / ...)` |
|
||||||
|
| `--markstream-diff-shell-*` | → token 化 |
|
||||||
|
| `--markstream-diff-header-border` | → `var(--code-border)` |
|
||||||
|
| `--markstream-diff-stage-bg` | → **删除**(stage 背景移除) |
|
||||||
|
| `--markstream-diff-focus` | → `var(--focus-ring)` |
|
||||||
|
| `--markstream-diff-action-hover` | → `var(--code-action-hover-bg)` |
|
||||||
|
| `--markstream-diff-panel-bg*` | → token 化 |
|
||||||
|
| `--markstream-diff-panel-border` | → token 化 |
|
||||||
|
| `--markstream-diff-pane-divider` | → token 化 |
|
||||||
|
| `--markstream-diff-widget-shadow` | → 基于 `hsl(var(--ms-foreground) / ...)` |
|
||||||
|
|
||||||
|
### 归 Monaco(保留组件内,跟随 `.is-dark`)
|
||||||
|
|
||||||
|
| 变量 | 原因 |
|
||||||
|
|---|---|
|
||||||
|
| `--markstream-diff-editor-bg/fg` | 匹配 Monaco 表面 |
|
||||||
|
| `--markstream-diff-added-fg/bg` | 匹配编辑器表面亮度 |
|
||||||
|
| `--markstream-diff-removed-fg/bg` | 匹配编辑器表面亮度 |
|
||||||
|
| `--markstream-diff-added/removed-inline` | 匹配编辑器表面亮度 |
|
||||||
|
| `--markstream-diff-added/removed-gutter` | 匹配编辑器表面亮度 |
|
||||||
|
| `--markstream-diff-line-number*` | 匹配 Monaco 行号色 |
|
||||||
|
| `--markstream-diff-unchanged-*` | 匹配编辑器表面 |
|
||||||
|
| `--markstream-diff-gutter-*` | 匹配编辑器表面 |
|
||||||
|
| `--markstream-diff-added/removed-line-fill` | 匹配编辑器表面亮度 |
|
||||||
|
|
||||||
|
Diff 基色仍引用全局 token:`--ms-diff-added` / `--ms-diff-removed`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MarkdownCodeBlockNode 处理
|
||||||
|
|
||||||
|
### 现状
|
||||||
|
|
||||||
|
- 默认不使用,仅在用户通过 `setCustomComponents` 注册时生效
|
||||||
|
- 用 Shiki 渲染语法高亮(比 Monaco 轻量),不加载编辑器
|
||||||
|
- Shell 部分(header、操作按钮、骨架屏)与 CodeBlockNode **大量重复**
|
||||||
|
- 已作为公开 API 导出
|
||||||
|
|
||||||
|
### 决策:保留,接入共享 Shell
|
||||||
|
|
||||||
|
Shiki 版本服务于**大量只读代码展示**场景(如文档站、博客、教程),
|
||||||
|
这类场景下加载 Monaco 编辑器不合理(包体积大、初始化慢、无需编辑功能)。
|
||||||
|
|
||||||
|
**重构方向**:所有内置渲染器接入共享 Shell
|
||||||
|
|
||||||
|
```
|
||||||
|
CodeBlockShell(共享)
|
||||||
|
├─ header(标题、语言图标、操作按钮)
|
||||||
|
├─ 边框 / 圆角 / 阴影
|
||||||
|
├─ 骨架屏
|
||||||
|
├─ 折叠/展开
|
||||||
|
└─ token 体系接入
|
||||||
|
└─ slot: 内容区域 → 由渲染器填充
|
||||||
|
|
||||||
|
三档内置渲染器:
|
||||||
|
Shell + CodeBlockNode(Monaco) ← 默认
|
||||||
|
Shell + MarkdownCodeBlockNode(Shiki) ← 轻量高亮
|
||||||
|
Shell + PreCodeNode(<pre>) ← 纯文本
|
||||||
|
|
||||||
|
自定义组件(setCustomComponents):
|
||||||
|
→ 不套 Shell,直接渲染,用户全权控制
|
||||||
|
```
|
||||||
|
|
||||||
|
步骤:
|
||||||
|
1. 抽取 Shell 为共享子组件(header、按钮、骨架屏、折叠逻辑)
|
||||||
|
2. CodeBlockNode、MarkdownCodeBlockNode、PreCodeNode 各自作为 Shell 的内容渲染器
|
||||||
|
3. 三者的 `theme` prop 设计保持一致(字符串 = 固定,`{ light, dark }` = 配对)
|
||||||
|
4. NodeRenderer 路由逻辑:内置组件走 Shell 包裹,自定义组件直接渲染
|
||||||
|
5. MarkdownCodeBlockNode 的 Shiki 主题切换与 CodeBlockNode 的 Monaco 主题切换对齐
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 已识别的边界问题及处理
|
||||||
|
|
||||||
|
| 问题 | 决策 |
|
||||||
|
|---|---|
|
||||||
|
| Header 当前用 `--vscode-*` 颜色 | **改为 token**。Header 归 Shell,用 `--code-header-bg/fg` |
|
||||||
|
| 操作按钮 hover 用 `--vscode-editor-selectionBackground` | **改为 token**。用 `--code-action-hover-bg` |
|
||||||
|
| 骨架屏受 `.is-dark` 影响 | **改为纯 token**。骨架屏归 Shell,不看 `.is-dark` |
|
||||||
|
| 折叠时 CSS 变量仍活跃 | **可接受**。`v-show` 隐藏但变量不影响其他元素 |
|
||||||
|
| `resolvedChromeIsDark` 命名 | **重命名为 `editorSurfaceIsDark`** |
|
||||||
|
| 主题名字符串检测不精确 | **增加对象主题亮度检测**(读取 `editor.background`) |
|
||||||
|
| `syncEditorCssVars` 跨边界同步 | **限制范围**。仅同步到 Monaco 容器内,不上浮到根元素 |
|
||||||
|
| Diff stage 渐变背景 | **删除**。让 Monaco 自己控制背景 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
| 阶段 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| 1 | **Header 归 Shell**:移除 `headerStyle` 中的 `--vscode-*` 引用,改用 token |
|
||||||
|
| 2 | **按钮归 Shell**:hover 改用 `--code-action-hover-bg`,移除 `--vscode-*` |
|
||||||
|
| 3 | **骨架屏归 Shell**:移除 `.is-dark .skeleton-line` 规则 |
|
||||||
|
| 4 | **Diff stage 简化**:移除 `--markstream-diff-stage-bg` 渐变 |
|
||||||
|
| 5 | **Shell diff 变量 token 化**:将 Shell 归属变量替换为 token 引用 |
|
||||||
|
| 6 | **限制 `syncEditorCssVars`**:不再将 `--vscode-*` 同步到根容器 |
|
||||||
|
| 7 | **实现 `theme` prop**:统一 API,向后兼容 `darkTheme`/`lightTheme` |
|
||||||
|
| 8 | **重命名 `.is-dark` 推导**:`resolvedChromeIsDark` → `editorSurfaceIsDark`,增加对象主题检测 |
|
||||||
|
| 9 | **抽取共享 Shell**:CodeBlockShell.vue — header / 按钮 / 骨架屏 / 折叠,内容区域用 slot |
|
||||||
|
| 10 | **CodeBlockNode + MarkdownCodeBlockNode 接入 Shell**:PreCodeNode 不套 Shell(超轻量设计意图) |
|
||||||
|
| 11 | NodeRenderer 路由不变 — Shell 内嵌在各组件中,自定义组件直接渲染 |
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
# Component Style Inventory
|
||||||
|
|
||||||
|
> 每个组件的当前样式详情,按功能分类。
|
||||||
|
> 来源:`src/components/` 下各 `.vue` 文件的 `<style>` 块。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 文本类组件
|
||||||
|
|
||||||
|
### 1.1 HeadingNode — 标题
|
||||||
|
|
||||||
|
**文件**:`src/components/HeadingNode/HeadingNode.vue`
|
||||||
|
|
||||||
|
| 级别 | 类名 | 字号 | 字重 | 行高 | 上下边距 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| h1 | `.heading-1` | `text-4xl` | `extrabold` | `calc(10/9*1)` | `0` / `calc(8/9*1em)` |
|
||||||
|
| h2 | `.heading-2` | `text-2xl` | `bold` | `calc(4/3*1)` | `2rem` / `1rem` |
|
||||||
|
| h3 | `.heading-3` | `text-xl` | `semibold` | `calc(5/3*1)` | `calc(8/5*1em)` / `calc(3/5*1em)` |
|
||||||
|
| h4 | `.heading-4` | `text-base` | `semibold` | 默认 | `1.5rem` / `0.5rem` |
|
||||||
|
| h5 | `.heading-5` | `text-base` | 默认 | 默认 | `0` / `0` |
|
||||||
|
| h6 | `.heading-6` | `text-base` | 默认 | 默认 | `0` / `0` |
|
||||||
|
|
||||||
|
- 通用类 `.heading-node`:`font-medium leading-tight`
|
||||||
|
- 特殊规则:`hr + .heading-node` 取消 `margin-top`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 ParagraphNode — 段落
|
||||||
|
|
||||||
|
**文件**:`src/components/ParagraphNode/ParagraphNode.vue`
|
||||||
|
|
||||||
|
- `.paragraph-node`:`margin: 1.25em 0`
|
||||||
|
- 列表内嵌套时:`li .paragraph-node { margin: 0 }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.3 StrongNode / EmphasisNode — 加粗与斜体
|
||||||
|
|
||||||
|
- 无自定义样式,继承 HTML 默认 `<strong>` / `<em>` 行为
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.4 StrikethroughNode — 删除线
|
||||||
|
|
||||||
|
**文件**:`src/components/StrikethroughNode/StrikethroughNode.vue`
|
||||||
|
|
||||||
|
- `.strikethrough-node`:`text-decoration: line-through`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.5 HighlightNode — 高亮
|
||||||
|
|
||||||
|
**文件**:`src/components/HighlightNode/HighlightNode.vue`
|
||||||
|
|
||||||
|
- `.highlight-node`:`background-color: #ffff00`,`padding: 0 0.2rem`
|
||||||
|
- **待改进**:仅有亮黄色,无暗色模式适配
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.6 SubscriptNode / SuperscriptNode — 上下标
|
||||||
|
|
||||||
|
- 无自定义样式,使用 HTML 原生 `<sub>` / `<sup>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.7 InsertNode — 插入标记
|
||||||
|
|
||||||
|
- 使用 HTML 原生 `<ins>` 样式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.8 EmojiNode — 表情
|
||||||
|
|
||||||
|
**文件**:`src/components/EmojiNode/EmojiNode.vue`
|
||||||
|
|
||||||
|
- `.emoji-node`:`display: inline-block`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 链接与引用
|
||||||
|
|
||||||
|
### 2.1 LinkNode — 链接
|
||||||
|
|
||||||
|
**文件**:`src/components/LinkNode/LinkNode.vue`
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 颜色 | `var(--link-color, #0366d6)` |
|
||||||
|
| 装饰 | 默认无下划线,hover 显示 |
|
||||||
|
| hover 效果 | `text-decoration: underline`,`text-underline-offset: 0.2rem` |
|
||||||
|
| 加载态 | 脉冲下划线动画(`underlinePulse` keyframe) |
|
||||||
|
|
||||||
|
CSS 变量(可自定义):
|
||||||
|
|
||||||
|
```
|
||||||
|
--link-color, --underline-height, --underline-bottom,
|
||||||
|
--underline-opacity, --underline-rest-opacity,
|
||||||
|
--underline-duration, --underline-timing, --underline-iteration
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 FootnoteNode — 脚注
|
||||||
|
|
||||||
|
**文件**:`src/components/FootnoteNode/FootnoteNode.vue`
|
||||||
|
|
||||||
|
- 容器:`flex mt-2 mb-2 text-sm leading-relaxed`
|
||||||
|
- 上方分隔线:`border-t border-[#eaecef] pt-2`
|
||||||
|
|
||||||
|
### 2.3 FootnoteAnchorNode / FootnoteReferenceNode
|
||||||
|
|
||||||
|
- 上标链接样式,无复杂自定义
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 代码类组件
|
||||||
|
|
||||||
|
### 3.1 InlineCodeNode — 行内代码
|
||||||
|
|
||||||
|
**文件**:`src/components/InlineCodeNode/InlineCodeNode.vue`
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 字号 | `85%` |
|
||||||
|
| 字体 | `font-mono` |
|
||||||
|
| 颜色 | `hsl(var(--foreground))` |
|
||||||
|
| 背景 | `hsl(var(--secondary))` |
|
||||||
|
| 边框 | `1px solid hsl(var(--border) / 0.9)` |
|
||||||
|
| 内边距 | `px-1 py-0.5` |
|
||||||
|
| 圆角 | `rounded` |
|
||||||
|
| 换行 | `whitespace-normal break-words` |
|
||||||
|
|
||||||
|
流式更新动画:`inline-code-stream-update-fade-a/b`,duration 900ms
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 CodeBlockNode — 代码块
|
||||||
|
|
||||||
|
**文件**:`src/components/CodeBlockNode/CodeBlockNode.vue`(~2700 行)
|
||||||
|
|
||||||
|
**容器**:
|
||||||
|
- `contain: content`,`content-visibility: auto`
|
||||||
|
- `container-type: inline-size`(支持 Container Queries)
|
||||||
|
- `contain-intrinsic-size: 320px 180px`
|
||||||
|
|
||||||
|
**Header**:
|
||||||
|
- `gap: 16px`
|
||||||
|
- 标题:`font-size: 14px`,`font-weight: 650`,`letter-spacing: 0.01em`
|
||||||
|
- 说明:`font-size: 12px`,62% 透明度
|
||||||
|
- 操作按钮:`gap: 8px`,hover 态 `scale(0.98)`
|
||||||
|
|
||||||
|
**代码编辑器区域**:
|
||||||
|
- 使用 Monaco Editor / Shiki 高亮
|
||||||
|
- 主题可通过 `darkTheme` / `lightTheme` prop 切换
|
||||||
|
- 大量 `--markstream-code-*` 和 `--markstream-diff-*` CSS 变量(详见 tokens.md)
|
||||||
|
|
||||||
|
**加载骨架屏**:shimmer 动画,`linear-gradient(90deg, ...)` 背景滑动
|
||||||
|
|
||||||
|
**Diff 视图**:完整的增删行/内联 diff 颜色系统,含 Light/Dark 两套
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 PreCodeNode
|
||||||
|
|
||||||
|
**文件**:`src/components/PreCodeNode/PreCodeNode.vue`
|
||||||
|
|
||||||
|
- 简化版代码块,用于无高亮场景
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 MarkdownCodeBlockNode
|
||||||
|
|
||||||
|
- Markdown 渲染的代码块封装
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 容器类组件
|
||||||
|
|
||||||
|
### 4.1 BlockquoteNode — 引用块
|
||||||
|
|
||||||
|
**文件**:`src/components/BlockquoteNode/BlockquoteNode.vue`
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 字重 | `500` |
|
||||||
|
| 字体样式 | `italic` |
|
||||||
|
| 左边框 | `0.25rem solid var(--blockquote-border-color, #e2e8f0)` |
|
||||||
|
| 左内边距 | `1em` |
|
||||||
|
| 上下边距 | `1.6em` |
|
||||||
|
| 引号字符 | `"\201C" "\201D" "\2018" "\2019"` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 AdmonitionNode — 提示框
|
||||||
|
|
||||||
|
**文件**:`src/components/AdmonitionNode/AdmonitionNode.vue`
|
||||||
|
|
||||||
|
**基础样式**:
|
||||||
|
- 左边框:`4px solid`
|
||||||
|
- 圆角:`4px`
|
||||||
|
- 无 `overflow: hidden`
|
||||||
|
|
||||||
|
**类型色彩**:
|
||||||
|
|
||||||
|
| 类型 | 边框色 | 标题背景 |
|
||||||
|
|---|---|---|
|
||||||
|
| note / info | `#448aff` | `rgba(68, 138, 255, 0.06)` |
|
||||||
|
| tip | `#00bfa5` | `rgba(0, 191, 165, 0.06)` |
|
||||||
|
| warning / caution | `#ff9100` | `rgba(255, 145, 0, 0.06)` |
|
||||||
|
| danger / error | `#ff5252` | `rgba(255, 82, 82, 0.06)` |
|
||||||
|
|
||||||
|
**Dark 模式**:背景透明度从 6% 提升到 12%
|
||||||
|
|
||||||
|
**标题栏**:`padding: 0.5rem 1rem`,`font-weight: 600`,带图标
|
||||||
|
**内容区**:`padding: 0.5rem 1rem 1rem`
|
||||||
|
**折叠按钮**:`border-radius: 4px`,`focus: outline 2px`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 VmrContainerNode — 通用容器
|
||||||
|
|
||||||
|
- 自定义容器节点封装
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 列表类组件
|
||||||
|
|
||||||
|
### 5.1 ListNode — 列表
|
||||||
|
|
||||||
|
**文件**:`src/components/ListNode/ListNode.vue`
|
||||||
|
|
||||||
|
- `.list-node`:`my-5 pl-[calc(13/8*1em)]`
|
||||||
|
- `.list-decimal`:`list-style-type: decimal`
|
||||||
|
- `.list-disc`:`list-style-type: disc`
|
||||||
|
- 移动端:`max-lg:my-[calc(4/3*1em)] max-lg:pl-[calc(14/9*1em)]`
|
||||||
|
|
||||||
|
### 5.2 ListItemNode — 列表项
|
||||||
|
|
||||||
|
**文件**:`src/components/ListItemNode/ListItemNode.vue`
|
||||||
|
|
||||||
|
- `.list-item`:`pl-1.5 my-2`,`dir="auto"`
|
||||||
|
- 有序列表标记色:`var(--list-item-counter-marker, #64748b)`,`line-height: 1.6`
|
||||||
|
- 无序列表标记色:`var(--list-item-marker, #cbd5e1)`
|
||||||
|
|
||||||
|
### 5.3 CheckboxNode — 复选框
|
||||||
|
|
||||||
|
**文件**:`src/components/CheckboxNode/CheckboxNode.vue`
|
||||||
|
|
||||||
|
- `.checkbox-node`:`margin-right: 0.5rem`,`vertical-align: middle`
|
||||||
|
- `.checkbox-input`:`margin: 0`,`cursor: default`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 DefinitionListNode — 定义列表
|
||||||
|
|
||||||
|
**文件**:`src/components/DefinitionListNode/DefinitionListNode.vue`
|
||||||
|
|
||||||
|
- `.definition-list`:`margin: 0 0 1rem`
|
||||||
|
- `.definition-term`:`font-weight: 600`,`margin-top: 0.5rem`
|
||||||
|
- `.definition-desc`:`margin-left: 1rem`,`margin-bottom: 0.5rem`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 表格组件
|
||||||
|
|
||||||
|
### 6.1 TableNode — 表格
|
||||||
|
|
||||||
|
**文件**:`src/components/TableNode/TableNode.vue`
|
||||||
|
|
||||||
|
**容器**:
|
||||||
|
- `.table-node-wrapper`:`max-width: 100%`,`overflow-x: auto`,`-webkit-overflow-scrolling: touch`,`scrollbar-gutter: stable`
|
||||||
|
|
||||||
|
**表格**:
|
||||||
|
- `table-layout: fixed`,`width: 100%`,`border-collapse: collapse`
|
||||||
|
- `my-8 text-sm`
|
||||||
|
- `th`:`font-semibold p-[calc(4/7*1em)]`
|
||||||
|
- `td`:`p-[calc(4/7*1em)]`
|
||||||
|
- 边框色:`var(--table-border, #cbd5e1)`
|
||||||
|
|
||||||
|
**加载态**:shimmer 骨架动画 + 旋转 spinner
|
||||||
|
|
||||||
|
**无障碍**:`.sr-only` 屏幕阅读器文本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 媒体类组件
|
||||||
|
|
||||||
|
### 7.1 ImageNode — 图片
|
||||||
|
|
||||||
|
**文件**:`src/components/ImageNode/ImageNode.vue`
|
||||||
|
|
||||||
|
- `.image-node__img`:`max-width: 24rem`
|
||||||
|
- 内联图片:`display: inline-block`,`vertical-align: middle`,无固定尺寸
|
||||||
|
- 切换动画:`opacity 220ms ease` + `translateY(6px)` 过渡
|
||||||
|
- 加载旋转器:`border: 2px solid`,`animate-spin`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7.2 MermaidBlockNode — Mermaid 图表
|
||||||
|
|
||||||
|
**文件**:`src/components/MermaidBlockNode/MermaidBlockNode.vue`
|
||||||
|
|
||||||
|
**容器**:`rounded-lg border shadow-sm overflow-hidden`
|
||||||
|
|
||||||
|
**Header**:
|
||||||
|
- Light:`bg-gray-50 border-gray-200`
|
||||||
|
- Dark:`bg-gray-800 border-gray-700/30`
|
||||||
|
- 模式切换按钮带 `shadow-sm`
|
||||||
|
|
||||||
|
**图表区域**:
|
||||||
|
- `.mermaid-container`:`min-h-[360px]`,`transition-[height] duration-150 ease-out`
|
||||||
|
- `content-visibility: auto`,`contain: content`,`contain-intrinsic-size: 360px 240px`
|
||||||
|
- 支持缩放、拖拽(`cursor-grab` / `cursor-grabbing`)
|
||||||
|
|
||||||
|
**全屏弹窗**:
|
||||||
|
- 遮罩:`bg-black/70`,`z-50`
|
||||||
|
- 面板:`shadow-lg`,入场 `scale(0.98)→1.0`
|
||||||
|
|
||||||
|
**操作按钮**:
|
||||||
|
- Light:`text-gray-600 hover:bg-gray-200`
|
||||||
|
- Dark:`text-gray-400 hover:bg-gray-700`
|
||||||
|
- Active:`scale(0.98)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7.3 MathBlockNode / MathInlineNode — 数学公式
|
||||||
|
|
||||||
|
**文件**:`src/components/MathBlockNode/MathBlockNode.vue`
|
||||||
|
|
||||||
|
**块级**:
|
||||||
|
- `text-center overflow-x-auto min-h-[40px]`
|
||||||
|
- 加载遮罩:`backdrop-filter: blur(2px)`
|
||||||
|
- Spinner:`20px`,`border: 2px solid`,`0.8s linear spin`
|
||||||
|
- 渲染中:`opacity: 0.3`,`transition: opacity 0.2s ease`
|
||||||
|
- 切换:`opacity 0.3s ease`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7.4 D2BlockNode — D2 图表
|
||||||
|
|
||||||
|
- 类似 MermaidBlockNode,用于 D2 图表渲染
|
||||||
|
|
||||||
|
### 7.5 InfographicBlockNode — 信息图
|
||||||
|
|
||||||
|
- 信息图嵌入容器
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 分隔与装饰组件
|
||||||
|
|
||||||
|
### 8.1 ThematicBreakNode — 分割线
|
||||||
|
|
||||||
|
**文件**:`src/components/ThematicBreakNode/ThematicBreakNode.vue`
|
||||||
|
|
||||||
|
- `.hr-node`:`border-t border-[var(--hr-border-color, #e2e8f0)] my-12`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. HTML 嵌入
|
||||||
|
|
||||||
|
### 9.1 HtmlBlockNode / HtmlInlineNode
|
||||||
|
|
||||||
|
- 原始 HTML 渲染,样式由内容自身决定
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 辅助组件
|
||||||
|
|
||||||
|
### 10.1 Tooltip — 提示浮层
|
||||||
|
|
||||||
|
**文件**:`src/components/Tooltip/Tooltip.vue`
|
||||||
|
|
||||||
|
| 属性 | Light | Dark |
|
||||||
|
|---|---|---|
|
||||||
|
| 背景 | `bg-white` | `bg-gray-900` |
|
||||||
|
| 文字 | `text-gray-900` | `text-white` |
|
||||||
|
| 边框 | `border-gray-200` | `border-gray-700` |
|
||||||
|
| 圆角 | `rounded-md` | 同 |
|
||||||
|
| 阴影 | `shadow-md` | 同 |
|
||||||
|
| 层级 | `z-[9999]` | 同 |
|
||||||
|
|
||||||
|
入场动画:`opacity 120ms linear`,`translateY(-6px) scale(0.98) → 0 scale(1)`
|
||||||
|
位移动画:`transform 220ms cubic-bezier(.16,1,.3,1)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.2 NodeRenderer — 渲染器
|
||||||
|
|
||||||
|
**文件**:`src/components/NodeRenderer/NodeRenderer.vue`
|
||||||
|
|
||||||
|
- 根容器传递 `isDark` prop 到所有子组件
|
||||||
|
- 使用 `.markstream-vue` 作为作用域根类
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.3 ReferenceNode — 引用
|
||||||
|
|
||||||
|
- 引用标记渲染
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# Redesign Checklist
|
||||||
|
|
||||||
|
> 所有需要重新设计的元素清单,按优先级和类别组织。
|
||||||
|
> 架构方案:**带默认样式 + CSS 变量全量可定制**(详见 architecture.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Token 化改造(最高优先级,前置工作)
|
||||||
|
|
||||||
|
> 所有后续设计工作的基础。详细架构见 `architecture.md`。
|
||||||
|
|
||||||
|
- [ ] **定义 Base Tokens** — 在 `index.css` 中建立完整的基础变量集(色彩、排版、间距、圆角、阴影)
|
||||||
|
- [ ] **定义 Semantic Tokens** — 组件级语义变量,引用 Base Tokens(如 `--code-bg: var(--secondary)`)
|
||||||
|
- [ ] **逐组件替换硬编码值** — 把所有组件中的硬编码色值、间距替换为 Semantic Token 引用
|
||||||
|
- [ ] **补全 Dark Base Tokens** — `.dark` 下覆盖全部 Base Tokens,实现完整暗色
|
||||||
|
- [ ] **移除组件级 `.is-dark` CSS** — 暗色样式全部由 Base Token 覆盖驱动,组件不再单独处理
|
||||||
|
- [ ] **统一暗色触发方式** — 去掉 `.is-dark`,统一为根容器 `.dark` 类
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A. 全局体系(高优先级)
|
||||||
|
|
||||||
|
### A1. Design Tokens 统一
|
||||||
|
|
||||||
|
- [ ] **CSS 变量命名规范** — 当前全局用 HSL 裸值 (`--border: 214.3 31.8% 91.4%`),组件用 HEX (`#111827`)、RGB (`rgb(229 231 235)`)、rgba 混用。需统一为一套命名和格式体系
|
||||||
|
- [ ] **颜色格式统一** — 决定统一使用 HSL、HEX 还是 oklch,消除混用
|
||||||
|
- [ ] **语义色彩层级** — 目前仅 6 个全局变量,缺乏 `--accent`、`--destructive`、`--success`、`--warning` 等语义色
|
||||||
|
- [ ] **组件硬编码颜色提取** — 大量颜色硬编码在组件中:`#e2e8f0`、`#eaecef`、`#cbd5e1`、`#64748b`、`#0366d6`、`#ffff00` 等,需抽取为全局变量
|
||||||
|
|
||||||
|
### A2. 暗色模式补全
|
||||||
|
|
||||||
|
- [ ] **HighlightNode** — 高亮仅 `#ffff00`,暗色下刺眼且与暗色背景冲突
|
||||||
|
- [ ] **FootnoteNode** — 固定 `#eaecef` 边框色,无暗色适配
|
||||||
|
- [ ] **BlockquoteNode** — `--blockquote-border-color` 无暗色默认值
|
||||||
|
- [ ] **TableNode** — `--table-border` 无暗色默认值
|
||||||
|
- [ ] **ListItemNode** — `--list-item-marker` / `--list-item-counter-marker` 无暗色默认值
|
||||||
|
- [ ] **ThematicBreakNode** — `--hr-border-color` 无暗色默认值
|
||||||
|
- [ ] **ImageNode** — 无暗色背景适配
|
||||||
|
|
||||||
|
### A3. 暗色触发方式统一
|
||||||
|
|
||||||
|
- [ ] **统一暗色类名** — 全局用 `.dark`,组件用 `.is-dark`,MermaidBlockNode 用条件 Tailwind 类,需统一为一种方式
|
||||||
|
- [ ] **消除冗余判断** — `isDark` prop 传递链 + CSS class + media query 三层并存,简化为清晰的单一优先级链
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## B. 色彩体系(高优先级)
|
||||||
|
|
||||||
|
### B1. 基础色板
|
||||||
|
|
||||||
|
- [ ] **重新定义色板** — 当前 Light/Dark 色彩对比度、和谐度需审查
|
||||||
|
- [ ] **灰阶系统** — 散落多个灰色系:Tailwind gray、slate、自定义 HEX,需统一
|
||||||
|
- [ ] **中性色梯度** — 建立从 50-950 的完整中性色阶(用于背景、边框、文字等)
|
||||||
|
|
||||||
|
### B2. 语义色彩
|
||||||
|
|
||||||
|
- [ ] **Admonition 色彩** — `#448aff`、`#00bfa5`、`#ff9100`、`#ff5252` 需要重新评估与整体色板的协调性
|
||||||
|
- [ ] **Diff 色彩** — Added/Removed 的 Light/Dark 配色需与新色板统一
|
||||||
|
- [ ] **Link 色** — `#0366d6` 来自 GitHub 风格,需要决定是否保留
|
||||||
|
- [ ] **Highlight 色** — `#ffff00` 过于刺眼,需要更精致的方案
|
||||||
|
|
||||||
|
### B3. 对比度与可读性
|
||||||
|
|
||||||
|
- [ ] **WCAG AA 达标检查** — 所有前景/背景色组合的对比度检查
|
||||||
|
- [ ] **暗色模式对比度** — 确保暗色下文字可读性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C. 排版系统(中优先级)
|
||||||
|
|
||||||
|
### C1. 标题层级
|
||||||
|
|
||||||
|
- [ ] **h1-h6 字号梯度** — 当前 h4/h5/h6 都是 `text-base`,层级区分不足
|
||||||
|
- [ ] **标题间距** — 使用 calc 表达式(如 `calc(8/9*1em)`),可读性差,考虑简化
|
||||||
|
- [ ] **标题字重梯度** — extrabold → bold → semibold → semibold → 默认,h5/h6 缺乏区分
|
||||||
|
|
||||||
|
### C2. 正文排版
|
||||||
|
|
||||||
|
- [ ] **段落间距** — `1.25em` 是否合适需评估
|
||||||
|
- [ ] **行高系统** — 各标题用不同 calc 行高,缺乏统一的行高阶梯
|
||||||
|
- [ ] **字体栈** — 未显式定义,依赖 Tailwind 默认,考虑是否需要自定义
|
||||||
|
|
||||||
|
### C3. 代码排版
|
||||||
|
|
||||||
|
- [ ] **行内代码字号** — `85%` 与正文的视觉平衡
|
||||||
|
- [ ] **代码块字号** — `12px` 固定值,缺乏响应式
|
||||||
|
- [ ] **代码标题字号** — `14px` / `12px`,考虑是否与排版系统关联
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D. 间距系统(中优先级)
|
||||||
|
|
||||||
|
### D1. 垂直间距
|
||||||
|
|
||||||
|
- [ ] **统一间距阶梯** — 当前混用 rem、em、calc 表达式、Tailwind 间距值
|
||||||
|
- [ ] **块间距规范** — 段落 `1.25em`、引用块 `1.6em`、列表 `1.25rem`、表格 `2rem`、分割线 `3rem`,缺乏统一节奏
|
||||||
|
- [ ] **嵌套间距** — 列表内段落归零、其他嵌套场景需检查
|
||||||
|
|
||||||
|
### D2. 水平间距
|
||||||
|
|
||||||
|
- [ ] **列表缩进** — `calc(13/8*1em)` 难以理解,考虑使用更直观的值
|
||||||
|
- [ ] **提示框内边距** — `0.5rem 1rem` 是否与其他容器协调
|
||||||
|
- [ ] **表格单元格** — `calc(4/7*1em)` 不够直观
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## E. 边框与装饰(中优先级)
|
||||||
|
|
||||||
|
### E1. 边框系统
|
||||||
|
|
||||||
|
- [ ] **边框色统一** — 至少 5 种边框色散落各处:`var(--border)`、`#eaecef`、`#e2e8f0`、`#cbd5e1`、`rgb(229 231 235)`
|
||||||
|
- [ ] **边框粗细** — `1px`、`0.25rem`、`4px` 混用
|
||||||
|
- [ ] **边框圆角** — `rounded`(4px)、`4px`、`rounded-md`(6px)、`rounded-lg`(8px) 混用,需建立圆角阶梯
|
||||||
|
|
||||||
|
### E2. 阴影系统
|
||||||
|
|
||||||
|
- [ ] **阴影层级** — `shadow-sm`、`shadow-md`、`shadow-lg` 加上自定义 Diff 阴影,需统一为设计令牌
|
||||||
|
- [ ] **暗色阴影** — 当前阴影在暗色下效果需检查
|
||||||
|
|
||||||
|
### E3. 分隔线
|
||||||
|
|
||||||
|
- [ ] **ThematicBreakNode** — 仅 `border-t`,考虑是否需要更多样式选项
|
||||||
|
- [ ] **FootnoteNode** — 使用独立边框色 `#eaecef`,应统一
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## F. 组件逐一审查
|
||||||
|
|
||||||
|
### F1. 代码块(CodeBlockNode)— 复杂度最高
|
||||||
|
|
||||||
|
- [ ] Header 布局与视觉
|
||||||
|
- [ ] 操作按钮样式
|
||||||
|
- [ ] 加载骨架屏效果
|
||||||
|
- [ ] Diff 视图完整样式
|
||||||
|
- [ ] Diff 阴影和渐变(是否过度设计?)
|
||||||
|
- [ ] Monaco 主题集成
|
||||||
|
- [ ] Container Query 断点
|
||||||
|
- [ ] 行号区域样式
|
||||||
|
|
||||||
|
### F2. 提示框(AdmonitionNode)
|
||||||
|
|
||||||
|
- [ ] 整体视觉风格(左边框 vs 全边框 vs 背景色块)
|
||||||
|
- [ ] 图标设计
|
||||||
|
- [ ] 折叠交互
|
||||||
|
- [ ] 4 种语义类型的色彩协调
|
||||||
|
- [ ] 暗色模式效果
|
||||||
|
|
||||||
|
### F3. 表格(TableNode)
|
||||||
|
|
||||||
|
- [ ] 表头样式
|
||||||
|
- [ ] 斑马纹/分隔线选择
|
||||||
|
- [ ] 溢出横向滚动指示
|
||||||
|
- [ ] 加载态设计
|
||||||
|
- [ ] 对齐方式视觉
|
||||||
|
|
||||||
|
### F4. Mermaid 图表(MermaidBlockNode)
|
||||||
|
|
||||||
|
- [ ] 容器边框与阴影
|
||||||
|
- [ ] Header 栏布局
|
||||||
|
- [ ] 模式切换按钮组
|
||||||
|
- [ ] 缩放/拖拽交互反馈
|
||||||
|
- [ ] 全屏弹窗设计
|
||||||
|
- [ ] 操作按钮组样式
|
||||||
|
|
||||||
|
### F5. 数学公式(MathBlockNode)
|
||||||
|
|
||||||
|
- [ ] 加载态(blur 遮罩是否合适)
|
||||||
|
- [ ] 渲染过渡效果
|
||||||
|
- [ ] 块级/内联一致性
|
||||||
|
|
||||||
|
### F6. 图片(ImageNode)
|
||||||
|
|
||||||
|
- [ ] 最大宽度(`24rem` 是否合理)
|
||||||
|
- [ ] 占位与加载态
|
||||||
|
- [ ] 错误态设计
|
||||||
|
- [ ] 切换过渡效果
|
||||||
|
|
||||||
|
### F7. 链接(LinkNode)
|
||||||
|
|
||||||
|
- [ ] 链接色
|
||||||
|
- [ ] hover 效果
|
||||||
|
- [ ] 加载脉冲动画
|
||||||
|
- [ ] 外部链接指示
|
||||||
|
|
||||||
|
### F8. 引用块(BlockquoteNode)
|
||||||
|
|
||||||
|
- [ ] 左边框风格
|
||||||
|
- [ ] 斜体是否保留
|
||||||
|
- [ ] 嵌套引用视觉
|
||||||
|
|
||||||
|
### F9. 列表(ListNode / ListItemNode)
|
||||||
|
|
||||||
|
- [ ] 标记颜色与样式
|
||||||
|
- [ ] 嵌套缩进
|
||||||
|
- [ ] 任务列表(Checkbox)样式
|
||||||
|
|
||||||
|
### F10. Tooltip
|
||||||
|
|
||||||
|
- [ ] 背景/边框/阴影
|
||||||
|
- [ ] 入场出场动画
|
||||||
|
- [ ] 箭头指示器(当前无箭头)
|
||||||
|
|
||||||
|
### F11. 其他组件
|
||||||
|
|
||||||
|
- [ ] HighlightNode — 高亮色
|
||||||
|
- [ ] StrikethroughNode — 删除线样式
|
||||||
|
- [ ] SubscriptNode / SuperscriptNode — 上下标
|
||||||
|
- [ ] EmojiNode — 表情显示
|
||||||
|
- [ ] DefinitionListNode — 定义列表排版
|
||||||
|
- [ ] FootnoteNode — 脚注分隔与排版
|
||||||
|
- [ ] HtmlBlockNode / HtmlInlineNode — HTML 嵌入样式隔离
|
||||||
|
- [ ] D2BlockNode — D2 图表容器
|
||||||
|
- [ ] InfographicBlockNode — 信息图容器
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## G. 动画与交互(低优先级)
|
||||||
|
|
||||||
|
### G1. 动画时长体系
|
||||||
|
|
||||||
|
- [ ] **建立统一时长阶梯** — 当前 120ms / 150ms / 180ms / 200ms / 220ms / 300ms / 900ms / 1600ms 过于分散
|
||||||
|
- [ ] **缓动函数规范** — ease / ease-out / ease-in-out / linear / cubic-bezier 各用各的
|
||||||
|
|
||||||
|
### G2. 交互反馈
|
||||||
|
|
||||||
|
- [ ] **按钮点击效果** — `scale(0.98)` 是否统一应用于所有可交互元素
|
||||||
|
- [ ] **Focus 状态** — 仅 Admonition 折叠按钮有 focus outline,其他按钮需补全
|
||||||
|
- [ ] **Hover 效果** — 各组件 hover 效果不统一
|
||||||
|
|
||||||
|
### G3. 加载态
|
||||||
|
|
||||||
|
- [ ] **Shimmer 动画** — CodeBlock 和 Table 各自实现,需抽取为复用
|
||||||
|
- [ ] **Spinner 样式** — MathBlock / Table / Image 三种不同 spinner,需统一
|
||||||
|
- [ ] **Blur 遮罩** — MathBlock 使用 `backdrop-filter: blur(2px)`,是否推广
|
||||||
|
|
||||||
|
### G4. 无障碍动效
|
||||||
|
|
||||||
|
- [ ] **`prefers-reduced-motion` 覆盖** — 仅 InlineCodeNode 和 LinkNode 支持,需全面覆盖
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## H. 架构改进(低优先级)
|
||||||
|
|
||||||
|
### H1. CSS 架构
|
||||||
|
|
||||||
|
- [ ] **样式文件组织** — 当前仅一个 `index.css`,考虑拆分为 tokens / base / components
|
||||||
|
- [ ] **Tailwind @apply 过度使用** — 部分组件大量 `@apply`,考虑平衡
|
||||||
|
- [ ] **Scoped vs Global** — 部分样式需要全局穿透(`:deep()`),架构是否合理
|
||||||
|
|
||||||
|
### H2. 构建优化
|
||||||
|
|
||||||
|
- [ ] **CSS 产物体积** — 检查未使用的 Tailwind 类
|
||||||
|
- [ ] **双构建是否必要** — `index.css` + `index.tailwind.css` 的策略是否最优
|
||||||
|
- [ ] **PostCSS 插件链** — 是否需要增加 cssnano 等优化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 统计摘要
|
||||||
|
|
||||||
|
| 类别 | 待设计项数 |
|
||||||
|
|---|---|
|
||||||
|
| A. 全局体系 | 10 |
|
||||||
|
| B. 色彩体系 | 9 |
|
||||||
|
| C. 排版系统 | 8 |
|
||||||
|
| D. 间距系统 | 6 |
|
||||||
|
| E. 边框与装饰 | 7 |
|
||||||
|
| F. 组件逐一审查 | 40+ |
|
||||||
|
| G. 动画与交互 | 10 |
|
||||||
|
| H. 架构改进 | 5 |
|
||||||
|
| **总计** | **~95 项** |
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# 需求分析
|
||||||
|
|
||||||
|
> 来源:[Simon-He95/markstream-vue#343](https://github.com/Simon-He95/markstream-vue/issues/343)
|
||||||
|
> 标签:`enhancement` `help wanted` `pr welcome` `render`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 原文核心要求
|
||||||
|
|
||||||
|
**目标**:Make the streaming rendering UI look significantly better, component by component, without losing readability.
|
||||||
|
|
||||||
|
### 1. 视觉重设计(对方核心诉求)
|
||||||
|
|
||||||
|
> "the visual presentation during streaming still feels rough in places"
|
||||||
|
> "feel more intentional, attractive, and consistent"
|
||||||
|
|
||||||
|
- 重新思考流式内容的视觉设计
|
||||||
|
- 调整各组件的样式
|
||||||
|
- 改善层级(hierarchy)、间距(spacing)、节奏(rhythm)
|
||||||
|
- 整体美感提升
|
||||||
|
|
||||||
|
### 2. 流式渲染特有体验
|
||||||
|
|
||||||
|
> "Making partially streamed content feel smoother and more pleasant to read"
|
||||||
|
> "streaming-specific states or transitions that should look better"
|
||||||
|
|
||||||
|
- 部分渲染时的过渡效果
|
||||||
|
- 流式内容逐步呈现的视觉连贯性
|
||||||
|
- 打字机效果 / 骨架屏 / 渐入动画的打磨
|
||||||
|
|
||||||
|
### 3. 逐组件打磨
|
||||||
|
|
||||||
|
涉及所有渲染组件:
|
||||||
|
|
||||||
|
| 组件 | 需关注 |
|
||||||
|
|---|---|
|
||||||
|
| headings | 字号梯度、字重、间距 |
|
||||||
|
| paragraphs | 行高、段间距 |
|
||||||
|
| lists | 缩进、标记样式、嵌套 |
|
||||||
|
| blockquotes | 边框风格、斜体、配色 |
|
||||||
|
| links | 链接色、hover 效果 |
|
||||||
|
| inline code / code blocks | 背景、边框、字号、语法高亮主题 |
|
||||||
|
| tables | 表头、斑马纹、边框、溢出 |
|
||||||
|
| images | 占位、加载态、错误态 |
|
||||||
|
| separators | 分割线样式 |
|
||||||
|
|
||||||
|
### 4. 对方接受的贡献形式
|
||||||
|
|
||||||
|
- design direction proposals(设计方向提案)
|
||||||
|
- screenshots or mockups(截图或原型)
|
||||||
|
- style audits(样式审计)
|
||||||
|
- focused PRs for specific components(逐组件 PR)
|
||||||
|
- a broader visual refresh(整体视觉刷新)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 我们的工作 vs 需求的映射
|
||||||
|
|
||||||
|
| 需求 | 我们的进展 | 状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| 样式审计 | `design/tokens.md` + `design/components.md` + `design/themes.md` | ✅ 已完成 |
|
||||||
|
| 设计方向提案 | `design/architecture.md` token 体系 | ✅ 已完成 |
|
||||||
|
| 代码块专项设计 | `design/codeblock.md` Shell/Monaco 分离 | ✅ 已完成 |
|
||||||
|
| token 基础设施落地 | `src/index.css` Layer 1 + Layer 2,25 组件迁移 | ✅ 已完成 |
|
||||||
|
| 暗色模式修复 | Teleport dark、独立组件 dark | ✅ 已完成 |
|
||||||
|
| **视觉重设计** | 未开始 | ❌ **核心差距** |
|
||||||
|
| **流式渲染体验打磨** | 未开始 | ❌ **核心差距** |
|
||||||
|
| **逐组件视觉优化** | 未开始 | ❌ **核心差距** |
|
||||||
|
| **截图/对比** | 无 | ❌ 需要产出 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步工作优先级
|
||||||
|
|
||||||
|
### P0 — 对方最想看到的
|
||||||
|
|
||||||
|
1. **逐组件视觉重设计** — 在 token 基础上调整每个组件的实际样式值(颜色搭配、间距节奏、圆角阴影、字号层级)
|
||||||
|
2. **流式渲染过渡效果** — 打字机淡入、骨架屏到内容的过渡、部分渲染时的视觉连贯性
|
||||||
|
3. **Before/After 截图** — 每个改进需要有视觉对比,这是 PR review 的关键材料
|
||||||
|
|
||||||
|
### P1 — 架构收尾
|
||||||
|
|
||||||
|
4. CodeBlock Shell/Monaco 分离实施(`design/codeblock.md` 阶段 1-11)
|
||||||
|
5. 共享 Shell 抽取(三档内置渲染器统一外壳)
|
||||||
|
|
||||||
|
### P2 — 长期改进
|
||||||
|
|
||||||
|
6. 动画时长体系统一
|
||||||
|
7. 无障碍(prefers-reduced-motion 全覆盖、focus 环统一)
|
||||||
|
8. 性能优化(content-visibility 策略审查)
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# Theming System
|
||||||
|
|
||||||
|
> markstream-vue 的主题机制文档:Light/Dark 模式实现、CSS 变量架构、主题切换方式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 主题架构概览
|
||||||
|
|
||||||
|
markstream-vue 采用 **混合主题方案**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Props (isDark) → CSS Class (.is-dark / .dark) → CSS Variables Override → 组件渲染
|
||||||
|
```
|
||||||
|
|
||||||
|
三层机制互为补充:
|
||||||
|
|
||||||
|
| 层级 | 机制 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. Props | `isDark: boolean` | 由宿主应用传入,最高优先级 |
|
||||||
|
| 2. CSS Class | `.dark` / `.is-dark` | 添加到根容器或组件上 |
|
||||||
|
| 3. Media Query | `prefers-color-scheme: dark` | 系统偏好兜底 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 全局主题变量
|
||||||
|
|
||||||
|
### 2.1 挂载方式
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Light(默认) */
|
||||||
|
.markstream-vue {
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--secondary: 210 40% 96%;
|
||||||
|
--muted: 210 40% 96%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark — 两种触发方式 */
|
||||||
|
.dark .markstream-vue, /* 父级添加 .dark 类 */
|
||||||
|
.markstream-vue.dark { /* 自身添加 .dark 类 */
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 使用方式
|
||||||
|
|
||||||
|
在组件中通过 `hsl()` 引用:
|
||||||
|
|
||||||
|
```css
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
background-color: hsl(var(--secondary));
|
||||||
|
border: 1px solid hsl(var(--border) / 0.9);
|
||||||
|
```
|
||||||
|
|
||||||
|
Tailwind 配置中也引用这些变量(带回退值):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const fallback = {
|
||||||
|
foreground: 'hsl(var(--foreground), 217.2 22.08% 19.5%)',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 组件级暗色模式
|
||||||
|
|
||||||
|
### 3.1 Props 传递链
|
||||||
|
|
||||||
|
```
|
||||||
|
NodeRenderer (isDark prop)
|
||||||
|
└─ 所有子组件 (isDark prop)
|
||||||
|
└─ :class="{ 'is-dark': props.isDark }"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 CSS 类切换模式
|
||||||
|
|
||||||
|
各组件使用 `.is-dark` 类覆盖 CSS 变量:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<div class="code-block-container" :class="{ 'is-dark': props.isDark }">
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.code-block-container {
|
||||||
|
--markstream-code-fallback-bg: #ffffff;
|
||||||
|
}
|
||||||
|
.code-block-container.is-dark {
|
||||||
|
--markstream-code-fallback-bg: #111827;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 各组件暗色实现方式
|
||||||
|
|
||||||
|
| 组件 | 暗色触发 | 方式 |
|
||||||
|
|---|---|---|
|
||||||
|
| 全局变量 | `.dark .markstream-vue` | CSS 变量覆盖 |
|
||||||
|
| CodeBlockNode | `.is-dark` class | ~30+ CSS 变量覆盖 |
|
||||||
|
| AdmonitionNode | `.is-dark` class | CSS 变量覆盖 + 透明度调整 |
|
||||||
|
| MermaidBlockNode | `:class` 条件 | Tailwind dark 类名切换 |
|
||||||
|
| Tooltip | `.is-dark` class | `bg-gray-900 text-white` |
|
||||||
|
| MathBlockNode | `.is-dark` class | spinner 颜色反转 |
|
||||||
|
| ImageNode | - | 无暗色适配 |
|
||||||
|
| HighlightNode | - | **无暗色适配**(固定 `#ffff00`) |
|
||||||
|
| BlockquoteNode | - | 仅通过全局 `--blockquote-border-color` |
|
||||||
|
| TableNode | - | 仅通过 `--table-border` |
|
||||||
|
| ListItemNode | - | 仅通过 `--list-item-marker` 等 |
|
||||||
|
| FootnoteNode | - | 固定 `#eaecef` 边框色,**无暗色适配** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Monaco Editor 主题
|
||||||
|
|
||||||
|
代码块支持独立的编辑器主题系统:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Props {
|
||||||
|
darkTheme?: CodeBlockMonacoTheme // 暗色编辑器主题
|
||||||
|
lightTheme?: CodeBlockMonacoTheme // 亮色编辑器主题
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行时根据 isDark 切换
|
||||||
|
const activeTheme = props.isDark ? props.darkTheme : props.lightTheme
|
||||||
|
```
|
||||||
|
|
||||||
|
支持:
|
||||||
|
- 字符串形式:内置主题名
|
||||||
|
- 对象形式:`CodeBlockMonacoThemeObject` 自定义主题
|
||||||
|
|
||||||
|
VSCode 变量自动映射:
|
||||||
|
- `--vscode-editor-background`
|
||||||
|
- `--vscode-editor-foreground`
|
||||||
|
- `--vscode-editor-selectionBackground`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 媒体查询兜底
|
||||||
|
|
||||||
|
部分组件使用系统偏好作为兜底:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.math-loading-spinner {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
JavaScript 检测:
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Tailwind 作用域隔离
|
||||||
|
|
||||||
|
所有 Tailwind 工具类限定在 `.markstream-vue` 内生效,避免全局污染:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// tailwind.config.js
|
||||||
|
important: '.markstream-vue'
|
||||||
|
```
|
||||||
|
|
||||||
|
按钮重置使用 `:where()` 保持低特异性:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:where(.markstream-vue) button {
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 双构建输出
|
||||||
|
|
||||||
|
| 构建 | 输出文件 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 标准构建 | `dist/index.css` | 包含完整 Tailwind 工具类 |
|
||||||
|
| Tailwind 构建 | `dist/index.tailwind.css` | 不含 Tailwind,供宿主项目自行引入 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 当前主题体系的问题
|
||||||
|
|
||||||
|
| 问题 | 详情 |
|
||||||
|
|---|---|
|
||||||
|
| 暗色覆盖不完整 | HighlightNode、FootnoteNode、ImageNode 等无暗色适配 |
|
||||||
|
| 变量命名不统一 | 全局用 HSL 裸值,组件用 HEX/RGB/rgba,缺乏统一规范 |
|
||||||
|
| 暗色触发方式不一致 | 全局用 `.dark`,组件用 `.is-dark`,Mermaid 用条件 class |
|
||||||
|
| 硬编码颜色散落 | `#e2e8f0`、`#eaecef`、`#cbd5e1` 等直接写在组件中 |
|
||||||
|
| 语义色彩未统一 | Admonition 语义色与全局体系分离 |
|
||||||
|
| CSS 变量缺乏 Dark 值 | `--blockquote-border-color`、`--table-border` 等无暗色默认值 |
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
# Token 扩展方案(v2)
|
||||||
|
|
||||||
|
> 状态:**经外部 review 修正后的版本**。
|
||||||
|
> 原则:保真优先,不硬归一。"基础刻度 + 语义 token + 组件私有常量"三层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前 token 系统只覆盖了**颜色**和**圆角**。非颜色维度全部硬编码,且存在两类问题:
|
||||||
|
|
||||||
|
1. **CSS 硬编码** — 组件 `<style>` 中的固定值(间距、阴影、动画时长),无法通过 CSS 变量覆盖
|
||||||
|
2. **JS 硬编码** — props 默认值和 JS 中的 `style.transition = 'height 180ms'` 等,CSS 变量完全触达不到
|
||||||
|
|
||||||
|
> **关键约束**:JS 侧的硬编码(`360px`、`500px`、`180ms` 等)必须同步改为读取 CSS 变量
|
||||||
|
> 或 computed 值,否则密度主题只是 CSS 层面的假象,JS 控制的高度/动画不会跟随。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、基础刻度(Scale)
|
||||||
|
|
||||||
|
只收项目里真实反复出现的值,包括半步值。
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
--ms-space-1: 0.25rem; /* 4px */
|
||||||
|
--ms-space-1_5: 0.375rem; /* 6px — ListItemNode pl */
|
||||||
|
--ms-space-2: 0.5rem; /* 8px */
|
||||||
|
--ms-space-2_5: 0.625rem; /* 10px — header py-2.5 */
|
||||||
|
--ms-space-3: 0.75rem; /* 12px */
|
||||||
|
--ms-space-4: 1rem; /* 16px */
|
||||||
|
--ms-space-5: 1.25rem; /* 20px — ListNode my-5 */
|
||||||
|
--ms-space-6: 1.5rem; /* 24px */
|
||||||
|
--ms-space-8: 2rem; /* 32px */
|
||||||
|
--ms-space-12: 3rem; /* 48px */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、语义 Token — 文档流间距
|
||||||
|
|
||||||
|
保留原始值和单位,不做近似归并。
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
|
||||||
|
/* ── 块元素流 ── */
|
||||||
|
--ms-flow-paragraph-y: 1.25em; /* ParagraphNode margin */
|
||||||
|
--ms-flow-list-y: var(--ms-space-5); /* ListNode my-5 = 1.25rem */
|
||||||
|
--ms-flow-list-item-y: var(--ms-space-2); /* ListItemNode my-2 = 0.5rem */
|
||||||
|
--ms-flow-list-indent: calc(13 / 8 * 1em); /* ListNode pl */
|
||||||
|
--ms-flow-list-indent-mobile: calc(14 / 9 * 1em); /* ListNode max-lg:pl */
|
||||||
|
--ms-flow-table-y: var(--ms-space-8); /* TableNode my-8 = 2rem */
|
||||||
|
--ms-flow-table-cell: calc(4 / 7 * 1em); /* TableNode td/th padding */
|
||||||
|
--ms-flow-blockquote-y: 1.6em; /* BlockquoteNode margin-top/bottom */
|
||||||
|
--ms-flow-blockquote-indent: 1em; /* BlockquoteNode padding-left */
|
||||||
|
--ms-flow-admonition-y: var(--ms-space-4); /* AdmonitionNode margin = 1rem */
|
||||||
|
--ms-flow-footnote-y: var(--ms-space-2); /* FootnoteNode mt-2 mb-2 */
|
||||||
|
--ms-flow-hr-y: var(--ms-space-12); /* ThematicBreakNode my-12 = 3rem */
|
||||||
|
--ms-flow-diagram-y: var(--ms-space-4); /* Mermaid/D2/Infographic my-4 = 1rem */
|
||||||
|
--ms-flow-codeblock-y: var(--ms-space-4); /* CodeBlockNode my-4 = 1rem */
|
||||||
|
--ms-flow-definition-term-mt: var(--ms-space-2); /* DefinitionListNode term margin-top */
|
||||||
|
--ms-flow-definition-desc-ml: var(--ms-space-4); /* DefinitionListNode desc margin-left */
|
||||||
|
--ms-flow-definition-desc-mb: var(--ms-space-2); /* DefinitionListNode desc margin-bottom */
|
||||||
|
|
||||||
|
/* ── 标题节奏(每级独立,保留原始 calc)── */
|
||||||
|
--ms-flow-heading-1-mt: 0;
|
||||||
|
--ms-flow-heading-1-mb: calc(8 / 9 * 1em);
|
||||||
|
--ms-flow-heading-2-mt: var(--ms-space-8);
|
||||||
|
--ms-flow-heading-2-mb: var(--ms-space-4);
|
||||||
|
--ms-flow-heading-3-mt: calc(8 / 5 * 1em);
|
||||||
|
--ms-flow-heading-3-mb: calc(3 / 5 * 1em);
|
||||||
|
--ms-flow-heading-4-mt: var(--ms-space-6);
|
||||||
|
--ms-flow-heading-4-mb: var(--ms-space-2);
|
||||||
|
--ms-flow-heading-5-mt: 0;
|
||||||
|
--ms-flow-heading-5-mb: 0;
|
||||||
|
--ms-flow-heading-6-mt: 0;
|
||||||
|
--ms-flow-heading-6-mb: 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、语义 Token — 容器内边距与布局间隙
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
|
||||||
|
/* ── 面板内边距 ── */
|
||||||
|
--ms-inset-panel-x: var(--ms-space-4); /* code/mermaid/d2/infographic header px-4 */
|
||||||
|
--ms-inset-panel-y: var(--ms-space-2_5); /* header py-2.5 = 0.625rem */
|
||||||
|
--ms-inset-panel-body-sm: var(--ms-space-2); /* admonition header padding = 0.5rem */
|
||||||
|
--ms-inset-panel-body: var(--ms-space-4); /* admonition content padding-x = 1rem */
|
||||||
|
--ms-inset-admonition-body-top: var(--ms-space-2); /* admonition content padding-top = 0.5rem */
|
||||||
|
--ms-inset-admonition-body-bottom: var(--ms-space-4); /* admonition content padding-bottom = 1rem */
|
||||||
|
|
||||||
|
/* ── 布局间隙 ── */
|
||||||
|
--ms-gap-header: var(--ms-space-4); /* code header gap = 16px */
|
||||||
|
--ms-gap-header-main: var(--ms-space-2_5); /* header main section gap = 10px */
|
||||||
|
--ms-gap-header-actions: var(--ms-space-2); /* header actions gap = 8px */
|
||||||
|
--ms-gap-toggle: var(--ms-space-1); /* mode toggle内gap = 4px */
|
||||||
|
--ms-gap-skeleton: var(--ms-space-3); /* skeleton line gap */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、阴影 Token
|
||||||
|
|
||||||
|
按语义角色命名,不按 Tailwind 尺寸名。避免值混淆。
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
/* subtle: 卡片/容器的轻微立体感 */
|
||||||
|
--ms-shadow-subtle: 0 1px 2px 0 hsl(var(--ms-foreground) / 0.05);
|
||||||
|
|
||||||
|
/* popover: tooltip / 下拉 / 浮层 */
|
||||||
|
--ms-shadow-popover: 0 4px 6px -1px hsl(var(--ms-foreground) / 0.1),
|
||||||
|
0 2px 4px -2px hsl(var(--ms-foreground) / 0.1);
|
||||||
|
|
||||||
|
/* modal: 全屏弹窗 / dialog */
|
||||||
|
--ms-shadow-modal: 0 10px 15px -3px hsl(var(--ms-foreground) / 0.1),
|
||||||
|
0 4px 6px -4px hsl(var(--ms-foreground) / 0.1);
|
||||||
|
|
||||||
|
/* preview: HtmlPreviewFrame 专用,独立不混 */
|
||||||
|
--ms-shadow-preview: 0 10px 40px hsl(var(--ms-foreground) / 0.25);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 映射
|
||||||
|
|
||||||
|
| 当前值 | 出现位置 | 映射到 |
|
||||||
|
|---|---|---|
|
||||||
|
| Tailwind `shadow-sm` | CodeBlock, D2, Infographic, MarkdownCodeBlock | `--ms-shadow-subtle` |
|
||||||
|
| Tailwind `shadow-md` | Tooltip | `--ms-shadow-popover` |
|
||||||
|
| Tailwind `shadow-lg` | Mermaid modal, Infographic modal | `--ms-shadow-modal` |
|
||||||
|
| `0 1px 2px 0 hsl(...)` | Mermaid/Infographic action bar | `--ms-shadow-subtle` |
|
||||||
|
| `0 10px 40px hsl(...)` | HtmlPreviewFrame | `--ms-shadow-preview` |
|
||||||
|
| Diff 专用阴影 | CodeBlockNode `.is-dark` 块 | 保留组件内(Monaco 区域) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、动画 Token
|
||||||
|
|
||||||
|
保留 5 档时长 + `linear` 纳入缓动表。
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
/* ── 时长:5 档,不混并 ── */
|
||||||
|
--ms-duration-fast: 120ms; /* tooltip fade */
|
||||||
|
--ms-duration-standard: 180ms; /* 高度过渡、表格淡入、代码块折叠 */
|
||||||
|
--ms-duration-overlay: 200ms; /* dialog enter/leave、MathBlock 渲染 */
|
||||||
|
--ms-duration-emphasis: 220ms; /* ImageNode 切换、Tooltip 位移 */
|
||||||
|
--ms-duration-slow: 300ms; /* MathBlock 切换 */
|
||||||
|
--ms-duration-stream: 900ms; /* 流式打字机淡入 */
|
||||||
|
|
||||||
|
/* ── 缓动 ── */
|
||||||
|
--ms-ease-linear: linear;
|
||||||
|
--ms-ease-standard: ease;
|
||||||
|
--ms-ease-out: ease-out;
|
||||||
|
--ms-ease-in-out: ease-in-out; /* LinkNode 脉冲 */
|
||||||
|
--ms-ease-spring: cubic-bezier(.16, 1, .3, 1); /* Tooltip 位移 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 映射
|
||||||
|
|
||||||
|
| 当前值 | 出现位置 | 映射到 |
|
||||||
|
|---|---|---|
|
||||||
|
| `120ms` | Tooltip enter/leave | `--ms-duration-fast` |
|
||||||
|
| `150ms` | Mermaid 容器高度 | `--ms-duration-fast`(最近档,或保留组件私有) |
|
||||||
|
| `180ms` | CodeBlock 高度、TableNode 淡入 | `--ms-duration-standard` |
|
||||||
|
| `200ms` | Mermaid/Infographic dialog、MathBlock 渲染 | `--ms-duration-overlay` |
|
||||||
|
| `220ms` | ImageNode 切换、Tooltip 位移 | `--ms-duration-emphasis` |
|
||||||
|
| `300ms` / `0.3s` | MathBlock 切换 | `--ms-duration-slow` |
|
||||||
|
| `900ms` | InlineCodeNode 流式淡入 | `--ms-duration-stream` |
|
||||||
|
| `1.2s` | Shimmer 动画 | 不 token 化(shimmer 专用) |
|
||||||
|
| `1.6s` | LinkNode 脉冲 | 不 token 化(脉冲专用) |
|
||||||
|
| `ease` | 多处 | `--ms-ease-standard` |
|
||||||
|
| `ease-out` | Mermaid 容器、打字机 | `--ms-ease-out` |
|
||||||
|
| `linear` | Spinner 旋转、Tooltip fade | `--ms-ease-linear` |
|
||||||
|
| `ease-in-out` | LinkNode 脉冲 | `--ms-ease-in-out` |
|
||||||
|
| `cubic-bezier(.16,1,.3,1)` | Tooltip 位移 | `--ms-ease-spring` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、边框与焦点 Token
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
--ms-border-width: 1px; /* 标准边框 */
|
||||||
|
--ms-border-width-strong: 4px; /* 强调左边框(admonition、blockquote、vmr-container) */
|
||||||
|
--ms-focus-ring-width: 2px; /* focus outline 宽度 */
|
||||||
|
--ms-focus-ring-offset: 2px; /* focus outline offset */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 映射
|
||||||
|
|
||||||
|
| 当前值 | 出现位置 | 映射到 |
|
||||||
|
|---|---|---|
|
||||||
|
| `1px solid` | InlineCodeNode, 多处 `border` | `--ms-border-width` |
|
||||||
|
| `border-left: 4px solid` | AdmonitionNode | `--ms-border-width-strong` |
|
||||||
|
| `border-left: 0.25rem solid` | BlockquoteNode (= 4px) | `--ms-border-width-strong` |
|
||||||
|
| `border-left-width: 4px` | VmrContainerNode | `--ms-border-width-strong` |
|
||||||
|
| `outline: 2px solid` | AdmonitionNode toggle focus | `--ms-focus-ring-width` |
|
||||||
|
| `outline-offset: 2px` | AdmonitionNode toggle focus | `--ms-focus-ring-offset` |
|
||||||
|
| `border: 2px solid` | Spinner (MathBlock, Table, Image) | 不 token 化(spinner 专用) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、容器尺寸 Token
|
||||||
|
|
||||||
|
```css
|
||||||
|
.markstream-vue {
|
||||||
|
--ms-size-diagram-min-height: 360px; /* Mermaid / Infographic 最小高度 */
|
||||||
|
--ms-size-code-max-height: 500px; /* 代码块折叠最大高度 */
|
||||||
|
--ms-size-image-max-width: 24rem; /* 图片最大宽度 */
|
||||||
|
--ms-size-math-min-height: 40px; /* MathBlock 最小高度 */
|
||||||
|
--ms-size-skeleton-min-height: 120px; /* 骨架屏占位最小高度 */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JS 硬编码问题
|
||||||
|
|
||||||
|
以下值**同时存在于 CSS 和 JS 中**,只改 CSS token 不够,JS 也必须同步:
|
||||||
|
|
||||||
|
| 值 | CSS 位置 | JS 位置 | 解决方案 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `360px` | `min-h-[360px]` | MermaidBlockNode props default、`contain-intrinsic-size` | JS 读取 `getComputedStyle` 或改为 CSS-only 控制 |
|
||||||
|
| `500px` | `max-height` | MarkdownCodeBlock/CodeBlock 折叠逻辑 | 同上 |
|
||||||
|
| `180ms` | `transition: height 180ms` | MermaidBlockNode `el.style.transition = 'height 180ms'` | JS 读取 `var(--ms-duration-standard)` |
|
||||||
|
| `120px` | skeleton 高度 | CodeBlockNode loading 占位 | 改用 CSS var |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、不 token 化的项
|
||||||
|
|
||||||
|
| 项 | 原因 |
|
||||||
|
|---|---|
|
||||||
|
| 标题 calc 表达式(`calc(8/9*1em)` 等) | 已作为 `--ms-flow-heading-*` token 的值保留原始表达式 |
|
||||||
|
| z-index (`z-50`, `z-[9999]`) | 内部层叠实现,对外无意义 |
|
||||||
|
| icon 尺寸 (`w-4 h-4`, `w-3 h-3`) | 过于细碎,改了破坏对齐 |
|
||||||
|
| Spinner border-width (`2px`) | 极少定制需求 |
|
||||||
|
| Shimmer 时长 (`1.2s`) / 脉冲时长 (`1.6s`) | 单一用途,不值得抽象 |
|
||||||
|
| viewport 相对值 (`70vh`, `80vw`) | 运行时响应式,不适合固定 token |
|
||||||
|
| Tooltip offset (`8px`) / shift padding (`8px`) | Floating-UI 配置,非视觉 token |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、密度主题示例
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 舒适模式(默认) */
|
||||||
|
.markstream-vue {
|
||||||
|
/* 使用上述所有默认值 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 紧凑模式 */
|
||||||
|
.markstream-vue.compact {
|
||||||
|
--ms-flow-paragraph-y: 1em;
|
||||||
|
--ms-flow-list-y: var(--ms-space-4);
|
||||||
|
--ms-flow-list-item-y: var(--ms-space-1_5);
|
||||||
|
--ms-flow-table-y: var(--ms-space-6);
|
||||||
|
--ms-flow-admonition-y: 0.75rem;
|
||||||
|
|
||||||
|
--ms-flow-heading-2-mt: var(--ms-space-6);
|
||||||
|
--ms-flow-heading-2-mb: 0.75rem;
|
||||||
|
--ms-flow-heading-4-mt: var(--ms-space-4);
|
||||||
|
--ms-flow-heading-4-mb: 0.375rem;
|
||||||
|
|
||||||
|
--ms-inset-panel-x: 0.75rem;
|
||||||
|
--ms-inset-panel-y: 0.5rem;
|
||||||
|
--ms-inset-panel-body-sm: 0.5rem;
|
||||||
|
--ms-inset-panel-body: 0.75rem;
|
||||||
|
|
||||||
|
--ms-gap-header-main: 0.5rem;
|
||||||
|
--ms-gap-header-actions: 0.375rem;
|
||||||
|
|
||||||
|
--ms-size-diagram-min-height: 280px; /* 不要太激进,280 比 240 稳 */
|
||||||
|
--ms-size-code-max-height: 420px;
|
||||||
|
|
||||||
|
--ms-duration-standard: 160ms;
|
||||||
|
--ms-duration-overlay: 180ms;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、实施注意事项
|
||||||
|
|
||||||
|
### "等值映射" vs "设计归一"
|
||||||
|
|
||||||
|
本文档的所有映射都是**严格等值**——token 的默认值就是当前代码里的实际值。
|
||||||
|
如果后续视觉重设计需要调整值(如 `1.6em` → `1.5rem`),应在单独的 PR 中进行,
|
||||||
|
并明确标注为"设计归一",不要混在 token 提取中。
|
||||||
|
|
||||||
|
### JS 硬编码必须同步
|
||||||
|
|
||||||
|
仅在 CSS 中定义 token 是不够的。所有在 JS 中硬编码的尺寸/时长值必须改为
|
||||||
|
通过 `getComputedStyle` 读取 CSS 变量值,或通过 props/computed 传递 token 值。
|
||||||
|
否则 compact 主题只影响 CSS 层面,JS 控制的高度/动画不会跟随。
|
||||||
|
|
||||||
|
### 保真优先
|
||||||
|
|
||||||
|
- `1.25rem`(ListNode my-5)和 `1.25em`(ParagraphNode margin)是**不同单位**,不要混成同一个 token
|
||||||
|
- `padding-left: 1em`(BlockquoteNode)不等于 `--ms-space-4`(1rem),单位不同
|
||||||
|
- `py-2.5 = 0.625rem` 不等于 `0.5rem`
|
||||||
|
- Admonition content 的 `padding: 0.5rem 1rem 1rem` 是三值(上 ≠ 下),不能和 header 共用
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
# Design Tokens
|
||||||
|
|
||||||
|
> markstream-vue 当前所有设计令牌(Design Tokens)清单。
|
||||||
|
> 来源:`src/index.css`、`tailwind.config.js`、各组件 `<style>` 块。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 全局 CSS 变量(Global CSS Variables)
|
||||||
|
|
||||||
|
定义在 `src/index.css`,挂载于 `.markstream-vue` 根选择器。
|
||||||
|
|
||||||
|
### 1.1 基础色彩(HSL 格式)
|
||||||
|
|
||||||
|
| 变量名 | Light 值 | Dark 值 | 用途 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--border` | `214.3 31.8% 91.4%` | `217.2 32.6% 17.5%` | 边框色 |
|
||||||
|
| `--background` | `0 0% 100%` | `222.2 84% 4.9%` | 背景色 |
|
||||||
|
| `--foreground` | `222.2 84% 4.9%` | `210 40% 98%` | 前景/文字色 |
|
||||||
|
| `--secondary` | `210 40% 96%` | `217.2 32.6% 17.5%` | 次要背景(如行内代码底色) |
|
||||||
|
| `--muted` | `210 40% 96%` | `217.2 32.6% 17.5%` | 弱化背景 |
|
||||||
|
| `--muted-foreground` | `215.4 16.3% 46.9%` | `215 20.2% 65.1%` | 弱化文字 |
|
||||||
|
|
||||||
|
### 1.2 Tailwind 扩展色彩映射
|
||||||
|
|
||||||
|
定义在 `tailwind.config.js`,带回退值:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const colors = {
|
||||||
|
'border': 'hsl(var(--border), 214.3 21.44% 93.4%)',
|
||||||
|
'background': 'hsl(var(--background), 0 0% 100%)',
|
||||||
|
'foreground': 'hsl(var(--foreground), 217.2 22.08% 19.5%)',
|
||||||
|
'secondary': 'hsl(var(--secondary), 214.3 21.44% 93.4%)',
|
||||||
|
'muted': 'hsl(var(--muted), 210 28% 98.1%)',
|
||||||
|
'muted-foreground': 'hsl(var(--muted-foreground), 215 12.16% 67.1%)',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 组件级 CSS 变量
|
||||||
|
|
||||||
|
### 2.1 代码块(CodeBlockNode)
|
||||||
|
|
||||||
|
| 变量名 | Light | Dark | 用途 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--markstream-code-fallback-bg` | `#ffffff` | `#111827` | 代码区背景 |
|
||||||
|
| `--markstream-code-fallback-fg` | `#111827` | `#e5e7eb` | 代码区文字 |
|
||||||
|
| `--markstream-code-border-color` | `rgb(229 231 235)` | `rgb(55 65 81 / 0.3)` | 代码块边框 |
|
||||||
|
| `--markstream-code-fallback-selection-bg` | `rgba(0,0,0,0.06)` | `rgba(255,255,255,0.08)` | 选区背景 |
|
||||||
|
| `--markstream-code-padding-y` | `8px` | - | 代码内上下边距 |
|
||||||
|
| `--markstream-code-padding-x` | `12px` | - | 代码内左右边距 |
|
||||||
|
| `--markstream-code-padding-left` | `52px` | - | 代码内左边距(含行号) |
|
||||||
|
| `--vscode-editor-font-size` | `12px` | - | 编辑器字号 |
|
||||||
|
| `--vscode-editor-background` | 来自 Monaco 主题 | - | Monaco 背景色 |
|
||||||
|
| `--vscode-editor-foreground` | 来自 Monaco 主题 | - | Monaco 前景色 |
|
||||||
|
|
||||||
|
### 2.2 Diff 视图变量
|
||||||
|
|
||||||
|
| 变量名 | Light | Dark |
|
||||||
|
|---|---|---|
|
||||||
|
| `--markstream-diff-frame-border` | `rgb(203 213 225 / 0.56)` | 深色变体 |
|
||||||
|
| `--markstream-diff-frame-shadow` | `0 16px 40px -32px rgb(15 23 42 / 0.18)` | 深色变体 |
|
||||||
|
| `--markstream-diff-shell-fg` | `#0f172a` | 深色变体 |
|
||||||
|
| `--markstream-diff-shell-muted` | `#64748b` | 深色变体 |
|
||||||
|
| `--markstream-diff-shell-border` | `rgb(148 163 184 / 0.18)` | 深色变体 |
|
||||||
|
| `--markstream-diff-editor-bg` | `#ffffff` | 深色变体 |
|
||||||
|
| `--markstream-diff-editor-fg` | `#435266` | 深色变体 |
|
||||||
|
| `--markstream-diff-added-fg` | `#14b8a6` | `#5eead4` |
|
||||||
|
| `--markstream-diff-removed-fg` | `#ff3658` | `#fda4af` |
|
||||||
|
| `--markstream-diff-added-line` | `rgb(232 249 245 / 0.98)` | 深色变体 |
|
||||||
|
| `--markstream-diff-removed-line` | `rgb(255 241 241 / 0.98)` | 深色变体 |
|
||||||
|
| `--markstream-diff-added-inline` | `rgb(197 245 219 / 0.96)` | 深色变体 |
|
||||||
|
| `--markstream-diff-removed-inline` | `rgb(255 215 217 / 0.92)` | 深色变体 |
|
||||||
|
|
||||||
|
### 2.3 提示框(AdmonitionNode)
|
||||||
|
|
||||||
|
| 变量名 | Light | Dark | 用途 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--admonition-bg` | `#f8f8f8` | `#0b1220` | 提示框背景 |
|
||||||
|
| `--admonition-border` | `#eaecef` | `rgba(255,255,255,0.06)` | 提示框边框 |
|
||||||
|
| `--admonition-header-bg` | `rgba(0,0,0,0.03)` | `rgba(255,255,255,0.03)` | 标题栏背景 |
|
||||||
|
| `--admonition-text` | `#111827` | `#e6eef8` | 正文色 |
|
||||||
|
| `--admonition-muted` | `#374151` | `#cbd5e1` | 标题文字色 |
|
||||||
|
| `--admonition-note-color` | `#448aff` | 同 | 蓝色-提示 |
|
||||||
|
| `--admonition-tip-color` | `#00bfa5` | 同 | 绿色-技巧 |
|
||||||
|
| `--admonition-warning-color` | `#ff9100` | 同 | 橙色-警告 |
|
||||||
|
| `--admonition-danger-color` | `#ff5252` | 同 | 红色-危险 |
|
||||||
|
|
||||||
|
### 2.4 其他组件变量
|
||||||
|
|
||||||
|
| 变量名 | 默认值 | 组件 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--blockquote-border-color` | `#e2e8f0` | BlockquoteNode |
|
||||||
|
| `--hr-border-color` | `#e2e8f0` | ThematicBreakNode |
|
||||||
|
| `--table-border` | `#cbd5e1` | TableNode |
|
||||||
|
| `--list-item-counter-marker` | `#64748b` | ListItemNode(有序列表数字) |
|
||||||
|
| `--list-item-marker` | `#cbd5e1` | ListItemNode(无序列表圆点) |
|
||||||
|
| `--link-color` | `#0366d6` | LinkNode |
|
||||||
|
| `--underline-height` | `2px` | LinkNode |
|
||||||
|
| `--underline-bottom` | `-3px` | LinkNode |
|
||||||
|
| `--underline-opacity` | `0.35` | LinkNode(动画最大透明度) |
|
||||||
|
| `--underline-rest-opacity` | `0.18` | LinkNode(静止态透明度) |
|
||||||
|
| `--underline-duration` | `1.6s` | LinkNode |
|
||||||
|
| `--underline-timing` | `ease-in-out` | LinkNode |
|
||||||
|
|
||||||
|
### 2.5 动画相关变量
|
||||||
|
|
||||||
|
| 变量名 | 默认值 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `--typewriter-fade-duration` | `900ms` | 打字机淡入时长 |
|
||||||
|
| `--typewriter-fade-ease` | `ease-out` | 打字机缓动函数 |
|
||||||
|
| `--stream-update-fade-duration` | 同上 | 流式更新淡入时长 |
|
||||||
|
| `--stream-update-fade-ease` | 同上 | 流式更新缓动函数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 色彩体系(Color Palette)
|
||||||
|
|
||||||
|
### 3.1 语义色彩
|
||||||
|
|
||||||
|
| 语义 | 色值 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| Note / Info | `#448aff` | 信息提示 |
|
||||||
|
| Tip | `#00bfa5` | 技巧提示 |
|
||||||
|
| Warning | `#ff9100` | 警告提示 |
|
||||||
|
| Danger / Error | `#ff5252` | 危险/错误提示 |
|
||||||
|
| Link | `#0366d6` | 链接色 |
|
||||||
|
| Highlight | `#ffff00` | 文本高亮 |
|
||||||
|
| Diff Added | `#14b8a6` (light) / `#5eead4` (dark) | 新增行 |
|
||||||
|
| Diff Removed | `#ff3658` (light) / `#fda4af` (dark) | 删除行 |
|
||||||
|
|
||||||
|
### 3.2 灰阶色彩(散落于各组件)
|
||||||
|
|
||||||
|
| 色值 | 出现位置 |
|
||||||
|
|---|---|
|
||||||
|
| `#111827` (gray-900) | 代码区前景、提示框文字 |
|
||||||
|
| `#e5e7eb` (gray-200) | 代码区暗色前景 |
|
||||||
|
| `#e2e8f0` (slate-200) | 引用线、分割线 |
|
||||||
|
| `#cbd5e1` (slate-300) | 表格边框、列表标记 |
|
||||||
|
| `#64748b` (slate-500) | 有序列表标记 |
|
||||||
|
| `#374151` (gray-700) | 提示框标题文字 |
|
||||||
|
| `#eaecef` | 提示框边框、脚注边框 |
|
||||||
|
| `#0b1220` | 暗色提示框背景 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 排版(Typography)
|
||||||
|
|
||||||
|
### 4.1 标题层级
|
||||||
|
|
||||||
|
| 等级 | 字号 | 字重 | 上边距 | 下边距 | 行高 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| h1 | `text-4xl` (2.25rem) | `font-extrabold` (800) | `0` | `calc(8/9*1em)` | `calc(10/9*1)` |
|
||||||
|
| h2 | `text-2xl` (1.5rem) | `font-bold` (700) | `2rem` | `1rem` | `calc(4/3*1)` |
|
||||||
|
| h3 | `text-xl` (1.25rem) | `font-semibold` (600) | `calc(8/5*1em)` | `calc(3/5*1em)` | `calc(5/3*1)` |
|
||||||
|
| h4 | `text-base` (1rem) | `font-semibold` (600) | `1.5rem` | `0.5rem` | 默认 |
|
||||||
|
| h5 | `text-base` (1rem) | 默认 | `0` | `0` | 默认 |
|
||||||
|
| h6 | `text-base` (1rem) | 默认 | `0` | `0` | 默认 |
|
||||||
|
|
||||||
|
共有基类:`font-medium leading-tight`
|
||||||
|
|
||||||
|
### 4.2 正文
|
||||||
|
|
||||||
|
| 元素 | 字号 | 字重 | 行距 | 边距 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 段落 `<p>` | 继承 | 继承 | 继承 | `1.25em 0` |
|
||||||
|
| 行内代码 | `85%` | `font-mono` | - | `px-1 py-0.5` |
|
||||||
|
| 引用块 | 继承 | `500` | - | `mt-1.6em mb-1.6em pl-1em` |
|
||||||
|
| 定义列表 term | 继承 | `600` | - | `mt-0.5rem` |
|
||||||
|
| 定义列表 desc | 继承 | 继承 | - | `ml-1rem mb-0.5rem` |
|
||||||
|
| 脚注 | `text-sm` | - | `leading-relaxed` | `mt-2 mb-2` |
|
||||||
|
|
||||||
|
### 4.3 代码块
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| 字号 | `var(--vscode-editor-font-size, 12px)` |
|
||||||
|
| 字体 | monospace(系统等宽) |
|
||||||
|
| 标题字号 | `14px` |
|
||||||
|
| 标题字重 | `650` |
|
||||||
|
| 标题字间距 | `0.01em` |
|
||||||
|
| 说明文字字号 | `12px` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 间距(Spacing)
|
||||||
|
|
||||||
|
### 5.1 块级元素垂直间距
|
||||||
|
|
||||||
|
| 元素 | margin-top | margin-bottom |
|
||||||
|
|---|---|---|
|
||||||
|
| 段落 | `1.25em` | `1.25em` |
|
||||||
|
| 标题 h1 | `0` | `calc(8/9*1em)` |
|
||||||
|
| 标题 h2 | `2rem` | `1rem` |
|
||||||
|
| 标题 h3 | `calc(8/5*1em)` | `calc(3/5*1em)` |
|
||||||
|
| 标题 h4 | `1.5rem` | `0.5rem` |
|
||||||
|
| 引用块 | `1.6em` | `1.6em` |
|
||||||
|
| 列表 | `1.25rem` | `1.25rem` |
|
||||||
|
| 列表项 | `0.5rem` | `0.5rem` |
|
||||||
|
| 提示框 | `1rem` | `1rem` |
|
||||||
|
| 表格 | `2rem` | `2rem` |
|
||||||
|
| 分割线 | `3rem` | `3rem` |
|
||||||
|
| Mermaid 图 | `1rem` | `1rem` |
|
||||||
|
| 定义列表 | `0` | `1rem` |
|
||||||
|
|
||||||
|
### 5.2 内边距(Padding)
|
||||||
|
|
||||||
|
| 元素 | padding |
|
||||||
|
|---|---|
|
||||||
|
| 行内代码 | `px-1 py-0.5` (4px 2px) |
|
||||||
|
| 提示框标题 | `0.5rem 1rem` |
|
||||||
|
| 提示框内容 | `0.5rem 1rem 1rem` |
|
||||||
|
| 引用块左侧 | `1em` |
|
||||||
|
| 列表缩进 | `calc(13/8*1em)` |
|
||||||
|
| 列表缩进(移动端) | `calc(14/9*1em)` |
|
||||||
|
| 列表项 | `pl-1.5` (6px) |
|
||||||
|
| 表格单元格 | `calc(4/7*1em)` |
|
||||||
|
| 代码块内容 | `8px 12px` (左侧 `52px` 含行号) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 边框与圆角(Borders & Radius)
|
||||||
|
|
||||||
|
| 元素 | 边框 | 圆角 |
|
||||||
|
|---|---|---|
|
||||||
|
| 行内代码 | `1px solid hsl(var(--border) / 0.9)` | `rounded` (0.25rem) |
|
||||||
|
| 提示框 | `border-left: 4px solid` | `4px` |
|
||||||
|
| 引用块 | `border-left: 0.25rem solid` | - |
|
||||||
|
| 代码块 | `var(--markstream-code-border-color)` | 由 Monaco 控制 |
|
||||||
|
| 表格 | `var(--table-border, #cbd5e1)` | - |
|
||||||
|
| 分割线 | `border-t` | - |
|
||||||
|
| 脚注 | `border-t border-[#eaecef]` | - |
|
||||||
|
| Mermaid 图 | `border rounded-lg` | `0.5rem` |
|
||||||
|
| Tooltip | `border rounded-md` | `0.375rem` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 阴影(Shadows)
|
||||||
|
|
||||||
|
| 元素 | 阴影值 |
|
||||||
|
|---|---|
|
||||||
|
| Mermaid 块 | `shadow-sm` |
|
||||||
|
| Tooltip | `shadow-md` |
|
||||||
|
| Diff 外框 | `0 16px 40px -32px rgb(15 23 42 / 0.18)` |
|
||||||
|
| Diff shell | `0 30px 70px -48px rgb(15 23 42 / 0.42)` |
|
||||||
|
| Mermaid 模态弹窗 | `shadow-lg` |
|
||||||
|
| Mermaid 激活按钮 | `shadow-sm` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 性能相关令牌
|
||||||
|
|
||||||
|
| 属性 | 值 | 组件 |
|
||||||
|
|---|---|---|
|
||||||
|
| `content-visibility` | `auto` | markdown-renderer, CodeBlockNode, MermaidBlockNode |
|
||||||
|
| `contain` | `content` / `layout` | 多个组件 |
|
||||||
|
| `contain-intrinsic-size` | `320px 180px` | CodeBlockNode |
|
||||||
|
| `contain-intrinsic-size` | `360px 240px` | MermaidBlockNode |
|
||||||
|
| `container-type` | `inline-size` | CodeBlockNode |
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# 排版调研:什么样的文本排版是舒适的
|
||||||
|
|
||||||
|
> 来源:2025-2026 主流产品分析 + 排版设计理论
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心发现
|
||||||
|
|
||||||
|
### 1. 舒适感来自"看不见的设计"
|
||||||
|
|
||||||
|
所有顶级产品(GitHub、Notion、Stripe、Linear)的排版有一个共同点:**你不会注意到它**。
|
||||||
|
用户从来不会说"Notion 的行高好舒服"——他们只会说"用 Notion 写东西很舒服"。
|
||||||
|
排版做对了,它就消失了。做错了,用户会感到莫名的不适但说不出为什么。
|
||||||
|
|
||||||
|
### 2. 字重比字号更重要
|
||||||
|
|
||||||
|
Apple HIG 的做法最能说明这一点:body 和 title 可以是同样的 17pt,
|
||||||
|
区分靠的是 Regular vs Semibold,而不是 16px vs 36px。
|
||||||
|
|
||||||
|
**行业趋势**:
|
||||||
|
- SemiBold (600) 正在取代 Bold (700) 成为标题首选字重——自信但不粗暴
|
||||||
|
- 极细字重(Ultralight/Thin)被 Apple 明确不推荐
|
||||||
|
- shadcn 的 h1 用 `extrabold`(800),h2 用 `semibold`(600)——标题之间也有字重梯度
|
||||||
|
|
||||||
|
**启示**:与其让 h4/h5/h6 都用 `text-base` 然后靠字重区分失败,不如用颜色、opacity、
|
||||||
|
大写字母这些维度来建立层级。Linear 用 dim(降低亮度)来区分次要信息。
|
||||||
|
|
||||||
|
### 3. 行宽是阅读舒适度的第一变量
|
||||||
|
|
||||||
|
| 来源 | 推荐行宽 |
|
||||||
|
|---|---|
|
||||||
|
| 排版理论 | 50-75 字符,66 字符最佳 |
|
||||||
|
| WCAG 无障碍 | 不超过 80 字符 |
|
||||||
|
| Baymard Institute | 超过 80 字符的内容被跳过率 +41% |
|
||||||
|
| CSS 实现 | `max-width: 65ch` 或 `30-40rem` |
|
||||||
|
|
||||||
|
**所有严肃的内容产品都限制行宽**:GitHub 980px,Notion 宽边距,Stripe 居中列。
|
||||||
|
没有一个做全宽正文。
|
||||||
|
|
||||||
|
### 4. 行高与行宽是联动的
|
||||||
|
|
||||||
|
| 场景 | 行高 |
|
||||||
|
|---|---|
|
||||||
|
| 正文(桌面) | 1.5-1.6x |
|
||||||
|
| 窄栏/移动端 | 1.3-1.45x |
|
||||||
|
| 标题 | 1.1-1.2x(紧凑,防止视觉碎片化) |
|
||||||
|
| UI 组件 | 1.2-1.3x |
|
||||||
|
|
||||||
|
**关键关系**:行宽越长,行高应该越大。短行配大行高会"散架",长行配小行高会"织成布"。
|
||||||
|
目标是"均匀的排版色彩"——看起来舒服、邀请阅读。
|
||||||
|
|
||||||
|
### 5. 标题缩放比例决定整体调性
|
||||||
|
|
||||||
|
| 比例 | 值 | 适用场景 | 感觉 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Major Second | 1.125 | 紧凑 UI、仪表盘 | 克制、高密度 |
|
||||||
|
| **Major Third** | **1.25** | **文档、内容应用** | **平衡、专业** |
|
||||||
|
| Perfect Fourth | 1.333 | 营销、多样内容 | 有层次、明快 |
|
||||||
|
| Perfect Fifth | 1.5 | 编辑排版、长文 | 戏剧性、正式 |
|
||||||
|
|
||||||
|
**Major Third (1.25) 是多数内容应用的最佳起点**。
|
||||||
|
|
||||||
|
### 6. 留白是奢侈品
|
||||||
|
|
||||||
|
感觉最"高级"的产品——Notion、Stripe、Linear——恰恰是留白最慷慨的。
|
||||||
|
留白传达品质感。但过多的留白会让内容感觉断裂、需要过多滚动。
|
||||||
|
|
||||||
|
**段落间距共识**:
|
||||||
|
- 段落之间:至少 1em,理想为行高的 1.5 倍
|
||||||
|
- 标题上方的空间应该是下方的 1.5 倍以上(接近原则:标题属于后面的内容)
|
||||||
|
- Wichita State 研究:合理的段落间白可以提升理解力 20%
|
||||||
|
|
||||||
|
### 7. 衬线字体正在回归
|
||||||
|
|
||||||
|
**Claude.ai 是旗舰案例**:AI 回复用衬线体,UI 用无衬线——这是对无衬线单一文化的声明。
|
||||||
|
|
||||||
|
驱动因素:
|
||||||
|
- 高分辨率屏幕让衬线清晰锐利
|
||||||
|
- 几何无衬线的审美疲劳
|
||||||
|
- 追求温暖感和人格化
|
||||||
|
- "过度抛光的 AI 审美正在造成视觉疲劳,设计师转向更有质感的字体"
|
||||||
|
|
||||||
|
**对渲染库的启示**:同时支持衬线和无衬线(通过 `--ms-font-sans` / `--ms-font-mono` token)
|
||||||
|
是前瞻性的。
|
||||||
|
|
||||||
|
### 8. 流式渲染需要系统字体
|
||||||
|
|
||||||
|
ChatGPT 主要用 `system-ui` 不是偷懒——是性能工程。
|
||||||
|
自定义字体意味着流式渲染时的布局跳动(layout shift),系统字体意味着即时稳定。
|
||||||
|
|
||||||
|
**对 markstream-vue 的启示**:
|
||||||
|
- 默认字体栈应该是 `system-ui` 优先(零加载、零跳动)
|
||||||
|
- 通过 `--ms-font-sans` token 允许用户切换到 Inter/Geist/衬线
|
||||||
|
- 字体加载策略本身就是排版决策
|
||||||
|
|
||||||
|
### 9. Dark 模式不是可选的
|
||||||
|
|
||||||
|
每一个调研的产品都支持。Claude、Linear、Cursor 默认暗色。
|
||||||
|
Tailwind 的 `prose-invert`、github-markdown-css 的 `prefers-color-scheme`
|
||||||
|
都说明这是基础能力。
|
||||||
|
|
||||||
|
### 10. 排版正在变成设计令牌系统
|
||||||
|
|
||||||
|
Vercel(Geist tokens)、Stripe(design tokens)、USWDS(font-size tokens)、
|
||||||
|
shadcn(CSS variables)。硬编码值在消亡,一切通过变量/token 流转。
|
||||||
|
这正是我们在做的。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 主流产品排版定位
|
||||||
|
|
||||||
|
| 产品 | 感觉 | 关键特征 |
|
||||||
|
|---|---|---|
|
||||||
|
| **GitHub** | 功能性、隐形、实用 | 不引人注目的排版,让内容说话 |
|
||||||
|
| **Notion** | 干净、通透、冥想感 | 大量留白,内容漂浮在空间中 |
|
||||||
|
| **Linear** | 专业、克制、工程感 | 次要元素降亮度,只让重要内容突出 |
|
||||||
|
| **Stripe** | 精致、权威、可信赖 | 三栏布局,内容与代码对话 |
|
||||||
|
| **Vercel/Geist** | 瑞士现代、精确、开发者原生 | 三层类型系统(Heading/Label/Copy) |
|
||||||
|
| **ChatGPT** | 干净、机构感、高效 | 系统字体优先,零布局跳动 |
|
||||||
|
| **Claude** | 温暖、文学感、有人格 | **衬线体做 AI 回复**——反无衬线单一文化 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 对 markstream-vue 的建议
|
||||||
|
|
||||||
|
### 默认排版方向
|
||||||
|
|
||||||
|
不需要5种 preset,需要**1种好的默认**+ 通过 token 可调。
|
||||||
|
|
||||||
|
**推荐方向:Technical Clean + 一点 Notion 的呼吸感**
|
||||||
|
|
||||||
|
理由:
|
||||||
|
- markstream-vue 的主要场景是 AI 对话 + 开发文档 + 内容渲染
|
||||||
|
- 用户期望"专业但不冷淡"的感觉
|
||||||
|
- 需要同时服务代码密集和纯文字密集的场景
|
||||||
|
|
||||||
|
### 具体参数方向
|
||||||
|
|
||||||
|
| 维度 | 建议 | 依据 |
|
||||||
|
|---|---|---|
|
||||||
|
| 内容宽度 | `max-width: 65ch` | 行业共识 |
|
||||||
|
| 正文字号 | 16px (1rem) | 所有产品的最低线 |
|
||||||
|
| 正文行高 | 1.6 | 无障碍 + 舒适的交叉点 |
|
||||||
|
| 标题缩放 | Major Third (1.25) | 文档/内容应用的平衡点 |
|
||||||
|
| 标题字重 | h1: 700, h2: 600, h3-h4: 600, h5-h6: 500 | Apple + shadcn 的 weight > size 趋势 |
|
||||||
|
| 段落间距 | 1.5em | 行高的约 1x |
|
||||||
|
| 标题上下比 | 上 1.5x : 下 1x | 接近原则 |
|
||||||
|
| 默认字体 | `system-ui` → 可通过 token 切换 | 流式渲染零跳动 |
|
||||||
|
| h5/h6 区分 | opacity/color 降级,不是字号 | Linear/Apple 的做法 |
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# 视觉设计评审
|
||||||
|
|
||||||
|
> 基于 `/example` 页面的 light + dark 截图,逐组件审视当前视觉状态。
|
||||||
|
> 标注"需重设计"的项是 issue #343 视觉重设计的具体工作。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 需要重设计的元素
|
||||||
|
|
||||||
|
### 1. 标题层级 — 需重设计
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- h4/h5/h6 视觉上几乎无法区分,都是同样大小的 `text-base`
|
||||||
|
- h1 和 h2 之间跳跃太大(4xl → 2xl)
|
||||||
|
- h5/h6 没有任何视觉区分度(无字重差异、无颜色差异、无间距差异)
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- h4 可用 muted-foreground 或小号全大写字母来区分
|
||||||
|
- h5/h6 可用更小字号或不同字重
|
||||||
|
- 整体建立更平滑的字号梯度
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 表格 — 需重设计
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- 表头没有背景色,和正文行无法区分
|
||||||
|
- 无斑马纹,多行时视觉追踪困难
|
||||||
|
- 边框太淡(尤其 dark 模式下几乎不可见)
|
||||||
|
- 无 hover 行高亮
|
||||||
|
- 整体非常朴素,缺乏结构感
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- 表头加 `--table-header-bg` 背景
|
||||||
|
- 偶数行加微弱背景色(斑马纹)
|
||||||
|
- 增强 dark 模式下边框可见度
|
||||||
|
- 增加 hover 行高亮
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 引用块(Blockquote) — 需重设计
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- 左边框颜色太淡,dark 模式下几乎不可见
|
||||||
|
- 整体过于朴素,没有背景色区分
|
||||||
|
- 与正文的视觉层级差异不够
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- 增加微弱的背景色(如 `--ms-muted` 低透明度)
|
||||||
|
- 左边框加粗或加深颜色
|
||||||
|
- 可考虑使用 accent 色作为左边框
|
||||||
|
- Dark 模式下需要明显更亮的边框
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 代码块 — 需重设计
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- Header 背景和页面背景融为一体,没有区分
|
||||||
|
- 代码区域在 light 模式下是白色底,和页面白色底无边界感
|
||||||
|
- 整个容器的阴影太轻(`shadow-sm`),不够突出
|
||||||
|
- Dark 模式下代码块容器和页面背景的对比度不足
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- Header 需要明确的 `--code-header-bg` 背景(当前 token 已定义但视觉太淡)
|
||||||
|
- 考虑加深容器边框或增加阴影
|
||||||
|
- Light 模式代码区域可用微灰背景(而非纯白)
|
||||||
|
- 代码块作为"嵌入内容"应该在视觉上与正文有明确的层级分离
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 分割线(HR) — 需微调
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- 太细太淡,dark 模式下几乎不可见
|
||||||
|
- 上下间距很大(`my-12`)但线本身视觉权重不够
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- 增加线的粗细或颜色深度
|
||||||
|
- 或改为渐变消隐风格(从中心向两侧淡出)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 列表标记 — 需微调
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- 无序列表圆点颜色太淡(`--list-marker` = border 色)
|
||||||
|
- 有序列表数字颜色也偏淡
|
||||||
|
- Task list 的 checkbox 太小,无自定义样式
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- 标记色加深一档(用 `--ms-muted-foreground` 而非 `--ms-border`)
|
||||||
|
- Checkbox 可自定义样式(圆角、颜色、大小)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 定义列表 — 需重设计
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- 纯文本渲染,术语和描述之间缺乏视觉层级
|
||||||
|
- 没有背景、边框或缩进的视觉区分
|
||||||
|
- 和普通段落看起来没区别
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- 术语可用 accent 色或加背景标签
|
||||||
|
- 描述增加左缩进 + 左边框(类似引用块)
|
||||||
|
- 或使用交替背景色
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 脚注 — 需微调
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- 脚注区域和正文的分隔线太淡
|
||||||
|
- 脚注内容在页面底部容易被忽略
|
||||||
|
- 缺乏"脚注区域"的整体框架感
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- 分隔线加深或改用双线
|
||||||
|
- 增加微弱背景色标示脚注区域
|
||||||
|
- 脚注编号可加 accent 色
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 高亮文本(Highlight) — 需微调
|
||||||
|
|
||||||
|
**现状问题**:
|
||||||
|
- Light 模式下亮黄色 OK
|
||||||
|
- Dark 模式下黄色变暗了但可能和文字对比度不够
|
||||||
|
|
||||||
|
**建议方向**:
|
||||||
|
- Dark 模式下可用更亮的荧光色或调整透明度
|
||||||
|
- 确保 WCAG AA 对比度达标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前表现良好的元素
|
||||||
|
|
||||||
|
### Admonitions — 良好
|
||||||
|
- 6 种类型颜色区分明显
|
||||||
|
- 左边框 + 标题栏 + 图标结构清晰
|
||||||
|
- Light/Dark 模式切换正常
|
||||||
|
- 有自定义标题功能
|
||||||
|
|
||||||
|
### 数学公式 — 良好
|
||||||
|
- 行内和块级都正确渲染
|
||||||
|
- KaTeX 排版质量好
|
||||||
|
|
||||||
|
### 链接 — 良好
|
||||||
|
- 蓝色链接色在两种模式下都清晰
|
||||||
|
- Hover 下划线效果正常
|
||||||
|
|
||||||
|
### 段落 & 内联元素 — 良好
|
||||||
|
- Bold、italic、strikethrough、inline code、subscript、superscript 都正常
|
||||||
|
- Inline code 有背景色和边框,辨识度高
|
||||||
|
|
||||||
|
### Mermaid 图 — 良好
|
||||||
|
- 容器有 header + 模式切换
|
||||||
|
- 边框和阴影提供了层级感
|
||||||
|
|
||||||
|
### 图片 — 良好
|
||||||
|
- 正确加载并显示
|
||||||
|
- 有 max-width 限制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 整体节奏问题
|
||||||
|
|
||||||
|
### 间距不统一
|
||||||
|
- 段落 `1.25em`、引用块 `1.6em`、列表 `1.25rem`、表格 `2rem`、分割线 `3rem` — 缺乏统一节奏
|
||||||
|
- 代码块和表格的上下间距不协调
|
||||||
|
|
||||||
|
### Dark 模式对比度
|
||||||
|
- 多个元素在 dark 模式下对比度不足(分割线、表格边框、引用块边框、列表标记)
|
||||||
|
- 需要整体提升 dark 模式下的边框/分隔元素亮度
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 优先级排序
|
||||||
|
|
||||||
|
| 优先级 | 元素 | 原因 |
|
||||||
|
|---|---|---|
|
||||||
|
| P0 | 表格 | 当前最朴素,用户感知最强 |
|
||||||
|
| P0 | 代码块容器 | 高频组件,当前缺乏层级感 |
|
||||||
|
| P1 | 引用块 | dark 模式下几乎不可见 |
|
||||||
|
| P1 | 标题层级 | h4-h6 无法区分 |
|
||||||
|
| P1 | Dark 模式整体对比度 | 多个元素受影响 |
|
||||||
|
| P2 | 定义列表 | 缺乏视觉结构 |
|
||||||
|
| P2 | 分割线 | 视觉权重不够 |
|
||||||
|
| P2 | 列表标记 | 太淡 |
|
||||||
|
| P2 | 脚注 | 框架感不足 |
|
||||||
|
| P3 | 高亮文本 | dark 对比度 |
|
||||||
@@ -0,0 +1,660 @@
|
|||||||
|
import { readdirSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { transformerTwoslash } from '@shikijs/vitepress-twoslash'
|
||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
|
||||||
|
const docsSiteUrl = 'https://markstream-vue-docs.simonhe.me'
|
||||||
|
const docsOgImageUrl = `${docsSiteUrl}/og-image.svg`
|
||||||
|
const docsDefaultDescription = 'Streaming-friendly Markdown renderer for Vue 3, Vue 2, React, and Angular'
|
||||||
|
const githubRepoUrl = 'https://github.com/Simon-He95/markstream-vue'
|
||||||
|
const docsRootDir = fileURLToPath(new URL('..', import.meta.url))
|
||||||
|
const workspaceRootDir = fileURLToPath(new URL('../..', import.meta.url))
|
||||||
|
|
||||||
|
// TypeScript sometimes rejects VitePress site locales on the `Config` type.
|
||||||
|
// Cast to `any` to avoid strict type errors in the docs config while keeping intellisense.
|
||||||
|
const markdownParserSrc = fileURLToPath(new URL('../../packages/markdown-parser/src/index.ts', import.meta.url))
|
||||||
|
const markdownParserSrcDir = path.dirname(markdownParserSrc)
|
||||||
|
|
||||||
|
const playgroundItems = [
|
||||||
|
{ text: 'Vue 3', link: 'https://markstream-vue.simonhe.me/' },
|
||||||
|
{ text: 'React', link: 'https://markstream-react.pages.dev/' },
|
||||||
|
{ text: 'Nuxt', link: 'https://markstream-nuxt.pages.dev/' },
|
||||||
|
{ text: 'Vue 2', link: 'https://markstream-vue2.pages.dev/' },
|
||||||
|
{ text: 'Angular', link: 'https://markstream-angular.pages.dev/' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const rootNav = [
|
||||||
|
{ text: 'Get started', link: '/guide/installation' },
|
||||||
|
{ text: 'Customization', link: '/guide/component-overrides' },
|
||||||
|
{ text: 'Migration / AI', link: '/guide/ai-workflows' },
|
||||||
|
{ text: 'API Reference', link: '/guide/api' },
|
||||||
|
{ text: 'Playground', items: playgroundItems },
|
||||||
|
{ text: 'GitHub', link: 'https://github.com/Simon-He95/markstream-vue' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const zhNav = [
|
||||||
|
{ text: '快速开始', link: '/zh/guide/installation' },
|
||||||
|
{ text: '自定义', link: '/zh/guide/component-overrides' },
|
||||||
|
{ text: '接入与迁移', link: '/zh/guide/ai-workflows' },
|
||||||
|
{ text: 'API 参考', link: '/zh/guide/api' },
|
||||||
|
{ text: '演示', items: playgroundItems },
|
||||||
|
{ text: '搜索', link: '/zh/guide/search' },
|
||||||
|
{ text: 'GitHub', link: 'https://github.com/Simon-He95/markstream-vue' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const englishGuideSidebar = [
|
||||||
|
{
|
||||||
|
text: 'Start Here',
|
||||||
|
items: [
|
||||||
|
{ text: 'Guide Home', link: '/guide/' },
|
||||||
|
{ text: 'Installation', link: '/guide/installation' },
|
||||||
|
{ text: 'Quick Start', link: '/guide/quick-start' },
|
||||||
|
{ text: 'Usage & Streaming', link: '/guide/usage' },
|
||||||
|
{ text: 'Docs Site & VitePress', link: '/guide/vitepress-docs-integration' },
|
||||||
|
{ text: 'AI Chat & Streaming', link: '/guide/ai-chat-streaming' },
|
||||||
|
{ text: 'Troubleshooting by Symptom', link: '/guide/troubleshooting-path' },
|
||||||
|
{ text: 'Props & Options', link: '/guide/props' },
|
||||||
|
{ text: 'Features Overview', link: '/guide/features' },
|
||||||
|
{ text: 'Examples', link: '/guide/examples' },
|
||||||
|
{ text: 'Playground', link: '/guide/playground' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Customization',
|
||||||
|
items: [
|
||||||
|
{ text: 'Renderer & Node Components', link: '/guide/components' },
|
||||||
|
{ text: 'Override Built-in Components', link: '/guide/component-overrides' },
|
||||||
|
{ text: 'Custom Tags & Advanced Components', link: '/guide/custom-components' },
|
||||||
|
{ text: 'Advanced Parser Hooks', link: '/guide/advanced' },
|
||||||
|
{ text: 'Parser Overview', link: '/guide/parser' },
|
||||||
|
{ text: 'Parser API', link: '/guide/parser-api' },
|
||||||
|
{ text: 'Tailwind & Styling', link: '/guide/tailwind' },
|
||||||
|
{ text: 'Troubleshooting', link: '/guide/troubleshooting' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Feature Guides',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Code Blocks', link: '/guide/code-blocks' },
|
||||||
|
{ text: 'Code Block Header', link: '/guide/codeblock-header' },
|
||||||
|
{ text: 'CodeBlockNode', link: '/guide/code-block-node' },
|
||||||
|
{ text: 'ImageNode', link: '/guide/image-node' },
|
||||||
|
{ text: 'Math', link: '/guide/math' },
|
||||||
|
{ text: 'Mermaid', link: '/guide/mermaid' },
|
||||||
|
{ text: 'MermaidBlockNode', link: '/guide/mermaid-block-node' },
|
||||||
|
{ text: 'MermaidBlockNode (override)', link: '/guide/mermaid-block-node-override' },
|
||||||
|
{ text: 'Mermaid export demo', link: '/guide/mermaid-export-demo' },
|
||||||
|
{ text: 'D2', link: '/guide/d2' },
|
||||||
|
{ text: 'AntV Infographic', link: '/guide/infographic' },
|
||||||
|
{ text: 'ECharts', link: '/guide/echarts' },
|
||||||
|
{ text: 'Monaco', link: '/guide/monaco' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Frameworks & Migration',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Nuxt SSR', link: '/nuxt-ssr' },
|
||||||
|
{ text: 'Vue 2 Quick Start', link: '/guide/vue2-quick-start' },
|
||||||
|
{ text: 'Vue 2 Installation', link: '/guide/vue2-installation' },
|
||||||
|
{ text: 'Vue 2 Components', link: '/guide/vue2-components' },
|
||||||
|
{ text: 'React Quick Start', link: '/guide/react-quick-start' },
|
||||||
|
{ text: 'React Installation', link: '/guide/react-installation' },
|
||||||
|
{ text: 'React Components', link: '/guide/react-components' },
|
||||||
|
{ text: 'Migrate from react-markdown', link: '/guide/react-markdown-migration' },
|
||||||
|
{ text: 'Migration Cookbook', link: '/guide/react-markdown-migration-cookbook' },
|
||||||
|
{ text: 'Angular Quick Start', link: '/guide/angular-quick-start' },
|
||||||
|
{ text: 'Angular Installation', link: '/guide/angular-installation' },
|
||||||
|
{ text: 'AI / Skills workflows', link: '/guide/ai-workflows' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Reference',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'API Overview', link: '/guide/api' },
|
||||||
|
{ text: 'Performance', link: '/guide/performance' },
|
||||||
|
{ text: 'Why use it?', link: '/guide/why' },
|
||||||
|
{ text: 'Compared', link: '/guide/compared' },
|
||||||
|
{ text: 'Monaco Internals', link: '/guide/monaco-internals' },
|
||||||
|
{ text: 'Legacy builds & iOS regex compatibility', link: '/guide/legacy-builds' },
|
||||||
|
{ text: 'Contributing', link: '/guide/contributing' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const chineseGuideSidebar = [
|
||||||
|
{
|
||||||
|
text: '开始使用',
|
||||||
|
items: [
|
||||||
|
{ text: '指南首页', link: '/zh/guide/' },
|
||||||
|
{ text: '安装', link: '/zh/guide/installation' },
|
||||||
|
{ text: '快速开始', link: '/zh/guide/quick-start' },
|
||||||
|
{ text: '使用与流式渲染', link: '/zh/guide/usage' },
|
||||||
|
{ text: '文档站与 VitePress 集成', link: '/zh/guide/vitepress-docs-integration' },
|
||||||
|
{ text: 'AI 聊天与流式输出', link: '/zh/guide/ai-chat-streaming' },
|
||||||
|
{ text: '按症状排查', link: '/zh/guide/troubleshooting-path' },
|
||||||
|
{ text: 'Props 与选项', link: '/zh/guide/props' },
|
||||||
|
{ text: '功能特性', link: '/zh/guide/features' },
|
||||||
|
{ text: '示例', link: '/zh/guide/examples' },
|
||||||
|
{ text: 'Playground', link: '/zh/guide/playground' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '自定义',
|
||||||
|
items: [
|
||||||
|
{ text: '渲染器与节点组件', link: '/zh/guide/components' },
|
||||||
|
{ text: '覆盖内置组件', link: '/zh/guide/component-overrides' },
|
||||||
|
{ text: '自定义标签与高级组件', link: '/zh/guide/custom-components' },
|
||||||
|
{ text: '高级解析', link: '/zh/guide/advanced' },
|
||||||
|
{ text: '解析器概览', link: '/zh/guide/parser' },
|
||||||
|
{ text: '解析器 API', link: '/zh/guide/parser-api' },
|
||||||
|
{ text: 'Tailwind 与样式', link: '/zh/guide/tailwind' },
|
||||||
|
{ text: '故障排除', link: '/zh/guide/troubleshooting' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '功能专题',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: '代码块', link: '/zh/guide/code-blocks' },
|
||||||
|
{ text: '代码块头部', link: '/zh/guide/codeblock-header' },
|
||||||
|
{ text: 'CodeBlockNode', link: '/zh/guide/code-block-node' },
|
||||||
|
{ text: 'ImageNode', link: '/zh/guide/image-node' },
|
||||||
|
{ text: 'Math', link: '/zh/guide/math' },
|
||||||
|
{ text: 'Mermaid', link: '/zh/guide/mermaid' },
|
||||||
|
{ text: 'MermaidBlockNode', link: '/zh/guide/mermaid-block-node' },
|
||||||
|
{ text: '覆盖 MermaidBlockNode', link: '/zh/guide/mermaid-block-node-override' },
|
||||||
|
{ text: 'Mermaid 导出示例', link: '/zh/guide/mermaid-export-demo' },
|
||||||
|
{ text: 'D2 图表', link: '/zh/guide/d2' },
|
||||||
|
{ text: 'AntV Infographic', link: '/zh/guide/infographic' },
|
||||||
|
{ text: 'ECharts', link: '/zh/guide/echarts' },
|
||||||
|
{ text: 'Monaco', link: '/zh/guide/monaco' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '框架与迁移',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Nuxt SSR', link: '/zh/nuxt-ssr' },
|
||||||
|
{ text: 'Vue 2 快速开始', link: '/zh/guide/vue2-quick-start' },
|
||||||
|
{ text: 'Vue 2 安装', link: '/zh/guide/vue2-installation' },
|
||||||
|
{ text: 'Vue 2 组件', link: '/zh/guide/vue2-components' },
|
||||||
|
{ text: 'React 快速开始', link: '/zh/guide/react-quick-start' },
|
||||||
|
{ text: 'React 安装', link: '/zh/guide/react-installation' },
|
||||||
|
{ text: 'React 组件', link: '/zh/guide/react-components' },
|
||||||
|
{ text: '从 react-markdown 迁移', link: '/zh/guide/react-markdown-migration' },
|
||||||
|
{ text: '迁移 Cookbook', link: '/zh/guide/react-markdown-migration-cookbook' },
|
||||||
|
{ text: 'Angular 快速开始', link: '/zh/guide/angular-quick-start' },
|
||||||
|
{ text: 'Angular 安装', link: '/zh/guide/angular-installation' },
|
||||||
|
{ text: 'AI / Skills 工作流', link: '/zh/guide/ai-workflows' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '参考',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'API 总览', link: '/zh/guide/api' },
|
||||||
|
{ text: '搜索', link: '/zh/guide/search' },
|
||||||
|
{ text: '性能', link: '/zh/guide/performance' },
|
||||||
|
{ text: '为什么使用?', link: '/zh/guide/why' },
|
||||||
|
{ text: '对比', link: '/zh/guide/compared' },
|
||||||
|
{ text: 'Monaco 内部', link: '/zh/guide/monaco-internals' },
|
||||||
|
{ text: 'Legacy 构建与 iOS 正则兼容', link: '/zh/guide/legacy-builds' },
|
||||||
|
{ text: '贡献指南', link: '/zh/guide/contributing' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const siteHead = [
|
||||||
|
['link', { rel: 'icon', href: '/logo.svg', type: 'image/svg+xml' }],
|
||||||
|
['meta', { name: 'theme-color', content: '#111827' }],
|
||||||
|
['meta', { name: 'robots', content: 'index,follow' }],
|
||||||
|
['meta', { property: 'og:type', content: 'website' }],
|
||||||
|
['meta', { property: 'og:site_name', content: 'markstream-vue' }],
|
||||||
|
['meta', { property: 'og:image', content: docsOgImageUrl }],
|
||||||
|
['meta', { property: 'og:image:alt', content: 'markstream-vue documentation and playground previews' }],
|
||||||
|
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||||
|
['meta', { name: 'twitter:image', content: docsOgImageUrl }],
|
||||||
|
]
|
||||||
|
|
||||||
|
const seoExcludedDocsPaths = new Set([
|
||||||
|
'/404',
|
||||||
|
'/LEGACY-BUILDS',
|
||||||
|
'/e2e-testing-report',
|
||||||
|
'/zh/e2e-testing-report',
|
||||||
|
'/guide/docs-style',
|
||||||
|
'/zh/guide/docs-style',
|
||||||
|
'/guide/e2e-testing-report',
|
||||||
|
'/zh/guide/e2e-testing-report',
|
||||||
|
'/guide/katex-worker-performance-analysis',
|
||||||
|
'/zh/guide/katex-worker-performance-analysis',
|
||||||
|
'/guide/monorepo-migration',
|
||||||
|
'/zh/guide/monorepo-migration',
|
||||||
|
'/guide/thanks',
|
||||||
|
'/zh/guide/thanks',
|
||||||
|
'/guide/translation',
|
||||||
|
'/zh/guide/translation',
|
||||||
|
'/katex-cache-analysis',
|
||||||
|
'/zh/katex-cache-analysis',
|
||||||
|
'/katex-worker-performance-analysis',
|
||||||
|
'/zh/katex-worker-performance-analysis',
|
||||||
|
'/llms',
|
||||||
|
'/llms.zh-CN',
|
||||||
|
'/monorepo-migration',
|
||||||
|
'/zh/monorepo-migration',
|
||||||
|
'/nuxt-ssr.zh-CN',
|
||||||
|
])
|
||||||
|
|
||||||
|
function normalizeDocsSeoPath(page: string) {
|
||||||
|
if (!page || page === '.')
|
||||||
|
return '/'
|
||||||
|
|
||||||
|
let normalized = page
|
||||||
|
|
||||||
|
if (!normalized.startsWith('/'))
|
||||||
|
normalized = `/${normalized}`
|
||||||
|
|
||||||
|
normalized = normalized.replace(/\/index\.md$/, '/')
|
||||||
|
normalized = normalized.replace(/\/index\.html$/, '/')
|
||||||
|
normalized = normalized.replace(/\.md$/, '')
|
||||||
|
normalized = normalized.replace(/\.html$/, '')
|
||||||
|
normalized = normalized.replace(/\/index$/, '/')
|
||||||
|
|
||||||
|
if (normalized !== '/' && normalized.endsWith('/'))
|
||||||
|
normalized = normalized.slice(0, -1)
|
||||||
|
|
||||||
|
return normalized || '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDocsSeoTitle(pageTitle?: string) {
|
||||||
|
if (!pageTitle || pageTitle === 'markstream-vue')
|
||||||
|
return 'markstream-vue'
|
||||||
|
|
||||||
|
return `${pageTitle} | markstream-vue`
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDocsSeoExcluded(path: string) {
|
||||||
|
return seoExcludedDocsPaths.has(path) || path.endsWith('.zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDocsPriorityKey(path: string) {
|
||||||
|
if (path === '/zh')
|
||||||
|
return '/'
|
||||||
|
|
||||||
|
if (path.startsWith('/zh/'))
|
||||||
|
return path.slice(3) || '/'
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDocsRoutePaths(rootDir: string) {
|
||||||
|
const routePaths = new Set<string>()
|
||||||
|
|
||||||
|
function walk(currentDir: string) {
|
||||||
|
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
||||||
|
if (entry.name === '.vitepress' || entry.name === 'node_modules')
|
||||||
|
continue
|
||||||
|
|
||||||
|
const absolutePath = path.join(currentDir, entry.name)
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
walk(absolutePath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
const relativePath = path.relative(rootDir, absolutePath).replace(/\\/g, '/')
|
||||||
|
routePaths.add(normalizeDocsSeoPath(`/${relativePath}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(rootDir)
|
||||||
|
return routePaths
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableDocsRoutePaths = collectDocsRoutePaths(docsRootDir)
|
||||||
|
|
||||||
|
const docsPrimaryLandingPaths = new Set([
|
||||||
|
'/guide',
|
||||||
|
'/guide/installation',
|
||||||
|
'/guide/quick-start',
|
||||||
|
'/guide/usage',
|
||||||
|
'/guide/vitepress-docs-integration',
|
||||||
|
'/guide/ai-chat-streaming',
|
||||||
|
'/guide/troubleshooting-path',
|
||||||
|
'/guide/component-overrides',
|
||||||
|
'/guide/custom-components',
|
||||||
|
'/guide/api',
|
||||||
|
'/guide/components',
|
||||||
|
'/guide/props',
|
||||||
|
'/guide/performance',
|
||||||
|
'/guide/tailwind',
|
||||||
|
'/guide/react-markdown-migration',
|
||||||
|
'/guide/playground',
|
||||||
|
'/nuxt-ssr',
|
||||||
|
])
|
||||||
|
|
||||||
|
const docsSecondaryLandingPaths = new Set([
|
||||||
|
'/guide/parser',
|
||||||
|
'/guide/parser-api',
|
||||||
|
'/guide/features',
|
||||||
|
'/guide/examples',
|
||||||
|
'/guide/react-markdown-migration-cookbook',
|
||||||
|
'/guide/vue2-quick-start',
|
||||||
|
'/guide/react-quick-start',
|
||||||
|
'/guide/angular-quick-start',
|
||||||
|
'/guide/troubleshooting',
|
||||||
|
])
|
||||||
|
|
||||||
|
function getDocsSitemapHints(path: string) {
|
||||||
|
const priorityKey = getDocsPriorityKey(path)
|
||||||
|
|
||||||
|
if (priorityKey === '/')
|
||||||
|
return { priority: 1, changefreq: 'weekly' as const }
|
||||||
|
|
||||||
|
if (priorityKey === '/guide')
|
||||||
|
return { priority: 0.95, changefreq: 'weekly' as const }
|
||||||
|
|
||||||
|
if (docsPrimaryLandingPaths.has(priorityKey))
|
||||||
|
return { priority: 0.88, changefreq: 'weekly' as const }
|
||||||
|
|
||||||
|
if (docsSecondaryLandingPaths.has(priorityKey))
|
||||||
|
return { priority: 0.78, changefreq: 'monthly' as const }
|
||||||
|
|
||||||
|
if (priorityKey.startsWith('/guide/'))
|
||||||
|
return { priority: 0.68, changefreq: 'monthly' as const }
|
||||||
|
|
||||||
|
return { priority: 0.6, changefreq: 'monthly' as const }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDocsAlternatePaths(path: string) {
|
||||||
|
const englishPath = path === '/zh' ? '/' : path.startsWith('/zh/') ? path.slice(3) || '/' : path
|
||||||
|
const chinesePath = path === '/' ? '/zh' : path.startsWith('/zh/') ? path : `/zh${path}`
|
||||||
|
|
||||||
|
const alternates = {
|
||||||
|
english: availableDocsRoutePaths.has(englishPath) ? englishPath : null,
|
||||||
|
chinese: availableDocsRoutePaths.has(chinesePath) ? chinesePath : null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return alternates
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDocsBreadcrumbItems(path: string, title: string, isChinese: boolean) {
|
||||||
|
const items = []
|
||||||
|
const homeName = isChinese ? '首页' : 'Home'
|
||||||
|
const guideName = isChinese ? '指南' : 'Guide'
|
||||||
|
const homePath = isChinese ? '/zh' : '/'
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
'position': 1,
|
||||||
|
'name': homeName,
|
||||||
|
'item': `${docsSiteUrl}${homePath === '/' ? '/' : homePath}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (path === homePath || (isChinese && path === '/zh') || (!isChinese && path === '/'))
|
||||||
|
return items
|
||||||
|
|
||||||
|
const guideRoot = isChinese ? '/zh/guide' : '/guide'
|
||||||
|
if (path === guideRoot || path.startsWith(`${guideRoot}/`)) {
|
||||||
|
items.push({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
'position': 2,
|
||||||
|
'name': guideName,
|
||||||
|
'item': `${docsSiteUrl}${guideRoot}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (path !== guideRoot) {
|
||||||
|
items.push({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
'position': 3,
|
||||||
|
'name': title,
|
||||||
|
'item': `${docsSiteUrl}${path}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
'@type': 'ListItem',
|
||||||
|
'position': 2,
|
||||||
|
'name': title,
|
||||||
|
'item': `${docsSiteUrl}${path}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDocsStructuredData(path: string, title: string, description: string, isChinese: boolean) {
|
||||||
|
const graph: Record<string, any>[] = []
|
||||||
|
const homePath = isChinese ? '/zh' : '/'
|
||||||
|
|
||||||
|
if (path === homePath) {
|
||||||
|
graph.push({
|
||||||
|
'@type': 'WebSite',
|
||||||
|
'@id': `${docsSiteUrl}${homePath === '/' ? '/' : homePath}#website`,
|
||||||
|
'name': 'markstream-vue Docs',
|
||||||
|
'url': `${docsSiteUrl}${homePath === '/' ? '/' : homePath}`,
|
||||||
|
'inLanguage': isChinese ? 'zh-CN' : 'en-US',
|
||||||
|
description,
|
||||||
|
'publisher': {
|
||||||
|
'@type': 'Organization',
|
||||||
|
'name': 'markstream-vue',
|
||||||
|
'url': githubRepoUrl,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
graph.push({
|
||||||
|
'@type': 'SoftwareApplication',
|
||||||
|
'name': 'markstream-vue',
|
||||||
|
'applicationCategory': 'DeveloperApplication',
|
||||||
|
'operatingSystem': 'Web',
|
||||||
|
'url': `${docsSiteUrl}${homePath === '/' ? '/' : homePath}`,
|
||||||
|
description,
|
||||||
|
'offers': {
|
||||||
|
'@type': 'Offer',
|
||||||
|
'price': '0',
|
||||||
|
'priceCurrency': 'USD',
|
||||||
|
},
|
||||||
|
'sameAs': [
|
||||||
|
githubRepoUrl,
|
||||||
|
docsSiteUrl,
|
||||||
|
'https://markstream-vue.simonhe.me/',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbItems = createDocsBreadcrumbItems(path, title, isChinese)
|
||||||
|
if (breadcrumbItems.length > 0) {
|
||||||
|
graph.push({
|
||||||
|
'@type': 'BreadcrumbList',
|
||||||
|
'itemListElement': breadcrumbItems,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graph.length === 0)
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [[
|
||||||
|
'script',
|
||||||
|
{ type: 'application/ld+json' },
|
||||||
|
JSON.stringify({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': graph,
|
||||||
|
}),
|
||||||
|
]] as [string, Record<string, string>, string][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
title: 'markstream-vue',
|
||||||
|
description: docsDefaultDescription,
|
||||||
|
base: process.env.VITEPRESS_BASE || '/',
|
||||||
|
head: siteHead,
|
||||||
|
markdown: {
|
||||||
|
languages: ['ts', 'tsx', 'js', 'jsx', 'vue'],
|
||||||
|
codeTransformers: [
|
||||||
|
transformerTwoslash({
|
||||||
|
explicitTrigger: true,
|
||||||
|
twoslashOptions: {
|
||||||
|
compilerOptions: {
|
||||||
|
baseUrl: workspaceRootDir,
|
||||||
|
paths: {
|
||||||
|
'markstream-vue': ['docs/.vitepress/twoslash/markstream-vue.d.ts'],
|
||||||
|
'stream-markdown-parser': ['packages/markdown-parser/src/index.ts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sitemap: {
|
||||||
|
hostname: docsSiteUrl,
|
||||||
|
transformItems(items) {
|
||||||
|
return items
|
||||||
|
.map((item) => {
|
||||||
|
const url = new URL(item.url, docsSiteUrl)
|
||||||
|
const path = normalizeDocsSeoPath(url.pathname)
|
||||||
|
const { priority, changefreq } = getDocsSitemapHints(path)
|
||||||
|
url.pathname = path === '/' ? '/' : path
|
||||||
|
const links = item.links?.map((link) => {
|
||||||
|
const alternateUrl = new URL(link.url, docsSiteUrl)
|
||||||
|
const alternatePath = normalizeDocsSeoPath(alternateUrl.pathname)
|
||||||
|
alternateUrl.pathname = alternatePath === '/' ? '/' : alternatePath
|
||||||
|
|
||||||
|
return {
|
||||||
|
...link,
|
||||||
|
url: alternateUrl.toString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
url: url.toString(),
|
||||||
|
links,
|
||||||
|
priority,
|
||||||
|
changefreq,
|
||||||
|
fullPrecisionPriority: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(item => !isDocsSeoExcluded(normalizeDocsSeoPath(new URL(item.url, docsSiteUrl).pathname)))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
// Docs import the built `markstream-vue` package, which keeps
|
||||||
|
// `stream-markdown-parser` external. Point VitePress at the workspace
|
||||||
|
// source so clean CI checkouts don't need a prebuilt parser dist.
|
||||||
|
alias: [
|
||||||
|
{
|
||||||
|
find: /^stream-markdown-parser$/,
|
||||||
|
replacement: markdownParserSrc,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^stream-markdown-parser\//,
|
||||||
|
replacement: `${markdownParserSrcDir}/`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
label: 'English',
|
||||||
|
lang: 'en-US',
|
||||||
|
title: 'markstream-vue',
|
||||||
|
description: docsDefaultDescription,
|
||||||
|
link: '/',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
label: '简体中文',
|
||||||
|
lang: 'zh-CN',
|
||||||
|
title: 'markstream-vue',
|
||||||
|
description: '适用于 Vue 3 的流式 Markdown 渲染器',
|
||||||
|
link: '/zh/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
themeConfig: {
|
||||||
|
logo: '/logo.svg',
|
||||||
|
search: {
|
||||||
|
provider: 'local',
|
||||||
|
},
|
||||||
|
nav: rootNav,
|
||||||
|
sidebar: {
|
||||||
|
'/guide/': englishGuideSidebar,
|
||||||
|
'/zh/guide/': chineseGuideSidebar,
|
||||||
|
'/zh/': chineseGuideSidebar,
|
||||||
|
'/': englishGuideSidebar,
|
||||||
|
},
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
selectText: 'Languages',
|
||||||
|
label: 'English',
|
||||||
|
ariaLabel: 'Select language',
|
||||||
|
nav: rootNav,
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
selectText: '选择语言',
|
||||||
|
label: '简体中文',
|
||||||
|
ariaLabel: '选择语言',
|
||||||
|
nav: zhNav,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transformHead(ctx) {
|
||||||
|
const normalizedPath = normalizeDocsSeoPath(ctx.page)
|
||||||
|
const canonicalUrl = `${docsSiteUrl}${normalizedPath}`
|
||||||
|
const frontmatter = ctx.pageData.frontmatter ?? {}
|
||||||
|
const isChinese = normalizedPath === '/zh' || normalizedPath.startsWith('/zh/')
|
||||||
|
const description = frontmatter.description || ctx.description || docsDefaultDescription
|
||||||
|
const title = createDocsSeoTitle(frontmatter.title || ctx.pageData.title || ctx.title)
|
||||||
|
const shouldIndex = !ctx.pageData.isNotFound && !frontmatter.noindex && !isDocsSeoExcluded(normalizedPath)
|
||||||
|
const alternates = getDocsAlternatePaths(normalizedPath)
|
||||||
|
const alternateHead = []
|
||||||
|
const alternateLocales = []
|
||||||
|
|
||||||
|
if (alternates.english) {
|
||||||
|
alternateHead.push(['link', { rel: 'alternate', hreflang: 'en-US', href: `${docsSiteUrl}${alternates.english}` }])
|
||||||
|
alternateLocales.push('en_US')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alternates.chinese) {
|
||||||
|
alternateHead.push(['link', { rel: 'alternate', hreflang: 'zh-CN', href: `${docsSiteUrl}${alternates.chinese}` }])
|
||||||
|
alternateLocales.push('zh_CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAlternate = alternates.english || alternates.chinese
|
||||||
|
if (defaultAlternate)
|
||||||
|
alternateHead.push(['link', { rel: 'alternate', hreflang: 'x-default', href: `${docsSiteUrl}${defaultAlternate}` }])
|
||||||
|
|
||||||
|
const structuredData = shouldIndex
|
||||||
|
? createDocsStructuredData(
|
||||||
|
normalizedPath,
|
||||||
|
frontmatter.title || ctx.pageData.title || ctx.title || 'markstream-vue',
|
||||||
|
description,
|
||||||
|
isChinese,
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return [
|
||||||
|
['link', { rel: 'canonical', href: canonicalUrl }],
|
||||||
|
...alternateHead,
|
||||||
|
['meta', { name: 'description', content: description }],
|
||||||
|
['meta', { name: 'robots', content: shouldIndex ? 'index,follow' : 'noindex,nofollow' }],
|
||||||
|
['meta', { property: 'og:locale', content: isChinese ? 'zh_CN' : 'en_US' }],
|
||||||
|
...alternateLocales
|
||||||
|
.filter(locale => locale !== (isChinese ? 'zh_CN' : 'en_US'))
|
||||||
|
.map(locale => ['meta', { property: 'og:locale:alternate', content: locale }]),
|
||||||
|
['meta', { property: 'og:title', content: title }],
|
||||||
|
['meta', { property: 'og:description', content: description }],
|
||||||
|
['meta', { property: 'og:url', content: canonicalUrl }],
|
||||||
|
['meta', { name: 'twitter:title', content: title }],
|
||||||
|
['meta', { name: 'twitter:description', content: description }],
|
||||||
|
...structuredData,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none">
|
||||||
|
<rect width="120" height="120" rx="24" fill="#0ea5e9"/>
|
||||||
|
<text x="50%" y="58%" dominant-baseline="middle" text-anchor="middle" font-size="48" font-family="Arial" fill="#fff">MD</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 269 B |
@@ -0,0 +1,35 @@
|
|||||||
|
<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="1200" height="630" fill="#081122"/>
|
||||||
|
<rect x="38" y="38" width="1124" height="554" rx="32" fill="url(#panel)"/>
|
||||||
|
<rect x="86" y="96" width="492" height="340" rx="28" fill="#0F172A" stroke="#334155" stroke-width="2"/>
|
||||||
|
<rect x="118" y="132" width="184" height="18" rx="9" fill="#22C55E" fill-opacity="0.9"/>
|
||||||
|
<rect x="118" y="172" width="402" height="20" rx="10" fill="#E2E8F0" fill-opacity="0.92"/>
|
||||||
|
<rect x="118" y="208" width="348" height="20" rx="10" fill="#94A3B8" fill-opacity="0.8"/>
|
||||||
|
<rect x="118" y="258" width="116" height="116" rx="24" fill="#111827"/>
|
||||||
|
<rect x="254" y="258" width="116" height="116" rx="24" fill="#0B1220"/>
|
||||||
|
<rect x="390" y="258" width="156" height="116" rx="24" fill="#111827"/>
|
||||||
|
<path d="M174 319H224" stroke="#38BDF8" stroke-width="18" stroke-linecap="round"/>
|
||||||
|
<path d="M312 289L312 349" stroke="#22C55E" stroke-width="18" stroke-linecap="round"/>
|
||||||
|
<path d="M446 290H486C506.987 290 524 307.013 524 328C524 348.987 506.987 366 486 366H446V290Z" fill="#F59E0B"/>
|
||||||
|
<rect x="608" y="118" width="502" height="394" rx="32" fill="#020617" stroke="#1E293B" stroke-width="2"/>
|
||||||
|
<rect x="652" y="152" width="92" height="34" rx="17" fill="#22C55E" fill-opacity="0.14" stroke="#22C55E" stroke-opacity="0.7"/>
|
||||||
|
<text x="698" y="169" fill="#DCFCE7" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" dominant-baseline="middle">DOCS</text>
|
||||||
|
<text x="652" y="232" fill="#F8FAFC" font-family="Arial, Helvetica, sans-serif" font-size="46" font-weight="700">markstream-vue</text>
|
||||||
|
<text x="652" y="288" fill="#93C5FD" font-family="Arial, Helvetica, sans-serif" font-size="28" font-weight="700">Guides and API</text>
|
||||||
|
<text x="652" y="342" fill="#CBD5E1" font-family="Arial, Helvetica, sans-serif" font-size="20">Streaming Markdown for Vue</text>
|
||||||
|
<text x="652" y="378" fill="#CBD5E1" font-family="Arial, Helvetica, sans-serif" font-size="20">VitePress, overrides, custom tags</text>
|
||||||
|
<rect x="652" y="416" width="100" height="36" rx="18" fill="#0F1D33" stroke="#334155"/>
|
||||||
|
<text x="702" y="434" fill="#E2E8F0" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" dominant-baseline="middle">Guides</text>
|
||||||
|
<rect x="764" y="416" width="72" height="36" rx="18" fill="#0F1D33" stroke="#334155"/>
|
||||||
|
<text x="800" y="434" fill="#E2E8F0" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" dominant-baseline="middle">API</text>
|
||||||
|
<rect x="848" y="416" width="118" height="36" rx="18" fill="#0F1D33" stroke="#334155"/>
|
||||||
|
<text x="907" y="434" fill="#E2E8F0" font-family="Arial, Helvetica, sans-serif" font-size="16" font-weight="700" text-anchor="middle" dominant-baseline="middle">Migration</text>
|
||||||
|
<rect x="652" y="462" width="164" height="42" rx="21" fill="#22C55E" fill-opacity="0.18" stroke="#22C55E"/>
|
||||||
|
<text x="734" y="483" fill="#DCFCE7" font-family="Arial, Helvetica, sans-serif" font-size="22" font-weight="700" text-anchor="middle" dominant-baseline="middle">Read docs</text>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="panel" x1="78" y1="48" x2="1082" y2="582" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#111827"/>
|
||||||
|
<stop offset="1" stop-color="#0F766E"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://markstream-vue-docs.simonhe.me/sitemap.xml
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 360' width='600' height='360'>
|
||||||
|
<rect width='100%' height='100%' fill='#111827'/>
|
||||||
|
<rect x='56' y='40' width='488' height='280' rx='8' fill='#0f172a'/>
|
||||||
|
<text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' font-size='22' fill='#9CA3AF' font-family='Courier New, monospace'>Monaco / CodeBlock demo</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 388 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 360' width='600' height='360'>
|
||||||
|
<rect width='100%' height='100%' fill='#0ea5e9'/>
|
||||||
|
<rect x='24' y='24' width='552' height='312' rx='12' fill='#fff'/>
|
||||||
|
<text x='50%' y='40%' dominant-baseline='middle' text-anchor='middle' font-size='18' fill='#0ea5e9' font-family='Arial'>Mermaid Diagram Example</text>
|
||||||
|
<text x='50%' y='56%' dominant-baseline='middle' text-anchor='middle' font-size='14' fill='#0ea5e9' font-family='Arial'>Progressive diagram rendering</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 528 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 600 360' width='600' height='360'>
|
||||||
|
<rect width='100%' height='100%' fill='#0ea5e9'/>
|
||||||
|
<rect x='24' y='24' width='552' height='312' rx='12' fill='#fff'/>
|
||||||
|
<text x='50%' y='40%' dominant-baseline='middle' text-anchor='middle' font-size='28' fill='#0ea5e9' font-family='Arial'>Playground Demo</text>
|
||||||
|
<text x='50%' y='54%' dominant-baseline='middle' text-anchor='middle' font-size='16' fill='#0ea5e9' font-family='Arial'>Live editor + preview</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 512 B |
@@ -0,0 +1,136 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
repo: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const stars = ref<number | null>(null)
|
||||||
|
const isLoading = ref(true)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.github.com/repos/${props.repo}`)
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
stars.value = data.stargazers_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Silently fail if API request fails
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
if (num >= 1000) {
|
||||||
|
return `${(num / 1000).toFixed(1)}k`
|
||||||
|
}
|
||||||
|
return num.toString()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a
|
||||||
|
:href="`https://github.com/${repo}`"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="github-star-badge"
|
||||||
|
aria-label="Star on GitHub"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="star-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span v-if="isLoading" class="stars-count">...</span>
|
||||||
|
<span v-else-if="stars !== null" class="stars-count">{{ formatNumber(stars) }}</span>
|
||||||
|
<span v-else class="stars-count">Star</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.github-star-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.4375rem 0.875rem;
|
||||||
|
background: linear-gradient(135deg, #f6f8fa 0%, #f1f3f5 100%);
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
border-radius: 24px;
|
||||||
|
color: #24292f;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge:hover {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f6f8fa 100%);
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.github-star-badge {
|
||||||
|
background: linear-gradient(135deg, #161b22 0%, #0d1117 100%);
|
||||||
|
border-color: #30363d;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge:hover {
|
||||||
|
background: linear-gradient(135deg, #21262d 0%, #161b22 100%);
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge:hover .star-icon {
|
||||||
|
transform: scale(1.15) rotate(8deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars-count {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
min-width: 2ch;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars-count:empty::before {
|
||||||
|
content: '...';
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const stars = ref<number | null>(null)
|
||||||
|
const isLoading = ref(true)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.github.com/repos/Simon-He95/markstream-vue')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
stars.value = data.stargazers_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Silently fail if API request fails
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
if (num >= 1000) {
|
||||||
|
return `${(num / 1000).toFixed(1)}k`
|
||||||
|
}
|
||||||
|
return num.toString()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DefaultTheme.Layout>
|
||||||
|
<template #nav-bar-content-after>
|
||||||
|
<a
|
||||||
|
href="https://github.com/Simon-He95/markstream-vue"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="github-star-badge-nav"
|
||||||
|
aria-label="Star on GitHub"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="star-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span v-if="isLoading" class="stars-count">...</span>
|
||||||
|
<span v-else-if="stars !== null" class="stars-count">{{ formatNumber(stars) }}</span>
|
||||||
|
<span v-else class="stars-count">Star</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</DefaultTheme.Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.github-star-badge-nav {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #f6f8fa 0%, #f1f3f5 100%);
|
||||||
|
border: 1px solid #d0d7de;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #24292f;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge-nav:hover {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f6f8fa 100%);
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge-nav:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .github-star-badge-nav {
|
||||||
|
background: linear-gradient(135deg, #161b22 0%, #0d1117 100%);
|
||||||
|
border-color: #30363d;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .github-star-badge-nav:hover {
|
||||||
|
background: linear-gradient(135deg, #21262d 0%, #161b22 100%);
|
||||||
|
border-color: var(--vp-c-brand);
|
||||||
|
color: var(--vp-c-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge-nav .star-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.1));
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge-nav:hover .star-icon {
|
||||||
|
transform: scale(1.1) rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-star-badge-nav .stars-count {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for loading state */
|
||||||
|
.github-star-badge-nav .stars-count {
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove animation when loaded */
|
||||||
|
.github-star-badge-nav .stars-count:not(:empty) {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { withBase } from 'vitepress'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
note?: string
|
||||||
|
alipayLabel?: string
|
||||||
|
wechatLabel?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
alipayLabel: 'Alipay',
|
||||||
|
wechatLabel: 'WeChat Pay',
|
||||||
|
})
|
||||||
|
|
||||||
|
const alipaySrc = withBase('/sponsor/zhifubao.jpg')
|
||||||
|
const wechatSrc = withBase('/sponsor/weixin.jpg')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="support-qrs">
|
||||||
|
<div class="support-qrs__copy">
|
||||||
|
<h2 class="support-qrs__title">
|
||||||
|
{{ props.title }}
|
||||||
|
</h2>
|
||||||
|
<p class="support-qrs__description">
|
||||||
|
{{ props.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="support-qrs__grid">
|
||||||
|
<figure class="support-qrs__card">
|
||||||
|
<div class="support-qrs__image-frame">
|
||||||
|
<img
|
||||||
|
class="support-qrs__image"
|
||||||
|
:src="alipaySrc"
|
||||||
|
:alt="`${props.alipayLabel} QR code`"
|
||||||
|
loading="lazy"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<figcaption class="support-qrs__label">
|
||||||
|
{{ props.alipayLabel }}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="support-qrs__card">
|
||||||
|
<div class="support-qrs__image-frame">
|
||||||
|
<img
|
||||||
|
class="support-qrs__image"
|
||||||
|
:src="wechatSrc"
|
||||||
|
:alt="`${props.wechatLabel} QR code`"
|
||||||
|
loading="lazy"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<figcaption class="support-qrs__label">
|
||||||
|
{{ props.wechatLabel }}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="props.note" class="support-qrs__note">
|
||||||
|
{{ props.note }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.support-qrs {
|
||||||
|
margin: 1.5rem 0 2.25rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 20px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(61, 120, 255, 0.12), transparent 34%),
|
||||||
|
linear-gradient(180deg, rgba(248, 250, 252, 0.92), rgba(255, 255, 255, 0.98));
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__copy {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__description {
|
||||||
|
max-width: 44rem;
|
||||||
|
margin: 0.6rem auto 0;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--vp-c-divider);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.84);
|
||||||
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__image-frame {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 320px;
|
||||||
|
padding: 0.875rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(180deg, rgba(248, 250, 252, 0.94), rgba(241, 245, 249, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__image {
|
||||||
|
width: auto;
|
||||||
|
max-width: min(100%, 240px);
|
||||||
|
max-height: 320px;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 16px 32px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__label {
|
||||||
|
margin-top: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--vp-c-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__note {
|
||||||
|
margin: 1rem auto 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--vp-c-text-2);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .support-qrs {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(122, 162, 255, 0.16), transparent 36%),
|
||||||
|
linear-gradient(180deg, rgba(16, 24, 40, 0.92), rgba(10, 15, 26, 0.98));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .support-qrs__card {
|
||||||
|
background: rgba(15, 23, 42, 0.78);
|
||||||
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .support-qrs__image-frame {
|
||||||
|
background: linear-gradient(180deg, rgba(30, 41, 59, 0.82), rgba(15, 23, 42, 0.92));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.support-qrs {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__card {
|
||||||
|
padding: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__image-frame {
|
||||||
|
min-height: 260px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-qrs__image {
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useData } from 'vitepress'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VitePress theme composable for dark mode
|
||||||
|
* Uses VitePress's built-in isDark from useData()
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```vue
|
||||||
|
* <script setup>
|
||||||
|
* const isDark = useDark()
|
||||||
|
* </script>
|
||||||
|
*
|
||||||
|
* <template>
|
||||||
|
* <MarkdownRender :is-dark="isDark" />
|
||||||
|
* </template>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useDark() {
|
||||||
|
const { isDark } = useData()
|
||||||
|
return isDark
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { EnhanceAppContext } from 'vitepress'
|
||||||
|
import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client'
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
import Theme from 'vitepress/theme'
|
||||||
|
import GitHubStarBadge from './GitHubStarBadge.vue'
|
||||||
|
import Layout from './Layout.vue'
|
||||||
|
import SupportQRCodes from './SupportQRCodes.vue'
|
||||||
|
import '@shikijs/vitepress-twoslash/style.css'
|
||||||
|
import 'markstream-vue/index.css'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: Theme,
|
||||||
|
Layout,
|
||||||
|
enhanceApp({ app }: EnhanceAppContext) {
|
||||||
|
app.use(TwoslashFloatingVue)
|
||||||
|
app.component('GitHubStarBadge', GitHubStarBadge)
|
||||||
|
app.component('MarkdownRender', MarkdownRender)
|
||||||
|
app.component('SupportQRCodes', SupportQRCodes)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export useDark for use in VitePress markdown files and components
|
||||||
|
export { useDark } from './composables/useDark'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/* Custom styles for markstream-vue theme */
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
import type { BaseNode } from 'stream-markdown-parser'
|
||||||
|
import type {
|
||||||
|
CodeBlockDiffHunkActionContext,
|
||||||
|
CodeBlockMonacoOptions,
|
||||||
|
CodeBlockMonacoTheme,
|
||||||
|
CodeBlockNodeProps,
|
||||||
|
CustomComponents,
|
||||||
|
D2BlockNodeProps,
|
||||||
|
ImageNodeProps,
|
||||||
|
MermaidBlockEvent,
|
||||||
|
MermaidBlockNodeProps,
|
||||||
|
PreCodeNodeProps,
|
||||||
|
} from '../../../src/exports'
|
||||||
|
import type { NodeRendererProps } from '../../../src/types/node-renderer-props'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
BaseNode,
|
||||||
|
CodeBlockDiffHunkActionContext,
|
||||||
|
CodeBlockMonacoOptions,
|
||||||
|
CodeBlockMonacoTheme,
|
||||||
|
CodeBlockNodeProps,
|
||||||
|
CustomComponents,
|
||||||
|
D2BlockNodeProps,
|
||||||
|
ImageNodeProps,
|
||||||
|
MermaidBlockEvent,
|
||||||
|
MermaidBlockNodeProps,
|
||||||
|
NodeRendererProps,
|
||||||
|
PreCodeNodeProps,
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
CodeBlockNode,
|
||||||
|
D2BlockNode,
|
||||||
|
ImageNode,
|
||||||
|
MarkdownCodeBlockNode,
|
||||||
|
MermaidBlockNode,
|
||||||
|
PreCodeNode,
|
||||||
|
createKaTeXWorkerFromCDN,
|
||||||
|
createMermaidWorkerFromCDN,
|
||||||
|
enableD2,
|
||||||
|
enableKatex,
|
||||||
|
enableMermaid,
|
||||||
|
getMarkdown,
|
||||||
|
getUseMonaco,
|
||||||
|
parseMarkdownToStructure,
|
||||||
|
preloadExtendedLanguageIcons,
|
||||||
|
removeCustomComponents,
|
||||||
|
setCustomComponents,
|
||||||
|
setD2Loader,
|
||||||
|
setDefaultI18nMap,
|
||||||
|
setDefaultMathOptions,
|
||||||
|
setKaTeXWorker,
|
||||||
|
setKatexLoader,
|
||||||
|
setMermaidLoader,
|
||||||
|
setMermaidWorker,
|
||||||
|
} from '../../../src/exports'
|
||||||
|
|
||||||
|
export declare const MarkdownRender: DefineComponent<NodeRendererProps>
|
||||||
|
|
||||||
|
export default MarkdownRender
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# Legacy 构建与兼容性指南(Vite 与 Webpack)
|
||||||
|
|
||||||
|
目的
|
||||||
|
- 当你遇到旧浏览器(例如低版本 iOS / Safari)导致页面白屏或运行时报错的兼容性问题时,应考虑两类方案:
|
||||||
|
1. 在源码层修复(首选,必要时必须做的,比如 RegExp 语法不可能被 polyfill)
|
||||||
|
2. 在构建/部署层使用 legacy 构建(为旧浏览器生成降级 bundle + polyfills)
|
||||||
|
|
||||||
|
重要提醒(核心)
|
||||||
|
- 一些问题(例如使用正则 lookbehind `(?<!...)`)是 JS 引擎级语法问题:无法用 polyfill 在旧引擎中“添加”正则语法支持。换言之,legacy 构建能转换语法和注入 API polyfill,但不能把运行时未实现的正则语法自动转换为等价旧写法。遇到这类问题,应在源码层重写或在运行时做特性检测并 fallback(参见 `docs/IOS-regex-compat.md`)。
|
||||||
|
|
||||||
|
下面文档提供:
|
||||||
|
- Vite 的 `@vitejs/plugin-legacy` 用法与 caveats
|
||||||
|
- Webpack 下实现 modern/legacy 差异化构建的常见模式
|
||||||
|
- polyfills 管理(`core-js` / `regenerator-runtime` / 浏览器目标)
|
||||||
|
- 测试与 CI 建议
|
||||||
|
|
||||||
|
1) Vite(推荐用于 Vite 项目)
|
||||||
|
|
||||||
|
- 安装(项目根):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D @vitejs/plugin-legacy
|
||||||
|
```
|
||||||
|
|
||||||
|
- 最简单的配置(vite.config.ts)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import legacy from '@vitejs/plugin-legacy'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
legacy({
|
||||||
|
targets: ['defaults', 'iOS >= 11'],
|
||||||
|
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
|
||||||
|
// modern polyfilled bundle + legacy (nomodule) bundle -> differential serving
|
||||||
|
renderLegacyChunks: true,
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- 解释与建议:
|
||||||
|
- `targets` 使用浏览器目标来决定哪些语法/transform 需要降级;根据你需要支持的最低版本设置(例如 `iOS >= 11`)。
|
||||||
|
- `additionalLegacyPolyfills` 可以把某些运行时库插入 legacy bundle;但大多数 API polyfill(例如 `Promise` / `fetch`)应通过 `core-js` 或手动引入更精细地控制。
|
||||||
|
- `renderLegacyChunks: true` 会生成兼容旧浏览器的 chunk(按需 polyfill 注入),并配合 `<script type="module">` / `<script nomodule>` 差异化加载。
|
||||||
|
- plugin-legacy 使用 Babel 来转换语法,但**无法**把正则 lookbehind 之类的引擎级语法“改写”为等价旧写法:如果源码里有不支持的正则语法,仍可能在打包时或运行时报错(取决于是否把该正则放到运行时构造)。因此仍要在源码层修复正则语法。
|
||||||
|
|
||||||
|
- 使用建议流程:
|
||||||
|
1. 尝试用 runtime feature detection(例如 `new RegExp('(?<!a)b')`)判断是否需要改写/延迟构造正则字面量。
|
||||||
|
2. 为支持低版本平台保留 legacy 构建(plugin-legacy)以覆盖大多数语法 / API 差异。
|
||||||
|
3. 在 QA/CI 上用旧 iOS Safari(或 BrowserStack)验证构建产物。查看是否有 `SyntaxError: Invalid regular expression` 类型错误(若有,说明源码中有不兼容的正则字面量仍需改写)。
|
||||||
|
|
||||||
|
2) Webpack(适用于基于 Webpack 的项目)
|
||||||
|
|
||||||
|
Webpack 没有像 Vite 那样开箱即用的单一插件来生成 differential bundles,但常见方案是:使用 Babel 对代码做两套构建(modern + legacy),并用 `type="module"` / `nomodule` 或不同 HTML 输出来差异化服务。
|
||||||
|
|
||||||
|
- 关键依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add -D @babel/core babel-loader @babel/preset-env core-js regenerator-runtime webpack webpack-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
- 简单思路(多配置/多构建)
|
||||||
|
- 配置 A(modern):目标现代浏览器(browserslist 可设置),输出 `module` bundle
|
||||||
|
- 配置 B(legacy):目标旧浏览器,开启更激进的 `@babel/preset-env` 转换与 `useBuiltIns: 'usage'`,并注入 `core-js` polyfills
|
||||||
|
|
||||||
|
示例 `babel.config.js`(用于 legacy 构建)
|
||||||
|
|
||||||
|
```js
|
||||||
|
module.exports = function (api) {
|
||||||
|
const isLegacy = api.env('legacy')
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'@babel/preset-env',
|
||||||
|
{
|
||||||
|
targets: isLegacy ? { ie: '11', ios: '10' } : { esmodules: true },
|
||||||
|
useBuiltIns: isLegacy ? 'usage' : false,
|
||||||
|
corejs: isLegacy ? { version: 3, proposals: false } : undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 package.json 中定义两个构建命令(分别以不同的 NODE_ENV/env 来调用 babel):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"build:modern": "cross-env BABEL_ENV=modern webpack --config webpack.modern.config.js",
|
||||||
|
"build:legacy": "cross-env BABEL_ENV=legacy webpack --config webpack.legacy.config.js",
|
||||||
|
"build": "pnpm run build:modern && pnpm run build:legacy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- HTML 部署时使用 `type="module"` / `nomodule`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module" src="/assets/app.modern.js"></script>
|
||||||
|
<script nomodule src="/assets/app.legacy.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
- 注意事项:
|
||||||
|
- 两套构建会增加构建复杂度与 CI 时间;但可以确保对旧浏览器的兼容性控制更粒度。
|
||||||
|
- 同样强调:如果源码中包含引擎级不支持的正则字面量(例如 lookbehind),在构建阶段仍可能失败或在旧浏览器上报错 —— 这些必须在源码层修复或用运行时延迟构造。
|
||||||
|
|
||||||
|
3) polyfills 管理建议
|
||||||
|
- 使用 `core-js`(v3)做标准 API polyfill,结合 Babel 的 `useBuiltIns: 'usage'` 自动注入,或手动在入口文件按需 `import 'core-js/stable'`。
|
||||||
|
- 对于 async/await:`regenerator-runtime/runtime`。
|
||||||
|
- 注意不要把不必要的 polyfill注入到 modern bundle;使用 differential serving(或 plugin-legacy)确保最小现代 bundle。
|
||||||
|
|
||||||
|
4) 正则(RegExp)与引擎级语法的特殊说明
|
||||||
|
- 正则语法(例如 lookbehind)是 JS 引擎实现的语言特性,无法用 runtime polyfill 修复。解决办法:
|
||||||
|
- 在源码中改写为兼容写法(例如把 `(?<!a)b` 改为 `(^|[^a])b` 或使用字符串检查回调),或
|
||||||
|
- 把潜在会抛错的正则延迟为运行时构造(`new RegExp(...)`)并在运行时做 feature-detection(参考 `docs/IOS-regex-compat.md`),以避免模块加载解析时报错。
|
||||||
|
|
||||||
|
5) 测试与 CI / 预提交检查
|
||||||
|
- 在 CI 中加入:
|
||||||
|
- `pnpm run build`(确保 modern+legacy 构建在 CI 执行无异常)
|
||||||
|
- 旧版浏览器 smoke tests(可选):用 BrowserStack / SauceLabs 在 iOS 16.2 / Safari 等上做速测,或在 Xcode Simulator 上跑关键页面。
|
||||||
|
- 静态检测脚本:阻止新提交引入诸如 `(?<!` / `(?<=` 的字面量正则(示例脚本见下)。把脚本加入 `package.json` 并在 CI/预提交钩子里运行。
|
||||||
|
|
||||||
|
示例检查脚本(package.json -> scripts)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"check:regex-advanced": "rg --hidden --glob '!node_modules' --glob '!dist' -n --glob '**/*.{js,ts,vue,jsx,tsx,mjs,cjs}' '\\(\\?<!|\\(\\?<=|\\(\\?<[^=]|\\\\p\\{' || true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
6) 快速排错清单(当你看到旧设备白屏或控制台有错误时)
|
||||||
|
- 打开控制台,查找 `SyntaxError: Invalid regular expression` 或语法错误;如果是 RegExp 报错,优先检查源码是否含 lookbehind 等高级特性。
|
||||||
|
- 用 `rg '\\(\\?<!|\\(\\?<=|\\\\p\\{'` 在源码中搜索(排除缓存文件与 node_modules)。
|
||||||
|
- 若查明是正则语法导致:在源码中改写或延迟构造;不要仅依赖 legacy 构建来修复此类问题。
|
||||||
|
- 若是其它语法/API:确保 legacy 构建或 Babel preset-env 的 targets 包含你需要支持的版本,并把 polyfill 策略设置为 `usage`(或按需引入)。
|
||||||
|
|
||||||
|
7) 总结要点(简短)
|
||||||
|
- legacy 构建(Vite plugin-legacy / Webpack + Babel 两套构建)能解决大量语法与 API 的兼容问题,但不能修复 JS 引擎本身不支持的正则语法(如 lookbehind)。遇到这类问题,必须在源码层改写或使用运行时检测与 fallback。
|
||||||
|
- 推荐流程:优先源码兼容 -> 在构建中添加 legacy 支持 -> 在 CI/QA 上验证旧设备的真实表现 -> 在项目中加入静态检查以防回归。
|
||||||
|
|
||||||
|
附录:参考命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vite 本地打包 (含 legacy)
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Webpack 两套构建 (示例)
|
||||||
|
pnpm run build:modern
|
||||||
|
pnpm run build:legacy
|
||||||
|
|
||||||
|
# 静态检查(本地快速跑)
|
||||||
|
pnpm dlx rg "\\(\\?<!|\\(\\?<=|\\\\p\\{" --glob "**/*.{js,ts,vue,jsx,tsx,mjs,cjs}" -n
|
||||||
|
```
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# E2E Testing Report
|
||||||
|
|
||||||
|
> 中文版请查看 [E2E 测试报告(中文)](/zh/guide/e2e-testing-report)。
|
||||||
|
|
||||||
|
This document summarizes the end-to-end markdown parsing tests added to this repository.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Provide a small, reproducible E2E test harness that validates the parser and renderer handle common markdown constructs and to serve as a base for future coverage expansion.
|
||||||
|
|
||||||
|
## What was added
|
||||||
|
|
||||||
|
- `test/fixtures/` — directory with representative markdown fixtures:
|
||||||
|
- `headings.md`
|
||||||
|
- `code-diff.md`
|
||||||
|
- `table.md`
|
||||||
|
- `admonition.md`
|
||||||
|
- `math.md`
|
||||||
|
- `footnotes.md`
|
||||||
|
- `image-link.md`
|
||||||
|
- `checkbox.md`
|
||||||
|
- `mermaid.md`
|
||||||
|
|
||||||
|
- `test/e2e.markdown.test.ts` — a Vitest test that:
|
||||||
|
- reads each fixture
|
||||||
|
- parses it via `parseMarkdownToStructure` (using `getMarkdown` instance)
|
||||||
|
- serializes a minimal stable structure and asserts a snapshot per fixture
|
||||||
|
- includes a small `renderMarkdown` assertion for inline math/code block rendering
|
||||||
|
|
||||||
|
- Snapshots — generated by Vitest (placed in the default snapshot location).
|
||||||
|
|
||||||
|
- `.github/workflows/ci.yml` — GitHub Actions workflow that runs on `push` and `pull_request` to:
|
||||||
|
- install dependencies
|
||||||
|
- run `pnpm test`
|
||||||
|
- run `pnpm typecheck`
|
||||||
|
- run `pnpm lint`
|
||||||
|
|
||||||
|
## How to run locally
|
||||||
|
|
||||||
|
Run tests (and update snapshots if you intentionally changed output):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test -- -u
|
||||||
|
```
|
||||||
|
|
||||||
|
Run tests without updating snapshots:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Typecheck and lint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
pnpm lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quality gates (local validation)
|
||||||
|
|
||||||
|
- Tests: PASS (73 passed locally)
|
||||||
|
- Typecheck: PASS
|
||||||
|
- Lint: PASS
|
||||||
|
|
||||||
|
## Recommendations / Next steps
|
||||||
|
|
||||||
|
1. Convert snapshots to include more detailed fields in `minimal` serialization (for example, include `children` text snippets or node `meta`), to catch regressions in node payloads.
|
||||||
|
2. Add fixtures for edge cases:
|
||||||
|
- Unclosed fences
|
||||||
|
- Complex nested lists with markup inside
|
||||||
|
- Escaped bracket edge-cases
|
||||||
|
- Streaming fence edgecases
|
||||||
|
3. Add integration tests for rendering plugins that require DOM or workers (Mermaid, KaTeX worker, Monaco). Consider adding a browser-based test job (Playwright) for those.
|
||||||
|
4. Configure CI to run snapshots or a visual regression step if the renderer's HTML output becomes more important.
|
||||||
|
|
||||||
|
If you want, I can:
|
||||||
|
1. Expand snapshots to include richer metadata (children snippets, meta fields).
|
||||||
|
2. Add the proposed edge-case fixtures and assertions.
|
||||||
|
3. Draft a Playwright-based CI job for DOM-heavy plugins.
|
||||||
|
|
||||||
|
Let me know which option you’d prefer next。
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: E2E 测试报告(中文)已迁移
|
||||||
|
search: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# E2E 测试报告(中文)已迁移
|
||||||
|
|
||||||
|
该文档现在位于 `/zh/guide/e2e-testing-report`。
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if (typeof window !== 'undefined')
|
||||||
|
window.location.replace('/zh/guide/e2e-testing-report')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
页面已迁移,请访问 <a href="/zh/guide/e2e-testing-report">/zh/guide/e2e-testing-report</a>。
|
||||||
|
</noscript>
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# Advanced parser hooks & low-level customization
|
||||||
|
|
||||||
|
This page is for low-level parser work: token transforms, AST reshaping, and custom node pipelines.
|
||||||
|
|
||||||
|
If you only need one of these common tasks, start elsewhere first:
|
||||||
|
|
||||||
|
- Replace a built-in renderer such as `image`, `code_block`, or `mermaid`: [Override Built-in Components](/guide/component-overrides)
|
||||||
|
- Support trusted tags such as `thinking`: [Custom Tags & Advanced Components](/guide/custom-components)
|
||||||
|
- Tune behavior without replacing renderers: [Props & Options](/guide/props)
|
||||||
|
|
||||||
|
## parseOptions
|
||||||
|
`parseOptions` can be passed to `MarkdownRender` or used directly with `parseMarkdownToStructure`.
|
||||||
|
|
||||||
|
- `preTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — mutate tokens immediately after the `markdown-it` parse
|
||||||
|
- `postTransformTokens?: (tokens: MarkdownToken[]) => MarkdownToken[]` — further token transforms
|
||||||
|
|
||||||
|
If you need to reshape the AST, post-process the returned `ParsedNode[]` and pass it to `MarkdownRender` via the `nodes` prop.
|
||||||
|
|
||||||
|
### Example: custom HTML-like tags (recommended)
|
||||||
|
For simple custom tags like `<thinking>...</thinking>`, you no longer need to normalize the source or rewrite tokens. Just opt the tag into the allowlist and register a component:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
import ThinkingNode from './ThinkingNode.vue'
|
||||||
|
|
||||||
|
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MarkdownRender
|
||||||
|
custom-id="docs"
|
||||||
|
:custom-html-tags="['thinking']"
|
||||||
|
:content="markdown"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
When `custom-html-tags` includes a tag name, the parser:
|
||||||
|
- suppresses streaming mid‑states until `<tag ...>` is complete,
|
||||||
|
- emits a `CustomComponentNode` with `type: 'thinking'` (the tag name, no angle brackets), plus `content`, optional `attrs`, and `loading/autoClosed` flags.
|
||||||
|
|
||||||
|
## Custom component parsing
|
||||||
|
|
||||||
|
The built‑in custom tag pipeline above handles most “component‑like” tags (inline or block).
|
||||||
|
Hooks are still useful when you need to reshape the node — for example, to strip wrappers, merge adjacent blocks, or map attributes:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||||
|
|
||||||
|
function preTransformTokens(tokens: MarkdownToken[]) {
|
||||||
|
return tokens.map((t) => {
|
||||||
|
if (t.type === 'html_block' && t.tag === 'thinking')
|
||||||
|
return { ...t, content: String(t.content ?? '').replace(/<\/?thinking[^>]*>/g, '').trim() }
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const md = getMarkdown()
|
||||||
|
const nodes = parseMarkdownToStructure(markdown, md, { preTransformTokens })
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternative flows (pick what fits your pipeline):
|
||||||
|
- Have the backend split `thinking` into its own field/type; render `thinking` with one `MarkdownRender` and the remaining content with another, so the parser never sees raw custom HTML inline.
|
||||||
|
- Replace custom blocks with placeholders before parsing: capture `<thinking>...</thinking>` via regex, render the cleaned body with `MarkdownRender`, then swap placeholders back to your custom component. When `thinking` is always at the top, you can also slice the head section out for dedicated rendering.
|
||||||
|
|
||||||
|
## setCustomComponents(id, mapping)
|
||||||
|
- Use `setCustomComponents('docs', { thinking: ThinkingComponent })` to scope to `MarkdownRender` instances with `custom-id="docs"`.
|
||||||
|
- Call `removeCustomComponents` to clean up mappings and avoid memory leaks in single-page apps.
|
||||||
|
|
||||||
|
## Scoped example
|
||||||
|
```vue
|
||||||
|
<MarkdownRender content="..." custom-id="docs" />
|
||||||
|
// In setup
|
||||||
|
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||||
|
```
|
||||||
|
|
||||||
|
Advanced hooks are a powerful way to add domain-specific grammar to Markdown without changing the core parser.
|
||||||
|
|
||||||
|
### Typewriter prop
|
||||||
|
|
||||||
|
`MarkdownRender` accepts a `typewriter` boolean prop which controls whether non-`code_block` nodes use subtle fade treatment. New nodes still get the small enter transition, and when streamed text grows in place the newly appended text fragment replays a short fade instead of dimming the whole node container. This is useful for demo UIs but may be undesirable in SSR or print/export flows where deterministic output is needed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<MarkdownRender :content="markdown" :typewriter="false" />
|
||||||
|
```
|
||||||
|
|
||||||
|
CSS variables: `--typewriter-fade-duration` and `--typewriter-fade-ease` tune the initial enter transition; `--stream-update-fade-duration` and `--stream-update-fade-ease` tune the short replayed fade used for newly appended streamed text. When the stream-specific variables are not set, the appended-text fade falls back to the same duration and easing as `typewriter`.
|
||||||
|
|
||||||
|
## Internationalization (i18n)
|
||||||
|
|
||||||
|
By default, `getMarkdown` uses English text for UI elements (e.g., "Copy" button in code blocks). You can customize these texts by providing an `i18n` option:
|
||||||
|
|
||||||
|
**Using a translation map:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getMarkdown } from 'markstream-vue'
|
||||||
|
|
||||||
|
const md = getMarkdown('editor-1', {
|
||||||
|
i18n: {
|
||||||
|
'common.copy': '复制',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using a translation function:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getMarkdown } from 'markstream-vue'
|
||||||
|
import { useI18n } from 'vue-i18n' // or any i18n library
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const md = getMarkdown('editor-1', {
|
||||||
|
i18n: (key: string) => t(key)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Overriding default texts globally:**
|
||||||
|
|
||||||
|
If you just want to change some default UI texts (e.g., "Copy" → "复制") without setting up full i18n, use `setDefaultI18nMap` to replace the fallback translations:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { setDefaultI18nMap } from 'markstream-vue'
|
||||||
|
|
||||||
|
setDefaultI18nMap({
|
||||||
|
'common.copy': '复制',
|
||||||
|
'common.copied': '已复制',
|
||||||
|
'common.decrease': '减少',
|
||||||
|
'common.reset': '重置',
|
||||||
|
'common.increase': '增加',
|
||||||
|
'common.expand': '展开',
|
||||||
|
'common.collapse': '收起',
|
||||||
|
'common.preview': '预览',
|
||||||
|
'common.source': '源码',
|
||||||
|
'common.export': '导出',
|
||||||
|
'common.open': '打开',
|
||||||
|
'common.zoomIn': '放大',
|
||||||
|
'common.zoomOut': '缩小',
|
||||||
|
'common.resetZoom': '重置缩放',
|
||||||
|
'image.loadError': '图片加载失败',
|
||||||
|
'image.loading': '图片加载中...',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful when:
|
||||||
|
- You don't need language switching but prefer different wording
|
||||||
|
- You want Chinese (or other language) UI by default
|
||||||
|
- You're not using `vue-i18n` but still want custom UI texts
|
||||||
|
|
||||||
|
**Default translations:**
|
||||||
|
|
||||||
|
- `common.copy`: "Copy" — Used in code block copy buttons
|
||||||
|
- `common.copied`: "Copied" — Shown after copying
|
||||||
|
- `image.loadError`: "Image failed to load" — Image fallback text
|
||||||
|
- `image.loading`: "Loading image..." — Shown while loading
|
||||||
|
|
||||||
|
This design keeps the markdown utilities pure and free from global side effects, allowing you to integrate with any i18n solution or provide static translations.
|
||||||
|
|
||||||
|
Try this — minimal example using parseOptions and a custom component registration:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getMarkdown, parseMarkdownToStructure, setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
const md = getMarkdown()
|
||||||
|
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||||
|
const nodes = parseMarkdownToStructure('[[CUSTOM:1]]', md)
|
||||||
|
```
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
description: Build AI chat and streaming Markdown UIs with nodes plus final patterns, performance guidance, trusted custom tags, and SSR-safe setup.
|
||||||
|
---
|
||||||
|
|
||||||
|
# AI Chat & Streaming
|
||||||
|
|
||||||
|
Use this path when you are building a chat UI, token stream, SSE response viewer, or any screen where Markdown updates frequently while the user is watching.
|
||||||
|
|
||||||
|
If you only render static articles or docs pages, go back to [Usage & Streaming](/guide/usage) and prefer the simpler `content` path.
|
||||||
|
|
||||||
|
## 1. Choose the leanest install
|
||||||
|
|
||||||
|
| Need | Packages | Best for |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Text-only or lightweight chat UI | `markstream-vue` | Basic Markdown, lists, links, blockquotes |
|
||||||
|
| Syntax-highlighted code without Monaco | `markstream-vue stream-markdown` | SSR-friendly transcripts, lower bundle budgets |
|
||||||
|
| Rich code interactions | `markstream-vue stream-monaco` | Copy, preview, diff, and Monaco-powered code blocks |
|
||||||
|
| Diagrams or math in chat output | `markstream-vue mermaid katex` | Mermaid fences and KaTeX formulas |
|
||||||
|
|
||||||
|
Install only the peers you actually expect to show up in responses.
|
||||||
|
|
||||||
|
## 2. Recommended data flow
|
||||||
|
|
||||||
|
For frequent updates, keep parsing outside `MarkdownRender` and pass `nodes` + `final`.
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender, { getMarkdown, parseMarkdownToStructure } from 'markstream-vue'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const streamedText = ref('')
|
||||||
|
const final = ref(false)
|
||||||
|
const md = getMarkdown('chat-message')
|
||||||
|
|
||||||
|
const nodes = computed(() =>
|
||||||
|
parseMarkdownToStructure(streamedText.value, md, { final: final.value }),
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender
|
||||||
|
custom-id="chat"
|
||||||
|
:nodes="nodes"
|
||||||
|
:final="final"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Why this path works better:
|
||||||
|
|
||||||
|
- `MarkdownRender` does not need to reparse the full string on every tiny token update.
|
||||||
|
- You can move parsing into a store, worker, or message pipeline later without changing the renderer contract.
|
||||||
|
- `custom-id="chat"` gives you a scoped place to theme the chat surface or override one renderer safely.
|
||||||
|
|
||||||
|
## 3. Renderer settings that usually work well
|
||||||
|
|
||||||
|
- Keep the default virtualization behavior for long transcripts. Only tune `maxLiveNodes` if you have a measured reason.
|
||||||
|
- Use `renderCodeBlocksAsPre` when code fences appear often but Monaco is too heavy for your chat surface.
|
||||||
|
- Leave heavy peers off until you need them. Chat UIs get a large win from not shipping Mermaid, KaTeX, or Monaco by default.
|
||||||
|
- If you disable virtualization (`:max-live-nodes="0"`), then the batching props in [Props & Options](/guide/props) become more important.
|
||||||
|
|
||||||
|
## 4. Common upgrade paths
|
||||||
|
|
||||||
|
### Better code blocks
|
||||||
|
|
||||||
|
- Want a lighter docs-style look: use `MarkdownCodeBlockNode` with `stream-markdown`
|
||||||
|
- Want richer preview/diff controls: use `CodeBlockNode` with `stream-monaco`
|
||||||
|
|
||||||
|
See [Renderer & Node Components](/guide/components) for the trade-offs.
|
||||||
|
|
||||||
|
### Trusted tags such as `thinking`
|
||||||
|
|
||||||
|
Use `custom-html-tags` plus `setCustomComponents('chat', mapping)` so custom tags only affect the chat surface.
|
||||||
|
|
||||||
|
See [Custom Tags & Advanced Components](/guide/custom-components).
|
||||||
|
|
||||||
|
### Scoped overrides for one message surface
|
||||||
|
|
||||||
|
Use `setCustomComponents('chat', { image: ChatImageNode })` and render with `custom-id="chat"`.
|
||||||
|
|
||||||
|
See [Override Built-in Components](/guide/component-overrides).
|
||||||
|
|
||||||
|
## 5. CSS and SSR checklist
|
||||||
|
|
||||||
|
- Load your reset first, then import `markstream-vue/index.css` inside `@layer components`.
|
||||||
|
- Import `katex/dist/katex.min.css` only if math is enabled.
|
||||||
|
- Gate browser-only peers such as Mermaid, D2, or Monaco behind client-only boundaries in SSR setups.
|
||||||
|
- If styles leak, scope your chat tweaks under `[data-custom-id="chat"]`.
|
||||||
|
|
||||||
|
Start here when visuals look wrong: [Troubleshooting](/guide/troubleshooting#css-looks-wrong-start-here)
|
||||||
|
|
||||||
|
## 6. When not to use this path
|
||||||
|
|
||||||
|
- Use `content` when updates are infrequent or the page is basically static.
|
||||||
|
- Use server-side preparse + `nodes` when another layer already owns Markdown parsing.
|
||||||
|
- Use the framework-specific guides when SSR/runtime rules matter more than streaming itself.
|
||||||
|
|
||||||
|
## Next pages
|
||||||
|
|
||||||
|
- [Installation](/guide/installation) for peer decisions
|
||||||
|
- [Usage & Streaming](/guide/usage) for `content` vs `nodes`
|
||||||
|
- [Performance](/guide/performance) for larger transcripts
|
||||||
|
- [Renderer & Node Components](/guide/components) for code/math/diagram component choices
|
||||||
|
- [Troubleshooting](/guide/troubleshooting) when CSS, peers, or SSR behave unexpectedly
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# AI / Skills Workflows
|
||||||
|
|
||||||
|
If you use Codex, Cursor, Claude Code, ChatGPT, or another coding assistant to adopt markstream, the fastest way to get useful output is to give the tool the right constraints up front.
|
||||||
|
|
||||||
|
This page is intentionally practical: copyable prompts, rollout checklists, and what a reusable markstream-focused skill should cover.
|
||||||
|
|
||||||
|
## Start with your goal
|
||||||
|
|
||||||
|
- Want packaged skills you can install right now: jump to [Install the packaged skills](#install-the-packaged-skills)
|
||||||
|
- Want ready-made prompt templates without cloning the repo: jump to [Copyable prompts](#2-copyable-prompts)
|
||||||
|
- Want to migrate from another renderer with an assistant: start with [Copyable prompts](#2-copyable-prompts) and the rollout pattern below
|
||||||
|
- Want to version reusable AI assets inside your own repo: read [Repository assets](#repository-assets)
|
||||||
|
|
||||||
|
## Install the packaged skills
|
||||||
|
|
||||||
|
For end users, the recommended command is the shared `skills` installer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills add Simon-He95/markstream-vue
|
||||||
|
```
|
||||||
|
|
||||||
|
That path works because the repository now publishes reusable skills under `.agents/skills`, which `npx skills` can discover directly from GitHub.
|
||||||
|
|
||||||
|
The Vercel `skills` installer also supports multiple source formats:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GitHub shorthand (owner/repo)
|
||||||
|
npx skills add Simon-He95/markstream-vue
|
||||||
|
|
||||||
|
# Full GitHub URL
|
||||||
|
npx skills add https://github.com/Simon-He95/markstream-vue
|
||||||
|
|
||||||
|
# Direct path to one skill in this repo
|
||||||
|
npx skills add https://github.com/Simon-He95/markstream-vue/tree/main/.agents/skills/markstream-install
|
||||||
|
|
||||||
|
# Any git URL
|
||||||
|
npx skills add git@github.com:Simon-He95/markstream-vue.git
|
||||||
|
```
|
||||||
|
|
||||||
|
If you specifically want the package CLI, you can still use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx markstream-vue skills install
|
||||||
|
# or
|
||||||
|
pnpm dlx markstream-vue skills install
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful variants:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx markstream-vue skills list
|
||||||
|
npx markstream-vue skills install --target codex
|
||||||
|
npx markstream-vue skills install --target ./tmp/skills-test --mode copy --force
|
||||||
|
```
|
||||||
|
|
||||||
|
- `npx skills add Simon-He95/markstream-vue` is the primary recommendation for Codex-compatible skill installation
|
||||||
|
- `skills install` defaults to `copy`, which is safer for `npx` and `dlx` usage
|
||||||
|
- the default target is `~/.agents/skills`
|
||||||
|
- `--target codex` targets `${CODEX_HOME:-~/.codex}/skills`
|
||||||
|
- `--target` accepts either `agents`, `codex`, or any custom path
|
||||||
|
- `--force` replaces existing installs at the target path
|
||||||
|
|
||||||
|
The prompts stay in the repository under `prompts/`; the packaged installer only handles the skill folders.
|
||||||
|
|
||||||
|
The same CLI can also expose bundled prompt templates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx markstream-vue prompts list
|
||||||
|
npx markstream-vue prompts show install-markstream
|
||||||
|
```
|
||||||
|
|
||||||
|
That is usually the easiest way for npm users to discover the maintained prompt set without cloning the repository.
|
||||||
|
|
||||||
|
## Repository assets
|
||||||
|
|
||||||
|
This repository now includes reusable assets you can version with the codebase:
|
||||||
|
|
||||||
|
- Task skills: `markstream-install`, `markstream-custom-components`, `markstream-migration`
|
||||||
|
- Framework entry skills: `markstream-vue`, `markstream-nuxt`, `markstream-vue2`, `markstream-vue2-cli`, `markstream-vue2-vite`, `markstream-react`, `markstream-angular`
|
||||||
|
- `prompts/install-markstream.md`
|
||||||
|
- `prompts/override-built-in-components.md`
|
||||||
|
- `prompts/add-custom-tag-thinking.md`
|
||||||
|
- `prompts/migrate-react-markdown.md`
|
||||||
|
- `prompts/audit-markstream-integration.md`
|
||||||
|
|
||||||
|
Use `.agents/skills/` when you want reusable Codex workflow assets that GitHub-based installers can discover automatically. The task skills cover cross-framework work; the framework entry skills make Vue 3, Nuxt, Vue 2, Vue 2 CLI, Vue 2 Vite, React, and Angular scenarios directly discoverable. Use `prompts/` when you want copyable starting prompts for humans or other assistants.
|
||||||
|
|
||||||
|
## 1. Give the AI these five facts first
|
||||||
|
|
||||||
|
Before you ask for code changes, include:
|
||||||
|
|
||||||
|
- framework and version, for example Vue 3, Nuxt 3, React 18, or Angular 20
|
||||||
|
- CSS stack, for example Tailwind, UnoCSS, reset libraries, or a design system
|
||||||
|
- rendering mode: static article, docs site, SSR, or streaming chat
|
||||||
|
- required peers: Monaco, Mermaid, D2, KaTeX, or Shiki
|
||||||
|
- whether overrides must stay scoped to one area of the app
|
||||||
|
|
||||||
|
Without that context, assistants often install the wrong peers or place CSS in the wrong order.
|
||||||
|
|
||||||
|
## 2. Copyable prompts
|
||||||
|
|
||||||
|
### Install markstream in an existing Vue 3 app
|
||||||
|
|
||||||
|
```text
|
||||||
|
Install markstream-vue into this Vue 3 app.
|
||||||
|
Use the smallest peer-dependency set that matches these needs: [fill in Monaco / Mermaid / D2 / KaTeX / Shiki].
|
||||||
|
Keep CSS order safe with my existing stack: [fill in Tailwind / UnoCSS / reset library].
|
||||||
|
Add one minimal render example, and explain whether I should use `content` or `nodes`.
|
||||||
|
If you replace or override anything, keep it scoped with `custom-id`.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migrate from `react-markdown`
|
||||||
|
|
||||||
|
```text
|
||||||
|
Migrate this React codebase from react-markdown to markstream-react.
|
||||||
|
Audit which remark/rehype behaviors have no direct 1:1 equivalent.
|
||||||
|
Replace simple renderer overrides first, then call out anything that needs custom nodes, customHtmlTags, or parser/node post-processing.
|
||||||
|
Preserve existing user-visible behavior where practical.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add a custom tag such as `thinking`
|
||||||
|
|
||||||
|
```text
|
||||||
|
Add support for a trusted custom Markdown tag named `thinking`.
|
||||||
|
Use `customHtmlTags` plus a scoped `setCustomComponents` mapping.
|
||||||
|
Render nested Markdown inside the custom component, and keep the implementation streaming-friendly.
|
||||||
|
Do not use global overrides unless there is a clear reason.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug styling or SSR issues
|
||||||
|
|
||||||
|
```text
|
||||||
|
Audit this markstream integration for CSS order, reset conflicts, optional peers, and SSR-only problems.
|
||||||
|
Check whether `markstream-vue/index.css` is loaded in the correct layer, whether KaTeX CSS is missing, and whether browser-only peers are gated behind client-only boundaries.
|
||||||
|
Explain the smallest fix set.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. What a reusable markstream skill should do
|
||||||
|
|
||||||
|
If you are building a reusable prompt, template, or coding skill around markstream, it should reliably cover these steps:
|
||||||
|
|
||||||
|
- detect the framework and package manager
|
||||||
|
- install only the required peers for the requested features
|
||||||
|
- place CSS after resets and inside `@layer components` when utility frameworks are involved
|
||||||
|
- choose `content` for simple rendering and `nodes` plus `final` for streaming
|
||||||
|
- use `custom-id` for overrides by default
|
||||||
|
- point the user to the right docs page after making changes
|
||||||
|
|
||||||
|
Good follow-up links for a skill to cite:
|
||||||
|
|
||||||
|
- [Installation](/guide/installation)
|
||||||
|
- [Usage & Streaming](/guide/usage)
|
||||||
|
- [Override Built-in Components](/guide/component-overrides)
|
||||||
|
- [Custom Tags & Advanced Components](/guide/custom-components)
|
||||||
|
- [Troubleshooting](/guide/troubleshooting)
|
||||||
|
|
||||||
|
## 4. Point AI agents at the agent context file
|
||||||
|
|
||||||
|
For repository-aware assistants, also point them to:
|
||||||
|
|
||||||
|
- [AI / LLM context](/llms)
|
||||||
|
|
||||||
|
That file is agent-facing. This page is human-facing. In practice, the best results come from giving the assistant both:
|
||||||
|
|
||||||
|
- the task-specific prompt from this page
|
||||||
|
- the repository context file from `/llms`
|
||||||
|
|
||||||
|
## 5. A rollout pattern that works well
|
||||||
|
|
||||||
|
When the task is bigger than a one-file demo, ask the assistant to work in this order:
|
||||||
|
|
||||||
|
1. install and verify peers
|
||||||
|
2. make the base renderer work
|
||||||
|
3. fix CSS order
|
||||||
|
4. add scoped overrides or custom tags
|
||||||
|
5. run tests or docs build
|
||||||
|
|
||||||
|
That sequence prevents most “it compiled but looks wrong” outcomes.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Angular Installation
|
||||||
|
|
||||||
|
Install `markstream-angular` into your Angular 20+ project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add markstream-angular @angular/core @angular/common
|
||||||
|
```
|
||||||
|
|
||||||
|
## Required CSS
|
||||||
|
|
||||||
|
Import the renderer stylesheet once in your app entry:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import 'markstream-angular/index.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
If you enable math rendering, also import:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional Peer Dependencies
|
||||||
|
|
||||||
|
Install only the heavy features you need:
|
||||||
|
|
||||||
|
| Feature | Package |
|
||||||
|
| --- | --- |
|
||||||
|
| Monaco code blocks | `stream-monaco` |
|
||||||
|
| Mermaid diagrams | `mermaid` |
|
||||||
|
| KaTeX math | `katex` |
|
||||||
|
| D2 diagrams | `@terrastruct/d2` |
|
||||||
|
| AntV infographic blocks | `@antv/infographic` |
|
||||||
|
|
||||||
|
Quick install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add stream-monaco mermaid katex @terrastruct/d2 @antv/infographic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional Workers
|
||||||
|
|
||||||
|
`markstream-angular` exports the same worker entry points used by the React / Vue integrations:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { setKaTeXWorker, setMermaidWorker } from 'markstream-angular'
|
||||||
|
import KatexWorker from 'markstream-angular/workers/katexRenderer.worker?worker'
|
||||||
|
import MermaidWorker from 'markstream-angular/workers/mermaidParser.worker?worker'
|
||||||
|
|
||||||
|
setKaTeXWorker(new KatexWorker())
|
||||||
|
setMermaidWorker(new MermaidWorker())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Playground
|
||||||
|
|
||||||
|
Inside this monorepo, the Angular playground routes are:
|
||||||
|
|
||||||
|
- `/` for the main streaming demo
|
||||||
|
- `/test` for the regression lab
|
||||||
|
- `/test-sandbox` for isolated framework/version rendering
|
||||||
|
|
||||||
|
Run it locally with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm play:angular
|
||||||
|
```
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Angular Quick Start
|
||||||
|
|
||||||
|
Get `markstream-angular` rendering markdown in a standalone Angular app.
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Component, signal } from '@angular/core'
|
||||||
|
import { bootstrapApplication } from '@angular/platform-browser'
|
||||||
|
import { MarkstreamAngularComponent } from 'markstream-angular'
|
||||||
|
import 'markstream-angular/index.css'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MarkstreamAngularComponent],
|
||||||
|
template: `
|
||||||
|
<markstream-angular
|
||||||
|
[content]="markdown()"
|
||||||
|
[final]="true"
|
||||||
|
[codeBlockStream]="true"
|
||||||
|
/>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class AppComponent {
|
||||||
|
readonly markdown = signal(`# Hello Angular
|
||||||
|
|
||||||
|
This is **markstream-angular**.
|
||||||
|
|
||||||
|
\`\`\`ts
|
||||||
|
export function greet(name: string) {
|
||||||
|
return \`hello \${name}\`
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
\`\`\`mermaid
|
||||||
|
flowchart LR
|
||||||
|
Input --> Parser --> Renderer
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
$E = mc^2$
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapApplication(AppComponent)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Streaming Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Component, signal } from '@angular/core'
|
||||||
|
import { MarkstreamAngularComponent } from 'markstream-angular'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-streaming-demo',
|
||||||
|
standalone: true,
|
||||||
|
imports: [MarkstreamAngularComponent],
|
||||||
|
template: `
|
||||||
|
<markstream-angular
|
||||||
|
[content]="content()"
|
||||||
|
[final]="done()"
|
||||||
|
[codeBlockStream]="true"
|
||||||
|
[batchRendering]="true"
|
||||||
|
[typewriter]="true"
|
||||||
|
/>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class StreamingDemoComponent {
|
||||||
|
readonly content = signal('# Partial output')
|
||||||
|
readonly done = signal(false)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Props
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { CodeBlockMonacoOptions, NodeRendererProps } from 'markstream-angular'
|
||||||
|
|
||||||
|
const monacoOptions: CodeBlockMonacoOptions = {
|
||||||
|
fontSize: 13,
|
||||||
|
wordWrap: 'on',
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: NodeRendererProps = {
|
||||||
|
content: '# Angular',
|
||||||
|
codeBlockMonacoOptions: monacoOptions,
|
||||||
|
final: true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Tags
|
||||||
|
|
||||||
|
The Angular package supports the same custom HTML tag flow used in the playground `thinking` demos:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<markstream-angular
|
||||||
|
[content]="markdown()"
|
||||||
|
[customHtmlTags]="['thinking']"
|
||||||
|
[customComponents]="customComponents"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Angular Installation](/guide/angular-installation)
|
||||||
|
- [Guide Index](/guide/)
|
||||||
|
- [Playground Guide](/guide/playground)
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
---
|
||||||
|
description: Reference the low-level markstream-vue API for parser helpers, render-pipeline decisions, scoped overrides, and AST-aware integration points.
|
||||||
|
---
|
||||||
|
|
||||||
|
# API Reference
|
||||||
|
|
||||||
|
This page covers the low-level entry points behind `markstream-vue`: parser helpers, render-pipeline decisions, and scoping hooks.
|
||||||
|
|
||||||
|
If you are trying to look up exported Vue components such as `MarkdownRender`, `CodeBlockNode`, or `ImageNode`, use [Renderer & Node Components](/guide/components) instead. Pair this page with [Usage](/guide/usage) and [Props](/guide/props) when wiring everything together.
|
||||||
|
|
||||||
|
## Render pipeline at a glance
|
||||||
|
|
||||||
|
```
|
||||||
|
Markdown string → getMarkdown() → markdown-it-ts instance
|
||||||
|
↓
|
||||||
|
parseMarkdownToStructure(content, md) → AST (BaseNode[])
|
||||||
|
↓
|
||||||
|
<MarkdownRender> → node components (CodeBlockNode, ImageNode, …)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can jump in at any stage:
|
||||||
|
- Provide `content` to let the component handle parsing automatically.
|
||||||
|
- Provide `nodes` when you need full control over the AST (server-side parsing, custom transforms).
|
||||||
|
|
||||||
|
## Parser helpers
|
||||||
|
|
||||||
|
| Helper | Purpose | When to use |
|
||||||
|
| ------ | ------- | ----------- |
|
||||||
|
| `getMarkdown(msgId?, options?)` | Returns a configured `markdown-it-ts` instance with the plugins this package expects. | Customize parser options (HTML toggles, additional plugins) before transforming tokens. |
|
||||||
|
| `parseMarkdownToStructure(content, md)` | Generates the AST consumed by `MarkdownRender` from a Markdown string. | Pre-parse on the server, run validations, or reuse the AST across renders. |
|
||||||
|
|
||||||
|
Both helpers are framework-agnostic and can run in Node or the browser. For large documents you can reuse the `md` instance between parses to avoid re-initializing plugins.
|
||||||
|
|
||||||
|
## Custom components & scoping
|
||||||
|
|
||||||
|
Use `setCustomComponents(customId?, mapping)` to override any node renderer. Pair it with the `custom-id` prop on `MarkdownRender` so replacements stay scoped.
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
declare const CustomImageNode: Component
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
image: CustomImageNode,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
const md = ''
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender custom-id="docs" :content="md" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
- Use descriptive IDs (`docs`, `playground`, `pdf-export`) for tracing.
|
||||||
|
- Call `setCustomComponents(mapping)` to set globals, but prefer scoped IDs to avoid surprises in multi-instance apps.
|
||||||
|
- Clean up mappings in SPA routers if you register them dynamically.
|
||||||
|
|
||||||
|
## Parse hooks & transforms
|
||||||
|
|
||||||
|
When passing `content`, you can intercept parser stages through `parse-options` (prop) or the `ParseOptions` parameter of `parseMarkdownToStructure`.
|
||||||
|
|
||||||
|
Hooks:
|
||||||
|
- `preTransformTokens(tokens)` — mutate tokens before default handling.
|
||||||
|
- `postTransformTokens(tokens)` — inspect/adjust tokens before node generation.
|
||||||
|
|
||||||
|
If you need to reshape the AST, post-process the returned nodes and pass them to `MarkdownRender` via `nodes`.
|
||||||
|
|
||||||
|
Example: render AI “thinking” tags as custom components (no hooks needed):
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
declare const ThinkingNode: Component
|
||||||
|
|
||||||
|
setCustomComponents('docs', { thinking: ThinkingNode })
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
const doc = '<thinking>Need a plan</thinking>'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender
|
||||||
|
custom-id="docs"
|
||||||
|
:custom-html-tags="['thinking']"
|
||||||
|
:content="doc"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Hooks remain useful if you want to reshape the emitted `thinking` node (strip wrappers, remap attrs, merge blocks, etc.), or post-process the parsed nodes before rendering.
|
||||||
|
|
||||||
|
## Utility exports
|
||||||
|
|
||||||
|
Besides the core renderer and parser helpers, the package exposes:
|
||||||
|
|
||||||
|
- `CodeBlockNode`, `MarkdownCodeBlockNode`, `MermaidBlockNode`, `MathBlockNode`, `ImageNode`, etc. — see [Components](/guide/components) for their props and CSS requirements.
|
||||||
|
- `VueRendererMarkdown` (global component plugin) and shared type exports (component prop interfaces, parser types).
|
||||||
|
|
||||||
|
For parser types and hooks, see [/guide/parser-api](/guide/parser-api) (or the `stream-markdown-parser` README on npm).
|
||||||
|
|
||||||
|
## Styling + troubleshooting reminders
|
||||||
|
|
||||||
|
- Always include a reset before `markstream-vue/index.css` and wrap it with `@layer components` when using Tailwind or UnoCSS. See the [Tailwind guide](/guide/tailwind) and the [CSS checklist](/guide/troubleshooting#css-looks-wrong-start-here).
|
||||||
|
- Code/graph peers: KaTeX needs its own CSS import; Mermaid does not. Missing KaTeX styles often manifest as invisible formulas.
|
||||||
|
- Use `custom-id` to scope overrides and avoid global selector conflicts.
|
||||||
|
|
||||||
|
Need more examples? Jump into the [Playground](/guide/playground) or run `pnpm play` locally to experiment with custom parsers and renderers.
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
# CodeBlockNode (Component)
|
||||||
|
|
||||||
|
`CodeBlockNode` 是库中用于渲染富交互代码块的组件。对于需要编辑/高亮/增量渲染的场景,推荐安装 `stream-monaco`。组件为头部提供灵活的自定义点(props + slots),并在常见场景下提供可拦截的事件。
|
||||||
|
|
||||||
|
## Quick summary
|
||||||
|
- Monaco mode (install `stream-monaco`) — editor-like rendering with workers
|
||||||
|
- Fallback — plain `<pre><code>` when `stream-monaco` is not installed
|
||||||
|
- If you want Shiki-based highlighting (no Monaco), use `MarkdownCodeBlockNode` (peer: `stream-markdown`)
|
||||||
|
|
||||||
|
## Props
|
||||||
|
Refer to `src/types/component-props.ts` for full signature. Key props:
|
||||||
|
- `node` — code_block node (required)
|
||||||
|
- `loading`, `stream`, `isShowPreview`
|
||||||
|
- `monacoOptions` — typed as `CodeBlockMonacoOptions` and forwarded to `stream-monaco`
|
||||||
|
- diff options such as `diffHideUnchangedRegions`, `diffLineStyle`, `diffAppearance`, `diffUnchangedRegionStyle`, `diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, and `onDiffHunkAction` belong here
|
||||||
|
- Header controls: `showHeader`, `showCollapseButton`, `showCopyButton`, `showExpandButton`, `showPreviewButton`, `showFontSizeButtons`, `showTooltips`
|
||||||
|
|
||||||
|
Default diff UX in Monaco mode:
|
||||||
|
|
||||||
|
- `diffHideUnchangedRegions: { enabled: true, contextLineCount: 2, minimumLineCount: 4, revealLineCount: 5 }`
|
||||||
|
- `diffLineStyle: 'background'`
|
||||||
|
- `diffAppearance: 'auto'`
|
||||||
|
- `diffUnchangedRegionStyle: 'line-info'`
|
||||||
|
- `diffHunkActionsOnHover: true`
|
||||||
|
- `diffHunkHoverHideDelayMs: 160`
|
||||||
|
|
||||||
|
You can override any of them through `monacoOptions`.
|
||||||
|
When the preset uses `diffAppearance: 'auto'`, `CodeBlockNode` resolves it to the current light/dark surface before passing the options to `stream-monaco`.
|
||||||
|
|
||||||
|
Diff blocks also show `- / +` line counts in the built-in header.
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
- `header-left` — replace left header
|
||||||
|
- `header-right` — replace right header
|
||||||
|
- `loading` — customize placeholder when streaming is disabled
|
||||||
|
|
||||||
|
## Emits
|
||||||
|
- `copy(text: string)` — when copy pressed
|
||||||
|
- `previewCode(payload)` — only emitted when you attach a `@preview-code` listener; payload is `{ node, artifactType, artifactTitle, id }`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
### Install and run (Monaco)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add stream-monaco
|
||||||
|
```
|
||||||
|
|
||||||
|
### Basic example
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
type: 'code_block',
|
||||||
|
language: 'js',
|
||||||
|
code: 'console.log(1)',
|
||||||
|
raw: 'console.log(1)',
|
||||||
|
} satisfies CodeBlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode :node="node" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace header and hide copy button
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
type: 'code_block',
|
||||||
|
language: 'js',
|
||||||
|
code: 'console.log(1)',
|
||||||
|
raw: 'console.log(1)',
|
||||||
|
} satisfies CodeBlockNodeProps['node']
|
||||||
|
|
||||||
|
function runSnippet() {}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode :node="node" :show-copy-button="false">
|
||||||
|
<template #header-left>
|
||||||
|
<div class="flex items-center">
|
||||||
|
Custom left
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #header-right>
|
||||||
|
<button @click="runSnippet">
|
||||||
|
Run
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</CodeBlockNode>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom loading placeholder
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
type: 'code_block',
|
||||||
|
language: 'ts',
|
||||||
|
code: 'console.log("loading")',
|
||||||
|
raw: 'console.log("loading")',
|
||||||
|
} satisfies CodeBlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode :node="node" :stream="false" :loading="true">
|
||||||
|
<template #loading="{ loading, stream }">
|
||||||
|
<div v-if="loading && !stream">
|
||||||
|
Loading editor assets…
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CodeBlockNode>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Theme Switching
|
||||||
|
|
||||||
|
`CodeBlockNode` supports automatic theme switching based on dark/light mode. Use `@vueuse/core`'s `useDark` composable to track the theme state and pass theme names to `MarkdownRender` or `CodeBlockNode`.
|
||||||
|
|
||||||
|
### Using @vueuse/core in standalone Vue apps
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
const isDark = useDark() // Ref<boolean> reactive to system/theme preference
|
||||||
|
const toggleDark = useToggle(isDark)
|
||||||
|
const content = '# Example\n\n```js\nconsole.log("dark mode")\n```'
|
||||||
|
|
||||||
|
// Available themes (must include the themes you want to use)
|
||||||
|
const themes = [
|
||||||
|
'vitesse-dark',
|
||||||
|
'vitesse-light',
|
||||||
|
'github-dark',
|
||||||
|
'github-light',
|
||||||
|
// ... more themes
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button @click="toggleDark()">
|
||||||
|
Toggle Theme
|
||||||
|
</button>
|
||||||
|
<MarkdownRender
|
||||||
|
:is-dark="isDark"
|
||||||
|
code-block-dark-theme="vitesse-dark"
|
||||||
|
code-block-light-theme="vitesse-light"
|
||||||
|
:themes="themes"
|
||||||
|
:content="content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### VitePress integration
|
||||||
|
|
||||||
|
For VitePress, use the built-in `isDark` from VitePress's `useData()`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// docs/.vitepress/theme/composables/useDark.ts
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VitePress theme composable for dark mode
|
||||||
|
* Uses VitePress's built-in isDark from useData()
|
||||||
|
*/
|
||||||
|
export function useDark() {
|
||||||
|
const { isDark } = useData()
|
||||||
|
return isDark
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- In any .md file or component -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
import { useDark } from '../../.vitepress/theme'
|
||||||
|
|
||||||
|
const isDark = useDark()
|
||||||
|
const content = '# Example\n\n```js\nconsole.log("dark mode")\n```'
|
||||||
|
|
||||||
|
const themes = [
|
||||||
|
'vitesse-dark',
|
||||||
|
'vitesse-light',
|
||||||
|
'github-dark',
|
||||||
|
'github-light',
|
||||||
|
// ... more themes
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender
|
||||||
|
:is-dark="isDark"
|
||||||
|
code-block-dark-theme="vitesse-dark"
|
||||||
|
code-block-light-theme="vitesse-light"
|
||||||
|
:themes="themes"
|
||||||
|
:content="content"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
|
||||||
|
The `theme` prop accepts either a fixed theme or a light/dark pair:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- Auto-switch between light and dark (recommended) -->
|
||||||
|
<CodeBlockNode :theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }" />
|
||||||
|
|
||||||
|
<!-- Fixed theme (ignores isDark) -->
|
||||||
|
<CodeBlockNode theme="monokai" />
|
||||||
|
|
||||||
|
<!-- Theme object (fixed, ignores isDark) -->
|
||||||
|
<CodeBlockNode :theme="{ name: 'my-theme', colors: { ... } }" />
|
||||||
|
```
|
||||||
|
|
||||||
|
When using a `{ light, dark }` pair, the component automatically switches based on the `isDark` prop.
|
||||||
|
|
||||||
|
The `themes` prop registers the available themes so Monaco can lazy-load them on demand.
|
||||||
|
|
||||||
|
> **Backward compatibility:** `darkTheme` / `lightTheme` props still work but are deprecated. Prefer the unified `theme` prop.
|
||||||
|
|
||||||
|
**Key differences for CodeBlockNode:**
|
||||||
|
|
||||||
|
| Prop | Direct CodeBlockNode | Via MarkdownRender |
|
||||||
|
|------|---------------------|-------------------|
|
||||||
|
| `isDark` | Passed directly to `<CodeBlockNode :is-dark="isDark" />` | Passed via `<MarkdownRender :is-dark="isDark" />` and automatically forwarded |
|
||||||
|
| Theme | `:theme="{ light: 'vitesse-light', dark: 'vitesse-dark' }"` | `:code-block-dark-theme="'vitesse-dark'"` `:code-block-light-theme="'vitesse-light'"` (legacy) |
|
||||||
|
| Themes list | `:themes="['vitesse-dark', 'vitesse-light', ...]"` | `:themes="['vitesse-dark', 'vitesse-light', ...]"` |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The CodeBlock header API is documented in `docs/guide/codeblock-header.md` (examples for replacing header and custom loading placeholder).
|
||||||
|
- `CodeBlockNode` and `MermaidBlockNode` intentionally use different `copy` event payloads: `CodeBlockNode` emits `copy(text: string)`, while `MermaidBlockNode` emits `copy(ev: MermaidBlockEvent<{ type: 'copy'; text: string }>)` (supports `preventDefault()`).
|
||||||
|
|
||||||
|
Try this — simple snapshot example (inline usage):
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = { type: 'code_block', language: 'js', code: 'console.log("hello")', raw: 'console.log("hello")' } satisfies CodeBlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode :node="node" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Tell me if you want me to also:
|
||||||
|
- Add stricter TS types for `defineEmits` in the SFC
|
||||||
|
- Update the docs sidebar to link this page (I can add the entry to `docs/guide/index.md` or the sidebar config)
|
||||||
|
- Add a runnable example in `playground/` demonstrating slot usage and event interception
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Code Block Rendering
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Code blocks can be rendered in three ways depending on which optional dependencies you install and how you configure the library:
|
||||||
|
|
||||||
|
- Monaco (recommended for large/interactive code blocks): installs and uses `stream-monaco` to provide an editor-like, incremental rendering experience. The library lazy-loads `stream-monaco` at runtime when available.
|
||||||
|
- Shiki (MarkdownCodeBlockNode): install `stream-markdown` and override the `code_block` node via `setCustomComponents` to use a lightweight Markdown-driven renderer.
|
||||||
|
- Fallback (no extra deps): if neither optional package is installed, code blocks render as plain `<pre><code>` blocks (basic styling / no Monaco features).
|
||||||
|
|
||||||
|
## Monaco (recommended)
|
||||||
|
|
||||||
|
- Install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add stream-monaco
|
||||||
|
# or
|
||||||
|
npm i stream-monaco
|
||||||
|
```
|
||||||
|
|
||||||
|
- Behavior: when `stream-monaco` is present the built-in `CodeBlockNode` will use Monaco-based streaming updates for large or frequently-updated code blocks.
|
||||||
|
|
||||||
|
- Vite worker note: Monaco and some worker-backed features require appropriate worker bundling configuration in your bundler (Vite) so the editor/workers are available at runtime. See [/nuxt-ssr](/nuxt-ssr) for guidance and examples of configuring workers and client-only initialization.
|
||||||
|
- See also: [/guide/monaco](/guide/monaco) for worker bundling tips and preload snippets.
|
||||||
|
|
||||||
|
## Shiki mode (MarkdownCodeBlockNode)
|
||||||
|
|
||||||
|
- Install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add stream-markdown
|
||||||
|
# or
|
||||||
|
npm i stream-markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
- Override the `code_block` node via `setCustomComponents` to register the Shiki-based renderer:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
setCustomComponents({ code_block: MarkdownCodeBlockNode })
|
||||||
|
```
|
||||||
|
|
||||||
|
Once set, `MarkdownCodeBlockNode` (powered by Shiki via `stream-markdown`) will be used for `code_block` nodes. You can also supply your own component that uses `stream-markdown` directly.
|
||||||
|
|
||||||
|
### Language icon lazy loading
|
||||||
|
|
||||||
|
To keep the main bundle smaller, infrequent language icons are split into an async chunk:
|
||||||
|
|
||||||
|
- Common languages (JS/TS/HTML/CSS/JSON/Python/etc.) stay in the main bundle.
|
||||||
|
- Rare languages load on demand and will update icon output automatically after the async chunk resolves.
|
||||||
|
- If you prefer to avoid first-hit fallback icons, preload once during app idle:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import { preloadExtendedLanguageIcons } from 'markstream-vue'
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined')
|
||||||
|
void preloadExtendedLanguageIcons()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vue CLI 4 (Webpack 4) notes
|
||||||
|
|
||||||
|
If you use Vue CLI 4 (Webpack 4), it’s recommended to use the Shiki mode for code blocks and **override** `code_block` to avoid Monaco + legacy-bundler edge cases.
|
||||||
|
|
||||||
|
Key pitfalls and fixes (see `playground-vue2-cli`):
|
||||||
|
|
||||||
|
- Webpack 4 doesn’t support `package.json#exports` → prefer `dist/*` paths via `resolve.alias`.
|
||||||
|
- ESM-only packages (like `stream-markdown`) may not be discoverable via `require.resolve()` inside `vue.config.js` (CJS) → use a filesystem fallback to find `node_modules/stream-markdown`, and alias it to `dist/index.js`.
|
||||||
|
- If you use `IgnorePlugin` to skip optional deps, don’t accidentally ignore `stream-markdown` (otherwise you’ll get `webpackMissingModule` at runtime).
|
||||||
|
|
||||||
|
## Fallback
|
||||||
|
|
||||||
|
If you don't install either optional package the renderer falls back to a simple `pre`/`code` representation.
|
||||||
|
|
||||||
|
## Links & further reading
|
||||||
|
|
||||||
|
- Worker / SSR guidance: [/nuxt-ssr](/nuxt-ssr)
|
||||||
|
- Installation notes: [/guide/installation](/guide/installation)
|
||||||
|
|
||||||
|
Try this — simple CodeBlock render:
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
type: 'code_block',
|
||||||
|
language: 'js',
|
||||||
|
code: 'console.log(42)',
|
||||||
|
raw: 'console.log(42)',
|
||||||
|
} satisfies CodeBlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode :node="node" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Code block header customization
|
||||||
|
|
||||||
|
The code block component exposes a flexible header API to adapt the header to your app UX:
|
||||||
|
|
||||||
|
- Toggle header on/off
|
||||||
|
- Show or hide built-in toolbar buttons (collapse, copy, expand, preview, font-size)
|
||||||
|
- Replace the left/right header content via named slots (`header-left`, `header-right`)
|
||||||
|
|
||||||
|
Slots
|
||||||
|
|
||||||
|
- `header-left` — Replace the left side of the header
|
||||||
|
- `header-right` — Replace the right side of the header
|
||||||
|
- `loading` — Customize loading placeholder when streaming is disabled
|
||||||
|
|
||||||
|
Examples
|
||||||
|
|
||||||
|
Below are a few common examples. You can find the full set of examples in the project README; these are the most useful patterns to re-use in your app.
|
||||||
|
|
||||||
|
Hide the header (simple)
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
type: 'code_block',
|
||||||
|
language: 'javascript',
|
||||||
|
code: 'console.log(1)',
|
||||||
|
raw: 'console.log(1)',
|
||||||
|
} satisfies CodeBlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode
|
||||||
|
:node="node"
|
||||||
|
:show-header="false"
|
||||||
|
:loading="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the default header using the `#header-left` and `#header-right` named slots
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
type: 'code_block',
|
||||||
|
language: 'html',
|
||||||
|
code: '<div>Hello</div>',
|
||||||
|
raw: '<div>Hello</div>',
|
||||||
|
} satisfies CodeBlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode
|
||||||
|
:node="node"
|
||||||
|
:loading="false"
|
||||||
|
:show-copy-button="false"
|
||||||
|
>
|
||||||
|
<template #header-left>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-sm font-medium">My HTML</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #header-right>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button class="px-2 py-1 bg-blue-600 text-white rounded">
|
||||||
|
Run
|
||||||
|
</button>
|
||||||
|
<button class="px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">
|
||||||
|
Inspect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CodeBlockNode>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Custom loading placeholder (shown when `stream` is false and `loading` is true)
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const code = 'print("hello")'
|
||||||
|
const isLoading = true
|
||||||
|
const node = {
|
||||||
|
type: 'code_block',
|
||||||
|
language: 'python',
|
||||||
|
code,
|
||||||
|
raw: code,
|
||||||
|
} satisfies CodeBlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode
|
||||||
|
:node="node"
|
||||||
|
:stream="false"
|
||||||
|
:loading="isLoading"
|
||||||
|
>
|
||||||
|
<template #loading="{ loading, stream }">
|
||||||
|
<div v-if="loading && !stream" class="p-4 text-center">
|
||||||
|
<div class="animate-spin inline-block w-6 h-6 border-2 border-current border-t-transparent rounded-full" />
|
||||||
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
|
Initializing editor...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CodeBlockNode>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Events and notes
|
||||||
|
|
||||||
|
- The component emits `copy` when the copy button is clicked (the copy text is supplied as the event payload). Use `@copy="onCopy"` to listen for it.
|
||||||
|
- The component emits `previewCode` only when you attach a `@preview-code` listener; payload is `{ node, artifactType, artifactTitle, id }`. (Without a listener, the built-in preview overlay is available for HTML only.)
|
||||||
|
- To hide a specific toolbar button set, use the boolean props `showCollapseButton`, `showCopyButton`, `showExpandButton`, `showPreviewButton`, `showFontSizeButtons`.
|
||||||
|
- Use `showTooltips` to toggle header action tooltips globally for this block.
|
||||||
|
- The `showHeader` prop controls whether the header is rendered at all.
|
||||||
|
|
||||||
|
Try this — replace the header in a small example:
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import { CodeBlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = { type: 'code_block', language: 'js', code: 'console.log("run")', raw: 'console.log("run")' } satisfies CodeBlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodeBlockNode :node="node" :show-copy-button="false">
|
||||||
|
<template #header-right>
|
||||||
|
<button @click="() => console.log('run')">
|
||||||
|
Run
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</CodeBlockNode>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
|
||||||
|
If you run into style conflicts (for instance, Tailwind reset/utility rules changing the header's layout), see the main Troubleshooting page which covers Tailwind import ordering and quick fixes: `/guide/troubleshooting`.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Compared to traditional Markdown renderers
|
||||||
|
|
||||||
|
Traditional Markdown renderers typically convert a finished Markdown string into a static HTML tree. This library is designed for streaming and interactive workflows and therefore provides capabilities you won't find in a classic renderer:
|
||||||
|
|
||||||
|
- Streaming-first rendering: render partial or incrementally-updated Markdown content without re-parsing the whole document each time. This enables live previews for AI outputs or editors that emit tokens progressively.
|
||||||
|
- Streaming-aware code blocks and "code-jump" UX: large code blocks are updated incrementally and the renderer can maintain cursor/selection context and fine-grained edits.
|
||||||
|
- Built-in diff/code-stream components: show diffs as they arrive (line-by-line or token-by-token) with minimal reflow.
|
||||||
|
- Progressive diagrams and editors: Mermaid and Monaco-based previews update progressively.
|
||||||
|
- Flexible code block rendering: pick Monaco for interactive editing or Shiki for display-only highlighting.
|
||||||
|
|
||||||
|
Quick try — render streaming content and static fallback side-by-side:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const streaming = '# Streaming example\n\nThis appears progressively'
|
||||||
|
const staticMd = '# Static example\n\nRendered once'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MarkdownRender :content="streaming" />
|
||||||
|
<MarkdownRender :content="staticMd" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
---
|
||||||
|
description: Override built-in markstream-vue components like image, code_block, mermaid, and link with scoped, project-safe custom mappings.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Override Built-in Components
|
||||||
|
|
||||||
|
Most advanced customization starts with `setCustomComponents`, not parser hooks.
|
||||||
|
|
||||||
|
Use this page when you want to:
|
||||||
|
|
||||||
|
- replace built-in renderers such as `image`, `code_block`, `mermaid`, or `link`
|
||||||
|
- keep overrides scoped to one part of the app
|
||||||
|
- understand which override key matches which node type
|
||||||
|
|
||||||
|
If you want trusted custom tags such as `thinking`, go to [Custom Tags & Advanced Components](/guide/custom-components). If props and slots are enough, stay with [Props & Options](/guide/props).
|
||||||
|
|
||||||
|
## 1. Prefer scoped overrides
|
||||||
|
|
||||||
|
Use `custom-id` on the renderer and register overrides under the same id:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
declare const DocImage: Component
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
image: DocImage,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
const markdown = ''
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender custom-id="docs" :content="markdown" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Why this is the default recommendation:
|
||||||
|
|
||||||
|
- it prevents one project area from changing another
|
||||||
|
- it makes testing and cleanup easier
|
||||||
|
- it matches how the renderer merges scoped and global mappings
|
||||||
|
|
||||||
|
The single-argument form `setCustomComponents({ ... })` is still supported, but it is best treated as a legacy global override.
|
||||||
|
|
||||||
|
## 2. The override keys you will actually use
|
||||||
|
|
||||||
|
| Key | What it replaces | Notes |
|
||||||
|
|-----|------------------|-------|
|
||||||
|
| `image` | `ImageNode` | Great first override for lightboxes, captions, or lazy-loading wrappers |
|
||||||
|
| `link` | `LinkNode` | Useful for analytics, router links, or custom tooltip behavior |
|
||||||
|
| `code_block` | Regular fenced code blocks | Used as the generic fallback after exact language keys and the built-in `mermaid` / `d2` / `infographic` routes |
|
||||||
|
| `mermaid` | Mermaid fenced blocks | Preferred over `code_block` when only Mermaid needs a custom renderer |
|
||||||
|
| `d2` | D2 and `d2lang` fenced blocks | Same routing idea as Mermaid |
|
||||||
|
| `infographic` | Infographic fenced blocks | Use when only infographic output changes |
|
||||||
|
| `inline_code` | Inline code spans | Useful for docs typography or special inline tooling |
|
||||||
|
| `heading`, `paragraph`, `list_item`, `blockquote` | Container nodes | Powerful, but you must render children yourself |
|
||||||
|
|
||||||
|
The full list of built-in node types lives in [Components API](/guide/components) and the exported `CustomComponents` type.
|
||||||
|
|
||||||
|
## 3. Start with leaf-like nodes
|
||||||
|
|
||||||
|
Leaf-like nodes are the easiest place to begin because they do not require you to manually re-render child nodes.
|
||||||
|
|
||||||
|
### Example: replace `image`
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ImageNodeProps } from 'markstream-vue'
|
||||||
|
|
||||||
|
const props = defineProps<ImageNodeProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<figure class="docs-image">
|
||||||
|
<img :src="props.node.src" :alt="props.node.alt" loading="lazy">
|
||||||
|
<figcaption v-if="props.node.title || props.node.alt">
|
||||||
|
{{ props.node.title || props.node.alt }}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
declare const DocImage: Component
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
image: DocImage,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: switch regular code blocks to `MarkdownCodeBlockNode`
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import { MarkdownCodeBlockNode, setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
code_block: MarkdownCodeBlockNode,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This only changes regular fenced code blocks. Mermaid, D2, and infographic blocks still route to their specialized renderers unless you override `mermaid`, `d2`, or `infographic`.
|
||||||
|
|
||||||
|
### Example: target one fenced language directly
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
declare const EChartsBlockNode: Component
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
echarts: EChartsBlockNode,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This only catches fences whose language is `echarts`. The routing order for code blocks is:
|
||||||
|
|
||||||
|
- exact language key such as `echarts`
|
||||||
|
- built-in specialized routes such as `mermaid`, `d2`, and `infographic`
|
||||||
|
- generic `code_block`
|
||||||
|
|
||||||
|
### Example: override Mermaid only
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
declare const CustomMermaidBlock: Component
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
mermaid: CustomMermaidBlock,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the safest pattern when you want custom export controls, branding, or diagram chrome without affecting all code blocks.
|
||||||
|
|
||||||
|
## 4. What props your override receives
|
||||||
|
|
||||||
|
Every custom renderer receives the basics:
|
||||||
|
|
||||||
|
- `node`
|
||||||
|
- `loading`
|
||||||
|
- `indexKey`
|
||||||
|
- `customId`
|
||||||
|
- `isDark`
|
||||||
|
|
||||||
|
The renderer also forwards feature-specific props:
|
||||||
|
|
||||||
|
- regular code block overrides receive `stream`, `theme`, `darkTheme`, `lightTheme`, `monacoOptions`, `themes`, `minWidth`, `maxWidth`, and anything passed through `codeBlockProps` (`theme` is the recommended field; `darkTheme` / `lightTheme` remain for compatibility)
|
||||||
|
- Mermaid, D2, and infographic overrides receive the corresponding `mermaidProps`, `d2Props`, or `infographicProps`
|
||||||
|
- link and list overrides inherit tooltip and typewriter-related bindings from the parent renderer
|
||||||
|
|
||||||
|
The exported prop types in `markstream-vue` are the easiest way to keep your overrides strongly typed.
|
||||||
|
|
||||||
|
## 5. Container nodes need extra care
|
||||||
|
|
||||||
|
Overriding `heading`, `paragraph`, `list_item`, or `blockquote` is absolutely supported, but those nodes carry children. Your component needs to decide how to render them.
|
||||||
|
|
||||||
|
Practical advice:
|
||||||
|
|
||||||
|
- start with `image`, `link`, `code_block`, `mermaid`, or `inline_code` unless you truly need container-node control
|
||||||
|
- if you override a container node, inspect the built-in renderer in `src/components/*` and mirror the child-rendering pattern you need
|
||||||
|
- keep the override scoped with `custom-id` so you can iterate safely
|
||||||
|
|
||||||
|
## 6. Cleanup for dynamic scopes
|
||||||
|
|
||||||
|
If the override only lives for a temporary view, remove it when that scope is no longer needed:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import { removeCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
removeCustomComponents('docs')
|
||||||
|
```
|
||||||
|
|
||||||
|
This is especially helpful in SPAs, Storybook stories, playgrounds, and test suites.
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
---
|
||||||
|
description: Look up MarkdownRender, CodeBlockNode, MermaidBlockNode, ImageNode, LinkNode, and other exported markstream-vue renderer components.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Renderer & Node Components
|
||||||
|
|
||||||
|
Use this page when you already know you need a specific exported component and want the shortest path to its props, events, CSS requirements, and the page that goes deeper.
|
||||||
|
|
||||||
|
If you are still deciding where to customize the pipeline, start with:
|
||||||
|
|
||||||
|
- Need parser helpers, AST hooks, or `setCustomComponents()` strategy: [API Reference](/guide/api)
|
||||||
|
- Need a scoped renderer replacement: [Override Built-in Components](/guide/component-overrides)
|
||||||
|
- Need trusted custom tags such as `thinking`: [Custom Tags & Advanced Components](/guide/custom-components)
|
||||||
|
- Need configuration, performance, or toolbar tuning: [Props & Options](/guide/props)
|
||||||
|
|
||||||
|
## Quick reference
|
||||||
|
|
||||||
|
| Component | Best for | Key props/events | Extra CSS / peers | Troubleshooting hooks |
|
||||||
|
| --------- | -------- | ---------------- | ----------------- | --------------------- |
|
||||||
|
| `MarkdownRender` | Rendering full AST trees (default export) | Props: `content` / `nodes`, `custom-id`, `final`, `parse-options`, `custom-html-tags`, `is-dark`, `code-block-props`, `mermaid-props`, `d2-props`, `infographic-props`; events: `copy`, `handleArtifactClick`, `click`, `mouseover`, `mouseout` | Import `markstream-vue/index.css` inside a reset-aware layer (CSS is scoped under an internal `.markstream-vue` container) | Use `setCustomComponents(customId, mapping)` + `custom-id` to scope overrides; see [CSS checklist](/guide/troubleshooting#css-looks-wrong-start-here) |
|
||||||
|
| `CodeBlockNode` | Monaco-powered code blocks, streaming diffs | `node`, `monacoOptions`, `stream`, `loading`; events: `copy`, `previewCode`; slots `header-left` / `header-right`; diff hover actions live under `monacoOptions` (`diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, `onDiffHunkAction`) | Install `stream-monaco` (peer) + bundle Monaco workers | SSR sends a `<pre><code>` fallback first; blank editor => check worker bundling + client enhancement setup |
|
||||||
|
| `MarkdownCodeBlockNode` | Lightweight highlighting via `shiki` | `node`, `stream`, `loading`; slots `header-left` / `header-right` | Requires `stream-markdown` | Use for SSR-friendly or low-bundle scenarios |
|
||||||
|
| `MermaidBlockNode` | Progressive Mermaid diagrams | `node`, `isDark`, `isStrict`, `maxHeight`, `estimatedPreviewHeightPx`; emits `copy`, `export`, `openModal`, `toggleMode` | Peer `mermaid` >= 11; no extra CSS required | SSR sends readable fallback markup first; for async errors see `/guide/mermaid` |
|
||||||
|
| `D2BlockNode` | Progressive D2 diagrams | `node`, `isDark`, `maxHeight`, `progressiveRender`, `progressiveIntervalMs`; toolbar toggles | Peer `@terrastruct/d2`; no extra CSS | SSR sends fallback/source first; missing peer stays on fallback; see `/guide/d2` |
|
||||||
|
| `MathBlockNode` / `MathInlineNode` | KaTeX rendering | `node` | Install `katex` and import `katex/dist/katex.min.css` | SSR can emit KaTeX HTML when you register a sync loader; otherwise it falls back to raw text |
|
||||||
|
| `ImageNode` | Custom previews/lightboxes | Props: `fallback-src`, `show-caption`, `lazy`, `svg-min-height`, `use-placeholder`; emits `click`, `load`, `error` | None, but respects global CSS | Wrap in a custom component + `setCustomComponents` to intercept events |
|
||||||
|
| `LinkNode` | Animated underline, tooltips | `color`, `underlineHeight`, `showTooltip` | No extra CSS | Browser defaults can override `a` styles; import reset |
|
||||||
|
| `VmrContainerNode` | Custom `:::` containers | `node` (`name`, `attrs`, `loading`, `children`) | Minimal base CSS; override via `setCustomComponents` | JSON attrs are normalized onto `node.attrs` (keys without `data-`); invalid/partial JSON becomes `attrs.attrs`; args after name stored in `attrs.args` |
|
||||||
|
|
||||||
|
## TypeScript exports
|
||||||
|
|
||||||
|
`markstream-vue` exports renderer and component prop interfaces:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { CodeBlockNodeProps } from 'markstream-vue'
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
|
||||||
|
type MarkdownRenderCodeBlockProps = NonNullable<MarkdownRenderProps['codeBlockProps']>
|
||||||
|
|
||||||
|
declare const markdownRenderProps: MarkdownRenderProps
|
||||||
|
declare const markdownRenderCodeBlockProps: MarkdownRenderCodeBlockProps
|
||||||
|
declare const codeBlockProps: CodeBlockNodeProps
|
||||||
|
|
||||||
|
// Hover the property names after each dot.
|
||||||
|
markdownRenderProps.content
|
||||||
|
markdownRenderProps.customId
|
||||||
|
markdownRenderProps.isDark
|
||||||
|
markdownRenderProps.codeBlockProps?.showHeader
|
||||||
|
markdownRenderProps.codeBlockProps?.showTooltips
|
||||||
|
markdownRenderProps.codeBlockMonacoOptions
|
||||||
|
markdownRenderProps.codeBlockMonacoOptions?.theme
|
||||||
|
markdownRenderProps.codeBlockMonacoOptions?.languages
|
||||||
|
markdownRenderProps.codeBlockMonacoOptions?.diffHunkActionsOnHover
|
||||||
|
markdownRenderProps.themes
|
||||||
|
|
||||||
|
markdownRenderCodeBlockProps.showFontSizeButtons
|
||||||
|
markdownRenderCodeBlockProps.showCollapseButton
|
||||||
|
|
||||||
|
codeBlockProps.monacoOptions
|
||||||
|
codeBlockProps.monacoOptions?.MAX_HEIGHT
|
||||||
|
codeBlockProps.theme
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `InstanceType<typeof MarkdownRender>['$props']` is the most direct way to inspect the exported component props.
|
||||||
|
- `NodeRendererProps` is the named export for the same public prop surface.
|
||||||
|
- `codeBlockProps` now follows the public `CodeBlockNode` prop surface except for `node`, so hover/completion works for flags like `showHeader`, `showFontSizeButtons`, and `showTooltips`.
|
||||||
|
- Prefer `codeBlockProps.theme` for new code. `darkTheme` / `lightTheme` still exist for backward compatibility.
|
||||||
|
- Hover the property names after each dot in the snippet above, not the imported type names.
|
||||||
|
- If you specifically want the best component-prop hover targets, use the `MarkdownRender` snippet below first.
|
||||||
|
- Only `ts twoslash` and `vue twoslash` fences in this docs site enable hoverable type details.
|
||||||
|
|
||||||
|
Language icons default to the built-in `material` theme. Advanced integrations can inspect or switch icon themes with the exported helpers:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { getRegisteredThemes, registerIconTheme, setIconTheme } from 'markstream-vue'
|
||||||
|
|
||||||
|
console.log(getRegisteredThemes()) // ['material']
|
||||||
|
setIconTheme('material')
|
||||||
|
|
||||||
|
// registerIconTheme(...) lets you add your own icon pack before switching.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pick the right component quickly
|
||||||
|
|
||||||
|
- Use `MarkdownRender` for almost every app-level integration.
|
||||||
|
- Use `CodeBlockNode`, `MermaidBlockNode`, `D2BlockNode`, or `MathBlockNode` when you are composing lower-level node renderers yourself.
|
||||||
|
- Use `ImageNode`, `LinkNode`, or `VmrContainerNode` when you only need to replace one built-in node with custom behavior.
|
||||||
|
- If you render node components directly, wrap them in `<div class="markstream-vue">...</div>` so packaged CSS variables and component styles apply.
|
||||||
|
|
||||||
|
## MarkdownRender
|
||||||
|
|
||||||
|
> Main entry point that takes Markdown AST content (string or parsed structure) and renders with built-in node components.
|
||||||
|
|
||||||
|
### Quick reference
|
||||||
|
|
||||||
|
- **Best for**: full markdown documents in Vite, Nuxt, VitePress.
|
||||||
|
- **Key props**: `content` / `nodes`, `custom-id`, `final`, `parse-options`, `custom-html-tags`
|
||||||
|
- **CSS order**: import reset first, then add `markstream-vue/index.css` inside `@layer components`.
|
||||||
|
|
||||||
|
### CSS scope
|
||||||
|
|
||||||
|
`markstream-vue` scopes its packaged CSS under an internal `.markstream-vue` container to reduce global style conflicts.
|
||||||
|
|
||||||
|
- When you use `MarkdownRender`, this container is already present.
|
||||||
|
- When you render node components directly, wrap them with `<div class="markstream-vue">...</div>` so theme variables and component styles apply.
|
||||||
|
|
||||||
|
### Best hover targets
|
||||||
|
|
||||||
|
Start by hovering `:content`, `custom-id`, `:is-dark`, and `:code-block-monaco-options` in this example:
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
type MarkdownRenderProps = InstanceType<typeof MarkdownRender>['$props']
|
||||||
|
|
||||||
|
const content: MarkdownRenderProps['content'] = '# Hello'
|
||||||
|
const customId: MarkdownRenderProps['customId'] = 'docs'
|
||||||
|
const isDark: MarkdownRenderProps['isDark'] = true
|
||||||
|
const monacoOptions: MarkdownRenderProps['codeBlockMonacoOptions'] = {
|
||||||
|
theme: 'vitesse-dark',
|
||||||
|
languages: ['typescript', 'vue'],
|
||||||
|
MAX_HEIGHT: 520,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender
|
||||||
|
:content="content"
|
||||||
|
:custom-id="customId"
|
||||||
|
:is-dark="isDark"
|
||||||
|
:code-block-monaco-options="monacoOptions"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage ladder
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { CodeBlockMonacoOptions } from 'markstream-vue'
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
const md = '# Hello\n\nUse custom-id to scope styles.'
|
||||||
|
const monacoOptions = {
|
||||||
|
theme: 'vitesse-dark',
|
||||||
|
themes: ['vitesse-dark', 'vitesse-light'],
|
||||||
|
languages: ['typescript', 'vue'],
|
||||||
|
MAX_HEIGHT: 520,
|
||||||
|
diffHunkActionsOnHover: true,
|
||||||
|
} satisfies CodeBlockMonacoOptions
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender
|
||||||
|
custom-id="docs"
|
||||||
|
:content="md"
|
||||||
|
:code-block-monaco-options="monacoOptions"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Register custom nodes
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
import CustomImageNode from './CustomImageNode.vue'
|
||||||
|
|
||||||
|
setCustomComponents('docs', {
|
||||||
|
image: CustomImageNode,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* styles/main.css */
|
||||||
|
@import 'modern-css-reset';
|
||||||
|
@tailwind base;
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
@import 'markstream-vue/index.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-custom-id='docs'] .prose {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance props
|
||||||
|
|
||||||
|
- **Batch rendering**: `batchRendering`, `initialRenderBatchSize`, `renderBatchSize`, `renderBatchDelay`, and `renderBatchBudgetMs` control how many nodes switch from skeletons to real components per frame. This only activates when virtualization is disabled (`:max-live-nodes="0"`).
|
||||||
|
- **Deferred heavy nodes**: `deferNodesUntilVisible` and `viewportPriority` are enabled by default so Mermaid, D2, Monaco, and KaTeX only load near the viewport.
|
||||||
|
- **Virtualization window**: `maxLiveNodes` limits how many rendered nodes stay in the DOM, and `liveNodeBuffer` controls overscan. See [Performance](/guide/performance).
|
||||||
|
- **Code block fallback**: use `renderCodeBlocksAsPre` and `codeBlockStream` to downgrade standard code fences to `<pre><code>` or disable streaming updates during heavy workloads.
|
||||||
|
|
||||||
|
### Common issues
|
||||||
|
|
||||||
|
- **Broken styles**: start with the [CSS checklist](/guide/troubleshooting#css-looks-wrong-start-here).
|
||||||
|
- **Utility class leakage**: pass `custom-id` and scope overrides with `[data-custom-id="docs"]`.
|
||||||
|
- **SSR errors**: the renderer itself is SSR-safe; only gate your own browser-only page logic or manual peer initialization with client-only / mounted guards.
|
||||||
|
|
||||||
|
### Use this when
|
||||||
|
|
||||||
|
- You want the default parser + renderer path with the fewest moving parts.
|
||||||
|
- You need one integration point for streaming, virtualization, batching, custom tags, and scoped overrides.
|
||||||
|
- You want to choose between `content` and `nodes` without wiring individual node renderers yourself.
|
||||||
|
|
||||||
|
### Most common combinations
|
||||||
|
|
||||||
|
- `content` + `custom-id`: static or lightly customized documents.
|
||||||
|
- `nodes` + `final`: streaming or server-preparsed workflows.
|
||||||
|
- `custom-html-tags` + `setCustomComponents(customId, mapping)`: trusted custom tags such as `thinking`.
|
||||||
|
- `renderCodeBlocksAsPre`: downgrade heavy code blocks during SSR or constrained environments.
|
||||||
|
|
||||||
|
## CodeBlockNode
|
||||||
|
|
||||||
|
> Monaco-backed code block renderer with streaming diff support and interactive toolbar actions.
|
||||||
|
|
||||||
|
- **Best for**: rich code review, diff inspection, live patches, hover actions.
|
||||||
|
- **Key props**: `node`, `monacoOptions`, `stream`, `loading`
|
||||||
|
- **Events**: `copy`, `previewCode`
|
||||||
|
- **Slots**: `header-left`, `header-right`
|
||||||
|
- **Peers**: `stream-monaco`, plus Monaco worker setup in your bundler
|
||||||
|
- **Common gotcha**: if the editor area is blank, verify worker bundling and SSR guards first
|
||||||
|
|
||||||
|
Reach for this when the code block itself is part of the product experience. If you only need syntax highlighting, prefer `MarkdownCodeBlockNode`.
|
||||||
|
|
||||||
|
Deep dive: [CodeBlockNode](/guide/code-block-node), [Monaco](/guide/monaco)
|
||||||
|
|
||||||
|
## MarkdownCodeBlockNode
|
||||||
|
|
||||||
|
> Lightweight syntax-highlighted code block renderer powered by Shiki via `stream-markdown`.
|
||||||
|
|
||||||
|
- **Best for**: SSR-friendly docs, blog-style pages, smaller bundles
|
||||||
|
- **Key props**: `node`, `stream`, `loading`
|
||||||
|
- **Slots**: `header-left`, `header-right`
|
||||||
|
- **Peers**: `stream-markdown`
|
||||||
|
- **Common gotcha**: if highlighting never appears, confirm `stream-markdown` is installed and loaded in the environment where rendering happens
|
||||||
|
|
||||||
|
Choose this when you do not need Monaco's editing surface or diff interactions.
|
||||||
|
|
||||||
|
## MermaidBlockNode
|
||||||
|
|
||||||
|
> Progressive Mermaid renderer with copy/export/modal hooks.
|
||||||
|
|
||||||
|
- **Best for**: large Mermaid graphs, AI-generated diagrams, user-triggered export
|
||||||
|
- **Key props**: `node`, `isDark`, `isStrict`, `maxHeight`, `estimatedPreviewHeightPx`
|
||||||
|
- **Events**: `copy`, `export`, `openModal`, `toggleMode`
|
||||||
|
- **Peer**: `mermaid` >= 11
|
||||||
|
- **Common gotcha**: treat Mermaid as client enhancement; SSR already returns fallback markup, but preview rendering still initializes on the client
|
||||||
|
|
||||||
|
`MarkdownRender` estimates `estimatedPreviewHeightPx` for Mermaid fences when callers do not provide it. Pass it manually only when rendering `MermaidBlockNode` directly or when a custom `mermaid` renderer already knows the first-preview height.
|
||||||
|
|
||||||
|
Deep dive: [Mermaid](/guide/mermaid), [MermaidBlockNode](/guide/mermaid-block-node)
|
||||||
|
|
||||||
|
## D2BlockNode
|
||||||
|
|
||||||
|
> Progressive D2 diagram renderer for structured architecture diagrams and sequence-like layouts.
|
||||||
|
|
||||||
|
- **Best for**: D2-driven docs, generated architecture views, progressive rendering
|
||||||
|
- **Key props**: `node`, `isDark`, `maxHeight`, `progressiveRender`, `progressiveIntervalMs`
|
||||||
|
- **Peer**: `@terrastruct/d2`
|
||||||
|
- **Common gotcha**: missing peer falls back to source display instead of the rendered diagram
|
||||||
|
|
||||||
|
Deep dive: [D2](/guide/d2)
|
||||||
|
|
||||||
|
## MathBlockNode / MathInlineNode
|
||||||
|
|
||||||
|
> KaTeX-backed block and inline math renderers.
|
||||||
|
|
||||||
|
- **Best for**: technical docs, formulas, AI math responses
|
||||||
|
- **Key prop**: `node`
|
||||||
|
- **Peer**: `katex`
|
||||||
|
- **Required CSS**: `katex/dist/katex.min.css`
|
||||||
|
- **Common gotcha**: invisible math usually means KaTeX CSS is missing, not that parsing failed
|
||||||
|
|
||||||
|
## ImageNode
|
||||||
|
|
||||||
|
> Built-in image renderer with sensible defaults for captioning, fallbacks, and lazy behavior.
|
||||||
|
|
||||||
|
- **Best for**: custom previews, analytics hooks, lightbox integration
|
||||||
|
- **Key props**: `fallback-src`, `show-caption`, `lazy`, `svg-min-height`, `use-placeholder`
|
||||||
|
- **Events**: `click`, `load`, `error`
|
||||||
|
- **Common pattern**: wrap this with a custom component and register it through `setCustomComponents(customId, { image: CustomImageNode })`
|
||||||
|
|
||||||
|
Deep dive: [ImageNode](/guide/image-node)
|
||||||
|
|
||||||
|
## LinkNode
|
||||||
|
|
||||||
|
> Link renderer with underline animation and optional tooltip behavior.
|
||||||
|
|
||||||
|
- **Best for**: docs sites and chat surfaces where link affordance needs to match your design system
|
||||||
|
- **Key props**: `color`, `underlineHeight`, `showTooltip`
|
||||||
|
- **Common gotcha**: browser defaults or resets can override anchor styles, so verify CSS order before assuming the component is broken
|
||||||
|
|
||||||
|
## VmrContainerNode
|
||||||
|
|
||||||
|
> Renderer for `:::` containers and other normalized block containers coming from the parser.
|
||||||
|
|
||||||
|
- **Best for**: callouts, notices, AI-specific block types, container-style custom components
|
||||||
|
- **Key prop**: `node` with `name`, `attrs`, `loading`, and `children`
|
||||||
|
- **Normalization detail**: JSON attrs are normalized onto `node.attrs`; invalid or partial JSON is preserved under `attrs.attrs`; args after the container name are stored in `attrs.args`
|
||||||
|
- **Common pattern**: use this together with `custom-html-tags` or parser hooks when you need trusted structured blocks
|
||||||
|
|
||||||
|
## Direct node rendering checklist
|
||||||
|
|
||||||
|
When you bypass `MarkdownRender` and mount node components directly:
|
||||||
|
|
||||||
|
- Wrap them in a `.markstream-vue` container.
|
||||||
|
- Import the same CSS you would for the full renderer.
|
||||||
|
- Install and guard only the peers needed by the nodes you actually render.
|
||||||
|
- Scope any replacement styles with a parent selector or `data-custom-id` equivalent.
|
||||||
|
|
||||||
|
## Need a different layer of the stack?
|
||||||
|
|
||||||
|
- Use [API Reference](/guide/api) for parser helpers, `setCustomComponents`, and AST hooks.
|
||||||
|
- Use [Props & Options](/guide/props) for full renderer prop behavior.
|
||||||
|
- Use [Troubleshooting](/guide/troubleshooting) if the right component is already chosen but CSS, peers, or SSR still misbehave.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Thanks for wanting to contribute!
|
||||||
|
|
||||||
|
Development commands to know:
|
||||||
|
|
||||||
|
- `pnpm dev` — start the library playground
|
||||||
|
- `pnpm play` — start the playground (same as dev)
|
||||||
|
- `pnpm build` — build the library
|
||||||
|
- `pnpm build:analyze` — build with bundle visualizer reports
|
||||||
|
- `pnpm size:check` — enforce package size budgets
|
||||||
|
- `pnpm test` — run unit tests
|
||||||
|
- `pnpm docs:dev` — start the VitePress docs server
|
||||||
|
|
||||||
|
Size budget env overrides (optional):
|
||||||
|
|
||||||
|
- `MAX_DIST_BYTES`
|
||||||
|
- `MAX_JS_CHUNK_BYTES`
|
||||||
|
- `MAX_PACK_TGZ_BYTES`
|
||||||
|
- `MAX_PACK_UNPACKED_BYTES`
|
||||||
|
|
||||||
|
How to add a docs page:
|
||||||
|
|
||||||
|
1. Add a markdown file under `docs/` or `docs/guide`.
|
||||||
|
2. Update `docs/.vitepress/config.ts` sidebar if needed.
|
||||||
|
3. Run `pnpm docs:dev` to preview the page locally and ensure everything builds.
|
||||||
|
4. Open a PR with your changes.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
|
||||||
|
- Keep docs concise and example-driven
|
||||||
|
- If adding code snippets to docs, verify they run in `playground` or tests
|
||||||
|
- Add tests for any new features in `test/` and update `README` / docs accordingly
|
||||||
|
|
||||||
|
Quick try — run the docs site locally and add a simple page to verify the flow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm docs:dev
|
||||||
|
# open http://localhost:5173, add docs/guide/new-page.md and confirm build
|
||||||
|
```
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
description: Add trusted custom tags such as thinking and map them to advanced markstream-vue components without rewriting the parser.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Custom Tags & Advanced Components
|
||||||
|
|
||||||
|
Use this page when Markdown needs to contain trusted, component-like tags such as `thinking`, `answer-box`, or other domain-specific blocks.
|
||||||
|
|
||||||
|
The recommended path is:
|
||||||
|
|
||||||
|
1. register the tag with `custom-html-tags`
|
||||||
|
2. map the resulting node type with `setCustomComponents`
|
||||||
|
3. keep the mapping scoped with `custom-id`
|
||||||
|
|
||||||
|
Use tag-like names in `custom-html-tags`, for example `thinking`, `answer-box`, or `my_component`.
|
||||||
|
Namespaced forms like `foo:bar` are ignored. Built-in override keys such as `code_block` stay reserved for node renderer overrides, so trusted custom tags should still be declared explicitly via `custom-html-tags`.
|
||||||
|
|
||||||
|
Reach for parser hooks only after this flow stops being enough.
|
||||||
|
|
||||||
|
If these tags live inside a docs site or VitePress theme, pair this page with [Docs Site & VitePress](/guide/vitepress-docs-integration) so the theme-level registration and CSS order stay in one place.
|
||||||
|
|
||||||
|
## 1. The simplest custom-tag setup
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
declare const ThinkingNode: Component
|
||||||
|
|
||||||
|
setCustomComponents('chat', {
|
||||||
|
thinking: ThinkingNode,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
const markdown = '<thinking>Step by step</thinking>'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender
|
||||||
|
custom-id="chat"
|
||||||
|
:custom-html-tags="['thinking']"
|
||||||
|
:content="markdown"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the tag is allowlisted, the parser emits a custom node whose `type` is the tag name itself.
|
||||||
|
|
||||||
|
## 2. A practical Vue component for nested content
|
||||||
|
|
||||||
|
Custom tags usually contain Markdown inside them. The easiest way to preserve that nested Markdown is to render the tag body with another `MarkdownRender`.
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
node: {
|
||||||
|
type: 'thinking'
|
||||||
|
content?: string
|
||||||
|
loading?: boolean
|
||||||
|
}
|
||||||
|
customId?: string
|
||||||
|
isDark?: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="thinking-box" :data-loading="props.node.loading || undefined">
|
||||||
|
<header class="thinking-box__title">
|
||||||
|
Thinking
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<MarkdownRender
|
||||||
|
:content="String(props.node.content ?? '')"
|
||||||
|
:custom-id="props.customId"
|
||||||
|
:is-dark="props.isDark"
|
||||||
|
:custom-html-tags="['thinking']"
|
||||||
|
:typewriter="false"
|
||||||
|
:viewport-priority="false"
|
||||||
|
:defer-nodes-until-visible="false"
|
||||||
|
:max-live-nodes="0"
|
||||||
|
:batch-rendering="false"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
That nested-renderer pattern is also what keeps repeated and nested custom tags predictable.
|
||||||
|
|
||||||
|
## 3. What the parser gives you
|
||||||
|
|
||||||
|
For a trusted custom tag, the emitted node typically includes:
|
||||||
|
|
||||||
|
- `type`: the tag name, for example `thinking`
|
||||||
|
- `tag`: the original tag name
|
||||||
|
- `content`: the inner Markdown or text content
|
||||||
|
- `attrs`: extracted tag attributes when available
|
||||||
|
- `loading`: whether the tag is still in a streaming mid-state
|
||||||
|
- `autoClosed`: whether the parser temporarily auto-closed the tag during streaming
|
||||||
|
|
||||||
|
The exact `attrs` shape can vary, so treat it as raw attribute data that your component normalizes for its own needs.
|
||||||
|
|
||||||
|
## 4. Repeated and nested custom tags
|
||||||
|
|
||||||
|
This flow is designed to support:
|
||||||
|
|
||||||
|
- repeated tags in the same document
|
||||||
|
- nested custom tags
|
||||||
|
- streaming mid-states while the closing tag has not arrived yet
|
||||||
|
|
||||||
|
Tips that help in practice:
|
||||||
|
|
||||||
|
- pass the same `custom-html-tags` list into nested renderers
|
||||||
|
- disable batching and viewport deferral inside small nested shells when you want the most predictable streaming behavior
|
||||||
|
- keep the outer renderer scoped with `custom-id`
|
||||||
|
|
||||||
|
## 5. When `custom-html-tags` is enough, and when it is not
|
||||||
|
|
||||||
|
Use `custom-html-tags` plus `setCustomComponents` when:
|
||||||
|
|
||||||
|
- the syntax is already tag-like
|
||||||
|
- you trust the source
|
||||||
|
- you mainly need a different renderer, not a different grammar
|
||||||
|
|
||||||
|
Move to [Advanced Parser Hooks](/guide/advanced) when:
|
||||||
|
|
||||||
|
- tags need token rewriting before they become stable nodes
|
||||||
|
- you must merge, split, or reshape nodes after parsing
|
||||||
|
- the source format is not well represented by a tag-like wrapper
|
||||||
|
|
||||||
|
## 6. Scope and cleanup still matter
|
||||||
|
|
||||||
|
Even for custom tags, prefer scoped mappings:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { removeCustomComponents, setCustomComponents } from 'markstream-vue'
|
||||||
|
|
||||||
|
declare const ThinkingNode: Component
|
||||||
|
|
||||||
|
setCustomComponents('chat', { thinking: ThinkingNode })
|
||||||
|
|
||||||
|
// later, when the scoped renderer is no longer needed
|
||||||
|
removeCustomComponents('chat')
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps custom behavior local to the page, route, or app surface that actually needs it.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# D2 quick start
|
||||||
|
|
||||||
|
D2 diagrams render progressively in markstream-vue: as soon as the syntax becomes valid, a preview appears and keeps refining as more tokens stream in. If rendering fails, the last successful preview stays visible and the source is preserved.
|
||||||
|
|
||||||
|
## 1. Install & import
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @terrastruct/d2
|
||||||
|
```
|
||||||
|
|
||||||
|
No extra CSS import is required. Keep `markstream-vue/index.css` after your reset and inside `@layer components` if you use Tailwind/UnoCSS.
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import 'modern-css-reset';
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
@import 'markstream-vue/index.css';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Streaming example
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MarkdownRender from 'markstream-vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const content = ref('')
|
||||||
|
const steps = [
|
||||||
|
'```d2\n',
|
||||||
|
'direction: right\n',
|
||||||
|
'Client -> API: request\n',
|
||||||
|
'API -> DB: query\n',
|
||||||
|
'DB -> API: rows\n',
|
||||||
|
'API -> Client: response\n',
|
||||||
|
'```\n',
|
||||||
|
]
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
const id = setInterval(() => {
|
||||||
|
content.value += steps[i] || ''
|
||||||
|
i++
|
||||||
|
if (i >= steps.length)
|
||||||
|
clearInterval(id)
|
||||||
|
}, 120)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<MarkdownRender :content="content" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Quick try — paste this Markdown into a page or component:
|
||||||
|
|
||||||
|
```md
|
||||||
|
\`\`\`d2
|
||||||
|
direction: right
|
||||||
|
Client -> API: request
|
||||||
|
API -> DB: query
|
||||||
|
DB -> API: rows
|
||||||
|
API -> Client: response
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Advanced component: `D2BlockNode`
|
||||||
|
|
||||||
|
Use `D2BlockNode` if you want toolbar controls (copy/export/mode toggle) or to override themes and progressive timing.
|
||||||
|
|
||||||
|
```vue twoslash
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { D2BlockNodeProps } from 'markstream-vue'
|
||||||
|
import { D2BlockNode } from 'markstream-vue'
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
type: 'code_block',
|
||||||
|
language: 'd2',
|
||||||
|
code: 'direction: right\nClient -> API: request\nAPI -> DB: query',
|
||||||
|
raw: 'direction: right\nClient -> API: request\nAPI -> DB: query',
|
||||||
|
} satisfies D2BlockNodeProps['node']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<D2BlockNode :node="node" :progressive-interval-ms="500" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
Props to know:
|
||||||
|
- `progressiveRender`, `progressiveIntervalMs` control incremental re-rendering while streaming.
|
||||||
|
- `themeId`, `darkThemeId` let you pin a D2 theme.
|
||||||
|
- `showHeader`, `showModeToggle`, `showCopyButton`, `showExportButton`, `showCollapseButton` control the toolbar.
|
||||||
|
|
||||||
|
## 4. Dark mode
|
||||||
|
|
||||||
|
Pass `:is-dark="true"` (or add a `.dark` ancestor via `MarkdownRender`) to render the diagram in dark mode. markstream-vue prefers the app theme over OS preference to keep previews stable.
|
||||||
|
|
||||||
|
## 5. Custom loaders (optional)
|
||||||
|
|
||||||
|
If you disabled the loader earlier, or want to load from a CDN/global build:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import { enableD2, setD2Loader } from 'markstream-vue'
|
||||||
|
|
||||||
|
// Re-enable the default loader
|
||||||
|
enableD2()
|
||||||
|
|
||||||
|
// Or supply your own loader
|
||||||
|
setD2Loader(() => (window as any).D2)
|
||||||
|
```
|
||||||
|
|
||||||
|
If `@terrastruct/d2` is missing, the renderer falls back to showing the source text until it becomes available.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Docs assets & screenshots
|
||||||
|
|
||||||
|
If you're adding screenshots or GIFs to the documentation, please follow these guidelines:
|
||||||
|
|
||||||
|
- Keep file size small (use SVG where possible or optimized PNG/JPG/GIF)
|
||||||
|
- Store assets in `docs/.vitepress/public/` so they are served as static files
|
||||||
|
- Use descriptive filenames and include a caption under the image
|
||||||
|
- Prefer SVG for diagrams and small screenshots
|
||||||
|
|
||||||
|
Example usage in a markdown page:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Thanks for making docs better! Please link the original playground page if you add a screenshot for reproducibility.
|
||||||
|
|
||||||
|
Quick try — add an SVG screenshot (example):
|
||||||
|
|
||||||
|
```md
|
||||||
|

|
||||||
|
```
|
||||||