mirror of
https://github.com/Polymer/polymer.git
synced 2025-02-25 18:55:30 -06:00
395 lines
14 KiB
HTML
395 lines
14 KiB
HTML
<!--
|
|
@license
|
|
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
|
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
Code distributed by Google as part of the polymer project is also
|
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
-->
|
|
<link rel="import" href="../case-map.html">
|
|
|
|
<script>
|
|
/**
|
|
* Scans a template to produce an annotation list that that associates
|
|
* metadata culled from markup with tree locations
|
|
* metadata and information to associate the metadata with nodes in an instance.
|
|
*
|
|
* Supported expressions include:
|
|
*
|
|
* Double-mustache annotations in text content. The annotation must be the only
|
|
* content in the tag, compound expressions are not supported.
|
|
*
|
|
* <[tag]>{{annotation}}<[tag]>
|
|
*
|
|
* Double-escaped annotations in an attribute, either {{}} or [[]].
|
|
*
|
|
* <[tag] someAttribute="{{annotation}}" another="[[annotation]]"><[tag]>
|
|
*
|
|
* `on-` style event declarations.
|
|
*
|
|
* <[tag] on-<event-name>="annotation"><[tag]>
|
|
*
|
|
* Note that the `annotations` feature does not implement any behaviors
|
|
* associated with these expressions, it only captures the data.
|
|
*
|
|
* Generated data-structure:
|
|
*
|
|
* [
|
|
* {
|
|
* id: '<id>',
|
|
* events: [
|
|
* {
|
|
* name: '<name>'
|
|
* value: '<annotation>'
|
|
* }, ...
|
|
* ],
|
|
* bindings: [
|
|
* {
|
|
* kind: ['text'|'attribute'],
|
|
* mode: ['{'|'['],
|
|
* name: '<name>'
|
|
* value: '<annotation>'
|
|
* }, ...
|
|
* ],
|
|
* // TODO(sjmiles): this is annotation-parent, not node-parent
|
|
* parent: <reference to parent annotation object>,
|
|
* index: <integer index in parent's childNodes collection>
|
|
* },
|
|
* ...
|
|
* ]
|
|
*
|
|
* @class Template feature
|
|
*/
|
|
|
|
// null-array (shared empty array to avoid null-checks)
|
|
Polymer.nar = [];
|
|
|
|
Polymer.Annotations = {
|
|
|
|
// preprocess-time
|
|
|
|
// construct and return a list of annotation records
|
|
// by scanning `template`'s content
|
|
//
|
|
parseAnnotations: function(template) {
|
|
var list = [];
|
|
var content = template._content || template.content;
|
|
this._parseNodeAnnotations(content, list,
|
|
template.hasAttribute('strip-whitespace'));
|
|
return list;
|
|
},
|
|
|
|
// add annotations gleaned from subtree at `node` to `list`
|
|
_parseNodeAnnotations: function(node, list, stripWhiteSpace) {
|
|
return node.nodeType === Node.TEXT_NODE ?
|
|
this._parseTextNodeAnnotation(node, list) :
|
|
// TODO(sjmiles): are there other nodes we may encounter
|
|
// that are not TEXT_NODE but also not ELEMENT?
|
|
this._parseElementAnnotations(node, list, stripWhiteSpace);
|
|
},
|
|
|
|
_bindingRegex: (function() {
|
|
var IDENT = '([\\w\\s-_.:$]+)';
|
|
// The following lookahead group (?= ... ) and backreference \\4, etc.
|
|
// for repeating identifier characters prevent catastrophic
|
|
// backtracking when the argument which contains the identifier itself
|
|
// is repeated. This approximates possessive/atomic groups which are
|
|
// otherwise unsupported in Javascript. Unfortunately this means each
|
|
// usage of identifier must be separated to index the backreference
|
|
// correctly.
|
|
// v..............v v
|
|
var PROPERTY = '(?:(?=' + IDENT + ')\\4)';
|
|
var METHOD = '(?:(?=' + IDENT + ')\\5)';
|
|
var ARG_IDENT = '(?:(?=' + IDENT + ')\\6)';
|
|
var NUMBER = '(?:' + '[0-9]+' + ')';
|
|
var SQUOTE_STRING = '(?:' + '\'(?:[^\'\\\\]|\\\\\'|\\\\,)*\'' + ')';
|
|
var DQUOTE_STRING = '(?:' + '"(?:[^"\\\\]|\\\\"|\\\\,)*"' + ')';
|
|
var STRING = '(?:' + SQUOTE_STRING + '|' + DQUOTE_STRING + ')';
|
|
var ARGUMENT = '(?:' + ARG_IDENT + '|' + NUMBER + '|' + STRING + ')';
|
|
var ARGUMENT_LIST = '(?:(?:' + ARGUMENT + ',?' + ')*)';
|
|
var COMPUTED_FUNCTION = '(?:' + METHOD + '\\(' + ARGUMENT_LIST + '\\)' + ')';
|
|
var BINDING = '(' + PROPERTY + '|' + COMPUTED_FUNCTION + ')'; // Group 3
|
|
var OPEN_BRACKET = '(\\[\\[|{{)'; // Group 1
|
|
var CLOSE_BRACKET = '(?:]]|}})';
|
|
var NEGATE = '(!?)'; // Group 2
|
|
var EXPRESSION = OPEN_BRACKET + NEGATE + BINDING + CLOSE_BRACKET;
|
|
return new RegExp(EXPRESSION, "g");
|
|
})(),
|
|
|
|
// TODO(kschaaf): We could modify this to allow an escape mechanism by
|
|
// looking for the escape sequence in each of the matches and converting
|
|
// the part back to a literal type, and then bailing if only literals
|
|
// were found
|
|
_parseBindings: function(text) {
|
|
var re = this._bindingRegex;
|
|
var parts = [];
|
|
var lastIndex = 0;
|
|
var m;
|
|
// Example: "literal1{{prop}}literal2[[!compute(foo,bar)]]final"
|
|
// Regex matches:
|
|
// Iteration 1: Iteration 2:
|
|
// m[1]: '{{' '[['
|
|
// m[2]: '' '!'
|
|
// m[3]: 'prop' 'compute(foo,bar)'
|
|
while ((m = re.exec(text)) !== null) {
|
|
// Add literal part
|
|
if (m.index > lastIndex) {
|
|
parts.push({literal: text.slice(lastIndex, m.index)});
|
|
}
|
|
// Add binding part
|
|
// Mode (one-way or two)
|
|
var mode = m[1][0];
|
|
var negate = Boolean(m[2]);
|
|
var value = m[3].trim();
|
|
var customEvent, notifyEvent, colon;
|
|
if (mode == '{' && (colon = value.indexOf('::')) > 0) {
|
|
notifyEvent = value.substring(colon + 2);
|
|
value = value.substring(0, colon);
|
|
customEvent = true;
|
|
}
|
|
parts.push({
|
|
compoundIndex: parts.length,
|
|
value: value,
|
|
mode: mode,
|
|
negate: negate,
|
|
event: notifyEvent,
|
|
customEvent: customEvent
|
|
});
|
|
lastIndex = re.lastIndex;
|
|
}
|
|
// Add a final literal part
|
|
if (lastIndex && lastIndex < text.length) {
|
|
var literal = text.substring(lastIndex);
|
|
if (literal) {
|
|
parts.push({
|
|
literal: literal
|
|
});
|
|
}
|
|
}
|
|
if (parts.length) {
|
|
return parts;
|
|
}
|
|
},
|
|
|
|
_literalFromParts: function(parts) {
|
|
var s = '';
|
|
for (var i=0; i<parts.length; i++) {
|
|
var literal = parts[i].literal;
|
|
s += literal || '';
|
|
}
|
|
return s;
|
|
},
|
|
|
|
// add annotations gleaned from TextNode `node` to `list`
|
|
_parseTextNodeAnnotation: function(node, list) {
|
|
var parts = this._parseBindings(node.textContent);
|
|
if (parts) {
|
|
// Initialize the textContent with any literal parts
|
|
// NOTE: default to a space here so the textNode remains; some browsers
|
|
// (IE) evacipate an empty textNode following cloneNode/importNode.
|
|
node.textContent = this._literalFromParts(parts) || ' ';
|
|
var annote = {
|
|
bindings: [{
|
|
kind: 'text',
|
|
name: 'textContent',
|
|
parts: parts,
|
|
isCompound: parts.length !== 1
|
|
}]
|
|
};
|
|
list.push(annote);
|
|
return annote;
|
|
}
|
|
},
|
|
|
|
// add annotations gleaned from Element `node` to `list`
|
|
_parseElementAnnotations: function(element, list, stripWhiteSpace) {
|
|
var annote = {
|
|
bindings: [],
|
|
events: []
|
|
};
|
|
if (element.localName === 'content') {
|
|
list._hasContent = true;
|
|
}
|
|
this._parseChildNodesAnnotations(element, annote, list, stripWhiteSpace);
|
|
// TODO(sjmiles): is this for non-ELEMENT nodes? If so, we should
|
|
// change the contract of this method, or filter these out above.
|
|
if (element.attributes) {
|
|
this._parseNodeAttributeAnnotations(element, annote, list);
|
|
// TODO(sorvell): ad hoc callback for doing work on elements while
|
|
// leveraging annotator's tree walk.
|
|
// Consider adding an node callback registry and moving specific
|
|
// processing out of this module.
|
|
if (this.prepElement) {
|
|
this.prepElement(element);
|
|
}
|
|
}
|
|
if (annote.bindings.length || annote.events.length || annote.id) {
|
|
list.push(annote);
|
|
}
|
|
return annote;
|
|
},
|
|
|
|
// add annotations gleaned from children of `root` to `list`, `root`'s
|
|
// `annote` is supplied as it is the annote.parent of added annotations
|
|
_parseChildNodesAnnotations: function(root, annote, list, stripWhiteSpace) {
|
|
if (root.firstChild) {
|
|
var node = root.firstChild;
|
|
var i = 0;
|
|
while (node) {
|
|
var next = node.nextSibling;
|
|
if (node.localName === 'template' &&
|
|
!node.hasAttribute('preserve-content')) {
|
|
this._parseTemplate(node, i, list, annote);
|
|
}
|
|
// collapse adjacent textNodes: fixes an IE issue that can cause
|
|
// text nodes to be inexplicably split =(
|
|
// note that root.normalize() should work but does not so we do this
|
|
// manually.
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
var n = next;
|
|
while (n && (n.nodeType === Node.TEXT_NODE)) {
|
|
node.textContent += n.textContent;
|
|
next = n.nextSibling;
|
|
root.removeChild(n);
|
|
n = next;
|
|
}
|
|
// optionally strip whitespace
|
|
if (stripWhiteSpace && !node.textContent.trim()) {
|
|
root.removeChild(node);
|
|
// decrement index since node is removed
|
|
i--;
|
|
}
|
|
}
|
|
// if this node didn't get evacipated, parse it.
|
|
if (node.parentNode) {
|
|
var childAnnotation = this._parseNodeAnnotations(node, list,
|
|
stripWhiteSpace);
|
|
if (childAnnotation) {
|
|
childAnnotation.parent = annote;
|
|
childAnnotation.index = i;
|
|
}
|
|
}
|
|
node = next;
|
|
i++;
|
|
}
|
|
}
|
|
},
|
|
|
|
// 1. Parse annotations from the template and memoize them on
|
|
// content._notes (recurses into nested templates)
|
|
// 2. Remove template.content and store it in annotation list, where it
|
|
// will be the responsibility of the host to set it back to the template
|
|
// (this is both an optimization to avoid re-stamping nested template
|
|
// children and avoids a bug in Chrome where nested template children
|
|
// upgrade)
|
|
_parseTemplate: function(node, index, list, parent) {
|
|
// TODO(sjmiles): simply altering the .content reference didn't
|
|
// work (there was some confusion, might need verification)
|
|
var content = document.createDocumentFragment();
|
|
content._notes = this.parseAnnotations(node);
|
|
content.appendChild(node.content);
|
|
// TODO(sjmiles): using `nar` to avoid unnecessary allocation;
|
|
// in general the handling of these arrays needs some cleanup
|
|
// in this module
|
|
list.push({
|
|
bindings: Polymer.nar,
|
|
events: Polymer.nar,
|
|
templateContent: content,
|
|
parent: parent,
|
|
index: index
|
|
});
|
|
},
|
|
|
|
// add annotation data from attributes to the `annotation` for node `node`
|
|
// TODO(sjmiles): the distinction between an `annotation` and
|
|
// `annotation data` is not as clear as it could be
|
|
_parseNodeAttributeAnnotations: function(node, annotation) {
|
|
// Make copy of original attribute list, since the order may change
|
|
// as attributes are added and removed
|
|
var attrs = Array.prototype.slice.call(node.attributes);
|
|
for (var i=attrs.length-1, a; (a=attrs[i]); i--) {
|
|
var n = a.name;
|
|
var v = a.value;
|
|
var b;
|
|
// events (on-*)
|
|
if (n.slice(0, 3) === 'on-') {
|
|
node.removeAttribute(n);
|
|
annotation.events.push({
|
|
name: n.slice(3),
|
|
value: v
|
|
});
|
|
}
|
|
// bindings (other attributes)
|
|
else if (b = this._parseNodeAttributeAnnotation(node, n, v)) {
|
|
annotation.bindings.push(b);
|
|
}
|
|
// static id
|
|
else if (n === 'id') {
|
|
annotation.id = v;
|
|
}
|
|
}
|
|
},
|
|
|
|
// construct annotation data from a generic attribute, or undefined
|
|
_parseNodeAttributeAnnotation: function(node, name, value) {
|
|
var parts = this._parseBindings(value);
|
|
if (parts) {
|
|
// Attribute or property
|
|
var origName = name;
|
|
var kind = 'property';
|
|
if (name[name.length-1] == '$') {
|
|
name = name.slice(0, -1);
|
|
kind = 'attribute';
|
|
}
|
|
// Initialize attribute bindings with any literal parts
|
|
var literal = this._literalFromParts(parts);
|
|
if (literal && kind == 'attribute') {
|
|
node.setAttribute(name, literal);
|
|
}
|
|
// Clear attribute before removing, since IE won't allow removing
|
|
// `value` attribute if it previously had a value (can't
|
|
// unconditionally set '' before removing since attributes with `$`
|
|
// can't be set using setAttribute)
|
|
if (node.localName == 'input' && name == 'value') {
|
|
node.setAttribute(origName, '');
|
|
}
|
|
// Remove annotation
|
|
node.removeAttribute(origName);
|
|
// Case hackery: attributes are lower-case, but bind targets
|
|
// (properties) are case sensitive. Gambit is to map dash-case to
|
|
// camel-case: `foo-bar` becomes `fooBar`.
|
|
// Attribute bindings are excepted.
|
|
if (kind === 'property') {
|
|
name = Polymer.CaseMap.dashToCamelCase(name);
|
|
}
|
|
return {
|
|
kind: kind,
|
|
name: name,
|
|
parts: parts,
|
|
literal: literal,
|
|
isCompound: parts.length !== 1
|
|
};
|
|
}
|
|
},
|
|
|
|
// instance-time
|
|
|
|
_localSubTree: function(node, host) {
|
|
return (node === host) ? node.childNodes :
|
|
(node._lightChildren || node.childNodes);
|
|
},
|
|
|
|
findAnnotatedNode: function(root, annote) {
|
|
// recursively ascend tree until we hit root
|
|
var parent = annote.parent &&
|
|
Polymer.Annotations.findAnnotatedNode(root, annote.parent);
|
|
// unwind the stack, returning the indexed node at each level
|
|
return !parent ? root :
|
|
Polymer.Annotations._localSubTree(parent, root)[annote.index];
|
|
}
|
|
|
|
};
|
|
|
|
</script>
|