From 17a413d627d3bfde7e662800d66d736f93af0e85 Mon Sep 17 00:00:00 2001 From: jrtxio Date: Sun, 9 Aug 2026 21:39:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BB=A3=E7=A0=81=E5=9D=97=E5=90=9E?= =?UTF-8?q?=E6=AD=A3=E6=96=87=EF=BC=88=E9=97=AD=E5=90=88=E5=9B=B4=E6=A0=8F?= =?UTF-8?q?=E8=A2=AB=E8=AF=AF=E6=94=B9=E4=B8=BA=20```text=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crawler.py 的 format_content() 围栏正则 ^```\w*$ 无法识别含特殊 字符的语言标记(如 ```c#、```c++,因 # 不属于 \w),导致开启围栏 漏识别、状态机不翻转。随后遇到合法的闭合围栏 ``` 时被误判为开启 围栏,改写成 ```text —— 而 CommonMark 规定闭合围栏不能带 info string,于是 ```text 成了新代码块的起始标记,后续所有正文被吞进 未闭合的代码块。 影响 3 篇文章共 16 处坏围栏: - Kotlin 和 Checked Exception.md(1 处) - C# 的 IDisposable 接口.md(5 处) - 对 Rust 语言的分析.md(10 处) 修复: 1. crawler.py: 围栏正则改为 ^(\s{0,3})(`{3,})([^`]*)$,可识别任意 语言标记;状态机区分开启/闭合分支,闭合围栏不再被改写。 2. 新增 scripts/fix_code_fences.py: 用修复后的状态机扫描全部文章, 将错误的 ```text 闭合围栏还原为裸 ```(幂等,已跑 0 改动验证)。 3. 修复 3 篇文章的 16 处坏数据。 验证:用项目 markdown-it 重新渲染全部 173 篇文章,0 处代码块吞正文。 --- ...4 IDisposable \346\216\245\345\217\243.md" | 10 +- .../Kotlin \345\222\214 Checked Exception.md" | 2 +- ...00\347\232\204\345\210\206\346\236\220.md" | 20 ++-- scripts/crawler.py | 21 +++- scripts/fix_code_fences.py | 100 ++++++++++++++++++ 5 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 scripts/fix_code_fences.py diff --git "a/posts/C# \347\232\204 IDisposable \346\216\245\345\217\243.md" "b/posts/C# \347\232\204 IDisposable \346\216\245\345\217\243.md" index a12010e..4b22c9d 100644 --- "a/posts/C# \347\232\204 IDisposable \346\216\245\345\217\243.md" +++ "b/posts/C# \347\232\204 IDisposable \346\216\245\345\217\243.md" @@ -23,7 +23,7 @@ void foo() // 使用 _event ... event.Dispose(); } -```text +``` 貌似没什么困难嘛,我们把每个对象的 Dispose 方法都调用一下,不就得了?然而问题远远不是这么简单。很多时候你根本搞不清楚什么时候该释放一个对象,因为它存在于一个复杂,动态变化的数据结构里面。除非你使用引用计数,否则你没有办法确定调用 Dispose 的时机。如果你过早调用了 Dispose 方法,而其实还有人在用它,就会出现严重的错误。 @@ -45,7 +45,7 @@ void main() printf("%d, %d, %d\n", *a, *b, *c); } -```text +``` 你知道这个程序最后是什么结果吗?自己运行一下看看吧。所以对于复杂的数据结构,比如图节点,你就只好给对象加上引用计数。相信我,使用引用计数很痛苦。或者如果你的内存够用,也不需要分配释放很多中间结果,那你就干脆把这些对象都放进一个“池子”,到算法结束以后再一并释放它们…… @@ -88,7 +88,7 @@ void foo() // ... x = null; } -```text +``` `x = null` 是毫无意义的。写出这样的代码,说明他们不明白 GC 是如何工作的,以为把引用设为 null 就可以释放内存,以为不把引用设为 null ,内存就不会被回收!再进一步,如果你仔细看 HashAlgorithm 的源代码,就会发现 HashValue 这个成员数组其实没有必要存在,因为它保存的只是上一次调用 ComputeHash() 的结果而已。这种保存结果的事情,本来应该交给使用者去做,而不是包揽到自己身上。这个数组的存在,还导致你没法重用同一个 HashAlgorithm 对象,因为有共享的成员 HashValue ,所以不再是 thread safe 的。 @@ -116,7 +116,7 @@ void UseFoo() foo.Dispose(); // 没必要 foo = null; // 没必要 } -```text +``` 这里的 `foo.Dispose()` 是完全没必要的。你甚至没必要写 `foo = null`,因为 foo 是一个局部变量,它一般很快就会离开作用域的。当函数执行完毕,或者编译器推断 foo 不会再次被使用的时候, GC 会回收整个 Foo 对象,包括里面的巨大数组。 @@ -158,7 +158,7 @@ protected override void Dispose(bool disposing) { Dispose(false); } -```text +``` 当 SafeHandle 被 GC 回收的时候, GC 会自动自动调用这个析构函数,进而调用 Dispose 。也就是说,你其实并不需要手动调用这些对象(例如 ManualResetEvent, Semaphore 之类)的 Dispose 方法,因为 GC 会调用它们。这些对象占用资源不多,系统里也不会有很多这种对象,所以 GC 完全应该有能力释放它们占用的系统资源。 diff --git "a/posts/Kotlin \345\222\214 Checked Exception.md" "b/posts/Kotlin \345\222\214 Checked Exception.md" index 38c51ae..c37bba0 100644 --- "a/posts/Kotlin \345\222\214 Checked Exception.md" +++ "b/posts/Kotlin \345\222\214 Checked Exception.md" @@ -83,7 +83,7 @@ catch (Exception) { ... } -```text +``` 注意到了吗,这也就是你写 Java 代码时,能写出的最糟糕的异常处理代码!因为不知道 foo 函数里面会有什么异常出现,所以你的 catch 语句里面也不知道该做什么。大部分人只能在里面放一条 log ,记录异常的发生。这是一种非常糟糕的写法,不但繁复,而且可能掩盖运行时错误。有时候你发现有些语句莫名其妙没有执行,折腾好久才发现是因为某个地方抛出了异常,所以跳到了这种 catch 的地方,然后被忽略了。如果你忘了写 catch (Exception),那么你的代码可能运行了一段时间之后当掉,因为忽然出现一个测试时没出现过的异常…… diff --git "a/posts/\345\257\271 Rust \350\257\255\350\250\200\347\232\204\345\210\206\346\236\220.md" "b/posts/\345\257\271 Rust \350\257\255\350\250\200\347\232\204\345\210\206\346\236\220.md" index 63ff78b..8c2c166 100644 --- "a/posts/\345\257\271 Rust \350\257\255\350\250\200\347\232\204\345\210\206\346\236\220.md" +++ "b/posts/\345\257\271 Rust \350\257\255\350\250\200\347\232\204\345\210\206\346\236\220.md" @@ -80,7 +80,7 @@ var id = ...; var slot = ...; var user = ...; var passwd = ...; -```text +``` 我需要把鼠标移到变量上面,让 Visual Studio 显示出它推导出来的类型,可是鼠标移开之后,我可能又忘了它是什么。有时候发现看同一片代码,都需要反复的做这件事,鼠标移来移去的。而且要是没有 Visual Studio ,用其它编辑器,或者在 github 上看代码或者 code review 的时候,你就得不到这种信息了。很多 C# 程序员为了避免这个问题,开始用很长的变量名,把类型的名字加在变量名字里面去,这样一来反而更复杂了,却没有想到直接把类型写出来。所以这种形式的类型推导,看似先进或者方便,其实还不如直接在声明处写下变量的类型,就像 Java 那样。 @@ -93,7 +93,7 @@ Rust 的文档说它是一种“[大部分基于表达式](https://doc.rust-lang ```rust let mut y = 5; let x = (y = 6); // x has the value `()`, not `6` -```text +``` 奇怪的是,这里变量 `x` 会得到一个值,空的 tuple ,`()`。这种思路不大对,它是从像 OCaml 那样的语言照搬过来的,而 OCaml 本身就有问题。在 OCaml 里面,如果你使用 `print_string`,那你会得到如下的结果: @@ -102,7 +102,7 @@ print_string "hello world!\n";; hello world! - : unit = () -```text +``` 这里,`print_string` 是一个“动作”,它对应过程式语言里面的“statement”。就像 C 语言的 `printf`。动作通常只产生“副作用”,而不返回值。在 OCaml 里面,为了“理论的优雅”,动作也会返回一个值,这个值叫做 `()`。其实 `()` 相当于 C 语言的 void 。 C 语言里面有 void 类型,然而它却不允许你声明一个 void 类型的变量。比如你写 @@ -111,7 +111,7 @@ int main() { void x; } -```text +``` 程序是没法编译通过的(试一试?)。让人惊讶的是,古老的 C 的做法其实是正确的,这里有比较深入的原因。如果你把一个类型看成是一个集合(比如 int 是机器整数的集合),那么 void 所表示的集合是个空集,它里面是不含有任何元素的。声明一个 void 类型的变量是没有任何意义的,因为它不可能有一个值。如果一个函数返回 void ,你是没法把它赋值给一个变量的。 @@ -129,7 +129,7 @@ Rust 的设计者似乎很推崇“面向表达式”的语言,所以在 Rust fn add_one(x: i32) -> i32 { x + 1 } -```text +``` 返回函数里的最后一个表达式,而不需要写 return 语句,这是函数式语言共有的特征。然而其实我觉得直接写 return 其实是更好的作法,像这个样子: @@ -137,7 +137,7 @@ fn add_one(x: i32) -> i32 { fn foo(x: i32) -> i32 { return x + 1; } -```text +``` 编程有一个容易引起问题的作法,叫做“不够明确”,总想让编译器自动去处理一些问题,在这里也是一样的问题。如果你隐性的返回函数里最后一个表达式,那么每一次看见这个函数,你都必须去搞清楚最后一个表达式是什么,这并不是每次都那么明显的。比如下面这段代码: @@ -160,7 +160,7 @@ fn add_one(x: i32) -> i32 { x / 2 } } -```text +``` 由于 if 语句里面有嵌套,每个分支又有好些代码,而且 if 语句又是最后一个语句,所以这个嵌套 if 的三个出口的最后一个表达式都是返回值。如果你写了“return”,那么你可以直接看有几个“return”,或者拿编辑器加亮一下,就知道这个函数有几个出口。然而现在没有了“return”这个关键字,你就必须把最后那个 if 语句自己看清楚了,找到每一个分支的“最后表达式”。很多时候这不是那么明显,你总需要找一下,而且这件事在读代码的时候总是反复做。 @@ -180,7 +180,7 @@ fn main() { m[0] = 10; // 出错 m = [4, 5, 6]; // 也出错 } -```text +``` ```rust fn main() { @@ -188,7 +188,7 @@ fn main() { m[0] = 10; // 不出错 m = [4, 5, 6]; // 也不出错 } -```text +``` ### 内存管理 @@ -203,7 +203,7 @@ Rust 那些炫酷的 move semantics, borrowing, lifetime 之类的概念加在 ```rust fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { } -```text +``` 上一次我看 Rust 文档的时候,没发现有 lifetime 这概念。文档对此的介绍非常粗略,仔细看了也不知道他们在说些什么,更不要说相信这办法真的管用了。对不起,我根本不想去理解这些尖括号里的 `'a` 和 `'b` 是什么,除非你先向我证明这些东西真的能解决内存管理的问题。实际上这个 lifetime 我感觉像是跨过程静态分析时产生的一些标记,要知道静态分析是无法解决内存管理的问题的,我猜想这种 lifetime 在有递归函数的情况下就会遇到麻烦。 diff --git a/scripts/crawler.py b/scripts/crawler.py index e83842e..840d2b3 100644 --- a/scripts/crawler.py +++ b/scripts/crawler.py @@ -166,14 +166,25 @@ def format_content(content: str) -> str: ) # --- code-lang: add `text` to bare opening code fences --- + # Matches fenced code blocks: up to 3 leading whitespace chars, 3+ ticks, + # then an info string of any non-backtick chars (so ```c#, ```c++ work). + # ```code``` (inline) won't match because info ends before a closing run. + FENCE_RE = re.compile(r"^(\s{0,3})(`{3,})([^`]*)$") in_code_block = False lines = content.split("\n") for i, line in enumerate(lines): - stripped = line.strip() - if re.match(r"^```\w*$", stripped): - if not in_code_block and stripped == "```": - lines[i] = "```text" - in_code_block = not in_code_block + m = FENCE_RE.match(line) + if not m: + continue + fence, info = m.group(2), m.group(3).strip() + if not in_code_block: + # Opening fence: add `text` if language is missing + if not info: + lines[i] = f"{fence}text" + in_code_block = True + else: + # Closing fence: leave as-is (CommonMark forbids info on close) + in_code_block = False content = "\n".join(lines) # --- cjk-spacing: Pangu spacing (CJK <-> half-width) --- diff --git a/scripts/fix_code_fences.py b/scripts/fix_code_fences.py new file mode 100644 index 0000000..7323d50 --- /dev/null +++ b/scripts/fix_code_fences.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +One-shot fix for malformed code fences produced by a bug in crawler.py's +format_content() (now fixed). + +The old fence regex `^```\w*$` failed to recognize opening fences whose +language tag contains non-word characters (e.g. ```c#, ```c++, ```f#). When +such an opening fence went unrecognized, the state machine did not flip, so +the *next* bare ``` (a legitimate closing fence) was misclassified as an +opening fence and rewritten to ```text. Per CommonMark, a closing fence +cannot carry an info string, so that ```text actually *opened* a new code +block, swallowing all following body text. + +This script walks every post, tracks fence state with a regex that accepts +any language tag, and reverts any closing fence that incorrectly carries an +info string (```text → ```). Idempotent and minimal: it only touches lines +inside a code block whose closing fence has a non-empty info string. + +Usage: + python3 scripts/fix_code_fences.py [--dry-run] +""" + +import argparse +import re +import sys +from pathlib import Path + +POSTS_DIR = Path(__file__).resolve().parent.parent / "posts" + +# Matches fenced code blocks: up to 3 leading whitespace chars, 3+ ticks, +# then an info string of any non-backtick chars (so ```c#, ```c++ work). +# ```code``` (inline) won't match because info ends before a closing run. +FENCE_RE = re.compile(r"^(\s{0,3})(`{3,})([^`]*)$") + + +def fix_body(body: str) -> tuple[str, list[str]]: + """Fix malformed closing fences. Returns (new_body, list_of_changes).""" + lines = body.split("\n") + changes = [] + in_block = False + for i, line in enumerate(lines): + m = FENCE_RE.match(line) + if not m: + continue + fence, info = m.group(2), m.group(3).strip() + if not in_block: + in_block = True + else: + # Closing fence: CommonMark forbids info strings here. If present, + # it is the crawler bug — strip it. + if info: + changes.append(f" line {i + 1}: {line!r} -> {fence!r}") + lines[i] = fence + in_block = False + return "\n".join(lines), changes + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--dry-run", action="store_true", help="report without writing") + args = parser.parse_args() + + if not POSTS_DIR.exists(): + print(f"Posts dir not found: {POSTS_DIR}", file=sys.stderr) + return 1 + + total_fixed = 0 + files_fixed = 0 + for md in sorted(POSTS_DIR.glob("*.md")): + text = md.read_text(encoding="utf-8") + + # Split off frontmatter (anything before the first ``` block) + fm_end = 0 + if text.startswith("---"): + end = text.find("\n---", 3) + if end != -1: + fm_end = end + 4 # include the closing ```\n... actually closing '---' + # content after '---\n' + frontmatter = text[:fm_end] if fm_end else "" + body = text[fm_end:] + + new_body, changes = fix_body(body) + if not changes: + continue + + files_fixed += 1 + total_fixed += len(changes) + print(f"{md.name}:") + for c in changes: + print(c) + if not args.dry_run: + md.write_text(frontmatter + new_body, encoding="utf-8") + + mode = "DRY RUN — " if args.dry_run else "" + print(f"\n{mode}{files_fixed} file(s), {total_fixed} fence(s) fixed") + return 0 + + +if __name__ == "__main__": + sys.exit(main())