mirror of
https://github.com/cupcakearmy/cometa.git
synced 2025-03-12 14:27:28 +00:00
V0.1 Recursive if statements
This commit is contained in:
parent
1bf9876f7e
commit
47c89612b9
113
actions.ts
Normal file
113
actions.ts
Normal 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
55
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<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
75
compiler.ts
Normal 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
|
||||
|
||||
}
|
41
options.ts
41
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: '/',
|
||||
}
|
106
parser.ts
106
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<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
76
reader.ts
Normal 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
53
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<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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user