blob: 062ef045cecb11d5684806e2875439dad8efdfbd [file] [log] [blame]
Avi Drissmandfd880852022-09-15 20:11:091# Copyright 2022 The Chromium Authors
dpapad5d67ad172022-01-24 10:04:052# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
dpapadb0e74d32022-05-10 18:39:315# 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.
dpapad5d67ad172022-01-24 10:04:0520
21import argparse
dpapad5d67ad172022-01-24 10:04:0522import io
dpapadf3a48a12024-01-04 09:55:1323import re
dpapadd55b8f72022-06-29 19:37:3224import shutil
25import sys
26import tempfile
dpapad5d67ad172022-01-24 10:04:0527from os import path, getcwd, makedirs
28
dpapadd55b8f72022-06-29 19:37:3229_HERE_PATH = path.dirname(__file__)
30_SRC_PATH = path.normpath(path.join(_HERE_PATH, '..', '..'))
dpapad5d67ad172022-01-24 10:04:0531_CWD = getcwd()
32
dpapadd55b8f72022-06-29 19:37:3233sys.path.append(path.join(_SRC_PATH, 'third_party', 'node'))
34import node
35
dpapad05e92572023-12-01 23:40:1936# Template for native web component HTML templates.
37_NATIVE_ELEMENT_TEMPLATE = """import {getTrustedHTML} from '%(scheme)s//resources/js/static_types.js';
dpapadbc6fc932022-05-28 00:47:5738export function getTemplate() {
dpapad24071ff2022-09-02 01:07:5439 return getTrustedHTML`<!--_html_template_start_-->%(content)s<!--_html_template_end_-->`;
dpapadbc6fc932022-05-28 00:47:5740}"""
41
dpapad05e92572023-12-01 23:40:1942# 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';
dpapadb0e74d32022-05-10 18:39:3144export function getTemplate() {
dpapad24071ff2022-09-02 01:07:5445 return html`<!--_html_template_start_-->%(content)s<!--_html_template_end_-->`;
dpapadb0e74d32022-05-10 18:39:3146}"""
47
dpapad05e92572023-12-01 23:40:1948# Template for Lit component HTML templates.
49_LIT_ELEMENT_TEMPLATE = """import {html} from '%(scheme)s//resources/lit/v3_0/lit.rollup.js';
50import type {%(class_name)s} from './%(file_name)s.js';
dpapadf3a48a12024-01-04 09:55:1351%(imports)s
dpapad05e92572023-12-01 23:40:1952export 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';
dpapad24071ff2022-09-02 01:07:5458import {html} from '%(scheme)s//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
dpapadb0e74d32022-05-10 18:39:3159
dpapad24071ff2022-09-02 01:07:5460const template = html`%(content)s`;
dpapadb0e74d32022-05-10 18:39:3161document.head.appendChild(template.content);
62"""
dpapad5d67ad172022-01-24 10:04:0563
rbpotterdc36c40c2024-04-17 22:54:3264# Template for Lit icon HTML files.
65_LIT_ICONS_TEMPLATE = """import '%(scheme)s//resources/cr_elements/cr_icon/cr_iconset.js';
dpapad0f680172024-04-23 20:50:5966import {getTrustedHTML} from '%(scheme)s//resources/js/static_types.js';
rbpotterdc36c40c2024-04-17 22:54:3267
dpapad0f680172024-04-23 20:50:5968const div = document.createElement('div');
69div.innerHTML = getTrustedHTML`%(content)s`;
70const iconsets = div.querySelectorAll('cr-iconset');
71for (const iconset of iconsets) {
72 document.head.appendChild(iconset);
73}
rbpotterdc36c40c2024-04-17 22:54:3274"""
75
dpapad05e92572023-12-01 23:40:1976# Tokens used to detect whether the underlying custom element is based on
77# Polymer or Lit.
dpapad07cd33f2023-01-20 19:08:3078POLYMER_TOKEN = '//resources/polymer/v3_0/polymer/polymer_bundled.min.js'
dpapad05e92572023-12-01 23:40:1979LIT_TOKEN = '//resources/lit/v3_0/lit.rollup.js'
80
81# Map holding all the different types of HTML files to generate wrappers for.
82TEMPLATE_MAP = {
83 'lit': _LIT_ELEMENT_TEMPLATE,
rbpotterdc36c40c2024-04-17 22:54:3284 'lit_icons': _LIT_ICONS_TEMPLATE,
dpapad05e92572023-12-01 23:40:1985 'native': _NATIVE_ELEMENT_TEMPLATE,
86 'polymer_icons': _POLYMER_ICONS_TEMPLATE,
87 'polymer': _POLYMER_ELEMENT_TEMPLATE,
88}
dpapad07cd33f2023-01-20 19:08:3089
90
dpapad05e92572023-12-01 23:40:1991def detect_template_type(definition_file):
92 with io.open(definition_file, encoding='utf-8', mode='r') as f:
93 content = f.read()
dpapad07cd33f2023-01-20 19:08:3094
dpapad05e92572023-12-01 23:40:1995 if POLYMER_TOKEN in content:
96 return 'polymer'
97 elif LIT_TOKEN in content:
98 return 'lit'
dpapad07cd33f2023-01-20 19:08:3099
dpapad05e92572023-12-01 23:40:19100 return 'native'
dpapad07cd33f2023-01-20 19:08:30101
dpapad5d67ad172022-01-24 10:04:05102
rbpotterdc36c40c2024-04-17 22:54:32103def 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
dpapadf3a48a12024-01-04 09:55:13113_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.
118def _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
dpapad5d67ad172022-01-24 10:04:05151def 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="*")
dpapadd55b8f72022-06-29 19:37:32156 parser.add_argument('--minify', action='store_true')
dpapad0fb37802022-07-13 01:00:52157 parser.add_argument('--use_js', action='store_true')
dpapadbc6fc932022-05-28 00:47:57158 parser.add_argument('--template',
dpapad05e92572023-12-01 23:40:19159 choices=['polymer', 'lit', 'native', 'detect'],
dpapadbc6fc932022-05-28 00:47:57160 default='polymer')
dpapad24071ff2022-09-02 01:07:54161 parser.add_argument('--scheme',
162 choices=['chrome', 'relative'],
dpapad2efd4452023-04-06 01:43:45163 default='relative')
dpapadbc6fc932022-05-28 00:47:57164
dpapad5d67ad172022-01-24 10:04:05165 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))
dpapad0fb37802022-07-13 01:00:52169 extension = '.js' if args.use_js else '.ts'
dpapad5d67ad172022-01-24 10:04:05170
171 results = []
172
dpapadd55b8f72022-06-29 19:37:32173 # 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
dpapad07cd33f2023-01-20 19:08:30200 out_files = []
201
dpapadd55b8f72022-06-29 19:37:32202 # Wrap the input files (minified or not) with an enclosing .ts file.
dpapad5d67ad172022-01-24 10:04:05203 for in_file in args.in_files:
dpapadd55b8f72022-06-29 19:37:32204 wrapper_in_file = path.join(wrapper_in_folder, in_file)
rbpotterdc36c40c2024-04-17 22:54:32205 template = None
206 template_type = args.template
207 filename = path.basename(in_file)
208 effective_in_file = wrapper_in_file
dpapadd55b8f72022-06-29 19:37:32209
rbpotterdc36c40c2024-04-17 22:54:32210 if filename == 'icons.html' or filename.endswith('_icons.html'):
211 if args.template == 'polymer':
dpapad05e92572023-12-01 23:40:19212 template_type = 'polymer_icons'
rbpotterdc36c40c2024-04-17 22:54:32213 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()
dpapadb0e74d32022-05-10 18:39:31238
dpapad05e92572023-12-01 23:40:19239 substitutions = {
dpapad24071ff2022-09-02 01:07:54240 'content': html_content,
241 'scheme': 'chrome:' if args.scheme == 'chrome' else '',
242 }
dpapad5d67ad172022-01-24 10:04:05243
rbpotterdc36c40c2024-04-17 22:54:32244 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':
dpapad05e92572023-12-01 23:40:19251 # 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
dpapadf3a48a12024-01-04 09:55:13259 # 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
dpapad05e92572023-12-01 23:40:19268 wrapper = TEMPLATE_MAP[template_type] % substitutions
269
dpapad5d67ad172022-01-24 10:04:05270 out_folder_for_file = path.join(out_folder, path.dirname(in_file))
271 makedirs(out_folder_for_file, exist_ok=True)
dpapad07cd33f2023-01-20 19:08:30272 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:
dpapadb0e74d32022-05-10 18:39:31275 f.write(wrapper.encode('utf-8'))
dpapadd55b8f72022-06-29 19:37:32276
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
dpapad5d67ad172022-01-24 10:04:05282 return
283
284
285if __name__ == '__main__':
286 main(sys.argv[1:])