starwait/components/Home/Blog/Marked.vue
2025-04-29 17:01:17 +08:00

271 lines
7.1 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import {Marked} from 'marked';
// 默认导入所有语言
import hljs from 'highlight.js';
import 'highlight.js/styles/atom-one-light.css';
import DOMPurify from 'dompurify';
import markedFootnote from 'marked-footnote'
import markedCodeFormat from 'marked-code-format'
import { markedEmoji } from 'marked-emoji';
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: false,
breaks: false,
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>`;
}
}
})
// 支持脚注[^1] 会影响原始文本
marked.use(markedFootnote())
marked.use(markedCodeFormat({
/* Prettier options */
}))
const markedEmojiOptions = {
emojis: {
"heart": "❤️",
"tada": "🎉"
},
renderer: (token) => {
console.log(token)
return token.emoji
}
};
marked.use(markedEmoji(markedEmojiOptions))
const content = ref(`
# Hello Vue 3!
AAA
AAA
BBB
B
Hello :smile:
- [ ] xsxs
I :heart: marked! :tada:
## 脚注
这是标记[^1]
[^1]: This is a footnote content.
Here is a simple footnote[^1]. With some additional[^3] text after it[^@#$%] and without disrupting the blocks[^3].
[^2]: The first paragraph of the definition2.
[^3]: The first paragraph of the definition3.
[^4]: ABC
**Markdown 内容示例:**
1. 潇洒些
2. 下洒下阿萨[^4]
3. 想啊伤心啊伤心啊
1. 下洒下阿萨
2. 下洒下阿萨
a. sxaas
b. xsaxa
c. xsaxa
- xasx
- cascade
1. 潇洒些
2. 下洒下阿萨
3. 想啊伤心啊伤心啊
1. 下洒下阿萨
2. 下洒下阿萨
a. sxaas
b. xsaxa
c. xsaxa
- xasx
- cascade
> 引用
- 列表项
- 另一项
\`\`\`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 |
`);
// 使用编译
async function make(contents: string){
// 过滤不能识别,难识别的字符,空字符
let content = contents.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,"");
// 编译
content = await marked.parse(content)
// 加强安全防止xss攻击
content = DOMPurify.sanitize(content);
return content
}
const compiledMarkdown = ref(await make(content.value));
const editableDiv = ref(null); // 绑定可编辑的 div
const rawText = ref(""); // 存储纯文本内容
// 实时监听输入事件
const handleInput = async (event) => {
if (editableDiv.value) {
// 直接获取去标签后的纯文本
rawText.value = editableDiv.value.innerText.trim().replace(/  /g, ' ');
console.log(rawText.value)
compiledMarkdown.value = await make(rawText.value);
}
};
// 手动获取内容(通过按钮触发)
const getContent = () => {
if (editableDiv.value) {
const text = editableDiv.value.innerText.trim();
console.log("手动获取:", text);
}
};
// 处理粘贴内容(自动去除粘贴的 HTML 标签)
const handlePaste = (event) => {
event.preventDefault(); // 阻止默认粘贴行为
const text = event.clipboardData.getData("text/plain"); // 获取纯文本
document.execCommand("insertText", false, text); // 插入纯文本
};
onMounted(async () => {
const worker = new Worker(new URL("~/assets/workers/example.worker.js", import.meta.url));
worker.postMessage("Hello Worker");
worker.onmessage = (e) => {
console.log(e)
worker.terminate();
};
})
</script>
<template>
<div class="NiMarked">
<div class="inputMarkdown"
ref="editableDiv"
contenteditable="true"
@input="handleInput"
@paste="handlePaste"
></div>
<div
ref="markdownContainer"
class="markdown-content"
v-html="compiledMarkdown"
/>
</div>
</template>
<style scoped lang="scss">
.NiMarked{
position: relative;
display: flex;
height: 100%;
overflow: hidden;
}
.NiMarked>div{
outline: none;
border: 1px solid var(--Ni-theme-border-color);
}
.inputMarkdown{
position: relative;
height: 100%;
flex: 1;
overflow: auto;
}
.markdown-content {
position: relative;
height: 100%;
flex: 1;
overflow: auto;
// 基础样式...
/* 代码块样式 */
: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>