1122 lines
49 KiB
Python
1122 lines
49 KiB
Python
'''
|
|
Copyright (C) 2023 CG Cookie
|
|
http://cgcookie.com
|
|
hello@cgcookie.com
|
|
|
|
Created by Jonathan Denning, Jonathan Williamson
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
import os
|
|
import re
|
|
import copy
|
|
import math
|
|
import time
|
|
import struct
|
|
import random
|
|
import traceback
|
|
import functools
|
|
import urllib.request
|
|
from itertools import chain, zip_longest
|
|
|
|
import bpy
|
|
from bpy.types import BoolProperty
|
|
from mathutils import Matrix
|
|
|
|
from .parse import Parse_CharStream, Parse_Lexer
|
|
from .ui_core_utilities import (
|
|
convert_token_to_string, convert_token_to_cursor,
|
|
convert_token_to_color, convert_token_to_numberunit,
|
|
get_converter_to_string,
|
|
skip_token,
|
|
)
|
|
|
|
from .blender import get_path_from_addon_common
|
|
from .decorators import blender_version_wrapper, debug_test_call, add_cache
|
|
from .maths import Point2D, Vec2D, clamp, mid, Color, NumberUnit
|
|
from .profiler import profiler
|
|
from .utils import iter_head, UniqueCounter, join
|
|
|
|
|
|
'''
|
|
|
|
reference: https://www.w3.org/TR/selectors/
|
|
|
|
note: remove the element selector, and work directly with the DOM so that
|
|
relational combinators and pseudoclasses are doable
|
|
examples: h1 + p, p ~ p, h1:has(a)
|
|
|
|
'''
|
|
|
|
|
|
|
|
'''
|
|
|
|
CookieCutter UI Styling
|
|
|
|
This styling file is formatted _very_ similarly to CSS, but below is a list of a few important notes/differences:
|
|
|
|
- rules have specificity and are applied top-down (later conflicting rules override earlier rules)
|
|
- specificity: https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity
|
|
- there is no `!important` keyword, so if you want to override a setting, place it lower in the styling input.
|
|
|
|
- several units are supported: `px`, `%`, `pt` (handled same as `px`)
|
|
- `vw` and `vh` are recognized, but they are not implemented yet
|
|
|
|
- colors can come in various formats
|
|
- `rgb(<r>,<g>,<b>)` or `rgba(<r>,<g>,<b>,<a>)`, where r,g,b values in 0--255; a in 0.0--1.0
|
|
- `hsl(<h>,<s>%,<l>%)` or `hsla(<h>,<s>%,<l>%,<a>)`, where h in 0--360; s,l in 0--100 (%); a in 0.0--1.0
|
|
- `#RRGGBB`, where r,g,b in 00--FF
|
|
- or by colorname
|
|
|
|
- spaces,tabs,newlines are completely ignored except to separate tokens
|
|
|
|
- selectors
|
|
- all element types must be explicitly specified, except at beginning or when following a `>`; use `*` to match any type
|
|
- ex: `elem1 .class` is the same as `elem1.class` and `elem1 . class`, but never `elem1 *.class`
|
|
- only `>` and ` ` combinators are implemented
|
|
|
|
- background has only solid color or image
|
|
|
|
- border has no style (such as dotted or dashed) and has uniform width (no left, right, top, bottom widths)
|
|
- `border: <border-width> <border-color>`
|
|
|
|
- setting `width` or `height` will set both of the corresponding `min-*` and `max-*` properties
|
|
|
|
- `min-*` and `max-*` are used as suggestions to the UI system; they will not be strictly followed
|
|
|
|
- flex rules are recognized, but are not implemented yet
|
|
|
|
|
|
Things to think about:
|
|
|
|
- `:scrolling` pseudoclass, for when we're scrolling through content
|
|
- `:focus` pseudoclass, for when textbox has focus, or changing a number input
|
|
- add drop shadow (draws in the margins?) and outline (for focus viz)
|
|
- allow for float boxes?
|
|
- ability to be drag-able?
|
|
|
|
|
|
'''
|
|
|
|
def convert_token_to_var(m):
|
|
#print(f'token->var: {m.group("var")} {m.group("default")}')
|
|
return (m.group('var'), m.group('default'))
|
|
|
|
token_attribute = r'\[(?P<key>[-a-zA-Z_]+)((?P<op>=)"(?P<val>[^"]*)")?\]'
|
|
|
|
token_rules = [
|
|
('ignore', skip_token, [
|
|
r'[ \t\r\n]', # ignoring any tab, space, newline
|
|
r'/[*][\s\S]*?[*]/', # multi-line comments
|
|
]),
|
|
('special', convert_token_to_string, [
|
|
r'[-.*>{},();#~+]|[:]+',
|
|
]),
|
|
('combinator', convert_token_to_string, [
|
|
r'[>~+]',
|
|
]),
|
|
('attribute', convert_token_to_string, [
|
|
token_attribute,
|
|
]),
|
|
('key', convert_token_to_string, [
|
|
r'color',
|
|
r'display',
|
|
r'background(-(color|image))?',
|
|
r'margin(-(left|right|top|bottom))?',
|
|
r'padding(-(left|right|top|bottom))?',
|
|
r'border(-(width|radius))?',
|
|
r'border(-(left|right|top|bottom))?-color',
|
|
r'((min|max)-)?width',
|
|
r'((min|max)-)?height',
|
|
r'left|top|right|bottom',
|
|
r'cursor',
|
|
r'overflow(-x|-y)?',
|
|
r'position',
|
|
r'flex(-(direction|wrap|grow|shrink|basis))?',
|
|
r'justify-content|align-content|align-items',
|
|
r'font(-(style|weight|size|family))?',
|
|
r'white-space',
|
|
r'content',
|
|
r'object-fit',
|
|
r'text-shadow',
|
|
r'text-align(-last)?',
|
|
r'z-index',
|
|
]),
|
|
('value', convert_token_to_string, [
|
|
r'auto',
|
|
r'inline|block|none|flexbox|table(-row|-cell)?', # display
|
|
r'visible|hidden|scroll|auto', # overflow, overflow-x, overflow-y
|
|
r'static|absolute|relative|fixed|sticky', # position
|
|
r'column|row', # flex-direction
|
|
r'nowrap|wrap', # flex-wrap
|
|
r'flex-start|flex-end|center|stretch', # justify-content, align-content, align-items
|
|
r'normal|italic', # font-style
|
|
r'normal|bold', # font-weight
|
|
r'serif|sans-serif|monospace', # font-family
|
|
r'normal|nowrap|pre|pre-wrap|pre-line', # white-space
|
|
r'normal|none', # content (more in url and string below)
|
|
r'fill|contain|cover|none|scale-down', # object-fit
|
|
r'none', # text-shadow
|
|
r'left|center|justify|right', # text-align, text-align-last
|
|
]),
|
|
('url', get_converter_to_string('url'), [
|
|
r'url\([\'"]?(?P<url>[^)]*?)[\'"]?\)',
|
|
]),
|
|
('string', get_converter_to_string('string'), [
|
|
r'"(?P<string>[^"]*?)"',
|
|
]),
|
|
('color', convert_token_to_color, [
|
|
r'rgb\( *(?P<red>\d+) *, *(?P<green>\d+) *, *(?P<blue>\d+) *\)',
|
|
r'rgba\( *(?P<red>\d+) *, *(?P<green>\d+) *, *(?P<blue>\d+) *, *(?P<alpha>\d+(\.\d+)?) *\)',
|
|
r'hsl\( *(?P<hue>\d+) *, *(?P<saturation>\d+)% *, *(?P<lightness>\d+)% *\)',
|
|
r'hsla\( *(?P<hue>\d+([.]\d*)?) *, *(?P<saturation>\d+([.]\d*)?)% *, *(?P<lightness>\d+([.]\d*)?)% *, *(?P<alpha>\d+([.]\d+)?) *\)',
|
|
r'#[a-fA-F0-9]{6}',
|
|
|
|
r'transparent',
|
|
|
|
# https://www.quackit.com/css/css_color_codes.cfm
|
|
r'indianred|lightcoral|salmon|darksalmon|lightsalmon|crimson|red|firebrick|darkred', # reds
|
|
r'pink|lightpink|hotpink|deeppink|mediumvioletred|palevioletred', # pinks
|
|
r'coral|tomato|orangered|darkorange|orange', # oranges
|
|
r'gold|yellow|lightyellow|lemonchiffon|lightgoldenrodyellow|papayawhip|moccasin', # yellows
|
|
r'peachpuff|palegoldenrod|khaki|darkkhaki', # __^
|
|
r'lavender|thistle|plum|violet|orchid|fuchsia|magenta|mediumorchid|mediumpurple', # purples
|
|
r'blueviolet|darkviolet|darkorchid|darkmagenta|purple|rebeccapurple|indigo', # ^
|
|
r'mediumslateblue|slateblue|darkslateblue', # __/
|
|
r'greenyellow|chartreuse|lawngreen|lime|limegreen|palegreen|lightgreen', # greens
|
|
r'mediumspringgreen|springgreen|mediumseagreen|seagreen|forestgreen|green', # ^
|
|
r'darkgreen|yellowgreen|olivedrab|olive|darkolivegreen|mediumaquamarine', # |
|
|
r'darkseagreen|lightseagreen|darkcyan|teal', # __/
|
|
r'aqua|cyan|lightcyan|paleturquoise|aquamarine|turquoise|mediumturquoise', # blues
|
|
r'darkturquoise|cadetblue|steelblue|lightsteelblue|powderblue|lightblue|skyblue', # ^
|
|
r'lightskyblue|deepskyblue|dodgerblue|cornflowerblue|royalblue|blue|mediumblue', # |
|
|
r'darkblue|navy|midnightblue', # __/
|
|
r'cornsilk|blanchedalmond|bisque|navajowhite|wheat|burlywood|tan|rosybrown', # browns
|
|
r'sandybrown|goldenrod|darkgoldenrod|peru|chocolate|saddlebrown|sienna|brown|maroon', # __^
|
|
r'white|snow|honeydew|mintcream|azure|aliceblue|ghostwhite|whitesmoke|seashell', # whites
|
|
r'beige|oldlace|floralwhite|ivory|antiquewhite|linen|lavenderblush|mistyrose', # __^
|
|
r'gainsboro|lightgray|lightgrey|silver|darkgray|darkgrey|gray|grey|dimgray|dimgrey', # grays
|
|
r'lightslategray|lightslategrey|slategray|slategrey|darkslategray|darkslategrey|black', # __^
|
|
]),
|
|
('pseudoclass', convert_token_to_string, [
|
|
r'hover', # applied when mouse is hovering over
|
|
r'active', # applied between mousedown and mouseup
|
|
r'focus', # applied if element has focus
|
|
r'disabled', # applied if element is disabled
|
|
r'checked', # applied if element is checked (radio or checkbox)
|
|
r'root', # applies to document
|
|
# r'link', # unvisited link
|
|
# r'visited', # visited link
|
|
]),
|
|
('pseudoelement', convert_token_to_string, [
|
|
r'before', # inserts content before element
|
|
r'after', # inserts content after element
|
|
r'marker', # marker for <summary>, <li>, etc.
|
|
# r'first-letter',
|
|
# r'first-line',
|
|
# r'selection',
|
|
r'text', # innerText
|
|
]),
|
|
('num', convert_token_to_numberunit, [
|
|
r'(?P<num>-?((\d*[.]\d+)|\d+))(?P<unit>px|vw|vh|pt|%|)',
|
|
]),
|
|
('id', convert_token_to_string, [
|
|
r'[a-zA-Z_][a-zA-Z_\-0-9]*',
|
|
]),
|
|
('cursor', convert_token_to_cursor, [
|
|
r'default|auto|initial',
|
|
r'none|wait|grab|crosshair|pointer',
|
|
r'text',
|
|
r'e-resize|w-resize|ew-resize',
|
|
r'n-resize|s-resize|ns-resize',
|
|
r'all-scroll',
|
|
]),
|
|
('variable', convert_token_to_string, [
|
|
r'--[a-zA-Z-]+',
|
|
]),
|
|
('var', convert_token_to_var, [
|
|
r'var\( *(?P<var>--[a-zA-Z-]+) *(,(?P<default>[^\)\]};!]))?\)',
|
|
]),
|
|
]
|
|
|
|
|
|
default_fonts = {
|
|
# style weight size family
|
|
'default': ('normal', 'normal', '12', 'sans-serif'),
|
|
'caption': ('normal', 'normal', '12', 'sans-serif'),
|
|
'icon': ('normal', 'normal', '12', 'sans-serif'),
|
|
'menu': ('normal', 'normal', '12', 'sans-serif'),
|
|
'message-box': ('normal', 'normal', '12', 'sans-serif'),
|
|
'small-caption': ('normal', 'normal', '12', 'sans-serif'),
|
|
'status-bar': ('normal', 'normal', '12', 'sans-serif'),
|
|
}
|
|
|
|
default_styling = {
|
|
'background': convert_token_to_color('transparent'),
|
|
'display': 'inline',
|
|
}
|
|
|
|
|
|
# (?P<type>[^\n .#:[=\]]+)(?:(?:\.(?P<class>[^\n .#:[=\]]+))|(?:::(?P<pseudoelement>[^\n .#:[=\]]+))|(?::(?P<pseudoclass>[^\n .#:[=\]]+))|(?:#(?P<id>[^\n .#:[=\]]+))|(?:\[(?P<akey>[^\n .#:[=\]]+)(?:=\"(?P<aval>[^\"]+)\")?\]))*
|
|
# (?:(?P<type>[ .#:[]+)(?P<name>[^\n .#:[=\]]+)(?:=\"(?P<val>[^\"]+)\")?]?)
|
|
# (?:(?P<type>[.#:[]+)?(?P<name>[^\n .#:[=\]]+)(?:=\"(?P<val>[^\"]+)\")?]?)
|
|
|
|
selector_splitter = re.compile(r"(?:(?P<type>[.#:\[]+)?(?P<name>[^\n .#:\[=\]]+)(?:=\"(?P<val>[^\"]+)\")?\]?)")
|
|
|
|
|
|
# XXX: this is a hack!
|
|
css_variables = {}
|
|
|
|
|
|
class UI_Style_Declaration:
|
|
'''
|
|
CSS Declarations are of the form:
|
|
|
|
property: value;
|
|
property: val0 val1 ...;
|
|
|
|
Value is either a single token or a tuple if the token immediately following the first value is not ';'.
|
|
|
|
ex: border: 1 yellow;
|
|
|
|
'''
|
|
def __init__(self, prop="", val=""):
|
|
self.property = prop
|
|
self.value = val
|
|
def __str__(self):
|
|
return f'<UI_Style_Declaration "{self.property}={self.value}">'
|
|
def __repr__(self): return self.__str__()
|
|
|
|
|
|
class UI_Style_RuleSet:
|
|
'''
|
|
CSS RuleSets are in the form shown below, where there is a single list of selectors followed by block set of styling rules
|
|
Note: each `property: value;` is a UI_Style_Declaration
|
|
|
|
selector, selector {
|
|
property0: value;
|
|
property1: val0 val1 val2;
|
|
...
|
|
}
|
|
|
|
'''
|
|
|
|
uid_generator = UniqueCounter()
|
|
|
|
@staticmethod
|
|
def process_rules(lexer):
|
|
l = []
|
|
while lexer.peek_v() not in {';', '}', 'eof'}:
|
|
if 'var' in lexer.peek_t():
|
|
var, default = lexer.next_v()
|
|
if not (default or var in css_variables): continue
|
|
if var in css_variables:
|
|
l.append(css_variables[var])
|
|
else:
|
|
# evaluating default requires recursive call, because
|
|
# the default is still a string (has not been processed, yet)
|
|
defcharstream = Parse_CharStream(default)
|
|
deflexer = Parse_Lexer(defcharstream, token_rules)
|
|
l.extend(UI_Style_RuleSet.process_rules(deflexer))
|
|
else:
|
|
l.append(lexer.next_v())
|
|
return l
|
|
|
|
@staticmethod
|
|
def process_decl_var(lexer):
|
|
prop, varname = None, None
|
|
if 'variable' in lexer.peek_t():
|
|
varname = lexer.match_t_v('variable')
|
|
else:
|
|
prop = lexer.match_t_v('key')
|
|
lexer.match_v_v(':')
|
|
l = UI_Style_RuleSet.process_rules(lexer)
|
|
if lexer.peek_v() == ';': lexer.match_v_v(';')
|
|
if len(l) == 0: return None
|
|
val = l[0] if len(l) == 1 else tuple(l)
|
|
if varname:
|
|
#print(f'variable: {prop} {val}')
|
|
css_variables[varname] = val
|
|
return None
|
|
#print(f'decl: {prop} {val}')
|
|
return UI_Style_Declaration(prop, val)
|
|
|
|
@staticmethod
|
|
def from_lexer(lexer, inline=False, defaults=False):
|
|
rs = UI_Style_RuleSet(inline=inline, defaults=defaults)
|
|
|
|
def match_identifier():
|
|
if lexer.peek_v() in {'.','#',':','::'}:
|
|
e = '*'
|
|
elif lexer.peek_v() == '*':
|
|
e = lexer.match_v_v('*')
|
|
else:
|
|
e = lexer.match_t_v('id')
|
|
while True:
|
|
if lexer.peek_v() in {'.','#'}:
|
|
e += lexer.match_v_v({'.','#'})
|
|
e += lexer.match_t_v('id')
|
|
elif lexer.peek_v() == ':':
|
|
e += lexer.match_v_v(':')
|
|
e += lexer.match_t_v('pseudoclass')
|
|
elif lexer.peek_v() == '::':
|
|
e += lexer.match_v_v('::')
|
|
e += lexer.match_t_v('pseudoelement')
|
|
elif 'attribute' in lexer.peek_t():
|
|
e += lexer.match_t_v('attribute')
|
|
else:
|
|
break
|
|
return e
|
|
|
|
# get selector
|
|
rs.selectors = [[]]
|
|
while lexer.peek_v() != '{':
|
|
if lexer.peek_v() == '*' or 'id' in lexer.peek_t() or lexer.peek_v() in {'.','#',':','::'}:
|
|
rs.selectors[-1].append(match_identifier())
|
|
elif 'combinator' in lexer.peek_t():
|
|
# TODO: handle + and ~ combinators?
|
|
combinator = lexer.match_t_v('combinator')
|
|
rs.selectors[-1].append(combinator)
|
|
rs.selectors[-1].append(match_identifier())
|
|
elif lexer.peek_v() == ',':
|
|
lexer.match_v_v(',')
|
|
rs.selectors.append([])
|
|
else:
|
|
assert False, 'expected selector or "{" but saw "%s" on line %d' % (lexer.peek_v(),lexer.current_line())
|
|
|
|
# get declarations list
|
|
rs.decllist = []
|
|
lexer.match_v_v('{')
|
|
while lexer.peek_v() != '}':
|
|
while lexer.peek_v() == ';': lexer.match_v_v(';')
|
|
if lexer.peek_v() == '}': break
|
|
decl_var = UI_Style_RuleSet.process_decl_var(lexer)
|
|
if decl_var: rs.decllist.append(decl_var)
|
|
lexer.match_v_v('}')
|
|
|
|
return rs
|
|
|
|
@staticmethod
|
|
def from_decllist(decllist, selector, inline=False, defaults=False): # tagname, pseudoclass=None):
|
|
# t = type(pseudoclass)
|
|
# if t is list or t is set: pseudoclass = ':'.join(pseudoclass)
|
|
rs = UI_Style_RuleSet(inline=inline, defaults=defaults)
|
|
# rs.selectors = [[tagname + (':%s'%pseudoclass if pseudoclass else '')]]
|
|
rs.selectors = [selector]
|
|
for k,v in decllist.items():
|
|
rs.decllist.append(UI_Style_Declaration(k,v))
|
|
return rs
|
|
|
|
def __init__(self, inline=False, defaults=False):
|
|
self._uid = UI_Style_RuleSet.uid_generator.next()
|
|
self.selectors = [] # can have multiple selectors for same decllist
|
|
self.decllist = [] # list of style declarations that apply
|
|
self._match_cache = {}
|
|
self._inline = inline
|
|
self._defaults = defaults
|
|
|
|
def __str__(self):
|
|
s = ', '.join(' '.join(selector) for selector in self.selectors)
|
|
if not self.decllist: return '<UI_Style_RuleSet "%s">' % (s,)
|
|
return '<UI_Style_RuleSet "%s"\n%s\n>' % (s,'\n'.join(' '+l for d in self.decllist for l in str(d).splitlines()))
|
|
def __repr__(self): return self.__str__()
|
|
|
|
@staticmethod
|
|
@add_cache('_cache', {})
|
|
def _split_selector(sel):
|
|
# (?:(?P<type>[.#:[]+)?(?P<name>[^\n .#:[=\]]+)(?:=\"(?P<val>[^\"]+)\")?]?)
|
|
cache = UI_Style_RuleSet._split_selector._cache
|
|
osel = str(sel)
|
|
if osel not in cache:
|
|
p = {'type':'*', 'class':set(), 'id':'', 'pseudoelement':'', 'pseudoclass':set(), 'attribs':set(), 'attribvals':{}}
|
|
|
|
for part in selector_splitter.finditer(sel):
|
|
t,n,v = part.group('type'),part.group('name'),part.group('val')
|
|
if t is None: p['type'] = n
|
|
elif t == '.': p['class'].add(n)
|
|
elif t == '#': p['id'] = n
|
|
elif t == ':': p['pseudoclass'].add(n)
|
|
elif t == '::': p['pseudoelement'] = n
|
|
elif t == '[':
|
|
if v is None: p['attribs'].add(n)
|
|
else: p['attribvals'][n] = v
|
|
else: assert False, 'Unhandled selector type "%s" (%s, %s) in "%s"' % (str(t), str(n), str(v), str(sel))
|
|
|
|
# p['names'] is a set of all identifying elements in selector
|
|
# useful for quickly and conservatively deciding that selector does NOT match
|
|
p['names'] = p['class'] | p['pseudoclass'] | p['attribs'] | p['attribvals'].keys() # | p['attribvals'].values()
|
|
if p['type'] not in {'*','>','+','~'}: p['names'].add(p['type'])
|
|
if p['id']: p['names'].add(p['id'])
|
|
if p['pseudoelement']: p['names'].add(p['pseudoelement'])
|
|
|
|
cache[osel] = p
|
|
return dict(cache[osel]) # NOTE: _not_ a deep copy!
|
|
|
|
@staticmethod
|
|
@add_cache('_cache', {})
|
|
def _join_selector_parts(p):
|
|
cache = UI_Style_RuleSet._join_selector_parts._cache
|
|
op = str(p)
|
|
if op not in cache:
|
|
sel = p['type'] or '*'
|
|
if p['id']: sel += f'#{p["id"]}'
|
|
if p['class']: sel += join('.', p['class'], preSep='.')
|
|
if p['pseudoclass']: sel += join(':', p['pseudoclass'], preSep=':')
|
|
if p['pseudoelement']: sel += f'::{p["pseudoelement"]}'
|
|
if p['attribs']: sel += join('][', p['attribs'], preSep='[', postSep=']')
|
|
if p['attribvals']: sel += join('][', p['attribvals'].items(), preSep='[', postSep=']', toStr=lambda kv:f'{kv[0]}="{kv[1]}"')
|
|
cache[op] = sel
|
|
return cache[op]
|
|
|
|
@staticmethod
|
|
def _match_selector_approx(parts_elem, parts_style, check_end=False):
|
|
if check_end:
|
|
if parts_style[-1]['type'] not in {'*','>','+','~'} and parts_elem[-1]['type'] != parts_style[-1]['type']:
|
|
return False
|
|
if parts_style[-1]['id'] and parts_elem[-1]['id'] != parts_style[-1]['id']:
|
|
return False
|
|
names_elem = {n for p in parts_elem for n in p['names']}
|
|
names_style = {n for p in parts_style for n in p['names']} - {'*', '>'}
|
|
if not all(n in names_elem for n in names_style): return False
|
|
return True
|
|
|
|
@staticmethod
|
|
def _match_selector_parts(ap, bp):
|
|
# NOTE: ap['type'] == '' with UI_Elements that contain the innertext
|
|
# TODO: consider giving this a special type, ex: **text**
|
|
return all([
|
|
((bp['type'] == '*' and ap['type'] != '') or ap['type'] == bp['type']),
|
|
(bp['id'] == '' or ap['id'] == bp['id']),
|
|
all(c in ap['class'] for c in bp['class']),
|
|
ap['pseudoelement'] == bp['pseudoelement'],
|
|
all(c in ap['pseudoclass'] for c in bp['pseudoclass']),
|
|
all(key in ap['attribs'] for key in bp['attribs']),
|
|
all(key in ap['attribvals'] and ap['attribvals'][key] == val for (key,val) in bp['attribvals'].items()),
|
|
])
|
|
|
|
@staticmethod
|
|
# @profiler.function
|
|
def _match_selector(sel_elem, pts_elem, sel_style, pts_style, cont=False):
|
|
'''
|
|
sel_elem/pts_elem and sel_style/pts_style are corresponding lists for element and style
|
|
sel_*: selector pts_*: selector broken into parts
|
|
cont:
|
|
False: end of sel_elem/pts_elem and sel_style/pts_style must be exactly the same
|
|
True: can allow skipping end of sel_style/pts_style
|
|
'''
|
|
# ex:
|
|
# sel_elem = ['body:hover', 'button:hover']
|
|
# sel_style = ['button:hover']
|
|
if not sel_style: return True # nothing left to match (potential extra in element)
|
|
if not sel_elem: return False # nothing left to match, but still have extra in style
|
|
msel = UI_Style_RuleSet._match_selector
|
|
mparts = UI_Style_RuleSet._match_selector_parts
|
|
if sel_style[-1] == '>':
|
|
# parent selector in style MUST match (> means child, not descendant)
|
|
return msel(sel_elem, pts_elem, sel_style[:-1], pts_style[:-1])
|
|
elif not UI_Style_RuleSet._match_selector_approx(pts_elem, pts_style, check_end=not cont):
|
|
return False
|
|
elif mparts(pts_elem[-1], pts_style[-1]) and msel(sel_elem[:-1], pts_elem[:-1], sel_style[:-1], pts_style[:-1], True):
|
|
return True
|
|
elif not cont:
|
|
return False
|
|
else:
|
|
return msel(sel_elem[:-1], pts_elem[:-1], sel_style, pts_style, True)
|
|
|
|
@staticmethod
|
|
def match_selector(sel_elem, sel_style, strip=None):
|
|
split = UI_Style_RuleSet._split_selector
|
|
# print('UI_Style_RuleSet', sel_elem, sel_style)
|
|
sel_elem = UI_Styling.strip_selector_parts(sel_elem, strip)
|
|
sel_style = UI_Styling.strip_selector_parts(sel_style, strip)
|
|
parts_elem = [split(p) for p in sel_elem]
|
|
parts_style = [split(p) for p in sel_style]
|
|
return UI_Style_RuleSet._match_selector(sel_elem, parts_elem, sel_style, parts_style)
|
|
|
|
# @profiler.function
|
|
# def match(self, sel_elem, strip=None):
|
|
# # returns true if passed selector matches any selector in self.selectors
|
|
# cache = self._match_cache
|
|
# key = f'{sel_elem} {strip}'
|
|
# if key not in cache:
|
|
# cache[key] = any(UI_Style_RuleSet.match_selector(sel_elem, sel_style, strip=strip) for sel_style in self.selectors)
|
|
# return cache[key]
|
|
|
|
def get_all_matches(self, sel_elem):
|
|
return [sel_style for sel_style in self.selectors if UI_Style_RuleSet.match_selector(sel_elem, sel_style)]
|
|
|
|
@staticmethod
|
|
@add_cache('_cache', {})
|
|
def selector_specificity(selector, ruleset): #, uid, inline=False, defaults=False):
|
|
uid = ruleset._uid
|
|
inline = ruleset._inline
|
|
defaults = ruleset._defaults
|
|
k = f'{uid} {selector} {inline} {defaults}'
|
|
cache = UI_Style_RuleSet.selector_specificity._cache
|
|
if k not in cache:
|
|
split = UI_Style_RuleSet._split_selector
|
|
a = 1 if inline else -1 if defaults else 0 # inline/defaults
|
|
b = 0 # id
|
|
c = 0 # class, pseudoclass, attrib, attribval
|
|
d = 0 # type, pseudoelement
|
|
e = uid # uid (used for ordering)
|
|
parts = [split(sel) for sel in selector]
|
|
for part in parts:
|
|
b += 1 if part['id'] else 0
|
|
c += len(part['class']) + len(part['pseudoclass']) + len(part['attribs']) + len(part['attribvals'])
|
|
if part['pseudoelement']: d += 1
|
|
if part['type'] not in {'', '*', '>', '+', '~'}: d += 1
|
|
cache[k] = (a, b, c, d, e)
|
|
return cache[k]
|
|
|
|
|
|
class UI_Styling:
|
|
'''
|
|
Parses input to a CSSOM-like object
|
|
'''
|
|
uid_generator = UniqueCounter()
|
|
|
|
@staticmethod
|
|
# @profiler.function
|
|
def from_var(var, tagname='*', pseudoclass=None, inline=False, defaults=False):
|
|
if not var: return UI_Styling(inline=inline, defaults=defaults)
|
|
if type(var) is UI_Styling: return var
|
|
sel = tagname + (':%s' % pseudoclass if pseudoclass else '')
|
|
# NOTE: do not convert below into `t = type(var)` and change `if`s below into `elif`s!
|
|
if type(var) is dict: var = ['%s:%s' % (k,v) for (k,v) in var.items()]
|
|
if type(var) is list: var = ';'.join(var)
|
|
if type(var) is str: var = UI_Styling(lines=f'{sel}{{{var};}}', inline=inline, defaults=defaults)
|
|
assert type(var) is UI_Styling
|
|
return var
|
|
|
|
@staticmethod
|
|
# @profiler.function
|
|
def from_file(filename, inline=False, defaults=False):
|
|
lines = open(filename, 'rt').read()
|
|
return UI_Styling(lines=lines, inline=inline, defaults=defaults)
|
|
|
|
def load_from_file(self, filename):
|
|
text = open(filename, 'rt').read()
|
|
self.load_from_text(text)
|
|
|
|
# @profiler.function
|
|
def load_from_text(self, text):
|
|
self.clear_cache()
|
|
self.dirty_optimization()
|
|
self._rules = []
|
|
if not text: return
|
|
charstream = Parse_CharStream(text) # convert input into character stream
|
|
lexer = Parse_Lexer(charstream, token_rules) # tokenize the character stream
|
|
while lexer.peek_t() != 'eof':
|
|
self._rules.append(UI_Style_RuleSet.from_lexer(lexer, self._inline, self._defaults))
|
|
# print('UI_Styling.load_from_text: Loaded %d rules' % len(self._rules))
|
|
|
|
def clear_cache(self):
|
|
# print(f'UI_Styling{self._uid}.clear_cache')
|
|
self._decllist_cache = {}
|
|
UI_Styling.trim_styling._cache = {}
|
|
UI_Styling.strip_selector_parts._cache = {}
|
|
|
|
|
|
def _print_trie_to_node(self, node):
|
|
# find path from node to root
|
|
path = set()
|
|
node_root = node
|
|
while True:
|
|
path.add(node_root['__uid'])
|
|
if node_root['__parent'] is None: break
|
|
node_root = node_root['__parent']
|
|
print(path)
|
|
def p(node_cur, depth):
|
|
for k in node_cur:
|
|
k2 = str(k).replace('"', '\\"')
|
|
spc = " " * depth
|
|
if k == '__rulesets':
|
|
print(f'{spc}"{k2}":["... ({len(node_cur[k])})"],')
|
|
elif k == '__selectors':
|
|
v = str(node_cur[k]).replace('"', '\\"')
|
|
print(f'{spc}"{k2}":"{v}",')
|
|
elif k == '__parent':
|
|
pass
|
|
elif k == '__uid':
|
|
print(f'{spc}"__uid":{node_cur[k]},')
|
|
else:
|
|
node_next = node_cur[k]
|
|
if node_next['__uid'] in path:
|
|
print(f'{spc}"{k2}":{{')
|
|
p(node_next, depth+1)
|
|
print(f'{spc}}},')
|
|
else:
|
|
print(f'{spc}"{k2}":{{ }},')
|
|
print('{')
|
|
p(node_root, 1)
|
|
print('}')
|
|
|
|
def _print_trie(self, full_trie=True):
|
|
def p(node_cur, depth):
|
|
for k in node_cur:
|
|
k2 = str(k).replace('"', '\\"')
|
|
spc = " " * depth
|
|
if k == '__rulesets':
|
|
print(f'{spc}"{k2}":["... ({len(node_cur[k])})"],')
|
|
elif k == '__selectors':
|
|
v = str(node_cur[k]).replace('"', '\\"')
|
|
print(f'{spc}"{k2}":"{v}",')
|
|
elif k == '__parent':
|
|
pass
|
|
elif k == '__uid':
|
|
pass
|
|
else:
|
|
print(f'{spc}"{k2}":{{')
|
|
p(node_cur[k], depth+1)
|
|
print(f'{spc}}},')
|
|
print('{')
|
|
p(self._trie_full if full_trie else self._trie_stripped, 1)
|
|
print('}')
|
|
|
|
def optimize(self):
|
|
'''
|
|
build a trie of selectors for faster matching
|
|
the trie consists of
|
|
selector parts: type (str, t), class (set, .c), id (str, #i), pseudoelement (str, ::pe), pseudoclass (set, :pc), attribs (set, [k]), attribvals (dict, [k=v])
|
|
and >
|
|
'''
|
|
|
|
node_uid_generator = UniqueCounter()
|
|
def new_node(node_parent):
|
|
return {
|
|
'__uid': node_uid_generator.next(), # allowing for hashing in set
|
|
'__parent':node_parent, # parent node (debugging)
|
|
}
|
|
def get_node(cur, key):
|
|
if key not in cur: cur[key] = new_node(cur)
|
|
return cur[key]
|
|
|
|
def build_trie(strip=None):
|
|
# print(f'UI_Styling.optimize! {self._uid}')
|
|
split = UI_Style_RuleSet._split_selector
|
|
trie = new_node(None)
|
|
# insert all items into trie
|
|
for rule in self._rules:
|
|
for selector in rule.selectors:
|
|
specificity = UI_Style_RuleSet.selector_specificity(selector, rule)
|
|
nselector = UI_Styling.strip_selector_parts(selector, strip)
|
|
# print(f'selector specificity: {selector} => {specificity}')
|
|
parts = [split(p) for p in nselector]
|
|
part = {'type':'', 'id':'', 'class': set(), 'pseudoelement':'', 'pseudoclass':set(), 'attribs':set(), 'attribvals':dict()}
|
|
node_cur = trie
|
|
while True:
|
|
if part['pseudoelement']:
|
|
# print(f'build_trie {rule} {part["pseudoelement"]}')
|
|
node_cur = get_node(node_cur, f"::{part['pseudoelement']}")
|
|
part['pseudoelement'] = ''
|
|
elif part['type']:
|
|
# NOTE: type can be '>', but this _should_ get handled in final `else`
|
|
assert part['type'] != '>', f'type can be `>` but not here. check if style has `> >`\nselector: {selector}\nstrip: {strip}\nnselector: {nselector}\npart: {part}\nparts: {parts}\n{self._trie}'
|
|
assert part['type'] != '+', f'type can be `+` but not here. check if style has `> +`\nselector: {selector}\nstrip: {strip}\nnselector: {nselector}\npart: {part}\nparts: {parts}\n{self._trie}'
|
|
assert part['type'] != '~', f'type can be `~` but not here. check if style has `> ~`\nselector: {selector}\nstrip: {strip}\nnselector: {nselector}\npart: {part}\nparts: {parts}\n{self._trie}'
|
|
node_cur = get_node(node_cur, f"{part['type']}")
|
|
part['type'] = ''
|
|
elif part['id']:
|
|
node_cur = get_node(node_cur, f"#{part['id']}")
|
|
part['id'] = ''
|
|
elif part['class']:
|
|
c = part['class'].pop()
|
|
node_cur = get_node(node_cur, f".{c}")
|
|
elif part['pseudoclass']:
|
|
pc = part['pseudoclass'].pop()
|
|
node_cur = get_node(node_cur, f":{pc}")
|
|
elif part['attribs']:
|
|
a = part['attribs'].pop()
|
|
node_cur = get_node(node_cur, f"[{a}]")
|
|
elif part['attribvals']:
|
|
k,v = part['attribvals'].popitem()
|
|
node_cur = get_node(node_cur, f'[{k}="{v}"]')
|
|
elif not parts:
|
|
break
|
|
else:
|
|
skip = 1
|
|
if node_cur != trie:
|
|
if parts[-1]['type'] == '>':
|
|
node_cur = get_node(node_cur, '>')
|
|
skip = 2
|
|
elif parts[-1]['type'] == '+':
|
|
assert False, f'adjacent sibling combinator (`+`) is not yet supported'
|
|
node_cur = get_node(node_cur, '+')
|
|
skip = 2
|
|
elif parts[-1]['type'] == '~':
|
|
assert False, f'general sibling combinator (`~`) is not yet supported'
|
|
node_cur = get_node(node_cur, '~')
|
|
skip = 2
|
|
else:
|
|
node_cur = get_node(node_cur, ' ')
|
|
part, parts = copy.deepcopy(parts[-skip]), parts[:-skip]
|
|
node_cur.setdefault('__rulesets', list()).append((specificity, rule)) # styling rules to apply along with specificity (sorting)
|
|
node_cur.setdefault('__selectors', list()).append(nselector) # only informational (debugging)
|
|
return trie
|
|
|
|
if not self._trie_full:
|
|
self._trie_full = build_trie()
|
|
if not self._trie_stripped:
|
|
self._trie_stripped = build_trie(strip={
|
|
# 'type',
|
|
# 'classes',
|
|
# 'id',
|
|
# 'pseudoelements',
|
|
'pseudoclasses',
|
|
'attributes',
|
|
'attributevalues',
|
|
})
|
|
|
|
def get_matching_rules(self, selector, full_trie=True):
|
|
self.optimize()
|
|
rules = []
|
|
split = UI_Style_RuleSet._split_selector
|
|
parts = [split(p) for p in selector]
|
|
if not parts: return []
|
|
def m(node_cur, part, parts, pseudoelement_handled, depth):
|
|
nonlocal rules
|
|
part_has_pseudoelement = bool(part['pseudoelement'])
|
|
for (edge_label, node_next) in node_cur.items():
|
|
if edge_label == ' ':
|
|
ps = parts
|
|
while ps:
|
|
p,ps = ps[-1],ps[:-1]
|
|
m(node_next, p, ps, False, depth+1)
|
|
elif edge_label == '>':
|
|
if parts:
|
|
m(node_next, parts[-1], parts[:-1], False, depth+1)
|
|
elif edge_label == '*' and (pseudoelement_handled or not part_has_pseudoelement):
|
|
m(node_next, part, parts, pseudoelement_handled, depth+1)
|
|
elif edge_label[0] == '#':
|
|
if edge_label[1:] == part['id']:
|
|
m(node_next, part, parts, pseudoelement_handled, depth+1)
|
|
elif edge_label[0] == '.':
|
|
if edge_label[1:] in part['class']:
|
|
m(node_next, part, parts, pseudoelement_handled, depth+1)
|
|
elif edge_label[:2] == '::':
|
|
if edge_label[2:] == part['pseudoelement']:
|
|
m(node_next, part, parts, True, depth+1)
|
|
elif edge_label[0] == ':':
|
|
if edge_label[1:] in part['pseudoclass']:
|
|
m(node_next, part, parts, pseudoelement_handled, depth+1)
|
|
elif edge_label[0] == '[':
|
|
attrib_parts = edge_label[1:-1].split('=') # remove square brackets and split on `=`
|
|
attrib_key = attrib_parts[0]
|
|
if len(attrib_parts) == 1:
|
|
if attrib_key in part['attribs']:
|
|
m(node_next, part, parts, pseudoelement_handled, depth+1)
|
|
else:
|
|
attrib_val = attrib_parts[1][1:-1] # remove quotes from attribute value
|
|
if part['attribvals'].get(attrib_key) == attrib_val:
|
|
m(node_next, part, parts, pseudoelement_handled, depth+1)
|
|
elif edge_label in {'__selectors', '__parent', '__uid'}:
|
|
pass
|
|
elif edge_label == '__rulesets':
|
|
rules.extend(node_cur['__rulesets'])
|
|
else:
|
|
# assuming type
|
|
if edge_label == part['type'] and (pseudoelement_handled or not part_has_pseudoelement):
|
|
m(node_next, part, parts, pseudoelement_handled, depth+1)
|
|
m(self._trie_full if full_trie else self._trie_stripped, parts[-1], parts[:-1], False, 0)
|
|
rules.sort(key=lambda sr:sr[0])
|
|
return [r for (s,r) in rules]
|
|
|
|
def has_matches_trie(self, selector, full_trie=True):
|
|
self.optimize()
|
|
rules = []
|
|
def m(node_cur, part, parts, depth):
|
|
nonlocal rules
|
|
for (edge_label, node_next) in node_cur.items():
|
|
if edge_label == ' ':
|
|
ps = parts
|
|
while ps:
|
|
p,ps = ps[-1],ps[:-1]
|
|
if m(node_next, p, ps, depth+1): return True
|
|
elif edge_label == '>':
|
|
if parts and m(node_next, parts[-1], parts[:-1], depth+1): return True
|
|
elif edge_label == '*':
|
|
if m(node_next, part, parts, depth+1): return True
|
|
elif edge_label[0] == '#':
|
|
if edge_label[1:] == part['id'] and m(node_next, part, parts, depth+1): return True
|
|
elif edge_label[0] == '.':
|
|
if edge_label[1:] in part['class'] and m(node_next, part, parts, depth+1): return True
|
|
elif edge_label[:2] == '::':
|
|
if edge_label[2:] == part['pseudoelement'] and m(node_next, part, parts, depth+1): return True
|
|
elif edge_label[0] == ':':
|
|
if edge_label[1:] in part['pseudoclass'] and m(node_next, part, parts, depth+1): return True
|
|
elif edge_label[0] == '[':
|
|
attrib_parts = edge_label[1:-1].split('=') # remove square brackets and split on `=`
|
|
attrib_key = attrib_parts[0]
|
|
if len(attrib_parts) == 1:
|
|
if attrib_key in part['attribs'] and m(node_next, part, parts, depth+1): return True
|
|
else:
|
|
attrib_val = attrib_parts[1][1:-1] # remove quotes from attribute value
|
|
if part['attribvals'].get(attrib_key) == attrib_val and m(node_next, part, parts, depth+1): return True
|
|
elif edge_label in {'__selectors', '__parent', '__uid'}:
|
|
pass
|
|
elif edge_label == '__rulesets':
|
|
return True
|
|
else:
|
|
# assuming type
|
|
if edge_label == part['type'] and m(node_next, part, parts, depth+1): return True
|
|
return False
|
|
split = UI_Style_RuleSet._split_selector
|
|
parts = [split(p) for p in selector]
|
|
if not parts: return False
|
|
return m(self._trie_full if full_trie else self._trie_stripped, parts[-1], parts[:-1], 0)
|
|
|
|
|
|
@staticmethod
|
|
def from_decllist(decllist, selector=None, var=None, inline=False, defaults=False):
|
|
if selector is None: selector = ['*']
|
|
if var is None: var = UI_Styling(inline=inline, defaults=defaults)
|
|
var.rules = [UI_Style_RuleSet.from_decllist(decllist, selector, inline=inline, defaults=defaults)]
|
|
return var
|
|
|
|
@staticmethod
|
|
def from_selector_decllist_list(l, inline=False, defaults=False):
|
|
var = UI_Styling(inline=inline, defaults=defaults)
|
|
var.rules = [UI_Style_RuleSet.from_decllist(decllist, selector, inline=inline, defaults=defaults) for (selector,decllist) in l]
|
|
return var
|
|
|
|
def __init__(self, lines=None, inline=False, defaults=False):
|
|
self._uid = UI_Styling.uid_generator.next()
|
|
self._inline = inline
|
|
self._defaults = defaults
|
|
self._rules = []
|
|
self._decllist_cache = {}
|
|
self._matches_cache = {}
|
|
if lines:
|
|
self.load_from_text(lines)
|
|
self.dirty_optimization()
|
|
|
|
def __str__(self):
|
|
if not self._rules: return f'<UI_Styling{self._uid}>'
|
|
s = '\n'.join(' '+l for r in self._rules for l in str(r).splitlines())
|
|
return f'<UI_Styling{self._uid}\n{s}\n>'
|
|
|
|
def __repr__(self): return self.__str__()
|
|
|
|
|
|
@property
|
|
def rules(self):
|
|
return list(self._rules)
|
|
@rules.setter
|
|
def rules(self, v):
|
|
self._rules = v
|
|
self.dirty_optimization()
|
|
|
|
def dirty_optimization(self):
|
|
self._trie_full = None
|
|
self._trie_stripped = None
|
|
|
|
@property
|
|
def simple_str(self): return f'<UI_Styling{self._uid}>'
|
|
|
|
# @profiler.function
|
|
def get_decllist(self, selector):
|
|
cache = self._decllist_cache
|
|
if not self._rules: return []
|
|
oselector = str(selector)
|
|
if oselector not in cache:
|
|
# print('UI_Styling.get_decllist', selector)
|
|
if True: # with profiler.code('UI_Styling.get_decllist: creating cached value'):
|
|
decllist = [d for rule in self.get_matching_rules(selector) for d in rule.decllist]
|
|
# decllist = [d for rule in self._rules if rule.match(selector) for d in rule.decllist]
|
|
cache[oselector] = decllist
|
|
return cache[oselector]
|
|
|
|
def _has_matches(self, selector):
|
|
if not self._rules: return False
|
|
selector_key = tuple(selector)
|
|
if selector_key not in self._matches_cache:
|
|
self._matches_cache[selector_key] = self.has_matches_trie(selector)
|
|
# self._matches_cache[selector_key] = any(rule.match(selector) for rule in self._rules)
|
|
return self._matches_cache[selector_key]
|
|
|
|
def append(self, other_styling):
|
|
self.clear_cache()
|
|
self._rules += other_styling.rules
|
|
self.dirty_optimization()
|
|
return self
|
|
|
|
|
|
@staticmethod
|
|
def _trbl_split(v):
|
|
# NOTE: if v is a tuple, either: (scalar, unit) or ((scalar, unit), (scalar, unit), ...)
|
|
# TODO: IGNORING UNITS??
|
|
if type(v) is not tuple: return (v, v, v, v)
|
|
l = len(v)
|
|
if l == 1: return (v[0], v[0], v[0], v[0])
|
|
if l == 2: return (v[0], v[1], v[0], v[1])
|
|
if l == 3: return (v[0], v[1], v[2], v[1])
|
|
return (v[0], v[1], v[2], v[3])
|
|
|
|
@staticmethod
|
|
def _font_split(vs):
|
|
if type(vs) is not tuple:
|
|
return default_fonts[vs] if vs in default_fonts else default_fonts['default']
|
|
return tuple(v if v else d for (v,d) in zip_longest(vs,default_fonts['default']))
|
|
|
|
@staticmethod
|
|
# @profiler.function
|
|
def _expand_declarations(decls):
|
|
decllist = {}
|
|
for decl in decls:
|
|
p,v = decl.property, decl.value
|
|
if p in {'margin','padding'}:
|
|
vals = UI_Styling._trbl_split(v)
|
|
decllist['%s-top'%p] = vals[0]
|
|
decllist['%s-right'%p] = vals[1]
|
|
decllist['%s-bottom'%p] = vals[2]
|
|
decllist['%s-left'%p] = vals[3]
|
|
elif p == 'border':
|
|
if type(v) is not tuple: v = (v,)
|
|
if type(v[0]) is NumberUnit or type(v[0]) is float:
|
|
decllist['border-width'] = v[0]
|
|
v = v[1:]
|
|
if v:
|
|
vals = UI_Styling._trbl_split(v)
|
|
decllist['border-top-color'] = vals[0]
|
|
decllist['border-right-color'] = vals[1]
|
|
decllist['border-bottom-color'] = vals[2]
|
|
decllist['border-left-color'] = vals[3]
|
|
elif p == 'border-color':
|
|
vals = UI_Styling._trbl_split(v)
|
|
decllist['border-top-color'] = vals[0]
|
|
decllist['border-right-color'] = vals[1]
|
|
decllist['border-bottom-color'] = vals[2]
|
|
decllist['border-left-color'] = vals[3]
|
|
elif p == 'font':
|
|
vals = UI_Styling._font_split(v)
|
|
decllist['font-style'] = vals[0]
|
|
decllist['font-weight'] = vals[1]
|
|
decllist['font-size'] = vals[2]
|
|
decllist['font-family'] = vals[3]
|
|
elif p == 'background':
|
|
if type(v) is not tuple: v = (v,)
|
|
for ev in v:
|
|
if type(ev) is Color:
|
|
decllist['background-color'] = ev
|
|
else:
|
|
decllist['background-image'] = ev
|
|
elif p == 'width':
|
|
decllist['width'] = v
|
|
# decllist['min-width'] = v
|
|
# decllist['max-width'] = v
|
|
elif p == 'height':
|
|
decllist['height'] = v
|
|
decllist['min-height'] = v
|
|
decllist['max-height'] = v
|
|
elif p == 'overflow':
|
|
if v == 'scroll':
|
|
decllist['overflow-x'] = 'auto'
|
|
decllist['overflow-y'] = 'scroll'
|
|
else:
|
|
decllist['overflow-x'] = v
|
|
decllist['overflow-y'] = v
|
|
else:
|
|
decllist[p] = v
|
|
# filter out properties with `initial` values
|
|
decllist = { k:v for (k,v) in decllist.items() if v != 'initial' }
|
|
return decllist
|
|
|
|
@staticmethod
|
|
# @profiler.function
|
|
def compute_style(selector, *stylings):
|
|
if selector is None: return {}
|
|
full_decllist = [dl for styling in stylings if styling for dl in styling.get_decllist(selector)]
|
|
decllist = UI_Styling._expand_declarations(full_decllist)
|
|
return decllist
|
|
|
|
@staticmethod
|
|
@add_cache('_cache', {})
|
|
def strip_selector_parts(selector, strip):
|
|
if not strip: return selector
|
|
cache = UI_Styling.strip_selector_parts._cache
|
|
oselector = str((selector, strip))
|
|
if oselector not in cache:
|
|
nselector = []
|
|
strip_type = 'type' in strip
|
|
strip_id = 'id' in strip
|
|
strip_classes = 'classes' in strip
|
|
strip_pseudoelements = 'pseudoelements' in strip
|
|
strip_pseudoclasses = 'pseudoclasses' in strip
|
|
strip_attributes = 'attributes' in strip
|
|
strip_attributevalues = 'attributevalues' in strip
|
|
for sel in selector:
|
|
# p = {'type':'', 'class':set(), 'id':'', 'pseudoelement':set(), 'pseudoclass':set(), 'attribs':set(), 'attribvals':{}}
|
|
p = UI_Style_RuleSet._split_selector(str(sel))
|
|
if strip_type: p['type'] = '*'
|
|
if strip_id: p['id'] = ''
|
|
if strip_classes: p['class'] = set()
|
|
if strip_pseudoelements: p['pseudoelement'] = ''
|
|
if strip_pseudoclasses: p['pseudoclass'] = set()
|
|
if strip_attributes: p['attribs'] = set()
|
|
if strip_attributevalues: p['attribvals'] = dict()
|
|
nselector.append(UI_Style_RuleSet._join_selector_parts(p))
|
|
cache[oselector] = nselector
|
|
return cache[oselector]
|
|
|
|
@staticmethod
|
|
@add_cache('_cache', {})
|
|
def trim_styling(selector, *stylings):
|
|
cache = UI_Styling.trim_styling._cache
|
|
strip = {
|
|
# 'type',
|
|
# 'classes',
|
|
# 'id',
|
|
# 'pseudoelements',
|
|
'pseudoclasses',
|
|
'attributes',
|
|
'attributevalues',
|
|
}
|
|
nselector = UI_Styling.strip_selector_parts(selector, strip)
|
|
onselector = str(nselector)
|
|
if onselector not in cache:
|
|
nstyling = UI_Styling()
|
|
# include only the rules that _might_ apply to selector (assumes some selector parts change but others do not)
|
|
nstyling.rules = [rule for styling in stylings if styling for rule in styling.get_matching_rules(nselector, full_trie=False)]
|
|
# nstyling.rules = [rule for styling in stylings for rule in styling.rules if rule.match(nselector, strip=strip)]
|
|
cache[onselector] = nstyling
|
|
return cache[onselector]
|
|
|
|
@staticmethod
|
|
def combine_styling(*stylings, inline=False, defaults=False):
|
|
nstyling = UI_Styling(inline=inline, defaults=defaults)
|
|
nstyling.rules = [rule for styling in stylings for rule in styling.rules]
|
|
return nstyling
|
|
|
|
@staticmethod
|
|
def has_matches(selector, *stylings):
|
|
if selector is None: return False
|
|
return any(styling._has_matches(selector) for styling in stylings if styling)
|
|
|
|
# @profiler.function
|
|
def filter_styling(self, selector):
|
|
decllist = self.compute_style(selector, self)
|
|
styling = UI_Styling.from_decllist(decllist, selector=selector)
|
|
return styling
|
|
|
|
|
|
ui_defaultstylings = UI_Styling(defaults=True)
|
|
def load_defaultstylings():
|
|
global ui_defaultstylings
|
|
path = get_path_from_addon_common('common', 'config', 'ui_defaultstyles.css')
|
|
if os.path.exists(path): ui_defaultstylings.load_from_file(path)
|
|
else: ui_defaultstylings.rules = []
|
|
load_defaultstylings() |