A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName, hasClosestByTag} from "./hasClosest";
2import * as dayjs from "dayjs";
3import {transaction, updateTransaction} from "../wysiwyg/transaction";
4import {getContenteditableElement} from "../wysiwyg/getBlock";
5import {
6 fixTableRange,
7 focusBlock,
8 focusByRange,
9 focusByWbr,
10 getEditorRange,
11 getSelectionOffset,
12 setLastNodeRange,
13} from "./selection";
14import {Constants} from "../../constants";
15import {highlightRender} from "../render/highlightRender";
16import {scrollCenter} from "../../util/highlightById";
17import {updateAttrViewCellAnimation, updateAVName} from "../render/av/action";
18import {updateCellsValue} from "../render/av/cell";
19import {input} from "../wysiwyg/input";
20import {fetchPost} from "../../util/fetch";
21import {isIncludeCell} from "./table";
22import {getFieldIdByCellElement} from "../render/av/row";
23import {processClonePHElement} from "../render/util";
24import {setFold} from "../../menus/protyle";
25
26const processAV = (range: Range, html: string, protyle: IProtyle, blockElement: HTMLElement) => {
27 const tempElement = document.createElement("template");
28 tempElement.innerHTML = html;
29 let values: string[][] = [];
30 if (html.endsWith("]") && html.startsWith("[")) {
31 try {
32 values = JSON.parse(html);
33 } catch (e) {
34 console.warn("insert cell: JSON.parse error");
35 }
36 } else if (tempElement.content.querySelector("table")) {
37 tempElement.content.querySelectorAll("tr").forEach(item => {
38 values.push([]);
39 Array.from(item.children).forEach(cell => {
40 values[values.length - 1].push(cell.textContent);
41 });
42 });
43 }
44 const avID = blockElement.dataset.avId;
45 fetchPost("/api/av/getAttributeViewKeysByAvID", {avID}, async (response) => {
46 const columns: IAVColumn[] = response.data;
47 const cellElements: HTMLElement[] = Array.from(blockElement.querySelectorAll(".av__cell--active, .av__cell--select")) || [];
48 if (values && Array.isArray(values) && values.length > 0) {
49 if (cellElements.length === 0) {
50 blockElement.querySelectorAll(".av__row--select:not(.av__row--header)").forEach(rowElement => {
51 rowElement.querySelectorAll(".av__cell").forEach((cellElement: HTMLElement) => {
52 cellElements.push(cellElement);
53 });
54 });
55 }
56 if (cellElements.length === 0) {
57 cellElements.push(blockElement.querySelector(".av__row:not(.av__row--header) .av__cell"));
58 }
59 const doOperations: IOperation[] = [];
60 const undoOperations: IOperation[] = [];
61
62 const id = blockElement.dataset.nodeId;
63 let currentRowElement: Element;
64 const firstColIndex = cellElements[0].getAttribute("data-col-id");
65 for (let i = 0; i < values.length; i++) {
66 if (!currentRowElement) {
67 currentRowElement = hasClosestByClassName(cellElements[0].parentElement, "av__row") as HTMLElement;
68 } else {
69 currentRowElement = currentRowElement.nextElementSibling;
70 }
71 if (!currentRowElement.classList.contains("av__row")) {
72 break;
73 }
74 let cellElement: HTMLElement;
75 for (let j = 0; j < values[i].length; j++) {
76 const cellValue = values[i][j];
77 if (!cellElement) {
78 cellElement = currentRowElement.querySelector(`.av__cell[data-col-id="${firstColIndex}"]`) as HTMLElement;
79 } else {
80 if (cellElement.nextElementSibling) {
81 cellElement = cellElement.nextElementSibling as HTMLElement;
82 } else if (cellElement.parentElement.classList.contains("av__colsticky")) {
83 cellElement = cellElement.parentElement.nextElementSibling as HTMLElement;
84 }
85 }
86 if (!cellElement.classList.contains("av__cell")) {
87 break;
88 }
89 const operations = await updateCellsValue(protyle, blockElement as HTMLElement,
90 cellValue, [cellElement], columns, html, true);
91 if (operations.doOperations.length > 0) {
92 doOperations.push(...operations.doOperations);
93 undoOperations.push(...operations.undoOperations);
94 }
95 }
96 }
97 if (doOperations.length > 0) {
98 doOperations.push({
99 action: "doUpdateUpdated",
100 id,
101 data: dayjs().format("YYYYMMDDHHmmss"),
102 });
103 undoOperations.push({
104 action: "doUpdateUpdated",
105 id,
106 data: blockElement.getAttribute("updated"),
107 });
108 transaction(protyle, doOperations, undoOperations);
109 }
110 return;
111 }
112
113 const contenteditableElement = getContenteditableElement(tempElement.content.firstElementChild);
114 if (contenteditableElement && contenteditableElement.childNodes.length === 1 && contenteditableElement.firstElementChild?.getAttribute("data-type") === "block-ref") {
115 const selectCellElement = blockElement.querySelector(".av__cell--select") as HTMLElement;
116 if (selectCellElement) {
117 const sourceId = contenteditableElement.firstElementChild.getAttribute("data-id");
118 const previousID = getFieldIdByCellElement(selectCellElement, blockElement.getAttribute("data-av-type") as TAVView);
119 transaction(protyle, [{
120 action: "replaceAttrViewBlock",
121 avID,
122 previousID,
123 nextID: sourceId,
124 isDetached: false,
125 }], [{
126 action: "replaceAttrViewBlock",
127 avID,
128 previousID: sourceId,
129 nextID: previousID,
130 isDetached: selectCellElement.dataset.detached === "true",
131 }]);
132 updateAttrViewCellAnimation(selectCellElement, {
133 type: "block",
134 isDetached: false,
135 block: {content: contenteditableElement.firstElementChild.textContent, id: sourceId}
136 });
137 return;
138 }
139 }
140
141 const text = protyle.lute.BlockDOM2Content(html);
142 const rowsElement = blockElement.querySelectorAll(".av__row--select");
143
144 const textJSON: string[][] = [];
145 text.split("\n").forEach(row => {
146 textJSON.push(row.split("\t"));
147 });
148 if (rowsElement.length > 0 && textJSON.length === 1 && textJSON[0].length === 1) {
149 updateCellsValue(protyle, blockElement as HTMLElement, text, undefined, columns, html);
150 return;
151 }
152 if (rowsElement.length > 0) {
153 rowsElement.forEach(rowElement => {
154 rowElement.querySelectorAll(".av__cell").forEach((cellElement: HTMLElement) => {
155 cellElements.push(cellElement);
156 });
157 });
158 }
159 if (cellElements.length > 0) {
160 if (textJSON.length === 1 && textJSON[0].length === 1) {
161 updateCellsValue(protyle, blockElement as HTMLElement, text, cellElements, columns, html);
162 } else {
163 let currentRowElement: Element;
164 const doOperations: IOperation[] = [];
165 const undoOperations: IOperation[] = [];
166 const firstColIndex = cellElements[0].getAttribute("data-col-id");
167 for (let i = 0; i < textJSON.length; i++) {
168 if (!currentRowElement) {
169 currentRowElement = hasClosestByClassName(cellElements[0].parentElement, "av__row") as HTMLElement;
170 } else {
171 currentRowElement = currentRowElement.nextElementSibling;
172 }
173 if (!currentRowElement.classList.contains("av__row")) {
174 break;
175 }
176 let cellElement: HTMLElement;
177 for (let j = 0; j < textJSON[i].length; j++) {
178 if (!cellElement) {
179 cellElement = currentRowElement.querySelector(`.av__cell[data-col-id="${firstColIndex}"]`) as HTMLElement;
180 } else {
181 if (cellElement.nextElementSibling) {
182 cellElement = cellElement.nextElementSibling as HTMLElement;
183 } else if (cellElement.parentElement.classList.contains("av__colsticky")) {
184 cellElement = cellElement.parentElement.nextElementSibling as HTMLElement;
185 }
186 }
187 if (!cellElement.classList.contains("av__cell")) {
188 break;
189 }
190 const cellValue = textJSON[i][j];
191 const operations = await updateCellsValue(protyle, blockElement as HTMLElement, cellValue, [cellElement], columns, html, true);
192 if (operations.doOperations.length > 0) {
193 doOperations.push(...operations.doOperations);
194 undoOperations.push(...operations.undoOperations);
195 }
196 }
197 }
198 if (doOperations.length > 0) {
199 const id = blockElement.getAttribute("data-node-id");
200 doOperations.push({
201 action: "doUpdateUpdated",
202 id,
203 data: dayjs().format("YYYYMMDDHHmmss"),
204 });
205 undoOperations.push({
206 action: "doUpdateUpdated",
207 id,
208 data: blockElement.getAttribute("updated"),
209 });
210 transaction(protyle, doOperations, undoOperations);
211 }
212 }
213 document.querySelector(".av__panel")?.remove();
214 } else if (hasClosestByClassName(range.startContainer, "av__title")) {
215 const node = document.createTextNode(text);
216 range.insertNode(node);
217 range.setEnd(node, text.length);
218 range.collapse(false);
219 focusByRange(range);
220 updateAVName(protyle, blockElement);
221 }
222 });
223};
224
225const processTable = (range: Range, html: string, protyle: IProtyle, blockElement: HTMLElement) => {
226 const tempElement = document.createElement("template");
227 tempElement.innerHTML = html;
228 const copyCellElements = tempElement.content.querySelectorAll("th, td");
229 if (copyCellElements.length === 0) {
230 return false;
231 }
232 const scrollLeft = blockElement.firstElementChild.scrollLeft;
233 const scrollTop = blockElement.querySelector("table").scrollTop;
234 const tableSelectElement = blockElement.querySelector(".table__select") as HTMLElement;
235 let index = 0;
236 const matchCellsElement: HTMLTableCellElement[] = [];
237 blockElement.querySelectorAll("th, td").forEach((item: HTMLTableCellElement) => {
238 if (!item.classList.contains("fn__none") && copyCellElements.length > index &&
239 isIncludeCell({
240 tableSelectElement,
241 scrollLeft,
242 scrollTop,
243 item,
244 })) {
245 matchCellsElement.push(item);
246 index++;
247 }
248 });
249 tableSelectElement.removeAttribute("style");
250 const oldHTML = blockElement.outerHTML;
251 blockElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
252 matchCellsElement.forEach((item, matchIndex) => {
253 item.innerHTML = copyCellElements[matchIndex].innerHTML;
254 if (matchIndex === matchCellsElement.length - 1) {
255 setLastNodeRange(item, range, false);
256 }
257 });
258 range.collapse(false);
259 updateTransaction(protyle, blockElement.getAttribute("data-node-id"), blockElement.outerHTML, oldHTML);
260 return true;
261};
262
263export const insertHTML = (html: string, protyle: IProtyle, isBlock = false,
264 // 移动端插入嵌入块时,获取到的 range 为旧值
265 useProtyleRange = false,
266 // 在开头粘贴块则插入上方
267 insertByCursor = false) => {
268 if (html === "") {
269 return;
270 }
271 const range = useProtyleRange ? protyle.toolbar.range : getEditorRange(protyle.wysiwyg.element);
272 fixTableRange(range);
273 let unSpinHTML;
274 if (hasClosestByAttribute(range.startContainer, "data-type", "NodeTable") && !isBlock) {
275 if (hasClosestByTag(range.startContainer, "TABLE")) {
276 unSpinHTML = protyle.lute.BlockDOM2InlineBlockDOM(html);
277 } else {
278 // https://github.com/siyuan-note/siyuan/issues/9411
279 isBlock = true;
280 }
281 }
282 let blockElement = hasClosestBlock(range.startContainer) as HTMLElement;
283 if (!blockElement) {
284 // 使用鼠标点击选则模版提示列表后 range 丢失
285 if (protyle.toolbar.range) {
286 blockElement = hasClosestBlock(protyle.toolbar.range.startContainer) as HTMLElement;
287 } else {
288 blockElement = protyle.wysiwyg.element.firstElementChild as HTMLElement;
289 }
290 }
291 if (!blockElement) {
292 return;
293 }
294
295 if (blockElement.classList.contains("av")) {
296 range.deleteContents();
297 processAV(range, html, protyle, blockElement as HTMLElement);
298 return;
299 }
300 if (blockElement.classList.contains("table") && blockElement.querySelector(".table__select").clientWidth > 0 &&
301 processTable(range, html, protyle, blockElement)) {
302 return;
303 }
304
305 let id = blockElement.getAttribute("data-node-id");
306 range.insertNode(document.createElement("wbr"));
307 let oldHTML = blockElement.outerHTML;
308 const type = blockElement.getAttribute("data-type");
309 const isNodeCodeBlock = type === "NodeCodeBlock";
310 const editableElement = getContenteditableElement(blockElement);
311 if (!isBlock &&
312 (isNodeCodeBlock || protyle.toolbar.getCurrentType(range).includes("code"))) {
313 range.deleteContents();
314 // 代码块需保持至少一个 \n https://github.com/siyuan-note/siyuan/pull/13271#issuecomment-2502672155
315 let codeBlockIsEmpty = false;
316 if (isNodeCodeBlock && editableElement.textContent === "") {
317 codeBlockIsEmpty = true;
318 }
319 range.insertNode(document.createTextNode(html.replace(/\r\n|\r|\u2028|\u2029/g, "\n")));
320 range.collapse(false);
321 range.insertNode(document.createElement("wbr"));
322 if (codeBlockIsEmpty) {
323 // 代码块为空添加的 \n 需放在最后 https://github.com/siyuan-note/siyuan/issues/15399
324 range.collapse(false);
325 range.insertNode(document.createTextNode("\n"));
326 }
327 if (isNodeCodeBlock) {
328 blockElement.querySelector('[data-render="true"]')?.removeAttribute("data-render");
329 highlightRender(blockElement);
330 } else {
331 focusByWbr(blockElement, range);
332 }
333 blockElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
334 updateTransaction(protyle, id, blockElement.outerHTML, oldHTML);
335 setTimeout(() => {
336 scrollCenter(protyle, blockElement, false, "smooth");
337 }, Constants.TIMEOUT_LOAD);
338 return;
339 }
340
341 const undoOperation: IOperation[] = [];
342 const doOperation: IOperation[] = [];
343 if (range.toString() !== "") {
344 const inlineMathElement = hasClosestByAttribute(range.commonAncestorContainer, "data-type", "inline-math");
345 if (inlineMathElement) {
346 // 表格内选中数学公式 https://ld246.com/article/1631708573504
347 inlineMathElement.remove();
348 } else if (range.startContainer.nodeType === 3 && range.startContainer.parentElement.getAttribute("data-type")?.indexOf("block-ref") > -1) {
349 // 选中 ref**bbb** 后 alt+[
350 range.deleteContents();
351 // https://github.com/siyuan-note/siyuan/issues/14035
352 if (range.startContainer.nodeType !== 3 && range.startContainer.textContent === "") {
353 // ref 选中处理 https://ld246.com/article/1629214377537
354 (range.startContainer as HTMLElement).remove();
355 }
356 } else {
357 range.deleteContents();
358 }
359 range.insertNode(document.createElement("wbr"));
360 undoOperation.push({
361 action: "update",
362 id,
363 data: oldHTML
364 });
365 doOperation.push({
366 action: "update",
367 id,
368 data: blockElement.outerHTML
369 });
370 }
371 const tempElement = document.createElement("template");
372
373 // https://github.com/siyuan-note/siyuan/issues/14162 & https://github.com/siyuan-note/siyuan/issues/14965
374 if (/^\s*>|\*|-|\+|\d*.|\[ \]|[x]/.test(html) &&
375 editableElement.textContent.replace(Constants.ZWSP, "") !== "") {
376 unSpinHTML = html;
377 }
378
379 let innerHTML = unSpinHTML || // 在 table 中插入需要使用转换好的行内元素 https://github.com/siyuan-note/siyuan/issues/9358
380 html; // 空格会被 Spin 不再,需要使用原文
381 // 粘贴纯文本时会进行内部转义,这里需要进行反转义 https://github.com/siyuan-note/siyuan/issues/10620
382 innerHTML = innerHTML.replace(/;;;lt;;;/g, "<").replace(/;;;gt;;;/g, ">");
383 tempElement.innerHTML = innerHTML;
384
385 let block2text = false;
386 if ((
387 editableElement.textContent.replace(Constants.ZWSP, "") !== "" ||
388 type === "NodeHeading"
389 ) &&
390 tempElement.content.childElementCount === 1 &&
391 tempElement.content.firstChild.nodeType !== 3 &&
392 tempElement.content.firstElementChild.getAttribute("data-type") === "NodeHeading") {
393 // https://github.com/siyuan-note/siyuan/issues/14114
394 isBlock = false;
395 block2text = true;
396 }
397 // 使用 lute 方法会添加 p 元素,只有一个 p 元素或者只有一个字符串或者为 <u>b</u> 时的时候只拷贝内部
398 if (!isBlock) {
399 if (tempElement.content.firstChild.nodeType === 3 || block2text ||
400 (tempElement.content.firstChild.nodeType !== 3 &&
401 ((tempElement.content.firstElementChild.classList.contains("p") && tempElement.content.childElementCount === 1) ||
402 tempElement.content.firstElementChild.tagName !== "DIV"))) {
403 if (tempElement.content.firstChild.nodeType !== 3 && tempElement.content.firstElementChild.classList.contains("p")) {
404 tempElement.innerHTML = tempElement.content.firstElementChild.firstElementChild.innerHTML.trim();
405 }
406 // 粘贴带样式的行内元素到另一个行内元素中需进行切割
407 const spanElement = range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer as HTMLElement;
408 if (spanElement.tagName === "SPAN" && spanElement === (range.endContainer.nodeType === 3 ? range.endContainer.parentElement : range.endContainer) &&
409 // 粘贴纯文本不需切割 https://ld246.com/article/1665556907936
410 // emoji 图片需要切割 https://github.com/siyuan-note/siyuan/issues/9370
411 tempElement.content.querySelector("span, img")
412 ) {
413 const afterElement = document.createElement("span");
414 const attributes = spanElement.attributes;
415 for (let i = 0; i < attributes.length; i++) {
416 afterElement.setAttribute(attributes[i].name, attributes[i].value);
417 }
418 range.setEnd(spanElement.lastChild, spanElement.lastChild.textContent.length);
419 afterElement.append(range.extractContents());
420 spanElement.after(afterElement);
421 range.setStartBefore(afterElement);
422 range.collapse(true);
423 }
424 range.insertNode(tempElement.content.cloneNode(true));
425 range.collapse(false);
426 blockElement.querySelector("wbr")?.remove();
427 protyle.wysiwyg.lastHTMLs[id] = oldHTML;
428 input(protyle, blockElement as HTMLElement, range);
429 return;
430 }
431 }
432 const cursorLiElement = hasClosestByClassName(blockElement, "li");
433 // 列表项不能单独进行粘贴 https://ld246.com/article/1628681120576/comment/1628681209731#comments
434 if (tempElement.content.children[0]?.getAttribute("data-type") === "NodeListItem") {
435 if (cursorLiElement) {
436 blockElement = cursorLiElement;
437 id = blockElement.getAttribute("data-node-id");
438 oldHTML = blockElement.outerHTML;
439 } else {
440 const liItemElement = tempElement.content.children[0];
441 const subType = liItemElement.getAttribute("data-subtype");
442 tempElement.innerHTML = `<div${subType === "o" ? " data-marker=\"1.\"" : ""} data-subtype="${subType}" data-node-id="${Lute.NewNodeID()}" data-type="NodeList" class="list">${html}<div class="protyle-attr" contenteditable="false">${Constants.ZWSP}</div></div>`;
443 }
444 }
445 let lastElement: Element;
446 let insertBefore = false;
447 if (!range.toString() && insertByCursor) {
448 const positon = getSelectionOffset(blockElement, protyle.wysiwyg.element, range);
449 if (positon.start === 0 && editableElement.textContent !== "") {
450 insertBefore = true;
451 }
452 }
453 // https://github.com/siyuan-note/siyuan/issues/15768
454 if (tempElement.content.firstChild.nodeType === 3 || (tempElement.content.firstChild.nodeType === 1 && tempElement.content.firstElementChild.tagName !== "DIV")) {
455 tempElement.innerHTML = protyle.lute.SpinBlockDOM(tempElement.innerHTML);
456 }
457 (insertBefore ? Array.from(tempElement.content.children) : Array.from(tempElement.content.children).reverse()).find((item) => {
458 let addId = item.getAttribute("data-node-id");
459 const hasParentHeading = item.getAttribute("parent-heading");
460 if (addId === id) {
461 doOperation.push({
462 action: "update",
463 data: item.outerHTML,
464 id: addId,
465 });
466 undoOperation.push({
467 action: "update",
468 id: addId,
469 data: oldHTML,
470 });
471 } else {
472 if (item.classList.contains("li") && !blockElement.parentElement.classList.contains("list")) {
473 // https://github.com/siyuan-note/siyuan/issues/6534
474 addId = Lute.NewNodeID();
475 const liElement = document.createElement("div");
476 liElement.setAttribute("data-subtype", item.getAttribute("data-subtype"));
477 liElement.setAttribute("data-node-id", addId);
478 liElement.setAttribute("data-type", "NodeList");
479 liElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
480 liElement.classList.add("list");
481 liElement.append(item);
482 item = liElement;
483 }
484 item.removeAttribute("parent-heading");
485 doOperation.push({
486 action: "insert",
487 data: item.outerHTML,
488 id: addId,
489 context: {ignoreProcess: hasParentHeading ? "true" : "false"},
490 nextID: insertBefore ? id : undefined,
491 previousID: insertBefore ? undefined : id
492 });
493 undoOperation.push({
494 action: "delete",
495 id: addId,
496 });
497 }
498 if (!hasParentHeading) {
499 const rendersElement = [];
500 if (item.classList.contains("render-node") && item.getAttribute("data-type") === "NodeCodeBlock") {
501 rendersElement.push(item);
502 } else {
503 rendersElement.push(...item.querySelectorAll('.render-node[data-type="NodeCodeBlock"]'));
504 }
505 rendersElement.forEach((renderItem) => {
506 renderItem.querySelector(".protyle-icons")?.remove();
507 const spinElement = renderItem.querySelector('[spin="1"]');
508 if (spinElement) {
509 spinElement.innerHTML = "";
510 }
511 renderItem.removeAttribute("data-render");
512 });
513 processClonePHElement(item);
514 if (insertBefore) {
515 blockElement.before(item);
516 } else {
517 blockElement.after(item);
518 }
519 }
520 if (!lastElement) {
521 lastElement = item;
522 }
523 });
524 if (editableElement && editableElement.textContent === "" && blockElement.classList.contains("p")) {
525 // 选中当前块所有内容粘贴再撤销会导致异常 https://ld246.com/article/1662542137636
526 doOperation.find((item, index) => {
527 if (item.id === id) {
528 doOperation.splice(index, 1);
529 return true;
530 }
531 });
532 doOperation.push({
533 action: "delete",
534 id
535 });
536 // 选中当前块所有内容粘贴再撤销会导致异常 https://ld246.com/article/1662542137636
537 undoOperation.find((item, index) => {
538 if (item.id === id && item.action === "update") {
539 undoOperation.splice(index, 1);
540 return true;
541 }
542 });
543 undoOperation.push({
544 action: "insert",
545 data: oldHTML,
546 id,
547 previousID: blockElement.previousElementSibling ? blockElement.previousElementSibling.getAttribute("data-node-id") : "",
548 parentID: blockElement.parentElement.getAttribute("data-node-id") || protyle.block.parentID
549 });
550 blockElement.remove();
551 }
552 if (lastElement) {
553 // https://github.com/siyuan-note/siyuan/issues/5591
554 focusBlock(lastElement, undefined, false);
555 }
556 const wbrElement = protyle.wysiwyg.element.querySelector("wbr");
557 if (wbrElement) {
558 wbrElement.remove();
559 }
560 let foldData;
561 if (blockElement.getAttribute("data-type") === "NodeHeading" &&
562 blockElement.getAttribute("fold") === "1") {
563 foldData = setFold(protyle, blockElement, true, false, false, true);
564 doOperation.reverse();
565 foldData.doOperations[0].context = {
566 focusId: lastElement?.getAttribute("data-node-id"),
567 };
568 doOperation.push(...foldData.doOperations);
569 undoOperation.push(...foldData.undoOperations);
570 }
571 transaction(protyle, doOperation, undoOperation);
572 // 复制容器块中包含折叠标题块
573 protyle.wysiwyg.element.querySelectorAll("[parent-heading]").forEach(item => {
574 item.remove();
575 });
576};