1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Prepares a Chrome HTML file by inlining resources and adding references to
7high DPI resources and removing references to unsupported scale factors.
8
9This is a small gatherer that takes a HTML file, looks for src attributes
10and inlines the specified file, producing one HTML file with no external
11dependencies. It recursively inlines the included files. When inlining CSS
12image files this script also checks for the existence of high DPI versions
13of the inlined file including those on relevant platforms. Unsupported scale
14factors are also removed from existing image sets to support explicitly
15referencing all available images.
16"""
17
18import os
19import re
20
21from grit import lazy_re
22from grit import util
23from grit.format import html_inline
24from grit.gather import interface
25
26
27# Distribution string to replace with distribution.
28DIST_SUBSTR = '%DISTRIBUTION%'
29
30
31# Matches a chrome theme source URL.
32_THEME_SOURCE = lazy_re.compile(
33    '(?P<baseurl>chrome://theme/IDR_[A-Z0-9_]*)(?P<query>\?.*)?')
34# Matches CSS image urls with the capture group 'filename'.
35_CSS_IMAGE_URLS = lazy_re.compile(
36    '(?P<attribute>content|background|[\w-]*-image):[ ]*' +
37    'url\((?P<quote>"|\'|)(?P<filename>[^"\'()]*)(?P=quote)')
38# Matches CSS image sets.
39_CSS_IMAGE_SETS = lazy_re.compile(
40    '(?P<attribute>content|background|[\w-]*-image):[ ]*' +
41        '-webkit-image-set\((?P<images>' +
42        '([,\r\n ]*url\((?P<quote>"|\'|)[^"\'()]*(?P=quote)\)[ ]*[0-9.]*x)*)\)',
43    re.MULTILINE)
44# Matches a single image in a CSS image set with the capture group scale.
45_CSS_IMAGE_SET_IMAGE = lazy_re.compile('[,\r\n ]*' +
46    'url\((?P<quote>"|\'|)[^"\'()]*(?P=quote)\)[ ]*(?P<scale>[0-9.]*x)',
47    re.MULTILINE)
48_HTML_IMAGE_SRC = lazy_re.compile(
49    '<img[^>]+src=\"(?P<filename>[^">]*)\"[^>]*>')
50
51def GetImageList(
52    base_path, filename, scale_factors, distribution,
53    filename_expansion_function=None):
54  """Generate the list of images which match the provided scale factors.
55
56  Takes an image filename and checks for files of the same name in folders
57  corresponding to the supported scale factors. If the file is from a
58  chrome://theme/ source, inserts supported @Nx scale factors as high DPI
59  versions.
60
61  Args:
62    base_path: path to look for relative file paths in
63    filename: name of the base image file
64    scale_factors: a list of the supported scale factors (i.e. ['2x'])
65    distribution: string that should replace %DISTRIBUTION%
66
67  Returns:
68    array of tuples containing scale factor and image (i.e.
69        [('1x', 'image.png'), ('2x', '2x/image.png')]).
70  """
71  # Any matches for which a chrome URL handler will serve all scale factors
72  # can simply request all scale factors.
73  theme_match = _THEME_SOURCE.match(filename)
74  if theme_match:
75    images = [('1x', filename)]
76    for scale_factor in scale_factors:
77      scale_filename = "%s@%s" % (theme_match.group('baseurl'), scale_factor)
78      if theme_match.group('query'):
79        scale_filename += theme_match.group('query')
80      images.append((scale_factor, scale_filename))
81    return images
82
83  if filename.find(':') != -1:
84    # filename is probably a URL, only return filename itself.
85    return [('1x', filename)]
86
87  filename = filename.replace(DIST_SUBSTR, distribution)
88  if filename_expansion_function:
89    filename = filename_expansion_function(filename)
90  filepath = os.path.join(base_path, filename)
91  images = [('1x', filename)]
92
93  for scale_factor in scale_factors:
94    # Check for existence of file and add to image set.
95    scale_path = os.path.split(os.path.join(base_path, filename))
96    scale_image_path = os.path.join(scale_path[0], scale_factor, scale_path[1])
97    if os.path.isfile(scale_image_path):
98      # HTML/CSS always uses forward slashed paths.
99      scale_image_name = re.sub('(?P<path>(.*/)?)(?P<file>[^/]*)',
100                                '\\g<path>' + scale_factor + '/\\g<file>',
101                                filename)
102      images.append((scale_factor, scale_image_name))
103  return images
104
105
106def GenerateImageSet(images, quote):
107  """Generates a -webkit-image-set for the provided list of images.
108
109  Args:
110    images: an array of tuples giving scale factor and file path
111            (i.e. [('1x', 'image.png'), ('2x', '2x/image.png')]).
112    quote: a string giving the quotation character to use (i.e. "'")
113
114  Returns:
115    string giving a -webkit-image-set rule referencing the provided images.
116        (i.e. '-webkit-image-set(url('image.png') 1x, url('2x/image.png') 2x)')
117  """
118  imageset = []
119  for (scale_factor, filename) in images:
120    imageset.append("url(%s%s%s) %s" % (quote, filename, quote, scale_factor))
121  return "-webkit-image-set(%s)" % (', '.join(imageset))
122
123
124def InsertImageSet(
125    src_match, base_path, scale_factors, distribution,
126    filename_expansion_function=None):
127  """Regex replace function which inserts -webkit-image-set.
128
129  Takes a regex match for url('path'). If the file is local, checks for
130  files of the same name in folders corresponding to the supported scale
131  factors. If the file is from a chrome://theme/ source, inserts the
132  supported @Nx scale factor request. In either case inserts a
133  -webkit-image-set rule to fetch the appropriate image for the current
134  scale factor.
135
136  Args:
137    src_match: regex match object from _CSS_IMAGE_URLS
138    base_path: path to look for relative file paths in
139    scale_factors: a list of the supported scale factors (i.e. ['2x'])
140    distribution: string that should replace %DISTRIBUTION%.
141
142  Returns:
143    string
144  """
145  quote = src_match.group('quote')
146  filename = src_match.group('filename')
147  attr = src_match.group('attribute')
148  image_list = GetImageList(
149      base_path, filename, scale_factors, distribution,
150      filename_expansion_function=filename_expansion_function)
151
152  # Don't modify the source if there is only one image.
153  if len(image_list) == 1:
154    return src_match.group(0)
155
156  return "%s: %s" % (attr, GenerateImageSet(image_list, quote)[:-1])
157
158
159def InsertImageStyle(
160    src_match, base_path, scale_factors, distribution,
161    filename_expansion_function=None):
162  """Regex replace function which adds a content style to an <img>.
163
164  Takes a regex match from _HTML_IMAGE_SRC and replaces the attribute with a CSS
165  style which defines the image set.
166  """
167  filename = src_match.group('filename')
168  image_list = GetImageList(
169      base_path, filename, scale_factors, distribution,
170      filename_expansion_function=filename_expansion_function)
171
172  # Don't modify the source if there is only one image or image already defines
173  # a style.
174  if src_match.group(0).find(" style=\"") != -1 or len(image_list) == 1:
175    return src_match.group(0)
176
177  return "%s style=\"content: %s;\">" % (src_match.group(0)[:-1],
178                                        GenerateImageSet(image_list, "'"))
179
180
181def InsertImageSets(
182    filepath, text, scale_factors, distribution,
183    filename_expansion_function=None):
184  """Helper function that adds references to external images available in any of
185  scale_factors in CSS backgrounds.
186  """
187  # Add high DPI urls for css attributes: content, background,
188  # or *-image or <img src="foo">.
189  return _CSS_IMAGE_URLS.sub(
190      lambda m: InsertImageSet(
191          m, filepath, scale_factors, distribution,
192          filename_expansion_function=filename_expansion_function),
193      _HTML_IMAGE_SRC.sub(
194          lambda m: InsertImageStyle(
195              m, filepath, scale_factors, distribution,
196              filename_expansion_function=filename_expansion_function),
197          text)).decode('utf-8').encode('utf-8')
198
199
200def RemoveImagesNotIn(scale_factors, src_match):
201  """Regex replace function which removes images for scale factors not in
202  scale_factors.
203
204  Takes a regex match for _CSS_IMAGE_SETS. For each image in the group images,
205  checks if this scale factor is in scale_factors and if not, removes it.
206
207  Args:
208    scale_factors: a list of the supported scale factors (i.e. ['1x', '2x'])
209    src_match: regex match object from _CSS_IMAGE_SETS
210
211  Returns:
212    string
213  """
214  attr = src_match.group('attribute')
215  images = _CSS_IMAGE_SET_IMAGE.sub(
216      lambda m: m.group(0) if m.group('scale') in scale_factors else '',
217      src_match.group('images'))
218  return "%s: -webkit-image-set(%s)" % (attr, images)
219
220
221def RemoveImageSetImages(text, scale_factors):
222  """Helper function which removes images in image sets not in the list of
223  supported scale_factors.
224  """
225  return _CSS_IMAGE_SETS.sub(
226      lambda m: RemoveImagesNotIn(scale_factors, m), text)
227
228
229def ProcessImageSets(
230    filepath, text, scale_factors, distribution,
231    filename_expansion_function=None):
232  """Helper function that adds references to external images available in other
233  scale_factors and removes images from image-sets in unsupported scale_factors.
234  """
235  # Explicitly add 1x to supported scale factors so that it is not removed.
236  supported_scale_factors = ['1x']
237  supported_scale_factors.extend(scale_factors)
238  return InsertImageSets(
239      filepath,
240      RemoveImageSetImages(text, supported_scale_factors),
241      scale_factors,
242      distribution,
243      filename_expansion_function=filename_expansion_function)
244
245
246class ChromeHtml(interface.GathererBase):
247  """Represents an HTML document processed for Chrome WebUI.
248
249  HTML documents used in Chrome WebUI have local resources inlined and
250  automatically insert references to high DPI assets used in CSS properties
251  with the use of the -webkit-image-set value. References to unsupported scale
252  factors in image sets are also removed. This does not generate any
253  translateable messages and instead generates a single DataPack resource.
254  """
255
256  def __init__(self, *args, **kwargs):
257    super(ChromeHtml, self).__init__(*args, **kwargs)
258    self.allow_external_script_ = False
259    self.flatten_html_ = False
260    # 1x resources are implicitly already in the source and do not need to be
261    # added.
262    self.scale_factors_ = []
263    self.filename_expansion_function = None
264
265  def SetAttributes(self, attrs):
266    self.allow_external_script_ = ('allowexternalscript' in attrs and
267                                   attrs['allowexternalscript'] == 'true')
268    self.flatten_html_ = ('flattenhtml' in attrs and
269                          attrs['flattenhtml'] == 'true')
270
271  def SetDefines(self, defines):
272    if 'scale_factors' in defines:
273      self.scale_factors_ = defines['scale_factors'].split(',')
274
275  def GetText(self):
276    """Returns inlined text of the HTML document."""
277    return self.inlined_text_
278
279  def GetTextualIds(self):
280    return [self.extkey]
281
282  def GetData(self, lang, encoding):
283    """Returns inlined text of the HTML document."""
284    return self.inlined_text_
285
286  def GetHtmlResourceFilenames(self):
287    """Returns a set of all filenames inlined by this file."""
288    if self.flatten_html_:
289      return html_inline.GetResourceFilenames(
290          self.grd_node.ToRealPath(self.GetInputPath()),
291          allow_external_script=self.allow_external_script_,
292          rewrite_function=lambda fp, t, d: ProcessImageSets(
293              fp, t, self.scale_factors_, d,
294              filename_expansion_function=self.filename_expansion_function),
295          filename_expansion_function=self.filename_expansion_function)
296    return []
297
298  def Translate(self, lang, pseudo_if_not_available=True,
299                skeleton_gatherer=None, fallback_to_english=False):
300    """Returns this document translated."""
301    return self.inlined_text_
302
303  def SetFilenameExpansionFunction(self, fn):
304    self.filename_expansion_function = fn
305
306  def Parse(self):
307    """Parses and inlines the represented file."""
308
309    filename = self.GetInputPath()
310    if self.filename_expansion_function:
311      filename = self.filename_expansion_function(filename)
312    # Hack: some unit tests supply an absolute path and no root node.
313    if not os.path.isabs(filename):
314      filename = self.grd_node.ToRealPath(filename)
315    if self.flatten_html_:
316      self.inlined_text_ = html_inline.InlineToString(
317          filename,
318          self.grd_node,
319          allow_external_script = self.allow_external_script_,
320          rewrite_function=lambda fp, t, d: ProcessImageSets(
321              fp, t, self.scale_factors_, d,
322              filename_expansion_function=self.filename_expansion_function),
323          filename_expansion_function=self.filename_expansion_function)
324    else:
325      distribution = html_inline.GetDistribution()
326      self.inlined_text_ = ProcessImageSets(
327          os.path.dirname(filename),
328          util.ReadFile(filename, 'utf-8'),
329          self.scale_factors_,
330          distribution,
331          filename_expansion_function=self.filename_expansion_function)
332