const {
DISPLAY_TYPES,
NODE_TYPE,
MARK_TYPE,
LINK_PROTOCOL,
MENTION_PREFIX,
BREAKLINE_CHAR
} = require('../constants.js')
const { normalizeUrl } = require('../utils.js')
const {
MENTION,
HTTP_LINK,
PEAR_LINK,
BOLD,
ITALIC,
CODE,
EMOJI,
CODE_BLOCK,
STRIKE_THROUGH
} = DISPLAY_TYPES
module.exports = { documentToDisplay }
function documentToDisplay(doc) {
return parseNode(doc)
}
function parseNode(node, start = 0) {
let text = ''
let display = []
if (isLeaf(node)) {
text = getNodeText(node)
display = getNodeDisplay(node, start)
} else {
const blockStart = start
let textChildren = ''
let displayChildren = []
for (const [index, childNode] of node.content.entries()) {
const child = parseNode(childNode, start)
if (needsBreakline(node, index)) {
child.text += BREAKLINE_CHAR
}
textChildren += child.text
displayChildren = displayChildren.concat(child.display)
start += child.text.length
}
const textBlock = getNodeText(node, textChildren)
if (textBlock) {
text = textBlock
} else {
text = textChildren
}
const displayBlock = getNodeDisplay(node, blockStart, text.length)
if (Object.keys(displayBlock).length > 0) {
display = displayBlock
} else {
display = displayChildren
}
}
return { text, display }
}
function getNodeText(node, textChildren = '') {
let text = ''
switch (node.type) {
case NODE_TYPE.TEXT:
text = node.text
break
case NODE_TYPE.EMOJI:
text = node.attrs['data-emoji']
break
case NODE_TYPE.MENTION:
text = `${MENTION_PREFIX}${node.attrs.label}`
break
case NODE_TYPE.CODE_BLOCK:
text = ` ${textChildren} `
break
case NODE_TYPE.HARD_BREAK:
text = BREAKLINE_CHAR
break
default:
break
}
return text
}
function getNodeDisplay(node, start, lengthChildren = 0) {
const display = []
switch (node.type) {
case NODE_TYPE.TEXT: {
const marks = node.marks || []
for (const mark of marks) {
const markDisplay = getMarkDisplay(mark)
if (markDisplay) {
const { type, ...rest } = markDisplay
display.push({
type,
start,
length: node.text.length,
...rest
})
}
}
break
}
case NODE_TYPE.EMOJI: {
display.push({
type: EMOJI,
start,
length: node.attrs['data-emoji'].length,
content: node.attrs['data-shortcode']
})
break
}
case NODE_TYPE.MENTION: {
display.push({
type: MENTION,
start,
length: MENTION_PREFIX.length + node.attrs.label.length,
memberId: node.attrs.id
})
break
}
case NODE_TYPE.CODE_BLOCK: {
display.push({
type: CODE_BLOCK,
start: start + 1,
length: Math.max(0, lengthChildren - 2)
})
break
}
default:
break
}
return display
}
function getMarkDisplay(mark) {
let display
switch (mark.type) {
case MARK_TYPE.BOLD:
display = { type: BOLD }
break
case MARK_TYPE.ITALIC:
display = { type: ITALIC }
break
case MARK_TYPE.STRIKE_THROUGH:
display = { type: STRIKE_THROUGH }
break
case MARK_TYPE.CODE:
display = { type: CODE }
break
case MARK_TYPE.LINK:
if (isHttpLink(mark)) {
display = {
type: HTTP_LINK,
content: getLink(mark)
}
} else if (isPearLink(mark)) {
display = {
type: PEAR_LINK,
content: getLink(mark)
}
}
break
default:
break
}
return display
}
function needsBreakline(node, index) {
return (
node.content[index].type === NODE_TYPE.PARAGRAPH &&
node.content[index + 1]?.type === NODE_TYPE.PARAGRAPH
)
}
function isLeaf(node) {
return !node.content
}
function getLink(mark) {
return normalizeUrl(mark.attrs.href)
}
function isHttpLink(mark) {
return getLink(mark).startsWith(LINK_PROTOCOL.HTTP)
}
function isPearLink(mark) {
return getLink(mark).startsWith(LINK_PROTOCOL.PEAR)
}