| # Copyright 2022 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # Genaretes a wrapper TS file around a source HTML file holding either |
| # 1) a Polymer element template or |
| # 2) an <iron-iconset-svg> definitions |
| # |
| # Note: The HTML file must be named either 'icons.html' or be suffixed with |
| # '_icons.html' for this tool to treat them as #2. Consequently, files holding |
| # Polymer element templates should not use such naming to be treated as #1. |
| # |
| # In case #1 the wrapper exports a getTemplate() function that can be used at |
| # runtime to import the template. This is useful for implementing Web Components |
| # using JS modules, where all the HTML needs to reside in a JS file (no more |
| # HTML imports). |
| # |
| # In case #2 the wrapper adds the <iron-iconset-svg> element to <head>, so that |
| # it can be used by <iron-icon> instances. |
| |
| import argparse |
| import io |
| import re |
| import shutil |
| import sys |
| import tempfile |
| from os import path, getcwd, makedirs |
| |
| _HERE_PATH = path.dirname(__file__) |
| _SRC_PATH = path.normpath(path.join(_HERE_PATH, '..', '..')) |
| _CWD = getcwd() |
| |
| sys.path.append(path.join(_SRC_PATH, 'third_party', 'node')) |
| import node |
| |
| # Template for native web component HTML templates. |
| _NATIVE_ELEMENT_TEMPLATE = """import {getTrustedHTML} from '%(scheme)s//resources/js/static_types.js'; |
| export function getTemplate() { |
| return getTrustedHTML`<!--_html_template_start_-->%(content)s<!--_html_template_end_-->`; |
| }""" |
| |
| # Template for Polymer web component HTML templates. |
| _POLYMER_ELEMENT_TEMPLATE = """import {html} from '%(scheme)s//resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| export function getTemplate() { |
| return html`<!--_html_template_start_-->%(content)s<!--_html_template_end_-->`; |
| }""" |
| |
| # Template for Lit component HTML templates. |
| _LIT_ELEMENT_TEMPLATE = """import {html} from '%(scheme)s//resources/lit/v3_0/lit.rollup.js'; |
| import type {%(class_name)s} from './%(file_name)s.js'; |
| %(imports)s |
| export function getHtml(this: %(class_name)s) { |
| return html`<!--_html_template_start_-->%(content)s<!--_html_template_end_-->`; |
| }""" |
| |
| # Template for Polymer icon HTML files. |
| _POLYMER_ICONS_TEMPLATE = """import '%(scheme)s//resources/polymer/v3_0/iron-iconset-svg/iron-iconset-svg.js'; |
| import {html} from '%(scheme)s//resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| |
| const template = html`%(content)s`; |
| document.head.appendChild(template.content); |
| """ |
| |
| # Template for Lit icon HTML files. |
| _LIT_ICONS_TEMPLATE = """import '%(scheme)s//resources/cr_elements/cr_icon/cr_iconset.js'; |
| import {getTrustedHTML} from '%(scheme)s//resources/js/static_types.js'; |
| |
| const div = document.createElement('div'); |
| div.innerHTML = getTrustedHTML`%(content)s`; |
| const iconsets = div.querySelectorAll('cr-iconset'); |
| for (const iconset of iconsets) { |
| document.head.appendChild(iconset); |
| } |
| """ |
| |
| # Tokens used to detect whether the underlying custom element is based on |
| # Polymer or Lit. |
| POLYMER_TOKEN = '//resources/polymer/v3_0/polymer/polymer_bundled.min.js' |
| LIT_TOKEN = '//resources/lit/v3_0/lit.rollup.js' |
| |
| # Map holding all the different types of HTML files to generate wrappers for. |
| TEMPLATE_MAP = { |
| 'lit': _LIT_ELEMENT_TEMPLATE, |
| 'lit_icons': _LIT_ICONS_TEMPLATE, |
| 'native': _NATIVE_ELEMENT_TEMPLATE, |
| 'polymer_icons': _POLYMER_ICONS_TEMPLATE, |
| 'polymer': _POLYMER_ELEMENT_TEMPLATE, |
| } |
| |
| |
| def detect_template_type(definition_file): |
| with io.open(definition_file, encoding='utf-8', mode='r') as f: |
| content = f.read() |
| |
| if POLYMER_TOKEN in content: |
| return 'polymer' |
| elif LIT_TOKEN in content: |
| return 'lit' |
| |
| return 'native' |
| |
| |
| def detect_icon_template_type(icons_file): |
| with io.open(icons_file, encoding='utf-8', mode='r') as f: |
| content = f.read() |
| if 'iron-iconset-svg' in content: |
| return 'polymer_icons' |
| assert 'cr-iconset' in content, \ |
| 'icons files must include iron-iconset-svg or cr-iconset' |
| return 'lit_icons' |
| |
| |
| _IMPORTS_START_REGEX = '^<!-- #html_wrapper_imports_start$' |
| _IMPORTS_END_REGEX = '^#html_wrapper_imports_end -->$' |
| |
| |
| # Extract additional imports to carry over to the HTML wrapper file. |
| def _extract_import_metadata(file, minify): |
| start_line = -1 |
| end_line = -1 |
| |
| with io.open(file, encoding='utf-8', mode='r') as f: |
| lines = f.read().splitlines() |
| |
| for i, line in enumerate(lines): |
| if start_line == -1: |
| if re.search(_IMPORTS_START_REGEX, line): |
| assert end_line == -1 |
| start_line = i |
| else: |
| assert end_line == -1 |
| |
| if re.search(_IMPORTS_END_REGEX, line): |
| assert start_line > -1 |
| end_line = i |
| break |
| |
| if start_line == -1 or end_line == -1: |
| assert start_line == -1 |
| assert end_line == -1 |
| return None |
| |
| return { |
| # Strip metadata from content, unless minification is used, which will |
| # strip any HTML comments anyway. |
| 'content': None if minify else '\n'.join(lines[end_line + 1:]), |
| 'imports': '\n'.join(lines[start_line + 1:end_line]) + '\n', |
| } |
| |
| |
| def main(argv): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--in_folder', required=True) |
| parser.add_argument('--out_folder', required=True) |
| parser.add_argument('--in_files', required=True, nargs="*") |
| parser.add_argument('--minify', action='store_true') |
| parser.add_argument('--use_js', action='store_true') |
| parser.add_argument('--template', |
| choices=['polymer', 'lit', 'native', 'detect'], |
| default='polymer') |
| parser.add_argument('--scheme', |
| choices=['chrome', 'relative'], |
| default='relative') |
| |
| args = parser.parse_args(argv) |
| |
| in_folder = path.normpath(path.join(_CWD, args.in_folder)) |
| out_folder = path.normpath(path.join(_CWD, args.out_folder)) |
| extension = '.js' if args.use_js else '.ts' |
| |
| results = [] |
| |
| # The folder to be used to read the HTML files to be wrapped. |
| wrapper_in_folder = in_folder |
| |
| if args.minify: |
| # Minify the HTML files with html-minifier before generating the wrapper |
| # .ts files. |
| # Note: Passing all HTML files to html-minifier all at once because |
| # passing them individually takes a lot longer. |
| # Storing the output in a temporary folder, which is used further below when |
| # creating the final wrapper files. |
| tmp_out_dir = tempfile.mkdtemp(dir=out_folder) |
| try: |
| wrapper_in_folder = tmp_out_dir |
| |
| # Using the programmatic Node API to invoke html-minifier, because the |
| # built-in command line API does not support explicitly specifying |
| # multiple files to be processed, and only supports specifying an input |
| # folder, which would lead to potentially processing unnecessary HTML |
| # files that are not part of the build (stale), or handled by other |
| # html_to_wrapper targets. |
| node.RunNode( |
| [path.join(_HERE_PATH, 'html_minifier.js'), in_folder, tmp_out_dir] + |
| args.in_files) |
| except RuntimeError as err: |
| shutil.rmtree(tmp_out_dir) |
| raise err |
| |
| out_files = [] |
| |
| # Wrap the input files (minified or not) with an enclosing .ts file. |
| for in_file in args.in_files: |
| wrapper_in_file = path.join(wrapper_in_folder, in_file) |
| template = None |
| template_type = args.template |
| filename = path.basename(in_file) |
| effective_in_file = wrapper_in_file |
| |
| if filename == 'icons.html' or filename.endswith('_icons.html'): |
| if args.template == 'polymer': |
| template_type = 'polymer_icons' |
| elif args.template == 'lit': |
| template_type = 'lit_icons' |
| else: |
| assert args.template == 'detect', ( |
| r'Polymer/Lit icons files not supported with template="%s"' % |
| args.template) |
| template_type = detect_icon_template_type(wrapper_in_file) |
| elif filename.endswith('icons_lit.html'): |
| assert args.template == 'lit' or args.template == 'detect', ( |
| r'Lit icons files not supported with template="%s"' % args.template) |
| # Grab the content from the equivalent Polymer file, and substitute |
| # cr-iconset for iron-iconset-svg. |
| polymer_file = path.join(wrapper_in_folder, |
| in_file.replace('icons_lit', 'icons')) |
| effective_in_file = polymer_file |
| template_type = 'lit_icons' |
| elif template_type == 'detect': |
| # Locate the file that holds the web component's definition. Assumed to |
| # be in the same folder as input HTML template file. |
| definition_file = path.splitext(path.join(in_folder, |
| in_file))[0] + extension |
| template_type = detect_template_type(definition_file) |
| |
| with io.open(effective_in_file, encoding='utf-8', mode='r') as f: |
| html_content = f.read() |
| |
| substitutions = { |
| 'content': html_content, |
| 'scheme': 'chrome:' if args.scheme == 'chrome' else '', |
| } |
| |
| if template_type == 'lit_icons': |
| # Replace iron-iconset-svg for the case of Lit icons files generated |
| # from a Polymer icons file. |
| if 'iron-iconset-svg' in html_content: |
| html_content = html_content.replace('iron-iconset-svg', 'cr-iconset') |
| substitutions['content'] = html_content |
| elif template_type == 'lit': |
| # Add Lit specific substitutions. |
| basename = path.splitext(path.basename(in_file))[0] |
| # Derive class name from file name. For example |
| # foo_bar.html -> FooBarElement. |
| class_name = ''.join(map(str.title, basename.split('_'))) + 'Element' |
| substitutions['class_name'] = class_name |
| substitutions['file_name'] = basename |
| |
| # Extracting import metadata from original non-minified template. |
| import_metadata = _extract_import_metadata( |
| path.join(args.in_folder, in_file), args.minify) |
| substitutions['imports'] = \ |
| '' if import_metadata is None else import_metadata['imports'] |
| if import_metadata is not None and not args.minify: |
| # Remove metadata lines from content. |
| substitutions['content'] = import_metadata['content'] |
| |
| wrapper = TEMPLATE_MAP[template_type] % substitutions |
| |
| out_folder_for_file = path.join(out_folder, path.dirname(in_file)) |
| makedirs(out_folder_for_file, exist_ok=True) |
| out_file = path.join(out_folder, in_file) + extension |
| out_files.append(out_file) |
| with io.open(out_file, mode='wb') as f: |
| f.write(wrapper.encode('utf-8')) |
| |
| if args.minify: |
| # Delete the temporary folder that was holding minified HTML files, no |
| # longer needed. |
| shutil.rmtree(tmp_out_dir) |
| |
| return |
| |
| |
| if __name__ == '__main__': |
| main(sys.argv[1:]) |