Skip to content

Commit

Permalink
feat: String literals templating engine
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyguerra committed Dec 18, 2023
1 parent 40361d6 commit 9a5aa6e
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 0 deletions.
74 changes: 74 additions & 0 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Build and release pipeline
on:
push:
branches:
- main
- next
pull_request:
branches:
- main
- next
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
jobs:
build:
name: Build and Verify
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
test:
name: Fast Tests
runs-on: ubuntu-latest
needs: build
strategy:
matrix:
node-version:
- latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
release:
name: Release
if: github.ref == 'refs/heads/main' && success()
needs: [build, test]
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release
45 changes: 45 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import vm from 'node:vm'
class Template {
constructor (template, data, partials = new Map(), functions = {}) {
this.template = template
this.data = data
this.partials = partials
this.functions = functions
}
renderWithLayout (layout, body) {
if (!layout || layout.length === 0) {
return body
}
const sandbox = {
...this.data,
...this.functions,
body,
}
const script = new vm.Script(`result = \`${layout}\``)
const context = new vm.createContext(sandbox)
script.runInContext(context)
return context.result
}
async render() {
let layout = null
const hasLayout = layoutPath => {
layout = layoutPath
return ''
}
const script = new vm.Script(`result = \`${this.template}\``)
const context = new vm.createContext({
...this.data,
...this.functions,
hasLayout
})
script.runInContext(context)
if (layout) {
context.result = context.result.replace(/^\n/, '')
return this.renderWithLayout(this.partials.get(layout), context.result)
}
return context.result
}
}
export {
Template
}
93 changes: 93 additions & 0 deletions index.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { Template } from './index.mjs'
describe('Templating Engine', () => {
it('should render a string literal with the variables interpolated', async () => {
const data = {
name: 'John',
time: 'today'
}
const template = new Template('Hello ${name}! How are you ${time}?', data)
const expected = 'Hello John! How are you today?'
const actual = await template.render()
assert.equal(actual, expected)
})

it('should inject a file into a template with ${await file(path/to/file)}', async () => {
const partials = new Map()
partials.set('layout.html', await readFile('./layout.html', 'utf8'))
const template = new Template(await readFile('./test.html', 'utf8'), {}, partials)

const expected = `<html>
<h1>This is a test html file</h1>
</html>`
const actual = await template.render()
assert.equal(actual, expected)
})
it('should loop over a list of items and render them in the template', async () => {
const data = {
items: [
{
name: 'John'
},
{
name: 'Jane'
}
]
}
const template = new Template(await readFile('./test-loop.html', 'utf8'), data)
const expected = `<html>
<h1>John</h1>
<h1>Jane</h1>
</html>`
const actual = await template.render()
assert.equal(actual, expected)
})
it('should render complex objects in the template', async () => {
const data = {
users: [
{
name: 'John',
address: {
street: '123 Main St',
city: 'Anytown',
state: 'CA'
}
},
{
name: 'Jane',
address: {
street: '456 Main St',
city: 'Anytown',
state: 'CA'
}
}
]
}
const template = new Template(await readFile('./test-complex.html', 'utf8'), data)
const expected = `
<h1>John</h1>
<p>123 Main St</p>
<p>Anytown</p>
<p>CA</p>
<h1>Jane</h1>
<p>456 Main St</p>
<p>Anytown</p>
<p>CA</p>`
const actual = await template.render()
assert.equal(actual, expected)
})
it('should allow custom functions to be used the template', async () => {
const data = {
name: 'John',
time: 'today'
}
const template = new Template(await readFile('./test-custom-functions.html'), data, null, {
customFunction: () => 'Hello'
})
const expected = 'Hello John! How are you today?'
const actual = await template.render()
assert.equal(actual, expected)
})
})
3 changes: 3 additions & 0 deletions layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<html>
${body}
</html>
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@hubot-friends/hubot-templating",
"version": "0.0.0-development",
"description": "Javascript Server side templating engine with string literals",
"main": "index.mjs",
"scripts": {
"test": "node --test --experimental-vm-modules"
},
"keywords": [
"templating",
"engine",
"string",
"literals",
"javascript"
],
"author": "Joey Guerra",
"licenses": [
{
"type": "MIT",
"url": "https://opensource.org/licenses/MIT"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/hubot-friends/hubot-templating.git"
},
"engines": {
"node": ">=20"
},
"release": {
"branches": [
"main",
"next"
],
"dryRun": false
},
"publishConfig": {
"access": "public"
}
}
6 changes: 6 additions & 0 deletions test-complex.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
${hasLayout('layout.html')}
${users.map(user => `
<h1>${user.name}</h1>
<p>${user.address.street}</p>
<p>${user.address.city}</p>
<p>${user.address.state}</p>`).join('')}
1 change: 1 addition & 0 deletions test-custom-functions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
${customFunction()} John! How are you today?
3 changes: 3 additions & 0 deletions test-loop.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<html>
${items.map(item => '<h1>' + item.name + '</h1>\n').join('').replace(/\n$/, '')}
</html>
2 changes: 2 additions & 0 deletions test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
${hasLayout('layout.html')}
<h1>This is a test html file</h1>

0 comments on commit 9a5aa6e

Please sign in to comment.