|
228 | 228 | },
|
229 | 229 |
|
230 | 230 | processAlerts: () => {
|
231 |
| - // Process GitHub-style markdown alerts |
232 | 231 | document.querySelectorAll('blockquote').forEach(blockquote => {
|
233 | 232 | const firstParagraph = blockquote.querySelector('p:first-child');
|
234 | 233 | if (!firstParagraph) return;
|
235 | 234 |
|
236 |
| - const alertMatch = firstParagraph.textContent.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(.*)/i); |
| 235 | + // 1. 检查段落的第一个子节点是否是文本节点 |
| 236 | + let firstChildNode = firstParagraph.firstChild; |
| 237 | + let alertMatch = null; |
| 238 | + |
| 239 | + if (firstChildNode && firstChildNode.nodeType === Node.TEXT_NODE) { |
| 240 | + // 2. 在第一个文本节点的值中查找匹配项 |
| 241 | + // 注意:这里的 \s* 仍然匹配 tag 后面的空格或换行符 |
| 242 | + alertMatch = firstChildNode.nodeValue.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/i); |
| 243 | + } |
| 244 | + |
| 245 | + // 如果第一个子节点不是文本节点,或者文本节点不匹配,则退出 |
237 | 246 | if (!alertMatch) return;
|
238 | 247 |
|
239 | 248 | const iconPaths = {
|
|
246 | 255 |
|
247 | 256 | const alertType = alertMatch[1].toLowerCase();
|
248 | 257 | const alertTitle = alertType.charAt(0).toUpperCase() + alertType.slice(1);
|
249 |
| - const additionalTitleText = alertMatch[2] ? alertMatch[2] : ''; |
250 | 258 |
|
251 | 259 | // Create alert container
|
252 | 260 | const alertDiv = document.createElement('div');
|
253 | 261 | alertDiv.className = `markdown-alert ${alertType}`;
|
254 | 262 |
|
255 |
| - // Create title element with SVG icon |
| 263 | + // Create title element |
256 | 264 | const titleDiv = document.createElement('div');
|
257 | 265 | titleDiv.className = 'markdown-alert-title';
|
258 |
| - |
259 | 266 | if (iconPaths[alertType]) {
|
260 | 267 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
261 | 268 | svg.setAttribute('viewBox', '0 0 16 16');
|
262 | 269 | svg.setAttribute('width', '16');
|
263 | 270 | svg.setAttribute('height', '16');
|
| 271 | + svg.setAttribute('aria-hidden', 'true'); |
264 | 272 | const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
265 | 273 | path.setAttribute('d', iconPaths[alertType]);
|
266 | 274 | svg.appendChild(path);
|
267 | 275 | titleDiv.appendChild(svg);
|
268 | 276 | }
|
269 |
| - |
270 | 277 | const titleText = document.createTextNode(alertTitle);
|
271 | 278 | titleDiv.appendChild(titleText);
|
272 | 279 | alertDiv.appendChild(titleDiv);
|
273 | 280 |
|
274 |
| - // Create content paragraph if there's additional text |
275 |
| - if (additionalTitleText) { |
276 |
| - const contentParagraph = document.createElement('p'); |
277 |
| - contentParagraph.textContent = additionalTitleText; |
278 |
| - alertDiv.appendChild(contentParagraph); |
| 281 | + // 3. 获取匹配到的字符串的实际长度 (e.g., "[!NOTE]\n") |
| 282 | + const matchedLength = alertMatch[0].length; |
| 283 | + |
| 284 | + // 4. 修改第一个文本节点的值,移除匹配到的部分 |
| 285 | + firstChildNode.nodeValue = firstChildNode.nodeValue.substring(matchedLength); |
| 286 | + |
| 287 | + if (firstChildNode.nodeValue.length === 0) { |
| 288 | + const nodeToRemove = firstChildNode; |
| 289 | + firstChildNode = firstChildNode.nextSibling; // 先移动到下一个兄弟节点 |
| 290 | + nodeToRemove.remove(); // 再移除空节点 |
279 | 291 | }
|
280 | 292 |
|
281 |
| - // Remove the first paragraph |
282 |
| - firstParagraph.remove(); |
| 293 | + // 6. 检查紧随其后的节点是否是 <br>,如果是,则移除 |
| 294 | + // 这处理了 Markdown 渲染器可能在 tag 后的换行处插入 <br> 的情况 |
| 295 | + if (firstChildNode && firstChildNode.nodeType === Node.ELEMENT_NODE && firstChildNode.tagName === 'BR') { |
| 296 | + const brToRemove = firstChildNode; |
| 297 | + firstChildNode = firstChildNode.nextSibling; // 移动到 <br> 之后的节点 |
| 298 | + brToRemove.remove(); |
| 299 | + } |
| 300 | + |
| 301 | + // 7. 对当前的第一个文本节点(如果存在)执行 trimStart |
| 302 | + // 这可以清除移除 tag 或 <br> 后可能残留的前导空格 |
| 303 | + if (firstChildNode && firstChildNode.nodeType === Node.TEXT_NODE) { |
| 304 | + firstChildNode.nodeValue = firstChildNode.nodeValue.trimStart(); |
| 305 | + // 如果 trim 后变空,也移除 |
| 306 | + if (firstChildNode.nodeValue.length === 0) { |
| 307 | + firstChildNode.remove(); |
| 308 | + // 注意:这里我们不需要再追踪 nextSibling,因为段落的整体内容检查会处理 |
| 309 | + } |
| 310 | + } |
| 311 | + |
| 312 | + // 8. 检查整个段落现在是否为空 (没有子节点,或者所有子节点加起来的 textContent 为空) |
| 313 | + const paragraphIsEmpty = !firstParagraph.hasChildNodes() || firstParagraph.textContent.trim() === ''; |
| 314 | + |
| 315 | + // 9. 将处理过的段落(如果不为空)和 blockquote 中剩余的所有其他内容移动到 alertDiv |
| 316 | + if (!paragraphIsEmpty) { |
| 317 | + alertDiv.appendChild(firstParagraph); |
| 318 | + } else { |
| 319 | + firstParagraph.remove(); // 如果段落变空了,就彻底移除 |
| 320 | + } |
283 | 321 |
|
284 |
| - // Move remaining content to the alert |
| 322 | + // 移动 blockquote 中的任何剩余子节点(例如多段落情况) |
285 | 323 | while (blockquote.firstChild) {
|
286 | 324 | alertDiv.appendChild(blockquote.firstChild);
|
287 | 325 | }
|
|
0 commit comments