使用marked

This commit is contained in:
expressgy 2025-04-29 02:39:55 +08:00
parent 3710642bc0
commit 15779301fa
10 changed files with 427 additions and 1624 deletions

7
assets/svg/time.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1745843719544" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2701"
xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<path d="M509.9 64.4c-246.6 0-446.6 200-446.6 446.6 0 246.6 199.9 446.6 446.6 446.6S956.5 757.7 956.5 511c0-246.6-199.9-446.6-446.6-446.6zM627 556.3H502.4c-25 0-45.3-20.3-45.3-45.3V274.3c0-25 20.3-45.3 45.3-45.3s45.3 20.3 45.3 45.3v191.5H627c25 0 45.3 20.3 45.3 45.3S652 556.3 627 556.3z"
fill="" p-id="2702"></path>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View File

@ -1,21 +0,0 @@
<script setup lang="ts">
import { Editor } from '@bytemd/vue-next'
import gfm from '@bytemd/plugin-gfm'
const content = ref('')
const plugins = [gfm()]
const handleChange = (v: string) => {
content.value = v
}
</script>
<template>
<Editor :value="content" :plugins="plugins" @change="handleChange" class="byteMd"/>
</template>
<style scoped lang="scss">
.byteMd{
position: relative;
height: 100%;
}
</style>

View File

@ -0,0 +1,124 @@
<script setup lang="ts">
import {Marked} from 'marked';
//
import hljs from 'highlight.js';
import 'highlight.js/styles/atom-one-light.css';
//
// import hljs from 'highlight.js/lib/core';
// import javascript from 'highlight.js/lib/languages/javascript';
// hljs.registerLanguage('javascript', javascript);
const marked = new Marked();
//
const injectLineNumbers = (highlightedCode: string) => {
const lines = highlightedCode.split('\n')
//
if (lines[lines.length - 1] === '') lines.pop()
//
return lines.map((line, index) => `<div class="code-line"><span class="line-number">${index + 1}</span><span class="line-content">${line}</span></div>`).join('')
}
marked.use({
async: true,
pedantic: false,
gfm: true,
silent: true,
renderer: {
code: ({text, lang}) => {
console.log(text);
const validLang = hljs.getLanguage(lang) ? lang : 'plaintext'
const highlighted = hljs.highlight(text, {language: lang}).value
console.log(highlighted)
const withLineNumbers = injectLineNumbers(highlighted)
return `<pre class="hljs ${validLang}"><code>${withLineNumbers}</code></pre>`;
}
}
})
const content = ref(`
# Hello Vue 3!
**Markdown 内容示例**
- 列表项
- 另一项
\`\`\`javascript
//
function greet() {
console.log('Hello marked!');
}
\`\`\`
| 参数 | \t类型 | \t作用 | \t默认值 |
|:-----------|:---------|:---------------------------------------------|:----------------|
| breaks | \tboolean | \t将换行符 \\n 渲染为 <br>类似 GitHub | \tfalse |
| gfm | \tboolean | \t启用 GitHub Flavored Markdown 扩展表格删除线等 | \ttrue |
| headerIds\t | boolean | \t自动为标题添加 id 属性 \`<h1 id="hello-world"></h1>\` | \ttrue |
| highlight\t | function | \t代码高亮处理函数需返回高亮后的 HTML | \tnull |
| renderer\t | object | \t自定义渲染器对象覆盖默认渲染逻辑 | \tnew Renderer() |
| sanitize\t | boolean | \t过滤危险 HTML 标签防止 XSS 攻击 | \tfalse |
| sanitizer\t | function | \t自定义 HTML 过滤函数 | \t- |
| silent\t | boolean | \t静默模式忽略解析错误如未闭合的代码块 | \tfalse |
`);
const compiledMarkdown = ref(await marked.parse(content.value));
</script>
<template>
<div
ref="markdownContainer"
class="markdown-content"
v-html="compiledMarkdown"
/>
</template>
<style scoped lang="scss">
.markdown-content {
// ...
/* 代码块样式 */
:deep(pre) {
white-space: pre-wrap;
background: #cdcdcd;
padding: 15px;
border-radius: 5px;
code {
font-family: 'Consolas', 'Fira Code', monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre;
}
/* 确保代码块正确换行 */
code {
display: block;
overflow-x: auto;
padding: 1em;
}
.code-line {
display: flex;
min-height: 1em; /* 防止空行高度塌陷 */
}
.line-number {
width: 40px;
padding-right: 12px;
color: #666;
text-align: right;
user-select: none;
flex-shrink: 0;
}
.line-content {
flex-grow: 1;
white-space: pre-wrap; /* 允许代码换行 */
}
}
}
</style>

View File

@ -0,0 +1,137 @@
<script setup lang="ts">
import { Editor } from '@bytemd/vue-next'
import type { BytemdPlugin } from "bytemd";
import gfm from '@bytemd/plugin-gfm'
import gfmZhHans from "@bytemd/plugin-gfm/locales/zh_Hans.json";
import math from '@bytemd/plugin-math'
import highlight from '@bytemd/plugin-highlight'
import 'highlight.js/styles/default.css'
import mediumZoom from '@bytemd/plugin-medium-zoom'
import mermaid from '@bytemd/plugin-mermaid'
import breaks from '@bytemd/plugin-breaks'
import gemoji from '@bytemd/plugin-gemoji'
import rehypeHighlightCodeLines from "rehype-highlight-code-lines";
import {useNiMessage} from "~/composables/useNiMessage";
const niMessage = useNiMessage()
// Markdown
const content = ref('')
//
const customPlugins = {
actions: [{
title: '插入时间',
icon: h('span', { class: 'iconfont icon-time' }, ['🕒']),
handler: {
type: 'action',
click: (targer) => {
// wrapText <>
// replaceLines((line) => "- [ ] " + line); //
// editor.focus()
targer.replaceLines((line) => new Date() + line);
targer.editor.focus();
},
},
}]
}
//
const highlightCodeLinesPlugin = (): BytemdPlugin => {
return {
rehype: (processor) =>
processor
//
.use(rehypeHighlightCodeLines, {
showLineNumbers: true,
lineContainerTagName: "div",
}),
};
};
//
const codeCopyPlugin = (): BytemdPlugin => {
const createCopyDom = (text: any): HTMLElement => {
const copyDom = document.createElement("div");
copyDom.className = "icon-[ph--copy-bold] absolute right-2 top-2 cursor-pointer";
copyDom.addEventListener("click", () => {
window.copyToClipboard(text);
niMessage.info("复制成功");
});
return copyDom;
};
return {
viewerEffect: ({ markdownBody }) => {
// code
const els = markdownBody.querySelectorAll("pre>code");
if (els.length === 0) return;
// preappend copy
els.forEach((itm: HTMLElement) => {
itm.parentNode.appendChild(createCopyDom(itm.innerText));
});
},
};
};
// ByteMD
const plugins = [
// DFM 线
gfm({locale: gfmZhHans}),
//
math(),
//
highlight(),
//
mediumZoom(),
// Mermaid
mermaid(),
// <br> GitHub
breaks(),
// GitHub :+1: 👍
gemoji(),
//
highlightCodeLinesPlugin(),
//
codeCopyPlugin(),
//
customPlugins,]
// Markdown
const handleChange = (v: string) => {
content.value = v
}
// ['auto', 'split', 'tab']
const mode = ref('auto');
//
const language = await import('~/node_modules/bytemd/locales/zh_Hans.json')
//
const editorConfig = {
toolbar: [] //
}
</script>
<template>
<Editor class="byteMd"
:value="content"
:plugins="plugins"
:editorConfig
:locale="language"
@change="handleChange" >
<!-- 添加自定义工具栏 -->
<template #toolbar="{ editor }">
<div class="custom-toolbar">
<button
title="插入时间"
@click="editor.insertContent(`当前时间:${new Date().toLocaleString()}`)">
插入时间
</button>
</div>
</template>
</Editor>
</template>
<style scoped lang="scss">
.byteMd{
position: relative;
height: 100%;
}
:global(.bytemd) {
height: 100%;
}
</style>

View File

@ -0,0 +1,77 @@
## 导入
```js
// 只作用于当前空间
import { Marked } from 'marked';
const marked = new Marked();
// 全局
import { marked } from 'marked';
```
## 核心方法
| 方法 | 作用 | 示例 |
|:--------------------------|:----------------|:-----------------------------------------|
| marked.parse(md) | 同步解析 Markdown | await marked.parse('**bold**') |
| marked.parse(md, callback)| 异步解析(处理异步高亮等场景) | marked.parse(md, (err, html) => { ... }) |
| marked.use(options) | 全局配置 | marked.use({ breaks: true }) |
## 配置
```js
marked.use({
async: true,
pedantic: false,
gfm: true,
});
```
| 参数 | 类型 | 作用 | 默认值 |
|:-----------|:---------|:---------------------------------------------|:----------------|
| breaks | boolean | 将换行符 \n 渲染为 <br>(类似 GitHub | false |
| gfm | boolean | 启用 GitHub Flavored Markdown 扩展(表格、删除线等) | true |
| headerIds | boolean | 自动为标题添加 id 属性(如 `<h1 id="hello-world"></h1>` | true |
| highlight | function | 代码高亮处理函数,需返回高亮后的 HTML | null |
| renderer | object | 自定义渲染器对象(覆盖默认渲染逻辑) | new Renderer() |
| sanitize | boolean | 过滤危险 HTML 标签(防止 XSS 攻击) | false |
| sanitizer | function | 自定义 HTML 过滤函数 | - |
| silent | boolean | 静默模式:忽略解析错误(如未闭合的代码块) | false |
## 扩展 Markdown 语法
## 使用worker多线程
## 开启 sanitize: true 或使用 DOMPurify 二次过滤
## 性能优化
? 建议缓存常用操作
## renderer方法
| 方法名及参数 | 对应 Markdown 元素 | 默认返回值示例 |
|:-----------------------------------|:---------------------|:------------------------------------------|
| code(code, language, isEscaped) | 代码块(三个反引号包裹) | `<pre><code class="lang">...</code></pre>` |
| blockquote(quote) | 引用块 > | `<blockquote>...</blockquote>` |
| html(html) | 原生 HTML 片段 | 直接返回原始 HTML |
| heading(text, level, raw, slugger) | 标题 # | `<h1>...</h1>` |
| hr() | 分割线 --- | `<hr>` |
| list(body, ordered, start) | 列表(有序/无序) | `<ul>...</ul> 或 <ol>...</ol>` |
| listitem(text, task, checked) | 列表项 - item | `<li>...</li>` |
| checkbox(checked) | 任务列表复选框 - [x] | `<input type="checkbox" checked="">` |
| paragraph(text) | 段落 | `<p>...</p>` |
| table(header, body) | 表格 | `<table>...</table>` |
| tablerow(content) | 表格行 | `<tr>...</tr>` |
| tablecell(content, flags) | 表格单元格flags 含对齐信息) | `<td>...</td> 或 <th>...</th>` |
| strong(text) | 加粗 **text** | `<strong>...</strong>` |
| em(text) | 斜体 *text* | `<em>...</em>` |
| codespan(code) | 行内代码 `code` | `<code>...</code>` |
| br() | 换行(两个空格结尾或 br 配置) | `<br>` |
| del(text) | 删除线 ~~text~~ | `<del>...</del>` |
| link(href, title, text) | 链接 [text](url) | `<a href="...">...</a>` |
| image(href, title, text) | 图片 ![alt](url) | `<img src="..." alt="...">` |
| text(text) | 普通文本 | 直接返回文本(会转义 HTML |

View File

@ -0,0 +1,43 @@
## 按需导入
```js
// 导入核心库和所需语言
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import xml from 'highlight.js/lib/languages/xml';
// 注册语言
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('xml', xml);
// 使用接口
const code = '<div>Test</div>';
const result = hljs.highlight(code, { language: 'xml' }).value;
console.log(result);
```
## 全部导入
```js
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
```
关键接口说明
- highlightAll()
自动检测页面中所有 `<pre><code>` 块的代码语言并高亮。
- highlight(code, { language })
手动指定语言高亮代码(需提前注册对应语言)。
- highlightAuto(code)
自动检测语言并高亮,返回包含语言类型和高亮结果的对象。
- registerLanguage(langName, languageDefinition)
注册自定义或第三方语言模块。
## 注意事项
- 主题切换:替换 CSS 文件路径即可(如 styles/default.min.css → styles/monokai-sublime.min.css3。
- TypeScript 类型:安装 @types/highlight.js 或自定义类型声明参考此方案6。
- 性能优化:动态加载语言模块(如通过 import() 动态导入10。

View File

@ -32,7 +32,6 @@ export default defineNuxtConfig({
'~/assets/css/iconfont.css',
'~/assets/css/transitions.css',
'~/assets/css/Ni.css',
'bytemd/dist/index.css', // byteMD 编辑器
],
app: {
head: {

1633
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,15 +14,15 @@
"sqlV": "drizzle-kit studio"
},
"dependencies": {
"@bytemd/plugin-gfm": "^1.22.0",
"@bytemd/vue-next": "^1.22.0",
"@nuxt/eslint": "^1.3.0",
"bytemd": "^1.22.0",
"consola": "^3.4.2",
"dayjs": "^1.11.13",
"dompurify": "^3.2.5",
"drizzle-orm": "^0.42.0",
"eslint": "^9.25.0",
"highlight.js": "^11.11.1",
"jsonwebtoken": "^9.0.2",
"marked": "^15.0.11",
"mysql2": "^3.14.0",
"nuxt": "^3.16.2",
"redis": "^4.7.0",

View File

@ -4,7 +4,7 @@
<template>
<div class="BlogEntity">
<HomeBlogMarkdown></HomeBlogMarkdown>
<HomeBlogMarked></HomeBlogMarked>
</div>
</template>