summaryrefslogtreecommitdiff
path: root/chromium/tools/polymer/polymer.py
blob: e75672396585e1143e5bf7431c60842badc81be4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# 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 = {}

# Populated from command line arguments. Specifies a list of HTML imports to
# ignore when converting HTML imports to JS modules.
_ignore_imports = []

_migrated_imports = []

_chrome_redirects = {
    'chrome://resources/polymer/v1_0/': POLYMER_V1_DIR,
    'chrome://resources/html/': 'ui/webui/resources/html/',
    'chrome://resources/cr_elements/': 'ui/webui/resources/cr_elements/',
    '//resources/polymer/v1_0/': POLYMER_V1_DIR,
    '//resources/html/': 'ui/webui/resources/html/',
    '//resources/cr_elements/': 'ui/webui/resources/cr_elements/',
}

_chrome_reverse_redirects = {
    POLYMER_V3_DIR: '//resources/polymer/v3_0/',
    'ui/webui/resources/': '//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://')
                         or self.html_path.startswith('//') 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'))

    extension = (
        '.js' if self.html_path_normalized in _migrated_imports else '.m.js')
    return self.html_path_normalized.replace(r'.html', extension)

  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 _generate_js_imports(html_file):
  output = []
  imports_end_index = -1
  imports_found = False
  with io.open(html_file, encoding='utf-8', mode='r') as f:
    lines = f.readlines()
    deps = []
    for i, line in enumerate(lines):
      match = re.search(r'\s*<link rel="import" href="(.*)"', line)
      if match:
        if not imports_found:
          imports_found = True
          # Include the previous line if it is an opening <if> tag.
          if (i > 0):
            previous_line = lines[i - 1]
            if re.search(r'^\s*<if', previous_line):
              previous_line = '// ' + previous_line
              output.append(previous_line.rstrip('\n'))

        imports_end_index = i

        # Convert HTML import URL to equivalent JS import URL.
        dep = Dependency(html_file, match.group(1))
        js_import = dep.to_js_import(_auto_imports)
        if dep.html_path_normalized in _ignore_imports:
          output.append('// ' + js_import)
        else:
          output.append(js_import)

      elif imports_found:
        if re.search(r'^\s*</?if', line):
          line = '// ' + line
        output.append(line.rstrip('\n'))

  if len(output) == 0:
    return output

  # Include the next line if it is a closing </if> tag.
  if re.search(r'^// \s*</if>', output[imports_end_index + 1]):
    imports_end_index += 1

  return output[0:imports_end_index + 1]


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;
        # Should not have an iron-iconset-svg in a dom-module file.
        assert not re.match(r'\s*<iron-iconset-svg ', line)

    # If an opening <dom-module> tag was found, check that a closing one was
    # found as well.
    if start_line != - 1:
      assert end_line != -1

    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 */'

  # Ignore lines with an ignore annotation.
  IGNORE_LINE_REGEX = '\s*/\* #ignore \*/(\S|\s)*'

  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 re.match(CR_DEFINE_END_REGEX, line):
      assert cr_define_found, 'Found cr_define_end without cr.define()'
      cr_define_end_line = i
      break

    if re.match(IGNORE_LINE_REGEX, line):
      line = ''

    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 _resetGlobals():
  global _namespace_rewrites
  _namespace_rewrites = {}
  global _auto_imports
  _auto_imports = {}
  global _ignore_imports
  _ignore_imports = []
  global _migrated_imports
  _migrated_imports = []

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('--ignore_imports', required=False, nargs="*")
  parser.add_argument('--auto_imports', required=False, nargs="*")
  parser.add_argument('--migrated_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:
    global _auto_imports
    for entry in args.auto_imports:
      path, imports = entry.split('|')
      _auto_imports[path] = imports.split(',')

  # Extract ignored imports from arguments.
  if args.ignore_imports:
    assert args.html_type != 'v3-ready'
    global _ignore_imports
    _ignore_imports = args.ignore_imports

  # Extract migrated imports from arguments.
  if args.migrated_imports:
    assert args.html_type != 'v3-ready'
    global _migrated_imports
    _migrated_imports = args.migrated_imports

  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='wb') as f:
    for l in result[0]:
      f.write(l.encode('utf-8'))

  # Reset global variables so that main() can be invoked multiple times during
  # testing without leaking state from one test to the next.
  _resetGlobals()
  return


if __name__ == '__main__':
  main(sys.argv[1:])