diff --git a/src/AST.js b/src/AST.js deleted file mode 100644 index c73cd50..0000000 --- a/src/AST.js +++ /dev/null @@ -1,72 +0,0 @@ -const acorn = require('acorn5-object-spread') -const base = require('acorn/dist/walk').base -const astring = require('astring') - -export class AST { - - static parse(string) { - return acorn.parse(string, { - ecmaVersion: 9, - plugins: { - objectSpread: true - } - }) - } - - static walk(node, visitors) { - (function c(node, st, override) { - if(node.isNil) { - // This happens during RestElement, unsure why. - return - } - - const type = override || node.type - const found = visitors[type] - - if(found) { - found(node, st) - } - - base[type](node, st, c) - })(node) - } - - static walkVariables(node, callback) { - if(node.type === 'ArrayPattern') { - for(const element of node.elements) { - this.walkVariables(element, callback) - } - } else if(node.type === 'ObjectPattern') { - for(const property of node.properties) { - if(property.type === 'RestElement') { - callback(property.argument) - } else { - this.walkVariables(property.value, callback) - } - } - } else if(node.type === 'AssignmentPattern') { - this.walkVariables(node.left, callback) - } else { - callback(node) - } - } - - 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/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/Compiler.js b/src/Compiler.js index cdc4b79..04d708e 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -1,7 +1,8 @@ -import './Errors/StoneCompilerError' -import './StoneTemplate' +import './Stone' +import './Support/StoneSections' const fs = require('fs') +const vm = require('vm') export class Compiler { @@ -34,10 +35,10 @@ export class Compiler { this.engine.view.emit('compile:start', file) } - const template = new StoneTemplate(this, contents, file) + let template = null try { - template.compile() + template = Stone.stringify(Stone.parse(contents, file)) } catch(err) { if(!err._hasTemplate) { err._hasTemplate = true @@ -56,46 +57,16 @@ export class Compiler { } if(!shouldEval) { - return template.toString() + return template } - return template.toFunction() - } - - 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.`) + try { + const script = new vm.Script(`(${template})`, { filename: file }) + return script.runInNewContext({ StoneSections }) + } catch(err) { + console.log('template', template) + throw err } - - 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/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/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/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/Layouts.js b/src/Compiler/Layouts.js deleted file mode 100644 index e7b7de8..0000000 --- a/src/Compiler/Layouts.js +++ /dev/null @@ -1,136 +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) {` -} - -/** - * 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/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/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/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/Stone.js b/src/Stone.js new file mode 100644 index 0000000..db73a2b --- /dev/null +++ b/src/Stone.js @@ -0,0 +1,92 @@ +import './Stone/Generator' +import './Stone/Parser' +import './Stone/Scoper' +import './Stone/Walker' + +const acorn = require('acorn5-object-spread/inject')(require('acorn')) +const astring = require('astring').generate + +export class Stone { + + static _register() { + if(acorn.plugins.stone) { + return + } + + acorn.plugins.stone = (parser, config) => { + parser._stoneTemplatePathname = config.template || null + + 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, pathname = null) { + this._register() + + return acorn.parse(code.replace(/\s*$/g, ''), { + ecmaVersion: 9, + plugins: { + objectSpread: true, + stone: { + template: pathname + } + } + }) + } + + static stringify(tree) { + Scoper.scope(tree) + return astring(tree, { generator: Generator }) + } + + static walk(node, visitors) { + (function c(node, st, override) { + if(node.isNil) { + // This happens during RestElement, unsure why. + return + } + + const type = override || node.type + const found = visitors[type] + + if(found) { + found(node, st) + } + + Walker[type](node, st, c) + })(node) + } + + static walkVariables(node, callback) { + if(node.type === 'ArrayPattern') { + for(const element of node.elements) { + this.walkVariables(element, callback) + } + } else if(node.type === 'ObjectPattern') { + for(const property of node.properties) { + 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) + } + } + +} 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/Generator.js b/src/Stone/Generator.js new file mode 100644 index 0000000..945be6e --- /dev/null +++ b/src/Stone/Generator.js @@ -0,0 +1,88 @@ +import './Types' + +const { baseGenerator } = require('astring') + +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') { + 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 + } + + 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 [ + 'BlockStatement', + 'FunctionDeclaration', + 'ForStatement', + 'ForOfStatement', + 'ForInStatement', + 'WhileStatement', + 'FunctionExpression', + 'ArrowFunctionExpression' +]) { + Generator[key] = function(node, state) { + state.pushScope(node.scope) + const value = baseGenerator[key].call(this, node, state) + state.popScope() + return value + } +} + +for(const type of Object.values(Types)) { + type.registerGenerate(Generator) +} diff --git a/src/Stone/Parser.js b/src/Stone/Parser.js new file mode 100644 index 0000000..77ff1b9 --- /dev/null +++ b/src/Stone/Parser.js @@ -0,0 +1,301 @@ +import './Types' + +import './Contexts/DirectiveArgs' +import './Contexts/PreserveSpace' + +import './Support/MakeNode' + +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 template = new acorn.Node(this) + template.type = 'StoneTemplate' + template.pathname = this._stoneTemplatePathname + this._stoneTemplate = template + this.make = new MakeNode(this) + + const node = this.startNode() + this.nextToken() + + const result = this.parseTopLevel(node) + + template.output = new acorn.Node(this) + template.output.type = 'StoneOutputBlock' + template.output.body = this.make.block(result.body) + + result.body = [ template ] + + 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) + } + + 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.make.block([ ]) + } + + 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 + } + + if(directive.length === 0) { + this.unexpected() + } + + let args = null + const parse = `parse${directive[0].toUpperCase()}${directive.substring(1).toLowerCase()}Directive` + const node = this.startNode() + node.directive = directive + + 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 DirectiveArgs) + + const parseArgs = `${parse}Args` + + if(typeof this[parseArgs] === 'function') { + args = this[parseArgs](node) + } else { + args = this.parseDirectiveArgs() + } + + this.context.pop() + this.inDirective = false + } + } else { + this.next() + } + + if(typeof this[parse] !== 'function') { + this.raise(this.start, `Unknown directive: ${directive}`) + } + + 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) + } else { + directives = new Set([ directives ]) + } + + const node = this.startNode() + 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.reset() + } + + break contents + } else if(node.type !== 'BlankExpression') { + statements.push(node) + } + + this.reset() + 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 + } + } + + node.body = statements + 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 + } + + _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 ] + } + + _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 parsers for each type +for(const type of Object.values(Types)) { + type.registerParse(Parser.prototype) +} diff --git a/src/Stone/Scoper.js b/src/Stone/Scoper.js new file mode 100644 index 0000000..be1ac6d --- /dev/null +++ b/src/Stone/Scoper.js @@ -0,0 +1,170 @@ +import './Types' +import './Support/Scope' + +export class Scoper { + + static defaultScope = new Scope(null, [ + 'Object', + 'Set', + 'Date', + 'Array', + 'String', + 'global', + 'process', + 'StoneSections' + ]) + + 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 = scope.branch() + + if(!declarations.isNil) { + this._scope(declarations, node.scope) + } + + return this._scope(node.body, node.scope) + } + + static BlockStatement(node, scope) { + node.scope = scope.branch() + + for(const statement of node.body) { + this._scope(statement, node.scope) + } + } + + static Program = Scoper.BlockStatement + + static FunctionDeclaration(node, scope) { + node.scope = scope.branch() + + 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 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) + } + + 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 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) + } + } + + 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 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 + } + + scope.add(node.name) + } + +} + +for(const type of Object.values(Types)) { + type.registerScope(Scoper) +} diff --git a/src/Stone/Support/MakeNode.js b/src/Stone/Support/MakeNode.js new file mode 100644 index 0000000..11c5b15 --- /dev/null +++ b/src/Stone/Support/MakeNode.js @@ -0,0 +1,121 @@ +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') + } + + break() { + 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' + 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 + } + +} 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/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..f0d7526 --- /dev/null +++ b/src/Stone/Tokens/StoneOutput.js @@ -0,0 +1,88 @@ +import './TokenType' + +import './StoneDirective' +import './StoneOutput/Chunk' +import './StoneOutput/OpenSafe' +import './StoneOutput/OpenUnsafe' + +const { TokenType: AcornTokenType, tokTypes: tt } = 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 => 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 new file mode 100644 index 0000000..72f0f34 --- /dev/null +++ b/src/Stone/Tokens/StoneOutput/Chunk.js @@ -0,0 +1,24 @@ +import '../TokenType' +import '../StoneOutput' + +export class Chunk extends TokenType { + + constructor() { + super('stoneOutputChunk') + + this.context.isExpr = true + this.context.preserveSpace = true + this.context.override = p => StoneOutput.readOutputToken(p) + } + + 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/StoneBreak.js b/src/Stone/Types/StoneBreak.js new file mode 100644 index 0000000..3ce6443 --- /dev/null +++ b/src/Stone/Types/StoneBreak.js @@ -0,0 +1,43 @@ +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( + (!Array.isArray(parser._whileStack) || parser._whileStack.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\``) + } + + 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/StoneComponent.js b/src/Stone/Types/StoneComponent.js new file mode 100644 index 0000000..b18530d --- /dev/null +++ b/src/Stone/Types/StoneComponent.js @@ -0,0 +1,98 @@ +import './StoneDirectiveBlockType' + +export class StoneComponent extends StoneDirectiveBlockType { + + static directive = 'component' + + static parse(parser, node, args) { + args = parser._flattenArgs(args) + + this.assertArgs(parser, args, 1, 2) + + node.view = args.shift() + + if(args.length > 0) { + node.context = args.pop() + } + + (parser._currentComponent = (parser._currentComponent || [ ])).push(node) + + const output = parser.startNode() + output.params = args + output.body = parser.parseUntilEndDirective('endcomponent') + node.output = parser.finishNode(output, 'StoneOutputBlock') + + return parser.finishNode(node, 'StoneComponent') + } + + /** + * 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`') + } + + parser._currentComponent.pop() + + return parser.finishNode(node, 'Directive') + } + + static generate(generator, node, state) { + node.output.assignments = node.output.assignments || [ ] + + 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: { + type: 'MemberExpression', + object: this.make.identifier('_'), + property: this.make.identifier('$stone'), + }, + property: this.make.identifier('include'), + }, + 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(')();') + } + + static walk(walker, node, st, c) { + // TODO + } + + static scope(scoper, node, scope) { + node.scope = scope.branch([ + '__componentView', + '__componentContext' + ]) + + scoper._scope(node.output, node.scope) + } + +} 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/StoneDirectiveBlockType.js b/src/Stone/Types/StoneDirectiveBlockType.js new file mode 100644 index 0000000..421c3e1 --- /dev/null +++ b/src/Stone/Types/StoneDirectiveBlockType.js @@ -0,0 +1,46 @@ +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 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) { + if(!this.hasStack(parser, node)) { + parser.raise(parser.start, `\`@${node.directive}\` outside of \`@${this.startDirective}\``) + } + + this.popStack(parser, node) + + return parser.finishNode(node, 'Directive') + } + +} diff --git a/src/Stone/Types/StoneDirectiveType.js b/src/Stone/Types/StoneDirectiveType.js new file mode 100644 index 0000000..4bde03b --- /dev/null +++ b/src/Stone/Types/StoneDirectiveType.js @@ -0,0 +1,38 @@ +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') + } + } + + 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 */) { + throw new Error('Subclasses must implement') + } + +} diff --git a/src/Stone/Types/StoneDump.js b/src/Stone/Types/StoneDump.js new file mode 100644 index 0000000..86c5e8d --- /dev/null +++ b/src/Stone/Types/StoneDump.js @@ -0,0 +1,30 @@ +import './StoneDirectiveType' + +export class StoneDump extends StoneDirectiveType { + + 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') + } + +} diff --git a/src/Stone/Types/StoneEach.js b/src/Stone/Types/StoneEach.js new file mode 100644 index 0000000..68e249d --- /dev/null +++ b/src/Stone/Types/StoneEach.js @@ -0,0 +1,41 @@ +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) + this.assertArgs(parser, node.params, 3, 5) + + parser.next() + return parser.finishNode(node, 'StoneEach') + } + + static generate(generator, node, state) { + node.params.unshift({ + type: 'Identifier', + name: '_' + }, { + type: 'Identifier', + name: '_templatePathname' + }) + + state.write('output += _.$stone.each') + generator.SequenceExpression({ expressions: node.params }, state) + state.write(';') + } + + static walk() { + // Do nothing + } + +} diff --git a/src/Stone/Types/StoneElse.js b/src/Stone/Types/StoneElse.js new file mode 100644 index 0000000..e052ae6 --- /dev/null +++ b/src/Stone/Types/StoneElse.js @@ -0,0 +1,40 @@ +import './StoneDirectiveType' +import './StoneIf' + +export class StoneElse extends StoneDirectiveType { + + static directive = 'else' + + static parse(parser, node) { + if(!parser._ifStack || parser._ifStack.length === 0) { + parser.raise(parser.start, '`@else` outside of `@if`') + } + + const level = parser._ifStack.length - 1 + + if(parser._ifStack[level].alternate) { + parser.raise(parser.start, '`@else` after `@else`') + } + + parser._ifStack[level].alternate = true + parser._ifStack[level].alternate = Object.assign( + node, + parser.parseUntilEndDirective(StoneIf.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/StoneElseif.js b/src/Stone/Types/StoneElseif.js new file mode 100644 index 0000000..29b8565 --- /dev/null +++ b/src/Stone/Types/StoneElseif.js @@ -0,0 +1,39 @@ +import './StoneDirectiveType' +import './StoneIf' + +export class StoneElseif extends StoneDirectiveType { + + static directive = 'elseif' + + static parse(parser, node, condition) { + if(!parser._ifStack || parser._ifStack.length === 0) { + parser.raise(parser.start, '`@elseif` outside of `@if`') + } + + const level = parser._ifStack.length - 1 + + if(parser._ifStack[level].alternate) { + parser.raise(parser.start, '`@elseif` after `@else`') + } + + parser._ifStack[level].alternate = node + parser._ifStack[level] = node + node.test = condition + node.consequent = parser.parseUntilEndDirective(StoneIf.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/StoneEmptyExpression.js b/src/Stone/Types/StoneEmptyExpression.js new file mode 100644 index 0000000..d9d9364 --- /dev/null +++ b/src/Stone/Types/StoneEmptyExpression.js @@ -0,0 +1,13 @@ +import './StoneType' + +export class StoneEmptyExpression extends StoneType { + + static generate() { + // Do nothing + } + + static walk() { + // Do nothing + } + +} diff --git a/src/Stone/Types/StoneExtends.js b/src/Stone/Types/StoneExtends.js new file mode 100644 index 0000000..ddeea9f --- /dev/null +++ b/src/Stone/Types/StoneExtends.js @@ -0,0 +1,66 @@ +import './StoneDirectiveType' + +export class StoneExtends extends StoneDirectiveType { + + static directive = 'extends' + + static parse(parser, node, args) { + if(parser._stoneTemplate.isNil) { + parser.unexpected() + } + + 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) + this.assertArgs(parser, args, 1, 2) + + node.view = args.shift() + + if(args.length > 0) { + node.context = args.shift() + parser._stoneTemplate.hasLayoutContext = true + } + + parser.next() + return parser.finishNode(node, 'StoneExtends') + } + + static generate(generator, node, state) { + state.write('__extendsLayout = ') + generator[node.view.type](node.view, state) + state.write(';') + + 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(';') + } + + static walk(walker, node, st, c) { + c(node.view, st, 'Pattern') + + if(node.context.isNil) { + return + } + + c(node.context, st, 'Expression') + } + + static scope(scoper, node, scope) { + if(node.context.isNil) { + return + } + + scoper._scope(node.context, scope) + } + +} 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/StoneHasSection.js b/src/Stone/Types/StoneHasSection.js new file mode 100644 index 0000000..4ce7682 --- /dev/null +++ b/src/Stone/Types/StoneHasSection.js @@ -0,0 +1,54 @@ +import './StoneDirectiveType' +import './StoneIf' + +/** + * Convenience directive to determine if a section has content + */ +export class StoneHasSection extends StoneDirectiveType { + + static directive = 'hassection' + + static parse(parser, node, args) { + args = parser._flattenArgs(args) + this.assertArgs(parser, args, 1, 1) + + parser._ifStack = parser._ifStack || [ ] + parser._ifStack.push(node) + + node.section = args.pop() + node.consequent = parser.parseUntilEndDirective(StoneIf.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' + } + }, + arguments: [ node.section ] + } + + return generator.IfStatement(node, state) + } + + static walk(walker, 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/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/StoneInclude.js b/src/Stone/Types/StoneInclude.js new file mode 100644 index 0000000..c7748c6 --- /dev/null +++ b/src/Stone/Types/StoneInclude.js @@ -0,0 +1,58 @@ +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) + this.assertArgs(parser, args, 1, 2) + + node.view = args.shift() + + if(args.length > 0) { + node.context = args.shift() + } + + parser.next() + return parser.finishNode(node, 'StoneInclude') + } + + static generate(generator, node, state) { + state.write('output += _.$stone.include(_, _sections, _templatePathname, ') + generator[node.view.type](node.view, state) + + if(!node.context.isNil) { + state.write(', ') + generator[node.context.type](node.context, state) + } + + state.write(');') + } + + static walk(walker, node, st, c) { + c(node.view, st, 'Pattern') + + if(node.context.isNil) { + return + } + + c(node.context, st, 'Expression') + } + + static scope(scoper, node, scope) { + if(node.context.isNil) { + return + } + + scoper._scope(node.context, scope) + } + +} diff --git a/src/Stone/Types/StoneMacro.js b/src/Stone/Types/StoneMacro.js new file mode 100644 index 0000000..1fe6c44 --- /dev/null +++ b/src/Stone/Types/StoneMacro.js @@ -0,0 +1,38 @@ +import './StoneDirectiveBlockType' + +export class StoneMacro extends StoneDirectiveBlockType { + + static directive = 'macro' + + static parse(parser, node, args) { + args = parser._flattenArgs(args) + this.assertArgs(parser, args, 1) + + node.id = args.shift() + + const output = parser.startNode() + output.rescopeContext = true + output.params = args + output.body = this.parseUntilEndDirective(parser, node) + + node.output = parser.finishNode(output, 'StoneOutputBlock') + return parser.finishNode(node, 'StoneMacro') + } + + 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) + } + +} diff --git a/src/Stone/Types/StoneOutput.js b/src/Stone/Types/StoneOutput.js new file mode 100644 index 0000000..cf98e45 --- /dev/null +++ b/src/Stone/Types/StoneOutput.js @@ -0,0 +1,141 @@ +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') + } + + 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 + + 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') + } + + /** + * Parses chunks of output between braces and directives + * + * @return {object} Template element node + */ + static parseOutputElement(parser, first = false) { + const elem = parser.startNode() + 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) { + 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, true) + 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, false)) + } + + parser.next() + return parser.finishNode(node, 'TemplateLiteral') + } + + 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) + } + + 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/StoneOutputBlock.js b/src/Stone/Types/StoneOutputBlock.js new file mode 100644 index 0000000..791bb5a --- /dev/null +++ b/src/Stone/Types/StoneOutputBlock.js @@ -0,0 +1,130 @@ +import './StoneType' + +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.assignments = node.assignments || [ ] + + if(node.rescopeContext) { + // _ = { ..._ } + node.assignments.push({ + operator: '=', + left: { + type: 'Identifier', + name: '_' + }, + right: { + type: 'ObjectExpression', + properties: [ + { + type: 'SpreadElement', + argument: { + type: 'Identifier', + name: '_' + } + } + ] + } + }) + } + + // 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 + + 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: { + type: 'Identifier', + name: 'HtmlString' + }, + arguments: [ + { + type: 'Identifier', + name: 'output' + } + ] + } + } + + node.body.body.push({ + type: 'ReturnStatement', + argument: _return + }) + + generator[node.body.type](node.body, state) + state.popScope() + } + + static walk(walker, node, st, c) { + for(const param of node.params) { + c(param, st, 'Pattern') + } + + c(node.body, st, 'ScopeBody') + } + + 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) + } + +} diff --git a/src/Stone/Types/StoneOutputExpression.js b/src/Stone/Types/StoneOutputExpression.js new file mode 100644 index 0000000..73ec01e --- /dev/null +++ b/src/Stone/Types/StoneOutputExpression.js @@ -0,0 +1,25 @@ +import './StoneType' + +export class StoneOutputExpression extends StoneType { + + static generate(generator, { safe = true, value }, state) { + if(safe) { + state.write('_.escape(') + } + + generator[value.type](value, state) + + if(safe) { + state.write(')') + } + } + + static walk(walker, { value }, st, c) { + c(value, st, 'Expression') + } + + static scope(scoper, { value }, scope) { + return scoper._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 new file mode 100644 index 0000000..bdc38fb --- /dev/null +++ b/src/Stone/Types/StoneSection.js @@ -0,0 +1,65 @@ +import './StoneDirectiveBlockType' + +export class StoneSection extends StoneDirectiveBlockType { + + static directive = 'section' + + static parse(parser, node, args) { + args = parser._flattenArgs(args) + this.assertArgs(parser, args, 1, 2) + + node.id = args.shift() + + if(args.length > 0) { + node.output = args.pop() + node.inline = true + parser.next() + } else { + const output = parser.startNode() + output.params = args + output.body = this.parseUntilEndDirective(parser, node, [ 'show', 'endsection' ]) + output.returnRaw = true + node.output = parser.finishNode(output, 'StoneOutputBlock') + } + + return parser.finishNode(node, 'StoneSection') + } + + static generate(generator, node, state) { + state.write('_sections.push(') + generator[node.id.type](node.id, state) + state.write(', ') + + 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) + } + + static walk(walker, node, st, c) { + c(node.id, st, 'Pattern') + + if(node.inline) { + return + } + + c(node.output, st, 'Expression') + } + + static scope(scoper, node, scope) { + scoper._scope(node.output, scope) + } + +} diff --git a/src/Stone/Types/StoneSet.js b/src/Stone/Types/StoneSet.js new file mode 100644 index 0000000..11f30e5 --- /dev/null +++ b/src/Stone/Types/StoneSet.js @@ -0,0 +1,129 @@ +import './StoneDirectiveType' +import '../../Stone' + +export class StoneSet extends StoneDirectiveType { + + static directive = 'set' + + static parseArgs(parser) { + parser.skipSpace() + + let kind = null + + 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() + } + + parser.pos += kind.length + + const node = parser.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 + */ + static parse(parser, node, args) { + const kind = args.kind || null + args = parser._flattenArgs(args) + + this.assertArgs(parser, args, 1, 2) + + 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 + this.expressionToPattern(node.left) + + parser.next() + return parser.finishNode(node, 'StoneSet') + } + + static generate(generator, { kind, left, right }, state) { + if(right.isNil) { + generator[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 = this.make.declaration(left, right, kind) + require('../Scoper').Scoper._scope(left, state.scope, true) + return generator[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 = this.make.assignment(left, right) + return generator[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 = this.make.block([ + this.make.declaration(left, right, 'const'), + this.make.return(this.make.object(extracted.map(value => this.make.property(value, value)))) + ]) + + block.scope = state.scope.branch(extracted.map(({ name }) => name)) + + state.write('Object.assign(_, (function() ') + generator[block.type](block, state) + state.write(')());') + } + + static walk(walker, { left, right }, st, c) { + if(right.isNil) { + c(left, st, 'Expression') + return + } + + c(left, st, 'Pattern') + c(right, st, 'Pattern') + } + + /** + * `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) { + this.expressionToPattern(property.value) + } + } + } + +} diff --git a/src/Stone/Types/StoneShow.js b/src/Stone/Types/StoneShow.js new file mode 100644 index 0000000..c5cdd7a --- /dev/null +++ b/src/Stone/Types/StoneShow.js @@ -0,0 +1,31 @@ +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) { + const stack = parser._sectionStack + + if(!Array.isArray(stack) || stack.length === 0) { + parser.raise(parser.start, '`@show` outside of `@section`') + } + + stack.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 new file mode 100644 index 0000000..ce545e4 --- /dev/null +++ b/src/Stone/Types/StoneSlot.js @@ -0,0 +1,57 @@ +import './StoneDirectiveBlockType' + +export class StoneSlot extends StoneDirectiveBlockType { + + static directive = 'slot' + + static parse(parser, node, args) { + args = parser._flattenArgs(args) + this.assertArgs(parser, args, 1, 2) + + node.id = args.shift() + + if(args.length > 0) { + node.output = args.pop() + node.inline = true + parser.next() + } else { + const output = parser.startNode() + output.params = args + output.body = this.parseUntilEndDirective(parser, node) + node.output = parser.finishNode(output, 'StoneOutputBlock') + } + + return parser.finishNode(node, 'StoneSlot') + } + + static generate(generator, node, state) { + state.write('__componentContext[') + generator[node.id.type](node.id, state) + state.write('] = ') + + if(node.inline) { + generator.StoneOutputExpression({ safe: true, value: node.output }, state) + } else { + state.write('(') + generator[node.output.type](node.output, state) + state.write(')()') + } + + state.write(';') + } + + static walk(walker, node, st, c) { + c(node.id, st, 'Pattern') + + if(node.inline) { + return + } + + c(node.output, st, 'Expression') + } + + static scope(scoper, node, scope) { + scoper._scope(node.output, scope) + } + +} 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/StoneSuper.js b/src/Stone/Types/StoneSuper.js new file mode 100644 index 0000000..9452193 --- /dev/null +++ b/src/Stone/Types/StoneSuper.js @@ -0,0 +1,26 @@ +import './StoneYield' + +// 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 + */ + static parse(parser, node) { + const stack = parser._sectionStack + + if(!Array.isArray(stack) || stack.length === 0) { + parser.raise(parser.start, `\`@${node.directive}\` outside of \`@section\``) + } + + node.section = { ...stack[stack.length - 1].id } + return parser.finishNode(node, 'StoneSuper') + } + +} diff --git a/src/Stone/Types/StoneTemplate.js b/src/Stone/Types/StoneTemplate.js new file mode 100644 index 0000000..36c6f1a --- /dev/null +++ b/src/Stone/Types/StoneTemplate.js @@ -0,0 +1,146 @@ +import './StoneType' + +export class StoneTemplate extends StoneType { + + static generate(generator, { pathname, output, isLayout, hasLayoutContext }, state) { + output.id = { + type: 'Identifier', + name: 'template' + } + + output.params = [ + { + type: 'Identifier', + name: '_' + }, { + type: 'AssignmentPattern', + left: { + type: 'Identifier', + 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.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 + } + + generator[output.type](output, state) + } + + static walk(walker, { output }, st, c) { + c(output, st, 'Expression') + } + + static scope(scoper, { output, isLayout, hasLayoutContext }, scope) { + scope.add('_') + scope.add('_sections') + scope.add('_templatePathname') + + if(isLayout) { + scope.add('__extendsLayout') + + if(hasLayoutContext) { + scope.add('__extendsContext') + } + } + + scoper._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/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/StoneUnset.js b/src/Stone/Types/StoneUnset.js new file mode 100644 index 0000000..425a9d2 --- /dev/null +++ b/src/Stone/Types/StoneUnset.js @@ -0,0 +1,42 @@ +import './StoneDirectiveType' + +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) + this.assertArgs(parser, args, 1) + + parser.next() + return parser.finishNode(node, 'StoneUnset') + } + + 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(';') + } + } + + static walk() { + // Do nothing + } + +} 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/StoneYield.js b/src/Stone/Types/StoneYield.js new file mode 100644 index 0000000..6d894f6 --- /dev/null +++ b/src/Stone/Types/StoneYield.js @@ -0,0 +1,59 @@ +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) + + this.assertArgs(parser, args, 1, 2) + + node.section = args.shift() + + if(args.length > 0) { + node.output = args.pop() + } + + parser.next() + return parser.finishNode(node, 'StoneYield') + } + + static generate(generator, node, state) { + state.write('output += _sections.render(') + generator[node.section.type](node.section, state) + + if(!node.output.isNil) { + state.write(', ') + generator.StoneOutputExpression({ safe: true, value: node.output }, state) + } + + state.write(');') + } + + static walk(walker, node, st, c) { + c(node.section, st, 'Pattern') + + if(node.output.isNil) { + return + } + + c(node.output, st, 'Expression') + } + + static scope(scoper, node, scope) { + if(node.output.isNil) { + return + } + + scoper._scope(node.output, scope) + } + +} diff --git a/src/Stone/Types/index.js b/src/Stone/Types/index.js new file mode 100644 index 0000000..4612f6b --- /dev/null +++ b/src/Stone/Types/index.js @@ -0,0 +1,32 @@ +export const Types = { + ...require('./StoneBreak'), + ...require('./StoneComponent'), + ...require('./StoneContinue'), + ...require('./StoneDump'), + ...require('./StoneEach'), + ...require('./StoneElse'), + ...require('./StoneElseif'), + ...require('./StoneEmptyExpression'), + ...require('./StoneExtends'), + ...require('./StoneFor'), + ...require('./StoneForeach'), + ...require('./StoneHasSection'), + ...require('./StoneIf'), + ...require('./StoneInclude'), + ...require('./StoneMacro'), + ...require('./StoneOutput'), + ...require('./StoneOutputBlock'), + ...require('./StoneOutputExpression'), + ...require('./StoneParent'), + ...require('./StoneSection'), + ...require('./StoneSet'), + ...require('./StoneShow'), + ...require('./StoneSlot'), + ...require('./StoneSpaceless'), + ...require('./StoneSuper'), + ...require('./StoneTemplate'), + ...require('./StoneUnless'), + ...require('./StoneUnset'), + ...require('./StoneWhile'), + ...require('./StoneYield'), +} diff --git a/src/Stone/Walker.js b/src/Stone/Walker.js new file mode 100644 index 0000000..1d46bc0 --- /dev/null +++ b/src/Stone/Walker.js @@ -0,0 +1,7 @@ +import './Types' + +export const Walker = { ...require('acorn/dist/walk').base } + +for(const type of Object.values(Types)) { + type.registerWalk(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/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 + } + +} diff --git a/src/Support/contextualize.js b/src/Support/contextualize.js deleted file mode 100644 index c6f1280..0000000 --- a/src/Support/contextualize.js +++ /dev/null @@ -1,181 +0,0 @@ -import '../AST' - -/** - * Runs through the template code and prefixes - * any non-local variables with the context - * object. - * - * @param {string} code Code for the template - * @return {string} Contextualized template code - */ -export function contextualize(code) { - let tree = null - - try { - tree = AST.parse(code) - } catch(err) { - err._code = code - throw err - } - - 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) - } - - AST.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}` - } - }) - - return AST.stringify(tree) -} - -/** - * 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) { - AST.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 -} 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/control-structures/each-array-empty.stone b/test/views/control-structures/each-array-empty.stone deleted file mode 100644 index c354d49..0000000 --- a/test/views/control-structures/each-array-empty.stone +++ /dev/null @@ -1 +0,0 @@ -@each(null, [ ], 'value', 'control-structures._each-empty-partial') diff --git a/test/views/control-structures/each-array-extra.stone b/test/views/control-structures/each-array-extra.stone deleted file mode 100644 index 2b949ce..0000000 --- a/test/views/control-structures/each-array-extra.stone +++ /dev/null @@ -1,3 +0,0 @@ -@each('control-structures._each-array-partial', numbers, 'value', null, { - label: 'Value:' -}) diff --git a/test/views/control-structures/each-array.stone b/test/views/control-structures/each-array.stone deleted file mode 100644 index f8a3147..0000000 --- a/test/views/control-structures/each-array.stone +++ /dev/null @@ -1 +0,0 @@ -@each('control-structures._each-array-partial', numbers, 'value') diff --git a/test/views/control-structures/each-empty-extra.stone b/test/views/control-structures/each-empty-extra.stone deleted file mode 100644 index aaf16ff..0000000 --- a/test/views/control-structures/each-empty-extra.stone +++ /dev/null @@ -1,3 +0,0 @@ -@each(null, [ ], 'value', 'control-structures._each-empty-partial', { - placeholder: 'This is an alternative placeholder.' -}) diff --git a/test/views/control-structures/each-object-empty.stone b/test/views/control-structures/each-object-empty.stone deleted file mode 100644 index 87805b5..0000000 --- a/test/views/control-structures/each-object-empty.stone +++ /dev/null @@ -1 +0,0 @@ -@each(null, { }, 'value', 'control-structures._each-empty-partial') diff --git a/test/views/control-structures/each-object-extra.stone b/test/views/control-structures/each-object-extra.stone deleted file mode 100644 index 5d1c996..0000000 --- a/test/views/control-structures/each-object-extra.stone +++ /dev/null @@ -1,3 +0,0 @@ -@each('control-structures._each-object-partial', pairs[0], 'value', null, { - label: 'Value is' -}) diff --git a/test/views/control-structures/each-object.stone b/test/views/control-structures/each-object.stone deleted file mode 100644 index 4a2c000..0000000 --- a/test/views/control-structures/each-object.stone +++ /dev/null @@ -1 +0,0 @@ -@each('control-structures._each-object-partial', pairs[0], 'value') 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 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/layouts/each-array-empty.stone b/test/views/layouts/each-array-empty.stone new file mode 100644 index 0000000..d455826 --- /dev/null +++ b/test/views/layouts/each-array-empty.stone @@ -0,0 +1 @@ +@each(null, [ ], 'value', 'layouts._each-empty-partial') 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/layouts/each-array-extra.stone b/test/views/layouts/each-array-extra.stone new file mode 100644 index 0000000..c4d3b8d --- /dev/null +++ b/test/views/layouts/each-array-extra.stone @@ -0,0 +1,3 @@ +@each('layouts._each-array-partial', numbers, 'value', null, { + label: 'Value:' +}) 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/layouts/each-array.stone b/test/views/layouts/each-array.stone new file mode 100644 index 0000000..54c80ac --- /dev/null +++ b/test/views/layouts/each-array.stone @@ -0,0 +1 @@ +@each('layouts._each-array-partial', numbers, 'value') 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/layouts/each-empty-extra.stone b/test/views/layouts/each-empty-extra.stone new file mode 100644 index 0000000..7aa563c --- /dev/null +++ b/test/views/layouts/each-empty-extra.stone @@ -0,0 +1,3 @@ +@each(null, [ ], 'value', 'layouts._each-empty-partial', { + placeholder: 'This is an alternative placeholder.' +}) 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/layouts/each-object-empty.stone b/test/views/layouts/each-object-empty.stone new file mode 100644 index 0000000..469799d --- /dev/null +++ b/test/views/layouts/each-object-empty.stone @@ -0,0 +1 @@ +@each(null, { }, 'value', 'layouts._each-empty-partial') 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/layouts/each-object-extra.stone b/test/views/layouts/each-object-extra.stone new file mode 100644 index 0000000..17b7797 --- /dev/null +++ b/test/views/layouts/each-object-extra.stone @@ -0,0 +1,3 @@ +@each('layouts._each-object-partial', pairs[0], 'value', null, { + label: 'Value is' +}) 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/layouts/each-object.stone b/test/views/layouts/each-object.stone new file mode 100644 index 0000000..8fd0ebd --- /dev/null +++ b/test/views/layouts/each-object.stone @@ -0,0 +1 @@ +@each('layouts._each-object-partial', pairs[0], 'value') 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 @@ - +