A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1import {addScript} from "../util/addScript";
2import {Constants} from "../../constants";
3import {focusByOffset} from "../util/selection";
4import {setCodeTheme} from "../../util/assets";
5
6export const highlightRender = (element: Element, cdn = Constants.PROTYLE_CDN, zoom = 1) => {
7 let codeElements: NodeListOf<Element>;
8 let isPreview = false;
9 if (element.classList.contains("code-block")) {
10 // 编辑器内代码块编辑渲染
11 codeElements = element.querySelectorAll(".hljs");
12 } else {
13 if (element.classList.contains("item__readme")) {
14 // bazaar reademe
15 codeElements = element.querySelectorAll("pre code");
16 codeElements.forEach(item => {
17 item.parentElement.setAttribute("linenumber", "false");
18 });
19 } else if (element.classList.contains("b3-typography")) {
20 // preview & export html markdown
21 codeElements = element.querySelectorAll(".code-block code");
22 isPreview = true;
23 } else {
24 codeElements = element.querySelectorAll(".code-block .hljs");
25 }
26 }
27 if (codeElements.length === 0) {
28 return;
29 }
30
31 setCodeTheme(cdn);
32
33 addScript(`${cdn}/js/highlight.js/highlight.min.js?v=11.11.1`, "protyleHljsScript").then(() => {
34 addScript(`${cdn}/js/highlight.js/third-languages.js?v=2.0.1`, "protyleHljsThirdScript").then(() => {
35 codeElements.forEach((block: HTMLElement) => {
36 const iconElements = block.parentElement.querySelectorAll(".protyle-icon");
37 if (iconElements.length === 2) {
38 iconElements[0].setAttribute("aria-label", window.siyuan.languages.copy);
39 iconElements[1].setAttribute("aria-label", window.siyuan.languages.more);
40 }
41 if (block.getAttribute("data-render") === "true") {
42 return;
43 }
44 const wbrElement = block.querySelector("wbr");
45 let startIndex = 0;
46 if (wbrElement) {
47 let previousSibling = wbrElement.previousSibling;
48 while (previousSibling) {
49 startIndex += previousSibling.textContent.length;
50 while (!previousSibling.previousSibling && previousSibling.parentElement.tagName !== "DIV") {
51 // 高亮 span 中输入
52 previousSibling = previousSibling.parentElement;
53 }
54 previousSibling = previousSibling.previousSibling;
55 }
56 wbrElement.remove();
57 }
58
59 let language;
60 if (isPreview) {
61 language = block.parentElement.getAttribute("data-language"); // preview
62 } else if (block.previousElementSibling) {
63 language = block.previousElementSibling.firstElementChild.textContent;
64 } else {
65 // bazaar readme
66 language = block.className.replace("language-", "");
67 }
68 if (!window.hljs.getLanguage(language)) {
69 language = "plaintext";
70 }
71 block.classList.add("hljs");
72 block.setAttribute("data-render", "true");
73 const autoEnter = block.parentElement.getAttribute("linewrap");
74 const ligatures = block.parentElement.getAttribute("ligatures");
75 const lineNumber = block.parentElement.getAttribute("linenumber");
76 const hljsElement = block.lastElementChild ? block.lastElementChild as HTMLElement : block;
77 if (autoEnter === "true" || (autoEnter !== "false" && window.siyuan.config.editor.codeLineWrap)) {
78 hljsElement.style.setProperty("white-space", "pre-wrap");
79 hljsElement.style.setProperty("word-break", "break-word");
80 } else {
81 // https://ld246.com/article/1684031600711 该属性会导致有 tab 后光标跳至末尾,目前无解
82 hljsElement.style.setProperty("white-space", "pre");
83 hljsElement.style.setProperty("word-break", "initial");
84 }
85 if (ligatures === "true" || (ligatures !== "false" && window.siyuan.config.editor.codeLigatures)) {
86 hljsElement.style.fontVariantLigatures = "normal";
87 } else {
88 hljsElement.style.fontVariantLigatures = "none";
89 }
90 const codeText = hljsElement.textContent;
91 if (block.firstElementChild) {
92 if (!isPreview && (lineNumber === "true" || (lineNumber !== "false" && window.siyuan.config.editor.codeSyntaxHighlightLineNum))) {
93 // 需要先添加 class 以防止抖动 https://ld246.com/article/1648116585443
94 block.firstElementChild.className = "protyle-linenumber__rows";
95 block.firstElementChild.setAttribute("contenteditable", "false");
96 lineNumberRender(block, zoom);
97 block.style.display = "";
98 } else {
99 block.firstElementChild.className = "fn__none";
100 block.firstElementChild.innerHTML = "";
101 hljsElement.style.paddingLeft = "";
102 block.style.display = "block";
103 }
104 }
105 hljsElement.innerHTML = window.hljs.highlight(
106 codeText + (codeText.endsWith("\n") ? "" : "\n"), // https://github.com/siyuan-note/siyuan/issues/4609
107 {
108 language,
109 ignoreIllegals: true
110 }).value;
111 if (wbrElement && getSelection().rangeCount > 0) {
112 focusByOffset(block, startIndex, startIndex);
113 }
114 });
115 });
116 });
117};
118
119export const lineNumberRender = (block: HTMLElement, zoom = 1) => {
120 const lineNumber = block.parentElement.getAttribute("lineNumber");
121 if (lineNumber === "false") {
122 return;
123 }
124 if (!window.siyuan.config.editor.codeSyntaxHighlightLineNum && lineNumber !== "true") {
125 return;
126 }
127 // clientHeight 总是取的整数
128 block.parentElement.style.lineHeight = `${((parseInt(block.parentElement.style.fontSize) || window.siyuan.config.editor.fontSize) * 1.625 * 0.85).toFixed(0)}px`;
129 const codeElement = block.lastElementChild as HTMLElement;
130
131 const lineList = codeElement.textContent.split(/\r\n|\r|\n|\u2028|\u2029/g);
132 if (lineList[lineList.length - 1] === "" && lineList.length > 1) {
133 lineList.pop();
134 }
135 block.firstElementChild.innerHTML = `<span>${lineList.length}</span>`;
136 codeElement.style.paddingLeft = `${block.firstElementChild.clientWidth + 16}px`;
137 let lineNumberHTML = "";
138 if (codeElement.style.wordBreak === "break-word") {
139 // 代码块开启了换行
140 const codeElementStyle = window.getComputedStyle(codeElement);
141 const lineNumberTemp = document.createElement("div");
142 lineNumberTemp.className = "hljs";
143 // 不能使用 codeElement.clientWidth,被忽略小数点导致宽度不一致
144 lineNumberTemp.setAttribute("style", `padding-left:${codeElement.style.paddingLeft};
145width: ${codeElement.getBoundingClientRect().width / zoom}px;
146white-space:${codeElementStyle.whiteSpace};
147word-break:${codeElementStyle.wordBreak};
148font-variant-ligatures:${codeElementStyle.fontVariantLigatures};
149padding-right:0;max-height: none;box-sizing: border-box;position: absolute;padding-top:0 !important;padding-bottom:0 !important;min-height:auto !important;`);
150 lineNumberTemp.setAttribute("contenteditable", "true");
151 block.insertAdjacentElement("afterend", lineNumberTemp);
152
153 lineList.map((line) => {
154 // windows 下空格高度为 0 https://github.com/siyuan-note/siyuan/issues/12346
155 lineNumberTemp.textContent = line.trim() ? line : "<br>";
156 // 不能使用 lineNumberTemp.getBoundingClientRect().height.toFixed(1) 否则
157 // windows 需等待字体下载完成再计算,否则导致不换行,高度计算错误
158 // https://github.com/siyuan-note/siyuan/issues/9029
159 // https://github.com/siyuan-note/siyuan/issues/9140
160 lineNumberHTML += `<span style="height:${lineNumberTemp.clientHeight}px"></span>`;
161 });
162 lineNumberTemp.remove();
163 } else {
164 lineNumberHTML = "<span></span>".repeat(lineList.length);
165 }
166
167 block.firstElementChild.innerHTML = lineNumberHTML;
168
169 // https://github.com/siyuan-note/siyuan/issues/12726
170 if (block.scrollHeight > block.clientHeight) {
171 if (getSelection().rangeCount > 0) {
172 const range = getSelection().getRangeAt(0);
173 if (block.contains(range.startContainer)) {
174 const brElement = document.createElement("br");
175 range.insertNode(brElement);
176 brElement.scrollIntoView({block: "nearest"});
177 brElement.remove();
178 }
179 }
180 }
181};