Files
polymer/src/lib/annotations/annotations.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>