V0.1 Recursive if statements

This commit is contained in:
nicco 2018-02-06 20:00:09 +01:00
parent 1bf9876f7e
commit 47c89612b9
7 changed files with 391 additions and 130 deletions

113
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
}
}

55
app.ts
View File

@ -1,11 +1,10 @@
import * as fs from 'fs'
import * as path from 'path'
import * as util from './util'
import * as parser from './parser'
import { Render, LOG_TYPE, options } from './options'
import { replaceVars, addParts } from './parser';
import * as compiler from './compiler'
import { Compiled, LOG_TYPE, options } from './options'
const cache: Map<string, Render> = new Map()
const cache: Map<string, Compiled> = new Map()
function logger(type: LOG_TYPE, msg: object | string): void {
if (typeof msg === 'object')
@ -28,47 +27,41 @@ function logger(type: LOG_TYPE, msg: object | string): void {
console.log(`${typeString}:`, msg)
}
async function compile(html: string): Promise<Render> {
html = await parser.insertImports(html)
html = parser.removeComments(html)
const compiled = replaceVars(html)
async function compile(html: string): Promise<Compiled> {
return {
do(data) {
return addParts(data, compiled)
},
template: compiler.process(html),
hash: await util.checksum(html, true),
time: Date.now()
}
}
async function render(template_name: string, data?: any): Promise<string | undefined> {
const compiled_path = path.join(options.compiled_dir, `${template_name}.${options.compiled_ext}`)
if (!options.caching || !await util.exists(compiled_path)) {
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) {
logger(LOG_TYPE.Error, 'No file found')
return
}
else
if (html !== undefined)
cache.set(template_name, await compile(html))
else {
logger(LOG_TYPE.Error, 'No file found')
return ''
}
}
const render = cache.get(template_name)
if (render) {
return render.do(data)
}
const compiled = cache.get(template_name)
if (compiled)
return parser.computeParts(compiled.template, data)
else
return
return ''
}
async function go() {
console.log(await render('test', {
title: 'title',
const ret = await render('new', {
test: true,
testa: true,
title: 'test',
body: {
p: [
'omg',
@ -77,7 +70,9 @@ async function go() {
}
]
},
}))
})
ret.log()
}
go()

75
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
}

View File

@ -4,18 +4,37 @@ export const enum LOG_TYPE {
Error,
}
export interface Render {
do: ((data: any) => string)
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 interface Error {
parse: string
}
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',
},
export const error: Error = {
parse: 'Parse Error.'
}
interface Options {
@ -33,8 +52,8 @@ export const options: Options = {
caching: true,
template_dir: './views',
template_ext: 'html',
compiled_dir: './views',
compiled_ext: 'htmlbin',
compiled_dir: './cache',
compiled_ext: 'bjs',
max_recursion: 100,
}
@ -45,8 +64,10 @@ interface Expressions {
incude: string
if: string
if_else: string
if_invert: string
for: string
for_in: string
closing_tag: string
}
export const re: Expressions = {
@ -56,6 +77,8 @@ export const re: Expressions = {
incude: '>',
if: '?',
if_else: '!',
if_invert: '!',
for: '*',
for_in: 'in',
closing_tag: '/',
}

106
parser.ts
View File

@ -1,99 +1,27 @@
import { re, error } from "./options";
import { readFile } from "./util";
import * as path from 'path'
import { options } from './options'
import { Part, Render, isRender, PartFunction } from "./options";
function getFromData(data: any, name: string): string {
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)
export function computeParts(parts: Part[], data = {}): Render {
if (parts.length === 0)
return ''
name = name.replace(/('|")/g, '')
name = name.replace(/\[(\w+)\]/g, '.$1')
for (const i of name.split('.'))
data = data[i]
return String(data)
return computePart(parts[0], data) + computeParts(parts.slice(1), data)
}
function replaceBetween(start: number, end: number, str: string, replace: string): string {
return str.substring(0, start) + replace + str.substring(end)
function computePart(part: Part, data = {}): Render {
if (isRender(part))
return part
else
return computePartFunction(part, data)
}
export async function insertImports(html: string, depth = 0): Promise<string> {
if (depth > options.max_recursion)
throw new Error('Maximal recursion in include statement')
const begin = re.begin + re.incude
const ending = re.ending
const exp = new RegExp(`${begin}.*?${ending}`, 'g')
const includes = html.match(exp)
if (includes !== null)
for (const i of includes) {
const template_name = i.slice(begin.length, -ending.length).trim()
const file = await readFile(path.join(options.template_dir, `${template_name}.${options.template_ext}`))
const render = await insertImports(file, ++depth)
html = html.replace(i, render)
}
return html
}
export function addParts(data: any, parts: (((data: any) => string) | string)[]): string {
if (parts.length === 0) return ''
const part: string | ((data: any) => string) = parts[0]
return (typeof part === 'string' ? part : part(data)) + addParts(data, parts.slice(1))
}
export function removeComments(html: string): string {
return html.replace(
new RegExp(`${re.begin}${re.comment}(.|\n)*?${re.comment}${re.ending}`, 'g'), '')
}
export function replaceVars(html: string): (((data: any) => string) | string)[] {
const begin = new RegExp(re.begin)
const ending = new RegExp(re.ending)
const ret: (((data: any) => string) | string)[] = []
let i = html.match(begin) // Starting char
while (i !== null) {
if (i.index === undefined)
throw new Error(error.parse)
// Push text before
ret.push(html.substr(0, i.index))
html = html.slice(i.index + i[0].length)
// Get closing tag
const j = html.match(ending)
if (j === null || j.index === undefined)
throw new Error(error.parse)
const sub: string = html.substring(0, j.index)
ret.push((data) => {
return getFromData(data, sub)
})
html = html.slice(j.index + j[0].length)
i = html.match(begin) // Starting char
}
ret.push(html)
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
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
}

53
util.ts
View File

@ -1,6 +1,16 @@
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) => {
@ -12,7 +22,26 @@ export function readFile(url: string): Promise<string> {
})
}
export function exists(url: string): Promise<boolean> {
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(_)
@ -34,3 +63,25 @@ export function checksum(url: string, plain = false, alg = 'sha1'): Promise<stri
}
})
}
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
}