Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Add font properties handling #52

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6feae5a
Add font shorthand parser
goanpeca Jan 2, 2020
c7b7dc5
Add font properties to declaration
goanpeca Jan 2, 2020
e1f1401
Add tests for font parser and properties
goanpeca Jan 2, 2020
ba2cc2f
Fix codestyle
goanpeca Jan 2, 2020
32b9f47
Add font_family list property, fix code review comments
goanpeca Jan 2, 2020
f497c6f
Fix code style
goanpeca Jan 2, 2020
fc14114
Update list property and tests
goanpeca Jan 3, 2020
3fad53c
Fix code style
goanpeca Jan 3, 2020
c4cc7bb
Add initial support to search for fonts on mac with rubicon
goanpeca Jan 3, 2020
4f8c4b9
Update font for linux
goanpeca Jan 3, 2020
925bf17
Update tests
goanpeca Jan 3, 2020
bc376e5
Copy ahem font
goanpeca Jan 4, 2020
71eb789
Add win font query
goanpeca Jan 4, 2020
ba7de67
Refactor code, cache fonts and address code review comments
goanpeca Jan 8, 2020
d496261
Add fontdb and refactor code. Fix code style
goanpeca Jan 8, 2020
d755bc5
Update CI and add font copy initialization
goanpeca Jan 9, 2020
ed23e67
Test CI
goanpeca Jan 9, 2020
dfe6e61
Update font registry on windows
goanpeca Jan 9, 2020
8d063b9
Add more tests
goanpeca Jan 11, 2020
8e40d6d
Fix code style
goanpeca Jan 11, 2020
2fb5815
Fix tests
goanpeca Jan 11, 2020
be72024
Fix tests
goanpeca Jan 11, 2020
91b816d
More tests
goanpeca Jan 11, 2020
1e496c6
Remove print statements
goanpeca Jan 11, 2020
382e758
Update CI config and add exception message for incorrect font shortha…
goanpeca Jan 11, 2020
8b3b6f1
Add font shorthand wrapers and update tests
goanpeca Jan 16, 2020
e4cd59c
Fix code style
goanpeca Jan 16, 2020
dc1710f
Fix error on GTK test
goanpeca Jan 16, 2020
c2bd8cd
Break tests, add pytest-xdist, clean up code
goanpeca Jan 20, 2020
944e53f
Fix tests
goanpeca Jan 20, 2020
94cb398
Merge branch 'master' into enh/add-font-properties
goanpeca Jan 27, 2020
40faa93
Merge branch 'master' into enh/add-font-properties
goanpeca Jan 27, 2020
b754c81
Fix code style
goanpeca Jan 27, 2020
94329c9
Merge with master
goanpeca Apr 27, 2020
d824c86
Fix code style
goanpeca Apr 27, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Copy Ahem font
run: |
mkdir -p ~/.local/share/fonts/
cp tests/fonts/ahem.ttf ~/.local/share/fonts/ahem.ttf
goanpeca marked this conversation as resolved.
Show resolved Hide resolved
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
Expand All @@ -52,6 +56,10 @@ jobs:
python-version: [3.5, 3.6]
steps:
- uses: actions/checkout@v1
- name: Copy Ahem font
run: |
mkdir -p ~/.local/share/fonts/
cp tests/fonts/ahem.ttf ~/.local/share/fonts/ahem.ttf
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
Expand Down
166 changes: 160 additions & 6 deletions colosseum/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from .validators import (is_color, is_integer, is_length, is_number,
is_percentage, ValidationError)
import os
import sys

from .validators import (ValidationError, is_color, is_font_family,
is_integer, is_length,
is_number, is_percentage)


class Choices:
Expand Down Expand Up @@ -34,15 +38,15 @@ def validate(self, value):
raise ValueError()

def __str__(self):
choices = set([str(c).lower().replace('_', '-') for c in self.constants])
choices = [str(c).lower().replace('_', '-') for c in self.constants]
for validator in self.validators:
choices.add(validator.description)
choices += validator.description.split(', ')
goanpeca marked this conversation as resolved.
Show resolved Hide resolved

if self.explicit_defaulting_constants:
for item in self.explicit_defaulting_constants:
choices.add(item)
choices.append(item)

return ", ".join(sorted(choices))
return ", ".join(sorted(set(choices)))


######################################################################
Expand Down Expand Up @@ -235,6 +239,8 @@ def __str__(self):
# 10.8 Leading and half-leading
######################################################################
# line_height
LINE_HEIGHT_CHOICES = Choices(NORMAL, validators=[is_number, is_length, is_percentage],
explicit_defaulting_constants=[INHERIT])
# vertical_align

######################################################################
Expand Down Expand Up @@ -317,26 +323,174 @@ def __str__(self):
######################################################################
# font_family

SERIF = 'serif'
SANS_SERIF = 'sans-serif'
CURSIVE = 'cursive'
FANTASY = 'fantasy'
MONOSPACE = 'monospace'

GENERIC_FAMILY_FONTS = [SERIF, SANS_SERIF, CURSIVE, FANTASY, MONOSPACE]


def available_font_families():
"""List available font family names."""
if sys.platform == 'darwin':
return _available_font_families_mac()
elif sys.platform.startswith('linux'):
return _available_font_families_unix()
elif os.name == 'nt':
return _available_font_families_win()
goanpeca marked this conversation as resolved.
Show resolved Hide resolved

return []


def _available_font_families_mac():
"""List available font family names on mac."""
from ctypes import cdll, util
from rubicon.objc import ObjCClass
appkit = cdll.LoadLibrary(util.find_library('AppKit')) # noqa
NSFontManager = ObjCClass("NSFontManager")
NSFontManager.declare_class_property('sharedFontManager')
NSFontManager.declare_class_property("sharedFontManager")
goanpeca marked this conversation as resolved.
Show resolved Hide resolved
NSFontManager.declare_property("availableFontFamilies")
manager = NSFontManager.sharedFontManager
return list(sorted(str(item) for item in manager.availableFontFamilies if item))


def _available_font_families_unix():
"""List available font family names on unix."""
import subprocess
p = subprocess.check_output(['fc-list', ':', 'family'])
goanpeca marked this conversation as resolved.
Show resolved Hide resolved
fonts = p.decode().split('\n')
return list(sorted(set(item for item in fonts if item)))


def _available_font_families_win():
"""List available font family names on windows."""
import winreg
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
r"Software\Microsoft\Windows NT\CurrentVersion\Fonts",
0,
winreg.KEY_READ)
fonts = set()
for idx in range(0, winreg.QueryInfoKey(key)[1]):
font_name = winreg.EnumValue(key, idx)[0]
font_name = font_name.replace(' (TrueType)', '')
fonts.add(font_name)
return list(sorted(fonts))


AVAILABLE_FONT_FAMILIES = available_font_families()
FONT_FAMILY_CHOICES = Choices(
validators=[is_font_family(generic_family=GENERIC_FAMILY_FONTS, font_families=AVAILABLE_FONT_FAMILIES)],
goanpeca marked this conversation as resolved.
Show resolved Hide resolved
explicit_defaulting_constants=[INHERIT, INITIAL],
)

######################################################################
# 15.4 Font Styling
######################################################################
# font_style
NORMAL = 'normal'
ITALIC = 'italic'
OBLIQUE = 'oblique'

FONT_STYLE_CHOICES = Choices(
NORMAL,
ITALIC,
OBLIQUE,
explicit_defaulting_constants=[INHERIT],
)

######################################################################
# 15.5 Small-caps
######################################################################
# font_variant
SMALL_CAPS = 'small-caps'
FONT_VARIANT_CHOICES = Choices(
NORMAL,
SMALL_CAPS,
explicit_defaulting_constants=[INHERIT],
)

######################################################################
# 15.6 Font boldness
######################################################################
# font_weight
BOLD = 'bold'
BOLDER = 'bolder'
LIGHTER = 'lighter'

FONT_WEIGHT_CHOICES = Choices(
NORMAL,
BOLD,
BOLDER,
LIGHTER,
100,
200,
300,
400,
500,
600,
700,
800,
900,
explicit_defaulting_constants=[INHERIT],
)

######################################################################
# 15.7 Font size
######################################################################
# font_size

# <absolute-size>
XX_SMALL = 'xx-small'
X_SMALL = 'x-small'
SMALL = 'small'
MEDIUM = 'medium'
LARGE = 'large'
X_LARGE = 'x-large'
XX_LARGE = 'xx-large'

# <relative-size>
LARGER = 'larger'
SMALLER = 'smaller'

FONT_SIZE_CHOICES = Choices(
XX_SMALL,
X_SMALL,
SMALL,
MEDIUM,
LARGE,
X_LARGE,
XX_LARGE,
LARGER,
SMALLER,
validators=[is_length, is_percentage],
explicit_defaulting_constants=[INHERIT],
)

######################################################################
# 15.8 Font shorthand
######################################################################

ICON = 'icon'
CAPTION = 'caption'
MENU = 'menu'
MESSAGE_BOX = 'message-box'
SMALL_CAPTION = 'small-caption'
STATUS_BAR = 'status-bar'

SYSTEM_FONT_KEYWORDS = [ICON, CAPTION, MENU, MESSAGE_BOX, SMALL_CAPTION, STATUS_BAR]

INITIAL_FONT_VALUES = {
'font_style': NORMAL,
'font_variant': NORMAL,
'font_weight': NORMAL,
'font_size': MEDIUM,
'line_height': NORMAL,
goanpeca marked this conversation as resolved.
Show resolved Hide resolved
'font_family': [INITIAL], # TODO: Depends on user agent. What to use?
}

######################################################################
# 16. Text ###########################################################
######################################################################
Expand Down
110 changes: 96 additions & 14 deletions colosseum/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,80 @@
MIN_SIZE_CHOICES, NORMAL, NOWRAP, ORDER_CHOICES, PADDING_CHOICES,
POSITION_CHOICES, ROW, SIZE_CHOICES, STATIC, STRETCH,
TRANSPARENT, UNICODE_BIDI_CHOICES, VISIBILITY_CHOICES, VISIBLE,
Z_INDEX_CHOICES, default,
Z_INDEX_CHOICES, default, FONT_STYLE_CHOICES, FONT_VARIANT_CHOICES,
FONT_WEIGHT_CHOICES, FONT_SIZE_CHOICES, MEDIUM, FONT_FAMILY_CHOICES, INITIAL,
INITIAL_FONT_VALUES, LINE_HEIGHT_CHOICES,
)
from .fonts import construct_font_property, parse_font_property


_CSS_PROPERTIES = set()


def unvalidated_property(name, choices, initial):
"Define a simple CSS property attribute."
initial = choices.validate(initial)
def validated_font_property(name, initial):
"""Define the shorthand CSS font property."""
assert isinstance(initial, dict)
initial = initial.copy()

def getter(self):
return getattr(self, '_%s' % name, initial)
font = initial
goanpeca marked this conversation as resolved.
Show resolved Hide resolved
for property_name in font:
font[property_name] = getattr(self, property_name)
return getattr(self, '_%s' % name, construct_font_property(font))

def setter(self, value):
if value != getattr(self, '_%s' % name, initial):
setattr(self, '_%s' % name, value)
font = parse_font_property(value)
for property_name, property_value in font.items():
setattr(self, property_name, property_value)
self.dirty = True
setattr(self, '_%s' % name, value)

def deleter(self):
try:
delattr(self, '_%s' % name)
self.dirty = True
except AttributeError:
# Attribute doesn't exist
pass

for property_name in INITIAL_FONT_VALUES:
try:
delattr(self, property_name)
self.dirty = True
except AttributeError:
# Attribute doesn't exist
pass

_CSS_PROPERTIES.add(name)
return property(getter, setter, deleter)


def validated_list_property(name, choices, initial, separator=','):
"""Define a property holding a list values."""
if not isinstance(initial, list):
raise ValueError('Initial value must be a list!')

def getter(self):
# TODO: A copy is returned so if the user mutates it,
# it will not affect the stored value
return getattr(self, '_%s' % name, initial)[:]

def setter(self, value):
try:
if not isinstance(value, str):
value = separator.join(value)

# This should be a list of values
values = choices.validate(value)
if not isinstance(values, list):
values.split(separator)
except ValueError:
raise ValueError("Invalid value '%s' for CSS property '%s'; Valid values are: %s" % (
value, name, choices
))
goanpeca marked this conversation as resolved.
Show resolved Hide resolved

if values != getattr(self, '_%s' % name, initial):
setattr(self, '_%s' % name, values[:])
self.dirty = True

def deleter(self):
Expand Down Expand Up @@ -127,6 +185,30 @@ def deleter(self):
return property(getter, setter, deleter)


def unvalidated_property(name, choices, initial):
"Define a simple CSS property attribute."
initial = choices.validate(initial)

def getter(self):
return getattr(self, '_%s' % name, initial)

def setter(self, value):
if value != getattr(self, '_%s' % name, initial):
setattr(self, '_%s' % name, value)
self.dirty = True

def deleter(self):
try:
delattr(self, '_%s' % name)
self.dirty = True
except AttributeError:
# Attribute doesn't exist
pass

_CSS_PROPERTIES.add(name)
return property(getter, setter, deleter)


class CSS:
def __init__(self, **style):
self._node = None
Expand Down Expand Up @@ -222,7 +304,7 @@ def __init__(self, **style):
max_height = validated_property('max_height', choices=MAX_SIZE_CHOICES, initial=None)

# 10.8 Leading and half-leading
# line_height
line_height = validated_property('line_height', choices=LINE_HEIGHT_CHOICES, initial=NORMAL)
# vertical_align

# 11. Visual effects #################################################
Expand Down Expand Up @@ -276,22 +358,22 @@ def __init__(self, **style):

# 15. Fonts ##########################################################
# 15.3 Font family
# font_family
font_family = validated_list_property('font_family', choices=FONT_FAMILY_CHOICES, initial=[INITIAL])

# 15.4 Font Styling
# font_style
font_style = validated_property('font_style', choices=FONT_STYLE_CHOICES, initial=NORMAL)

# 15.5 Small-caps
# font_variant
font_variant = validated_property('font_variant', choices=FONT_VARIANT_CHOICES, initial=NORMAL)

# 15.6 Font boldness
# font_weight
font_weight = validated_property('font_weight', choices=FONT_WEIGHT_CHOICES, initial=NORMAL)

# 15.7 Font size
# font_size
font_size = validated_property('font_size', choices=FONT_SIZE_CHOICES, initial=MEDIUM)

# 15.8 Shorthand font property
# font
font = validated_font_property('font', initial=INITIAL_FONT_VALUES)

# 16. Text ###########################################################
# 16.1 Indentation
Expand Down
Loading