Skip to content

Commit 7339931

Browse files
committed
✨ Handle whitespace characters when pasting HTML
fix #14775
1 parent bdee7c6 commit 7339931

File tree

1 file changed

+11
-84
lines changed

1 file changed

+11
-84
lines changed

app/src/protyle/util/paste.ts

Lines changed: 11 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {hideElements} from "../ui/hideElements";
1717
import {avRender} from "../render/av/render";
1818
import {cellScrollIntoView, getCellText} from "../render/av/cell";
1919
import {getContenteditableElement} from "../wysiwyg/getBlock";
20-
import {escapeAttr} from "../../util/escape";
2120

2221
export const getTextStar = (blockElement: HTMLElement) => {
2322
const dataType = blockElement.dataset.type;
@@ -321,8 +320,7 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven
321320
}
322321
// 剪切复制中首位包含空格或仅有空格 https://github.com/siyuan-note/siyuan/issues/5667
323322
if (!siyuanHTML) {
324-
// 从网页复制时,浏览器已经将符合条件的回车符转换为空格写入剪贴板了,其他回车符都是多余的,需移除
325-
// 否则 new DOMParser().parseFromString(textHTML, "text/html") 之后所有的单独回车符都会转换为换行符,产生多余的换行
323+
// 需移除回车符,否则 new DOMParser().parseFromString(textHTML, "text/html") 之后所有单独的回车符都会转换为换行符,产生多余的换行
326324
textHTML = textHTML.replace(/\r/g, "");
327325
// process word
328326
const doc = new DOMParser().parseFromString(textHTML, "text/html");
@@ -510,17 +508,17 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven
510508
}
511509
}
512510
if (isHTML) {
513-
// 不能用 new DOMParser().parseFromString(textHTML, "text/html"),否则之后 getComputedStyle() 无效
514-
const element = document.createElement("div");
515-
element.style.visibility = "hidden";
516-
element.style.position = "absolute";
517-
document.body.appendChild(element);
518-
element.innerHTML = textHTML;
519-
520511
const tempElement = document.createElement("div");
521-
tempElement.innerHTML = processHTMLElement(element);
522-
element.remove();
523-
512+
tempElement.innerHTML = textHTML.replace(/\n/g, "<br>");
513+
tempElement.querySelectorAll("[style]").forEach((e) => {
514+
e.removeAttribute("style");
515+
});
516+
// 移除空的 A 标签
517+
tempElement.querySelectorAll("a").forEach((e) => {
518+
if (e.innerHTML.trim() === "") {
519+
e.remove();
520+
}
521+
});
524522
// https://github.com/siyuan-note/siyuan/issues/14625#issuecomment-2869618067
525523
let linkElement;
526524
if (tempElement.childElementCount === 1 && tempElement.childNodes.length === 1) {
@@ -612,74 +610,3 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven
612610
scrollCenter(protyle, undefined, false, "smooth");
613611
}
614612
};
615-
616-
const processHTMLElement = (element: Element) => {
617-
let html = "";
618-
// 遍历所有子节点
619-
Array.from(element.childNodes).forEach(node => {
620-
if (node.nodeType === 3) {
621-
// 处理文本节点
622-
const whiteSpace = getComputedStyle(element).whiteSpace;
623-
html += processTextByWhiteSpace(node.textContent, whiteSpace);
624-
} else if (node.nodeType === 1) {
625-
// 处理元素节点
626-
const childElement = node as HTMLElement;
627-
const tagName = childElement.tagName.toLowerCase();
628-
if (new Set(["area", "base", "br", "col", "embed", "hr", "img", "input",
629-
"link", "meta", "param", "source", "track", "wbr"]).has(tagName)) {
630-
// 空元素不能存在子节点、不能有结束标签
631-
childElement.removeAttribute("style");
632-
html += childElement.outerHTML;
633-
} else if (tagName === "a" && (node as HTMLElement).innerHTML.trim() === "") {
634-
// 过滤空的 A 标签
635-
} else {
636-
// 递归处理元素节点
637-
const attrs = getAttributes(childElement);
638-
const processedInnerHTML = processHTMLElement(childElement);
639-
html += `<${tagName}${attrs}>${processedInnerHTML}</${tagName}>`;
640-
}
641-
}
642-
});
643-
644-
return html;
645-
};
646-
647-
// 根据 white-space 属性处理文本中的空白字符 https://github.com/siyuan-note/siyuan/issues/14775
648-
const processTextByWhiteSpace = (text: string, whiteSpace: string) => {
649-
// 替换不使用'\s',因为它包含不断行空白字符等其他字符
650-
// 替换不使用'\r',因为换行符在前面已经移除
651-
// 行末的空白字符会被 Lute 移除,这里无需处理
652-
switch (whiteSpace) {
653-
default:
654-
case "normal":
655-
case "nowrap":
656-
// 合并相连的空白符为一个空格(空格、制表符、换行符)
657-
// return text.replace(/[ \t\n]+/g, " ");
658-
// 前面不知道哪个步骤已经合并空白符
659-
// return text;
660-
/* falls through */
661-
case "pre-line":
662-
// 合并其他相连的空白符为一个空格,保留换行(换行符转换为<br>)
663-
// text = text.replace(/[ \t]+/g, " ");
664-
// 前面不知道哪个步骤已经合并空白符
665-
/* falls through */
666-
case "pre":
667-
case "pre-wrap":
668-
case "break-spaces":
669-
// 不合并其他空白符,保留换行(换行符转换为<br>)
670-
return text.replace(/\n/g, "<br>");
671-
}
672-
};
673-
674-
const getAttributes = (element: Element) => {
675-
const attrs: string[] = [];
676-
for (let i = 0; i < element.attributes.length; i++) {
677-
const attr = element.attributes[i];
678-
// 顺便移除 style 属性
679-
if (attr.name.toLowerCase() === "style") {
680-
continue;
681-
}
682-
attrs.push(`${attr.name}="${escapeAttr(attr.value)}"`);
683-
}
684-
return attrs.length > 0 ? " " + attrs.join(" ") : "";
685-
};

0 commit comments

Comments
 (0)