diff --git a/internal/service/post_service.go b/internal/service/post_service.go index f4a52f4..9edf841 100644 --- a/internal/service/post_service.go +++ b/internal/service/post_service.go @@ -2,6 +2,7 @@ package service import ( "fmt" + "regexp" "metazone.cc/metalab/internal/common" "metazone.cc/metalab/internal/config" @@ -12,11 +13,13 @@ import ( // sanitizePolicy Tiptap 输出 HTML 的消毒策略 // 前端 WYSIWYG 编辑器输出 HTML,由 bluemonday UGC 策略消毒 -// 显式放行 code/pre 的 class 属性以保留代码块样式 -// 放行 img 的 src/alt 属性以保留图片 +// 放行 code/pre 的 class/data-language 属性以保留代码高亮和语言标签 +// 放行 span 的 class 属性以保留 highlight.js 的 hljs-* 高亮 +// 放行 img 的 src/alt/title 属性以保留图片 var sanitizePolicy = func() *bluemonday.Policy { p := bluemonday.UGCPolicy() - p.AllowAttrs("class").OnElements("code", "pre") + p.AllowAttrs("class", "data-language").OnElements("code", "pre") + p.AllowAttrs("class").Matching(regexp.MustCompile("^hljs-")).OnElements("span") p.AllowAttrs("src", "alt", "title").OnElements("img") return p }() diff --git a/templates/MetaLab-2026/html/posts/new.html b/templates/MetaLab-2026/html/posts/new.html index 0bae38e..c32cba0 100644 --- a/templates/MetaLab-2026/html/posts/new.html +++ b/templates/MetaLab-2026/html/posts/new.html @@ -35,7 +35,7 @@ /* 引用 */ .tiptap blockquote { - border-left: 3px solid #e5e7eb; + border-left: 3px solid #3498db; padding-left: 1em; margin: 0.5em 0; color: #6b7280; @@ -43,16 +43,17 @@ /* 代码块 */ .tiptap pre { - background: #1f2937; - color: #f3f4f6; + background: #1a2332; + color: #e2e8f0; padding: 12px 16px; border-radius: 6px; font-size: 13px; line-height: 1.5; overflow-x: auto; margin: 0.5em 0; + position: relative; } -.tiptap pre code { background: transparent; padding: 0; color: inherit; font-size: inherit; } +.tiptap pre code { background: transparent; padding: 0; color: inherit; font-size: inherit; white-space: pre; display: block; } .tiptap code { background: #f3f4f6; padding: 2px 5px; @@ -60,6 +61,59 @@ font-size: 0.9em; font-family: 'SF Mono', 'Menlo', 'Monaco', monospace; } +/* 代码块语言标签 */ +.tiptap pre .code-lang-label { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 4px 16px; + font-size: 11px; + font-weight: 600; + color: #94a3b8; + background: #2c3e50; + border-radius: 6px 6px 0 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; + pointer-events: none; + z-index: 1; +} + +/* highlight.js 语法高亮主题 — 基于 One Dark,适配 MetaLab 深蓝背景 */ +.tiptap pre code .hljs-keyword, +.tiptap pre code .hljs-selector-tag, +.tiptap pre code .hljs-literal, +.tiptap pre code .hljs-section, +.tiptap pre code .hljs-link { color: #c792ea; } +.tiptap pre code .hljs-string, +.tiptap pre code .hljs-title, +.tiptap pre code .hljs-name, +.tiptap pre code .hljs-type, +.tiptap pre code .hljs-attribute, +.tiptap pre code .hljs-symbol, +.tiptap pre code .hljs-bullet, +.tiptap pre code .hljs-addition, +.tiptap pre code .hljs-variable, +.tiptap pre code .hljs-template-tag, +.tiptap pre code .hljs-template-variable { color: #c3e88d; } +.tiptap pre code .hljs-comment, +.tiptap pre code .hljs-quote, +.tiptap pre code .hljs-deletion, +.tiptap pre code .hljs-meta { color: #676e95; } +.tiptap pre code .hljs-number, +.tiptap pre code .hljs-regexp, +.tiptap pre code .hljs-built_in { color: #f78c6c; } +.tiptap pre code .hljs-function .hljs-title, +.tiptap pre code .hljs-doctag, +.tiptap pre code .hljs-formula { color: #82aaff; } +.tiptap pre code .hljs-tag, +.tiptap pre code .hljs-subst { color: #f07178; } +.tiptap pre code .hljs-emphasis { font-style: italic; } +.tiptap pre code .hljs-strong { font-weight: bold; } +.tiptap pre code .hljs-attr, +.tiptap pre code .hljs-selector-attr, +.tiptap pre code .hljs-selector-class, +.tiptap pre code .hljs-selector-id, +.tiptap pre code .hljs-selector-pseudo { color: #ffcb6b; } /* 水平线 */ .tiptap hr { border: none; border-top: 1px solid #e5e7eb; margin: 1em 0; } @@ -68,7 +122,7 @@ .tiptap img { max-width: 100%; height: auto; border-radius: 6px; } /* 链接 */ -.tiptap a { color: #6366f1; text-decoration: underline; } +.tiptap a { color: #3498db; text-decoration: underline; } /* Placeholder */ .tiptap p.is-editor-empty:first-child::before { @@ -104,7 +158,21 @@ transition: background 0.15s; } .editor-toolbar button:hover { background: #e5e7eb; } -.editor-toolbar button.is-active { background: #1f2937; color: #fff; } +.editor-toolbar button.is-active { background: #1a2332; color: #fff; } +.code-lang-select { + height: 30px; + padding: 0 6px; + border: 1px solid transparent; + border-radius: 4px; + background: transparent; + color: #374151; + font-size: 12px; + cursor: pointer; + font-family: inherit; + outline: none; +} +.code-lang-select:hover { border-color: #e5e7eb; } +.code-lang-select:focus { border-color: #3498db; } .editor-toolbar .toolbar-divider { width: 1px; background: #e5e7eb; @@ -143,6 +211,35 @@ + @@ -170,6 +267,19 @@ {{template "layout/footer.html" .}} + diff --git a/templates/MetaLab-2026/html/posts/show.html b/templates/MetaLab-2026/html/posts/show.html index ed9c7d4..248a427 100644 --- a/templates/MetaLab-2026/html/posts/show.html +++ b/templates/MetaLab-2026/html/posts/show.html @@ -47,6 +47,20 @@