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