266 lines
8.2 KiB
Python
266 lines
8.2 KiB
Python
"""Base class for structured (hierarchical) document formats.
|
|
"""
|
|
|
|
import re
|
|
|
|
import pyglet
|
|
|
|
from pyglet.gl import *
|
|
|
|
|
|
class _InlineElementGroup(pyglet.graphics.Group):
|
|
def __init__(self, texture, program, order=0, parent=None):
|
|
super().__init__(order, parent)
|
|
self.texture = texture
|
|
self.program = program
|
|
|
|
def set_state(self):
|
|
self.program.use()
|
|
|
|
glActiveTexture(GL_TEXTURE0)
|
|
glBindTexture(self.texture.target, self.texture.id)
|
|
|
|
glEnable(GL_BLEND)
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
|
|
|
def unset_state(self):
|
|
glDisable(GL_BLEND)
|
|
self.program.stop()
|
|
|
|
def __eq__(self, other):
|
|
return (self.__class__ is other.__class__ and
|
|
self._order == other.order and
|
|
self.program == other.program and
|
|
self.parent == other.parent and
|
|
self.texture.target == other.texture.target and
|
|
self.texture.id == other.texture.id)
|
|
|
|
def __hash__(self):
|
|
return hash((self._order, self.program, self.parent,
|
|
self.texture.target, self.texture.id))
|
|
|
|
|
|
class ImageElement(pyglet.text.document.InlineElement):
|
|
def __init__(self, image, width=None, height=None):
|
|
self.image = image.get_texture()
|
|
self.width = width is None and image.width or width
|
|
self.height = height is None and image.height or height
|
|
self.vertex_lists = {}
|
|
|
|
anchor_y = self.height // image.height * image.anchor_y
|
|
ascent = max(0, self.height - anchor_y)
|
|
descent = min(0, -anchor_y)
|
|
super().__init__(ascent, descent, self.width)
|
|
|
|
def place(self, layout, x, y, z):
|
|
program = pyglet.text.layout.get_default_image_layout_shader()
|
|
group = _InlineElementGroup(self.image.get_texture(), program, 0, layout.group)
|
|
x1 = x
|
|
y1 = y + self.descent
|
|
x2 = x + self.width
|
|
y2 = y + self.height + self.descent
|
|
vertex_list = program.vertex_list_indexed(4, pyglet.gl.GL_TRIANGLES, [0, 1, 2, 0, 2, 3],
|
|
layout.batch, group,
|
|
position=('f', (x1, y1, z, x2, y1, z, x2, y2, z, x1, y2, z)),
|
|
tex_coords=('f', self.image.tex_coords))
|
|
self.vertex_lists[layout] = vertex_list
|
|
|
|
def remove(self, layout):
|
|
self.vertex_lists[layout].delete()
|
|
del self.vertex_lists[layout]
|
|
|
|
|
|
def _int_to_roman(number):
|
|
# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81611
|
|
if not 0 < number < 4000:
|
|
raise ValueError("Argument must be between 1 and 3999")
|
|
integers = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
|
|
numerals = ('M', 'CM', 'D', 'CD','C', 'XC','L','XL','X','IX','V','IV','I')
|
|
result = ""
|
|
for i in range(len(integers)):
|
|
count = int(number // integers[i])
|
|
result += numerals[i] * count
|
|
number -= integers[i] * count
|
|
return result
|
|
|
|
|
|
class ListBuilder:
|
|
|
|
def begin(self, decoder, style):
|
|
"""Begin a list.
|
|
|
|
:Parameters:
|
|
`decoder` : `StructuredTextDecoder`
|
|
Decoder.
|
|
`style` : dict
|
|
Style dictionary that applies over the entire list.
|
|
|
|
"""
|
|
left_margin = decoder.current_style.get('margin_left') or 0
|
|
tab_stops = decoder.current_style.get('tab_stops')
|
|
if tab_stops:
|
|
tab_stops = list(tab_stops)
|
|
else:
|
|
tab_stops = []
|
|
tab_stops.append(left_margin + 50)
|
|
style['margin_left'] = left_margin + 50
|
|
style['indent'] = -30
|
|
style['tab_stops'] = tab_stops
|
|
|
|
def item(self, decoder, style, value=None):
|
|
"""Begin a list item.
|
|
|
|
:Parameters:
|
|
`decoder` : `StructuredTextDecoder`
|
|
Decoder.
|
|
`style` : dict
|
|
Style dictionary that applies over the list item.
|
|
`value` : str
|
|
Optional value of the list item. The meaning is list-type
|
|
dependent.
|
|
|
|
"""
|
|
mark = self.get_mark(value)
|
|
if mark:
|
|
decoder.add_text(mark)
|
|
decoder.add_text('\t')
|
|
|
|
def get_mark(self, value=None):
|
|
"""Get the mark text for the next list item.
|
|
|
|
:Parameters:
|
|
`value` : str
|
|
Optional value of the list item. The meaning is list-type
|
|
dependent.
|
|
|
|
:rtype: str
|
|
"""
|
|
return ''
|
|
|
|
|
|
class UnorderedListBuilder(ListBuilder):
|
|
|
|
def __init__(self, mark):
|
|
"""Create an unordered list with constant mark text.
|
|
|
|
:Parameters:
|
|
`mark` : str
|
|
Mark to prepend to each list item.
|
|
|
|
"""
|
|
self.mark = mark
|
|
|
|
def get_mark(self, value):
|
|
return self.mark
|
|
|
|
|
|
class OrderedListBuilder(ListBuilder):
|
|
format_re = re.compile('(.*?)([1aAiI])(.*)')
|
|
|
|
def __init__(self, start, fmt):
|
|
"""Create an ordered list with sequentially numbered mark text.
|
|
|
|
The format is composed of an optional prefix text, a numbering
|
|
scheme character followed by suffix text. Valid numbering schemes
|
|
are:
|
|
|
|
``1``
|
|
Decimal Arabic
|
|
``a``
|
|
Lowercase alphanumeric
|
|
``A``
|
|
Uppercase alphanumeric
|
|
``i``
|
|
Lowercase Roman
|
|
``I``
|
|
Uppercase Roman
|
|
|
|
Prefix text may typically be ``(`` or ``[`` and suffix text is
|
|
typically ``.``, ``)`` or empty, but either can be any string.
|
|
|
|
:Parameters:
|
|
`start` : int
|
|
First list item number.
|
|
`fmt` : str
|
|
Format style, for example ``"1."``.
|
|
|
|
"""
|
|
self.next_value = start
|
|
|
|
self.prefix, self.numbering, self.suffix = self.format_re.match(fmt).groups()
|
|
assert self.numbering in '1aAiI'
|
|
|
|
def get_mark(self, value):
|
|
if value is None:
|
|
value = self.next_value
|
|
self.next_value = value + 1
|
|
if self.numbering in 'aA':
|
|
try:
|
|
mark = 'abcdefghijklmnopqrstuvwxyz'[value - 1]
|
|
except ValueError:
|
|
mark = '?'
|
|
if self.numbering == 'A':
|
|
mark = mark.upper()
|
|
return f'{self.prefix}{mark}{self.suffix}'
|
|
elif self.numbering in 'iI':
|
|
try:
|
|
mark = _int_to_roman(value)
|
|
except ValueError:
|
|
mark = '?'
|
|
if self.numbering == 'i':
|
|
mark = mark.lower()
|
|
return f'{self.prefix}{mark}{self.suffix}'
|
|
else:
|
|
return f'{self.prefix}{value}{self.suffix}'
|
|
|
|
|
|
class StructuredTextDecoder(pyglet.text.DocumentDecoder):
|
|
def decode(self, text, location=None):
|
|
self.len_text = 0
|
|
self.current_style = {}
|
|
self.next_style = {}
|
|
self.stack = []
|
|
self.list_stack = []
|
|
self.document = pyglet.text.document.FormattedDocument()
|
|
if location is None:
|
|
location = pyglet.resource.FileLocation('')
|
|
self.decode_structured(text, location)
|
|
return self.document
|
|
|
|
def decode_structured(self, text, location):
|
|
raise NotImplementedError('abstract')
|
|
|
|
def push_style(self, key, styles):
|
|
old_styles = {}
|
|
for name in styles.keys():
|
|
old_styles[name] = self.current_style.get(name)
|
|
self.stack.append((key, old_styles))
|
|
self.current_style.update(styles)
|
|
self.next_style.update(styles)
|
|
|
|
def pop_style(self, key):
|
|
# Don't do anything if key is not in stack
|
|
for match, _ in self.stack:
|
|
if key == match:
|
|
break
|
|
else:
|
|
return
|
|
|
|
# Remove all innermost elements until key is closed.
|
|
while True:
|
|
match, old_styles = self.stack.pop()
|
|
self.next_style.update(old_styles)
|
|
self.current_style.update(old_styles)
|
|
if match == key:
|
|
break
|
|
|
|
def add_text(self, text):
|
|
self.document.insert_text(self.len_text, text, self.next_style)
|
|
self.next_style.clear()
|
|
self.len_text += len(text)
|
|
|
|
def add_element(self, element):
|
|
self.document.insert_element(self.len_text, element, self.next_style)
|
|
self.next_style.clear()
|
|
self.len_text += 1
|