console
class MarkdownParser {
compile(nodes) {
let matchArr
let html = ''
for (let i = 0, len = nodes.length; i < len; i++) {
let text = ''
let index = 0
const node = nodes[i]
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.innerHTML === '<br>') {
html += `<div></div>`
while (nodes[i + 1].nodeType === Node.ELEMENT_NODE && nodes[i + 1].innerHTML === '<br>') {
i++
}
continue
}
index = node.dataset.index
text = node.textContent.trim()
} else {
text = node.nodeValue.trim()
}
matchArr = text.match(/^#\s/)
|| text.match(/^##\s/)
|| text.match(/^###\s/)
|| text.match(/^####\s/)
|| text.match(/^#####\s/)
|| text.match(/^######\s/)
|| text.match(/^\*{3,}/)
|| text.match(/^>/)
|| text.match(/^\*\s/)
|| text.match(/^\d*\.\s/)
|| text.match(/^```/)
|| text.match(/^\|.*\|/)
if (matchArr) {
let temp = ''
const re1 = /^>/
const re2 = /^\*\s/
const re3 = /^\d*\.\s/
const re4 = /^```/
const re5 = /^\|.*\|/
switch(matchArr[0]) {
case '# ':
html += `<h1 data-index="${index}">` + this.format(text.substring(2)) + '</h1>'
break
case '## ':
html += `<h2 data-index="${index}">` + this.format(text.substring(3)) + '</h2>'
break
case '### ':
html += `<h3 data-index="${index}">` + this.format(text.substring(4)) + '</h3>'
break
case '#### ':
html += `<h4 data-index="${index}">` + this.format(text.substring(5)) + '</h4>'
break
case '##### ':
html += `<h5 data-index="${index}">` + this.format(text.substring(6)) + '</h5>'
break
case '###### ':
html += `<h6 data-index="${index}">` + this.format(text.substring(7)) + '</h6>'
break
case text.match(/^\*{3,}/) && text.match(/^\*{3,}/)[0]:
html += text.replace(/^\*{3,}/g, '<hr>')
break
case '>':
while (i < len && nodes[i].textContent.match(re1)) {
const str = nodes[i].textContent
temp += '<div>' + this.format(str.slice(1)) + '</div>'
i++
}
i--
html += `<blockquote data-index="${index}">` + temp + '</blockquote>'
break
case '* ':
while (i < len && nodes[i].textContent?.match(re2)) {
const str = nodes[i].textContent
temp += `<li data-index="${nodes[i]?.dataset?.index}">` + this.format(str.slice(2)) + '</li>'
i++
}
i--
html += `<ul data-index="${index}">` + temp + '</ul>'
break
case text.match(/^\d*\.\s/) && text.match(/^\d*\.\s/)[0]:
while (i < len && nodes[i].textContent?.match(re3)) {
const str = nodes[i].textContent
temp += `<li data-index="${nodes[i]?.dataset?.index}">` + this.format(str.replace(/^\d*\.\s/, '')) + '</li>'
i++
}
i--
html += `<ol data-index="${index}">` + temp + '</ol>'
break
case '```':
i++
while (i < len && !re4.test(nodes[i].textContent)) {
temp += `<div data-index="${nodes[i]?.dataset?.index}">` + escapeHTML(nodes[i].textContent) + '</div>'
i++
}
html += `<pre data-index="${index}"><code>` + temp + '</code></pre>'
break
case text.match(/^\|.*\|/) && text.match(/^\|.*\|/)[0]:
let thRe = /^\[th\]/
let arr, j, jlen
while (i < len && re5.test(nodes[i].textContent)) {
arr = nodes[i].textContent.split('|')
temp += `<tr data-index="${nodes[i]?.dataset?.index}">`
for (j = 1, jlen = arr.length - 1; j < jlen; j++) {
if (thRe.test(arr[1])) {
temp += '<th>' + arr[j] + '</th>'
} else {
temp += '<td>' + arr[j] + '</td>'
}
}
temp += '</tr>'
temp = temp.replace('[th]', '')
i++
}
html += '<table>' + temp + '</table>'
break
}
} else if (text) {
html += `<div data-index="${index}">` + this.format(text) + '</div>'
}
}
return html
}
format(str) {
str = str.replace(/\s/g, ' ')
const bold = str.match(/\*{2}[^*].*?\*{2}/g)
if (bold) {
for (let i = 0, len = bold.length; i < len; i++) {
str = str.replace(bold[i], '<b>' + bold[i].substring(2, bold[i].length - 2) + '</b>')
}
}
const italic = str.match(/\*[^*].*?\*/g)
if (italic) {
for (let i = 0, len = italic.length; i < len; i++) {
str = str.replace(italic[i], '<i>' + italic[i].substring(1, italic[i].length - 1) + '</i>')
}
}
const code = str.match(/`[^`]*`/g)
if (code) {
for (let i = 0, len = code.length; i < len; i++) {
str = str.replace(code[i], '<code>' + code[i].substring(1, code[i].length - 1) + '</code>')
}
}
const img = str.match(/!\[.*\]\(.*\)/g)
const re1 = /\(.*\)/
const re2 = /\[.*\]/
if (img) {
for (let i = 0, len = img.length; i < len; i++) {
const url = img[i].match(re1)[0]
const title = img[i].match(re2)[0]
str = str.replace(img[i], '<img src=' + url.substring(1, url.length - 1) + ' alt=' + title.substring(1, title.length -1) + '>')
}
}
const a = str.match(/\[.*?\]\(.*?\)/g)
if (a) {
for (let i = 0, len = a.length; i < len; i++) {
const url = a[i].match(re1)[0]
const title = a[i].match(re2)[0]
str = str.replace(a[i], '<a href=' + url.substring(1, url.length - 1) + '>' + title.substring(1, title.length -1) + '</a>')
}
}
return escapeHTML2(str)
}
}
function escapeHTML(html) {
return html.replace(/</g, '<').replace(/>/g, '>')
}
function escapeHTML2(html) {
return html.replace(/<(\/)?script>/g, '<$1script>')
}
function $(selector) {
return document.querySelector(selector)
}
function changeCursorPosition(node) {
const selection = window.getSelection()
selection.removeAllRanges()
const range = document.createRange()
range.selectNode(node)
selection.addRange(range)
selection.collapseToEnd()
}
function clearTextStyle(e) {
e.preventDefault()
let text
const clp = (e.originalEvent || e).clipboardData
if (clp === undefined || clp === null) {
text = window.clipboardData.getData('text') || ''
if (text !== '') {
if (window.getSelection) {
var newNode = document.createElement('span')
newNode.innerHTML = text
window.getSelection().getRangeAt(0).insertNode(newNode)
} else {
document.selection.createRange().pasteHTML(text)
}
}
} else {
text = clp.getData('text/plain') || ''
if (text !== '') {
document.execCommand('insertText', false, text)
}
}
}
function throttle(delay) {
let waitForCallFunc
let canCall = true
return function helper(callback, ...args) {
if (!canCall) {
if (callback) waitForCallFunc = callback
return
}
callback(...args)
canCall = false
setTimeout(() => {
canCall = true
if (waitForCallFunc) {
helper(waitForCallFunc, ...args)
waitForCallFunc = null
}
}, delay)
}
}
function debounce(delay) {
let timer
return function(callback, ...args) {
clearTimeout(timer)
timer = setTimeout(() => callback.call(null, ...args), delay)
}
}
function getHeightToTop(dom) {
let height = dom.offsetTop
let parent = dom.offsetParent
while (parent) {
height += parent.offsetTop
parent = parent.offsetParent
}
return height
}
function isInScreen(dom) {
const { top, bottom } = dom.getBoundingClientRect()
return bottom >= 0 && top < window.innerHeight
}
function percentOfdomInScreen(dom) {
const { height, bottom } = dom.getBoundingClientRect()
if (bottom <= 0) return 0
if (bottom >= height) return 1
return bottom / height
}
function canNodeCalculate(node) {
return (
node.innerHTML
&& node.innerHTML !== '<br>'
&& !node.textContent.startsWith('```')
&& isInScreen(node)
&& percentOfdomInScreen(node) >= 0
)
}
const editor = $('#editor')
const showDom = $('#show-content')
const markdown = new MarkdownParser()
showDom.innerHTML = markdown.compile(editor.children)
function onInput() {
let index = 0
const data = Array.from(editor.children)
data.forEach(item => {
delete item.dataset.index
if (item.tagName !== 'BR' && item.innerText.trim() !== '') {
if (!item.children.length || (item.children.length === 1 && item.children[0].tagName === 'BR')) {
item.dataset.index = index++
return
}
const frag = document.createDocumentFragment()
Array.from(item.childNodes).forEach(e => {
if (e.nodeType === Node.TEXT_NODE) {
const div = document.createElement('div')
div.textContent = e.nodeValue
item.replaceChild(div, e)
div.dataset.index = index++
frag.appendChild(div)
} else if (item.tagName !== 'BR') {
e.dataset?.index && delete e.dataset.index
e.dataset.index = index++
frag.appendChild(e)
}
})
editor.replaceChild(frag, item)
changeCursorPosition(editor.querySelector(`[data-index="${index - 1}"]`))
}
})
showDom.innerHTML = markdown.compile(editor.childNodes)
}
const debounceFn = debounce(100)
editor.oninput = () => {
debounceFn(onInput)
}
editor.onpaste = (e) => {
clearTextStyle(e)
}
const canScroll = {
editor: true,
showDom: true,
}
const debounceFn2 = debounce(100)
const throttleFn = throttle(50)
editor.onscroll = () => {
if (!canScroll.editor) return
canScroll.showDom = false
throttleFn(onScroll, editor, showDom)
debounceFn2(resumeScroll)
}
showDom.onscroll = () => {
if (!canScroll.showDom) return
canScroll.editor = false
throttleFn(onScroll, showDom, editor)
debounceFn(resumeScroll)
}
function resumeScroll() {
canScroll.editor = true
canScroll.showDom = true
}
function onScroll(scrollContainer, ShowContainer) {
const scrollHeight = ShowContainer.scrollHeight
if (scrollContainer.offsetHeight + scrollContainer.scrollTop >= scrollContainer.scrollHeight) {
ShowContainer.scrollTo({ top: scrollHeight - ShowContainer.clientHeight })
return
}
if (scrollContainer.scrollTop === 0) {
ShowContainer.scrollTo({ top: 0 })
return
}
const nodes = Array.from(scrollContainer.children)
for (const node of nodes) {
if (canNodeCalculate(node)) {
if (node.tagName === 'PRE' || node.tagName === 'TABLE') {
if (hasPreElementInScrollContainerScroll(node, ShowContainer)) return
continue
}
const index = node.dataset.index
const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
if (!dom) continue
const percent = percentOfdomInScreen(node)
const heightToTop = getHeightToTop(dom)
const domNeedHideHeight = dom.offsetHeight * (1 - percent)
ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
break
}
}
}
function hasPreElementInScrollContainerScroll(preElement, ShowContainer) {
for (const node of preElement.children[0].children) {
if (isInScreen(node) && percentOfdomInScreen(node) >= 0) {
const index = node.dataset.index
const dom = ShowContainer.querySelector(`[data-index="${index}"]`)
if (!dom) continue
const percent = percentOfdomInScreen(node)
const heightToTop = getHeightToTop(dom)
const domNeedHideHeight = dom.offsetHeight * (1 - percent)
ShowContainer.scrollTo({ top: heightToTop + domNeedHideHeight })
return true
}
}
return false
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>markdown 编辑器双屏同步滚动 demo1</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="editor" contenteditable class="text-container">
<div data-index="0">
</div><div data-index="1">### h3
</div><div data-index="2">### h3
</div><div data-index="3">### h3
</div><div data-index="4">### h3
</div><div data-index="5">### h3
</div><div data-index="6">### h3</div><div data-index="7">
</div><div data-index="8">### h3
</div><div data-index="9">### h3
</div><div data-index="10">### h3
</div><div data-index="11">### h3
</div><div data-index="12">### h3
</div><div data-index="13">
</div><div data-index="14">### h3
</div><div data-index="15">### h3
</div><div data-index="16">### h3
</div><div data-index="17">### h3
</div><div data-index="18">### h3
</div><div data-index="19">
</div><div data-index="20">### h3
</div><div data-index="21">### h3
</div><div data-index="22">### h3
</div><div data-index="23">
</div><div data-index="24">### h3
</div><div data-index="25">### h3
</div><div data-index="26">### h3</div><div data-index="27">### h3</div><div data-index="28">### h3</div><div data-index="29">### h3</div><div data-index="30"></div><div data-index="31">### h3</div><div data-index="32">### h3</div><div data-index="33">### h3</div><div data-index="34">### h3</div><div data-index="35"></div><div data-index="36">### h3</div><div data-index="37">### h3</div><div data-index="38">### h3</div><div data-index="39">### h3</div><div data-index="40">### h3</div><div data-index="41">### h3</div><div data-index="42"></div><div data-index="43">### h3
</div><div data-index="44">### h3
</div><div data-index="45">### h3
</div><div data-index="46">### h3
</div><div data-index="47">### h3
</div><div data-index="48"></div><div data-index="49">### h3
</div><div data-index="50">### h3
</div><div data-index="51">### h3
</div><div data-index="52">### h3
</div><div data-index="53">### h3
</div><div data-index="54"></div><div data-index="55">### h3
</div><div data-index="56">### h3
</div><div data-index="57">### h3
</div><div data-index="58">### h3
</div><div data-index="59">### h3
</div><div data-index="60"></div><div data-index="61">### h3
</div><div data-index="62">### h3
</div><div data-index="63">### h3
</div><div data-index="64">### h3
</div><div data-index="65">### h3
</div><div data-index="66"></div>
</div>
<div id="show-content" class="text-container markdown"></div>
</body>s
</html>
html,
body {
height: 100%;
font-size: 0;
}
* {
margin: 0;
padding: 0;
font-family: 'Microsoft yahei';
}
body {
display: flex;
}
table {
border-collapse: collapse;
}
.text-container {
box-sizing: border-box;
width: 50%;
border: 1px solid #e6e6e6;
height: 100%;
font-size: 14px;
background: #f5f5f5;
padding: 10px;
line-height: 20px;
outline: none;
overflow: auto;
}
.text-container > div {
min-height: 20px;
}
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: #ddd;
}
.markdown {
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
background-color: #fff;
color: #24292f;
}
.markdown img {
max-width: 100%;
margin: 10px 0;
}
.markdown>:first-child {
margin-top: 0!important;
}
.markdown h1,
.markdown h2 {
border-bottom: 1px solid #eaecef;
padding-bottom: .3em;
}
.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
font-weight: 600;
line-height: 1.25;
margin-bottom: 16px;
margin-top: 24px;
}
.markdown div {
min-height: 20px;
}
code,
pre,
tt {
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
font-size: 14px;
}
.markdown pre {
background-color: #f6f8fa;
border-radius: 3px;
font-size: 85%;
line-height: 1.45;
overflow: auto;
padding: 16px;
color: #24292e;
font-size: 14px;
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;
margin: 0;
margin-bottom: 1em;
}
.markdown code {
background-color: #f6f8fa;
border: 0;
display: inline;
line-height: inherit;
margin: 0;
max-width: auto;
overflow: visible;
padding: 0;
word-wrap: normal;
word-break: break-all;
}
.markdown blockquote {
background-color: #f6f8fa;
padding: 10px;
}
.markdown table {
display: block;
overflow: auto;
width: 100%;
margin-bottom: 15px;
}
.markdown table tr {
background-color: #fff;
}
.markdown table td,
.markdown table th {
border: 1px solid #dfe2e5;
padding: 6px 13px;
}
.markdown p {
margin-bottom: 1em;
}
.markdown li {
word-wrap: break-all;
}
.markdown ul li {
list-style: disc;
}
.markdown ol,
.markdown ul {
padding-left: 2em;
}
.markdown blockquote,
.markdown details,
.markdown dl,
.markdown ol,
.markdown p,
.markdown pre,
.markdown table,
.markdown ul {
margin-bottom: 16px;
margin-top: 0;
}
.markdown a {
color: #0366d6;
text-decoration: none;
}