Avi Drissman | dfd88085 | 2022-09-15 20:11:09 | [diff] [blame] | 1 | # Copyright 2022 The Chromium Authors |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
dpapad | b0e74d3 | 2022-05-10 18:39:31 | [diff] [blame] | 5 | # Genaretes a wrapper TS file around a source HTML file holding either |
| 6 | # 1) a Polymer element template or |
| 7 | # 2) an <iron-iconset-svg> definitions |
| 8 | # |
| 9 | # Note: The HTML file must be named either 'icons.html' or be suffixed with |
| 10 | # '_icons.html' for this tool to treat them as #2. Consequently, files holding |
| 11 | # Polymer element templates should not use such naming to be treated as #1. |
| 12 | # |
| 13 | # In case #1 the wrapper exports a getTemplate() function that can be used at |
| 14 | # runtime to import the template. This is useful for implementing Web Components |
| 15 | # using JS modules, where all the HTML needs to reside in a JS file (no more |
| 16 | # HTML imports). |
| 17 | # |
| 18 | # In case #2 the wrapper adds the <iron-iconset-svg> element to <head>, so that |
| 19 | # it can be used by <iron-icon> instances. |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 20 | |
| 21 | import argparse |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 22 | import io |
dpapad | f3a48a1 | 2024-01-04 09:55:13 | [diff] [blame] | 23 | import re |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 24 | import shutil |
| 25 | import sys |
| 26 | import tempfile |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 27 | from os import path, getcwd, makedirs |
| 28 | |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 29 | _HERE_PATH = path.dirname(__file__) |
| 30 | _SRC_PATH = path.normpath(path.join(_HERE_PATH, '..', '..')) |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 31 | _CWD = getcwd() |
| 32 | |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 33 | sys.path.append(path.join(_SRC_PATH, 'third_party', 'node')) |
| 34 | import node |
| 35 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 36 | # Template for native web component HTML templates. |
| 37 | _NATIVE_ELEMENT_TEMPLATE = """import {getTrustedHTML} from '%(scheme)s//resources/js/static_types.js'; |
dpapad | bc6fc93 | 2022-05-28 00:47:57 | [diff] [blame] | 38 | export function getTemplate() { |
dpapad | 24071ff | 2022-09-02 01:07:54 | [diff] [blame] | 39 | return getTrustedHTML`<!--_html_template_start_-->%(content)s<!--_html_template_end_-->`; |
dpapad | bc6fc93 | 2022-05-28 00:47:57 | [diff] [blame] | 40 | }""" |
| 41 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 42 | # Template for Polymer web component HTML templates. |
| 43 | _POLYMER_ELEMENT_TEMPLATE = """import {html} from '%(scheme)s//resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
dpapad | b0e74d3 | 2022-05-10 18:39:31 | [diff] [blame] | 44 | export function getTemplate() { |
dpapad | 24071ff | 2022-09-02 01:07:54 | [diff] [blame] | 45 | return html`<!--_html_template_start_-->%(content)s<!--_html_template_end_-->`; |
dpapad | b0e74d3 | 2022-05-10 18:39:31 | [diff] [blame] | 46 | }""" |
| 47 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 48 | # Template for Lit component HTML templates. |
| 49 | _LIT_ELEMENT_TEMPLATE = """import {html} from '%(scheme)s//resources/lit/v3_0/lit.rollup.js'; |
| 50 | import type {%(class_name)s} from './%(file_name)s.js'; |
dpapad | f3a48a1 | 2024-01-04 09:55:13 | [diff] [blame] | 51 | %(imports)s |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 52 | export function getHtml(this: %(class_name)s) { |
| 53 | return html`<!--_html_template_start_-->%(content)s<!--_html_template_end_-->`; |
| 54 | }""" |
| 55 | |
| 56 | # Template for Polymer icon HTML files. |
| 57 | _POLYMER_ICONS_TEMPLATE = """import '%(scheme)s//resources/polymer/v3_0/iron-iconset-svg/iron-iconset-svg.js'; |
dpapad | 24071ff | 2022-09-02 01:07:54 | [diff] [blame] | 58 | import {html} from '%(scheme)s//resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
dpapad | b0e74d3 | 2022-05-10 18:39:31 | [diff] [blame] | 59 | |
dpapad | 24071ff | 2022-09-02 01:07:54 | [diff] [blame] | 60 | const template = html`%(content)s`; |
dpapad | b0e74d3 | 2022-05-10 18:39:31 | [diff] [blame] | 61 | document.head.appendChild(template.content); |
| 62 | """ |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 63 | |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 64 | # Template for Lit icon HTML files. |
| 65 | _LIT_ICONS_TEMPLATE = """import '%(scheme)s//resources/cr_elements/cr_icon/cr_iconset.js'; |
dpapad | 0f68017 | 2024-04-23 20:50:59 | [diff] [blame] | 66 | import {getTrustedHTML} from '%(scheme)s//resources/js/static_types.js'; |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 67 | |
dpapad | 0f68017 | 2024-04-23 20:50:59 | [diff] [blame] | 68 | const div = document.createElement('div'); |
| 69 | div.innerHTML = getTrustedHTML`%(content)s`; |
| 70 | const iconsets = div.querySelectorAll('cr-iconset'); |
| 71 | for (const iconset of iconsets) { |
| 72 | document.head.appendChild(iconset); |
| 73 | } |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 74 | """ |
| 75 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 76 | # Tokens used to detect whether the underlying custom element is based on |
| 77 | # Polymer or Lit. |
dpapad | 07cd33f | 2023-01-20 19:08:30 | [diff] [blame] | 78 | POLYMER_TOKEN = '//resources/polymer/v3_0/polymer/polymer_bundled.min.js' |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 79 | LIT_TOKEN = '//resources/lit/v3_0/lit.rollup.js' |
| 80 | |
| 81 | # Map holding all the different types of HTML files to generate wrappers for. |
| 82 | TEMPLATE_MAP = { |
| 83 | 'lit': _LIT_ELEMENT_TEMPLATE, |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 84 | 'lit_icons': _LIT_ICONS_TEMPLATE, |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 85 | 'native': _NATIVE_ELEMENT_TEMPLATE, |
| 86 | 'polymer_icons': _POLYMER_ICONS_TEMPLATE, |
| 87 | 'polymer': _POLYMER_ELEMENT_TEMPLATE, |
| 88 | } |
dpapad | 07cd33f | 2023-01-20 19:08:30 | [diff] [blame] | 89 | |
| 90 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 91 | def detect_template_type(definition_file): |
| 92 | with io.open(definition_file, encoding='utf-8', mode='r') as f: |
| 93 | content = f.read() |
dpapad | 07cd33f | 2023-01-20 19:08:30 | [diff] [blame] | 94 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 95 | if POLYMER_TOKEN in content: |
| 96 | return 'polymer' |
| 97 | elif LIT_TOKEN in content: |
| 98 | return 'lit' |
dpapad | 07cd33f | 2023-01-20 19:08:30 | [diff] [blame] | 99 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 100 | return 'native' |
dpapad | 07cd33f | 2023-01-20 19:08:30 | [diff] [blame] | 101 | |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 102 | |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 103 | def detect_icon_template_type(icons_file): |
| 104 | with io.open(icons_file, encoding='utf-8', mode='r') as f: |
| 105 | content = f.read() |
| 106 | if 'iron-iconset-svg' in content: |
| 107 | return 'polymer_icons' |
| 108 | assert 'cr-iconset' in content, \ |
| 109 | 'icons files must include iron-iconset-svg or cr-iconset' |
| 110 | return 'lit_icons' |
| 111 | |
| 112 | |
dpapad | f3a48a1 | 2024-01-04 09:55:13 | [diff] [blame] | 113 | _IMPORTS_START_REGEX = '^<!-- #html_wrapper_imports_start$' |
| 114 | _IMPORTS_END_REGEX = '^#html_wrapper_imports_end -->$' |
| 115 | |
| 116 | |
| 117 | # Extract additional imports to carry over to the HTML wrapper file. |
| 118 | def _extract_import_metadata(file, minify): |
| 119 | start_line = -1 |
| 120 | end_line = -1 |
| 121 | |
| 122 | with io.open(file, encoding='utf-8', mode='r') as f: |
| 123 | lines = f.read().splitlines() |
| 124 | |
| 125 | for i, line in enumerate(lines): |
| 126 | if start_line == -1: |
| 127 | if re.search(_IMPORTS_START_REGEX, line): |
| 128 | assert end_line == -1 |
| 129 | start_line = i |
| 130 | else: |
| 131 | assert end_line == -1 |
| 132 | |
| 133 | if re.search(_IMPORTS_END_REGEX, line): |
| 134 | assert start_line > -1 |
| 135 | end_line = i |
| 136 | break |
| 137 | |
| 138 | if start_line == -1 or end_line == -1: |
| 139 | assert start_line == -1 |
| 140 | assert end_line == -1 |
| 141 | return None |
| 142 | |
| 143 | return { |
| 144 | # Strip metadata from content, unless minification is used, which will |
| 145 | # strip any HTML comments anyway. |
| 146 | 'content': None if minify else '\n'.join(lines[end_line + 1:]), |
| 147 | 'imports': '\n'.join(lines[start_line + 1:end_line]) + '\n', |
| 148 | } |
| 149 | |
| 150 | |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 151 | def main(argv): |
| 152 | parser = argparse.ArgumentParser() |
| 153 | parser.add_argument('--in_folder', required=True) |
| 154 | parser.add_argument('--out_folder', required=True) |
| 155 | parser.add_argument('--in_files', required=True, nargs="*") |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 156 | parser.add_argument('--minify', action='store_true') |
dpapad | 0fb3780 | 2022-07-13 01:00:52 | [diff] [blame] | 157 | parser.add_argument('--use_js', action='store_true') |
dpapad | bc6fc93 | 2022-05-28 00:47:57 | [diff] [blame] | 158 | parser.add_argument('--template', |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 159 | choices=['polymer', 'lit', 'native', 'detect'], |
dpapad | bc6fc93 | 2022-05-28 00:47:57 | [diff] [blame] | 160 | default='polymer') |
dpapad | 24071ff | 2022-09-02 01:07:54 | [diff] [blame] | 161 | parser.add_argument('--scheme', |
| 162 | choices=['chrome', 'relative'], |
dpapad | 2efd445 | 2023-04-06 01:43:45 | [diff] [blame] | 163 | default='relative') |
dpapad | bc6fc93 | 2022-05-28 00:47:57 | [diff] [blame] | 164 | |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 165 | args = parser.parse_args(argv) |
| 166 | |
| 167 | in_folder = path.normpath(path.join(_CWD, args.in_folder)) |
| 168 | out_folder = path.normpath(path.join(_CWD, args.out_folder)) |
dpapad | 0fb3780 | 2022-07-13 01:00:52 | [diff] [blame] | 169 | extension = '.js' if args.use_js else '.ts' |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 170 | |
| 171 | results = [] |
| 172 | |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 173 | # The folder to be used to read the HTML files to be wrapped. |
| 174 | wrapper_in_folder = in_folder |
| 175 | |
| 176 | if args.minify: |
| 177 | # Minify the HTML files with html-minifier before generating the wrapper |
| 178 | # .ts files. |
| 179 | # Note: Passing all HTML files to html-minifier all at once because |
| 180 | # passing them individually takes a lot longer. |
| 181 | # Storing the output in a temporary folder, which is used further below when |
| 182 | # creating the final wrapper files. |
| 183 | tmp_out_dir = tempfile.mkdtemp(dir=out_folder) |
| 184 | try: |
| 185 | wrapper_in_folder = tmp_out_dir |
| 186 | |
| 187 | # Using the programmatic Node API to invoke html-minifier, because the |
| 188 | # built-in command line API does not support explicitly specifying |
| 189 | # multiple files to be processed, and only supports specifying an input |
| 190 | # folder, which would lead to potentially processing unnecessary HTML |
| 191 | # files that are not part of the build (stale), or handled by other |
| 192 | # html_to_wrapper targets. |
| 193 | node.RunNode( |
| 194 | [path.join(_HERE_PATH, 'html_minifier.js'), in_folder, tmp_out_dir] + |
| 195 | args.in_files) |
| 196 | except RuntimeError as err: |
| 197 | shutil.rmtree(tmp_out_dir) |
| 198 | raise err |
| 199 | |
dpapad | 07cd33f | 2023-01-20 19:08:30 | [diff] [blame] | 200 | out_files = [] |
| 201 | |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 202 | # Wrap the input files (minified or not) with an enclosing .ts file. |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 203 | for in_file in args.in_files: |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 204 | wrapper_in_file = path.join(wrapper_in_folder, in_file) |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 205 | template = None |
| 206 | template_type = args.template |
| 207 | filename = path.basename(in_file) |
| 208 | effective_in_file = wrapper_in_file |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 209 | |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 210 | if filename == 'icons.html' or filename.endswith('_icons.html'): |
| 211 | if args.template == 'polymer': |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 212 | template_type = 'polymer_icons' |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 213 | elif args.template == 'lit': |
| 214 | template_type = 'lit_icons' |
| 215 | else: |
| 216 | assert args.template == 'detect', ( |
| 217 | r'Polymer/Lit icons files not supported with template="%s"' % |
| 218 | args.template) |
| 219 | template_type = detect_icon_template_type(wrapper_in_file) |
| 220 | elif filename.endswith('icons_lit.html'): |
| 221 | assert args.template == 'lit' or args.template == 'detect', ( |
| 222 | r'Lit icons files not supported with template="%s"' % args.template) |
| 223 | # Grab the content from the equivalent Polymer file, and substitute |
| 224 | # cr-iconset for iron-iconset-svg. |
| 225 | polymer_file = path.join(wrapper_in_folder, |
| 226 | in_file.replace('icons_lit', 'icons')) |
| 227 | effective_in_file = polymer_file |
| 228 | template_type = 'lit_icons' |
| 229 | elif template_type == 'detect': |
| 230 | # Locate the file that holds the web component's definition. Assumed to |
| 231 | # be in the same folder as input HTML template file. |
| 232 | definition_file = path.splitext(path.join(in_folder, |
| 233 | in_file))[0] + extension |
| 234 | template_type = detect_template_type(definition_file) |
| 235 | |
| 236 | with io.open(effective_in_file, encoding='utf-8', mode='r') as f: |
| 237 | html_content = f.read() |
dpapad | b0e74d3 | 2022-05-10 18:39:31 | [diff] [blame] | 238 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 239 | substitutions = { |
dpapad | 24071ff | 2022-09-02 01:07:54 | [diff] [blame] | 240 | 'content': html_content, |
| 241 | 'scheme': 'chrome:' if args.scheme == 'chrome' else '', |
| 242 | } |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 243 | |
rbpotter | dc36c40c | 2024-04-17 22:54:32 | [diff] [blame] | 244 | if template_type == 'lit_icons': |
| 245 | # Replace iron-iconset-svg for the case of Lit icons files generated |
| 246 | # from a Polymer icons file. |
| 247 | if 'iron-iconset-svg' in html_content: |
| 248 | html_content = html_content.replace('iron-iconset-svg', 'cr-iconset') |
| 249 | substitutions['content'] = html_content |
| 250 | elif template_type == 'lit': |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 251 | # Add Lit specific substitutions. |
| 252 | basename = path.splitext(path.basename(in_file))[0] |
| 253 | # Derive class name from file name. For example |
| 254 | # foo_bar.html -> FooBarElement. |
| 255 | class_name = ''.join(map(str.title, basename.split('_'))) + 'Element' |
| 256 | substitutions['class_name'] = class_name |
| 257 | substitutions['file_name'] = basename |
| 258 | |
dpapad | f3a48a1 | 2024-01-04 09:55:13 | [diff] [blame] | 259 | # Extracting import metadata from original non-minified template. |
| 260 | import_metadata = _extract_import_metadata( |
| 261 | path.join(args.in_folder, in_file), args.minify) |
| 262 | substitutions['imports'] = \ |
| 263 | '' if import_metadata is None else import_metadata['imports'] |
| 264 | if import_metadata is not None and not args.minify: |
| 265 | # Remove metadata lines from content. |
| 266 | substitutions['content'] = import_metadata['content'] |
| 267 | |
dpapad | 05e9257 | 2023-12-01 23:40:19 | [diff] [blame] | 268 | wrapper = TEMPLATE_MAP[template_type] % substitutions |
| 269 | |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 270 | out_folder_for_file = path.join(out_folder, path.dirname(in_file)) |
| 271 | makedirs(out_folder_for_file, exist_ok=True) |
dpapad | 07cd33f | 2023-01-20 19:08:30 | [diff] [blame] | 272 | out_file = path.join(out_folder, in_file) + extension |
| 273 | out_files.append(out_file) |
| 274 | with io.open(out_file, mode='wb') as f: |
dpapad | b0e74d3 | 2022-05-10 18:39:31 | [diff] [blame] | 275 | f.write(wrapper.encode('utf-8')) |
dpapad | d55b8f7 | 2022-06-29 19:37:32 | [diff] [blame] | 276 | |
| 277 | if args.minify: |
| 278 | # Delete the temporary folder that was holding minified HTML files, no |
| 279 | # longer needed. |
| 280 | shutil.rmtree(tmp_out_dir) |
| 281 | |
dpapad | 5d67ad17 | 2022-01-24 10:04:05 | [diff] [blame] | 282 | return |
| 283 | |
| 284 | |
| 285 | if __name__ == '__main__': |
| 286 | main(sys.argv[1:]) |