文章排版四件套(参考 xLog):盘古之白 + 表格圆角斑马纹 + 代码块封顶与复制按钮 + 标题悬停锚点
- PanguHTML:CJK 与拉丁/数字间补空格,标签原样保留、code/pre 内文本不动;详情/时间线短文/RSS 描述三个出口生效(带单测) - 表格改圆角容器 + tbody 斑马纹 + 纵向大留白(classic/vivid 双主题) - pre max-height 30.5em 封顶内部滚动;enhanceProse 注入悬停复制按钮(App 全局委托),按下有 2px 物理感 - 带 id 的 h2-h4 悬停浮现 # 锚点,点击跳转 - 中文字体栈此前已配(PingFang SC 等),无需改动
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// 盘古之白:在 CJK 与拉丁字母/数字之间补一个空格,中文混排的阅读观感
|
||||
// 差别很大(「把MVP搬进仓库」→「把 MVP 搬进仓库」)。
|
||||
// 在 HTML 层做:标签原样保留,<code>/<pre>/<script>/<style> 内的文本
|
||||
// 一律不动(代码语义容不得我们加空格),其余文本段做补空格。
|
||||
package render
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var tagRe = regexp.MustCompile(`<[^>]*>`)
|
||||
|
||||
// code 上下文里的文本不处理
|
||||
var codeOpenRe = regexp.MustCompile(`(?i)<(code|pre|script|style|kbd|samp)\b[^>]*>`)
|
||||
var codeCloseRe = regexp.MustCompile(`(?i)</(code|pre|script|style|kbd|samp)>`)
|
||||
|
||||
// CJK 统一表意区 + 扩展A/兼容 + 日文假名已在 2E80-9FFF 覆盖,
|
||||
// 再补全角标点区(FF00-FFEF)与 CJK 标点(3000-303F)
|
||||
var cjkRe = regexp.MustCompile(`[\x{2E80}-\x{9FFF}\x{3000}-\x{303F}\x{F900}-\x{FAFF}\x{FF00}-\x{FFEF}]`)
|
||||
var latinTailRe = regexp.MustCompile(`([\x{2E80}-\x{9FFF}\x{3000}-\x{303F}\x{F900}-\x{FAFF}\x{FF00}-\x{FFEF}])([A-Za-z0-9_$@])`)
|
||||
var latinHeadRe = regexp.MustCompile(`([A-Za-z0-9+%,.;:!?%)\]])([\x{2E80}-\x{9FFF}\x{3000}-\x{303F}\x{F900}-\x{FAFF}\x{FF00}-\x{FFEF}])`)
|
||||
|
||||
func isCJK(r rune) bool {
|
||||
return cjkRe.MatchString(string(r))
|
||||
}
|
||||
|
||||
// panguText 对纯文本做补空格。已经隔着空格的不动(正则天然要求相邻)。
|
||||
func panguText(s string) string {
|
||||
if !hasCJKAndLatin(s) {
|
||||
return s
|
||||
}
|
||||
prev := ""
|
||||
for prev != s {
|
||||
prev = s
|
||||
s = latinTailRe.ReplaceAllString(s, "$1 $2")
|
||||
s = latinHeadRe.ReplaceAllString(s, "$1 $2")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func hasCJKAndLatin(s string) bool {
|
||||
var cjk, latin bool
|
||||
for _, r := range s {
|
||||
if isCJK(r) {
|
||||
cjk = true
|
||||
} else if unicode.IsLetter(r) || unicode.IsNumber(r) {
|
||||
latin = true
|
||||
}
|
||||
if cjk && latin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PanguHTML 给渲染出的正文 HTML 做盘古之白。流式扫描:标签原样输出,
|
||||
// 普通文本段补空格,代码上下文里的文本跳过。
|
||||
func PanguHTML(html string) string {
|
||||
if !strings.Contains(html, "<") {
|
||||
return html
|
||||
}
|
||||
var b strings.Builder
|
||||
inCode := false
|
||||
for {
|
||||
loc := tagRe.FindStringIndex(html)
|
||||
if loc == nil {
|
||||
rest := html
|
||||
if !inCode {
|
||||
rest = panguText(rest)
|
||||
}
|
||||
b.WriteString(rest)
|
||||
return b.String()
|
||||
}
|
||||
head, tag := html[:loc[0]], html[loc[0]:loc[1]]
|
||||
if !inCode {
|
||||
head = panguText(head)
|
||||
}
|
||||
b.WriteString(head)
|
||||
b.WriteString(tag)
|
||||
if !inCode {
|
||||
if m := codeOpenRe.FindStringSubmatch(tag); m != nil {
|
||||
// 自闭合或行内 code 直接开着也按进/出配对处理(</code> 总会出现)
|
||||
inCode = true
|
||||
}
|
||||
} else if codeCloseRe.MatchString(tag) {
|
||||
inCode = false
|
||||
}
|
||||
html = html[loc[1]:]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package render
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPanguText(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"把MVP搬进仓库": "把 MVP 搬进仓库",
|
||||
"今天写了3个小时代码": "今天写了 3 个小时代码",
|
||||
"使用Go和Vue3开发": "使用 Go 和 Vue3 开发",
|
||||
"纯中文没有混排": "纯中文没有混排",
|
||||
"已经隔了空格的 Go 语言": "已经隔了空格的 Go 语言",
|
||||
"数字100在前": "数字 100 在前",
|
||||
"英文后接标点。followed": "英文后接标点。 followed",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := panguText(in); got != want {
|
||||
t.Errorf("panguText(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPanguHTML(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
// 普通文本:处理
|
||||
"<p>把MVP搬进仓库</p>": "<p>把 MVP 搬进仓库</p>",
|
||||
// 行内 code:不动
|
||||
"<p>运行<code>go build ./...</code>即可</p>": "<p>运行<code>go build ./...</code>即可</p>",
|
||||
// pre 代码块:整个内部不动
|
||||
"<pre><code>把MVP搬进仓库\nfmt.Println(1)</code></pre>": "<pre><code>把MVP搬进仓库\nfmt.Println(1)</code></pre>",
|
||||
// 属性里不是文本:不动(src 含字母数字混合不受影响)
|
||||
`<img alt="测试图1" src="a1.png">`: `<img alt="测试图1" src="a1.png">`,
|
||||
// 嵌套:code 结束后恢复处理
|
||||
"<p>先<code>跑起来</code>再看结果100%</p>": "<p>先<code>跑起来</code>再看结果 100%</p>",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := PanguHTML(in); got != want {
|
||||
t.Errorf("PanguHTML(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user