文章排版四件套(参考 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:
@@ -18,6 +18,7 @@ import (
|
|||||||
"oneblog/internal/hub"
|
"oneblog/internal/hub"
|
||||||
"oneblog/internal/model"
|
"oneblog/internal/model"
|
||||||
"oneblog/internal/ratelimit"
|
"oneblog/internal/ratelimit"
|
||||||
|
"oneblog/internal/render"
|
||||||
"oneblog/internal/storage"
|
"oneblog/internal/storage"
|
||||||
"oneblog/internal/store"
|
"oneblog/internal/store"
|
||||||
"oneblog/internal/thumbs"
|
"oneblog/internal/thumbs"
|
||||||
@@ -128,6 +129,12 @@ func (a *API) listPosts(w http.ResponseWriter, r *http.Request) {
|
|||||||
httpx.ServerError(w, err)
|
httpx.ServerError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 时间线上短文直接铺正文:出口处做盘古之白(库里存的是原始渲染结果)
|
||||||
|
for i := range page.Items {
|
||||||
|
if page.Items[i].Kind == model.KindShort {
|
||||||
|
page.Items[i].ContentHTML = render.PanguHTML(page.Items[i].ContentHTML)
|
||||||
|
}
|
||||||
|
}
|
||||||
httpx.OK(w, page)
|
httpx.OK(w, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +163,8 @@ func (a *API) getPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
nb = nil
|
nb = nil
|
||||||
}
|
}
|
||||||
|
// 详情正文出口做盘古之白(存库不动,改的是渲染层)
|
||||||
|
p.ContentHTML = render.PanguHTML(p.ContentHTML)
|
||||||
httpx.OK(w, struct {
|
httpx.OK(w, struct {
|
||||||
model.Post
|
model.Post
|
||||||
Neighbors []model.Post `json:"neighbors"`
|
Neighbors []model.Post `json:"neighbors"`
|
||||||
@@ -255,7 +264,7 @@ func (a *API) RSS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
desc := p.Summary
|
desc := p.Summary
|
||||||
if desc == "" {
|
if desc == "" {
|
||||||
desc = trimRunes(stripTags(p.ContentHTML), 160)
|
desc = trimRunes(stripTags(render.PanguHTML(p.ContentHTML)), 160)
|
||||||
}
|
}
|
||||||
item := rssItem{
|
item := rssItem{
|
||||||
Title: title,
|
Title: title,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-1
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, watch } from 'vue'
|
import { computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import TopBar from './components/TopBar.vue'
|
import TopBar from './components/TopBar.vue'
|
||||||
import ThemeSwitcher from './components/ThemeSwitcher.vue'
|
import ThemeSwitcher from './components/ThemeSwitcher.vue'
|
||||||
@@ -9,6 +9,31 @@ import YohakuFoot from './ui/yohaku/YohakuFoot.vue'
|
|||||||
import { site, setAdminScope } from './site'
|
import { site, setAdminScope } from './site'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 正文代码块复制按钮(事件委托:复制按钮是 enhanceProse 注入的静态 HTML)
|
||||||
|
function onCopyClick(e) {
|
||||||
|
const btn = e.target.closest?.('.pre-copy')
|
||||||
|
if (!btn) return
|
||||||
|
const pre = btn.closest('.pre-wrap')?.querySelector('pre')
|
||||||
|
if (!pre) return
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(pre.innerText.replace(/\n$/, ''))
|
||||||
|
.then(() => {
|
||||||
|
btn.textContent = '已复制'
|
||||||
|
btn.classList.add('done')
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = '复制'
|
||||||
|
btn.classList.remove('done')
|
||||||
|
}, 1600)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
btn.textContent = '复制失败'
|
||||||
|
setTimeout(() => (btn.textContent = '复制'), 1600)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onMounted(() => document.addEventListener('click', onCopyClick))
|
||||||
|
onBeforeUnmount(() => document.removeEventListener('click', onCopyClick))
|
||||||
|
|
||||||
const isAdmin = computed(() => route.path.startsWith('/admin'))
|
const isAdmin = computed(() => route.path.startsWith('/admin'))
|
||||||
// vivid 只在公开前台生效:后台永远走 classic 那套(--admin-* token + 原 data-ui/data-theme),
|
// vivid 只在公开前台生效:后台永远走 classic 那套(--admin-* token + 原 data-ui/data-theme),
|
||||||
// 不受 ui_id 影响。vivid.css 会命中 .btn / .input 这类全局类,后台表单正在用,
|
// 不受 ui_id 影响。vivid.css 会命中 .btn / .input 这类全局类,后台表单正在用,
|
||||||
|
|||||||
+48
-9
@@ -254,6 +254,17 @@ html.topbar-hidden {--topbar-h: 0px;}
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
background: var(--accent);}
|
background: var(--accent);}
|
||||||
.prose h4 {font-size: 16.5px;}
|
.prose h4 {font-size: 16.5px;}
|
||||||
|
/* 悬停浮现的标题锚点(enhanceProse 注入,指向 heading id) */
|
||||||
|
.prose .h-anchor {opacity: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.72em;
|
||||||
|
color: var(--faint);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 0;
|
||||||
|
transition: opacity .2s;}
|
||||||
|
.prose h2:hover .h-anchor, .prose h3:hover .h-anchor, .prose h4:hover .h-anchor {opacity: 1;}
|
||||||
|
.prose .h-anchor:hover {color: var(--accent);}
|
||||||
.prose blockquote {margin: 1.4em 0;
|
.prose blockquote {margin: 1.4em 0;
|
||||||
padding: 2px 0 2px 18px;
|
padding: 2px 0 2px 18px;
|
||||||
border-left: 2px solid var(--accent-line);
|
border-left: 2px solid var(--accent-line);
|
||||||
@@ -273,12 +284,32 @@ html.topbar-hidden {--topbar-h: 0px;}
|
|||||||
background: var(--paper-sunken);
|
background: var(--paper-sunken);
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
border-radius: 3px;}
|
border-radius: 3px;}
|
||||||
|
.prose .pre-wrap {position: relative;}
|
||||||
|
.pre-copy {position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: inherit;
|
||||||
|
background: color-mix(in srgb, var(--ink) 14%, transparent);
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity .15s, transform .08s;}
|
||||||
|
.pre-copy:hover {opacity: 1;}
|
||||||
|
.pre-wrap:hover .pre-copy {opacity: 0.85;}
|
||||||
|
.pre-copy:active {transform: translateY(2px);}
|
||||||
|
.pre-copy.done {opacity: 1;
|
||||||
|
color: var(--accent);}
|
||||||
.prose pre {background: var(--paper-sunken);
|
.prose pre {background: var(--paper-sunken);
|
||||||
border: 1px solid var(--line-soft);
|
border: 1px solid var(--line-soft);
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
padding: 14px 16px;
|
padding: 16px 18px;
|
||||||
overflow-x: auto;
|
overflow: auto;
|
||||||
line-height: 1.7;}
|
line-height: 1.7;
|
||||||
|
max-height: 30.5em;}
|
||||||
.prose pre code {background: none;
|
.prose pre code {background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 13.5px;}
|
font-size: 13.5px;}
|
||||||
@@ -289,14 +320,22 @@ html.topbar-hidden {--topbar-h: 0px;}
|
|||||||
background: var(--line);
|
background: var(--line);
|
||||||
margin: 2em 0;}
|
margin: 2em 0;}
|
||||||
.prose table {width: 100%;
|
.prose table {width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: separate;
|
||||||
margin: 1.4em 0;
|
border-spacing: 0;
|
||||||
font-size: 15px;}
|
margin: 1.6em 0;
|
||||||
.prose th, .prose td {border: 1px solid var(--line);
|
font-size: 14.5px;
|
||||||
padding: 7px 10px;
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;}
|
||||||
|
.prose th, .prose td {border: 0;
|
||||||
|
border-bottom: 1px solid var(--line-soft);
|
||||||
|
padding: 11px 16px;
|
||||||
text-align: left;}
|
text-align: left;}
|
||||||
|
.prose tr > * + * {border-left: 1px solid var(--line-soft);}
|
||||||
|
.prose tbody tr:last-child td {border-bottom: 0;}
|
||||||
.prose th {background: var(--paper-sunken);
|
.prose th {background: var(--paper-sunken);
|
||||||
font-weight: 600;}
|
font-weight: 600;}
|
||||||
|
.prose tbody tr:nth-child(even) {background: color-mix(in srgb, var(--paper-sunken) 55%, transparent);}
|
||||||
.prose-short {font-size: 20px;
|
.prose-short {font-size: 20px;
|
||||||
line-height: 1.9;}
|
line-height: 1.9;}
|
||||||
.prose-short > p:first-of-type::first-letter {float: none;
|
.prose-short > p:first-of-type::first-letter {float: none;
|
||||||
|
|||||||
@@ -96,8 +96,12 @@ html[data-ui='vivid'] .prose code {font-family:var(--mono);font-size:.88em;
|
|||||||
padding:2px 7px;border-radius:6px;}
|
padding:2px 7px;border-radius:6px;}
|
||||||
html[data-ui='vivid'] .prose pre {background:color-mix(in srgb,var(--ink) 92%,var(--accent));
|
html[data-ui='vivid'] .prose pre {background:color-mix(in srgb,var(--ink) 92%,var(--accent));
|
||||||
color:#e8e8f0;padding:18px 20px;border-radius:12px;
|
color:#e8e8f0;padding:18px 20px;border-radius:12px;
|
||||||
overflow-x:auto;font-size:13.5px;line-height:1.7;
|
overflow:auto;font-size:13.5px;line-height:1.7;
|
||||||
box-shadow:var(--shadow-lg);}
|
box-shadow:var(--shadow-lg);
|
||||||
|
max-height:30.5em;}
|
||||||
|
/* 复制按钮(vivid 配色:浅字浮在深底上) */
|
||||||
|
html[data-ui='vivid'] .pre-copy {color:#e8e8f0;
|
||||||
|
background:rgba(255,255,255,.14);}
|
||||||
/* 暗色下「92% ink」混出的是浅底,浅底配浅字没法读;暗端换深底微混强调色 */
|
/* 暗色下「92% ink」混出的是浅底,浅底配浅字没法读;暗端换深底微混强调色 */
|
||||||
html[data-ui='vivid'][data-theme='vivid-dark'] .prose pre {
|
html[data-ui='vivid'][data-theme='vivid-dark'] .prose pre {
|
||||||
background:color-mix(in srgb,var(--paper) 86%,var(--accent));
|
background:color-mix(in srgb,var(--paper) 86%,var(--accent));
|
||||||
@@ -215,3 +219,17 @@ html[data-ui='vivid'] .sr-only {position: absolute;
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 0;}
|
border: 0;}
|
||||||
html[data-ui='vivid'] .toc a:active { color: var(--accent); }
|
html[data-ui='vivid'] .toc a:active { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ---- 表格(xLog 式:圆角容器 + 斑马纹,替代满格线) ---- */
|
||||||
|
html[data-ui='vivid'] .prose table {width:100%;
|
||||||
|
border-collapse:separate;border-spacing:0;
|
||||||
|
margin:1.8em 0;font-size:14px;
|
||||||
|
border:1px solid var(--line);border-radius:12px;overflow:hidden;}
|
||||||
|
html[data-ui='vivid'] .prose th, html[data-ui='vivid'] .prose td {border:0;
|
||||||
|
border-bottom:1px solid var(--line-soft);
|
||||||
|
padding:12px 18px;text-align:left;}
|
||||||
|
html[data-ui='vivid'] .prose tr > * + * {border-left:1px solid var(--line-soft);}
|
||||||
|
html[data-ui='vivid'] .prose tbody tr:last-child td {border-bottom:0;}
|
||||||
|
html[data-ui='vivid'] .prose th {background:var(--accent-soft);font-weight:600;}
|
||||||
|
html[data-ui='vivid'] .prose tbody tr:nth-child(even) {background:var(--card);}
|
||||||
|
/* 标题锚点 + 复制按钮沿用 classic 的 .h-anchor/.pre-copy 基础样式 */
|
||||||
|
|||||||
+21
-1
@@ -3,7 +3,7 @@ import DOMPurify from 'dompurify'
|
|||||||
// 后端 goldmark 已转义原始 HTML,这里再过一道 DOMPurify 作纵深防御,
|
// 后端 goldmark 已转义原始 HTML,这里再过一道 DOMPurify 作纵深防御,
|
||||||
// 所有 v-html 出口必须经过它。
|
// 所有 v-html 出口必须经过它。
|
||||||
export function sanitizeHtml(html) {
|
export function sanitizeHtml(html) {
|
||||||
return DOMPurify.sanitize(html || '')
|
return enhanceProse(DOMPurify.sanitize(html || ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 缩略图 ----------
|
// ---------- 缩略图 ----------
|
||||||
@@ -28,6 +28,26 @@ export function thumbURL(u, w = 480) {
|
|||||||
return '/uploads/thumb/' + key + '?w=' + w
|
return '/uploads/thumb/' + key + '?w=' + w
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- xLog 式排版增强 ----------
|
||||||
|
// 对消毒后的正文 HTML 做三件展示层增强(markdown/库里内容不动):
|
||||||
|
// 1) pre 包一层容器并注入悬停复制按钮;
|
||||||
|
// 2) 带 id 的标题(withHeadingIds 生成)尾部加 # 锚点;
|
||||||
|
// 3) (盘古之白在后端渲染出口做,见 backend/internal/render/pangu.go)
|
||||||
|
export function enhanceProse(html) {
|
||||||
|
if (!html) return html
|
||||||
|
// 复制按钮:包在 .pre-wrap 里,按钮悬浮于 pre 右上
|
||||||
|
html = html
|
||||||
|
.replace(/<pre\b/g, '<div class="pre-wrap"><button type="button" class="pre-copy" aria-label="复制代码">复制</button><pre')
|
||||||
|
.replace(/<\/pre>/g, '</pre></div>')
|
||||||
|
// 标题锚点:h2-h4 且有 id 的,末尾补一个 # 链接(悬停浮现)
|
||||||
|
html = html.replace(
|
||||||
|
/<h([2-4])([^>]*\bid="([^"]+)"[^>]*)>([\s\S]*?)<\/h\1>/g,
|
||||||
|
(m, lvl, attrs, id, inner) =>
|
||||||
|
`<h${lvl}${attrs}>${inner}<a class="h-anchor" href="#${id}" aria-label="标题链接">#</a></h${lvl}>`
|
||||||
|
)
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑器拖拽缩放的比例展示:Milkdown 图片块把高度比例写进 markdown 的 alt
|
// 编辑器拖拽缩放的比例展示:Milkdown 图片块把高度比例写进 markdown 的 alt
|
||||||
// (),渲染出的 <img alt="0.6"> 在这里还原成对应的宽度并居中。
|
// (),渲染出的 <img alt="0.6"> 在这里还原成对应的宽度并居中。
|
||||||
// 纯数字 alt 才生效(>=1 的当 100% 处理),正常图片的描述性 alt 不受影响。
|
// 纯数字 alt 才生效(>=1 的当 100% 处理),正常图片的描述性 alt 不受影响。
|
||||||
|
|||||||
Reference in New Issue
Block a user