From ec33215a7f262fd384027a3ea09c829540cbbe35 Mon Sep 17 00:00:00 2001 From: Sashko Stubailo Date: Tue, 28 Jul 2015 10:33:40 -0700 Subject: [PATCH] Static HTML package and templating refactor 1. Make a package called `static-html` that just compiles `` and `` tags inside `.html` files. Much like templating but without the templates. 2. Refactor `templating` to avoid duplicating code. Split out code that would be shared between `templating` and `static-html` into a new `templating-tools` package. These tools could also be used to simplify implementation of other build plugins, like `simple:markdown-templating`. This also has the added benefit of moving as much code as humanly possible out of the `templating` package, so that it can be reused in other packages. 1. `templating-tools` package and its README 2. `static-html` package and its README 3. `caching-html-compiler` is not new code; it is just code factored out of the batch plugin version of `templating`, but the README and some comments are new. 1. `tools/tests/static-html.js` tests static html and error handling 2. `templating-tools/html-scanner-tests.js` tests `scanHtmlForTags` and `compileTagsWithSpacebars` together. All unit tests pass on this branch. --- packages/caching-html-compiler/README.md | 37 +++ .../caching-html-compiler.js | 136 +++++++++++ packages/caching-html-compiler/package.js | 21 ++ packages/static-html/README.md | 0 packages/static-html/package.js | 28 +++ packages/static-html/static-html.js | 90 +++++++ packages/templating-tools/README.md | 102 ++++++++ packages/templating-tools/code-generation.js | 18 ++ .../compile-tags-with-spacebars.js | 104 ++++++++ .../html-scanner-tests.js} | 72 +++--- packages/templating-tools/html-scanner.js | 172 ++++++++++++++ packages/templating-tools/package.js | 47 ++++ packages/templating-tools/templating-tools.js | 4 + .../templating-tools/throw-compile-error.js | 11 + packages/templating/README.md | 2 +- packages/templating/package.js | 32 +-- .../templating/plugin/compile-templates.js | 105 +------- packages/templating/plugin/html_scanner.js | 224 ------------------ .../.meteor/.gitignore | 1 + .../.meteor/.id | 7 + .../.meteor/packages | 16 ++ .../.meteor/platforms | 2 + .../.meteor/release | 1 + .../.meteor/versions | 53 +++++ .../static.html | 7 + .../.meteor/.gitignore | 1 + .../compiler-plugin-static-html/.meteor/.id | 7 + .../.meteor/packages | 16 ++ .../.meteor/platforms | 2 + .../.meteor/release | 1 + .../.meteor/versions | 53 +++++ .../compiler-plugin-static-html/static.html | 7 + tools/tests/static-html.js | 60 +++++ 33 files changed, 1056 insertions(+), 383 deletions(-) create mode 100644 packages/caching-html-compiler/README.md create mode 100644 packages/caching-html-compiler/caching-html-compiler.js create mode 100644 packages/caching-html-compiler/package.js create mode 100644 packages/static-html/README.md create mode 100644 packages/static-html/package.js create mode 100644 packages/static-html/static-html.js create mode 100644 packages/templating-tools/README.md create mode 100644 packages/templating-tools/code-generation.js create mode 100644 packages/templating-tools/compile-tags-with-spacebars.js rename packages/{templating/scanner_tests.js => templating-tools/html-scanner-tests.js} (66%) create mode 100644 packages/templating-tools/html-scanner.js create mode 100644 packages/templating-tools/package.js create mode 100644 packages/templating-tools/templating-tools.js create mode 100644 packages/templating-tools/throw-compile-error.js delete mode 100644 packages/templating/plugin/html_scanner.js create mode 100644 tools/tests/apps/compiler-plugin-static-html-error/.meteor/.gitignore create mode 100644 tools/tests/apps/compiler-plugin-static-html-error/.meteor/.id create mode 100644 tools/tests/apps/compiler-plugin-static-html-error/.meteor/packages create mode 100644 tools/tests/apps/compiler-plugin-static-html-error/.meteor/platforms create mode 100644 tools/tests/apps/compiler-plugin-static-html-error/.meteor/release create mode 100644 tools/tests/apps/compiler-plugin-static-html-error/.meteor/versions create mode 100644 tools/tests/apps/compiler-plugin-static-html-error/static.html create mode 100644 tools/tests/apps/compiler-plugin-static-html/.meteor/.gitignore create mode 100644 tools/tests/apps/compiler-plugin-static-html/.meteor/.id create mode 100644 tools/tests/apps/compiler-plugin-static-html/.meteor/packages create mode 100644 tools/tests/apps/compiler-plugin-static-html/.meteor/platforms create mode 100644 tools/tests/apps/compiler-plugin-static-html/.meteor/release create mode 100644 tools/tests/apps/compiler-plugin-static-html/.meteor/versions create mode 100644 tools/tests/apps/compiler-plugin-static-html/static.html create mode 100644 tools/tests/static-html.js diff --git a/packages/caching-html-compiler/README.md b/packages/caching-html-compiler/README.md new file mode 100644 index 0000000000..d64298c59f --- /dev/null +++ b/packages/caching-html-compiler/README.md @@ -0,0 +1,37 @@ +# caching-html-compiler + +Provides a pluggable class used to compile HTML-style templates in Meteor build plugins. This abstracts out a lot of the functionality you would need to implement the following plugins: + +1. `templating` +2. `static-html` +3. `simple:markdown-templating` + +It provides automatic caching and handles communicating with the build plugin APIs. The actual functions that convert HTML into compiled form are passed in as arguments into the constructor, allowing those functions to be unit tested separately from the caching and file system functionality. + +------- + +### new CachingHtmlCompiler(name, tagScannerFunc, tagHandlerFunc) + +Constructs a new CachingHtmlCompiler that can be passed into `Plugin.registerCompiler`. + +#### Arguments + +1. `name` The name of the compiler, used when printing errors. Should probably be the same as the name of the build plugin and package it is used in. +2. `tagScannerFunc` A function that takes a string representing a template file as input, and returns an array of Tag objects. See the README for `templating-tools` for more information about the Tag object. +3. `tagHandlerFunc` A function that takes an array of Tag objects (the output of the previous argument) and returns an object with `js`, `body`, `head`, and `bodyAttr` properties, which will be added to the app through the build plugin API. + +#### Example + +Here is some example code from the `templating` package: + +```js +Plugin.registerCompiler({ + extensions: ['html'], + archMatching: 'web', + isTemplate: true +}, () => new CachingHtmlCompiler( + "templating", + TemplatingTools.scanHtmlForTags, + TemplatingTools.compileTagsWithSpacebars +)); +``` diff --git a/packages/caching-html-compiler/caching-html-compiler.js b/packages/caching-html-compiler/caching-html-compiler.js new file mode 100644 index 0000000000..b4c2456ab2 --- /dev/null +++ b/packages/caching-html-compiler/caching-html-compiler.js @@ -0,0 +1,136 @@ +const path = Plugin.path; + +// The CompileResult type for this CachingCompiler is the return value of +// htmlScanner.scan: a {js, head, body, bodyAttrs} object. +CachingHtmlCompiler = class CachingHtmlCompiler extends CachingCompiler { + /** + * Constructor for CachingHtmlCompiler + * @param {String} name The name of the compiler, printed in errors - + * should probably always be the same as the name of the build + * plugin/package + * @param {Function} tagScannerFunc Transforms a template file (commonly + * .html) into an array of Tags + * @param {Function} tagHandlerFunc Transforms an array of tags into a + * results object with js, body, head, and bodyAttrs properties + */ + constructor(name, tagScannerFunc, tagHandlerFunc) { + super({ + compilerName: name, + defaultCacheSize: 1024*1024*10, + }); + + this._bodyAttrInfo = null; + + this.tagScannerFunc = tagScannerFunc; + this.tagHandlerFunc = tagHandlerFunc; + } + + // Implements method from CachingCompilerBase + compileResultSize(compileResult) { + function lengthOrZero(field) { + return field ? field.length : 0; + } + return lengthOrZero(compileResult.head) + lengthOrZero(compileResult.body) + + lengthOrZero(compileResult.js); + } + + // Overrides method from CachingCompiler + processFilesForTarget(inputFiles) { + this._bodyAttrInfo = {}; + super.processFilesForTarget(inputFiles); + } + + // Implements method from CachingCompilerBase + getCacheKey(inputFile) { + // Note: the path is only used for errors, so it doesn't have to be part + // of the cache key. + return inputFile.getSourceHash(); + } + + // Implements method from CachingCompiler + compileOneFile(inputFile) { + const contents = inputFile.getContentsAsString(); + const inputPath = inputFile.getPathInPackage(); + try { + const tags = this.tagScannerFunc({ + sourceName: inputPath, + contents: contents, + tagNames: ["body", "head", "template"] + }); + + return this.tagHandlerFunc(tags); + } catch (e) { + if (e instanceof TemplatingTools.CompileError) { + inputFile.error({ + message: e.message, + line: e.line + }); + return null; + } else { + throw e; + } + } + } + + // Implements method from CachingCompilerBase + addCompileResult(inputFile, compileResult) { + let allJavaScript = ""; + + if (compileResult.head) { + inputFile.addHtml({ section: "head", data: compileResult.head }); + } + + if (compileResult.body) { + inputFile.addHtml({ section: "body", data: compileResult.body }); + } + + if (compileResult.js) { + allJavaScript += compileResult.js; + } + + if (! _.isEmpty(compileResult.bodyAttrs)) { + Object.keys(compileResult.bodyAttrs).forEach((attr) => { + const value = compileResult.bodyAttrs[attr]; + if (this._bodyAttrInfo.hasOwnProperty(attr) && + this._bodyAttrInfo[attr].value !== value) { + // two conflicting attributes on tags in two different template + // files + inputFile.error({ + message: + ` declarations have conflicting values for the '${ attr }' ` + + `attribute in the following files: ` + + this._bodyAttrInfo[attr].inputFile.getPathInPackage() + + `, ${ inputFile.getPathInPackage() }` + }); + } else { + this._bodyAttrInfo[attr] = {inputFile, value}; + } + }); + + // Add JavaScript code to set attributes on body + allJavaScript += ` +Meteor.startup(function() { $('body').attr(${JSON.stringify(compileResult.bodyAttrs)}); }); +`; + } + + + if (allJavaScript) { + const filePath = inputFile.getPathInPackage(); + // XXX this path manipulation may be unnecessarily complex + let pathPart = path.dirname(filePath); + if (pathPart === '.') + pathPart = ''; + if (pathPart.length && pathPart !== path.sep) + pathPart = pathPart + path.sep; + const ext = path.extname(filePath); + const basename = path.basename(filePath, ext); + + // XXX generate a source map + + inputFile.addJavaScript({ + path: path.join(pathPart, "template." + basename + ".js"), + data: allJavaScript + }); + } + } +} diff --git a/packages/caching-html-compiler/package.js b/packages/caching-html-compiler/package.js new file mode 100644 index 0000000000..b601d98e61 --- /dev/null +++ b/packages/caching-html-compiler/package.js @@ -0,0 +1,21 @@ +Package.describe({ + version: '1.0.0', + // Brief, one-line summary of the package. + summary: 'Pluggable class for compiling HTML into templates', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md' +}); + +Package.onUse(function(api) { + api.use([ + 'underscore', + 'caching-compiler', + 'templating-tools', + 'ecmascript' + ]); + + api.addFiles('caching-html-compiler.js', 'server'); + + api.export("CachingHtmlCompiler", 'server'); +}); diff --git a/packages/static-html/README.md b/packages/static-html/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/static-html/package.js b/packages/static-html/package.js new file mode 100644 index 0000000000..ec1e8bb2b8 --- /dev/null +++ b/packages/static-html/package.js @@ -0,0 +1,28 @@ +Package.describe({ + version: '0.0.1', + // Brief, one-line summary of the package. + summary: 'Define static page content in .html files', + // By default, Meteor will default to using README.md for documentation. + // To avoid submitting documentation, set this field to null. + documentation: 'README.md' +}); + +Package.registerBuildPlugin({ + name: "compileStaticHtmlBatch", + use: [ + 'caching-html-compiler', + 'ecmascript', + 'templating-tools', + 'underscore' + ], + sources: [ + 'static-html.js' + ] +}); + +Package.onUse(function(api) { + api.use('isobuild:compiler-plugin@1.0.0'); + + // Body attributes are compiled to code that uses Meteor.startup + api.imply('meteor', 'client'); +}); diff --git a/packages/static-html/static-html.js b/packages/static-html/static-html.js new file mode 100644 index 0000000000..2d223a65aa --- /dev/null +++ b/packages/static-html/static-html.js @@ -0,0 +1,90 @@ +Plugin.registerCompiler({ + extensions: ['html'], + archMatching: 'web', + isTemplate: true +}, () => new CachingHtmlCompiler("static-html", TemplatingTools.scanHtmlForTags, compileTagsToStaticHtml)); + +// Same API as TutorialTools.compileTagsWithSpacebars, but instead of compiling +// with Spacebars, it just returns static HTML +function compileTagsToStaticHtml(tags) { + var handler = new StaticHtmlTagHandler(); + + tags.forEach((tag) => { + handler.addTagToResults(tag); + }); + + return handler.getResults(); +}; + +class StaticHtmlTagHandler { + constructor() { + this.results = { + head: '', + body: '', + js: '', + bodyAttrs: {} + }; + } + + getResults() { + return this.results; + } + + addTagToResults(tag) { + this.tag = tag; + + // do we have 1 or more attributes? + const hasAttribs = ! _.isEmpty(this.tag.attribs); + + if (this.tag.tagName === "head") { + if (hasAttribs) { + this.throwCompileError("Attributes on not supported"); + } + + this.results.head += this.tag.contents; + return; + } + + + // or