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