271 lines
7.1 KiB
Vue
271 lines
7.1 KiB
Vue
<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>
|