223 lines
5.6 KiB
Vue
223 lines
5.6 KiB
Vue
<script setup lang="ts">
|
||
import 'highlight.js/styles/atom-one-light.css';
|
||
import DOMPurify from "dompurify";
|
||
const props = defineProps({
|
||
content: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
contentId: {
|
||
type: String,
|
||
required: true,
|
||
}
|
||
})
|
||
|
||
// 原始内容
|
||
const originContent = ref(props.content);
|
||
// 编辑器内容
|
||
const editorContent = ref('');
|
||
// 显示器内容
|
||
const viewerContent = ref('');
|
||
// 编辑器元素对象
|
||
const editorRef =ref<null | HTMLElement>(null)
|
||
// 编译器
|
||
const markedWorker = ref<null | Worker>(null)
|
||
|
||
|
||
// 初始化Markdown编译线程
|
||
function initMarked(){
|
||
const worker = new Worker(new URL("./Markdown.worker.js", import.meta.url), {type: 'module' });
|
||
worker.postMessage({
|
||
type: 'init',
|
||
})
|
||
worker.onmessage = (e) => {
|
||
switch (e.data.type) {
|
||
case 'render': {
|
||
// 加强安全,防止xss攻击
|
||
console.log(e.data.content.editContent)
|
||
// editorContent.value = DOMPurify.sanitize(e.data.content.editContent);
|
||
viewerContent.value = DOMPurify.sanitize(e.data.content.viewContent);
|
||
break;
|
||
}
|
||
default: {
|
||
console.log(e)
|
||
}
|
||
}
|
||
// worker.terminate();
|
||
};
|
||
markedWorker.value = worker
|
||
}
|
||
|
||
let isUpdating = false;
|
||
|
||
const getCursorPos = (event) => {
|
||
const content = event.target.innerText;
|
||
const selection = window.getSelection();
|
||
const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
||
|
||
if (range) {
|
||
// 获取光标位置(相对于整个内容的偏移量)
|
||
const cursorPosition = range.startOffset;
|
||
|
||
// 将内容按换行符分割成数组
|
||
const lines = content.split(/\n/i);
|
||
|
||
// 累计每行的长度,找到光标所在的行
|
||
let lineIndex = 0;
|
||
let cumulativeLength = 0;
|
||
|
||
for (let i = 0; i < lines.length; i++) {
|
||
cumulativeLength += lines[i].length;
|
||
|
||
// 如果光标位置小于当前累计长度,说明光标在这一行
|
||
if (cursorPosition <= cumulativeLength) {
|
||
lineIndex = i + 1; // 行号从1开始
|
||
break;
|
||
}
|
||
|
||
// 考虑换行符的长度(HTML 中的 `<br>`)
|
||
cumulativeLength += 4; // `<br>` 的长度
|
||
}
|
||
console.log("光标所在的行:", lineIndex, range.startOffset);
|
||
}
|
||
}
|
||
|
||
// 实时监听编辑器输入事件
|
||
const handleEditorInput = async (event) => {
|
||
getCursorPos(event)
|
||
const editorContent = event.target.innerText;
|
||
markedWorker.value?.postMessage({
|
||
type: 'render',
|
||
content: editorContent,
|
||
})
|
||
// restoreCaretPosition( event.target, caretPos);
|
||
isUpdating = false;
|
||
};
|
||
|
||
// 手动获取内容(通过按钮触发)
|
||
const getEditorContent = () => {
|
||
if (editorRef.value) {
|
||
const text = editorRef.value.innerText.trim();
|
||
console.log("手动获取:", text);
|
||
}
|
||
};
|
||
// 处理粘贴内容(自动去除粘贴的 HTML 标签)
|
||
const handleEditorPaste = (event) => {
|
||
event.preventDefault(); // 阻止默认粘贴行为
|
||
const text = event.clipboardData.getData("text/plain"); // 获取纯文本
|
||
document.execCommand("insertText", false, text); // 插入纯文本
|
||
};
|
||
|
||
onMounted(() => {
|
||
initMarked()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="NiMarkdown">
|
||
<header></header>
|
||
<main>
|
||
<div class="markdownEditor"
|
||
ref="editorRef"
|
||
contenteditable="true"
|
||
@input="handleEditorInput"
|
||
v-html="editorContent"
|
||
/>
|
||
<div class="markdownViewer"
|
||
v-html="viewerContent"
|
||
/>
|
||
</main>
|
||
<footer></footer>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.NiMarkdown{
|
||
position: relative;
|
||
height: 100%;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
& > header{
|
||
position: relative;
|
||
width: 100%;
|
||
height: 20px;
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
}
|
||
& > footer{
|
||
position: relative;
|
||
width: 100%;
|
||
height: 60px;
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
}
|
||
& > main{
|
||
position: relative;
|
||
width: 100%;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
display: flex;
|
||
& > div{
|
||
position: relative;
|
||
width: 50%;
|
||
border: 1px solid var(--Ni-theme-border-color);
|
||
overflowY: auto;
|
||
padding: 1rem;
|
||
}
|
||
}
|
||
}
|
||
|
||
.markdownEditor{
|
||
outline: none;
|
||
}
|
||
.markdownViewer{
|
||
|
||
/* 代码块样式 */
|
||
: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>
|