[{"data":1,"prerenderedAt":789},["ShallowReactive",2],{"msfe-doc:\u002Fextensions\u002Fcustom-node":3},{"id":4,"title":5,"body":6,"description":771,"extension":772,"keywords":773,"locale":781,"meta":782,"navigation":320,"order":783,"path":784,"section":785,"seo":786,"stem":787,"__hash__":788},"docs\u002Fextensions\u002Fcustom-node.zh-CN.md","自定义节点",{"type":7,"value":8,"toc":756},"minimark",[9,13,22,26,29,51,65,68,74,78,81,111,199,202,224,231,234,255,412,442,460,463,489,492,514,637,640,643,647,660,670,673,677,684,687,690,722,725,746,749,752],[10,11,12],"h2",{"id":12},"体验字段修改",[14,15,16,17,21],"p",{},"点击「待处理」将它切换为「已完成」，下方阅读预览同步改变。再读取 Markdown，确认按钮只改写 ",[18,19,20],"code",{},"[[todo]]"," 中的状态，其他正文保持原样。撤销与重做同时更新编辑和阅读视图；切换只读后不能修改字段。",[23,24],"msfe-demo",{"name":25},"status\u002Feditor",[10,27,28],{"id":28},"定义语法和字段",[14,30,31,34,35,38,39,42,43,46,47,50],{},[18,32,33],{},"status.ts"," 使用 ",[18,36,37],{},"Node.create()"," 声明语义；",[18,40,41],{},"addAttributes()"," 允许 ",[18,44,45],{},"value"," 字段并校验取值。",[18,48,49],{},"defineSource()"," 的字面分隔符语法适合单行、不嵌套的 token。",[14,52,53,56,57,60,61,64],{},[18,54,55],{},"read(document, node)"," 只读取已确认范围；不符合约定的值返回 ",[18,58,59],{},"null","，继续显示源码。复杂或嵌套语法应通过 ",[18,62,63],{},"addMarkdownExtensions()"," 提供解析规则。",[10,66,67],{"id":67},"精确写回",[14,69,70,73],{},[18,71,72],{},"patch(document, node, patch)"," 返回源码修改。这个例子保留两侧括号，只替换内部值。字段验证、只读、过期目标和撤销由编辑器统一处理。",[75,76],"msfe-api",{"name":77},"SourceDefinition",[10,79,80],{"id":80},"添加命令",[14,82,83,86,87,90,91,94,95,98,99,102,103,106,107,110],{},[18,84,85],{},"addCommands()"," 定义 ",[18,88,89],{},"insertStatus()","；",[18,92,93],{},"types.ts"," 的模块增强让 ",[18,96,97],{},"editor.commands","、",[18,100,101],{},"editor.can()"," 和 ",[18,104,105],{},"editor.chain()"," 获得类型提示。命令使用提供的 ",[18,108,109],{},"tr","，避免自己提交另一笔事务。",[112,113,118],"pre",{"className":114,"code":115,"language":116,"meta":117,"style":117},"language-ts shiki shiki-themes github-light github-light github-dark","editor.commands.insertStatus('todo')\neditor.can().insertStatus('done')\neditor.chain().insertStatus('todo').insertText(' ').run()\n","ts","",[18,119,120,143,164],{"__ignoreMap":117},[121,122,125,129,133,136,140],"span",{"class":123,"line":124},"line",1,[121,126,128],{"class":127},"sVPnS","editor.commands.",[121,130,132],{"class":131},"sgQzM","insertStatus",[121,134,135],{"class":127},"(",[121,137,139],{"class":138},"s-HEn","'todo'",[121,141,142],{"class":127},")\n",[121,144,146,149,152,155,157,159,162],{"class":123,"line":145},2,[121,147,148],{"class":127},"editor.",[121,150,151],{"class":131},"can",[121,153,154],{"class":127},"().",[121,156,132],{"class":131},[121,158,135],{"class":127},[121,160,161],{"class":138},"'done'",[121,163,142],{"class":127},[121,165,167,169,172,174,176,178,180,183,186,188,191,193,196],{"class":123,"line":166},3,[121,168,148],{"class":127},[121,170,171],{"class":131},"chain",[121,173,154],{"class":127},[121,175,132],{"class":131},[121,177,135],{"class":127},[121,179,139],{"class":138},[121,181,182],{"class":127},").",[121,184,185],{"class":131},"insertText",[121,187,135],{"class":127},[121,189,190],{"class":138},"' '",[121,192,182],{"class":127},[121,194,195],{"class":131},"run",[121,197,198],{"class":127},"()\n",[10,200,201],{"id":201},"替换组件",[14,203,204,205,208,209,212,213,216,217,102,220,223],{},"应用用 ",[18,206,207],{},".extend({ addNodeView() { ... } })"," 接入 ",[18,210,211],{},"VueNodeViewRenderer()","。",[18,214,215],{},"status-view.vue"," 接收 ",[18,218,219],{},"nodeView.attrs",[18,221,222],{},"updateAttributes()","，无需自行计算源码偏移。",[14,225,226,227,230],{},"组件树必须包含 ",[18,228,229],{},"VueNodeViewHost","。更换为 React 时保留同一语法扩展，使用对应的 renderer 与 host。",[10,232,233],{"id":233},"接入静态阅读",[14,235,236,34,239,242,243,246,247,250,251,254],{},[18,237,238],{},"MdProse",[18,240,241],{},"withMdReading(extension, definition)"," 接收第三方阅读贡献。先完成扩展的\n",[18,244,245],{},"configure()"," \u002F ",[18,248,249],{},"extend()","，再包装并安装到同一个编辑器。示例使用的公开入口是\n",[18,252,253],{},"@quirekit\u002Fmd-editor\u002Fvue","，不需要修改产品内部的语法分支。",[112,256,258],{"className":114,"code":257,"language":116,"meta":117,"style":117},"import { h } from 'vue'\nimport { withMdReading } from '@quirekit\u002Fmd-editor\u002Fvue'\nimport { Status } from '.\u002Fstatus'\nimport type { StatusAttributes } from '.\u002Ftypes'\n\nconst ReadableStatus = withMdReading\u003CStatusAttributes>(Status, {\n  render({ attrs }) {\n    return h('strong', attrs.value === 'done' ? '已完成' : '待处理')\n  },\n})\n",[18,259,260,275,287,299,315,322,347,363,400,406],{"__ignoreMap":117},[121,261,262,266,269,272],{"class":123,"line":124},[121,263,265],{"class":264},"sDD9d","import",[121,267,268],{"class":127}," { h } ",[121,270,271],{"class":264},"from",[121,273,274],{"class":138}," 'vue'\n",[121,276,277,279,282,284],{"class":123,"line":145},[121,278,265],{"class":264},[121,280,281],{"class":127}," { withMdReading } ",[121,283,271],{"class":264},[121,285,286],{"class":138}," '@quirekit\u002Fmd-editor\u002Fvue'\n",[121,288,289,291,294,296],{"class":123,"line":166},[121,290,265],{"class":264},[121,292,293],{"class":127}," { Status } ",[121,295,271],{"class":264},[121,297,298],{"class":138}," '.\u002Fstatus'\n",[121,300,302,304,307,310,312],{"class":123,"line":301},4,[121,303,265],{"class":264},[121,305,306],{"class":264}," type",[121,308,309],{"class":127}," { StatusAttributes } ",[121,311,271],{"class":264},[121,313,314],{"class":138}," '.\u002Ftypes'\n",[121,316,318],{"class":123,"line":317},5,[121,319,321],{"emptyLinePlaceholder":320},true,"\n",[121,323,325,328,332,335,338,341,344],{"class":123,"line":324},6,[121,326,327],{"class":264},"const",[121,329,331],{"class":330},"sUNQB"," ReadableStatus",[121,333,334],{"class":264}," =",[121,336,337],{"class":131}," withMdReading",[121,339,340],{"class":127},"\u003C",[121,342,343],{"class":131},"StatusAttributes",[121,345,346],{"class":127},">(Status, {\n",[121,348,350,353,356,360],{"class":123,"line":349},7,[121,351,352],{"class":131},"  render",[121,354,355],{"class":127},"({ ",[121,357,359],{"class":358},"sde8i","attrs",[121,361,362],{"class":127}," }) {\n",[121,364,366,369,372,374,377,380,383,386,389,392,395,398],{"class":123,"line":365},8,[121,367,368],{"class":264},"    return",[121,370,371],{"class":131}," h",[121,373,135],{"class":127},[121,375,376],{"class":138},"'strong'",[121,378,379],{"class":127},", attrs.value ",[121,381,382],{"class":264},"===",[121,384,385],{"class":138}," 'done'",[121,387,388],{"class":264}," ?",[121,390,391],{"class":138}," '已完成'",[121,393,394],{"class":264}," :",[121,396,397],{"class":138}," '待处理'",[121,399,142],{"class":127},[121,401,403],{"class":123,"line":402},9,[121,404,405],{"class":127},"  },\n",[121,407,409],{"class":123,"line":408},10,[121,410,411],{"class":127},"})\n",[14,413,414,417,418,420,421,98,424,98,427,102,430,433,434,437,438,441],{},[18,415,416],{},"render(context, resource)"," 的 ",[18,419,359],{}," 来自原扩展安装后的 codec，字段默认值与校验只定义\n一次。context 同时提供 ",[18,422,423],{},"name",[18,425,426],{},"node",[18,428,429],{},"document",[18,431,432],{},"locale","，不提供编辑动作。\n",[18,435,436],{},"renderInline(from?, to?)"," 与 ",[18,439,440],{},"renderBlocks(omitted?)"," 将子内容交回公共阅读器；行内范围\n必须在当前节点内且不能截断一个子语法节点，非法范围会触发局部源码回退。",[14,443,444,417,446,449,450,453,454,459],{},[18,445,238],{},[18,447,448],{},"state"," 接收原编辑器的不可变快照。示例用 ",[18,451,452],{},"const state = useMsfeState(editor)","\n接入 Vue，公共 hook 自动管理订阅与清理，见 ",[455,456,458],"a",{"href":457},"\u002Fguide\u002Fvue#%E5%B0%86%E7%BC%96%E8%BE%91%E5%99%A8%E7%8A%B6%E6%80%81%E6%8E%A5%E5%85%A5-vue","Vue 状态订阅","。\n每次输入不序列化 Markdown，纯选区与焦点变化不重复解码或加载。\n阅读只依赖源码、解析树、已安装贡献、呈现配置与 locale；codec 不应读取选区或可变外部状态\n来决定静态内容。文档变化可以保守重建阅读快照，这个接口不承诺逐节点增量更新。",[10,461,462],{"id":462},"阅读资源与失败回退",[14,464,465,466,469,470,473,474,477,478,481,482,485,486,212],{},"有 IO 的贡献可声明 ",[18,467,468],{},"load(context, signal)","，返回 ",[18,471,472],{},"ResourceLease\u003CResource>","，即\n",[18,475,476],{},"{ value, release? }","，或相应 Promise。",[18,479,480],{},"render"," 只在资源就绪后运行。取消信号在快照替换\n或卸载时触发；即使加载方忽略取消，迟到资源仍会调用 ",[18,483,484],{},"release()","。阅读资源与编辑\nNodeView 的资源分别拥有生命周期，不自动重用 ",[18,487,488],{},"addSource().load",[14,490,491],{},"待加载、失败和服务端渲染时保留原节点的精确源码；SSR 不启动加载。同步 renderer 可以\n服务端渲染。Vue 子组件继承应用 provider，客户端子组件异常也隔离在本节点。\n渲染器是宿主提供的可信代码，原始 Markdown 不会被当作 HTML 执行。",[14,493,494,495,216,498,501,502,246,505,246,508,246,510,513],{},"用 ",[18,496,497],{},"\u003CMdProse @error=\"reportReadingError\" \u002F>",[18,499,500],{},"MdReadingError","，其中包含 owner、\n",[18,503,504],{},"read",[18,506,507],{},"load",[18,509,480],{},[18,511,512],{},"dispose"," 阶段、原始异常与捕获快照的 UTF-16 范围。\n这个范围是诊断信息，不能用于异步写回当前编辑器。无错误监听器时，Vue 阅读入口只保留\n可见源码；宿主可按需接入日志。资源释放失败会被报告，不替换已经成功显示的新内容。",[515,516,517,532],"table",{},[518,519,520],"thead",{},[109,521,522,526,529],{},[523,524,525],"th",{},"场景",[523,527,528],{},"编辑 \u002F 源码",[523,530,531],{},"静态阅读",[533,534,535,547,558,569,580,593,604,615,626],"tbody",{},[109,536,537,541,544],{},[538,539,540],"td",{},"合法的自定义字段",[538,542,543],{},"由原扩展 patch 精确修改，可撤销\u002F重做",[538,545,546],{},"使用同一个 codec 的默认值与校验结果",[109,548,549,552,555],{},[538,550,551],{},"字段非法、未知或未闭合语法",[538,553,554],{},"保留原文",[538,556,557],{},"未命中贡献时保留内置展示或源码",[109,559,560,563,566],{},[538,561,562],{},"代码块 \u002F 行内代码中的 token",[538,564,565],{},"仍是代码文本",[538,567,568],{},"不额外解析成自定义节点",[109,570,571,574,577],{},[538,572,573],{},"同语法有多个语义候选",[538,575,576],{},"沿用安装顺序",[538,578,579],{},"首个 codec 返回非 null 的候选拥有该节点",[109,581,582,587,590],{},[538,583,584,586],{},[18,585,480],{}," 返回 undefined \u002F null",[538,588,589],{},"原文不变",[538,591,592],{},"undefined 保留精确源码；null 明确不输出内容",[109,594,595,598,601],{},[538,596,597],{},"加载等待、拒绝或渲染异常",[538,599,600],{},"原文不变，其他块正常",[538,602,603],{},"仅失败节点回退并可报告错误",[109,605,606,609,612],{},[538,607,608],{},"只读模式",[538,610,611],{},"字段动作与历史写入被拒绝",[538,613,614],{},"继续展示同一份内容",[109,616,617,620,623],{},[538,618,619],{},"没有 source codec \u002F 重复阅读 owner",[538,621,622],{},"明确拒绝安装",[538,624,625],{},"不创建无效阅读贡献",[109,627,628,631,634],{},[538,629,630],{},"React 或 Word\u002FPDF\u002FOFD 导出",[538,632,633],{},"保留中立语义扩展",[538,635,636],{},"此适配器只负责 Vue 阅读；导出需独立转换贡献",[10,638,639],{"id":639},"生命周期约束",[14,641,642],{},"离开视口、显露源码、换文档或销毁时，节点组件可能卸载。持久字段写回源码，临时 UI 状态按合适的生命周期管理；不要依赖组件永远挂载。",[10,644,646],{"id":645},"readpatch-与字段校验的契约","read、patch 与字段校验的契约",[14,648,649,651,652,655,656,659],{},[18,650,41],{}," 是允许写入的字段清单，默认值与 ",[18,653,654],{},"validate"," 不能替代源码编码。",[18,657,658],{},"read()"," 返回 null 表示这个已识别语法不属于该语义形式，原文保留；它不应修复或标准化用户文本。",[14,661,662,665,666,669],{},[18,663,664],{},"patch()"," 接收局部字段，不保证包含所有字段。返回 ",[18,667,668],{},"{ from, to, insert }","、多个不重叠修改或 null。只替换需要的字段区域，保留空白、引号、其他属性和分隔符风格。如果新值包含分隔符，必须编码或拒绝，不能直接拼入语法。",[14,671,672],{},"只传入未知字段或不合法值时，运行时会拒绝。对多种源码拼写（例如单引号、双引号、参考定义）先定义可逆写回策略；不能写回的形式继续可读，不假装支持编辑。",[10,674,676],{"id":675},"source-指纹何时需要","source 指纹何时需要",[14,678,679,680,683],{},"默认源码身份由相关范围确定。提供 ",[18,681,682],{},"source(document, node)"," 可给出更小的精确指纹，但必须覆盖所有会被 patch 修改的字段。若控件只修改代码围栏语言，指纹可以只覆盖 header；若控件同时修改正文，就不能仍只比较 header。",[14,685,686],{},"指纹用于防止过期组件修改不同内容，不能用作持久业务 ID，也不能用节点在视口内的 id 保存到数据库。需要业务 ID 时把它定义为可校验的源码字段。",[10,688,689],{"id":689},"从字段到异步组件",[14,691,692,695,696,699,700,703,704,707,708,98,710,98,713,716,717,721],{},[18,693,694],{},"defineSource\u003CAttrs, Resource>()"," 可增加 ",[18,697,698],{},"load(attrs, signal)","。组件收到 ",[18,701,702],{},"status: 'loading' | 'ready' | 'error'"," 和可选 ",[18,705,706],{},"resource","；字段变化和卸载由资源协议管理。组件仍通过 ",[18,709,222],{},[18,711,712],{},"deleteNode()",[18,714,715],{},"revealSource()"," 等受控动作操作源码。先看 ",[455,718,720],{"href":719},"\u002Fextensions\u002Fresources","异步资源","，再增加上传或服务器查询。",[10,723,724],{"id":724},"块节点应该如何选择",[14,726,727,728,731,732,736,737,741,742,212],{},"本页的字面语法是单行、不嵌套 token，NodeView 只拥有这个 token 的显示。",[18,729,730],{},"syntax"," helper 不适合冒号嵌套容器或任意跨行语法。容器正文沿用原生 Markdown，见 ",[455,733,735],{"href":734},"\u002Fextensions\u002Fcontainers","容器","；跨行静态预览见 ",[455,738,740],{"href":739},"\u002Fextensions\u002Fdiagrams","图表","；自定义解析见 ",[455,743,745],{"href":744},"\u002Fextensions\u002Fmarkdown-plugins","Markdown 插件",[10,747,748],{"id":748},"检查新增节点是否完整",[14,750,751],{},"先验证合法、非法、未闭合和转义形式；再验证字段更新只改变对应源码、撤销恢复原样、只读动作拒绝、快速改字段时旧资源释放、组件卸载后动作失效。最后在窄屏和深浅主题下检视按钮与周围文本的基线、选区和焦点。",[753,754,755],"style",{},"html pre.shiki code .sVPnS, html code.shiki .sVPnS{--shiki-default:#24292E;--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sgQzM, html code.shiki .sgQzM{--shiki-default:#6F42C1;--shiki-light:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s-HEn, html code.shiki .s-HEn{--shiki-default:#032F62;--shiki-light:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sDD9d, html code.shiki .sDD9d{--shiki-default:#D73A49;--shiki-light:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sUNQB, html code.shiki .sUNQB{--shiki-default:#005CC5;--shiki-light:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sde8i, html code.shiki .sde8i{--shiki-default:#E36209;--shiki-light:#E36209;--shiki-dark:#FFAB70}",{"title":117,"searchDepth":145,"depth":145,"links":757},[758,759,760,761,762,763,764,765,766,767,768,769,770],{"id":12,"depth":145,"text":12},{"id":28,"depth":145,"text":28},{"id":67,"depth":145,"text":67},{"id":80,"depth":145,"text":80},{"id":201,"depth":145,"text":201},{"id":233,"depth":145,"text":233},{"id":462,"depth":145,"text":462},{"id":639,"depth":145,"text":639},{"id":645,"depth":145,"text":646},{"id":675,"depth":145,"text":676},{"id":689,"depth":145,"text":689},{"id":724,"depth":145,"text":724},{"id":748,"depth":145,"text":748},"从一个可点击的状态标记，理解语法、字段、命令和组件的完整接入。","md",[774,775,776,777,778,779,780],"Node","defineSource","patch","addNodeView","withMdReading","自定义 UI","status","zh-CN",{},0,"\u002Fextensions\u002Fcustom-node","扩展开发",{"title":5,"description":771},"extensions\u002Fcustom-node.zh-CN","0FLFuYHmDycTUn3gOnXK0WLkTKZLHR421n6VTNN-H9w",1793046311419]