css_to_wrapper.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. # Copyright 2022 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. # Generetes a wrapper TS file around a source CSS file holding a Polymer style
  5. # module, or a Polymer <custom-style> holding CSS variable. Any metadata
  6. # necessary for populating the wrapper file are provided in the form of special
  7. # CSS comments. The ID of a style module is inferred from the filename, for
  8. # example foo_style.css will be held in a module with ID 'foo-style'.
  9. import argparse
  10. import io
  11. import re
  12. import shutil
  13. import sys
  14. import tempfile
  15. from os import path, getcwd, makedirs
  16. _HERE_PATH = path.dirname(__file__)
  17. _SRC_PATH = path.normpath(path.join(_HERE_PATH, '..', '..'))
  18. _CWD = getcwd()
  19. sys.path.append(path.join(_SRC_PATH, 'third_party', 'node'))
  20. import node
  21. _METADATA_START_REGEX = '#css_wrapper_metadata_start'
  22. _METADATA_END_REGEX = '#css_wrapper_metadata_end'
  23. _IMPORT_REGEX = '#import='
  24. _INCLUDE_REGEX = '#include='
  25. _TYPE_REGEX = '#type='
  26. _STYLE_TEMPLATE = """import \'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js\';
  27. %(imports)s
  28. const styleMod = document.createElement(\'dom-module\');
  29. styleMod.innerHTML = `
  30. <template>
  31. <style%(include)s>
  32. %(content)s
  33. </style>
  34. </template>
  35. `;
  36. styleMod.register(\'%(id)s\');"""
  37. _VARS_TEMPLATE = """import \'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js\';
  38. %(imports)s
  39. const $_documentContainer = document.createElement('template');
  40. $_documentContainer.innerHTML = `
  41. <custom-style>
  42. <style>
  43. %(content)s
  44. </style>
  45. </custom-style>
  46. `;
  47. document.head.appendChild($_documentContainer.content);"""
  48. def _parse_style_line(line, metadata):
  49. include_match = re.search(_INCLUDE_REGEX, line)
  50. if include_match:
  51. assert not metadata[
  52. 'include'], f'Found multiple "{_INCLUDE_REGEX}" lines. Only one should exist.'
  53. metadata['include'] = line[include_match.end():]
  54. import_match = re.search(_IMPORT_REGEX, line)
  55. if import_match:
  56. metadata['imports'].append(line[import_match.end():])
  57. def _parse_vars_line(line, metadata):
  58. import_match = re.search(_IMPORT_REGEX, line)
  59. if import_match:
  60. metadata['imports'].append(line[import_match.end():])
  61. def _extract_content(css_file, metadata, minified):
  62. with io.open(css_file, encoding='utf-8', mode='r') as f:
  63. # If minification is on, just grab the result from html-minifier's output.
  64. if minified:
  65. return f.read()
  66. # If minification is off, strip the special metadata comments from the
  67. # original file.
  68. lines = f.read().splitlines()
  69. return '\n'.join(lines[metadata['metadata_end_line'] + 1:])
  70. def _extract_metadata(css_file):
  71. metadata_start_line = -1
  72. metadata_end_line = -1
  73. metadata = {'type': None}
  74. with io.open(css_file, encoding='utf-8', mode='r') as f:
  75. lines = f.read().splitlines()
  76. for i, line in enumerate(lines):
  77. if metadata_start_line == -1:
  78. if _METADATA_START_REGEX in line:
  79. assert metadata_end_line == -1
  80. metadata_start_line = i
  81. else:
  82. assert metadata_end_line == -1
  83. if not metadata['type']:
  84. type_match = re.search(_TYPE_REGEX, line)
  85. if type_match:
  86. type = line[type_match.end():]
  87. assert type in ['style', 'vars']
  88. if type == 'style':
  89. id = path.splitext(path.basename(css_file))[0].replace('_', '-')
  90. metadata = {
  91. 'id': id,
  92. 'imports': [],
  93. 'include': None,
  94. 'metadata_end_line': -1,
  95. 'type': type,
  96. }
  97. elif type == 'vars':
  98. metadata = {
  99. 'imports': [],
  100. 'metadata_end_line': -1,
  101. 'type': type,
  102. }
  103. elif metadata['type'] == 'style':
  104. _parse_style_line(line, metadata)
  105. elif metadata['type'] == 'vars':
  106. _parse_vars_line(line, metadata)
  107. if _METADATA_END_REGEX in line:
  108. assert metadata_start_line > -1
  109. metadata_end_line = i
  110. metadata['metadata_end_line'] = metadata_end_line
  111. break
  112. assert metadata_start_line > -1
  113. assert metadata_end_line > -1
  114. return metadata
  115. def main(argv):
  116. parser = argparse.ArgumentParser()
  117. parser.add_argument('--in_folder', required=True)
  118. parser.add_argument('--out_folder', required=True)
  119. parser.add_argument('--in_files', required=True, nargs="*")
  120. parser.add_argument('--minify', action='store_true')
  121. parser.add_argument('--use_js', action='store_true')
  122. args = parser.parse_args(argv)
  123. in_folder = path.normpath(path.join(_CWD, args.in_folder))
  124. out_folder = path.normpath(path.join(_CWD, args.out_folder))
  125. extension = '.js' if args.use_js else '.ts'
  126. # The folder to be used to read the CSS files to be wrapped.
  127. wrapper_in_folder = in_folder
  128. if args.minify:
  129. # Minify the CSS files with html-minifier before generating the wrapper
  130. # .ts files.
  131. # Note: Passing all CSS files to html-minifier all at once because
  132. # passing them individually takes a lot longer.
  133. # Storing the output in a temporary folder, which is used further below when
  134. # creating the final wrapper files.
  135. tmp_out_dir = tempfile.mkdtemp(dir=out_folder)
  136. try:
  137. wrapper_in_folder = tmp_out_dir
  138. # Using the programmatic Node API to invoke html-minifier, because the
  139. # built-in command line API does not support explicitly specifying
  140. # multiple files to be processed, and only supports specifying an input
  141. # folder, which would lead to potentially processing unnecessary HTML
  142. # files that are not part of the build (stale), or handled by other
  143. # css_to_wrapper targets.
  144. node.RunNode(
  145. [path.join(_HERE_PATH, 'html_minifier.js'), in_folder, tmp_out_dir] +
  146. args.in_files)
  147. except RuntimeError as err:
  148. shutil.rmtree(tmp_out_dir)
  149. raise err
  150. def _urls_to_imports(urls):
  151. return '\n'.join(map(lambda i: f'import \'{i}\';', urls))
  152. for in_file in args.in_files:
  153. # Extract metadata from the original file, as the special metadata comments
  154. # only exist there.
  155. metadata = _extract_metadata(path.join(in_folder, in_file))
  156. # Extract the CSS content from either the original or the minified files.
  157. content = _extract_content(path.join(wrapper_in_folder, in_file), metadata,
  158. args.minify)
  159. wrapper = None
  160. if metadata['type'] == 'style':
  161. include = ''
  162. if metadata['include']:
  163. parsed_include = metadata['include']
  164. include = f' include="{parsed_include}"'
  165. wrapper = _STYLE_TEMPLATE % {
  166. 'imports': _urls_to_imports(metadata['imports']),
  167. 'content': content,
  168. 'include': include,
  169. 'id': metadata['id'],
  170. }
  171. elif metadata['type'] == 'vars':
  172. wrapper = _VARS_TEMPLATE % {
  173. 'imports': _urls_to_imports(metadata['imports']),
  174. 'content': content,
  175. }
  176. assert wrapper
  177. out_folder_for_file = path.join(out_folder, path.dirname(in_file))
  178. makedirs(out_folder_for_file, exist_ok=True)
  179. with io.open(path.join(out_folder, in_file) + extension, mode='wb') as f:
  180. f.write(wrapper.encode('utf-8'))
  181. if args.minify:
  182. # Delete the temporary folder that was holding minified CSS files, no
  183. # longer needed.
  184. shutil.rmtree(tmp_out_dir)
  185. return
  186. if __name__ == '__main__':
  187. main(sys.argv[1:])