Merge pull request #68 from patrick-steele-idem/parent-child

Fixes #61 Simplify parent/child relationships
This commit is contained in:
ramahadevan 2015-05-01 17:32:46 -07:00
commit d4e53cc0df
50 changed files with 2333 additions and 1036 deletions

View File

@ -3,12 +3,188 @@ Changelog
# 2.x
## 2.0.x
## 2.4.x
### 2.4.0
- Added support for short-hand tags and attributes
Old `marko-taglib.json`:
```json
{
"tags": {
"my-hello": {
"renderer": "./hello-renderer",
"attributes": {
"name": "string"
}
}
}
}
```
Short-hand `marko-taglib.json`:
```json
{
"<my-hello>": {
"renderer": "./hello-renderer",
"@name": "string"
}
}
```
- Fixes #61 Simplify parent/child relationships
Marko now supports custom tags in the following format: `<parent_tag.nested_tag>`
Example usage:
```html
<ui-tabs orientation="horizontal">
<ui-tabs.tab title="Home">
Content for Home
</ui-tabs.tab>
<ui-tabs.tab title="Profile">
Content for Profile
</ui-tabs.tab>
<ui-tabs.tab title="Messages">
Content for Messages
</ui-tabs.tab>
</ui-tabs>
```
___ui-tabs/marko-tag.json___
```json
{
"@orientation": "string",
"@tabs <tab>[]": {
"@title": "string"
}
}
```
___ui-tabs/renderer.js___
```javascript
var template = require('marko').load(require.resolve('./template.marko'));
exports.renderer = function(input, out) {
var tabs = input.tabs;
// Tabs will be in the following form:
// [
// {
// title: 'Home',
// renderBody: function(out) { ... }
// },
// {
// title: 'Profile',
// renderBody: function(out) { ... }
// },
// {
// title: 'Messages',
// renderBody: function(out) { ... }
// }
// ]
console.log(tabs.length); // Output: 3
template.render({
tabs: tabs
}, out);
};
```
___ui-tabs/template.marko___
```html
<div class="tabs">
<ul class="nav nav-tabs">
<li class="tab" for="tab in data.tabs">
<a href="#${tab.title}">
${tab.title}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane" for="tab in data.tabs">
<invoke function="tab.renderBody(out)"/>
</div>
</div>
</div>
```
## 2.3.x
### 2.3.2
Fixes #66 - Allow circular dependencies when loading templates
### 2.3.1
- Testing framework changes
- Fixes #65 - Generated variable name is an empty string in some cases
### 2.3.0
- Fixes #53 Merge c-input with attr props
## 2.2.x
### 2.2.2
Fixes #60 Don't replace special operators for body functions
### 2.2.1
- Fixes #58 Added support for MARKO_CLEAN env variable (force recompile of all loaded templates). Example usage:
```bash
MARKO_CLEAN=true node run.js
```
- Code formatting: add spaces in var code
### 2.2.0
- Fixes #51 Allow body content to be mapped to a String input property
- Fixes #52 Remove JavaScript comments from JSON taglib files before parsing
## 2.1.x
### 2.1.6
- Fixes #50 Initialize the loader after the runtime is fully initialized
### 2.1.5
- Fixes #50 Ensure that all instances of marko have hot-reload and browser-refresh enabled
### 2.1.4
- Allowing complex var names (i.e. LHS) for the `<assign>` tag.
### 2.1.3
- Minor change: Slight improvement to code to resolve tag handler
### 2.1.2
- Minor change: Improve how renderer is resolved
### 2.1.1
- Fixes #48 name in marko-tag.json should override default name given during discovery
### 2.1.0
- Fixes #47 - Added support for "taglib-imports"
## 2.0.x
### 2.0.12
- Fixes #31 - Add support for providing prefix when scanning for tags

376
README.md
View File

@ -6,13 +6,15 @@ Marko is an extensible, streaming, asynchronous, [high performance](https://gith
Marko is a perfect match for Node.js since it supports writing directly to an output stream so that HTML can be sent over the wire sooner. Marko automatically flushes around asynchronous fragments so that the HTML is delivered in the optimized number of chunks. Because Marko is an asynchronous templating language, additional data can be asynchronously fetched even after rendering has begun. These characteristics make Marko an excellent choice for creating high performance websites.
For building rich UI components with client-side behavior please check out the companion [marko-widgets](https://github.com/raptorjs/marko-widgets) taglib.
__[Try Marko Online!](http://raptorjs.org/marko/try-online/)__
![Marko Syntax](syntax.png)
Syntax highlighting available for [Atom](https://atom.io/) by installing the [language-marko](https://atom.io/packages/language-marko) package.
![eBay Open Source](https://raw.githubusercontent.com/raptorjs/optimizer/master/images/ebay.png)
![eBay Open Source](https://raw.githubusercontent.com/lasso-js/lasso/master/images/ebay.png)
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
@ -24,56 +26,62 @@ Syntax highlighting available for [Atom](https://atom.io/) by installing the [la
- [Another Templating Language?](#another-templating-language)
- [Design Philosophy](#design-philosophy)
- [Usage](#usage)
- [Template Rendering](#template-rendering)
- [Callback API](#callback-api)
- [Streaming API](#streaming-api)
- [Synchronous API](#synchronous-api)
- [Asynchronous Rendering API](#asynchronous-rendering-api)
- [Browser-side Rendering](#browser-side-rendering)
- [Using the RaptorJS Optimizer](#using-the-raptorjs-optimizer)
- [Using Browserify](#using-browserify)
- [Template Compilation](#template-compilation)
- [Sample Compiled Template](#sample-compiled-template)
- [Template Rendering](#template-rendering)
- [Callback API](#callback-api)
- [Streaming API](#streaming-api)
- [Synchronous API](#synchronous-api)
- [Asynchronous Rendering API](#asynchronous-rendering-api)
- [Browser-side Rendering](#browser-side-rendering)
- [Using Lasso.js](#using-lassojs)
- [Using Browserify](#using-browserify)
- [Template Compilation](#template-compilation)
- [Sample Compiled Template](#sample-compiled-template)
- [Language Guide](#language-guide)
- [Template Directives Overview](#template-directives-overview)
- [Text Replacement](#text-replacement)
- [Expressions](#expressions)
- [Includes](#includes)
- [Variables](#variables)
- [Conditionals](#conditionals)
- [if...else-if...else](#ifelse-ifelse)
- [Shorthand Conditionals](#shorthand-conditionals)
- [Conditional Attributes](#conditional-attributes)
- [Looping](#looping)
- [for](#for)
- [Loop Status Variable](#loop-status-variable)
- [Loop Separator](#loop-separator)
- [Range Looping](#range-looping)
- [Property Looping](#property-looping)
- [Custom Iterator](#custom-iterator)
- [Macros](#macros)
- [def](#def)
- [invoke](#invoke)
- [Structure Manipulation](#structure-manipulation)
- [attrs](#attrs)
- [body-only-if](#body-only-if)
- [Comments](#comments)
- [Whitespace](#whitespace)
- [Helpers](#helpers)
[Global Properties](#global-properties)
- [Custom Tags and Attributes](#custom-tags-and-attributes)
- [Async Taglib](#async-taglib)
- [Layout Taglib](#layout-taglib)
- [Template Directives Overview](#template-directives-overview)
- [Text Replacement](#text-replacement)
- [Expressions](#expressions)
- [Includes](#includes)
- [Variables](#variables)
- [Conditionals](#conditionals)
- [if...else-if...else](#ifelse-ifelse)
- [Shorthand Conditionals](#shorthand-conditionals)
- [Conditional Attributes](#conditional-attributes)
- [Looping](#looping)
- [for](#for)
- [Loop Status Variable](#loop-status-variable)
- [Loop Separator](#loop-separator)
- [Range Looping](#range-looping)
- [Property Looping](#property-looping)
- [Custom Iterator](#custom-iterator)
- [Macros](#macros)
- [def](#def)
- [invoke](#invoke)
- [Structure Manipulation](#structure-manipulation)
- [attrs](#attrs)
- [body-only-if](#body-only-if)
- [Comments](#comments)
- [Whitespace](#whitespace)
- [Helpers](#helpers)
- [Global Properties](#global-properties)
- [Custom Tags and Attributes](#custom-tags-and-attributes)
- [Async Taglib](#async-taglib)
- [Layout Taglib](#layout-taglib)
- [Custom Taglibs](#custom-taglibs)
- [Tag Renderer](#tag-renderer)
- [marko-taglib.json](#marko-taglibjson)
- [Sample Taglib](#sample-taglib)
- [Defining Tags](#defining-tags)
- [Defining Attributes](#defining-attributes)
- [Scanning for Tags](#scanning-for-tags)
- [Nested Tags](#nested-tags)
- [Taglib Discovery](#taglib-discovery)
- [Tag Renderer](#tag-renderer)
- [marko-taglib.json](#marko-taglibjson)
- [Sample Taglib](#sample-taglib)
- [Defining Tags](#defining-tags)
- [Defining Attributes](#defining-attributes)
- [Scanning for Tags](#scanning-for-tags)
- [Nested Tags](#nested-tags)
- [Taglib Discovery](#taglib-discovery)
- [FAQ](#faq)
- [Additional Resources](#additional-resources)
- [Further Reading](#further-reading)
- [Screencasts](#screencasts)
- [Demo Apps](#demo-apps)
- [Tools](#tools)
- [Changelog](#changelog)
- [Discuss](#discuss)
- [Contributors](#contributors)
- [Contribute](#contribute)
@ -406,18 +414,18 @@ templatePath.render({
});
```
You can then bundle up the above program for running in the browser using either [optimizer](https://github.com/raptorjs/optimizer) (recommended) or [browserify](https://github.com/substack/node-browserify).
You can then bundle up the above program for running in the browser using either [Lasso.js](https://github.com/lasso-js/lasso) (recommended) or [browserify](https://github.com/substack/node-browserify).
### Using the RaptorJS Optimizer
### Using Lasso.js
The `optimizer` CLI can be used to generate resource bundles that includes all application modules and all referenced Marko template files using a command similar to the following:
The `lasso` CLI can be used to generate resource bundles that includes all application modules and all referenced Marko template files using a command similar to the following:
```bash
# First install the optimizer and the optimizer-marko plugin
npm install optimizer --global
npm install optimizer-marko
# First install the lasso and the lasso-marko plugin
npm install lasso --global
npm install lasso-marko
optimizer --main run.js --name my-page --plugins optimizer-marko
lasso --main run.js --name my-page --plugins lasso-marko
```
This will produce a JSON file named `build/my-page.html.json` that contains the HTML markup that should be used to include the required JavaScript and CSS resources that resulted from the page optimization.
@ -425,7 +433,7 @@ This will produce a JSON file named `build/my-page.html.json` that contains the
Alternatively, you can inject the HTML markup into a static HTML file using the following command:
```bash
optimizer --main run.js --name my-page --plugins optimizer-marko --inject-into my-page.html
lasso --main run.js --name my-page --plugins lasso-marko --inject-into my-page.html
```
@ -444,7 +452,7 @@ browserify -t markoify run.js > browser.js
## Template Compilation
The Marko compiler produces a Node.js-compatible, CommonJS module as output. This output format has the advantage that compiled template modules can benefit from a context-aware module loader and templates can easily be transported to work in the browser using the [RaptorJS Optimizer](https://github.com/raptorjs/optimizer) or [Browserify](https://github.com/substack/node-browserify).
The Marko compiler produces a Node.js-compatible, CommonJS module as output. This output format has the advantage that compiled template modules can benefit from a context-aware module loader and templates can easily be transported to work in the browser using [Lasso.js](https://github.com/lasso-js/lasso) or [Browserify](https://github.com/substack/node-browserify).
The `marko` module will automatically compile templates loaded by your application on the server, but you can also choose to precompile all templates. This can be helpful as a build or test step to catch errors early.
@ -1414,45 +1422,57 @@ A tag renderer should be mapped to a custom tag by creating a `marko-taglib.json
}
```
### Defining Tags
Tags can be defined by adding a `"tags"` property to your `marko-taglib.json`:
Marko also supports a short-hand for declaring tags and attributes. The following `marko-taglib.json` is equivalent to the `marko-taglib.json` above:
```json
{
"tags": {
"my-hello": {
"renderer": "./hello-renderer",
"attributes": {
"name": "string"
}
},
"my-foo": {
"renderer": "./foo-renderer",
"attributes": {
"*": "string"
}
}
"<my-hello>": {
"renderer": "./hello-renderer",
"@name": "string"
}
}
```
Every tag should be associated with a renderer. When a custom tag is used in a template, the renderer will be invoked at render time to produce the HTML/output.
The short-hand will be used for the remaining of this documentation.
#### Defining Attributes
## Defining Tags
If you provide attributes then the Marko compiler will do validation to make sure only the supported attributes are provided. A wildcard attribute (`"*"`) allows any attribute to be passed in. Below are sample attribute definitions:
Tags can be defined by adding `"<tag_name>": <tag_def>` properties to your `marko-taglib.json`:
_Multiple attributes:_
```javascript
"attributes": {
"message": "string", // String
"my-data": "expression", // JavaScript expression
"*": "string" // Everything else will be added to a special "*" property
```json
{
"<my-hello>": {
"renderer": "./hello-renderer",
"@name": "string"
},
"<my-foo>": {
"renderer": "./foo-renderer",
"@*": "string"
},
"<my-bar>": "./path/to/my-bar/marko-tag.json",
"<my-baz>": {
"template": "./baz-template.marko"
},
}
```
### Scanning for Tags
Every tag should be associated with a renderer or a template. When a custom tag is used in a template, the renderer (or template) will be invoked at render time to produce the HTML/output. If a `String` path to a `marko-tag.json` for a custom tag then the target `marko-tag.json` is loaded to define the tag.
## Defining Attributes
If you provide attributes then the Marko compiler will do validation to make sure only the supported attributes are provided. A wildcard attribute (`"@*"`) allows any attribute to be passed in. Below are sample attribute definitions:
_Multiple attributes:_
```javascript
{
"@message": "string", // String
"@my-data": "expression", // JavaScript expression
"@*": "string" // Everything else will be added to a special "*" property
}
```
## Scanning for Tags
Marko supports a directory scanner to make it easier to maintain a taglib by introducing a few conventions:
@ -1494,9 +1514,7 @@ _In `renderer.js`:_
```javascript
exports.tag = {
"attributes": {
"name": "string"
}
"@name": "string"
}
```
@ -1504,116 +1522,180 @@ _In `marko-tag.json`:_
```javascript
{
"attributes": {
"name": "string"
}
"@name": "string"
}
```
_NOTE: It is not necessary to declare the `renderer` since the scanner will automatically use `renderer.js` as the renderer._
### Nested Tags
## Nested Tags
It is often necessary for tags to have a parent/child or ancestor/descendent relationship. For example:
```html
<ui-tabs>
<ui-tab title="Home">
<ui-tabs orientation="horizontal">
<ui-tabs.tab title="Home">
Content for Home
</ui-tab>
<ui-tab title="Profile">
</ui-tabs.tab>
<ui-tabs.tab title="Profile">
Content for Profile
</ui-tab>
<ui-tab title="Messages">
</ui-tabs.tab>
<ui-tabs.tab title="Messages">
Content for Messages
</ui-tab>
</ui-tabs.tab>
</ui-tabs>
```
Marko supports this by leveraging JavaScript closures in the compiled output. A tag can introduce scoped variables that are available to nested tags. This is shown in the sample `marko-taglib.json` below:
Nested tags can be declared in the parent tag's `marko-tag.json` as shown below:
___ui-tabs/marko-tag.json___
```json
{
"tags": {
"ui-tabs": {
"renderer": "./tabs-tag",
"body-function": "getTabs(__tabsHelper)"
},
"ui-tab": {
"renderer": "./tab-tag",
"import-var": {
"tabs": "__tabsHelper"
},
"attributes": {
"title": "string"
}
}
"@orientation": "string",
"@tabs <tab>[]": {
"@title": "string"
}
}
```
In the above example, the `<ui-tabs>` tag will introduce a scoped variable named `tabs` that is then automatically imported by the nested `<ui-tab>` tags. When the nested `<ui-tab>` tags render they can use the scoped variable to communicate with the renderer for the `<ui-tabs>` tag.
This allows a `tabs` to be provided using nested `<ui-tabs.tab>` tags or the tabs can be provided as a `tabs` attribute (e.g. `<ui-tabs tabs="[tab1, tab2, tab3]"`). The nested `<ui-tabs.tab>` tags will be made available to the renderer as part of the `tabs` property for the parent `<ui-tabs>`. Because of the `[]` suffix on `<tab>[]` the tabs property will be of type `Array` and not a single object. That is, the `[]` suffix is used to declare that a nested tag can be repeated. The sample renderer that accesses the nested tabs is shown below:
The complete code for this example is shown below:
_components/tabs/renderer.js:_
___ui-tabs/renderer.js___
```javascript
var templatePath = require.resolve('./template.marko');
var template = require('marko').load(templatePath);
var template = require('marko').load(require.resolve('./template.marko'));
exports.render = function(input, out) {
var nestedTabs;
exports.renderer = function(input, out) {
var tabs = input.tabs;
if (input.getTabs) {
nestedTabs = [];
// Invoke the body function to discover nested <ui-tab> tags
input.getTabs({ // Invoke the body with the scoped "tabs" variable
addTab: function(tab) {
tab.id = tab.id || ("tab" + nestedTabs.length);
nestedTabs.push(tab);
}
});
} else {
nestedTabs = input.tabs || [];
}
// Tabs will be in the following form:
// [
// {
// title: 'Home',
// renderBody: function(out) { ... }
// },
// {
// title: 'Profile',
// renderBody: function(out) { ... }
// },
// {
// title: 'Messages',
// renderBody: function(out) { ... }
// }
// ]
console.log(tabs.length); // Output: 3
// Now render the markup for the tabs:
template.render({
tabs: nestedTabs
tabs: tabs
}, out);
};
```
_components/tab/renderer.js:_
Finally, the template to render the `<ui-tabs>` component will be similar to the following:
```javascript
exports.render = function(input, out) {
// Register with parent but don't render anything
input.tabs.addTab(input);
};
```
_components/tabs/template.marko:_
___ui-tabs/template.marko___
```html
<div class="tabs">
<ul class="nav nav-tabs">
<li class="tab" for="tab in data.tabs">
<a href="#${tab.id}" data-toggle="tab">
<a href="#${tab.title}">
${tab.title}
</a>
</li>
</ul>
<div class="tab-content">
<div id="${tab.id}" class="tab-pane" for="tab in data.tabs">
<div class="tab-pane" for="tab in data.tabs">
<invoke function="tab.renderBody(out)"/>
</div>
</div>
</div>
```
Below is an example of using nested tags that are not repeated:
```html
<ui-overlay>
<ui-overlay.header class="my-header">
Header content
</ui-overlay.header>
<ui-overlay.body class="my-body">
Body content
</ui-overlay.body>
<ui-overlay.footer class="my-footer">
Footer content
</ui-overlay.footer>
</ui-overlay>
```
The `marko-tag.json` for the `<ui-overlay>` tag will be similar to the following:
___ui-overlay/marko-tag.json___
```json
{
"@header <header>": {
"@class": "string"
},
"@body <body>": {
"@class": "string"
},
"@footer <footer>": {
"@class": "string"
}
}
```
The renderer for the `<ui-overlay>` tag will be similar to the following:
```javascript
var template = require('marko').load(require.resolve('./template.marko'));
exports.renderer = function(input, out) {
var header = input.header;
var body = input.body;
var footer = input.footer;
// NOTE: header, body and footer will be of the following form:
//
// {
// 'class': 'my-header',
// renderBody: function(out) { ... }
// }
template.render({
header: header,
body: body,
footer: footer
}, out);
};
```
Finally, the sample template to render the `<ui-overlay>` tag is shown below:
```html
<div class="overlay">
<!-- Header -->
<div class="overlay-header ${data.header['class']}" if="data.header">
<invoke function="data.header.renderBody(out)"/>
</div>
<!-- Body -->
<div class="overlay-body ${data.body['class']}" if="data.body">
<invoke function="data.body.renderBody(out)"/>
</div>
<!-- Footer -->
<div class="overlay-footer ${data.footer['class']}" if="data.footer">
<invoke function="data.footer.renderBody(out)"/>
</div>
</div>
```
## Taglib Discovery
@ -1640,7 +1722,7 @@ __Answer__: Yes, Marko has been battle-tested at [eBay](http://www.ebay.com/) an
__Question:__ _Can templates be compiled on the client?_
__Answer__: Possibly, but it is not recommended and it will likely not work in older browsers. The compiler is optimized to produce small, high performance compiled templates, but the compiler itself is not small and it comes bundled with some heavyweight modules such as a [JavaScript HTML parser](https://github.com/fb55/htmlparser2). In short, always compile your templates on the server. The [RaptorJS Optimizer](https://github.com/raptorjs/optimizer) is recommended for including compiled templates as part of a web page.
__Answer__: Possibly, but it is not recommended and it will likely not work in older browsers. The compiler is optimized to produce small, high performance compiled templates, but the compiler itself is not small and it comes bundled with some heavyweight modules such as a [JavaScript HTML parser](https://github.com/fb55/htmlparser2). In short, always compile your templates on the server. [Lasso.js](https://github.com/lasso-js/lasso) is recommended for including compiled templates as part of a web page.
<hr>

View File

@ -30,9 +30,7 @@ TypeConverter.convert = function (value, targetType, allowExpressions) {
return value;
}
if (targetType === 'expression') {
if (targetType === 'expression' || targetType === 'object' || targetType === 'array') {
if (value === '') {
value = 'null';
}

View File

@ -1,297 +0,0 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var forEachEntry = require('raptor-util').forEachEntry;
var ok = require('assert').ok;
var makeClass = require('raptor-util').makeClass;
function inheritProps(sub, sup) {
forEachEntry(sup, function (k, v) {
if (!sub[k]) {
sub[k] = v;
}
});
}
function Taglib(id) {
ok(id, '"id" expected');
this.id = id;
this.dirname = null;
this.tags = {};
this.textTransformers = [];
this.attributes = {};
this.patternAttributes = [];
this.inputFilesLookup = {};
this.imports = null;
}
Taglib.prototype = {
addInputFile: function(path) {
this.inputFilesLookup[path] = true;
},
getInputFiles: function() {
return Object.keys(this.inputFilesLookup);
},
addAttribute: function (attribute) {
if (attribute.pattern) {
this.patternAttributes.push(attribute);
} else if (attribute.name) {
this.attributes[attribute.name] = attribute;
} else {
throw new Error('Invalid attribute: ' + require('util').inspect(attribute));
}
},
getAttribute: function (name) {
var attribute = this.attributes[name];
if (!attribute) {
for (var i = 0, len = this.patternAttributes.length; i < len; i++) {
var patternAttribute = this.patternAttributes[i];
if (patternAttribute.pattern.test(name)) {
attribute = patternAttribute;
}
}
}
return attribute;
},
addTag: function (tag) {
ok(arguments.length === 1, 'Invalid args');
ok(tag.name, '"tag.name" is required');
this.tags[tag.name] = tag;
tag.taglibId = this.id;
},
addTextTransformer: function (transformer) {
this.textTransformers.push(transformer);
},
forEachTag: function (callback, thisObj) {
forEachEntry(this.tags, function (key, tag) {
callback.call(thisObj, tag);
}, this);
},
addImport: function(path) {
if (!this.imports) {
this.imports = [];
}
this.imports.push(path);
}
};
Taglib.Tag = makeClass({
$init: function(taglib) {
this.taglibId = taglib ? taglib.id : null;
this.renderer = null;
this.nodeClass = null;
this.template = null;
this.attributes = {};
this.transformers = {};
this.nestedVariables = null;
this.importedVariables = null;
this.patternAttributes = [];
this.bodyFunction = null;
},
inheritFrom: function (superTag) {
var subTag = this;
/*
* Have the sub tag inherit any properties from the super tag that are not in the sub tag
*/
forEachEntry(superTag, function (k, v) {
if (subTag[k] === undefined) {
subTag[k] = v;
}
});
[
'attributes',
'transformers',
'nestedVariables',
'importedVariables',
'bodyFunction'
].forEach(function (propName) {
inheritProps(subTag[propName], superTag[propName]);
});
subTag.patternAttributes = superTag.patternAttributes.concat(subTag.patternAttributes);
},
forEachVariable: function (callback, thisObj) {
if (!this.nestedVariables) {
return;
}
this.nestedVariables.vars.forEach(callback, thisObj);
},
forEachImportedVariable: function (callback, thisObj) {
if (!this.importedVariables) {
return;
}
forEachEntry(this.importedVariables, function (key, importedVariable) {
callback.call(thisObj, importedVariable);
});
},
forEachTransformer: function (callback, thisObj) {
forEachEntry(this.transformers, function (key, transformer) {
callback.call(thisObj, transformer);
});
},
hasTransformers: function () {
/*jshint unused:false */
for (var k in this.transformers) {
if (this.transformers.hasOwnProperty(k)) {
return true;
}
}
return false;
},
addAttribute: function (attr) {
if (attr.pattern) {
this.patternAttributes.push(attr);
} else {
if (attr.name === '*') {
attr.dynamicAttribute = true;
if (attr.targetProperty === null || attr.targetProperty === '') {
attr.targetProperty = null;
}
else if (!attr.targetProperty) {
attr.targetProperty = '*';
}
}
this.attributes[attr.name] = attr;
}
},
toString: function () {
return '[Tag: <' + this.name + '@' + this.taglibId + '>]';
},
forEachAttribute: function (callback, thisObj) {
for (var attrName in this.attributes) {
if (this.attributes.hasOwnProperty(attrName)) {
callback.call(thisObj, this.attributes[attrName]);
}
}
},
addNestedVariable: function (nestedVariable) {
if (!this.nestedVariables) {
this.nestedVariables = {
__noMerge: true,
vars: []
};
}
this.nestedVariables.vars.push(nestedVariable);
},
addImportedVariable: function (importedVariable) {
if (!this.importedVariables) {
this.importedVariables = {};
}
var key = importedVariable.targetProperty;
this.importedVariables[key] = importedVariable;
},
addTransformer: function (transformer) {
var key = transformer.path;
transformer.taglibId = this.taglibId;
this.transformers[key] = transformer;
},
setBodyFunction: function(name, params) {
this.bodyFunction = {
__noMerge: true,
name: name,
params: params
};
},
setBodyProperty: function(propertyName) {
this.bodyProperty = propertyName;
}
});
Taglib.Attribute = makeClass({
$init: function(name) {
this.name = name;
this.type = null;
this.required = false;
this.type = 'string';
this.allowExpressions = true;
this.setFlag = null;
}
});
Taglib.Property = makeClass({
$init: function() {
this.name = null;
this.type = 'string';
this.value = undefined;
}
});
Taglib.NestedVariable = makeClass({
$init: function() {
this.name = null;
}
});
Taglib.ImportedVariable = makeClass({
$init: function() {
this.targetProperty = null;
this.expression = null;
}
});
var nextTransformerId = 0;
Taglib.Transformer = makeClass({
$init: function() {
this.id = nextTransformerId++;
this.name = null;
this.tag = null;
this.path = null;
this.priority = null;
this._func = null;
this.properties = {};
},
getFunc: function () {
if (!this.path) {
throw new Error('Transformer path not defined for tag transformer (tag=' + this.tag + ')');
}
if (!this._func) {
var transformer = require(this.path);
if (typeof transformer === 'function') {
if (transformer.prototype.process) {
var Clazz = transformer;
var instance = new Clazz();
instance.id = this.id;
this._func = instance.process.bind(instance);
} else {
this._func = transformer;
}
} else {
this._func = transformer.process || transformer.transform;
}
}
return this._func;
},
toString: function () {
return '[Taglib.Transformer: ' + this.path + ']';
}
});
module.exports = Taglib;

View File

@ -0,0 +1,12 @@
var makeClass = require('raptor-util').makeClass;
module.exports = makeClass({
$init: function(name) {
this.name = name;
this.type = null;
this.required = false;
this.type = 'string';
this.allowExpressions = true;
this.setFlag = null;
}
});

View File

@ -0,0 +1,8 @@
var makeClass = require('raptor-util').makeClass;
module.exports = makeClass({
$init: function() {
this.targetProperty = null;
this.expression = null;
}
});

View File

@ -0,0 +1,7 @@
var makeClass = require('raptor-util').makeClass;
module.exports = makeClass({
$init: function() {
this.name = null;
}
});

View File

@ -0,0 +1,9 @@
var makeClass = require('raptor-util').makeClass;
module.exports = makeClass({
$init: function() {
this.name = null;
this.type = 'string';
this.value = undefined;
}
});

View File

@ -0,0 +1,171 @@
var makeClass = require('raptor-util').makeClass;
var forEachEntry = require('raptor-util').forEachEntry;
var ok = require('assert').ok;
function inheritProps(sub, sup) {
forEachEntry(sup, function (k, v) {
if (!sub[k]) {
sub[k] = v;
}
});
}
module.exports = makeClass({
$init: function(taglib) {
this.taglibId = taglib ? taglib.id : null;
this.renderer = null;
this.nodeClass = null;
this.template = null;
this.attributes = {};
this.transformers = {};
this.nestedVariables = null;
this.importedVariables = null;
this.patternAttributes = [];
this.bodyFunction = null;
this.nestedTags = null;
this.isRepeated = null;
this.isNestedTag = false;
this.parentTagName = null;
this.type = null; // Only applicable for nested tags
},
inheritFrom: function (superTag) {
var subTag = this;
/*
* Have the sub tag inherit any properties from the super tag that are not in the sub tag
*/
forEachEntry(superTag, function (k, v) {
if (subTag[k] === undefined) {
subTag[k] = v;
}
});
[
'attributes',
'transformers',
'nestedVariables',
'importedVariables',
'bodyFunction'
].forEach(function (propName) {
inheritProps(subTag[propName], superTag[propName]);
});
subTag.patternAttributes = superTag.patternAttributes.concat(subTag.patternAttributes);
},
forEachVariable: function (callback, thisObj) {
if (!this.nestedVariables) {
return;
}
this.nestedVariables.vars.forEach(callback, thisObj);
},
forEachImportedVariable: function (callback, thisObj) {
if (!this.importedVariables) {
return;
}
forEachEntry(this.importedVariables, function (key, importedVariable) {
callback.call(thisObj, importedVariable);
});
},
forEachTransformer: function (callback, thisObj) {
forEachEntry(this.transformers, function (key, transformer) {
callback.call(thisObj, transformer);
});
},
hasTransformers: function () {
/*jshint unused:false */
for (var k in this.transformers) {
if (this.transformers.hasOwnProperty(k)) {
return true;
}
}
return false;
},
addAttribute: function (attr) {
if (attr.pattern) {
this.patternAttributes.push(attr);
} else {
if (attr.name === '*') {
attr.dynamicAttribute = true;
if (attr.targetProperty === null || attr.targetProperty === '') {
attr.targetProperty = null;
}
else if (!attr.targetProperty) {
attr.targetProperty = '*';
}
}
this.attributes[attr.name] = attr;
}
},
toString: function () {
return '[Tag: <' + this.name + '@' + this.taglibId + '>]';
},
forEachAttribute: function (callback, thisObj) {
for (var attrName in this.attributes) {
if (this.attributes.hasOwnProperty(attrName)) {
callback.call(thisObj, this.attributes[attrName]);
}
}
},
addNestedVariable: function (nestedVariable) {
if (!this.nestedVariables) {
this.nestedVariables = {
__noMerge: true,
vars: []
};
}
this.nestedVariables.vars.push(nestedVariable);
},
addImportedVariable: function (importedVariable) {
if (!this.importedVariables) {
this.importedVariables = {};
}
var key = importedVariable.targetProperty;
this.importedVariables[key] = importedVariable;
},
addTransformer: function (transformer) {
var key = transformer.path;
transformer.taglibId = this.taglibId;
this.transformers[key] = transformer;
},
setBodyFunction: function(name, params) {
this.bodyFunction = {
__noMerge: true,
name: name,
params: params
};
},
setBodyProperty: function(propertyName) {
this.bodyProperty = propertyName;
},
addNestedTag: function(nestedTag) {
ok(nestedTag.name, '"nestedTag.name" is required');
if (!this.nestedTags) {
this.nestedTags = {};
}
nestedTag.isNestedTag = true;
if (!nestedTag.targetProperty) {
nestedTag.targetProperty = nestedTag.name;
}
this.nestedTags[nestedTag.name] = nestedTag;
},
forEachNestedTag: function (callback, thisObj) {
if (!this.nestedTags) {
return;
}
forEachEntry(this.nestedTags, function (key, nestedTag) {
callback.call(thisObj, nestedTag);
});
},
hasNestedTags: function() {
return this.nestedTags != null;
}
});

View File

@ -0,0 +1,42 @@
var makeClass = require('raptor-util').makeClass;
var nextTransformerId = 0;
module.exports = makeClass({
$init: function() {
this.id = nextTransformerId++;
this.name = null;
this.tag = null;
this.path = null;
this.priority = null;
this._func = null;
this.properties = {};
},
getFunc: function () {
if (!this.path) {
throw new Error('Transformer path not defined for tag transformer (tag=' + this.tag + ')');
}
if (!this._func) {
var transformer = require(this.path);
if (typeof transformer === 'function') {
if (transformer.prototype.process) {
var Clazz = transformer;
var instance = new Clazz();
instance.id = this.id;
this._func = instance.process.bind(instance);
} else {
this._func = transformer;
}
} else {
this._func = transformer.process || transformer.transform;
}
}
return this._func;
},
toString: function () {
return '[Taglib.Transformer: ' + this.path + ']';
}
});

View File

@ -0,0 +1,94 @@
/*
* Copyright 2011 eBay Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var forEachEntry = require('raptor-util').forEachEntry;
var ok = require('assert').ok;
function Taglib(id) {
ok(id, '"id" expected');
this.id = id;
this.dirname = null;
this.tags = {};
this.textTransformers = [];
this.attributes = {};
this.patternAttributes = [];
this.inputFilesLookup = {};
this.imports = null;
}
Taglib.prototype = {
addInputFile: function(path) {
this.inputFilesLookup[path] = true;
},
getInputFiles: function() {
return Object.keys(this.inputFilesLookup);
},
addAttribute: function (attribute) {
if (attribute.pattern) {
this.patternAttributes.push(attribute);
} else if (attribute.name) {
this.attributes[attribute.name] = attribute;
} else {
throw new Error('Invalid attribute: ' + require('util').inspect(attribute));
}
},
getAttribute: function (name) {
var attribute = this.attributes[name];
if (!attribute) {
for (var i = 0, len = this.patternAttributes.length; i < len; i++) {
var patternAttribute = this.patternAttributes[i];
if (patternAttribute.pattern.test(name)) {
attribute = patternAttribute;
}
}
}
return attribute;
},
addTag: function (tag) {
ok(arguments.length === 1, 'Invalid args');
ok(tag.name, '"tag.name" is required');
this.tags[tag.name] = tag;
tag.taglibId = this.id;
},
addTextTransformer: function (transformer) {
this.textTransformers.push(transformer);
},
forEachTag: function (callback, thisObj) {
forEachEntry(this.tags, function (key, tag) {
callback.call(thisObj, tag);
}, this);
},
addImport: function(path) {
if (!this.imports) {
this.imports = [];
}
this.imports.push(path);
}
};
Taglib.Tag = require('./Tag');
Taglib.Attribute = require('./Attribute');
Taglib.Property = require('./Property');
Taglib.NestedVariable = require('./NestedVariable');
Taglib.ImportedVariable = require('./ImportedVariable');
Taglib.Transformer = require('./Transformer');
module.exports = Taglib;

View File

@ -1,5 +1,7 @@
var ok = require('assert').ok;
var createError = require('raptor-util').createError;
var Taglib = require('./Taglib');
var extend = require('raptor-util/extend');
function transformerComparator(a, b) {
a = a.priority;
@ -66,7 +68,33 @@ function TaglibLookup() {
}
TaglibLookup.prototype = {
_mergeNestedTags: function(taglib) {
var Tag = Taglib.Tag;
// Loop over all of the nested tags and register a new custom tag
// with the fully qualified name
var merged = this.merged;
function handleNestedTag(nestedTag, parentTagName) {
var fullyQualifiedName = parentTagName + '.' + nestedTag.name;
// Create a clone of the nested tag since we need to add some new
// properties
var clonedNestedTag = new Tag();
extend(clonedNestedTag ,nestedTag);
// Record the fully qualified name of the parent tag that this
// custom tag is associated with.
clonedNestedTag.parentTagName = parentTagName;
clonedNestedTag.name = fullyQualifiedName;
merged.tags[fullyQualifiedName] = clonedNestedTag;
}
taglib.forEachTag(function(tag) {
tag.forEachNestedTag(function(nestedTag) {
handleNestedTag(nestedTag, tag.name);
});
});
},
addTaglib: function (taglib) {
ok(taglib, '"taglib" is required');
@ -79,6 +107,8 @@ TaglibLookup.prototype = {
this.taglibsById[taglib.id] = taglib;
merge(this.merged, taglib);
this._mergeNestedTags(taglib);
},
getTag: function (element) {

View File

@ -1,525 +0,0 @@
var fs ;
var req = require;
try {
fs = req('fs');
} catch(e) {
}
var ok = require('assert').ok;
var nodePath = require('path');
var Taglib = require('./Taglib');
var cache = {};
var forEachEntry = require('raptor-util').forEachEntry;
var raptorRegexp = require('raptor-regexp');
var tagDefFromCode = require('./tag-def-from-code');
var resolve = require('../util/resolve'); // NOTE: different implementation for browser
var propertyHandlers = require('property-handlers');
var jsonminify = require('jsonminify');
var safeVarName = /^[A-Za-z_$][A-Za-z0-9_]*$/;
var bodyFunctionRegExp = /^([A-Za-z_$][A-Za-z0-9_]*)(?:\(([^)]*)\))?$/;
function exists(path) {
try {
require.resolve(path);
return true;
} catch(e) {
return false;
}
}
function createDefaultTagDef() {
return {
attributes: {
'*': {
type: 'string',
targetProperty: null,
preserveName: false
}
}
};
}
function buildAttribute(attr, attrProps, path) {
propertyHandlers(attrProps, {
type: function(value) {
attr.type = value;
},
targetProperty: function(value) {
attr.targetProperty = value;
},
defaultValue: function(value) {
attr.defaultValue = value;
},
pattern: function(value) {
if (value === true) {
var patternRegExp = raptorRegexp.simple(attr.name);
attr.pattern = patternRegExp;
}
},
allowExpressions: function(value) {
attr.allowExpressions = value;
},
preserveName: function(value) {
attr.preserveName = value;
},
required: function(value) {
attr.required = value === true;
},
removeDashes: function(value) {
attr.removeDashes = value === true;
},
description: function() {
},
setFlag: function(value) {
attr.setFlag = value;
},
ignore: function(value) {
if (value === true) {
attr.ignore = true;
}
}
}, path);
return attr;
}
function handleAttributes(value, parent, path) {
forEachEntry(value, function(attrName, attrProps) {
var attr = new Taglib.Attribute(attrName);
if (attrProps == null) {
attrProps = {
type: 'string'
};
} else if (typeof attrProps === 'string') {
attrProps = {
type: attrProps
};
}
buildAttribute(attr, attrProps, '"' + attrName + '" attribute as part of ' + path);
parent.addAttribute(attr);
});
}
function buildTag(tagObject, path, taglib, dirname) {
ok(tagObject);
ok(typeof path === 'string');
ok(taglib);
ok(typeof dirname === 'string');
var tag = new Taglib.Tag(taglib);
if (tagObject.attributes == null) {
// allow any attributes if no attributes are declared
tagObject.attributes = {
'*': 'string'
};
}
propertyHandlers(tagObject, {
name: function(value) {
tag.name = value;
},
renderer: function(value) {
var path = resolve(value, dirname);
tag.renderer = path;
},
template: function(value) {
var path = nodePath.resolve(dirname, value);
if (!exists(path)) {
throw new Error('Template at path "' + path + '" does not exist.');
}
tag.template = path;
},
attributes: function(value) {
handleAttributes(value, tag, path);
},
nodeClass: function(value) {
var path = resolve(value, dirname);
tag.nodeClass = path;
},
preserveWhitespace: function(value) {
tag.preserveWhitespace = !!value;
},
transformer: function(value) {
var transformer = new Taglib.Transformer();
if (typeof value === 'string') {
value = {
path: value
};
}
propertyHandlers(value, {
path: function(value) {
var path = resolve(value, dirname);
transformer.path = path;
},
priority: function(value) {
transformer.priority = value;
},
name: function(value) {
transformer.name = value;
},
properties: function(value) {
var properties = transformer.properties || (transformer.properties = {});
for (var k in value) {
if (value.hasOwnProperty(k)) {
properties[k] = value[k];
}
}
}
}, 'transformer in ' + path);
ok(transformer.path, '"path" is required for transformer');
tag.addTransformer(transformer);
},
'var': function(value) {
tag.addNestedVariable({
name: value
});
},
bodyFunction: function(value) {
var parts = bodyFunctionRegExp.exec(value);
if (!parts) {
throw new Error('Invalid value of "' + value + '" for "body-function". Expected value to be of the following form: <function-name>([param1, param2, ...])');
}
var functionName = parts[1];
var params = parts[2];
if (params) {
params = params.trim().split(/\s*,\s*/);
for (var i=0; i<params.length; i++) {
if (params[i].length === 0) {
throw new Error('Invalid parameters for body-function with value of "' + value + '"');
} else if (!safeVarName.test(params[i])) {
throw new Error('Invalid parameter name of "' + params[i] + '" for body-function with value of "' + value + '"');
}
}
} else {
params = [];
}
tag.setBodyFunction(functionName, params);
},
bodyProperty: function(value) {
tag.setBodyProperty(value);
},
vars: function(value) {
if (value) {
value.forEach(function(v, i) {
var nestedVariable;
if (typeof v === 'string') {
nestedVariable = {
name: v
};
} else {
nestedVariable = {};
propertyHandlers(v, {
name: function(value) {
nestedVariable.name = value;
},
nameFromAttribute: function(value) {
nestedVariable.nameFromAttribute = value;
}
}, 'var at index ' + i);
if (!nestedVariable.name && !nestedVariable.nameFromAttribute) {
throw new Error('The "name" or "name-from-attribute" attribute is required for a nested variable');
}
}
tag.addNestedVariable(nestedVariable);
});
}
},
importVar: function(value) {
forEachEntry(value, function(varName, varValue) {
var importedVar = {
targetProperty: varName
};
var expression = varValue;
if (!expression) {
expression = varName;
}
else if (typeof expression === 'object') {
expression = expression.expression;
}
if (!expression) {
throw new Error('Invalid "import-var": ' + require('util').inspect(varValue));
}
importedVar.expression = expression;
tag.addImportedVariable(importedVar);
});
}
}, path);
return tag;
}
/**
* @param {String} tagsConfigPath path to tag definition file
* @param {String} tagsConfigDirname path to directory of tags config file (should be path.dirname(tagsConfigPath))
* @param {String|Object} dir the path to directory to scan
* @param {String} taglib the taglib that is being loaded
*/
function scanTagsDir(tagsConfigPath, tagsConfigDirname, dir, taglib) {
var prefix;
if (typeof dir === 'object') {
prefix = dir.prefix;
dir = dir.path;
}
if (prefix == null) {
// no prefix by default
prefix = '';
}
dir = nodePath.resolve(tagsConfigDirname, dir);
var children = fs.readdirSync(dir);
var rendererJSFile;
for (var i=0, len=children.length; i<len; i++) {
rendererJSFile = null;
var childFilename = children[i];
if (childFilename === 'node_modules') {
continue;
}
var tagName = prefix + childFilename;
var tagDirname = nodePath.join(dir, childFilename);
var tagFile = nodePath.join(dir, childFilename, 'marko-tag.json');
var tag = null;
var rendererFile = nodePath.join(dir, childFilename, 'renderer.js');
var indexFile = nodePath.join(dir, childFilename, 'index.js');
var templateFile = nodePath.join(dir, childFilename, 'template.marko');
var tagDef = null;
// Record dependencies so that we can check if a template is up-to-date
taglib.addInputFile(tagFile);
taglib.addInputFile(rendererFile);
if (fs.existsSync(tagFile)) {
// marko-tag.json exists in the directory, use that as the tag definition
tagDef = JSON.parse(jsonminify(fs.readFileSync(tagFile, {encoding: 'utf8'})));
if (!tagDef.renderer && !tagDef.template) {
if (fs.existsSync(rendererFile)) {
tagDef.renderer = rendererFile;
} else if (fs.existsSync(indexFile)) {
tagDef.renderer = indexFile;
} else if (fs.existsSync(templateFile)) {
tagDef.template = templateFile;
} else if (fs.existsSync(templateFile + ".html")) {
tagDef.template = templateFile + ".html";
} else {
throw new Error('Invalid tag file: ' + tagFile + '. Neither a renderer or a template was found for tag.');
}
}
tag = buildTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tag.name || tagName;
taglib.addTag(tag);
} else {
// marko-tag.json does *not* exist... checking for a 'renderer.js'
if (fs.existsSync(rendererFile)) {
rendererJSFile = rendererFile;
} else if (fs.existsSync(indexFile)) {
rendererJSFile = indexFile;
} else {
var exTemplateFile;
if (fs.existsSync(templateFile)) {
exTemplateFile = templateFile;
}
else if (fs.existsSync(templateFile + ".html")){
exTemplateFile = templateFile + ".html";
}
if(exTemplateFile){
var templateCode = fs.readFileSync(exTemplateFile, {encoding: 'utf8'});
tagDef = tagDefFromCode.extractTagDef(templateCode);
if (!tagDef) {
tagDef = createDefaultTagDef();
}
tagDef.template = exTemplateFile;
}
}
if (rendererJSFile) {
var rendererCode = fs.readFileSync(rendererJSFile, {encoding: 'utf8'});
tagDef = tagDefFromCode.extractTagDef(rendererCode);
if (!tagDef) {
tagDef = createDefaultTagDef();
}
tagDef.renderer = rendererJSFile;
tag = buildTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tagName;
taglib.addTag(tag);
}
if (tagDef) {
tag = buildTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tagName;
taglib.addTag(tag);
}
}
}
}
function load(path) {
if (cache[path]) {
return cache[path];
}
var taglibObject;
try {
taglibObject = require(path);
} catch(e) {
throw new Error('Unable to parse taglib JSON at path "' + path + '". Exception: ' + e);
}
var taglib = new Taglib(path);
taglib.addInputFile(path);
var dirname = nodePath.dirname(path);
propertyHandlers(taglibObject, {
attributes: function(value) {
handleAttributes(value, taglib, path);
},
tags: function(tags) {
forEachEntry(tags, function(tagName, path) {
ok(path, 'Invalid tag definition for "' + tagName + '"');
var tagObject;
var tagDirname;
if (typeof path === 'string') {
path = nodePath.resolve(dirname, path);
taglib.addInputFile(path);
tagDirname = nodePath.dirname(path);
if (!exists(path)) {
throw new Error('Tag at path "' + path + '" does not exist. Taglib: ' + taglib.id);
}
try {
tagObject = require(path);
} catch(e) {
throw new Error('Unable to parse tag JSON for tag at path "' + path + '"');
}
} else {
tagDirname = dirname; // Tag is in the same taglib file
tagObject = path;
path = '<' + tagName + '> tag in ' + taglib.id;
}
var tag = buildTag(tagObject, path, taglib, tagDirname);
if (tag.name === undefined) {
tag.name = tagName;
}
taglib.addTag(tag);
});
},
tagsDir: function(dir) {
if (Array.isArray(dir)) {
for (var i = 0; i < dir.length; i++) {
scanTagsDir(path, dirname, dir[i], taglib);
}
} else {
scanTagsDir(path, dirname, dir, taglib);
}
},
taglibImports: function(imports) {
if (imports && Array.isArray(imports)) {
for (var i=0; i<imports.length; i++) {
var curImport = imports[i];
if (typeof curImport === 'string') {
var basename = nodePath.basename(curImport);
if (basename === 'package.json') {
var packagePath = resolve(curImport, dirname);
var pkg = require(packagePath);
var dependencies = pkg.dependencies;
if (dependencies) {
var dependencyNames = Object.keys(dependencies);
for (var j=0; j<dependencyNames.length; j++) {
var dependencyName = dependencyNames[j];
var importPath;
try {
importPath = require('resolve-from')(dirname, dependencyName + '/marko-taglib.json');
} catch(e) {}
if (importPath) {
taglib.addImport(importPath);
}
}
}
}
}
}
}
},
textTransformer: function(value) {
var transformer = new Taglib.Transformer();
if (typeof value === 'string') {
value = {
path: value
};
}
propertyHandlers(value, {
path: function(value) {
var path = resolve(value, dirname);
transformer.path = path;
}
}, 'text-transformer in ' + path);
ok(transformer.path, '"path" is required for transformer');
taglib.addTextTransformer(transformer);
}
}, path);
taglib.id = taglib.path = path;
cache[path] = taglib;
return taglib;
}
exports.load = load;

View File

@ -0,0 +1,16 @@
var ok = require('assert').ok;
var forEachEntry = require('raptor-util').forEachEntry;
var loader = require('./loader');
module.exports = function handleAttributes(value, parent, path) {
ok(parent);
forEachEntry(value, function(attrName, attrProps) {
var attr = loader.attributeLoader.loadAttribute(
attrName,
attrProps,
'"' + attrName + '" attribute as part of ' + path);
parent.addAttribute(attr);
});
};

View File

@ -0,0 +1,20 @@
require('raptor-polyfill/string/startsWith');
var loader = require('./loader');
var cache = {};
function load(path) {
if (cache[path]) {
return cache[path];
}
var taglib = loader.taglibLoader.loadTaglib(path);
cache[path] = taglib;
return taglib;
}
exports.load = load;

View File

@ -0,0 +1,166 @@
var assert = require('assert');
var raptorRegexp = require('raptor-regexp');
var propertyHandlers = require('property-handlers');
var Taglib = require('../Taglib');
function AttrHandlers(attr){
assert.ok(attr);
assert.equal(typeof attr, 'object');
this.attr = attr;
}
AttrHandlers.prototype = {
/**
* The attribute type. One of the following:
* - string (the default)
* - expression (a JavaScript expression)
* - number
* - integer
* - int
* - boolean
* - float
* - double
* - object
* - array
*
*/
type: function(value) {
var attr = this.attr;
attr.type = value;
},
/**
* The name of the target property to use when mapping
* the attribute to a property on the target object.
*/
targetProperty: function(value) {
var attr = this.attr;
attr.targetProperty = value;
},
/**
* The "default-value" property allows a default value
* to be provided when the attribute is not declared
* on the custom tag.
*/
defaultValue: function(value) {
var attr = this.attr;
attr.defaultValue = value;
},
/**
* The "pattern" property allows the attribute
* to be matched based on a simplified regular expression.
*
* Example:
*
* "pattern": "myprefix-*"
*/
pattern: function(value) {
var attr = this.attr;
if (value === true) {
var patternRegExp = raptorRegexp.simple(attr.name);
attr.pattern = patternRegExp;
}
},
/**
* If "allow-expressions" is set to true (the default) then
* the the attribute value will be parsed to find any dynamic
* parts.
*/
allowExpressions: function(value) {
var attr = this.attr;
attr.allowExpressions = value;
},
/**
* By default, the Marko compiler maps an attribute
* to a property by removing all dashes from the attribute
* name and converting each character after a dash to
* an uppercase character (e.g. "my-attr" --> "myAttr").
*
* Setting "preserve-name" to true will prevent this from
* happening for the attribute.
*/
preserveName: function(value) {
var attr = this.attr;
attr.preserveName = value;
},
/**
* Declares an attribute as required. Currently, this is
* not enforced and is only used for documentation purposes.
*
* Example:
* "required": true
*/
required: function(value) {
var attr = this.attr;
attr.required = value === true;
},
/**
* This is the opposite of "preserve-name" and will result
* in dashes being removed from the attribute if set to true.
*/
removeDashes: function(value) {
var attr = this.attr;
attr.removeDashes = value === true;
},
/**
* The description of the attribute. Only used for documentation.
*/
description: function() {
},
/**
* The "set-flag" property allows a "flag" to be added to a Node instance
* at compile time if the attribute is found on the node. This is helpful
* if an attribute uses a pattern and a transformer wants to have a simple
* check to see if the Node has an attribute that matched the pattern.
*
* Example:
*
* "set-flag": "myCustomFlag"
*
* A Node instance can be checked if it has a flag set as shown below:
*
* if (node.hasFlag('myCustomFlag')) { ... }
*
*
*/
setFlag: function(value) {
var attr = this.attr;
attr.setFlag = value;
},
/**
* An attribute can be marked for ignore. Ignored attributes
* will be ignored during compilation.
*/
ignore: function(value) {
var attr = this.attr;
if (value === true) {
attr.ignore = true;
}
}
};
exports.isSupportedProperty = function(name) {
return AttrHandlers.prototype.hasOwnProperty(name);
};
exports.loadAttribute = function loadAttribute(attrName, attrProps, path) {
var attr = new Taglib.Attribute(attrName);
if (attrProps == null) {
attrProps = {
type: 'string'
};
} else if (typeof attrProps === 'string') {
attrProps = {
type: attrProps
};
}
var attrHandlers = new AttrHandlers(attr);
propertyHandlers(attrProps, attrHandlers, path);
return attr;
};

View File

@ -0,0 +1,543 @@
var ok = require('assert').ok;
var Taglib = require('../Taglib');
var propertyHandlers = require('property-handlers');
var isObjectEmpty = require('raptor-util/isObjectEmpty');
var nodePath = require('path');
var resolve = require('../../util/resolve'); // NOTE: different implementation for browser
var ok = require('assert').ok;
var bodyFunctionRegExp = /^([A-Za-z_$][A-Za-z0-9_]*)(?:\(([^)]*)\))?$/;
var safeVarName = /^[A-Za-z_$][A-Za-z0-9_]*$/;
var handleAttributes = require('./handleAttributes');
var Taglib = require('../Taglib');
var propertyHandlers = require('property-handlers');
var forEachEntry = require('raptor-util').forEachEntry;
var loader = require('./loader');
function exists(path) {
try {
require.resolve(path);
return true;
} catch(e) {
return false;
}
}
function removeDashes(str) {
return str.replace(/-([a-z])/g, function (match, lower) {
return lower.toUpperCase();
});
}
function handleVar(tag, value, path) {
var nestedVariable;
if (typeof value === 'string') {
nestedVariable = {
name: value
};
} else {
nestedVariable = {};
propertyHandlers(value, {
name: function(value) {
nestedVariable.name = value;
},
nameFromAttribute: function(value) {
nestedVariable.nameFromAttribute = value;
}
}, path);
if (!nestedVariable.name && !nestedVariable.nameFromAttribute) {
throw new Error('The "name" or "name-from-attribute" attribute is required for a nested variable');
}
}
tag.addNestedVariable(nestedVariable);
}
/**
* We load tag definition using this class. Properties in the taglib
* definition (which is just a JavaScript object with properties)
* are mapped to handler methods in an instance of this type.
*
* @param {Tag} tag The initially empty Tag instance that we populate
* @param {String} dirname The full file system path associated with the tag being loaded
* @param {String} path An informational path associated with this tag (used for error reporting)
*/
function TagHandlers(tag, dirname, path, taglib) {
this.tag = tag;
this.dirname = dirname;
this.path = path;
this.taglib = taglib;
}
TagHandlers.prototype = {
/**
* The tag name
* @param {String} value The tag name
*/
name: function(value) {
var tag = this.tag;
tag.name = value;
},
/**
* The path to the renderer JS module to use for this tag.
*
* NOTE: We use the equivalent of require.resolve to resolve the JS module
* and use the tag directory as the "from".
*
* @param {String} value The renderer path
*/
renderer: function(value) {
var tag = this.tag;
var dirname = this.dirname;
var path = resolve(value, dirname);
tag.renderer = path;
},
/**
* A tag can use a renderer or a template to do the rendering. If
* a template is provided then the value should be the path to the
* template to use to render the custom tag.
*/
template: function(value) {
var tag = this.tag;
var dirname = this.dirname;
var path = nodePath.resolve(dirname, value);
if (!exists(path)) {
throw new Error('Template at path "' + path + '" does not exist.');
}
tag.template = path;
},
/**
* An Object where each property maps to an attribute definition.
* The property key will be the attribute name and the property value
* will be the attribute definition. Example:
* {
* "attributes": {
* "foo": "string",
* "bar": "expression"
* }
* }
*/
attributes: function(value) {
var tag = this.tag;
var path = this.path;
handleAttributes(value, tag, path);
},
/**
* A custom tag can be mapped to a compile-time Node that gets
* added to the parsed Abstract Syntax Tree (AST). The Node can
* then generate custom JS code at compile time. The value
* should be a path to a JS module that gets resolved using the
* equivalent of require.resolve(path)
*/
nodeClass: function(value) {
var tag = this.tag;
var dirname = this.dirname;
var path = resolve(value, dirname);
tag.nodeClass = path;
},
/**
* If the "preserve-whitespace" property is set to true then
* all whitespace nested below the custom tag in a template
* will be stripped instead of going through the normal whitespace
* removal rules.
*/
preserveWhitespace: function(value) {
var tag = this.tag;
tag.preserveWhitespace = !!value;
},
/**
* If a custom tag has an associated transformer then the transformer
* will be called on the compile-time Node. The transformer can manipulate
* the AST using the DOM-like API to change how the code gets generated.
*/
transformer: function(value) {
var tag = this.tag;
var dirname = this.dirname;
var path = this.path;
var transformer = new Taglib.Transformer();
if (typeof value === 'string') {
// The value is a simple string type
// so treat the value as the path to the JS
// module for the transformer
value = {
path: value
};
}
/**
* The transformer is a complex type and we need
* to process each property to load the Transformer
* definition.
*/
propertyHandlers(value, {
path: function(value) {
var path = resolve(value, dirname);
transformer.path = path;
},
priority: function(value) {
transformer.priority = value;
},
name: function(value) {
transformer.name = value;
},
properties: function(value) {
var properties = transformer.properties || (transformer.properties = {});
for (var k in value) {
if (value.hasOwnProperty(k)) {
properties[k] = value[k];
}
}
}
}, 'transformer in ' + path);
ok(transformer.path, '"path" is required for transformer');
tag.addTransformer(transformer);
},
/**
* The "var" property is used to declared nested variables that get
* added as JavaScript variables at compile time.
*
* Examples:
*
* "var": "myScopedVariable",
*
* "var": {
* "name": "myScopedVariable"
* }
*
* "var": {
* "name-from-attribute": "var"
* }
*/
'var': function(value) {
handleVar(this.tag, value, '"var" in tag ' + this.path);
},
/**
* The "vars" property is equivalent to the "var" property
* except that it expects an array of nested variables.
*/
vars: function(value) {
var tag = this.tag;
var self = this;
if (value) {
value.forEach(function(v, i) {
handleVar(tag, v, '"vars"[' + i + '] in tag ' + self.path);
});
}
},
/**
* The "body-function" property" allows the nested body content to be mapped
* to a function at compile time. The body function gets mapped to a property
* of the tag renderer at render time. The body function can have any number
* of parameters.
*
* Example:
* - "body-function": "_handleBody(param1, param2, param3)"
*/
bodyFunction: function(value) {
var tag = this.tag;
var parts = bodyFunctionRegExp.exec(value);
if (!parts) {
throw new Error('Invalid value of "' + value + '" for "body-function". Expected value to be of the following form: <function-name>([param1, param2, ...])');
}
var functionName = parts[1];
var params = parts[2];
if (params) {
params = params.trim().split(/\s*,\s*/);
for (var i=0; i<params.length; i++) {
if (params[i].length === 0) {
throw new Error('Invalid parameters for body-function with value of "' + value + '"');
} else if (!safeVarName.test(params[i])) {
throw new Error('Invalid parameter name of "' + params[i] + '" for body-function with value of "' + value + '"');
}
}
} else {
params = [];
}
tag.setBodyFunction(functionName, params);
},
/**
* The "body-property" property can be used to map the body content
* to a String property on the renderer's input object.
*
* Example:
* "body-property": "label"
*/
bodyProperty: function(value) {
var tag = this.tag;
tag.setBodyProperty(value);
},
/**
* The "import-var" property can be used to add a property to the
* input object of the tag renderer whose value is determined by
* a JavaScript expression.
*
* Example:
* "import-var": {
* "myTargetProperty": "data.myCompileTimeJavaScriptExpression",
* }
*/
importVar: function(value) {
var tag = this.tag;
forEachEntry(value, function(varName, varValue) {
var importedVar = {
targetProperty: varName
};
var expression = varValue;
if (!expression) {
expression = varName;
}
else if (typeof expression === 'object') {
expression = expression.expression;
}
if (!expression) {
throw new Error('Invalid "import-var": ' + require('util').inspect(varValue));
}
importedVar.expression = expression;
tag.addImportedVariable(importedVar);
});
},
/**
* The tag type.
*/
type: function(value) {
var tag = this.tag;
tag.type = value;
},
/**
* Declare a nested tag.
*
* Example:
* {
* ...
* "nested-tags": {
* "tab": {
* "target-property": "tabs",
* "isRepeated": true
* }
* }
* }
*/
nestedTags: function(value) {
var tagPath = this.path;
var taglib = this.taglib;
var dirname = this.dirname;
var tag = this.tag;
forEachEntry(value, function(nestedTagName, nestedTagDef) {
var nestedTag = loadTag(
nestedTagDef,
nestedTagName + ' of ' + tagPath,
taglib,
dirname);
nestedTag.name = nestedTagName;
tag.addNestedTag(nestedTag);
});
}
};
exports.isSupportedProperty = function(name) {
return TagHandlers.prototype.hasOwnProperty(name);
};
function loadTag(tagProps, path, taglib, dirname) {
ok(tagProps);
ok(typeof path === 'string');
ok(taglib);
ok(typeof dirname === 'string');
var tag = new Taglib.Tag(taglib);
if (tagProps.attributes == null) {
// allow any attributes if no attributes are declared
tagProps.attributes = {
'*': 'string'
};
}
var tagHandlers = new TagHandlers(tag, dirname, path, taglib);
// We add a handler for any properties that didn't match
// one of the default property handlers. This is used to
// match properties in the form of "@attr_name" or
// "<nested_tag_name>"
tagHandlers['*'] = function(name, value) {
var parts = name.split(/\s+|\s+[,]\s+/);
var i;
var part;
var hasNestedTag = false;
var hasAttr = false;
var nestedTagTargetProperty = null;
// We do one pass to figure out if there is an
// attribute or nested tag or both
for (i=0; i<parts.length; i++) {
part = parts[i];
if (part.startsWith('@')) {
hasAttr = true;
if (i === 0) {
// Use the first attribute value as the name of the target property
nestedTagTargetProperty = part.substring(1);
}
} else if (part.startsWith('<')) {
hasNestedTag = true;
} else {
// Unmatched property that is not an attribute or a
// nested tag
return false;
}
}
var attrProps = {};
var tagProps = {};
var k;
if (value != null && typeof value === 'object') {
for (k in value) {
if (value.hasOwnProperty(k)) {
if (k.startsWith('@') || k.startsWith('<')) {
// Move over all of the attributes and nested tags
// to the tag definition.
tagProps[k] = value[k];
delete value[k];
} else {
// The property is not a shorthand attribute or shorthand
// tag so move it over to either the tag definition
// or the attribute definition or both the tag definition
// and attribute definition.
var propNameDashes = removeDashes(k);
if (loader.tagLoader.isSupportedProperty(propNameDashes) &&
loader.attributeLoader.isSupportedProperty(propNameDashes)) {
// Move over all of the properties that are associated with a tag
// and attribute
tagProps[k] = value[k];
attrProps[k] = value[k];
delete value[k];
} else if (loader.tagLoader.isSupportedProperty(propNameDashes)) {
// Move over all of the properties that are associated with a tag
tagProps[k] = value[k];
delete value[k];
} else if (loader.attributeLoader.isSupportedProperty(propNameDashes)) {
// Move over all of the properties that are associated with an attr
attrProps[k] = value[k];
delete value[k];
}
}
}
}
// If there are any left over properties then something is wrong
// with the user's taglib.
if (!isObjectEmpty(value)) {
throw new Error('Unsupported properties of [' +
Object.keys(value).join(', ') +
'] for "' + name + '" in "' + path + '"');
}
var type = attrProps.type;
if (!type && hasAttr && hasNestedTag) {
// If we have an attribute and a nested tag then default
// the attribute type to "expression"
attrProps.type = 'expression';
}
} else if (typeof value === 'string') {
if (hasNestedTag && hasAttr) {
tagProps = attrProps = {
type: value
};
} else if (hasNestedTag) {
tagProps = {
type: value
};
} else {
attrProps = {
type: value
};
}
}
// Now that we have separated out attribute properties and tag properties
// we need to create the actual attributes and nested tags
for (i=0; i<parts.length; i++) {
part = parts[i];
if (part.startsWith('@')) {
// This is a shorthand attribute
var attrName = part.substring(1);
var attr = loader.attributeLoader.loadAttribute(
attrName,
attrProps,
'"' + attrName + '" attribute as part of ' + path);
tag.addAttribute(attr);
} else if (part.startsWith('<')) {
// This is a shorthand nested tag
var nestedTag = loadTag(
tagProps,
name + ' of ' + path,
taglib,
dirname);
// We use the '[]' suffix to indicate that a nested tag
// can be repeated
var isNestedTagRepeated = false;
if (part.endsWith('[]')) {
isNestedTagRepeated = true;
part = part.slice(0, -2);
}
var nestedTagName = part.substring(1, part.length-1);
nestedTag.name = nestedTagName;
nestedTag.isRepeated = isNestedTagRepeated;
// Use the name of the attribute as the target property unless
// this target property was explicitly provided
nestedTag.targetProperty = attrProps.targetProperty || nestedTagTargetProperty;
tag.addNestedTag(nestedTag);
} else {
return false;
}
}
};
propertyHandlers(tagProps, tagHandlers, path);
return tag;
}
exports.loadTag = loadTag;

View File

@ -0,0 +1,243 @@
var ok = require('assert').ok;
var nodePath = require('path');
var handleAttributes = require('./handleAttributes');
var scanTagsDir = require('./scanTagsDir');
var resolve = require('../../util/resolve'); // NOTE: different implementation for browser
var propertyHandlers = require('property-handlers');
var Taglib = require('../Taglib');
var taglibReader = require('./taglib-reader');
var loader = require('./loader');
function exists(path) {
try {
require.resolve(path);
return true;
} catch(e) {
return false;
}
}
function handleTag(taglibHandlers, tagName, path) {
var taglib = taglibHandlers.taglib;
var dirname = taglibHandlers.dirname;
ok(path, 'Invalid tag definition for "' + tagName + '"');
var tagObject;
var tagDirname;
if (typeof path === 'string') {
path = nodePath.resolve(dirname, path);
taglib.addInputFile(path);
tagDirname = nodePath.dirname(path);
if (!exists(path)) {
throw new Error('Tag at path "' + path + '" does not exist. Taglib: ' + taglib.id);
}
try {
tagObject = require(path);
} catch(e) {
throw new Error('Unable to parse tag JSON for tag at path "' + path + '"');
}
} else {
tagDirname = dirname; // Tag is in the same taglib file
tagObject = path;
path = '<' + tagName + '> tag in ' + taglib.id;
}
var tag = loader.tagLoader.loadTag(tagObject, path, taglib, tagDirname);
if (tag.name === undefined) {
tag.name = tagName;
}
taglib.addTag(tag);
}
/**
* We load a taglib definion using this class. Properties in the taglib
* definition (which is just a JavaScript object with properties)
* are mapped to handler methods in an instance of this type.
*
*
* @param {Taglib} taglib The initially empty Taglib instance that we will populate
* @param {String} path The file system path to the taglib that we are loading
*/
function TaglibHandlers(taglib, path) {
ok(taglib);
ok(path);
this.taglib = taglib;
this.path = path;
this.dirname = nodePath.dirname(path);
}
TaglibHandlers.prototype = {
attributes: function(value) {
// The value of the "attributes" property will be an object
// where each property maps to an attribute definition. Since these
// attributes are on the taglib they will be "global" attribute
// defintions.
//
// The property key will be the attribute name and the property value
// will be the attribute definition. Example:
// {
// "attributes": {
// "foo": "string",
// "bar": "expression"
// }
// }
var taglib = this.taglib;
var path = this.path;
handleAttributes(value, taglib, path);
},
tags: function(tags) {
// The value of the "tags" property will be an object
// where each property maps to an attribute definition. The property
// key will be the tag name and the property value
// will be the tag definition. Example:
// {
// "tags": {
// "foo": {
// "attributes": { ... }
// },
// "bar": {
// "attributes": { ... }
// },
// }
// }
for (var tagName in tags) {
if (tags.hasOwnProperty(tagName)) {
handleTag(this, tagName, tags[tagName]);
}
}
},
tagsDir: function(dir) {
// The "tags-dir" property is used to supporting scanning
// of a directory to discover custom tags. Scanning a directory
// is a much simpler way for a developer to create custom tags.
// Only one tag is allowed per directory and the directory name
// corresponds to the tag name. We only search for directories
// one level deep.
var taglib = this.taglib;
var path = this.path;
var dirname = this.dirname;
if (Array.isArray(dir)) {
for (var i = 0; i < dir.length; i++) {
scanTagsDir(path, dirname, dir[i], taglib);
}
} else {
scanTagsDir(path, dirname, dir, taglib);
}
},
taglibImports: function(imports) {
// The "taglib-imports" property allows another taglib to be imported
// into this taglib so that the tags defined in the imported taglib
// will be part of this taglib.
//
// NOTE: If a taglib import refers to a package.json file then we read
// the package.json file and automatically import *all* of the
// taglibs from the installed modules found in the "dependencies"
// section
var taglib = this.taglib;
var dirname = this.dirname;
if (imports && Array.isArray(imports)) {
for (var i=0; i<imports.length; i++) {
var curImport = imports[i];
if (typeof curImport === 'string') {
var basename = nodePath.basename(curImport);
if (basename === 'package.json') {
var packagePath = resolve(curImport, dirname);
var pkg = require(packagePath);
var dependencies = pkg.dependencies;
if (dependencies) {
var dependencyNames = Object.keys(dependencies);
for (var j=0; j<dependencyNames.length; j++) {
var dependencyName = dependencyNames[j];
var importPath;
try {
importPath = require('resolve-from')(dirname, dependencyName + '/marko-taglib.json');
} catch(e) {}
if (importPath) {
taglib.addImport(importPath);
}
}
}
}
}
}
}
},
textTransformer: function(value) {
// Marko allows a "text-transformer" to be registered. The provided
// text transformer will be called for any static text found in a template.
var taglib = this.taglib;
var path = this.path;
var dirname = this.dirname;
var transformer = new Taglib.Transformer();
if (typeof value === 'string') {
value = {
path: value
};
}
propertyHandlers(value, {
path: function(value) {
var path = resolve(value, dirname);
transformer.path = path;
}
}, 'text-transformer in ' + path);
ok(transformer.path, '"path" is required for transformer');
taglib.addTextTransformer(transformer);
}
};
exports.loadTaglib = function(path) {
var taglibProps = taglibReader.readTaglib(path);
var taglib = new Taglib(path);
taglib.addInputFile(path);
var taglibHandlers = new TaglibHandlers(taglib, path);
// We register a wildcard handler to handle "@my-attr" and "<my-tag>"
// properties (shorthand syntax)
taglibHandlers['*'] = function(name, value) {
var taglib = this.taglib;
var path = this.path;
if (name.startsWith('<')) {
handleTag(this, name.slice(1, -1), value);
} else if (name.startsWith('@')) {
var attrName = name.substring(1);
var attr = loader.attributeLoader.loadAttribute(
attrName,
value,
'"' + attrName + '" attribute as part of ' + path);
taglib.addAttribute(attr);
} else {
return false;
}
};
propertyHandlers(taglibProps, taglibHandlers, path);
taglib.id = taglib.path = path;
return taglib;
};

View File

@ -0,0 +1,3 @@
exports.taglibLoader = require('./loader-taglib');
exports.tagLoader = require('./loader-tag');
exports.attributeLoader = require('./loader-attribute');

View File

@ -0,0 +1,6 @@
{
"browser": {
"./scanTagsDir.js": "./scanTagsDir-browser.js",
"./taglib-reader.js": "./taglib-reader-browser.js"
}
}

View File

@ -0,0 +1,3 @@
module.exports = function scanTagsDir() {
// no-op in the browser
};

View File

@ -0,0 +1,128 @@
var nodePath = require('path');
var fs = require('fs');
var jsonminify = require('jsonminify');
var tagDefFromCode = require('./tag-def-from-code');
var loader = require('./loader');
function createDefaultTagDef() {
return {
attributes: {
'*': {
type: 'string',
targetProperty: null,
preserveName: false
}
}
};
}
/**
* @param {String} tagsConfigPath path to tag definition file
* @param {String} tagsConfigDirname path to directory of tags config file (should be path.dirname(tagsConfigPath))
* @param {String|Object} dir the path to directory to scan
* @param {String} taglib the taglib that is being loaded
*/
module.exports = function scanTagsDir(tagsConfigPath, tagsConfigDirname, dir, taglib) {
var prefix;
if (typeof dir === 'object') {
prefix = dir.prefix;
dir = dir.path;
}
if (prefix == null) {
// no prefix by default
prefix = '';
}
dir = nodePath.resolve(tagsConfigDirname, dir);
var children = fs.readdirSync(dir);
var rendererJSFile;
for (var i=0, len=children.length; i<len; i++) {
rendererJSFile = null;
var childFilename = children[i];
if (childFilename === 'node_modules') {
continue;
}
var tagName = prefix + childFilename;
var tagDirname = nodePath.join(dir, childFilename);
var tagFile = nodePath.join(dir, childFilename, 'marko-tag.json');
var tag = null;
var rendererFile = nodePath.join(dir, childFilename, 'renderer.js');
var indexFile = nodePath.join(dir, childFilename, 'index.js');
var templateFile = nodePath.join(dir, childFilename, 'template.marko');
var tagDef = null;
// Record dependencies so that we can check if a template is up-to-date
taglib.addInputFile(tagFile);
taglib.addInputFile(rendererFile);
if (fs.existsSync(tagFile)) {
// marko-tag.json exists in the directory, use that as the tag definition
tagDef = JSON.parse(jsonminify(fs.readFileSync(tagFile, {encoding: 'utf8'})));
if (!tagDef.renderer && !tagDef.template) {
if (fs.existsSync(rendererFile)) {
tagDef.renderer = rendererFile;
} else if (fs.existsSync(indexFile)) {
tagDef.renderer = indexFile;
} else if (fs.existsSync(templateFile)) {
tagDef.template = templateFile;
} else if (fs.existsSync(templateFile + ".html")) {
tagDef.template = templateFile + ".html";
} else {
throw new Error('Invalid tag file: ' + tagFile + '. Neither a renderer or a template was found for tag.');
}
}
tag = loader.tagLoader.loadTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tag.name || tagName;
taglib.addTag(tag);
} else {
// marko-tag.json does *not* exist... checking for a 'renderer.js'
if (fs.existsSync(rendererFile)) {
rendererJSFile = rendererFile;
} else if (fs.existsSync(indexFile)) {
rendererJSFile = indexFile;
} else {
var exTemplateFile;
if (fs.existsSync(templateFile)) {
exTemplateFile = templateFile;
}
else if (fs.existsSync(templateFile + ".html")){
exTemplateFile = templateFile + ".html";
}
if(exTemplateFile){
var templateCode = fs.readFileSync(exTemplateFile, {encoding: 'utf8'});
tagDef = tagDefFromCode.extractTagDef(templateCode);
if (!tagDef) {
tagDef = createDefaultTagDef();
}
tagDef.template = exTemplateFile;
}
}
if (rendererJSFile) {
var rendererCode = fs.readFileSync(rendererJSFile, {encoding: 'utf8'});
tagDef = tagDefFromCode.extractTagDef(rendererCode);
if (!tagDef) {
tagDef = createDefaultTagDef();
}
tagDef.renderer = rendererJSFile;
tag = loader.tagLoader.loadTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tagName;
taglib.addTag(tag);
}
if (tagDef) {
tag = loader.tagLoader.loadTag(tagDef, tagsConfigPath, taglib, tagDirname);
tag.name = tagName;
taglib.addTag(tag);
}
}
}
};

View File

@ -0,0 +1,11 @@
exports.readTaglib = function (path) {
var taglibProps;
try {
taglibProps = require(path);
} catch(e) {
throw new Error('Unable to parse taglib JSON at path "' + path + '". Exception: ' + e);
}
return taglibProps;
};

View File

@ -0,0 +1,13 @@
var fs = require('fs');
var jsonminify = require('jsonminify');
exports.readTaglib = function (path) {
var json = fs.readFileSync(path, 'utf8');
try {
var taglibProps = JSON.parse(jsonminify(json));
return taglibProps;
} catch(e) {
throw new Error('Unable to parse taglib at path "' + path + '". Error: ' + e);
}
};

View File

@ -14,7 +14,9 @@
"scripts": {
"test": "node_modules/.bin/mocha --ui bdd --reporter spec ./test && node_modules/.bin/jshint compiler/ runtime/ taglibs/",
"test-fast": "node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-test",
"test-async": "node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-async-test"
"test-async": "node_modules/.bin/mocha --ui bdd --reporter spec ./test/render-async-test",
"test-taglib-loader": "node_modules/.bin/mocha --ui bdd --reporter spec ./test/taglib-loader-test",
"jshint": "node_modules/.bin/jshint compiler/ runtime/ taglibs/"
},
"author": "Patrick Steele-Idem <pnidem@gmail.com>",
"maintainers": [
@ -61,5 +63,5 @@
"publishConfig": {
"registry": "https://registry.npmjs.org/"
},
"version": "2.3.2"
"version": "2.4.0"
}

View File

@ -155,30 +155,61 @@ module.exports = {
/**
* Invoke a tag handler render function
*/
t: function (out, renderFunc, input, body, hasOutParam) {
t: function (out, renderFunc, input, renderBody, options) {
if (!input) {
input = {};
}
if (body) {
input.renderBody = body;
input.invokeBody = function() {
if (!WARNED_INVOKE_BODY) {
WARNED_INVOKE_BODY = 1;
logger.warn('invokeBody(...) deprecated. Use renderBody(out) instead.', new Error().stack);
}
var hasOutParam;
var targetProperty;
var parent;
var hasNestedTags;
var isRepeated;
if (!hasOutParam) {
var args = arrayFromArguments(arguments);
args.unshift(out);
body.apply(this, args);
} else {
body.apply(this, arguments);
}
};
if (options) {
hasOutParam = options.hasOutParam;
parent = options.parent;
targetProperty = options.targetProperty;
hasNestedTags = options.hasNestedTags;
isRepeated = options.isRepeated;
}
renderFunc(input, out);
if (renderBody) {
if (hasNestedTags) {
renderBody(out, input);
} else {
input.renderBody = renderBody;
input.invokeBody = function() {
if (!WARNED_INVOKE_BODY) {
WARNED_INVOKE_BODY = 1;
logger.warn('invokeBody(...) deprecated. Use renderBody(out) instead.', new Error().stack);
}
if (!hasOutParam) {
var args = arrayFromArguments(arguments);
args.unshift(out);
renderBody.apply(this, args);
} else {
renderBody.apply(this, arguments);
}
};
}
}
if (renderFunc) {
renderFunc(input, out);
} else if (targetProperty) {
if (isRepeated) {
var existingArray = parent[targetProperty];
if (existingArray) {
existingArray.push(input);
} else {
parent[targetProperty] = [input];
}
} else {
parent[targetProperty] = input;
}
}
},
c: function (out, func) {
var output = out.captureString(func);

View File

@ -70,6 +70,28 @@ function getPropsStr(props, template) {
return '{}';
}
}
function getNextNestedTagVarName(template) {
if (template.data.nextNestedTagId == null) {
template.data.nextNestedTagId = 0;
}
return '__nestedTagInput' + (template.data.nextNestedTagId++);
}
function getNestedTagParentNode(nestedTagNode, tag) {
var parentTagName = tag.parentTagName;
var currentNode = nestedTagNode.parentNode;
while (currentNode) {
if (currentNode.localName === parentTagName) {
return currentNode;
}
currentNode = currentNode.parentNode;
}
}
function TagHandlerNode(tag) {
if (!this.nodeType) {
TagHandlerNode.$super.call(this);
@ -112,13 +134,41 @@ TagHandlerNode.prototype = {
doGenerateCode: function (template) {
template.addStaticVar('__renderer', '__helpers.r');
var _this = this;
var rendererPath = template.getRequirePath(this.tag.renderer); // Resolve a path to the renderer relative to the directory of the template
var handlerVar = addHandlerVar(template, rendererPath);
var tagHelperVar = template.addStaticVar('__tag', '__helpers.t');
var bodyFunction = this.tag.bodyFunction;
var bodyProperty = this.tag.bodyProperty;
var tag = this.tag;
this.tag.forEachImportedVariable(function (importedVariable) {
var rendererPath;
var handlerVar;
if (tag.renderer) {
rendererPath = template.getRequirePath(this.tag.renderer); // Resolve a path to the renderer relative to the directory of the template
handlerVar = addHandlerVar(template, rendererPath);
}
var bodyFunction = tag.bodyFunction;
var bodyProperty = tag.bodyProperty;
var isNestedTag = tag.isNestedTag === true;
var hasNestedTags = tag.hasNestedTags();
var tagHelperVar = template.addStaticVar('__tag', '__helpers.t');
var nestedTagVar;
var nestedTagParentNode = null;
if (isNestedTag) {
nestedTagParentNode = getNestedTagParentNode(this, tag);
if (nestedTagParentNode == null) {
this.addError('Invalid usage of the ' + this + ' nested tag. Tag not nested within a <' + tag.parentTagName + '> tag.');
return;
}
nestedTagVar = nestedTagParentNode.data.nestedTagVar;
}
if (hasNestedTags) {
nestedTagVar = this.data.nestedTagVar = getNextNestedTagVarName(template);
}
tag.forEachImportedVariable(function (importedVariable) {
this.setProperty(importedVariable.targetProperty, template.makeExpression(importedVariable.expression));
}, this);
@ -138,7 +188,7 @@ TagHandlerNode.prototype = {
var variableNames = [];
_this.tag.forEachVariable(function (nestedVar) {
tag.forEachVariable(function (nestedVar) {
var varName;
if (nestedVar.nameFromAttribute) {
var possibleNameAttributes = nestedVar.nameFromAttribute.split(/\s+or\s+|\s*,\s*/i);
@ -178,7 +228,7 @@ TagHandlerNode.prototype = {
template.functionCall(tagHelperVar, function () {
template.code('out,\n').indent(function () {
template.line(handlerVar + ',').indent();
template.line((handlerVar ? handlerVar : 'null') + ',').indent();
if (_this.dynamicAttributes) {
template.indent(function() {
@ -215,32 +265,56 @@ TagHandlerNode.prototype = {
template.code(propsCode);
if (_this.hasChildren() && !_this.tag.bodyFunction) {
var bodyParams = [];
var hasOutParam = false;
var hasOutParam = false;
variableNames.forEach(function (varName) {
if (varName === 'out') {
hasOutParam = true;
}
bodyParams.push(varName);
});
if (_this.hasChildren() && !tag.bodyFunction) {
var bodyParams = [];
if (hasNestedTags) {
bodyParams.push(nestedTagVar);
} else {
variableNames.forEach(function (varName) {
if (varName === 'out') {
hasOutParam = true;
}
bodyParams.push(varName);
});
}
var params;
if (hasOutParam) {
params = bodyParams.join(',');
} else {
params = 'out' + (bodyParams.length ? ',' + bodyParams.join(',') : '');
params = 'out' + (bodyParams.length ? ', ' + bodyParams.join(', ') : '');
}
template.code(',\n').line('function(' + params + ') {').indent(function () {
_this.generateCodeForChildren(template);
}).indent().code('}');
}
if (hasNestedTags || isNestedTag || hasOutParam) {
var options = [];
if (hasNestedTags) {
options.push('hasNestedTags: 1');
}
if (hasOutParam) {
template.code(',\n').code(template.indentStr() + '1');
options.push('hasOutParam: 1');
}
if (isNestedTag) {
options.push('targetProperty: ' + JSON.stringify(tag.targetProperty));
options.push('parent: ' + nestedTagVar);
if (tag.isRepeated) {
options.push('isRepeated: 1');
}
}
template.code(',\n').code(template.indentStr() + '{ ' + options.join(', ') + ' }');
}
});
});

View File

@ -269,24 +269,23 @@ module.exports = function transform(node, compiler, template) {
node.setPreserveWhitespace(true);
}
if (tag.renderer || tag.template) {
if (tag.renderer || tag.isNestedTag) {
shouldRemoveAttr = false;
if (tag.renderer) {
//Instead of compiling as a static XML element, we'll
//make the node render as a tag handler node so that
//writes code that invokes the handler
TagHandlerNode.convertNode(node, tag);
if (inputAttr) {
node.setInputExpression(template.makeExpression(inputAttr));
}
} else {
var templatePath = compiler.getRequirePath(tag.template);
// The tag is mapped to a template that will be used to perform
// the rendering so convert the node into a "IncludeNode" that can
// be used to include the output of rendering a template
IncludeNode.convertNode(node, templatePath);
//Instead of compiling as a static XML element, we'll
//make the node render as a tag handler node so that
//writes code that invokes the handler
TagHandlerNode.convertNode(node, tag);
if (inputAttr) {
node.setInputExpression(template.makeExpression(inputAttr));
}
} else if (tag.template) {
shouldRemoveAttr = false;
var templatePath = compiler.getRequirePath(tag.template);
// The tag is mapped to a template that will be used to perform
// the rendering so convert the node into a "IncludeNode" that can
// be used to include the output of rendering a template
IncludeNode.convertNode(node, templatePath);
} else if (tag.nodeClass) {
shouldRemoveAttr = false;

View File

@ -1,7 +1,6 @@
'use strict';
var chai = require('chai');
chai.Assertion.includeStack = true;
require('chai').should();
var expect = require('chai').expect;
var nodePath = require('path');
var marko = require('../');

View File

@ -1,7 +1,6 @@
'use strict';
var chai = require('chai');
chai.Assertion.includeStack = true;
require('chai').should();
var nodePath = require('path');
var fs = require('fs');

View File

@ -116,6 +116,8 @@
"template": "./taglib/test-circular-template-b/template.marko"
}
},
"<test-nested-tags-tabs>": "./taglib/test-nested-tags-tabs/marko-tag.json",
"<test-nested-tags-overlay>": "./taglib/test-nested-tags-overlay/marko-tag.json",
"tags-dir": "./taglib/scanned-tags",
"taglib-imports": ["./package.json"]
}

View File

@ -0,0 +1,25 @@
{
"<shorthand-checkbox>": {
"@label <label>": "string",
"@checked": "boolean",
"<checked>": "boolean"
},
"<shorthand-overlay>": {
"nested-tags": {
"body": {
"@condensed": "boolean"
}
}
},
"tags": {
"shorthand-button": {
"@label": "string"
},
"shorthand-tabs": {
"@tabs <tab>[]": {
"@label": "string"
},
"@orientation": "string"
}
}
}

View File

@ -0,0 +1,23 @@
{
"renderer": "./renderer",
"@header <header>": {
"@class": {
"type": "string",
"target-property": "className"
}
},
"@body <body>": {
"@class": {
"type": "string",
"target-property": "className"
}
},
"nested-tags": {
"footer": {
"@class": {
"type": "string",
"target-property": "className"
}
}
}
}

View File

@ -0,0 +1,15 @@
var marko = require('../../../../');
var template = marko.load(require.resolve('./template.marko'));
exports.renderer = function(input, out) {
var header = input.header;
var body = input.body;
var footer = input.footer;
template.render({
header: header,
body: body,
footer: footer
}, out);
};

View File

@ -0,0 +1,13 @@
<div class="overlay">
<div class="overlay-header ${data.header.className}" if="data.header">
<invoke function="data.header.renderBody(out)"/>
</div>
<div class="overlay-body ${data.body.className}" if="data.body">
<invoke function="data.body.renderBody(out)"/>
</div>
<div class="overlay-footer ${data.footer.className}" if="data.footer">
<invoke function="data.footer.renderBody(out)"/>
</div>
</div>

View File

@ -0,0 +1,7 @@
{
"renderer": "./renderer",
"@orientation": "string",
"@tabs <tab>[]": {
"@title": "string"
}
}

View File

@ -0,0 +1,11 @@
var marko = require('../../../../');
var template = marko.load(require.resolve('./template.marko'));
exports.renderer = function(input, out) {
var tabs = input.tabs;
template.render({
tabs: tabs
}, out);
};

View File

@ -0,0 +1,14 @@
<var name="tabs" value="data.tabs"/>
<div class="tabs">
<ul>
<li for="tab in tabs">
${tab.title}
</li>
</ul>
<div class="tab-content">
<div class="tab" for="tab in tabs">
<invoke function="tab.renderBody(out)"/>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
<div>
<test-nested-tags-overlay.header class="my-header">
Header content
</test-nested-tags-overlay.header>
Body content
<test-nested-tags-overlay.footer class="my-footer">
Footer content
</test-nested-tags-overlay.footer>
</div>

View File

@ -0,0 +1,11 @@
var expect = require('chai').expect;
exports.templateData = {};
exports.options = {
handleCompileError: function(e) {
expect(e.toString()).to.contain('Tag not nested within a');
expect(e.toString()).to.contain('<test-nested-tags-overlay>');
expect(e.toString()).to.contain('<test-nested-tags-overlay.header>');
expect(e.toString()).to.contain('<test-nested-tags-overlay.footer>');
}
};

View File

@ -0,0 +1 @@
<div class="tabs"><ul><li>Tab 1</li><li>Tab 3</li></ul><div class="tab-content"><div class="tab">Tab 1 content</div><div class="tab">Tab 3 content</div></div></div>

View File

@ -0,0 +1,14 @@
<test-nested-tags-tabs>
<test-nested-tags-tabs.tab title="Tab 1">
Tab 1 content
</test-nested-tags-tabs.tab>
<test-nested-tags-tabs.tab title="Tab 2" if="false">
Tab 2 content
</test-nested-tags-tabs.tab>
<test-nested-tags-tabs.tab title="Tab 3">
Tab 3 content
</test-nested-tags-tabs.tab>
</test-nested-tags-tabs>

View File

@ -0,0 +1 @@
exports.templateData = {};

View File

@ -0,0 +1 @@
<div class="overlay"><div class="overlay-header ">Header content!</div><div class="overlay-body my-body">Body content</div><div class="overlay-footer my-footer">Footer content</div></div>

View File

@ -0,0 +1,10 @@
<test-nested-tags-overlay header="data.header">
<test-nested-tags-overlay.body class="my-body">
Body content
</test-nested-tags-overlay.body>
<test-nested-tags-overlay.footer class="my-footer">
Footer content
</test-nested-tags-overlay.footer>
</test-nested-tags-overlay>

View File

@ -0,0 +1,7 @@
exports.templateData = {
header: {
renderBody: function(out) {
out.write('Header content!');
}
}
};

View File

@ -0,0 +1,43 @@
'use strict';
var chai = require('chai');
chai.Assertion.includeStack = true;
require('chai').should();
var expect = require('chai').expect;
var nodePath = require('path');
describe('taglib-loader' , function() {
beforeEach(function(done) {
for (var k in require.cache) {
if (require.cache.hasOwnProperty(k)) {
delete require.cache[k];
}
}
done();
});
it('should load a taglib with shorthand attributes and tags', function() {
var taglibLoader = require('../compiler/taglibs').loader;
var taglib = taglibLoader.load(nodePath.join(__dirname, 'fixtures/taglib-shorthand/marko-taglib.json'));
expect(taglib != null).to.equal(true);
var shorthandCheckbox = taglib.tags['shorthand-checkbox'];
expect(shorthandCheckbox.attributes.checked.type).to.equal('boolean');
expect(shorthandCheckbox.attributes.label.type).to.equal('string');
expect(shorthandCheckbox.nestedTags.label.type).to.equal('string');
expect(shorthandCheckbox.nestedTags.checked.type).to.equal('boolean');
var shorthandTabsTag = taglib.tags['shorthand-tabs'];
expect(shorthandTabsTag.attributes.orientation != null).to.equal(true);
expect(shorthandTabsTag.attributes.orientation.type).to.equal('string');
expect(shorthandTabsTag.attributes.tabs.type).to.equal('expression');
var nestedTabTag = shorthandTabsTag.nestedTags.tab;
expect(nestedTabTag.attributes.label != null).to.equal(true);
expect(nestedTabTag.isRepeated).to.equal(true);
expect(nestedTabTag.targetProperty).to.equal('tabs');
});
});

View File

@ -22,11 +22,6 @@ function createTestRender(options) {
var actualPath = nodePath.join(dir, 'actual.html');
options = options || {};
// if (foundDirs[dir]) {
// console.log('DUPLICATE DIRECTORY: ' + dir);
// }
// foundDirs[dir] = true;
var marko = require('../');
require('../compiler').defaultOptions.checkUpToDate = false;
@ -34,7 +29,25 @@ function createTestRender(options) {
var AsyncWriter = marko.AsyncWriter;
var out = options.out || new AsyncWriter(new StringBuilder());
marko.render(templatePath, templateData, out)
var template;
try {
template = marko.load(templatePath);
} catch(err) {
if (options.handleCompileError) {
try {
options.handleCompileError(err);
done();
} catch(err) {
done(err);
}
} else {
done(err);
}
return;
}
template.render(templateData, out)
.on('finish', function() {
var output = out.getOutput();
@ -56,7 +69,18 @@ function createTestRender(options) {
done();
})
.on('error', done)
.on('error', function(err) {
if (options.handleError) {
try {
options.handleError(err);
done();
} catch(err) {
done(err);
}
} else {
done(err);
}
})
.end();
};
@ -83,7 +107,7 @@ exports.loadRenderTests = function(dirname, desc, options) {
var testOptions = testInfo.options;
testRender(dir, templateData || {}, expectedFile, testOptions, done);
testRender(dir, templateData || {}, expectedFile, testOptions || {}, done);
});
}