[{"data":1,"prerenderedAt":870},["ShallowReactive",2],{"post-\u002Fposts\u002Fxml-escaping-stress-test":3,"post-nav-\u002Fposts\u002Fxml-escaping-stress-test":831,"post-related-\u002Fposts\u002Fxml-escaping-stress-test":851},{"id":4,"title":5,"body":6,"category":816,"cover":787,"date":817,"description":818,"draft":819,"extension":820,"featured":819,"meta":821,"navigation":387,"path":822,"pinned":387,"seo":823,"stem":824,"tags":825,"updated":829,"__hash__":830},"posts\u002Fposts\u002Fxml-escaping-stress-test.md","特殊字符与代码块压测：& \u003C > \" ' 全都要",{"type":7,"value":8,"toc":807},"minimark",[9,13,16,20,23,52,63,66,314,327,330,633,636,643,646,659,662,755,758,775,778,781,788,791,794,797,800,803],[10,11,12],"p",{},"这篇文章没有别的目的，就是用来把 feed 生成逻辑撑到极限。",[10,14,15],{},"如果你的 RSS 阅读器能正常显示这一篇，说明转义是对的。",[17,18,19],"h2",{"id":19},"裸字符",[10,21,22],{},"下面这些字符在 XML 里都是危险的，必须转义：",[24,25,26,30,37,43,46],"ul",{},[27,28,29],"li",{},"与号：AT&T、Q&A、R&D",[27,31,32,33],{},"小于号：",[34,35,36],"code",{},"a \u003C b",[27,38,39,40],{},"大于号：",[34,41,42],{},"a > b",[27,44,45],{},"引号：他说 \"这样\" 和 '那样'",[27,47,48,49],{},"CDATA 结束标记本身：",[34,50,51],{},"]]>",[10,53,54,55,58,59,62],{},"如果不转义，",[34,56,57],{},"AT&T"," 会把整个 feed 撑坏——因为解析器会以为 ",[34,60,61],{},"&A"," 是一个实体引用的开始。",[17,64,65],{"id":65},"代码块里的尖括号",[67,68,73],"pre",{"className":69,"code":70,"language":71,"meta":72,"style":72},"language-html shiki shiki-themes github-light github-dark","\u003C!DOCTYPE html>\n\u003Chtml lang=\"zh-CN\">\n  \u003Chead>\n    \u003Cmeta charset=\"utf-8\" \u002F>\n    \u003Ctitle>喵洛阁 &amp; 朋友\u003C\u002Ftitle>\n  \u003C\u002Fhead>\n  \u003Cbody>\n    \u003Cp class=\"note\">如果 a \u003C b 并且 b > c，那么 a \u003C c\u003C\u002Fp>\n    \u003Cscript>\n      const html = '\u003Cdiv class=\"x\">' + text + '\u003C\u002Fdiv>'\n      if (a && b) console.log('&lt;ok&gt;')\n    \u003C\u002Fscript>\n  \u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html","",[34,74,75,95,115,126,146,168,178,188,221,231,258,285,295,304],{"__ignoreMap":72},[76,77,80,84,88,92],"span",{"class":78,"line":79},"line",1,[76,81,83],{"class":82},"sVt8B","\u003C!",[76,85,87],{"class":86},"s9eBZ","DOCTYPE",[76,89,91],{"class":90},"sScJk"," html",[76,93,94],{"class":82},">\n",[76,96,98,101,103,106,109,113],{"class":78,"line":97},2,[76,99,100],{"class":82},"\u003C",[76,102,71],{"class":86},[76,104,105],{"class":90}," lang",[76,107,108],{"class":82},"=",[76,110,112],{"class":111},"sZZnC","\"zh-CN\"",[76,114,94],{"class":82},[76,116,118,121,124],{"class":78,"line":117},3,[76,119,120],{"class":82},"  \u003C",[76,122,123],{"class":86},"head",[76,125,94],{"class":82},[76,127,129,132,135,138,140,143],{"class":78,"line":128},4,[76,130,131],{"class":82},"    \u003C",[76,133,134],{"class":86},"meta",[76,136,137],{"class":90}," charset",[76,139,108],{"class":82},[76,141,142],{"class":111},"\"utf-8\"",[76,144,145],{"class":82}," \u002F>\n",[76,147,149,151,154,157,161,164,166],{"class":78,"line":148},5,[76,150,131],{"class":82},[76,152,153],{"class":86},"title",[76,155,156],{"class":82},">喵洛阁 ",[76,158,160],{"class":159},"sj4cs","&amp;",[76,162,163],{"class":82}," 朋友\u003C\u002F",[76,165,153],{"class":86},[76,167,94],{"class":82},[76,169,171,174,176],{"class":78,"line":170},6,[76,172,173],{"class":82},"  \u003C\u002F",[76,175,123],{"class":86},[76,177,94],{"class":82},[76,179,181,183,186],{"class":78,"line":180},7,[76,182,120],{"class":82},[76,184,185],{"class":86},"body",[76,187,94],{"class":82},[76,189,191,193,195,198,200,203,206,209,212,214,217,219],{"class":78,"line":190},8,[76,192,131],{"class":82},[76,194,10],{"class":86},[76,196,197],{"class":90}," class",[76,199,108],{"class":82},[76,201,202],{"class":111},"\"note\"",[76,204,205],{"class":82},">如果 a ",[76,207,100],{"class":208},"s7hpK",[76,210,211],{"class":82}," b 并且 b > c，那么 a ",[76,213,100],{"class":208},[76,215,216],{"class":82}," c\u003C\u002F",[76,218,10],{"class":86},[76,220,94],{"class":82},[76,222,224,226,229],{"class":78,"line":223},9,[76,225,131],{"class":82},[76,227,228],{"class":86},"script",[76,230,94],{"class":82},[76,232,234,238,240,243,246,249,252,255],{"class":78,"line":233},10,[76,235,237],{"class":236},"szBVR","      const",[76,239,91],{"class":159},[76,241,242],{"class":236}," =",[76,244,245],{"class":111}," '\u003Cdiv class=\"x\">'",[76,247,248],{"class":236}," +",[76,250,251],{"class":82}," text ",[76,253,254],{"class":236},"+",[76,256,257],{"class":111}," '\u003C\u002Fdiv>'\n",[76,259,261,264,267,270,273,276,279,282],{"class":78,"line":260},11,[76,262,263],{"class":236},"      if",[76,265,266],{"class":82}," (a ",[76,268,269],{"class":236},"&&",[76,271,272],{"class":82}," b) console.",[76,274,275],{"class":90},"log",[76,277,278],{"class":82},"(",[76,280,281],{"class":111},"'&lt;ok&gt;'",[76,283,284],{"class":82},")\n",[76,286,288,291,293],{"class":78,"line":287},12,[76,289,290],{"class":82},"    \u003C\u002F",[76,292,228],{"class":86},[76,294,94],{"class":82},[76,296,298,300,302],{"class":78,"line":297},13,[76,299,173],{"class":82},[76,301,185],{"class":86},[76,303,94],{"class":82},[76,305,307,310,312],{"class":78,"line":306},14,[76,308,309],{"class":82},"\u003C\u002F",[76,311,71],{"class":86},[76,313,94],{"class":82},[10,315,316,317,319,320,319,323,326],{},"上面这块 HTML 里 ",[34,318,100],{},"、",[34,321,322],{},">",[34,324,325],{},"&","、引号全都有。它在页面里应该原样显示，在 feed 里应该被正确转义。",[10,328,329],{},"再来一段 TypeScript：",[67,331,335],{"className":332,"code":333,"language":334,"meta":72,"style":72},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F 这里的箭头函数、泛型尖括号、比较符号都不应该破坏 XML\ntype Predicate\u003CT> = (value: T) => boolean\n\nfunction filterAnd\u003CT>(items: T[], pred: Predicate\u003CT>): T[] {\n  const out: T[] = []\n  for (const item of items) {\n    if (item && pred(item) && (out as unknown[]).length \u003C 100) {\n      out.push(item)\n    }\n  }\n  return out\n}\n\nconst flags = filterAnd([1, 2, 3], (n) => n > 1 && n \u003C 3)\nconsole.log('结果：', flags, '& 完成')\n","ts",[34,336,337,343,383,389,435,455,474,516,527,532,537,545,550,554,612],{"__ignoreMap":72},[76,338,339],{"class":78,"line":79},[76,340,342],{"class":341},"sJ8bj","\u002F\u002F 这里的箭头函数、泛型尖括号、比较符号都不应该破坏 XML\n",[76,344,345,348,351,353,356,359,361,364,368,371,374,377,380],{"class":78,"line":97},[76,346,347],{"class":236},"type",[76,349,350],{"class":90}," Predicate",[76,352,100],{"class":82},[76,354,355],{"class":90},"T",[76,357,358],{"class":82},"> ",[76,360,108],{"class":236},[76,362,363],{"class":82}," (",[76,365,367],{"class":366},"s4XuR","value",[76,369,370],{"class":236},":",[76,372,373],{"class":90}," T",[76,375,376],{"class":82},") ",[76,378,379],{"class":236},"=>",[76,381,382],{"class":159}," boolean\n",[76,384,385],{"class":78,"line":117},[76,386,388],{"emptyLinePlaceholder":387},true,"\n",[76,390,391,394,397,399,401,404,407,409,411,414,417,419,421,423,425,428,430,432],{"class":78,"line":128},[76,392,393],{"class":236},"function",[76,395,396],{"class":90}," filterAnd",[76,398,100],{"class":82},[76,400,355],{"class":90},[76,402,403],{"class":82},">(",[76,405,406],{"class":366},"items",[76,408,370],{"class":236},[76,410,373],{"class":90},[76,412,413],{"class":82},"[], ",[76,415,416],{"class":366},"pred",[76,418,370],{"class":236},[76,420,350],{"class":90},[76,422,100],{"class":82},[76,424,355],{"class":90},[76,426,427],{"class":82},">)",[76,429,370],{"class":236},[76,431,373],{"class":90},[76,433,434],{"class":82},"[] {\n",[76,436,437,440,443,445,447,450,452],{"class":78,"line":148},[76,438,439],{"class":236},"  const",[76,441,442],{"class":159}," out",[76,444,370],{"class":236},[76,446,373],{"class":90},[76,448,449],{"class":82},"[] ",[76,451,108],{"class":236},[76,453,454],{"class":82}," []\n",[76,456,457,460,462,465,468,471],{"class":78,"line":170},[76,458,459],{"class":236},"  for",[76,461,363],{"class":82},[76,463,464],{"class":236},"const",[76,466,467],{"class":159}," item",[76,469,470],{"class":236}," of",[76,472,473],{"class":82}," items) {\n",[76,475,476,479,482,484,487,490,492,495,498,501,504,507,510,513],{"class":78,"line":180},[76,477,478],{"class":236},"    if",[76,480,481],{"class":82}," (item ",[76,483,269],{"class":236},[76,485,486],{"class":90}," pred",[76,488,489],{"class":82},"(item) ",[76,491,269],{"class":236},[76,493,494],{"class":82}," (out ",[76,496,497],{"class":236},"as",[76,499,500],{"class":159}," unknown",[76,502,503],{"class":82},"[]).",[76,505,506],{"class":159},"length",[76,508,509],{"class":236}," \u003C",[76,511,512],{"class":159}," 100",[76,514,515],{"class":82},") {\n",[76,517,518,521,524],{"class":78,"line":190},[76,519,520],{"class":82},"      out.",[76,522,523],{"class":90},"push",[76,525,526],{"class":82},"(item)\n",[76,528,529],{"class":78,"line":223},[76,530,531],{"class":82},"    }\n",[76,533,534],{"class":78,"line":233},[76,535,536],{"class":82},"  }\n",[76,538,539,542],{"class":78,"line":260},[76,540,541],{"class":236},"  return",[76,543,544],{"class":82}," out\n",[76,546,547],{"class":78,"line":287},[76,548,549],{"class":82},"}\n",[76,551,552],{"class":78,"line":297},[76,553,388],{"emptyLinePlaceholder":387},[76,555,556,558,561,563,565,568,571,574,577,579,582,585,588,590,592,595,597,600,603,605,607,610],{"class":78,"line":306},[76,557,464],{"class":236},[76,559,560],{"class":159}," flags",[76,562,242],{"class":236},[76,564,396],{"class":90},[76,566,567],{"class":82},"([",[76,569,570],{"class":159},"1",[76,572,573],{"class":82},", ",[76,575,576],{"class":159},"2",[76,578,573],{"class":82},[76,580,581],{"class":159},"3",[76,583,584],{"class":82},"], (",[76,586,587],{"class":366},"n",[76,589,376],{"class":82},[76,591,379],{"class":236},[76,593,594],{"class":82}," n ",[76,596,322],{"class":236},[76,598,599],{"class":159}," 1",[76,601,602],{"class":236}," &&",[76,604,594],{"class":82},[76,606,100],{"class":236},[76,608,609],{"class":159}," 3",[76,611,284],{"class":82},[76,613,615,618,620,622,625,628,631],{"class":78,"line":614},15,[76,616,617],{"class":82},"console.",[76,619,275],{"class":90},[76,621,278],{"class":82},[76,623,624],{"class":111},"'结果：'",[76,626,627],{"class":82},", flags, ",[76,629,630],{"class":111},"'& 完成'",[76,632,284],{"class":82},[17,634,635],{"id":635},"内联代码与引用",[10,637,638,639,642],{},"行内代码 ",[34,640,641],{},"const re = \u002F\u003C(\\w+)>\u002Fg"," 里也有尖括号。",[10,644,645],{},"下面是一个引用块，里面同时含 HTML 和 shell：",[647,648,649,656],"blockquote",{},[10,650,651,652,655],{},"试试 ",[34,653,654],{},"echo \"\u003Chello> & goodbye\""," 这条命令。",[10,657,658],{},"它会输出带尖括号和与号的字符串，而 shell 会尝试把它当成重定向或子命令。",[17,660,661],{"id":661},"表格",[663,664,665,681],"table",{},[666,667,668],"thead",{},[669,670,671,675,678],"tr",{},[672,673,674],"th",{},"字符",[672,676,677],{},"XML 实体",[672,679,680],{},"是否必须",[682,683,684,698,711,725,740],"tbody",{},[669,685,686,691,695],{},[687,688,689],"td",{},[34,690,325],{},[687,692,693],{},[34,694,160],{},[687,696,697],{},"必须",[669,699,700,704,709],{},[687,701,702],{},[34,703,100],{},[687,705,706],{},[34,707,708],{},"&lt;",[687,710,697],{},[669,712,713,717,722],{},[687,714,715],{},[34,716,322],{},[687,718,719],{},[34,720,721],{},"&gt;",[687,723,724],{},"建议",[669,726,727,732,737],{},[687,728,729],{},[34,730,731],{},"\"",[687,733,734],{},[34,735,736],{},"&quot;",[687,738,739],{},"属性内必须",[669,741,742,747,752],{},[687,743,744],{},[34,745,746],{},"'",[687,748,749],{},[34,750,751],{},"&apos;",[687,753,754],{},"属性内建议",[17,756,757],{"id":757},"链接与图片",[10,759,760,761,319,766,319,770,774],{},"站内绝对链接：",[762,763,765],"a",{"href":764},"\u002Farchive","归档",[762,767,769],{"href":768},"\u002Ftags","标签",[762,771,773],{"href":772},"\u002Fabout","关于","。",[10,776,777],{},"相对链接在 feed 里必须被改写成基于站点的绝对链接，否则阅读器里点不动。",[10,779,780],{},"图片同理：",[10,782,783],{},[784,785],"img",{"alt":786,"src":787},"占位图","\u002Fimages\u002Fcovers\u002Fcharset.png",[17,789,790],{"id":790},"一段长文本",[10,792,793],{},"Lorem ipsum 不如直接说清楚：这一段就是用来测行高和段距的。中文排版里，1.75 的行高配合 17px 的字号，一行大约能放下 44 个汉字，这个宽度在多数屏幕上读起来是舒服的。再宽一点，眼睛回行会累；再窄一点，一句话要折三次，节奏就散了。",[10,795,796],{},"段落间距必须明显大于行距。我用的是 1.2em——看起来差别不大，但眯眼一看就知道哪里断开了。如果段落间距等于行距，整页会糊成一块灰，读者找不到停顿点，读三行就想走。",[17,798,799],{"id":799},"结尾",[10,801,802],{},"如果这一篇在 reader 里结构完好、代码块没跑版、图片能加载、链接可点，那 feed 就是对的。",[804,805,806],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#57606A;--shiki-dark:#9AA4B0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":72,"searchDepth":97,"depth":97,"links":808},[809,810,811,812,813,814,815],{"id":19,"depth":97,"text":19},{"id":65,"depth":97,"text":65},{"id":635,"depth":97,"text":635},{"id":661,"depth":97,"text":661},{"id":757,"depth":97,"text":757},{"id":790,"depth":97,"text":790},{"id":799,"depth":97,"text":799},"工程","2025-10-20","一篇专门用来喂 RSS\u002FAtom 校验器的文章。正文里塞满了 & \u003C > 引号、CDATA 边界、嵌套代码块和原始 HTML 片段。",false,"md",{},"\u002Fposts\u002Fxml-escaping-stress-test",{"title":5,"description":818},"posts\u002Fxml-escaping-stress-test",[826,827,828],"RSS","XML","测试",null,"l2UEWoh2O9VsDBdNBDseh4UDp-hC_p9il6jZjuWoDns",{"prev":832,"next":842},{"path":833,"title":834,"description":835,"date":836,"updated":829,"tags":837,"category":839,"cover":841,"draft":819,"pinned":387,"featured":819},"\u002Fposts\u002Ftypography-over-color","排版比配色更决定气质","把同样一套颜色交给两个人排，出来的东西可以完全不同。这篇讲我怎么用字重、行距和留白，在不换颜色的前提下改变一个页面的脾气。","2025-10-28",[838,839,840],"排版","设计","CSS","\u002Fimages\u002Fcovers\u002Ftypography.png",{"path":843,"title":844,"description":845,"date":846,"updated":829,"tags":847,"category":816,"cover":829,"draft":819,"pinned":819,"featured":819},"\u002Fposts\u002Fdeleted-90-percent","我删掉了站点的 90% 功能","一个博客需要什么？列表、文章、归档、搜索、订阅。其他东西我全删了，然后发现加载时间从 2.3 秒掉到了 380 毫秒。","2025-10-12",[848,849,850],"性能","架构","反思",[852,854,863],{"path":843,"title":844,"description":845,"date":846,"updated":829,"tags":853,"category":816,"cover":829,"draft":819,"pinned":819,"featured":819},[848,849,850],{"path":855,"title":856,"description":857,"date":858,"updated":829,"tags":859,"category":816,"cover":829,"draft":819,"pinned":819,"featured":819},"\u002Fposts\u002Fcjk-tokenizer","中文分词为什么把搜索做坏了","MiniSearch 默认按空格切词，对中文来说等于完全失效。这篇讲 CJK 该怎么切，以及 bigram 加单字兜底为什么比单纯整词匹配更靠谱。","2025-09-30",[860,861,862],"搜索","MiniSearch","中文",{"path":864,"title":865,"description":866,"date":867,"updated":829,"tags":868,"category":816,"cover":829,"draft":819,"pinned":819,"featured":819},"\u002Fposts\u002Fstatic-search","静态博客的搜索为什么不用后端","300KB 的索引、一次懒加载、纯前端查询。对一个几百篇文章的博客来说，这个方案比任何搜索引擎都合适。","2025-09-05",[860,849,869],"SSG",1794113901485]