Browse Source

feat(markdown): integrate highlight.js for enhanced code syntax highlighting

- Added highlight.js for improved syntax highlighting in markdown code blocks.
- Updated markdown rendering configuration to support language-specific highlighting.
- Introduced a new SCSS file for styling highlighted code blocks, enhancing visual presentation.
- Implemented a copy button functionality for code blocks to improve usability.

These changes significantly enhance the markdown rendering experience, making code snippets more readable and interactive.
main
npmrun 3 weeks ago
parent
commit
cee0f99538
  1. 44
      app/assets/scss/common.scss
  2. 168
      app/assets/scss/markdown/green.scss
  3. 53
      app/assets/scss/markdown/highlight.scss
  4. 120
      app/utils/render-markdown.ts
  5. 3
      bun.lock
  6. 1
      package.json
  7. BIN
      packages/drizzle-pkg/db.sqlite

44
app/assets/scss/common.scss

@ -1,45 +1,3 @@
@use "./markdown/reset.scss";
@use "./markdown/green.scss";
// .code-block-wrapper {
// position: relative;
// margin: 16px 0;
// border-radius: 8px;
// background: #1e1e1e;
// overflow: hidden;
// }
// .code-header {
// display: flex;
// justify-content: space-between;
// align-items: center;
// padding: 8px 12px;
// background: #2d2d2d;
// color: #ccc;
// font-size: 14px;
// }
// .code-lang {
// font-weight: 600;
// text-transform: uppercase;
// }
// .copy-btn {
// padding: 4px 8px;
// border: none;
// border-radius: 4px;
// background: #444;
// color: #fff;
// cursor: pointer;
// font-size: 12px;
// transition: all 0.2s;
// }
// .copy-btn:hover {
// background: #555;
// }
// pre {
// margin: 0;
// padding: 12px;
// overflow-x: auto;
// }
// code {
// color: #d4d4d4;
// font-family: 'Consolas', 'Monaco', monospace;
// }
@use "./markdown/highlight.scss";

168
app/assets/scss/markdown/green.scss

@ -171,108 +171,134 @@
del {
text-decoration: line-through var(--color-base) 2px;
}
code {
color: rgb(45, 55, 72);
background-color: rgba(160,174,192,0.25);
/* 行内代码:仅匹配非代码块场景,避免和 pre/code 嵌套互相覆盖 */
:not(pre) > code {
color: #d73a49;
background-color: rgba(160, 174, 192, 0.25);
font-family: inherit;
font-size: 1em;
// color: #ffffff;
// background-color: #ff4c209c;
border-radius: 4px;
padding: 1px 6px;
// font-size: 0.875em;
// font-size: 1.0769em;
// position: relative;
// top: 0.1em;
margin: 0 2px;
vertical-align: bottom;
}
pre {
code {
padding: 0;
font-size: inherit;
font-weight: inherit;
color: inherit;
white-space: pre;
background-color: transparent;
vertical-align: baseline;
border-radius: 0;
margin: 0;
}
}
/* 代码块整体 */
pre {
background: #f7f7f7; /* One Dark 背景 */
color: #333; /* 文字色 */
border-radius: 10px; /* 圆角 */
padding: 1.2rem;
/* MarkdownIt highlight 输出的代码块容器 */
.code-block-wrapper {
margin: 1rem 0;
overflow-x: auto; /* 长代码横向滚动 */
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
background: #f7f7f7;
color: #333;
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
font-size: 0.95em;
line-height: 1.5;
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1rem;
background: #efefef;
.code-lang {
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
color: #64748b;
text-transform: uppercase;
}
/* 行内代码 `code` */
code {
background: #f0f0f0;
color: #d73a49;
padding: 2px 6px;
border-radius: 4px;
.copy-btn {
border: 1px solid #d0d7de;
background: #fff;
color: #475569;
border-radius: 6px;
padding: 0.2rem 0.6rem;
font-size: 0.75rem;
line-height: 1.3;
cursor: pointer;
transition: all 0.18s ease;
&:hover {
background: #f8fafc;
border-color: #b8c2cc;
color: #334155;
}
pre code {
&.is-copied {
background: #dcfce7;
border-color: #86efac;
color: #166534;
}
}
}
pre {
margin: 0;
padding: 1.2rem;
overflow-x: auto;
background: transparent;
box-shadow: none;
code {
display: block;
padding: 0;
margin: 0;
color: inherit;
background: transparent;
border-radius: 0;
white-space: pre;
}
table {
width: 100%;
display: table;
border: 1px solid var(--color-light);
tr:hover {
background: var(--color-light);
}
&:hover{
border: 1px solid var(--color-extra);
th {
border-right: 1px solid var(--color-extra);
background: var(--color-extra);
border-bottom: 1px solid var(--color-extra);
}
td {
border-right: 1px solid var(--color-extra);
}
table {
width: fit-content;
display: block;
max-width: 100%;
overflow-x: auto;
border: 1px solid var(--color-light);
border-radius: 8px;
border-collapse: separate;
border-spacing: 0;
margin: 1rem 0;
background: #fff;
caption {
border-right: 1px solid var(--color-extra);
padding: 0.6em 1em;
text-align: left;
color: var(--color-fg-subtle);
border-bottom: 1px solid var(--color-light);
background: #fff;
}
thead {
th {
thead th {
background: var(--color-extra);
}
}
}
// table-layout: fixed;
th {
border-right: 1px solid var(--color-light);
padding: 0.5em 1em;
background: var(--color-light);
border-bottom: 1px solid var(--color-light);
font-weight: 600;
}
th,
td {
padding: 0.55em 1em;
white-space: nowrap;
border-right: 1px solid var(--color-light);
padding: 0.5em 1em;
border-bottom: 1px solid var(--color-light);
}
caption {
border-right: 1px solid var(--color-light);
padding: 0.5em 1em;
border-bottom: none;
th:last-child,
td:last-child {
border-right: 0;
}
thead {
th {
background: var(--color-extra);
tbody tr:last-child td {
border-bottom: 0;
}
tbody tr:hover td {
background: var(--color-light);
}
}
h1,

53
app/assets/scss/markdown/highlight.scss

@ -0,0 +1,53 @@
.markdown-body {
.code-block-wrapper {
pre {
code.hljs {
color: #1f2937;
}
.hljs-comment,
.hljs-quote {
color: #6b7280;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-section,
.hljs-link {
color: #9333ea;
}
.hljs-string,
.hljs-title,
.hljs-name,
.hljs-type,
.hljs-attribute,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #0f766e;
}
.hljs-number,
.hljs-variable,
.hljs-template-variable,
.hljs-regexp,
.hljs-meta .hljs-string {
color: #d97706;
}
.hljs-built_in,
.hljs-function,
.hljs-class .hljs-title {
color: #2563eb;
}
.hljs-deletion,
.hljs-subst {
color: #dc2626;
}
}
}
}

120
app/utils/render-markdown.ts

@ -1,55 +1,93 @@
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import DOMPurify from 'isomorphic-dompurify'
import { stripFrontMatter } from './markdown-front-matter'
const escapeHtml = (value: string): string =>
value
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')
const toSafeLanguageClass = (lang: string): string => {
const normalized = lang.trim().toLowerCase().replaceAll(/[^a-z0-9_-]/g, '')
return normalized || 'text'
}
const getHighlightedCode = (source: string, lang: string): string => {
const normalizedLang = lang.trim().toLowerCase()
if (normalizedLang && hljs.getLanguage(normalizedLang)) {
return hljs.highlight(source, { language: normalizedLang, ignoreIllegals: true }).value
}
if (normalizedLang) {
return hljs.highlightAuto(source).value
}
return escapeHtml(source)
}
const md = new MarkdownIt({
html: false,
linkify: true,
typographer: false,
breaks: true,
// // 开启代码块语言识别
// highlight: function (str: string, lang: string) {
// // 处理语言标签(如果没有指定语言,默认 text)
// const language = lang || 'text'
// // 生成带语言标识 + 复制按钮的代码块外壳
// return `
// <div class="code-block-wrapper">
// <div class="code-header">
// <span class="code-lang">${language}</span>
// <button class="copy-btn" onclick="copyCode(this)">复制</button>
// </div>
// <pre><code class="language-${language}">${md.utils.escapeHtml(str)}</code></pre>
// </div>
// `
// },
// 开启代码块语言识别
highlight: function (str: string, lang: string) {
// 处理语言标签(如果没有指定语言,默认 text)
const language = toSafeLanguageClass(lang || 'text')
const highlightedCode = getHighlightedCode(str, lang || '')
// 生成带语言标识 + 复制按钮的代码块外壳
return (
`<div class="code-block-wrapper"><div class="code-header"><span class="code-lang">${language}</span>` +
`<button class="copy-btn" type="button" data-copy-code>复制</button></div>` +
`<pre><code class="hljs language-${language}">${highlightedCode}</code></pre></div>`
)
},
})
// /** 全局复制代码函数(挂载到 window,确保页面可调用) */
// declare global {
// interface Window {
// copyCode: (btn: HTMLButtonElement) => void
// }
// }
// // 只在客户端环境注册复制函数
// if (typeof window !== 'undefined') {
// window.copyCode = function (btn: HTMLButtonElement) {
// const codeBlock = btn.closest('.code-block-wrapper')!.querySelector('code')!
// const text = codeBlock.textContent || ''
// navigator.clipboard.writeText(text).then(() => {
// const originalText = btn.textContent
// btn.textContent = '已复制'
// btn.style.backgroundColor = '#10b981'
// // 2秒后恢复按钮文字
// setTimeout(() => {
// btn.textContent = originalText
// btn.style.backgroundColor = ''
// }, 2000)
// })
// }
// }
/** 全局复制代码函数与事件绑定标记 */
declare global {
interface Window {
copyCode: (btn: HTMLButtonElement) => void
__markdownCopyBound?: boolean
}
}
// 只在客户端环境注册复制函数
if (typeof window !== 'undefined') {
window.copyCode = function (btn: HTMLButtonElement) {
const codeBlock = btn.closest('.code-block-wrapper')!.querySelector('code')!
const text = codeBlock.textContent || ''
navigator.clipboard.writeText(text).then(() => {
const originalText = btn.textContent
btn.textContent = '已复制'
btn.classList.add('is-copied')
// 2秒后恢复按钮文字
setTimeout(() => {
btn.textContent = originalText
btn.classList.remove('is-copied')
}, 2000)
})
}
if (!window.__markdownCopyBound) {
document.addEventListener('click', (event) => {
const target = event.target
if (!(target instanceof Element)) {
return
}
const btn = target.closest<HTMLButtonElement>('button[data-copy-code]')
if (!btn) {
return
}
window.copyCode(btn)
})
window.__markdownCopyBound = true
}
}
/** 将 Markdown 转为可安全用于 `v-html` 的 HTML(禁用源码中的原始 HTML)。 */
export function renderSafeMarkdown(src: string): string {

3
bun.lock

@ -14,6 +14,7 @@
"drizzle-seed": "0.3.1",
"drizzle-zod": "0.8.3",
"fast-xml-parser": "5.7.0",
"highlight.js": "^11.11.1",
"isomorphic-dompurify": "3.9.0",
"log4js": "6.9.1",
"logger": "workspace:*",
@ -1332,6 +1333,8 @@
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
"hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],

1
package.json

@ -28,6 +28,7 @@
"drizzle-seed": "0.3.1",
"drizzle-zod": "0.8.3",
"fast-xml-parser": "5.7.0",
"highlight.js": "^11.11.1",
"isomorphic-dompurify": "3.9.0",
"log4js": "6.9.1",
"logger": "workspace:*",

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save