mirror of https://github.com/kivy/kivy.git
471 lines
14 KiB
Python
471 lines
14 KiB
Python
'''
|
|
Implement the grammar of Kivy language
|
|
|
|
One level = 4 spaces. No more, no less. Tab not allowed.
|
|
|
|
Example of Kivy files ::
|
|
|
|
#:kivy 1.0
|
|
|
|
ColorScheme:
|
|
id: cs
|
|
backgroundcolor: #927836
|
|
|
|
Layout:
|
|
pos: 100, 100
|
|
size: 867, 567
|
|
|
|
canvas:
|
|
Color:
|
|
rgb: cs.backgroundcolor
|
|
Rectangle:
|
|
pos: self.pos
|
|
size: self.size
|
|
'''
|
|
|
|
__all__ = ('Builder', 'Parser')
|
|
|
|
import re
|
|
from os.path import join
|
|
from copy import copy
|
|
from kivy.factory import Factory
|
|
from kivy.logger import Logger
|
|
from kivy import kivy_data_dir
|
|
|
|
trace = Logger.trace
|
|
|
|
class ParserError(Exception):
|
|
def __init__(self, context, line, message):
|
|
self.filename = context.filename or '<inline>'
|
|
self.line = line
|
|
sourcecode = context.sourcecode
|
|
sc_start = max(0, line - 3)
|
|
sc_stop = min(len(sourcecode), line + 3)
|
|
sc = ['...']
|
|
sc += [' %4d:%s' % x for x in sourcecode[sc_start:line]]
|
|
sc += ['>> %4d:%s' % (line, sourcecode[line][1])]
|
|
sc += [' %4d:%s' % x for x in sourcecode[line+1:sc_stop]]
|
|
sc += ['...']
|
|
sc = '\n'.join(sc)
|
|
|
|
message = 'Parser: File "%s", line %d:\n%s\n%s' % (
|
|
self.filename, self.line, sc, message)
|
|
super(ParserError, self).__init__(message)
|
|
|
|
|
|
class Parser(object):
|
|
'''Create an Parser object to parse a Kivy file or Kivy content.
|
|
'''
|
|
|
|
CLASS_RANGE = range(ord('A'), ord('Z') + 1)
|
|
|
|
def __init__(self, **kwargs):
|
|
super(Parser, self).__init__()
|
|
self.sourcecode = []
|
|
self.objects = []
|
|
content = kwargs.get('content', None)
|
|
self.filename = filename = kwargs.get('filename', None)
|
|
if filename:
|
|
content = self.load_resource(filename)
|
|
if content is None:
|
|
raise ValueError('No content passed. Use filename or '
|
|
'content attribute')
|
|
self.parse(content)
|
|
|
|
|
|
def parse(self, content):
|
|
'''Parse the content of a Parser file, and return a list
|
|
of root objects.
|
|
'''
|
|
# Read and parse the lines of the file
|
|
lines = content.split('\n')
|
|
if not lines:
|
|
return
|
|
lines = zip(range(len(lines)), lines)
|
|
self.sourcecode = lines[:]
|
|
|
|
trace('Parser: parse %d lines' % len(lines))
|
|
|
|
# Ensure the version
|
|
if self.filename:
|
|
self.parse_version(lines[0])
|
|
|
|
# Strip all comments
|
|
self.strip_comments(lines)
|
|
|
|
# Get object from the first level
|
|
objects, lines = self.parse_level(0, lines)
|
|
|
|
if len(lines):
|
|
ln, content = lines[0]
|
|
raise ParserError(self, ln, 'Invalid data (not parsed)')
|
|
|
|
self.objects = objects
|
|
|
|
|
|
def parse_version(self, line):
|
|
'''Parse the version line.
|
|
The version line is always #:kivy <version>
|
|
'''
|
|
|
|
ln, content = line
|
|
|
|
if not content.startswith('#:kivy '):
|
|
raise ParserError(self, ln,
|
|
'Invalid doctype, must start with '
|
|
'#:kivy <version>')
|
|
|
|
version = content[6:].strip()
|
|
if version != '1.0':
|
|
raise ParserError(self, ln, 'Only Kivy 1.0 are supported'
|
|
' (<%s> found)' % version)
|
|
trace('Parser: Kivy version is %s' % version)
|
|
|
|
def strip_comments(self, lines):
|
|
'''Remove all comments from lines inplace.
|
|
'''
|
|
for x in lines[:]:
|
|
if x[1].startswith('#'):
|
|
lines.remove(x)
|
|
if not len(x[1]):
|
|
lines.remove(x)
|
|
|
|
def parse_level(self, level, lines):
|
|
'''Parse the current level (level * 4) indentation
|
|
'''
|
|
indent = 4 * level
|
|
objects = []
|
|
|
|
current_object = None
|
|
current_property = None
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
ln, content = line
|
|
|
|
# Get the number of space
|
|
tmp = content.lstrip(' \t')
|
|
|
|
# Replace any tab with 4 spaces
|
|
tmp = content[:len(content)-len(tmp)]
|
|
tmp = tmp.replace('\t', ' ')
|
|
count = len(tmp)
|
|
|
|
if count % 4 != 0:
|
|
raise ParserError(self, ln,
|
|
'Invalid indentation, must be a multiple of 4')
|
|
content = content.strip()
|
|
|
|
# Level finished
|
|
if count < indent:
|
|
return objects, lines[i-1:]
|
|
|
|
# Current level, create an object
|
|
elif count == indent:
|
|
current_object = {'__line__': ln, '__ctx__': self}
|
|
current_property = None
|
|
x = content.split(':', 2)
|
|
if not len(x[0]):
|
|
raise ParserError(self, ln, 'Identifier missing')
|
|
if len(x) == 2 and len(x[1]):
|
|
raise ParserError(self, ln, 'Invalid data after declaration')
|
|
objects.append((x[0], current_object))
|
|
|
|
# Next level, is it a property or an object ?
|
|
elif count == indent + 4:
|
|
x = content.split(':', 2)
|
|
if not len(x[0]):
|
|
raise ParserError(self, ln, 'Identifier missing')
|
|
|
|
# It's a class, add to the current object as a children
|
|
current_property = None
|
|
name = x[0]
|
|
if ord(name[0]) in Parser.CLASS_RANGE:
|
|
_objects, _lines = self.parse_level(level + 1, lines[i:])
|
|
current_object['children'] = (_objects, ln, self)
|
|
lines = _lines
|
|
i = 0
|
|
|
|
# It's a property
|
|
else:
|
|
if len(x) == 1:
|
|
raise ParserError(self, ln, 'Syntax error')
|
|
value = x[1].strip()
|
|
if len(value):
|
|
current_object[name] = (value, ln, self)
|
|
else:
|
|
current_property = name
|
|
|
|
# Two more level ?
|
|
elif count == indent + 8:
|
|
if current_property not in ('canvas', ):
|
|
raise ParserError(self, ln,
|
|
'Invalid indentation, only allowed '
|
|
'for canvas')
|
|
_objects, _lines = self.parse_level(level + 2, lines[i:])
|
|
current_object[current_property] = (_objects, ln, self)
|
|
current_property = None
|
|
lines = _lines
|
|
i = 0
|
|
|
|
# Too much indent, invalid
|
|
else:
|
|
raise ParserError(self, ln,
|
|
'Invalid indentation (too much level)')
|
|
|
|
# Check the next line
|
|
i += 1
|
|
|
|
return objects, []
|
|
|
|
def load_resource(self, filename):
|
|
'''Load an external resource
|
|
'''
|
|
trace('Parser: load external <%s>' % filename)
|
|
with open(filename, 'r') as fd:
|
|
return fd.read()
|
|
|
|
#
|
|
# Utilities for eval()
|
|
#
|
|
_eval_globals = {}
|
|
def _eval_center(boxsize, center):
|
|
return center[0] - boxsize[0] / 2., center[1] - boxsize[1] / 2.
|
|
_eval_globals['center'] = _eval_center
|
|
|
|
|
|
def create_handler(element, key, value, idmap):
|
|
|
|
# first, remove all the string from the value
|
|
tmp = re.sub('([\'"][^\'"]*[\'"])', '', value)
|
|
|
|
# detect key.value inside value
|
|
kw = re.findall('([a-zA-Z0-9_.]+\.[a-zA-Z0-9_.]+)', tmp)
|
|
if not kw:
|
|
# look like no reference, just pass it
|
|
return eval(value, _eval_globals)
|
|
|
|
# create an handler
|
|
idmap = copy(idmap)
|
|
def call_fn(sender, _value):
|
|
trace('Builder: call_fn %s, key=%s, value=%s' % (element, key, value))
|
|
e_value = eval(value, _eval_globals, idmap)
|
|
trace('Builder: call_fn => value=%s' % str(e_value))
|
|
setattr(element, key, e_value)
|
|
|
|
# bind every key.value
|
|
for x in kw:
|
|
k = x.split('.')
|
|
if len(k) != 2:
|
|
continue
|
|
f = idmap[k[0]]
|
|
f.bind(**{k[1]: call_fn})
|
|
|
|
return eval(value, _eval_globals, idmap)
|
|
|
|
class BuilderRule(object):
|
|
def __init__(self, key):
|
|
self.key = key.lower()
|
|
|
|
def match(self, widget):
|
|
raise NotImplemented()
|
|
|
|
def __repr__(self):
|
|
return '<%s key=%s>' % (self.__class__.__name__, self.key)
|
|
|
|
class BuilderRuleId(BuilderRule):
|
|
def match(self, widget):
|
|
return widget.id.lower() == self.key
|
|
|
|
class BuilderRuleClass(BuilderRule):
|
|
def match(self, widget):
|
|
return self.key in widget.cls
|
|
|
|
class BuilderRuleName(BuilderRule):
|
|
def match(self, widget):
|
|
return widget.__class__.__name__.lower() == self.key
|
|
|
|
class BuilderBase(object):
|
|
'''Kv object are able to load Kv file or string, return the root object of
|
|
the file, and inject rules in the rules database.
|
|
'''
|
|
def __init__(self):
|
|
super(BuilderBase, self).__init__()
|
|
self.rules = []
|
|
self.idmap = {}
|
|
self.idmaps = []
|
|
|
|
def add_rule(self, rule, defs):
|
|
trace('Builder: add rule %s' % str(rule))
|
|
self.rules.append((rule, defs))
|
|
|
|
def load_file(self, filename, **kwargs):
|
|
trace('Builder: load file %s' % filename)
|
|
with open(filename, 'r') as fd:
|
|
return self.load_string(fd.read(), **kwargs)
|
|
|
|
def load_string(self, string, rulesonly=False):
|
|
parser = Parser(content=string)
|
|
root = self.build(parser.objects)
|
|
if rulesonly and root:
|
|
raise Exception('The file <%s> contain also non-rules '
|
|
'directives' % filename)
|
|
return root
|
|
|
|
def match(self, widget):
|
|
'''Return the list of the rules matching the widget
|
|
'''
|
|
matches = []
|
|
for rule, defs in self.rules:
|
|
if rule.match(widget):
|
|
matches.append(defs)
|
|
return matches
|
|
|
|
def apply(self, widget):
|
|
'''Apply all the Kivy rules matching the widget on the widget.
|
|
'''
|
|
matches = self.match(widget)
|
|
trace('Builder: Found %d matches for %s' % (len(matches), widget))
|
|
if not matches:
|
|
return
|
|
self._push_ids()
|
|
have_root = 'root' in self.idmap
|
|
if not have_root:
|
|
self.idmap['root'] = widget
|
|
for defs in matches:
|
|
self.build_item(widget, defs, is_instance=True)
|
|
if not have_root:
|
|
del self.idmap['root']
|
|
self._pop_ids()
|
|
|
|
|
|
#
|
|
# Private
|
|
#
|
|
|
|
def _push_ids(self):
|
|
self.idmaps.append(self.idmap)
|
|
self.idmap = copy(self.idmap)
|
|
|
|
def _pop_ids(self):
|
|
self.idmap = self.idmaps.pop()
|
|
|
|
def build(self, objects):
|
|
root = None
|
|
for item, params in objects:
|
|
if item.startswith('<'):
|
|
self.build_rule(item, params)
|
|
else:
|
|
if root is not None:
|
|
raise ParserError(params['__ctx__'], params['__line__'],
|
|
'Only one root object is allowed')
|
|
root = self.build_item(item, params)
|
|
return root
|
|
|
|
def build_item(self, item, params, is_instance=False):
|
|
self._push_ids()
|
|
|
|
if is_instance is False:
|
|
trace('Builder: build item %s' % item)
|
|
if item.startswith('<'):
|
|
raise ParserError(params['__ctx__'], params['__line__'],
|
|
'Rules are not accepted inside Widget')
|
|
widget = Factory.get(item)()
|
|
else:
|
|
widget = item
|
|
self.idmap['self'] = widget
|
|
|
|
# first loop, do attributes
|
|
for key, value in params.iteritems():
|
|
if key in ('__line__', '__ctx__'):
|
|
continue
|
|
value, ln, ctx = value
|
|
if key == 'children':
|
|
for citem, cparams, in value:
|
|
child = self.build_item(citem, cparams)
|
|
widget.add_widget(child)
|
|
elif key == 'canvas':
|
|
pass
|
|
else:
|
|
try:
|
|
value = create_handler(widget, key, value, self.idmap)
|
|
trace('Builder: set %s=%s for %s' % (key, value, widget))
|
|
setattr(widget, key, value)
|
|
except Exception, e:
|
|
m = ParserError(ctx, ln, str(e))
|
|
print m
|
|
raise
|
|
|
|
# second loop, only for canvas
|
|
for key, value in params.iteritems():
|
|
if key != 'canvas':
|
|
continue
|
|
value, ln, ctx = value
|
|
with widget.canvas:
|
|
self.build_canvas(item, value)
|
|
|
|
self._pop_ids()
|
|
return widget
|
|
|
|
def build_canvas(self, item, elements):
|
|
trace('Builder: build canvas for %s' % item)
|
|
for name, params in elements:
|
|
element = Factory.get(name)()
|
|
for key, value in params.iteritems():
|
|
if key in ('__line__', '__ctx__'):
|
|
continue
|
|
value, ln, ctx = value
|
|
try:
|
|
value = create_handler(element, key, value, self.idmap)
|
|
trace('Builder: set %s=%s for %s' % (key, value, element))
|
|
setattr(element, key, value)
|
|
except Exception, e:
|
|
m = ParserError(ctx, ln, str(e))
|
|
print m.message
|
|
raise
|
|
|
|
def build_rule(self, item, params):
|
|
trace('Builder: build rule for %s' % item)
|
|
if item[0] != '<' or item[-1] != '>':
|
|
raise ParserError(params['__ctx__'], params['__line__'],
|
|
'Invalid rule (must be inside <>)')
|
|
rules = item[1:-1].split(',')
|
|
for rule in rules:
|
|
if not len(rule):
|
|
raise ParserError(params['__ctx__'], params['__line__'],
|
|
'Empty rule detected')
|
|
crule = None
|
|
if rule[0] == '.':
|
|
crule = BuilderRuleClass(rule[1:])
|
|
elif rule[0] == '#':
|
|
crule = BuilderRuleId(rule[1:])
|
|
else:
|
|
crule = BuilderRuleName(rule)
|
|
self.add_rule(crule, params)
|
|
|
|
|
|
#: Rules instance, can be use for asking if we are matching a rule or not
|
|
Builder = BuilderBase()
|
|
Builder.load_file(join(kivy_data_dir, 'style.kv'), rulesonly=True)
|
|
|
|
if __name__ == '__main__':
|
|
content = '''#:Kv 1.0
|
|
|
|
ColorScheme:
|
|
id: cs
|
|
backgroundcolor: #927836
|
|
|
|
Layout:
|
|
pos: 100, 100
|
|
size: 867, 567
|
|
|
|
canvas:
|
|
Color:
|
|
rgb: cs.backgroundcolor
|
|
Rectangle:
|
|
pos: self.pos
|
|
size: self.size
|
|
'''
|
|
|
|
Builder.load(content=content)
|