行,但仅适用于单层、无嵌套、不跨行、不混排的极简场景;Java正则不支持递归,无法处理上下文敏感的Markdown结构,必须先块级切分再行内解析。

用 Pattern 和 Matcher 做基础 Markdown 行内替换行不行?
行,但只适合极简场景——比如把 **bold** 换成 <strong>bold</strong>,且不嵌套、不跨行、不混排。Java 的正则引擎不支持递归匹配,遇到 *nested *italic* text* 或 `code `inline` ` 就容易错位或漏匹配。
常见错误现象:Matcher.replaceAll() 把 **a *b* c** 错切成 <strong>a <em>b</em> c</strong>(正确),但换成 **a *b **c*** 就崩,因为贪婪匹配会吞掉中间的 ** 边界。
- 只处理单层、无重叠的行内标记(
**、*、`) - 必须按优先级顺序执行:先处理代码块(防干扰)、再强调、再链接
- 避免用
.*匹配内容,改用[^*`\n]+这类否定字符类限定范围 - 性能上,每调一次
replaceAll()都重新编译 Pattern(除非缓存Pattern.compile(...)静态实例)
为什么不能跳过解析器直接正则扫一遍就完事?
因为 Markdown 是上下文敏感的:同一段文本在标题、列表、代码块里语义完全不同。正则无法判断当前是否在 ```java 代码块内——它只会把里面的 **bold** 也替换成 HTML,导致语法污染。
典型翻车场景:
**not bold**被转成
<pre class="brush:php;toolbar:false;"><code><strong>not bold</strong></code></pre>,而实际该原样保留。
- 必须先按块级结构切分(标题、段落、代码块、引用等),再对非代码块内容做行内解析
- 块级切分本身就不能靠正则:比如列表项要识别缩进+符号组合,
- item和-item(少空格)语义不同 - 主流库如
commonmark-java用状态机而非正则,就是为了稳住嵌套和边界
commonmark-java 怎么快速接入并定制输出?
它不是“黑盒”,而是暴露了 HtmlRenderer 和 NodeVisitor 接口,你可以只改某几类节点的渲染逻辑,不用从头写解析器。
立即学习“Java免费学习笔记(深入)”;
比如想把所有链接默认加 rel="noopener",不改源码也能做到:
HtmlRenderer renderer = HtmlRenderer.builder()
.attributeProviderFactory(context -> new CustomAttributeProvider())
.build();
-
CustomAttributeProvider实现accept(Node node, String tagName, Attributes attributes) - 当
node instanceof Link且tagName.equals("a")时,往attributes里塞new Attribute("rel", "noopener") - 不碰解析过程,也不影响性能——
commonmark-java的 AST 构建和渲染是分离的 - 注意别在
visit()里做耗时操作,否则拖慢整个渲染链
自己写简易解析器时最容易漏掉的边界条件
不是语法难,是那些“看起来不像问题”的地方:换行、空格、转义、Unicode 符号位置。比如 \*escaped\* 应该输出 *escaped*,但若没提前处理反斜杠,就会被当成强调。
- 行首空格数决定列表嵌套层级,但 Tab 和空格混合时,Java 的
String.stripLeading()会误判(它按 Unicode 空白算,而 CommonMark 规范只认 ASCII 空格和 Tab) - 链接描述文本允许换行,但只限于括号内,且需缩进对齐——正则根本没法跨行捕获
- HTML 实体如
©在原始 Markdown 中应保留原义,但若先做 HTML 转义再解析,就变成 “©” 文字而非符号 - 最常被忽略的是“空白行”:两个段落之间必须有且仅有一个空行,多一个就变成两个
<p></p>,少一个就合并成一段
真正麻烦的从来不是怎么写第一个 ** 替换,而是第 17 种边缘 case 出现在上线前两小时。











