Skip to content

Instantly share code, notes, and snippets.

@nebrelbug
Last active July 15, 2019 20:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nebrelbug/7f1d0d0c80b90c86ed629cc8a10e6cb5 to your computer and use it in GitHub Desktop.
Save nebrelbug/7f1d0d0c80b90c86ed629cc8a10e6cb5 to your computer and use it in GitHub Desktop.
A parser for SquirrellyJS
// Version 1.0.21
var parseTag = require('./TagParse')
module.exports = function Parse (str, tagOpen, tagClose) {
var lastIndex = 0 // Because lastIndex can be complicated, and this way the minifier can minify more
var regEx = new RegExp(tagOpen + '(-)?([^]*?)(-)?' + tagClose, 'g')
var stringLength = str.length
function parseContext (parentObj, firstParse) {
var lastBlock = false
var buffer = []
function pushString (indx) {
if (lastIndex !== indx) {
buffer.push(
str
.slice(lastIndex, indx)
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
)
}
}
// Random TODO: parentObj.b doesn't need to have t: #
var m
while ((m = regEx.exec(str)) !== null) {
pushString(m.index)
lastIndex = regEx.lastIndex // TODO: Check performance gains
var currentObj = parseTag(m)
// ===== NOW ADD THE OBJECT TO OUR BUFFER =====
var currentType = currentObj.t
if (currentType === '~') {
currentObj = parseContext(currentObj) // No need to pass in false, undefined is falsy
buffer.push(currentObj)
} else if (currentType === '/') {
if (parentObj.n === currentObj.n) {
if (lastBlock) {
lastBlock.d = buffer
parentObj.b.push(lastBlock)
} else {
parentObj.d = buffer
}
// console.log('parentObj: ' + JSON.stringify(parentObj))
return parentObj
} else {
throw Error("Helper start and end don't match")
}
} else if (currentType === '#') {
if (lastBlock) {
lastBlock.d = buffer
parentObj.b.push(lastBlock)
} else {
parentObj.d = buffer
parentObj.b = [] // Create a new array to store the parent's blocks
}
lastBlock = currentObj // Set the 'lastBlock' object to the value of the current block
buffer = []
} else {
buffer.push(currentObj)
}
// ===== DONE ADDING OBJECT TO BUFFER =====
}
if (firstParse) {
// TODO: more intuitive
pushString(stringLength)
parentObj.d = buffer
return parentObj
}
return parentObj
}
var parseResult = parseContext({}, true)
// console.log(JSON.stringify(parseResult))
return parseResult.d // Parse the very outside context
}
// Version 1.0.21
module.exports = function parseTag (match) {
// console.log(JSON.stringify(match))
var currentObj = { s: match[1], e: match[3] }
var innerTag = match[2].trim()
if (/^\/\*[^]*\*\/$/.test(innerTag) || !innerTag) {
// It's a comment, or innerTag is blank: like {{}}. TODO: Run tests
return currentObj
}
var escaped = false
var currentAttribute = '' // Valid values: 'c'=content, 'f'=filter, 'fp'=filter params, 'p'=param, 'n'=name
var quoteType // Valid values: '"', "'", "`", false
var insideQuotes = false // Valid values: true, false
var numParens = 0
var filterNumber = 0
var currentType = ''
var startInd = 0
function addAttrValue (indx, strng) {
var val = (innerTag.slice(startInd, indx) + (strng || '')).trim()
if (currentAttribute === 'f') {
currentObj.f[filterNumber - 1][0] += val // filterNumber - 1 because first filter: 0->1, but zero-indexed arrays
} else if (currentAttribute === 'fp') {
currentObj.f[filterNumber - 1][1] += val
} else if (currentAttribute !== '') {
if (currentObj[currentAttribute]) {
currentObj[currentAttribute] += val
} else {
currentObj[currentAttribute] = val
}
}
startInd = indx + 1
}
var i = 0
for (; i < innerTag.length; i++) {
var char = innerTag[i]
if (currentType === '') {
startInd = i + 1 // Default
currentAttribute = 'c' // Default
currentType = char // Default
if (/[a-zA-Z$_]/.test(char)) {
currentType = 'r' // Reference
startInd -= 1 // Include the first character
} else if (char === '~' || char === '#' || char === '/') {
currentAttribute = 'n'
} else if (char === '=' /* || char === '>' */ || char === '!') {
// Do nothing
} else if (char === '@') {
currentObj.l = getHrefScope(i, innerTag)
i += 3 * currentObj.l
startInd += 3 * currentObj.l // TODO: Eventually, put this in getHrefScope
} else {
currentType = 'c' // Custom
startInd -= 1 // Include the first character
}
} else {
if (char === '\\') {
escaped = !escaped // Toggle the escape
} else if (!escaped && (char === '"' || char === "'" || char === '`')) {
// Test if it's a valid quote
if (insideQuotes && quoteType === char) {
// If inside quotes, and the quote type is the current char
// then this is a closing quote
insideQuotes = false
quoteType = '' // We should be able to remove this...
} else if (!insideQuotes) {
insideQuotes = true
quoteType = char
}
} else if (!insideQuotes) {
if (char === '@') {
var j = getHrefScope(i, innerTag)
// str.slice includes that index
addAttrValue(i, 'h.r(' + j + ').')
/* In the generated function: 'function hr(a,b){return a[a.length-1-b]}' */
i += 3 * j
startInd = i + 1
} else if (
currentAttribute === 'f' &&
currentType === '~' &&
char === '/'
) {
// Assume it's a self-closing helper
addAttrValue(i - 1)
startInd += 1
// I removed error checking, all error checking for bad syntax will be done in the Compiler
} else if (char === '(' && !escaped) {
if (numParens === 0) {
if (currentAttribute === 'n') {
addAttrValue(i)
currentAttribute = 'p'
} else if (currentAttribute === 'f') {
addAttrValue(i)
currentAttribute = 'fp'
}
}
numParens++
} else if (char === ')' && !escaped) {
numParens--
if (numParens === 0 && currentAttribute !== 'c') {
// Then it's closing a filter, block, or helper
addAttrValue(i)
currentAttribute = '' // Reset the current attribute
}
} else if (
numParens === 0 &&
!escaped &&
char === '|' &&
innerTag[i - 1] !== '|' && // Checking to make sure it's not an OR ||
innerTag[i + 1] !== '|' &&
(currentType === '@' || currentType === 'r' || currentType === '~') // TODO: Add >?
) {
addAttrValue(i)
currentAttribute = 'f'
if (filterNumber === 0) {
currentObj.f = [] // Initial assign
}
filterNumber++
currentObj.f[filterNumber - 1] = ['', '']
} else {
// It's a regular character
escaped = false
}
} else {
// Inside quotes and not one of '"`
escaped = false
}
}
}
// ===== NOW LAST STEPS, RETURN CURRENTOBJ =====
addAttrValue(i)
if (
innerTag.slice(-1) === '/' && // Self-closing helper. innerTag.slice(-1) returns last character of innerTag
currentType === '~' // Make sure it is a helper
) {
currentType = 's' // For self-closing
}
currentObj.t = currentType
return currentObj
}
function getHrefScope (indx, str) {
var j = 0 // Number of '../'
while (
str[indx + 3 * j + 1] === '.' &&
str[indx + 3 * j + 2] === '.' &&
str[indx + 3 * j + 3] === '/'
) {
j++ // TODO: Change to += 3 (ACTUALLY PROBABLY DON'T)
}
return j
}
@nebrelbug
Copy link
Author

With the 4th revision I fixed an issue where characters outside of tags, but inside comments, would be added to the buffer

@nebrelbug
Copy link
Author

With the 5th revision I added support for self-closing helpers, throw an error when an invalid character is in a filter name

@nebrelbug
Copy link
Author

In revision six I created a currentType variable and moved buffer.push(str.slice(... into a separate function

@nebrelbug
Copy link
Author

Still slower than Sqrl.Compile. I'm going to have to get really tricky

@nebrelbug
Copy link
Author

The 8th revision makes it significantly faster

@nebrelbug
Copy link
Author

With the 9th revision, PARSING IS FASTER THAN SQRL.COMPILE!

Instead of adding each individual character to currentObj[currentAttribute], I push a sliced string (using addAttrValue()) containing the last characters. I do this whenever the attribute changes or the tag closes

@nebrelbug
Copy link
Author

Revision 10 fixes an issue where {{val}}{{someval}} wasn't being parsed correctly.

Instead of i += cTag.length, I have i += cTag.length -1 and lastInd = i + 1

@nebrelbug
Copy link
Author

More intelligent tags with revision 11

@nebrelbug
Copy link
Author

Revision 12, which I like to call speedyTags2 or speedyTagsCached, creates a variable to hold tagOpen.length (it also renamed oTag to tagOpen ) and tagClose.length

Benchmarks (Compile is the old version of Squirrelly, turned into a string)

Compile x 59,106 ops/sec ±1.06% (92 runs sampled)
Parse#WithBrackets x 71,463 ops/sec ±1.23% (91 runs sampled)
Parse#SpeedyTags x 88,479 ops/sec ±1.16% (96 runs sampled)
Parse#SpeedyTagsCached x 91,750 ops/sec ±0.59% (93 runs sampled)
Fastest is Parse#SpeedyTagsCached

@nebrelbug
Copy link
Author

Revision 13 uses indexOf so it's faster and more concise.

It also slightly modifies the pushString function

@nebrelbug
Copy link
Author

Revision 15 DOES NOT WORK but it's to save a WIP

@nebrelbug
Copy link
Author

Revision 16 should work again

@nebrelbug
Copy link
Author

With the 18th revision, I refactored the parser into 2 parts: Parse.js, which handles high-level TT (Template Tree - I may come up with a better name) generation, and TagParse.js, which handles parsing inside of tags. Additionally, I refactored so the Parser uses RegExp to loop through tags

@nebrelbug
Copy link
Author

Revision 19: IT WORKS!

@nebrelbug
Copy link
Author

With version 21 I updated {l and r} to {s, e} since l was already taken

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment