blob: 797b69942f3d2ff84646cd4e7447132cc72b30ba [file] [log] [blame]
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Generates Polymer3 UI elements (using JS modules) from existing Polymer2
# elements (using HTML imports). This is useful for avoiding code duplication
# while Polymer2 to Polymer3 migration is in progress.
#
# Variables:
# html_file:
# The input Polymer2 HTML file to be processed.
#
# js_file:
# The input Polymer2 JS file to be processed, or the name of the output JS
# file when no input JS file exists (see |html_type| below).
#
# in_folder:
# The folder where |html_file| and |js_file| (when it exists) reside.
#
# out_folder:
# The output folder for the generated Polymer JS file.
#
# html_type:
# Specifies the type of the |html_file| such that the script knows how to
# process the |html_file|. Available values are:
# dom-module: A file holding a <dom-module> for a UI element (this is
# the majority case). Note: having multiple <dom-module>s
# within a single HTML file is not currently supported
# style-module: A file holding a shared style <dom-module>
# (no corresponding Polymer2 JS file exists)
# custom-style: A file holding a <custom-style> (usually a *_vars_css.html
# file, no corresponding Polymer2 JS file exists)
# iron-iconset: A file holding one or more <iron-iconset-svg> instances
# (no corresponding Polymer2 JS file exists)
# v3-ready: A file holding HTML that is already written for Polymer3. A
# Polymer3 JS file already exists for such cases. In this mode
# HTML content is simply pasted within the JS file. This mode
# will be the only supported mode after migration finishes.
#
# namespace_rewrites:
# A list of string replacements for replacing global namespaced references
# with explicitly imported dependencies in the generated JS module.
# For example "cr.foo.Bar|Bar" will replace all occurrences of "cr.foo.Bar"
# with "Bar".
#
# auto_imports:
# A list of of auto-imports, to inform the script on which variables to
# import from a JS module. For example "ui/webui/foo/bar/baz.html|Foo,Bar"
# will result in something like "import {Foo, Bar} from ...;" when
# encountering any dependency to that file.
import argparse
import io
import os
import re
import sys
_CWD = os.getcwd()
_HERE_PATH = os.path.dirname(__file__)
_ROOT = os.path.normpath(os.path.join(_HERE_PATH, '..', '..'))
POLYMER_V1_DIR = 'third_party/polymer/v1_0/components-chromium/'
POLYMER_V3_DIR = 'third_party/polymer/v3_0/components-chromium/'
# Rewrite rules for replacing global namespace references like "cr.ui.Foo", to
# "Foo" within a generated JS module. Populated from command line arguments.
_namespace_rewrites = {}
# Auto-imports map, populated from command line arguments. Specifies which
# variables to import from a given dependency. For example this is used to
# import |FocusOutlineManager| whenever a dependency to
# ui/webui/resources/html/cr/ui/focus_outline_manager.html is encountered.
_auto_imports = {}
_chrome_redirects = {
'chrome://resources/polymer/v1_0/': POLYMER_V1_DIR,
'chrome://resources/html/': 'ui/webui/resources/html/',
}
_chrome_reverse_redirects = {
POLYMER_V3_DIR: 'chrome://resources/polymer/v3_0/',
'ui/webui/resources/': 'chrome://resources/',
}
# Helper class for converting dependencies expressed in HTML imports, to JS
# imports. |to_js_import()| is the only public method exposed by this class.
# Internally an HTML import path is
#
# 1) normalized, meaning converted from a chrome or relative URL to to an
# absolute path starting at the repo's root
# 2) converted to an equivalent JS normalized path
# 3) de-normalized, meaning converted back to a relative or chrome URL
# 4) converted to a JS import statement
class Dependency:
def __init__(self, src, dst):
self.html_file = src
self.html_path = dst
self.input_format = (
'chrome' if self.html_path.startswith('chrome://') else 'relative')
self.output_format = self.input_format
self.html_path_normalized = self._to_html_normalized()
self.js_path_normalized = self._to_js_normalized()
self.js_path = self._to_js()
def _to_html_normalized(self):
if self.input_format == 'chrome':
self.html_path_normalized = self.html_path
for r in _chrome_redirects:
if self.html_path.startswith(r):
self.html_path_normalized = (
self.html_path.replace(r, _chrome_redirects[r]))
break
return self.html_path_normalized
input_dir = os.path.relpath(os.path.dirname(self.html_file), _ROOT)
return os.path.normpath(
os.path.join(input_dir, self.html_path)).replace("\\", "/")
def _to_js_normalized(self):
if re.match(POLYMER_V1_DIR, self.html_path_normalized):
return (self.html_path_normalized
.replace(POLYMER_V1_DIR, POLYMER_V3_DIR)
.replace(r'.html', '.js'))
if self.html_path_normalized == 'ui/webui/resources/html/polymer.html':
self.output_format = 'chrome'
return POLYMER_V3_DIR + 'polymer/polymer_bundled.min.js'
if re.match(r'ui/webui/resources/html/', self.html_path_normalized):
return (self.html_path_normalized
.replace(r'ui/webui/resources/html/', 'ui/webui/resources/js/')
.replace(r'.html', '.m.js'))
return self.html_path_normalized.replace(r'.html', '.m.js')
def _to_js(self):
js_path = self.js_path_normalized
if self.output_format == 'chrome':
for r in _chrome_reverse_redirects:
if self.js_path_normalized.startswith(r):
js_path = self.js_path_normalized.replace(
r, _chrome_reverse_redirects[r])
break
return js_path
input_dir = os.path.relpath(os.path.dirname(self.html_file), _ROOT)
relpath = os.path.relpath(
self.js_path_normalized, input_dir).replace("\\", "/")
# Prepend "./" if |relpath| refers to a relative subpath, that is not "../".
# This prefix is required for JS Modules paths.
if not relpath.startswith('.'):
relpath = './' + relpath
return relpath
def to_js_import(self, auto_imports):
if self.html_path_normalized in auto_imports:
imports = auto_imports[self.html_path_normalized]
return 'import {%s} from \'%s\';' % (', '.join(imports), self.js_path)
return 'import \'%s\';' % self.js_path
def _extract_dependencies(html_file):
with io.open(html_file, encoding='utf-8', mode='r') as f:
lines = f.readlines()
deps = []
for line in lines:
match = re.search(r'\s*<link rel="import" href="(.*)"', line)
if match:
deps.append(match.group(1))
return deps;
def _generate_js_imports(html_file):
return map(
lambda dep: Dependency(html_file, dep).to_js_import(_auto_imports),
_extract_dependencies(html_file))
def _extract_dom_module_id(html_file):
with io.open(html_file, encoding='utf-8', mode='r') as f:
contents = f.read()
match = re.search(r'\s*<dom-module id="(.*)"', contents)
assert match
return match.group(1)
def _add_template_markers(html_template):
return '<!--_html_template_start_-->%s<!--_html_template_end_-->' % \
html_template;
def _extract_template(html_file, html_type):
if html_type == 'v3-ready':
with io.open(html_file, encoding='utf-8', mode='r') as f:
template = f.read()
return _add_template_markers('\n' + template)
if html_type == 'dom-module':
with io.open(html_file, encoding='utf-8', mode='r') as f:
lines = f.readlines()
start_line = -1
end_line = -1
for i, line in enumerate(lines):
if re.match(r'\s*<dom-module ', line):
assert start_line == -1
assert end_line == -1
assert re.match(r'\s*<template', lines[i + 1])
start_line = i + 2;
if re.match(r'\s*</dom-module>', line):
assert start_line != -1
assert end_line == -1
assert re.match(r'\s*</template>', lines[i - 2])
assert re.match(r'\s*<script ', lines[i - 1])
end_line = i - 3;
return _add_template_markers('\n' + ''.join(lines[start_line:end_line + 1]))
if html_type == 'style-module':
with io.open(html_file, encoding='utf-8', mode='r') as f:
lines = f.readlines()
start_line = -1
end_line = -1
for i, line in enumerate(lines):
if re.match(r'\s*<dom-module ', line):
assert start_line == -1
assert end_line == -1
assert re.match(r'\s*<template', lines[i + 1])
start_line = i + 1;
if re.match(r'\s*</dom-module>', line):
assert start_line != -1
assert end_line == -1
assert re.match(r'\s*</template>', lines[i - 1])
end_line = i - 1;
return '\n' + ''.join(lines[start_line:end_line + 1])
if html_type == 'iron-iconset':
templates = []
with io.open(html_file, encoding='utf-8', mode='r') as f:
lines = f.readlines()
start_line = -1
end_line = -1
for i, line in enumerate(lines):
if re.match(r'\s*<iron-iconset-svg ', line):
assert start_line == -1
assert end_line == -1
start_line = i;
if re.match(r'\s*</iron-iconset-svg>', line):
assert start_line != -1
assert end_line == -1
end_line = i
templates.append(''.join(lines[start_line:end_line + 1]))
# Reset indices.
start_line = -1
end_line = -1
return '\n' + ''.join(templates)
assert html_type == 'custom-style'
with io.open(html_file, encoding='utf-8', mode='r') as f:
lines = f.readlines()
start_line = -1
end_line = -1
for i, line in enumerate(lines):
if re.match(r'\s*<custom-style>', line):
assert start_line == -1
assert end_line == -1
start_line = i;
if re.match(r'\s*</custom-style>', line):
assert start_line != -1
assert end_line == -1
end_line = i;
return '\n' + ''.join(lines[start_line:end_line + 1])
# Replace various global references with their non-namespaced version, for
# example "cr.ui.Foo" becomes "Foo".
def _rewrite_namespaces(string):
for rewrite in _namespace_rewrites:
string = string.replace(rewrite, _namespace_rewrites[rewrite])
return string
def _process_v3_ready(js_file, html_file):
# Extract HTML template and place in JS file.
html_template = _extract_template(html_file, 'v3-ready')
with io.open(js_file, encoding='utf-8') as f:
lines = f.readlines()
HTML_TEMPLATE_REGEX = '{__html_template__}'
for i, line in enumerate(lines):
line = line.replace(HTML_TEMPLATE_REGEX, html_template)
lines[i] = line
out_filename = os.path.basename(js_file)
return lines, out_filename
def _process_dom_module(js_file, html_file):
html_template = _extract_template(html_file, 'dom-module')
js_imports = _generate_js_imports(html_file)
# Remove IFFE opening/closing lines.
IIFE_OPENING = '(function() {\n'
IIFE_OPENING_ARROW = '(() => {\n'
IIFE_CLOSING = '})();'
# Remove this line.
CR_DEFINE_START_REGEX = r'cr.define\('
# Ignore all lines after this comment, including the line it appears on.
CR_DEFINE_END_REGEX = r'\s*// #cr_define_end'
# Replace export annotations with 'export'.
EXPORT_LINE_REGEX = '/* #export */'
with io.open(js_file, encoding='utf-8') as f:
lines = f.readlines()
imports_added = False
iife_found = False
cr_define_found = False
cr_define_end_line = -1
for i, line in enumerate(lines):
if not imports_added:
if line.startswith(IIFE_OPENING) or line.startswith(IIFE_OPENING_ARROW):
assert not cr_define_found, 'cr.define() and IFFE in the same file'
# Replace the IIFE opening line with the JS imports.
line = '\n'.join(js_imports) + '\n\n'
imports_added = True
iife_found = True
elif re.match(CR_DEFINE_START_REGEX, line):
assert not cr_define_found, 'Multiple cr.define()s are not supported'
assert not iife_found, 'cr.define() and IFFE in the same file'
line = '\n'.join(js_imports) + '\n\n'
cr_define_found = True
imports_added = True
elif line.startswith('Polymer({\n'):
# Place the JS imports right before the opening "Polymer({" line.
line = line.replace(
r'Polymer({', '%s\n\nPolymer({' % '\n'.join(js_imports))
imports_added = True
# Place the HTML content right after the opening "Polymer({" line.
# Note: There is currently an assumption that only one Polymer() declaration
# exists per file.
line = line.replace(
r'Polymer({',
'Polymer({\n _template: html`%s`,' % html_template)
line = line.replace(EXPORT_LINE_REGEX, 'export')
if line.startswith('cr.exportPath('):
line = ''
if re.match(CR_DEFINE_END_REGEX, line):
assert cr_define_found, 'Found cr_define_end without cr.define()'
cr_define_end_line = i
break
line = _rewrite_namespaces(line)
lines[i] = line
if cr_define_found:
assert cr_define_end_line != -1, 'No cr_define_end found'
lines = lines[0:cr_define_end_line]
if iife_found:
last_line = lines[-1]
assert last_line.startswith(IIFE_CLOSING), 'Could not detect IIFE closing'
lines[-1] = ''
# Use .m.js extension for the generated JS file, since both files need to be
# served by a chrome:// URL side-by-side.
out_filename = os.path.basename(js_file).replace('.js', '.m.js')
return lines, out_filename
def _process_style_module(js_file, html_file):
html_template = _extract_template(html_file, 'style-module')
js_imports = _generate_js_imports(html_file)
style_id = _extract_dom_module_id(html_file)
# Add |assetpath| attribute so that relative CSS url()s are resolved
# correctly. Without this they are resolved with respect to the main HTML
# documents location (unlike Polymer2). Note: This is assuming that only style
# modules under ui/webui/resources/ are processed by polymer_modulizer(), for
# example cr_icons_css.html.
js_template = \
"""%(js_imports)s
const template = document.createElement('template');
template.innerHTML = `
<dom-module id="%(style_id)s" assetpath="chrome://resources/">%(html_template)s</dom-module>
`;
document.body.appendChild(template.content.cloneNode(true));""" % {
'html_template': html_template,
'js_imports': '\n'.join(js_imports),
'style_id': style_id,
}
out_filename = os.path.basename(js_file)
return js_template, out_filename
def _process_custom_style(js_file, html_file):
html_template = _extract_template(html_file, 'custom-style')
js_imports = _generate_js_imports(html_file)
js_template = \
"""%(js_imports)s
const $_documentContainer = document.createElement('template');
$_documentContainer.innerHTML = `%(html_template)s`;
document.head.appendChild($_documentContainer.content);""" % {
'js_imports': '\n'.join(js_imports),
'html_template': html_template,
}
out_filename = os.path.basename(js_file)
return js_template, out_filename
def _process_iron_iconset(js_file, html_file):
html_template = _extract_template(html_file, 'iron-iconset')
js_imports = _generate_js_imports(html_file)
js_template = \
"""%(js_imports)s
const template = html`%(html_template)s`;
document.head.appendChild(template.content);
""" % {
'js_imports': '\n'.join(js_imports),
'html_template': html_template,
}
out_filename = os.path.basename(js_file)
return js_template, out_filename
def main(argv):
parser = argparse.ArgumentParser()
parser.add_argument('--in_folder', required=True)
parser.add_argument('--out_folder', required=True)
parser.add_argument('--js_file', required=True)
parser.add_argument('--html_file', required=True)
parser.add_argument('--namespace_rewrites', required=False, nargs="*")
parser.add_argument('--auto_imports', required=False, nargs="*")
parser.add_argument(
'--html_type', choices=['dom-module', 'style-module', 'custom-style',
'iron-iconset', 'v3-ready'],
required=True)
args = parser.parse_args(argv)
# Extract namespace rewrites from arguments.
if args.namespace_rewrites:
for r in args.namespace_rewrites:
before, after = r.split('|')
_namespace_rewrites[before] = after
# Extract automatic imports from arguments.
if args.auto_imports:
for entry in args.auto_imports:
path, imports = entry.split('|')
_auto_imports[path] = imports.split(',')
in_folder = os.path.normpath(os.path.join(_CWD, args.in_folder))
out_folder = os.path.normpath(os.path.join(_CWD, args.out_folder))
js_file = os.path.join(in_folder, args.js_file)
html_file = os.path.join(in_folder, args.html_file)
result = ()
if args.html_type == 'dom-module':
result = _process_dom_module(js_file, html_file)
if args.html_type == 'style-module':
result = _process_style_module(js_file, html_file)
elif args.html_type == 'custom-style':
result = _process_custom_style(js_file, html_file)
elif args.html_type == 'iron-iconset':
result = _process_iron_iconset(js_file, html_file)
elif args.html_type == 'v3-ready':
result = _process_v3_ready(js_file, html_file)
# Reconstruct file.
# Specify the newline character so that the exact same file is generated
# across platforms.
with io.open(os.path.join(out_folder, result[1]), mode='w', encoding='utf-8', newline='\n') as f:
for l in result[0]:
if (type(l) != unicode):
l = unicode(l, encoding='utf-8')
f.write(l)
return
if __name__ == '__main__':
main(sys.argv[1:])