moved to src folder

This commit is contained in:
nicco
2018-02-06 20:02:11 +01:00
parent 47c89612b9
commit 660870acb9
7 changed files with 0 additions and 0 deletions

113
src/actions.ts Normal file
View File

@@ -0,0 +1,113 @@
import { compileBlock } from './reader'
import { ActionFunction, re, error, options, Part } from './options'
import { getFromObject, readFileSync } from './util'
import { join } from 'path'
export const comment: ActionFunction = html => {
const tag = re.comment + re.ending
const end = html.indexOf(tag)
if (end === -1) throw new Error(error.parse.comment_not_closed)
return {
parts: [],
length: end + tag.length
}
}
export const logic: ActionFunction = html => {
const rexp = {
start: new RegExp(`${re.begin}\\${re.if} *\\${re.if_else}?[A-z]\\w*? *${re.ending}`, 'g'),
else: new RegExp(`${re.begin} *\\${re.if_else} *${re.ending}`, 'g'),
end: RegExp(`${re.begin} *\\${re.closing_tag} *\\${re.if} *${re.ending}`, 'g'),
}
// First occurence of the if statement
const current = {
found: rexp.start.exec(html),
variable: '',
inverted: false,
}
// If there is no starting tag for an if statement return an error
if (current.found === null || current.found.index !== 0)
throw new Error(error.parse.default)
// Extract variable name from the if statemtent
current.variable = current.found[0].slice(re.begin.length + re.if.length, -re.ending.length).trim()
current.inverted = current.variable[0] === re.if_invert
if (current.inverted)
current.variable = current.variable.slice(re.if_invert.length)
let next
do {
next = {
start: rexp.start.exec(html),
else: rexp.else.exec(html),
end: rexp.end.exec(html),
}
if (next.end === null)
throw new Error(error.parse.default)
} while (next.start !== null && next.start.index < next.end.index)
const body = {
if: html.substring(current.found[0].length, next.end.index),
else: ''
}
return {
parts: [(data: any) => {
const ret: any = getFromObject(data, current.variable)
let isTrue: boolean = ret !== undefined && ret !== false && ret !== null && ret !== ''
if (current.inverted) isTrue = !isTrue
if (isTrue)
return compileBlock(body.if).parts
else
return compileBlock(body.else).parts
}],
length: next.end.index + next.end[0].length
}
}
export const importer: ActionFunction = html => {
const end = html.indexOf(re.ending)
if (end === -1) throw new Error(error.parse.include_not_closed)
const template_name: string = html.substring(re.begin.length + re.incude.length, end).trim()
const file_name: string = join(options.template_dir, `${template_name}.${options.template_ext}`)
const file: Part = readFileSync(file_name)
return {
parts: compileBlock(file).parts,
length: end + re.ending.length
}
}
export const variables: ActionFunction = html => {
const end = html.indexOf(re.ending)
if (end === -1) throw new Error(error.parse.variable_not_closed)
const variable_name = html.substring(re.begin.length, end).trim()
return {
parts: [(data: any) => String(getFromObject(data, variable_name))],
length: end + re.ending.length
}
}
export const loop: ActionFunction = html => {
return {
parts: [],
length: html.length
}
}

78
src/app.ts Normal file
View File

@@ -0,0 +1,78 @@
import * as path from 'path'
import * as util from './util'
import * as parser from './parser'
import * as compiler from './compiler'
import { Compiled, LOG_TYPE, options } from './options'
const cache: Map<string, Compiled> = new Map()
function logger(type: LOG_TYPE, msg: object | string): void {
if (typeof msg === 'object')
msg = JSON.stringify(msg)
let typeString: string = ''
switch (type) {
case LOG_TYPE.Info:
typeString = 'Info'
break
case LOG_TYPE.Warning:
typeString = 'Warning'
break
case LOG_TYPE.Error:
typeString = 'Error'
break
}
console.log(`${typeString}:`, msg)
}
async function compile(html: string): Promise<Compiled> {
return {
template: compiler.process(html),
hash: await util.checksum(html, true),
time: Date.now()
}
}
async function render(template_name: string, data?: any): Promise<string> {
// const compiled_path = path.join(options.compiled_dir, `${template_name}.${options.compiled_ext}`)
const template_path = path.join(options.template_dir, `${template_name}.${options.template_ext}`)
// Compile Template if is not in cache
if (options.caching && !cache.get(template_name)) {
const html = await util.readFile(template_path)
if (html !== undefined)
cache.set(template_name, await compile(html))
else {
logger(LOG_TYPE.Error, 'No file found')
return ''
}
}
const compiled = cache.get(template_name)
if (compiled)
return parser.computeParts(compiled.template, data)
else
return ''
}
async function go() {
const ret = await render('new', {
test: true,
testa: true,
title: 'test',
body: {
p: [
'omg',
{
check: 'let goo'
}
]
},
})
ret.log()
}
go()

75
src/compiler.ts Normal file
View File

@@ -0,0 +1,75 @@
import { Part, re, error, ActionFunction, ActionReturn } from './options'
import * as actions from './actions'
const rexp = Object.freeze({
begin: new RegExp(re.begin, 'g'),
end: new RegExp(re.ending, 'g'),
})
export const compileBlock: ActionFunction = part => {
interface Next {
start: number
end: number
}
let next: Next
const getNext = (s: string): Next => Object.freeze({
start: s.search(rexp.begin),
end: s.search(rexp.end),
})
let ret: ActionReturn = {
parts: [],
length: NaN
}
function addToRet(item: any) {
ret.parts = ret.parts.concat(item)
}
next = getNext(part)
while (next.start !== -1) {
if (next.start === null || next.end === null)
throw new Error(error.parse.default)
addToRet(part.substr(0, next.start))
part = part.slice(next.start)
let func: ActionFunction
switch (part[re.begin.length]) {
case re.comment:
func = actions.comment
break
case re.if:
func = actions.logic
break
// case re.for:
// func = actions.loop
// break
case re.incude:
func = actions.importer
break
default:
func = actions.variables
break
}
const result = func(part)
addToRet(result.parts)
part = part.slice(result.length)
next = getNext(part)
}
addToRet(part)
return ret
}
export function process(html: string, options = {}): Part[] {
const parts: Part[] = compileBlock(html).parts
return parts
}

84
src/options.ts Normal file
View File

@@ -0,0 +1,84 @@
export const enum LOG_TYPE {
Info,
Warning,
Error,
}
export function isRender(obj: any): obj is Render {
return typeof obj === 'string'
}
export type Render = string
export type Part = (PartFunction | Render)
export type PartFunction = (data: any) => (Part[] | Render)
export interface ActionReturn {
parts: Part[]
length: number
}
export type ActionFunction = (part: Render) => ActionReturn
export interface Compiled {
template: Part[]
hash: string
time: number
}
export const error = {
parse: {
default: 'Parse Error.',
import_recursion: 'Maximal recusion achieved in import module',
not_supported: 'Not supported yet',
comment_not_closed: 'Comment was not closed properly',
variable_not_closed: 'Variable was not closed properly',
include_not_closed: 'Include not closed',
},
}
interface Options {
encoding: string
caching: boolean
template_dir: string
template_ext: string
compiled_dir: string
compiled_ext: string
max_recursion: number
}
export const options: Options = {
encoding: 'utf-8',
caching: true,
template_dir: './views',
template_ext: 'html',
compiled_dir: './cache',
compiled_ext: 'bjs',
max_recursion: 100,
}
interface Expressions {
begin: string
ending: string
comment: string
incude: string
if: string
if_else: string
if_invert: string
for: string
for_in: string
closing_tag: string
}
export const re: Expressions = {
begin: '{{',
ending: '}}',
comment: '#',
incude: '>',
if: '?',
if_else: '!',
if_invert: '!',
for: '*',
for_in: 'in',
closing_tag: '/',
}

27
src/parser.ts Normal file
View File

@@ -0,0 +1,27 @@
import { Part, Render, isRender, PartFunction } from "./options";
export function computeParts(parts: Part[], data = {}): Render {
if (parts.length === 0)
return ''
return computePart(parts[0], data) + computeParts(parts.slice(1), data)
}
function computePart(part: Part, data = {}): Render {
if (isRender(part))
return part
else
return computePartFunction(part, data)
}
function computePartFunction(func: PartFunction, data = {}): Render {
if (isRender(func))
return func;
else {
const ret = func(data)
if (isRender(ret))
return ret
else return computeParts(ret, data)
}
}

76
src/reader.ts Normal file
View File

@@ -0,0 +1,76 @@
import { Part, re, error, ActionFunction, ActionReturn } from './options'
import * as actions from './actions'
const rexp = Object.freeze({
begin: new RegExp(re.begin, 'g'),
end: new RegExp(re.ending, 'g'),
})
interface Next {
start: number
end: number
}
export const compileBlock: ActionFunction = part => {
let next: Next
const getNext = (s: string): Next => Object.freeze({
start: s.search(rexp.begin),
end: s.search(rexp.end),
})
let ret: ActionReturn = {
parts: [],
length: NaN
}
function addToRet(item: any) {
ret.parts = ret.parts.concat(item)
}
next = getNext(part)
while (next.start !== -1) {
if (next.start === null || next.end === null)
throw new Error(error.parse.default)
addToRet(part.substr(0, next.start))
part = part.slice(next.start)
let func: ActionFunction
switch (part[re.begin.length]) {
case re.comment:
func = actions.comment
break
case re.if:
func = actions.logic
break
// case re.for:
// func = actions.loop
// break
case re.incude:
func = actions.importer
break
default:
func = actions.variables
break
}
const result = func(part)
addToRet(result.parts)
part = part.slice(result.length)
next = getNext(part)
}
addToRet(part)
return ret
}
export function compile(html: string, options = {}): Part[] {
const parts: Part[] = compileBlock(html).parts
return parts
}

87
src/util.ts Normal file
View File

@@ -0,0 +1,87 @@
import * as fs from 'fs'
import * as crypto from 'crypto'
declare global {
interface String {
log: () => void
}
}
String.prototype.log = function (): void {
console.log(this)
}
export function readFile(url: string): Promise<string> {
return new Promise(res => {
fs.readFile(url, (err, data) => {
if (err)
throw new Error(`No such file: ${url}`)
else
res(data.toString())
})
})
}
export function readFileSync(url: string): string {
return fs.readFileSync(url).toString()
}
export function writeFile(url: string, data: any): Promise<boolean> {
return new Promise(res => {
fs.writeFile(url, data, err => {
if (err)
res(false)
res(true)
})
})
}
export function writeFileSync(url: string, data: any): void {
fs.writeFileSync(url, data)
}
export function fileExists(url: string): Promise<boolean> {
return new Promise(res => {
fs.exists(url, _ => {
res(_)
})
})
}
export function checksum(url: string, plain = false, alg = 'sha1'): Promise<string> {
return new Promise(res => {
const hash = crypto.createHash(alg)
if (plain) {
res(hash.update(url).digest('hex'))
}
else {
const stream = fs.createReadStream(url)
stream.on('data', data => hash.update(data, 'utf8'))
stream.on('end', _ => { res(hash.digest('hex')) })
}
})
}
export function replaceBetween(start: number, end: number, str: string, replace: string): string {
return str.substring(0, start) + replace + str.substring(end)
}
export function getFromObject(data: any, name: string): any {
name = name.trim()
// If not matches the valid pattern of a getter, return empty string
const valid: boolean = /^[A-z]\w*(\.[A-z]\w*|\[\d+\]|\[('|")\w+\2\]|\[[A-z]\w*\])*$/.test(name)
if (!valid)
return ''
name = name.replace(/('|")/g, '')
name = name.replace(/\[(\w+)\]/g, '.$1')
for (const i of name.split('.'))
data = data[i]
return data
}