OLD | NEW |
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 Loading... |
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 }()); |
OLD | NEW |