npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@one2x/medeo-rte

v0.3.1

Published

自绘文档渲染引擎:字符流模型,排版输出纯几何,绘制后端可替换(HTML / Canvas)

Readme

medeo-rte

自绘的文档渲染引擎。不使用 contenteditable —— 文档是一条字符流,排版自己算出每个字的坐标,绘制后端可替换。

同一份排版结果可以画到 DOM,也可以一键切到 Canvas,光标和选区不变。

在线 demo:https://editor-html-demo.pages.dev/

仓库结构

src/     SDK。发布出去的就是这部分
demo/    演示应用。同时是 SDK 的用法样例,不参与发布

SDK 只负责编辑区本身。工具栏、状态栏、小地图、基线网格这些宿主外壳全在 demo/, 产品接入时应自己实现——照着 demo/main.ts 抄即可。模板混排样例在 demo/vblocks.ts。

在其它项目里使用

npm i @one2x/medeo-rte
import { Editor } from '@one2x/medeo-rte';
import '@one2x/medeo-rte/style.css';

const editor = new Editor(document.getElementById('app')!);

// 下命令
editor.commands.toggleBold();

// 同步自己的工具栏
editor.on('statechange', () => renderToolbar(editor.getState()));

// 存取内容(Slice 可 JSON 序列化)
const json = editor.getContent();
editor.setContent(json);

公式需要单独引入,因为它依赖 KaTeX + MathLive,是包体积的大头:

import { builtinEmbedPlugins, Editor } from '@one2x/medeo-rte';
import { formulaPlugin } from '@one2x/medeo-rte/formula';

new Editor(el, { plugins: [...builtinEmbedPlugins, formulaPlugin] });

不引入就完全不会打进产物(katex / mathlive 是可选 peer 依赖)。

主要能力

文字与段落样式、列表、表格、链接、图文绕排、Markdown 边打边转、撤销重做、中文 IME。

嵌入对象走同一套插件接口:图片、视频、代码块、公式、同步块引用、以及下面三类「槽 / 胶囊」。名字都带 slot,用途完全不同,不要混用。

三类 slot 对照(接入必读)

| | internalSlot | hostSlot | virtualSlot(+ region) | | --- | --- | --- | --- | | 是什么 | SDK 内置芯片(slotPlugin) | 宿主自绘芯片 | 虚拟块文本槽;外层皮是 region | | 落在哪 | EmbedEntity,流上一个 * | 同上 | virtualBlocks 区间,不占额外字符 | | 内容 | 实体字段(label / slotValue 等) | hostType + data,DOM 由宿主画 | 就是那段 spacers 文字 | | 折行 | 整颗盒子,放不下换行 | 同左 | 槽里的字跟外层句子一起按字折 | | 谁用 | demo / 简单内置控件 | 产品业务胶囊(下拉、@、上传等) | 模板里可填灰字、外层底色分组 | | 插入 | commands.insertSlot('text'\|'image'\|'select') | commands.insertHostEmbed(...) 或 insertSlice 的 entities | insertSlice 的 virtualBlocks,或 insertVirtualBlock |

另外还有 同步块(fragment / ref):选区提成可复用内容,改一处全变——和上面三类无关。

产品模板句里「可填写的那截灰字」用 virtualSlot,不要用 internalSlot / hostSlot。后两者是另一颗盒子,点进去才填,不能按字切开。

虚拟块怎么用(业务侧)

虚拟块是盖在字符流上的假块:负责皮(背景、圆角)和「整段选中」交互。区间里的字仍是文档正文。用户打字不会新建虚拟块,只能由业务 API 插入。

三种 kind:

  • mark:纯样式叠层(高亮底色、边框、文字缺省色等)。交互与普通文本流完全一致:点击落光标、方向键逐字走、能选中、能编辑;不进选中/进出状态机,不参与嵌套(parentId 恒为 null),可与其他块任意叠放。右边缘继续输入算区间内(背景跟着延伸),遇到换行就断;区间内文字删光后自动移除。样式只需配 chrome.background、chrome.radius(圆角,默认 3;折行时只有首行的上两角和末行的下两角是圆角,其余角为直角,单行自然四角全圆)、chrome.paddingX(水平内边距)和 chrome.hoverBackground(鼠标悬停时整段含折行各行换成这个底色):

    editor.insertVirtualBlock({
      kind: 'mark',
      chrome: { background: '#fff3bf', hoverBackground: '#ffe066', radius: 4, paddingX: 5 }
    });

    paddingX 只外扩底色/边框,不参与排版(字形位置、折行、行高都不变),行首/行尾也会外扩,所以相邻文字和相邻 mark 的底色可能互相重叠;行首行尾的外扩部分画在内容区外:SDK 默认让画布和 .mini-page-wrap 左右各多出 5px(负 margin 抵消,文字位置不变),所以 paddingX 在 5px 以内开箱即用,更大就要靠编辑器 padding.x 补足。宿主若覆盖 .mini-page-wrap 的 padding,请保留 padding-inline: 5px; margin-inline: -5px;,否则文字会左移 5px、外扩被裁。不支持垂直内边距。

  • region:一层皮 / 分组。整段选中时打字、删除会被忽略(保住模板壳)。没有 editing 模式。

  • virtualSlot:最内层文本槽。可进 editing;空着时显示 placeholder,填了就是用户字;整段删回 placeholder,槽本身留下。

嵌套规则(针对 region / virtualSlot):只允许包含、不允许交叉;virtualSlot 必须是叶子。mark 不受此限。日常编辑里业务不用维护 [start, end),插删字时 SDK 自动挪区间。

路径 A:整段模板一次插入(推荐)

文案 + 槽 + 胶囊一起走 insertSlice。virtualBlocks 里的下标是相对这段 text 的半开区间:

editor.insertSlice({
  text: 'Write a prompt for Input a theme.\n',
  styles: [],
  entities: [],
  virtualBlocks: [
    {
      id: 'region',
      parentId: null,
      kind: 'region',
      start: 0,
      end: 33,
      chrome: { background: 'rgba(43,108,255,0.08)', radius: 8 }
    },
    {
      id: 'slot',
      parentId: 'region',
      kind: 'virtualSlot',
      start: 19,
      end: 32,
      placeholder: 'Input a theme',
      empty: true,
      chrome: { background: 'rgba(16,185,129,0.18)', placeholderColor: '#98a2b3' }
    }
  ]
});

完整样例见 demo/vblocks.ts。

路径 B:给已有字套皮

// 不传 start/end → 用当前选区
editor.insertVirtualBlock({
  kind: 'region',
  chrome: { background: 'rgba(43,108,255,0.08)', radius: 8 }
});

editor.insertVirtualBlock({
  kind: 'virtualSlot',
  placeholder: 'Input a theme',
  empty: true,
  chrome: { background: 'rgba(16,185,129,0.18)', placeholderColor: '#98a2b3' }
});

// 也可显式下标
editor.insertVirtualBlock({ kind: 'region', start: 0, end: 20, chrome: { ... } });

可配的 chrome 字段:background、hoverBackground(仅 mark)、radius、borderColor / borderWidth、paddingX(仅水平)、文字缺省(color、fontSize、fontFamily、fontWeight、bold、italic)、placeholderColor(仅 virtualSlot)。

改皮 / 拆皮:updateVirtualBlock(id, chrome)、removeVirtualBlock(id)(只拆皮,字和胶囊留下)。

业务浮层(Sample / AI 等)

SDK 不画业务菜单。宿主自己:

  1. 听 statechange,看 getState().vblockStack 栈顶是否是某槽的 editing(点击或键盘进入都会触发)
  2. editor.getVirtualBlockRect(id) 拿屏幕坐标(clientX/Y,含滚动)定位浮层:left/top/width/height 是外接矩形,折行的块还有 lines;下拉放在 left / top + height 即可。滚动或重排后(layoutchange)重新取
  3. 写入:槽正处于 editing 时直接 editor.insertText(text)——空槽会整段覆盖 placeholder,不会进段落
  4. 关闭浮层:栈顶不再是该槽,或点到编辑器外(宿主自己监听)

和 host 胶囊一起用

胶囊是流上的 *,可以落在 region 覆盖范围内,但不是虚拟块的子节点。自定义芯片:

const editor = new Editor(el, {
  hostEmbed: {
    mount(container, ctx) {
      // 按 ctx.entity.hostType 画自己的组件;实体 kind 为 'hostSlot'
      return { update(next) {}, destroy() {} };
    }
  }
});
editor.commands.insertHostEmbed('Upscale', { label: '@Upscale' }, { width: 88, height: 24 });

更细的模型与选区栈见 virtual-blocks.md。

API 一览

| 成员 | 说明 | | --- | --- | | new Editor(el, options?) | doc / paint / plugins / hostEmbed / width / padding / placeholder / textDefaults / shortcuts / autofocus / chrome | | commands | 公开编辑命令(加粗、插表、插图、insertHostEmbed、同步块等),宿主工具栏调这些 | | on(event, fn) | change / selectionchange / statechange / layoutchange,返回退订函数 | | getState() | 当前状态快照(含 vblockStack),用于同步 UI | | getContent() / setContent() | 整篇内容读写,Slice 结构(含 virtualBlocks) | | insertSlice(slice, at?) | 插入切片;模板混排走这条 | | insertVirtualBlock / updateVirtualBlock / removeVirtualBlock | 套皮、改 chrome、拆皮 | | insertHostEmbed(hostType, data?, size?, at?) | 插入宿主自绘胶囊 | | getSelection() / setSelection() | 选区读写 | | getLayout() | 排版几何(只读),做标尺、小地图、槽上浮层用 | | getDocument() | 文档模型,做批量操作或自定义命令用 | | setPaintMode('html'\|'canvas') | 切换绘制后端 | | setChrome / setTextDefaults | 光标选区外观;默认字号/颜色(主题切换) | | root / scroller / pageElement | 根节点 / 滚动容器 / 内容定位上下文 | | focus() / destroy() | | | setPlaceholder(text) | 空态占位;组合输入一开始即隐藏 | | composing() / setDisabled() | 组合态查询;禁用输入与快捷键 | | INPUT_SHORTCUTS / inputShortcuts() | 输入框快捷键;默认 Enter 换行,Cmd+Enter 留给宿主 |

脚本化改文档也可 import { operations, runOperation } from '@one2x/medeo-rte',不要直接改 DocumentModel 的内部表。

性能探针(开发用)

默认关闭、零开销。在浏览器控制台里:

editor.perf.start()   // demo 里 window.editor 已暴露;业务里用自己的 editor 实例
// 连续打字 / 按住方向键 / 中文输入法连续上屏 / 按快捷键……
editor.perf.report()  // 输出各阶段耗时(平均 / p95 / 最大 ms)和帧率
editor.perf.stop()

详细读法与历次优化记录见 docs/performance.md。输出包含:insertText、handleComposition、moveCaret 等入口的同步耗时;relayout、paintDoc、paintChrome、scrollCaret 等阶段耗时;xxx→下一帧(处理开始到下一帧开始,近似输入延迟);以及帧率、帧间隔 p95、掉帧数。120Hz 屏的帧预算是 8.3ms。

设计文档

docs/ 下是分层架构与技术方案:

| 文档 | 内容 | | --- | --- | | architecture.md | 分层结构、五条不变式、样式模型 | | mutations.md | 原子操作的类型、坐标语义、三套 handler 契约 | | operations.md | 编排层规范与 Operation 清单 | | edits.md | 事务边界、撤销栈、片段坐标空间、协同预留 | | rendering.md | 块级增量排版、脏区带、绘制后端 | | virtual-blocks.md | 虚拟块:模板混排、文本槽、按层选区 | | agent-segment-mapping.md | 面向 Agent:用户的 segment 概念如何对应到真实结构与 API | | performance.md | 性能目标、探针用法、长文档优化记录与待办 |

改动模型前先读 architecture.md 的不变式那一节。

本地开发

npm install
npm run dev          # http://localhost:5273/  跑 demo

| 命令 | 作用 | | ------------------- | ----------------------------- | | npm run dev | 起 demo 开发服务器 | | npm run build | 构建 demo 站点 → dist-demo/ | | npm run build:lib | 构建 SDK → dist/(含 .d.ts) | | npm run selfcheck | 分层自检:编辑语义、变换算术、撤销栈、快捷键表、增量排版 |

发布到 npm

包名 @one2x/medeo-rte。在 main 上打与版本号一致的 tag,Actions 会自动发布。

  1. 把要发的改动合进 main
  2. 改 package.json 的 version(例如 0.1.1),一并合进 main
  3. 在 main tip 打同名 tag 并推送:
git checkout main && git pull
git tag v0.1.1
git push origin v0.1.1

tag 必须指向 main 上的提交,且 v* 与 package.json 的 version 一致,否则 Publish 会失败。