Skill: baojun-billion/pretext-reporter-bao (version 0.1.0)

--- name: pretext-reporter-bao description: 文本测量和Canvas布局报告工具 - 基于Pretext库，支持多语言文本测量、行布局计算、可视化报告生成 compatibility: opencode --- # Pretext Reporter 基于 Pretext 库的文本测量和布局报告工具，支持多语言（包括表情符号、BIDI、CJK）、Canvas 可视化输出。 ## 主要功能 ### 文本测量 - 快速测量文本高度和宽度 - 支持所有语言（中文、日文、韩文、阿拉伯语、希伯来语等） - 表情符号和混合文本支持 - 避免DOM布局回流，纯算术计算 ### 行布局计算 - 自动换行计算 - 支持固定宽度和行高 - 流式布局支持（可变宽度） - Shrink Wrap - 找到最窄的适配容器 ### 报告生成 - 文本统计（字符数、行数） - 布局信息（高度、宽度） - 多种输出格式（Markdown、JSON） - Canvas 可视化（PNG 导出） ## 使用场景 1. **虚拟列表高度计算** - 精确计算项目容器高度 2. **动态UI布局** - 预先测量文本防止布局偏移 3. **Canvas/SVG渲染** - 文本渲染到Canvas或SVG 4. **开发时验证** - 测试按钮标签是否溢出 5. **文本编辑器** - 实现自定义文本编辑和测量 ## 核心优势 - **性能优异** - prepare 约19ms，layout 约0.09ms（500个文本批次） - **无DOM依赖** - 纯算术计算，不触发浏览器回流 - **精确测量** - 使用浏览器字体引擎作为基准 - **全面支持** - 所有语言、表情符号、混合BIDI ## API 对接 使用 Pretext 的核心 API： ```typescript // 用例1：简单测量 import { prepare, layout } from '@chenglou/pretext' const prepared = prepare('你好世界 🚀', '16px Inter') const { height, lineCount } = layout(prepared, 320, 24) // 用例2：行级控制 import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext' const prepared = prepareWithSegments('你好世界 🚀', '16px Inter') const { lines } = layoutWithLines(prepared, 320, 24) // 用例3：流式布局 import { layoutNextLine } from '@chenglou/pretext' let cursor = { segmentIndex: 0, graphemeIndex: 0 } let y = 0 while (true) { const width = y < image.bottom ? 300 : 320 const line = layoutNextLine(prepared, cursor, width) if (line === null) break ctx.fillText(line.text, 0, y) cursor = line.end y += 24 } ``` ## 使用方法 ### 基础用法 ```typescript // 测量文本 import { measureText } from '@claw/skills/pretext-reporter-bao' const result = await measureText('你好世界 🚀', { font: '16px Inter', maxWidth: 320, lineHeight: 24 }) console.log(result) // { // text: '你好世界 🚀', // characterCount: 5, // lineCount: 1, // height: 24, // lines: [{ text: '你好世界 🚀', width: 87.5 }] // } ``` ### 生成Canvas报告 ```typescript // 生成Canvas可视化 import { generateCanvasReport } from '@claw/skills/pretext-reporter-bao' const canvas = await generateCanvasReport('你好世界 🚀', { font: '16px Inter', maxWidth: 320, lineHeight: 24, backgroundColor: '#ffffff', textColor: '#000000' }) // 保存或使用Canvas const png = canvas.toDataURL('image/png') ``` ### 流式布局 ```typescript // 流式布局（支持图片环绕等） import { createFlowLayout } from '@claw/skills/pretext-reporter-bao' const layout = createFlowLayout('你好世界 🚀', { font: '16px Inter', lineHeight: 24 }) // 逐行布局 let line while ((line = layout.nextLine(320)) !== null) { console.log(line.text, line.width) } ``` ## 配置选项 | 选项 | 类型 | 默认值 | 说明 | |------|------|---------|------| | `font` | string | `'16px Inter'` | 字体（与CSS font属性格式一致） | | `maxWidth` | number | `320` | 最大宽度（像素） | | `lineHeight` | number | `24` | 行高（像素） | | `whiteSpace` | `'normal' \| 'pre-wrap'` | `'normal'` | 空白处理方式 | | `backgroundColor` | string | `'#ffffff'` | Canvas背景色 | | `textColor` | string | `'#000000'` | Canvas文字颜色 | ## 输出格式 ### Markdown报告 ```markdown # 文本测量报告 ## 基本信息 - 文本：你好世界 🚀 - 字符数：5 - 行数：1 - 字体：16px Inter ## 布局信息 - 最大宽度：320px - 计算高度：24px - 行高：24px ## 行详情 | 行号 | 文本 | 宽度 | |------|------|------| | 1 | 你好世界 🚀 | 87.5px | ``` ### JSON报告 ```json { "text": "你好世界 🚀", "font": "16px Inter", "config": { "maxWidth": 320, "lineHeight": 24, "whiteSpace": "normal" }, "stats": { "characterCount": 5, "lineCount": 1, "height": 24 }, "lines": [ { "index": 0, "text": "你好世界 🚀", "width": 87.5, "start": { "segmentIndex": 0, "graphemeIndex": 0 }, "end": { "segmentIndex": 0, "graphemeIndex": 5 } } ] } ``` ## 性能基准 | 操作 | 时间 | 说明 | |------|------|------| | `prepare()` | ~19ms | 500个文本批次（一次性分析） | | `layout()` | ~0.09ms | 热路径（纯算术计算） | | `layoutWithLines()` | ~0.12ms | 包含行信息返回 | ## 依赖 - `@chenglou/pretext` - 核心文本测量库（需手动安装） ## 安装说明 **前置步骤：Pretext 还未发布到 npm，需要手动安装：** ```bash # 方式1：从 GitHub 克隆 git clone https://github.com/chenglou/pretext.git cd pretext npm install npm link # 全局链接 # 方式2：在项目中本地引用 # 将 pretext 目录复制到 node_modules/@chenglou/pretext ``` **然后安装本 Skill：** ```bash cd skills/pretext-reporter-bao npm install ``` ## 参考资料 - Pretext GitHub: https://github.com/chenglou/pretext - Pretext Demo: https://chenglou.me/pretext - Pretext Docs: https://github.com/chenglou/pretext#readme

path: dist/canvas-mock.d.ts
content: /**
 * Pretext 测试脚本
 * 测试文本测量和布局功能
 */
declare class MockCanvas {
    private ctx;
    constructor(width: number, height: number);
    get context(): MockContext;
    toDataURL(type?: string): string;
}
declare class MockContext {
    private width;
    private height;
    private fillColor;
    private font;
    private textAlign;
    private textBaseline;
    private content;
    constructor(width: number, height: number);
    get canvas(): {
        width: number;
        height: number;
    };
    fillRect(x: number, y: number, w: number, h: number): void;
    fillStyle: string;
    set fillStyle(value: string);
    measureText(text: string): {
        width: number;
        actualBoundingBoxLeft: number;
        actualBoundingBoxRight: number;
        actualBoundingBoxAscent: number;
        actualBoundingBoxDescent: number;
        fontBoundingBoxAscent: number;
        fontBoundingBoxDescent: number;
    };
}
export { MockCanvas, MockContext };
//# sourceMappingURL=canvas-mock.d.ts.map

path: dist/canvas-mock.js
content: /**
 * Pretext 测试脚本
 * 测试文本测量和布局功能
 */
// 模拟 Canvas API（用于测试，实际环境需要浏览器）
class MockCanvas {
    constructor(width, height) {
        this.ctx = new MockContext(width, height);
    }
    get context() {
        return this.ctx;
    }
    toDataURL(type) {
        return `data:${type || 'image/png'};base64,iVBORw0KG...`;
    }
}
class MockContext {
    constructor(width, height) {
        this.fillColor = '#000000';
        this.font = '16px Inter';
        this.textAlign = 'left';
        this.textBaseline = 'top';
        this.content = [];
        this.fillStyle = this.fillColor;
        this.width = width;
        this.height = height;
    }
    get canvas() {
        return {
            width: this.width,
            height: this.height,
        };
    }
    fillText(text, x, y) {
        this.content.push(`fillText: "${text}" at (${x}, ${y})`);
    }
    fillRect(x, y, w, h) {
        this.content.push(`fillRect: (${x}, ${y}, ${w}, ${h})`);
    }
    set fillStyle(value) {
        this.fillColor = value;
    }
    fillText(text, x, y) {
        this.content.push(`fillText: "${text}" at (${x}, ${y})`);
    }
    measureText(text) {
        // 简单估算：每个字符约 8px 宽
        const width = text.length * 8;
        return {
            width,
            actualBoundingBoxLeft: 0,
            actualBoundingBoxRight: width,
            actualBoundingBoxAscent: 14,
            actualBoundingBoxDescent: 4,
            fontBoundingBoxAscent: 14,
            fontBoundingBoxDescent: 4,
        };
    }
}
// 导出 MockCanvas
export { MockCanvas, MockContext };


path: dist/index.d.ts
content: /**
 * Pretext Reporter Bao
 * 基于 Pretext 库的文本测量和布局工具
 */
import { type LayoutLine } from '../pretext/dist/layout.js';
/**
 * 文本测量结果
 */
export interface MeasureResult {
    text: string;
    font: string;
    characterCount: number;
    lineCount: number;
    height: number;
    maxWidth: number;
    lineHeight: number;
    width: number;
    lines: LayoutLine[];
}
/**
 * 流式布局器
 */
export declare class FlowLayout {
    private prepared;
    private cursor;
    private font;
    private lineHeight;
    constructor(text: string, font: string, lineHeight: number);
    /**
     * 布局下一行
     */
    nextLine(maxWidth: number): LayoutLine | null;
    /**
     * 重置游标
     */
    reset(): void;
}
/**
 * 文本测量选项
 */
export interface MeasureOptions {
    font?: string;
    maxWidth?: number;
    lineHeight?: number;
    whiteSpace?: 'normal' | 'pre-wrap';
}
/**
 * Canvas 选项
 */
export interface CanvasOptions extends MeasureOptions {
    backgroundColor?: string;
    textColor?: string;
    padding?: number;
}
/**
 * Shrink Wrap 选项
 */
export interface ShrinkWrapOptions extends MeasureOptions {
    maxWidth?: number;
    step?: number;
}
/**
 * 测量文本并返回布局信息
 */
export declare function measureText(text: string, options?: MeasureOptions): Promise<MeasureResult>;
/**
 * 生成 Canvas 可视化报告
 */
export declare function generateCanvasReport(text: string, options?: CanvasOptions): Promise<any>;
/**
 * 生成 Markdown 格式的报告
 */
export declare function generateMarkdownReport(result: MeasureResult): string;
/**
 * 生成 JSON 格式的报告
 */
export declare function generateJSONReport(result: MeasureResult): object;
/**
 * 创建流式布局器
 */
export declare function createFlowLayout(text: string, options?: MeasureOptions): FlowLayout;
/**
 * 找到最窄的适配宽度（Shrink Wrap）
 */
export declare function findTightestWidth(text: string, options?: ShrinkWrapOptions): number;
//# sourceMappingURL=index.d.ts.map

path: dist/index.js
content: /**
 * Pretext Reporter Bao
 * 基于 Pretext 库的文本测量和布局工具
 */
// 使用本地 Pretext
import { prepare, layout, prepareWithSegments, layoutWithLines, layoutNextLine, measureNaturalWidth } from '../pretext/dist/layout.js';
/**
 * 流式布局器
 */
export class FlowLayout {
    constructor(text, font, lineHeight) {
        this.prepared = prepareWithSegments(text, font);
        this.cursor = { segmentIndex: 0, graphemeIndex: 0 };
        this.font = font;
        this.lineHeight = lineHeight;
    }
    /**
     * 布局下一行
     */
    nextLine(maxWidth) {
        const line = layoutNextLine(this.prepared, this.cursor, maxWidth);
        if (line === null) {
            return null;
        }
        this.cursor = line.end;
        return line;
    }
    /**
     * 重置游标
     */
    reset() {
        this.cursor = { segmentIndex: 0, graphemeIndex: 0 };
    }
}
/**
 * 测量文本并返回布局信息
 */
export async function measureText(text, options = {}) {
    const { font = '16px Inter', maxWidth = 320, lineHeight = 24 } = options;
    const prepared = prepareWithSegments(text, font);
    const result = layoutWithLines(prepared, maxWidth, lineHeight);
    const naturalWidth = measureNaturalWidth(prepared);
    return {
        text,
        font,
        characterCount: text.length,
        lineCount: result.lineCount,
        height: result.height,
        maxWidth,
        lineHeight,
        width: Math.min(naturalWidth, maxWidth),
        lines: result.lines
    };
}
/**
 * 生成 Canvas 可视化报告
 */
export async function generateCanvasReport(text, options = {}) {
    const { font = '16px Inter', maxWidth = 320, lineHeight = 24, backgroundColor = '#ffffff', textColor = '#000000', padding = 20 } = options;
    const prepared = prepareWithSegments(text, font);
    const layoutResult = layoutWithLines(prepared, maxWidth, lineHeight);
    const naturalWidth = measureNaturalWidth(prepared);
    // 创建 Canvas（需要在浏览器环境中）
    // 这里返回配置对象，实际使用时需要传入 Canvas
    return {
        config: {
            text,
            font,
            maxWidth,
            lineHeight,
            backgroundColor,
            textColor,
            padding
        },
        layout: layoutResult,
        width: Math.max(maxWidth + padding * 2, Math.min(naturalWidth, maxWidth) + padding * 2),
        height: layoutResult.height + padding * 2
    };
}
/**
 * 生成 Markdown 格式的报告
 */
export function generateMarkdownReport(result) {
    const linesInfo = result.lines.map((line, i) => `行 ${i + 1}: "${line.text}" (宽度: ${line.width.toFixed(2)}px)`).join('\n');
    return `
# 文本测量报告

## 基本信息
- **文本**: \`${result.text}\`
- **字体**: ${result.font}
- **字符数**: ${result.characterCount}
- **行数**: ${result.lineCount}
- **实际宽度**: ${result.width.toFixed(2)}px
- **高度**: ${result.height}px
- **行高**: ${result.lineHeight}px
- **最大宽度**: ${result.maxWidth}px

## 布局信息
${linesInfo}
`;
}
/**
 * 生成 JSON 格式的报告
 */
export function generateJSONReport(result) {
    return {
        metadata: {
            timestamp: new Date().toISOString(),
            version: '1.0.0'
        },
        measurement: result
    };
}
/**
 * 创建流式布局器
 */
export function createFlowLayout(text, options = {}) {
    const { font = '16px Inter', lineHeight = 24 } = options;
    return new FlowLayout(text, font, lineHeight);
}
/**
 * 找到最窄的适配宽度（Shrink Wrap）
 */
export function findTightestWidth(text, options = {}) {
    const { font = '16px Inter', lineHeight = 24, maxWidth = 1000, step = 10 } = options;
    const prepared = prepare(text, font);
    // 二分查找最窄宽度
    let low = 0;
    let high = maxWidth;
    while (high - low > step) {
        const mid = Math.floor((low + high) / 2);
        const result = layout(prepared, mid, lineHeight);
        // 如果行数增加，说明宽度不够
        if (result.lineCount > 1) {
            low = mid;
        }
        else {
            high = mid;
        }
    }
    return high;
}


path: package-lock.json
content: {
  "name": "pretext-reporter-bao",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "pretext-reporter-bao",
      "version": "1.0.0",
      "license": "MIT",
      "devDependencies": {
        "@types/node": "^20.0.0",
        "typescript": "^5.0.0"
      }
    },
    "node_modules/@types/node": {
      "version": "20.19.38",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.38.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~6.21.0"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "6.21.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    }
  }
}


path: package.json
content: {
  "name": "pretext-reporter-bao",
  "version": "0.1.0",
  "description": "文本测量和Canvas布局报告工具 - 基于Pretext库，支持多语言文本测量、行布局计算、可视化报告生成",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "test": "node test.mjs",
    "prepublishOnly": "npm run build"
  },
  "keywords": [
    "openclaw",
    "skill",
    "text-layout",
    "pretext",
    "canvas",
    "measure",
    "i18n",
    "typescript"
  ],
  "author": "Bao",
  "license": "MIT",
  "files": [
    "dist",
    "SKILL.md",
    "README.md",
    "pretext"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/baojun-billion/pretext-reporter-bao.git"
  },
  "bugs": {
    "url": "https://github.com/baojun-billion/pretext-reporter-bao/issues"
  },
  "homepage": "https://github.com/baojun-billion/pretext-reporter-bao#readme",
  "dependencies": {},
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0"
  }
}


path: pretext/CHANGELOG.md
content: # Changelog

## Unreleased

## 0.0.4 - 2026-04-02

### Added

- A justification comparison demo that shows native CSS justification, greedy hyphenation, and a Knuth-Plass-style paragraph layout side by side.
- Machine-readable status dashboards under `status/dashboard.json` and `corpora/dashboard.json` for tooling and release-time inspection.

### Changed

- Browser automation and reporting are more robust: batched sweep transport, phase-aware timeout diagnostics, background-safe correctness runs, and unified Firefox accuracy automation.
- Rich-line benchmark coverage now includes chunk-heavy and long-breakable stress cases, and chunk lookup in the rich path now uses binary search.

### Fixed

- `layout()`, `layoutWithLines()`, and `layoutNextLine()` stay aligned on narrow `ZWSP` / grapheme-breaking edge cases.
- The justification comparison demo no longer paints justified lines wider than their column.

## 0.0.3 - 2026-03-29

### Changed

- npm now publishes built ESM JavaScript from `dist/` instead of exposing raw TypeScript source as the package entrypoint.
- TypeScript consumers now pick up shipped declaration files automatically from the published package, while plain JavaScript consumers can install and import the package without relying on dependency-side TypeScript transpilation.

## 0.0.2 - 2026-03-28

### Added

- `{ whiteSpace: 'pre-wrap' }` mode for textarea-like text, preserving ordinary spaces, tabs, and hard breaks.

## 0.0.1 - 2026-03-27

### Changed

- Safari line breaking now has a clearer browser-specific policy path for narrow soft-hyphen and breakable-run cases.
- Browser tooling is more stable: fresh per-run page ports, diagnostics derived from the public rich layout API, and a non-watch `bun start` by default.

## 0.0.0 - 2026-03-26

Initial public npm release of `@chenglou/pretext`.

### Added

- `prepare()` and `layout()` as the core fast path for DOM-free multiline text height prediction.
- Rich layout APIs including `prepareWithSegments()`, `layoutWithLines()`, `layoutNextLine()`, and `walkLineRanges()` for custom rendering and manual layout.
- Browser accuracy, benchmark, and corpus tooling with checked-in snapshots and representative canaries.


path: pretext/CLAUDE.md
content: AGENTS.md

path: pretext/dist/bidi.d.ts
content: export declare function computeSegmentLevels(normalized: string, segStarts: number[]): Int8Array | null;


path: pretext/pages/demos/masonry/index.html
content: <!DOCTYPE html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />

<body>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
      background: #f0f0f0;
      overflow-y: scroll;
    }
    .card {
      position: absolute;
      background: white;
      border-radius: 8px;
      padding: 16px;
      font-size: 15px;
      line-height: 22px;
      color: #333;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
      overflow: hidden;
    }
  </style>
  <script type="module" src="./index.ts"></script>
</body>


path: pretext/pages/demos/svg.d.ts
content: declare module '*.svg' {
  const src: string
  export default src
}


path: pretext/src/text-modules.d.ts
content: declare module '*.txt' {
  const text: string
  export default text
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)