posthtml-sugarml
Version:
Sugar Syntax for PostHTML
205 lines (163 loc) • 5.11 kB
JavaScript
// ------------------------------------
// #SugarML - Parser
// ------------------------------------
// Our target output is a Posthtml AST
module.exports = (tokens) => {
let current = 0
let indentLevel = 0
let token = tokens[current]
function walk (ctx) {
token = tokens[current]
const dt = doctype()
if (typeof dt !== 'undefined') { return dt }
const co = content()
if (typeof co !== 'undefined') { return co }
const cm = comment()
if (typeof cm !== 'undefined') { return cm }
const ta = tag()
if (typeof ta !== 'undefined') { return ta }
const ne = newline(ctx)
if (typeof ne !== 'undefined') { return ne }
const en = eof()
if (typeof en !== 'undefined') { return en }
// if we haven't matched here, there must be an error
throw new TypeError(`Unrecognized token type: ${token.type}`)
}
// run the parser
const root = []
while (current < tokens.length) { root.push(walk(root)) }
return root
/**
* Doctype
*/
function doctype () {
if (token && token.type === 'doctype') {
current++
return `<!DOCTYPE ${token.value}>`
}
}
/**
* Contents
*/
function content () {
if (token && token.type === 'content') {
current++
return token.value
}
}
/**
* Comments
*/
function comment () {
if (token && token.type === 'comment') {
current++
return `<!-- ${token.value} -->`
}
}
/**
* Tag
*
* If there's a tag, we get the attributes, recurse if there's nested
* content, then return the tag node.
*/
function tag () {
if (token && token.type === 'tag') {
// create our base node with the tag's name
const node = { tag: token.value, content: [] }
// move past the tag token
next()
// Now we do the attributes by looping until we are through all the
// attributes and on to another token type
if (token.type === 'attrKey') { node.attrs = {} }
while (token.type === 'attrKey' || token.type === 'attrVal') {
// if we have a key, add it to attrs with empty string value
if (token.type === 'attrKey') {
// if this attr hasn't already been populated, initialize it
if (!node.attrs[token.value]) { node.attrs[token.value] = '' }
next()
}
// if we have a value, add it as the value for the previous key
if (token.type === 'attrVal') {
const previousKey = tokens[current - 1].value
if (node.attrs[previousKey].length > 1) {
node.attrs[previousKey] += ' '
}
node.attrs[previousKey] += token.value
next()
}
// TODO need a way to handle multiples and conflicts
}
// grab the current indent level, we need to to decide how long to keep
// searching for contents
const currentIndent = indentLevel
// now we recurse to get the contents, looping while the indent level is
// greater than that of the current node, to pick up everything nested
node.content.push(walk(node.content))
while (indentLevel > currentIndent) { // eslint-disable-line
node.content.push(walk(node.content))
}
// when finished, return the node
return node
}
}
/**
* We handle newlines and indents in one shot, since indents always come after
* a newline.
*/
function newline (ctx) {
if (token && token.type === 'newline') {
// move past the newline
next()
// ctx.push('\n')
// if the next token is a tag, it must be at the root indent level, so
// we reset the indent level before moving forward. this happens at the
// end of the page, with the last element usually
if (token && token.type === 'tag') {
indentLevel = 0
}
// if the next token is an indent, we need to deal with nesting
if (token && token.type === 'indent') {
// if our indent level is greater than what we were at before, we
// recurse again, and update the current indent level
if (token.level > indentLevel) {
indentLevel = token.level
current++
ctx.push('')
return walk(ctx)
}
// if the indent level is the same as before, we just continue parsing
// at the same level
if (token.level === indentLevel) {
current++
return ''
}
// if the indent level is less than before, we decrease the indent level
// to match and return
if (token.level < indentLevel) {
indentLevel = token.level
current++
return ''
}
} else {
return ''
}
}
}
/**
* End of input, this means our indent level is back to zero. This will
* break out of any existing nest loops.
*/
function eof () {
if (typeof token === 'undefined') {
indentLevel = 0
return ''
}
}
/**
* Move forward to the next token.
*/
function next () {
token = tokens[++current]
}
}