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 @@ -