mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 20:01:34 +08:00
247 lines
5.9 KiB
JavaScript
247 lines
5.9 KiB
JavaScript
import {
|
|
emptyElementNames,
|
|
paragraphClassName,
|
|
blockContainerElementNames
|
|
} from './config.js'
|
|
/**
|
|
* RegExp constants
|
|
*/
|
|
const HEAD_REG = /^(#{1,6})(.+)$/
|
|
const EMPHASIZE_REG_G = /(\*{1,3})(.*)(\1)/g
|
|
const EMPHASIZE_REG = /(\*{1,3})(.*)(\1)/
|
|
const LINE_BREAK_BLOCK_REG = /^(?:#{1,6}|>\s|`{3,}(.*))/
|
|
const INLINE_BLOCK_REG = /^(?:[*+-]\s(\[\s\]\s)?|\d+\.\s)/
|
|
|
|
/**
|
|
* help functions
|
|
*/
|
|
const conflict = (arr1, arr2) => {
|
|
// Are two array have intersection
|
|
return !(arr1[1] < arr2[0] || arr2[1] < arr1[0])
|
|
}
|
|
const getId = () => {
|
|
const prefix = 'ag-'
|
|
return `${prefix}${Math.random().toString(32).slice(2)}`
|
|
}
|
|
/**
|
|
* get unique id name
|
|
*/
|
|
const getUniqueId = set => {
|
|
let id = getId()
|
|
while (set.has(id)) {
|
|
id = getId()
|
|
}
|
|
set.add(id)
|
|
return id
|
|
}
|
|
|
|
/**
|
|
* markedText to html
|
|
*/
|
|
const markedText2Html = (markedText, cursorRange) => {
|
|
let result = markedText
|
|
// handle head mark symble
|
|
if (HEAD_REG.test(markedText)) {
|
|
result = markedText.replace(HEAD_REG, '<span class="gray">$1</span>$2')
|
|
}
|
|
// handle emphasize
|
|
if (EMPHASIZE_REG.test(markedText)) {
|
|
const offset = markedText.match(EMPHASIZE_REG).index
|
|
result = result.replace(EMPHASIZE_REG_G, (match, p1, p2, p3) => {
|
|
const isConflicted = conflict([offset, offset + match.length], cursorRange)
|
|
const className = isConflicted ? 'gray' : 'hidden'
|
|
const tags = { startTags: '', endTags: '' }
|
|
switch (p1.length) {
|
|
case 1:
|
|
tags.startTags = '<em>'
|
|
tags.endTags = '</em>'
|
|
break
|
|
case 2:
|
|
tags.startTags = '<strong>'
|
|
tags.endTags = '</strong>'
|
|
break
|
|
case 3:
|
|
tags.startTags = '<strong><em>'
|
|
tags.endTags = '</em></strong>'
|
|
}
|
|
const { startTags, endTags } = tags
|
|
return `<span class="${className}">${p1}</span>${startTags}${p2}${endTags}<span class="${className}">${p2}</span>`
|
|
})
|
|
}
|
|
// handle link
|
|
// TODO
|
|
// handle picture
|
|
// TODO
|
|
// handle code
|
|
// TODO
|
|
// handle auto link
|
|
// TODO
|
|
return result
|
|
}
|
|
/**
|
|
* checkInlineUpdate
|
|
*/
|
|
|
|
const checkInlineUpdate = text => {
|
|
const token = text.match(INLINE_BLOCK_REG)
|
|
if (!token) return false
|
|
const match = token[0]
|
|
switch (true) {
|
|
case /[*+-]\s/.test(match):
|
|
return token[1] ? { type: 'ul', info: 'tasklist' } : { type: 'ul', info: 'disorder' }
|
|
case /\d+\.\s/.test(match):
|
|
return { type: 'ol' }
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
const checkLineBreakUpdate = text => {
|
|
const token = text.match(LINE_BREAK_BLOCK_REG)
|
|
if (!token) return false
|
|
const match = token[0]
|
|
switch (true) {
|
|
case /#{1,6}/.test(match):
|
|
return { type: `h${match.length + 1}` }
|
|
case />\s/.test(match):
|
|
return { type: 'blockquote' }
|
|
case /`{3,}.*/.test(match):
|
|
return { type: 'blockcode', info: token[1] }
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* translate paragraph to ohter block
|
|
*/
|
|
|
|
/**
|
|
* viewModel2Html
|
|
*/
|
|
|
|
const paragraph2Element = paph => {
|
|
const { id, paragraphType, markedText, cursorRange } = paph
|
|
let element = null
|
|
switch (paragraphType) {
|
|
case 'p':
|
|
element = document.createElement('p')
|
|
element.id = id
|
|
element.classList.add(paragraphClassName)
|
|
element.innerHTML = markedText2Html(markedText, cursorRange)
|
|
break
|
|
default: break
|
|
}
|
|
element.id = id
|
|
return element
|
|
}
|
|
|
|
const viewModel2Html = vm => {
|
|
const htmls = vm.map(p => paragraph2Element(p).outerHTML)
|
|
return htmls.join('\n')
|
|
}
|
|
|
|
const isBlockContainer = element => {
|
|
return element && element.nodeType !== 3 &&
|
|
blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1
|
|
}
|
|
|
|
const isAganippeEditorElement = element => {
|
|
return element && element.getAttribute && !!element.getAttribute('aganippe-editor-element')
|
|
}
|
|
|
|
const traverseUp = (current, testElementFunction) => {
|
|
if (!current) {
|
|
return false
|
|
}
|
|
|
|
do {
|
|
if (current.nodeType === 1) {
|
|
if (testElementFunction(current)) {
|
|
return current
|
|
}
|
|
// do not traverse upwards past the nearest containing editor
|
|
if (isAganippeEditorElement(current)) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
current = current.parentNode
|
|
} while (current)
|
|
|
|
return false
|
|
}
|
|
|
|
const getFirstSelectableLeafNode = element => {
|
|
while (element && element.firstChild) {
|
|
element = element.firstChild
|
|
}
|
|
|
|
// We don't want to set the selection to an element that can't have children, this messes up Gecko.
|
|
element = traverseUp(element, el => {
|
|
return emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1
|
|
})
|
|
// Selecting at the beginning of a table doesn't work in PhantomJS.
|
|
if (element.nodeName.toLowerCase() === 'table') {
|
|
const firstCell = element.querySelector('th, td')
|
|
if (firstCell) {
|
|
element = firstCell
|
|
}
|
|
}
|
|
return element
|
|
}
|
|
|
|
const isElementAtBeginningOfBlock = node => {
|
|
let textVal
|
|
let sibling
|
|
while (!isBlockContainer(node) && !isAganippeEditorElement(node)) {
|
|
sibling = node.previousSibling
|
|
while (sibling) {
|
|
textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent
|
|
if (textVal.length > 0) {
|
|
return false
|
|
}
|
|
sibling = sibling.previousSibling
|
|
}
|
|
node = node.parentNode
|
|
}
|
|
return true
|
|
}
|
|
|
|
const findPreviousSibling = node => {
|
|
if (!node || isAganippeEditorElement(node)) {
|
|
return false
|
|
}
|
|
|
|
var previousSibling = node.previousSibling
|
|
while (!previousSibling && !isAganippeEditorElement(node.parentNode)) {
|
|
node = node.parentNode
|
|
previousSibling = node.previousSibling
|
|
}
|
|
|
|
return previousSibling
|
|
}
|
|
|
|
const getClosestBlockContainer = node => {
|
|
return traverseUp(node, node => {
|
|
return isBlockContainer(node) || isAganippeEditorElement(node)
|
|
})
|
|
}
|
|
|
|
export {
|
|
getUniqueId,
|
|
markedText2Html,
|
|
checkInlineUpdate,
|
|
checkLineBreakUpdate,
|
|
viewModel2Html,
|
|
paragraph2Element,
|
|
|
|
isBlockContainer,
|
|
traverseUp,
|
|
isAganippeEditorElement,
|
|
isElementAtBeginningOfBlock,
|
|
getFirstSelectableLeafNode,
|
|
findPreviousSibling,
|
|
getClosestBlockContainer
|
|
}
|