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