├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── HtmlHelper.ts ├── HtmlString.ts ├── babel.config.js ├── index.ts ├── package.json ├── razor.test.ts ├── razor.ts ├── reader.test.ts ├── reader.ts ├── readme.md ├── tests ├── Example.html ├── async-test.html ├── encoding.js ├── index.html ├── node.js ├── parse.js ├── shBrushRazor.js └── views │ ├── _layout.html │ ├── _sub-layout.html │ └── index.html ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended"] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | tests/cg/* 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "printWidth": 180, "singleQuote": true } 2 | -------------------------------------------------------------------------------- /HtmlHelper.ts: -------------------------------------------------------------------------------- 1 | import { HtmlString } from './HtmlString'; 2 | 3 | export function doubleEncode(txt: string): string { 4 | return txt.split('\\').join('\\\\').split('\r').join('\\r').split('\n').join('\\n').split('"').join('\\"'); 5 | } 6 | 7 | export class HtmlHelper { 8 | encode(value: HtmlString | string | undefined): HtmlString { 9 | if (value === null || value === undefined) value = ''; 10 | if (HtmlString.isHtmlString(value)) return value; 11 | if (typeof value !== 'string') value = String(value); 12 | value = value.split('&').join('&').split('<').join('<').split('>').join('>').split('"').join('"'); 13 | return new HtmlString(value); 14 | } 15 | attributeEncode(value: HtmlString | string | undefined): HtmlString { 16 | return this.encode(value); 17 | } 18 | raw(value: unknown): HtmlString { 19 | return new HtmlString(value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /HtmlString.ts: -------------------------------------------------------------------------------- 1 | export class HtmlString { 2 | #value: unknown; 3 | 4 | get isHtmlString(): boolean { 5 | return true; 6 | } 7 | 8 | constructor(value: unknown) { 9 | this.#value = value; 10 | } 11 | toString(): string { 12 | return String(this.#value ?? ''); 13 | } 14 | 15 | static isHtmlString(value: unknown): value is HtmlString { 16 | const html = value as HtmlString; 17 | return typeof html === 'object' && html.isHtmlString; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './razor'; 2 | export * from './reader'; 3 | export * from './HtmlHelper'; 4 | export * from './HtmlString'; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "RazorJS", 3 | "name": "@andyedinborough/razor-js", 4 | "description": "A JavaScript implementation of the Razor view engine", 5 | "version": "0.4.3", 6 | "author": { 7 | "name": "Andy Edinborough (@andyedinborough)" 8 | }, 9 | "repository": "https://github.com/andyedinborough/RazorJS", 10 | "licenses": [ 11 | { 12 | "type": "MIT" 13 | } 14 | ], 15 | "devDependencies": { 16 | "@babel/core": "^7.15.0", 17 | "@babel/preset-env": "^7.15.0", 18 | "@babel/preset-typescript": "^7.15.0", 19 | "@types/jest": "^27.0.1", 20 | "@typescript-eslint/eslint-plugin": "^4.29.3", 21 | "@typescript-eslint/parser": "^4.29.3", 22 | "babel-jest": "^27.1.0", 23 | "eslint": "^7.32.0", 24 | "jest": "^27.1.0", 25 | "typescript": "4.4.2" 26 | }, 27 | "scripts": { 28 | "compile": "./node_modules/.bin/tsc", 29 | "build": "rm -rf ./dist/ && yarn lint && yarn test && yarn compile", 30 | "lint": "eslint . --ext .ts --fix", 31 | "test": "jest", 32 | "x-publish": "yarn build && cp ./package.json ./dist/package.json && npm publish ./dist --access=public" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /razor.test.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import { Razor } from './razor'; 3 | 4 | it('can render without code blocks', async () => expect(await new Razor().render('hi')).toBe('hi')); 5 | it('can render code blocks', async () => 6 | expect( 7 | ( 8 | await new Razor().render(` 9 | @{ var test = 'bob';} 10 | hi @test 11 | `) 12 | ).trim() 13 | ).toBe('hi bob')); 14 | 15 | it('can render ', async () => 16 | expect( 17 | ( 18 | await new Razor().render(` 19 | @{ hi bob } 20 | `) 21 | ).trim() 22 | ).toBe('hi bob')); 23 | 24 | it('can render in if', async () => 25 | expect( 26 | ( 27 | await new Razor().render(` 28 | @{ var test = 'bob';} 29 | hi @if(test === 'bob') { bill } 30 | `) 31 | ).trim() 32 | ).toBe('hi bill')); 33 | 34 | it('can render if statements', async () => 35 | expect( 36 | ( 37 | await new Razor().render(` 38 | @{ var test = 'bob';} 39 | hi @if(test === 'bob') { @:bill } 40 | `) 41 | ).trim() 42 | ).toBe('hi bill')); 43 | 44 | it('can render helpers', async () => 45 | expect( 46 | ( 47 | await new Razor().render(` 48 | @helper name(x){ @:bill } 49 | hi @name('bill') 50 | `) 51 | ).trim() 52 | ).toBe('hi bill')); 53 | 54 | it('renders layouts', async () => { 55 | const razor = new Razor({ 56 | async findView(id) { 57 | switch (id) { 58 | case 'layout': 59 | return 'begin @renderBody() end'; 60 | } 61 | }, 62 | }); 63 | 64 | const result = await razor.render('@{ layout = "layout"; } test'); 65 | expect(result.replace(/\s+/g, ' ')).toBe('begin test end'); 66 | }); 67 | 68 | it('renders layouts with sections', async () => { 69 | const razor = new Razor({ 70 | async findView(id) { 71 | switch (id) { 72 | case 'layout': 73 | return 'begin @renderBody() end @renderSection("afterEnd", false)'; 74 | } 75 | }, 76 | }); 77 | 78 | const result = await razor.render('@{ layout = "layout"; } test @section afterEnd(){ @: after }'); 79 | expect(result.replace(/\s+/g, ' ').trim()).toBe('begin test end after'); 80 | }); 81 | 82 | it('does not try to process emails', async () => expect(await new Razor().render('my email is test@test.com')).toBe('my email is test@test.com')); 83 | 84 | it('encodes values', async () => { 85 | const result = await new Razor().render(` 86 | @{ var test = ' 10 | 33 | 34 | 47 | 48 | -------------------------------------------------------------------------------- /tests/async-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 65 | 66 | -------------------------------------------------------------------------------- /tests/encoding.js: -------------------------------------------------------------------------------- 1 | var Razor = require('../bin/node/razor.js'); 2 | exports.encoding = function(test){ 3 | var equal = test.equal; 4 | var encode = new Razor.HtmlHelper().encode; 5 | 6 | equal(encode('') + '', '<test>', 'encodes'); 7 | equal(encode(encode('')) + '', '<test>', 'won\'t double-encode'); 8 | equal(Razor.compile('hello @html.raw("")')(), 'hello ', 'won\'t encode for html.raw()'); 9 | 10 | test.done(); 11 | }; -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | 21 |

Razor

22 |

23 |
24 |

25 |
    26 | 27 | 28 | 29 | 30 | 31 |

    Views

    32 | 33 | 48 | 49 | 52 | 57 | 64 | 67 | 68 | 72 | 81 | 82 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /tests/node.js: -------------------------------------------------------------------------------- 1 | var Razor = require('../bin/node/razor.js'); 2 | 3 | function parseHtml(html, cb){ 4 | var htmlparser = require('htmlparser'), 5 | select = require('soupselect').select, 6 | parser = new htmlparser.Parser(new htmlparser.DefaultHandler(function(err, dom) { 7 | cb(function(selector) { 8 | return select(dom, selector); 9 | }); 10 | })); 11 | parser.parseComplete(html); 12 | } 13 | 14 | exports.node = function(test){ 15 | var equal = test.equal, ok = test.ok; 16 | test.expect(2); 17 | 18 | Razor.getViewFile = (function(getViewFile){ 19 | return function(){ 20 | var result = getViewFile.apply(this, arguments); 21 | result = './tests/' + result.replace(/^[\.\/]+/g, ''); 22 | return result; 23 | }; 24 | })(Razor.getViewFile); 25 | 26 | 27 | Razor.view('index', function(view){ 28 | view({ name: 'RazorJS' }, function(html){ 29 | 30 | parseHtml(html, function($){ 31 | equal($('meta').length, 1, ''); 32 | equal($('strong')[0].children[0].data, 'RazorJS', ''); 33 | test.done(); 34 | }); 35 | 36 | }); 37 | }); 38 | 39 | }; -------------------------------------------------------------------------------- /tests/parse.js: -------------------------------------------------------------------------------- 1 | var Razor = require('../bin/node/razor.js'); 2 | 3 | exports.parse = function (test) { 4 | var equal = test.equal, ok = test.ok; 5 | 6 | equal(Razor.compile('@{ }')().trim(), ' ', 'multiple code snippets in a tag opener inside a code block'); 7 | equal(Razor.compile('test\\test')().trim(), 'test\\test', '\\ needs to be double-encoded'); 8 | equal(Razor.compile('@if(true) { if(true){ 1?0:1) /> } }')().trim(), '', 'ternary inside tag inside nested if'); 9 | equal(Razor.compile('@if(true) { if(true){ 1?0:1) /> } }')().trim(), ' ', 'ternary inside tag inside nested if followed by another tag'); 10 | equal(Razor.compile('@{ model.items.forEach(function(x){ @x }); }')({ items: [0] }), '0', 'forEach'); 11 | equal(Razor.compile('test')(), 'test', 'no razor'); 12 | equal(Razor.compile('@@test')(), '@test', 'escaped @'); 13 | equal(Razor.compile('test@test.com')(), 'test@test.com', 'email address'); 14 | equal(Razor.compile('test@@@(model.test).com')({ test: 'test' }), 'test@test.com', 'explicit code'); 15 | equal(Razor.compile('hello @model.name')({ name: 'world' }), 'hello world', 'model'); 16 | equal(Razor.compile('hello @model.name[0]')({ name: 'world'.split('') }), 'hello w', 'model w/ indexers'); 17 | equal(Razor.compile('hello @model[\'name\']')({ name: 'world' }), 'hello world', 'model w/ string indexers'); 18 | equal(Razor.compile('hello @model.name("world")')({ name: function (n) { return n; } }), 'hello world', 'model w/ method'); 19 | equal(Razor.compile('te@*FAIL*@st')(), 'test', 'comment'); 20 | equal(Razor.compile('@if(model.name){ @model.name }')({ name: 'test' }), 'test', 'if statement'); 21 | equal(Razor.compile('@if(!model.name){ @fail(); } else { @model.name; }')({ name: 'test' }), 'test', 'if-else statement'); 22 | equal(Razor.compile('@if(true){ @:test }')().trim(), 'test', 'text-mode'); 23 | equal(Razor.compile('@helper test(name){ @:Hi @name } @test("bob")')().trim(), 'Hi bob', 'helper'); 24 | equal(Razor.compile('@if(true){
    nested
    }')().trim(), '
    nested
    ', 'nested tags inside code'); 25 | equal(Razor.compile('@{ }')().trim(), '', 'javascript code block'); 26 | equal(Razor.compile('@switch(model.test){ case 0:
    break; }')({ test: 0 }).trim(), '
    ', 'switch'); 27 | equal(Razor.compile('@if(true){ hi }')().trim(), 'hi', 'using '); 28 | equal(Razor.compile('@if(true){ if(false) { @:fail } else { @:win } }')().trim(), 'win', 'nested if'); 29 | equal(Razor.compile('@if(true){ if(false) { @:fail } else {
    Hi!
    if(false) { }
    Hi!
    } }')().trim(), '
    Hi!
    Hi!
    ', 'nested if w/ html'); 30 | 31 | try { 32 | Razor.compile('@('); 33 | Razor.compile('@{'); 34 | } catch (x) { } 35 | ok(true, 'Didn\'t crash'); 36 | 37 | equal(Razor.compile('@model.forEach(function(x){ @x })')([0]).trim(), '0', 'rendering from inside an inlined-function'); 38 | 39 | test.done(); 40 | }; -------------------------------------------------------------------------------- /tests/shBrushRazor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SyntaxHighlighter 3 | * http://alexgorbatchev.com/SyntaxHighlighter 4 | * 5 | * SyntaxHighlighter is donationware. If you are using it, please donate. 6 | * http://alexgorbatchev.com/SyntaxHighlighter/donate.html 7 | * 8 | * @version 9 | * 3.0.83 (July 02 2010) 10 | * 11 | * @copyright 12 | * Copyright (C) 2004-2010 Alex Gorbatchev. 13 | * 14 | * @license 15 | * Dual licensed under the MIT and GPL licenses. 16 | */ 17 | ;(function() 18 | { 19 | // CommonJS 20 | typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; 21 | 22 | function Brush() 23 | { 24 | function process(match, regexInfo) 25 | { 26 | var constructor = SyntaxHighlighter.Match, 27 | code = match[0], 28 | tag = new XRegExp('(<|<)[\\s\\/\\?]*(?[:\\w-\\.]+)', 'xg').exec(code), 29 | result = [] 30 | ; 31 | 32 | if (match.attributes != null) 33 | { 34 | var attributes, 35 | regex = new XRegExp('(? [\\w:\\-\\.]+)' + 36 | '\\s*=\\s*' + 37 | '(? ".*?"|\'.*?\'|\\w+)', 38 | 'xg'); 39 | 40 | while ((attributes = regex.exec(code)) != null) 41 | { 42 | result.push(new constructor(attributes.name, match.index + attributes.index, 'color1')); 43 | result.push(new constructor(attributes.value, match.index + attributes.index + attributes[0].indexOf(attributes.value), 'string')); 44 | } 45 | } 46 | 47 | if (tag != null) 48 | result.push( 49 | new constructor(tag.name, match.index + tag[0].indexOf(tag.name), 'keyword') 50 | ); 51 | 52 | return result; 53 | } 54 | 55 | this.regexList = [ 56 | { regex: new XRegExp('(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)', 'gm'), css: 'color2' }, // 57 | { regex: SyntaxHighlighter.regexLib.xmlComments, css: 'comments' }, // 58 | { regex: /@\*[\s\S]*?\*@$/gm, css: 'comments' }, // server side comments 59 | { regex: new XRegExp('(<|<)[\\s\\/\\?]*(\\w+)(?.*?)[\\s\\/\\?]*(>|>)', 'sg'), func: process }, 60 | { regex: new RegExp("(^|[^@])@(functions|helper|inherits|model|section)", 'gmi'), css: 'keyword' } // Razor-specific keywords 61 | ]; 62 | }; 63 | 64 | Brush.prototype = new SyntaxHighlighter.Highlighter(); 65 | Brush.aliases = ['razor']; 66 | 67 | SyntaxHighlighter.brushes.Razor = Brush; 68 | 69 | // CommonJS 70 | typeof(exports) != 'undefined' ? exports.Brush = Brush : null; 71 | })(); 72 | -------------------------------------------------------------------------------- /tests/views/_layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @renderSection('head') 5 | 6 | 7 | 8 | @renderBody() 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/views/_sub-layout.html: -------------------------------------------------------------------------------- 1 | @{ layout = '_layout'; } 2 | 3 | @section head { 4 | @renderSection('head') 5 | } 6 | 7 | @renderBody() 8 | -------------------------------------------------------------------------------- /tests/views/index.html: -------------------------------------------------------------------------------- 1 | @{ 2 | layout = '_sub-layout'; 3 | } 4 | 5 | Hello @model.name! 6 | 7 | @section head { 8 | 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "exclude": ["node_modules", "**/*.test.ts"], 4 | "compilerOptions": { 5 | "target": "es6", 6 | "esModuleInterop": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "noImplicitAny": true, 11 | "outDir": "./dist", 12 | "strictNullChecks": true, 13 | "declaration": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------