UNPKG

micro-mdx-parser

Version:

A tiny parser to convert markdown or html into JSON

177 lines (161 loc) 5.25 kB
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, }