From 47c89612b933dae82261eff90dc29dc994b78019 Mon Sep 17 00:00:00 2001 From: nicco Date: Tue, 6 Feb 2018 20:00:09 +0100 Subject: [PATCH] V0.1 Recursive if statements --- actions.ts | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++ app.ts | 57 ++++++++++++-------------- compiler.ts | 75 ++++++++++++++++++++++++++++++++++ options.ts | 41 ++++++++++++++----- parser.ts | 106 ++++++++---------------------------------------- reader.ts | 76 +++++++++++++++++++++++++++++++++++ util.ts | 53 +++++++++++++++++++++++- 7 files changed, 391 insertions(+), 130 deletions(-) create mode 100644 actions.ts create mode 100644 compiler.ts create mode 100644 reader.ts diff --git a/actions.ts b/actions.ts new file mode 100644 index 0000000..99f57c5 --- /dev/null +++ b/actions.ts @@ -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 + } +} \ No newline at end of file diff --git a/app.ts b/app.ts index 068ea19..3c70dd9 100644 --- a/app.ts +++ b/app.ts @@ -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 = new Map() +const cache: Map = 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 { - html = await parser.insertImports(html) - html = parser.removeComments(html) - - const compiled = replaceVars(html) - +async function compile(html: string): Promise { 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 { - const compiled_path = path.join(options.compiled_dir, `${template_name}.${options.compiled_ext}`) - - if (!options.caching || !await util.exists(compiled_path)) { - const template_path = path.join(options.template_dir, `${template_name}.${options.template_ext}`) +async function render(template_name: string, data?: any): Promise { + // 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() \ No newline at end of file diff --git a/compiler.ts b/compiler.ts new file mode 100644 index 0000000..9348225 --- /dev/null +++ b/compiler.ts @@ -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 + +} \ No newline at end of file diff --git a/options.ts b/options.ts index e108aad..83e335f 100644 --- a/options.ts +++ b/options.ts @@ -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: '/', } \ No newline at end of file diff --git a/parser.ts b/parser.ts index 78b0625..e7a78e8 100644 --- a/parser.ts +++ b/parser.ts @@ -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 { - 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 +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) } - - ret.push(html) - - return ret } \ No newline at end of file diff --git a/reader.ts b/reader.ts new file mode 100644 index 0000000..c2abffc --- /dev/null +++ b/reader.ts @@ -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 + +} \ No newline at end of file diff --git a/util.ts b/util.ts index bd02c49..0b12097 100644 --- a/util.ts +++ b/util.ts @@ -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 { return new Promise(res => { fs.readFile(url, (err, data) => { @@ -12,7 +22,26 @@ export function readFile(url: string): Promise { }) } -export function exists(url: string): Promise { +export function readFileSync(url: string): string { + return fs.readFileSync(url).toString() +} + +export function writeFile(url: string, data: any): Promise { + 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 { return new Promise(res => { fs.exists(url, _ => { res(_) @@ -33,4 +62,26 @@ export function checksum(url: string, plain = false, alg = 'sha1'): Promise { 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 } \ No newline at end of file