micro-mdx-parser
Version:
A tiny parser to convert markdown or html into JSON
177 lines (161 loc) • 5.25 kB
JavaScript
const optionsParse = require('oparser').parse
const { arrayIncludes }= require('./utils')
const { fixOpenBracket, ARROW_SYMBOL_PATTERN } = require('./utils/find-code')
const { CLOSE_ELEMENT_SYMBOL_PATTERN } = require('./utils/find-components')
const { CLOSE_BRACKET_PATTERN, OPEN_BRACKET_PATTERN } = require('./utils/find-inline-arrow-fn')
function parser (tokens, options) {
const root = {tagName: null, children: []}
const state = {tokens, options, cursor: 0, stack: [root]}
parse(state)
return root.children
}
function hasTerminalParent (tagName, stack, terminals) {
const tagParents = terminals[tagName]
if (tagParents) {
let currentIndex = stack.length - 1
while (currentIndex >= 0) {
const parentTagName = stack[currentIndex].tagName
if (parentTagName === tagName) {
break
}
if (arrayIncludes(tagParents, parentTagName)) {
return true
}
currentIndex--
}
}
return false
}
function rewindStack (stack, newLength, childrenEndPosition, endPosition) {
stack[newLength].position.end = endPosition
for (let i = newLength + 1, len = stack.length; i < len; i++) {
stack[i].position.end = childrenEndPosition
}
stack.splice(newLength)
}
function parse(state) {
const {tokens, options} = state
// console.log('tokens', tokens)
let {stack} = state
let nodes = stack[stack.length - 1].children
const len = tokens.length
let {cursor} = state
while (cursor < len) {
const token = tokens[cursor]
if (token.type !== 'tag-start') {
nodes.push(token)
cursor++
continue
}
const tagToken = tokens[++cursor]
cursor++
const tagName = tagToken.content.toLowerCase()
if (token.close) {
let index = stack.length
let shouldRewind = false
while (--index > -1) {
if (stack[index].tagName === tagName) {
shouldRewind = true
break
}
}
while (cursor < len) {
const endToken = tokens[cursor]
if (endToken.type !== 'tag-end') break
cursor++
}
if (shouldRewind) {
rewindStack(stack, index, token.position.start, tokens[cursor - 1].position.end)
break
} else {
continue
}
}
const isClosingTag = arrayIncludes(options.closingTags, tagName)
let shouldRewindToAutoClose = isClosingTag
if (shouldRewindToAutoClose) {
const { closingTagAncestorBreakers: terminals } = options
shouldRewindToAutoClose = !hasTerminalParent(tagName, stack, terminals)
}
if (shouldRewindToAutoClose) {
// rewind the stack to just above the previous
// closing tag of the same name
let currentIndex = stack.length - 1
while (currentIndex > 0) {
if (tagName === stack[currentIndex].tagName) {
rewindStack(stack, currentIndex, token.position.start, token.position.start)
const previousIndex = currentIndex - 1
nodes = stack[previousIndex].children
break
}
currentIndex = currentIndex - 1
}
}
let propsRaw = ''
let isSelfClosing = false
let attrToken
while (cursor < len) {
attrToken = tokens[cursor]
// console.log('attrToken', attrToken)
if (attrToken.type === 'tag-end') {
// console.log(`x attrToken ${attrToken.name}`, attrToken)
isSelfClosing = attrToken.isSelfClosing
break
}
// console.log('attrToken.content', attrToken.content)
// propsRaw+= ((propsRaw !== '') ? ' ' : '') + attrToken.content
propsRaw = attrToken.src || ''
cursor++
}
cursor++
const children = []
const position = {
start: token.position.start,
end: attrToken.position.end
}
// console.log('propsRaw', propsRaw)
const raw = fixOpenBracket(propsRaw.replace(ARROW_SYMBOL_PATTERN, ' => '))
.replace(CLOSE_ELEMENT_SYMBOL_PATTERN, '/>')
.replace(CLOSE_BRACKET_PATTERN, '}')
.replace(OPEN_BRACKET_PATTERN, '{')
// console.log('propsRaw two', raw)
// const props = (raw) ? optionsParse(raw) : {}
// console.log('props', props)
const elementNode = {
type: 'element',
tagName: tagToken.content,
// attributes,
props: (raw) ? optionsParse(raw) : {},
propsRaw: raw,
// isSelfClosing: Boolean(children.length),
children,
position
}
if (isSelfClosing) {
elementNode.isSelfClosing = isSelfClosing
}
/* Check if valid html void element */
// if (!children.length && arrayIncludes(options.voidTags, tagToken.content.toLowerCase())) {
// elementNode.isVoidTag = true
// }
nodes.push(elementNode)
const hasChildren = !(attrToken.close || arrayIncludes(options.voidTags, tagName))
if (hasChildren) {
const size = stack.push({tagName, children, position})
const innerState = {tokens, options, cursor, stack}
parse(innerState)
cursor = innerState.cursor
const rewoundInElement = stack.length === size
if (rewoundInElement) {
elementNode.position.end = tokens[cursor - 1].position.end
}
}
}
state.cursor = cursor
}
module.exports = {
parser,
hasTerminalParent,
rewindStack,
parse,
}