From 63b66957427351a2433f5cb8133dc641478a2b68 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sat, 25 Nov 2017 00:20:41 -0500 Subject: [PATCH 01/37] Moved `each` tests from control structures to layouts --- .../{control-structures => layouts}/_each-array-partial.stone | 0 .../{control-structures => layouts}/_each-empty-partial.stone | 0 .../{control-structures => layouts}/_each-object-partial.stone | 0 test/views/{control-structures => layouts}/each-array-empty.html | 0 test/views/{control-structures => layouts}/each-array-empty.stone | 0 test/views/{control-structures => layouts}/each-array-extra.html | 0 test/views/{control-structures => layouts}/each-array-extra.stone | 0 test/views/{control-structures => layouts}/each-array.html | 0 test/views/{control-structures => layouts}/each-array.stone | 0 test/views/{control-structures => layouts}/each-empty-extra.html | 0 test/views/{control-structures => layouts}/each-empty-extra.stone | 0 test/views/{control-structures => layouts}/each-empty-raw.html | 0 test/views/{control-structures => layouts}/each-empty-raw.stone | 0 test/views/{control-structures => layouts}/each-object-empty.html | 0 .../views/{control-structures => layouts}/each-object-empty.stone | 0 test/views/{control-structures => layouts}/each-object-extra.html | 0 .../views/{control-structures => layouts}/each-object-extra.stone | 0 test/views/{control-structures => layouts}/each-object.html | 0 test/views/{control-structures => layouts}/each-object.stone | 0 19 files changed, 0 insertions(+), 0 deletions(-) rename test/views/{control-structures => layouts}/_each-array-partial.stone (100%) rename test/views/{control-structures => layouts}/_each-empty-partial.stone (100%) rename test/views/{control-structures => layouts}/_each-object-partial.stone (100%) rename test/views/{control-structures => layouts}/each-array-empty.html (100%) rename test/views/{control-structures => layouts}/each-array-empty.stone (100%) rename test/views/{control-structures => layouts}/each-array-extra.html (100%) rename test/views/{control-structures => layouts}/each-array-extra.stone (100%) rename test/views/{control-structures => layouts}/each-array.html (100%) rename test/views/{control-structures => layouts}/each-array.stone (100%) rename test/views/{control-structures => layouts}/each-empty-extra.html (100%) rename test/views/{control-structures => layouts}/each-empty-extra.stone (100%) rename test/views/{control-structures => layouts}/each-empty-raw.html (100%) rename test/views/{control-structures => layouts}/each-empty-raw.stone (100%) rename test/views/{control-structures => layouts}/each-object-empty.html (100%) rename test/views/{control-structures => layouts}/each-object-empty.stone (100%) rename test/views/{control-structures => layouts}/each-object-extra.html (100%) rename test/views/{control-structures => layouts}/each-object-extra.stone (100%) rename test/views/{control-structures => layouts}/each-object.html (100%) rename test/views/{control-structures => layouts}/each-object.stone (100%) diff --git a/test/views/control-structures/_each-array-partial.stone b/test/views/layouts/_each-array-partial.stone similarity index 100% rename from test/views/control-structures/_each-array-partial.stone rename to test/views/layouts/_each-array-partial.stone diff --git a/test/views/control-structures/_each-empty-partial.stone b/test/views/layouts/_each-empty-partial.stone similarity index 100% rename from test/views/control-structures/_each-empty-partial.stone rename to test/views/layouts/_each-empty-partial.stone diff --git a/test/views/control-structures/_each-object-partial.stone b/test/views/layouts/_each-object-partial.stone similarity index 100% rename from test/views/control-structures/_each-object-partial.stone rename to test/views/layouts/_each-object-partial.stone diff --git a/test/views/control-structures/each-array-empty.html b/test/views/layouts/each-array-empty.html similarity index 100% rename from test/views/control-structures/each-array-empty.html rename to test/views/layouts/each-array-empty.html diff --git a/test/views/control-structures/each-array-empty.stone b/test/views/layouts/each-array-empty.stone similarity index 100% rename from test/views/control-structures/each-array-empty.stone rename to test/views/layouts/each-array-empty.stone diff --git a/test/views/control-structures/each-array-extra.html b/test/views/layouts/each-array-extra.html similarity index 100% rename from test/views/control-structures/each-array-extra.html rename to test/views/layouts/each-array-extra.html diff --git a/test/views/control-structures/each-array-extra.stone b/test/views/layouts/each-array-extra.stone similarity index 100% rename from test/views/control-structures/each-array-extra.stone rename to test/views/layouts/each-array-extra.stone diff --git a/test/views/control-structures/each-array.html b/test/views/layouts/each-array.html similarity index 100% rename from test/views/control-structures/each-array.html rename to test/views/layouts/each-array.html diff --git a/test/views/control-structures/each-array.stone b/test/views/layouts/each-array.stone similarity index 100% rename from test/views/control-structures/each-array.stone rename to test/views/layouts/each-array.stone diff --git a/test/views/control-structures/each-empty-extra.html b/test/views/layouts/each-empty-extra.html similarity index 100% rename from test/views/control-structures/each-empty-extra.html rename to test/views/layouts/each-empty-extra.html diff --git a/test/views/control-structures/each-empty-extra.stone b/test/views/layouts/each-empty-extra.stone similarity index 100% rename from test/views/control-structures/each-empty-extra.stone rename to test/views/layouts/each-empty-extra.stone diff --git a/test/views/control-structures/each-empty-raw.html b/test/views/layouts/each-empty-raw.html similarity index 100% rename from test/views/control-structures/each-empty-raw.html rename to test/views/layouts/each-empty-raw.html diff --git a/test/views/control-structures/each-empty-raw.stone b/test/views/layouts/each-empty-raw.stone similarity index 100% rename from test/views/control-structures/each-empty-raw.stone rename to test/views/layouts/each-empty-raw.stone diff --git a/test/views/control-structures/each-object-empty.html b/test/views/layouts/each-object-empty.html similarity index 100% rename from test/views/control-structures/each-object-empty.html rename to test/views/layouts/each-object-empty.html diff --git a/test/views/control-structures/each-object-empty.stone b/test/views/layouts/each-object-empty.stone similarity index 100% rename from test/views/control-structures/each-object-empty.stone rename to test/views/layouts/each-object-empty.stone diff --git a/test/views/control-structures/each-object-extra.html b/test/views/layouts/each-object-extra.html similarity index 100% rename from test/views/control-structures/each-object-extra.html rename to test/views/layouts/each-object-extra.html diff --git a/test/views/control-structures/each-object-extra.stone b/test/views/layouts/each-object-extra.stone similarity index 100% rename from test/views/control-structures/each-object-extra.stone rename to test/views/layouts/each-object-extra.stone diff --git a/test/views/control-structures/each-object.html b/test/views/layouts/each-object.html similarity index 100% rename from test/views/control-structures/each-object.html rename to test/views/layouts/each-object.html diff --git a/test/views/control-structures/each-object.stone b/test/views/layouts/each-object.stone similarity index 100% rename from test/views/control-structures/each-object.stone rename to test/views/layouts/each-object.stone From 8f54ca95e64985280974befd25238afe0534a355 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sat, 25 Nov 2017 00:18:54 -0500 Subject: [PATCH 02/37] Began rebuilding to parse Stone directly through acorn and generated via astring --- src/Compiler.js | 52 +-- src/Compiler/Outputs.js | 35 -- src/{AST.js => Stone.js} | 68 ++-- src/Stone/Generator.js | 24 ++ src/Stone/Parser.js | 291 +++++++++++++++ src/Stone/Parsers/Output.js | 228 ++++++++++++ src/Stone/Parsers/index.js | 3 + src/Stone/Tokens/StoneDirective.js | 14 + src/Stone/Tokens/StoneOutput.js | 28 ++ src/Stone/Tokens/StoneOutput/Chunk.js | 23 ++ src/Stone/Tokens/StoneOutput/OpenSafe.js | 17 + src/Stone/Tokens/StoneOutput/OpenUnsafe.js | 17 + src/Stone/Tokens/TokenType.js | 25 ++ src/Stone/Types/StoneDump.js | 9 + src/Stone/Types/StoneEmptyExpression.js | 7 + src/Stone/Types/StoneOutput.js | 9 + src/Stone/Types/StoneOutputExpression.js | 15 + src/Stone/Types/index.js | 6 + src/Stone/Walker.js | 7 + src/StoneTemplate.js | 391 --------------------- src/Support/contextualize.js | 21 +- 21 files changed, 780 insertions(+), 510 deletions(-) delete mode 100644 src/Compiler/Outputs.js rename src/{AST.js => Stone.js} (50%) create mode 100644 src/Stone/Generator.js create mode 100644 src/Stone/Parser.js create mode 100644 src/Stone/Parsers/Output.js create mode 100644 src/Stone/Parsers/index.js create mode 100644 src/Stone/Tokens/StoneDirective.js create mode 100644 src/Stone/Tokens/StoneOutput.js create mode 100644 src/Stone/Tokens/StoneOutput/Chunk.js create mode 100644 src/Stone/Tokens/StoneOutput/OpenSafe.js create mode 100644 src/Stone/Tokens/StoneOutput/OpenUnsafe.js create mode 100644 src/Stone/Tokens/TokenType.js create mode 100644 src/Stone/Types/StoneDump.js create mode 100644 src/Stone/Types/StoneEmptyExpression.js create mode 100644 src/Stone/Types/StoneOutput.js create mode 100644 src/Stone/Types/StoneOutputExpression.js create mode 100644 src/Stone/Types/index.js create mode 100644 src/Stone/Walker.js delete mode 100644 src/StoneTemplate.js diff --git a/src/Compiler.js b/src/Compiler.js index cdc4b79..bd982c1 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -1,7 +1,8 @@ -import './Errors/StoneCompilerError' -import './StoneTemplate' +import './Stone' +import './Support/contextualize' const fs = require('fs') +const vm = require('vm') export class Compiler { @@ -34,10 +35,12 @@ export class Compiler { this.engine.view.emit('compile:start', file) } - const template = new StoneTemplate(this, contents, file) + let template = null try { - template.compile() + const tree = Stone.parse(contents) + contextualize(tree) + template = Stone.stringify(tree) } catch(err) { if(!err._hasTemplate) { err._hasTemplate = true @@ -56,46 +59,11 @@ export class Compiler { } if(!shouldEval) { - return template.toString() + return template } - return template.toFunction() + const script = new vm.Script(`(${template})`, { filename: file }) + return script.runInNewContext() } - compileDirective(context, name, args) { - if(name === 'directive') { - // Avoid infinite loop - return null - } - - if(typeof this.directives[name] === 'function') { - return this.directives[name](context, args) - } - - const method = `compile${name[0].toUpperCase()}${name.substring(1)}` - - if(typeof this[method] !== 'function') { - throw new StoneCompilerError(context, `@${name} is not a valid Stone directive.`) - } - - return this[method](context, args) - } - - compileEnd() { - return '}' - } - -} - -// Load in the rest of the compilers -for(const [ name, func ] of Object.entries({ - ...require('./Compiler/Assignments'), - ...require('./Compiler/Components'), - ...require('./Compiler/Conditionals'), - ...require('./Compiler/Layouts'), - ...require('./Compiler/Loops'), - ...require('./Compiler/Macros'), - ...require('./Compiler/Outputs'), -})) { - Compiler.prototype[name] = func } diff --git a/src/Compiler/Outputs.js b/src/Compiler/Outputs.js deleted file mode 100644 index 1a3665f..0000000 --- a/src/Compiler/Outputs.js +++ /dev/null @@ -1,35 +0,0 @@ -import '../Errors/StoneCompilerError' - -/** - * Displays the contents of an object or value - * - * @param {object} context Context for the compilation - * @param {mixed} value Object or value to display - * @return {string} Code to display the contents - */ -export function compileDump(context, value) { - context.validateSyntax(value) - return `output += \`
\${escape(stringify(${value}, null, '  '))}
\`` -} - -/** - * Increases the spaceless level - * - * @param {object} context Context for the compilation - */ -export function compileSpaceless(context) { - context.spaceless++ -} - -/** - * Decreases the spaceless level - * - * @param {object} context Context for the compilation - */ -export function compileEndspaceless(context) { - context.spaceless-- - - if(context.spaceless < 0) { - throw new StoneCompilerError(context, 'Unbalanced calls to @endspaceless') - } -} diff --git a/src/AST.js b/src/Stone.js similarity index 50% rename from src/AST.js rename to src/Stone.js index c73cd50..75532a8 100644 --- a/src/AST.js +++ b/src/Stone.js @@ -1,18 +1,52 @@ -const acorn = require('acorn5-object-spread') -const base = require('acorn/dist/walk').base -const astring = require('astring') +import './Stone/Generator' +import './Stone/Parser' +import './Stone/Walker' -export class AST { +const acorn = require('acorn5-object-spread/inject')(require('acorn')) +const astring = require('astring').generate - static parse(string) { - return acorn.parse(string, { +export class Stone { + + static _register() { + if(acorn.plugins.stone) { + return + } + + acorn.plugins.stone = parser => { + for(const name of Object.getOwnPropertyNames(Parser.prototype)) { + if(name === 'constructor') { + continue + } + + if(typeof parser[name] === 'function') { + parser.extend(name, next => { + return function(...args) { + return Parser.prototype[name].call(this, next, ...args) + } + }) + } else { + parser[name] = Parser.prototype[name] + } + } + } + } + + static parse(code) { + this._register() + + return acorn.parse(code, { ecmaVersion: 9, plugins: { - objectSpread: true + objectSpread: true, + stone: true } }) } + static stringify(tree) { + return astring(tree, { generator: Generator }) + } + static walk(node, visitors) { (function c(node, st, override) { if(node.isNil) { @@ -27,7 +61,7 @@ export class AST { found(node, st) } - base[type](node, st, c) + Walker[type](node, st, c) })(node) } @@ -51,22 +85,4 @@ export class AST { } } - static stringify(tree) { - return astring.generate(tree, { - generator: { - ...astring.baseGenerator, - Property(node, state) { - if(node.type === 'SpreadElement') { - state.write('...(') - this[node.argument.type](node.argument, state) - state.write(')') - return - } - - return astring.baseGenerator.Property.call(this, node, state) - } - } - }) - } - } diff --git a/src/Stone/Generator.js b/src/Stone/Generator.js new file mode 100644 index 0000000..b255435 --- /dev/null +++ b/src/Stone/Generator.js @@ -0,0 +1,24 @@ +import './Types' + +const { baseGenerator } = require('astring') + +export const Generator = { + + ...baseGenerator, + + Property(node, state) { + if(node.type === 'SpreadElement') { + state.write('...(') + this[node.argument.type](node.argument, state) + state.write(')') + return + } + + return baseGenerator.Property.call(this, node, state) + } + +} + +for(const key of Object.keys(Types)) { + Generator[key] = Types[key].generate.bind(Generator) +} diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js new file mode 100644 index 0000000..1d34b3b --- /dev/null +++ b/src/Stone/Parser.js @@ -0,0 +1,291 @@ +import './Parsers' +import './Tokens/StoneOutput' +import './Tokens/StoneDirective' + +const acorn = require('acorn') + +const tt = acorn.tokTypes + +const directiveCodes = new Set( + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'.split('').map(c => c.charCodeAt(0)) +) + +export class Parser { + + parse() { + const node = this.startNode() + this.nextToken() + + const output = this._createDeclaration('output', this._createLiteral('\'\''), 'let') + const result = this.parseTopLevel(node) + + const template = new acorn.Node(this) + template.type = 'FunctionDeclaration' + template.id = this._createIdentifier('template') + template.params = [ this._createIdentifier('_') ] + template.body = this._createBlockStatement([ + output, + ...result.body, + this._createReturn('output') + ]) + + result.body = [ template ] + + return result + } + + skipSpace(next, ...args) { + return next.call(this, ...args) + } + + readToken(next, code) { + if(code === 64 && !this._isCharCode(123, 1)) { + this.pos++ + code = this.fullCharCodeAtPos() + return this.finishToken(StoneDirective.type) + } else if(!this.inDirective && !this.inOutput) { + return this.finishToken(StoneOutput.type) + } + + return next.call(this, code) + } + + parseTopLevel(next, node) { + const exports = { } + + if(!node.body) { + node.body = [ ] + } + + while(this.type !== tt.eof) { + node.body.push(this.parseStatement(true, true, exports)) + } + + this.next() + + if(this.options.ecmaVersion >= 6) { + node.sourceType = this.options.sourceType + } + + return this.finishNode(node, 'Program') + } + + parseStatement(next, declaration, topLevel, exports) { + if(this.inDirective) { + // When parsing directives, by avoiding this call + // we can leverage the built in acorn functionality + // for parsing things like for loops without it + // trying to parse the block + return this._createBlockStatement([ ]) + } + + switch(this.type) { + case StoneDirective.type: + return this.parseDirective() + case StoneOutput.type: + return this.parseStoneOutput() + default: + return next.call(this, declaration, topLevel, exports) + } + } + + parseDirective() { + let directive = '' + + while(this.pos < this.input.length) { + if(!directiveCodes.has(this.input.charCodeAt(this.pos))) { + break + } + + directive += this.input[this.pos] + ++this.pos + } + + let args = null + const parse = `parse${directive[0].toUpperCase()}${directive.substring(1)}Directive` + const node = this.startNode() + node.directive = directive + + if(this.input.charCodeAt(this.pos) === 40) { + this.inDirective = true + this.next() + this.context.push(new acorn.TokContext) + + const parseArgs = `${parse}Args` + + if(typeof this[parseArgs] === 'function') { + args = this[parseArgs](node) + } else { + args = this.parseParenExpression() + } + + this.context.pop() + this.inDirective = false + this.pos = this.start // Fixes an issue where output after the directive is cutoff, but feels wrong. + } else { + this.next() + } + + if(typeof this[parse] !== 'function') { + this.raise(this.start, `Unknown directive: ${directive}`) + } + + return this[parse](node, args) + } + + parseUntilEndDirective(directives) { + if(Array.isArray(directives)) { + directives = new Set(directives) + } else { + directives = new Set([ directives ]) + } + + const statements = [ ] + + contents: for(;;) { + switch(this.type) { + case StoneDirective.type: { + const node = this.parseDirective() + + if(node.directive && directives.has(node.directive)) { + if(node.type !== 'Directive') { + this.next() + } + + break contents + } else if(node.type !== 'BlankExpression') { + statements.push(node) + } + + this.next() + break + } + + case StoneOutput.type: + statements.push(this.parseStoneOutput()) + break + + case tt.eof: { + const array = Array.from(directives) + let expecting = null + + if(array.length > 1) { + const last = array.length - 1 + expecting = array.slice(0, last).join('`, `@') + expecting += `\` or \`@${array[last]}` + } else { + expecting = array[0] + } + + this.raise(this.start, `Unexpected end of file, expected \`@${expecting}\``) + break + } + + default: + this.finishToken(StoneOutput.type) + break + } + } + + return this._createBlockStatement(statements) + } + + _isCharCode(code, delta = 0) { + return this.input.charCodeAt(this.pos + delta) === code + } + + _createIdentifier(identifier) { + const node = new acorn.Node(this) + node.type = 'Identifier' + node.name = identifier + return node + } + + _maybeCreateIdentifier(name) { + if(typeof name !== 'string') { + return name + } + + return this._createIdentifier(name) + } + + _createBlockStatement(statements) { + const node = new acorn.Node(this) + node.type = 'BlockStatement' + node.body = statements + return node + } + + _createEmptyNode() { + const node = new acorn.Node(this) + node.type = 'BlankExpression' + return node + } + + _createAssignment(left, right, operator = '=') { + const node = this.startNodeAt(this.start, this.startLoc) + node.operator = operator + node.left = this._maybeCreateIdentifier(left) + node.right = right + + return this.finishNode(node, 'AssignmentExpression') + } + + _createDeclaration(lhs, rhs, kind = 'const') { + const declarator = this.startNode() + declarator.id = this._maybeCreateIdentifier(lhs) + declarator.init = rhs + this.finishNode(declarator, 'VariableDeclarator') + + const declaration = this.startNode() + declaration.declarations = [ declarator ] + declaration.kind = kind + return this.finishNode(declaration, 'VariableDeclaration') + } + + _createLiteral(value) { + const node = this.startNode() + node.value = value + node.raw = value + return this.finishNode(node, 'Literal') + } + + _createReturn(value) { + const declarator = this.startNode() + + if(value) { + declarator.argument = this._maybeCreateIdentifier(value) + } + + return this.finishNode(declarator, 'ReturnStatement') + } + + _debug(message = 'DEBUG', peek = false) { + let debug = { + start: this.start, + pos: this.pos, + end: this.end, + code: this.input.charCodeAt(this.pos), + char: this.input.substring(this.pos, this.pos + 1), + type: this.type, + context: this.curContext() + } + + if(peek) { + debug.peek = { + pos: this.input.substring(this.pos, this.pos + 5), + start: this.input.substring(this.start, this.start + 5) + } + } + + debug = require('cardinal').highlight(JSON.stringify(debug, null, 2)) + + console.log(require('chalk').cyan(message), debug) + } + +} + +// Inject the parsers +for(const [ name, func ] of Object.entries(Parsers)) { + Parser.prototype[name] = func +} diff --git a/src/Stone/Parsers/Output.js b/src/Stone/Parsers/Output.js new file mode 100644 index 0000000..09b16c6 --- /dev/null +++ b/src/Stone/Parsers/Output.js @@ -0,0 +1,228 @@ +import '../Tokens/StoneOutput' +import '../Tokens/StoneDirective' + +const { tokTypes: tt } = require('acorn') + +/** + * Displays the contents of an object or value + * + * @param {object} node Blank node + * @param {mixed} value Value to display + * @return {object} Finished node + */ +export function parseDumpDirective(node, value) { + node.value = value + return this.finishNode(node, 'StoneDump') +} + +/** + * Increases the spaceless level + * + * @param {object} node Blank node + * @return {object} Finished node + */ +export function parseSpacelessDirective(node) { + this._spaceless = (this._spaceless || 0) + 1 + Object.assign(node, this.parseUntilEndDirective('endspaceless')) + return this.finishNode(node, 'BlockStatement') +} + +/** + * Decreases the spaceless level + * + * @param {object} node Blank node + * @return {object} Finished node + */ +export function parseEndspacelessDirective(node) { + if(!this._spaceless || this._spaceless === 0) { + this.raise(this.start, '`@endspaceless` outside of `@spaceless`') + } + + this._spaceless-- + + return this.finishNode(node, 'Directive') +} + +/** + * Parses output in Stone files + * + * @return {object} Finished node + */ +export function parseStoneOutput() { + if(this.type !== StoneOutput.type) { + this.unexpected() + } + + const node = this.startNode() + + this.inOutput = true + node.output = this.readStoneOutput() + this.inOutput = false + + return this.finishNode(node, 'StoneOutput') +} + +/** + * Reads the output in Stone files + * + * @return {object} Template literal node + */ +export function readStoneOutput() { + const node = this.startNode() + node.expressions = [ ] + this.next() + + let curElt = this.parseStoneOutputElement() + node.quasis = [ curElt ] + + while(!curElt.tail) { + const isUnsafe = this.type === StoneOutput.openUnsafe + + if(isUnsafe) { + this.expect(StoneOutput.openUnsafe) + } else { + this.expect(StoneOutput.openSafe) + } + + const expression = this.startNode() + expression.safe = !isUnsafe + expression.value = this.parseExpression() + node.expressions.push(this.finishNode(expression, 'StoneOutputExpression')) + + this.skipSpace() + this.pos++ + + if(isUnsafe) { + if(this.type !== tt.prefix) { + this.unexpected() + } else { + this.type = tt.braceR + this.context.pop() + } + + this.pos++ + } + + this.next() + + node.quasis.push(curElt = this.parseStoneOutputElement()) + } + + this.next() + return this.finishNode(node, 'TemplateLiteral') +} + +/** + * Parses chunks of output between braces and directives + * + * @return {object} Template element node + */ +export function parseStoneOutputElement() { + const elem = this.startNode() + let output = this.value + + // Strip space between tags if spaceless + if(this._spaceless > 0) { + output = output.replace(/>\s+<').trim() + } + + // Escape escape characters + output = output.replace(/\\/g, '\\\\') + + // Escape backticks + output = output.replace(/`/g, '\\`') + + // Escape whitespace characters + output = output.replace(/[\n]/g, '\\n') + output = output.replace(/[\r]/g, '\\r') + output = output.replace(/[\t]/g, '\\t') + + elem.value = { + raw: output, + cooked: this.value + } + + this.next() + + elem.tail = this.type === StoneDirective.type || this.type === tt.eof + return this.finishNode(elem, 'TemplateElement') +} + +/** + * Controls the output flow + */ +export function readOutputToken() { + let chunkStart = this.pos + let out = '' + + const pushChunk = () => { + out += this.input.slice(chunkStart, this.pos) + chunkStart = this.pos + } + + const finishChunk = () => { + pushChunk() + return this.finishToken(StoneOutput.output, out) + } + + for(;;) { + if(this.pos >= this.input.length) { + if(this.pos === this.start) { + return this.finishToken(tt.eof) + } + + return finishChunk() + } + + const ch = this.input.charCodeAt(this.pos) + + if(ch === 64 && this._isCharCode(123, 1)) { + if(this._isCharCode(123, 2)) { + pushChunk() + chunkStart = this.pos + 1 + } + + this.pos++ + } else if( + ch === 64 + || (ch === 123 && this._isCharCode(123, 1) && !this._isCharCode(64, -1)) + || (ch === 123 && this._isCharCode(33, 1) && this._isCharCode(33, 2)) + ) { + if(ch === 123 && this._isCharCode(45, 2) && this._isCharCode(45, 3)) { + pushChunk() + this.skipStoneComment() + chunkStart = this.pos + continue + } else if(this.pos === this.start && this.type === StoneOutput.output) { + if(ch === 123) { + if(this._isCharCode(33, 1)) { + this.pos += 3 + return this.finishToken(StoneOutput.openUnsafe) + } else { + this.pos += 2 + return this.finishToken(StoneOutput.openSafe) + } + } + + return this.finishToken(StoneDirective.type) + } + + return finishChunk() + } else { + ++this.pos + } + } +} + +/** + * Skips past the current Stone comment + */ +export function skipStoneComment() { + const end = this.input.indexOf('--}}', this.pos += 4) + + if(end === -1) { + this.raise(this.pos - 4, 'Unterminated comment') + } + + this.pos = end + 4 +} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js new file mode 100644 index 0000000..13678f8 --- /dev/null +++ b/src/Stone/Parsers/index.js @@ -0,0 +1,3 @@ +export const Parsers = { + ...require('./Output'), +} diff --git a/src/Stone/Tokens/StoneDirective.js b/src/Stone/Tokens/StoneDirective.js new file mode 100644 index 0000000..c063c90 --- /dev/null +++ b/src/Stone/Tokens/StoneDirective.js @@ -0,0 +1,14 @@ +import './TokenType' + +export class StoneDirective extends TokenType { + + static type = new StoneDirective + + constructor() { + super('stoneDirective') + + this.context.preserveSpace = true + } + +} + diff --git a/src/Stone/Tokens/StoneOutput.js b/src/Stone/Tokens/StoneOutput.js new file mode 100644 index 0000000..f44c508 --- /dev/null +++ b/src/Stone/Tokens/StoneOutput.js @@ -0,0 +1,28 @@ +import './TokenType' + +import './StoneOutput/Chunk' +import './StoneOutput/OpenSafe' +import './StoneOutput/OpenUnsafe' + +const { TokenType: AcornTokenType } = require('acorn') + +export class StoneOutput extends TokenType { + + static type = new StoneOutput + static output = new Chunk + + static openSafe = new OpenSafe + static closeSafe = new AcornTokenType('}}') + + static openUnsafe = new OpenUnsafe + static closeUnsafe = new AcornTokenType('!!}') + + constructor() { + super('stoneOutput') + + this.context.isExpr = true + this.context.preserveSpace = true + this.context.override = p => p.readOutputToken() + } + +} diff --git a/src/Stone/Tokens/StoneOutput/Chunk.js b/src/Stone/Tokens/StoneOutput/Chunk.js new file mode 100644 index 0000000..84a4e91 --- /dev/null +++ b/src/Stone/Tokens/StoneOutput/Chunk.js @@ -0,0 +1,23 @@ +import '../TokenType' + +export class Chunk extends TokenType { + + constructor() { + super('stoneOutputChunk') + + this.context.isExpr = true + this.context.preserveSpace = true + this.context.override = p => p.readOutputToken() + } + + update(parser) { + const curContext = parser.curContext() + + if(curContext === this.context) { + parser.context.pop() + } else { + parser.context.push(this.context) + } + } + +} diff --git a/src/Stone/Tokens/StoneOutput/OpenSafe.js b/src/Stone/Tokens/StoneOutput/OpenSafe.js new file mode 100644 index 0000000..63bcb4b --- /dev/null +++ b/src/Stone/Tokens/StoneOutput/OpenSafe.js @@ -0,0 +1,17 @@ +import '../TokenType' + +export class OpenSafe extends TokenType { + + constructor() { + super('{{', { beforeExpr: true, startsExpr: true }) + + this.context.preserveSpace = false + } + + update(parser) { + super.update(parser) + + parser.exprAllowed = true + } + +} diff --git a/src/Stone/Tokens/StoneOutput/OpenUnsafe.js b/src/Stone/Tokens/StoneOutput/OpenUnsafe.js new file mode 100644 index 0000000..2242b56 --- /dev/null +++ b/src/Stone/Tokens/StoneOutput/OpenUnsafe.js @@ -0,0 +1,17 @@ +import '../TokenType' + +export class OpenUnsafe extends TokenType { + + constructor() { + super('{!!', { beforeExpr: true, startsExpr: true }) + + this.context.preserveSpace = false + } + + update(parser) { + super.update(parser) + + parser.exprAllowed = true + } + +} diff --git a/src/Stone/Tokens/TokenType.js b/src/Stone/Tokens/TokenType.js new file mode 100644 index 0000000..9963799 --- /dev/null +++ b/src/Stone/Tokens/TokenType.js @@ -0,0 +1,25 @@ +const { TokenType: BaseTokenType, TokContext } = require('acorn') + +export class TokenType extends BaseTokenType { + + constructor(name, ...args) { + super(name, ...args) + + const token = this + this.updateContext = function(...args) { return token.update(this, ...args) } + + if(!this.constructor.context) { + this.constructor.context = new TokContext(name, false) + } + } + + update(parser) { + parser.context.push(this.context) + parser.exprAllowed = parser.type.beforeExpr + } + + get context() { + return this.constructor.context + } + +} diff --git a/src/Stone/Types/StoneDump.js b/src/Stone/Types/StoneDump.js new file mode 100644 index 0000000..e41c96c --- /dev/null +++ b/src/Stone/Types/StoneDump.js @@ -0,0 +1,9 @@ +export function generate({ value }, state) { + state.write('output += `
${_.escape(_.stringify(')
+	this[value.type](value, state)
+	state.write(', null, 2))}
`;') +} + +export function walk({ value }, st, c) { + c(value, st, 'Expression') +} diff --git a/src/Stone/Types/StoneEmptyExpression.js b/src/Stone/Types/StoneEmptyExpression.js new file mode 100644 index 0000000..b8a9ab9 --- /dev/null +++ b/src/Stone/Types/StoneEmptyExpression.js @@ -0,0 +1,7 @@ +export function generate() { + // Do nothing +} + +export function walk() { + // Do nothing +} diff --git a/src/Stone/Types/StoneOutput.js b/src/Stone/Types/StoneOutput.js new file mode 100644 index 0000000..aebbb72 --- /dev/null +++ b/src/Stone/Types/StoneOutput.js @@ -0,0 +1,9 @@ +export function generate({ output }, state) { + state.write('output += ') + this[output.type](output, state) + state.write(';') +} + +export function walk({ output }, st, c) { + c(output, st, 'Expression') +} diff --git a/src/Stone/Types/StoneOutputExpression.js b/src/Stone/Types/StoneOutputExpression.js new file mode 100644 index 0000000..fa7d506 --- /dev/null +++ b/src/Stone/Types/StoneOutputExpression.js @@ -0,0 +1,15 @@ +export function generate({ safe = true, value }, state) { + if(safe) { + state.write('_.escape(') + } + + this[value.type](value, state) + + if(safe) { + state.write(')') + } +} + +export function walk({ value }, st, c) { + c(value, st, 'Expression') +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js new file mode 100644 index 0000000..ed9e803 --- /dev/null +++ b/src/Stone/Types/index.js @@ -0,0 +1,6 @@ +export const Types = { + StoneDump: require('./StoneDump'), + StoneEmptyExpression: require('./StoneEmptyExpression'), + StoneOutput: require('./StoneOutput'), + StoneOutputExpression: require('./StoneOutputExpression'), +} diff --git a/src/Stone/Walker.js b/src/Stone/Walker.js new file mode 100644 index 0000000..b966174 --- /dev/null +++ b/src/Stone/Walker.js @@ -0,0 +1,7 @@ +import './Types' + +export const Walker = { ...require('acorn/dist/walk').base } + +for(const key of Object.keys(Types)) { + Walker[key] = Types[key].walk.bind(Walker) +} diff --git a/src/StoneTemplate.js b/src/StoneTemplate.js deleted file mode 100644 index fdf2657..0000000 --- a/src/StoneTemplate.js +++ /dev/null @@ -1,391 +0,0 @@ -/* eslint-disable max-lines */ -import './Errors/StoneSyntaxError' -import './Errors/StoneCompilerError' - -import './AST' -import './Support/contextualize' -import './Support/nextIndexOf' -import './Support/nextClosingIndexOf' -import './Support/convertTaggedComponents' - -const vm = require('vm') - -export class StoneTemplate { - - compiler = null - - state = { - file: null, - contents: null, - lines: null, - index: 0 - } - - isLayout = false - hasLayoutContext = false - sections = [ ] - - expressions = [ ] - spaceless = 0 - - _template = null - - constructor(compiler, contents, file = null) { - this.compiler = compiler - this.state.contents = contents - this.state.file = file - - const lines = contents.split(/\n/) - const last = lines.length - 1 - let index = 0 - - this.state.lines = lines.map((line, i) => { - const length = line.length + (last === i ? 0 : 1) - - const range = { - start: index, - end: index + length, - code: line, - subsring: contents.substring(index, index + length) - } - - index = range.end - - return range - }) - } - - compile() { - // Strip comments - // TODO: This is going to break source maps - let contents = this.state.contents.trim().replace(/\{\{--([\s\S]+?)--\}\}/g, '') - - // Convert tagged components to regular components - contents = convertTaggedComponents(this.compiler.tags, contents) - - // Parse through the template - contents = contents.substring(this.advance(contents, 0)).trim() - - // If there’s anything left in `contents` after parsing is done - // append is as an output string - if(contents.trim().length > 0) { - this.addOutputExpression(this.state.index, contents) - } - - let code = '' - - // Loop through the expressions and add the code - for(const { type, contents } of this.expressions) { - if(type !== 'code') { - throw new Error('Unsupported type') - } - - code += `${contents.trim()}\n` - } - - // Determine correct return value for the template: - // * For non-layout templates it’s the `output` var - // * For templates that extend a layout, it’s calling the parent layout - let returns = null - - if(!this.isLayout) { - returns = 'output' - } else { - let context = '_' - - if(this.hasLayoutContext) { - // If `@extends` was called with a second context - // parameter, we assign those values over the - // current context - context = 'Object.assign(_, __extendsContext)' - } - - returns = `_.$stone.extends(__templatePathname, __extendsLayout, ${context}, _sections)` - } - - // Wrap the compiled code in a template func with it’s return value - const template = `function template(_, _sections = { }) {\nlet output = '';const __templatePathname = '${this.state.file}';\n${code}\nreturn ${returns};\n}` - - // Contextualize the template so all global vars are prefixed with `_.` - const contextualized = contextualize(template) - - // Take the contextualized template and wrap it in function - // that will be called immediately. This enables us to set - // properties on the template function - let wrapped = `(function() { const t = ${contextualized};` - - if(this.isLayout) { - wrapped += 't.isLayout = true;' - } - - wrapped += 'return t; })()' - - this._template = wrapped - } - - /** - * Parses contents to the next directive - * Recursive and will continue calling itself - * until there are no more directives to parse. - * - * @param string contents Template to parse - * @param number index Current position - * @return number End position - */ - advance(contents, index) { - // Find the next @ index (indicating a directive) that occurs - // outside of an output block - const set = [ '@', '{{', '{!!' ] - let startIndex = index - - while(startIndex >= 0 && startIndex + 1 < contents.length) { - startIndex = nextIndexOf(contents, set, startIndex) - - // Break if we’ve found an @ char or if we’re at - // the end of the road - if(startIndex === -1 || contents[startIndex] !== '{') { - break - } - - if(contents[startIndex + 1] === '{') { - startIndex = nextClosingIndexOf(contents, '{{', '}}', startIndex) - } else { - startIndex = nextClosingIndexOf(contents, '{!!', '!!}', startIndex) - } - } - - if(startIndex === -1) { - // If we haven’t matched anything, we can bail out - return index - } - - const match = contents.substring(startIndex).match(/@(\w+)([ \t]*\()?\n*/) - - if(!match) { - return index - } - - match.index += startIndex - this.state.index = index - - if(match.index > index) { - // If the match starts after 0, it means there’s - // output to display - let string = contents.substring(index, match.index) - - // Only add the output if the string isn’t - // blank to avoid unnecessary whitespace before - // a directive - if(string.trim().length > 0) { - if(this.spaceless > 0) { - string = string.replace(/>\s+<').trim() - } - - this.addOutputExpression(this.state.index, string) - } - - index = match.index - this.state.index = match.index - } - - let args = null - let nextIndex = match.index + match[0].length - - if(match[2]) { - let openCount = -1 - let startIndex = index - let lastIndex = index - - while(openCount !== 0 && (index = nextIndexOf(contents, [ '(', ')' ], index)) >= 0) { - const parenthesis = contents.substring(index, index + 1) - - if(parenthesis === ')') { - openCount-- - } else if(openCount === -1) { - startIndex = index - openCount = 1 - } else { - openCount++ - } - - lastIndex = index - index++ - } - - args = contents.substring(startIndex + 1, lastIndex) - nextIndex = lastIndex + 1 - } - - const result = this.compiler.compileDirective(this, match[1].toLowerCase(), args) - - if(!result.isNil) { - this.expressions.push({ - type: 'code', - contents: result, - index: match.index - }) - } - - if(contents[nextIndex] === '\n') { - nextIndex++ - } - - this.state.index = nextIndex - - return this.advance(contents, nextIndex) - } - - /** - * Adds an output code expression - * - * @param number index Index in the source file this occurs - * @param string output Output to display - */ - addOutputExpression(index, output) { - this.expressions.push({ - type: 'code', - contents: `output += ${this.finalizeOutput(index, output)}\n`, - index: index - }) - } - - /** - * Finalizes an output block by replacing white space - * and converting output tags to placeholders for - * use within template literals - * - * @param {number} sourceIndex Index in the source file this occurs - * @param {string} output Raw output - * @return {string} Finalized output - */ - finalizeOutput(sourceIndex, output) { - const placeholders = { } - let placeholderOrdinal = 0 - - // Store raw blocks - output = output.replace(/@\{\{([\s\S]+?)\}\}/gm, ($0, $1) => { - const placeholder = `@@__stone_placeholder_${++placeholderOrdinal}__@@` - placeholders[placeholder] = `{{${$1}}}` - return placeholder - }) - - // Store regular output blocks - output = output.replace(/\{\{\s*([\s\S]+?)\s*\}\}/gm, ($0, $1) => { - const placeholder = `@@__stone_placeholder_${++placeholderOrdinal}__@@` - placeholders[placeholder] = `\${escape(${$1})}` - return placeholder - }) - - // Store raw output blocks - output = output.replace(/\{!!\s*([\s\S]+?)\s*!!\}/gm, ($0, $1) => { - const placeholder = `@@__stone_placeholder_${++placeholderOrdinal}__@@` - placeholders[placeholder] = `\${${$1}}` - return placeholder - }) - - // Escape escape characters - output = output.replace(/\\/g, '\\\\') - - // Escape backticks - output = output.replace(/`/g, '\\`') - - // Escape whitespace characters - output = output.replace(/[\n]/g, '\\n') - output = output.replace(/[\r]/g, '\\r') - output = output.replace(/[\t]/g, '\\t') - - // Restore placeholders - for(const [ placeholder, content ] of Object.entries(placeholders)) { - // Content is returned as a function to avoid any processing - // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter - output = output.replace(placeholder, () => content) - } - - return this.validateSyntax(`\`${output}\`;`, sourceIndex) - } - - findLineColumn(position) { - let min = 0 - let max = this.state.lines.length - - while(min < max) { - const mid = min + ((max - min) >> 1) - const { start, end } = this.state.lines[mid] - - if(position < start) { - max = mid - } else if(position >= end) { - min = mid + 1 - } else { - return { - line: mid + 1, - column: (position - start) + 1 - } - } - } - - return { line: max, column: 1 } - } - - /** - * Validates the syntax of raw code and optionally - * throws StoneSyntaxError if it’s invalid - * - * @param string code Code to validate - * @param number position Location of this code in the template - * @return string Passed in code - */ - validateSyntax(code, position) { - try { - AST.parse(code) - } catch(err) { - if(err instanceof SyntaxError) { - throw new StoneSyntaxError(this, err, position || this.state.index) - } - - throw err - } - - return code - } - - parseArguments(args, index = this.state.index) { - let tree = null - - try { - tree = AST.parse(`args(${args})`) - } catch(err) { - if(err instanceof SyntaxError) { - throw new StoneSyntaxError(this, err, index) - } - - throw err - } - - if( - tree.body.length > 1 - || tree.body[0].type !== 'ExpressionStatement' - || tree.body[0].expression.type !== 'CallExpression' - || !Array.isArray(tree.body[0].expression.arguments) - || tree.body[0].expression.arguments.length < 1 - ) { - throw new StoneCompilerError(this, 'Unexpected arguments.') - } - - return tree.body[0].expression.arguments - } - - toString() { - if(typeof this._template !== 'string') { - throw new Error('Templates must be compiled first.') - } - - return this._template - } - - toFunction() { - const script = new vm.Script(`(${this.toString()})`, { filename: this.state.file }) - return script.runInNewContext() - } - -} diff --git a/src/Support/contextualize.js b/src/Support/contextualize.js index c6f1280..86daef0 100644 --- a/src/Support/contextualize.js +++ b/src/Support/contextualize.js @@ -1,23 +1,14 @@ -import '../AST' +import '../Stone' /** * Runs through the template code and prefixes * any non-local variables with the context * object. * - * @param {string} code Code for the template + * @param {string} tree Tree to contextualize * @return {string} Contextualized template code */ -export function contextualize(code) { - let tree = null - - try { - tree = AST.parse(code) - } catch(err) { - err._code = code - throw err - } - +export function contextualize(tree) { const scopes = [ { locals: new Set([ @@ -41,7 +32,7 @@ export function contextualize(code) { scope = pushScope(scopes, node) } - AST.walk(tree, { + Stone.walk(tree, { Statement: node => { scope = checkScope(scopes, node) }, @@ -124,8 +115,6 @@ export function contextualize(code) { node.name = `_.${node.name}` } }) - - return AST.stringify(tree) } /** @@ -137,7 +126,7 @@ export function contextualize(code) { * @param {object} node Node to add */ function scopeVariable(scope, node) { - AST.walkVariables(node, node => { + Stone.walkVariables(node, node => { scope.locals.add(node.name) }) } From 6c1702807e44d165b72073d74ba2cf50a54c9c07 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sat, 25 Nov 2017 17:17:17 -0500 Subject: [PATCH 03/37] Parser.parseDirective() will now handle directives with parens but no args --- src/Stone/Parser.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index 1d34b3b..d45c9c8 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -106,21 +106,30 @@ export class Parser { const node = this.startNode() node.directive = directive - if(this.input.charCodeAt(this.pos) === 40) { - this.inDirective = true - this.next() - this.context.push(new acorn.TokContext) + if(this._isCharCode(40)) { + const start = this.pos + this.pos++ + this.skipSpace() + if(this._isCharCode(41)) { + this.pos++ + this.next() + } else { + this.pos = start + this.inDirective = true + this.next() + this.context.push(new acorn.TokContext) - const parseArgs = `${parse}Args` + const parseArgs = `${parse}Args` - if(typeof this[parseArgs] === 'function') { - args = this[parseArgs](node) - } else { - args = this.parseParenExpression() - } + if(typeof this[parseArgs] === 'function') { + args = this[parseArgs](node) + } else { + args = this.parseParenExpression() + } - this.context.pop() - this.inDirective = false + this.context.pop() + this.inDirective = false + } this.pos = this.start // Fixes an issue where output after the directive is cutoff, but feels wrong. } else { this.next() From 361d3de8c3e5804d648e9258703a29154278b602 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 01:01:47 -0500 Subject: [PATCH 04/37] =?UTF-8?q?Fixed=20issues=20in=20Parser.parseDirecti?= =?UTF-8?q?ve()=20where=20it=E2=80=99d=20skip=20whitespace/characters=20af?= =?UTF-8?q?ter=20directives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Stone/Contexts/DirectiveArgs.js | 9 +++++ src/Stone/Contexts/PreserveSpace.js | 30 +++++++++++++++ src/Stone/Parser.js | 58 +++++++++++++++++++++++++---- 3 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 src/Stone/Contexts/DirectiveArgs.js create mode 100644 src/Stone/Contexts/PreserveSpace.js diff --git a/src/Stone/Contexts/DirectiveArgs.js b/src/Stone/Contexts/DirectiveArgs.js new file mode 100644 index 0000000..dee904b --- /dev/null +++ b/src/Stone/Contexts/DirectiveArgs.js @@ -0,0 +1,9 @@ +const { TokContext } = require('acorn') + +export class DirectiveArgs extends TokContext { + + constructor() { + super('@args') + } + +} diff --git a/src/Stone/Contexts/PreserveSpace.js b/src/Stone/Contexts/PreserveSpace.js new file mode 100644 index 0000000..eb73588 --- /dev/null +++ b/src/Stone/Contexts/PreserveSpace.js @@ -0,0 +1,30 @@ +const { TokContext } = require('acorn') + +export class PreserveSpace extends TokContext { + + breakOnSpace = false + breakOnRead = false + + constructor(breakOnSpace = false, breakOnRead = false) { + super('preserveSpace') + + this.preserveSpace = true + this.breakOnSpace = breakOnSpace + this.breakOnRead = breakOnRead + } + + override = p => { + const code = this.breakOnRead ? 32 : (!this.breakOnSpace ? -1 : p.fullCharCodeAtPos()) + + switch(code) { + case 9: // \t + case 10: // \n + case 13: // \r + case 32: // space + return + default: + return p.readToken(p.fullCharCodeAtPos()) + } + } + +} diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index d45c9c8..d23d8e0 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -1,9 +1,12 @@ import './Parsers' + +import './Contexts/DirectiveArgs' +import './Contexts/PreserveSpace' + import './Tokens/StoneOutput' import './Tokens/StoneDirective' const acorn = require('acorn') - const tt = acorn.tokTypes const directiveCodes = new Set( @@ -34,6 +37,21 @@ export class Parser { return result } + expect(next, type) { + if(type === tt.parenR && (this.curContext() instanceof DirectiveArgs)) { + // Awkward workaround so acorn doesn’t + // advance beyond the close parenthesis + // otherwise it tries to eat spaces + // and in some cases the first word + this.context.push(new PreserveSpace(true, true)) + const value = next.call(this, type) + this.context.pop() + return value + } + + return next.call(this, type) + } + skipSpace(next, ...args) { return next.call(this, ...args) } @@ -101,6 +119,10 @@ export class Parser { ++this.pos } + if(directive.length === 0) { + this.unexpected() + } + let args = null const parse = `parse${directive[0].toUpperCase()}${directive.substring(1)}Directive` const node = this.startNode() @@ -117,20 +139,19 @@ export class Parser { this.pos = start this.inDirective = true this.next() - this.context.push(new acorn.TokContext) + this.context.push(new DirectiveArgs) const parseArgs = `${parse}Args` if(typeof this[parseArgs] === 'function') { args = this[parseArgs](node) } else { - args = this.parseParenExpression() + args = this.parseDirectiveArgs() } this.context.pop() this.inDirective = false } - this.pos = this.start // Fixes an issue where output after the directive is cutoff, but feels wrong. } else { this.next() } @@ -142,6 +163,27 @@ export class Parser { return this[parse](node, args) } + parseDirectiveArgs() { + this.expect(tt.parenL) + const val = this.parseExpression() + + // Awkward workaround so acorn doesn’t + // advance beyond the close parenthesis + // otherwise it tries to eat spaces + // and in some cases the first word + this.context.push(new PreserveSpace(true, true)) + this.expect(tt.parenR) + this.context.pop() + + return val + } + + reset() { + this.context.push(new PreserveSpace(true, true)) + this.next() + this.context.pop() + } + parseUntilEndDirective(directives) { if(Array.isArray(directives)) { directives = new Set(directives) @@ -149,6 +191,7 @@ export class Parser { directives = new Set([ directives ]) } + const node = this.startNode() const statements = [ ] contents: for(;;) { @@ -158,7 +201,7 @@ export class Parser { if(node.directive && directives.has(node.directive)) { if(node.type !== 'Directive') { - this.next() + this.reset() } break contents @@ -166,7 +209,7 @@ export class Parser { statements.push(node) } - this.next() + this.reset() break } @@ -196,7 +239,8 @@ export class Parser { } } - return this._createBlockStatement(statements) + node.body = statements + return this.finishNode(node, 'BlockStatement') } _isCharCode(code, delta = 0) { From c3ec8a8c221e19182a7865547a7b9fde7ef82779 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 01:06:23 -0500 Subject: [PATCH 05/37] Replaced contextualize with a new Scoper class and built in support during code generation --- src/Compiler.js | 5 +- src/Stone.js | 2 + src/Stone/Generator.js | 42 ++++++ src/Stone/Scoper.js | 156 +++++++++++++++++++++ src/Stone/Types/StoneOutput.js | 4 + src/Stone/Types/StoneOutputExpression.js | 4 + src/Support/contextualize.js | 170 ----------------------- 7 files changed, 209 insertions(+), 174 deletions(-) create mode 100644 src/Stone/Scoper.js delete mode 100644 src/Support/contextualize.js diff --git a/src/Compiler.js b/src/Compiler.js index bd982c1..3158d1a 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -1,5 +1,4 @@ import './Stone' -import './Support/contextualize' const fs = require('fs') const vm = require('vm') @@ -38,9 +37,7 @@ export class Compiler { let template = null try { - const tree = Stone.parse(contents) - contextualize(tree) - template = Stone.stringify(tree) + template = Stone.stringify(Stone.parse(contents)) } catch(err) { if(!err._hasTemplate) { err._hasTemplate = true diff --git a/src/Stone.js b/src/Stone.js index 75532a8..b21d5d5 100644 --- a/src/Stone.js +++ b/src/Stone.js @@ -1,5 +1,6 @@ import './Stone/Generator' import './Stone/Parser' +import './Stone/Scoper' import './Stone/Walker' const acorn = require('acorn5-object-spread/inject')(require('acorn')) @@ -44,6 +45,7 @@ export class Stone { } static stringify(tree) { + Scoper.scope(tree) return astring(tree, { generator: Generator }) } diff --git a/src/Stone/Generator.js b/src/Stone/Generator.js index b255435..428d85c 100644 --- a/src/Stone/Generator.js +++ b/src/Stone/Generator.js @@ -14,11 +14,53 @@ export const Generator = { return } + if(!node.key.isNil && node.key.type === 'Identifier') { + node.key = { + ...node.key, + isScoped: true + } + + node.shorthand = false + } + return baseGenerator.Property.call(this, node, state) + }, + + Identifier(node, state) { + if(node.isScoped || (!state.scope.isNil && state.scope.has(node.name))) { + state.write(node.name, node) + } else { + state.write(`_.${node.name}`, node) + } + }, + + MemberExpression(node, state) { + node.property.isScoped = true + return baseGenerator.MemberExpression.call(this, node, state) } } +for(const key of [ + 'Program', + 'BlockStatement', + 'FunctionDeclaration', + 'ForStatement', + 'ForOfStatement', + 'ForInStatement', + 'WhileStatement', + 'FunctionExpression', + 'ArrowFunctionExpression' +]) { + Generator[key] = function(node, state) { + const oldScope = state.scope + state.scope = node.scope + const value = baseGenerator[key].call(this, node, state) + state.scope = oldScope + return value + } +} + for(const key of Object.keys(Types)) { Generator[key] = Types[key].generate.bind(Generator) } diff --git a/src/Stone/Scoper.js b/src/Stone/Scoper.js new file mode 100644 index 0000000..1072b0e --- /dev/null +++ b/src/Stone/Scoper.js @@ -0,0 +1,156 @@ +import './Types' + +export class Scoper { + + static defaultScope = new Set([ + '_', + '_sections', + 'Object', + 'Set', + 'Date', + 'Array', + 'String', + 'global', + 'process' + ]) + + static scope(node) { + return this._scope(node, this.defaultScope) + } + + static _scope(node, scope, force = false) { + if(typeof this[node.type] !== 'function') { + return + } + + return this[node.type](node, scope, force) + } + + // Handlers + + static _bodyStatement(node, declarations, scope) { + node.scope = new Set(scope) + + if(!declarations.isNil) { + this._scope(declarations, node.scope) + } + + return this._scope(node.body, node.scope) + } + + static BlockStatement(node, scope) { + node.scope = new Set(scope) + + for(const statement of node.body) { + this._scope(statement, node.scope) + } + } + + static Program = Scoper.BlockStatement + + static FunctionDeclaration(node, scope) { + node.scope = new Set(scope) + + if(Array.isArray(node.params)) { + for(const param of node.params) { + this._scope(param, node.scope, true) + } + } + + return this._scope(node.body, node.scope) + } + + static FunctionExpression = Scoper.FunctionDeclaration + static ArrowFunctionExpression = Scoper.FunctionDeclaration + + static CallExpression(node, scope) { + if(!Array.isArray(node.arguments)) { + return + } + + for(const argument of node.arguments) { + this._scope(argument, scope) + } + + return this._scope(node.callee, scope) + } + + static MemberExpression(node, scope) { + return this._scope(node.object, scope) + } + + static TemplateLiteral(node, scope) { + if(!Array.isArray(node.expressions)) { + return + } + + for(const expression of node.expressions) { + this._scope(expression, scope) + } + } + + static ForStatement(node, scope) { + return this._bodyStatement(node, node.init, scope) + } + + static ForOfStatement(node, scope) { + return this._bodyStatement(node, node.left, scope) + } + + static ForInStatement(node, scope) { + return this._bodyStatement(node, node.left, scope) + } + + static WhileStatement(node, scope) { + return this._bodyStatement(node, null, scope) + } + + static AssignmentExpression(node, scope) { + this._scope(node.left, scope) + } + + static VariableDeclaration(node, scope) { + for(const declaration of node.declarations) { + this._scope(declaration, scope) + } + } + + static VariableDeclarator(node, scope) { + this._scope(node.id, scope, true) + } + + static ArrayPattern(node, scope, force) { + for(const element of node.elements) { + this._scope(element, scope, force) + } + } + + static ObjectPattern(node, scope, force) { + for(const property of node.properties) { + this._scope(property, scope, force) + } + } + + static Property(node, scope, force) { + this._scope(node.value, scope, force) + } + + static Identifier(node, scope, force) { + if(!force) { + return + } + + scope.add(node.name) + } + +} + +for(const key of Object.keys(Types)) { + const scope = Types[key].scope + + if(typeof scope !== 'function') { + continue + } + + Scoper[key] = scope.bind(Scoper) +} diff --git a/src/Stone/Types/StoneOutput.js b/src/Stone/Types/StoneOutput.js index aebbb72..a219dd2 100644 --- a/src/Stone/Types/StoneOutput.js +++ b/src/Stone/Types/StoneOutput.js @@ -7,3 +7,7 @@ export function generate({ output }, state) { export function walk({ output }, st, c) { c(output, st, 'Expression') } + +export function scope({ output }, scope) { + return this._scope(output, scope) +} diff --git a/src/Stone/Types/StoneOutputExpression.js b/src/Stone/Types/StoneOutputExpression.js index fa7d506..2151f81 100644 --- a/src/Stone/Types/StoneOutputExpression.js +++ b/src/Stone/Types/StoneOutputExpression.js @@ -13,3 +13,7 @@ export function generate({ safe = true, value }, state) { export function walk({ value }, st, c) { c(value, st, 'Expression') } + +export function scope({ value }, scope) { + return this._scope(value, scope) +} diff --git a/src/Support/contextualize.js b/src/Support/contextualize.js deleted file mode 100644 index 86daef0..0000000 --- a/src/Support/contextualize.js +++ /dev/null @@ -1,170 +0,0 @@ -import '../Stone' - -/** - * Runs through the template code and prefixes - * any non-local variables with the context - * object. - * - * @param {string} tree Tree to contextualize - * @return {string} Contextualized template code - */ -export function contextualize(tree) { - const scopes = [ - { - locals: new Set([ - '_', - '_sections', - 'Object', - 'Set', - 'Date', - 'Array', - 'String', - 'global', - 'process' - ]), - end: Number.MAX_VALUE - } - ] - - let scope = scopes[0] - - const processStatement = node => { - scope = pushScope(scopes, node) - } - - Stone.walk(tree, { - Statement: node => { - scope = checkScope(scopes, node) - }, - - BlockStatement: processStatement, - ForStatement: processStatement, - ForOfStatement: processStatement, - WhileStatement: processStatement, - - ArrowFunctionExpression: node => { - scope = pushScope(scopes, node) - - for(const parameter of node.params) { - scopeVariable(scope, parameter) - } - }, - - FunctionExpression: node => { - scope = pushScope(scopes, node) - - for(const parameter of node.params) { - scopeVariable(scope, parameter) - } - }, - - AssignmentExpression: node => { - if(node.left.name.isNil) { - return - } - - if(node.left.name.substring(0, 13) !== '__auto_scope_') { - return - } - - node.left.name = node.left.name.substring(13) - - if(!scope.locals.has(node.left.name)) { - node.left.name = `_.${node.left.name}` - } - }, - - VariableDeclarator: node => { - scope = checkScope(scopes, node) - - scopeVariable(scope, node.id) - }, - - ObjectExpression: node => { - for(const property of node.properties) { - if(property.shorthand !== true) { - continue - } - - property.shorthand = false - property.key = new property.key.constructor({ options: { } }) - property.key.shouldntContextualize = true - Object.assign(property.key, property.value) - - if(property.key.name.startsWith('_.')) { - property.key.name = property.key.name.substring(2) - } - } - }, - - RestElement: node => { - node.name = `_.${node.name}` - }, - - Identifier: node => { - if(node.name.substring(0, 13) === '__auto_scope_') { - node.name = node.name.substring(13) - } - - scope = checkScope(scopes, node) - - if(node.shouldntContextualize || scope.locals.has(node.name)) { - return - } - - node.name = `_.${node.name}` - } - }) -} - -/** - * Walks through each variable in the node, - * including destructured, and adds them to - * the current scope - * - * @param {object} scope Scope to add through - * @param {object} node Node to add - */ -function scopeVariable(scope, node) { - Stone.walkVariables(node, node => { - scope.locals.add(node.name) - }) -} - -/** - * Checks if the current scope is still active - * - * @param {[object]} scopes Stack of scopes - * @param {object} fromNode Node that’s checking scope - * @return {object} Current scaope - */ -function checkScope(scopes, fromNode) { - let scope = scopes[scopes.length - 1] - - while(fromNode.start >= scope.end && scopes.length > 1) { - scopes.pop() - scope = scopes[scopes.length - 1] - } - - return scope -} - -/** - * Pushes a new scope on stack - * - * @param {[object]} scopes Stack of scopes - * @param {object} node Node that’s creating this scope - * @return {object} New scope - */ -function pushScope(scopes, node) { - checkScope(scopes, node) - - const scope = { - locals: new Set(scopes[scopes.length - 1].locals), - node: node, - end: node.end - } - - scopes.push(scope) - return scope -} From d01497cb5d596f00db37442cadcd5a245d42cda5 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 01:07:53 -0500 Subject: [PATCH 06/37] Fixed bug in `parseDumpDirective` --- src/Stone/Parsers/Output.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Stone/Parsers/Output.js b/src/Stone/Parsers/Output.js index 09b16c6..bfde62b 100644 --- a/src/Stone/Parsers/Output.js +++ b/src/Stone/Parsers/Output.js @@ -12,6 +12,7 @@ const { tokTypes: tt } = require('acorn') */ export function parseDumpDirective(node, value) { node.value = value + this.next() return this.finishNode(node, 'StoneDump') } From 3d1188db59f405ac86c70b579dcb96e71a3289e2 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 01:12:30 -0500 Subject: [PATCH 07/37] Rebuilt loops and conditionals on acorn --- src/Compiler/Conditionals.js | 26 ----- src/Compiler/Loops.js | 116 -------------------- src/Stone/Parsers/Conditionals.js | 76 +++++++++++++ src/Stone/Parsers/Loops.js | 128 ++++++++++++++++++++++ src/Stone/Parsers/index.js | 2 + src/Stone/Types/StoneLoop.js | 82 ++++++++++++++ src/Stone/Types/index.js | 1 + test/views/control-structures/while.html | 1 + test/views/control-structures/while.stone | 4 + 9 files changed, 294 insertions(+), 142 deletions(-) delete mode 100644 src/Compiler/Conditionals.js delete mode 100644 src/Compiler/Loops.js create mode 100644 src/Stone/Parsers/Conditionals.js create mode 100644 src/Stone/Parsers/Loops.js create mode 100644 src/Stone/Types/StoneLoop.js create mode 100644 test/views/control-structures/while.html create mode 100644 test/views/control-structures/while.stone diff --git a/src/Compiler/Conditionals.js b/src/Compiler/Conditionals.js deleted file mode 100644 index 449d5bd..0000000 --- a/src/Compiler/Conditionals.js +++ /dev/null @@ -1,26 +0,0 @@ -export function compileIf(context, condition) { - context.validateSyntax(condition) - return `if(${condition}) {` -} - -export function compileElseif(context, condition) { - context.validateSyntax(condition) - return `} else if(${condition}) {` -} - -export function compileElse() { - return '} else {' -} - -export function compileEndif(context) { - return this.compileEnd(context) -} - -export function compileUnless(context, condition) { - context.validateSyntax(condition) - return `if(!${condition}) {` -} - -export function compileEndunless(context) { - return this.compileEnd(context) -} diff --git a/src/Compiler/Loops.js b/src/Compiler/Loops.js deleted file mode 100644 index 1635690..0000000 --- a/src/Compiler/Loops.js +++ /dev/null @@ -1,116 +0,0 @@ -import '../AST' -import '../Errors/StoneSyntaxError' - -export function compileFor(context, args) { - context.loopStack = context.loopStack || [ ] - - args = `for(${args}) {` - let tree = null - - try { - tree = AST.parse(`${args} }`) - } catch(err) { - if(err instanceof SyntaxError) { - throw new StoneSyntaxError(context, err, context.state.index) - } - - throw err - } - - if(tree.body.length > 1 || (tree.body[0].type !== 'ForInStatement' && tree.body[0].type !== 'ForOfStatement')) { - context.loopStack.push(false) - return args - } - - const node = tree.body[0] - const lhs = AST.stringify(node.left).trim().replace(/;$/, '') - let rhs = AST.stringify(node.right).trim().replace(/;$/, '') - - if(node.type === 'ForInStatement') { - rhs = `new StoneLoop(Object.keys(${rhs}))` - } else { - rhs = `new StoneLoop(${rhs})` - } - - context.loops = context.loops || 0 - context.loopVariableStack = context.loopVariableStack || [ ] - - const loopVariable = `__loop${context.loops++}` - context.loopVariableStack.push(loopVariable) - context.loopStack.push(true) - - let code = `const ${loopVariable} = ${rhs};\n` - code += `${loopVariable}.depth = ${context.loopVariableStack.length};\n` - - if(context.loopStack.length > 1) { - code += `${loopVariable}.parent = ${context.loopVariableStack[context.loopVariableStack.length - 2]};\n` - } - - code += `for(${lhs} of ${loopVariable}) {\n` - code += `\tconst loop = ${loopVariable};` - - return code -} - -export function compileForeach(context, args) { - // No difference between for and foreach - // Included for consistency with Blade - return this.compileFor(context, args) -} - -export function compileEndfor(context) { - if(context.loopStack.pop()) { - context.loopVariableStack.pop() - } - - return this.compileEnd(context) -} - -export function compileEndforeach(context) { - // No difference between for and foreach - // Included for consistency with Blade - return this.compileEnd(context) -} - -/** - * Generate continue code that optionally has a condition - * associated with it. - * - * @param {object} context Context for the compilation - * @param {string} condition Optional condition to continue on - * @return {string} Code to continue - */ -export function compileContinue(context, condition) { - if(condition.isNil) { - return 'continue;' - } - - context.validateSyntax(condition) - return `if(${condition}) { continue; }` -} - -/** - * Generate break code that optionally has a condition - * associated with it. - * - * @param {object} context Context for the compilation - * @param {string} condition Optional condition to break on - * @return {string} Code to break - */ -export function compileBreak(context, condition) { - if(condition.isNil) { - return 'break;' - } - - context.validateSyntax(condition) - return `if(${condition}) { break; }` -} - -export function compileWhile(context, condition) { - context.validateSyntax(condition) - return `while(${condition}) {` -} - -export function compileEndwhile(context) { - return this.compileEnd(context) -} diff --git a/src/Stone/Parsers/Conditionals.js b/src/Stone/Parsers/Conditionals.js new file mode 100644 index 0000000..ae16511 --- /dev/null +++ b/src/Stone/Parsers/Conditionals.js @@ -0,0 +1,76 @@ +const endDirectives = [ 'endif', 'elseif', 'else' ] + +export function parseIfDirective(node, args) { + (this._currentIf = (this._currentIf || [ ])).push(node) + node.test = args + node.consequent = this.parseUntilEndDirective(endDirectives) + return this.finishNode(node, 'IfStatement') +} + +export function parseElseifDirective(node, args) { + if(!this._currentIf || this._currentIf.length === 0) { + this.raise(this.start, '`@elseif` outside of `@if`') + } + + const level = this._currentIf.length - 1 + + if(this._currentIf[level].alternate) { + this.raise(this.start, '`@elseif` after `@else`') + } + + this._currentIf[level].alternate = node + this._currentIf[level] = node + node.test = args + node.consequent = this.parseUntilEndDirective(endDirectives) + return this.finishNode(node, 'IfStatement') +} + +export function parseElseDirective(node) { + if(!this._currentIf || this._currentIf.length === 0) { + this.raise(this.start, '`@else` outside of `@if`') + } + + const level = this._currentIf.length - 1 + + if(this._currentIf[level].alternate) { + this.raise(this.start, '`@else` after `@else`') + } + + this._currentIf[level].alternate = true + this._currentIf[level].alternate = this.parseUntilEndDirective(endDirectives) + return this.finishNode(node, 'BlockStatement') +} + +export function parseEndifDirective(node) { + if(!this._currentIf || this._currentIf.length === 0) { + this.raise(this.start, '`@endif` outside of `@if`') + } + + this._currentIf.pop() + + return this.finishNode(node, 'Directive') +} + +export function parseUnlessDirective(node, args) { + (this._currentUnless = (this._currentUneless || [ ])).push(node) + + const unary = this.startNode() + unary.operator = '!' + unary.prefix = true + unary.argument = args + this.finishNode(unary, 'UnaryExpression') + + node.test = unary + node.consequent = this.parseUntilEndDirective('endunless') + return this.finishNode(node, 'IfStatement') +} + +export function parseEndunlessDirective(node) { + if(!this._currentUnless || this._currentUnless.length === 0) { + this.raise(this.start, '`@endunless` outside of `@unless`') + } + + this._currentUnless.pop() + + return this.finishNode(node, 'Directive') +} diff --git a/src/Stone/Parsers/Loops.js b/src/Stone/Parsers/Loops.js new file mode 100644 index 0000000..e6147e2 --- /dev/null +++ b/src/Stone/Parsers/Loops.js @@ -0,0 +1,128 @@ +export function parseForDirectiveArgs(node) { + this.pos-- + this.parseForStatement(node) + + return null +} + +export function parseForDirective(node, args, until = 'endfor') { + const loop = node + + if(node.type === 'ForOfStatement' || node.type === 'ForInStatement') { + node = this.startNode() + node.type = 'StoneLoop' + node.loop = loop + } + + (this._currentFor = (this._currentFor || [ ])).push(node) + loop.body = this.parseUntilEndDirective(until) + return this.finishNode(node, node.type) +} + +export function parseEndforDirective(node) { + if(!this._currentFor || this._currentFor.length === 0) { + if(node.directive === 'endforeach') { + this.raise(this.start, '`@endforeach` outside of `@foreach`') + } else { + this.raise(this.start, '`@endfor` outside of `@for`') + } + } + + const open = this._currentFor.pop() + + if(open.directive === 'for' && node.directive === 'endforeach') { + this.raise(this.start, '`@endfor` must be used with `@for`') + } else if(open.directive === 'foreach' && node.directive === 'endfor') { + this.raise(this.start, '`@endforeach` must be used with `@foreach`') + } + + return this.finishNode(node, 'Directive') +} + +export function parseForeachDirective(node, args) { + // No difference between for and foreach + // Included for consistency with Blade + return this.parseForDirective(node, args, 'endforeach') +} + +export const parseForeachDirectiveArgs = parseForDirectiveArgs +export const parseEndforeachDirective = parseEndforDirective + +/** + * Generate continue node that optionally has a condition + * associated with it. + * + * @param {object} node Blank node + * @param {mixed} condition Optional condition to continue on + * @return {object} Finished node + */ +export function parseContinueDirective(node, condition) { + if( + (!this._currentWhile || this._currentWhile.length === 0) + && (!this._currentFor || this._currentFor.length === 0) + ) { + this.raise(this.start, '`@continue` outside of `@for` or `@while`') + } + + if(condition.isNil) { + return this.finishNode(node, 'ContinueStatement') + } + + const block = this.startNode() + block.body = [ this.finishNode(this.startNode(), 'ContinueStatement') ] + + node.test = condition + node.consequent = this.finishNode(block, 'ContinueStatement') + return this.finishNode(node, 'IfStatement') +} + +/** + * Generate break node that optionally has a condition + * associated with it. + * + * @param {object} node Blank node + * @param {mixed} condition Optional condition to break on + * @return {object} Finished node + */ +export function parseBreakDirective(node, condition) { + if( + (!this._currentWhile || this._currentWhile.length === 0) + && (!this._currentFor || this._currentFor.length === 0) + ) { + this.raise(this.start, '`@break` outside of `@for` or `@while`') + } + + if(condition.isNil) { + return this.finishNode(node, 'BreakStatement') + } + + const block = this.startNode() + block.body = [ this.finishNode(this.startNode(), 'BreakStatement') ] + + node.test = condition + node.consequent = this.finishNode(block, 'BlockStatement') + return this.finishNode(node, 'IfStatement') +} + +export function parseWhileDirectiveArgs(node) { + this.pos-- + this.parseWhileStatement(node) + + return null +} + +export function parseWhileDirective(node) { + (this._currentWhile = (this._currentWhile || [ ])).push(node) + node.body = this.parseUntilEndDirective('endwhile') + return this.finishNode(node, node.type) +} + +export function parseEndwhileDirective(node) { + if(!this._currentWhile || this._currentWhile.length === 0) { + this.raise(this.start, '`@endwhile` outside of `@while`') + } + + this._currentWhile.pop() + + return this.finishNode(node, 'Directive') +} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index 13678f8..54fbe48 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -1,3 +1,5 @@ export const Parsers = { + ...require('./Conditionals'), ...require('./Output'), + ...require('./Loops'), } diff --git a/src/Stone/Types/StoneLoop.js b/src/Stone/Types/StoneLoop.js new file mode 100644 index 0000000..d554b53 --- /dev/null +++ b/src/Stone/Types/StoneLoop.js @@ -0,0 +1,82 @@ +export function generate({ loop }, state) { + // TODO: Future optimizations should check if + // the `loop` var is used before injecting + // support for it. + + state.__loops = (state.__loops || 0) + 1 + const loopVariable = `__loop${state.__loops}` + loop.scope.add(loopVariable) + loop.body.scope.add(loopVariable) + loop.body.scope.add('loop') + + state.write(`const ${loopVariable} = new _.StoneLoop(`) + + if(loop.type === 'ForInStatement') { + state.write('Object.keys(') + } + + this[loop.right.type](loop.right, state) + + if(loop.type === 'ForInStatement') { + state.write(')') + } + + state.write(');') + state.write(state.lineEnd) + state.write(state.indent) + + state.write(`${loopVariable}.depth = ${state.__loops};`) + state.write(state.lineEnd) + state.write(state.indent) + + if(state.__loops > 1) { + state.write(`${loopVariable}.parent = __loop${state.__loops - 1};`) + state.write(state.lineEnd) + state.write(state.indent) + } + + const positions = { + start: loop.body.start, + end: loop.body.end + } + + loop.body.body.unshift({ + ...positions, + type: 'VariableDeclaration', + declarations: [ + { + ...positions, + type: 'VariableDeclarator', + id: { + ...positions, + type: 'Identifier', + name: 'loop' + }, + init: { + ...positions, + type: 'Identifier', + name: loopVariable + } + } + ], + kind: 'const' + }) + + this.ForOfStatement({ + ...loop, + type: 'ForOfStatement', + right: { + ...loop.right, + type: 'Identifier', + name: loopVariable + } + }, state) +} + +export function walk({ loop }, st, c) { + c(loop, st, 'Expression') +} + +export function scope(node, scope) { + return this._scope(node.loop, scope) +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index ed9e803..a641f3b 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -3,4 +3,5 @@ export const Types = { StoneEmptyExpression: require('./StoneEmptyExpression'), StoneOutput: require('./StoneOutput'), StoneOutputExpression: require('./StoneOutputExpression'), + StoneLoop: require('./StoneLoop'), } diff --git a/test/views/control-structures/while.html b/test/views/control-structures/while.html new file mode 100644 index 0000000..8531e99 --- /dev/null +++ b/test/views/control-structures/while.html @@ -0,0 +1 @@ +

Looping just once.

diff --git a/test/views/control-structures/while.stone b/test/views/control-structures/while.stone new file mode 100644 index 0000000..9024d0c --- /dev/null +++ b/test/views/control-structures/while.stone @@ -0,0 +1,4 @@ +@while(true) +

Looping just once.

+@break +@endwhile From 7f112864c238340128d3cde5282832a8c6c5c866 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 03:14:27 -0500 Subject: [PATCH 08/37] Updated Generator to add pushScope/popScope to state --- src/Stone/Generator.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Stone/Generator.js b/src/Stone/Generator.js index 428d85c..8dbc257 100644 --- a/src/Stone/Generator.js +++ b/src/Stone/Generator.js @@ -6,6 +6,23 @@ export const Generator = { ...baseGenerator, + Program(node, state) { + state._scopes = [ ] + state.pushScope = function(scope) { + this._scopes.push(this.scope) + this.scope = scope + }.bind(state) + + state.popScope = function() { + this.scope = this._scopes.pop() + }.bind(state) + + state.pushScope(node.scope) + const value = baseGenerator.Program.call(this, node, state) + state.popScope() + return value + }, + Property(node, state) { if(node.type === 'SpreadElement') { state.write('...(') @@ -42,7 +59,6 @@ export const Generator = { } for(const key of [ - 'Program', 'BlockStatement', 'FunctionDeclaration', 'ForStatement', @@ -53,10 +69,9 @@ for(const key of [ 'ArrowFunctionExpression' ]) { Generator[key] = function(node, state) { - const oldScope = state.scope - state.scope = node.scope + state.pushScope(node.scope) const value = baseGenerator[key].call(this, node, state) - state.scope = oldScope + state.popScope() return value } } From 7fc9cc7af3701e6caae2044fb0f5bc6c6debd50a Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 03:16:00 -0500 Subject: [PATCH 09/37] Rebuilt macros on acorn --- src/Compiler/Macros.js | 18 ------- src/Stone/Parsers/Macros.js | 30 ++++++++++++ src/Stone/Parsers/index.js | 1 + src/Stone/Scoper.js | 4 ++ src/Stone/Types/StoneMacro.js | 92 +++++++++++++++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 6 files changed, 128 insertions(+), 18 deletions(-) delete mode 100644 src/Compiler/Macros.js create mode 100644 src/Stone/Parsers/Macros.js create mode 100644 src/Stone/Types/StoneMacro.js diff --git a/src/Compiler/Macros.js b/src/Compiler/Macros.js deleted file mode 100644 index 60c3832..0000000 --- a/src/Compiler/Macros.js +++ /dev/null @@ -1,18 +0,0 @@ -import '../AST' - -export function compileMacro(context, args) { - args = context.parseArguments(args) - - const name = AST.stringify(args.shift()) - args = args.map(arg => AST.stringify(arg)).join(', ') - - let code = `_[${name}] = function(${args}) {` - code += '\n_ = Object.assign({ }, _);' - code += '\nlet output = \'\';' - - return code -} - -export function compileEndmacro() { - return 'return new HtmlString(output);\n};' -} diff --git a/src/Stone/Parsers/Macros.js b/src/Stone/Parsers/Macros.js new file mode 100644 index 0000000..50658eb --- /dev/null +++ b/src/Stone/Parsers/Macros.js @@ -0,0 +1,30 @@ +export function parseMacroDirective(node, args) { + (this._currentMacro = (this._currentMacro || [ ])).push(node) + + if(args.type === 'SequenceExpression') { + node.id = args.expressions.shift() + node.params = args.expressions.map(expression => { + if(expression.type === 'AssignmentExpression') { + expression.type = 'AssignmentPattern' + } + + return expression + }) + } else { + node.id = args + node.params = [ ] + } + + node.body = this.parseUntilEndDirective('endmacro') + return this.finishNode(node, 'StoneMacro') +} + +export function parseEndmacroDirective(node) { + if(!this._currentMacro || this._currentMacro.length === 0) { + this.raise(this.start, '`@endmacro` outside of `@macro`') + } + + this._currentMacro.pop() + + return this.finishNode(node, 'Directive') +} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index 54fbe48..e7d4544 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -2,4 +2,5 @@ export const Parsers = { ...require('./Conditionals'), ...require('./Output'), ...require('./Loops'), + ...require('./Macros'), } diff --git a/src/Stone/Scoper.js b/src/Stone/Scoper.js index 1072b0e..dcf02ca 100644 --- a/src/Stone/Scoper.js +++ b/src/Stone/Scoper.js @@ -119,6 +119,10 @@ export class Scoper { this._scope(node.id, scope, true) } + static AssignmentPattern(node, scope, force) { + this._scope(node.left, scope, force) + } + static ArrayPattern(node, scope, force) { for(const element of node.elements) { this._scope(element, scope, force) diff --git a/src/Stone/Types/StoneMacro.js b/src/Stone/Types/StoneMacro.js new file mode 100644 index 0000000..c0b07ef --- /dev/null +++ b/src/Stone/Types/StoneMacro.js @@ -0,0 +1,92 @@ +export function generate(node, state) { + state.pushScope(node.scope) + state.write('_[') + this[node.id.type](node.id, state) + state.write('] = function') + this.SequenceExpression({ expressions: node.params }, state) + state.write(' ') + + node.body.body.unshift({ + // let output = '' (rescopes output) + type: 'VariableDeclaration', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'output' + }, + init: { + type: 'Literal', + value: '\'\'', + raw: '\'\'', + } + } + ], + kind: 'let' + }, { + // _ = { ..._ } (rescopes context) + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: '_' + }, + right: { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + type: 'Identifier', + name: '_' + } + } + ] + } + } + }) + + // return new HtmlString(output) + node.body.body.push({ + type: 'ReturnStatement', + argument: { + type: 'NewExpression', + callee: { + type: 'Identifier', + name: 'HtmlString' + }, + arguments: [ + { + type: 'Identifier', + name: 'output' + } + ] + } + }) + + this[node.body.type](node.body, state) + state.popScope() +} + +export function walk(node, st, c) { + c(node.id, st, 'Pattern') + + for(const param of node.params) { + c(param, st, 'Pattern') + } + + c(node.body, st, 'ScopeBody') +} + +export function scope(node, scope) { + node.scope = new Set(scope) + + for(const param of node.params) { + this._scope(param, node.scope, true) + } + + this._scope(node.body, node.scope) +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index a641f3b..ce0f674 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -4,4 +4,5 @@ export const Types = { StoneOutput: require('./StoneOutput'), StoneOutputExpression: require('./StoneOutputExpression'), StoneLoop: require('./StoneLoop'), + StoneMacro: require('./StoneMacro'), } From c9088d46c35da036af13bda751f003085b4a1ff5 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 16:29:29 -0500 Subject: [PATCH 10/37] Abstracted logic out of StoneMacro into StoneOutputBlock --- src/Stone/Parser.js | 16 ++++ src/Stone/Parsers/Macros.js | 24 +++--- src/Stone/Types/StoneMacro.js | 87 ++------------------- src/Stone/Types/StoneOutputBlock.js | 112 ++++++++++++++++++++++++++++ src/Stone/Types/index.js | 5 +- 5 files changed, 147 insertions(+), 97 deletions(-) create mode 100644 src/Stone/Types/StoneOutputBlock.js diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index d23d8e0..36ba622 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -247,6 +247,22 @@ export class Parser { return this.input.charCodeAt(this.pos + delta) === code } + _flattenArgs(args) { + if(args.isNil) { + return [ ] + } else if(args.type === 'SequenceExpression') { + return args.expressions.map(expression => { + if(expression.type === 'AssignmentExpression') { + expression.type = 'AssignmentPattern' + } + + return expression + }) + } + + return [ args ] + } + _createIdentifier(identifier) { const node = new acorn.Node(this) node.type = 'Identifier' diff --git a/src/Stone/Parsers/Macros.js b/src/Stone/Parsers/Macros.js index 50658eb..e7c30f5 100644 --- a/src/Stone/Parsers/Macros.js +++ b/src/Stone/Parsers/Macros.js @@ -1,21 +1,19 @@ export function parseMacroDirective(node, args) { (this._currentMacro = (this._currentMacro || [ ])).push(node) + args = this._flattenArgs(args) - if(args.type === 'SequenceExpression') { - node.id = args.expressions.shift() - node.params = args.expressions.map(expression => { - if(expression.type === 'AssignmentExpression') { - expression.type = 'AssignmentPattern' - } - - return expression - }) - } else { - node.id = args - node.params = [ ] + if(args.length === 0) { + this.raise(this.start, '`@macro` must contain at least 1 argument') } - node.body = this.parseUntilEndDirective('endmacro') + node.id = args.shift() + + const output = this.startNode() + output.rescopeContext = true + output.params = args + output.body = this.parseUntilEndDirective('endmacro') + + node.output = this.finishNode(output, 'StoneOutputBlock') return this.finishNode(node, 'StoneMacro') } diff --git a/src/Stone/Types/StoneMacro.js b/src/Stone/Types/StoneMacro.js index c0b07ef..bff9e0a 100644 --- a/src/Stone/Types/StoneMacro.js +++ b/src/Stone/Types/StoneMacro.js @@ -1,92 +1,15 @@ export function generate(node, state) { - state.pushScope(node.scope) state.write('_[') this[node.id.type](node.id, state) - state.write('] = function') - this.SequenceExpression({ expressions: node.params }, state) - state.write(' ') - - node.body.body.unshift({ - // let output = '' (rescopes output) - type: 'VariableDeclaration', - declarations: [ - { - type: 'VariableDeclarator', - id: { - type: 'Identifier', - name: 'output' - }, - init: { - type: 'Literal', - value: '\'\'', - raw: '\'\'', - } - } - ], - kind: 'let' - }, { - // _ = { ..._ } (rescopes context) - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'Identifier', - name: '_' - }, - right: { - type: 'ObjectExpression', - properties: [ - { - type: 'SpreadElement', - argument: { - type: 'Identifier', - name: '_' - } - } - ] - } - } - }) - - // return new HtmlString(output) - node.body.body.push({ - type: 'ReturnStatement', - argument: { - type: 'NewExpression', - callee: { - type: 'Identifier', - name: 'HtmlString' - }, - arguments: [ - { - type: 'Identifier', - name: 'output' - } - ] - } - }) - - this[node.body.type](node.body, state) - state.popScope() + state.write('] = ') + return this[node.output.type](node.output, state) } export function walk(node, st, c) { c(node.id, st, 'Pattern') - - for(const param of node.params) { - c(param, st, 'Pattern') - } - - c(node.body, st, 'ScopeBody') + c(node.output, st, 'Expression') } -export function scope(node, scope) { - node.scope = new Set(scope) - - for(const param of node.params) { - this._scope(param, node.scope, true) - } - - this._scope(node.body, node.scope) +export function scope({ output }, scope) { + this._scope(output, scope) } diff --git a/src/Stone/Types/StoneOutputBlock.js b/src/Stone/Types/StoneOutputBlock.js new file mode 100644 index 0000000..2baaae7 --- /dev/null +++ b/src/Stone/Types/StoneOutputBlock.js @@ -0,0 +1,112 @@ +export function generate(node, state) { + state.pushScope(node.scope) + state.write('function') + + if(!node.id.isNil) { + state.write(' ') + node.id.isScoped = true + this[node.id.type](node.id, state) + } + + this.SequenceExpression({ expressions: node.params || [ ] }, state) + state.write(' ') + + if(node.rescopeContext) { + // _ = { ..._ } + node.body.body.unshift({ + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: '_' + }, + right: { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + type: 'Identifier', + name: '_' + } + } + ] + } + } + }) + } + + // let output = '' + node.body.body.unshift({ + type: 'VariableDeclaration', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'output' + }, + init: { + type: 'Literal', + value: '\'\'', + raw: '\'\'', + } + } + ], + kind: 'let' + }) + + if(node.returnRaw) { + // return output + node.body.body.push({ + type: 'ReturnStatement', + argument: { + type: 'Identifier', + name: 'output' + } + }) + } else { + // return new HtmlString(output) + node.body.body.push({ + type: 'ReturnStatement', + argument: { + type: 'NewExpression', + callee: { + type: 'Identifier', + name: 'HtmlString' + }, + arguments: [ + { + type: 'Identifier', + name: 'output' + } + ] + } + }) + } + + this[node.body.type](node.body, state) + state.popScope() +} + +export function walk(node, st, c) { + for(const param of node.params) { + c(param, st, 'Pattern') + } + + c(node.body, st, 'ScopeBody') +} + +export function scope(node, scope) { + node.scope = new Set(scope) + + if(Array.isArray(node.params)) { + for(const param of node.params) { + this._scope(param, node.scope, true) + } + } + + this._scope(node.body, node.scope) +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index ce0f674..fc5aa84 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -1,8 +1,9 @@ export const Types = { StoneDump: require('./StoneDump'), StoneEmptyExpression: require('./StoneEmptyExpression'), - StoneOutput: require('./StoneOutput'), - StoneOutputExpression: require('./StoneOutputExpression'), StoneLoop: require('./StoneLoop'), StoneMacro: require('./StoneMacro'), + StoneOutput: require('./StoneOutput'), + StoneOutputBlock: require('./StoneOutputBlock'), + StoneOutputExpression: require('./StoneOutputExpression'), } From 2ec00eebbd527c5709170de9b11d64ffd84f4982 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 16:30:31 -0500 Subject: [PATCH 11/37] Added StoneTemplate type --- src/Stone/Parser.js | 15 +++++------- src/Stone/Scoper.js | 2 -- src/Stone/Types/StoneOutputBlock.js | 1 + src/Stone/Types/StoneTemplate.js | 37 +++++++++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 5 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 src/Stone/Types/StoneTemplate.js diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index 36ba622..d41adb2 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -19,18 +19,15 @@ export class Parser { const node = this.startNode() this.nextToken() - const output = this._createDeclaration('output', this._createLiteral('\'\''), 'let') const result = this.parseTopLevel(node) + const output = new acorn.Node(this) + output.type = 'StoneOutputBlock' + output.body = this._createBlockStatement(result.body) + const template = new acorn.Node(this) - template.type = 'FunctionDeclaration' - template.id = this._createIdentifier('template') - template.params = [ this._createIdentifier('_') ] - template.body = this._createBlockStatement([ - output, - ...result.body, - this._createReturn('output') - ]) + template.type = 'StoneTemplate' + template.output = output result.body = [ template ] diff --git a/src/Stone/Scoper.js b/src/Stone/Scoper.js index dcf02ca..e8213a8 100644 --- a/src/Stone/Scoper.js +++ b/src/Stone/Scoper.js @@ -3,8 +3,6 @@ import './Types' export class Scoper { static defaultScope = new Set([ - '_', - '_sections', 'Object', 'Set', 'Date', diff --git a/src/Stone/Types/StoneOutputBlock.js b/src/Stone/Types/StoneOutputBlock.js index 2baaae7..efd9cb1 100644 --- a/src/Stone/Types/StoneOutputBlock.js +++ b/src/Stone/Types/StoneOutputBlock.js @@ -101,6 +101,7 @@ export function walk(node, st, c) { export function scope(node, scope) { node.scope = new Set(scope) + node.scope.add('output') if(Array.isArray(node.params)) { for(const param of node.params) { diff --git a/src/Stone/Types/StoneTemplate.js b/src/Stone/Types/StoneTemplate.js new file mode 100644 index 0000000..bdfe2b3 --- /dev/null +++ b/src/Stone/Types/StoneTemplate.js @@ -0,0 +1,37 @@ +export function generate({ output }, state) { + output.returnRaw = true + + output.id = { + type: 'Identifier', + name: 'template' + } + + output.params = [ + { + type: 'Identifier', + name: '_' + }, { + type: 'AssignmentPattern', + left: { + type: 'Identifier', + name: '_sections' + }, + right: { + type: 'ObjectExpression', + properties: [ ] + } + } + ] + + this[output.type](output, state) +} + +export function walk({ output }, st, c) { + c(output, st, 'Expression') +} + +export function scope({ output }, scope) { + scope.add('_') + scope.add('_sections') + this._scope(output, scope) +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index fc5aa84..51d2360 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -6,4 +6,5 @@ export const Types = { StoneOutput: require('./StoneOutput'), StoneOutputBlock: require('./StoneOutputBlock'), StoneOutputExpression: require('./StoneOutputExpression'), + StoneTemplate: require('./StoneTemplate'), } From b04663f3a1c8ccc9569a87abbae6458ef0c39f89 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 17:25:30 -0500 Subject: [PATCH 12/37] Added assignments to StoneOutputBlock --- src/Stone/Types/StoneOutputBlock.js | 82 ++++++++++++++++------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/src/Stone/Types/StoneOutputBlock.js b/src/Stone/Types/StoneOutputBlock.js index efd9cb1..c083091 100644 --- a/src/Stone/Types/StoneOutputBlock.js +++ b/src/Stone/Types/StoneOutputBlock.js @@ -11,53 +11,61 @@ export function generate(node, state) { this.SequenceExpression({ expressions: node.params || [ ] }, state) state.write(' ') + node.assignments = node.assignments || [ ] + if(node.rescopeContext) { // _ = { ..._ } - node.body.body.unshift({ - type: 'ExpressionStatement', - expression: { - type: 'AssignmentExpression', - operator: '=', - left: { - type: 'Identifier', - name: '_' - }, - right: { - type: 'ObjectExpression', - properties: [ - { - type: 'SpreadElement', - argument: { - type: 'Identifier', - name: '_' - } + node.assignments.push({ + operator: '=', + left: { + type: 'Identifier', + name: '_' + }, + right: { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + type: 'Identifier', + name: '_' } - ] - } + } + ] } }) } // let output = '' - node.body.body.unshift({ - type: 'VariableDeclaration', - declarations: [ - { - type: 'VariableDeclarator', - id: { - type: 'Identifier', - name: 'output' - }, - init: { - type: 'Literal', - value: '\'\'', - raw: '\'\'', - } - } - ], - kind: 'let' + node.assignments.push({ + kind: 'let', + left: { + type: 'Identifier', + name: 'output' + }, + right: { + type: 'Literal', + value: '', + raw: '\'\'', + } }) + node.body.body.unshift(...node.assignments.map(({ kind, ...assignment }) => { + const hasKind = !kind.isNil + return { + type: hasKind ? 'VariableDeclaration' : 'ExpressionStatement', + kind: kind, + expression: hasKind ? void 0 : { ...assignment, type: 'AssignmentExpression' }, + declarations: !hasKind ? void 0 : [ + { + type: 'VariableDeclarator', + id: assignment.left, + init: assignment.right + } + ] + } + })) + if(node.returnRaw) { // return output node.body.body.push({ From 32211370066340e706ca5844f92b9a0d84908bb4 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 17:27:49 -0500 Subject: [PATCH 13/37] Template path is now shared with StoneTemplate --- src/Compiler.js | 2 +- src/Stone.js | 10 +++++++--- src/Stone/Parser.js | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Compiler.js b/src/Compiler.js index 3158d1a..367f15a 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -37,7 +37,7 @@ export class Compiler { let template = null try { - template = Stone.stringify(Stone.parse(contents)) + template = Stone.stringify(Stone.parse(contents, file)) } catch(err) { if(!err._hasTemplate) { err._hasTemplate = true diff --git a/src/Stone.js b/src/Stone.js index b21d5d5..76c9af6 100644 --- a/src/Stone.js +++ b/src/Stone.js @@ -13,7 +13,9 @@ export class Stone { return } - acorn.plugins.stone = parser => { + acorn.plugins.stone = (parser, config) => { + parser._stoneTemplate = config.template || null + for(const name of Object.getOwnPropertyNames(Parser.prototype)) { if(name === 'constructor') { continue @@ -32,14 +34,16 @@ export class Stone { } } - static parse(code) { + static parse(code, pathname = null) { this._register() return acorn.parse(code, { ecmaVersion: 9, plugins: { objectSpread: true, - stone: true + stone: { + template: pathname + } } }) } diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index d41adb2..f9ee410 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -28,6 +28,7 @@ export class Parser { const template = new acorn.Node(this) template.type = 'StoneTemplate' template.output = output + template.pathname = this._stoneTemplate result.body = [ template ] From a4e99daf54e2e325cc34870fc67bb87a9a281760 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 17:49:40 -0500 Subject: [PATCH 14/37] Rebuilt include/each on acorn --- src/Compiler/Layouts.js | 25 ------------ src/Stone/Parsers/Includes.js | 45 ++++++++++++++++++++++ src/Stone/Parsers/index.js | 3 +- src/Stone/Types/StoneEach.js | 17 ++++++++ src/Stone/Types/StoneInclude.js | 15 ++++++++ src/Stone/Types/StoneTemplate.js | 17 +++++++- src/Stone/Types/index.js | 2 + test/views/layouts/each-array-empty.stone | 2 +- test/views/layouts/each-array-extra.stone | 2 +- test/views/layouts/each-array.stone | 2 +- test/views/layouts/each-empty-extra.stone | 2 +- test/views/layouts/each-object-empty.stone | 2 +- test/views/layouts/each-object-extra.stone | 2 +- test/views/layouts/each-object.stone | 2 +- 14 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 src/Stone/Parsers/Includes.js create mode 100644 src/Stone/Types/StoneEach.js create mode 100644 src/Stone/Types/StoneInclude.js diff --git a/src/Compiler/Layouts.js b/src/Compiler/Layouts.js index e7b7de8..158cc16 100644 --- a/src/Compiler/Layouts.js +++ b/src/Compiler/Layouts.js @@ -109,28 +109,3 @@ export function compileHassection(context, section) { context.validateSyntax(section) return `if((_sections[${section}] || [ ]).length > 0) {` } - -/** - * Renders content from a subview - * - * @param {object} context Context for the compilation - * @param {string} view Subview to include - * @return {string} Code to render the subview - */ -export function compileInclude(context, view) { - context.validateSyntax(view) - return `output += (_.$stone.include(_, _sections, __templatePathname, ${view}));\n` -} - -/** - * Compiles each directive to call the runtime and output - * the result. - * - * @param object context Context for the compilation - * @param string args Arguments to pass through to runtime - * @return string Code to render the each block - */ -export function compileEach(context, args) { - context.validateSyntax(`each(${args})`) - return `output += (_.$stone.each(_, __templatePathname, ${args}));\n` -} diff --git a/src/Stone/Parsers/Includes.js b/src/Stone/Parsers/Includes.js new file mode 100644 index 0000000..fa589f9 --- /dev/null +++ b/src/Stone/Parsers/Includes.js @@ -0,0 +1,45 @@ +/** + * Renders content from a subview + * + * @param {object} node Blank node + * @param {mixed} args View name and optional context + * @return {object} Finished node + */ +export function parseIncludeDirective(node, args) { + if(args.length === 0) { + this.raise(this.start, '`@include` must contain at least 1 argument') + } + + args = this._flattenArgs(args) + node.view = args.shift() + + if(args.length > 1) { + this.raise(this.start, '`@include` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.context = args.shift() + } + + this.next() + return this.finishNode(node, 'StoneInclude') +} + +/** + * Compiles each directive to call the runtime and output + * the result. + * + * @param {object} node Blank node + * @param {mixed} params Arguments to pass through to runtime + * @return {object} Finished node + */ +export function parseEachDirective(node, params) { + node.params = this._flattenArgs(params) + + if(node.params.length < 3) { + this.raise(this.start, '`@each` must contain at least 3 arguments') + } else if(node.params.length > 5) { + this.raise(this.start, '`@each` cannot contain more than 5 arguments') + } + + this.next() + return this.finishNode(node, 'StoneEach') +} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index e7d4544..e26cd85 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -1,6 +1,7 @@ export const Parsers = { ...require('./Conditionals'), - ...require('./Output'), + ...require('./Includes'), ...require('./Loops'), ...require('./Macros'), + ...require('./Output'), } diff --git a/src/Stone/Types/StoneEach.js b/src/Stone/Types/StoneEach.js new file mode 100644 index 0000000..db5b8b1 --- /dev/null +++ b/src/Stone/Types/StoneEach.js @@ -0,0 +1,17 @@ +export function generate(node, state) { + node.params.unshift({ + type: 'Identifier', + name: '_' + }, { + type: 'Identifier', + name: '_templatePathname' + }) + + state.write('output += _.$stone.each') + this.SequenceExpression({ expressions: node.params }, state) + state.write(';') +} + +export function walk() { + // Do nothing +} diff --git a/src/Stone/Types/StoneInclude.js b/src/Stone/Types/StoneInclude.js new file mode 100644 index 0000000..8549acc --- /dev/null +++ b/src/Stone/Types/StoneInclude.js @@ -0,0 +1,15 @@ +export function generate(node, state) { + state.write('output += _.$stone.include(_, _sections, _templatePathname, ') + this[node.view.type](node.view, state) + + if(!node.context.isNil) { + state.write(', ') + this[node.context.type](node.context, state) + } + + state.write(');') +} + +export function walk() { + // Do nothing +} diff --git a/src/Stone/Types/StoneTemplate.js b/src/Stone/Types/StoneTemplate.js index bdfe2b3..06f16e2 100644 --- a/src/Stone/Types/StoneTemplate.js +++ b/src/Stone/Types/StoneTemplate.js @@ -1,4 +1,4 @@ -export function generate({ output }, state) { +export function generate({ pathname, output }, state) { output.returnRaw = true output.id = { @@ -23,6 +23,20 @@ export function generate({ output }, state) { } ] + output.assignments = output.assignments || [ ] + output.assignments.push({ + kind: 'const', + left: { + type: 'Identifier', + name: '_templatePathname' + }, + right: { + type: 'Literal', + value: pathname.isNil ? null : pathname, + raw: pathname.isNil ? null : `'${pathname}'` + } + }) + this[output.type](output, state) } @@ -33,5 +47,6 @@ export function walk({ output }, st, c) { export function scope({ output }, scope) { scope.add('_') scope.add('_sections') + scope.add('_templatePathname') this._scope(output, scope) } diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index 51d2360..ac13521 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -1,6 +1,8 @@ export const Types = { StoneDump: require('./StoneDump'), + StoneEach: require('./StoneEach'), StoneEmptyExpression: require('./StoneEmptyExpression'), + StoneInclude: require('./StoneInclude'), StoneLoop: require('./StoneLoop'), StoneMacro: require('./StoneMacro'), StoneOutput: require('./StoneOutput'), diff --git a/test/views/layouts/each-array-empty.stone b/test/views/layouts/each-array-empty.stone index c354d49..d455826 100644 --- a/test/views/layouts/each-array-empty.stone +++ b/test/views/layouts/each-array-empty.stone @@ -1 +1 @@ -@each(null, [ ], 'value', 'control-structures._each-empty-partial') +@each(null, [ ], 'value', 'layouts._each-empty-partial') diff --git a/test/views/layouts/each-array-extra.stone b/test/views/layouts/each-array-extra.stone index 2b949ce..c4d3b8d 100644 --- a/test/views/layouts/each-array-extra.stone +++ b/test/views/layouts/each-array-extra.stone @@ -1,3 +1,3 @@ -@each('control-structures._each-array-partial', numbers, 'value', null, { +@each('layouts._each-array-partial', numbers, 'value', null, { label: 'Value:' }) diff --git a/test/views/layouts/each-array.stone b/test/views/layouts/each-array.stone index f8a3147..54c80ac 100644 --- a/test/views/layouts/each-array.stone +++ b/test/views/layouts/each-array.stone @@ -1 +1 @@ -@each('control-structures._each-array-partial', numbers, 'value') +@each('layouts._each-array-partial', numbers, 'value') diff --git a/test/views/layouts/each-empty-extra.stone b/test/views/layouts/each-empty-extra.stone index aaf16ff..7aa563c 100644 --- a/test/views/layouts/each-empty-extra.stone +++ b/test/views/layouts/each-empty-extra.stone @@ -1,3 +1,3 @@ -@each(null, [ ], 'value', 'control-structures._each-empty-partial', { +@each(null, [ ], 'value', 'layouts._each-empty-partial', { placeholder: 'This is an alternative placeholder.' }) diff --git a/test/views/layouts/each-object-empty.stone b/test/views/layouts/each-object-empty.stone index 87805b5..469799d 100644 --- a/test/views/layouts/each-object-empty.stone +++ b/test/views/layouts/each-object-empty.stone @@ -1 +1 @@ -@each(null, { }, 'value', 'control-structures._each-empty-partial') +@each(null, { }, 'value', 'layouts._each-empty-partial') diff --git a/test/views/layouts/each-object-extra.stone b/test/views/layouts/each-object-extra.stone index 5d1c996..17b7797 100644 --- a/test/views/layouts/each-object-extra.stone +++ b/test/views/layouts/each-object-extra.stone @@ -1,3 +1,3 @@ -@each('control-structures._each-object-partial', pairs[0], 'value', null, { +@each('layouts._each-object-partial', pairs[0], 'value', null, { label: 'Value is' }) diff --git a/test/views/layouts/each-object.stone b/test/views/layouts/each-object.stone index 4a2c000..8fd0ebd 100644 --- a/test/views/layouts/each-object.stone +++ b/test/views/layouts/each-object.stone @@ -1 +1 @@ -@each('control-structures._each-object-partial', pairs[0], 'value') +@each('layouts._each-object-partial', pairs[0], 'value') From a95ec47bcf840688a5e989443389d4b1cdd10d5d Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 26 Nov 2017 23:41:08 -0500 Subject: [PATCH 15/37] Rebuilt layouts on acorn --- src/Compiler.js | 10 +- src/Compiler/Layouts.js | 111 -------------------- src/Stone.js | 2 +- src/Stone/Generator.js | 13 ++- src/Stone/Parser.js | 18 ++-- src/Stone/Parsers/Conditionals.js | 2 +- src/Stone/Parsers/Includes.js | 3 +- src/Stone/Parsers/Layouts.js | 151 ++++++++++++++++++++++++++++ src/Stone/Parsers/index.js | 1 + src/Stone/Scoper.js | 3 +- src/Stone/Types/StoneExtends.js | 33 ++++++ src/Stone/Types/StoneHasSection.js | 30 ++++++ src/Stone/Types/StoneInclude.js | 18 +++- src/Stone/Types/StoneOutputBlock.js | 49 ++++----- src/Stone/Types/StoneSection.js | 36 +++++++ src/Stone/Types/StoneSuper.js | 4 + src/Stone/Types/StoneTemplate.js | 100 ++++++++++++++++-- src/Stone/Types/StoneYield.js | 29 ++++++ src/Stone/Types/index.js | 5 + src/Support/StoneSections.js | 21 ++++ 20 files changed, 479 insertions(+), 160 deletions(-) delete mode 100644 src/Compiler/Layouts.js create mode 100644 src/Stone/Parsers/Layouts.js create mode 100644 src/Stone/Types/StoneExtends.js create mode 100644 src/Stone/Types/StoneHasSection.js create mode 100644 src/Stone/Types/StoneSection.js create mode 100644 src/Stone/Types/StoneSuper.js create mode 100644 src/Stone/Types/StoneYield.js create mode 100644 src/Support/StoneSections.js diff --git a/src/Compiler.js b/src/Compiler.js index 367f15a..04d708e 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -1,4 +1,5 @@ import './Stone' +import './Support/StoneSections' const fs = require('fs') const vm = require('vm') @@ -59,8 +60,13 @@ export class Compiler { return template } - const script = new vm.Script(`(${template})`, { filename: file }) - return script.runInNewContext() + try { + const script = new vm.Script(`(${template})`, { filename: file }) + return script.runInNewContext({ StoneSections }) + } catch(err) { + console.log('template', template) + throw err + } } } diff --git a/src/Compiler/Layouts.js b/src/Compiler/Layouts.js deleted file mode 100644 index 158cc16..0000000 --- a/src/Compiler/Layouts.js +++ /dev/null @@ -1,111 +0,0 @@ -import '../AST' -import '../Errors/StoneCompilerError' - -export function compileExtends(context, args) { - if(context.isLayout === true) { - throw new StoneCompilerError(context, '@extends may only be called once per view.') - } - - args = context.parseArguments(args) - - context.isLayout = true - context.hasLayoutContext = args.length > 1 - - let code = `const __extendsLayout = ${AST.stringify(args[0])};` - - if(context.hasLayoutContext) { - code += `\nconst __extendsContext = ${AST.stringify(args[1])};` - } - - return code -} - -export function compileSection(context, args) { - args = context.parseArguments(args) - - if(args.length === 1) { - args = AST.stringify(args[0]) - context.sections.push(args) - return this._compileSection(context, args, 'function() {\nlet output = \'\';') - } - - if(args.length !== 2) { - throw new StoneCompilerError(context, 'Invalid section block') - } - - return this._compileSection( - context, - AST.stringify(args[0]), - `function() { return escape(${AST.stringify(args[1])}); });` - ) -} - -export function _compileSection(context, name, code) { - return `(_sections[${name}] = (_sections[${name}] || [ ])).unshift(${code}\n` -} - -/** - * Ends the current section and returns output - * @return {string} Output from the section - */ -export function compileEndsection(context) { - context.sections.pop() - return 'return output;\n});' -} - -/** - * Ends the current section and yields it for display - * @return {string} Output from the section - */ -export function compileShow(context) { - const section = context.sections[context.sections.length - 1] - return `${this.compileEndsection(context)}\n${this.compileYield(context, section)}` -} - -/** - * Compiles the yield directive to output a section - * - * @param {object} context Context for the compilation - * @param {string} section Name of the section to yield - * @return {string} Code to render the section - */ -export function compileYield(context, section) { - let code = '' - - if(section.indexOf(',') >= 0) { - const sectionName = section.split(/,/)[0] - code = `${this.compileSection(context, section)}\n` - section = sectionName - } - - context.validateSyntax(section) - return `${code}output += (_sections[${section}] || [ ]).length > 0 ? (_sections[${section}].pop())() : '';` -} - -/** - * Renders content from the section section - * @return {string} Code to render the super section - */ -export function compileSuper(context) { - // Due to how sections work, we can cheat by just calling yeild - // which will pop off the next chunk of content in this section - // and render it within ours - return this.compileYield(context, context.sections[context.sections.length - 1]) -} - -/** - * Alias of compileSuper for compatibility with Blade - * @return {string} Code to render the super section - */ -export function compileParent(context) { - return this.compileSuper(context) -} - -/** - * Convenience directive to determine if a section has content - * @return {string} If statement that determines if a section has content - */ -export function compileHassection(context, section) { - context.validateSyntax(section) - return `if((_sections[${section}] || [ ]).length > 0) {` -} diff --git a/src/Stone.js b/src/Stone.js index 76c9af6..03719a4 100644 --- a/src/Stone.js +++ b/src/Stone.js @@ -14,7 +14,7 @@ export class Stone { } acorn.plugins.stone = (parser, config) => { - parser._stoneTemplate = config.template || null + parser._stoneTemplatePathname = config.template || null for(const name of Object.getOwnPropertyNames(Parser.prototype)) { if(name === 'constructor') { diff --git a/src/Stone/Generator.js b/src/Stone/Generator.js index 8dbc257..40af40d 100644 --- a/src/Stone/Generator.js +++ b/src/Stone/Generator.js @@ -25,9 +25,16 @@ export const Generator = { Property(node, state) { if(node.type === 'SpreadElement') { - state.write('...(') - this[node.argument.type](node.argument, state) - state.write(')') + if(node.argument.type === 'Identifier') { + state.write('...') + this.Identifier(node.argument, state) + state.write('') + } else { + state.write('...(') + this[node.argument.type](node.argument, state) + state.write(')') + } + return } diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index f9ee410..28c4a17 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -16,19 +16,19 @@ const directiveCodes = new Set( export class Parser { parse() { + const template = new acorn.Node(this) + template.type = 'StoneTemplate' + template.pathname = this._stoneTemplatePathname + this._stoneTemplate = template + const node = this.startNode() this.nextToken() const result = this.parseTopLevel(node) - const output = new acorn.Node(this) - output.type = 'StoneOutputBlock' - output.body = this._createBlockStatement(result.body) - - const template = new acorn.Node(this) - template.type = 'StoneTemplate' - template.output = output - template.pathname = this._stoneTemplate + template.output = new acorn.Node(this) + template.output.type = 'StoneOutputBlock' + template.output.body = this._createBlockStatement(result.body) result.body = [ template ] @@ -122,7 +122,7 @@ export class Parser { } let args = null - const parse = `parse${directive[0].toUpperCase()}${directive.substring(1)}Directive` + const parse = `parse${directive[0].toUpperCase()}${directive.substring(1).toLowerCase()}Directive` const node = this.startNode() node.directive = directive diff --git a/src/Stone/Parsers/Conditionals.js b/src/Stone/Parsers/Conditionals.js index ae16511..b0cea90 100644 --- a/src/Stone/Parsers/Conditionals.js +++ b/src/Stone/Parsers/Conditionals.js @@ -1,4 +1,4 @@ -const endDirectives = [ 'endif', 'elseif', 'else' ] +export const endDirectives = [ 'endif', 'elseif', 'else' ] export function parseIfDirective(node, args) { (this._currentIf = (this._currentIf || [ ])).push(node) diff --git a/src/Stone/Parsers/Includes.js b/src/Stone/Parsers/Includes.js index fa589f9..18ce5d9 100644 --- a/src/Stone/Parsers/Includes.js +++ b/src/Stone/Parsers/Includes.js @@ -6,11 +6,12 @@ * @return {object} Finished node */ export function parseIncludeDirective(node, args) { + args = this._flattenArgs(args) + if(args.length === 0) { this.raise(this.start, '`@include` must contain at least 1 argument') } - args = this._flattenArgs(args) node.view = args.shift() if(args.length > 1) { diff --git a/src/Stone/Parsers/Layouts.js b/src/Stone/Parsers/Layouts.js new file mode 100644 index 0000000..8a15a71 --- /dev/null +++ b/src/Stone/Parsers/Layouts.js @@ -0,0 +1,151 @@ +import { endDirectives } from './Conditionals' + +export function parseExtendsDirective(node, args) { + if(this._stoneTemplate.isNil) { + this.unexpected() + } + + if(this._stoneTemplate.isLayout === true) { + this.raise(this.start, '`@extends` may only be called once per view.') + } else { + this._stoneTemplate.isLayout = true + } + + args = this._flattenArgs(args) + + if(args.length === 0) { + this.raise(this.start, '`@extends` must contain at least 1 argument') + } + + node.view = args.shift() + + if(args.length > 1) { + this.raise(this.start, '`@extends` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.context = args.shift() + this._stoneTemplate.hasLayoutContext = true + } + + this.next() + return this.finishNode(node, 'StoneExtends') +} + +export function parseSectionDirective(node, args) { + args = this._flattenArgs(args) + + if(args.length === 0) { + this.raise(this.start, '`@section` must contain at least 1 argument') + } + + node.id = args.shift() + + if(args.length > 1) { + this.raise(this.start, '`@section` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.output = args.pop() + node.inline = true + this.next() + } else { + (this._currentSection = (this._currentSection || [ ])).push(node) + + const output = this.startNode() + output.params = args + output.body = this.parseUntilEndDirective([ 'show', 'endsection' ]) + node.output = this.finishNode(output, 'StoneOutputBlock') + } + + return this.finishNode(node, 'StoneSection') +} + +/** + * Ends the current section and returns output + * @return {string} Output from the section + */ +export function parseEndsectionDirective(node) { + if(!this._currentSection || this._currentSection.length === 0) { + this.raise(this.start, '`@endsection` outside of `@section`') + } + + this._currentSection.pop() + + return this.finishNode(node, 'Directive') +} + +/** + * Ends the current section and yields it for display + * @return {string} Output from the section + */ +export function parseShowDirective(node) { + if(!this._currentSection || this._currentSection.length === 0) { + this.raise(this.start, '`@show` outside of `@section`') + } + + this._currentSection.pop().yield = true + + return this.finishNode(node, 'Directive') +} + +/** + * Compiles the yield directive to output a section + * + * @param {object} context Context for the compilation + * @param {string} section Name of the section to yield + * @return {string} Code to render the section + */ +export function parseYieldDirective(node, args) { + args = this._flattenArgs(args) + + if(args.length === 0) { + this.raise(this.start, '`@yield` must contain at least 1 argument') + } + + node.section = args.shift() + + if(args.length > 1) { + this.raise(this.start, '`@yield` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.output = args.pop() + } + + this.next() + return this.finishNode(node, 'StoneYield') +} + +/** + * Renders content from the section section + * @return {string} Code to render the super section + */ +export function parseSuperDirective(node) { + if(!this._currentSection || this._currentSection.length === 0) { + this.raise(this.start, `\`@${node.directive}\` outside of \`@section\``) + } + + node.section = { ...this._currentSection[this._currentSection.length - 1].id } + return this.finishNode(node, 'StoneSuper') +} + +/** + * Alias of compileSuper for compatibility with Blade + * @return {string} Code to render the super section + */ +export function parseParentDirective(node) { + return this.parseSuperDirective(node) +} + +/** + * Convenience directive to determine if a section has content + * @return {string} If statement that determines if a section has content + */ +export function parseHassectionDirective(node, args) { + args = this._flattenArgs(args) + + if(args.length !== 1) { + this.raise(this.start, '`@hassection` must contain exactly 1 argument') + } + + (this._currentIf = (this._currentIf || [ ])).push(node) + + node.section = args.pop() + node.consequent = this.parseUntilEndDirective(endDirectives) + return this.finishNode(node, 'StoneHasSection') +} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index e26cd85..d36ba22 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -1,6 +1,7 @@ export const Parsers = { ...require('./Conditionals'), ...require('./Includes'), + ...require('./Layouts'), ...require('./Loops'), ...require('./Macros'), ...require('./Output'), diff --git a/src/Stone/Scoper.js b/src/Stone/Scoper.js index e8213a8..47c3f1d 100644 --- a/src/Stone/Scoper.js +++ b/src/Stone/Scoper.js @@ -9,7 +9,8 @@ export class Scoper { 'Array', 'String', 'global', - 'process' + 'process', + 'StoneSections' ]) static scope(node) { diff --git a/src/Stone/Types/StoneExtends.js b/src/Stone/Types/StoneExtends.js new file mode 100644 index 0000000..5cac43b --- /dev/null +++ b/src/Stone/Types/StoneExtends.js @@ -0,0 +1,33 @@ +export function generate(node, state) { + state.write('__extendsLayout = ') + this[node.view.type](node.view, state) + state.write(';') + + if(node.context.isNil) { + return + } + + state.write(state.lineEnd) + state.write(state.indent) + state.write('__extendsContext = ') + this[node.context.type](node.context, state) + state.write(';') +} + +export function walk(node, st, c) { + c(node.view, st, 'Pattern') + + if(node.context.isNil) { + return + } + + c(node.context, st, 'Expression') +} + +export function scope(node, scope) { + if(node.context.isNil) { + return + } + + this._scope(node.context, scope) +} diff --git a/src/Stone/Types/StoneHasSection.js b/src/Stone/Types/StoneHasSection.js new file mode 100644 index 0000000..8dea4aa --- /dev/null +++ b/src/Stone/Types/StoneHasSection.js @@ -0,0 +1,30 @@ +export function generate(node, state) { + node.test = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: '_sections' + }, + property: { + type: 'Identifier', + name: 'has' + } + }, + arguments: [ node.section ] + } + + return this.IfStatement(node, state) +} + +export function walk(node, st, c) { + c(node.section, st, 'Pattern') + c(node.consequence, st, 'Expression') + + if(node.alternate.isNil) { + return + } + + c(node.alternate, st, 'Expression') +} diff --git a/src/Stone/Types/StoneInclude.js b/src/Stone/Types/StoneInclude.js index 8549acc..96c3bc6 100644 --- a/src/Stone/Types/StoneInclude.js +++ b/src/Stone/Types/StoneInclude.js @@ -10,6 +10,20 @@ export function generate(node, state) { state.write(');') } -export function walk() { - // Do nothing +export function walk(node, st, c) { + c(node.view, st, 'Pattern') + + if(node.context.isNil) { + return + } + + c(node.context, st, 'Expression') +} + +export function scope(node, scope) { + if(node.context.isNil) { + return + } + + this._scope(node.context, scope) } diff --git a/src/Stone/Types/StoneOutputBlock.js b/src/Stone/Types/StoneOutputBlock.js index c083091..ae81096 100644 --- a/src/Stone/Types/StoneOutputBlock.js +++ b/src/Stone/Types/StoneOutputBlock.js @@ -66,35 +66,38 @@ export function generate(node, state) { } })) - if(node.returnRaw) { + let _return = null + + if(!node.return.isNil) { + _return = node.return + } else if(node.returnRaw) { // return output - node.body.body.push({ - type: 'ReturnStatement', - argument: { - type: 'Identifier', - name: 'output' - } - }) + _return = { + type: 'Identifier', + name: 'output' + } } else { // return new HtmlString(output) - node.body.body.push({ - type: 'ReturnStatement', - argument: { - type: 'NewExpression', - callee: { + _return = { + type: 'NewExpression', + callee: { + type: 'Identifier', + name: 'HtmlString' + }, + arguments: [ + { type: 'Identifier', - name: 'HtmlString' - }, - arguments: [ - { - type: 'Identifier', - name: 'output' - } - ] - } - }) + name: 'output' + } + ] + } } + node.body.body.push({ + type: 'ReturnStatement', + argument: _return + }) + this[node.body.type](node.body, state) state.popScope() } diff --git a/src/Stone/Types/StoneSection.js b/src/Stone/Types/StoneSection.js new file mode 100644 index 0000000..98b299b --- /dev/null +++ b/src/Stone/Types/StoneSection.js @@ -0,0 +1,36 @@ +export function generate(node, state) { + state.write('_sections.push(') + this[node.id.type](node.id, state) + state.write(', ') + + if(node.inline) { + state.write('() => ') + this.StoneOutputExpression({ safe: true, value: node.output }, state) + } else { + this[node.output.type](node.output, state) + } + + state.write(');') + + if(!node.yield) { + return + } + + state.write(state.lineEnd) + state.write(state.indent) + this.StoneYield({ section: node.id }, state) +} + +export function walk(node, st, c) { + c(node.id, st, 'Pattern') + + if(node.inline) { + return + } + + c(node.output, st, 'Expression') +} + +export function scope(node, scope) { + this._scope(node.output, scope) +} diff --git a/src/Stone/Types/StoneSuper.js b/src/Stone/Types/StoneSuper.js new file mode 100644 index 0000000..c03a034 --- /dev/null +++ b/src/Stone/Types/StoneSuper.js @@ -0,0 +1,4 @@ +// Due to how sections work, we can cheat by treating as yield +// which will pop off the next chunk of content in the section +// and render it within ours +export * from './StoneYield' diff --git a/src/Stone/Types/StoneTemplate.js b/src/Stone/Types/StoneTemplate.js index 06f16e2..f7aad52 100644 --- a/src/Stone/Types/StoneTemplate.js +++ b/src/Stone/Types/StoneTemplate.js @@ -1,6 +1,4 @@ -export function generate({ pathname, output }, state) { - output.returnRaw = true - +export function generate({ pathname, output, isLayout, hasLayoutContext }, state) { output.id = { type: 'Identifier', name: 'template' @@ -17,8 +15,11 @@ export function generate({ pathname, output }, state) { name: '_sections' }, right: { - type: 'ObjectExpression', - properties: [ ] + type: 'NewExpression', + callee: { + type: 'Identifier', + name: 'StoneSections' + } } } ] @@ -37,6 +38,84 @@ export function generate({ pathname, output }, state) { } }) + if(isLayout) { + output.assignments.push({ + kind: 'let', + left: { + type: 'Identifier', + name: '__extendsLayout' + } + }) + + const context = { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + type: 'Identifier', + name: '_' + } + } + ] + } + + if(hasLayoutContext) { + const extendsContext = { + type: 'Identifier', + name: '__extendsContext' + } + + output.assignments.push({ + kind: 'let', + left: extendsContext + }) + + context.properties.push({ + type: 'SpreadElement', + argument: extendsContext + }) + } + + output.return = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: '_' + }, + property: { + type: 'Identifier', + name: '$stone' + } + }, + property: { + type: 'Identifier', + name: 'extends' + } + }, + arguments: [ + { + type: 'Identifier', + name: '_templatePathname' + }, { + type: 'Identifier', + name: '__extendsLayout' + }, + context, + { + type: 'Identifier', + name: '_sections' + } + ] + } + } else { + output.returnRaw = true + } + this[output.type](output, state) } @@ -44,9 +123,18 @@ export function walk({ output }, st, c) { c(output, st, 'Expression') } -export function scope({ output }, scope) { +export function scope({ output, isLayout, hasLayoutContext }, scope) { scope.add('_') scope.add('_sections') scope.add('_templatePathname') + + if(isLayout) { + scope.add('__extendsLayout') + + if(hasLayoutContext) { + scope.add('__extendsContext') + } + } + this._scope(output, scope) } diff --git a/src/Stone/Types/StoneYield.js b/src/Stone/Types/StoneYield.js new file mode 100644 index 0000000..3f09442 --- /dev/null +++ b/src/Stone/Types/StoneYield.js @@ -0,0 +1,29 @@ +export function generate(node, state) { + state.write('output += _sections.render(') + this[node.section.type](node.section, state) + + if(!node.output.isNil) { + state.write(', ') + this.StoneOutputExpression({ safe: true, value: node.output }, state) + } + + state.write(');') +} + +export function walk(node, st, c) { + c(node.section, st, 'Pattern') + + if(node.output.isNil) { + return + } + + c(node.output, st, 'Expression') +} + +export function scope(node, scope) { + if(node.output.isNil) { + return + } + + this._scope(node.output, scope) +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index ac13521..4cbf872 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -2,11 +2,16 @@ export const Types = { StoneDump: require('./StoneDump'), StoneEach: require('./StoneEach'), StoneEmptyExpression: require('./StoneEmptyExpression'), + StoneExtends: require('./StoneExtends'), + StoneHasSection: require('./StoneHasSection'), StoneInclude: require('./StoneInclude'), StoneLoop: require('./StoneLoop'), StoneMacro: require('./StoneMacro'), StoneOutput: require('./StoneOutput'), StoneOutputBlock: require('./StoneOutputBlock'), StoneOutputExpression: require('./StoneOutputExpression'), + StoneSection: require('./StoneSection'), + StoneSuper: require('./StoneSuper'), StoneTemplate: require('./StoneTemplate'), + StoneYield: require('./StoneYield'), } diff --git a/src/Support/StoneSections.js b/src/Support/StoneSections.js new file mode 100644 index 0000000..a5342bd --- /dev/null +++ b/src/Support/StoneSections.js @@ -0,0 +1,21 @@ +export class StoneSections { + + _sections = { } + + push(name, func) { + (this._sections[name] = this._sections[name] || [ ]).push(func) + } + + render(name, defaultValue) { + if(!this.has(name)) { + return defaultValue || '' + } + + return (this._sections[name].shift())() + } + + has(name) { + return (this._sections[name] || [ ]).length > 0 + } + +} From ebe5e58a5a0abeca449bca927764fe75315a368a Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sat, 2 Dec 2017 14:09:45 -0500 Subject: [PATCH 16/37] Added `MakeNode` and `MockParser` --- src/Stone/Parser.js | 77 ++------------------- src/Stone/Support/MakeNode.js | 114 ++++++++++++++++++++++++++++++++ src/Stone/Support/MockParser.js | 29 ++++++++ 3 files changed, 148 insertions(+), 72 deletions(-) create mode 100644 src/Stone/Support/MakeNode.js create mode 100644 src/Stone/Support/MockParser.js diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index 28c4a17..014eb66 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -3,6 +3,8 @@ import './Parsers' import './Contexts/DirectiveArgs' import './Contexts/PreserveSpace' +import './Support/MakeNode' + import './Tokens/StoneOutput' import './Tokens/StoneDirective' @@ -20,6 +22,7 @@ export class Parser { template.type = 'StoneTemplate' template.pathname = this._stoneTemplatePathname this._stoneTemplate = template + this.make = new MakeNode(this) const node = this.startNode() this.nextToken() @@ -28,7 +31,7 @@ export class Parser { template.output = new acorn.Node(this) template.output.type = 'StoneOutputBlock' - template.output.body = this._createBlockStatement(result.body) + template.output.body = this.make.block(result.body) result.body = [ template ] @@ -50,10 +53,6 @@ export class Parser { return next.call(this, type) } - skipSpace(next, ...args) { - return next.call(this, ...args) - } - readToken(next, code) { if(code === 64 && !this._isCharCode(123, 1)) { this.pos++ @@ -92,7 +91,7 @@ export class Parser { // we can leverage the built in acorn functionality // for parsing things like for loops without it // trying to parse the block - return this._createBlockStatement([ ]) + return this.make.block([ ]) } switch(this.type) { @@ -261,72 +260,6 @@ export class Parser { return [ args ] } - _createIdentifier(identifier) { - const node = new acorn.Node(this) - node.type = 'Identifier' - node.name = identifier - return node - } - - _maybeCreateIdentifier(name) { - if(typeof name !== 'string') { - return name - } - - return this._createIdentifier(name) - } - - _createBlockStatement(statements) { - const node = new acorn.Node(this) - node.type = 'BlockStatement' - node.body = statements - return node - } - - _createEmptyNode() { - const node = new acorn.Node(this) - node.type = 'BlankExpression' - return node - } - - _createAssignment(left, right, operator = '=') { - const node = this.startNodeAt(this.start, this.startLoc) - node.operator = operator - node.left = this._maybeCreateIdentifier(left) - node.right = right - - return this.finishNode(node, 'AssignmentExpression') - } - - _createDeclaration(lhs, rhs, kind = 'const') { - const declarator = this.startNode() - declarator.id = this._maybeCreateIdentifier(lhs) - declarator.init = rhs - this.finishNode(declarator, 'VariableDeclarator') - - const declaration = this.startNode() - declaration.declarations = [ declarator ] - declaration.kind = kind - return this.finishNode(declaration, 'VariableDeclaration') - } - - _createLiteral(value) { - const node = this.startNode() - node.value = value - node.raw = value - return this.finishNode(node, 'Literal') - } - - _createReturn(value) { - const declarator = this.startNode() - - if(value) { - declarator.argument = this._maybeCreateIdentifier(value) - } - - return this.finishNode(declarator, 'ReturnStatement') - } - _debug(message = 'DEBUG', peek = false) { let debug = { start: this.start, diff --git a/src/Stone/Support/MakeNode.js b/src/Stone/Support/MakeNode.js new file mode 100644 index 0000000..c133a3a --- /dev/null +++ b/src/Stone/Support/MakeNode.js @@ -0,0 +1,114 @@ +import './MockParser' + +export class MakeNode { + + constructor(parser = null) { + this.parser = parser || new MockParser + } + + identifier(identifier) { + const node = this.parser.startNode() + node.name = identifier + return this.parser.finishNode(node, 'Identifier') + } + + auto(type) { + if(typeof type !== 'string') { + return type + } + + return this.identifier(type, this.parser) + } + + new(callee, args) { + const node = this.parser.startNode() + node.type = 'NewExpression' + node.callee = this.auto(callee) + node.arguments = Array.isArray(args) ? args.map(arg => this.auto(arg)) : [ this.auto(args) ] + return this.parser.finishNode(node, 'NewExpression') + } + + object(properties) { + const node = this.parser.startNode() + node.type = 'ObjectExpression' + node.properties = properties || [ ] + return this.parser.finishNode(node, 'ObjectExpression') + } + + property(key, value) { + const node = this.parser.startNode() + node.key = this.auto(key) + + if(!value.isNil) { + node.value = this.auto(value) + node.kind = 'init' + } + + return this.parser.finishNode(node, 'Property') + } + + spread(type) { + const node = this.parser.startNode() + node.argument = this.auto(type, this.parser) + return this.parser.finishNode(node, 'SpreadElement') + } + + assignment(left, right, operator = '=') { + const node = this.parser.startNode() + node.operator = operator + node.left = this.auto(left) + node.right = this.auto(right) + return this.parser.finishNode(node, 'AssignmentExpression') + } + + declaration(left, right, kind = 'const') { + const declarator = this.parser.startNode() + declarator.id = this.auto(left) + declarator.init = this.auto(right) + this.parser.finishNode(declarator, 'VariableDeclarator') + + const declaration = this.parser.startNode() + declaration.declarations = [ declarator ] + declaration.kind = kind + return this.parser.finishNode(declaration, 'VariableDeclaration') + } + + literal(value) { + const node = this.parser.startNode() + node.value = value + node.raw = value + return this.parser.finishNode(node, 'Literal') + } + + return(value) { + const node = this.parser.startNode() + + if(!value.isNil) { + node.argument = this.auto(value) + } + + return this.parser.finishNode(node, 'ReturnStatement') + } + + block(statements) { + const node = this.parser.startNode() + node.body = statements + return this.parser.finishNode(node, 'BlockStatement') + } + + + null() { + const node = this.parser.startNode() + node.type = 'Literal' + node.value = null + node.raw = 'null' + return this.parser.finishNode(node, 'Literal') + } + + empty() { + return this.parser.finishNode(this.parser.startNode(), 'StoneEmptyExpression') + } + +} + +export const make = new MakeNode diff --git a/src/Stone/Support/MockParser.js b/src/Stone/Support/MockParser.js new file mode 100644 index 0000000..2a9c453 --- /dev/null +++ b/src/Stone/Support/MockParser.js @@ -0,0 +1,29 @@ +const { Node } = require('acorn') + +export class MockParser { + + options = { } + + startNode() { + return new Node(this) + } + + startNodeAt(pos, loc) { + return new Node(this, pos, loc) + } + + finishNode(node, type) { + return this.finishNodeAt(node, type) + } + + finishNodeAt(node, type, pos) { + node.type = type + + if(!pos.isNil) { + node.end = pos + } + + return node + } + +} From ec74c85d640f2bf929ee0f0875be8ff477173e0c Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sat, 2 Dec 2017 14:13:24 -0500 Subject: [PATCH 17/37] Rebuilt components on acorn --- src/Compiler/Components.js | 42 ---------------- src/Stone/Parsers/Components.js | 79 +++++++++++++++++++++++++++++++ src/Stone/Parsers/index.js | 1 + src/Stone/Types/StoneComponent.js | 56 ++++++++++++++++++++++ src/Stone/Types/StoneSlot.js | 29 ++++++++++++ src/Stone/Types/index.js | 2 + 6 files changed, 167 insertions(+), 42 deletions(-) delete mode 100644 src/Compiler/Components.js create mode 100644 src/Stone/Parsers/Components.js create mode 100644 src/Stone/Types/StoneComponent.js create mode 100644 src/Stone/Types/StoneSlot.js diff --git a/src/Compiler/Components.js b/src/Compiler/Components.js deleted file mode 100644 index 0df178b..0000000 --- a/src/Compiler/Components.js +++ /dev/null @@ -1,42 +0,0 @@ -import '../AST' -import '../Errors/StoneCompilerError' - -export function compileComponent(context, args) { - args = context.parseArguments(args) - - let code = 'output += (function() {' - code += `\nconst __componentView = ${AST.stringify(args[0])};` - - if(args.length > 1) { - code += `\nconst __componentContext = ${AST.stringify(args[1])};` - } else { - code += '\nconst __componentContext = { };' - } - - code += '\nlet output = \'\';' - - return code -} - -export function compileEndcomponent() { - const context = 'Object.assign({ slot: new HtmlString(output) }, __componentContext)' - return `return _.$stone.include(_, { }, __templatePathname, __componentView, ${context});\n})()` -} - -export function compileSlot(context, args) { - args = context.parseArguments(args) - - if(args.length === 1) { - return `__componentContext[${AST.stringify(args[0])}] = (function() {\nlet output = '';` - } - - if(args.length !== 2) { - throw new StoneCompilerError(context, 'Invalid slot') - } - - return `__componentContext[${AST.stringify(args[0])}] = escape(${AST.stringify(args[1])});` -} - -export function compileEndslot() { - return 'return new HtmlString(output); })()' -} diff --git a/src/Stone/Parsers/Components.js b/src/Stone/Parsers/Components.js new file mode 100644 index 0000000..f2a0b69 --- /dev/null +++ b/src/Stone/Parsers/Components.js @@ -0,0 +1,79 @@ +export function parseComponentDirective(node, args) { + args = this._flattenArgs(args) + + if(args.length === 0) { + this.raise(this.start, '`@component` must contain at least 1 argument') + } + + node.view = args.shift() + + if(args.length > 1) { + this.raise(this.start, '`@component` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.context = args.pop() + } + + (this._currentComponent = (this._currentComponent || [ ])).push(node) + + const output = this.startNode() + output.params = args + output.body = this.parseUntilEndDirective('endcomponent') + node.output = this.finishNode(output, 'StoneOutputBlock') + + return this.finishNode(node, 'StoneComponent') +} + +/** + * Ends the current component and returns output + * @return {string} Output from the component + */ +export function parseEndcomponentDirective(node) { + if(!this._currentComponent || this._currentComponent.length === 0) { + this.raise(this.start, '`@endcomponent` outside of `@component`') + } + + this._currentComponent.pop() + + return this.finishNode(node, 'Directive') +} + +export function parseSlotDirective(node, args) { + args = this._flattenArgs(args) + + if(args.length === 0) { + this.raise(this.start, '`@slot` must contain at least 1 argument') + } + + node.id = args.shift() + + if(args.length > 1) { + this.raise(this.start, '`@slot` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.output = args.pop() + node.inline = true + this.next() + } else { + (this._currentSlot = (this._currentSlot || [ ])).push(node) + + const output = this.startNode() + output.params = args + output.body = this.parseUntilEndDirective('endslot') + node.output = this.finishNode(output, 'StoneOutputBlock') + } + + return this.finishNode(node, 'StoneSlot') +} + +/** + * Ends the current slot and returns output + * @return {string} Output from the slot + */ +export function parseEndslotDirective(node) { + if(!this._currentSlot || this._currentSlot.length === 0) { + this.raise(this.start, '`@endslot` outside of `@slot`') + } + + this._currentSlot.pop() + + return this.finishNode(node, 'Directive') +} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index d36ba22..59e14b8 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -1,4 +1,5 @@ export const Parsers = { + ...require('./Components'), ...require('./Conditionals'), ...require('./Includes'), ...require('./Layouts'), diff --git a/src/Stone/Types/StoneComponent.js b/src/Stone/Types/StoneComponent.js new file mode 100644 index 0000000..8bd1405 --- /dev/null +++ b/src/Stone/Types/StoneComponent.js @@ -0,0 +1,56 @@ +import { make } from '../Support/MakeNode' + +export function generate(node, state) { + node.output.assignments = node.output.assignments || [ ] + + node.output.assignments.push({ + kind: 'const', + left: make.identifier('__componentView'), + right: node.view + }) + + node.output.assignments.push({ + kind: 'const', + left: make.identifier('__componentContext'), + right: !node.context.isNil ? node.context : make.object() + }) + + node.output.return = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: make.identifier('_'), + property: make.identifier('$stone'), + }, + property: make.identifier('include'), + }, + arguments: [ + make.identifier('_'), + make.null(), + make.identifier('_templatePathname'), + make.identifier('__componentView'), + make.object([ + make.property('slot', make.new('HtmlString', 'output')), + make.spread('__componentContext') + ]) + ] + } + + state.write('output += (') + this[node.output.type](node.output, state) + state.write(')();') +} + +export function walk(node, st, c) { + +} + +export function scope(node, scope) { + node.scope = new Set(scope) + node.scope.add('__componentView') + node.scope.add('__componentContext') + + this._scope(node.output, node.scope) +} diff --git a/src/Stone/Types/StoneSlot.js b/src/Stone/Types/StoneSlot.js new file mode 100644 index 0000000..a6dca2c --- /dev/null +++ b/src/Stone/Types/StoneSlot.js @@ -0,0 +1,29 @@ +export function generate(node, state) { + state.write('__componentContext[') + this[node.id.type](node.id, state) + state.write('] = ') + + if(node.inline) { + this.StoneOutputExpression({ safe: true, value: node.output }, state) + } else { + state.write('(') + this[node.output.type](node.output, state) + state.write(')()') + } + + state.write(';') +} + +export function walk(node, st, c) { + c(node.id, st, 'Pattern') + + if(node.inline) { + return + } + + c(node.output, st, 'Expression') +} + +export function scope(node, scope) { + this._scope(node.output, scope) +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index 4cbf872..e176bba 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -1,4 +1,5 @@ export const Types = { + StoneComponent: require('./StoneComponent'), StoneDump: require('./StoneDump'), StoneEach: require('./StoneEach'), StoneEmptyExpression: require('./StoneEmptyExpression'), @@ -11,6 +12,7 @@ export const Types = { StoneOutputBlock: require('./StoneOutputBlock'), StoneOutputExpression: require('./StoneOutputExpression'), StoneSection: require('./StoneSection'), + StoneSlot: require('./StoneSlot'), StoneSuper: require('./StoneSuper'), StoneTemplate: require('./StoneTemplate'), StoneYield: require('./StoneYield'), From 839ffef7d3160d08186792393de31ca3a9da6393 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 3 Dec 2017 14:52:39 -0500 Subject: [PATCH 18/37] Added Scope class to wrap `Set` to maintain parent scope to children --- src/Stone/Scoper.js | 9 +++++---- src/Stone/Support/Scope.js | 29 +++++++++++++++++++++++++++++ src/Stone/Types/StoneComponent.js | 7 ++++--- src/Stone/Types/StoneLoop.js | 1 - src/Stone/Types/StoneOutputBlock.js | 2 +- 5 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 src/Stone/Support/Scope.js diff --git a/src/Stone/Scoper.js b/src/Stone/Scoper.js index 47c3f1d..e23c1b5 100644 --- a/src/Stone/Scoper.js +++ b/src/Stone/Scoper.js @@ -1,8 +1,9 @@ import './Types' +import './Support/Scope' export class Scoper { - static defaultScope = new Set([ + static defaultScope = new Scope(null, [ 'Object', 'Set', 'Date', @@ -28,7 +29,7 @@ export class Scoper { // Handlers static _bodyStatement(node, declarations, scope) { - node.scope = new Set(scope) + node.scope = scope.branch() if(!declarations.isNil) { this._scope(declarations, node.scope) @@ -38,7 +39,7 @@ export class Scoper { } static BlockStatement(node, scope) { - node.scope = new Set(scope) + node.scope = scope.branch() for(const statement of node.body) { this._scope(statement, node.scope) @@ -48,7 +49,7 @@ export class Scoper { static Program = Scoper.BlockStatement static FunctionDeclaration(node, scope) { - node.scope = new Set(scope) + node.scope = scope.branch() if(Array.isArray(node.params)) { for(const param of node.params) { diff --git a/src/Stone/Support/Scope.js b/src/Stone/Support/Scope.js new file mode 100644 index 0000000..f29d8eb --- /dev/null +++ b/src/Stone/Support/Scope.js @@ -0,0 +1,29 @@ +export class Scope { + + parent = null + storage = null + + constructor(parent = null, variables = [ ]) { + this.parent = parent + this.storage = new Set(variables) + } + + add(variable) { + this.storage.add(variable) + } + + has(variable) { + if(this.storage.has(variable)) { + return true + } else if(this.parent.isNil) { + return false + } + + return this.parent.has(variable) + } + + branch(variables = [ ]) { + return new Scope(this, variables) + } + +} diff --git a/src/Stone/Types/StoneComponent.js b/src/Stone/Types/StoneComponent.js index 8bd1405..90dca53 100644 --- a/src/Stone/Types/StoneComponent.js +++ b/src/Stone/Types/StoneComponent.js @@ -48,9 +48,10 @@ export function walk(node, st, c) { } export function scope(node, scope) { - node.scope = new Set(scope) - node.scope.add('__componentView') - node.scope.add('__componentContext') + node.scope = scope.branch([ + '__componentView', + '__componentContext' + ]) this._scope(node.output, node.scope) } diff --git a/src/Stone/Types/StoneLoop.js b/src/Stone/Types/StoneLoop.js index d554b53..0df3313 100644 --- a/src/Stone/Types/StoneLoop.js +++ b/src/Stone/Types/StoneLoop.js @@ -6,7 +6,6 @@ export function generate({ loop }, state) { state.__loops = (state.__loops || 0) + 1 const loopVariable = `__loop${state.__loops}` loop.scope.add(loopVariable) - loop.body.scope.add(loopVariable) loop.body.scope.add('loop') state.write(`const ${loopVariable} = new _.StoneLoop(`) diff --git a/src/Stone/Types/StoneOutputBlock.js b/src/Stone/Types/StoneOutputBlock.js index ae81096..77c2fa7 100644 --- a/src/Stone/Types/StoneOutputBlock.js +++ b/src/Stone/Types/StoneOutputBlock.js @@ -111,7 +111,7 @@ export function walk(node, st, c) { } export function scope(node, scope) { - node.scope = new Set(scope) + node.scope = scope.branch() node.scope.add('output') if(Array.isArray(node.params)) { From 894f1ae18ad4ad4ff52a2b28ced5115888ea0d51 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 3 Dec 2017 22:25:09 -0500 Subject: [PATCH 19/37] Updated Stone.walkVariables to better suppor spread/rest --- src/Stone.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Stone.js b/src/Stone.js index 03719a4..e697a2e 100644 --- a/src/Stone.js +++ b/src/Stone.js @@ -78,14 +78,12 @@ export class Stone { } } else if(node.type === 'ObjectPattern') { for(const property of node.properties) { - if(property.type === 'RestElement') { - callback(property.argument) - } else { - this.walkVariables(property.value, callback) - } + this.walkVariables(property.argument || property.value, callback) } } else if(node.type === 'AssignmentPattern') { this.walkVariables(node.left, callback) + } else if(node.type === 'RestElement' || node.type === 'SpreadElement') { + callback(node.argument) } else { callback(node) } From 9ad3ea14c62e6bc7e42d82b0ceb24b7edb0fd097 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 3 Dec 2017 22:31:51 -0500 Subject: [PATCH 20/37] Rebuilt assignments on acorn --- src/Compiler/Assignments.js | 138 ------------------------------- src/Stone/Parsers/Assignments.js | 88 ++++++++++++++++++++ src/Stone/Parsers/index.js | 1 + src/Stone/Scoper.js | 16 ++++ src/Stone/Types/StoneSet.js | 51 ++++++++++++ src/Stone/Types/StoneUnset.js | 19 +++++ src/Stone/Types/index.js | 2 + 7 files changed, 177 insertions(+), 138 deletions(-) delete mode 100644 src/Compiler/Assignments.js create mode 100644 src/Stone/Parsers/Assignments.js create mode 100644 src/Stone/Types/StoneSet.js create mode 100644 src/Stone/Types/StoneUnset.js diff --git a/src/Compiler/Assignments.js b/src/Compiler/Assignments.js deleted file mode 100644 index d5cba90..0000000 --- a/src/Compiler/Assignments.js +++ /dev/null @@ -1,138 +0,0 @@ -import '../AST' -import '../Errors/StoneCompilerError' -import '../Errors/StoneSyntaxError' -import '../Support/nextIndexOf' - -/** - * Sets a context variable - * - * @param {object} context Context for the compilation - * @param {string} args Arguments to set - * @return {string} Code to set the context variable - */ -export function compileSet(context, args) { - if(args.indexOf(',') === -1) { - // If there’s no commas, this is a simple raw code block - return context.validateSyntax(`${args};`) - } - - // If there are commas, we need to determine if - // the comma is at the top level or if it‘s inside - // an object, array or function call to determine - // the intended behavior - const open = { - '[': 0, - '(': 0, - '{': 0, - first: true - } - - const openCount = () => { - if(open.first) { - delete open.first - return -1 - } - - return Object.values(open).reduce((a, b) => a + b, 0) - } - - const set = [ '(', ')', '{', '}', '[', ']', ',' ] - let index = 0 - - while(openCount() !== 0 && (index = nextIndexOf(args, set, index)) >= 0) { - const character = args.substring(index, index + 1) - - switch(character) { - case '(': - open['(']++ - break - case ')': - open['(']-- - break - case '{': - open['{']++ - break - case '}': - open['{']-- - break - case '[': - open['[']++ - break - case ']': - open['[']-- - break - default: - break - } - - index++ - - if(character === ',' && openCount() === 0) { - break - } - } - - const lhs = args.substring(0, index).trim().replace(/,$/, '') - const rhs = args.substring(index).trim().replace(/^,/, '') - - if(rhs.length === 0) { - return context.validateSyntax(`${lhs};`) - } - - // If var type has been explicitly defined, we’ll - // pass through directly and scope locally - if(lhs.startsWith('const ') || lhs.startsWith('let ')) { - return context.validateSyntax(`${lhs} = ${rhs};`) - } - - // Otherwise, scoping is assumed to be on the context var - if(lhs[0] !== '{' && lhs[0] !== '[') { - // If we‘re not destructuring, we can assign it directly - // and bail out early. - // - // `__auto_scope_` will be processed by `contextualize` to - // determine whether or not the var should be set on the - // global `_` context or if there is a variable within the - // scope with the same name as `lhs` - - return context.validateSyntax(`__auto_scope_${lhs} = ${rhs};`) - } - - // If we are destructuring, we need to find the vars to extract - // then wrap them in a function and assign them to the context var - const code = `const ${lhs} = ${rhs};` - let tree = null - - try { - tree = AST.parse(code) - } catch(err) { - if(err instanceof SyntaxError) { - throw new StoneSyntaxError(context, err, context.state.index) - } - - throw err - } - - const extracted = [ ] - - if(tree.body.length > 1 || tree.body[0].type !== 'VariableDeclaration') { - throw new StoneCompilerError(context, 'Unexpected variable assignment.') - } - - for(const declaration of tree.body[0].declarations) { - AST.walkVariables(declaration.id, node => extracted.push(node.name)) - } - - return `Object.assign(_, (function() {\n\t${code}\n\treturn { ${extracted.join(', ')} };\n})());` -} - -/** - * Unsets a context variable - * - * @param {object} context Context for the compilation - * @param {string} args Arguments to unset - * @return {string} Code to set the context variable - */ -export function compileUnset(context, args) { - return context.validateSyntax(`delete ${args};`) -} diff --git a/src/Stone/Parsers/Assignments.js b/src/Stone/Parsers/Assignments.js new file mode 100644 index 0000000..3131df8 --- /dev/null +++ b/src/Stone/Parsers/Assignments.js @@ -0,0 +1,88 @@ +export function parseSetDirectiveArgs() { + this.skipSpace() + + let kind = null + + if(this.input.substring(this.pos, this.pos + 6).toLowerCase() === 'const ') { + kind = 'const' + } else if(this.input.substring(this.pos, this.pos + 4).toLowerCase() === 'let ') { + kind = 'let' + } else if(this.input.substring(this.pos, this.pos + 4).toLowerCase() === 'var ') { + this.raise(this.start, '`@set` does not support `var`') + } else { + return this.parseDirectiveArgs() + } + + this.pos += kind.length + + const node = this.parseDirectiveArgs() + node.kind = kind + return node +} + +/** + * Sets a context variable + * + * @param {object} context Context for the compilation + * @param {string} args Arguments to set + * @return {string} Code to set the context variable + */ +export function parseSetDirective(node, args) { + const kind = args.kind || null + args = this._flattenArgs(args) + + if(args.length === 0) { + this.raise(this.start, '`@set` must contain at least 1 argument') + } else if(args.length > 2) { + this.raise(this.start, '`@set` cannot contain more than 2 arguments') + } + + if(args.length === 1 && args[0].type === 'AssignmentExpression') { + Object.assign(node, args[0]) + } else { + node.operator = '=' + node.left = args[0] + node.right = args[1] + } + + node.kind = kind + expressionToPattern(node.left) + + this.next() + return this.finishNode(node, 'StoneSet') +} + +/** + * Unsets a context variable + * + * @param {object} context Context for the compilation + * @param {string} args Arguments to unset + * @return {string} Code to set the context variable + */ +export function parseUnsetDirective(node, args) { + node.properties = this._flattenArgs(args) + this.next() + return this.finishNode(node, 'StoneUnset') +} + +/** + * `parseSetDirectiveArgs` gets parsed into SequenceExpression + * which parses destructuring into Array/Object expressions + * instead of patterns + */ +function expressionToPattern(node) { + if(node.isNil) { + return + } + + if(node.type === 'ArrayExpression') { + node.type = 'ArrayPattern' + node.elements.forEach(expressionToPattern) + } else if(node.type === 'ObjectExpression') { + node.type = 'ObjectPattern' + + for(const property of node.properties) { + expressionToPattern(property.value) + } + } +} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index 59e14b8..b98cf40 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -1,4 +1,5 @@ export const Parsers = { + ...require('./Assignments'), ...require('./Components'), ...require('./Conditionals'), ...require('./Includes'), diff --git a/src/Stone/Scoper.js b/src/Stone/Scoper.js index e23c1b5..17693eb 100644 --- a/src/Stone/Scoper.js +++ b/src/Stone/Scoper.js @@ -105,6 +105,14 @@ export class Scoper { return this._bodyStatement(node, null, scope) } + static IfStatement(node, scope) { + this._scope(node.consequent, scope) + + if(!node.alternate.isNil) { + this._scope(node.alternate, scope) + } + } + static AssignmentExpression(node, scope) { this._scope(node.left, scope) } @@ -139,6 +147,14 @@ export class Scoper { this._scope(node.value, scope, force) } + static SpreadElement(node, scope, force) { + this._scope(node.argument, scope, force) + } + + static RestElement(node, scope, force) { + this.SpreadElement(node, scope, force) + } + static Identifier(node, scope, force) { if(!force) { return diff --git a/src/Stone/Types/StoneSet.js b/src/Stone/Types/StoneSet.js new file mode 100644 index 0000000..56a336a --- /dev/null +++ b/src/Stone/Types/StoneSet.js @@ -0,0 +1,51 @@ +import '../../Stone' +import { make } from '../Support/MakeNode' + +export function generate({ kind, left, right }, state) { + if(right.isNil) { + this[left.type](left, state) + return + } + + // If var type has been explicitly defined, we’ll + // pass through directly and scope locally + if(!kind.isNil) { + const declaration = make.declaration(left, right, kind) + require('../Scoper').Scoper._scope(left, state.scope, true) + return this[declaration.type](declaration, state) + } + + // Otherwise, scoping is assumed to be on the context var + if(left.type !== 'ArrayPattern' && left.type !== 'ObjectPattern') { + // If we‘re not destructuring, we can assign it directly + // and bail out early. + const assignment = make.assignment(left, right) + return this[assignment.type](assignment, state) + } + + // If we are destructuring, we need to find the vars to extract + // then wrap them in a function and assign them to the context var + const extracted = [ ] + Stone.walkVariables(left, node => extracted.push(node)) + + const block = make.block([ + make.declaration(left, right, 'const'), + make.return(make.object(extracted.map(value => make.property(value, value)))) + ]) + + block.scope = state.scope.branch(extracted.map(({ name }) => name)) + + state.write('Object.assign(_, (function() ') + this[block.type](block, state) + state.write(')());') +} + +export function walk({ left, right }, st, c) { + if(right.isNil) { + c(left, st, 'Expression') + return + } + + c(left, st, 'Pattern') + c(right, st, 'Pattern') +} diff --git a/src/Stone/Types/StoneUnset.js b/src/Stone/Types/StoneUnset.js new file mode 100644 index 0000000..f95cc01 --- /dev/null +++ b/src/Stone/Types/StoneUnset.js @@ -0,0 +1,19 @@ +export function generate({ properties }, state) { + let first = true + for(const property of properties) { + if(first) { + first = false + } else { + state.write(state.lineEnd) + state.write(state.indent) + } + + state.write('delete ') + this[property.type](property, state) + state.write(';') + } +} + +export function walk() { + // Do nothing +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index e176bba..7e05bbb 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -12,8 +12,10 @@ export const Types = { StoneOutputBlock: require('./StoneOutputBlock'), StoneOutputExpression: require('./StoneOutputExpression'), StoneSection: require('./StoneSection'), + StoneSet: require('./StoneSet'), StoneSlot: require('./StoneSlot'), StoneSuper: require('./StoneSuper'), StoneTemplate: require('./StoneTemplate'), + StoneUnset: require('./StoneUnset'), StoneYield: require('./StoneYield'), } From 852d34ecadc282d9b2beb2751c2851211338f4a1 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 3 Dec 2017 23:00:50 -0500 Subject: [PATCH 21/37] Merged (most) directive parsers into their respective types --- src/Stone/Parser.js | 25 +++++ src/Stone/Parsers/Assignments.js | 88 ----------------- src/Stone/Parsers/Components.js | 79 --------------- src/Stone/Parsers/Includes.js | 46 --------- src/Stone/Parsers/Layouts.js | 151 ----------------------------- src/Stone/Parsers/Macros.js | 28 ------ src/Stone/Parsers/Output.js | 13 --- src/Stone/Parsers/index.js | 5 - src/Stone/Types/StoneComponent.js | 44 ++++++++- src/Stone/Types/StoneDump.js | 15 +++ src/Stone/Types/StoneEach.js | 23 +++++ src/Stone/Types/StoneExtends.js | 32 ++++++ src/Stone/Types/StoneHasSection.js | 21 ++++ src/Stone/Types/StoneInclude.js | 28 ++++++ src/Stone/Types/StoneMacro.js | 32 ++++++ src/Stone/Types/StoneSection.js | 59 +++++++++++ src/Stone/Types/StoneSet.js | 78 +++++++++++++++ src/Stone/Types/StoneSlot.js | 44 +++++++++ src/Stone/Types/StoneSuper.js | 23 ++++- src/Stone/Types/StoneUnset.js | 15 +++ src/Stone/Types/StoneYield.js | 28 ++++++ 21 files changed, 465 insertions(+), 412 deletions(-) delete mode 100644 src/Stone/Parsers/Assignments.js delete mode 100644 src/Stone/Parsers/Components.js delete mode 100644 src/Stone/Parsers/Includes.js delete mode 100644 src/Stone/Parsers/Layouts.js delete mode 100644 src/Stone/Parsers/Macros.js diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index 014eb66..5a23600 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -1,4 +1,5 @@ import './Parsers' +import './Types' import './Contexts/DirectiveArgs' import './Contexts/PreserveSpace' @@ -289,3 +290,27 @@ export class Parser { for(const [ name, func ] of Object.entries(Parsers)) { Parser.prototype[name] = func } + +// Inject parsers for each type +for(const type of Object.values(Types)) { + if(!type.parsers.isNil) { + for(const [ name, func ] of Object.entries(type.parsers)) { + Parser.prototype[name] = func + } + } + + if(type.directive.isNil || typeof type.parse !== 'function') { + continue + } + + const directive = type.directive[0].toUpperCase() + type.directive.substring(1) + Parser.prototype[`parse${directive}Directive`] = type.parse + + if(typeof type.parseArgs === 'function') { + Parser.prototype[`parse${directive}DirectiveArgs`] = type.parseArgs + } + + if(type.hasEndDirective && typeof type.parseEnd === 'function') { + Parser.prototype[`parseEnd${type.directive}Directive`] = type.parseEnd + } +} diff --git a/src/Stone/Parsers/Assignments.js b/src/Stone/Parsers/Assignments.js deleted file mode 100644 index 3131df8..0000000 --- a/src/Stone/Parsers/Assignments.js +++ /dev/null @@ -1,88 +0,0 @@ -export function parseSetDirectiveArgs() { - this.skipSpace() - - let kind = null - - if(this.input.substring(this.pos, this.pos + 6).toLowerCase() === 'const ') { - kind = 'const' - } else if(this.input.substring(this.pos, this.pos + 4).toLowerCase() === 'let ') { - kind = 'let' - } else if(this.input.substring(this.pos, this.pos + 4).toLowerCase() === 'var ') { - this.raise(this.start, '`@set` does not support `var`') - } else { - return this.parseDirectiveArgs() - } - - this.pos += kind.length - - const node = this.parseDirectiveArgs() - node.kind = kind - return node -} - -/** - * Sets a context variable - * - * @param {object} context Context for the compilation - * @param {string} args Arguments to set - * @return {string} Code to set the context variable - */ -export function parseSetDirective(node, args) { - const kind = args.kind || null - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@set` must contain at least 1 argument') - } else if(args.length > 2) { - this.raise(this.start, '`@set` cannot contain more than 2 arguments') - } - - if(args.length === 1 && args[0].type === 'AssignmentExpression') { - Object.assign(node, args[0]) - } else { - node.operator = '=' - node.left = args[0] - node.right = args[1] - } - - node.kind = kind - expressionToPattern(node.left) - - this.next() - return this.finishNode(node, 'StoneSet') -} - -/** - * Unsets a context variable - * - * @param {object} context Context for the compilation - * @param {string} args Arguments to unset - * @return {string} Code to set the context variable - */ -export function parseUnsetDirective(node, args) { - node.properties = this._flattenArgs(args) - this.next() - return this.finishNode(node, 'StoneUnset') -} - -/** - * `parseSetDirectiveArgs` gets parsed into SequenceExpression - * which parses destructuring into Array/Object expressions - * instead of patterns - */ -function expressionToPattern(node) { - if(node.isNil) { - return - } - - if(node.type === 'ArrayExpression') { - node.type = 'ArrayPattern' - node.elements.forEach(expressionToPattern) - } else if(node.type === 'ObjectExpression') { - node.type = 'ObjectPattern' - - for(const property of node.properties) { - expressionToPattern(property.value) - } - } -} diff --git a/src/Stone/Parsers/Components.js b/src/Stone/Parsers/Components.js deleted file mode 100644 index f2a0b69..0000000 --- a/src/Stone/Parsers/Components.js +++ /dev/null @@ -1,79 +0,0 @@ -export function parseComponentDirective(node, args) { - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@component` must contain at least 1 argument') - } - - node.view = args.shift() - - if(args.length > 1) { - this.raise(this.start, '`@component` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.context = args.pop() - } - - (this._currentComponent = (this._currentComponent || [ ])).push(node) - - const output = this.startNode() - output.params = args - output.body = this.parseUntilEndDirective('endcomponent') - node.output = this.finishNode(output, 'StoneOutputBlock') - - return this.finishNode(node, 'StoneComponent') -} - -/** - * Ends the current component and returns output - * @return {string} Output from the component - */ -export function parseEndcomponentDirective(node) { - if(!this._currentComponent || this._currentComponent.length === 0) { - this.raise(this.start, '`@endcomponent` outside of `@component`') - } - - this._currentComponent.pop() - - return this.finishNode(node, 'Directive') -} - -export function parseSlotDirective(node, args) { - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@slot` must contain at least 1 argument') - } - - node.id = args.shift() - - if(args.length > 1) { - this.raise(this.start, '`@slot` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.output = args.pop() - node.inline = true - this.next() - } else { - (this._currentSlot = (this._currentSlot || [ ])).push(node) - - const output = this.startNode() - output.params = args - output.body = this.parseUntilEndDirective('endslot') - node.output = this.finishNode(output, 'StoneOutputBlock') - } - - return this.finishNode(node, 'StoneSlot') -} - -/** - * Ends the current slot and returns output - * @return {string} Output from the slot - */ -export function parseEndslotDirective(node) { - if(!this._currentSlot || this._currentSlot.length === 0) { - this.raise(this.start, '`@endslot` outside of `@slot`') - } - - this._currentSlot.pop() - - return this.finishNode(node, 'Directive') -} diff --git a/src/Stone/Parsers/Includes.js b/src/Stone/Parsers/Includes.js deleted file mode 100644 index 18ce5d9..0000000 --- a/src/Stone/Parsers/Includes.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Renders content from a subview - * - * @param {object} node Blank node - * @param {mixed} args View name and optional context - * @return {object} Finished node - */ -export function parseIncludeDirective(node, args) { - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@include` must contain at least 1 argument') - } - - node.view = args.shift() - - if(args.length > 1) { - this.raise(this.start, '`@include` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.context = args.shift() - } - - this.next() - return this.finishNode(node, 'StoneInclude') -} - -/** - * Compiles each directive to call the runtime and output - * the result. - * - * @param {object} node Blank node - * @param {mixed} params Arguments to pass through to runtime - * @return {object} Finished node - */ -export function parseEachDirective(node, params) { - node.params = this._flattenArgs(params) - - if(node.params.length < 3) { - this.raise(this.start, '`@each` must contain at least 3 arguments') - } else if(node.params.length > 5) { - this.raise(this.start, '`@each` cannot contain more than 5 arguments') - } - - this.next() - return this.finishNode(node, 'StoneEach') -} diff --git a/src/Stone/Parsers/Layouts.js b/src/Stone/Parsers/Layouts.js deleted file mode 100644 index 8a15a71..0000000 --- a/src/Stone/Parsers/Layouts.js +++ /dev/null @@ -1,151 +0,0 @@ -import { endDirectives } from './Conditionals' - -export function parseExtendsDirective(node, args) { - if(this._stoneTemplate.isNil) { - this.unexpected() - } - - if(this._stoneTemplate.isLayout === true) { - this.raise(this.start, '`@extends` may only be called once per view.') - } else { - this._stoneTemplate.isLayout = true - } - - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@extends` must contain at least 1 argument') - } - - node.view = args.shift() - - if(args.length > 1) { - this.raise(this.start, '`@extends` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.context = args.shift() - this._stoneTemplate.hasLayoutContext = true - } - - this.next() - return this.finishNode(node, 'StoneExtends') -} - -export function parseSectionDirective(node, args) { - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@section` must contain at least 1 argument') - } - - node.id = args.shift() - - if(args.length > 1) { - this.raise(this.start, '`@section` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.output = args.pop() - node.inline = true - this.next() - } else { - (this._currentSection = (this._currentSection || [ ])).push(node) - - const output = this.startNode() - output.params = args - output.body = this.parseUntilEndDirective([ 'show', 'endsection' ]) - node.output = this.finishNode(output, 'StoneOutputBlock') - } - - return this.finishNode(node, 'StoneSection') -} - -/** - * Ends the current section and returns output - * @return {string} Output from the section - */ -export function parseEndsectionDirective(node) { - if(!this._currentSection || this._currentSection.length === 0) { - this.raise(this.start, '`@endsection` outside of `@section`') - } - - this._currentSection.pop() - - return this.finishNode(node, 'Directive') -} - -/** - * Ends the current section and yields it for display - * @return {string} Output from the section - */ -export function parseShowDirective(node) { - if(!this._currentSection || this._currentSection.length === 0) { - this.raise(this.start, '`@show` outside of `@section`') - } - - this._currentSection.pop().yield = true - - return this.finishNode(node, 'Directive') -} - -/** - * Compiles the yield directive to output a section - * - * @param {object} context Context for the compilation - * @param {string} section Name of the section to yield - * @return {string} Code to render the section - */ -export function parseYieldDirective(node, args) { - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@yield` must contain at least 1 argument') - } - - node.section = args.shift() - - if(args.length > 1) { - this.raise(this.start, '`@yield` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.output = args.pop() - } - - this.next() - return this.finishNode(node, 'StoneYield') -} - -/** - * Renders content from the section section - * @return {string} Code to render the super section - */ -export function parseSuperDirective(node) { - if(!this._currentSection || this._currentSection.length === 0) { - this.raise(this.start, `\`@${node.directive}\` outside of \`@section\``) - } - - node.section = { ...this._currentSection[this._currentSection.length - 1].id } - return this.finishNode(node, 'StoneSuper') -} - -/** - * Alias of compileSuper for compatibility with Blade - * @return {string} Code to render the super section - */ -export function parseParentDirective(node) { - return this.parseSuperDirective(node) -} - -/** - * Convenience directive to determine if a section has content - * @return {string} If statement that determines if a section has content - */ -export function parseHassectionDirective(node, args) { - args = this._flattenArgs(args) - - if(args.length !== 1) { - this.raise(this.start, '`@hassection` must contain exactly 1 argument') - } - - (this._currentIf = (this._currentIf || [ ])).push(node) - - node.section = args.pop() - node.consequent = this.parseUntilEndDirective(endDirectives) - return this.finishNode(node, 'StoneHasSection') -} diff --git a/src/Stone/Parsers/Macros.js b/src/Stone/Parsers/Macros.js deleted file mode 100644 index e7c30f5..0000000 --- a/src/Stone/Parsers/Macros.js +++ /dev/null @@ -1,28 +0,0 @@ -export function parseMacroDirective(node, args) { - (this._currentMacro = (this._currentMacro || [ ])).push(node) - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@macro` must contain at least 1 argument') - } - - node.id = args.shift() - - const output = this.startNode() - output.rescopeContext = true - output.params = args - output.body = this.parseUntilEndDirective('endmacro') - - node.output = this.finishNode(output, 'StoneOutputBlock') - return this.finishNode(node, 'StoneMacro') -} - -export function parseEndmacroDirective(node) { - if(!this._currentMacro || this._currentMacro.length === 0) { - this.raise(this.start, '`@endmacro` outside of `@macro`') - } - - this._currentMacro.pop() - - return this.finishNode(node, 'Directive') -} diff --git a/src/Stone/Parsers/Output.js b/src/Stone/Parsers/Output.js index bfde62b..77e8f5e 100644 --- a/src/Stone/Parsers/Output.js +++ b/src/Stone/Parsers/Output.js @@ -3,19 +3,6 @@ import '../Tokens/StoneDirective' const { tokTypes: tt } = require('acorn') -/** - * Displays the contents of an object or value - * - * @param {object} node Blank node - * @param {mixed} value Value to display - * @return {object} Finished node - */ -export function parseDumpDirective(node, value) { - node.value = value - this.next() - return this.finishNode(node, 'StoneDump') -} - /** * Increases the spaceless level * diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index b98cf40..bf92f6f 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -1,10 +1,5 @@ export const Parsers = { - ...require('./Assignments'), - ...require('./Components'), ...require('./Conditionals'), - ...require('./Includes'), - ...require('./Layouts'), ...require('./Loops'), - ...require('./Macros'), ...require('./Output'), } diff --git a/src/Stone/Types/StoneComponent.js b/src/Stone/Types/StoneComponent.js index 90dca53..9838537 100644 --- a/src/Stone/Types/StoneComponent.js +++ b/src/Stone/Types/StoneComponent.js @@ -1,5 +1,47 @@ import { make } from '../Support/MakeNode' +export const directive = 'component' +export const hasEndDirective = true + +export function parse(node, args) { + args = this._flattenArgs(args) + + if(args.length === 0) { + this.raise(this.start, '`@component` must contain at least 1 argument') + } + + node.view = args.shift() + + if(args.length > 1) { + this.raise(this.start, '`@component` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.context = args.pop() + } + + (this._currentComponent = (this._currentComponent || [ ])).push(node) + + const output = this.startNode() + output.params = args + output.body = this.parseUntilEndDirective('endcomponent') + node.output = this.finishNode(output, 'StoneOutputBlock') + + return this.finishNode(node, 'StoneComponent') +} + +/** + * Ends the current component and returns output + * @return {string} Output from the component + */ +export function parseEnd(node) { + if(!this._currentComponent || this._currentComponent.length === 0) { + this.raise(this.start, '`@endcomponent` outside of `@component`') + } + + this._currentComponent.pop() + + return this.finishNode(node, 'Directive') +} + export function generate(node, state) { node.output.assignments = node.output.assignments || [ ] @@ -44,7 +86,7 @@ export function generate(node, state) { } export function walk(node, st, c) { - + // TODO } export function scope(node, scope) { diff --git a/src/Stone/Types/StoneDump.js b/src/Stone/Types/StoneDump.js index e41c96c..052fe24 100644 --- a/src/Stone/Types/StoneDump.js +++ b/src/Stone/Types/StoneDump.js @@ -1,3 +1,18 @@ +export const directive = 'dump' + +/** + * Displays the contents of an object or value + * + * @param {object} node Blank node + * @param {mixed} value Value to display + * @return {object} Finished node + */ +export function parse(node, value) { + node.value = value + this.next() + return this.finishNode(node, 'StoneDump') +} + export function generate({ value }, state) { state.write('output += `
${_.escape(_.stringify(')
 	this[value.type](value, state)
diff --git a/src/Stone/Types/StoneEach.js b/src/Stone/Types/StoneEach.js
index db5b8b1..2723e0e 100644
--- a/src/Stone/Types/StoneEach.js
+++ b/src/Stone/Types/StoneEach.js
@@ -1,3 +1,26 @@
+export const directive = 'each'
+
+/**
+ * Compiles each directive to call the runtime and output
+ * the result.
+ *
+ * @param  {object} node   Blank node
+ * @param  {mixed}  params Arguments to pass through to runtime
+ * @return {object}        Finished node
+ */
+export function parse(node, params) {
+	node.params = this._flattenArgs(params)
+
+	if(node.params.length < 3) {
+		this.raise(this.start, '`@each` must contain at least 3 arguments')
+	} else if(node.params.length > 5) {
+		this.raise(this.start, '`@each` cannot contain more than 5 arguments')
+	}
+
+	this.next()
+	return this.finishNode(node, 'StoneEach')
+}
+
 export function generate(node, state) {
 	node.params.unshift({
 		type: 'Identifier',
diff --git a/src/Stone/Types/StoneExtends.js b/src/Stone/Types/StoneExtends.js
index 5cac43b..070aca1 100644
--- a/src/Stone/Types/StoneExtends.js
+++ b/src/Stone/Types/StoneExtends.js
@@ -1,3 +1,35 @@
+export const directive = 'extends'
+
+export function parse(node, args) {
+	if(this._stoneTemplate.isNil) {
+		this.unexpected()
+	}
+
+	if(this._stoneTemplate.isLayout === true) {
+		this.raise(this.start, '`@extends` may only be called once per view.')
+	} else {
+		this._stoneTemplate.isLayout = true
+	}
+
+	args = this._flattenArgs(args)
+
+	if(args.length === 0) {
+		this.raise(this.start, '`@extends` must contain at least 1 argument')
+	}
+
+	node.view = args.shift()
+
+	if(args.length > 1) {
+		this.raise(this.start, '`@extends` cannot contain more than 2 arguments')
+	} else if(args.length === 1) {
+		node.context = args.shift()
+		this._stoneTemplate.hasLayoutContext = true
+	}
+
+	this.next()
+	return this.finishNode(node, 'StoneExtends')
+}
+
 export function generate(node, state) {
 	state.write('__extendsLayout = ')
 	this[node.view.type](node.view, state)
diff --git a/src/Stone/Types/StoneHasSection.js b/src/Stone/Types/StoneHasSection.js
index 8dea4aa..a7dde09 100644
--- a/src/Stone/Types/StoneHasSection.js
+++ b/src/Stone/Types/StoneHasSection.js
@@ -1,3 +1,24 @@
+import { endDirectives } from '../Parsers/Conditionals'
+
+/**
+ * Convenience directive to determine if a section has content
+ */
+export const directive = 'hassection'
+
+export function parse(node, args) {
+	args = this._flattenArgs(args)
+
+	if(args.length !== 1) {
+		this.raise(this.start, '`@hassection` must contain exactly 1 argument')
+	}
+
+	(this._currentIf = (this._currentIf || [ ])).push(node)
+
+	node.section = args.pop()
+	node.consequent = this.parseUntilEndDirective(endDirectives)
+	return this.finishNode(node, 'StoneHasSection')
+}
+
 export function generate(node, state) {
 	node.test = {
 		type: 'CallExpression',
diff --git a/src/Stone/Types/StoneInclude.js b/src/Stone/Types/StoneInclude.js
index 96c3bc6..c1d2402 100644
--- a/src/Stone/Types/StoneInclude.js
+++ b/src/Stone/Types/StoneInclude.js
@@ -1,3 +1,31 @@
+export const directive = 'include'
+
+/**
+ * Renders content from a subview
+ *
+ * @param  {object} node Blank node
+ * @param  {mixed}  args View name and optional context
+ * @return {object}      Finished node
+ */
+export function parse(node, args) {
+	args = this._flattenArgs(args)
+
+	if(args.length === 0) {
+		this.raise(this.start, '`@include` must contain at least 1 argument')
+	}
+
+	node.view = args.shift()
+
+	if(args.length > 1) {
+		this.raise(this.start, '`@include` cannot contain more than 2 arguments')
+	} else if(args.length === 1) {
+		node.context = args.shift()
+	}
+
+	this.next()
+	return this.finishNode(node, 'StoneInclude')
+}
+
 export function generate(node, state) {
 	state.write('output += _.$stone.include(_, _sections, _templatePathname, ')
 	this[node.view.type](node.view, state)
diff --git a/src/Stone/Types/StoneMacro.js b/src/Stone/Types/StoneMacro.js
index bff9e0a..376e05f 100644
--- a/src/Stone/Types/StoneMacro.js
+++ b/src/Stone/Types/StoneMacro.js
@@ -1,3 +1,35 @@
+export const directive = 'macro'
+export const hasEndDirective = true
+
+export function parse(node, args) {
+	(this._currentMacro = (this._currentMacro || [ ])).push(node)
+	args = this._flattenArgs(args)
+
+	if(args.length === 0) {
+		this.raise(this.start, '`@macro` must contain at least 1 argument')
+	}
+
+	node.id = args.shift()
+
+	const output = this.startNode()
+	output.rescopeContext = true
+	output.params = args
+	output.body = this.parseUntilEndDirective('endmacro')
+
+	node.output = this.finishNode(output, 'StoneOutputBlock')
+	return this.finishNode(node, 'StoneMacro')
+}
+
+export function parseEnd(node) {
+	if(!this._currentMacro || this._currentMacro.length === 0) {
+		this.raise(this.start, '`@endmacro` outside of `@macro`')
+	}
+
+	this._currentMacro.pop()
+
+	return this.finishNode(node, 'Directive')
+}
+
 export function generate(node, state) {
 	state.write('_[')
 	this[node.id.type](node.id, state)
diff --git a/src/Stone/Types/StoneSection.js b/src/Stone/Types/StoneSection.js
index 98b299b..8f7ab11 100644
--- a/src/Stone/Types/StoneSection.js
+++ b/src/Stone/Types/StoneSection.js
@@ -1,3 +1,62 @@
+export const directive = 'section'
+export const hasEndDirective = true
+export const parsers = { parseShowDirective }
+
+export function parse(node, args) {
+	args = this._flattenArgs(args)
+
+	if(args.length === 0) {
+		this.raise(this.start, '`@section` must contain at least 1 argument')
+	}
+
+	node.id = args.shift()
+
+	if(args.length > 1) {
+		this.raise(this.start, '`@section` cannot contain more than 2 arguments')
+	} else if(args.length === 1) {
+		node.output = args.pop()
+		node.inline = true
+		this.next()
+	} else {
+		(this._currentSection = (this._currentSection || [ ])).push(node)
+
+		const output = this.startNode()
+		output.params = args
+		output.body = this.parseUntilEndDirective([ 'show', 'endsection' ])
+		node.output = this.finishNode(output, 'StoneOutputBlock')
+	}
+
+	return this.finishNode(node, 'StoneSection')
+}
+
+/**
+ * Ends the current section and returns output
+ * @return {string} Output from the section
+ */
+export function parseEnd(node) {
+	if(!this._currentSection || this._currentSection.length === 0) {
+		this.raise(this.start, '`@endsection` outside of `@section`')
+	}
+
+	this._currentSection.pop()
+
+	return this.finishNode(node, 'Directive')
+}
+
+/**
+ * Ends the current section and yields it for display
+ * @return {string} Output from the section
+ */
+export function parseShowDirective(node) {
+	if(!this._currentSection || this._currentSection.length === 0) {
+		this.raise(this.start, '`@show` outside of `@section`')
+	}
+
+	this._currentSection.pop().yield = true
+
+	return this.finishNode(node, 'Directive')
+}
+
 export function generate(node, state) {
 	state.write('_sections.push(')
 	this[node.id.type](node.id, state)
diff --git a/src/Stone/Types/StoneSet.js b/src/Stone/Types/StoneSet.js
index 56a336a..a201b9c 100644
--- a/src/Stone/Types/StoneSet.js
+++ b/src/Stone/Types/StoneSet.js
@@ -1,6 +1,62 @@
 import '../../Stone'
 import { make } from '../Support/MakeNode'
 
+export const directive = 'set'
+
+export function parseArgs() {
+	this.skipSpace()
+
+	let kind = null
+
+	if(this.input.substring(this.pos, this.pos + 6).toLowerCase() === 'const ') {
+		kind = 'const'
+	} else if(this.input.substring(this.pos, this.pos + 4).toLowerCase() === 'let ') {
+		kind = 'let'
+	} else if(this.input.substring(this.pos, this.pos + 4).toLowerCase() === 'var ') {
+		this.raise(this.start, '`@set` does not support `var`')
+	} else {
+		return this.parseDirectiveArgs()
+	}
+
+	this.pos += kind.length
+
+	const node = this.parseDirectiveArgs()
+	node.kind = kind
+	return node
+}
+
+/**
+ * Sets a context variable
+ *
+ * @param  {object} context Context for the compilation
+ * @param  {string} args    Arguments to set
+ * @return {string} Code to set the context variable
+ */
+export function parse(node, args) {
+	const kind = args.kind || null
+	args = this._flattenArgs(args)
+
+	if(args.length === 0) {
+		this.raise(this.start, '`@set` must contain at least 1 argument')
+	} else if(args.length > 2) {
+		this.raise(this.start, '`@set` cannot contain more than 2 arguments')
+	}
+
+	if(args.length === 1 && args[0].type === 'AssignmentExpression') {
+		Object.assign(node, args[0])
+	} else {
+		node.operator = '='
+		node.left = args[0]
+		node.right = args[1]
+	}
+
+	node.kind = kind
+	expressionToPattern(node.left)
+
+	this.next()
+	return this.finishNode(node, 'StoneSet')
+}
+
 export function generate({ kind, left, right }, state) {
 	if(right.isNil) {
 		this[left.type](left, state)
@@ -49,3 +105,25 @@ export function walk({ left, right }, st, c) {
 	c(left, st, 'Pattern')
 	c(right, st, 'Pattern')
 }
+
+/**
+ * `parseSetDirectiveArgs` gets parsed into SequenceExpression
+ * which parses destructuring into Array/Object expressions
+ * instead of patterns
+ */
+function expressionToPattern(node) {
+	if(node.isNil) {
+		return
+	}
+
+	if(node.type === 'ArrayExpression') {
+		node.type = 'ArrayPattern'
+		node.elements.forEach(expressionToPattern)
+	} else if(node.type === 'ObjectExpression') {
+		node.type = 'ObjectPattern'
+
+		for(const property of node.properties) {
+			expressionToPattern(property.value)
+		}
+	}
+}
diff --git a/src/Stone/Types/StoneSlot.js b/src/Stone/Types/StoneSlot.js
index a6dca2c..ba61b9c 100644
--- a/src/Stone/Types/StoneSlot.js
+++ b/src/Stone/Types/StoneSlot.js
@@ -1,3 +1,47 @@
+export const directive = 'slot'
+export const hasEndDirective = true
+
+export function parse(node, args) {
+	args = this._flattenArgs(args)
+
+	if(args.length === 0) {
+		this.raise(this.start, '`@slot` must contain at least 1 argument')
+	}
+
+	node.id = args.shift()
+
+	if(args.length > 1) {
+		this.raise(this.start, '`@slot` cannot contain more than 2 arguments')
+	} else if(args.length === 1) {
+		node.output = args.pop()
+		node.inline = true
+		this.next()
+	} else {
+		(this._currentSlot = (this._currentSlot || [ ])).push(node)
+
+		const output = this.startNode()
+		output.params = args
+		output.body = this.parseUntilEndDirective('endslot')
+		node.output = this.finishNode(output, 'StoneOutputBlock')
+	}
+
+	return this.finishNode(node, 'StoneSlot')
+}
+
+/**
+ * Ends the current slot and returns output
+ * @return {string} Output from the slot
+ */
+export function parseEnd(node) {
+	if(!this._currentSlot || this._currentSlot.length === 0) {
+		this.raise(this.start, '`@endslot` outside of `@slot`')
+	}
+
+	this._currentSlot.pop()
+
+	return this.finishNode(node, 'Directive')
+}
+
 export function generate(node, state) {
 	state.write('__componentContext[')
 	this[node.id.type](node.id, state)
diff --git a/src/Stone/Types/StoneSuper.js b/src/Stone/Types/StoneSuper.js
index c03a034..6ad180d 100644
--- a/src/Stone/Types/StoneSuper.js
+++ b/src/Stone/Types/StoneSuper.js
@@ -1,4 +1,25 @@
+export const directive = 'super'
+
+/**
+ * Alias of compileSuper for compatibility with Blade
+ * @return {string} Code to render the super section
+ */
+export const parsers = { parseParentDirective: parse }
+
+/**
+ * Renders content from the section section
+ * @return {string} Code to render the super section
+ */
+export function parse(node) {
+	if(!this._currentSection || this._currentSection.length === 0) {
+		this.raise(this.start, `\`@${node.directive}\` outside of \`@section\``)
+	}
+
+	node.section = { ...this._currentSection[this._currentSection.length - 1].id }
+	return this.finishNode(node, 'StoneSuper')
+}
+
 // Due to how sections work, we can cheat by treating as yield
 // which will pop off the next chunk of content in the section
 // and render it within ours
-export * from './StoneYield'
+export { generate, walk, scope } from './StoneYield'
diff --git a/src/Stone/Types/StoneUnset.js b/src/Stone/Types/StoneUnset.js
index f95cc01..3a4cde6 100644
--- a/src/Stone/Types/StoneUnset.js
+++ b/src/Stone/Types/StoneUnset.js
@@ -1,3 +1,18 @@
+export const directive = 'unset'
+
+/**
+ * Unsets a context variable
+ *
+ * @param  {object} context Context for the compilation
+ * @param  {string} args    Arguments to unset
+ * @return {string} Code to set the context variable
+ */
+export function parse(node, args) {
+	node.properties = this._flattenArgs(args)
+	this.next()
+	return this.finishNode(node, 'StoneUnset')
+}
+
 export function generate({ properties }, state) {
 	let first = true
 	for(const property of properties) {
diff --git a/src/Stone/Types/StoneYield.js b/src/Stone/Types/StoneYield.js
index 3f09442..128d033 100644
--- a/src/Stone/Types/StoneYield.js
+++ b/src/Stone/Types/StoneYield.js
@@ -1,3 +1,31 @@
+export const directive = 'yield'
+
+/**
+ * Compiles the yield directive to output a section
+ *
+ * @param  {object} context Context for the compilation
+ * @param  {string} section Name of the section to yield
+ * @return {string}         Code to render the section
+ */
+export function parse(node, args) {
+	args = this._flattenArgs(args)
+
+	if(args.length === 0) {
+		this.raise(this.start, '`@yield` must contain at least 1 argument')
+	}
+
+	node.section = args.shift()
+
+	if(args.length > 1) {
+		this.raise(this.start, '`@yield` cannot contain more than 2 arguments')
+	} else if(args.length === 1) {
+		node.output = args.pop()
+	}
+
+	this.next()
+	return this.finishNode(node, 'StoneYield')
+}
+
 export function generate(node, state) {
 	state.write('output += _sections.render(')
 	this[node.section.type](node.section, state)

From e4e62d1876aaf660d02a67db4d918a76102ad9f0 Mon Sep 17 00:00:00 2001
From: shnhrrsn 
Date: Mon, 4 Dec 2017 01:04:43 -0500
Subject: [PATCH 22/37] Refactored custom types to be class based and extend
 from StoneType

---
 src/Stone/Generator.js                   |   4 +-
 src/Stone/Parser.js                      |  21 +--
 src/Stone/Scoper.js                      |  10 +-
 src/Stone/Types/StoneComponent.js        | 159 ++++++++---------
 src/Stone/Types/StoneDirectiveType.js    |  30 ++++
 src/Stone/Types/StoneDump.js             |  46 ++---
 src/Stone/Types/StoneEach.js             |  76 ++++----
 src/Stone/Types/StoneEmptyExpression.js  |  16 +-
 src/Stone/Types/StoneExtends.js          |  96 +++++-----
 src/Stone/Types/StoneHasSection.js       |  72 ++++----
 src/Stone/Types/StoneInclude.js          |  88 +++++-----
 src/Stone/Types/StoneLoop.js             | 132 +++++++-------
 src/Stone/Types/StoneMacro.js            |  73 ++++----
 src/Stone/Types/StoneOutput.js           |  26 +--
 src/Stone/Types/StoneOutputBlock.js      | 196 +++++++++++----------
 src/Stone/Types/StoneOutputExpression.js |  32 ++--
 src/Stone/Types/StoneParent.js           |  10 ++
 src/Stone/Types/StoneSection.js          | 134 +++++++-------
 src/Stone/Types/StoneSet.js              | 202 ++++++++++-----------
 src/Stone/Types/StoneShow.js             |  29 +++
 src/Stone/Types/StoneSlot.js             | 115 ++++++------
 src/Stone/Types/StoneSuper.js            |  39 ++---
 src/Stone/Types/StoneTemplate.js         | 214 ++++++++++++-----------
 src/Stone/Types/StoneType.js             |  42 +++++
 src/Stone/Types/StoneUnset.js            |  60 ++++---
 src/Stone/Types/StoneYield.js            |  88 +++++-----
 src/Stone/Types/index.js                 |  40 +++--
 src/Stone/Walker.js                      |   4 +-
 28 files changed, 1113 insertions(+), 941 deletions(-)
 create mode 100644 src/Stone/Types/StoneDirectiveType.js
 create mode 100644 src/Stone/Types/StoneParent.js
 create mode 100644 src/Stone/Types/StoneShow.js
 create mode 100644 src/Stone/Types/StoneType.js

diff --git a/src/Stone/Generator.js b/src/Stone/Generator.js
index 40af40d..945be6e 100644
--- a/src/Stone/Generator.js
+++ b/src/Stone/Generator.js
@@ -83,6 +83,6 @@ for(const key of [
 	}
 }
 
-for(const key of Object.keys(Types)) {
-	Generator[key] = Types[key].generate.bind(Generator)
+for(const type of Object.values(Types)) {
+	type.registerGenerate(Generator)
 }
diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js
index 5a23600..0231772 100644
--- a/src/Stone/Parser.js
+++ b/src/Stone/Parser.js
@@ -293,24 +293,5 @@ for(const [ name, func ] of Object.entries(Parsers)) {
 
 // Inject parsers for each type
 for(const type of Object.values(Types)) {
-	if(!type.parsers.isNil) {
-		for(const [ name, func ] of Object.entries(type.parsers)) {
-			Parser.prototype[name] = func
-		}
-	}
-
-	if(type.directive.isNil || typeof type.parse !== 'function') {
-		continue
-	}
-
-	const directive = type.directive[0].toUpperCase() + type.directive.substring(1)
-	Parser.prototype[`parse${directive}Directive`] = type.parse
-
-	if(typeof type.parseArgs === 'function') {
-		Parser.prototype[`parse${directive}DirectiveArgs`] = type.parseArgs
-	}
-
-	if(type.hasEndDirective && typeof type.parseEnd === 'function') {
-		Parser.prototype[`parseEnd${type.directive}Directive`] = type.parseEnd
-	}
+	type.registerParse(Parser.prototype)
 }
diff --git a/src/Stone/Scoper.js b/src/Stone/Scoper.js
index 17693eb..be1ac6d 100644
--- a/src/Stone/Scoper.js
+++ b/src/Stone/Scoper.js
@@ -165,12 +165,6 @@ export class Scoper {
 
 }
 
-for(const key of Object.keys(Types)) {
-	const scope = Types[key].scope
-
-	if(typeof scope !== 'function') {
-		continue
-	}
-
-	Scoper[key] = scope.bind(Scoper)
+for(const type of Object.values(Types)) {
+	type.registerScope(Scoper)
 }
diff --git a/src/Stone/Types/StoneComponent.js b/src/Stone/Types/StoneComponent.js
index 9838537..a7e72dc 100644
--- a/src/Stone/Types/StoneComponent.js
+++ b/src/Stone/Types/StoneComponent.js
@@ -1,99 +1,102 @@
-import { make } from '../Support/MakeNode'
+import './StoneDirectiveType'
 
-export const directive = 'component'
-export const hasEndDirective = true
+export class StoneComponent extends StoneDirectiveType {
 
-export function parse(node, args) {
-	args = this._flattenArgs(args)
+	static directive = 'component'
 
-	if(args.length === 0) {
-		this.raise(this.start, '`@component` must contain at least 1 argument')
-	}
+	static parse(parser, node, args) {
+		args = parser._flattenArgs(args)
 
-	node.view = args.shift()
+		if(args.length === 0) {
+			parser.raise(parser.start, '`@component` must contain at least 1 argument')
+		}
 
-	if(args.length > 1) {
-		this.raise(this.start, '`@component` cannot contain more than 2 arguments')
-	} else if(args.length === 1) {
-		node.context = args.pop()
-	}
+		node.view = args.shift()
 
-	(this._currentComponent = (this._currentComponent || [ ])).push(node)
+		if(args.length > 1) {
+			parser.raise(parser.start, '`@component` cannot contain more than 2 arguments')
+		} else if(args.length === 1) {
+			node.context = args.pop()
+		}
 
-	const output = this.startNode()
-	output.params = args
-	output.body = this.parseUntilEndDirective('endcomponent')
-	node.output = this.finishNode(output, 'StoneOutputBlock')
+		(parser._currentComponent = (parser._currentComponent || [ ])).push(node)
 
-	return this.finishNode(node, 'StoneComponent')
-}
+		const output = parser.startNode()
+		output.params = args
+		output.body = parser.parseUntilEndDirective('endcomponent')
+		node.output = parser.finishNode(output, 'StoneOutputBlock')
 
-/**
- * Ends the current component and returns output
- * @return {string} Output from the component
- */
-export function parseEnd(node) {
-	if(!this._currentComponent || this._currentComponent.length === 0) {
-		this.raise(this.start, '`@endcomponent` outside of `@component`')
+		return parser.finishNode(node, 'StoneComponent')
 	}
 
-	this._currentComponent.pop()
+	/**
+	 * Ends the current component and returns output
+	 * @return {string} Output from the component
+	 */
+	static parseEnd(parser, node) {
+		if(!parser._currentComponent || parser._currentComponent.length === 0) {
+			parser.raise(parser.start, '`@endcomponent` outside of `@component`')
+		}
 
-	return this.finishNode(node, 'Directive')
-}
+		parser._currentComponent.pop()
+
+		return parser.finishNode(node, 'Directive')
+	}
+
+	static generate(generator, node, state) {
+		node.output.assignments = node.output.assignments || [ ]
 
-export function generate(node, state) {
-	node.output.assignments = node.output.assignments || [ ]
-
-	node.output.assignments.push({
-		kind: 'const',
-		left: make.identifier('__componentView'),
-		right: node.view
-	})
-
-	node.output.assignments.push({
-		kind: 'const',
-		left: make.identifier('__componentContext'),
-		right: !node.context.isNil ? node.context : make.object()
-	})
-
-	node.output.return = {
-		type: 'CallExpression',
-		callee: {
-			type: 'MemberExpression',
-			object: {
+		node.output.assignments.push({
+			kind: 'const',
+			left: this.make.identifier('__componentView'),
+			right: node.view
+		})
+
+		node.output.assignments.push({
+			kind: 'const',
+			left: this.make.identifier('__componentContext'),
+			right: !node.context.isNil ? node.context : this.make.object()
+		})
+
+		node.output.return = {
+			type: 'CallExpression',
+			callee: {
 				type: 'MemberExpression',
-				object: make.identifier('_'),
-				property: make.identifier('$stone'),
+				object: {
+					type: 'MemberExpression',
+					object: this.make.identifier('_'),
+					property: this.make.identifier('$stone'),
+				},
+				property: this.make.identifier('include'),
 			},
-			property: make.identifier('include'),
-		},
-		arguments: [
-			make.identifier('_'),
-			make.null(),
-			make.identifier('_templatePathname'),
-			make.identifier('__componentView'),
-			make.object([
-				make.property('slot', make.new('HtmlString', 'output')),
-				make.spread('__componentContext')
-			])
-		]
+			arguments: [
+				this.make.identifier('_'),
+				this.make.null(),
+				this.make.identifier('_templatePathname'),
+				this.make.identifier('__componentView'),
+				this.make.object([
+					this.make.property('slot', this.make.new('HtmlString', 'output')),
+					this.make.spread('__componentContext')
+				])
+			]
+		}
+
+		state.write('output += (')
+		generator[node.output.type](node.output, state)
+		state.write(')();')
 	}
 
-	state.write('output += (')
-	this[node.output.type](node.output, state)
-	state.write(')();')
-}
+	static walk(walker, node, st, c) {
+		// TODO
+	}
 
-export function walk(node, st, c) {
-	// TODO
-}
+	static scope(scoper, node, scope) {
+		node.scope = scope.branch([
+			'__componentView',
+			'__componentContext'
+		])
 
-export function scope(node, scope) {
-	node.scope = scope.branch([
-		'__componentView',
-		'__componentContext'
-	])
+		scoper._scope(node.output, node.scope)
+	}
 
-	this._scope(node.output, node.scope)
 }
diff --git a/src/Stone/Types/StoneDirectiveType.js b/src/Stone/Types/StoneDirectiveType.js
new file mode 100644
index 0000000..510520a
--- /dev/null
+++ b/src/Stone/Types/StoneDirectiveType.js
@@ -0,0 +1,30 @@
+import './StoneType'
+
+export class StoneDirectiveType extends StoneType {
+
+	static directive = null
+
+	static registerParse(parser) {
+		if(this.directive.isNil) {
+			throw new Error('Directive must be set')
+		}
+
+		const directive = this.directive[0].toUpperCase() + this.directive.substring(1)
+		parser[`parse${directive}Directive`] = this._bind('parse')
+
+		if(typeof this.parseArgs === 'function') {
+			parser[`parse${directive}DirectiveArgs`] = this._bind('parseArgs')
+		}
+
+		if(typeof this.parseEnd === 'function') {
+			parser[`parseEnd${this.directive}Directive`] = this._bind('parseEnd')
+		}
+	}
+
+	// Abstract methods
+
+	static parse(/* parser, node, args */) {
+		throw new Error('Subclasses must implement')
+	}
+
+}
diff --git a/src/Stone/Types/StoneDump.js b/src/Stone/Types/StoneDump.js
index 052fe24..86c5e8d 100644
--- a/src/Stone/Types/StoneDump.js
+++ b/src/Stone/Types/StoneDump.js
@@ -1,24 +1,30 @@
-export const directive = 'dump'
+import './StoneDirectiveType'
 
-/**
- * Displays the contents of an object or value
- *
- * @param  {object} node  Blank node
- * @param  {mixed}  value Value to display
- * @return {object}       Finished node
- */
-export function parse(node, value) {
-	node.value = value
-	this.next()
-	return this.finishNode(node, 'StoneDump')
-}
+export class StoneDump extends StoneDirectiveType {
 
-export function generate({ value }, state) {
-	state.write('output += `
${_.escape(_.stringify(')
-	this[value.type](value, state)
-	state.write(', null, 2))}
`;') -} + static directive = 'dump' + + /** + * Displays the contents of an object or value + * + * @param {object} node Blank node + * @param {mixed} value Value to display + * @return {object} Finished node + */ + static parse(parser, node, value) { + node.value = value + parser.next() + return parser.finishNode(node, 'StoneDump') + } + + static generate(generator, { value }, state) { + state.write('output += `
${_.escape(_.stringify(')
+		generator[value.type](value, state)
+		state.write(', null, 2))}
`;') + } + + static walk(walker, { value }, st, c) { + c(value, st, 'Expression') + } -export function walk({ value }, st, c) { - c(value, st, 'Expression') } diff --git a/src/Stone/Types/StoneEach.js b/src/Stone/Types/StoneEach.js index 2723e0e..f49c520 100644 --- a/src/Stone/Types/StoneEach.js +++ b/src/Stone/Types/StoneEach.js @@ -1,40 +1,46 @@ -export const directive = 'each' - -/** - * Compiles each directive to call the runtime and output - * the result. - * - * @param {object} node Blank node - * @param {mixed} params Arguments to pass through to runtime - * @return {object} Finished node - */ -export function parse(node, params) { - node.params = this._flattenArgs(params) - - if(node.params.length < 3) { - this.raise(this.start, '`@each` must contain at least 3 arguments') - } else if(node.params.length > 5) { - this.raise(this.start, '`@each` cannot contain more than 5 arguments') +import './StoneDirectiveType' + +export class StoneEach extends StoneDirectiveType { + + static directive = 'each' + + /** + * Compiles each directive to call the runtime and output + * the result. + * + * @param {object} node Blank node + * @param {mixed} params Arguments to pass through to runtime + * @return {object} Finished node + */ + static parse(parser, node, params) { + node.params = parser._flattenArgs(params) + + if(node.params.length < 3) { + parser.raise(parser.start, '`@each` must contain at least 3 arguments') + } else if(node.params.length > 5) { + parser.raise(parser.start, '`@each` cannot contain more than 5 arguments') + } + + parser.next() + return parser.finishNode(node, 'StoneEach') } - this.next() - return this.finishNode(node, 'StoneEach') -} + static generate(generator, node, state) { + node.params.unshift({ + type: 'Identifier', + name: '_' + }, { + type: 'Identifier', + name: '_templatePathname' + }) -export function generate(node, state) { - node.params.unshift({ - type: 'Identifier', - name: '_' - }, { - type: 'Identifier', - name: '_templatePathname' - }) - - state.write('output += _.$stone.each') - this.SequenceExpression({ expressions: node.params }, state) - state.write(';') -} + state.write('output += _.$stone.each') + generator.SequenceExpression({ expressions: node.params }, state) + state.write(';') + } + + static walk() { + // Do nothing + } -export function walk() { - // Do nothing } diff --git a/src/Stone/Types/StoneEmptyExpression.js b/src/Stone/Types/StoneEmptyExpression.js index b8a9ab9..d9d9364 100644 --- a/src/Stone/Types/StoneEmptyExpression.js +++ b/src/Stone/Types/StoneEmptyExpression.js @@ -1,7 +1,13 @@ -export function generate() { - // Do nothing -} +import './StoneType' + +export class StoneEmptyExpression extends StoneType { + + static generate() { + // Do nothing + } + + static walk() { + // Do nothing + } -export function walk() { - // Do nothing } diff --git a/src/Stone/Types/StoneExtends.js b/src/Stone/Types/StoneExtends.js index 070aca1..6a6050f 100644 --- a/src/Stone/Types/StoneExtends.js +++ b/src/Stone/Types/StoneExtends.js @@ -1,65 +1,71 @@ -export const directive = 'extends' +import './StoneDirectiveType' -export function parse(node, args) { - if(this._stoneTemplate.isNil) { - this.unexpected() - } +export class StoneExtends extends StoneDirectiveType { - if(this._stoneTemplate.isLayout === true) { - this.raise(this.start, '`@extends` may only be called once per view.') - } else { - this._stoneTemplate.isLayout = true - } + static directive = 'extends' - args = this._flattenArgs(args) + static parse(parser, node, args) { + if(parser._stoneTemplate.isNil) { + parser.unexpected() + } - if(args.length === 0) { - this.raise(this.start, '`@extends` must contain at least 1 argument') - } + if(parser._stoneTemplate.isLayout === true) { + parser.raise(parser.start, '`@extends` may only be called once per view.') + } else { + parser._stoneTemplate.isLayout = true + } + + args = parser._flattenArgs(args) + + if(args.length === 0) { + parser.raise(parser.start, '`@extends` must contain at least 1 argument') + } - node.view = args.shift() + node.view = args.shift() - if(args.length > 1) { - this.raise(this.start, '`@extends` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.context = args.shift() - this._stoneTemplate.hasLayoutContext = true + if(args.length > 1) { + parser.raise(parser.start, '`@extends` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.context = args.shift() + parser._stoneTemplate.hasLayoutContext = true + } + + parser.next() + return parser.finishNode(node, 'StoneExtends') } - this.next() - return this.finishNode(node, 'StoneExtends') -} + static generate(generator, node, state) { + state.write('__extendsLayout = ') + generator[node.view.type](node.view, state) + state.write(';') -export function generate(node, state) { - state.write('__extendsLayout = ') - this[node.view.type](node.view, state) - state.write(';') + if(node.context.isNil) { + return + } - if(node.context.isNil) { - return + state.write(state.lineEnd) + state.write(state.indent) + state.write('__extendsContext = ') + generator[node.context.type](node.context, state) + state.write(';') } - state.write(state.lineEnd) - state.write(state.indent) - state.write('__extendsContext = ') - this[node.context.type](node.context, state) - state.write(';') -} + static walk(walker, node, st, c) { + c(node.view, st, 'Pattern') -export function walk(node, st, c) { - c(node.view, st, 'Pattern') + if(node.context.isNil) { + return + } - if(node.context.isNil) { - return + c(node.context, st, 'Expression') } - c(node.context, st, 'Expression') -} + static scope(scoper, node, scope) { + if(node.context.isNil) { + return + } -export function scope(node, scope) { - if(node.context.isNil) { - return + scoper._scope(node.context, scope) } - this._scope(node.context, scope) } diff --git a/src/Stone/Types/StoneHasSection.js b/src/Stone/Types/StoneHasSection.js index a7dde09..921f7ed 100644 --- a/src/Stone/Types/StoneHasSection.js +++ b/src/Stone/Types/StoneHasSection.js @@ -1,51 +1,57 @@ +import './StoneDirectiveType' + import { endDirectives } from '../Parsers/Conditionals' /** * Convenience directive to determine if a section has content */ -export const directive = 'hassection' +export class StoneHasSection extends StoneDirectiveType { -export function parse(node, args) { - args = this._flattenArgs(args) + static directive = 'hassection' - if(args.length !== 1) { - this.raise(this.start, '`@hassection` must contain exactly 1 argument') - } + static parse(parser, node, args) { + args = parser._flattenArgs(args) - (this._currentIf = (this._currentIf || [ ])).push(node) + if(args.length !== 1) { + parser.raise(parser.start, '`@hassection` must contain exactly 1 argument') + } - node.section = args.pop() - node.consequent = this.parseUntilEndDirective(endDirectives) - return this.finishNode(node, 'StoneHasSection') -} + (parser._currentIf = (parser._currentIf || [ ])).push(node) -export function generate(node, state) { - node.test = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: '_sections' + node.section = args.pop() + node.consequent = parser.parseUntilEndDirective(endDirectives) + return parser.finishNode(node, 'StoneHasSection') + } + + static generate(generator, node, state) { + node.test = { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: '_sections' + }, + property: { + type: 'Identifier', + name: 'has' + } }, - property: { - type: 'Identifier', - name: 'has' - } - }, - arguments: [ node.section ] + arguments: [ node.section ] + } + + return generator.IfStatement(node, state) } - return this.IfStatement(node, state) -} + static walk(walker, node, st, c) { + c(node.section, st, 'Pattern') + c(node.consequence, st, 'Expression') -export function walk(node, st, c) { - c(node.section, st, 'Pattern') - c(node.consequence, st, 'Expression') + if(node.alternate.isNil) { + return + } - if(node.alternate.isNil) { - return + c(node.alternate, st, 'Expression') } - c(node.alternate, st, 'Expression') } diff --git a/src/Stone/Types/StoneInclude.js b/src/Stone/Types/StoneInclude.js index c1d2402..b83a7ed 100644 --- a/src/Stone/Types/StoneInclude.js +++ b/src/Stone/Types/StoneInclude.js @@ -1,57 +1,63 @@ -export const directive = 'include' - -/** - * Renders content from a subview - * - * @param {object} node Blank node - * @param {mixed} args View name and optional context - * @return {object} Finished node - */ -export function parse(node, args) { - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@include` must contain at least 1 argument') - } +import './StoneDirectiveType' + +export class StoneInclude extends StoneDirectiveType { + + static directive = 'include' + + /** + * Renders content from a subview + * + * @param {object} node Blank node + * @param {mixed} args View name and optional context + * @return {object} Finished node + */ + static parse(parser, node, args) { + args = parser._flattenArgs(args) + + if(args.length === 0) { + parser.raise(parser.start, '`@include` must contain at least 1 argument') + } - node.view = args.shift() + node.view = args.shift() - if(args.length > 1) { - this.raise(this.start, '`@include` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.context = args.shift() + if(args.length > 1) { + parser.raise(parser.start, '`@include` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.context = args.shift() + } + + parser.next() + return parser.finishNode(node, 'StoneInclude') } - this.next() - return this.finishNode(node, 'StoneInclude') -} + static generate(generator, node, state) { + state.write('output += _.$stone.include(_, _sections, _templatePathname, ') + generator[node.view.type](node.view, state) -export function generate(node, state) { - state.write('output += _.$stone.include(_, _sections, _templatePathname, ') - this[node.view.type](node.view, state) + if(!node.context.isNil) { + state.write(', ') + generator[node.context.type](node.context, state) + } - if(!node.context.isNil) { - state.write(', ') - this[node.context.type](node.context, state) + state.write(');') } - state.write(');') -} + static walk(walker, node, st, c) { + c(node.view, st, 'Pattern') -export function walk(node, st, c) { - c(node.view, st, 'Pattern') + if(node.context.isNil) { + return + } - if(node.context.isNil) { - return + c(node.context, st, 'Expression') } - c(node.context, st, 'Expression') -} + static scope(scoper, node, scope) { + if(node.context.isNil) { + return + } -export function scope(node, scope) { - if(node.context.isNil) { - return + scoper._scope(node.context, scope) } - this._scope(node.context, scope) } diff --git a/src/Stone/Types/StoneLoop.js b/src/Stone/Types/StoneLoop.js index 0df3313..bb0cb25 100644 --- a/src/Stone/Types/StoneLoop.js +++ b/src/Stone/Types/StoneLoop.js @@ -1,81 +1,87 @@ -export function generate({ loop }, state) { - // TODO: Future optimizations should check if - // the `loop` var is used before injecting - // support for it. +import './StoneType' - state.__loops = (state.__loops || 0) + 1 - const loopVariable = `__loop${state.__loops}` - loop.scope.add(loopVariable) - loop.body.scope.add('loop') +export class StoneLoop extends StoneType { - state.write(`const ${loopVariable} = new _.StoneLoop(`) + static generate(generator, { loop }, state) { + // TODO: Future optimizations should check if + // the `loop` var is used before injecting + // support for it. - if(loop.type === 'ForInStatement') { - state.write('Object.keys(') - } + state.__loops = (state.__loops || 0) + 1 + const loopVariable = `__loop${state.__loops}` + loop.scope.add(loopVariable) + loop.body.scope.add('loop') - this[loop.right.type](loop.right, state) + state.write(`const ${loopVariable} = new _.StoneLoop(`) - if(loop.type === 'ForInStatement') { - state.write(')') - } + if(loop.type === 'ForInStatement') { + state.write('Object.keys(') + } - state.write(');') - state.write(state.lineEnd) - state.write(state.indent) + generator[loop.right.type](loop.right, state) - state.write(`${loopVariable}.depth = ${state.__loops};`) - state.write(state.lineEnd) - state.write(state.indent) + if(loop.type === 'ForInStatement') { + state.write(')') + } - if(state.__loops > 1) { - state.write(`${loopVariable}.parent = __loop${state.__loops - 1};`) + state.write(');') state.write(state.lineEnd) state.write(state.indent) - } - const positions = { - start: loop.body.start, - end: loop.body.end - } + state.write(`${loopVariable}.depth = ${state.__loops};`) + state.write(state.lineEnd) + state.write(state.indent) - loop.body.body.unshift({ - ...positions, - type: 'VariableDeclaration', - declarations: [ - { - ...positions, - type: 'VariableDeclarator', - id: { - ...positions, - type: 'Identifier', - name: 'loop' - }, - init: { + if(state.__loops > 1) { + state.write(`${loopVariable}.parent = __loop${state.__loops - 1};`) + state.write(state.lineEnd) + state.write(state.indent) + } + + const positions = { + start: loop.body.start, + end: loop.body.end + } + + loop.body.body.unshift({ + ...positions, + type: 'VariableDeclaration', + declarations: [ + { ...positions, - type: 'Identifier', - name: loopVariable + type: 'VariableDeclarator', + id: { + ...positions, + type: 'Identifier', + name: 'loop' + }, + init: { + ...positions, + type: 'Identifier', + name: loopVariable + } } + ], + kind: 'const' + }) + + generator.ForOfStatement({ + ...loop, + type: 'ForOfStatement', + right: { + ...loop.right, + type: 'Identifier', + name: loopVariable } - ], - kind: 'const' - }) - - this.ForOfStatement({ - ...loop, - type: 'ForOfStatement', - right: { - ...loop.right, - type: 'Identifier', - name: loopVariable - } - }, state) -} + }, state) + } -export function walk({ loop }, st, c) { - c(loop, st, 'Expression') -} + static walk(walker, { loop }, st, c) { + c(loop, st, 'Expression') + } + + static scope(scoper, node, scope) { + return scoper._scope(node.loop, scope) + } -export function scope(node, scope) { - return this._scope(node.loop, scope) } diff --git a/src/Stone/Types/StoneMacro.js b/src/Stone/Types/StoneMacro.js index 376e05f..b15cf15 100644 --- a/src/Stone/Types/StoneMacro.js +++ b/src/Stone/Types/StoneMacro.js @@ -1,47 +1,52 @@ -export const directive = 'macro' -export const hasEndDirective = true +import './StoneDirectiveType' -export function parse(node, args) { - (this._currentMacro = (this._currentMacro || [ ])).push(node) - args = this._flattenArgs(args) +export class StoneMacro extends StoneDirectiveType { - if(args.length === 0) { - this.raise(this.start, '`@macro` must contain at least 1 argument') - } + static directive = 'macro' - node.id = args.shift() + static parse(parser, node, args) { + (parser._currentMacro = (parser._currentMacro || [ ])).push(node) + args = parser._flattenArgs(args) - const output = this.startNode() - output.rescopeContext = true - output.params = args - output.body = this.parseUntilEndDirective('endmacro') + if(args.length === 0) { + parser.raise(parser.start, '`@macro` must contain at least 1 argument') + } - node.output = this.finishNode(output, 'StoneOutputBlock') - return this.finishNode(node, 'StoneMacro') -} + node.id = args.shift() + + const output = parser.startNode() + output.rescopeContext = true + output.params = args + output.body = parser.parseUntilEndDirective('endmacro') -export function parseEnd(node) { - if(!this._currentMacro || this._currentMacro.length === 0) { - this.raise(this.start, '`@endmacro` outside of `@macro`') + node.output = parser.finishNode(output, 'StoneOutputBlock') + return parser.finishNode(node, 'StoneMacro') } - this._currentMacro.pop() + static parseEnd(parser, node) { + if(!parser._currentMacro || parser._currentMacro.length === 0) { + parser.raise(parser.start, '`@endmacro` outside of `@macro`') + } - return this.finishNode(node, 'Directive') -} + parser._currentMacro.pop() -export function generate(node, state) { - state.write('_[') - this[node.id.type](node.id, state) - state.write('] = ') - return this[node.output.type](node.output, state) -} + return parser.finishNode(node, 'Directive') + } -export function walk(node, st, c) { - c(node.id, st, 'Pattern') - c(node.output, st, 'Expression') -} + static generate(generator, node, state) { + state.write('_[') + generator[node.id.type](node.id, state) + state.write('] = ') + return generator[node.output.type](node.output, state) + } + + static walk(walker, node, st, c) { + c(node.id, st, 'Pattern') + c(node.output, st, 'Expression') + } + + static scope(scoper, { output }, scope) { + scoper._scope(output, scope) + } -export function scope({ output }, scope) { - this._scope(output, scope) } diff --git a/src/Stone/Types/StoneOutput.js b/src/Stone/Types/StoneOutput.js index a219dd2..958d09a 100644 --- a/src/Stone/Types/StoneOutput.js +++ b/src/Stone/Types/StoneOutput.js @@ -1,13 +1,19 @@ -export function generate({ output }, state) { - state.write('output += ') - this[output.type](output, state) - state.write(';') -} +import './StoneType' -export function walk({ output }, st, c) { - c(output, st, 'Expression') -} +export class StoneOutput extends StoneType { + + static generate(generator, { output }, state) { + state.write('output += ') + generator[output.type](output, state) + state.write(';') + } + + static walk(walker, { output }, st, c) { + c(output, st, 'Expression') + } + + static scope(scoper, { output }, scope) { + return scoper._scope(output, scope) + } -export function scope({ output }, scope) { - return this._scope(output, scope) } diff --git a/src/Stone/Types/StoneOutputBlock.js b/src/Stone/Types/StoneOutputBlock.js index 77c2fa7..791bb5a 100644 --- a/src/Stone/Types/StoneOutputBlock.js +++ b/src/Stone/Types/StoneOutputBlock.js @@ -1,124 +1,130 @@ -export function generate(node, state) { - state.pushScope(node.scope) - state.write('function') +import './StoneType' - if(!node.id.isNil) { +export class StoneOutputBlock extends StoneType { + + static generate(generator, node, state) { + state.pushScope(node.scope) + state.write('function') + + if(!node.id.isNil) { + state.write(' ') + node.id.isScoped = true + generator[node.id.type](node.id, state) + } + + generator.SequenceExpression({ expressions: node.params || [ ] }, state) state.write(' ') - node.id.isScoped = true - this[node.id.type](node.id, state) - } - this.SequenceExpression({ expressions: node.params || [ ] }, state) - state.write(' ') + node.assignments = node.assignments || [ ] - node.assignments = node.assignments || [ ] + if(node.rescopeContext) { + // _ = { ..._ } + node.assignments.push({ + operator: '=', + left: { + type: 'Identifier', + name: '_' + }, + right: { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + type: 'Identifier', + name: '_' + } + } + ] + } + }) + } - if(node.rescopeContext) { - // _ = { ..._ } + // let output = '' node.assignments.push({ - operator: '=', + kind: 'let', left: { type: 'Identifier', - name: '_' + name: 'output' }, right: { - type: 'ObjectExpression', - properties: [ + type: 'Literal', + value: '', + raw: '\'\'', + } + }) + + node.body.body.unshift(...node.assignments.map(({ kind, ...assignment }) => { + const hasKind = !kind.isNil + return { + type: hasKind ? 'VariableDeclaration' : 'ExpressionStatement', + kind: kind, + expression: hasKind ? void 0 : { ...assignment, type: 'AssignmentExpression' }, + declarations: !hasKind ? void 0 : [ { - type: 'SpreadElement', - argument: { - type: 'Identifier', - name: '_' - } + type: 'VariableDeclarator', + id: assignment.left, + init: assignment.right } ] } - }) - } + })) - // let output = '' - node.assignments.push({ - kind: 'let', - left: { - type: 'Identifier', - name: 'output' - }, - right: { - type: 'Literal', - value: '', - raw: '\'\'', - } - }) - - node.body.body.unshift(...node.assignments.map(({ kind, ...assignment }) => { - const hasKind = !kind.isNil - return { - type: hasKind ? 'VariableDeclaration' : 'ExpressionStatement', - kind: kind, - expression: hasKind ? void 0 : { ...assignment, type: 'AssignmentExpression' }, - declarations: !hasKind ? void 0 : [ - { - type: 'VariableDeclarator', - id: assignment.left, - init: assignment.right - } - ] - } - })) + let _return = null - let _return = null - - if(!node.return.isNil) { - _return = node.return - } else if(node.returnRaw) { - // return output - _return = { - type: 'Identifier', - name: 'output' - } - } else { - // return new HtmlString(output) - _return = { - type: 'NewExpression', - callee: { + if(!node.return.isNil) { + _return = node.return + } else if(node.returnRaw) { + // return output + _return = { type: 'Identifier', - name: 'HtmlString' - }, - arguments: [ - { + name: 'output' + } + } else { + // return new HtmlString(output) + _return = { + type: 'NewExpression', + callee: { type: 'Identifier', - name: 'output' - } - ] + name: 'HtmlString' + }, + arguments: [ + { + type: 'Identifier', + name: 'output' + } + ] + } } - } - node.body.body.push({ - type: 'ReturnStatement', - argument: _return - }) - - this[node.body.type](node.body, state) - state.popScope() -} + node.body.body.push({ + type: 'ReturnStatement', + argument: _return + }) -export function walk(node, st, c) { - for(const param of node.params) { - c(param, st, 'Pattern') + generator[node.body.type](node.body, state) + state.popScope() } - c(node.body, st, 'ScopeBody') -} + static walk(walker, node, st, c) { + for(const param of node.params) { + c(param, st, 'Pattern') + } -export function scope(node, scope) { - node.scope = scope.branch() - node.scope.add('output') + c(node.body, st, 'ScopeBody') + } - if(Array.isArray(node.params)) { - for(const param of node.params) { - this._scope(param, node.scope, true) + static scope(scoper, node, scope) { + node.scope = scope.branch() + node.scope.add('output') + + if(Array.isArray(node.params)) { + for(const param of node.params) { + scoper._scope(param, node.scope, true) + } } + + scoper._scope(node.body, node.scope) } - this._scope(node.body, node.scope) } diff --git a/src/Stone/Types/StoneOutputExpression.js b/src/Stone/Types/StoneOutputExpression.js index 2151f81..73ec01e 100644 --- a/src/Stone/Types/StoneOutputExpression.js +++ b/src/Stone/Types/StoneOutputExpression.js @@ -1,19 +1,25 @@ -export function generate({ safe = true, value }, state) { - if(safe) { - state.write('_.escape(') - } +import './StoneType' + +export class StoneOutputExpression extends StoneType { - this[value.type](value, state) + static generate(generator, { safe = true, value }, state) { + if(safe) { + state.write('_.escape(') + } - if(safe) { - state.write(')') + generator[value.type](value, state) + + if(safe) { + state.write(')') + } } -} -export function walk({ value }, st, c) { - c(value, st, 'Expression') -} + static walk(walker, { value }, st, c) { + c(value, st, 'Expression') + } + + static scope(scoper, { value }, scope) { + return scoper._scope(value, scope) + } -export function scope({ value }, scope) { - return this._scope(value, scope) } diff --git a/src/Stone/Types/StoneParent.js b/src/Stone/Types/StoneParent.js new file mode 100644 index 0000000..9eaaad8 --- /dev/null +++ b/src/Stone/Types/StoneParent.js @@ -0,0 +1,10 @@ +import './StoneSuper' + +/** + * Alias of @super for compatibility with Blade + */ +export class StoneParent extends StoneSuper { + + static directive = 'parent' + +} diff --git a/src/Stone/Types/StoneSection.js b/src/Stone/Types/StoneSection.js index 8f7ab11..149b2ce 100644 --- a/src/Stone/Types/StoneSection.js +++ b/src/Stone/Types/StoneSection.js @@ -1,95 +1,85 @@ -export const directive = 'section' -export const hasEndDirective = true -export const parsers = { parseShowDirective } +import './StoneDirectiveType' -export function parse(node, args) { - args = this._flattenArgs(args) +export class StoneSection extends StoneDirectiveType { - if(args.length === 0) { - this.raise(this.start, '`@section` must contain at least 1 argument') - } + static directive = 'section' - node.id = args.shift() - - if(args.length > 1) { - this.raise(this.start, '`@section` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.output = args.pop() - node.inline = true - this.next() - } else { - (this._currentSection = (this._currentSection || [ ])).push(node) - - const output = this.startNode() - output.params = args - output.body = this.parseUntilEndDirective([ 'show', 'endsection' ]) - node.output = this.finishNode(output, 'StoneOutputBlock') - } + static parse(parser, node, args) { + args = parser._flattenArgs(args) - return this.finishNode(node, 'StoneSection') -} + if(args.length === 0) { + parser.raise(parser.start, '`@section` must contain at least 1 argument') + } -/** - * Ends the current section and returns output - * @return {string} Output from the section - */ -export function parseEnd(node) { - if(!this._currentSection || this._currentSection.length === 0) { - this.raise(this.start, '`@endsection` outside of `@section`') - } + node.id = args.shift() - this._currentSection.pop() + if(args.length > 1) { + parser.raise(parser.start, '`@section` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.output = args.pop() + node.inline = true + parser.next() + } else { + (parser._currentSection = (parser._currentSection || [ ])).push(node) - return this.finishNode(node, 'Directive') -} + const output = parser.startNode() + output.params = args + output.body = parser.parseUntilEndDirective([ 'show', 'endsection' ]) + node.output = parser.finishNode(output, 'StoneOutputBlock') + } -/** - * Ends the current section and yields it for display - * @return {string} Output from the section - */ -export function parseShowDirective(node) { - if(!this._currentSection || this._currentSection.length === 0) { - this.raise(this.start, '`@show` outside of `@section`') + return parser.finishNode(node, 'StoneSection') } - this._currentSection.pop().yield = true - - return this.finishNode(node, 'Directive') -} + /** + * Ends the current section and returns output + * @return {string} Output from the section + */ + static parseEnd(parser, node) { + if(!parser._currentSection || parser._currentSection.length === 0) { + parser.raise(parser.start, '`@endsection` outside of `@section`') + } -export function generate(node, state) { - state.write('_sections.push(') - this[node.id.type](node.id, state) - state.write(', ') + parser._currentSection.pop() - if(node.inline) { - state.write('() => ') - this.StoneOutputExpression({ safe: true, value: node.output }, state) - } else { - this[node.output.type](node.output, state) + return parser.finishNode(node, 'Directive') } - state.write(');') + static generate(generator, node, state) { + state.write('_sections.push(') + generator[node.id.type](node.id, state) + state.write(', ') - if(!node.yield) { - return + if(node.inline) { + state.write('() => ') + generator.StoneOutputExpression({ safe: true, value: node.output }, state) + } else { + generator[node.output.type](node.output, state) + } + + state.write(');') + + if(!node.yield) { + return + } + + state.write(state.lineEnd) + state.write(state.indent) + generator.StoneYield({ section: node.id }, state) } - state.write(state.lineEnd) - state.write(state.indent) - this.StoneYield({ section: node.id }, state) -} + static walk(walker, node, st, c) { + c(node.id, st, 'Pattern') -export function walk(node, st, c) { - c(node.id, st, 'Pattern') + if(node.inline) { + return + } - if(node.inline) { - return + c(node.output, st, 'Expression') } - c(node.output, st, 'Expression') -} + static scope(scoper, node, scope) { + scoper._scope(node.output, scope) + } -export function scope(node, scope) { - this._scope(node.output, scope) } diff --git a/src/Stone/Types/StoneSet.js b/src/Stone/Types/StoneSet.js index a201b9c..aa6fe8f 100644 --- a/src/Stone/Types/StoneSet.js +++ b/src/Stone/Types/StoneSet.js @@ -1,129 +1,133 @@ +import './StoneDirectiveType' import '../../Stone' -import { make } from '../Support/MakeNode' -export const directive = 'set' +export class StoneSet extends StoneDirectiveType { -export function parseArgs() { - this.skipSpace() + static directive = 'set' - let kind = null + static parseArgs(parser) { + parser.skipSpace() - if(this.input.substring(this.pos, this.pos + 6).toLowerCase() === 'const ') { - kind = 'const' - } else if(this.input.substring(this.pos, this.pos + 4).toLowerCase() === 'let ') { - kind = 'let' - } else if(this.input.substring(this.pos, this.pos + 4).toLowerCase() === 'var ') { - this.raise(this.start, '`@set` does not support `var`') - } else { - return this.parseDirectiveArgs() - } + let kind = null - this.pos += kind.length + if(parser.input.substring(parser.pos, parser.pos + 6).toLowerCase() === 'const ') { + kind = 'const' + } else if(parser.input.substring(parser.pos, parser.pos + 4).toLowerCase() === 'let ') { + kind = 'let' + } else if(parser.input.substring(parser.pos, parser.pos + 4).toLowerCase() === 'var ') { + parser.raise(parser.start, '`@set` does not support `var`') + } else { + return parser.parseDirectiveArgs() + } - const node = this.parseDirectiveArgs() - node.kind = kind - return node -} + parser.pos += kind.length -/** - * Sets a context variable - * - * @param {object} context Context for the compilation - * @param {string} args Arguments to set - * @return {string} Code to set the context variable - */ -export function parse(node, args) { - const kind = args.kind || null - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@set` must contain at least 1 argument') - } else if(args.length > 2) { - this.raise(this.start, '`@set` cannot contain more than 2 arguments') + const node = parser.parseDirectiveArgs() + node.kind = kind + return node } - if(args.length === 1 && args[0].type === 'AssignmentExpression') { - Object.assign(node, args[0]) - } else { - node.operator = '=' - node.left = args[0] - node.right = args[1] - } + /** + * Sets a context variable + * + * @param {object} context Context for the compilation + * @param {string} args Arguments to set + * @return {string} Code to set the context variable + */ + static parse(parser, node, args) { + const kind = args.kind || null + args = parser._flattenArgs(args) + + if(args.length === 0) { + parser.raise(parser.start, '`@set` must contain at least 1 argument') + } else if(args.length > 2) { + parser.raise(parser.start, '`@set` cannot contain more than 2 arguments') + } - node.kind = kind - expressionToPattern(node.left) + if(args.length === 1 && args[0].type === 'AssignmentExpression') { + Object.assign(node, args[0]) + } else { + node.operator = '=' + node.left = args[0] + node.right = args[1] + } - this.next() - return this.finishNode(node, 'StoneSet') -} + node.kind = kind + this.expressionToPattern(node.left) -export function generate({ kind, left, right }, state) { - if(right.isNil) { - this[left.type](left, state) - return + parser.next() + return parser.finishNode(node, 'StoneSet') } - // If var type has been explicitly defined, we’ll - // pass through directly and scope locally - if(!kind.isNil) { - const declaration = make.declaration(left, right, kind) - require('../Scoper').Scoper._scope(left, state.scope, true) - return this[declaration.type](declaration, state) - } + static generate(generator, { kind, left, right }, state) { + if(right.isNil) { + generator[left.type](left, state) + return + } - // Otherwise, scoping is assumed to be on the context var - if(left.type !== 'ArrayPattern' && left.type !== 'ObjectPattern') { - // If we‘re not destructuring, we can assign it directly - // and bail out early. - const assignment = make.assignment(left, right) - return this[assignment.type](assignment, state) - } + // If var type has been explicitly defined, we’ll + // pass through directly and scope locally + if(!kind.isNil) { + const declaration = this.make.declaration(left, right, kind) + require('../Scoper').Scoper._scope(left, state.scope, true) + return generator[declaration.type](declaration, state) + } - // If we are destructuring, we need to find the vars to extract - // then wrap them in a function and assign them to the context var - const extracted = [ ] - Stone.walkVariables(left, node => extracted.push(node)) + // Otherwise, scoping is assumed to be on the context var + if(left.type !== 'ArrayPattern' && left.type !== 'ObjectPattern') { + // If we‘re not destructuring, we can assign it directly + // and bail out early. + const assignment = this.make.assignment(left, right) + return generator[assignment.type](assignment, state) + } - const block = make.block([ - make.declaration(left, right, 'const'), - make.return(make.object(extracted.map(value => make.property(value, value)))) - ]) + // If we are destructuring, we need to find the vars to extract + // then wrap them in a function and assign them to the context var + const extracted = [ ] + Stone.walkVariables(left, node => extracted.push(node)) - block.scope = state.scope.branch(extracted.map(({ name }) => name)) + const block = this.make.block([ + this.make.declaration(left, right, 'const'), + this.make.return(this.make.object(extracted.map(value => this.make.property(value, value)))) + ]) - state.write('Object.assign(_, (function() ') - this[block.type](block, state) - state.write(')());') -} + block.scope = state.scope.branch(extracted.map(({ name }) => name)) -export function walk({ left, right }, st, c) { - if(right.isNil) { - c(left, st, 'Expression') - return + state.write('Object.assign(_, (function() ') + generator[block.type](block, state) + state.write(')());') } - c(left, st, 'Pattern') - c(right, st, 'Pattern') -} + static walk(walker, { left, right }, st, c) { + if(right.isNil) { + c(left, st, 'Expression') + return + } -/** - * `parseSetDirectiveArgs` gets parsed into SequenceExpression - * which parses destructuring into Array/Object expressions - * instead of patterns - */ -function expressionToPattern(node) { - if(node.isNil) { - return + c(left, st, 'Pattern') + c(right, st, 'Pattern') } - if(node.type === 'ArrayExpression') { - node.type = 'ArrayPattern' - node.elements.forEach(expressionToPattern) - } else if(node.type === 'ObjectExpression') { - node.type = 'ObjectPattern' + /** + * `parseSetDirectiveArgs` gets parsed into SequenceExpression + * which parses destructuring into Array/Object expressions + * instead of patterns + */ + static expressionToPattern(node) { + if(node.isNil) { + return + } + + if(node.type === 'ArrayExpression') { + node.type = 'ArrayPattern' + node.elements.forEach(this.expressionToPattern.bind(this)) + } else if(node.type === 'ObjectExpression') { + node.type = 'ObjectPattern' - for(const property of node.properties) { - expressionToPattern(property.value) + for(const property of node.properties) { + this.expressionToPattern(property.value) + } } } + } diff --git a/src/Stone/Types/StoneShow.js b/src/Stone/Types/StoneShow.js new file mode 100644 index 0000000..cdcab88 --- /dev/null +++ b/src/Stone/Types/StoneShow.js @@ -0,0 +1,29 @@ +import './StoneDirectiveType' + +export class StoneShow extends StoneDirectiveType { + + static directive = 'show' + + /** + * Ends the current section and yields it for display + * @return {string} Output from the section + */ + static parse(parser, node) { + if(!parser._currentSection || parser._currentSection.length === 0) { + parser.raise(parser.start, '`@show` outside of `@section`') + } + + parser._currentSection.pop().yield = true + + return parser.finishNode(node, 'Directive') + } + + static generate() { + throw new Error('This should not be called') + } + + static walk() { + throw new Error('This should not be called') + } + +} diff --git a/src/Stone/Types/StoneSlot.js b/src/Stone/Types/StoneSlot.js index ba61b9c..47a3a5e 100644 --- a/src/Stone/Types/StoneSlot.js +++ b/src/Stone/Types/StoneSlot.js @@ -1,73 +1,78 @@ -export const directive = 'slot' -export const hasEndDirective = true +import './StoneDirectiveType' -export function parse(node, args) { - args = this._flattenArgs(args) +export class StoneSlot extends StoneDirectiveType { - if(args.length === 0) { - this.raise(this.start, '`@slot` must contain at least 1 argument') - } + static directive = 'slot' + + static parse(parser, node, args) { + args = parser._flattenArgs(args) + + if(args.length === 0) { + parser.raise(parser.start, '`@slot` must contain at least 1 argument') + } + + node.id = args.shift() + + if(args.length > 1) { + parser.raise(parser.start, '`@slot` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.output = args.pop() + node.inline = true + parser.next() + } else { + (parser._currentSlot = (parser._currentSlot || [ ])).push(node) + + const output = parser.startNode() + output.params = args + output.body = parser.parseUntilEndDirective('endslot') + node.output = parser.finishNode(output, 'StoneOutputBlock') + } - node.id = args.shift() - - if(args.length > 1) { - this.raise(this.start, '`@slot` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.output = args.pop() - node.inline = true - this.next() - } else { - (this._currentSlot = (this._currentSlot || [ ])).push(node) - - const output = this.startNode() - output.params = args - output.body = this.parseUntilEndDirective('endslot') - node.output = this.finishNode(output, 'StoneOutputBlock') + return parser.finishNode(node, 'StoneSlot') } - return this.finishNode(node, 'StoneSlot') -} + /** + * Ends the current slot and returns output + * @return {string} Output from the slot + */ + static parseEnd(parser, node) { + if(!parser._currentSlot || parser._currentSlot.length === 0) { + parser.raise(parser.start, '`@endslot` outside of `@slot`') + } + + parser._currentSlot.pop() -/** - * Ends the current slot and returns output - * @return {string} Output from the slot - */ -export function parseEnd(node) { - if(!this._currentSlot || this._currentSlot.length === 0) { - this.raise(this.start, '`@endslot` outside of `@slot`') + return parser.finishNode(node, 'Directive') } - this._currentSlot.pop() + static generate(generator, node, state) { + state.write('__componentContext[') + generator[node.id.type](node.id, state) + state.write('] = ') - return this.finishNode(node, 'Directive') -} + if(node.inline) { + generator.StoneOutputExpression({ safe: true, value: node.output }, state) + } else { + state.write('(') + generator[node.output.type](node.output, state) + state.write(')()') + } -export function generate(node, state) { - state.write('__componentContext[') - this[node.id.type](node.id, state) - state.write('] = ') - - if(node.inline) { - this.StoneOutputExpression({ safe: true, value: node.output }, state) - } else { - state.write('(') - this[node.output.type](node.output, state) - state.write(')()') + state.write(';') } - state.write(';') -} + static walk(walker, node, st, c) { + c(node.id, st, 'Pattern') -export function walk(node, st, c) { - c(node.id, st, 'Pattern') + if(node.inline) { + return + } - if(node.inline) { - return + c(node.output, st, 'Expression') } - c(node.output, st, 'Expression') -} + static scope(scoper, node, scope) { + scoper._scope(node.output, scope) + } -export function scope(node, scope) { - this._scope(node.output, scope) } diff --git a/src/Stone/Types/StoneSuper.js b/src/Stone/Types/StoneSuper.js index 6ad180d..82839fb 100644 --- a/src/Stone/Types/StoneSuper.js +++ b/src/Stone/Types/StoneSuper.js @@ -1,25 +1,24 @@ -export const directive = 'super' +import './StoneYield' -/** - * Alias of compileSuper for compatibility with Blade - * @return {string} Code to render the super section - */ -export const parsers = { parseParentDirective: parse } +// Due to how sections work, we can cheat by treating as yield +// which will pop off the next chunk of content in the section +// and render it within ours + +export class StoneSuper extends StoneYield { + + static directive = 'super' -/** - * Renders content from the section section - * @return {string} Code to render the super section - */ -export function parse(node) { - if(!this._currentSection || this._currentSection.length === 0) { - this.raise(this.start, `\`@${node.directive}\` outside of \`@section\``) + /** + * Renders content from the section section + * @return {string} Code to render the super section + */ + static parse(parser, node) { + if(!parser._currentSection || parser._currentSection.length === 0) { + parser.raise(parser.start, `\`@${node.directive}\` outside of \`@section\``) + } + + node.section = { ...parser._currentSection[parser._currentSection.length - 1].id } + return parser.finishNode(node, 'StoneSuper') } - node.section = { ...this._currentSection[this._currentSection.length - 1].id } - return this.finishNode(node, 'StoneSuper') } - -// Due to how sections work, we can cheat by treating as yield -// which will pop off the next chunk of content in the section -// and render it within ours -export { generate, walk, scope } from './StoneYield' diff --git a/src/Stone/Types/StoneTemplate.js b/src/Stone/Types/StoneTemplate.js index f7aad52..36c6f1a 100644 --- a/src/Stone/Types/StoneTemplate.js +++ b/src/Stone/Types/StoneTemplate.js @@ -1,140 +1,146 @@ -export function generate({ pathname, output, isLayout, hasLayoutContext }, state) { - output.id = { - type: 'Identifier', - name: 'template' - } +import './StoneType' + +export class StoneTemplate extends StoneType { - output.params = [ - { + static generate(generator, { pathname, output, isLayout, hasLayoutContext }, state) { + output.id = { type: 'Identifier', - name: '_' - }, { - type: 'AssignmentPattern', - left: { + name: 'template' + } + + output.params = [ + { type: 'Identifier', - name: '_sections' - }, - right: { - type: 'NewExpression', - callee: { + name: '_' + }, { + type: 'AssignmentPattern', + left: { type: 'Identifier', - name: 'StoneSections' + name: '_sections' + }, + right: { + type: 'NewExpression', + callee: { + type: 'Identifier', + name: 'StoneSections' + } } } - } - ] - - output.assignments = output.assignments || [ ] - output.assignments.push({ - kind: 'const', - left: { - type: 'Identifier', - name: '_templatePathname' - }, - right: { - type: 'Literal', - value: pathname.isNil ? null : pathname, - raw: pathname.isNil ? null : `'${pathname}'` - } - }) + ] - if(isLayout) { + output.assignments = output.assignments || [ ] output.assignments.push({ - kind: 'let', + kind: 'const', left: { type: 'Identifier', - name: '__extendsLayout' + name: '_templatePathname' + }, + right: { + type: 'Literal', + value: pathname.isNil ? null : pathname, + raw: pathname.isNil ? null : `'${pathname}'` } }) - const context = { - type: 'ObjectExpression', - properties: [ - { - type: 'SpreadElement', - argument: { - type: 'Identifier', - name: '_' - } + if(isLayout) { + output.assignments.push({ + kind: 'let', + left: { + type: 'Identifier', + name: '__extendsLayout' } - ] - } + }) - if(hasLayoutContext) { - const extendsContext = { - type: 'Identifier', - name: '__extendsContext' + const context = { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + type: 'Identifier', + name: '_' + } + } + ] } - output.assignments.push({ - kind: 'let', - left: extendsContext - }) + if(hasLayoutContext) { + const extendsContext = { + type: 'Identifier', + name: '__extendsContext' + } - context.properties.push({ - type: 'SpreadElement', - argument: extendsContext - }) - } + output.assignments.push({ + kind: 'let', + left: extendsContext + }) - output.return = { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { + context.properties.push({ + type: 'SpreadElement', + argument: extendsContext + }) + } + + output.return = { + type: 'CallExpression', + callee: { type: 'MemberExpression', object: { - type: 'Identifier', - name: '_' + type: 'MemberExpression', + object: { + type: 'Identifier', + name: '_' + }, + property: { + type: 'Identifier', + name: '$stone' + } }, property: { type: 'Identifier', - name: '$stone' + name: 'extends' } }, - property: { - type: 'Identifier', - name: 'extends' - } - }, - arguments: [ - { - type: 'Identifier', - name: '_templatePathname' - }, { - type: 'Identifier', - name: '__extendsLayout' - }, - context, - { - type: 'Identifier', - name: '_sections' - } - ] + arguments: [ + { + type: 'Identifier', + name: '_templatePathname' + }, { + type: 'Identifier', + name: '__extendsLayout' + }, + context, + { + type: 'Identifier', + name: '_sections' + } + ] + } + } else { + output.returnRaw = true } - } else { - output.returnRaw = true - } - this[output.type](output, state) -} + generator[output.type](output, state) + } -export function walk({ output }, st, c) { - c(output, st, 'Expression') -} + static walk(walker, { output }, st, c) { + c(output, st, 'Expression') + } -export function scope({ output, isLayout, hasLayoutContext }, scope) { - scope.add('_') - scope.add('_sections') - scope.add('_templatePathname') + static scope(scoper, { output, isLayout, hasLayoutContext }, scope) { + scope.add('_') + scope.add('_sections') + scope.add('_templatePathname') - if(isLayout) { - scope.add('__extendsLayout') + if(isLayout) { + scope.add('__extendsLayout') - if(hasLayoutContext) { - scope.add('__extendsContext') + if(hasLayoutContext) { + scope.add('__extendsContext') + } } + + scoper._scope(output, scope) } - this._scope(output, scope) } diff --git a/src/Stone/Types/StoneType.js b/src/Stone/Types/StoneType.js new file mode 100644 index 0000000..9a62978 --- /dev/null +++ b/src/Stone/Types/StoneType.js @@ -0,0 +1,42 @@ +import { make } from '../Support/MakeNode' + +export class StoneType { + + static make = make + + static registerParse(/* parser */) { + // Default: noop + } + + static registerGenerate(generator) { + generator[this.name] = this._bind('generate') + } + + static registerWalk(walker) { + walker[this.name] = this._bind('walk') + } + + static registerScope(scoper) { + if(typeof this.scope !== 'function') { + return + } + + scoper[this.name] = this._bind('scope') + } + + static _bind(func) { + const bound = this[func].bind(this) + return function(...args) { return bound(this, ...args) } + } + + // Abstract methods + + static generate(/* generator, node, state */) { + throw new Error('Subclasses must implement') + } + + static walk(/* walker, node, st, c */) { + throw new Error('Subclasses must implement') + } + +} diff --git a/src/Stone/Types/StoneUnset.js b/src/Stone/Types/StoneUnset.js index 3a4cde6..939b192 100644 --- a/src/Stone/Types/StoneUnset.js +++ b/src/Stone/Types/StoneUnset.js @@ -1,34 +1,40 @@ -export const directive = 'unset' +import './StoneDirectiveType' -/** - * Unsets a context variable - * - * @param {object} context Context for the compilation - * @param {string} args Arguments to unset - * @return {string} Code to set the context variable - */ -export function parse(node, args) { - node.properties = this._flattenArgs(args) - this.next() - return this.finishNode(node, 'StoneUnset') -} +export class StoneUnset extends StoneDirectiveType { + + static directive = 'unset' + + /** + * Unsets a context variable + * + * @param {object} context Context for the compilation + * @param {string} args Arguments to unset + * @return {string} Code to set the context variable + */ + static parse(parser, node, args) { + node.properties = parser._flattenArgs(args) + parser.next() + return parser.finishNode(node, 'StoneUnset') + } -export function generate({ properties }, state) { - let first = true - for(const property of properties) { - if(first) { - first = false - } else { - state.write(state.lineEnd) - state.write(state.indent) + static generate(generator, { properties }, state) { + let first = true + for(const property of properties) { + if(first) { + first = false + } else { + state.write(state.lineEnd) + state.write(state.indent) + } + + state.write('delete ') + generator[property.type](property, state) + state.write(';') } + } - state.write('delete ') - this[property.type](property, state) - state.write(';') + static walk() { + // Do nothing } -} -export function walk() { - // Do nothing } diff --git a/src/Stone/Types/StoneYield.js b/src/Stone/Types/StoneYield.js index 128d033..3e544e9 100644 --- a/src/Stone/Types/StoneYield.js +++ b/src/Stone/Types/StoneYield.js @@ -1,57 +1,63 @@ -export const directive = 'yield' - -/** - * Compiles the yield directive to output a section - * - * @param {object} context Context for the compilation - * @param {string} section Name of the section to yield - * @return {string} Code to render the section - */ -export function parse(node, args) { - args = this._flattenArgs(args) - - if(args.length === 0) { - this.raise(this.start, '`@yield` must contain at least 1 argument') - } +import './StoneDirectiveType' + +export class StoneYield extends StoneDirectiveType { + + static directive = 'yield' + + /** + * Compiles the yield directive to output a section + * + * @param {object} context Context for the compilation + * @param {string} section Name of the section to yield + * @return {string} Code to render the section + */ + static parse(parser, node, args) { + args = parser._flattenArgs(args) + + if(args.length === 0) { + parser.raise(parser.start, '`@yield` must contain at least 1 argument') + } - node.section = args.shift() + node.section = args.shift() - if(args.length > 1) { - this.raise(this.start, '`@yield` cannot contain more than 2 arguments') - } else if(args.length === 1) { - node.output = args.pop() + if(args.length > 1) { + parser.raise(parser.start, '`@yield` cannot contain more than 2 arguments') + } else if(args.length === 1) { + node.output = args.pop() + } + + parser.next() + return parser.finishNode(node, 'StoneYield') } - this.next() - return this.finishNode(node, 'StoneYield') -} + static generate(generator, node, state) { + state.write('output += _sections.render(') + generator[node.section.type](node.section, state) -export function generate(node, state) { - state.write('output += _sections.render(') - this[node.section.type](node.section, state) + if(!node.output.isNil) { + state.write(', ') + generator.StoneOutputExpression({ safe: true, value: node.output }, state) + } - if(!node.output.isNil) { - state.write(', ') - this.StoneOutputExpression({ safe: true, value: node.output }, state) + state.write(');') } - state.write(');') -} + static walk(walker, node, st, c) { + c(node.section, st, 'Pattern') -export function walk(node, st, c) { - c(node.section, st, 'Pattern') + if(node.output.isNil) { + return + } - if(node.output.isNil) { - return + c(node.output, st, 'Expression') } - c(node.output, st, 'Expression') -} + static scope(scoper, node, scope) { + if(node.output.isNil) { + return + } -export function scope(node, scope) { - if(node.output.isNil) { - return + scoper._scope(node.output, scope) } - this._scope(node.output, scope) } diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index 7e05bbb..3741f22 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -1,21 +1,23 @@ export const Types = { - StoneComponent: require('./StoneComponent'), - StoneDump: require('./StoneDump'), - StoneEach: require('./StoneEach'), - StoneEmptyExpression: require('./StoneEmptyExpression'), - StoneExtends: require('./StoneExtends'), - StoneHasSection: require('./StoneHasSection'), - StoneInclude: require('./StoneInclude'), - StoneLoop: require('./StoneLoop'), - StoneMacro: require('./StoneMacro'), - StoneOutput: require('./StoneOutput'), - StoneOutputBlock: require('./StoneOutputBlock'), - StoneOutputExpression: require('./StoneOutputExpression'), - StoneSection: require('./StoneSection'), - StoneSet: require('./StoneSet'), - StoneSlot: require('./StoneSlot'), - StoneSuper: require('./StoneSuper'), - StoneTemplate: require('./StoneTemplate'), - StoneUnset: require('./StoneUnset'), - StoneYield: require('./StoneYield'), + ...require('./StoneComponent'), + ...require('./StoneDump'), + ...require('./StoneEach'), + ...require('./StoneEmptyExpression'), + ...require('./StoneExtends'), + ...require('./StoneHasSection'), + ...require('./StoneInclude'), + ...require('./StoneLoop'), + ...require('./StoneMacro'), + ...require('./StoneOutput'), + ...require('./StoneOutputBlock'), + ...require('./StoneOutputExpression'), + ...require('./StoneParent'), + ...require('./StoneSection'), + ...require('./StoneSet'), + ...require('./StoneShow'), + ...require('./StoneSlot'), + ...require('./StoneSuper'), + ...require('./StoneTemplate'), + ...require('./StoneUnset'), + ...require('./StoneYield'), } diff --git a/src/Stone/Walker.js b/src/Stone/Walker.js index b966174..1d46bc0 100644 --- a/src/Stone/Walker.js +++ b/src/Stone/Walker.js @@ -2,6 +2,6 @@ import './Types' export const Walker = { ...require('acorn/dist/walk').base } -for(const key of Object.keys(Types)) { - Walker[key] = Types[key].walk.bind(Walker) +for(const type of Object.values(Types)) { + type.registerWalk(Walker) } From 0f6444b55fb5dc456a383953cb1a018a97bbbfd4 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 01:19:19 -0500 Subject: [PATCH 23/37] Abstracted assertArgs --- src/Stone/Types/StoneComponent.js | 8 ++------ src/Stone/Types/StoneDirectiveType.js | 12 ++++++++++++ src/Stone/Types/StoneEach.js | 7 +------ src/Stone/Types/StoneExtends.js | 9 ++------- src/Stone/Types/StoneHasSection.js | 8 +++----- src/Stone/Types/StoneInclude.js | 9 ++------- src/Stone/Types/StoneMacro.js | 9 ++++----- src/Stone/Types/StoneSection.js | 9 ++------- src/Stone/Types/StoneSet.js | 6 +----- src/Stone/Types/StoneSlot.js | 9 ++------- src/Stone/Types/StoneUnset.js | 2 ++ src/Stone/Types/StoneYield.js | 8 ++------ 12 files changed, 35 insertions(+), 61 deletions(-) diff --git a/src/Stone/Types/StoneComponent.js b/src/Stone/Types/StoneComponent.js index a7e72dc..360b6ce 100644 --- a/src/Stone/Types/StoneComponent.js +++ b/src/Stone/Types/StoneComponent.js @@ -7,15 +7,11 @@ export class StoneComponent extends StoneDirectiveType { static parse(parser, node, args) { args = parser._flattenArgs(args) - if(args.length === 0) { - parser.raise(parser.start, '`@component` must contain at least 1 argument') - } + this.assertArgs(parser, args, 1, 2) node.view = args.shift() - if(args.length > 1) { - parser.raise(parser.start, '`@component` cannot contain more than 2 arguments') - } else if(args.length === 1) { + if(args.length > 0) { node.context = args.pop() } diff --git a/src/Stone/Types/StoneDirectiveType.js b/src/Stone/Types/StoneDirectiveType.js index 510520a..a6b3396 100644 --- a/src/Stone/Types/StoneDirectiveType.js +++ b/src/Stone/Types/StoneDirectiveType.js @@ -21,6 +21,18 @@ export class StoneDirectiveType extends StoneType { } } + static assertArgs(parser, args, minimum = 1, maximum = null) { + if(minimum === maximum) { + if(args.length !== minimum) { + parser.raise(parser.start, `\`@${this.directive}\` must contain exactly ${minimum} argument${minimum !== 1 ? 's' : ''}`) + } + } else if(args.length < minimum) { + parser.raise(parser.start, `\`@${this.directive}\` must contain at least ${minimum} argument${minimum !== 1 ? 's' : ''}`) + } else if(!maximum.isNil && args.length > maximum) { + parser.raise(parser.start, `\`@${this.directive}\` cannot contain more than ${maximum} argument${maximum !== 1 ? 's' : ''}`) + } + } + // Abstract methods static parse(/* parser, node, args */) { diff --git a/src/Stone/Types/StoneEach.js b/src/Stone/Types/StoneEach.js index f49c520..68e249d 100644 --- a/src/Stone/Types/StoneEach.js +++ b/src/Stone/Types/StoneEach.js @@ -14,12 +14,7 @@ export class StoneEach extends StoneDirectiveType { */ static parse(parser, node, params) { node.params = parser._flattenArgs(params) - - if(node.params.length < 3) { - parser.raise(parser.start, '`@each` must contain at least 3 arguments') - } else if(node.params.length > 5) { - parser.raise(parser.start, '`@each` cannot contain more than 5 arguments') - } + this.assertArgs(parser, node.params, 3, 5) parser.next() return parser.finishNode(node, 'StoneEach') diff --git a/src/Stone/Types/StoneExtends.js b/src/Stone/Types/StoneExtends.js index 6a6050f..ddeea9f 100644 --- a/src/Stone/Types/StoneExtends.js +++ b/src/Stone/Types/StoneExtends.js @@ -16,16 +16,11 @@ export class StoneExtends extends StoneDirectiveType { } args = parser._flattenArgs(args) - - if(args.length === 0) { - parser.raise(parser.start, '`@extends` must contain at least 1 argument') - } + this.assertArgs(parser, args, 1, 2) node.view = args.shift() - if(args.length > 1) { - parser.raise(parser.start, '`@extends` cannot contain more than 2 arguments') - } else if(args.length === 1) { + if(args.length > 0) { node.context = args.shift() parser._stoneTemplate.hasLayoutContext = true } diff --git a/src/Stone/Types/StoneHasSection.js b/src/Stone/Types/StoneHasSection.js index 921f7ed..0d71c1f 100644 --- a/src/Stone/Types/StoneHasSection.js +++ b/src/Stone/Types/StoneHasSection.js @@ -11,12 +11,10 @@ export class StoneHasSection extends StoneDirectiveType { static parse(parser, node, args) { args = parser._flattenArgs(args) + this.assertArgs(parser, args, 1, 1) - if(args.length !== 1) { - parser.raise(parser.start, '`@hassection` must contain exactly 1 argument') - } - - (parser._currentIf = (parser._currentIf || [ ])).push(node) + parser._currentIf = parser._currentIf || [ ] + parser._currentIf.push(node) node.section = args.pop() node.consequent = parser.parseUntilEndDirective(endDirectives) diff --git a/src/Stone/Types/StoneInclude.js b/src/Stone/Types/StoneInclude.js index b83a7ed..c7748c6 100644 --- a/src/Stone/Types/StoneInclude.js +++ b/src/Stone/Types/StoneInclude.js @@ -13,16 +13,11 @@ export class StoneInclude extends StoneDirectiveType { */ static parse(parser, node, args) { args = parser._flattenArgs(args) - - if(args.length === 0) { - parser.raise(parser.start, '`@include` must contain at least 1 argument') - } + this.assertArgs(parser, args, 1, 2) node.view = args.shift() - if(args.length > 1) { - parser.raise(parser.start, '`@include` cannot contain more than 2 arguments') - } else if(args.length === 1) { + if(args.length > 0) { node.context = args.shift() } diff --git a/src/Stone/Types/StoneMacro.js b/src/Stone/Types/StoneMacro.js index b15cf15..64f0f2f 100644 --- a/src/Stone/Types/StoneMacro.js +++ b/src/Stone/Types/StoneMacro.js @@ -5,15 +5,14 @@ export class StoneMacro extends StoneDirectiveType { static directive = 'macro' static parse(parser, node, args) { - (parser._currentMacro = (parser._currentMacro || [ ])).push(node) args = parser._flattenArgs(args) - - if(args.length === 0) { - parser.raise(parser.start, '`@macro` must contain at least 1 argument') - } + this.assertArgs(parser, args, 1) node.id = args.shift() + parser._currentMacro = parser._currentMacro || [ ] + parser._currentMacro.push(node) + const output = parser.startNode() output.rescopeContext = true output.params = args diff --git a/src/Stone/Types/StoneSection.js b/src/Stone/Types/StoneSection.js index 149b2ce..e2b4c77 100644 --- a/src/Stone/Types/StoneSection.js +++ b/src/Stone/Types/StoneSection.js @@ -6,16 +6,11 @@ export class StoneSection extends StoneDirectiveType { static parse(parser, node, args) { args = parser._flattenArgs(args) - - if(args.length === 0) { - parser.raise(parser.start, '`@section` must contain at least 1 argument') - } + this.assertArgs(parser, args, 1, 2) node.id = args.shift() - if(args.length > 1) { - parser.raise(parser.start, '`@section` cannot contain more than 2 arguments') - } else if(args.length === 1) { + if(args.length > 0) { node.output = args.pop() node.inline = true parser.next() diff --git a/src/Stone/Types/StoneSet.js b/src/Stone/Types/StoneSet.js index aa6fe8f..11f30e5 100644 --- a/src/Stone/Types/StoneSet.js +++ b/src/Stone/Types/StoneSet.js @@ -38,11 +38,7 @@ export class StoneSet extends StoneDirectiveType { const kind = args.kind || null args = parser._flattenArgs(args) - if(args.length === 0) { - parser.raise(parser.start, '`@set` must contain at least 1 argument') - } else if(args.length > 2) { - parser.raise(parser.start, '`@set` cannot contain more than 2 arguments') - } + this.assertArgs(parser, args, 1, 2) if(args.length === 1 && args[0].type === 'AssignmentExpression') { Object.assign(node, args[0]) diff --git a/src/Stone/Types/StoneSlot.js b/src/Stone/Types/StoneSlot.js index 47a3a5e..8d03636 100644 --- a/src/Stone/Types/StoneSlot.js +++ b/src/Stone/Types/StoneSlot.js @@ -6,16 +6,11 @@ export class StoneSlot extends StoneDirectiveType { static parse(parser, node, args) { args = parser._flattenArgs(args) - - if(args.length === 0) { - parser.raise(parser.start, '`@slot` must contain at least 1 argument') - } + this.assertArgs(parser, args, 1, 2) node.id = args.shift() - if(args.length > 1) { - parser.raise(parser.start, '`@slot` cannot contain more than 2 arguments') - } else if(args.length === 1) { + if(args.length > 0) { node.output = args.pop() node.inline = true parser.next() diff --git a/src/Stone/Types/StoneUnset.js b/src/Stone/Types/StoneUnset.js index 939b192..425a9d2 100644 --- a/src/Stone/Types/StoneUnset.js +++ b/src/Stone/Types/StoneUnset.js @@ -13,6 +13,8 @@ export class StoneUnset extends StoneDirectiveType { */ static parse(parser, node, args) { node.properties = parser._flattenArgs(args) + this.assertArgs(parser, args, 1) + parser.next() return parser.finishNode(node, 'StoneUnset') } diff --git a/src/Stone/Types/StoneYield.js b/src/Stone/Types/StoneYield.js index 3e544e9..6d894f6 100644 --- a/src/Stone/Types/StoneYield.js +++ b/src/Stone/Types/StoneYield.js @@ -14,15 +14,11 @@ export class StoneYield extends StoneDirectiveType { static parse(parser, node, args) { args = parser._flattenArgs(args) - if(args.length === 0) { - parser.raise(parser.start, '`@yield` must contain at least 1 argument') - } + this.assertArgs(parser, args, 1, 2) node.section = args.shift() - if(args.length > 1) { - parser.raise(parser.start, '`@yield` cannot contain more than 2 arguments') - } else if(args.length === 1) { + if(args.length > 0) { node.output = args.pop() } From 97bbb94737febafb735d16ceeded0c0bd3d7bd63 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 01:39:39 -0500 Subject: [PATCH 24/37] Added StoneDirectiveBlockType to abstract away some block logic --- src/Stone/Types/StoneComponent.js | 4 +-- src/Stone/Types/StoneDirectiveBlockType.js | 38 ++++++++++++++++++++++ src/Stone/Types/StoneDirectiveType.js | 4 --- src/Stone/Types/StoneMacro.js | 19 ++--------- src/Stone/Types/StoneSection.js | 22 ++----------- src/Stone/Types/StoneShow.js | 6 ++-- src/Stone/Types/StoneSlot.js | 22 ++----------- src/Stone/Types/StoneSuper.js | 6 ++-- 8 files changed, 57 insertions(+), 64 deletions(-) create mode 100644 src/Stone/Types/StoneDirectiveBlockType.js diff --git a/src/Stone/Types/StoneComponent.js b/src/Stone/Types/StoneComponent.js index 360b6ce..b18530d 100644 --- a/src/Stone/Types/StoneComponent.js +++ b/src/Stone/Types/StoneComponent.js @@ -1,6 +1,6 @@ -import './StoneDirectiveType' +import './StoneDirectiveBlockType' -export class StoneComponent extends StoneDirectiveType { +export class StoneComponent extends StoneDirectiveBlockType { static directive = 'component' diff --git a/src/Stone/Types/StoneDirectiveBlockType.js b/src/Stone/Types/StoneDirectiveBlockType.js new file mode 100644 index 0000000..1040aea --- /dev/null +++ b/src/Stone/Types/StoneDirectiveBlockType.js @@ -0,0 +1,38 @@ +import './StoneDirectiveType' + +// Block directives are directives that have @directive … @enddirective + +export class StoneDirectiveBlockType extends StoneDirectiveType { + + static get startDirective() { return this.directive } + static get endDirective() { return `end${this.directive}` } + static get stackKey() { return `_${this.directive}Stack` } + + static registerParse(parser) { + super.registerParse(parser) + + parser[`parseEnd${this.directive}Directive`] = this._bind('parseEnd') + } + + static pushStack(parser, node) { + (parser[this.stackKey] = parser[this.stackKey] || [ ]).push(node) + } + + static parseUntilEndDirective(parser, node, directive = null) { + this.pushStack(parser, node) + return parser.parseUntilEndDirective(directive || this.endDirective) + } + + static parseEnd(parser, node) { + const stack = parser[this.stackKey] + + if(!Array.isArray(stack) || stack.length === 0) { + parser.raise(parser.start, `\`@${node.directive}\` outside of \`@${this.startDirective}\``) + } + + stack.pop() + + return parser.finishNode(node, 'Directive') + } + +} diff --git a/src/Stone/Types/StoneDirectiveType.js b/src/Stone/Types/StoneDirectiveType.js index a6b3396..4bde03b 100644 --- a/src/Stone/Types/StoneDirectiveType.js +++ b/src/Stone/Types/StoneDirectiveType.js @@ -15,10 +15,6 @@ export class StoneDirectiveType extends StoneType { if(typeof this.parseArgs === 'function') { parser[`parse${directive}DirectiveArgs`] = this._bind('parseArgs') } - - if(typeof this.parseEnd === 'function') { - parser[`parseEnd${this.directive}Directive`] = this._bind('parseEnd') - } } static assertArgs(parser, args, minimum = 1, maximum = null) { diff --git a/src/Stone/Types/StoneMacro.js b/src/Stone/Types/StoneMacro.js index 64f0f2f..1fe6c44 100644 --- a/src/Stone/Types/StoneMacro.js +++ b/src/Stone/Types/StoneMacro.js @@ -1,6 +1,6 @@ -import './StoneDirectiveType' +import './StoneDirectiveBlockType' -export class StoneMacro extends StoneDirectiveType { +export class StoneMacro extends StoneDirectiveBlockType { static directive = 'macro' @@ -10,28 +10,15 @@ export class StoneMacro extends StoneDirectiveType { node.id = args.shift() - parser._currentMacro = parser._currentMacro || [ ] - parser._currentMacro.push(node) - const output = parser.startNode() output.rescopeContext = true output.params = args - output.body = parser.parseUntilEndDirective('endmacro') + output.body = this.parseUntilEndDirective(parser, node) node.output = parser.finishNode(output, 'StoneOutputBlock') return parser.finishNode(node, 'StoneMacro') } - static parseEnd(parser, node) { - if(!parser._currentMacro || parser._currentMacro.length === 0) { - parser.raise(parser.start, '`@endmacro` outside of `@macro`') - } - - parser._currentMacro.pop() - - return parser.finishNode(node, 'Directive') - } - static generate(generator, node, state) { state.write('_[') generator[node.id.type](node.id, state) diff --git a/src/Stone/Types/StoneSection.js b/src/Stone/Types/StoneSection.js index e2b4c77..5dc6e62 100644 --- a/src/Stone/Types/StoneSection.js +++ b/src/Stone/Types/StoneSection.js @@ -1,6 +1,6 @@ -import './StoneDirectiveType' +import './StoneDirectiveBlockType' -export class StoneSection extends StoneDirectiveType { +export class StoneSection extends StoneDirectiveBlockType { static directive = 'section' @@ -15,31 +15,15 @@ export class StoneSection extends StoneDirectiveType { node.inline = true parser.next() } else { - (parser._currentSection = (parser._currentSection || [ ])).push(node) - const output = parser.startNode() output.params = args - output.body = parser.parseUntilEndDirective([ 'show', 'endsection' ]) + output.body = this.parseUntilEndDirective(parser, node, [ 'show', 'endsection' ]) node.output = parser.finishNode(output, 'StoneOutputBlock') } return parser.finishNode(node, 'StoneSection') } - /** - * Ends the current section and returns output - * @return {string} Output from the section - */ - static parseEnd(parser, node) { - if(!parser._currentSection || parser._currentSection.length === 0) { - parser.raise(parser.start, '`@endsection` outside of `@section`') - } - - parser._currentSection.pop() - - return parser.finishNode(node, 'Directive') - } - static generate(generator, node, state) { state.write('_sections.push(') generator[node.id.type](node.id, state) diff --git a/src/Stone/Types/StoneShow.js b/src/Stone/Types/StoneShow.js index cdcab88..c5cdd7a 100644 --- a/src/Stone/Types/StoneShow.js +++ b/src/Stone/Types/StoneShow.js @@ -9,11 +9,13 @@ export class StoneShow extends StoneDirectiveType { * @return {string} Output from the section */ static parse(parser, node) { - if(!parser._currentSection || parser._currentSection.length === 0) { + const stack = parser._sectionStack + + if(!Array.isArray(stack) || stack.length === 0) { parser.raise(parser.start, '`@show` outside of `@section`') } - parser._currentSection.pop().yield = true + stack.pop().yield = true return parser.finishNode(node, 'Directive') } diff --git a/src/Stone/Types/StoneSlot.js b/src/Stone/Types/StoneSlot.js index 8d03636..ce545e4 100644 --- a/src/Stone/Types/StoneSlot.js +++ b/src/Stone/Types/StoneSlot.js @@ -1,6 +1,6 @@ -import './StoneDirectiveType' +import './StoneDirectiveBlockType' -export class StoneSlot extends StoneDirectiveType { +export class StoneSlot extends StoneDirectiveBlockType { static directive = 'slot' @@ -15,31 +15,15 @@ export class StoneSlot extends StoneDirectiveType { node.inline = true parser.next() } else { - (parser._currentSlot = (parser._currentSlot || [ ])).push(node) - const output = parser.startNode() output.params = args - output.body = parser.parseUntilEndDirective('endslot') + output.body = this.parseUntilEndDirective(parser, node) node.output = parser.finishNode(output, 'StoneOutputBlock') } return parser.finishNode(node, 'StoneSlot') } - /** - * Ends the current slot and returns output - * @return {string} Output from the slot - */ - static parseEnd(parser, node) { - if(!parser._currentSlot || parser._currentSlot.length === 0) { - parser.raise(parser.start, '`@endslot` outside of `@slot`') - } - - parser._currentSlot.pop() - - return parser.finishNode(node, 'Directive') - } - static generate(generator, node, state) { state.write('__componentContext[') generator[node.id.type](node.id, state) diff --git a/src/Stone/Types/StoneSuper.js b/src/Stone/Types/StoneSuper.js index 82839fb..9452193 100644 --- a/src/Stone/Types/StoneSuper.js +++ b/src/Stone/Types/StoneSuper.js @@ -13,11 +13,13 @@ export class StoneSuper extends StoneYield { * @return {string} Code to render the super section */ static parse(parser, node) { - if(!parser._currentSection || parser._currentSection.length === 0) { + const stack = parser._sectionStack + + if(!Array.isArray(stack) || stack.length === 0) { parser.raise(parser.start, `\`@${node.directive}\` outside of \`@section\``) } - node.section = { ...parser._currentSection[parser._currentSection.length - 1].id } + node.section = { ...stack[stack.length - 1].id } return parser.finishNode(node, 'StoneSuper') } From c187e80b64b6d8efe9b2e1b9d73a3ecd0d84d066 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 21:48:01 -0500 Subject: [PATCH 25/37] Added StoneBreak type, removing from Loops parser --- src/Stone/Parsers/Loops.js | 28 ----------------------- src/Stone/Support/MakeNode.js | 4 ++++ src/Stone/Types/StoneBreak.js | 42 +++++++++++++++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 4 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 src/Stone/Types/StoneBreak.js diff --git a/src/Stone/Parsers/Loops.js b/src/Stone/Parsers/Loops.js index e6147e2..98ad2fe 100644 --- a/src/Stone/Parsers/Loops.js +++ b/src/Stone/Parsers/Loops.js @@ -76,34 +76,6 @@ export function parseContinueDirective(node, condition) { return this.finishNode(node, 'IfStatement') } -/** - * Generate break node that optionally has a condition - * associated with it. - * - * @param {object} node Blank node - * @param {mixed} condition Optional condition to break on - * @return {object} Finished node - */ -export function parseBreakDirective(node, condition) { - if( - (!this._currentWhile || this._currentWhile.length === 0) - && (!this._currentFor || this._currentFor.length === 0) - ) { - this.raise(this.start, '`@break` outside of `@for` or `@while`') - } - - if(condition.isNil) { - return this.finishNode(node, 'BreakStatement') - } - - const block = this.startNode() - block.body = [ this.finishNode(this.startNode(), 'BreakStatement') ] - - node.test = condition - node.consequent = this.finishNode(block, 'BlockStatement') - return this.finishNode(node, 'IfStatement') -} - export function parseWhileDirectiveArgs(node) { this.pos-- this.parseWhileStatement(node) diff --git a/src/Stone/Support/MakeNode.js b/src/Stone/Support/MakeNode.js index c133a3a..9693fc4 100644 --- a/src/Stone/Support/MakeNode.js +++ b/src/Stone/Support/MakeNode.js @@ -90,6 +90,10 @@ export class MakeNode { return this.parser.finishNode(node, 'ReturnStatement') } + break() { + return this.parser.finishNode(this.parser.startNode(), 'BreakStatement') + } + block(statements) { const node = this.parser.startNode() node.body = statements diff --git a/src/Stone/Types/StoneBreak.js b/src/Stone/Types/StoneBreak.js new file mode 100644 index 0000000..2760d2e --- /dev/null +++ b/src/Stone/Types/StoneBreak.js @@ -0,0 +1,42 @@ +import './StoneDirectiveType' + +/** + * Generate break node that optionally has a condition + * associated with it. + */ +export class StoneBreak extends StoneDirectiveType { + + static directive = 'break' + + static parse(parser, node, condition) { + if( + (!parser._currentWhile || parser._currentWhile.length === 0) + && (!parser._currentFor || parser._currentFor.length === 0) + ) { + parser.raise(parser.start, `\`@${this.directive}\` outside of \`@for\` or \`@while\``) + } + + node.test = condition + return parser.finishNode(node, this.name) + } + + static generate(generator, node, state) { + if(node.test.isNil) { + return generator.BreakStatement(node, state) + } + + return generator.IfStatement({ + ...node, + consequent: this.make.block([ this.make.break() ]) + }, state) + } + + static walk(walker, { test }, st, c) { + if(test.isNil) { + return + } + + c(test, st, 'Expression') + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index 3741f22..1b7710a 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -1,4 +1,5 @@ export const Types = { + ...require('./StoneBreak'), ...require('./StoneComponent'), ...require('./StoneDump'), ...require('./StoneEach'), From 336b309d9341b882a21e9bc00873e9f6c6197196 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 21:50:13 -0500 Subject: [PATCH 26/37] Added StoneContinue type, removing from Loops parser --- src/Stone/Parsers/Loops.js | 28 ---------------------------- src/Stone/Support/MakeNode.js | 5 ++++- src/Stone/Types/StoneContinue.js | 22 ++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 4 files changed, 27 insertions(+), 29 deletions(-) create mode 100644 src/Stone/Types/StoneContinue.js diff --git a/src/Stone/Parsers/Loops.js b/src/Stone/Parsers/Loops.js index 98ad2fe..6b11506 100644 --- a/src/Stone/Parsers/Loops.js +++ b/src/Stone/Parsers/Loops.js @@ -48,34 +48,6 @@ export function parseForeachDirective(node, args) { export const parseForeachDirectiveArgs = parseForDirectiveArgs export const parseEndforeachDirective = parseEndforDirective -/** - * Generate continue node that optionally has a condition - * associated with it. - * - * @param {object} node Blank node - * @param {mixed} condition Optional condition to continue on - * @return {object} Finished node - */ -export function parseContinueDirective(node, condition) { - if( - (!this._currentWhile || this._currentWhile.length === 0) - && (!this._currentFor || this._currentFor.length === 0) - ) { - this.raise(this.start, '`@continue` outside of `@for` or `@while`') - } - - if(condition.isNil) { - return this.finishNode(node, 'ContinueStatement') - } - - const block = this.startNode() - block.body = [ this.finishNode(this.startNode(), 'ContinueStatement') ] - - node.test = condition - node.consequent = this.finishNode(block, 'ContinueStatement') - return this.finishNode(node, 'IfStatement') -} - export function parseWhileDirectiveArgs(node) { this.pos-- this.parseWhileStatement(node) diff --git a/src/Stone/Support/MakeNode.js b/src/Stone/Support/MakeNode.js index 9693fc4..11c5b15 100644 --- a/src/Stone/Support/MakeNode.js +++ b/src/Stone/Support/MakeNode.js @@ -94,13 +94,16 @@ export class MakeNode { return this.parser.finishNode(this.parser.startNode(), 'BreakStatement') } + continue() { + return this.parser.finishNode(this.parser.startNode(), 'ContinueStatement') + } + block(statements) { const node = this.parser.startNode() node.body = statements return this.parser.finishNode(node, 'BlockStatement') } - null() { const node = this.parser.startNode() node.type = 'Literal' diff --git a/src/Stone/Types/StoneContinue.js b/src/Stone/Types/StoneContinue.js new file mode 100644 index 0000000..a83f6f1 --- /dev/null +++ b/src/Stone/Types/StoneContinue.js @@ -0,0 +1,22 @@ +import './StoneBreak' + +/** + * Generate continue node that optionally has a condition + * associated with it. + */ +export class StoneContinue extends StoneBreak { + + static directive = 'continue' + + static generate(generator, node, state) { + if(node.test.isNil) { + return generator.ContinueStatement(node, state) + } + + return generator.IfStatement({ + ...node, + consequent: this.make.block([ this.make.continue() ]) + }, state) + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index 1b7710a..d1328c0 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -1,6 +1,7 @@ export const Types = { ...require('./StoneBreak'), ...require('./StoneComponent'), + ...require('./StoneContinue'), ...require('./StoneDump'), ...require('./StoneEach'), ...require('./StoneEmptyExpression'), From c6d1886e4d60d1beb06458ef0e7b166f63050cdd Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 21:55:00 -0500 Subject: [PATCH 27/37] Added StoneWhile type, removing from Loops parser --- src/Stone/Parsers/Loops.js | 23 ----------------------- src/Stone/Types/StoneBreak.js | 2 +- src/Stone/Types/StoneWhile.js | 31 +++++++++++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 4 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 src/Stone/Types/StoneWhile.js diff --git a/src/Stone/Parsers/Loops.js b/src/Stone/Parsers/Loops.js index 6b11506..83d1dae 100644 --- a/src/Stone/Parsers/Loops.js +++ b/src/Stone/Parsers/Loops.js @@ -47,26 +47,3 @@ export function parseForeachDirective(node, args) { export const parseForeachDirectiveArgs = parseForDirectiveArgs export const parseEndforeachDirective = parseEndforDirective - -export function parseWhileDirectiveArgs(node) { - this.pos-- - this.parseWhileStatement(node) - - return null -} - -export function parseWhileDirective(node) { - (this._currentWhile = (this._currentWhile || [ ])).push(node) - node.body = this.parseUntilEndDirective('endwhile') - return this.finishNode(node, node.type) -} - -export function parseEndwhileDirective(node) { - if(!this._currentWhile || this._currentWhile.length === 0) { - this.raise(this.start, '`@endwhile` outside of `@while`') - } - - this._currentWhile.pop() - - return this.finishNode(node, 'Directive') -} diff --git a/src/Stone/Types/StoneBreak.js b/src/Stone/Types/StoneBreak.js index 2760d2e..e1591d0 100644 --- a/src/Stone/Types/StoneBreak.js +++ b/src/Stone/Types/StoneBreak.js @@ -10,7 +10,7 @@ export class StoneBreak extends StoneDirectiveType { static parse(parser, node, condition) { if( - (!parser._currentWhile || parser._currentWhile.length === 0) + (!Array.isArray(parser._whileStack) || parser._whileStack.length === 0) && (!parser._currentFor || parser._currentFor.length === 0) ) { parser.raise(parser.start, `\`@${this.directive}\` outside of \`@for\` or \`@while\``) diff --git a/src/Stone/Types/StoneWhile.js b/src/Stone/Types/StoneWhile.js new file mode 100644 index 0000000..474bd74 --- /dev/null +++ b/src/Stone/Types/StoneWhile.js @@ -0,0 +1,31 @@ +import './StoneDirectiveBlockType' + +export class StoneWhile extends StoneDirectiveBlockType { + + static directive = 'while' + + static parseArgs(parser, node) { + parser.pos-- + parser.parseWhileStatement(node) + + return null + } + + static parse(parser, node) { + node.body = this.parseUntilEndDirective(parser, node) + return parser.finishNode(node, 'StoneWhile') + } + + static generate(generator, node, state) { + generator.WhileStatement(node, state) + } + + static walk(walker, node, st, c) { + walker.WhileStatement(node, st, c) + } + + static scope(scoper, node, scope) { + scoper.WhileStatement(node, scope) + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index d1328c0..2d3efb1 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -21,5 +21,6 @@ export const Types = { ...require('./StoneSuper'), ...require('./StoneTemplate'), ...require('./StoneUnset'), + ...require('./StoneWhile'), ...require('./StoneYield'), } From 6710794736e08024cfa979c1c5e4e7e3f756fb28 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 21:56:59 -0500 Subject: [PATCH 28/37] Merged StoneLoop into new StoneFor type (and added StoneForeach alias type) that now handles for loops --- src/Stone/Parsers/Loops.js | 49 ------------ src/Stone/Parsers/index.js | 1 - src/Stone/Types/StoneBreak.js | 3 +- src/Stone/Types/StoneFor.js | 129 ++++++++++++++++++++++++++++++++ src/Stone/Types/StoneForeach.js | 9 +++ src/Stone/Types/StoneLoop.js | 87 --------------------- src/Stone/Types/index.js | 3 +- 7 files changed, 142 insertions(+), 139 deletions(-) delete mode 100644 src/Stone/Parsers/Loops.js create mode 100644 src/Stone/Types/StoneFor.js create mode 100644 src/Stone/Types/StoneForeach.js delete mode 100644 src/Stone/Types/StoneLoop.js diff --git a/src/Stone/Parsers/Loops.js b/src/Stone/Parsers/Loops.js deleted file mode 100644 index 83d1dae..0000000 --- a/src/Stone/Parsers/Loops.js +++ /dev/null @@ -1,49 +0,0 @@ -export function parseForDirectiveArgs(node) { - this.pos-- - this.parseForStatement(node) - - return null -} - -export function parseForDirective(node, args, until = 'endfor') { - const loop = node - - if(node.type === 'ForOfStatement' || node.type === 'ForInStatement') { - node = this.startNode() - node.type = 'StoneLoop' - node.loop = loop - } - - (this._currentFor = (this._currentFor || [ ])).push(node) - loop.body = this.parseUntilEndDirective(until) - return this.finishNode(node, node.type) -} - -export function parseEndforDirective(node) { - if(!this._currentFor || this._currentFor.length === 0) { - if(node.directive === 'endforeach') { - this.raise(this.start, '`@endforeach` outside of `@foreach`') - } else { - this.raise(this.start, '`@endfor` outside of `@for`') - } - } - - const open = this._currentFor.pop() - - if(open.directive === 'for' && node.directive === 'endforeach') { - this.raise(this.start, '`@endfor` must be used with `@for`') - } else if(open.directive === 'foreach' && node.directive === 'endfor') { - this.raise(this.start, '`@endforeach` must be used with `@foreach`') - } - - return this.finishNode(node, 'Directive') -} - -export function parseForeachDirective(node, args) { - // No difference between for and foreach - // Included for consistency with Blade - return this.parseForDirective(node, args, 'endforeach') -} - -export const parseForeachDirectiveArgs = parseForDirectiveArgs -export const parseEndforeachDirective = parseEndforDirective diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index bf92f6f..3d49dba 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -1,5 +1,4 @@ export const Parsers = { ...require('./Conditionals'), - ...require('./Loops'), ...require('./Output'), } diff --git a/src/Stone/Types/StoneBreak.js b/src/Stone/Types/StoneBreak.js index e1591d0..3ce6443 100644 --- a/src/Stone/Types/StoneBreak.js +++ b/src/Stone/Types/StoneBreak.js @@ -11,7 +11,8 @@ export class StoneBreak extends StoneDirectiveType { static parse(parser, node, condition) { if( (!Array.isArray(parser._whileStack) || parser._whileStack.length === 0) - && (!parser._currentFor || parser._currentFor.length === 0) + && (!Array.isArray(parser._forStack) || parser._forStack.length === 0) + && (!Array.isArray(parser._foreachStack) || parser._foreachStack.length === 0) ) { parser.raise(parser.start, `\`@${this.directive}\` outside of \`@for\` or \`@while\``) } diff --git a/src/Stone/Types/StoneFor.js b/src/Stone/Types/StoneFor.js new file mode 100644 index 0000000..c5cd6b6 --- /dev/null +++ b/src/Stone/Types/StoneFor.js @@ -0,0 +1,129 @@ +import './StoneDirectiveBlockType' + +export class StoneFor extends StoneDirectiveBlockType { + + static directive = 'for' + + static parseArgs(parser, node) { + parser.pos-- + parser.parseForStatement(node) + + return null + } + + static parse(parser, node) { + switch(node.type) { + case 'ForOfStatement': + node.kind = 'of' + break + case 'ForInStatement': + node.kind = 'in' + break + case 'ForStatement': + node.kind = 'simple' + break + default: + parser.raise(parser.start, 'Unexpected `@for` type') + } + + node.body = this.parseUntilEndDirective(parser, node) + return parser.finishNode(node, this.name) + } + + static generate(generator, node, state) { + if(node.kind === 'simple') { + return generator.ForStatement(node, state) + } + + // TODO: Future optimizations should check if + // the `loop` var is used before injecting + // support for it. + state.__loops = (state.__loops || 0) + 1 + const loopVariable = `__loop${state.__loops}` + node.scope.add(loopVariable) + node.body.scope.add('loop') + + state.write(`const ${loopVariable} = new _.StoneLoop(`) + + if(node.kind === 'in') { + state.write('Object.keys(') + } + + generator[node.right.type](node.right, state) + + if(node.kind === 'in') { + state.write(')') + } + + state.write(');') + state.write(state.lineEnd) + state.write(state.indent) + + state.write(`${loopVariable}.depth = ${state.__loops};`) + state.write(state.lineEnd) + state.write(state.indent) + + if(state.__loops > 1) { + state.write(`${loopVariable}.parent = __loop${state.__loops - 1};`) + state.write(state.lineEnd) + state.write(state.indent) + } + + const positions = { + start: node.body.start, + end: node.body.end + } + + node.body.body.unshift({ + ...positions, + type: 'VariableDeclaration', + declarations: [ + { + ...positions, + type: 'VariableDeclarator', + id: { + ...positions, + type: 'Identifier', + name: 'loop' + }, + init: { + ...positions, + type: 'Identifier', + name: loopVariable + } + } + ], + kind: 'const' + }) + + generator.ForOfStatement({ + ...node, + type: 'ForOfStatement', + right: { + ...node.right, + type: 'Identifier', + name: loopVariable + } + }, state) + } + + static walk(walker, node, st, c) { + if(node.kind === 'simple') { + return walker.ForStatement(node, st, c) + } + + c(node, st, 'Expression') + } + + static scope(scoper, node, scope) { + switch(node.kind) { + case 'of': + return scoper.ForOfStatement(node, scope) + case 'in': + return scoper.ForInStatement(node, scope) + case 'simple': + return scoper.ForStatement(node, scope) + } + } + +} diff --git a/src/Stone/Types/StoneForeach.js b/src/Stone/Types/StoneForeach.js new file mode 100644 index 0000000..92bbc8d --- /dev/null +++ b/src/Stone/Types/StoneForeach.js @@ -0,0 +1,9 @@ +import './StoneFor' + +// No difference between for and foreach +// Included for consistency with Blade +export class StoneForeach extends StoneFor { + + static directive = 'foreach' + +} diff --git a/src/Stone/Types/StoneLoop.js b/src/Stone/Types/StoneLoop.js deleted file mode 100644 index bb0cb25..0000000 --- a/src/Stone/Types/StoneLoop.js +++ /dev/null @@ -1,87 +0,0 @@ -import './StoneType' - -export class StoneLoop extends StoneType { - - static generate(generator, { loop }, state) { - // TODO: Future optimizations should check if - // the `loop` var is used before injecting - // support for it. - - state.__loops = (state.__loops || 0) + 1 - const loopVariable = `__loop${state.__loops}` - loop.scope.add(loopVariable) - loop.body.scope.add('loop') - - state.write(`const ${loopVariable} = new _.StoneLoop(`) - - if(loop.type === 'ForInStatement') { - state.write('Object.keys(') - } - - generator[loop.right.type](loop.right, state) - - if(loop.type === 'ForInStatement') { - state.write(')') - } - - state.write(');') - state.write(state.lineEnd) - state.write(state.indent) - - state.write(`${loopVariable}.depth = ${state.__loops};`) - state.write(state.lineEnd) - state.write(state.indent) - - if(state.__loops > 1) { - state.write(`${loopVariable}.parent = __loop${state.__loops - 1};`) - state.write(state.lineEnd) - state.write(state.indent) - } - - const positions = { - start: loop.body.start, - end: loop.body.end - } - - loop.body.body.unshift({ - ...positions, - type: 'VariableDeclaration', - declarations: [ - { - ...positions, - type: 'VariableDeclarator', - id: { - ...positions, - type: 'Identifier', - name: 'loop' - }, - init: { - ...positions, - type: 'Identifier', - name: loopVariable - } - } - ], - kind: 'const' - }) - - generator.ForOfStatement({ - ...loop, - type: 'ForOfStatement', - right: { - ...loop.right, - type: 'Identifier', - name: loopVariable - } - }, state) - } - - static walk(walker, { loop }, st, c) { - c(loop, st, 'Expression') - } - - static scope(scoper, node, scope) { - return scoper._scope(node.loop, scope) - } - -} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index 2d3efb1..e6a92ae 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -6,9 +6,10 @@ export const Types = { ...require('./StoneEach'), ...require('./StoneEmptyExpression'), ...require('./StoneExtends'), + ...require('./StoneFor'), + ...require('./StoneForeach'), ...require('./StoneHasSection'), ...require('./StoneInclude'), - ...require('./StoneLoop'), ...require('./StoneMacro'), ...require('./StoneOutput'), ...require('./StoneOutputBlock'), From 03c2e8e81df1b337553893bb4514105ccef7107e Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 22:10:32 -0500 Subject: [PATCH 29/37] Added StoneUnless type, removing from Conditionals parser --- src/Stone/Parsers/Conditionals.js | 24 ---------------------- src/Stone/Types/StoneUnless.js | 33 +++++++++++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 3 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 src/Stone/Types/StoneUnless.js diff --git a/src/Stone/Parsers/Conditionals.js b/src/Stone/Parsers/Conditionals.js index b0cea90..69b4b55 100644 --- a/src/Stone/Parsers/Conditionals.js +++ b/src/Stone/Parsers/Conditionals.js @@ -50,27 +50,3 @@ export function parseEndifDirective(node) { return this.finishNode(node, 'Directive') } - -export function parseUnlessDirective(node, args) { - (this._currentUnless = (this._currentUneless || [ ])).push(node) - - const unary = this.startNode() - unary.operator = '!' - unary.prefix = true - unary.argument = args - this.finishNode(unary, 'UnaryExpression') - - node.test = unary - node.consequent = this.parseUntilEndDirective('endunless') - return this.finishNode(node, 'IfStatement') -} - -export function parseEndunlessDirective(node) { - if(!this._currentUnless || this._currentUnless.length === 0) { - this.raise(this.start, '`@endunless` outside of `@unless`') - } - - this._currentUnless.pop() - - return this.finishNode(node, 'Directive') -} diff --git a/src/Stone/Types/StoneUnless.js b/src/Stone/Types/StoneUnless.js new file mode 100644 index 0000000..4f304ee --- /dev/null +++ b/src/Stone/Types/StoneUnless.js @@ -0,0 +1,33 @@ +import './StoneDirectiveBlockType' + +export class StoneUnless extends StoneDirectiveBlockType { + + static directive = 'unless' + + static parse(parser, node, condition) { + node.test = condition + node.consequent = this.parseUntilEndDirective(parser, node) + return parser.finishNode(node, this.name) + } + + static generate(generator, node, state) { + generator.IfStatement({ + ...node, + test: { + type: 'UnaryExpression', + operator: '!', + prefix: true, + argument: node.test + } + }, state) + } + + static walk(walker, node, st, c) { + walker.IfStatement(node, st, c) + } + + static scope(scoper, node, scope) { + scoper.IfStatement(node, scope) + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index e6a92ae..9cfe8c3 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -22,6 +22,7 @@ export const Types = { ...require('./StoneSuper'), ...require('./StoneTemplate'), ...require('./StoneUnset'), + ...require('./StoneUnless'), ...require('./StoneWhile'), ...require('./StoneYield'), } From 8a404789933b43d8fd313d1e7f7e02f9334d37de Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 22:23:24 -0500 Subject: [PATCH 30/37] Added StoneElse type, removing from Conditionals parser --- src/Stone/Parsers/Conditionals.js | 16 ------------- src/Stone/Types/StoneElse.js | 37 +++++++++++++++++++++++++++++++ src/Stone/Types/index.js | 3 ++- 3 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 src/Stone/Types/StoneElse.js diff --git a/src/Stone/Parsers/Conditionals.js b/src/Stone/Parsers/Conditionals.js index 69b4b55..adc0469 100644 --- a/src/Stone/Parsers/Conditionals.js +++ b/src/Stone/Parsers/Conditionals.js @@ -25,22 +25,6 @@ export function parseElseifDirective(node, args) { return this.finishNode(node, 'IfStatement') } -export function parseElseDirective(node) { - if(!this._currentIf || this._currentIf.length === 0) { - this.raise(this.start, '`@else` outside of `@if`') - } - - const level = this._currentIf.length - 1 - - if(this._currentIf[level].alternate) { - this.raise(this.start, '`@else` after `@else`') - } - - this._currentIf[level].alternate = true - this._currentIf[level].alternate = this.parseUntilEndDirective(endDirectives) - return this.finishNode(node, 'BlockStatement') -} - export function parseEndifDirective(node) { if(!this._currentIf || this._currentIf.length === 0) { this.raise(this.start, '`@endif` outside of `@if`') diff --git a/src/Stone/Types/StoneElse.js b/src/Stone/Types/StoneElse.js new file mode 100644 index 0000000..c57f2b8 --- /dev/null +++ b/src/Stone/Types/StoneElse.js @@ -0,0 +1,37 @@ +import './StoneDirectiveType' +import { endDirectives } from '../Parsers/Conditionals' + +export class StoneElse extends StoneDirectiveType { + + static directive = 'else' + + static parse(parser, node) { + if(!parser._currentIf || parser._currentIf.length === 0) { + parser.raise(parser.start, '`@else` outside of `@if`') + } + + const level = parser._currentIf.length - 1 + + if(parser._currentIf[level].alternate) { + parser.raise(parser.start, '`@else` after `@else`') + } + + parser._currentIf[level].alternate = true + parser._currentIf[level].alternate = Object.assign(node, parser.parseUntilEndDirective(endDirectives)) + + return parser.finishNode(node, this.name) + } + + static generate(generator, node, state) { + generator.BlockStatement(node, state) + } + + static walk(walker, node, st, c) { + walker.BlockStatement(node, st, c) + } + + static scope(scoper, node, scope) { + scoper.BlockStatement(node, scope) + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index 9cfe8c3..dd926a5 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -4,6 +4,7 @@ export const Types = { ...require('./StoneContinue'), ...require('./StoneDump'), ...require('./StoneEach'), + ...require('./StoneElse'), ...require('./StoneEmptyExpression'), ...require('./StoneExtends'), ...require('./StoneFor'), @@ -21,8 +22,8 @@ export const Types = { ...require('./StoneSlot'), ...require('./StoneSuper'), ...require('./StoneTemplate'), - ...require('./StoneUnset'), ...require('./StoneUnless'), + ...require('./StoneUnset'), ...require('./StoneWhile'), ...require('./StoneYield'), } From 7036efde53f514c4922329cebf499dd74b298239 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 22:27:37 -0500 Subject: [PATCH 31/37] Added StoneElseif type, removing from Conditionals parser --- src/Stone/Parsers/Conditionals.js | 18 -------------- src/Stone/Types/StoneElseif.js | 39 +++++++++++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 3 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 src/Stone/Types/StoneElseif.js diff --git a/src/Stone/Parsers/Conditionals.js b/src/Stone/Parsers/Conditionals.js index adc0469..5ff0329 100644 --- a/src/Stone/Parsers/Conditionals.js +++ b/src/Stone/Parsers/Conditionals.js @@ -7,24 +7,6 @@ export function parseIfDirective(node, args) { return this.finishNode(node, 'IfStatement') } -export function parseElseifDirective(node, args) { - if(!this._currentIf || this._currentIf.length === 0) { - this.raise(this.start, '`@elseif` outside of `@if`') - } - - const level = this._currentIf.length - 1 - - if(this._currentIf[level].alternate) { - this.raise(this.start, '`@elseif` after `@else`') - } - - this._currentIf[level].alternate = node - this._currentIf[level] = node - node.test = args - node.consequent = this.parseUntilEndDirective(endDirectives) - return this.finishNode(node, 'IfStatement') -} - export function parseEndifDirective(node) { if(!this._currentIf || this._currentIf.length === 0) { this.raise(this.start, '`@endif` outside of `@if`') diff --git a/src/Stone/Types/StoneElseif.js b/src/Stone/Types/StoneElseif.js new file mode 100644 index 0000000..55f5d6c --- /dev/null +++ b/src/Stone/Types/StoneElseif.js @@ -0,0 +1,39 @@ +import './StoneDirectiveType' +import { endDirectives } from '../Parsers/Conditionals' + +export class StoneElseif extends StoneDirectiveType { + + static directive = 'elseif' + + static parse(parser, node, condition) { + if(!parser._currentIf || parser._currentIf.length === 0) { + parser.raise(parser.start, '`@elseif` outside of `@if`') + } + + const level = parser._currentIf.length - 1 + + if(parser._currentIf[level].alternate) { + parser.raise(parser.start, '`@elseif` after `@else`') + } + + parser._currentIf[level].alternate = node + parser._currentIf[level] = node + node.test = condition + node.consequent = parser.parseUntilEndDirective(endDirectives) + + return parser.finishNode(node, this.name) + } + + static generate(generator, node, state) { + generator.IfStatement(node, state) + } + + static walk(walker, node, st, c) { + walker.IfStatement(node, st, c) + } + + static scope(scoper, node, scope) { + scoper.IfStatement(node, scope) + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index dd926a5..c1d8615 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -5,6 +5,7 @@ export const Types = { ...require('./StoneDump'), ...require('./StoneEach'), ...require('./StoneElse'), + ...require('./StoneElseif'), ...require('./StoneEmptyExpression'), ...require('./StoneExtends'), ...require('./StoneFor'), From 20f747caedde662f71bcfd02e328fe6fa7bcbfa2 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 22:34:26 -0500 Subject: [PATCH 32/37] Added StoneIf type, removing Conditionals parser --- src/Stone/Parsers/Conditionals.js | 18 ----------------- src/Stone/Parsers/index.js | 1 - src/Stone/Types/StoneElse.js | 15 ++++++++------ src/Stone/Types/StoneElseif.js | 14 ++++++------- src/Stone/Types/StoneHasSection.js | 9 ++++----- src/Stone/Types/StoneIf.js | 32 ++++++++++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 7 files changed, 53 insertions(+), 37 deletions(-) delete mode 100644 src/Stone/Parsers/Conditionals.js create mode 100644 src/Stone/Types/StoneIf.js diff --git a/src/Stone/Parsers/Conditionals.js b/src/Stone/Parsers/Conditionals.js deleted file mode 100644 index 5ff0329..0000000 --- a/src/Stone/Parsers/Conditionals.js +++ /dev/null @@ -1,18 +0,0 @@ -export const endDirectives = [ 'endif', 'elseif', 'else' ] - -export function parseIfDirective(node, args) { - (this._currentIf = (this._currentIf || [ ])).push(node) - node.test = args - node.consequent = this.parseUntilEndDirective(endDirectives) - return this.finishNode(node, 'IfStatement') -} - -export function parseEndifDirective(node) { - if(!this._currentIf || this._currentIf.length === 0) { - this.raise(this.start, '`@endif` outside of `@if`') - } - - this._currentIf.pop() - - return this.finishNode(node, 'Directive') -} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js index 3d49dba..13678f8 100644 --- a/src/Stone/Parsers/index.js +++ b/src/Stone/Parsers/index.js @@ -1,4 +1,3 @@ export const Parsers = { - ...require('./Conditionals'), ...require('./Output'), } diff --git a/src/Stone/Types/StoneElse.js b/src/Stone/Types/StoneElse.js index c57f2b8..e052ae6 100644 --- a/src/Stone/Types/StoneElse.js +++ b/src/Stone/Types/StoneElse.js @@ -1,23 +1,26 @@ import './StoneDirectiveType' -import { endDirectives } from '../Parsers/Conditionals' +import './StoneIf' export class StoneElse extends StoneDirectiveType { static directive = 'else' static parse(parser, node) { - if(!parser._currentIf || parser._currentIf.length === 0) { + if(!parser._ifStack || parser._ifStack.length === 0) { parser.raise(parser.start, '`@else` outside of `@if`') } - const level = parser._currentIf.length - 1 + const level = parser._ifStack.length - 1 - if(parser._currentIf[level].alternate) { + if(parser._ifStack[level].alternate) { parser.raise(parser.start, '`@else` after `@else`') } - parser._currentIf[level].alternate = true - parser._currentIf[level].alternate = Object.assign(node, parser.parseUntilEndDirective(endDirectives)) + parser._ifStack[level].alternate = true + parser._ifStack[level].alternate = Object.assign( + node, + parser.parseUntilEndDirective(StoneIf.endDirectives) + ) return parser.finishNode(node, this.name) } diff --git a/src/Stone/Types/StoneElseif.js b/src/Stone/Types/StoneElseif.js index 55f5d6c..29b8565 100644 --- a/src/Stone/Types/StoneElseif.js +++ b/src/Stone/Types/StoneElseif.js @@ -1,25 +1,25 @@ import './StoneDirectiveType' -import { endDirectives } from '../Parsers/Conditionals' +import './StoneIf' export class StoneElseif extends StoneDirectiveType { static directive = 'elseif' static parse(parser, node, condition) { - if(!parser._currentIf || parser._currentIf.length === 0) { + if(!parser._ifStack || parser._ifStack.length === 0) { parser.raise(parser.start, '`@elseif` outside of `@if`') } - const level = parser._currentIf.length - 1 + const level = parser._ifStack.length - 1 - if(parser._currentIf[level].alternate) { + if(parser._ifStack[level].alternate) { parser.raise(parser.start, '`@elseif` after `@else`') } - parser._currentIf[level].alternate = node - parser._currentIf[level] = node + parser._ifStack[level].alternate = node + parser._ifStack[level] = node node.test = condition - node.consequent = parser.parseUntilEndDirective(endDirectives) + node.consequent = parser.parseUntilEndDirective(StoneIf.endDirectives) return parser.finishNode(node, this.name) } diff --git a/src/Stone/Types/StoneHasSection.js b/src/Stone/Types/StoneHasSection.js index 0d71c1f..4ce7682 100644 --- a/src/Stone/Types/StoneHasSection.js +++ b/src/Stone/Types/StoneHasSection.js @@ -1,6 +1,5 @@ import './StoneDirectiveType' - -import { endDirectives } from '../Parsers/Conditionals' +import './StoneIf' /** * Convenience directive to determine if a section has content @@ -13,11 +12,11 @@ export class StoneHasSection extends StoneDirectiveType { args = parser._flattenArgs(args) this.assertArgs(parser, args, 1, 1) - parser._currentIf = parser._currentIf || [ ] - parser._currentIf.push(node) + parser._ifStack = parser._ifStack || [ ] + parser._ifStack.push(node) node.section = args.pop() - node.consequent = parser.parseUntilEndDirective(endDirectives) + node.consequent = parser.parseUntilEndDirective(StoneIf.endDirectives) return parser.finishNode(node, 'StoneHasSection') } diff --git a/src/Stone/Types/StoneIf.js b/src/Stone/Types/StoneIf.js new file mode 100644 index 0000000..01fa750 --- /dev/null +++ b/src/Stone/Types/StoneIf.js @@ -0,0 +1,32 @@ +import './StoneDirectiveBlockType' + +import './StoneElse' +import './StoneElseif' + +export class StoneIf extends StoneDirectiveBlockType { + + static directive = 'if' + + static get endDirectives() { + return [ this.endDirective, StoneElse.directive, StoneElseif.directive ] + } + + static parse(parser, node, condition) { + node.test = condition + node.consequent = this.parseUntilEndDirective(parser, node, this.endDirectives) + return parser.finishNode(node, this.name) + } + + static generate(generator, node, state) { + generator.IfStatement(node, state) + } + + static walk(walker, node, st, c) { + walker.IfStatement(node, st, c) + } + + static scope(scoper, node, scope) { + scoper.IfStatement(node, scope) + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index c1d8615..8851760 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -11,6 +11,7 @@ export const Types = { ...require('./StoneFor'), ...require('./StoneForeach'), ...require('./StoneHasSection'), + ...require('./StoneIf'), ...require('./StoneInclude'), ...require('./StoneMacro'), ...require('./StoneOutput'), From 777ff5d540430265b43d548963d07ea59433bc40 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 22:46:50 -0500 Subject: [PATCH 33/37] Better abstracted stack logic in StoneDirectiveBlockType --- src/Stone/Types/StoneDirectiveBlockType.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Stone/Types/StoneDirectiveBlockType.js b/src/Stone/Types/StoneDirectiveBlockType.js index 1040aea..421c3e1 100644 --- a/src/Stone/Types/StoneDirectiveBlockType.js +++ b/src/Stone/Types/StoneDirectiveBlockType.js @@ -18,19 +18,27 @@ export class StoneDirectiveBlockType extends StoneDirectiveType { (parser[this.stackKey] = parser[this.stackKey] || [ ]).push(node) } + static popStack(parser) { + parser[this.stackKey].pop() + } + + static hasStack(parser) { + const stack = parser[this.stackKey] + + return Array.isArray(stack) && stack.length > 0 + } + static parseUntilEndDirective(parser, node, directive = null) { this.pushStack(parser, node) return parser.parseUntilEndDirective(directive || this.endDirective) } static parseEnd(parser, node) { - const stack = parser[this.stackKey] - - if(!Array.isArray(stack) || stack.length === 0) { + if(!this.hasStack(parser, node)) { parser.raise(parser.start, `\`@${node.directive}\` outside of \`@${this.startDirective}\``) } - stack.pop() + this.popStack(parser, node) return parser.finishNode(node, 'Directive') } From 6b33c26e5b5ff80993bd134392800e844ebad8d9 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 22:49:21 -0500 Subject: [PATCH 34/37] Added StoneSpaceless type, removing from Output parser --- src/Stone/Parsers/Output.js | 28 ------------------------ src/Stone/Types/StoneSpaceless.js | 36 +++++++++++++++++++++++++++++++ src/Stone/Types/index.js | 1 + 3 files changed, 37 insertions(+), 28 deletions(-) create mode 100644 src/Stone/Types/StoneSpaceless.js diff --git a/src/Stone/Parsers/Output.js b/src/Stone/Parsers/Output.js index 77e8f5e..aba162b 100644 --- a/src/Stone/Parsers/Output.js +++ b/src/Stone/Parsers/Output.js @@ -3,34 +3,6 @@ import '../Tokens/StoneDirective' const { tokTypes: tt } = require('acorn') -/** - * Increases the spaceless level - * - * @param {object} node Blank node - * @return {object} Finished node - */ -export function parseSpacelessDirective(node) { - this._spaceless = (this._spaceless || 0) + 1 - Object.assign(node, this.parseUntilEndDirective('endspaceless')) - return this.finishNode(node, 'BlockStatement') -} - -/** - * Decreases the spaceless level - * - * @param {object} node Blank node - * @return {object} Finished node - */ -export function parseEndspacelessDirective(node) { - if(!this._spaceless || this._spaceless === 0) { - this.raise(this.start, '`@endspaceless` outside of `@spaceless`') - } - - this._spaceless-- - - return this.finishNode(node, 'Directive') -} - /** * Parses output in Stone files * diff --git a/src/Stone/Types/StoneSpaceless.js b/src/Stone/Types/StoneSpaceless.js new file mode 100644 index 0000000..6c0feea --- /dev/null +++ b/src/Stone/Types/StoneSpaceless.js @@ -0,0 +1,36 @@ +import './StoneDirectiveBlockType' + +export class StoneSpaceless extends StoneDirectiveBlockType { + + static directive = 'spaceless' + + static parse(parser, node) { + Object.assign(node, this.parseUntilEndDirective(parser, node)) + return parser.finishNode(node, this.name) + } + + static pushStack(parser) { + parser._spaceless = (parser._spaceless || 0) + 1 + } + + static popStack(parser) { + parser._spaceless-- + } + + static hasStack(parser) { + return parser._spaceless > 0 + } + + static generate(generator, node, state) { + generator.BlockStatement(node, state) + } + + static walk(walker, node, st, c) { + walker.BlockStatement(node, st, c) + } + + static scope(scoper, node, scope) { + scoper.BlockStatement(node, scope) + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js index 8851760..4612f6b 100644 --- a/src/Stone/Types/index.js +++ b/src/Stone/Types/index.js @@ -22,6 +22,7 @@ export const Types = { ...require('./StoneSet'), ...require('./StoneShow'), ...require('./StoneSlot'), + ...require('./StoneSpaceless'), ...require('./StoneSuper'), ...require('./StoneTemplate'), ...require('./StoneUnless'), From 43d13f54331b317caf432d7790bfcf9cb7185ece Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Mon, 4 Dec 2017 23:05:38 -0500 Subject: [PATCH 35/37] Merged output parsing into the StoneOutput type --- src/Stone/Parser.js | 16 +-- src/Stone/Parsers/Output.js | 188 --------------------------------- src/Stone/Parsers/index.js | 3 - src/Stone/Types/StoneOutput.js | 170 +++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 197 deletions(-) delete mode 100644 src/Stone/Parsers/Output.js delete mode 100644 src/Stone/Parsers/index.js diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js index 0231772..77ff1b9 100644 --- a/src/Stone/Parser.js +++ b/src/Stone/Parser.js @@ -1,4 +1,3 @@ -import './Parsers' import './Types' import './Contexts/DirectiveArgs' @@ -241,6 +240,16 @@ export class Parser { return this.finishNode(node, 'BlockStatement') } + skipStoneComment() { + const end = this.input.indexOf('--}}', this.pos += 4) + + if(end === -1) { + this.raise(this.pos - 4, 'Unterminated comment') + } + + this.pos = end + 4 + } + _isCharCode(code, delta = 0) { return this.input.charCodeAt(this.pos + delta) === code } @@ -286,11 +295,6 @@ export class Parser { } -// Inject the parsers -for(const [ name, func ] of Object.entries(Parsers)) { - Parser.prototype[name] = func -} - // Inject parsers for each type for(const type of Object.values(Types)) { type.registerParse(Parser.prototype) diff --git a/src/Stone/Parsers/Output.js b/src/Stone/Parsers/Output.js deleted file mode 100644 index aba162b..0000000 --- a/src/Stone/Parsers/Output.js +++ /dev/null @@ -1,188 +0,0 @@ -import '../Tokens/StoneOutput' -import '../Tokens/StoneDirective' - -const { tokTypes: tt } = require('acorn') - -/** - * Parses output in Stone files - * - * @return {object} Finished node - */ -export function parseStoneOutput() { - if(this.type !== StoneOutput.type) { - this.unexpected() - } - - const node = this.startNode() - - this.inOutput = true - node.output = this.readStoneOutput() - this.inOutput = false - - return this.finishNode(node, 'StoneOutput') -} - -/** - * Reads the output in Stone files - * - * @return {object} Template literal node - */ -export function readStoneOutput() { - const node = this.startNode() - node.expressions = [ ] - this.next() - - let curElt = this.parseStoneOutputElement() - node.quasis = [ curElt ] - - while(!curElt.tail) { - const isUnsafe = this.type === StoneOutput.openUnsafe - - if(isUnsafe) { - this.expect(StoneOutput.openUnsafe) - } else { - this.expect(StoneOutput.openSafe) - } - - const expression = this.startNode() - expression.safe = !isUnsafe - expression.value = this.parseExpression() - node.expressions.push(this.finishNode(expression, 'StoneOutputExpression')) - - this.skipSpace() - this.pos++ - - if(isUnsafe) { - if(this.type !== tt.prefix) { - this.unexpected() - } else { - this.type = tt.braceR - this.context.pop() - } - - this.pos++ - } - - this.next() - - node.quasis.push(curElt = this.parseStoneOutputElement()) - } - - this.next() - return this.finishNode(node, 'TemplateLiteral') -} - -/** - * Parses chunks of output between braces and directives - * - * @return {object} Template element node - */ -export function parseStoneOutputElement() { - const elem = this.startNode() - let output = this.value - - // Strip space between tags if spaceless - if(this._spaceless > 0) { - output = output.replace(/>\s+<').trim() - } - - // Escape escape characters - output = output.replace(/\\/g, '\\\\') - - // Escape backticks - output = output.replace(/`/g, '\\`') - - // Escape whitespace characters - output = output.replace(/[\n]/g, '\\n') - output = output.replace(/[\r]/g, '\\r') - output = output.replace(/[\t]/g, '\\t') - - elem.value = { - raw: output, - cooked: this.value - } - - this.next() - - elem.tail = this.type === StoneDirective.type || this.type === tt.eof - return this.finishNode(elem, 'TemplateElement') -} - -/** - * Controls the output flow - */ -export function readOutputToken() { - let chunkStart = this.pos - let out = '' - - const pushChunk = () => { - out += this.input.slice(chunkStart, this.pos) - chunkStart = this.pos - } - - const finishChunk = () => { - pushChunk() - return this.finishToken(StoneOutput.output, out) - } - - for(;;) { - if(this.pos >= this.input.length) { - if(this.pos === this.start) { - return this.finishToken(tt.eof) - } - - return finishChunk() - } - - const ch = this.input.charCodeAt(this.pos) - - if(ch === 64 && this._isCharCode(123, 1)) { - if(this._isCharCode(123, 2)) { - pushChunk() - chunkStart = this.pos + 1 - } - - this.pos++ - } else if( - ch === 64 - || (ch === 123 && this._isCharCode(123, 1) && !this._isCharCode(64, -1)) - || (ch === 123 && this._isCharCode(33, 1) && this._isCharCode(33, 2)) - ) { - if(ch === 123 && this._isCharCode(45, 2) && this._isCharCode(45, 3)) { - pushChunk() - this.skipStoneComment() - chunkStart = this.pos - continue - } else if(this.pos === this.start && this.type === StoneOutput.output) { - if(ch === 123) { - if(this._isCharCode(33, 1)) { - this.pos += 3 - return this.finishToken(StoneOutput.openUnsafe) - } else { - this.pos += 2 - return this.finishToken(StoneOutput.openSafe) - } - } - - return this.finishToken(StoneDirective.type) - } - - return finishChunk() - } else { - ++this.pos - } - } -} - -/** - * Skips past the current Stone comment - */ -export function skipStoneComment() { - const end = this.input.indexOf('--}}', this.pos += 4) - - if(end === -1) { - this.raise(this.pos - 4, 'Unterminated comment') - } - - this.pos = end + 4 -} diff --git a/src/Stone/Parsers/index.js b/src/Stone/Parsers/index.js deleted file mode 100644 index 13678f8..0000000 --- a/src/Stone/Parsers/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export const Parsers = { - ...require('./Output'), -} diff --git a/src/Stone/Types/StoneOutput.js b/src/Stone/Types/StoneOutput.js index 958d09a..4fd32c6 100644 --- a/src/Stone/Types/StoneOutput.js +++ b/src/Stone/Types/StoneOutput.js @@ -1,7 +1,177 @@ import './StoneType' +import { StoneOutput as StoneOutputToken } from '../Tokens/StoneOutput' +import { StoneDirective as StoneDirectiveToken } from '../Tokens/StoneDirective' +const { tokTypes: tt } = require('acorn') + export class StoneOutput extends StoneType { + static registerParse(parser) { + parser.parseStoneOutput = this._bind('parse') + parser.readOutputToken = this._bind('readOutputToken') + } + + static parse(parser) { + if(parser.type !== StoneOutputToken.type) { + parser.unexpected() + } + + const node = parser.startNode() + + parser.inOutput = true + node.output = this.read(parser) + parser.inOutput = false + + return parser.finishNode(node, 'StoneOutput') + } + + /** + * Parses chunks of output between braces and directives + * + * @return {object} Template element node + */ + static parseOutputElement(parser) { + const elem = parser.startNode() + let output = parser.value + + // Strip space between tags if spaceless + if(parser._spaceless > 0) { + output = output.replace(/>\s+<').trim() + } + + // Escape escape characters + output = output.replace(/\\/g, '\\\\') + + // Escape backticks + output = output.replace(/`/g, '\\`') + + // Escape whitespace characters + output = output.replace(/[\n]/g, '\\n') + output = output.replace(/[\r]/g, '\\r') + output = output.replace(/[\t]/g, '\\t') + + elem.value = { + raw: output, + cooked: parser.value + } + + parser.next() + + elem.tail = parser.type === StoneDirectiveToken.type || parser.type === tt.eof + return parser.finishNode(elem, 'TemplateElement') + } + + static read(parser) { + const node = parser.startNode() + node.expressions = [ ] + parser.next() + + let curElt = this.parseOutputElement(parser) + node.quasis = [ curElt ] + + while(!curElt.tail) { + const isUnsafe = parser.type === StoneOutputToken.openUnsafe + + if(isUnsafe) { + parser.expect(StoneOutputToken.openUnsafe) + } else { + parser.expect(StoneOutputToken.openSafe) + } + + const expression = parser.startNode() + expression.safe = !isUnsafe + expression.value = parser.parseExpression() + node.expressions.push(parser.finishNode(expression, 'StoneOutputExpression')) + + parser.skipSpace() + parser.pos++ + + if(isUnsafe) { + if(parser.type !== tt.prefix) { + parser.unexpected() + } else { + parser.type = tt.braceR + parser.context.pop() + } + + parser.pos++ + } + + parser.next() + + node.quasis.push(curElt = this.parseOutputElement(parser)) + } + + parser.next() + return parser.finishNode(node, 'TemplateLiteral') + } + + /** + * Controls the output flow + */ + static readOutputToken(parser) { + let chunkStart = parser.pos + let out = '' + + const pushChunk = () => { + out += parser.input.slice(chunkStart, parser.pos) + chunkStart = parser.pos + } + + const finishChunk = () => { + pushChunk() + return parser.finishToken(StoneOutputToken.output, out) + } + + for(;;) { + if(parser.pos >= parser.input.length) { + if(parser.pos === parser.start) { + return parser.finishToken(tt.eof) + } + + return finishChunk() + } + + const ch = parser.input.charCodeAt(parser.pos) + + if(ch === 64 && parser._isCharCode(123, 1)) { + if(parser._isCharCode(123, 2)) { + pushChunk() + chunkStart = parser.pos + 1 + } + + parser.pos++ + } else if( + ch === 64 + || (ch === 123 && parser._isCharCode(123, 1) && !parser._isCharCode(64, -1)) + || (ch === 123 && parser._isCharCode(33, 1) && parser._isCharCode(33, 2)) + ) { + if(ch === 123 && parser._isCharCode(45, 2) && parser._isCharCode(45, 3)) { + pushChunk() + parser.skipStoneComment() + chunkStart = parser.pos + continue + } else if(parser.pos === parser.start && parser.type === StoneOutputToken.output) { + if(ch === 123) { + if(parser._isCharCode(33, 1)) { + parser.pos += 3 + return parser.finishToken(StoneOutputToken.openUnsafe) + } else { + parser.pos += 2 + return parser.finishToken(StoneOutputToken.openSafe) + } + } + + return parser.finishToken(StoneDirectiveToken.type) + } + + return finishChunk() + } else { + ++parser.pos + } + } + } + static generate(generator, { output }, state) { state.write('output += ') generator[output.type](output, state) From 23b4c83d1f77ee5b03d046e1c08dcab6b4d6e137 Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Tue, 5 Dec 2017 21:59:21 -0500 Subject: [PATCH 36/37] Updated whitespace to match pre-acorn outputs * All view tests should now pass except for tags, as tagged components have not yet been implemented * The only test case that was updated is `nested` because pre-acorn stone was incorrectly trimming the last line of a template if it came after a directive * Moved `set-destructuring` to `set-destructuring-array` and added `set-destructuring-object` --- src/CacheManager.js | 2 +- src/Stone.js | 2 +- src/Stone/Types/StoneOutput.js | 27 ++++++++++++++++--- src/Stone/Types/StoneSection.js | 1 + ...ring.html => set-destructuring-array.html} | 0 .../assignments/set-destructuring-array.stone | 2 ++ .../assignments/set-destructuring-object.html | 1 + ...g.stone => set-destructuring-object.stone} | 0 test/views/layouts/nested.html | 2 +- 9 files changed, 30 insertions(+), 7 deletions(-) rename test/views/assignments/{set-destructuring.html => set-destructuring-array.html} (100%) create mode 100644 test/views/assignments/set-destructuring-array.stone create mode 100644 test/views/assignments/set-destructuring-object.html rename test/views/assignments/{set-destructuring.stone => set-destructuring-object.stone} (100%) diff --git a/src/CacheManager.js b/src/CacheManager.js index c3ebe5a..82875f5 100644 --- a/src/CacheManager.js +++ b/src/CacheManager.js @@ -1,5 +1,5 @@ import { FS } from 'grind-support' -import './AST' +// import './AST' const path = require('path') diff --git a/src/Stone.js b/src/Stone.js index e697a2e..db73a2b 100644 --- a/src/Stone.js +++ b/src/Stone.js @@ -37,7 +37,7 @@ export class Stone { static parse(code, pathname = null) { this._register() - return acorn.parse(code, { + return acorn.parse(code.replace(/\s*$/g, ''), { ecmaVersion: 9, plugins: { objectSpread: true, diff --git a/src/Stone/Types/StoneOutput.js b/src/Stone/Types/StoneOutput.js index 4fd32c6..bad7848 100644 --- a/src/Stone/Types/StoneOutput.js +++ b/src/Stone/Types/StoneOutput.js @@ -22,6 +22,13 @@ export class StoneOutput extends StoneType { node.output = this.read(parser) parser.inOutput = false + if(this.isEmpty(node)) { + // Only add the output if the string isn’t + // blank to avoid unnecessary whitespace before + // a directive + return parser.finishNode(node, 'StoneEmptyExpression') + } + return parser.finishNode(node, 'StoneOutput') } @@ -30,9 +37,14 @@ export class StoneOutput extends StoneType { * * @return {object} Template element node */ - static parseOutputElement(parser) { + static parseOutputElement(parser, first = false) { const elem = parser.startNode() - let output = parser.value + let output = parser.value || '' + + if(first && output[0] === '\n') { + // Ignore the first newline after a directive + output = output.substring(1) + } // Strip space between tags if spaceless if(parser._spaceless > 0) { @@ -66,7 +78,7 @@ export class StoneOutput extends StoneType { node.expressions = [ ] parser.next() - let curElt = this.parseOutputElement(parser) + let curElt = this.parseOutputElement(parser, true) node.quasis = [ curElt ] while(!curElt.tail) { @@ -99,7 +111,7 @@ export class StoneOutput extends StoneType { parser.next() - node.quasis.push(curElt = this.parseOutputElement(parser)) + node.quasis.push(curElt = this.parseOutputElement(parser, false)) } parser.next() @@ -186,4 +198,11 @@ export class StoneOutput extends StoneType { return scoper._scope(output, scope) } + static isEmpty(node) { + return node.output.type === 'TemplateLiteral' + && node.output.expressions.length === 0 + && node.output.quasis.length === 1 + && node.output.quasis[0].value.cooked.trim().length === 0 + } + } diff --git a/src/Stone/Types/StoneSection.js b/src/Stone/Types/StoneSection.js index 5dc6e62..bdc38fb 100644 --- a/src/Stone/Types/StoneSection.js +++ b/src/Stone/Types/StoneSection.js @@ -18,6 +18,7 @@ export class StoneSection extends StoneDirectiveBlockType { const output = parser.startNode() output.params = args output.body = this.parseUntilEndDirective(parser, node, [ 'show', 'endsection' ]) + output.returnRaw = true node.output = parser.finishNode(output, 'StoneOutputBlock') } diff --git a/test/views/assignments/set-destructuring.html b/test/views/assignments/set-destructuring-array.html similarity index 100% rename from test/views/assignments/set-destructuring.html rename to test/views/assignments/set-destructuring-array.html diff --git a/test/views/assignments/set-destructuring-array.stone b/test/views/assignments/set-destructuring-array.stone new file mode 100644 index 0000000..e6831f7 --- /dev/null +++ b/test/views/assignments/set-destructuring-array.stone @@ -0,0 +1,2 @@ +@set([ i, j ], [ 1, 2 ]) +The values are {{ i }} and {{ j }}. diff --git a/test/views/assignments/set-destructuring-object.html b/test/views/assignments/set-destructuring-object.html new file mode 100644 index 0000000..a8d0f36 --- /dev/null +++ b/test/views/assignments/set-destructuring-object.html @@ -0,0 +1 @@ +The values are 1 and 2. diff --git a/test/views/assignments/set-destructuring.stone b/test/views/assignments/set-destructuring-object.stone similarity index 100% rename from test/views/assignments/set-destructuring.stone rename to test/views/assignments/set-destructuring-object.stone diff --git a/test/views/layouts/nested.html b/test/views/layouts/nested.html index 180d850..eef0230 100644 --- a/test/views/layouts/nested.html +++ b/test/views/layouts/nested.html @@ -7,5 +7,5 @@
- + From f3674586e875debfd29111220f41cec657a07f7d Mon Sep 17 00:00:00 2001 From: shnhrrsn Date: Sun, 10 Dec 2017 12:32:27 -0500 Subject: [PATCH 37/37] Moved readOutputToken to StoneOutput token --- src/Stone/Tokens/StoneOutput.js | 64 ++++++++++++++++++++++++- src/Stone/Tokens/StoneOutput/Chunk.js | 3 +- src/Stone/Types/StoneOutput.js | 67 --------------------------- 3 files changed, 64 insertions(+), 70 deletions(-) diff --git a/src/Stone/Tokens/StoneOutput.js b/src/Stone/Tokens/StoneOutput.js index f44c508..f0d7526 100644 --- a/src/Stone/Tokens/StoneOutput.js +++ b/src/Stone/Tokens/StoneOutput.js @@ -1,10 +1,11 @@ import './TokenType' +import './StoneDirective' import './StoneOutput/Chunk' import './StoneOutput/OpenSafe' import './StoneOutput/OpenUnsafe' -const { TokenType: AcornTokenType } = require('acorn') +const { TokenType: AcornTokenType, tokTypes: tt } = require('acorn') export class StoneOutput extends TokenType { @@ -22,7 +23,66 @@ export class StoneOutput extends TokenType { this.context.isExpr = true this.context.preserveSpace = true - this.context.override = p => p.readOutputToken() + this.context.override = p => this.constructor.readOutputToken(p) + } + + static readOutputToken(parser) { + let chunkStart = parser.pos + let out = '' + + const pushChunk = () => { + out += parser.input.slice(chunkStart, parser.pos) + chunkStart = parser.pos + } + + const finishChunk = () => { + pushChunk() + return parser.finishToken(this.output, out) + } + + for(;;) { + if(parser.pos >= parser.input.length) { + if(parser.pos === parser.start) { + return parser.finishToken(tt.eof) + } + + return finishChunk() + } + + const ch = parser.input.charCodeAt(parser.pos) + + if(ch === 64 && parser._isCharCode(123, 1)) { + if(parser._isCharCode(123, 2)) { + pushChunk() + chunkStart = parser.pos + 1 + } + } else if( + ch === 64 + || (ch === 123 && parser._isCharCode(123, 1) && !parser._isCharCode(64, -1)) + || (ch === 123 && parser._isCharCode(33, 1) && parser._isCharCode(33, 2)) + ) { + if(ch === 123 && parser._isCharCode(45, 2) && parser._isCharCode(45, 3)) { + pushChunk() + parser.skipStoneComment() + chunkStart = parser.pos + continue + } else if(parser.pos === parser.start && parser.type === this.output) { + if(ch !== 123) { + return parser.finishToken(StoneDirective.type) + } else if(parser._isCharCode(33, 1)) { + parser.pos += 3 + return parser.finishToken(this.openUnsafe) + } + + parser.pos += 2 + return parser.finishToken(this.openSafe) + } + + return finishChunk() + } + + ++parser.pos + } } } diff --git a/src/Stone/Tokens/StoneOutput/Chunk.js b/src/Stone/Tokens/StoneOutput/Chunk.js index 84a4e91..72f0f34 100644 --- a/src/Stone/Tokens/StoneOutput/Chunk.js +++ b/src/Stone/Tokens/StoneOutput/Chunk.js @@ -1,4 +1,5 @@ import '../TokenType' +import '../StoneOutput' export class Chunk extends TokenType { @@ -7,7 +8,7 @@ export class Chunk extends TokenType { this.context.isExpr = true this.context.preserveSpace = true - this.context.override = p => p.readOutputToken() + this.context.override = p => StoneOutput.readOutputToken(p) } update(parser) { diff --git a/src/Stone/Types/StoneOutput.js b/src/Stone/Types/StoneOutput.js index bad7848..cf98e45 100644 --- a/src/Stone/Types/StoneOutput.js +++ b/src/Stone/Types/StoneOutput.js @@ -8,7 +8,6 @@ export class StoneOutput extends StoneType { static registerParse(parser) { parser.parseStoneOutput = this._bind('parse') - parser.readOutputToken = this._bind('readOutputToken') } static parse(parser) { @@ -118,72 +117,6 @@ export class StoneOutput extends StoneType { return parser.finishNode(node, 'TemplateLiteral') } - /** - * Controls the output flow - */ - static readOutputToken(parser) { - let chunkStart = parser.pos - let out = '' - - const pushChunk = () => { - out += parser.input.slice(chunkStart, parser.pos) - chunkStart = parser.pos - } - - const finishChunk = () => { - pushChunk() - return parser.finishToken(StoneOutputToken.output, out) - } - - for(;;) { - if(parser.pos >= parser.input.length) { - if(parser.pos === parser.start) { - return parser.finishToken(tt.eof) - } - - return finishChunk() - } - - const ch = parser.input.charCodeAt(parser.pos) - - if(ch === 64 && parser._isCharCode(123, 1)) { - if(parser._isCharCode(123, 2)) { - pushChunk() - chunkStart = parser.pos + 1 - } - - parser.pos++ - } else if( - ch === 64 - || (ch === 123 && parser._isCharCode(123, 1) && !parser._isCharCode(64, -1)) - || (ch === 123 && parser._isCharCode(33, 1) && parser._isCharCode(33, 2)) - ) { - if(ch === 123 && parser._isCharCode(45, 2) && parser._isCharCode(45, 3)) { - pushChunk() - parser.skipStoneComment() - chunkStart = parser.pos - continue - } else if(parser.pos === parser.start && parser.type === StoneOutputToken.output) { - if(ch === 123) { - if(parser._isCharCode(33, 1)) { - parser.pos += 3 - return parser.finishToken(StoneOutputToken.openUnsafe) - } else { - parser.pos += 2 - return parser.finishToken(StoneOutputToken.openSafe) - } - } - - return parser.finishToken(StoneDirectiveToken.type) - } - - return finishChunk() - } else { - ++parser.pos - } - } - } - static generate(generator, { output }, state) { state.write('output += ') generator[output.type](output, state)