From 7f8494cf9f8399b61907e2ff938870db927ecea7 Mon Sep 17 00:00:00 2001 From: Harald Rudell Date: Mon, 20 Aug 2012 01:47:17 -0700 Subject: [PATCH] supports tagfinder 0.1, refactor renderer: addClass removeClass attribute replace, refactor test --- lib/compiler.js | 54 +++-- lib/expressadapter.js | 22 +- lib/html5text.js | 83 +++---- lib/renderer.js | 257 +++++++++++----------- lib/runtime.js | 91 ++++++-- package.json | 4 +- test/data/views/home.html | 1 + test/data/views/home/home.css | 1 + test/data/views/index.html | 1 + test/data/views/index/index.css | 1 + test/data/views/index/index.js | 1 + test/data/views/index/index_1.js | 1 + test/data/views/index/index_2.js | 1 + test/test-compiler.js | 65 ++++-- test/test-html5text.js | 3 +- test/test-package.js | 93 ++++---- test/test-render.js | 192 ----------------- test/test-renderer.js | 357 +++++++++++++++++++++++++++++++ 18 files changed, 760 insertions(+), 468 deletions(-) create mode 100644 test/data/views/home.html create mode 100644 test/data/views/home/home.css create mode 100644 test/data/views/index.html create mode 100644 test/data/views/index/index.css create mode 100644 test/data/views/index/index.js create mode 100644 test/data/views/index/index_1.js create mode 100644 test/data/views/index/index_2.js delete mode 100644 test/test-render.js create mode 100644 test/test-renderer.js diff --git a/lib/compiler.js b/lib/compiler.js index ab56ad2..3b6e40c 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -1,22 +1,24 @@ // compiler.js // compiles html and bindings json to a JavaScript render function -// (c) Harald Rudell 2012 +// © Harald Rudell 2012 var tagfinder = require('tagfinder') var renderer = require('./renderer') -var runtime = require('./runtime') module.exports = { compileHtml5: compileHtml5, loadFragment: loadFragment, } -// produce a runtime render function with an enclosed viewExecutable -// html: string: plain html with no injected data -// bindings: object: key: class, id, or tag name, value: data source -// return value: -// if instanceof Error: compilation error -// otherwise viewExecutable object +/* +produce a runtime render function with an enclosed viewExecutable +html: string: plain html with no injected data +bindings: object: key: class, id, or tag name, value: data source + +return value: +if instanceof Error: compilation error +otherwise viewExecutable object +*/ function compileHtml5(html, bindings) { var result @@ -55,33 +57,35 @@ function compileHtml5(html, bindings) { if (match = index == 0) useTagData = {} break case '#': // match tag id - match = tagData.attributes.id == key.substring(1) + match = tagData.a.id == key.substring(1) break case '.': // match tag class - match = tagData.classes.indexOf(key.substring(1)) != -1 + match = tagData.c.indexOf(key.substring(1)) != -1 break default: // match tag - match = key == tagData.tag + match = key == tagData.t } if (match) { // save each opening tag for which there was a matching binding var dataLink = { - linkage: bindings[key], - tag: useTagData, + d: bindings[key], + t: useTagData, } viewExecutable.dataLinks.push(dataLink) } } }) - if (!result) { - var theRenderer = renderer.render + if (!result) { // result did not hold an error - // return value + // successful return value var result = render result.getSource = getSource result.getIncludes = getIncludes + var theRenderer = renderer.render + + // save on memory html = null viewData = null bindings = null @@ -111,7 +115,16 @@ function compileHtml5(html, bindings) { function getIncludes() { var result = [] viewExecutable.dataLinks.forEach(function (dataLink) { - if (dataLink.view) result.push(dataLink) + scanSource(dataLink.d) + function scanSource(s) { + if (Array.isArray(s)) s.forEach(function (key) { + scanSource(key) + }) + else if (typeof s == 'object') { + if (s.fragment) result.push(s.fragment) + for (var p in s) scanSource(s[p]) + } + } }) return result } @@ -120,10 +133,11 @@ function compileHtml5(html, bindings) { /* render a fragment (server side only) - exposed to rge renderer so it can recursively load views when on server - return value: renderFunction + exposed to the renderer so it can recursively load views when on server + return value: renderFunction(record) + throws expcetion if fragment not found */ function loadFragment(fragmentName) { - // delay this require because the modules require each other + // delay this require to at runtime because the modules require each other return require('./viewloader').loadFragment(fragmentName) } \ No newline at end of file diff --git a/lib/expressadapter.js b/lib/expressadapter.js index f796406..c2a5b50 100644 --- a/lib/expressadapter.js +++ b/lib/expressadapter.js @@ -60,30 +60,27 @@ function compile(html, options) { express 3 adapter __express is invoked every time a page is to be rendered -added to express: -app.engine('html', require('htmlfive').__express) +- caching is our responsibility + +integration: add to app.js: +app.engine('html', require('webfiller').__express) app.set('view engine', 'html') filename: string: absolute path of existing file, including extension options object: -.cache: boolean: true if 'view cache' settings enabled -.locals(obj) -.settings object: env, port, view engine, views +.cache: boolean: true for caching: 'view cache' settings or value provided to render +._locals: function(obj) adding properties to local +.settings object: express' global settings: env, port, view engine, views .title -cb(err, str) str: string +cb(err, str) str: string: result callback for layout case, we need to return a function first rendering file then rendering layout using that result for no layout, return a template function based on the file -if a view does not have an extension, the default extension is added -if the view is not an absolute path the root folder is prepended -if that view does not exist, its considered a folder with a file index.ejs in it - -How do you know that the default layout is called layout? - notes: options.settings.views is absolute path to views root folder options.settings['view engine'] is view extension without leading period +express has its own cache for its View objects */ function __express(filename, options, callback) { @@ -106,6 +103,7 @@ function __express(filename, options, callback) { } else callback(err) }, options.cache) + // name is options.layout function getLayoutName(name) { if (!name || typeof name.valueOf() != 'string') name = 'layout' if (!path.extname(name)) name += '.' + options.settings['view engine'] diff --git a/lib/html5text.js b/lib/html5text.js index 3f78dec..9a3f55a 100644 --- a/lib/html5text.js +++ b/lib/html5text.js @@ -1,18 +1,22 @@ // html5text.js // convert between types of html5 character data -// (c) Harald Rudell 2012 +// © Harald Rudell 2012 +// code may run in browser or on server -// http://dev.w3.org/html5/markup/ -// http://dev.w3.org/html5/spec/named-character-references.html -// http://dev.w3.org/html5/markup/syntax.html#hex-charref +/* +http://dev.w3.org/html5/markup/ +http://dev.w3.org/html5/spec/named-character-references.html +http://dev.w3.org/html5/markup/syntax.html#hex-charref -// html5 text: -// http://dev.w3.org/html5/markup/syntax.html#syntax-text -// text does not contain: -// control characters other than html5 space characters -// permanently undefined unicode characters -;(function () { - var isNode = typeof module == 'object' && !!module.exports +html5 text: +http://dev.w3.org/html5/markup/syntax.html#syntax-text +text does not contain: +control characters other than html5 space characters +permanently undefined unicode characters +*/ +;(function (isNode, WF) { + + // the functions this file exports var funcs = { textToNormal: textToNormal, textToReplaceable: textToReplaceable, @@ -20,29 +24,30 @@ textToUnquotedAttributeValue: textToUnquotedAttributeValue, textToDoubleQuotedAttributeValue: textToDoubleQuotedAttributeValue, } - if (isNode) { - module.exports = funcs - WF = require('./runtime').WF - } - for (var func in funcs) { - WF.functions[func] = funcs[func] - } + if (isNode) module.exports = funcs - // escape text for use as normal character data - // http://dev.w3.org/html5/markup/syntax.html#normal-character-data - // & < + // both browser and Node: export to WF + for (var func in funcs) WF.functions[func] = funcs[func] + + /* + escape text for use as normal character data + http://dev.w3.org/html5/markup/syntax.html#normal-character-data + & < + */ function textToNormal(str) { return String(str) .replace(/&/g, '&') .replace(/ contentsIndex) { + result.push.apply(result, viewExecutable.pieces.slice(contentsIndex, flushToIndex)) + contentsIndex = flushToIndex } + + // output prints + result.push.apply(result, prints) + + if (suppressedContent) contentsIndex++ } } }) @@ -108,74 +109,81 @@ return result.join('') - function getTagClone() { - var tagClone = clone(this.tagData) - this.tag = null - return tagClone + function getField(name) { + var result + name = String(name) + if (name.length) result = record[name] + else result = clone(record) + return result } - function printTag(t) { - var result = [] - result.push('<') - result.push(t.tag) - - if (t.classes) { - result.push(' class=') - result.push(WF.functions.textToDoubleQuotedAttributeValue(t.classes.join(' '))) - } - for (var attribute in t.attributes) { - if (attribute != 'class') { - result.push(' ') - result.push(attribute) - var value = t.attrbibutes[attribute] - if (value) { - result.push('=') - result.push(WF.functions.textToUnquotedAttributeValue(value)) - } - } + function cloneTag() { + if (!clonedTag) { + clonedTag = true + if (!tagObject) tagObject = {t:'', i:0, a:{}, c:[]} + else tagObject = clone(tagObject) } - result.push('>') - this.printRaw(result.join('')) + return tagObject + } + + function suppressContent() { + suppressedContent = true + } + + function suppressTag() { + suppressedTag = true } function error(str) { if (typeof str != 'string') str = String(str) - this.print(str) + print(str) } function print(str) { printRaw(WF.functions.textToNormal(str)) } - // if renderStep.tag is set, flush including this index - // contentsIndex: the first index to print - // flushToIndex: the first index not to print, typically an opening tag function printRaw(str) { - if (flushToIndex > contentsIndex) { - // this is the first printing for this tag. We need to flush first - // print tag if it is not to be ignored - if (renderStep.tag) flushToIndex++ - result.push.apply(result, viewExecutable.pieces.slice(contentsIndex, flushToIndex)) - // if tag is to be ignored, that happens here - if (!renderStep.tag) flushToIndex++ - contentsIndex = flushToIndex + prints.push(str) + } + + function printTag(t) { + var result = [] + result.push('<') + result.push(t.t) + + if (t.c.length) { + result.push(' class=') + result.push(WF.functions.textToDoubleQuotedAttributeValue(t.c.join(' '))) } - if (str) result.push(str) + for (var attribute in t.a) { + result.push(' ') + result.push(attribute) + var value = t.a[attribute] + if (value) { + result.push('=') + result.push(WF.functions.textToUnquotedAttributeValue(value)) + } + } + if (t.v) result.push('/') + result.push('>') + printRaw(result.join('')) } + } // render - // cx contains data for the rendering + // this reference contains data for the rendering // binding is the data source description - function resolve(cx, dataSource) { + function resolve(dataSource) { switch (typeof dataSource) { - case 'string': // replace tag contents with field name in record - cx.content = null - cx.print(cx.record[dataSource]) + case 'string': // insert data ahead of tag contents + this.print(this.getField(dataSource)) break case 'object': // array or object if (Array.isArray(dataSource)) { // invoke each array element + var self = this dataSource.forEach(function (value) { - resolve(cx, value) + resolve.call(self, value) }) } else { // keys are function names for (var funcName in dataSource) { @@ -193,22 +201,21 @@ if (renderFunction instanceof Function) { // render the fragment - data = renderFunction(cx.record) - cx.content = null - cx.printRaw(data) - } else cx.error('Unknown view:' + includedView) - } else cx.error('Fragment name not string') + data = renderFunction(this.getField('')) + this.printRaw(data) + } else this.error('Unknown view:' + includedView) + } else this.error('Fragment name not string') } else { var func = WF.functions[funcName] if (func instanceof Function) { - func.call(cx, params) - } else cx.error('Unknown rendering function:' + func) + func.call(this, params) + } else this.error('Unknown rendering function:' + funcName) } } } break default: - cx.error('Unknown rendering data source type:' + typeof dataSource) + this.error('Unknown rendering data source type:' + typeof dataSource) } } @@ -226,4 +233,6 @@ } return result } -})() \ No newline at end of file + +})(typeof module == 'object' && !!module.exports, // isNode +typeof module == 'object' && !!module.exports ? require('./runtime').WF : WF) // WF \ No newline at end of file diff --git a/lib/runtime.js b/lib/runtime.js index c593391..a0a59f6 100644 --- a/lib/runtime.js +++ b/lib/runtime.js @@ -1,13 +1,8 @@ // runtime.js -// functions available to renderengine.js +// the WF object and built-in runtime functions +// © Harald Rudell 2012 // code may run on server or in browser -// define the global WF if in browser -if (typeof WF == 'undefined') WF = { - fragments: {}, - functions: {}, -} - /* cx object: cx.tag: content: the html of the opening tag '
' null for location 0 @@ -21,32 +16,82 @@ cx.error(Error object): ability to indicate an error condition params: the bindings object value following the function name */ +;(function (isNode) { -// wrapper so we have a local scope in the browser -;(function () { + // a seed WF if WF is not defined (it isn't) + var WF = { + fragments: {}, + functions: {}, + } - var isNode = typeof module == 'object' && !!module.exports - if (isNode) { - // export WF to other node modules - module.exports.WF = WF + // export or import WF + if (isNode) module.exports.WF = WF + else { + // in the browser this refers to the elusive global object + if (typeof this.WF != 'object') this.WF = WF + else { + WF = this.WF // import some WF object we found + if (typeof WF.fragments != 'object') WF.fragments = {} + if (typeof WF.functions != 'object') WF.functions = {} + } } + // built-in render functions + WF.functions.append = function(params) { this.print(this.content) - this.print(params == '' ? getAllFields(this.record) : this.record[params]) - this.content = null + this.suppressContent() + var data = this.getField(params) + if (typeof data == 'object') { + var result = [] + for (var p in data) result.push(p + ':' + data[p]) + data = result.join(', ') + } + this.print(data) + } + + WF.functions.replace = function (params) { + this.suppressContent() + var data = this.getField(params) + if (typeof data == 'object') { + var result = [] + for (var p in data) result.push(p + ':' + data[p]) + data = result.join(', ') + } + this.print(data) + } + + var classRegExp = /[^ \t\n\f\r]+/gm + WF.functions.addClass = function (params) { + params = String(params) + var classArray = params.match(classRegExp) + if (classArray) { + var classes = this.cloneTag().c + classArray.forEach(function (className) { + if (!~classes.indexOf(className)) classes.push(className) + }) + } } - WF.functions.insert = function (params) { - this.print(params == '' ? getAllFields(this.record) : this.record[params]) + WF.functions.removeClass = function (params) { + params = String(params) + var classArray = params.match(classRegExp) + if (classArray) { + var classes = this.cloneTag().c + classArray.forEach(function (className) { + var index = classes.indexOf(className) + if (~index) classes.splice(index, 1) + }) + } } - function getAllFields(record) { - var result = [] - for (var field in record) { - result.push(field + ':' + record[field]) + WF.functions.attribute = function (params) { + var attributes = this.cloneTag().a + for (var attributeName in params) { + var value = params[attributeName] + if (value === false) delete attributes[attributeName] + else attributes[attributeName] = String(value) } - return result.join(', ') } -})() \ No newline at end of file +})(typeof module == 'object' && !!module.exports) \ No newline at end of file diff --git a/package.json b/package.json index 68ed2ee..e69dc0d 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "webfiller", "description": "Webfiller puts data and styling into Web pages on the server and in the browser, by Harald Rudell", "author": "Harald Rudell (http://www.haraldrudell.com)", + "keywords": ["template", "engine", "express", "html", "html5", "json", "client", "webfiller", "parser"], "homepage": "https://github.com/haraldrudell/webfiller", "bugs": "https://github.com/haraldrudell/webfiller/issues", "version": "0.0.2", @@ -13,7 +14,8 @@ }, "devDependencies": { "nodeunit": "", - "uglify-js": "" + "uglify-js": "", + "greatjson": "" }, "repository" : { "type" : "git", diff --git a/test/data/views/home.html b/test/data/views/home.html new file mode 100644 index 0000000..339ada8 --- /dev/null +++ b/test/data/views/home.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/data/views/home/home.css b/test/data/views/home/home.css new file mode 100644 index 0000000..4a1a759 --- /dev/null +++ b/test/data/views/home/home.css @@ -0,0 +1 @@ +/* home.css */ \ No newline at end of file diff --git a/test/data/views/index.html b/test/data/views/index.html new file mode 100644 index 0000000..8cb5b1b --- /dev/null +++ b/test/data/views/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/data/views/index/index.css b/test/data/views/index/index.css new file mode 100644 index 0000000..38d83ae --- /dev/null +++ b/test/data/views/index/index.css @@ -0,0 +1 @@ +/* index.css */ \ No newline at end of file diff --git a/test/data/views/index/index.js b/test/data/views/index/index.js new file mode 100644 index 0000000..dc12b80 --- /dev/null +++ b/test/data/views/index/index.js @@ -0,0 +1 @@ +// index.js \ No newline at end of file diff --git a/test/data/views/index/index_1.js b/test/data/views/index/index_1.js new file mode 100644 index 0000000..c7302fd --- /dev/null +++ b/test/data/views/index/index_1.js @@ -0,0 +1 @@ +// index_1.js \ No newline at end of file diff --git a/test/data/views/index/index_2.js b/test/data/views/index/index_2.js new file mode 100644 index 0000000..40afd62 --- /dev/null +++ b/test/data/views/index/index_2.js @@ -0,0 +1 @@ +// index_2.js \ No newline at end of file diff --git a/test/test-compiler.js b/test/test-compiler.js index 0564c9c..73307e3 100644 --- a/test/test-compiler.js +++ b/test/test-compiler.js @@ -1,28 +1,17 @@ // test-compiler.js -// nodeunit test for the webfiller html compiler -// (c) Harald Rudell 2012 +// © Harald Rudell 2012 var compiler = require('../lib/compiler') -module.exports = { - testCompiler: testCompiler, -} - -function testCompiler(test) { - - // test compilation - var html = '
' - var bindings = { - 'div': 'data' - } - var actual = compiler.compileHtml5(html, bindings) +exports.testEmptyHtml = function testEmptyHtml(test) { + var html = '' + var actual = compiler.compileHtml5(html) test.ok(actual instanceof Function) test.ok(actual.getSource instanceof Function) test.ok(actual.getIncludes instanceof Function) - var renderFunction = actual - // verify JavaScript source - var expectedSource = 'WF.fragments["abc"]={"dataLinks":[{"linkage":"data","tag":{"tag":"div","index":1,"voidElement":false,"attributes":{},"classes":[]}}],"pieces":["","
","",""]};' + // verify source + var expectedSource = 'WF.fragments["abc"]={"dataLinks":[],"pieces":[""]};' var actualSource = actual.getSource('abc') test.equal(actualSource, expectedSource) @@ -31,11 +20,43 @@ function testCompiler(test) { var actualIncludes = actual.getIncludes() test.deepEqual(actualIncludes, expectedIncludes) - // render something - var data = {data: 5} - var expected = '
5' - var actual = renderFunction(data) - test.equal(actual, expected) + var expectedMarkup = '' + var actualMarkup = actual() + test.equal(actualMarkup, expectedMarkup) + + test.done() +} + +exports.testCompiler = function testCompiler(test) { + + // test compilation + var html = '
x

' + var bindings = { + '': 'LOC0', + '#anid': 'ANID', + '.aclass': 'ACLASS', + 'div': { + fragment: 'FRAGMENT', + }, + } + var renderFunction = compiler.compileHtml5(html, bindings) + test.equal(typeof renderFunction, 'function') + + // verify JavaScript source + var expectedSource = 'WF.fragments["abc"]={"dataLinks":[' + + '{"d":"LOC0","t":{}},' + + '{"d":"ANID","t":{"t":"taga","i":1,"a":{"id":"anid"},"c":[]}},' + + '{"d":"ACLASS","t":{"t":"tagb","i":4,"a":{},"c":["aclass"]}},' + + '{"d":{"fragment":"FRAGMENT"},"t":{"t":"div","i":7,"a":{},"c":[]}}' + + '],' + + '"pieces":["","","","","","","","

","x","
","

","",""]};' + var actualSource = renderFunction.getSource('abc') + test.equal(actualSource, expectedSource) + + // verify includes + var expectedIncludes = ['FRAGMENT'] + var actualIncludes = renderFunction.getIncludes() + test.deepEqual(actualIncludes, expectedIncludes) test.done() } \ No newline at end of file diff --git a/test/test-html5text.js b/test/test-html5text.js index 5f76f57..b861770 100644 --- a/test/test-html5text.js +++ b/test/test-html5text.js @@ -1,6 +1,5 @@ // test-html5text.js -// nodeunit test for html5text.js -// (c) Harald Rudell 2012 +// © Harald Rudell 2012 var html5text = require('../lib/html5text') diff --git a/test/test-package.js b/test/test-package.js index 93724cc..c5a705a 100644 --- a/test/test-package.js +++ b/test/test-package.js @@ -1,16 +1,21 @@ // packagetest.js // test javascript and json syntax // (c) Harald Rudell 2012 -// 2012-08-05: Better printouts, main not required +// 2012-08-09: package.json must have keywords, using greatjson, no throw for syntax +// https://github.com/haraldrudell/greatjson +var greatjson = require('greatjson') // http://nodejs.org/docs/latest/api/fs.html var fs = require('fs') // http://nodejs.org/api/path.html var path = require('path') // https://github.com/mishoo/UglifyJS/ var uglify = require('uglify-js') +// http://nodejs.org/api/util.html +var util = require('util') -if (!fs.exists) fs.exists = path.exists +// this function moved to a new module - make legacy node work +if (!fs.existsSync) fs.existsSync = path.existsSync module.exports = { syntaxTest: syntaxTest, @@ -21,7 +26,8 @@ module.exports = { // this script should be put one level down from the deployment folder var deployFolder = path.join(__dirname, '..') -packageJsonKeys = ['name', 'description', 'author', 'version', 'contributors', 'repository', 'devDependencies', 'dependencies', 'repository', 'scripts'] +packageJsonKeys = ['name', 'description', 'author', 'version', 'keywords', 'contributors', 'repository', 'devDependencies', 'dependencies', 'repository', 'scripts'] + // these defaults can be overriden by a file ./test-package.json var defaults = { // list of paths, relative to the deployFolder that will not be searched @@ -30,18 +36,21 @@ var defaults = { 'test', 'node_modules', ], + + // extensions that maps to fileTypeMap extensions: { 'js': 'javascript', 'json': 'json', } } +// maps filetypes to functions verifying syntax var fileTypeMap = { 'javascript': verifyJavaScriptSyntax, 'json': verifyJsonSyntax, } -// verify syntax of all JavaScript +// verify syntax of applicable files (JavaScript and json) function syntaxTest(test) { var cbCounter = 0 @@ -96,7 +105,7 @@ function syntaxTest(test) { // check syntax of json and JavaScript files cbCounter++ - parseFunc(absolutePath, relativePath, end) + parseFunc(absolutePath, relativePath, test, end) } } } @@ -109,7 +118,7 @@ function syntaxTest(test) { } } -function verifyJavaScriptSyntax(file, relPath, cb) { +function verifyJavaScriptSyntax(file, relPath, test, cb) { fs.readFile(file, 'utf-8', function (err, javascript) { if (!err) { var jsp = uglify.parser @@ -117,28 +126,24 @@ function verifyJavaScriptSyntax(file, relPath, cb) { try { ast = jsp.parse(javascript) } catch (e) { - console.log('File:', relPath ? relPath : file) - console.log('Syntax issue: %s at line:%s column:%s position:%s', + eMsg = util.format('%s at line:%d column:%d position:%d', e.message, e.line, e.col, - e.pos) - throw e + e.pos) + test.fail('File: ' + (relPath ? relPath : file) + ' has bad JavaScript:' + eMsg) } } cb(err) }) } -function verifyJsonSyntax(file, relPath, cb) { +function verifyJsonSyntax(file, relPath, test, cb) { fs.readFile(file, 'utf-8', function (err, jsonString) { if (!err) { - var object - try { - object = JSON.parse(jsonString) - } catch (e) { - console.log('File:', relPath ? relPath : file) - err = e + var object = greatjson.parse(jsonString) + if (object instanceof Error) { + test.fail('File: ' + (relPath ? relPath : file) + ' has bad json:' + object.toString()) } } cb(err) @@ -146,29 +151,45 @@ function verifyJsonSyntax(file, relPath, cb) { } function verifyPackageJson(test) { - var jsonString = fs.readFileSync(path.join(deployFolder, 'package.json'), 'utf-8') - var object - try { - object = JSON.parse(jsonString) - } catch (e) { - test.fail(e) + + // file should exist + var name = 'package.json' + var file = path.join(deployFolder, name) + var exists = fs.existsSync(file) + test.ok(exists, 'File missing:' + name + ' in folder:' + deployFolder) + if (exists) { + + // content should be json + var jsonString = fs.readFileSync(file, 'utf-8') + var object = greatjson.parse(jsonString) + if (object instanceof Error) test.fail('Bad json in file ' + name + ': ' + object.toString()) + else { + + // verify that content has all required keys + packageJsonKeys.forEach(function (key) { + test.ok(object[key] != undefined, 'Missing key: \'' + key + '\' in ' + name) + }) + } } - test.ok(!!object) - packageJsonKeys.forEach(function (key) { - var exists = object[key] != undefined ? - key : false - test.equal(exists, key) - }) test.done() } // ensure that .gitignore contains '/node_modules' function parseGitignore(test) { - var expected = '/node_modules' - var data = fs.readFileSync(path.join(deployFolder, '.gitignore'), 'utf-8') - test.ok(data.indexOf(expected) != -1, '.gitignore missing:' + expected) + + // file should exist + var name = '.gitignore' + var file = path.join(deployFolder, name) + var exists = fs.existsSync(file) + test.ok(exists, 'File missing:' + name + ' in folder:' + deployFolder) + + if (exists) { + var data = fs.readFileSync(file, 'utf-8') + + test.ok(data.indexOf(expected) != -1, 'File ' + name + ' is missing line:\'' + expected + '\'') + } test.done() } @@ -176,10 +197,10 @@ function parseGitignore(test) { // ensure that readme.md exists function findReadme(test) { - var file = 'readme.md' - var exists = fs.existsSync(path.join(deployFolder, file)) ? - file : false - test.equal(exists, file) + var name = 'readme.md' + var file = path.join(deployFolder, name) + var exists = fs.existsSync(file) + test.ok(exists, 'File missing:' + name + ' in folder:' + deployFolder) test.done() } diff --git a/test/test-render.js b/test/test-render.js deleted file mode 100644 index 47fbe52..0000000 --- a/test/test-render.js +++ /dev/null @@ -1,192 +0,0 @@ -// test-render.js -// nodeunit test for the webfiller rendering -// (c) Harald Rudell 2012 - -var compiler = require('../lib/compiler') -var render = require('../lib/renderer') - -module.exports = { - testEmptyStringLocation: testEmptyStringLocation, - testReplace: testReplace, - testInsert: testInsert, - testAppend: testAppend, - testTag: testTag, - testInclude: testInclude, - testError: testError, -} - -/* -Need to test: - -type: bindings key -- empty string acesss: testEmptyStringLocation -- #id: testInsert -- .class: testAppend -- tag: testTag - -type: bindings value string -- top level data field: testEmptyStringLocation, testReplace -- (data data field don't know how to implement this yes) -- -view include: TestInclude - -type: bindings data object -- append: testAppend -- insert: testInsert -- append-all fields: testTag - -type: bindings data array -- array binding value - -type: other functions -- html escaping: testReplace -- raw escaping: TestInclude -- error: testError -*/ - -function testEmptyStringLocation(test) { - var html = '' - var bindings = { - '': 'title', - } - var record = { title: 'HERE' } - var expected = 'HERE<!doctype html><title/>' - var viewExecutable = compiler.compileHtml5(html, bindings) - //console.log(viewExecutable.getSource('name')) - var actual = viewExecutable(record) - test.equal(actual, expected) - - test.done() -} - -function testReplace(test) { - var html = 'a<title>bc' - var bindings = { - 'title': 'here' // tags title, replace initial content with field here - } - var record = { - here: 'HERE<&', - } - var expected = 'aHERE<&c' - var viewExecutable = compiler.compileHtml5(html, bindings) - var actual = viewExecutable(record) - test.equal(actual, expected) - - test.done() -} - - -function testInsert(test) { - var html = 'abc

d' - var bindings = { - '#x': { - 'insert': 'here' - } - } - var record = { - here: 'HERE', - } - var expected = 'abc
HEREd' - var viewExecutable = compiler.compileHtml5(html, bindings) - var actual = viewExecutable(record) - test.equal(actual, expected) - - test.done() -} - -function testAppend(test) { - var html = 'a
b
' - var bindings = { - '.x': { // find class x - 'append': 'here' // append to tags its initial content - } - } - var record = { - here: 'HERE', // the value for field 'here' is 'HERE' - } - var expected = 'a
bHERE
' - var viewExecutable = compiler.compileHtml5(html, bindings) - //console.log(viewExecutable.getSource('name')) - var actual = viewExecutable(record) - test.equal(actual, expected) - - test.done() -} - -function testTag(test) { - var html = 'abc' - var bindings = { - 'title': { - 'append': ''// test append all fields - } - } - var record = { - here: 'HERE', - there: 'THERE', - } - var expected = 'abhere:HERE, there:THEREc' - var viewExecutable = compiler.compileHtml5(html, bindings) - var actual = viewExecutable(record) - test.equal(actual, expected) - - test.done() -} - -function testInclude(test) { - - // the included view - var fragmentName = 'FRAGMENT' - var includeHtml = 'A
B
C' - var includeBindings = { - 'div': 'there', - } - - // the main view - var html = 'abc' - var bindings = { - 'title': { - 'fragment': fragmentName, - } - } - - // the record - var record = { - here: 'HERE', - there: 'THERE' - } - - var lf = compiler.loadFragment - compiler.loadFragment = mockInclude - - var expected = 'aA<div>THERE</div>Cc' - var viewExecutable = compiler.compileHtml5(html, bindings) - //console.log(viewExecutable.getSource('name')) - var actual = viewExecutable(record) - test.equal(actual, expected) - - compiler.loadFragment = lf - test.done() - - function mockInclude(fragment) { - //console.log(arguments.callee.name, 'view&bindings', view, bindings) - test.equal(fragment, fragmentName) - var renderFunction = compiler.compileHtml5(includeHtml, includeBindings) - test.ok(renderFunction instanceof Function) - //console.log(arguments.callee.name, 'source', renderFunction.getSource('name')) - //console.log(arguments.callee.name, 'result', renderFunction(record)) - return renderFunction - } -} - -function testError(test) { - var html = 'ab' - var bindings = { - 'title': 5, // we can't have number here - } - var record = {} - var expected = 'a<title>Unknown rendering data source type:numberb' - var viewExecutable = compiler.compileHtml5(html, bindings) - var actual = viewExecutable(record) - test.equal(actual, expected) - - test.done() -} \ No newline at end of file diff --git a/test/test-renderer.js b/test/test-renderer.js new file mode 100644 index 0000000..cc7a550 --- /dev/null +++ b/test/test-renderer.js @@ -0,0 +1,357 @@ +// test-renderer.js +// © Harald Rudell 2012 + +var compiler = require('../lib/compiler') +var render = require('../lib/renderer') + +/* +Need to test: + +type: bindings key +- empty string acesss: testEmptyStringLocation +- #id: testInsert +- .class: testAppend +- tag: testTag + +type: bindings value string +- top level data field: testEmptyStringLocation, testReplace +- (data data field don't know how to implement this yes) +- -view include: TestInclude + +type: bindings data object +- append: testAppend +- insert: testInsert +- append-all fields: testTag + +type: bindings data array +- array binding value + +type: other functions +- html escaping: testReplace +- raw escaping: TestInclude +- error: testError +*/ + +exports.plainHtml = function plainHtml(test) { + var html = '<div/>' + var expected = html + var actual = compiler.compileHtml5(html)() + test.equal(actual, expected) + + test.done() +} + +exports.emptyStringLocation = function (test) { + var html = '<!doctype html><title/>' + var bindings = { + '': 'title', + } + var record = {title:'HERE'} + var expected = 'HERE<!doctype html><title/>' + var viewExecutable = compiler.compileHtml5(html, bindings) +//console.log(viewExecutable.getSource('name')) + var actual = viewExecutable(record) +//console.log('actual:', actual) + test.equal(actual, expected) + + test.done() +} + +exports.idLocation = function (test) { + var html = '<title id=x>y' + var bindings = { + '#x': 'title', + } + var record = {title:'HERE'} + var expected = 'HEREy' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.classLocation = function (test) { + var html = 'y' + var bindings = { + '.x': 'title', + } + var record = {title:'HERE'} + var expected = 'HEREy' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.tagLocation = function (test) { + var html = 'abc' + var bindings = { + title: 'title' + } + var record = { + title: 'HERE', + } + var expected = 'aHEREbc' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.testArray = function (test) { + var html = 'abc' + var bindings = { + title: [ + 'here', + 'there' + ] + } + var record = { + here: 'HERE', + there: 'THERE', + } + var expected = 'aHERETHEREbc' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.testFragment = function (test) { + + // the included view + var fragmentName = 'FRAGMENT' + var includeHtml = 'A
B
C' + var includeBindings = { + 'div': 'there', + } + + // the main view + var html = 'abc' + var bindings = { + 'title': { + 'fragment': fragmentName, + } + } + + // the record + var record = { + here: 'HERE', + there: 'THERE' + } + + var lf = compiler.loadFragment + compiler.loadFragment = mockInclude + + var expected = 'aA<div>THEREB</div>Cbc' + var viewExecutable = compiler.compileHtml5(html, bindings) + //console.log(viewExecutable.getSource('name')) + var actual = viewExecutable(record) + test.equal(actual, expected) + + compiler.loadFragment = lf + test.done() + + function mockInclude(fragment) { + //console.log(arguments.callee.name, 'view&bindings', view, bindings) + test.equal(fragment, fragmentName) + var renderFunction = compiler.compileHtml5(includeHtml, includeBindings) + test.ok(renderFunction instanceof Function) + //console.log(arguments.callee.name, 'source', renderFunction.getSource('name')) + //console.log(arguments.callee.name, 'result', renderFunction(record)) + return renderFunction + } +} + +exports.testReplace = function (test) { + var html = 'abc
d' + var bindings = { + '#x': { + replace: 'here' + } + } + var record = { + here: 'HERE', + } + var expected = 'abc
HERE' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.testAppend = function (test) { + var html = 'a
b
' + var bindings = { + '.x': { // find class x + 'append': 'here' // append to tags its initial content + } + } + var record = { + here: 'HERE', // the value for field 'here' is 'HERE' + } + var expected = 'a
bHERE
' + var viewExecutable = compiler.compileHtml5(html, bindings) + //console.log(viewExecutable.getSource('name')) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.testPrint = function (test) { + var html = 'a
b
c' + var bindings = { + div: 'here' + } + var record = { + here: '<&HERE', // the value for field 'here' is 'HERE' + } + var expected = 'a
<&HEREb
c' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.testPrintRaw = function (test) { + var html = 'a
b
c' + var fragName = 'XYZ' + var bindings = { + div: { + fragment: fragName + } + } + var record = {} + var expected = 'a
<&b
c' + WF = require('../lib/runtime').WF + WF.fragments[fragName] = function () { return '<&' } + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + delete WF.fragments[fragName] + test.done() +} + +exports.testUnknownDataSourceType = function (test) { + var html = 'ab' + var bindings = { + 'title': 5, // we can't have number here + } + var record = {} + var expected = 'a<title>Unknown rendering data source type:numberb' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.testFragmentNameNotString = function (test) { + var html = 'a<title>b' + var bindings = { + 'title': { + fragment: 5, // we can't have number here + } + } + var record = {} + var expected = 'a<title>Fragment name not stringb' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.testUnknownFunction = function (test) { + var html = 'a<title>b' + var bindings = { + 'title': { + unknown: '', + } + } + var record = {} + var expected = 'a<title>Unknown rendering function:unknownb' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable(record) + test.equal(actual, expected) + + test.done() +} + +exports.testAddClass = function (test) { + var html = 'a<title class=1>b' + var bindings = { + 'title': { + addClass: 'red blue' + } + } + var expected = 'ab' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable() + test.equal(actual, expected) + + test.done() +} + +exports.testRemoveClass = function (test) { + var html = 'ab' + var bindings = { + 'title': { + removeClass: 'red white' + } + } + var expected = 'ab' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable() + test.equal(actual, expected) + + test.done() +} + +exports.testAttribute = function (test) { + var html = 'ab' + var bindings = { + 'title': { + attribute: { + 'a1': false, + 'a3': 'hello', + 'a6': false, + } + } + } + var expected = 'ab' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable() + test.equal(actual, expected) + + test.done() +} + +exports.testSuppressTag = function (test) { + var html = 'ab' + var funcName = 'myFunc' + var bindings = { + 'title': { + myFunc: false + } + } + var WF = require('../lib/runtime').WF + WF.functions[funcName] = function (params) { + this.suppressTag() + } + var expected = 'ab' + var viewExecutable = compiler.compileHtml5(html, bindings) + var actual = viewExecutable() + test.equal(actual, expected) + delete WF.functions[funcName] + + test.done() +} \ No newline at end of file