Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Changelog

All notable changes to this project will be documented in this file.

## [0.2.0] - 2023-07-09

### Added

Added support for @if directive
6 changes: 6 additions & 0 deletions src/expressions/Interpreter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ class Interpreter {
return output
}

passes() {
const output = this.evaluate(this.ast)

return this.isTruthy(output)
}

visitLiteralExpression(expression) {
if (expression.value === null) {
return ''
Expand Down
35 changes: 35 additions & 0 deletions src/helpers/conform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import CompilationError from '@/errors/CompilationError'

export default function (input) {
let output = input

// If statements
const openingIfStatements = [
...input.matchAll(/@if+\((?<expression>.*)\)/g),
]
const closingIfStatements = [...input.matchAll(/@endif/g)]

if (openingIfStatements.length !== closingIfStatements.length) {
throw new CompilationError(
'There is an uneven number of opening and closing @if directives.',
)
}

for (const match of openingIfStatements) {
const lexeme = match[0]

if (!match.groups.expression) {
throw new CompilationError(
'No expressions provided to @if directive.',
)
}

const condition = encodeURI(match.groups.expression)

output = output.replace(lexeme, `<if condition="${condition}">`)
}

output = output.replaceAll('@endif', '</if>')

return output
}
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import parse from 'rehype-parse-ns'
import stringify from 'rehype-stringify'
import format from 'rehype-format'

import conform from '@/helpers/conform'
import isDocument from '@/helpers/isDocument'

import templates from '@/language/templates'
Expand Down Expand Up @@ -32,7 +33,7 @@ export const compile = async (input, providedContext = defaultContext) => {
.use(stringify, {
closeSelfClosing: true,
})
.process(input.trim())
.process(conform(input.trim()))

return result.toString()
}
3 changes: 2 additions & 1 deletion src/language/compileComponentNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import templates from '@/language/templates'
import compileAttributes from '@/language/compileAttributes'
import normaliseTree from '@/language/normaliseTree'

import conform from '@/helpers/conform'
import afterLast from '@/helpers/afterLast'
import isDocument from '@/helpers/isDocument'
import findDuplicates from '@/helpers/findDuplicates'
Expand All @@ -24,7 +25,7 @@ export default function (node, context) {

const componentTree = unified()
.use(parse, { fragment: !isDocument(definition.trim()) })
.parse(definition.trim())
.parse(conform(definition.trim()))

const normalisedTree = normaliseTree(componentTree)

Expand Down
43 changes: 43 additions & 0 deletions src/language/compileIfDirective.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import compileNode from '@/language/compileNode'
import Parser from '@/expressions/Parser'
import Tokenizer from '@/expressions/Tokenizer'
import Interpreter from '@/expressions/Interpreter'

export default function (node, context) {
const condition = decodeURI(node.properties.condition)

const tokenizer = new Tokenizer(condition)
const tokens = tokenizer.scanTokens()
const normalisedTokens = tokens.map(token => {
if (token.type !== 'IDENTIFIER') {
return token
}

if (token.lexeme !== 'class') {
return token
}

return {
...token,
lexeme: 'className',
}
})

let normalisedValues = { ...context.environment }

if (normalisedValues.hasOwnProperty('class')) {
normalisedValues.className = normalisedValues.class
delete normalisedValues.class
}

const parser = new Parser(normalisedTokens)
const ast = parser.parse()

const interpreter = new Interpreter(ast, normalisedValues)

if (!interpreter.passes()) {
return []
}

return node.children.flatMap(node => compileNode(node, context))
}
7 changes: 6 additions & 1 deletion src/language/compileNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import compileSlot from '@/language/compileSlot'
import compileTextNode from '@/language/compileTextNode'
import compileFragment from '@/language/compileFragment'
import compileElementNode from '@/language/compileElementNode'
import compileIfDirective from '@/language/compileIfDirective'
import compileComponentNode from '@/language/compileComponentNode'

// Signature: node => [node]
Expand All @@ -13,6 +14,10 @@ export default function (node, context) {
const compiler = match(node.type, {
text: compileTextNode,
element: (node, context) => {
if (node.tagName === 'if') {
return compileIfDirective(node, context)
}

if (node.tagName === 'slot') {
return compileSlot(node, context)
}
Expand All @@ -27,7 +32,7 @@ export default function (node, context) {

return compileElementNode(node, context)
},
default: node => node,
default: node => [node],
})

return compiler(node, context)
Expand Down
27 changes: 26 additions & 1 deletion tests/expressions/interpreter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ const evaluate = (input, scope) => {
return output
}

const evaluateTruthiness = (input, scope) => {
const tokenizer = new Tokenizer(input)
const tokens = tokenizer.scanTokens()
const parser = new Parser(tokens)
const ast = parser.parse()
const interpreter = new Interpreter(ast, scope)
const output = interpreter.passes()

return output
}

describe('Literals', () => {
test('it can evaluate booleans', () => {
const input = `false`
Expand Down Expand Up @@ -168,5 +179,19 @@ describe('Type checking', () => {
})

describe('Truthiness', () => {
// TODO
test('it can evaluate whether an expression is truthy', () => {
const input = `1 + 2 equals 3`
const expected = true
const output = evaluateTruthiness(input)

expect(output).toEqual(expected)
})

test('it can evaluate whether an expression is truthy', () => {
const input = `1 + 2 equals 4`
const expected = false
const output = evaluateTruthiness(input)

expect(output).toEqual(expected)
})
})
131 changes: 131 additions & 0 deletions tests/helpers/conform.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import conform from '@/helpers/conform'
import CompilationError from '@/errors/CompilationError'

describe('If directives', () => {
test('it can convert @if to <if>', async () => {
const input = `<div>
@if(true)
<span>Content</span>
@endif
</div>`
const expected = `<div>
<if condition="true">
<span>Content</span>
</if>
</div>`

const result = conform(input)

expect(result).toBe(expected)
})

test('it can convert sibling @ifs', async () => {
const input = `<div>
@if(true)
<span>Content</span>
@endif
@if(false)
<span>Content</span>
@endif
</div>`
const expected = `<div>
<if condition="true">
<span>Content</span>
</if>
<if condition="false">
<span>Content</span>
</if>
</div>`

const result = conform(input)

expect(result).toBe(expected)
})

test('it can convert nested @ifs', async () => {
const input = `<div>
@if(true)
@if(false)
<span>Content</span>
@endif
@endif
</div>`
const expected = `<div>
<if condition="true">
<if condition="false">
<span>Content</span>
</if>
</if>
</div>`

const result = conform(input)

expect(result).toBe(expected)
})

test('it can convert @ifs with expressions that contain parenthesis', async () => {
const input = `<div>
@if(2 * (3 + 4))
<span>Content</span>
@endif
</div>`

const condition = encodeURI('2 * (3 + 4)')
const expected = `<div>
<if condition="${condition}">
<span>Content</span>
</if>
</div>`

const result = conform(input)

expect(result).toBe(expected)
})

test('it fails if no expression is provided', async () => {
const input = `<div>
@if()
<span>Content</span>
@endif
</div>`

const runner = () => conform(input)

expect(runner).toThrow(
new CompilationError('No expressions provided to @if directive.'),
)
})

test('it fails if there is not the same number of opening and closing statements', async () => {
const input = `<div>
@if(true)
<span>Content</span>
</div>`

const runner = () => conform(input)

expect(runner).toThrow(
new CompilationError(
'There is an uneven number of opening and closing @if directives.',
),
)
})

test('it can handle expressions with quotes', async () => {
const input = `<div>
@if(name equals "Yo")
<span>Content</span>
@endif
</div>`
const condition = encodeURI('name equals "Yo"')
const expected = `<div>
<if condition="${condition}">
<span>Content</span>
</if>
</div>`

const result = conform(input)

expect(result).toBe(expected)
})
})
Loading