Skip to content

Commit

Permalink
feat(Template): get first version of template renderer ready
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jul 2, 2018
1 parent cc3aa63 commit e4935eb
Show file tree
Hide file tree
Showing 13 changed files with 1,184 additions and 286 deletions.
808 changes: 545 additions & 263 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"commitizen": "^2.10.1",
"coveralls": "^3.0.2",
"cz-conventional-changelog": "^2.1.0",
"dedent": "^0.7.0",
"del-cli": "^1.1.0",
"fs-extra": "^6.0.1",
"japa": "^1.0.6",
Expand Down Expand Up @@ -60,6 +61,9 @@
]
},
"dependencies": {
"edge-parser": "git+https://github.com/poppinss/edge-parser.git#develop"
"deep-extend": "^0.6.0",
"edge-parser": "^1.0.5",
"he": "^1.1.1",
"macroable": "^1.0.0"
}
}
50 changes: 50 additions & 0 deletions src/Compiler/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* edge
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { ILoader } from '../Contracts'
import { Parser } from 'edge-parser'
import { ITag } from 'edge-parser/build/src/Contracts'

export class Compiler {
private cache: Map<string, string> = new Map()

constructor (private loader: ILoader, private tags: { [key: string]: ITag }, private shouldCache: boolean = true) {
}

/**
* Compiles a given template by loading it using the loader
*/
private _compile (templatePath: string, diskName: string): string {
const template = this.loader.resolve(templatePath, diskName)
const parser = new Parser(this.tags)
return parser.parseTemplate(template)
}

/**
* Compiles a given template by loading it using the loader, also caches
* the template and returns from the cache (if exists).
*/
public compile (templatePath: string, diskName: string = 'default'): string {
templatePath = this.loader.makePath(templatePath, diskName)

/**
* Compile right away when cache is disabled
*/
if (!this.shouldCache) {
return this._compile(templatePath, diskName)
}

/* istanbul ignore else */
if (!this.cache.get(templatePath)) {
this.cache.set(templatePath, this._compile(templatePath, diskName))
}

return this.cache.get(templatePath)!
}
}
140 changes: 140 additions & 0 deletions src/Context/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* edge.js
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import * as Macroable from 'macroable'
import { IPresenter } from '../Contracts'
import * as he from 'he'

export class Context extends Macroable {
/* tslint:disable-next-line */
private static _macros: object = {}

/* tslint:disable-next-line */
private static _getters: object = {}

/**
* Remove all macros and getters
*/
public static hydrate () {
super.hydrate()
}

/**
* Define macro on the context
*/
public static macro (name: string, callback: Function) {
super.macro(name, callback)
}

/**
* Define getter on the context
*/
public static getter (name: string, callback: Function, singleton: boolean = false) {
super.getter(name, callback, singleton)
}

/**
* Frames are used to define a inner scope in which values will
* be resolved. The resolve function starts with the deepest
* frame and then resolve the value up until the first
* frame.
*/
private frames: object[] = []

constructor (public presenter: IPresenter, public sharedState: object) {
super()
}

/**
* Creates a new frame object.
*/
public newFrame (): void {
this.frames.unshift({})
}

/**
* Set key/value pair on the frame object
*/
public setOnFrame (key: string, value: any): void {
const recentFrame = this.frames[0]

if (!recentFrame) {
throw new Error('Make sure to call {newFrame} before calling {setOnFrame}')
}

recentFrame[key] = value
}

/**
* Removes the most recent frame
*/
public removeFrame (): void {
this.frames.shift()
}

/**
* Escapes the value to be HTML safe
*/
public escape (input: string): string {
return he.escape(input)
}

/**
* Resolves value for a given key. It will look for the value in different
* stores and continues till the end if `undefined` is returned.
*
* 1. Check for value inside frames
* 2. Then on the presenter instance
* 3. Then the presenter `state` object
* 4. Finally fallback to the sharedState
*/
public resolve (key: string): any {
let value

/**
* Pull from one of the nested frames
*/
value = this.getFromFrame(key)
if (value !== undefined) {
return typeof (value) === 'function' ? value.bind(this) : value
}

/**
* Check for value as a property on the presenter
* itself.
*/
value = this.presenter[key]
if (value !== undefined) {
return typeof (value) === 'function' ? value.bind(this.presenter) : value
}

/**
* Otherwise look into presenter state
*/
value = this.presenter.state[key]
if (value !== undefined) {
return typeof (value) === 'function' ? value.bind(this.presenter.state) : value
}

/**
* Finally fallback to defined globals
*/
value = this.sharedState[key]
return typeof (value) === 'function' ? value.bind(this.sharedState) : value
}

/**
* Returns value for a key inside frames. Stops looking for it,
* when value is found inside the first frame
*/
private getFromFrame (key: string): any {
const frameWithVal = this.frames.find((frame) => frame[key] !== undefined)
return frameWithVal ? frameWithVal[key] : undefined
}
}
11 changes: 11 additions & 0 deletions src/Contracts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ILoader {
mounted: object
mount (diskName: string, dirPath: string): void
unmount (diskName: string): void
resolve (templatePath: string, diskName: string): string
makePath (templatePath: string, diskName: string): string
}

export interface IPresenter {
state: any
}
38 changes: 22 additions & 16 deletions src/Loader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
* file that was distributed with this source code.
*/

import { join } from 'path'
import { join, isAbsolute } from 'path'
import { readFileSync } from 'fs'
import { ILoader } from '../Contracts'

export class Loader {
export class Loader implements ILoader {
private mountedDirs: Map<string, string> = new Map()

/**
Expand All @@ -27,39 +28,44 @@ export class Loader {
/**
* Mount a directory for resolving views
*/
public mount (name: string, dirPath: string): void {
this.mountedDirs.set(name, dirPath)
public mount (diskName: string, dirPath: string): void {
this.mountedDirs.set(diskName, dirPath)
}

/**
* Remove directory from the list of directories
* for resolving views
*/
public unmount (name: string): void {
this.mountedDirs.delete(name)
public unmount (diskName: string): void {
this.mountedDirs.delete(diskName)
}

/**
* Resolves a template from disk and returns it as a string
*/
public resolve (template: string, name: string = 'default'): string {
const mountedDir = this.mountedDirs.get(name)
public makePath (templatePath: string, diskName: string): string {
const mountedDir = this.mountedDirs.get(diskName)
if (!mountedDir) {
throw new Error(`Attempting to resolve ${template} template for unmounted ${name} location`)
throw new Error(`Attempting to resolve ${templatePath} template for unmounted ${diskName} location`)
}

/**
* Normalize template name by adding extension
*/
template = `${template.replace(/\.edge$/, '')}.edge`
templatePath = `${templatePath.replace(/\.edge$/, '')}.edge`
return join(mountedDir, templatePath)
}

/**
* Resolves a template from disk and returns it as a string
*/
public resolve (templatePath: string, diskName: string): string {
try {
return readFileSync(join(mountedDir, template), 'utf-8')
templatePath = isAbsolute(templatePath) ? templatePath : this.makePath(templatePath, diskName)
return readFileSync(templatePath, 'utf-8')
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`Cannot resolve ${template}. Make sure file exists at ${mountedDir} location.`)
throw new Error(`Cannot resolve ${templatePath}. Make sure file exists.`)
} else {
throw error
}
throw error
}
}
}
15 changes: 15 additions & 0 deletions src/Presenter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* edge
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { IPresenter } from '../Contracts'

export class Presenter implements IPresenter {
constructor (public state: any) {
}
}
23 changes: 23 additions & 0 deletions src/Template/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* edge
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Context } from '../Context'
import { IPresenter } from '../Contracts'
import { Compiler } from '../Compiler'

export class Template {
constructor (private compiler: Compiler, private sharedState: any) {
}

public render (template: string, presenter: IPresenter, diskName?: string): string {
const compiledTemplate = this.compiler.compile(template, diskName)
const ctx = new Context(presenter, this.sharedState)
return new Function('template', 'ctx', `return ${compiledTemplate}`)(this, ctx)
}
}
Loading

0 comments on commit e4935eb

Please sign in to comment.