264 lines
8.7 KiB
Python
264 lines
8.7 KiB
Python
|
# ----------------------------------------------------------------------------
|
||
|
# pyglet
|
||
|
# Copyright (c) 2006-2008 Alex Holkner
|
||
|
# Copyright (c) 2008-2021 pyglet contributors
|
||
|
# All rights reserved.
|
||
|
#
|
||
|
# Redistribution and use in source and binary forms, with or without
|
||
|
# modification, are permitted provided that the following conditions
|
||
|
# are met:
|
||
|
#
|
||
|
# * Redistributions of source code must retain the above copyright
|
||
|
# notice, this list of conditions and the following disclaimer.
|
||
|
# * Redistributions in binary form must reproduce the above copyright
|
||
|
# notice, this list of conditions and the following disclaimer in
|
||
|
# the documentation and/or other materials provided with the
|
||
|
# distribution.
|
||
|
# * Neither the name of pyglet nor the names of its
|
||
|
# contributors may be used to endorse or promote products
|
||
|
# derived from this software without specific prior written
|
||
|
# permission.
|
||
|
#
|
||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||
|
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||
|
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||
|
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||
|
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||
|
# ----------------------------------------------------------------------------
|
||
|
|
||
|
"""Base class for structured (hierarchical) document formats.
|
||
|
"""
|
||
|
|
||
|
import re
|
||
|
|
||
|
import pyglet
|
||
|
|
||
|
|
||
|
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(ImageElement, self).__init__(ascent, descent, self.width)
|
||
|
|
||
|
def place(self, layout, x, y):
|
||
|
group = pyglet.graphics.TextureGroup(self.image.get_texture(), layout.top_group)
|
||
|
x1 = x
|
||
|
y1 = y + self.descent
|
||
|
x2 = x + self.width
|
||
|
y2 = y + self.height + self.descent
|
||
|
vertex_list = layout.batch.add(4, pyglet.gl.GL_QUADS, group,
|
||
|
('v2i', (x1, y1, x2, y1, x2, y2, x1, y2)),
|
||
|
('c3B', (255, 255, 255) * 4),
|
||
|
('t3f', 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(input):
|
||
|
# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81611
|
||
|
if not 0 < input < 4000:
|
||
|
raise ValueError("Argument must be between 1 and 3999")
|
||
|
ints = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
|
||
|
nums = ('M', 'CM', 'D', 'CD','C', 'XC','L','XL','X','IX','V','IV','I')
|
||
|
result = ""
|
||
|
for i in range(len(ints)):
|
||
|
count = int(input // ints[i])
|
||
|
result += nums[i] * count
|
||
|
input -= ints[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, format):
|
||
|
"""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.
|
||
|
`format` : str
|
||
|
Format style, for example ``"1."``.
|
||
|
|
||
|
"""
|
||
|
self.next_value = start
|
||
|
|
||
|
self.prefix, self.numbering, self.suffix = self.format_re.match(format).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 '%s%s%s' % (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 '%s%s%s' % (self.prefix, mark, self.suffix)
|
||
|
else:
|
||
|
return '%s%d%s' % (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
|