diff --git a/README.md b/README.md index d37edcf..bd9c5c2 100644 --- a/README.md +++ b/README.md @@ -408,9 +408,4 @@ Here are the links to all repositories with examples used in the documentation: TODO list (Ideas for the next version) --- - -1. Implement [Razor-style `@* *@` comments](https://github.com/DevelAx/RazorExpress/blob/master/docs/syntax.md#comments). -2. ~~Make the library available for use on the client side (in the browser).~~([done](https://www.npmjs.com/package/razjs)) -3. Implement caching compiled templates. -4. Async partial views. -5. Make `HtmlString` class public for making functions returnin *raw-values* as expessions. +1. Implement caching compiled templates. diff --git a/core/HtmlString.js b/core/HtmlString.mjs similarity index 74% rename from core/HtmlString.js rename to core/HtmlString.mjs index da9050f..4873d6b 100644 --- a/core/HtmlString.js +++ b/core/HtmlString.mjs @@ -1,4 +1,4 @@ -module.exports = class HtmlString { +export class HtmlString { constructor(html) { this.html = html; } diff --git a/core/Razor.js b/core/Razor.mjs similarity index 58% rename from core/Razor.js rename to core/Razor.mjs index 0fcd145..0d39bd9 100644 --- a/core/Razor.js +++ b/core/Razor.mjs @@ -1,42 +1,49 @@ -require('./utils'); -const fs = require('fs'); +import * as utils from "./utils.mjs" -const path = require('path'); -Utils.path = path; -Utils.isServer = true; +import * as fs from "fs" +import * as pfs from "fs/promises" +import * as path from "path" + +utils.Utils.path = path; +utils.Utils.isServer = true; // path.fileName = function (fullFileName, withExt) { // if (withExt) return path.win32.basename(fullFileName); // let extension = path.extname(fullFileName); // return path.win32.basename(fullFileName, extension); // }; -path.cutLastSegment = function (dir) { +export let cutLastSegment = function (dir) { dir = path.normalize(dir); let pos = dir.lastIndexOf(path.sep); if (pos === -1) return ''; return dir.substring(0, pos); }; -const initParser = require('./parser'); -const ErrorsFactory = require('./errors/errors'); -const dbg = require('./dbg/debugger'); +import * as initParser_ from "./parser.mjs" +const initParser = initParser_.default; +import { ParserErrorFactory } from "./errors/errors.en.mjs" +import * as dbg from "./dbg/debugger.mjs" +import * as logger from "./dbg/logger.mjs" const allowLoggingInDebugModel = false; 'use strict'; const viewStartName = '_viewStart'; -const EOL = require('os').EOL; +import { EOL } from "os"; -module.exports = class Razor { +export class Razor { constructor(options, razorOpts) { this.options = options; this.ext = options.settings['view engine'] || razorOpts.ext; this.env = options.settings.env; const debug = dbg.isDebugMode; - const log = require('./dbg/logger')({ on: debug && allowLoggingInDebugModel }); + const log = logger.default({ on: debug && allowLoggingInDebugModel }); this.parser = initParser({ express: true, dbg, log }); this.viewsDir = path.resolve(this.options.settings.views); } renderFile(filepath, done) { + + + let originFilePath = filepath; filepath = path.normalize(filepath); //let fileName = path.fileName(filepath); @@ -47,7 +54,7 @@ module.exports = class Razor { fs.readFile(filepath, (err, data) => { if (err) { - let error = new ErrorsFactory({ filename: path.basename(originFilePath) }).errorReadingFile(err); + let error = new ParserErrorFactory({ filename: path.basename(originFilePath) }).errorReadingFile(err); return done(error); // Tested by [0# Razor.readFile]. } @@ -69,13 +76,86 @@ module.exports = class Razor { findPartialSync: (layoutName, filePath, errorsFactory, cache) => { var startDir = path.dirname(filePath); return this.findPartialSync(startDir, layoutName, [], errorsFactory, cache); + }, + findPartialAsync: (layoutName, filePath, errorsFactory, cache) => { + var startDir = path.dirname(filePath); + return this.findPartialAsync(startDir, layoutName, [], errorsFactory, cache); } }; - - this.parser.compile(parserArgs, done); + this.parser.compileAsync(parserArgs).then((data) => { + done(null, data); + }).catch((error) => { + done(error); + }); + //this.parser.compile(parserArgs,done); }); }); } + async renderFileAsync(filepath) { + let originFilePath = filepath; + filepath = path.normalize(filepath); + //let fileName = path.fileName(filepath); + //let viewsPath = path.normalize(this.options.settings.views); + + if (!path.extname(filepath)) + filepath += '.' + this.ext; + + try { + var data = await pfs.readFile(filepath); + let currentDir = path.dirname(filepath); + let jsHtml = this.addFileNameIfDev(data, filepath); + + + var viewStartsJsHtml = await this.findViewStartsAsync(currentDir, '') + + let parserArgs = { + filePath: filepath, + template: viewStartsJsHtml + jsHtml, + model: this.options, + findPartial: (layoutName, filePath, errorsFactory, done) => { + var startDir = path.dirname(filePath); + this.findPartial(startDir, layoutName, [], errorsFactory, done); + }, + findPartialSync: (layoutName, filePath, errorsFactory, cache) => { + var startDir = path.dirname(filePath); + return this.findPartialSync(startDir, layoutName, [], errorsFactory, cache); + }, + findPartialAsync: (layoutName, filePath, errorsFactory, cache) => { + var startDir = path.dirname(filePath); + return this.findPartialAsync(startDir, layoutName, [], errorsFactory, cache); + } + }; + + return this.parser.compileAsync(parserArgs); + } + catch (err) { + let error = new ErrorsFactory({ filename: path.basename(originFilePath) }).errorReadingFile(err); + throw error; // Tested by [0# Razor.readFile]. + } + } + + async findViewStartsAsync(startDir, buffer, done) { + const fileName = this.viewStartName(); + const filePath = path.join(startDir, fileName); + + try { + var data = await pfs.readFile(filePath) + let dataStr = this.addFileNameIfDev(data, filePath); + buffer = (buffer) ? dataStr + buffer : dataStr; // the `concat` order is important here! + } + catch (err) { + if (err.code !== 'ENOENT') { + let error = new ErrorsFactory({ filename: path.basename(filePath) }).errorReadingFile(err); + return done(error); // Tested by [#1 Razor.findViewStarts]. + } + } + startDir = startDir.equal(this.viewsDir, true) ? null : cutLastSegment(startDir); + + if (startDir) + return await this.findViewStartsAsync(startDir, buffer, done); + + return buffer; + } findViewStarts(startDir, buffer, done) { const fileName = this.viewStartName(); @@ -93,7 +173,7 @@ module.exports = class Razor { buffer = (buffer) ? dataStr + buffer : dataStr; // the `concat` order is important here! } - startDir = startDir.equal(this.viewsDir, true) ? null : path.cutLastSegment(startDir); + startDir = startDir.equal(this.viewsDir, true) ? null : cutLastSegment(startDir); if (startDir) return this.findViewStarts(startDir, buffer, done); @@ -161,7 +241,7 @@ module.exports = class Razor { } catch (err) { if (err.code === 'ENOENT') { // the file doesn't exist, lets see a dir up.. - startDir = startDir.equal(this.viewsDir, true) ? null : path.cutLastSegment(startDir); + startDir = startDir.equal(this.viewsDir, true) ? null : cutLastSegment(startDir); if (!startDir) throw errorsFactory.partialViewNotFound(partialViewName, searchedLocations); // [#2.1]. @@ -182,6 +262,86 @@ module.exports = class Razor { return result; } } + async findPartialAsync(startDir, partialViewName, searchedLocations, errorsFactory, cache) { + searchedLocations = searchedLocations || []; + + if (!partialViewName || !partialViewName.length) + throw errorsFactory.partialLayoutNameExpected(); // Tested by [2.2]. + + let viewFileExt = path.extname(partialViewName); + + if (!viewFileExt.equal('.' + this.ext)) + partialViewName += '.' + this.ext; + + if (partialViewName[0] === '/' || partialViewName[0] === '.') { + let viewPath = path.normalize(partialViewName); + + if (partialViewName[0] === '/') { // it's relative to the `views` root folder + if (!viewPath.startsWithIC(this.viewsDir)) // for linux only (in Windows an absolute path cannot start with '/') + viewPath = path.join(this.viewsDir, viewPath); + // [#2.4.1], [#2.4.2], [#2.4.3] + } + else if (partialViewName[0] === '.') { // it's relative to the current folder + viewPath = path.join(startDir, viewPath); // [#2.4.4], [#2.4.5] + } + + try { + searchedLocations.push(viewPath); + let cachedData = cache && cache[viewPath]; + + if (cachedData) + return cachedData; + + let data = await pfs.readFile(viewPath); + let dataStr = this.addFileNameIfDev(data, viewPath); + return successResult(dataStr, viewPath); + } + catch (err) { + if (err.code === 'ENOENT') + throw errorsFactory.partialViewNotFound(path.basename(partialViewName), searchedLocations); // [#2.3] + + throw errorsFactory.errorReadingView(viewPath, err); // [#2.3.1] + } + } + + partialViewName = path.normalize(partialViewName); + // it's just a layout name without any path, start search from the current dir.. + let filePath = path.join(startDir, partialViewName); + + try { + searchedLocations.push(filePath); + let cachedData = cache && cache[filePath]; + + if (cachedData) + return cachedData; + + let data = await pfs.readFile(filePath); + let dataStr = this.addFileNameIfDev(data, filePath); + return successResult(dataStr, filePath); + } + catch (err) { + if (err.code === 'ENOENT') { // the file doesn't exist, lets see a dir up.. + startDir = startDir.equal(this.viewsDir, true) ? null : cutLastSegment(startDir); + + if (!startDir) + throw errorsFactory.partialViewNotFound(partialViewName, searchedLocations); // [#2.1]. + else + return await this.findPartialAsync(startDir, partialViewName, searchedLocations, errorsFactory, cache); + } + else { + throw errorsFactory.errorReadingView(filePath, err); // [#2]. + } + } + + function successResult(data, filePath) { + var result = { data, filePath }; + + if (cache) + cache[filePath] = result; + + return result; + } + } findPartial(startDir, partialViewName, searchedLocations, errorsFactory, done) { searchedLocations = searchedLocations || []; @@ -228,7 +388,7 @@ module.exports = class Razor { fs.readFile(filePath, (err, data) => { if (err) { if (err.code === 'ENOENT') { // the file doesn't exist, lets see a dir up.. - startDir = startDir.equal(this.viewsDir, true) ? null : path.cutLastSegment(startDir); + startDir = startDir.equal(this.viewsDir, true) ? null : cutLastSegment(startDir); if (!startDir) return done(errorsFactory.partialViewNotFound(partialViewName, searchedLocations)); // [#2.5] diff --git a/core/bundle-js.js b/core/bundle-js.mjs similarity index 51% rename from core/bundle-js.js rename to core/bundle-js.mjs index 984af8c..ad074c9 100644 --- a/core/bundle-js.js +++ b/core/bundle-js.mjs @@ -1,15 +1,16 @@ 'use strict'; - +import { setDebugMode, isDebugMode } from './dbg/debugger.mjs'; +import * as p from "./parser.mjs" window.raz = { set debug(value) { - require('../core/dbg/debugger').isDebugMode = value; + setDebugMode(value); }, get debug(){ - return require('../core/dbg/debugger').isDebugMode; + return isDebugMode; }, render(template, model) { if (!this.parser) - this.parser = require('./parser')(); + this.parser = p.default(); return this.parser.compileSync(template, model); } diff --git a/core/dbg/debugger.js b/core/dbg/debugger.js deleted file mode 100644 index 99504a5..0000000 --- a/core/dbg/debugger.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - isDebugMode: false, - isBrowser: (typeof window !== 'undefined') -} \ No newline at end of file diff --git a/core/dbg/debugger.mjs b/core/dbg/debugger.mjs new file mode 100644 index 0000000..2b78854 --- /dev/null +++ b/core/dbg/debugger.mjs @@ -0,0 +1,6 @@ +export var isDebugMode= false; +export const isBrowser = (typeof window !== 'undefined'); +export function setDebugMode(value) +{ + isDebugMode = value; +} diff --git a/core/dbg/logger.js b/core/dbg/logger.js deleted file mode 100644 index 386e4d4..0000000 --- a/core/dbg/logger.js +++ /dev/null @@ -1,37 +0,0 @@ -(function () { - module.exports = function (opts) { - opts = opts || {}; - var exp = {}; - - if (opts.on) { - exp.debug = debug; - } - else { - exp.debug = noop; - } - - return exp; - - function debug(text) { - let funcName = callerName(); - text = text || ''; - console.debug(`[${funcName}]: ${text}`); - } - - function noop() {} - }; - - // helpers: - function callerName() { - try { - throw new Error(); - } - catch (e) { - try { - return e.stack.split('at ')[3].split(' ')[0]; - } catch (e) { - return ''; - } - } - } -})(); \ No newline at end of file diff --git a/core/dbg/logger.mjs b/core/dbg/logger.mjs new file mode 100644 index 0000000..d91a9ef --- /dev/null +++ b/core/dbg/logger.mjs @@ -0,0 +1,35 @@ +export default function (opts) { + opts = opts || {}; + var exp = {}; + + if (opts.on) { + exp.debug = debug; + } + else { + exp.debug = noop; + } + + return exp; + + function debug(text) { + let funcName = callerName(); + text = text || ''; + console.debug(`[${funcName}]: ${text}`); + } + + function noop() { } +}; + +// helpers: +export function callerName() { + try { + throw new Error(); + } + catch (e) { + try { + return e.stack.split('at ')[3].split(' ')[0]; + } catch (e) { + return ''; + } + } +} \ No newline at end of file diff --git a/core/errors/RazorError.js b/core/errors/RazorError.mjs similarity index 97% rename from core/errors/RazorError.js rename to core/errors/RazorError.mjs index 575d15f..74deb63 100644 --- a/core/errors/RazorError.js +++ b/core/errors/RazorError.mjs @@ -1,6 +1,7 @@ -const htmlEncode = require('../libs/js-htmlencode'); - -class RazorError extends Error { +import {htmlEncode} from '../libs/js-htmlencode.js'; +import { isDebugMode, isBrowser } from '../dbg/debugger.mjs'; +import { Utils } from '../utils.mjs'; +export default class RazorError extends Error { constructor(message, captureFrame) { super(message); @@ -15,7 +16,6 @@ class RazorError extends Error { } static extend(exc, args) { - const { isDebugMode, isBrowser } = require('../dbg/debugger'); exc.isRazorError = true; if (!isDebugMode) @@ -138,8 +138,6 @@ class RazorError extends Error { } } -module.exports = RazorError; - function stackToHtml(exc, data, mainInfo) { let lines = exc.stack.split('\n'); let fireFox = (typeof navigator !== 'undefined') && navigator.userAgent.toLowerCase().indexOf('firefox') !== -1; // for compatibility with FireFox @@ -226,7 +224,7 @@ function dataToHtml(data, mainInfo) { if (data.jshtml) { let textCursor = 0; - lines = data.jshtml.split('\n'); + let lines = data.jshtml.split('\n'); let startLine = data.startLine ? data.startLine : 0; html = `
The exception message: @ex.message
-} -The exception message: Test exception.
\nThe exception message: @ex.message
-} -finally { -finally
-} -The exception message: try exception
\nfinally
\nThe exception message: @ex.message
-} -finally -The exception message: @ex.message
+} +The exception message: Test exception.
\nThe exception message: @ex.message
+} +finally { +finally
+} +The exception message: try exception
\nfinally
\nThe exception message: @ex.message
+} +finally +