| 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} */ |
| 6 var ProcessingRoot; |
| 7 |
| 5 /** | 8 /** |
| 6 * @fileoverview This is a simple template engine inspired by JsTemplates | 9 * @fileoverview This is a simple template engine inspired by JsTemplates |
| 7 * optimized for i18n. | 10 * optimized for i18n. |
| 8 * | 11 * |
| 9 * It currently supports three handlers: | 12 * It currently supports three handlers: |
| 10 * | 13 * |
| 11 * * i18n-content which sets the textContent of the element. | 14 * * i18n-content which sets the textContent of the element. |
| 12 * | 15 * |
| 13 * <span i18n-content="myContent"></span> | 16 * <span i18n-content="myContent"></span> |
| 14 * | 17 * |
| (...skipping 16 matching lines...) Expand all Loading... |
| 31 * 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 |
| 32 * single node that has this attribute. | 35 * single node that has this attribute. |
| 33 * @type {!Object} | 36 * @type {!Object} |
| 34 */ | 37 */ |
| 35 var handlers = { | 38 var handlers = { |
| 36 /** | 39 /** |
| 37 * This handler sets the textContent of the element. | 40 * This handler sets the textContent of the element. |
| 38 * @param {HTMLElement} element The node to modify. | 41 * @param {HTMLElement} element The node to modify. |
| 39 * @param {string} key The name of the value in the dictionary. | 42 * @param {string} key The name of the value in the dictionary. |
| 40 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. | 43 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. |
| 44 * @param {!Array<ProcessingRoot>} visited |
| 41 */ | 45 */ |
| 42 'i18n-content': function(element, key, dictionary) { | 46 'i18n-content': function(element, key, dictionary, visited) { |
| 43 element.textContent = dictionary.getString(key); | 47 element.textContent = dictionary.getString(key); |
| 44 }, | 48 }, |
| 45 | 49 |
| 46 /** | 50 /** |
| 47 * This handler adds options to a <select> element. | 51 * This handler adds options to a <select> element. |
| 48 * @param {HTMLElement} select The node to modify. | 52 * @param {HTMLElement} select The node to modify. |
| 49 * @param {string} key The name of the value in the dictionary. It should | 53 * @param {string} key The name of the value in the dictionary. It should |
| 50 * identify an array of values to initialize an <option>. Each value, | 54 * identify an array of values to initialize an <option>. Each value, |
| 51 * if a pair, represents [content, value]. Otherwise, it should be a | 55 * if a pair, represents [content, value]. Otherwise, it should be a |
| 52 * content string with no value. | 56 * content string with no value. |
| 53 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. | 57 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. |
| 58 * @param {!Array<ProcessingRoot>} visited |
| 54 */ | 59 */ |
| 55 'i18n-options': function(select, key, dictionary) { | 60 'i18n-options': function(select, key, dictionary, visited) { |
| 56 var options = dictionary.getValue(key); | 61 var options = dictionary.getValue(key); |
| 57 options.forEach(function(optionData) { | 62 options.forEach(function(optionData) { |
| 58 var option = typeof optionData == 'string' ? | 63 var option = typeof optionData == 'string' ? |
| 59 new Option(optionData) : | 64 new Option(optionData) : |
| 60 new Option(optionData[1], optionData[0]); | 65 new Option(optionData[1], optionData[0]); |
| 61 select.appendChild(option); | 66 select.appendChild(option); |
| 62 }); | 67 }); |
| 63 }, | 68 }, |
| 64 | 69 |
| 65 /** | 70 /** |
| 66 * 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: |
| 67 * attributename:key; | 72 * attributename:key; |
| 68 * .domProperty:key; | 73 * .domProperty:key; |
| 69 * .nested.dom.property:key | 74 * .nested.dom.property:key |
| 70 * @param {HTMLElement} element The node to modify. | 75 * @param {HTMLElement} element The node to modify. |
| 71 * @param {string} attributeAndKeys The path of the attribute to modify | 76 * @param {string} attributeAndKeys The path of the attribute to modify |
| 72 * 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 the dictionary. |
| 73 * Multiple attribute/key pairs may be separated by semicolons. | 78 * Multiple attribute/key pairs may be separated by semicolons. |
| 74 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. | 79 * @param {LoadTimeData} dictionary The dictionary of strings to draw from. |
| 80 * @param {!Array<ProcessingRoot>} visited |
| 75 */ | 81 */ |
| 76 'i18n-values': function(element, attributeAndKeys, dictionary) { | 82 'i18n-values': function(element, attributeAndKeys, dictionary, visited) { |
| 77 var parts = attributeAndKeys.replace(/\s/g, '').split(/;/); | 83 var parts = attributeAndKeys.replace(/\s/g, '').split(/;/); |
| 78 parts.forEach(function(part) { | 84 parts.forEach(function(part) { |
| 79 if (!part) | 85 if (!part) |
| 80 return; | 86 return; |
| 81 | 87 |
| 82 var attributeAndKeyPair = part.match(/^([^:]+):(.+)$/); | 88 var attributeAndKeyPair = part.match(/^([^:]+):(.+)$/); |
| 83 if (!attributeAndKeyPair) | 89 if (!attributeAndKeyPair) |
| 84 throw new Error('malformed i18n-values: ' + attributeAndKeys); | 90 throw new Error('malformed i18n-values: ' + attributeAndKeys); |
| 85 | 91 |
| 86 var propName = attributeAndKeyPair[1]; | 92 var propName = attributeAndKeyPair[1]; |
| 87 var propExpr = attributeAndKeyPair[2]; | 93 var propExpr = attributeAndKeyPair[2]; |
| 88 | 94 |
| 89 var value = dictionary.getValue(propExpr); | 95 var value = dictionary.getValue(propExpr); |
| 90 | 96 |
| 91 // 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 |
| 92 // element.foo.bar. | 98 // element.foo.bar. |
| 93 if (propName[0] == '.') { | 99 if (propName[0] == '.') { |
| 94 var path = propName.slice(1).split('.'); | 100 var path = propName.slice(1).split('.'); |
| 95 var targetObject = element; | 101 var targetObject = element; |
| 96 while (targetObject && path.length > 1) { | 102 while (targetObject && path.length > 1) { |
| 97 targetObject = targetObject[path.shift()]; | 103 targetObject = targetObject[path.shift()]; |
| 98 } | 104 } |
| 99 if (targetObject) { | 105 if (targetObject) { |
| 100 targetObject[path] = value; | 106 targetObject[path] = value; |
| 101 // In case we set innerHTML (ignoring others) we need to | 107 // In case we set innerHTML (ignoring others) we need to recursively |
| 102 // recursively check the content. | 108 // check the content. |
| 103 if (path == 'innerHTML') | 109 if (path == 'innerHTML') { |
| 104 process(element, dictionary); | 110 for (var temp = element.firstElementChild; temp; |
| 111 temp = temp.nextElementSibling) { |
| 112 processWithoutCycles(temp, dictionary, visited); |
| 113 } |
| 114 } |
| 105 } | 115 } |
| 106 } else { | 116 } else { |
| 107 element.setAttribute(propName, /** @type {string} */(value)); | 117 element.setAttribute(propName, /** @type {string} */(value)); |
| 108 } | 118 } |
| 109 }); | 119 }); |
| 110 } | 120 } |
| 111 }; | 121 }; |
| 112 | 122 |
| 113 var attributeNames = Object.keys(handlers); | 123 var attributeNames = Object.keys(handlers); |
| 114 // Chrome for iOS must use Apple's UIWebView, which (as of April 2015) does | 124 // Only use /deep/ when shadow DOM is supported. As of April 2015 iOS Chrome |
| 115 // not have native shadow DOM support. If shadow DOM is supported (or | 125 // doesn't support shadow DOM. |
| 116 // polyfilled), search for i18n attributes using the /deep/ selector; | 126 var prefix = Element.prototype.createShadowRoot ? ':root /deep/ ' : ''; |
| 117 // otherwise, do not attempt to search within the shadow DOM. | 127 var selector = prefix + '[' + attributeNames.join('],' + prefix + '[') + ']'; |
| 118 var selector = | |
| 119 (window.document.body && window.document.body.createShadowRoot) ? | |
| 120 'html /deep/ [' + attributeNames.join('],[') + ']' : | |
| 121 '[' + attributeNames.join('],[') + ']'; | |
| 122 | 128 |
| 123 /** | 129 /** |
| 124 * Processes a DOM tree with the {@code dictionary} map. | 130 * Processes a DOM tree with the {@code dictionary} map. |
| 125 * @param {Document|Element} root The root of the DOM tree to process. | 131 * @param {ProcessingRoot} root The root of the DOM tree to process. |
| 126 * @param {LoadTimeData} dictionary The dictionary to draw from. | 132 * @param {LoadTimeData} dictionary The dictionary to draw from. |
| 127 */ | 133 */ |
| 128 function process(root, dictionary) { | 134 function process(root, dictionary) { |
| 135 processWithoutCycles(root, dictionary, []); |
| 136 } |
| 137 |
| 138 /** |
| 139 * Internal process() method that stops cycles while processing. |
| 140 * @param {ProcessingRoot} root |
| 141 * @param {LoadTimeData} dictionary |
| 142 * @param {!Array<ProcessingRoot>} visited Already visited roots. |
| 143 */ |
| 144 function processWithoutCycles(root, dictionary, visited) { |
| 145 if (visited.indexOf(root) >= 0) { |
| 146 // Found a cycle. Stop it. |
| 147 return; |
| 148 } |
| 149 |
| 150 // Mark the node as visited before recursing. |
| 151 visited.push(root); |
| 152 |
| 153 var importLinks = root.querySelectorAll('link[rel=import]'); |
| 154 for (var i = 0; i < importLinks.length; ++i) { |
| 155 var importLink = /** @type {!HTMLLinkElement} */(importLinks[i]); |
| 156 if (!importLink.import) { |
| 157 // Happens when a <link rel=import> is inside a <template>. |
| 158 // TODO(dbeam): should we log an error if we detect that here? |
| 159 continue; |
| 160 } |
| 161 processWithoutCycles(importLink.import, dictionary, visited); |
| 162 } |
| 163 |
| 164 var templates = root.querySelectorAll('template'); |
| 165 for (var i = 0; i < templates.length; ++i) { |
| 166 var template = /** @type {HTMLTemplateElement} */(templates[i]); |
| 167 processWithoutCycles(template.content, dictionary, visited); |
| 168 } |
| 169 |
| 170 var firstElement = root instanceof Element ? root : root.querySelector('*'); |
| 171 |
| 172 if (prefix) { |
| 173 // Prefixes skip root level elements. This is typically <html> but can |
| 174 // differ inside of DocumentFragments (i.e. <template>s). Process them |
| 175 // explicitly. |
| 176 for (var temp = firstElement; temp; temp = temp.nextElementSibling) { |
| 177 processElement(/** @type {Element} */(temp), dictionary, visited); |
| 178 } |
| 179 } |
| 180 |
| 129 var elements = root.querySelectorAll(selector); | 181 var elements = root.querySelectorAll(selector); |
| 130 for (var element, i = 0; element = elements[i]; i++) { | 182 for (var element, i = 0; element = elements[i]; i++) { |
| 131 for (var j = 0; j < attributeNames.length; j++) { | 183 processElement(element, dictionary, visited); |
| 132 var name = attributeNames[j]; | |
| 133 var attribute = element.getAttribute(name); | |
| 134 if (attribute != null) | |
| 135 handlers[name](element, attribute, dictionary); | |
| 136 } | |
| 137 } | 184 } |
| 138 var doc = root instanceof Document ? root : root.ownerDocument; | 185 |
| 139 if (doc) | 186 if (firstElement) |
| 140 doc.documentElement.classList.add('i18n-processed'); | 187 firstElement.setAttribute('i18n-processed', ''); |
| 188 } |
| 189 |
| 190 /** |
| 191 * Run through various [i18n-*] attributes and do activate replacements. |
| 192 * @param {Element} element |
| 193 * @param {LoadTimeData} dictionary |
| 194 * @param {!Array<ProcessingRoot>} visited |
| 195 */ |
| 196 function processElement(element, dictionary, visited) { |
| 197 for (var i = 0; i < attributeNames.length; i++) { |
| 198 var name = attributeNames[i]; |
| 199 var attribute = element.getAttribute(name); |
| 200 if (attribute != null) |
| 201 handlers[name](element, attribute, dictionary, visited); |
| 202 } |
| 141 } | 203 } |
| 142 | 204 |
| 143 return { | 205 return { |
| 144 process: process | 206 process: process |
| 145 }; | 207 }; |
| 146 }()); | 208 }()); |
| OLD | NEW |