1493 lines
41 KiB
Python
1493 lines
41 KiB
Python
'''
|
|
reStructuredText renderer
|
|
=========================
|
|
|
|
.. versionadded:: 1.1.0
|
|
|
|
`reStructuredText <http://docutils.sourceforge.net/rst.html>`_ is an
|
|
easy-to-read, what-you-see-is-what-you-get plaintext markup syntax and parser
|
|
system.
|
|
|
|
.. note::
|
|
|
|
This widget requires the ``docutils`` package to run. Install it with
|
|
``pip`` or include it as one of your deployment requirements.
|
|
|
|
.. warning::
|
|
|
|
This widget is highly experimental. The styling and implementation should
|
|
not be considered stable until this warning has been removed.
|
|
|
|
Usage with Text
|
|
---------------
|
|
|
|
::
|
|
|
|
text = """
|
|
.. _top:
|
|
|
|
Hello world
|
|
===========
|
|
|
|
This is an **emphased text**, some ``interpreted text``.
|
|
And this is a reference to top_::
|
|
|
|
$ print("Hello world")
|
|
|
|
"""
|
|
document = RstDocument(text=text)
|
|
|
|
The rendering will output:
|
|
|
|
.. image:: images/rstdocument.png
|
|
|
|
Usage with Source
|
|
-----------------
|
|
|
|
You can also render a rst file using the :attr:`~RstDocument.source` property::
|
|
|
|
document = RstDocument(source='index.rst')
|
|
|
|
You can reference other documents using the role ``:doc:``. For example, in the
|
|
document ``index.rst`` you can write::
|
|
|
|
Go to my next document: :doc:`moreinfo.rst`
|
|
|
|
It will generate a link that, when clicked, opens the ``moreinfo.rst``
|
|
document.
|
|
|
|
'''
|
|
|
|
__all__ = ('RstDocument', )
|
|
|
|
import os
|
|
from os.path import dirname, join, exists, abspath
|
|
from kivy.clock import Clock
|
|
from kivy.compat import PY2
|
|
from kivy.properties import ObjectProperty, NumericProperty, \
|
|
DictProperty, ListProperty, StringProperty, \
|
|
BooleanProperty, OptionProperty, AliasProperty
|
|
from kivy.lang import Builder
|
|
from kivy.utils import get_hex_from_color, get_color_from_hex
|
|
from kivy.uix.widget import Widget
|
|
from kivy.uix.scrollview import ScrollView
|
|
from kivy.uix.gridlayout import GridLayout
|
|
from kivy.uix.label import Label
|
|
from kivy.uix.image import AsyncImage, Image
|
|
from kivy.uix.videoplayer import VideoPlayer
|
|
from kivy.uix.anchorlayout import AnchorLayout
|
|
from kivy.animation import Animation
|
|
from kivy.logger import Logger
|
|
from docutils.parsers import rst
|
|
from docutils.parsers.rst import roles
|
|
from docutils import nodes, frontend, utils
|
|
from docutils.parsers.rst import Directive, directives
|
|
from docutils.parsers.rst.roles import set_classes
|
|
|
|
|
|
#
|
|
# Handle some additional roles
|
|
#
|
|
if 'KIVY_DOC' not in os.environ:
|
|
|
|
class role_doc(nodes.Inline, nodes.TextElement):
|
|
pass
|
|
|
|
class role_video(nodes.General, nodes.TextElement):
|
|
pass
|
|
|
|
class VideoDirective(Directive):
|
|
has_content = False
|
|
required_arguments = 1
|
|
optional_arguments = 0
|
|
final_argument_whitespace = True
|
|
option_spec = {'width': directives.nonnegative_int,
|
|
'height': directives.nonnegative_int}
|
|
|
|
def run(self):
|
|
set_classes(self.options)
|
|
node = role_video(source=self.arguments[0], **self.options)
|
|
return [node]
|
|
|
|
generic_docroles = {
|
|
'doc': role_doc}
|
|
|
|
for rolename, nodeclass in generic_docroles.items():
|
|
generic = roles.GenericRole(rolename, nodeclass)
|
|
role = roles.CustomRole(rolename, generic, {'classes': [rolename]})
|
|
roles.register_local_role(rolename, role)
|
|
|
|
directives.register_directive('video', VideoDirective)
|
|
|
|
Builder.load_string('''
|
|
#:import parse_color kivy.parser.parse_color
|
|
|
|
|
|
|
|
<RstDocument>:
|
|
content: content
|
|
scatter: scatter
|
|
do_scroll_x: False
|
|
canvas.before:
|
|
Color:
|
|
rgba: parse_color(root.colors['background'])
|
|
Rectangle:
|
|
pos: self.pos
|
|
size: self.size
|
|
|
|
Scatter:
|
|
id: scatter
|
|
size_hint_y: None
|
|
height: content.minimum_height
|
|
width: root.width
|
|
scale: 1
|
|
do_translation: False, False
|
|
do_scale: False
|
|
do_rotation: False
|
|
|
|
GridLayout:
|
|
id: content
|
|
cols: 1
|
|
height: self.minimum_height
|
|
width: root.width
|
|
padding: 10
|
|
|
|
<RstTitle>:
|
|
markup: True
|
|
valign: 'top'
|
|
font_size:
|
|
sp(self.document.base_font_size - self.section * (
|
|
self.document.base_font_size / 31.0 * 2))
|
|
size_hint_y: None
|
|
height: self.texture_size[1] + dp(20)
|
|
text_size: self.width, None
|
|
bold: True
|
|
|
|
canvas:
|
|
Color:
|
|
rgba: parse_color(self.document.underline_color)
|
|
Rectangle:
|
|
pos: self.x, self.y + 5
|
|
size: self.width, 1
|
|
|
|
|
|
<RstParagraph>:
|
|
markup: True
|
|
valign: 'top'
|
|
size_hint_y: None
|
|
height: self.texture_size[1] + self.my
|
|
text_size: self.width - self.mx, None
|
|
font_size: sp(self.document.base_font_size / 2.0)
|
|
|
|
<RstTerm>:
|
|
size_hint: None, None
|
|
height: label.height
|
|
anchor_x: 'left'
|
|
Label:
|
|
id: label
|
|
text: root.text
|
|
markup: True
|
|
valign: 'top'
|
|
size_hint: None, None
|
|
size: self.texture_size[0] + dp(10), self.texture_size[1] + dp(10)
|
|
font_size: sp(root.document.base_font_size / 2.0)
|
|
|
|
<RstBlockQuote>:
|
|
cols: 2
|
|
content: content
|
|
size_hint_y: None
|
|
height: content.height
|
|
Widget:
|
|
size_hint_x: None
|
|
width: 20
|
|
GridLayout:
|
|
id: content
|
|
cols: 1
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstLiteralBlock>:
|
|
cols: 1
|
|
content: content
|
|
size_hint_y: None
|
|
height: content.texture_size[1] + dp(20)
|
|
canvas:
|
|
Color:
|
|
rgb: parse_color('#cccccc')
|
|
Rectangle:
|
|
pos: self.x - 1, self.y - 1
|
|
size: self.width + 2, self.height + 2
|
|
Color:
|
|
rgb: parse_color('#eeeeee')
|
|
Rectangle:
|
|
pos: self.pos
|
|
size: self.size
|
|
Label:
|
|
id: content
|
|
markup: True
|
|
valign: 'top'
|
|
text_size: self.width - 20, None
|
|
font_name: 'data/fonts/RobotoMono-Regular.ttf'
|
|
color: (0, 0, 0, 1)
|
|
|
|
<RstList>:
|
|
cols: 2
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstListItem>:
|
|
cols: 1
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstSystemMessage>:
|
|
cols: 1
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
canvas:
|
|
Color:
|
|
rgba: 1, 0, 0, .3
|
|
Rectangle:
|
|
pos: self.pos
|
|
size: self.size
|
|
|
|
<RstWarning>:
|
|
content: content
|
|
cols: 1
|
|
padding: 20
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
canvas:
|
|
Color:
|
|
rgba: 1, 0, 0, .5
|
|
Rectangle:
|
|
pos: self.x + 10, self.y + 10
|
|
size: self.width - 20, self.height - 20
|
|
GridLayout:
|
|
cols: 1
|
|
id: content
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstNote>:
|
|
content: content
|
|
cols: 1
|
|
padding: 20
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
canvas:
|
|
Color:
|
|
rgba: 0, 1, 0, .5
|
|
Rectangle:
|
|
pos: self.x + 10, self.y + 10
|
|
size: self.width - 20, self.height - 20
|
|
GridLayout:
|
|
cols: 1
|
|
id: content
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstImage>:
|
|
size_hint: None, None
|
|
size: self.texture_size[0], self.texture_size[1] + dp(10)
|
|
|
|
<RstAsyncImage>:
|
|
size_hint: None, None
|
|
size: self.texture_size[0], self.texture_size[1] + dp(10)
|
|
|
|
<RstDefinitionList>:
|
|
cols: 1
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
font_size: sp(self.document.base_font_size / 2.0)
|
|
|
|
<RstDefinition>:
|
|
cols: 2
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
font_size: sp(self.document.base_font_size / 2.0)
|
|
|
|
<RstFieldList>:
|
|
cols: 2
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstFieldName>:
|
|
markup: True
|
|
valign: 'top'
|
|
size_hint: 0.2, 1
|
|
color: (0, 0, 0, 1)
|
|
bold: True
|
|
text_size: self.width - 10, self.height - 10
|
|
valign: 'top'
|
|
font_size: sp(self.document.base_font_size / 2.0)
|
|
|
|
<RstFieldBody>:
|
|
cols: 1
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstFootnote>:
|
|
cols: 2
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstFootName>:
|
|
markup: True
|
|
valign: 'top'
|
|
size_hint: 0.2, 1
|
|
color: (0, 0, 0, 1)
|
|
bold: True
|
|
text_size: self.width - 10, self.height - 10
|
|
valign: 'top'
|
|
font_size: sp(self.document.base_font_size / 2.0)
|
|
|
|
<RstTable>:
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
<RstEntry>:
|
|
cols: 1
|
|
size_hint_y: None
|
|
height: self.minimum_height
|
|
|
|
canvas:
|
|
Color:
|
|
rgb: .2, .2, .2
|
|
Line:
|
|
points: [\
|
|
self.x,\
|
|
self.y,\
|
|
self.right,\
|
|
self.y,\
|
|
self.right,\
|
|
self.top,\
|
|
self.x,\
|
|
self.top,\
|
|
self.x,\
|
|
self.y]
|
|
|
|
<RstTransition>:
|
|
size_hint_y: None
|
|
height: 20
|
|
canvas:
|
|
Color:
|
|
rgb: .2, .2, .2
|
|
Line:
|
|
points: [self.x, self.center_y, self.right, self.center_y]
|
|
|
|
<RstListBullet>:
|
|
markup: True
|
|
valign: 'top'
|
|
size_hint_x: None
|
|
width: self.texture_size[0] + dp(10)
|
|
text_size: None, self.height - dp(10)
|
|
font_size: sp(self.document.base_font_size / 2.0)
|
|
|
|
<RstEmptySpace>:
|
|
size_hint: 0.01, 0.01
|
|
|
|
<RstDefinitionSpace>:
|
|
size_hint: None, 0.1
|
|
width: 50
|
|
font_size: sp(self.document.base_font_size / 2.0)
|
|
|
|
<RstVideoPlayer>:
|
|
options: {'fit_mode': 'contain'}
|
|
canvas.before:
|
|
Color:
|
|
rgba: (1, 1, 1, 1)
|
|
BorderImage:
|
|
source: 'atlas://data/images/defaulttheme/player-background'
|
|
pos: self.x - 25, self.y - 25
|
|
size: self.width + 50, self.height + 50
|
|
border: (25, 25, 25, 25)
|
|
''')
|
|
|
|
|
|
class RstVideoPlayer(VideoPlayer):
|
|
pass
|
|
|
|
|
|
class RstDocument(ScrollView):
|
|
'''Base widget used to store an Rst document. See module documentation for
|
|
more information.
|
|
'''
|
|
source = StringProperty(None)
|
|
'''Filename of the RST document.
|
|
|
|
:attr:`source` is a :class:`~kivy.properties.StringProperty` and
|
|
defaults to None.
|
|
'''
|
|
|
|
source_encoding = StringProperty('utf-8')
|
|
'''Encoding to be used for the :attr:`source` file.
|
|
|
|
:attr:`source_encoding` is a :class:`~kivy.properties.StringProperty` and
|
|
defaults to `utf-8`.
|
|
|
|
.. Note::
|
|
It is your responsibility to ensure that the value provided is a
|
|
valid codec supported by python.
|
|
'''
|
|
|
|
source_error = OptionProperty('strict',
|
|
options=('strict', 'ignore', 'replace',
|
|
'xmlcharrefreplace',
|
|
'backslashreplac'))
|
|
'''Error handling to be used while encoding the :attr:`source` file.
|
|
|
|
:attr:`source_error` is an :class:`~kivy.properties.OptionProperty` and
|
|
defaults to `strict`. Can be one of 'strict', 'ignore', 'replace',
|
|
'xmlcharrefreplace' or 'backslashreplac'.
|
|
'''
|
|
|
|
text = StringProperty(None)
|
|
'''RST markup text of the document.
|
|
|
|
:attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to
|
|
None.
|
|
'''
|
|
|
|
document_root = StringProperty(None)
|
|
'''Root path where :doc: will search for rst documents. If no path is
|
|
given, it will use the directory of the first loaded source file.
|
|
|
|
:attr:`document_root` is a :class:`~kivy.properties.StringProperty` and
|
|
defaults to None.
|
|
'''
|
|
|
|
base_font_size = NumericProperty(31)
|
|
'''Font size for the biggest title, 31 by default. All other font sizes are
|
|
derived from this.
|
|
|
|
.. versionadded:: 1.8.0
|
|
'''
|
|
|
|
show_errors = BooleanProperty(False)
|
|
'''Indicate whether RST parsers errors should be shown on the screen
|
|
or not.
|
|
|
|
:attr:`show_errors` is a :class:`~kivy.properties.BooleanProperty` and
|
|
defaults to False.
|
|
'''
|
|
|
|
def _get_bgc(self):
|
|
return get_color_from_hex(self.colors.background)
|
|
|
|
def _set_bgc(self, value):
|
|
self.colors.background = get_hex_from_color(value)[1:]
|
|
|
|
background_color = AliasProperty(_get_bgc, _set_bgc,
|
|
bind=('colors',),
|
|
cache=True)
|
|
'''Specifies the background_color to be used for the RstDocument.
|
|
|
|
.. versionadded:: 1.8.0
|
|
|
|
:attr:`background_color` is an :class:`~kivy.properties.AliasProperty`
|
|
for colors['background'].
|
|
'''
|
|
|
|
colors = DictProperty({
|
|
'background': 'e5e6e9ff',
|
|
'link': 'ce5c00ff',
|
|
'paragraph': '202020ff',
|
|
'title': '204a87ff',
|
|
'bullet': '000000ff'})
|
|
'''Dictionary of all the colors used in the RST rendering.
|
|
|
|
.. warning::
|
|
|
|
This dictionary is needs special handling. You also need to call
|
|
:meth:`RstDocument.render` if you change them after loading.
|
|
|
|
:attr:`colors` is a :class:`~kivy.properties.DictProperty`.
|
|
'''
|
|
|
|
title = StringProperty('')
|
|
'''Title of the current document.
|
|
|
|
:attr:`title` is a :class:`~kivy.properties.StringProperty` and defaults to
|
|
''. It is read-only.
|
|
'''
|
|
|
|
toctrees = DictProperty({})
|
|
'''Toctree of all loaded or preloaded documents. This dictionary is filled
|
|
when a rst document is explicitly loaded or where :meth:`preload` has been
|
|
called.
|
|
|
|
If the document has no filename, e.g. when the document is loaded from a
|
|
text file, the key will be ''.
|
|
|
|
:attr:`toctrees` is a :class:`~kivy.properties.DictProperty` and defaults
|
|
to {}.
|
|
'''
|
|
|
|
underline_color = StringProperty('204a9699')
|
|
'''underline color of the titles, expressed in html color notation
|
|
|
|
:attr:`underline_color` is a
|
|
:class:`~kivy.properties.StringProperty` and defaults to '204a9699'.
|
|
|
|
.. versionadded: 1.9.0
|
|
'''
|
|
|
|
# internals.
|
|
content = ObjectProperty(None)
|
|
scatter = ObjectProperty(None)
|
|
anchors_widgets = ListProperty([])
|
|
refs_assoc = DictProperty({})
|
|
|
|
def __init__(self, **kwargs):
|
|
self._trigger_load = Clock.create_trigger(self._load_from_text, -1)
|
|
self._parser = rst.Parser()
|
|
self._settings = frontend.OptionParser(
|
|
components=(rst.Parser, )).get_default_values()
|
|
super(RstDocument, self).__init__(**kwargs)
|
|
|
|
def on_source(self, instance, value):
|
|
if not value:
|
|
return
|
|
if self.document_root is None:
|
|
# set the documentation root to the directory name of the
|
|
# first tile
|
|
self.document_root = abspath(dirname(value))
|
|
self._load_from_source()
|
|
|
|
def on_text(self, instance, value):
|
|
self._trigger_load()
|
|
|
|
def render(self):
|
|
'''Force document rendering.
|
|
'''
|
|
self._load_from_text()
|
|
|
|
def resolve_path(self, filename):
|
|
'''Get the path for this filename. If the filename doesn't exist,
|
|
it returns the document_root + filename.
|
|
'''
|
|
if exists(filename):
|
|
return filename
|
|
return join(self.document_root, filename)
|
|
|
|
def preload(self, filename, encoding='utf-8', errors='strict'):
|
|
'''Preload a rst file to get its toctree and its title.
|
|
|
|
The result will be stored in :attr:`toctrees` with the ``filename`` as
|
|
key.
|
|
'''
|
|
|
|
with open(filename, 'rb') as fd:
|
|
text = fd.read().decode(encoding, errors)
|
|
# parse the source
|
|
document = utils.new_document('Document', self._settings)
|
|
self._parser.parse(text, document)
|
|
# fill the current document node
|
|
visitor = _ToctreeVisitor(document)
|
|
document.walkabout(visitor)
|
|
self.toctrees[filename] = visitor.toctree
|
|
return text
|
|
|
|
def _load_from_source(self):
|
|
filename = self.resolve_path(self.source)
|
|
self.text = self.preload(filename,
|
|
self.source_encoding,
|
|
self.source_error)
|
|
|
|
def _load_from_text(self, *largs):
|
|
try:
|
|
# clear the current widgets
|
|
self.content.clear_widgets()
|
|
self.anchors_widgets = []
|
|
self.refs_assoc = {}
|
|
|
|
# parse the source
|
|
document = utils.new_document('Document', self._settings)
|
|
text = self.text
|
|
if PY2 and type(text) is str:
|
|
text = text.decode('utf-8')
|
|
self._parser.parse(text, document)
|
|
|
|
# fill the current document node
|
|
visitor = _Visitor(self, document)
|
|
document.walkabout(visitor)
|
|
|
|
self.title = visitor.title or 'No title'
|
|
except:
|
|
Logger.exception('Rst: error while loading text')
|
|
|
|
def on_ref_press(self, node, ref):
|
|
self.goto(ref)
|
|
|
|
def goto(self, ref, *largs):
|
|
'''Scroll to the reference. If it's not found, nothing will be done.
|
|
|
|
For this text::
|
|
|
|
.. _myref:
|
|
|
|
This is something I always wanted.
|
|
|
|
You can do::
|
|
|
|
from kivy.clock import Clock
|
|
from functools import partial
|
|
|
|
doc = RstDocument(...)
|
|
Clock.schedule_once(partial(doc.goto, 'myref'), 0.1)
|
|
|
|
.. note::
|
|
|
|
It is preferable to delay the call of the goto if you just loaded
|
|
the document because the layout might not be finished or the
|
|
size of the RstDocument has not yet been determined. In
|
|
either case, the calculation of the scrolling would be
|
|
wrong.
|
|
|
|
You can, however, do a direct call if the document is already
|
|
loaded.
|
|
|
|
.. versionadded:: 1.3.0
|
|
'''
|
|
# check if it's a file ?
|
|
if ref.endswith('.rst'):
|
|
# whether it's a valid or invalid file, let source deal with it
|
|
self.source = ref
|
|
return
|
|
|
|
# get the association
|
|
ref = self.refs_assoc.get(ref, ref)
|
|
|
|
# search into all the nodes containing anchors
|
|
ax = ay = None
|
|
for node in self.anchors_widgets:
|
|
if ref in node.anchors:
|
|
ax, ay = node.anchors[ref]
|
|
break
|
|
|
|
# not found, stop here
|
|
if ax is None:
|
|
return
|
|
|
|
# found, calculate the real coordinate
|
|
|
|
# get the anchor coordinate inside widget space
|
|
ax += node.x
|
|
ay = node.top - ay
|
|
# ay += node.y
|
|
|
|
# what's the current coordinate for us?
|
|
sx, sy = self.scatter.x, self.scatter.top
|
|
# ax, ay = self.scatter.to_parent(ax, ay)
|
|
|
|
ay -= self.height
|
|
|
|
dx, dy = self.convert_distance_to_scroll(0, ay)
|
|
dy = max(0, min(1, dy))
|
|
Animation(scroll_y=dy, d=.25, t='in_out_expo').start(self)
|
|
|
|
def add_anchors(self, node):
|
|
self.anchors_widgets.append(node)
|
|
|
|
|
|
class RstTitle(Label):
|
|
|
|
section = NumericProperty(0)
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class RstParagraph(Label):
|
|
|
|
mx = NumericProperty(10)
|
|
|
|
my = NumericProperty(10)
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class RstTerm(AnchorLayout):
|
|
|
|
text = StringProperty('')
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class RstBlockQuote(GridLayout):
|
|
content = ObjectProperty(None)
|
|
|
|
|
|
class RstLiteralBlock(GridLayout):
|
|
content = ObjectProperty(None)
|
|
|
|
|
|
class RstList(GridLayout):
|
|
pass
|
|
|
|
|
|
class RstListItem(GridLayout):
|
|
content = ObjectProperty(None)
|
|
|
|
|
|
class RstListBullet(Label):
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class RstSystemMessage(GridLayout):
|
|
pass
|
|
|
|
|
|
class RstWarning(GridLayout):
|
|
content = ObjectProperty(None)
|
|
|
|
|
|
class RstNote(GridLayout):
|
|
content = ObjectProperty(None)
|
|
|
|
|
|
class RstImage(Image):
|
|
pass
|
|
|
|
|
|
class RstAsyncImage(AsyncImage):
|
|
pass
|
|
|
|
|
|
class RstDefinitionList(GridLayout):
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class RstDefinition(GridLayout):
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class RstFieldList(GridLayout):
|
|
pass
|
|
|
|
|
|
class RstFieldName(Label):
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class RstFieldBody(GridLayout):
|
|
pass
|
|
|
|
|
|
class RstFootnote(GridLayout):
|
|
pass
|
|
|
|
|
|
class RstFootName(Label):
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class RstGridLayout(GridLayout):
|
|
pass
|
|
|
|
|
|
class RstTable(GridLayout):
|
|
pass
|
|
|
|
|
|
class RstEntry(GridLayout):
|
|
pass
|
|
|
|
|
|
class RstTransition(Widget):
|
|
pass
|
|
|
|
|
|
class RstEmptySpace(Widget):
|
|
pass
|
|
|
|
|
|
class RstDefinitionSpace(Widget):
|
|
|
|
document = ObjectProperty(None)
|
|
|
|
|
|
class _ToctreeVisitor(nodes.NodeVisitor):
|
|
|
|
def __init__(self, *largs):
|
|
self.toctree = self.current = []
|
|
self.queue = []
|
|
self.text = ''
|
|
nodes.NodeVisitor.__init__(self, *largs)
|
|
|
|
def push(self, tree):
|
|
self.queue.append(tree)
|
|
self.current = tree
|
|
|
|
def pop(self):
|
|
self.current = self.queue.pop()
|
|
|
|
def dispatch_visit(self, node):
|
|
cls = node.__class__
|
|
if cls is nodes.section:
|
|
section = {
|
|
'ids': node['ids'],
|
|
'names': node['names'],
|
|
'title': '',
|
|
'children': []}
|
|
if isinstance(self.current, dict):
|
|
self.current['children'].append(section)
|
|
else:
|
|
self.current.append(section)
|
|
self.push(section)
|
|
elif cls is nodes.title:
|
|
self.text = ''
|
|
elif cls is nodes.Text:
|
|
self.text += node
|
|
|
|
def dispatch_departure(self, node):
|
|
cls = node.__class__
|
|
if cls is nodes.section:
|
|
self.pop()
|
|
elif cls is nodes.title:
|
|
self.current['title'] = self.text
|
|
|
|
|
|
class _Visitor(nodes.NodeVisitor):
|
|
|
|
def __init__(self, root, *largs):
|
|
self.root = root
|
|
self.title = None
|
|
self.current_list = []
|
|
self.current = None
|
|
self.idx_list = None
|
|
self.text = ''
|
|
self.text_have_anchor = False
|
|
self.section = 0
|
|
self.do_strip_text = False
|
|
self.substitution = {}
|
|
|
|
# store refblock here while building
|
|
self.foot_refblock = None
|
|
|
|
# store order for autonum/sym footnotes+refs
|
|
self.footnotes = {
|
|
'autonum': 0,
|
|
'autosym': 0,
|
|
'autonum_ref': 0,
|
|
'autosym_ref': 0,
|
|
}
|
|
|
|
# last four default chars aren't in our Roboto font,
|
|
# those were replaced with something else
|
|
self.footlist = [
|
|
'\u002A', # asterisk
|
|
'\u2020', # dagger
|
|
'\u2021', # doubledagger
|
|
'\u00A7', # section
|
|
'\u00B6', # pilcrow
|
|
'\u0023', # number
|
|
'\u2206', # cap delta
|
|
'\u220F', # cap pi
|
|
'\u0470', # cap psi
|
|
'\u0466', # cap yus
|
|
]
|
|
nodes.NodeVisitor.__init__(self, *largs)
|
|
|
|
def push(self, widget):
|
|
self.current_list.append(self.current)
|
|
self.current = widget
|
|
|
|
def pop(self):
|
|
self.current = self.current_list.pop()
|
|
|
|
def brute_refs(self, node):
|
|
# get foot/cit refs manually because the output from
|
|
# docutils' parser doesn't contain any of these:
|
|
# node's refid, refname, backref, ... and/or are just ''/[]
|
|
|
|
def get_refs(condition, backref=False):
|
|
# backref=True is used in nodes.footnote
|
|
autonum = autosym = 0
|
|
_nodes = node.traverse(condition=condition, ascend=False)
|
|
|
|
for f in _nodes:
|
|
id = f['ids'][0]
|
|
auto = ''
|
|
if 'auto' in f:
|
|
auto = f['auto']
|
|
|
|
# auto is either 1(int) or '*'
|
|
if auto == 1:
|
|
autonum += 1
|
|
key = 'backref' + str(autonum) if backref else str(autonum)
|
|
self.root.refs_assoc[key] = id
|
|
elif auto == '*':
|
|
sym = self.footlist[
|
|
autosym % 10
|
|
] * (int(autosym / 10) + 1)
|
|
key = 'backref' + sym if backref else sym
|
|
self.root.refs_assoc[key] = id
|
|
autosym += 1
|
|
else:
|
|
if not backref:
|
|
key = f['names'][0]
|
|
if key:
|
|
self.root.refs_assoc[key] = id
|
|
continue
|
|
|
|
key = 'backref' + f['refname'][0]
|
|
|
|
if key in self.root.refs_assoc:
|
|
self.root.refs_assoc[key].append(id)
|
|
else:
|
|
self.root.refs_assoc[key] = [id, ]
|
|
|
|
# these are unique and need to go FIRST
|
|
get_refs(nodes.footnote, backref=False)
|
|
|
|
# autonum & autosym are unique
|
|
get_refs(nodes.footnote_reference, backref=True)
|
|
|
|
def dispatch_visit(self, node):
|
|
cls = node.__class__
|
|
if cls is nodes.document:
|
|
self.push(self.root.content)
|
|
self.brute_refs(node)
|
|
|
|
elif cls is nodes.comment:
|
|
return
|
|
|
|
elif cls is nodes.section:
|
|
self.section += 1
|
|
|
|
elif cls is nodes.substitution_definition:
|
|
name = node.attributes['names'][0]
|
|
self.substitution[name] = node.children[0]
|
|
|
|
elif cls is nodes.substitution_reference:
|
|
node = self.substitution[node.attributes['refname']]
|
|
# it can be e.g. image or something else too!
|
|
if isinstance(node, nodes.Text):
|
|
self.text += node
|
|
|
|
elif cls is nodes.footnote:
|
|
# .. [x] footnote
|
|
text = ''
|
|
foot = RstFootnote()
|
|
ids = node.attributes['ids']
|
|
self.current.add_widget(foot)
|
|
self.push(foot)
|
|
|
|
# check if its autonumbered
|
|
auto = ''
|
|
if 'auto' in node.attributes:
|
|
auto = node.attributes['auto']
|
|
|
|
# auto is either 1(int) or '*'
|
|
if auto == 1:
|
|
self.footnotes['autonum'] += 1
|
|
name = str(self.footnotes['autonum'])
|
|
node_id = node.attributes['ids'][0]
|
|
elif auto == '*':
|
|
autosym = self.footnotes['autosym']
|
|
name = self.footlist[
|
|
autosym % 10
|
|
] * (int(autosym / 10) + 1)
|
|
self.footnotes['autosym'] += 1
|
|
node_id = node.attributes['ids'][0]
|
|
else:
|
|
# can have multiple refs:
|
|
# [8] (1, 2) Footnote ref
|
|
name = node.attributes['names'][0]
|
|
node_id = node['ids'][0]
|
|
|
|
# we can have a footnote without any link or ref
|
|
# .. [1] Empty footnote
|
|
link = self.root.refs_assoc.get(name, '')
|
|
|
|
# handle no refs
|
|
ref = self.root.refs_assoc.get('backref' + name, '')
|
|
|
|
# colorize only with refs
|
|
colorized = self.colorize(name, 'link') if ref else name
|
|
|
|
# has no refs
|
|
if not ref:
|
|
text = '&bl;%s&br;' % (colorized)
|
|
# list of refs
|
|
elif ref and isinstance(ref, list):
|
|
ref_block = [
|
|
'[ref=%s][u]%s[/u][/ref]' % (r, i + 1)
|
|
for i, r in enumerate(ref)
|
|
]
|
|
# [1] ( 1, 2, ...) Footnote
|
|
self.foot_refblock = ''.join([
|
|
'[i]( ', ', '.join(ref_block), ' )[/i]'
|
|
])
|
|
|
|
text = '[anchor=%s]&bl;%s&br;' % (
|
|
node['ids'][0], colorized
|
|
)
|
|
# single ref
|
|
else:
|
|
text = '[anchor=%s][ref=%s]&bl;%s&br;[/ref]' % (
|
|
node['ids'][0], ref, colorized
|
|
)
|
|
|
|
name = RstFootName(
|
|
document=self.root,
|
|
text=text,
|
|
)
|
|
self.current.add_widget(name)
|
|
# give it anchor + event manually
|
|
self.root.add_anchors(name)
|
|
name.bind(on_ref_press=self.root.on_ref_press)
|
|
|
|
elif cls is nodes.footnote_reference:
|
|
self.text += '&bl;'
|
|
text = ''
|
|
name = ''
|
|
|
|
# check if its autonumbered
|
|
auto = ''
|
|
if 'auto' in node.attributes:
|
|
auto = node.attributes['auto']
|
|
|
|
# auto is either 1(int) or '*'
|
|
if auto == 1:
|
|
self.footnotes['autonum_ref'] += 1
|
|
name = str(self.footnotes['autonum_ref'])
|
|
node_id = node.attributes['ids'][0]
|
|
elif auto == '*':
|
|
autosym = self.footnotes['autosym_ref']
|
|
name = self.footlist[
|
|
autosym % 10
|
|
] * (int(autosym / 10) + 1)
|
|
self.footnotes['autosym_ref'] += 1
|
|
node_id = node.attributes['ids'][0]
|
|
else:
|
|
# can have multiple refs:
|
|
# [8] (1, 2) Footnote ref
|
|
name = node.children[0]
|
|
node_id = node['ids'][0]
|
|
text += name
|
|
|
|
refs = self.root.refs_assoc.get(name, '')
|
|
if not refs and auto in (1, '*'):
|
|
# parser should trigger it when checking
|
|
# for backlinks, but we don't have **any** refs
|
|
# to work with, so we have to trigger it manually
|
|
raise Exception(
|
|
'Too many autonumbered or autosymboled '
|
|
'footnote references!'
|
|
)
|
|
|
|
# has a single or no refs ( '' )
|
|
text = '[anchor=%s][ref=%s][color=%s]%s' % (
|
|
node_id, refs,
|
|
self.root.colors.get(
|
|
'link', self.root.colors.get('paragraph')
|
|
),
|
|
text
|
|
)
|
|
self.text += text
|
|
self.text_have_anchor = True
|
|
|
|
elif cls is nodes.title:
|
|
label = RstTitle(section=self.section, document=self.root)
|
|
self.current.add_widget(label)
|
|
self.push(label)
|
|
# assert self.text == ''
|
|
|
|
elif cls is nodes.Text:
|
|
# check if parent isn't a special directive
|
|
if hasattr(node, 'parent'):
|
|
if node.parent.tagname == 'substitution_definition':
|
|
# .. |ref| replace:: something
|
|
return
|
|
elif node.parent.tagname == 'substitution_reference':
|
|
# |ref|
|
|
return
|
|
elif node.parent.tagname == 'comment':
|
|
# .. COMMENT
|
|
return
|
|
elif node.parent.tagname == 'footnote_reference':
|
|
# .. [#]_
|
|
# .. [*]_
|
|
# rewrite it to handle autonum/sym here
|
|
# close tags with departure
|
|
return
|
|
|
|
if self.do_strip_text:
|
|
node = node.replace('\n', ' ')
|
|
node = node.replace(' ', ' ')
|
|
node = node.replace('\t', ' ')
|
|
node = node.replace(' ', ' ')
|
|
if node.startswith(' '):
|
|
node = ' ' + node.lstrip(' ')
|
|
if node.endswith(' '):
|
|
node = node.rstrip(' ') + ' '
|
|
if self.text.endswith(' ') and node.startswith(' '):
|
|
node = node[1:]
|
|
self.text += node
|
|
|
|
elif cls is nodes.paragraph:
|
|
self.do_strip_text = True
|
|
|
|
if isinstance(node.parent, nodes.footnote):
|
|
if self.foot_refblock:
|
|
self.text = self.foot_refblock + ' '
|
|
self.foot_refblock = None
|
|
# self.do_strip_text = False
|
|
|
|
label = RstParagraph(document=self.root)
|
|
if isinstance(self.current, RstEntry):
|
|
label.mx = 10
|
|
self.current.add_widget(label)
|
|
self.push(label)
|
|
|
|
elif cls is nodes.literal_block:
|
|
box = RstLiteralBlock()
|
|
self.current.add_widget(box)
|
|
self.push(box)
|
|
|
|
elif cls is nodes.emphasis:
|
|
self.text += '[i]'
|
|
|
|
elif cls is nodes.strong:
|
|
self.text += '[b]'
|
|
|
|
elif cls is nodes.literal:
|
|
self.text += '[font=fonts/RobotoMono-Regular.ttf]'
|
|
|
|
elif cls is nodes.block_quote:
|
|
box = RstBlockQuote()
|
|
self.current.add_widget(box)
|
|
self.push(box.content)
|
|
assert self.text == ''
|
|
|
|
elif cls is nodes.enumerated_list:
|
|
box = RstList()
|
|
self.current.add_widget(box)
|
|
self.push(box)
|
|
self.idx_list = 0
|
|
|
|
elif cls is nodes.bullet_list:
|
|
box = RstList()
|
|
self.current.add_widget(box)
|
|
self.push(box)
|
|
self.idx_list = None
|
|
|
|
elif cls is nodes.list_item:
|
|
bullet = '-'
|
|
if self.idx_list is not None:
|
|
self.idx_list += 1
|
|
bullet = '%d.' % self.idx_list
|
|
bullet = self.colorize(bullet, 'bullet')
|
|
item = RstListItem()
|
|
self.current.add_widget(RstListBullet(
|
|
text=bullet, document=self.root))
|
|
self.current.add_widget(item)
|
|
self.push(item)
|
|
|
|
elif cls is nodes.system_message:
|
|
label = RstSystemMessage()
|
|
if self.root.show_errors:
|
|
self.current.add_widget(label)
|
|
self.push(label)
|
|
|
|
elif cls is nodes.warning:
|
|
label = RstWarning()
|
|
self.current.add_widget(label)
|
|
self.push(label.content)
|
|
assert self.text == ''
|
|
|
|
elif cls is nodes.note:
|
|
label = RstNote()
|
|
self.current.add_widget(label)
|
|
self.push(label.content)
|
|
assert self.text == ''
|
|
|
|
elif cls is nodes.image:
|
|
# docutils parser breaks path with spaces
|
|
# e.g. "C:/my path" -> "C:/mypath"
|
|
uri = node['uri']
|
|
align = node.get('align', 'center')
|
|
image_size = [
|
|
node.get('width'),
|
|
node.get('height')
|
|
]
|
|
|
|
# use user's size if defined
|
|
def set_size(img, size):
|
|
img.size = [
|
|
size[0] or img.width,
|
|
size[1] or img.height
|
|
]
|
|
|
|
if uri.startswith('/') and self.root.document_root:
|
|
uri = join(self.root.document_root, uri[1:])
|
|
|
|
if uri.startswith('http://') or uri.startswith('https://'):
|
|
image = RstAsyncImage(source=uri)
|
|
image.bind(on_load=lambda *a: set_size(image, image_size))
|
|
else:
|
|
image = RstImage(source=uri)
|
|
set_size(image, image_size)
|
|
|
|
root = AnchorLayout(
|
|
size_hint_y=None,
|
|
anchor_x=align,
|
|
height=image.height
|
|
)
|
|
|
|
image.bind(height=root.setter('height'))
|
|
root.add_widget(image)
|
|
self.current.add_widget(root)
|
|
# TODO:
|
|
# .. _img: <url>
|
|
# .. |img| image:: <img>
|
|
# |img|_ <- needs refs and on_ref_press
|
|
|
|
elif cls is nodes.definition_list:
|
|
lst = RstDefinitionList(document=self.root)
|
|
self.current.add_widget(lst)
|
|
self.push(lst)
|
|
|
|
elif cls is nodes.term:
|
|
assert isinstance(self.current, RstDefinitionList)
|
|
term = RstTerm(document=self.root)
|
|
self.current.add_widget(term)
|
|
self.push(term)
|
|
|
|
elif cls is nodes.definition:
|
|
assert isinstance(self.current, RstDefinitionList)
|
|
definition = RstDefinition(document=self.root)
|
|
definition.add_widget(RstDefinitionSpace(document=self.root))
|
|
self.current.add_widget(definition)
|
|
self.push(definition)
|
|
|
|
elif cls is nodes.field_list:
|
|
fieldlist = RstFieldList()
|
|
self.current.add_widget(fieldlist)
|
|
self.push(fieldlist)
|
|
|
|
elif cls is nodes.field_name:
|
|
name = RstFieldName(document=self.root)
|
|
self.current.add_widget(name)
|
|
self.push(name)
|
|
|
|
elif cls is nodes.field_body:
|
|
body = RstFieldBody()
|
|
self.current.add_widget(body)
|
|
self.push(body)
|
|
|
|
elif cls is nodes.table:
|
|
table = RstTable(cols=0)
|
|
self.current.add_widget(table)
|
|
self.push(table)
|
|
|
|
elif cls is nodes.colspec:
|
|
self.current.cols += 1
|
|
|
|
elif cls is nodes.entry:
|
|
entry = RstEntry()
|
|
self.current.add_widget(entry)
|
|
self.push(entry)
|
|
|
|
elif cls is nodes.transition:
|
|
self.current.add_widget(RstTransition())
|
|
|
|
elif cls is nodes.reference:
|
|
name = node.get('name', node.get('refuri'))
|
|
self.text += '[ref=%s][color=%s]' % (
|
|
name, self.root.colors.get(
|
|
'link', self.root.colors.get('paragraph')))
|
|
if 'refname' in node and 'name' in node:
|
|
self.root.refs_assoc[node['name']] = node['refname']
|
|
|
|
elif cls is nodes.target:
|
|
name = None
|
|
if 'ids' in node:
|
|
name = node['ids'][0]
|
|
elif 'names' in node:
|
|
name = node['names'][0]
|
|
self.text += '[anchor=%s]' % name
|
|
self.text_have_anchor = True
|
|
|
|
elif cls is role_doc:
|
|
self.doc_index = len(self.text)
|
|
|
|
elif cls is role_video:
|
|
pass
|
|
|
|
def dispatch_departure(self, node):
|
|
cls = node.__class__
|
|
if cls is nodes.document:
|
|
self.pop()
|
|
|
|
elif cls is nodes.section:
|
|
self.section -= 1
|
|
|
|
elif cls is nodes.title:
|
|
assert isinstance(self.current, RstTitle)
|
|
if not self.title:
|
|
self.title = self.text
|
|
self.set_text(self.current, 'title')
|
|
self.pop()
|
|
|
|
elif cls is nodes.Text:
|
|
pass
|
|
|
|
elif cls is nodes.paragraph:
|
|
self.do_strip_text = False
|
|
assert isinstance(self.current, RstParagraph)
|
|
self.set_text(self.current, 'paragraph')
|
|
self.pop()
|
|
|
|
elif cls is nodes.literal_block:
|
|
assert isinstance(self.current, RstLiteralBlock)
|
|
self.set_text(self.current.content, 'literal_block')
|
|
self.pop()
|
|
|
|
elif cls is nodes.emphasis:
|
|
self.text += '[/i]'
|
|
|
|
elif cls is nodes.strong:
|
|
self.text += '[/b]'
|
|
|
|
elif cls is nodes.literal:
|
|
self.text += '[/font]'
|
|
|
|
elif cls is nodes.block_quote:
|
|
self.pop()
|
|
|
|
elif cls is nodes.enumerated_list:
|
|
self.idx_list = None
|
|
self.pop()
|
|
|
|
elif cls is nodes.bullet_list:
|
|
self.pop()
|
|
|
|
elif cls is nodes.list_item:
|
|
self.pop()
|
|
|
|
elif cls is nodes.system_message:
|
|
self.pop()
|
|
|
|
elif cls is nodes.warning:
|
|
self.pop()
|
|
|
|
elif cls is nodes.note:
|
|
self.pop()
|
|
|
|
elif cls is nodes.definition_list:
|
|
self.pop()
|
|
|
|
elif cls is nodes.term:
|
|
assert isinstance(self.current, RstTerm)
|
|
self.set_text(self.current, 'term')
|
|
self.pop()
|
|
|
|
elif cls is nodes.definition:
|
|
self.pop()
|
|
|
|
elif cls is nodes.field_list:
|
|
self.pop()
|
|
|
|
elif cls is nodes.field_name:
|
|
assert isinstance(self.current, RstFieldName)
|
|
self.set_text(self.current, 'field_name')
|
|
self.pop()
|
|
|
|
elif cls is nodes.field_body:
|
|
self.pop()
|
|
|
|
elif cls is nodes.table:
|
|
self.pop()
|
|
|
|
elif cls is nodes.colspec:
|
|
pass
|
|
|
|
elif cls is nodes.entry:
|
|
self.pop()
|
|
|
|
elif cls is nodes.reference:
|
|
self.text += '[/color][/ref]'
|
|
|
|
elif cls is nodes.footnote:
|
|
self.pop()
|
|
self.set_text(self.current, 'link')
|
|
|
|
elif cls is nodes.footnote_reference:
|
|
# close opened footnote [x]
|
|
# self.text += '[/ref]'
|
|
# self.set_text(self.current, 'link')
|
|
self.text += '[/color][/ref]'
|
|
# self.text += '[/color][/ref]'
|
|
self.text += '&br;'
|
|
|
|
elif cls is role_doc:
|
|
docname = self.text[self.doc_index:]
|
|
rst_docname = docname
|
|
if rst_docname.endswith('.rst'):
|
|
docname = docname[:-4]
|
|
else:
|
|
rst_docname += '.rst'
|
|
|
|
# try to preload it
|
|
filename = self.root.resolve_path(rst_docname)
|
|
self.root.preload(filename)
|
|
|
|
# if exist, use the title of the first section found in the
|
|
# document
|
|
title = docname
|
|
if filename in self.root.toctrees:
|
|
toctree = self.root.toctrees[filename]
|
|
if len(toctree):
|
|
title = toctree[0]['title']
|
|
|
|
# replace the text with a good reference
|
|
text = '[ref=%s]%s[/ref]' % (
|
|
rst_docname,
|
|
self.colorize(title, 'link'))
|
|
self.text = self.text[:self.doc_index] + text
|
|
|
|
elif cls is role_video:
|
|
width = node['width'] if 'width' in node.attlist() else 400
|
|
height = node['height'] if 'height' in node.attlist() else 300
|
|
uri = node['source']
|
|
if uri.startswith('/') and self.root.document_root:
|
|
uri = join(self.root.document_root, uri[1:])
|
|
video = RstVideoPlayer(
|
|
source=uri,
|
|
size_hint=(None, None),
|
|
size=(width, height))
|
|
anchor = AnchorLayout(size_hint_y=None, height=height + 20)
|
|
anchor.add_widget(video)
|
|
self.current.add_widget(anchor)
|
|
|
|
def set_text(self, node, parent):
|
|
text = self.text
|
|
if parent == 'term' or parent == 'field_name':
|
|
text = '[b]%s[/b]' % text
|
|
# search anchors
|
|
node.text = self.colorize(text, parent)
|
|
node.bind(on_ref_press=self.root.on_ref_press)
|
|
if self.text_have_anchor:
|
|
self.root.add_anchors(node)
|
|
self.text = ''
|
|
self.text_have_anchor = False
|
|
|
|
def colorize(self, text, name):
|
|
return '[color=%s]%s[/color]' % (
|
|
self.root.colors.get(name, self.root.colors['paragraph']),
|
|
text)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
from kivy.base import runTouchApp
|
|
import sys
|
|
runTouchApp(RstDocument(source=sys.argv[1]))
|