@quillmark /苗条的
编辑器+实时预览组件 Quillmark WASM消费者。一个所见即所得 VisualEditor,还有一块帆布。 预览 这将绘制编译后的文档,并将单击往返到编辑器 @quillmark/wasm 会话。香草- ts芯与薄苗条5包装。
安装
npm install @quillmark/svelte
svelte@^5.33 和 @quillmark/wasm 是对等依赖关系:会话的句柄跨越包边界,因此使用者提供双方生成它们的一个副本。
子路径
每个子路径都是它自己的模块根;捆绑器只提取您导入的条目所到达的内容。
| 进口 | 表面 |
|---|---|
@quillmark/svelte/core | 表面的共同点是: DocPath/Place 地址词汇, EditorError 频道, init. 没有UI框架。 |
@quillmark/svelte/preview | 现场预览: createPreview + <Preview>. 编辑器端没有达到任何值。 |
@quillmark/svelte/visual | 联邦所见即所得: <VisualEditor>,编解码器 createField 散文叶。 |
@quillmark/svelte | 再出口 /core. |
@quillmark/svelte/preset | 表面周围页面的可选样式表(THEMING.md)。 |
打开会话
这个 消费者 拥有会话和句柄,并驱动每个编辑;表面是它的视图。手柄来自 @quillmark/wasm Peer,从来没有在这里再出口过。
import { Engine } from '@quillmark/wasm';
import { init } from '@quillmark/svelte/core';
// Quill and Document are exported only through init().
const { Quill, Document } = await init();
const quill = Quill.fromTree(tree); // tree: Map<string, Uint8Array> of the quill dir
// A NEW document: seeded from the quill's schema.
const doc = quill.seedDocument();
// An EXISTING document: parsed back from what you stored.
const doc = Document.fromMarkdown(markdown); // canonical Quillmark markdown
const doc = Document.fromStored(json); // the versioned storage DTO (`doc.toStored()`)
const session = await new Engine().open(quill, doc);
// free() on teardown the handles you MINTED: this quill, this doc, this session.
<VisualEditor> 不需要会议,和第一 open 加载渲染构建,因此安装编辑器的主机 doc 和 quill 首先显示它,而不等待负载。会议在它后面开始: await engine.load(quill),然后 engine.open(quill, doc),因此它的第一次编译读取与此同时输入的内容(HOSTING.md)。
羽毛笔从文档中解析
存储的文档不携带其笔的字节,只携带一个引用: doc.quillRef 是 name@version,坚持降价本身。分辨率是 主机代码:读取ref,将其映射到a Quill、开放。没有表面分辨率,因此每个文档按结构保留一个分辨率。
const doc = Document.fromMarkdown(stored); // Document, from the gate above
const quill = await registry.getQuill(doc.quillRef); // your ref → Quill mapping
const session = await new Engine().open(quill, doc);
@quillmark/quiver 有这样的登记处吗?它交还的羽毛笔是这样的吗 借来的:它被缓存到每个引用并与每个调用者共享,因此释放它会影响下一个调用者。
打开命名为 不同的 Quill是相同的序列,顺序是:解析新的ref,交换编辑器的道具,释放被替换的句柄,然后在编辑器后面打开下一个会话。 <VisualEditor> 在新的键盘上重新设置键 doc (见下图); <Preview> 按金额交换({#key session})。
多么值得信任的文件啊
一、 Document 也许是读者没有写的 —一个导入、一个共享文件、来自多租户存储的一行—以及两个属性,无论如何都要占用一个属性。Markdown变成了类型化节点,然后是DOM,而不是标记字符串:这个包没有 {@html},没有 innerHTML 也没有 eval,因此文档的文本不能变成标记。并且链接仅为 http, https, mailto, tel, ftp,或者一个根本不命名方案的值;每一个其他方案绘制作为一个惰性跨度,在可编辑的表面和提示卡,油漆 $ext.editor.tips 以外的任何 contenteditable. 这个门是渲染的,而不是模型的,所以拒绝的href停留在标记上并往返:打开文档永远不会编辑它。
留给你的是文本边缘和参考。两端的降价是主人的,而且 doc.quillRef 是 文档的 Own -存储的文件命名为打开它的笔,因此ref→ Quill 上面的映射是它可以到达的模板的边界。
预览
createPreview 提供会话忽略的层:视口、DOM、DPR、点击映射。这是一种纯粹的观点:它从不呼唤 session.apply;您驱动编辑并将结果交给它 ChangeSet.
import { createPreview } from '@quillmark/svelte/preview';
const preview = createPreview(session, {
container: document.querySelector('#preview')!,
onPick: (at) => {
/* a click resolved to an address, with a caret where the compile has one */
},
onError: (err) => {
/* a page paint the backend refused; the preview shows its error state either way */
}
});
// after an edit lands on `doc`, from any source:
preview.refresh(session.update(doc)); // repaint dirtyPages ∩ visible
苗条的, <Preview {session} onPick={…} /> 暴露相同的动词(refresh, scrollToField, focusPosition, endFollow, setZoom)通过 bind:this.
<Preview> 结合 一次,在mount:交换 session, margin, onPick, onError 或者 strings 原地不改变屏幕和报告 rebind-ignored 通过 onError 在 dev 严重性,命名道具。重新挂载交换({#key session}); 驱动就地编辑 refresh(change). class 和 style 是例外,登陆到根元素并保持存活。
可视化编辑器
<VisualEditor> 是一个文档上许多小型编辑器的联合组合:每个内容页都是ProseMirror散文表面,每个标量都是一个表单控件,是编辑器自己的卡片。它直接提交给 doc 句柄。
<script lang="ts">
import { VisualEditor } from '@quillmark/svelte/visual';
</script>
<VisualEditor
{doc}
{quill}
onChange={(change) => {
/* an edit LANDED; `change.source` is 'prose' | 'field' | 'structure' */
}}
onActiveLeafChange={(active) => {
/* the active leaf: `active.field` (a DocPath), and `active.cardId` for its card */
}}
onCaretMove={(at) => {
/* the caret moved to `at.field` (a DocPath) at `at.pos` (UTF-16) */
}}
onError={(err) => {
/* a failure the editor recovered from; editing continues */
}}
diagnostics={external}
/>
换一种方式 doc 还有编辑 re-keys本身:每个叶子都重新装入新的句柄,id状态、提交-错误映射和活动地址重新装入。 quill 不是那个键的一部分-模式在每次派生时都会被重新读取,所以羽毛笔交换会自己重新投射;交换它 没有 医生报告说 rebind-ignored.
从外面开车
bind:this 到达卡片头调用的相同动词,因此工具栏、命令调色板或快捷方式不需要第二个路径进入文档。每个人都通过报告 onChange 就像滴答声一样。
<script lang="ts">
let editor: ReturnType<typeof VisualEditor> | undefined = $state();
let activeCard = $state<string | undefined>();
</script>
<VisualEditor
bind:this={editor}
{doc}
{quill}
onActiveLeafChange={(a) => (activeCard = a.cardId)}
/>
<button onclick={() => editor?.insertCard('indorsement')}>Add indorsement</button>
<button onclick={() => activeCard && editor?.moveCard(activeCard, -1)}>Move up</button>
<button onclick={() => editor?.focusField('main.subject')}>Jump to subject</button>
insertCard 把新卡递回去 cardId; removeCard, moveCard 和 setKind 拿一个。 focusField 到达任何已挂载的字段:一个散文页获取其视图的焦点,一个表单控件获取其标签上的点击所需要的相同切换,无论哪种方式,持有它的组都会首先显示。 washField 在一个字段上播放相同的到达洗涤,没有其他内容,主持人写道:焦点,滚动和封闭组保持原样,封闭组中的字段没有洗涤。卡钥匙或路径表面不支持是一个no-op报告 target-unknown 通过 onError 在 dev 严重性。
措辞
这个包裹是英文的,用一条缝代替。 strings 是键和 部分:设置你要翻译的内容,其余的保留在包中。几个关键是 访问的名字 而不是装饰(卡片控制,添加触发器),因此未翻译的表面将错误的语言读取到屏幕阅读器。 DEFAULT_VISUAL_STRINGS 是英文,导出后你可以对照它进行写作。 <Preview> 它有自己的三键 strings 当没有东西可画时,它所显示的状态。
<VisualEditor
{doc}
{quill}
strings={{
cardDelete: 'Supprimer la carte',
addCard: '+ Ajouter une carte'
}}
formatDiagnostic={(d) =>
d.code === 'validation::type_mismatch' ? `${d.path} : type incorrect` : undefined}
/>
formatDiagnostic 得到全部 Diagnostic 并返回 undefined 来传递自己的信息。验证和编辑诊断信息 code, path 和 args;解析错误和渲染警告在英文文本中包含详细信息,并保持原样(VISUAL_EDITOR.md)。
字体
可以将一系列文本设置为字体族 主人 供应,拼写 <qm-span family="…"> 在减价。使用字体文件(.ttf, .otf, .ttc, .otc),并将它列出的脸交给编辑器:然后弹出窗口提供每个族的格式,编辑器将这些脸注册到页面中,这样运行就可以绘制预览设置的脸。
const carlito = new Uint8Array(await (await fetch('/fonts/Carlito-Regular.ttf')).arrayBuffer());
const engine = new Engine({ fonts: [carlito] });
<script lang="ts">
let fonts = $state<FontFace[]>([]);
engine.fontFaces(quill).then((faces) => (fonts = faces));
</script>
<VisualEditor {doc} {quill} {fonts} />
@quillmark/fonts 是每个Quillmark客户端构建其引擎的库,因此一个家族的文档名称在每个文档中设置相同的方式;主机自己的文件跟踪它。 fontFaces 加载渲染构建,如 open 是的,所以先安装编辑器,让面孔到达它后面。文档已经设置了一个没有库的编辑,选择器提供了它,所以它可以被清除。
重新编译
onChange 信号是要重新编译的,它又覆盖了吗 三条车道:一个散文按键,一个标量写入,一个卡片操作。 onCaretMove 是一个 选择 信号,而不是更改信号:它在一个空箭头键上触发,因此挂起的重新编译会在每个箭头键上重新编译。
let timer: ReturnType<typeof setTimeout> | undefined;
onChange: (change) => {
// a structure op happens once per gesture, so it applies at once;
// prose and field edits arrive per keystroke, so they debounce.
if (change.source === 'structure') recompileNow();
else {
clearTimeout(timer);
timer = setTimeout(recompileNow, 120);
}
};
function recompileNow() {
timer = undefined;
const change = session.update(doc);
preview.refresh(change);
source.refresh();
diagnostics = [...session.warnings]; // → the editor's `diagnostics` prop
}
session.warnings 是一个 Svelte不跟踪的句柄上的getter:每次应用时拉出它,否则编辑器会显示前一次编译的错误。
调度是主机策略,因此包不提供调度程序。义务:您自己的编辑(导入,撤销,直接) doc Write)通过相同的调用重新编译,因为没有轮询文档;编译器重新读取诊断;然后拆除计时器 之前 释放手柄。
插入式桥
那座桥住在 消费者 层和是可选的;编辑不知道预览,预览也不知道编辑。两个跳点都是传递:两个表面已经使用一个地址语法,即规范 DocPath.
// preview → editor: a click resolved to an address lands in the field it names.
onPick: (at) => visualEditor.setCaret(at);
// editor → preview: a caret move scrolls the preview to follow it,
// and a focus into a leaf with no caret to report ends the follow.
onCaretMove: preview.focusPosition;
onActiveLeafChange: preview.endFollow;
编辑器→预览跳转或两者都不跳转。控件报告它的焦点,没有插入符号——没有日期字段可以命名的偏移量——所以如果没有插入符号,预览会一直跟随焦点离开的叶子,并且每次重新编译都会将窗格拉回它。一个散文页用它的下一个插入符号重新开始跟随。为自己的chrome调用读取活动叶子的主机 endFollow 从它的处理程序。
pick带有一个插入符号,编译器在其中跟踪该点下的内容,而只有一个字段,它只跟踪位置——一个标量,印版打印而不跟踪。 setCaret 两者皆有:缺席 pos 揭示和聚焦领域,这是整个什么点击板上放置的墨水可以意味着。该地址可以命名一个数组元素(main.keywords[0]),它落在那一行。
持有 Addr 它自己的地图 fieldPathForAddr (从 @quillmark/svelte/core). playground应用的分窗格路由是完整的引用shell:一个会话,两个桥接方向,编辑后的预览,以及内联路由的诊断。
错误
每个表面 onError. 它向地面报告故障 从提交边界被拒绝,卡片操作被抛出,页面绘制失败,序列化被抛出。他们都没有停止编辑。没有电线,每个都落在控制台。
onError: (err) => {
// err.code: 'commit-refused' | 'paint-failed' | … (see EditorErrorCode)
// err.severity: 'error' (runtime) | 'dev' (a contract violation)
// err.cause: whatever was thrown, unwrapped
if (err.severity === 'error') telemetry.capture(err);
};
这不是诊断通道。一个 Diagnostic 是关于 文档 在它所属的领域里画画;一个 EditorError 是关于 表面 画不出任何东西。被拒绝的标量提交会产生这两种结果。
规范的减价
doc.toMarkdown() 返回文档的规范Quillmark标记,并将其重新解析为等号 Document. 一个只读镜像是在 <pre>,在每次编辑完成后重新运行,所以它不需要这个包的表面:
el.textContent = doc.toMarkdown();
主题
表面承载着一种中性的、可超越的视觉基线:一组 --qm-* 您在任何祖先上重写的CSS自定义属性。看到 THEMING.md.
开发
这个包是一个工作空间 quillmark-js;门是根的,建筑就住在里面 prose/.
npm run build -w packages/svelte # svelte-package → dist/
npm run test -w packages/svelte # Vitest
npm run check -w packages/svelte # svelte-check
npm run dev # the playground, from the root