Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(70)

Side by Side Diff: ui/webui/resources/js/i18n_template_no_process.js

Issue 1233883005: Fix i18nTemplate.process() for DocumentFragments. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: stronger typing Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « ui/webui/resources/css/i18n_process.css ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** @typedef {Document|DocumentFragment|Element} */ 5 /** @typedef {Document|DocumentFragment|Element} */
6 var ProcessingRoot; 6 var ProcessingRoot;
7 7
8 /** 8 /**
9 * @fileoverview This is a simple template engine inspired by JsTemplates 9 * @fileoverview This is a simple template engine inspired by JsTemplates
10 * optimized for i18n. 10 * optimized for i18n.
(...skipping 20 matching lines...) Expand all
31 var i18nTemplate = (function() { 31 var i18nTemplate = (function() {
32 /** 32 /**
33 * This provides the handlers for the templating engine. The key is used as 33 * This provides the handlers for the templating engine. The key is used as
34 * the attribute name and the value is the function that gets called for every 34 * the attribute name and the value is the function that gets called for every
35 * single node that has this attribute. 35 * single node that has this attribute.
36 * @type {!Object} 36 * @type {!Object}
37 */ 37 */
38 var handlers = { 38 var handlers = {
39 /** 39 /**
40 * This handler sets the textContent of the element. 40 * This handler sets the textContent of the element.
41 * @param {HTMLElement} element The node to modify. 41 * @param {!HTMLElement} element The node to modify.
42 * @param {string} key The name of the value in the dictionary. 42 * @param {string} key The name of the value in |data|.
43 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. 43 * @param {!LoadTimeData} data The data source to draw from.
44 * @param {!Array<ProcessingRoot>} visited 44 * @param {!Array<ProcessingRoot>} visited
45 */ 45 */
46 'i18n-content': function(element, key, dictionary, visited) { 46 'i18n-content': function(element, key, data, visited) {
47 element.textContent = dictionary.getString(key); 47 element.textContent = data.getString(key);
48 }, 48 },
49 49
50 /** 50 /**
51 * This handler adds options to a <select> element. 51 * This handler adds options to a <select> element.
52 * @param {HTMLElement} select The node to modify. 52 * @param {!HTMLElement} select The node to modify.
53 * @param {string} key The name of the value in the dictionary. It should 53 * @param {string} key The name of the value in |data|. It should
54 * identify an array of values to initialize an <option>. Each value, 54 * identify an array of values to initialize an <option>. Each value,
55 * if a pair, represents [content, value]. Otherwise, it should be a 55 * if a pair, represents [content, value]. Otherwise, it should be a
56 * content string with no value. 56 * content string with no value.
57 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. 57 * @param {!LoadTimeData} data The data source to draw from.
58 * @param {!Array<ProcessingRoot>} visited 58 * @param {!Array<ProcessingRoot>} visited
59 */ 59 */
60 'i18n-options': function(select, key, dictionary, visited) { 60 'i18n-options': function(select, key, data, visited) {
61 var options = dictionary.getValue(key); 61 var options = data.getValue(key);
62 options.forEach(function(optionData) { 62 options.forEach(function(optionData) {
63 var option = typeof optionData == 'string' ? 63 var option = typeof optionData == 'string' ?
64 new Option(optionData) : 64 new Option(optionData) :
65 new Option(optionData[1], optionData[0]); 65 new Option(optionData[1], optionData[0]);
66 select.appendChild(option); 66 select.appendChild(option);
67 }); 67 });
68 }, 68 },
69 69
70 /** 70 /**
71 * This is used to set HTML attributes and DOM properties. The syntax is: 71 * This is used to set HTML attributes and DOM properties. The syntax is:
72 * attributename:key; 72 * attributename:key;
73 * .domProperty:key; 73 * .domProperty:key;
74 * .nested.dom.property:key 74 * .nested.dom.property:key
75 * @param {HTMLElement} element The node to modify. 75 * @param {!HTMLElement} element The node to modify.
76 * @param {string} attributeAndKeys The path of the attribute to modify 76 * @param {string} attributeAndKeys The path of the attribute to modify
77 * followed by a colon, and the name of the value in the dictionary. 77 * followed by a colon, and the name of the value in |data|.
78 * Multiple attribute/key pairs may be separated by semicolons. 78 * Multiple attribute/key pairs may be separated by semicolons.
79 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. 79 * @param {!LoadTimeData} data The data source to draw from.
80 * @param {!Array<ProcessingRoot>} visited 80 * @param {!Array<ProcessingRoot>} visited
81 */ 81 */
82 'i18n-values': function(element, attributeAndKeys, dictionary, visited) { 82 'i18n-values': function(element, attributeAndKeys, data, visited) {
83 var parts = attributeAndKeys.replace(/\s/g, '').split(/;/); 83 var parts = attributeAndKeys.replace(/\s/g, '').split(/;/);
84 parts.forEach(function(part) { 84 parts.forEach(function(part) {
85 if (!part) 85 if (!part)
86 return; 86 return;
87 87
88 var attributeAndKeyPair = part.match(/^([^:]+):(.+)$/); 88 var attributeAndKeyPair = part.match(/^([^:]+):(.+)$/);
89 if (!attributeAndKeyPair) 89 if (!attributeAndKeyPair)
90 throw new Error('malformed i18n-values: ' + attributeAndKeys); 90 throw new Error('malformed i18n-values: ' + attributeAndKeys);
91 91
92 var propName = attributeAndKeyPair[1]; 92 var propName = attributeAndKeyPair[1];
93 var propExpr = attributeAndKeyPair[2]; 93 var propExpr = attributeAndKeyPair[2];
94 94
95 var value = dictionary.getValue(propExpr); 95 var value = data.getValue(propExpr);
96 96
97 // Allow a property of the form '.foo.bar' to assign a value into 97 // Allow a property of the form '.foo.bar' to assign a value into
98 // element.foo.bar. 98 // element.foo.bar.
99 if (propName[0] == '.') { 99 if (propName[0] == '.') {
100 var path = propName.slice(1).split('.'); 100 var path = propName.slice(1).split('.');
101 var targetObject = element; 101 var targetObject = element;
102 while (targetObject && path.length > 1) { 102 while (targetObject && path.length > 1) {
103 targetObject = targetObject[path.shift()]; 103 targetObject = targetObject[path.shift()];
104 } 104 }
105 if (targetObject) { 105 if (targetObject) {
106 targetObject[path] = value; 106 targetObject[path] = value;
107 // In case we set innerHTML (ignoring others) we need to recursively 107 // In case we set innerHTML (ignoring others) we need to recursively
108 // check the content. 108 // check the content.
109 if (path == 'innerHTML') { 109 if (path == 'innerHTML') {
110 for (var temp = element.firstElementChild; temp; 110 for (var i = 0; i < element.children.length; ++i) {
111 temp = temp.nextElementSibling) { 111 processWithoutCycles(element.children[i], data, visited, false);
112 processWithoutCycles(temp, dictionary, visited);
113 } 112 }
114 } 113 }
115 } 114 }
116 } else { 115 } else {
117 element.setAttribute(propName, /** @type {string} */(value)); 116 element.setAttribute(propName, /** @type {string} */(value));
118 } 117 }
119 }); 118 });
120 } 119 }
121 }; 120 };
122 121
122 var prefixes = [''];
123
124 // Only look through shadow DOM when it's supported. As of April 2015, iOS
125 // Chrome doesn't support shadow DOM.
126 if (Element.prototype.createShadowRoot)
127 prefixes.push('::shadow ');
128
123 var attributeNames = Object.keys(handlers); 129 var attributeNames = Object.keys(handlers);
124 // Only use /deep/ when shadow DOM is supported. As of April 2015 iOS Chrome 130 var selector = prefixes.map(function(prefix) {
125 // doesn't support shadow DOM. 131 return prefix + '[' + attributeNames.join('], ' + prefix + '[') + ']';
126 var prefix = Element.prototype.createShadowRoot ? ':root /deep/ ' : ''; 132 }).join(', ');
127 var selector = prefix + '[' + attributeNames.join('],' + prefix + '[') + ']';
128 133
129 /** 134 /**
130 * Processes a DOM tree with the {@code dictionary} map. 135 * Processes a DOM tree using a |data| source to populate template values.
131 * @param {ProcessingRoot} root The root of the DOM tree to process. 136 * @param {!ProcessingRoot} root The root of the DOM tree to process.
132 * @param {LoadTimeData} dictionary The dictionary to draw from. 137 * @param {!LoadTimeData} data The data to draw from.
133 */ 138 */
134 function process(root, dictionary) { 139 function process(root, data) {
135 processWithoutCycles(root, dictionary, []); 140 processWithoutCycles(root, data, [], true);
136 } 141 }
137 142
138 /** 143 /**
139 * Internal process() method that stops cycles while processing. 144 * Internal process() method that stops cycles while processing.
140 * @param {ProcessingRoot} root 145 * @param {!ProcessingRoot} root
141 * @param {LoadTimeData} dictionary 146 * @param {!LoadTimeData} data
142 * @param {!Array<ProcessingRoot>} visited Already visited roots. 147 * @param {!Array<ProcessingRoot>} visited Already visited roots.
148 * @param {boolean} mark Whether nodes should be marked processed.
143 */ 149 */
144 function processWithoutCycles(root, dictionary, visited) { 150 function processWithoutCycles(root, data, visited, mark) {
145 if (visited.indexOf(root) >= 0) { 151 if (visited.indexOf(root) >= 0) {
146 // Found a cycle. Stop it. 152 // Found a cycle. Stop it.
147 return; 153 return;
148 } 154 }
149 155
150 // Mark the node as visited before recursing. 156 // Mark the node as visited before recursing.
151 visited.push(root); 157 visited.push(root);
152 158
153 var importLinks = root.querySelectorAll('link[rel=import]'); 159 var importLinks = root.querySelectorAll('link[rel=import]');
154 for (var i = 0; i < importLinks.length; ++i) { 160 for (var i = 0; i < importLinks.length; ++i) {
155 var importLink = /** @type {!HTMLLinkElement} */(importLinks[i]); 161 var importLink = /** @type {!HTMLLinkElement} */(importLinks[i]);
156 if (!importLink.import) { 162 if (!importLink.import) {
157 // Happens when a <link rel=import> is inside a <template>. 163 // Happens when a <link rel=import> is inside a <template>.
158 // TODO(dbeam): should we log an error if we detect that here? 164 // TODO(dbeam): should we log an error if we detect that here?
159 continue; 165 continue;
160 } 166 }
161 processWithoutCycles(importLink.import, dictionary, visited); 167 processWithoutCycles(importLink.import, data, visited, mark);
162 } 168 }
163 169
164 var templates = root.querySelectorAll('template'); 170 var templates = root.querySelectorAll('template');
165 for (var i = 0; i < templates.length; ++i) { 171 for (var i = 0; i < templates.length; ++i) {
166 var template = /** @type {HTMLTemplateElement} */(templates[i]); 172 var template = /** @type {HTMLTemplateElement} */(templates[i]);
167 processWithoutCycles(template.content, dictionary, visited); 173 processWithoutCycles(template.content, data, visited, mark);
168 } 174 }
169 175
170 var firstElement = root instanceof Element ? root : root.querySelector('*'); 176 var isElement = root instanceof Element;
177 if (isElement && root.matches(selector))
178 processElement(/** @type {!Element} */(root), data, visited);
171 179
172 if (prefix) { 180 var elements = root.querySelectorAll(selector);
173 // Prefixes skip root level elements. This is typically <html> but can 181 for (var i = 0; i < elements.length; ++i) {
174 // differ inside of DocumentFragments (i.e. <template>s). Process them 182 processElement(elements[i], data, visited);
175 // explicitly. 183 }
176 for (var temp = firstElement; temp; temp = temp.nextElementSibling) { 184
177 processElement(/** @type {Element} */(temp), dictionary, visited); 185 if (mark) {
186 var processed = isElement ? [root] : root.children;
187 for (var i = 0; i < processed.length; ++i) {
188 processed[i].setAttribute('i18n-processed', '');
178 } 189 }
179 } 190 }
180
181 var elements = root.querySelectorAll(selector);
182 for (var element, i = 0; element = elements[i]; i++) {
183 processElement(element, dictionary, visited);
184 }
185
186 if (firstElement)
187 firstElement.setAttribute('i18n-processed', '');
188 } 191 }
189 192
190 /** 193 /**
191 * Run through various [i18n-*] attributes and do activate replacements. 194 * Run through various [i18n-*] attributes and populate.
192 * @param {Element} element 195 * @param {!Element} element
193 * @param {LoadTimeData} dictionary 196 * @param {!LoadTimeData} data
194 * @param {!Array<ProcessingRoot>} visited 197 * @param {!Array<ProcessingRoot>} visited
195 */ 198 */
196 function processElement(element, dictionary, visited) { 199 function processElement(element, data, visited) {
197 for (var i = 0; i < attributeNames.length; i++) { 200 for (var i = 0; i < attributeNames.length; i++) {
198 var name = attributeNames[i]; 201 var name = attributeNames[i];
199 var attribute = element.getAttribute(name); 202 var attribute = element.getAttribute(name);
200 if (attribute != null) 203 if (attribute != null)
201 handlers[name](element, attribute, dictionary, visited); 204 handlers[name](element, attribute, data, visited);
202 } 205 }
203 } 206 }
204 207
205 return { 208 return {
206 process: process 209 process: process
207 }; 210 };
208 }()); 211 }());
OLDNEW
« no previous file with comments | « ui/webui/resources/css/i18n_process.css ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698