mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Static HTML package and templating refactor
1. Make a package called `static-html` that just compiles `<head>` and `<body>` 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.
This commit is contained in:
37
packages/caching-html-compiler/README.md
Normal file
37
packages/caching-html-compiler/README.md
Normal file
@@ -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
|
||||
));
|
||||
```
|
||||
136
packages/caching-html-compiler/caching-html-compiler.js
Normal file
136
packages/caching-html-compiler/caching-html-compiler.js
Normal file
@@ -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 <body> tags in two different template
|
||||
// files
|
||||
inputFile.error({
|
||||
message:
|
||||
`<body> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
21
packages/caching-html-compiler/package.js
Normal file
21
packages/caching-html-compiler/package.js
Normal file
@@ -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');
|
||||
});
|
||||
0
packages/static-html/README.md
Normal file
0
packages/static-html/README.md
Normal file
28
packages/static-html/package.js
Normal file
28
packages/static-html/package.js
Normal file
@@ -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');
|
||||
});
|
||||
90
packages/static-html/static-html.js
Normal file
90
packages/static-html/static-html.js
Normal file
@@ -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 <head> not supported");
|
||||
}
|
||||
|
||||
this.results.head += this.tag.contents;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// <body> or <template>
|
||||
|
||||
try {
|
||||
if (this.tag.tagName === "body") {
|
||||
this.addBodyAttrs(this.tag.attribs);
|
||||
|
||||
// We may be one of many `<body>` tags.
|
||||
this.results.body += this.tag.contents;
|
||||
} else {
|
||||
this.throwCompileError("Expected <head> or <body> tag", this.tag.tagStartIndex);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.scanner) {
|
||||
// The error came from Spacebars
|
||||
this.throwCompileError(e.message, this.tag.contentsStartIndex + e.offset);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addBodyAttrs(attrs) {
|
||||
Object.keys(attrs).forEach((attr) => {
|
||||
const val = attrs[attr];
|
||||
|
||||
// This check is for conflicting body attributes in the same file;
|
||||
// we check across multiple files in caching-html-compiler using the
|
||||
// attributes on results.bodyAttrs
|
||||
if (this.results.bodyAttrs.hasOwnProperty(attr) && this.results.bodyAttrs[attr] !== val) {
|
||||
this.throwCompileError(
|
||||
`<body> declarations have conflicting values for the '${attr}' attribute.`);
|
||||
}
|
||||
|
||||
this.results.bodyAttrs[attr] = val;
|
||||
});
|
||||
}
|
||||
|
||||
throwCompileError(message, overrideIndex) {
|
||||
TemplatingTools.throwCompileError(this.tag, message, overrideIndex);
|
||||
}
|
||||
}
|
||||
|
||||
102
packages/templating-tools/README.md
Normal file
102
packages/templating-tools/README.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# templating-tools
|
||||
|
||||
Has some conveniently abstracted functions that are used together with the `caching-html-compiler` package to implement different template compilers:
|
||||
|
||||
1. `templating`
|
||||
2. `static-html`
|
||||
|
||||
These functions contain some code shared between the above build plugins, and if you are building your own build plugin they can be useful too. But they aren't guaranteed to be helpful for every use case, so you should carefully decide if they are appropriate for your package.
|
||||
|
||||
---------
|
||||
|
||||
### TemplatingTools.scanHtmlForTags(options)
|
||||
|
||||
Scan an HTML file for top-level tags as specified by `options.tagNames`, and return an array of `Tag` objects. See more about `Tag` objects below.
|
||||
|
||||
#### Options
|
||||
|
||||
1. `sourceName` the name of the input file, used when throwing errors.
|
||||
2. `contents` the contents of the input file, these are parsed to find the top-level tags
|
||||
3. `tagNames` the top-level tags to look for in the HTML.
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
const tags = scanHtmlForTags({
|
||||
sourceName: inputPath,
|
||||
contents: contents,
|
||||
tagNames: ["body", "head", "template"]
|
||||
});
|
||||
```
|
||||
|
||||
### TemplatingTools.compileTagsWithSpacebars(tags)
|
||||
|
||||
Transform an array of tags into a result object of the following form:
|
||||
|
||||
```js
|
||||
{
|
||||
js: String,
|
||||
body: "",
|
||||
head: String,
|
||||
bodyAttrs: {
|
||||
[attrName]: String
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. The contents of every `<template>` and `<body>` tag will be compiled into JavaScript with `spacebars-compiler`, and the code appended to the `js` field of the result.
|
||||
2. The contents of every `<head>` tag will be concatenated into the `head` field of the result.
|
||||
3. Any attributes found on `<body>` tags will be added to the `bodyAtts` field of the result.
|
||||
4. Every `<template>` tag is required to have a `name` attribute, and no other attributes.
|
||||
5. The `<head>` tag is not allowed to have any attributes.
|
||||
|
||||
### TemplatingTools.CompileError
|
||||
|
||||
This error is thrown when a compilation error happens. If you catch it, look for the following fields, which are set by `TemplatingTools.throwCompileError`:
|
||||
|
||||
1. `message` The error message to show to the user.
|
||||
2. `file` The filename where the error occured.
|
||||
3. `line` The line number where the error occured.
|
||||
|
||||
### TemplatingTools.throwCompileError(tag, message, [overrideIndex])
|
||||
|
||||
Throw a `TemplatingTools.CompileError` with the right properties. Handles generating the line number of the error for you.
|
||||
|
||||
#### Arguments
|
||||
|
||||
1. `tag` the Tag object in which this compile error occured. The fields on this object are used to populate fields on the resulting error.
|
||||
2. `message` the error message, will be displayed to the user.
|
||||
3. `overrideIndex` optional - if provided will be used to determine the line number of the error; otherwise the index of the start of the tag will be used.
|
||||
|
||||
### Tag object
|
||||
|
||||
The `scanHtml` and `compileTagsWithSpacebars` functions communicate via an array of Tag objects, which have the following form:
|
||||
|
||||
```js
|
||||
{
|
||||
// Name of the tag - "body", "head", "template", etc
|
||||
tagName: String,
|
||||
|
||||
// Attributes on the tag
|
||||
attribs: { [attrName]: String },
|
||||
|
||||
// Contents of the tag
|
||||
contents: String,
|
||||
|
||||
// Starting index of the opening tag in the source file
|
||||
// (used to throw informative errors)
|
||||
tagStartIndex: Number,
|
||||
|
||||
// Starting index of the contents of the tag in the source file
|
||||
// (used to throw informative errors)
|
||||
contentsStartIndex: Number,
|
||||
|
||||
// The contents of the entire source file, should be used only to
|
||||
// throw informative errors (for example, this can be used to
|
||||
// determine the line number for an error)
|
||||
fileContents: String,
|
||||
|
||||
// The file name of the initial source file, used to throw errors
|
||||
sourceName: String
|
||||
};
|
||||
```
|
||||
18
packages/templating-tools/code-generation.js
Normal file
18
packages/templating-tools/code-generation.js
Normal file
@@ -0,0 +1,18 @@
|
||||
TemplatingTools.generateTemplateJS =
|
||||
function generateTemplateJS(name, renderFuncCode) {
|
||||
const nameLiteral = JSON.stringify(name);
|
||||
const templateDotNameLiteral = JSON.stringify(`Template.${name}`);
|
||||
|
||||
return `
|
||||
Template.__checkName(${nameLiteral});
|
||||
Template[${nameLiteral}] = new Template(${templateDotNameLiteral}, ${renderFuncCode});
|
||||
`;
|
||||
}
|
||||
|
||||
TemplatingTools.generateBodyJS =
|
||||
function generateBodyJS(renderFuncCode) {
|
||||
return `
|
||||
Template.body.addContent(${renderFuncCode});
|
||||
Meteor.startup(Template.body.renderToDocument);
|
||||
`;
|
||||
}
|
||||
104
packages/templating-tools/compile-tags-with-spacebars.js
Normal file
104
packages/templating-tools/compile-tags-with-spacebars.js
Normal file
@@ -0,0 +1,104 @@
|
||||
TemplatingTools.compileTagsWithSpacebars = function compileTagsWithSpacebars(tags) {
|
||||
var handler = new SpacebarsTagCompiler();
|
||||
|
||||
tags.forEach((tag) => {
|
||||
handler.addTagToResults(tag);
|
||||
});
|
||||
|
||||
return handler.getResults();
|
||||
};
|
||||
|
||||
class SpacebarsTagCompiler {
|
||||
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 <head> not supported");
|
||||
}
|
||||
|
||||
this.results.head += this.tag.contents;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// <body> or <template>
|
||||
|
||||
try {
|
||||
if (this.tag.tagName === "template") {
|
||||
const name = this.tag.attribs.name;
|
||||
|
||||
if (! name) {
|
||||
this.throwCompileError("Template has no 'name' attribute");
|
||||
}
|
||||
|
||||
if (SpacebarsCompiler.isReservedName(name)) {
|
||||
this.throwCompileError(`Template can't be named "${name}"`);
|
||||
}
|
||||
|
||||
const renderFuncCode = SpacebarsCompiler.compile(this.tag.contents, {
|
||||
isTemplate: true,
|
||||
sourceName: `Template "${name}"`
|
||||
});
|
||||
|
||||
this.results.js += TemplatingTools.generateTemplateJS(
|
||||
name, renderFuncCode);
|
||||
} else if (this.tag.tagName === "body") {
|
||||
this.addBodyAttrs(this.tag.attribs);
|
||||
|
||||
const renderFuncCode = SpacebarsCompiler.compile(this.tag.contents, {
|
||||
isBody: true,
|
||||
sourceName: "<body>"
|
||||
});
|
||||
|
||||
// We may be one of many `<body>` tags.
|
||||
this.results.js += TemplatingTools.generateBodyJS(renderFuncCode);
|
||||
} else {
|
||||
this.throwCompileError("Expected <template>, <head>, or <body> tag in template file", tagStartIndex);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.scanner) {
|
||||
// The error came from Spacebars
|
||||
this.throwCompileError(e.message, this.tag.contentsStartIndex + e.offset);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addBodyAttrs(attrs) {
|
||||
Object.keys(attrs).forEach((attr) => {
|
||||
const val = attrs[attr];
|
||||
|
||||
// This check is for conflicting body attributes in the same file;
|
||||
// we check across multiple files in caching-html-compiler using the
|
||||
// attributes on results.bodyAttrs
|
||||
if (this.results.bodyAttrs.hasOwnProperty(attr) && this.results.bodyAttrs[attr] !== val) {
|
||||
this.throwCompileError(
|
||||
`<body> declarations have conflicting values for the '${attr}' attribute.`);
|
||||
}
|
||||
|
||||
this.results.bodyAttrs[attr] = val;
|
||||
});
|
||||
}
|
||||
|
||||
throwCompileError(message, overrideIndex) {
|
||||
TemplatingTools.throwCompileError(this.tag, message, overrideIndex);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Tinytest.add("templating - html scanner", function (test) {
|
||||
Tinytest.add("templating-tools - html scanner", function (test) {
|
||||
var testInString = function(actualStr, wantedContents) {
|
||||
if (actualStr.indexOf(wantedContents) >= 0)
|
||||
test.ok();
|
||||
@@ -11,6 +11,10 @@ Tinytest.add("templating - html scanner", function (test) {
|
||||
try {
|
||||
f();
|
||||
} catch (e) {
|
||||
if (! e instanceof TemplatingTools.CompileError) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (e.line === lineNum)
|
||||
test.ok();
|
||||
else
|
||||
@@ -39,112 +43,122 @@ Tinytest.add("templating - html scanner", function (test) {
|
||||
', (function() {\n var view = this;\n return ' + content + ';\n}));\n';
|
||||
};
|
||||
|
||||
var checkResults = function(results, expectJs, expectHead) {
|
||||
var checkResults = function(results, expectJs, expectHead, expectBodyAttrs) {
|
||||
test.equal(results.body, '');
|
||||
test.equal(results.js, expectJs || '');
|
||||
test.equal(results.head, expectHead || '');
|
||||
test.equal(results.bodyAttrs, expectBodyAttrs || {});
|
||||
};
|
||||
|
||||
function scanForTest(contents) {
|
||||
const tags = TemplatingTools.scanHtmlForTags({
|
||||
sourceName: "",
|
||||
contents: contents,
|
||||
tagNames: ["body", "head", "template"]
|
||||
});
|
||||
|
||||
return TemplatingTools.compileTagsWithSpacebars(tags);
|
||||
}
|
||||
|
||||
checkError(function() {
|
||||
return html_scanner.scan("asdf");
|
||||
}, "Expected <template>, <head>, or <body> tag in template file", 1);
|
||||
return scanForTest("asdf");
|
||||
}, "Expected one of: <body>, <head>, <template>", 1);
|
||||
|
||||
// body all on one line
|
||||
checkResults(
|
||||
html_scanner.scan("<body>Hello</body>"),
|
||||
scanForTest("<body>Hello</body>"),
|
||||
simpleBody('"Hello"'));
|
||||
|
||||
// multi-line body, contents trimmed
|
||||
checkResults(
|
||||
html_scanner.scan("\n\n\n<body>\n\nHello\n\n</body>\n\n\n"),
|
||||
scanForTest("\n\n\n<body>\n\nHello\n\n</body>\n\n\n"),
|
||||
simpleBody('"Hello"'));
|
||||
|
||||
// same as previous, but with various HTML comments
|
||||
checkResults(
|
||||
html_scanner.scan("\n<!--\n\nfoo\n-->\n<!-- -->\n"+
|
||||
scanForTest("\n<!--\n\nfoo\n-->\n<!-- -->\n"+
|
||||
"<body>\n\nHello\n\n</body>\n\n<!----\n>\n\n"),
|
||||
simpleBody('"Hello"'));
|
||||
|
||||
// head and body
|
||||
checkResults(
|
||||
html_scanner.scan("<head>\n<title>Hello</title>\n</head>\n\n<body>World</body>\n\n"),
|
||||
scanForTest("<head>\n<title>Hello</title>\n</head>\n\n<body>World</body>\n\n"),
|
||||
simpleBody('"World"'),
|
||||
"<title>Hello</title>");
|
||||
|
||||
// head and body with tag whitespace
|
||||
checkResults(
|
||||
html_scanner.scan("<head\n>\n<title>Hello</title>\n</head >\n\n<body>World</body\n\n>\n\n"),
|
||||
scanForTest("<head\n>\n<title>Hello</title>\n</head >\n\n<body>World</body\n\n>\n\n"),
|
||||
simpleBody('"World"'),
|
||||
"<title>Hello</title>");
|
||||
|
||||
// head, body, and template
|
||||
checkResults(
|
||||
html_scanner.scan("<head>\n<title>Hello</title>\n</head>\n\n<body>World</body>\n\n"+
|
||||
scanForTest("<head>\n<title>Hello</title>\n</head>\n\n<body>World</body>\n\n"+
|
||||
'<template name="favoritefood">\n pizza\n</template>\n'),
|
||||
simpleBody('"World"') + simpleTemplate('"favoritefood"', '"pizza"'),
|
||||
"<title>Hello</title>");
|
||||
|
||||
// one-line template
|
||||
checkResults(
|
||||
html_scanner.scan('<template name="favoritefood">pizza</template>'),
|
||||
scanForTest('<template name="favoritefood">pizza</template>'),
|
||||
simpleTemplate('"favoritefood"', '"pizza"'));
|
||||
|
||||
// template with other attributes
|
||||
checkResults(
|
||||
html_scanner.scan('<template foo="bar" name="favoritefood" baz="qux">'+
|
||||
scanForTest('<template foo="bar" name="favoritefood" baz="qux">'+
|
||||
'pizza</template>'),
|
||||
simpleTemplate('"favoritefood"', '"pizza"'));
|
||||
|
||||
// whitespace around '=' in attributes and at end of tag
|
||||
checkResults(
|
||||
html_scanner.scan('<template foo = "bar" name ="favoritefood" baz= "qux" >'+
|
||||
scanForTest('<template foo = "bar" name ="favoritefood" baz= "qux" >'+
|
||||
'pizza</template\n\n>'),
|
||||
simpleTemplate('"favoritefood"', '"pizza"'));
|
||||
|
||||
// whitespace around template name
|
||||
checkResults(
|
||||
html_scanner.scan('<template name=" favoritefood ">pizza</template>'),
|
||||
scanForTest('<template name=" favoritefood ">pizza</template>'),
|
||||
simpleTemplate('"favoritefood"', '"pizza"'));
|
||||
|
||||
// single quotes around template name
|
||||
checkResults(
|
||||
html_scanner.scan('<template name=\'the "cool" template\'>'+
|
||||
scanForTest('<template name=\'the "cool" template\'>'+
|
||||
'pizza</template>'),
|
||||
simpleTemplate('"the \\"cool\\" template"', '"pizza"'));
|
||||
|
||||
checkResults(html_scanner.scan('<body foo="bar">\n Hello\n</body>'),
|
||||
"\nMeteor.startup(function() { $('body').attr({\"foo\":\"bar\"}); });\n" + simpleBody('"Hello"'));
|
||||
checkResults(scanForTest('<body foo="bar">\n Hello\n</body>'), simpleBody('"Hello"'), "", {foo: "bar"});
|
||||
|
||||
// error cases; exact line numbers are not critical, these just reflect
|
||||
// the current implementation
|
||||
|
||||
// unclosed body (error mentions body)
|
||||
checkError(function() {
|
||||
return html_scanner.scan("\n\n<body>\n Hello\n</body");
|
||||
return scanForTest("\n\n<body>\n Hello\n</body");
|
||||
}, "body", 3);
|
||||
|
||||
// bad open tag
|
||||
checkError(function() {
|
||||
return html_scanner.scan("\n\n\n<bodyd>\n Hello\n</body>");
|
||||
}, "Expected <template>, <head>, or <body> tag in template file", 4);
|
||||
return scanForTest("\n\n\n<bodyd>\n Hello\n</body>");
|
||||
}, "Expected one of: <body>, <head>, <template>", 4);
|
||||
checkError(function() {
|
||||
return html_scanner.scan("\n\n\n\n<body foo=>\n Hello\n</body>");
|
||||
return scanForTest("\n\n\n\n<body foo=>\n Hello\n</body>");
|
||||
}, "error in tag", 5);
|
||||
|
||||
// unclosed tag
|
||||
checkError(function() {
|
||||
return html_scanner.scan("\n<body>Hello");
|
||||
return scanForTest("\n<body>Hello");
|
||||
}, "nclosed", 2);
|
||||
|
||||
// unnamed template
|
||||
checkError(function() {
|
||||
return html_scanner.scan(
|
||||
return scanForTest(
|
||||
"\n\n<template>Hi</template>\n\n<template>Hi</template>");
|
||||
}, "name", 3);
|
||||
|
||||
// helpful doctype message
|
||||
checkError(function() {
|
||||
return html_scanner.scan(
|
||||
return scanForTest(
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '+
|
||||
'"http://www.w3.org/TR/html4/strict.dtd">'+
|
||||
'\n\n<head>\n</head>');
|
||||
@@ -152,24 +166,24 @@ Tinytest.add("templating - html scanner", function (test) {
|
||||
|
||||
// lowercase basic doctype
|
||||
checkError(function() {
|
||||
return html_scanner.scan(
|
||||
return scanForTest(
|
||||
'<!doctype html>');
|
||||
}, "DOCTYPE", 1);
|
||||
|
||||
// attributes on head not supported
|
||||
checkError(function() {
|
||||
return html_scanner.scan('<head foo="bar">\n Hello\n</head>');
|
||||
return scanForTest('<head foo="bar">\n Hello\n</head>');
|
||||
}, "<head>", 1);
|
||||
|
||||
// can't mismatch quotes
|
||||
checkError(function() {
|
||||
return html_scanner.scan('<template name="foo\'>'+
|
||||
return scanForTest('<template name="foo\'>'+
|
||||
'pizza</template>');
|
||||
}, "error in tag", 1);
|
||||
|
||||
// unexpected <html> at top level
|
||||
checkError(function() {
|
||||
return html_scanner.scan('\n<html>\n</html>');
|
||||
}, "Expected <template>, <head>, or <body> tag in template file", 2);
|
||||
return scanForTest('\n<html>\n</html>');
|
||||
}, "Expected one of: <body>, <head>, <template>", 2);
|
||||
|
||||
});
|
||||
172
packages/templating-tools/html-scanner.js
Normal file
172
packages/templating-tools/html-scanner.js
Normal file
@@ -0,0 +1,172 @@
|
||||
TemplatingTools.scanHtmlForTags = function scanHtmlForTags(options) {
|
||||
const scan = new HtmlScan(options);
|
||||
return scan.getTags();
|
||||
};
|
||||
|
||||
/**
|
||||
* Scan an HTML file for top-level tags and extract their contents. Pass them to
|
||||
* a tag handler (an object with a handleTag method)
|
||||
*
|
||||
* This is a primitive, regex-based scanner. It scans
|
||||
* top-level tags, which are allowed to have attributes,
|
||||
* and ignores top-level HTML comments.
|
||||
*/
|
||||
class HtmlScan {
|
||||
/**
|
||||
* Initialize and run a scan of a single file
|
||||
* @param {String} sourceName The filename, used in errors only
|
||||
* @param {String} contents The contents of the file
|
||||
* @param {String[]} tagNames An array of tag names that are accepted at the
|
||||
* top level. If any other tag is encountered, an error is thrown.
|
||||
*/
|
||||
constructor({
|
||||
sourceName,
|
||||
contents,
|
||||
tagNames
|
||||
}) {
|
||||
this.sourceName = sourceName;
|
||||
this.contents = contents;
|
||||
this.tagNames = tagNames;
|
||||
|
||||
this.rest = contents;
|
||||
this.index = 0;
|
||||
|
||||
this.tags = [];
|
||||
|
||||
tagNameRegex = this.tagNames.join("|");
|
||||
const openTagRegex = new RegExp(`^((<(${tagNameRegex})\\b)|(<!--)|(<!DOCTYPE|{{!)|$)`, "i");
|
||||
|
||||
while (this.rest) {
|
||||
// skip whitespace first (for better line numbers)
|
||||
this.advance(this.rest.match(/^\s*/)[0].length);
|
||||
|
||||
const match = openTagRegex.exec(this.rest);
|
||||
|
||||
if (! match) {
|
||||
this.throwCompileError(`Expected one of: <${this.tagNames.join('>, <')}>`);
|
||||
}
|
||||
|
||||
const matchToken = match[1];
|
||||
const matchTokenTagName = match[3];
|
||||
const matchTokenComment = match[4];
|
||||
const matchTokenUnsupported = match[5];
|
||||
|
||||
const tagStartIndex = this.index;
|
||||
this.advance(match.index + match[0].length);
|
||||
|
||||
if (! matchToken) {
|
||||
break; // matched $ (end of file)
|
||||
}
|
||||
|
||||
if (matchTokenComment === '<!--') {
|
||||
// top-level HTML comment
|
||||
const commentEnd = /--\s*>/.exec(this.rest);
|
||||
if (! commentEnd)
|
||||
this.throwCompileError("unclosed HTML comment in template file");
|
||||
this.advance(commentEnd.index + commentEnd[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matchTokenUnsupported) {
|
||||
switch (matchTokenUnsupported.toLowerCase()) {
|
||||
case '<!doctype':
|
||||
this.throwCompileError(
|
||||
"Can't set DOCTYPE here. (Meteor sets <!DOCTYPE html> for you)");
|
||||
case '{{!':
|
||||
this.throwCompileError(
|
||||
"Can't use '{{! }}' outside a template. Use '<!-- -->'.");
|
||||
}
|
||||
|
||||
this.throwCompileError();
|
||||
}
|
||||
|
||||
// otherwise, a <tag>
|
||||
const tagName = matchTokenTagName.toLowerCase();
|
||||
const tagAttribs = {}; // bare name -> value dict
|
||||
const tagPartRegex = /^\s*((([a-zA-Z0-9:_-]+)\s*=\s*(["'])(.*?)\4)|(>))/;
|
||||
|
||||
// read attributes
|
||||
let attr;
|
||||
while ((attr = tagPartRegex.exec(this.rest))) {
|
||||
const attrToken = attr[1];
|
||||
const attrKey = attr[3];
|
||||
let attrValue = attr[5];
|
||||
this.advance(attr.index + attr[0].length);
|
||||
|
||||
if (attrToken === '>') {
|
||||
break;
|
||||
}
|
||||
|
||||
// XXX we don't HTML unescape the attribute value
|
||||
// (e.g. to allow "abcd"efg") or protect against
|
||||
// collisions with methods of tagAttribs (e.g. for
|
||||
// a property named toString)
|
||||
attrValue = attrValue.match(/^\s*([\s\S]*?)\s*$/)[1]; // trim
|
||||
tagAttribs[attrKey] = attrValue;
|
||||
}
|
||||
|
||||
if (! attr) { // didn't end on '>'
|
||||
this.throwCompileError("Parse error in tag");
|
||||
}
|
||||
|
||||
// find </tag>
|
||||
const end = (new RegExp('</'+tagName+'\\s*>', 'i')).exec(this.rest);
|
||||
if (! end) {
|
||||
this.throwCompileError("unclosed <"+tagName+">");
|
||||
}
|
||||
|
||||
const tagContents = this.rest.slice(0, end.index);
|
||||
const contentsStartIndex = this.index;
|
||||
|
||||
// trim the tag contents.
|
||||
// this is a courtesy and is also relied on by some unit tests.
|
||||
var m = tagContents.match(/^([ \t\r\n]*)([\s\S]*?)[ \t\r\n]*$/);
|
||||
const trimmedContentsStartIndex = contentsStartIndex + m[1].length;
|
||||
const trimmedTagContents = m[2];
|
||||
|
||||
const tag = {
|
||||
tagName: tagName,
|
||||
attribs: tagAttribs,
|
||||
contents: trimmedTagContents,
|
||||
contentsStartIndex: trimmedContentsStartIndex,
|
||||
tagStartIndex: tagStartIndex,
|
||||
fileContents: this.contents,
|
||||
sourceName: this.sourceName
|
||||
};
|
||||
|
||||
// save the tag
|
||||
this.tags.push(tag);
|
||||
|
||||
// advance afterwards, so that line numbers in errors are correct
|
||||
this.advance(end.index + end[0].length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the parser
|
||||
* @param {Number} amount The amount of characters to advance
|
||||
*/
|
||||
advance(amount) {
|
||||
this.rest = this.rest.substring(amount);
|
||||
this.index += amount;
|
||||
}
|
||||
|
||||
throwCompileError(msg, overrideIndex) {
|
||||
const finalIndex = (typeof overrideIndex === 'number' ? overrideIndex : this.index);
|
||||
|
||||
const err = new TemplatingTools.CompileError();
|
||||
err.message = msg || "bad formatting in template file";
|
||||
err.file = this.sourceName;
|
||||
err.line = this.contents.substring(0, finalIndex).split('\n').length;
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
throwBodyAttrsError(msg) {
|
||||
this.parseError(msg);
|
||||
}
|
||||
|
||||
getTags() {
|
||||
return this.tags;
|
||||
}
|
||||
}
|
||||
47
packages/templating-tools/package.js
Normal file
47
packages/templating-tools/package.js
Normal file
@@ -0,0 +1,47 @@
|
||||
Package.describe({
|
||||
name: 'templating-tools',
|
||||
version: '0.0.1',
|
||||
// Brief, one-line summary of the package.
|
||||
summary: '',
|
||||
// URL to the Git repository containing the source code for this package.
|
||||
git: '',
|
||||
// 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',
|
||||
'ecmascript',
|
||||
'spacebars-compiler',
|
||||
|
||||
// minifiers is a weak dependency of spacebars-compiler; adding it here
|
||||
// ensures that the output is minified. (Having it as a weak dependency means
|
||||
// that we don't ship uglify etc with built apps just because
|
||||
// boilerplate-generator uses spacebars-compiler.)
|
||||
// XXX maybe uglify should be applied by this plugin instead of via magic
|
||||
// weak dependency.
|
||||
'minifiers'
|
||||
]);
|
||||
|
||||
api.addFiles([
|
||||
'templating-tools.js',
|
||||
'html-scanner.js',
|
||||
'compile-tags-with-spacebars.js',
|
||||
'throw-compile-error.js',
|
||||
'code-generation.js'
|
||||
]);
|
||||
|
||||
api.export('TemplatingTools');
|
||||
});
|
||||
|
||||
Package.onTest(function(api) {
|
||||
api.use([
|
||||
'tinytest',
|
||||
'templating-tools',
|
||||
'ecmascript'
|
||||
]);
|
||||
|
||||
api.addFiles('html-scanner-tests.js', 'server');
|
||||
});
|
||||
4
packages/templating-tools/templating-tools.js
Normal file
4
packages/templating-tools/templating-tools.js
Normal file
@@ -0,0 +1,4 @@
|
||||
TemplatingTools = {
|
||||
// This type of error should be thrown during compilation
|
||||
CompileError: class CompileError {}
|
||||
};
|
||||
11
packages/templating-tools/throw-compile-error.js
Normal file
11
packages/templating-tools/throw-compile-error.js
Normal file
@@ -0,0 +1,11 @@
|
||||
TemplatingTools.throwCompileError =
|
||||
function throwCompileError(tag, message, overrideIndex) {
|
||||
const finalIndex = (typeof overrideIndex === 'number' ?
|
||||
overrideIndex : tag.tagStartIndex);
|
||||
|
||||
const err = new TemplatingTools.CompileError();
|
||||
err.message = message || "bad formatting in template file";
|
||||
err.file = tag.sourceName;
|
||||
err.line = tag.fileContents.substring(0, finalIndex).split('\n').length;
|
||||
throw err;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# templating
|
||||
|
||||
Define Blaze templates in `.html` files. Most Meteor apps use this package.
|
||||
Compiles Blaze templates defined in `.html` files. Also automatically includes Blaze on the client.
|
||||
|
||||
This build plugin parses all of the HTML files in your app and looks for three top-level tags:
|
||||
|
||||
|
||||
@@ -17,13 +17,11 @@ Package.registerBuildPlugin({
|
||||
// XXX maybe uglify should be applied by this plugin instead of via magic
|
||||
// weak dependency.
|
||||
use: [
|
||||
'minifiers',
|
||||
'spacebars-compiler',
|
||||
'caching-compiler',
|
||||
'ecmascript'
|
||||
'caching-html-compiler',
|
||||
'ecmascript',
|
||||
'templating-tools'
|
||||
],
|
||||
sources: [
|
||||
'plugin/html_scanner.js',
|
||||
'plugin/compile-templates.js'
|
||||
]
|
||||
});
|
||||
@@ -48,27 +46,3 @@ Package.onUse(function (api) {
|
||||
|
||||
api.addFiles(['dynamic.html', 'dynamic.js'], 'client');
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('tinytest');
|
||||
api.use('htmljs');
|
||||
api.use('templating');
|
||||
api.use('underscore');
|
||||
api.use([
|
||||
'test-helpers',
|
||||
'session',
|
||||
'tracker',
|
||||
'minimongo',
|
||||
'reactive-var',
|
||||
'spacebars'
|
||||
], 'client');
|
||||
api.use('spacebars-compiler');
|
||||
api.use('minifiers'); // ensure compiler output is beautified
|
||||
|
||||
api.addFiles([
|
||||
'plugin/html_scanner.js',
|
||||
'scanner_tests.js'
|
||||
], 'server');
|
||||
|
||||
api.addFiles(["dynamic_tests.html", "dynamic_tests.js"], "client");
|
||||
});
|
||||
|
||||
@@ -1,104 +1,9 @@
|
||||
const path = Npm.require('path');
|
||||
|
||||
Plugin.registerCompiler({
|
||||
extensions: ['html'],
|
||||
archMatching: 'web',
|
||||
isTemplate: true
|
||||
}, () => new TemplateCompiler());
|
||||
|
||||
// The CompileResult type for this CachingCompiler is the return value of
|
||||
// htmlScanner.scan: a {js, head, body, bodyAttrs} object.
|
||||
class TemplateCompiler extends CachingCompiler {
|
||||
constructor() {
|
||||
super({
|
||||
compilerName: 'templating',
|
||||
defaultCacheSize: 1024*1024*10,
|
||||
});
|
||||
this._bodyAttrInfo = null;
|
||||
}
|
||||
|
||||
compileResultSize(compileResult) {
|
||||
function lengthOrZero(field) {
|
||||
return field ? field.length : 0;
|
||||
}
|
||||
return lengthOrZero(compileResult.head) + lengthOrZero(compileResult.body) +
|
||||
lengthOrZero(compileResult.js);
|
||||
}
|
||||
|
||||
processFilesForTarget(inputFiles) {
|
||||
this._bodyAttrInfo = {};
|
||||
super.processFilesForTarget(inputFiles);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
compileOneFile(inputFile) {
|
||||
const contents = inputFile.getContentsAsString();
|
||||
const path = inputFile.getPathInPackage();
|
||||
try {
|
||||
return html_scanner.scan(contents, path);
|
||||
} catch (e) {
|
||||
if ((e instanceof html_scanner.ParseError) ||
|
||||
(e instanceof html_scanner.BodyAttrsError)) {
|
||||
inputFile.error({
|
||||
message: e.message,
|
||||
line: e.line
|
||||
});
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addCompileResult(inputFile, compileResult) {
|
||||
if (compileResult.head) {
|
||||
inputFile.addHtml({ section: "head", data: compileResult.head });
|
||||
}
|
||||
|
||||
if (compileResult.body) {
|
||||
inputFile.addHtml({ section: "body", data: compileResult.body });
|
||||
}
|
||||
|
||||
if (compileResult.js) {
|
||||
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: compileResult.js
|
||||
});
|
||||
}
|
||||
|
||||
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 <body> tags in two different template
|
||||
// files
|
||||
inputFile.error({
|
||||
message:
|
||||
`<body> 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};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, () => new CachingHtmlCompiler(
|
||||
"templating",
|
||||
TemplatingTools.scanHtmlForTags,
|
||||
TemplatingTools.compileTagsWithSpacebars
|
||||
));
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
html_scanner = {
|
||||
// Scan a template file for <head>, <body>, and <template>
|
||||
// tags and extract their contents.
|
||||
//
|
||||
// This is a primitive, regex-based scanner. It scans
|
||||
// top-level tags, which are allowed to have attributes,
|
||||
// and ignores top-level HTML comments.
|
||||
|
||||
// Has fields 'message', 'line', 'file'
|
||||
ParseError: function () {},
|
||||
BodyAttrsError: function () {},
|
||||
|
||||
// Note: source_name is only used for errors (so it's not part of the cache
|
||||
// key in compile-templates.js).
|
||||
scan: function (contents, source_name) {
|
||||
var rest = contents;
|
||||
var index = 0;
|
||||
|
||||
var advance = function(amount) {
|
||||
rest = rest.substring(amount);
|
||||
index += amount;
|
||||
};
|
||||
|
||||
var throwSpecialError = function (msg, errorClass, overrideIndex) {
|
||||
var ret = new errorClass;
|
||||
ret.message = msg;
|
||||
ret.file = source_name;
|
||||
var theIndex = (typeof overrideIndex === 'number' ? overrideIndex : index);
|
||||
ret.line = contents.substring(0, theIndex).split('\n').length;
|
||||
throw ret;
|
||||
};
|
||||
var throwParseError = function (msg, overrideIndex) {
|
||||
throwSpecialError(
|
||||
msg || "bad formatting in template file",
|
||||
html_scanner.ParseError,
|
||||
overrideIndex);
|
||||
};
|
||||
var throwBodyAttrsError = function (msg) {
|
||||
throwSpecialError(msg, html_scanner.BodyAttrsError);
|
||||
};
|
||||
|
||||
var results = html_scanner._initResults();
|
||||
var rOpenTag = /^((<(template|head|body)\b)|(<!--)|(<!DOCTYPE|{{!)|$)/i;
|
||||
|
||||
while (rest) {
|
||||
// skip whitespace first (for better line numbers)
|
||||
advance(rest.match(/^\s*/)[0].length);
|
||||
|
||||
var match = rOpenTag.exec(rest);
|
||||
if (! match)
|
||||
throwParseError("Expected <template>, <head>, or <body> tag" +
|
||||
" in template file");
|
||||
|
||||
var matchToken = match[1];
|
||||
var matchTokenTagName = match[3];
|
||||
var matchTokenComment = match[4];
|
||||
var matchTokenUnsupported = match[5];
|
||||
|
||||
var tagStartIndex = index;
|
||||
advance(match.index + match[0].length);
|
||||
|
||||
if (! matchToken)
|
||||
break; // matched $ (end of file)
|
||||
if (matchTokenComment === '<!--') {
|
||||
// top-level HTML comment
|
||||
var commentEnd = /--\s*>/.exec(rest);
|
||||
if (! commentEnd)
|
||||
throwParseError("unclosed HTML comment in template file");
|
||||
advance(commentEnd.index + commentEnd[0].length);
|
||||
continue;
|
||||
}
|
||||
if (matchTokenUnsupported) {
|
||||
switch (matchTokenUnsupported.toLowerCase()) {
|
||||
case '<!doctype':
|
||||
throwParseError(
|
||||
"Can't set DOCTYPE here. (Meteor sets <!DOCTYPE html> for you)");
|
||||
case '{{!':
|
||||
throwParseError(
|
||||
"Can't use '{{! }}' outside a template. Use '<!-- -->'.");
|
||||
}
|
||||
throwParseError();
|
||||
}
|
||||
|
||||
// otherwise, a <tag>
|
||||
var tagName = matchTokenTagName.toLowerCase();
|
||||
var tagAttribs = {}; // bare name -> value dict
|
||||
var rTagPart = /^\s*((([a-zA-Z0-9:_-]+)\s*=\s*(["'])(.*?)\4)|(>))/;
|
||||
var attr;
|
||||
// read attributes
|
||||
while ((attr = rTagPart.exec(rest))) {
|
||||
var attrToken = attr[1];
|
||||
var attrKey = attr[3];
|
||||
var attrValue = attr[5];
|
||||
advance(attr.index + attr[0].length);
|
||||
if (attrToken === '>')
|
||||
break;
|
||||
// XXX we don't HTML unescape the attribute value
|
||||
// (e.g. to allow "abcd"efg") or protect against
|
||||
// collisions with methods of tagAttribs (e.g. for
|
||||
// a property named toString)
|
||||
attrValue = attrValue.match(/^\s*([\s\S]*?)\s*$/)[1]; // trim
|
||||
tagAttribs[attrKey] = attrValue;
|
||||
}
|
||||
if (! attr) // didn't end on '>'
|
||||
throwParseError("Parse error in tag");
|
||||
// find </tag>
|
||||
var end = (new RegExp('</'+tagName+'\\s*>', 'i')).exec(rest);
|
||||
if (! end)
|
||||
throwParseError("unclosed <"+tagName+">");
|
||||
var tagContents = rest.slice(0, end.index);
|
||||
var contentsStartIndex = index;
|
||||
|
||||
if (tagName === 'body') {
|
||||
this._addBodyAttrs(results, tagAttribs, throwBodyAttrsError);
|
||||
}
|
||||
|
||||
// act on the tag
|
||||
html_scanner._handleTag(results, tagName, tagAttribs, tagContents,
|
||||
throwParseError, contentsStartIndex,
|
||||
tagStartIndex);
|
||||
|
||||
// advance afterwards, so that line numbers in errors are correct
|
||||
advance(end.index + end[0].length);
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
_initResults: function() {
|
||||
var results = {};
|
||||
results.head = '';
|
||||
results.body = '';
|
||||
results.js = '';
|
||||
results.bodyAttrs = {};
|
||||
return results;
|
||||
},
|
||||
|
||||
_addBodyAttrs: function (results, attrs, throwBodyAttrsError) {
|
||||
Object.keys(attrs).forEach(function (attr) {
|
||||
var val = attrs[attr];
|
||||
|
||||
if (results.bodyAttrs.hasOwnProperty(attr) && results.bodyAttrs[attr] !== val) {
|
||||
throwBodyAttrsError(
|
||||
"<body> declarations have conflicting values for the '" + attr + "' attribute.");
|
||||
}
|
||||
|
||||
results.bodyAttrs[attr] = val;
|
||||
});
|
||||
},
|
||||
|
||||
_handleTag: function (results, tag, attribs, contents, throwParseError,
|
||||
contentsStartIndex, tagStartIndex) {
|
||||
|
||||
// trim the tag contents.
|
||||
// this is a courtesy and is also relied on by some unit tests.
|
||||
var m = contents.match(/^([ \t\r\n]*)([\s\S]*?)[ \t\r\n]*$/);
|
||||
contentsStartIndex += m[1].length;
|
||||
contents = m[2];
|
||||
|
||||
// do we have 1 or more attribs?
|
||||
var hasAttribs = false;
|
||||
for(var k in attribs) {
|
||||
if (attribs.hasOwnProperty(k)) {
|
||||
hasAttribs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tag === "head") {
|
||||
if (hasAttribs)
|
||||
throwParseError("Attributes on <head> not supported");
|
||||
results.head += contents;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// <body> or <template>
|
||||
|
||||
try {
|
||||
if (tag === "template") {
|
||||
var name = attribs.name;
|
||||
if (! name)
|
||||
throwParseError("Template has no 'name' attribute");
|
||||
|
||||
if (SpacebarsCompiler.isReservedName(name))
|
||||
throwParseError("Template can't be named \"" + name + "\"");
|
||||
|
||||
var renderFuncCode = SpacebarsCompiler.compile(
|
||||
contents, {
|
||||
isTemplate: true,
|
||||
sourceName: 'Template "' + name + '"'
|
||||
});
|
||||
|
||||
var nameLiteral = JSON.stringify(name);
|
||||
var templateDotNameLiteral = JSON.stringify("Template." + name);
|
||||
|
||||
results.js += "\nTemplate.__checkName(" + nameLiteral + ");\n" +
|
||||
"Template[" + nameLiteral + "] = new Template(" +
|
||||
templateDotNameLiteral + ", " + renderFuncCode + ");\n";
|
||||
} else {
|
||||
// <body>
|
||||
if (hasAttribs) {
|
||||
results.js += "\nMeteor.startup(function() { $('body').attr(" + JSON.stringify(attribs) + "); });\n";
|
||||
}
|
||||
|
||||
var renderFuncCode = SpacebarsCompiler.compile(
|
||||
contents, {
|
||||
isBody: true,
|
||||
sourceName: "<body>"
|
||||
});
|
||||
|
||||
// We may be one of many `<body>` tags.
|
||||
results.js += "\nTemplate.body.addContent(" + renderFuncCode + ");\nMeteor.startup(Template.body.renderToDocument);\n";
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.scanner) {
|
||||
// The error came from Spacebars
|
||||
throwParseError(e.message, contentsStartIndex + e.offset);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
1
tools/tests/apps/compiler-plugin-static-html-error/.meteor/.gitignore
vendored
Normal file
1
tools/tests/apps/compiler-plugin-static-html-error/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
@@ -0,0 +1,7 @@
|
||||
# This file contains a token that is unique to your project.
|
||||
# Check it into your repository along with the rest of this directory.
|
||||
# It can be used for purposes such as:
|
||||
# - ensuring you don't accidentally deploy one app on top of another
|
||||
# - providing package authors with aggregated statistics
|
||||
|
||||
o2vi6w1m82bge9k2mhb
|
||||
@@ -0,0 +1,16 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
meteor
|
||||
webapp
|
||||
logging
|
||||
tracker
|
||||
ddp
|
||||
mongo
|
||||
check
|
||||
jquery
|
||||
reload
|
||||
autoupdate
|
||||
|
||||
static-html
|
||||
@@ -0,0 +1,2 @@
|
||||
browser
|
||||
server
|
||||
@@ -0,0 +1 @@
|
||||
none
|
||||
@@ -0,0 +1,53 @@
|
||||
autoupdate@1.2.2-plugins.0
|
||||
babel-compiler@5.8.3-plugins.0_5
|
||||
babel-runtime@0.1.2
|
||||
base64@1.0.4-plugins.0
|
||||
binary-heap@1.0.4-plugins.0
|
||||
blaze@2.1.3-plugins.0
|
||||
blaze-tools@1.0.4-plugins.0
|
||||
boilerplate-generator@1.0.4-plugins.0
|
||||
caching-compiler@1.0.0-plugins.1
|
||||
caching-html-compiler@1.0.0
|
||||
callback-hook@1.0.4-plugins.0
|
||||
check@1.0.6-plugins.0
|
||||
ddp@1.2.0-plugins.0
|
||||
ddp-client@1.2.0-plugins.0
|
||||
ddp-common@1.2.0-plugins.0
|
||||
ddp-server@1.2.0-plugins.1
|
||||
deps@1.0.8-plugins.0
|
||||
diff-sequence@1.0.0-plugins.0
|
||||
ecmascript@0.1.3-plugins.1
|
||||
ejson@1.0.7-plugins.0
|
||||
geojson-utils@1.0.4-plugins.0
|
||||
html-tools@1.0.5-plugins.0
|
||||
htmljs@1.0.5-plugins.0
|
||||
http@1.1.1-plugins.0
|
||||
id-map@1.0.4-plugins.0
|
||||
jquery@1.11.3-plugins.0_3
|
||||
json@1.0.4-plugins.0
|
||||
logging@1.0.8-plugins.0
|
||||
meteor@1.1.7-plugins.1
|
||||
minifiers@1.1.6-plugins.0
|
||||
minimongo@1.0.9-plugins.0
|
||||
mongo@1.1.1-plugins.0
|
||||
mongo-id@1.0.0-plugins.0
|
||||
npm-mongo@1.4.32-plugins.0_1
|
||||
observe-sequence@1.0.7-plugins.0
|
||||
ordered-dict@1.0.4-plugins.0
|
||||
promise@0.4.2-plugins.0
|
||||
random@1.0.4-plugins.0
|
||||
reactive-var@1.0.6-plugins.0
|
||||
reload@1.1.4-plugins.0
|
||||
retry@1.0.4-plugins.0
|
||||
routepolicy@1.0.6-plugins.0
|
||||
spacebars@1.0.7-plugins.0
|
||||
spacebars-compiler@1.0.7-plugins.1
|
||||
static-html@0.0.1
|
||||
templating@1.1.2-plugins.1
|
||||
templating-tools@0.0.1
|
||||
tracker@1.0.8-plugins.0
|
||||
ui@1.0.7-plugins.0
|
||||
underscore@1.0.4-plugins.0
|
||||
url@1.0.5-plugins.0
|
||||
webapp@1.2.1-plugins.0
|
||||
webapp-hashing@1.0.4-plugins.0
|
||||
@@ -0,0 +1,7 @@
|
||||
<head attr="not allowed">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>I have a body, yet no Blaze!</div>
|
||||
</body>
|
||||
1
tools/tests/apps/compiler-plugin-static-html/.meteor/.gitignore
vendored
Normal file
1
tools/tests/apps/compiler-plugin-static-html/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
7
tools/tests/apps/compiler-plugin-static-html/.meteor/.id
Normal file
7
tools/tests/apps/compiler-plugin-static-html/.meteor/.id
Normal file
@@ -0,0 +1,7 @@
|
||||
# This file contains a token that is unique to your project.
|
||||
# Check it into your repository along with the rest of this directory.
|
||||
# It can be used for purposes such as:
|
||||
# - ensuring you don't accidentally deploy one app on top of another
|
||||
# - providing package authors with aggregated statistics
|
||||
|
||||
o2vi6w1m82bge9k2mhb
|
||||
@@ -0,0 +1,16 @@
|
||||
# Meteor packages used by this project, one per line.
|
||||
#
|
||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||
# but you can also edit it by hand.
|
||||
meteor
|
||||
webapp
|
||||
logging
|
||||
tracker
|
||||
ddp
|
||||
mongo
|
||||
check
|
||||
jquery
|
||||
reload
|
||||
autoupdate
|
||||
|
||||
static-html
|
||||
@@ -0,0 +1,2 @@
|
||||
browser
|
||||
server
|
||||
@@ -0,0 +1 @@
|
||||
none
|
||||
@@ -0,0 +1,53 @@
|
||||
autoupdate@1.2.2-plugins.0
|
||||
babel-compiler@5.8.3-plugins.0_5
|
||||
babel-runtime@0.1.2
|
||||
base64@1.0.4-plugins.0
|
||||
binary-heap@1.0.4-plugins.0
|
||||
blaze@2.1.3-plugins.0
|
||||
blaze-tools@1.0.4-plugins.0
|
||||
boilerplate-generator@1.0.4-plugins.0
|
||||
caching-compiler@1.0.0-plugins.1
|
||||
caching-html-compiler@1.0.0
|
||||
callback-hook@1.0.4-plugins.0
|
||||
check@1.0.6-plugins.0
|
||||
ddp@1.2.0-plugins.0
|
||||
ddp-client@1.2.0-plugins.0
|
||||
ddp-common@1.2.0-plugins.0
|
||||
ddp-server@1.2.0-plugins.1
|
||||
deps@1.0.8-plugins.0
|
||||
diff-sequence@1.0.0-plugins.0
|
||||
ecmascript@0.1.3-plugins.1
|
||||
ejson@1.0.7-plugins.0
|
||||
geojson-utils@1.0.4-plugins.0
|
||||
html-tools@1.0.5-plugins.0
|
||||
htmljs@1.0.5-plugins.0
|
||||
http@1.1.1-plugins.0
|
||||
id-map@1.0.4-plugins.0
|
||||
jquery@1.11.3-plugins.0_3
|
||||
json@1.0.4-plugins.0
|
||||
logging@1.0.8-plugins.0
|
||||
meteor@1.1.7-plugins.1
|
||||
minifiers@1.1.6-plugins.0
|
||||
minimongo@1.0.9-plugins.0
|
||||
mongo@1.1.1-plugins.0
|
||||
mongo-id@1.0.0-plugins.0
|
||||
npm-mongo@1.4.32-plugins.0_1
|
||||
observe-sequence@1.0.7-plugins.0
|
||||
ordered-dict@1.0.4-plugins.0
|
||||
promise@0.4.2-plugins.0
|
||||
random@1.0.4-plugins.0
|
||||
reactive-var@1.0.6-plugins.0
|
||||
reload@1.1.4-plugins.0
|
||||
retry@1.0.4-plugins.0
|
||||
routepolicy@1.0.6-plugins.0
|
||||
spacebars@1.0.7-plugins.0
|
||||
spacebars-compiler@1.0.7-plugins.1
|
||||
static-html@0.0.1
|
||||
templating@1.1.2-plugins.1
|
||||
templating-tools@0.0.1
|
||||
tracker@1.0.8-plugins.0
|
||||
ui@1.0.7-plugins.0
|
||||
underscore@1.0.4-plugins.0
|
||||
url@1.0.5-plugins.0
|
||||
webapp@1.2.1-plugins.0
|
||||
webapp-hashing@1.0.4-plugins.0
|
||||
7
tools/tests/apps/compiler-plugin-static-html/static.html
Normal file
7
tools/tests/apps/compiler-plugin-static-html/static.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>I have a body, yet no Blaze!</div>
|
||||
</body>
|
||||
60
tools/tests/static-html.js
Normal file
60
tools/tests/static-html.js
Normal file
@@ -0,0 +1,60 @@
|
||||
var _ = require('underscore');
|
||||
var selftest = require('../selftest.js');
|
||||
var files = require('../files.js');
|
||||
import { getUrl } from '../http-helpers.js';
|
||||
import { sleepMs } from '../utils.js';
|
||||
|
||||
var Sandbox = selftest.Sandbox;
|
||||
|
||||
var MONGO_LISTENING =
|
||||
{ stdout: " [initandlisten] waiting for connections on port" };
|
||||
|
||||
function startRun(sandbox) {
|
||||
var run = sandbox.run();
|
||||
run.waitSecs(90); // Running from checkout can take a _long_ time
|
||||
run.match("myapp");
|
||||
run.match("proxy");
|
||||
run.tellMongo(MONGO_LISTENING);
|
||||
run.match("MongoDB");
|
||||
return run;
|
||||
};
|
||||
|
||||
// Test that the static-html package works. It's hard to do this from a unit
|
||||
// test.
|
||||
selftest.define("static-html - add static content to head and body", () => {
|
||||
const s = new Sandbox({ fakeMongo: true });
|
||||
|
||||
s.createApp('myapp', 'compiler-plugin-static-html');
|
||||
s.cd('myapp');
|
||||
|
||||
const run = startRun(s);
|
||||
|
||||
// Test that static content is present in HTML response.
|
||||
const html = getUrl('http://localhost:3000/');
|
||||
selftest.expectTrue(
|
||||
html.indexOf(
|
||||
`<meta name="viewport" content="width=device-width, initial-scale=1">`
|
||||
) !== -1
|
||||
);
|
||||
|
||||
selftest.expectTrue(
|
||||
html.indexOf(
|
||||
`<div>I have a body, yet no Blaze!</div>`
|
||||
) !== -1
|
||||
);
|
||||
|
||||
run.stop();
|
||||
});
|
||||
|
||||
// Test that the static-html package throws the right error
|
||||
selftest.define("static-html - throws error", () => {
|
||||
const s = new Sandbox({ fakeMongo: true });
|
||||
|
||||
s.createApp('myapp', 'compiler-plugin-static-html-error');
|
||||
s.cd('myapp');
|
||||
|
||||
const run = startRun(s);
|
||||
run.match("Attributes on <head> not supported");
|
||||
|
||||
run.stop();
|
||||
});
|
||||
Reference in New Issue
Block a user