Fix renderMdToFragment infinite loop on unmatched ** / backtick (streaming-partial freeze)
Browse files- index.html +18 -1
index.html
CHANGED
|
@@ -1218,11 +1218,18 @@ function el(tag, opts){
|
|
| 1218 |
return e;
|
| 1219 |
}
|
| 1220 |
|
| 1221 |
-
/* Safe markdown — escape implicit, only **bold** and `code` are recognized.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1222 |
function renderMdToFragment(s){
|
| 1223 |
const frag = document.createDocumentFragment();
|
| 1224 |
let i = 0;
|
| 1225 |
while (i < s.length){
|
|
|
|
| 1226 |
// **bold**
|
| 1227 |
if (s[i] === '*' && s[i+1] === '*'){
|
| 1228 |
const end = s.indexOf('**', i + 2);
|
|
@@ -1231,6 +1238,10 @@ function renderMdToFragment(s){
|
|
| 1231 |
i = end + 2;
|
| 1232 |
continue;
|
| 1233 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1234 |
}
|
| 1235 |
// `code`
|
| 1236 |
if (s[i] === '`'){
|
|
@@ -1240,6 +1251,10 @@ function renderMdToFragment(s){
|
|
| 1240 |
i = end + 1;
|
| 1241 |
continue;
|
| 1242 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1243 |
}
|
| 1244 |
// Plain run — find next special char
|
| 1245 |
let next = i;
|
|
@@ -1250,6 +1265,8 @@ function renderMdToFragment(s){
|
|
| 1250 |
}
|
| 1251 |
frag.appendChild(document.createTextNode(s.slice(i, next)));
|
| 1252 |
i = next;
|
|
|
|
|
|
|
| 1253 |
}
|
| 1254 |
return frag;
|
| 1255 |
}
|
|
|
|
| 1218 |
return e;
|
| 1219 |
}
|
| 1220 |
|
| 1221 |
+
/* Safe markdown — escape implicit, only **bold** and `code` are recognized.
|
| 1222 |
+
IMPORTANT: streaming partial yields can land mid-`**` or mid-`` ` `` pair
|
| 1223 |
+
(e.g. cumulative text "... **The" before the closing `**` arrives). The
|
| 1224 |
+
unmatched-opener cases below are critical: without them the plain-run
|
| 1225 |
+
inner loop breaks immediately on the same `**`, the slice is empty, `i`
|
| 1226 |
+
doesn't advance, and the outer while loops forever — freezing the tab
|
| 1227 |
+
with "Page Unresponsive" once a streaming chunk happens to end mid-pair. */
|
| 1228 |
function renderMdToFragment(s){
|
| 1229 |
const frag = document.createDocumentFragment();
|
| 1230 |
let i = 0;
|
| 1231 |
while (i < s.length){
|
| 1232 |
+
const startI = i; // safety: guarantee forward progress every iteration
|
| 1233 |
// **bold**
|
| 1234 |
if (s[i] === '*' && s[i+1] === '*'){
|
| 1235 |
const end = s.indexOf('**', i + 2);
|
|
|
|
| 1238 |
i = end + 2;
|
| 1239 |
continue;
|
| 1240 |
}
|
| 1241 |
+
// Unmatched opening (mid-stream partial) — render as plain text and advance.
|
| 1242 |
+
frag.appendChild(document.createTextNode('**'));
|
| 1243 |
+
i += 2;
|
| 1244 |
+
continue;
|
| 1245 |
}
|
| 1246 |
// `code`
|
| 1247 |
if (s[i] === '`'){
|
|
|
|
| 1251 |
i = end + 1;
|
| 1252 |
continue;
|
| 1253 |
}
|
| 1254 |
+
// Unmatched opening — render as plain text and advance.
|
| 1255 |
+
frag.appendChild(document.createTextNode('`'));
|
| 1256 |
+
i += 1;
|
| 1257 |
+
continue;
|
| 1258 |
}
|
| 1259 |
// Plain run — find next special char
|
| 1260 |
let next = i;
|
|
|
|
| 1265 |
}
|
| 1266 |
frag.appendChild(document.createTextNode(s.slice(i, next)));
|
| 1267 |
i = next;
|
| 1268 |
+
// Defense in depth: never let the outer loop spin without progress.
|
| 1269 |
+
if (i === startI) i++;
|
| 1270 |
}
|
| 1271 |
return frag;
|
| 1272 |
}
|