mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Conversion: Handle shorthand CSS properties
Conversion: Fix bugs in the handling of shorthand CSS properties such as font or border, which could cause some properties to not be resolved correctly during conversion.
This commit is contained in:
parent
3d12ae2e9e
commit
5c1998610f
269
src/calibre/ebooks/oeb/normalize_css.py
Normal file
269
src/calibre/ebooks/oeb/normalize_css.py
Normal file
@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
from future_builtins import zip
|
||||
from functools import wraps
|
||||
|
||||
try:
|
||||
from cssutils.css import PropertyValue
|
||||
except ImportError:
|
||||
raise RuntimeError('You need cssutils >= 0.9.9 for calibre')
|
||||
from cssutils import profile as cssprofiles
|
||||
|
||||
DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll', # {{{
|
||||
'background-color': 'transparent', 'background-image': 'none',
|
||||
'background-position': '0% 0%', 'background-repeat': 'repeat',
|
||||
'border-bottom-color': 'currentColor', 'border-bottom-style': 'none',
|
||||
'border-bottom-width': 'medium', 'border-collapse': 'separate',
|
||||
'border-left-color': 'currentColor', 'border-left-style': 'none',
|
||||
'border-left-width': 'medium', 'border-right-color': 'currentColor',
|
||||
'border-right-style': 'none', 'border-right-width': 'medium',
|
||||
'border-spacing': 0, 'border-top-color': 'currentColor',
|
||||
'border-top-style': 'none', 'border-top-width': 'medium', 'bottom':
|
||||
'auto', 'caption-side': 'top', 'clear': 'none', 'clip': 'auto',
|
||||
'color': 'black', 'content': 'normal', 'counter-increment': 'none',
|
||||
'counter-reset': 'none', 'cue-after': 'none', 'cue-before': 'none',
|
||||
'cursor': 'auto', 'direction': 'ltr', 'display': 'inline',
|
||||
'elevation': 'level', 'empty-cells': 'show', 'float': 'none',
|
||||
'font-family': 'serif', 'font-size': 'medium', 'font-style':
|
||||
'normal', 'font-variant': 'normal', 'font-weight': 'normal',
|
||||
'height': 'auto', 'left': 'auto', 'letter-spacing': 'normal',
|
||||
'line-height': 'normal', 'list-style-image': 'none',
|
||||
'list-style-position': 'outside', 'list-style-type': 'disc',
|
||||
'margin-bottom': 0, 'margin-left': 0, 'margin-right': 0,
|
||||
'margin-top': 0, 'max-height': 'none', 'max-width': 'none',
|
||||
'min-height': 0, 'min-width': 0, 'orphans': '2',
|
||||
'outline-color': 'invert', 'outline-style': 'none',
|
||||
'outline-width': 'medium', 'overflow': 'visible', 'padding-bottom':
|
||||
0, 'padding-left': 0, 'padding-right': 0, 'padding-top': 0,
|
||||
'page-break-after': 'auto', 'page-break-before': 'auto',
|
||||
'page-break-inside': 'auto', 'pause-after': 0, 'pause-before':
|
||||
0, 'pitch': 'medium', 'pitch-range': '50', 'play-during': 'auto',
|
||||
'position': 'static', 'quotes': u"'“' '”' '‘' '’'", 'richness':
|
||||
'50', 'right': 'auto', 'speak': 'normal', 'speak-header': 'once',
|
||||
'speak-numeral': 'continuous', 'speak-punctuation': 'none',
|
||||
'speech-rate': 'medium', 'stress': '50', 'table-layout': 'auto',
|
||||
'text-align': 'auto', 'text-decoration': 'none', 'text-indent':
|
||||
0, 'text-transform': 'none', 'top': 'auto', 'unicode-bidi':
|
||||
'normal', 'vertical-align': 'baseline', 'visibility': 'visible',
|
||||
'voice-family': 'default', 'volume': 'medium', 'white-space':
|
||||
'normal', 'widows': '2', 'width': 'auto', 'word-spacing': 'normal',
|
||||
'z-index': 'auto'}
|
||||
# }}}
|
||||
|
||||
EDGES = ('top', 'right', 'bottom', 'left')
|
||||
|
||||
def normalize_edge(name, cssvalue):
|
||||
style = {}
|
||||
if isinstance(cssvalue, PropertyValue):
|
||||
primitives = [v.cssText for v in cssvalue]
|
||||
else:
|
||||
primitives = [cssvalue.cssText]
|
||||
if len(primitives) == 1:
|
||||
value, = primitives
|
||||
values = (value, value, value, value)
|
||||
elif len(primitives) == 2:
|
||||
vert, horiz = primitives
|
||||
values = (vert, horiz, vert, horiz)
|
||||
elif len(primitives) == 3:
|
||||
top, horiz, bottom = primitives
|
||||
values = (top, horiz, bottom, horiz)
|
||||
else:
|
||||
values = primitives[:4]
|
||||
if '-' in name:
|
||||
l, _, r = name.partition('-')
|
||||
for edge, value in zip(EDGES, values):
|
||||
style['%s-%s-%s' % (l, edge, r)] = value
|
||||
else:
|
||||
for edge, value in zip(EDGES, values):
|
||||
style['%s-%s' % (name, edge)] = value
|
||||
return style
|
||||
|
||||
|
||||
def simple_normalizer(prefix, names, check_inherit=True):
|
||||
composition = tuple('%s-%s' %(prefix, n) for n in names)
|
||||
@wraps(normalize_simple_composition)
|
||||
def wrapper(name, cssvalue):
|
||||
return normalize_simple_composition(name, cssvalue, composition, check_inherit=check_inherit)
|
||||
return wrapper
|
||||
|
||||
|
||||
def normalize_simple_composition(name, cssvalue, composition, check_inherit=True):
|
||||
if check_inherit and cssvalue.cssText == 'inherit':
|
||||
style = {k:'inherit' for k in composition}
|
||||
else:
|
||||
style = {k:DEFAULTS[k] for k in composition}
|
||||
try:
|
||||
primitives = [v.cssText for v in cssvalue]
|
||||
except TypeError:
|
||||
primitives = [cssvalue.cssText]
|
||||
while primitives:
|
||||
value = primitives.pop()
|
||||
for key in composition:
|
||||
if cssprofiles.validate(key, value):
|
||||
style[key] = value
|
||||
break
|
||||
return style
|
||||
|
||||
font_composition = ('font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family')
|
||||
|
||||
def normalize_font(name, cssvalue):
|
||||
# See https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
||||
composition = font_composition
|
||||
val = cssvalue.cssText
|
||||
if val == 'inherit':
|
||||
return {k:'inherit' for k in composition}
|
||||
if val in {'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'}:
|
||||
return {k:DEFAULTS[k] for k in composition}
|
||||
try:
|
||||
primitives = list(v.cssText for v in cssvalue)
|
||||
except TypeError:
|
||||
primitives = [cssvalue.cssText]
|
||||
if len(primitives) < 2:
|
||||
return {} # Mandatory to define both font size and font family
|
||||
style = {k:DEFAULTS[k] for k in composition}
|
||||
style['font-family'] = primitives.pop()
|
||||
if len(primitives) > 1 and cssprofiles.validate('line-height', primitives[-1]) and cssprofiles.validate('font-size', primitives[-2]):
|
||||
style['line-height'], style['font-size'] = primitives.pop(), primitives.pop()
|
||||
else:
|
||||
val = primitives.pop()
|
||||
if not cssprofiles.validate('font-size', val):
|
||||
return {}
|
||||
style['font-size'] = val
|
||||
composition = composition[:3]
|
||||
while primitives:
|
||||
value = primitives.pop()
|
||||
for key in composition:
|
||||
if cssprofiles.validate(key, value):
|
||||
style[key] = value
|
||||
break
|
||||
return style
|
||||
|
||||
def normalize_border(name, cssvalue):
|
||||
style = normalizers['border-' + EDGES[0]]('border-' + EDGES[0], cssvalue)
|
||||
vals = style.copy()
|
||||
for edge in EDGES[1:]:
|
||||
style.update({k.replace(EDGES[0], edge):v for k, v in vals.iteritems()})
|
||||
return style
|
||||
|
||||
normalizers = {
|
||||
'list-style': simple_normalizer('list-style', ('type', 'position', 'image')),
|
||||
'font': normalize_font,
|
||||
'border': normalize_border,
|
||||
}
|
||||
|
||||
for x in ('margin', 'padding', 'border-style', 'border-width', 'border-color'):
|
||||
normalizers[x] = normalize_edge
|
||||
|
||||
for x in EDGES:
|
||||
name = 'border-' + x
|
||||
normalizers[name] = simple_normalizer(name, ('color', 'style', 'width'), check_inherit=False)
|
||||
|
||||
def test_normalization():
|
||||
import unittest
|
||||
from cssutils import parseStyle
|
||||
|
||||
class TestNormalization(unittest.TestCase):
|
||||
longMessage = True
|
||||
maxDiff = None
|
||||
|
||||
def test_font_normalization(self):
|
||||
def font_dict(expected):
|
||||
ans = {k:DEFAULTS[k] for k in font_composition}
|
||||
ans.update(expected)
|
||||
return ans
|
||||
|
||||
for raw, expected in {
|
||||
'some_font': {}, 'none': {}, 'inherit':{k:'inherit' for k in font_composition},
|
||||
'1.2pt/1.4 A_Font': {'font-family':'A_Font', 'font-size':'1.2pt', 'line-height':'1.4'},
|
||||
'bad font': {}, '10% serif': {'font-family':'serif', 'font-size':'10%'},
|
||||
'bold italic large serif': {'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'large'},
|
||||
'bold italic small-caps larger/normal serif':
|
||||
{'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'larger',
|
||||
'line-height':'normal', 'font-variant':'small-caps'},
|
||||
}.iteritems():
|
||||
val = tuple(parseStyle('font: %s' % raw, validate=False))[0].cssValue
|
||||
style = normalizers['font']('font', val)
|
||||
if expected:
|
||||
self.assertDictEqual(font_dict(expected), style)
|
||||
else:
|
||||
self.assertDictEqual(expected, style)
|
||||
|
||||
def test_border_normalization(self):
|
||||
def border_edge_dict(expected, edge='right'):
|
||||
ans = {'border-%s-%s' % (edge, x): DEFAULTS['border-%s-%s' % (edge, x)] for x in ('style', 'width', 'color')}
|
||||
for x, v in expected.iteritems():
|
||||
ans['border-%s-%s' % (edge, x)] = v
|
||||
return ans
|
||||
def border_dict(expected):
|
||||
ans = {}
|
||||
for edge in EDGES:
|
||||
ans.update(border_edge_dict(expected, edge))
|
||||
return ans
|
||||
def border_val_dict(expected, val='color'):
|
||||
ans = {'border-%s-%s' % (edge, val): DEFAULTS['border-%s-%s' % (edge, val)] for edge in EDGES}
|
||||
for edge in EDGES:
|
||||
ans['border-%s-%s' % (edge, val)] = expected
|
||||
return ans
|
||||
|
||||
for raw, expected in {
|
||||
'solid 1px red': {'color':'red', 'width':'1px', 'style':'solid'},
|
||||
'1px': {'width': '1px'}, '#aaa': {'color': '#aaa'},
|
||||
'2em groove': {'width':'2em', 'style':'groove'},
|
||||
}.iteritems():
|
||||
for edge in EDGES:
|
||||
br = 'border-%s' % edge
|
||||
val = tuple(parseStyle('%s: %s' % (br, raw), validate=False))[0].cssValue
|
||||
self.assertDictEqual(border_edge_dict(expected, edge), normalizers[br](br, val))
|
||||
|
||||
for raw, expected in {
|
||||
'solid 1px red': {'color':'red', 'width':'1px', 'style':'solid'},
|
||||
'1px': {'width': '1px'}, '#aaa': {'color': '#aaa'},
|
||||
'thin groove': {'width':'thin', 'style':'groove'},
|
||||
}.iteritems():
|
||||
val = tuple(parseStyle('%s: %s' % ('border', raw), validate=False))[0].cssValue
|
||||
self.assertDictEqual(border_dict(expected), normalizers['border']('border', val))
|
||||
|
||||
for name, val in {
|
||||
'width': '10%', 'color': 'rgb(0, 1, 1)', 'style': 'double',
|
||||
}.iteritems():
|
||||
cval = tuple(parseStyle('border-%s: %s' % (name, val), validate=False))[0].cssValue
|
||||
self.assertDictEqual(border_val_dict(val, name), normalizers['border-'+name]('border-'+name, cval))
|
||||
|
||||
def test_edge_normalization(self):
|
||||
def edge_dict(prefix, expected):
|
||||
return {'%s-%s' % (prefix, edge) : x for edge, x in zip(EDGES, expected)}
|
||||
for raw, expected in {
|
||||
'2px': ('2px', '2px', '2px', '2px'),
|
||||
'1em 2em': ('1em', '2em', '1em', '2em'),
|
||||
'1em 2em 3em': ('1em', '2em', '3em', '2em'),
|
||||
'1 2 3 4': ('1', '2', '3', '4'),
|
||||
}.iteritems():
|
||||
for prefix in ('margin', 'padding'):
|
||||
cval = tuple(parseStyle('%s: %s' % (prefix, raw), validate=False))[0].cssValue
|
||||
self.assertDictEqual(edge_dict(prefix, expected), normalizers[prefix](prefix, cval))
|
||||
|
||||
def test_list_style_normalization(self):
|
||||
def ls_dict(expected):
|
||||
ans = {'list-style-%s' % x : DEFAULTS['list-style-%s' % x] for x in ('type', 'image', 'position')}
|
||||
for k, v in expected.iteritems():
|
||||
ans['list-style-%s' % k] = v
|
||||
return ans
|
||||
for raw, expected in {
|
||||
'url(http://www.example.com/images/list.png)': {'image': 'url(http://www.example.com/images/list.png)'},
|
||||
'inside square': {'position':'inside', 'type':'square'},
|
||||
'upper-roman url(img) outside': {'position':'outside', 'type':'upper-roman', 'image':'url(img)'},
|
||||
}.iteritems():
|
||||
cval = tuple(parseStyle('list-style: %s' % raw, validate=False))[0].cssValue
|
||||
self.assertDictEqual(ls_dict(expected), normalizers['list-style']('list-style', cval))
|
||||
|
||||
tests = unittest.defaultTestLoader.loadTestsFromTestCase(TestNormalization)
|
||||
unittest.TextTestRunner(verbosity=4).run(tests)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_normalization()
|
@ -8,15 +8,11 @@ from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||
|
||||
import os, itertools, re, logging, copy, unicodedata
|
||||
import os, re, logging, copy, unicodedata
|
||||
from weakref import WeakKeyDictionary
|
||||
from xml.dom import SyntaxErr as CSSSyntaxError
|
||||
from cssutils.css import (CSSStyleRule, CSSPageRule, CSSFontFaceRule,
|
||||
cssproperties)
|
||||
try:
|
||||
from cssutils.css import PropertyValue
|
||||
except ImportError:
|
||||
raise RuntimeError('You need cssutils >= 0.9.9 for calibre')
|
||||
from cssutils import (profile as cssprofiles, parseString, parseStyle, log as
|
||||
cssutils_log, CSSParser, profiles, replaceUrls)
|
||||
from lxml import etree
|
||||
@ -24,8 +20,8 @@ from cssselect import HTMLTranslator
|
||||
|
||||
from calibre import force_unicode
|
||||
from calibre.ebooks import unit_convert
|
||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
|
||||
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
|
||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES, XPNSMAP, xpath, urlnormalize
|
||||
from calibre.ebooks.oeb.normalize_css import DEFAULTS, normalizers
|
||||
|
||||
cssutils_log.setLevel(logging.WARN)
|
||||
|
||||
@ -54,46 +50,6 @@ INHERITED = set(['azimuth', 'border-collapse', 'border-spacing',
|
||||
'visibility', 'voice-family', 'volume', 'white-space',
|
||||
'widows', 'word-spacing'])
|
||||
|
||||
DEFAULTS = {'azimuth': 'center', 'background-attachment': 'scroll',
|
||||
'background-color': 'transparent', 'background-image': 'none',
|
||||
'background-position': '0% 0%', 'background-repeat': 'repeat',
|
||||
'border-bottom-color': ':color', 'border-bottom-style': 'none',
|
||||
'border-bottom-width': 'medium', 'border-collapse': 'separate',
|
||||
'border-left-color': ':color', 'border-left-style': 'none',
|
||||
'border-left-width': 'medium', 'border-right-color': ':color',
|
||||
'border-right-style': 'none', 'border-right-width': 'medium',
|
||||
'border-spacing': 0, 'border-top-color': ':color',
|
||||
'border-top-style': 'none', 'border-top-width': 'medium', 'bottom':
|
||||
'auto', 'caption-side': 'top', 'clear': 'none', 'clip': 'auto',
|
||||
'color': 'black', 'content': 'normal', 'counter-increment': 'none',
|
||||
'counter-reset': 'none', 'cue-after': 'none', 'cue-before': 'none',
|
||||
'cursor': 'auto', 'direction': 'ltr', 'display': 'inline',
|
||||
'elevation': 'level', 'empty-cells': 'show', 'float': 'none',
|
||||
'font-family': 'serif', 'font-size': 'medium', 'font-style':
|
||||
'normal', 'font-variant': 'normal', 'font-weight': 'normal',
|
||||
'height': 'auto', 'left': 'auto', 'letter-spacing': 'normal',
|
||||
'line-height': 'normal', 'list-style-image': 'none',
|
||||
'list-style-position': 'outside', 'list-style-type': 'disc',
|
||||
'margin-bottom': 0, 'margin-left': 0, 'margin-right': 0,
|
||||
'margin-top': 0, 'max-height': 'none', 'max-width': 'none',
|
||||
'min-height': 0, 'min-width': 0, 'orphans': '2',
|
||||
'outline-color': 'invert', 'outline-style': 'none',
|
||||
'outline-width': 'medium', 'overflow': 'visible', 'padding-bottom':
|
||||
0, 'padding-left': 0, 'padding-right': 0, 'padding-top': 0,
|
||||
'page-break-after': 'auto', 'page-break-before': 'auto',
|
||||
'page-break-inside': 'auto', 'pause-after': 0, 'pause-before':
|
||||
0, 'pitch': 'medium', 'pitch-range': '50', 'play-during': 'auto',
|
||||
'position': 'static', 'quotes': u"'“' '”' '‘' '’'", 'richness':
|
||||
'50', 'right': 'auto', 'speak': 'normal', 'speak-header': 'once',
|
||||
'speak-numeral': 'continuous', 'speak-punctuation': 'none',
|
||||
'speech-rate': 'medium', 'stress': '50', 'table-layout': 'auto',
|
||||
'text-align': 'auto', 'text-decoration': 'none', 'text-indent':
|
||||
0, 'text-transform': 'none', 'top': 'auto', 'unicode-bidi':
|
||||
'normal', 'vertical-align': 'baseline', 'visibility': 'visible',
|
||||
'voice-family': 'default', 'volume': 'medium', 'white-space':
|
||||
'normal', 'widows': '2', 'width': 'auto', 'word-spacing': 'normal',
|
||||
'z-index': 'auto'}
|
||||
|
||||
FONT_SIZE_NAMES = set(['xx-small', 'x-small', 'small', 'medium', 'large',
|
||||
'x-large', 'xx-large'])
|
||||
|
||||
@ -421,14 +377,11 @@ class Stylizer(object):
|
||||
style = {}
|
||||
for prop in cssstyle:
|
||||
name = prop.name
|
||||
if name in ('margin', 'padding'):
|
||||
style.update(self._normalize_edge(prop.cssValue, name))
|
||||
elif name == 'font':
|
||||
style.update(self._normalize_font(prop.cssValue))
|
||||
elif name == 'list-style':
|
||||
style.update(self._normalize_list_style(prop.cssValue))
|
||||
normalizer = normalizers.get(name, None)
|
||||
if normalizer is not None:
|
||||
style.update(normalizer(name, prop.cssValue))
|
||||
elif name == 'text-align':
|
||||
style.update(self._normalize_text_align(prop.cssValue))
|
||||
style['text-align'] = self._apply_text_align(prop.value)
|
||||
else:
|
||||
style[name] = prop.value
|
||||
if 'font-size' in style:
|
||||
@ -441,93 +394,10 @@ class Stylizer(object):
|
||||
style['font-size'] = "%dpt" % self.profile.fnames[size]
|
||||
return style
|
||||
|
||||
def _normalize_edge(self, cssvalue, name):
|
||||
style = {}
|
||||
if isinstance(cssvalue, PropertyValue):
|
||||
primitives = [v.cssText for v in cssvalue]
|
||||
else:
|
||||
primitives = [cssvalue.cssText]
|
||||
if len(primitives) == 1:
|
||||
value, = primitives
|
||||
values = [value, value, value, value]
|
||||
elif len(primitives) == 2:
|
||||
vert, horiz = primitives
|
||||
values = [vert, horiz, vert, horiz]
|
||||
elif len(primitives) == 3:
|
||||
top, horiz, bottom = primitives
|
||||
values = [top, horiz, bottom, horiz]
|
||||
else:
|
||||
values = primitives[:4]
|
||||
edges = ('top', 'right', 'bottom', 'left')
|
||||
for edge, value in itertools.izip(edges, values):
|
||||
style["%s-%s" % (name, edge)] = value
|
||||
return style
|
||||
|
||||
def _normalize_list_style(self, cssvalue):
|
||||
composition = ('list-style-type', 'list-style-position',
|
||||
'list-style-image')
|
||||
style = {}
|
||||
if cssvalue.cssText == 'inherit':
|
||||
for key in composition:
|
||||
style[key] = 'inherit'
|
||||
else:
|
||||
try:
|
||||
primitives = [v.cssText for v in cssvalue]
|
||||
except TypeError:
|
||||
primitives = [cssvalue.cssText]
|
||||
primitives.reverse()
|
||||
value = primitives.pop()
|
||||
for key in composition:
|
||||
if cssprofiles.validate(key, value):
|
||||
style[key] = value
|
||||
if not primitives:
|
||||
break
|
||||
value = primitives.pop()
|
||||
for key in composition:
|
||||
if key not in style:
|
||||
style[key] = DEFAULTS[key]
|
||||
|
||||
return style
|
||||
|
||||
def _normalize_text_align(self, cssvalue):
|
||||
style = {}
|
||||
text = cssvalue.cssText
|
||||
if text == 'inherit':
|
||||
style['text-align'] = 'inherit'
|
||||
else:
|
||||
def _apply_text_align(self, text):
|
||||
if text in ('left', 'justify') and self.opts.change_justification in ('left', 'justify'):
|
||||
val = self.opts.change_justification
|
||||
style['text-align'] = val
|
||||
else:
|
||||
style['text-align'] = text
|
||||
return style
|
||||
|
||||
def _normalize_font(self, cssvalue):
|
||||
composition = ('font-style', 'font-variant', 'font-weight',
|
||||
'font-size', 'line-height', 'font-family')
|
||||
style = {}
|
||||
if cssvalue.cssText == 'inherit':
|
||||
for key in composition:
|
||||
style[key] = 'inherit'
|
||||
else:
|
||||
try:
|
||||
primitives = [v.cssText for v in cssvalue]
|
||||
except TypeError:
|
||||
primitives = [cssvalue.cssText]
|
||||
primitives.reverse()
|
||||
value = primitives.pop()
|
||||
for key in composition:
|
||||
if cssprofiles.validate(key, value):
|
||||
style[key] = value
|
||||
if not primitives:
|
||||
break
|
||||
value = primitives.pop()
|
||||
for key in composition:
|
||||
if key not in style:
|
||||
val = ('inherit' if key in {'font-family', 'font-size'}
|
||||
else 'normal')
|
||||
style[key] = val
|
||||
return style
|
||||
text = self.opts.change_justification
|
||||
return text
|
||||
|
||||
def style(self, element):
|
||||
try:
|
||||
|
Loading…
x
Reference in New Issue
Block a user