Add ICU message format support
[chromium-blink-merge.git] / ui / webui / resources / js / i18n_template_no_process.js
blob38443f01b5bf868d9d9850503ed97576732a713e
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
3 // found in the LICENSE file.
5 /** @typedef {Document|DocumentFragment|Element} */
6 var ProcessingRoot;
8 /**
9  * @fileoverview This is a simple template engine inspired by JsTemplates
10  * optimized for i18n.
11  *
12  * It currently supports three handlers:
13  *
14  *   * i18n-content which sets the textContent of the element.
15  *
16  *     <span i18n-content="myContent"></span>
17  *
18  *   * i18n-options which generates <option> elements for a <select>.
19  *
20  *     <select i18n-options="myOptionList"></select>
21  *
22  *   * i18n-values is a list of attribute-value or property-value pairs.
23  *     Properties are prefixed with a '.' and can contain nested properties.
24  *
25  *     <span i18n-values="title:myTitle;.style.fontSize:fontSize"></span>
26  *
27  * This file is a copy of i18n_template.js, with minor tweaks to support using
28  * load_time_data.js. It should replace i18n_template.js eventually.
29  */
31 var i18nTemplate = (function() {
32   /**
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
35    * single node that has this attribute.
36    * @type {!Object}
37    */
38   var handlers = {
39     /**
40      * This handler sets the textContent of the element.
41      * @param {!HTMLElement} element The node to modify.
42      * @param {string} key The name of the value in |data|.
43      * @param {!LoadTimeData} data The data source to draw from.
44      * @param {!Array<ProcessingRoot>} visited
45      */
46     'i18n-content': function(element, key, data, visited) {
47       element.textContent = data.getString(key);
48     },
50     /**
51      * This handler adds options to a <select> element.
52      * @param {!HTMLElement} select The node to modify.
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,
55      *     if a pair, represents [content, value]. Otherwise, it should be a
56      *     content string with no value.
57      * @param {!LoadTimeData} data The data source to draw from.
58      * @param {!Array<ProcessingRoot>} visited
59      */
60     'i18n-options': function(select, key, data, visited) {
61       var options = data.getValue(key);
62       options.forEach(function(optionData) {
63         var option = typeof optionData == 'string' ?
64             new Option(optionData) :
65             new Option(optionData[1], optionData[0]);
66         select.appendChild(option);
67       });
68     },
70     /**
71      * This is used to set HTML attributes and DOM properties. The syntax is:
72      *   attributename:key;
73      *   .domProperty:key;
74      *   .nested.dom.property:key
75      * @param {!HTMLElement} element The node 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 |data|.
78      *     Multiple attribute/key pairs may be separated by semicolons.
79      * @param {!LoadTimeData} data The data source to draw from.
80      * @param {!Array<ProcessingRoot>} visited
81      */
82     'i18n-values': function(element, attributeAndKeys, data, visited) {
83       var parts = attributeAndKeys.replace(/\s/g, '').split(/;/);
84       parts.forEach(function(part) {
85         if (!part)
86           return;
88         var attributeAndKeyPair = part.match(/^([^:]+):(.+)$/);
89         if (!attributeAndKeyPair)
90           throw new Error('malformed i18n-values: ' + attributeAndKeys);
92         var propName = attributeAndKeyPair[1];
93         var propExpr = attributeAndKeyPair[2];
95         var value = data.getValue(propExpr);
97         // Allow a property of the form '.foo.bar' to assign a value into
98         // element.foo.bar.
99         if (propName[0] == '.') {
100           var path = propName.slice(1).split('.');
101           var targetObject = element;
102           while (targetObject && path.length > 1) {
103             targetObject = targetObject[path.shift()];
104           }
105           if (targetObject) {
106             targetObject[path] = value;
107             // In case we set innerHTML (ignoring others) we need to recursively
108             // check the content.
109             if (path == 'innerHTML') {
110               for (var i = 0; i < element.children.length; ++i) {
111                 processWithoutCycles(element.children[i], data, visited, false);
112               }
113             }
114           }
115         } else {
116           element.setAttribute(propName, /** @type {string} */(value));
117         }
118       });
119     }
120   };
122   var prefixes = [''];
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('* /deep/ ');
129   var attributeNames = Object.keys(handlers);
130   var selector = prefixes.map(function(prefix) {
131     return prefix + '[' + attributeNames.join('], ' + prefix + '[') + ']';
132   }).join(', ');
134   /**
135    * Processes a DOM tree using a |data| source to populate template values.
136    * @param {!ProcessingRoot} root The root of the DOM tree to process.
137    * @param {!LoadTimeData} data The data to draw from.
138    */
139   function process(root, data) {
140     processWithoutCycles(root, data, [], true);
141   }
143   /**
144    * Internal process() method that stops cycles while processing.
145    * @param {!ProcessingRoot} root
146    * @param {!LoadTimeData} data
147    * @param {!Array<ProcessingRoot>} visited Already visited roots.
148    * @param {boolean} mark Whether nodes should be marked processed.
149    */
150   function processWithoutCycles(root, data, visited, mark) {
151     if (visited.indexOf(root) >= 0) {
152       // Found a cycle. Stop it.
153       return;
154     }
156     // Mark the node as visited before recursing.
157     visited.push(root);
159     var importLinks = root.querySelectorAll('link[rel=import]');
160     for (var i = 0; i < importLinks.length; ++i) {
161       var importLink = /** @type {!HTMLLinkElement} */(importLinks[i]);
162       if (!importLink.import) {
163         // Happens when a <link rel=import> is inside a <template>.
164         // TODO(dbeam): should we log an error if we detect that here?
165         continue;
166       }
167       processWithoutCycles(importLink.import, data, visited, mark);
168     }
170     var templates = root.querySelectorAll('template');
171     for (var i = 0; i < templates.length; ++i) {
172       var template = /** @type {HTMLTemplateElement} */(templates[i]);
173       processWithoutCycles(template.content, data, visited, mark);
174     }
176     var isElement = root instanceof Element;
177     if (isElement && root.matches(selector))
178       processElement(/** @type {!Element} */(root), data, visited);
180     var elements = root.querySelectorAll(selector);
181     for (var i = 0; i < elements.length; ++i) {
182       processElement(elements[i], data, visited);
183     }
185     if (mark) {
186       var processed = isElement ? [root] : root.children;
187       if (processed) {
188         for (var i = 0; i < processed.length; ++i) {
189           processed[i].setAttribute('i18n-processed', '');
190         }
191       }
192     }
193   }
195   /**
196    * Run through various [i18n-*] attributes and populate.
197    * @param {!Element} element
198    * @param {!LoadTimeData} data
199    * @param {!Array<ProcessingRoot>} visited
200    */
201   function processElement(element, data, visited) {
202     for (var i = 0; i < attributeNames.length; i++) {
203       var name = attributeNames[i];
204       var attribute = element.getAttribute(name);
205       if (attribute != null)
206         handlers[name](element, attribute, data, visited);
207     }
208   }
210   return {
211     process: process
212   };
213 }());