光标处理未完成

This commit is contained in:
expressgy 2025-04-29 17:01:17 +08:00
parent 15779301fa
commit d08c85e70a
10 changed files with 709 additions and 17 deletions

View File

@ -0,0 +1,9 @@
addEventListener('message', (e) => {
const result = heavyTask(e.data)
postMessage(result)
})
postMessage('Hello World!ABC')
function heavyTask(data) {
// 执行耗时计算
return data * 2
}

View File

@ -3,13 +3,14 @@ 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);
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')
@ -25,24 +26,93 @@ marked.use({
async: true,
pedantic: false,
gfm: true,
silent: true,
silent: false,
breaks: false,
renderer: {
code: ({text, lang}) => {
console.log(text);
// console.log(text);
const validLang = hljs.getLanguage(lang) ? lang : 'plaintext'
const highlighted = hljs.highlight(text, {language: lang}).value
console.log(highlighted)
// 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
> 引用
- 列表项
- 另一项
@ -63,19 +133,95 @@ function greet() {
| 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 marked.parse(content.value));
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
ref="markdownContainer"
class="markdown-content"
v-html="compiledMarkdown"
/>
<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;
// ...
/* 代码块样式 */
@ -121,4 +267,4 @@ const compiledMarkdown = ref(await marked.parse(content.value));
}
}
</style>
</style>

222
components/Ni/Markdown.vue Normal file
View File

@ -0,0 +1,222 @@
<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>

View File

@ -0,0 +1,201 @@
import {Marked} from 'marked';
import hljs from 'highlight.js';
import markedFootnote from 'marked-footnote'
import markedCodeFormat from 'marked-code-format'
import { markedEmoji } from 'marked-emoji';
postMessage({
type: 'init',
data: 'Hello World!'
})
// 默认编辑器配置
const defaoltMarkOption = {
async: true, // 同步编译
pedantic: false,
gfm: true,
silent: true, // 输出错误
breaks: false, // 将回车编程<br/>
}
// 自定义行号注入函数
const injectLineNumbers = (highlightedCode) => {
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('')
}
// 编辑器render配置
const editorRenderOption = {
space: (...args) => {
console.log(args, 'space')
return `<div>space</div>`
},
code: (...args) => {
console.log(args, 'code')
return `<div>xscode</div>`
},
blockquote: (...args) => {
console.log(args, 'blockquote')
return `<div>blockquote</div>`
},
html: (...args) => {
console.log(args, 'html')
return `<div>html</div>`
},
heading: (...args) => {
console.log(args[0], 'heading')
return `<h${args[0].depth}>${args[0].raw}</h${args[0].depth}>`
},
hr: (...args) => {
console.log(args, 'hr')
return `<div>hr</div>`
},
list: (...args) => {
console.log(args, 'list')
return `<div>list</div>`
},
listitem: (...args) => {
console.log(args, 'listitem')
return `<div>listitem</div>`
},
checkbox: (...args) => {
console.log(args, 'checkbox')
return `<div>checkbox</div>`
},
paragraph: (...args) => {
console.log(args, 'paragraph')
return `<div>${args[0].raw}</div>`
},
table: (...args) => {
console.log(args, 'table')
return `<div>table</div>`
},
tablerow: (...args) => {
console.log(args, 'tablerow')
return `<div>tablerow</div>`
},
tablecell: (...args) => {
console.log(args, 'tablecell')
return `<div>tablecell</div>`
},
strong: (...args) => {
console.log(args, 'strong')
return `<div>strong</div>`
},
em: (...args) => {
console.log(args, 'em')
return `<div>em</div>`
},
codespan: (...args) => {
console.log(args, 'codespan')
return `<div>codespan</div>`
},
br: (...args) => {
console.log(args, 'br')
return `<div>br</div>`
},
del: (...args) => {
console.log(args, 'del')
return `<div>del</div>`
},
link: (...args) => {
console.log(args, 'link')
return `<div>link</div>`
},
image: (...args) => {
console.log(args, 'image')
return `<div>image</div>`
},
text: (...args) => {
console.log(args, 'text')
return `<div>text</div>`
},
}
// 显示器render配置
const viewerRenderOption = {
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>`;
}
}
// 编辑器Marked编译器
const editorMarked = new Marked({
...defaoltMarkOption,
renderer: editorRenderOption
});
// 显示器Marked编译器
const viewerMarked = new Marked({
...defaoltMarkOption,
renderer: viewerRenderOption
});
// 编辑器插件
editorMarked.use(markedCodeFormat()).use()
// 显示器插件
// 自定义表情快速插入
const markedEmojiOptions = {
emojis: {
"heart": "❤️",
"tada": "🎉"
},
renderer: (token) => {
console.log(token)
return token.emoji
}
};
// 支持脚注[^1] 会影响原始文本
viewerMarked.use(markedFootnote()).use(markedCodeFormat()).use(markedEmoji(markedEmojiOptions))
addEventListener('message', async (e) => {
console.log(e.data)
switch (e.data.type) {
case 'render': {
// const editContent = await makeEditContent(e.data.content)
const viewContent = await makeViewContent(e.data.content)
postMessage({
type: 'render',
content: {
viewContent,
// editContent,
},
})
break;
}
}
})
// 函数
// 使用编译
async function makeEditContent(contents){
// 去除html innerText两端的空格将&nbsp;转化为空格
let content = contents.trim().replace(/  /g, ' ');
// 过滤不能识别,难识别的字符,空字符
content = content.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,"");
// 编译
content = await editorMarked.parse(content)
return content
}
async function makeViewContent(contents){
// 过滤不能识别,难识别的字符,空字符
let content = contents.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,"");
// 编译
content = await viewerMarked.parse(content)
return content
}

View File

@ -13,6 +13,10 @@ export default defineNuxtConfig({
esbuildOptions: {
target: 'esnext'
}
},
worker: {
format: 'es'
}
},
compatibilityDate: '2024-11-01',

76
package-lock.json generated
View File

@ -16,6 +16,9 @@
"highlight.js": "^11.11.1",
"jsonwebtoken": "^9.0.2",
"marked": "^15.0.11",
"marked-code-format": "^1.1.6",
"marked-emoji": "^2.0.0",
"marked-footnote": "^1.2.4",
"mysql2": "^3.14.0",
"nuxt": "^3.16.2",
"redis": "^4.7.0",
@ -6157,6 +6160,15 @@
"node": ">= 4.0.0"
}
},
"node_modules/attributes-parser": {
"version": "2.2.3",
"resolved": "https://registry.npmmirror.com/attributes-parser/-/attributes-parser-2.2.3.tgz",
"integrity": "sha512-zjOUWt95la8AdUO+kP1GBOonWrV5jy9NjJP+z9tva/DSA6FIzGKcN/gk3tdqQf/pOeB8dkyd3FCPrjhELMmrkg==",
"license": "MIT",
"dependencies": {
"json-loose": "^1.2.4"
}
},
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.21.tgz",
@ -7710,7 +7722,7 @@
},
"node_modules/dompurify": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz",
"resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.5.tgz",
"integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
@ -10405,6 +10417,15 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-loose": {
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/json-loose/-/json-loose-1.2.4.tgz",
"integrity": "sha512-lwMWNC5pvVI33rhYWmAsmtICWE2IH7euDY/iIPeMFE5AuzAifYgqQrjqSMzwbrFV6MWPs41XD+CajElHI4cZMQ==",
"license": "MIT",
"dependencies": {
"moo": "^0.5.2"
}
},
"node_modules/json-schema-to-typescript-lite": {
"version": "14.1.0",
"resolved": "https://registry.npmmirror.com/json-schema-to-typescript-lite/-/json-schema-to-typescript-lite-14.1.0.tgz",
@ -11403,6 +11424,37 @@
"node": ">= 18"
}
},
"node_modules/marked-code-format": {
"version": "1.1.6",
"resolved": "https://registry.npmmirror.com/marked-code-format/-/marked-code-format-1.1.6.tgz",
"integrity": "sha512-DSzefm4DvOOGNdtCx+WekoLyXd8rwcEROSzyydnbJ3rQ2vuE2dKW8cpJui/ulUUHMvyrF9TCq351Fqjd81ocEQ==",
"license": "MIT",
"dependencies": {
"attributes-parser": "^2.2.3"
},
"peerDependencies": {
"marked": ">=7.0.0",
"prettier": ">=3.0.0"
}
},
"node_modules/marked-emoji": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/marked-emoji/-/marked-emoji-2.0.0.tgz",
"integrity": "sha512-oTZ8fqbdVDHFQnqCE1tg4ND7zEd7cUVNHliR9Ldu4eys0J86uz/5Uksjd2mt5xcX16OOScDEr3MmPjajI/ZDHA==",
"license": "MIT",
"peerDependencies": {
"marked": ">=4 <16"
}
},
"node_modules/marked-footnote": {
"version": "1.2.4",
"resolved": "https://registry.npmmirror.com/marked-footnote/-/marked-footnote-1.2.4.tgz",
"integrity": "sha512-DB2Kl+wFh6YwZd70qABMY6WUkG1UuyqoNTFoDfGyG79Pz24neYtLBkB+45a7o72V7gkfvbC3CGzIYFobxfMT1Q==",
"license": "MIT",
"peerDependencies": {
"marked": ">=7.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -11670,6 +11722,12 @@
"node": ">=14"
}
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmmirror.com/moo/-/moo-0.5.2.tgz",
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
"license": "BSD-3-Clause"
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz",
@ -13539,6 +13597,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/pretty-bytes": {
"version": "6.1.1",
"resolved": "https://registry.npmmirror.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz",

View File

@ -23,6 +23,9 @@
"highlight.js": "^11.11.1",
"jsonwebtoken": "^9.0.2",
"marked": "^15.0.11",
"marked-code-format": "^1.1.6",
"marked-emoji": "^2.0.0",
"marked-footnote": "^1.2.4",
"mysql2": "^3.14.0",
"nuxt": "^3.16.2",
"redis": "^4.7.0",

View File

@ -66,7 +66,6 @@ const formatBlogMenuListToPidMap = (blogMenuList: Array<ShowBlogMenu>) => {
const id = menuItem.id;
if(pidObj[id]){
menuItem.childrenLength = pidObj[id].length
consola.info(menuItem.name, menuItem.childrenLength)
}
idMap.set(id, menuItem)
}
@ -271,6 +270,7 @@ onMounted(() => {
overflow: auto;
flex: 1;
display: flex;
flex-direction: column;
& > header{
position: relative;
flex-shrink: 0;

View File

@ -1,10 +1,19 @@
<script setup lang="ts">
const route = useRoute()
const blogContent = ref('');
const blogId = ref<string>(route.params.blogId.toString());
watch(() => route, () => {
blogId.value = route.params.blogId.toString();
})
onMounted(() => {
consola.info('BlogId', blogId.value)
})
</script>
<template>
<div class="BlogEntity">
<HomeBlogMarked></HomeBlogMarked>
<NiMarkdown :content="blogContent" :contentId="blogId"></NiMarkdown>
</div>
</template>
@ -12,5 +21,6 @@
.BlogEntity{
position: relative;
height: 100%;
overflow: auto;
}
</style>

23
测试输入.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
html,body{
position: relative;
margin: 0;
padding: 0;
}
.box{
position: relative;
height: 100vh;
width: 100vw;
}
</style>
</head>
<body>
<div class="box"
contenteditable="true"></div>
</body>
</html>