model.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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. import re
  5. import collections
  6. from style_variable_generator.color import Color
  7. from style_variable_generator.opacity import Opacity
  8. from abc import ABC, abstractmethod
  9. class Modes:
  10. LIGHT = 'light'
  11. DARK = 'dark'
  12. DEBUG = 'debug'
  13. # The mode that colors will fallback to when not specified in a
  14. # non-default mode. An error will be raised if a color in any mode is
  15. # not specified in the default mode.
  16. DEFAULT = LIGHT
  17. ALL = [LIGHT, DARK, DEBUG]
  18. class VariableType:
  19. COLOR = 'color'
  20. OPACITY = 'opacity'
  21. UNTYPED_CSS = 'untyped_css'
  22. TYPEFACE = 'typeface'
  23. FONT_FAMILY = 'font_family'
  24. class StyleVariable(object):
  25. '''An intermediate representation of a single variable that the generator
  26. knows about.
  27. Some JSON entries will generate multiple StyleVariables (e.g when
  28. generate_per_mode is true), and different Generators may create multiple
  29. per-platform variables (e.g CSS generates an var and var-rgb).
  30. '''
  31. def __init__(self, variable_type, name, json_value, context):
  32. if not re.match(r'^[a-z0-9_\.\-]+$', name):
  33. raise ValueError(name + ' is not a valid variable name ' +
  34. '(lower case, 0-9, _)')
  35. self.variable_type = variable_type
  36. self.name = name
  37. self.json_value = json_value
  38. self.context = context or {}
  39. class Submodel(ABC):
  40. '''Abstract Base Class for all Submodels.'''
  41. @abstractmethod
  42. def Add(self, name, value_obj, context):
  43. '''Adds the a variable represented by |value_obj| to the submodel.
  44. Returns a list of |StyleVariable| objects representing the variables
  45. added.
  46. '''
  47. assert False
  48. # Submodels are expected to provide dict-like interfaces.
  49. @abstractmethod
  50. def keys(self):
  51. assert False
  52. @abstractmethod
  53. def items(self):
  54. assert False
  55. @abstractmethod
  56. def __getitem__(self, key):
  57. assert False
  58. class ModeKeyedModel(collections.OrderedDict, Submodel):
  59. def Add(self, name, value_obj, context):
  60. if name not in self:
  61. self[name] = {}
  62. if isinstance(value_obj, dict):
  63. for mode in value_obj:
  64. value = self._CreateValue(value_obj[mode])
  65. if mode == 'default':
  66. mode = Modes.DEFAULT
  67. assert mode in Modes.ALL, f"Invalid mode '{mode}' used in' \
  68. 'definition for '{name}'"
  69. assert mode not in self[
  70. name], f"{mode} mode for '{name}' defined multiple times"
  71. self[name][mode] = value
  72. else:
  73. self[name][Modes.DEFAULT] = self._CreateValue(value_obj)
  74. return [StyleVariable(self.variable_type, name, value_obj, context)]
  75. # Returns the value that |name| will have in |mode|. Resolves to the default
  76. # mode's value if the a value for |mode| isn't specified. Always returns a
  77. # value.
  78. def Resolve(self, name, mode):
  79. if mode in self[name]:
  80. return self[name][mode]
  81. return self[name][Modes.DEFAULT]
  82. def Flatten(self, resolve_missing=False):
  83. '''Builds a name to variable dictionary for each mode.
  84. If |resolve_missing| is true, colors that aren't specified in |mode|
  85. will be resolved to their default mode value.'''
  86. flattened = {}
  87. for mode in Modes.ALL:
  88. variables = collections.OrderedDict()
  89. for name, mode_values in self.items():
  90. if resolve_missing:
  91. variables[name] = self.Resolve(name, mode)
  92. else:
  93. if mode in mode_values:
  94. variables[name] = mode_values[mode]
  95. flattened[mode] = variables
  96. return flattened
  97. class OpacityModel(ModeKeyedModel):
  98. '''A dictionary of opacity names to their values in each mode.
  99. e.g OpacityModel['disabled_opacity'][Modes.LIGHT] = Opacity(...)
  100. '''
  101. def __init__(self):
  102. self.variable_type = VariableType.OPACITY
  103. # Returns a float from 0-1 representing the concrete value of |opacity|.
  104. def ResolveOpacity(self, opacity, mode):
  105. if opacity.a != -1:
  106. return opacity
  107. return self.ResolveOpacity(self.Resolve(opacity.var, mode), mode)
  108. def _CreateValue(self, value):
  109. return Opacity(value)
  110. class ColorModel(ModeKeyedModel):
  111. '''A dictionary of color names to their values in each mode.
  112. e.g ColorModel['blue'][Modes.LIGHT] = Color(...)
  113. '''
  114. def __init__(self, opacity_model):
  115. self.opacity_model = opacity_model
  116. self.variable_type = VariableType.COLOR
  117. def Add(self, name, value_obj, context):
  118. added = []
  119. # If a color has generate_per_mode set, a separate variable will be
  120. # created for each mode, suffixed by mode name.
  121. # (e.g my_color_light, my_color_debug)
  122. generate_per_mode = False
  123. # If a color has generate_inverted set, a |color_name|_inverted will be
  124. # generated which uses the dark color for light mode and vice versa.
  125. generate_inverted = False
  126. if isinstance(value_obj, dict):
  127. generate_per_mode = value_obj.pop('generate_per_mode', None)
  128. generate_inverted = value_obj.pop('generate_inverted', None)
  129. elif self._CreateValue(value_obj).blended_colors:
  130. # A blended color could evaluate to different colors in different
  131. # modes, so add it to all the modes.
  132. value_obj = {mode: value_obj for mode in Modes.ALL}
  133. generated_context = dict(context)
  134. generated_context['generated'] = True
  135. if generate_per_mode or generate_inverted:
  136. for mode, value in value_obj.items():
  137. per_mode_name = name + '_' + mode
  138. added += ModeKeyedModel.Add(self, per_mode_name, value,
  139. generated_context)
  140. value_obj[mode] = '$' + per_mode_name
  141. if generate_inverted:
  142. if Modes.LIGHT not in value_obj or Modes.DARK not in value_obj:
  143. raise ValueError(
  144. 'generate_inverted requires both dark and light modes to be'
  145. ' set')
  146. added += ModeKeyedModel.Add(
  147. self, name + '_inverted', {
  148. Modes.LIGHT: '$' + name + '_dark',
  149. Modes.DARK: '$' + name + '_light'
  150. }, generated_context)
  151. added += ModeKeyedModel.Add(self, name, value_obj, context)
  152. return added
  153. # Returns a Color that is the final RGBA value for |name| in |mode|.
  154. def ResolveToRGBA(self, name, mode):
  155. return self._ResolveColorToRGBA(self.Resolve(name, mode), mode)
  156. # Returns a Color that is the hexadecimal string for |name| in |mode|.
  157. def ResolveToHexString(self, name, mode):
  158. color = self._ResolveColorToRGBA(self.Resolve(name, mode), mode)
  159. opacity = int(float(repr(color.opacity)) * 255)
  160. return '#{:02x}{:02x}{:02x}{:02x}'.format(color.r, color.g, color.b,
  161. opacity)
  162. # Returns a Color that is the final RGBA value for |color| in |mode|.
  163. def _ResolveColorToRGBA(self, color, mode):
  164. if color.var:
  165. return self.ResolveToRGBA(color.var, mode)
  166. if len(color.blended_colors) == 2:
  167. return self._BlendColors(color.blended_colors[0],
  168. color.blended_colors[1], mode)
  169. result = Color()
  170. assert color.opacity
  171. result.opacity = self.opacity_model.ResolveOpacity(color.opacity, mode)
  172. rgb = color
  173. if color.rgb_var:
  174. rgb = self.ResolveToRGBA(color.RGBVarToVar(), mode)
  175. (result.r, result.g, result.b) = (rgb.r, rgb.g, rgb.b)
  176. return result
  177. def _ResolveBlendedColors(self):
  178. # Calculate the final RGBA for all blended colors because the
  179. # generator's subclasses can't blend yet.
  180. temp_model = {}
  181. for name, value in self.items():
  182. for mode, color in value.items():
  183. if color.blended_colors:
  184. assert len(color.blended_colors) == 2
  185. if name not in temp_model:
  186. temp_model[name] = {}
  187. temp_model[name][mode] = self.ResolveToRGBA(name, mode)
  188. for name, value in temp_model.items():
  189. for mode, color in value.items():
  190. self[name][mode] = temp_model[name][mode]
  191. # Returns a Color that is the final RGBA value for |color_a| over |color_b|
  192. # in |mode|.
  193. def _BlendColors(self, color_a, color_b, mode):
  194. # TODO(b/206887565): Check for circular references.
  195. color_a_res = self._ResolveColorToRGBA(color_a, mode)
  196. (alpha_a, r_a, g_a, b_a) = (color_a_res.opacity.a, color_a_res.r,
  197. color_a_res.g, color_a_res.b)
  198. color_b_res = self._ResolveColorToRGBA(color_b, mode)
  199. (alpha_b, r_b, g_b, b_b) = (color_b_res.opacity.a, color_b_res.r,
  200. color_b_res.g, color_b_res.b)
  201. # Blend using the formula for "A over B" from
  202. # https://wikipedia.org/wiki/Alpha_compositing.
  203. alpha_out = alpha_a + (alpha_b * (1 - alpha_a))
  204. r_out = round(
  205. (r_a * alpha_a + r_b * alpha_b * (1 - alpha_a)) / alpha_out)
  206. g_out = round(
  207. (g_a * alpha_a + g_b * alpha_b * (1 - alpha_a)) / alpha_out)
  208. b_out = round(
  209. (b_a * alpha_a + b_b * alpha_b * (1 - alpha_a)) / alpha_out)
  210. result = Color()
  211. (result.r, result.g, result.b) = (r_out, g_out, b_out)
  212. result.opacity = Opacity(alpha_out)
  213. return result
  214. def _CreateValue(self, value):
  215. return Color(value)
  216. class SimpleModel(collections.OrderedDict, Submodel):
  217. def __init__(self, variable_type, check_func=None):
  218. self.variable_type = variable_type
  219. self.check_func = check_func
  220. def Add(self, name, value_obj, context):
  221. if self.check_func:
  222. self.check_func(name, value_obj, context)
  223. self[name] = value_obj
  224. return [StyleVariable(self.variable_type, name, value_obj, context)]
  225. class Model(object):
  226. def __init__(self):
  227. # A map of all variables to their |StyleVariable| object.
  228. self.variable_map = dict()
  229. # A map of |VariableType| to its underlying model.
  230. self.submodels = dict()
  231. self.opacities = OpacityModel()
  232. self.submodels[VariableType.OPACITY] = self.opacities
  233. self.colors = ColorModel(self.opacities)
  234. self.submodels[VariableType.COLOR] = self.colors
  235. self.untyped_css = SimpleModel(VariableType.UNTYPED_CSS)
  236. self.submodels[VariableType.UNTYPED_CSS] = self.untyped_css
  237. def CheckTypeFace(name, value_obj, context):
  238. assert value_obj['font_family']
  239. assert value_obj['font_size']
  240. assert value_obj['font_weight']
  241. assert value_obj['line_height']
  242. self.typefaces = SimpleModel(VariableType.TYPEFACE, CheckTypeFace)
  243. self.submodels[VariableType.TYPEFACE] = self.typefaces
  244. def CheckFontFamily(name, value_obj, context):
  245. assert name.startswith('font_family_')
  246. self.font_families = SimpleModel(VariableType.FONT_FAMILY,
  247. CheckFontFamily)
  248. self.submodels[VariableType.FONT_FAMILY] = self.font_families
  249. def Add(self, variable_type, name, value_obj, context):
  250. '''Adds a new variable to the submodel for |variable_type|.
  251. '''
  252. try:
  253. full_name = self.FullTokenName(name, context)
  254. added = self.submodels[variable_type].Add(full_name, value_obj,
  255. context)
  256. except ValueError as err:
  257. raise ValueError(
  258. f'Error parsing {variable_type} "{full_name}": {value_obj}'
  259. ) from err
  260. for var in added:
  261. if var.name in self.variable_map:
  262. raise ValueError('Variable name "%s" is reused' % name)
  263. self.variable_map[var.name] = var
  264. def FullTokenName(self, name, context):
  265. namespace = context['token_namespace']
  266. if namespace:
  267. return f'{namespace}.{name}'
  268. return name
  269. def PostProcess(self, resolve_blended_colors=True):
  270. '''Called after all variables have been added to perform operations that
  271. require a complete worldview.
  272. '''
  273. if resolve_blended_colors:
  274. # Resolve blended colors after all the files are added because some
  275. # color dependencies are between different files.
  276. self.colors._ResolveBlendedColors()
  277. self.Validate()
  278. def Validate(self):
  279. colors = self.colors
  280. color_names = set(colors.keys())
  281. opacities = self.opacities
  282. opacity_names = set(opacities.keys())
  283. def CheckColorReference(name, referrer):
  284. if name == referrer:
  285. raise ValueError("{0} refers to itself".format(name))
  286. if name not in color_names:
  287. raise ValueError("Cannot find color %s referenced by %s" %
  288. (name, referrer))
  289. def CheckOpacityReference(name, referrer):
  290. if name == referrer:
  291. raise ValueError("{0} refers to itself".format(name))
  292. if name not in opacity_names:
  293. raise ValueError("Cannot find opacity %s referenced by %s" %
  294. (name, referrer))
  295. def CheckColor(color, name):
  296. if color.var:
  297. CheckColorReference(color.var, name)
  298. if color.rgb_var:
  299. CheckColorReference(color.RGBVarToVar(), name)
  300. if color.opacity and color.opacity.var:
  301. CheckOpacityReference(color.opacity.var, name)
  302. if color.blended_colors:
  303. assert len(color.blended_colors) == 2
  304. CheckColor(color.blended_colors[0], name)
  305. CheckColor(color.blended_colors[1], name)
  306. RESERVED_SUFFIXES = ['_' + s for s in Modes.ALL + ['rgb', 'inverted']]
  307. # Check all colors in all modes refer to colors that exist in the
  308. # default mode.
  309. for name, mode_values in colors.items():
  310. for suffix in RESERVED_SUFFIXES:
  311. if not self.variable_map[name].context.get(
  312. 'generated') and name.endswith(suffix):
  313. raise ValueError(
  314. 'Variable name "%s" uses a reserved suffix: %s' %
  315. (name, suffix))
  316. if Modes.DEFAULT not in mode_values:
  317. raise ValueError("Color %s not defined for default mode" %
  318. name)
  319. for mode, color in mode_values.items():
  320. CheckColor(color, name)
  321. for name, mode_values in opacities.items():
  322. for mode, opacity in mode_values.items():
  323. if opacity.var:
  324. CheckOpacityReference(opacity.var, name)
  325. # TODO(b/206887565): Check for circular references.