Skip to content

Commit 4c358f5

Browse files
committed
fix:safari lookbehind
1 parent a5e5fc5 commit 4c358f5

2 files changed

Lines changed: 194 additions & 143 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jx3box/jx3box-editor",
3-
"version": "3.1.5",
3+
"version": "3.2.3",
44
"description": "JX3BOX Article & Editor",
55
"main": "index.js",
66
"scripts": {

src/assets/js/katex.js

Lines changed: 193 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,181 @@ import $ from 'jquery';
22
import katex from 'katex';
33
import 'katex/dist/katex.min.css';
44

5+
function isEscaped(text, index) {
6+
let count = 0;
7+
for (let i = index - 1; i >= 0 && text[i] === '\\'; i--) {
8+
count++;
9+
}
10+
return count % 2 === 1;
11+
}
12+
13+
function findUnescaped(text, token, start) {
14+
let index = start;
15+
while (index < text.length) {
16+
const found = text.indexOf(token, index);
17+
if (found === -1) return -1;
18+
if (!isEscaped(text, found)) return found;
19+
index = found + token.length;
20+
}
21+
return -1;
22+
}
23+
24+
function getTextNodes(container, shouldAccept) {
25+
const walker = document.createTreeWalker(
26+
container,
27+
NodeFilter.SHOW_TEXT,
28+
{
29+
acceptNode: function (node) {
30+
// 跳过已渲染的节点
31+
if (
32+
node.parentNode &&
33+
(node.parentNode.classList?.contains('katex') ||
34+
node.parentNode.closest("pre, code, .katex"))
35+
) {
36+
return NodeFilter.FILTER_REJECT;
37+
}
38+
39+
return shouldAccept(node.nodeValue || '') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
40+
},
41+
}
42+
);
43+
44+
const nodesToReplace = [];
45+
while (walker.nextNode()) {
46+
nodesToReplace.push(walker.currentNode);
47+
}
48+
return nodesToReplace;
49+
}
50+
51+
function collectInlineMatches(text) {
52+
const matches = [];
53+
let index = 0;
54+
55+
while (index < text.length) {
56+
const parenStart = text.indexOf('\\(', index);
57+
const dollarStart = text.indexOf('$', index);
58+
const candidates = [parenStart, dollarStart].filter((value) => value !== -1 && !isEscaped(text, value));
59+
const start = candidates.length ? Math.min(...candidates) : -1;
60+
if (start === -1) break;
61+
62+
if (text.slice(start, start + 2) === '\\(') {
63+
const end = findUnescaped(text, '\\)', start + 2);
64+
if (end !== -1) {
65+
const raw = text.slice(start + 2, end);
66+
if (raw && !raw.includes('\n')) {
67+
matches.push({
68+
start,
69+
end: end + 2,
70+
raw,
71+
full: text.slice(start, end + 2),
72+
});
73+
}
74+
index = end + 2;
75+
continue;
76+
}
77+
} else if (text[start] === '$' && text[start + 1] !== '$') {
78+
const end = findUnescaped(text, '$', start + 1);
79+
if (end !== -1 && text[end + 1] !== '$') {
80+
const raw = text.slice(start + 1, end);
81+
if (raw && !raw.includes('\n')) {
82+
matches.push({
83+
start,
84+
end: end + 1,
85+
raw,
86+
full: text.slice(start, end + 1),
87+
});
88+
}
89+
index = end + 1;
90+
continue;
91+
}
92+
}
93+
94+
index = start + 1;
95+
}
96+
97+
return matches;
98+
}
99+
100+
function collectBlockMatches(text) {
101+
const matches = [];
102+
let index = 0;
103+
104+
while (index < text.length) {
105+
const dollarStart = text.indexOf('$$', index);
106+
const bracketStart = text.indexOf('\\[', index);
107+
const candidates = [dollarStart, bracketStart].filter((value) => value !== -1 && !isEscaped(text, value));
108+
const start = candidates.length ? Math.min(...candidates) : -1;
109+
if (start === -1) break;
110+
111+
if (text.slice(start, start + 2) === '$$') {
112+
const end = findUnescaped(text, '$$', start + 2);
113+
if (end !== -1) {
114+
const raw = text.slice(start + 2, end).trim();
115+
if (raw) {
116+
matches.push({
117+
start,
118+
end: end + 2,
119+
raw,
120+
full: text.slice(start, end + 2),
121+
});
122+
}
123+
index = end + 2;
124+
continue;
125+
}
126+
} else if (text.slice(start, start + 2) === '\\[') {
127+
const end = findUnescaped(text, '\\]', start + 2);
128+
if (end !== -1) {
129+
const raw = text.slice(start + 2, end).trim();
130+
if (raw) {
131+
matches.push({
132+
start,
133+
end: end + 2,
134+
raw,
135+
full: text.slice(start, end + 2),
136+
});
137+
}
138+
index = end + 2;
139+
continue;
140+
}
141+
}
142+
143+
index = start + 1;
144+
}
145+
146+
return matches;
147+
}
148+
149+
function replaceTextNode(node, matches, renderMatch, logPrefix) {
150+
if (!matches.length) return;
151+
152+
const text = node.nodeValue || '';
153+
const frag = document.createDocumentFragment();
154+
let lastIndex = 0;
155+
156+
matches.forEach((match) => {
157+
if (match.start > lastIndex) {
158+
frag.appendChild(document.createTextNode(text.slice(lastIndex, match.start)));
159+
}
160+
161+
try {
162+
frag.appendChild(renderMatch(match.raw));
163+
} catch (e) {
164+
frag.appendChild(document.createTextNode(match.full));
165+
console.error(logPrefix, match.raw, e.message);
166+
}
167+
168+
lastIndex = match.end;
169+
});
170+
171+
if (lastIndex < text.length) {
172+
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
173+
}
174+
175+
if (frag.hasChildNodes()) {
176+
node.parentNode.replaceChild(frag, node);
177+
}
178+
}
179+
5180
function renderKatexBlock(selector = ".w-latex") {
6181
try {
7182
$(selector).each(function() {
@@ -39,60 +214,11 @@ function renderKatexBlock(selector = ".w-latex") {
39214
}
40215

41216
function renderKatexInline(container = document.body) {
42-
// 改进的正则:不匹配换行符,支持转义
43-
const inlineRegex = /(?<!\\)(\\\((.+?)\\\)|(?<!\\)\$([^\n$]+?)(?<!\\)\$)/g;
44-
45-
const walker = document.createTreeWalker(
46-
container,
47-
NodeFilter.SHOW_TEXT,
48-
{
49-
acceptNode: function (node) {
50-
// 跳过已渲染的节点
51-
if (
52-
node.parentNode &&
53-
(node.parentNode.classList?.contains('katex') ||
54-
node.parentNode.closest("pre, code, .katex"))
55-
) {
56-
return NodeFilter.FILTER_REJECT;
57-
}
58-
59-
const value = node.nodeValue || '';
60-
if (value.includes("\\(") || value.includes("$")) {
61-
return NodeFilter.FILTER_ACCEPT;
62-
}
63-
return NodeFilter.FILTER_REJECT;
64-
},
65-
}
66-
);
67-
68-
const nodesToReplace = [];
69-
while (walker.nextNode()) {
70-
nodesToReplace.push(walker.currentNode);
71-
}
72-
73-
nodesToReplace.forEach((node) => {
74-
const text = node.nodeValue;
75-
const frag = document.createDocumentFragment();
76-
let lastIndex = 0;
77-
78-
// 重置正则状态
79-
inlineRegex.lastIndex = 0;
80-
81-
const matches = [...text.matchAll(inlineRegex)];
82-
83-
matches.forEach((match) => {
84-
const fullMatch = match[0];
85-
const parenContent = match[2];
86-
const dollarContent = match[3];
87-
const raw = parenContent || dollarContent;
88-
const matchStart = match.index;
89-
90-
// 添加匹配前的文本
91-
if (matchStart > lastIndex) {
92-
frag.appendChild(document.createTextNode(text.slice(lastIndex, matchStart)));
93-
}
94-
95-
try {
217+
getTextNodes(container, (value) => value.includes("\\(") || value.includes("$")).forEach((node) => {
218+
replaceTextNode(
219+
node,
220+
collectInlineMatches(node.nodeValue || ''),
221+
(raw) => {
96222
const span = document.createElement("span");
97223
span.className = "katex-inline";
98224
span.innerHTML = katex.renderToString(raw, {
@@ -101,81 +227,19 @@ function renderKatexInline(container = document.body) {
101227
strict: false,
102228
trust: true
103229
});
104-
frag.appendChild(span);
105-
} catch (e) {
106-
frag.appendChild(document.createTextNode(fullMatch));
107-
console.error("Inline render error:", raw, e.message);
108-
}
109-
110-
lastIndex = matchStart + fullMatch.length;
111-
});
112-
113-
// 添加剩余文本
114-
if (lastIndex < text.length) {
115-
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
116-
}
117-
118-
if (frag.hasChildNodes()) {
119-
node.parentNode.replaceChild(frag, node);
120-
}
230+
return span;
231+
},
232+
"Inline render error:"
233+
);
121234
});
122235
}
123236

124237
function renderKatexDisplayBlock(container = document.body) {
125-
// 使用非贪婪匹配,允许多行但不跨过多段落
126-
const blockRegex = /(?<!\\)(\$\$([\s\S]+?)\$\$|(?<!\\)\\\[([\s\S]+?)\\\])/g;
127-
128-
const walker = document.createTreeWalker(
129-
container,
130-
NodeFilter.SHOW_TEXT,
131-
{
132-
acceptNode: function (node) {
133-
// 跳过已渲染的节点
134-
if (
135-
node.parentNode &&
136-
(node.parentNode.classList?.contains('katex') ||
137-
node.parentNode.closest("pre, code, .katex"))
138-
) {
139-
return NodeFilter.FILTER_REJECT;
140-
}
141-
142-
const value = node.nodeValue || '';
143-
if (value.includes("$$") || value.includes("\\[")) {
144-
return NodeFilter.FILTER_ACCEPT;
145-
}
146-
return NodeFilter.FILTER_REJECT;
147-
},
148-
}
149-
);
150-
151-
const nodesToReplace = [];
152-
while (walker.nextNode()) {
153-
nodesToReplace.push(walker.currentNode);
154-
}
155-
156-
nodesToReplace.forEach((node) => {
157-
const text = node.nodeValue;
158-
const frag = document.createDocumentFragment();
159-
let lastIndex = 0;
160-
161-
// 重置正则状态
162-
blockRegex.lastIndex = 0;
163-
164-
const matches = [...text.matchAll(blockRegex)];
165-
166-
matches.forEach((match) => {
167-
const fullMatch = match[0];
168-
const dollarContent = match[2];
169-
const bracketContent = match[3];
170-
const raw = (dollarContent || bracketContent).trim();
171-
const matchStart = match.index;
172-
173-
// 添加匹配前的文本
174-
if (matchStart > lastIndex) {
175-
frag.appendChild(document.createTextNode(text.slice(lastIndex, matchStart)));
176-
}
177-
178-
try {
238+
getTextNodes(container, (value) => value.includes("$$") || value.includes("\\[")).forEach((node) => {
239+
replaceTextNode(
240+
node,
241+
collectBlockMatches(node.nodeValue || ''),
242+
(raw) => {
179243
const div = document.createElement("div");
180244
div.className = "katex-block";
181245
div.innerHTML = katex.renderToString(raw, {
@@ -184,23 +248,10 @@ function renderKatexDisplayBlock(container = document.body) {
184248
strict: false,
185249
trust: true
186250
});
187-
frag.appendChild(div);
188-
} catch (e) {
189-
frag.appendChild(document.createTextNode(fullMatch));
190-
console.error("Block render error:", raw, e.message);
191-
}
192-
193-
lastIndex = matchStart + fullMatch.length;
194-
});
195-
196-
// 添加剩余文本
197-
if (lastIndex < text.length) {
198-
frag.appendChild(document.createTextNode(text.slice(lastIndex)));
199-
}
200-
201-
if (frag.hasChildNodes()) {
202-
node.parentNode.replaceChild(frag, node);
203-
}
251+
return div;
252+
},
253+
"Block render error:"
254+
);
204255
});
205256
}
206257

0 commit comments

Comments
 (0)