Skip to content

Commit

Permalink
feat(tags): allow multiline tags
Browse files Browse the repository at this point in the history
templates must be expressive and hence multiline tags makes it possible
  • Loading branch information
thetutlage committed Sep 22, 2017
1 parent 593524a commit e7783f5
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 4 deletions.
95 changes: 94 additions & 1 deletion src/Ast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,81 @@ class Ast {
this._ast = []
this._insideBlockComment = false
this._openedTags = []
this._multilineOpened = null
}

/**
* Process all lines as part of the recently opened
* tag, when tag is multiline
*
* @method _waitUntilTagFinishes
*
* @param {String} line
*
* @return {void}
*
* @private
*/
_waitUntilTagFinishes (line) {
/**
* Remove inline comments from the line
*/
line = line.replace(singleLineComment, '')

/**
* Remove trailing spaces from the line,
* since they have no value
*/
line = line.trim()

/**
* If line is ending with `)`. We will consider
* it the end of the multiline tag
*/
const ending = line.endsWith(')')

/**
* Extract the content from the last
* line, since the line can be
* just `)` or it can be
* the content + `)`
*/
const content = ending ? line.replace(/\)$/, '') : line

/**
* If there was some content next to `args`
* then use append to it, otherwise set
* the first content
*/
this._multilineOpened.args = this._multilineOpened.args
? `${this._multilineOpened.args} ${content}`
: content

/**
* Finally if we are ending, then stop tracking the
* tag and start processing new content
*/
if (ending) {
this._multilineOpened = null
}
}

/**
* Returns a boolean telling if a line has more
* opening braces than closing braces
*
* @method _openingBracesAreMore
*
* @param {String} line
*
* @return {Boolean}
*
* @private
*/
_openingBracesAreMore (line) {
const openingBraces = line.match(/\(/g)
const closingBraces = line.match(/\)/g)
return (openingBraces ? openingBraces.length : 0) > (closingBraces ? closingBraces.length : 0)
}

/**
Expand All @@ -74,10 +149,11 @@ class Ast {
_tokenForTag (line, tag, args, index, selfClosing) {
return {
tag,
args,
args: args ? args.replace(/\)$/, '') : undefined,
selfClosing,
childs: [],
body: line,
multiline: this._openingBracesAreMore(line),
lineno: index + 1,
end: {
body: null,
Expand Down Expand Up @@ -225,6 +301,15 @@ class Ast {
return
}

/**
* Wait until the multi line opened tag closes. Till
* then everything will be args for that tag.
*/
if (this._multilineOpened) {
this._waitUntilTagFinishes(line)
return
}

const lastTag = _.last(this._openedTags)

/**
Expand Down Expand Up @@ -262,6 +347,14 @@ class Ast {
this._openedTags.push(token)
}

/**
* Track the opening of multiline tag opening and push all
* upcoming lines as args, unless a closing `)` is found
*/
if (token.tag && token.multiline) {
this._multilineOpened = token
}

/**
* Push to lastTag childs or the actual ast.
*/
Expand Down
10 changes: 9 additions & 1 deletion src/Tags/ElseIfTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,15 @@ class ElseIfTag extends BaseTag {
* @return {Array}
*/
get allowedExpressions () {
return ['BinaryExpression', 'Literal', 'Identifier', 'CallExpression', 'MemberExpression']
return [
'BinaryExpression',
'Literal',
'Identifier',
'CallExpression',
'MemberExpression',
'UnaryExpression',
'LogicalExpression'
]
}

/**
Expand Down
10 changes: 9 additions & 1 deletion src/Tags/IfTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ class IfTag extends BaseTag {
* @return {Array}
*/
get allowedExpressions () {
return ['BinaryExpression', 'Literal', 'Identifier', 'CallExpression', 'MemberExpression', 'UnaryExpression']
return [
'BinaryExpression',
'Literal',
'Identifier',
'CallExpression',
'MemberExpression',
'UnaryExpression',
'LogicalExpression'
]
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Template/Compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const expressions = {
class TemplateCompiler {
constructor (tags, loader, asFunction = false) {
this._tags = tags
this._blockRegExp = new RegExp(`^\\s*\\@(!?)(${_.keys(tags).join('|')})(?:\\((.*)\\))?`)
this._blockRegExp = new RegExp(`^\\s*\\@(!?)(${_.keys(tags).join('|')})(?:\\((.*)\\)?)?`)
this._loader = loader
this.buffer = new InternalBuffer(asFunction)
this._runtimeVarIndex = 0
Expand Down
28 changes: 28 additions & 0 deletions test/unit/ast.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,32 @@ test.group('Template Compiler', (group) => {
assert.equal(ast[0].selfClosing, true)
assert.equal(ast[0].body, `@!yield('foo')`)
})

test('jump line numbers when multi-line tags are detected', (assert) => {
const statement = `
@component(
'user',
{ username: 'virk' }
)
{{ user.username }}
@endcomponent
`

const tags = {
component: {
name: 'component',
isBlock: true,
compile () {}
}
}

const regExp = new RegExp(`^\\s*\\@(!?)(${_.keys(tags).join('|')})(?:\\((.*)\\))?`)
const ast = new Ast(tags, regExp).parse(statement)

assert.lengthOf(ast, 1)
assert.equal(ast[0].lineno, 1)
assert.equal(ast[0].end.lineno, 6)
assert.lengthOf(ast[0].childs, 1)
assert.equal(ast[0].tag, 'component')
})
})
125 changes: 125 additions & 0 deletions test/unit/template.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,131 @@ test.group('Template Compiler', (group) => {
assert.equal(error.stack.split('\n')[1].trim(), `at (${loader.getViewPath('includes/bad-partial.edge')}:2:0)`)
}
})

test('parse a template with multiline tags', (assert) => {
const statement = dedent`
@!component(
'components.alert',
username = 'virk'
)
`
const template = new Template(this.tags, {})
const output = template.compileString(statement)

assert.equal(output, dedent`
return (function templateFn () {
let out = new String()
this.isolate(function () {
out += \`\${this.renderWithContext('components.alert')}\`
}.bind(this.newContext({username: 'virk'},{$slot: { main: \`\` } })))
return out
}).bind(this)()
`)
})

test('parse a template with multiline tags with closing bracket net to content', (assert) => {
const statement = dedent`
@!component(
'components.alert',
username = 'virk')
`
const template = new Template(this.tags, {})
const output = template.compileString(statement)

assert.equal(output, dedent`
return (function templateFn () {
let out = new String()
this.isolate(function () {
out += \`\${this.renderWithContext('components.alert')}\`
}.bind(this.newContext({username: 'virk'},{$slot: { main: \`\` } })))
return out
}).bind(this)()
`)
})

test('parse a template with multiline tags with first line having partial content', (assert) => {
const statement = dedent`
@!component('components.alert',
{ username: 'virk' }
)
`
const template = new Template(this.tags, {})
const output = template.compileString(statement)

assert.equal(output, dedent`
return (function templateFn () {
let out = new String()
this.isolate(function () {
out += \`\${this.renderWithContext('components.alert')}\`
}.bind(this.newContext({username: 'virk'},{$slot: { main: \`\` } })))
return out
}).bind(this)()
`)
})

test('parse a template with multiline tags with comments in between', (assert) => {
const statement = dedent`
@!component('components.alert',
{{-- Data to be passed --}}
{ username: 'virk' }
)
`
const template = new Template(this.tags, {})
const output = template.compileString(statement)

assert.equal(output, dedent`
return (function templateFn () {
let out = new String()
this.isolate(function () {
out += \`\${this.renderWithContext('components.alert')}\`
}.bind(this.newContext({username: 'virk'},{$slot: { main: \`\` } })))
return out
}).bind(this)()
`)
})

test('parse a template with multiline tags with comments next to content', (assert) => {
const statement = dedent`
@!component('components.alert',
{ username: 'virk' } {{-- Data to be passed --}}
)
`
const template = new Template(this.tags, {})
const output = template.compileString(statement)

assert.equal(output, dedent`
return (function templateFn () {
let out = new String()
this.isolate(function () {
out += \`\${this.renderWithContext('components.alert')}\`
}.bind(this.newContext({username: 'virk'},{$slot: { main: \`\` } })))
return out
}).bind(this)()
`)
})

test('work fine with multiline if clause', (assert) => {
const statement = dedent`
@if(
username === 'virk'
&& age === 22
&& isAdmin
)
<p> You are super user </p>
@endif
`

const inlineStatement = dedent`
@if(username === 'virk' && age === 22 && isAdmin)
<p> You are super user </p>
@endif
`

const template = new Template(this.tags, {})
const output = template.compileString(statement)
const inlineOutput = template.compileString(inlineStatement)
assert.equal(output, inlineOutput)
})
})

test.group('Template Runner', () => {
Expand Down

0 comments on commit e7783f5

Please sign in to comment.