mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
Merge pull request #68 from patrick-steele-idem/parent-child
Fixes #61 Simplify parent/child relationships
This commit is contained in:
commit
d4e53cc0df
178
CHANGELOG.md
178
CHANGELOG.md
@ -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
376
README.md
@ -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/)__
|
||||
|
||||

|
||||
|
||||
Syntax highlighting available for [Atom](https://atom.io/) by installing the [language-marko](https://atom.io/packages/language-marko) package.
|
||||
|
||||

|
||||

|
||||
|
||||
<!-- 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>
|
||||
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
@ -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;
|
||||
12
compiler/taglibs/Taglib/Attribute.js
Normal file
12
compiler/taglibs/Taglib/Attribute.js
Normal 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;
|
||||
}
|
||||
});
|
||||
8
compiler/taglibs/Taglib/ImportedVariable.js
Normal file
8
compiler/taglibs/Taglib/ImportedVariable.js
Normal file
@ -0,0 +1,8 @@
|
||||
var makeClass = require('raptor-util').makeClass;
|
||||
|
||||
module.exports = makeClass({
|
||||
$init: function() {
|
||||
this.targetProperty = null;
|
||||
this.expression = null;
|
||||
}
|
||||
});
|
||||
7
compiler/taglibs/Taglib/NestedVariable.js
Normal file
7
compiler/taglibs/Taglib/NestedVariable.js
Normal file
@ -0,0 +1,7 @@
|
||||
var makeClass = require('raptor-util').makeClass;
|
||||
|
||||
module.exports = makeClass({
|
||||
$init: function() {
|
||||
this.name = null;
|
||||
}
|
||||
});
|
||||
9
compiler/taglibs/Taglib/Property.js
Normal file
9
compiler/taglibs/Taglib/Property.js
Normal 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;
|
||||
}
|
||||
});
|
||||
171
compiler/taglibs/Taglib/Tag.js
Normal file
171
compiler/taglibs/Taglib/Tag.js
Normal 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;
|
||||
}
|
||||
});
|
||||
42
compiler/taglibs/Taglib/Transformer.js
Normal file
42
compiler/taglibs/Taglib/Transformer.js
Normal 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 + ']';
|
||||
}
|
||||
});
|
||||
94
compiler/taglibs/Taglib/index.js
Normal file
94
compiler/taglibs/Taglib/index.js
Normal 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;
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
16
compiler/taglibs/taglib-loader/handleAttributes.js
Normal file
16
compiler/taglibs/taglib-loader/handleAttributes.js
Normal 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);
|
||||
});
|
||||
};
|
||||
20
compiler/taglibs/taglib-loader/index.js
Normal file
20
compiler/taglibs/taglib-loader/index.js
Normal 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;
|
||||
166
compiler/taglibs/taglib-loader/loader-attribute.js
Normal file
166
compiler/taglibs/taglib-loader/loader-attribute.js
Normal 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;
|
||||
};
|
||||
543
compiler/taglibs/taglib-loader/loader-tag.js
Normal file
543
compiler/taglibs/taglib-loader/loader-tag.js
Normal 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;
|
||||
243
compiler/taglibs/taglib-loader/loader-taglib.js
Normal file
243
compiler/taglibs/taglib-loader/loader-taglib.js
Normal 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;
|
||||
};
|
||||
3
compiler/taglibs/taglib-loader/loader.js
Normal file
3
compiler/taglibs/taglib-loader/loader.js
Normal file
@ -0,0 +1,3 @@
|
||||
exports.taglibLoader = require('./loader-taglib');
|
||||
exports.tagLoader = require('./loader-tag');
|
||||
exports.attributeLoader = require('./loader-attribute');
|
||||
6
compiler/taglibs/taglib-loader/package.json
Normal file
6
compiler/taglibs/taglib-loader/package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"browser": {
|
||||
"./scanTagsDir.js": "./scanTagsDir-browser.js",
|
||||
"./taglib-reader.js": "./taglib-reader-browser.js"
|
||||
}
|
||||
}
|
||||
3
compiler/taglibs/taglib-loader/scanTagsDir-browser.js
Normal file
3
compiler/taglibs/taglib-loader/scanTagsDir-browser.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = function scanTagsDir() {
|
||||
// no-op in the browser
|
||||
};
|
||||
128
compiler/taglibs/taglib-loader/scanTagsDir.js
Normal file
128
compiler/taglibs/taglib-loader/scanTagsDir.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
11
compiler/taglibs/taglib-loader/taglib-reader-browser.js
Normal file
11
compiler/taglibs/taglib-loader/taglib-reader-browser.js
Normal 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;
|
||||
};
|
||||
13
compiler/taglibs/taglib-loader/taglib-reader.js
Normal file
13
compiler/taglibs/taglib-loader/taglib-reader.js
Normal 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);
|
||||
}
|
||||
};
|
||||
@ -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"
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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(', ') + ' }');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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('../');
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
2
test/fixtures/marko-taglib.json
vendored
2
test/fixtures/marko-taglib.json
vendored
@ -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"]
|
||||
}
|
||||
25
test/fixtures/taglib-shorthand/marko-taglib.json
vendored
Normal file
25
test/fixtures/taglib-shorthand/marko-taglib.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
test/fixtures/taglib/test-nested-tags-overlay/marko-tag.json
vendored
Normal file
23
test/fixtures/taglib/test-nested-tags-overlay/marko-tag.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/fixtures/taglib/test-nested-tags-overlay/renderer.js
vendored
Normal file
15
test/fixtures/taglib/test-nested-tags-overlay/renderer.js
vendored
Normal 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);
|
||||
|
||||
};
|
||||
13
test/fixtures/taglib/test-nested-tags-overlay/template.marko
vendored
Normal file
13
test/fixtures/taglib/test-nested-tags-overlay/template.marko
vendored
Normal 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>
|
||||
7
test/fixtures/taglib/test-nested-tags-tabs/marko-tag.json
vendored
Normal file
7
test/fixtures/taglib/test-nested-tags-tabs/marko-tag.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"renderer": "./renderer",
|
||||
"@orientation": "string",
|
||||
"@tabs <tab>[]": {
|
||||
"@title": "string"
|
||||
}
|
||||
}
|
||||
11
test/fixtures/taglib/test-nested-tags-tabs/renderer.js
vendored
Normal file
11
test/fixtures/taglib/test-nested-tags-tabs/renderer.js
vendored
Normal 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);
|
||||
|
||||
};
|
||||
14
test/fixtures/taglib/test-nested-tags-tabs/template.marko
vendored
Normal file
14
test/fixtures/taglib/test-nested-tags-tabs/template.marko
vendored
Normal 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>
|
||||
0
test/fixtures/templates/nested-tags-missing-parent/expected.html
vendored
Normal file
0
test/fixtures/templates/nested-tags-missing-parent/expected.html
vendored
Normal file
12
test/fixtures/templates/nested-tags-missing-parent/template.marko
vendored
Normal file
12
test/fixtures/templates/nested-tags-missing-parent/template.marko
vendored
Normal 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>
|
||||
11
test/fixtures/templates/nested-tags-missing-parent/test.js
vendored
Normal file
11
test/fixtures/templates/nested-tags-missing-parent/test.js
vendored
Normal 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>');
|
||||
}
|
||||
};
|
||||
1
test/fixtures/templates/nested-tags-repeated/expected.html
vendored
Normal file
1
test/fixtures/templates/nested-tags-repeated/expected.html
vendored
Normal 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>
|
||||
14
test/fixtures/templates/nested-tags-repeated/template.marko
vendored
Normal file
14
test/fixtures/templates/nested-tags-repeated/template.marko
vendored
Normal 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>
|
||||
1
test/fixtures/templates/nested-tags-repeated/test.js
vendored
Normal file
1
test/fixtures/templates/nested-tags-repeated/test.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
exports.templateData = {};
|
||||
1
test/fixtures/templates/nested-tags/expected.html
vendored
Normal file
1
test/fixtures/templates/nested-tags/expected.html
vendored
Normal 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>
|
||||
10
test/fixtures/templates/nested-tags/template.marko
vendored
Normal file
10
test/fixtures/templates/nested-tags/template.marko
vendored
Normal 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>
|
||||
7
test/fixtures/templates/nested-tags/test.js
vendored
Normal file
7
test/fixtures/templates/nested-tags/test.js
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
exports.templateData = {
|
||||
header: {
|
||||
renderBody: function(out) {
|
||||
out.write('Header content!');
|
||||
}
|
||||
}
|
||||
};
|
||||
43
test/taglib-loader-test.js
Normal file
43
test/taglib-loader-test.js
Normal 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');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
40
test/util.js
40
test/util.js
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user