mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 09:40:47 +08:00
331 lines
10 KiB
JavaScript
331 lines
10 KiB
JavaScript
import selection from '../selection'
|
|
import { tokenizer, generator } from '../parser/'
|
|
import { FORMAT_MARKER_MAP, FORMAT_TYPES } from '../config'
|
|
import { getImageInfo } from '../utils/getImageInfo'
|
|
|
|
const getOffset = (offset, { range: { start, end }, type, tag, anchor, alt }) => {
|
|
const dis = offset - start
|
|
const len = end - start
|
|
switch (type) {
|
|
case 'strong':
|
|
case 'del':
|
|
case 'em':
|
|
case 'inline_code':
|
|
case 'inline_math': {
|
|
const MARKER_LEN = (type === 'strong' || type === 'del') ? 2 : 1
|
|
if (dis < 0) return 0
|
|
if (dis >= 0 && dis < MARKER_LEN) return -dis
|
|
if (dis >= MARKER_LEN && dis <= len - MARKER_LEN) return -MARKER_LEN
|
|
if (dis > len - MARKER_LEN && dis <= len) return len - dis - 2 * MARKER_LEN
|
|
if (dis > len) return -2 * MARKER_LEN
|
|
break
|
|
}
|
|
case 'html_tag': { // handle underline, sup, sub
|
|
const OPEN_MARKER_LEN = FORMAT_MARKER_MAP[tag].open.length
|
|
const CLOSE_MARKER_LEN = FORMAT_MARKER_MAP[tag].close.length
|
|
if (dis < 0) return 0
|
|
if (dis >= 0 && dis < OPEN_MARKER_LEN) return -dis
|
|
if (dis >= OPEN_MARKER_LEN && dis <= len - CLOSE_MARKER_LEN) return -OPEN_MARKER_LEN
|
|
if (dis > len - CLOSE_MARKER_LEN && dis <= len) return len - dis - OPEN_MARKER_LEN - CLOSE_MARKER_LEN
|
|
if (dis > len) return -OPEN_MARKER_LEN - CLOSE_MARKER_LEN
|
|
break
|
|
}
|
|
case 'link': {
|
|
const MARKER_LEN = 1
|
|
if (dis < MARKER_LEN) return 0
|
|
if (dis >= MARKER_LEN && dis <= MARKER_LEN + anchor.length) return -1
|
|
if (dis > MARKER_LEN + anchor.length) return anchor.length - dis
|
|
break
|
|
}
|
|
case 'image': {
|
|
const MARKER_LEN = 1
|
|
if (dis < MARKER_LEN) return 0
|
|
if (dis >= MARKER_LEN && dis < MARKER_LEN * 2) return -1
|
|
if (dis >= MARKER_LEN * 2 && dis <= MARKER_LEN * 2 + alt.length) return -2
|
|
if (dis > MARKER_LEN * 2 + alt.length) return alt.length - dis
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const clearFormat = (token, { start, end }) => {
|
|
if (start) {
|
|
const deltaStart = getOffset(start.offset, token)
|
|
start.delata += deltaStart
|
|
}
|
|
if (end) {
|
|
const delataEnd = getOffset(end.offset, token)
|
|
end.delata += delataEnd
|
|
}
|
|
switch (token.type) {
|
|
case 'strong':
|
|
case 'del':
|
|
case 'em':
|
|
case 'link':
|
|
case 'html_tag': { // underline, sub, sup
|
|
const { parent } = token
|
|
const index = parent.indexOf(token)
|
|
parent.splice(index, 1, ...token.children)
|
|
break
|
|
}
|
|
case 'image': {
|
|
token.type = 'text'
|
|
token.raw = token.alt
|
|
delete token.marker
|
|
delete token.src
|
|
break
|
|
}
|
|
case 'inline_math':
|
|
case 'inline_code': {
|
|
token.type = 'text'
|
|
token.raw = token.content
|
|
delete token.marker
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const addFormat = (type, block, { start, end }) => {
|
|
if (
|
|
block.type !== 'span' ||
|
|
(block.type === 'span' && !/paragraphContent|cellConntent|atxLine/.test(block.functionType))
|
|
) {
|
|
return false
|
|
}
|
|
switch (type) {
|
|
case 'em':
|
|
case 'del':
|
|
case 'inline_code':
|
|
case 'strong':
|
|
case 'inline_math': {
|
|
const MARKER = FORMAT_MARKER_MAP[type]
|
|
const oldText = block.text
|
|
block.text = oldText.substring(0, start.offset) +
|
|
MARKER + oldText.substring(start.offset, end.offset) +
|
|
MARKER + oldText.substring(end.offset)
|
|
start.offset += MARKER.length
|
|
end.offset += MARKER.length
|
|
break
|
|
}
|
|
case 'sub':
|
|
case 'sup':
|
|
case 'mark':
|
|
case 'u': {
|
|
const MARKER = FORMAT_MARKER_MAP[type]
|
|
const oldText = block.text
|
|
block.text = oldText.substring(0, start.offset) +
|
|
MARKER.open + oldText.substring(start.offset, end.offset) +
|
|
MARKER.close + oldText.substring(end.offset)
|
|
start.offset += MARKER.open.length
|
|
end.offset += MARKER.open.length
|
|
break
|
|
}
|
|
case 'link':
|
|
case 'image': {
|
|
const oldText = block.text
|
|
const anchorTextLen = end.offset - start.offset
|
|
block.text = oldText.substring(0, start.offset) +
|
|
(type === 'link' ? '[' : '![') +
|
|
oldText.substring(start.offset, end.offset) + ']()' +
|
|
oldText.substring(end.offset)
|
|
// put cursor between `()`
|
|
start.offset += type === 'link' ? 3 + anchorTextLen : 4 + anchorTextLen
|
|
end.offset = start.offset
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const checkTokenIsInlineFormat = token => {
|
|
const { type, tag } = token
|
|
if (FORMAT_TYPES.includes(type)) return true
|
|
if (type === 'html_tag' && /^(?:u|sub|sup|mark)$/i.test(tag)) return true
|
|
return false
|
|
}
|
|
|
|
const formatCtrl = ContentState => {
|
|
ContentState.prototype.selectionFormats = function ({ start, end } = selection.getCursorRange()) {
|
|
if (!start || !end) {
|
|
return { formats: [], tokens: [], neighbors: [] }
|
|
}
|
|
const startBlock = this.getBlock(start.key)
|
|
const formats = []
|
|
const neighbors = []
|
|
let tokens = []
|
|
if (start.key === end.key) {
|
|
const { text } = startBlock
|
|
tokens = tokenizer(text, {
|
|
options: this.muya.options
|
|
})
|
|
;(function iterator (tks) {
|
|
for (const token of tks) {
|
|
if (
|
|
checkTokenIsInlineFormat(token) &&
|
|
start.offset >= token.range.start &&
|
|
end.offset <= token.range.end
|
|
) {
|
|
formats.push(token)
|
|
}
|
|
if (
|
|
checkTokenIsInlineFormat(token) &&
|
|
((start.offset >= token.range.start && start.offset <= token.range.end) ||
|
|
(end.offset >= token.range.start && end.offset <= token.range.end) ||
|
|
(start.offset <= token.range.start && token.range.end <= end.offset))
|
|
) {
|
|
neighbors.push(token)
|
|
}
|
|
if (token.children && token.children.length) {
|
|
iterator(token.children)
|
|
}
|
|
}
|
|
})(tokens)
|
|
}
|
|
|
|
return { formats, tokens, neighbors }
|
|
}
|
|
|
|
ContentState.prototype.clearBlockFormat = function (block, { start, end } = selection.getCursorRange(), type) {
|
|
if (!start || !end) {
|
|
return
|
|
}
|
|
if (block.type === 'pre') return false
|
|
const { key } = block
|
|
let tokens
|
|
let neighbors
|
|
if (start.key === end.key && start.key === key) {
|
|
({ tokens, neighbors } = this.selectionFormats({ start, end }))
|
|
} else if (start.key !== end.key && start.key === key) {
|
|
({ tokens, neighbors } = this.selectionFormats({ start, end: { key: start.key, offset: block.text.length } }))
|
|
} else if (start.key !== end.key && end.key === key) {
|
|
({ tokens, neighbors } = this.selectionFormats({
|
|
start: {
|
|
key: end.key,
|
|
offset: 0
|
|
},
|
|
end
|
|
}))
|
|
} else {
|
|
({ tokens, neighbors } = this.selectionFormats({
|
|
start: {
|
|
key,
|
|
offset: 0
|
|
},
|
|
end: {
|
|
key,
|
|
offset: block.text.length
|
|
}
|
|
}))
|
|
}
|
|
|
|
neighbors = type ? neighbors.filter(n => {
|
|
return n.type === type ||
|
|
n.type === 'html_tag' && n.tag === type
|
|
}) : neighbors
|
|
|
|
for (const neighbor of neighbors) {
|
|
clearFormat(neighbor, { start, end })
|
|
}
|
|
start.offset += start.delata
|
|
end.offset += end.delata
|
|
block.text = generator(tokens)
|
|
}
|
|
|
|
ContentState.prototype.format = function (type) {
|
|
const { start, end } = selection.getCursorRange()
|
|
if (!start || !end) {
|
|
return
|
|
}
|
|
const startBlock = this.getBlock(start.key)
|
|
const endBlock = this.getBlock(end.key)
|
|
start.delata = end.delata = 0
|
|
if (start.key === end.key) {
|
|
const { formats, tokens, neighbors } = this.selectionFormats()
|
|
const currentFormats = formats.filter(format => {
|
|
return format.type === type ||
|
|
format.type === 'html_tag' && format.tag === type
|
|
}).reverse()
|
|
const currentNeightbors = neighbors.filter(format => {
|
|
return format.type === type ||
|
|
format.type === 'html_tag' && format.tag === type
|
|
}).reverse()
|
|
// cache delata
|
|
if (type === 'clear') {
|
|
for (const neighbor of neighbors) {
|
|
clearFormat(neighbor, { start, end })
|
|
}
|
|
start.offset += start.delata
|
|
end.offset += end.delata
|
|
startBlock.text = generator(tokens)
|
|
} else if (currentFormats.length) {
|
|
for (const token of currentFormats) {
|
|
clearFormat(token, { start, end })
|
|
}
|
|
start.offset += start.delata
|
|
end.offset += end.delata
|
|
startBlock.text = generator(tokens)
|
|
} else {
|
|
if (currentNeightbors.length) {
|
|
for (const neighbor of currentNeightbors) {
|
|
clearFormat(neighbor, { start, end })
|
|
}
|
|
}
|
|
start.offset += start.delata
|
|
end.offset += end.delata
|
|
startBlock.text = generator(tokens)
|
|
addFormat(type, startBlock, { start, end })
|
|
if (type === 'image') {
|
|
// Show image selector when create a inline image by menu/shortcut/or just input `![]()`
|
|
requestAnimationFrame(() => {
|
|
const startNode = selection.getSelectionStart()
|
|
if (startNode) {
|
|
const imageWrapper = startNode.closest('.ag-inline-image')
|
|
if (imageWrapper && imageWrapper.classList.contains('ag-empty-image')) {
|
|
const imageInfo = getImageInfo(imageWrapper)
|
|
this.muya.eventCenter.dispatch('muya-image-selector', {
|
|
reference: imageWrapper,
|
|
imageInfo,
|
|
cb: () => {}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
this.cursor = { start, end }
|
|
this.partialRender()
|
|
} else {
|
|
let nextBlock = startBlock
|
|
const formatType = type !== 'clear' ? type : undefined
|
|
while (nextBlock && nextBlock !== endBlock) {
|
|
this.clearBlockFormat(nextBlock, { start, end }, formatType)
|
|
nextBlock = this.findNextBlockInLocation(nextBlock)
|
|
}
|
|
this.clearBlockFormat(endBlock, { start, end }, formatType)
|
|
|
|
if (type !== 'clear') {
|
|
addFormat(type, startBlock, {
|
|
start,
|
|
end: { offset: startBlock.text.length }
|
|
})
|
|
nextBlock = this.findNextBlockInLocation(startBlock)
|
|
while (nextBlock && nextBlock !== endBlock) {
|
|
addFormat(type, nextBlock, {
|
|
start: { offset: 0 },
|
|
end: { offset: nextBlock.text.length }
|
|
})
|
|
nextBlock = this.findNextBlockInLocation(nextBlock)
|
|
}
|
|
addFormat(type, endBlock, {
|
|
start: { offset: 0 },
|
|
end
|
|
})
|
|
}
|
|
|
|
this.cursor = { start, end }
|
|
this.partialRender()
|
|
}
|
|
}
|
|
}
|
|
|
|
export default formatCtrl
|