mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
47886804ae
@ -12,6 +12,8 @@ import sys
|
||||
import traceback
|
||||
from functools import partial
|
||||
|
||||
from qt.webengine import QWebEngineView
|
||||
|
||||
from qt.core import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
@ -24,14 +26,17 @@ from qt.core import (
|
||||
QFontDatabase,
|
||||
QFontInfo,
|
||||
QFontMetrics,
|
||||
QHBoxLayout,
|
||||
QIcon,
|
||||
QLineEdit,
|
||||
QPalette,
|
||||
QPushButton,
|
||||
QSize,
|
||||
QSyntaxHighlighter,
|
||||
Qt,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QTextBrowser,
|
||||
QTextCharFormat,
|
||||
QTextOption,
|
||||
QToolButton,
|
||||
@ -48,6 +53,7 @@ from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog
|
||||
from calibre.library.coloring import color_row_key, displayable_columns
|
||||
from calibre.utils.config_base import tweaks
|
||||
from calibre.utils.date import DEFAULT_DATE
|
||||
from calibre.utils.ffml_processor import FFMLProcessor
|
||||
from calibre.utils.formatter import PythonTemplateContext, StopException
|
||||
from calibre.utils.formatter_functions import StoredObjectType, formatter_functions
|
||||
from calibre.utils.icu import lower as icu_lower
|
||||
@ -364,6 +370,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
self.setupUi(self)
|
||||
self.setWindowIcon(self.windowIcon())
|
||||
|
||||
self.docs_dsl = FFMLProcessor()
|
||||
self.dialog_number = dialog_number
|
||||
self.coloring = color_field is not None
|
||||
self.iconing = icon_field_key is not None
|
||||
@ -459,8 +466,11 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
self.textbox.textChanged.connect(self.textbox_changed)
|
||||
self.set_editor_font()
|
||||
|
||||
self.doc_viewer = None
|
||||
self.current_function_name = None
|
||||
self.documentation.setReadOnly(True)
|
||||
self.source_code.setReadOnly(True)
|
||||
self.doc_button.clicked.connect(self.open_documentation_viewer)
|
||||
|
||||
if text is not None:
|
||||
if text_is_placeholder:
|
||||
@ -501,7 +511,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
'<a href="{}">{}</a>'.format(
|
||||
localize_user_manual_link('https://manual.calibre-ebook.com/template_lang.html'), tt))
|
||||
tt = _('Template function reference')
|
||||
self.template_func_reference.setText(
|
||||
self.tf_ref.setText(
|
||||
'<a href="{}">{}</a>'.format(
|
||||
localize_user_manual_link('https://manual.calibre-ebook.com/generated/en/template_ref.html'), tt))
|
||||
|
||||
@ -520,6 +530,51 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
# Now geometry
|
||||
self.restore_geometry(gprefs, self.geometry_string('template_editor_dialog_geometry'))
|
||||
|
||||
def open_documentation_viewer(self):
|
||||
if self.doc_viewer is None:
|
||||
dv = self.doc_viewer = QDialog(self)
|
||||
l = QVBoxLayout()
|
||||
dv.setLayout(l)
|
||||
e = self.doc_viewer_widget = QWebEngineView() #QTextBrowser()
|
||||
# e.setOpenExternalLinks(True)
|
||||
# e.setReadOnly(True)
|
||||
l.addWidget(e)
|
||||
b = QHBoxLayout()
|
||||
b.addStretch(10)
|
||||
pb = QPushButton(_('Show all functions'))
|
||||
pb.setToolTip((_('Shows a list of all built-in functions in alphabetic order')))
|
||||
pb.clicked.connect(self.doc_viewer_show_all)
|
||||
b.addWidget(pb)
|
||||
|
||||
pb = QPushButton(_('Close'))
|
||||
pb.clicked.connect(dv.close)
|
||||
b.addWidget(pb)
|
||||
l.addLayout(b)
|
||||
e.setHtml('')
|
||||
dv.restore_geometry(gprefs, 'template_editor_doc_viewer')
|
||||
dv.finished.connect(self.doc_viewer_finished)
|
||||
dv.show()
|
||||
if self.current_function_name is not None:
|
||||
self.doc_viewer_widget.setHtml(
|
||||
self.docs_dsl.document_to_html(self.all_functions[self.current_function_name].doc,
|
||||
self.current_function_name))
|
||||
|
||||
def doc_viewer_show_all(self):
|
||||
funcs = formatter_functions().get_builtins()
|
||||
result = ''
|
||||
for name in sorted(funcs):
|
||||
result += f'\n<h2>{name}</h2>\n'
|
||||
try:
|
||||
result += self.docs_dsl.document_to_html(funcs[name].doc.strip(), name)
|
||||
except Exception:
|
||||
print('Exception in', name)
|
||||
raise
|
||||
self.doc_viewer_widget.setHtml(result)
|
||||
|
||||
def doc_viewer_finished(self):
|
||||
self.doc_viewer.save_geometry(gprefs, 'template_editor_doc_viewer')
|
||||
self.doc_viewer = None
|
||||
|
||||
def geometry_string(self, txt):
|
||||
if self.dialog_number is None or self.dialog_number == 0:
|
||||
return txt
|
||||
@ -947,12 +1002,15 @@ def evaluate(book, context):
|
||||
return (_('Stored user defined GPM template') if longform else _('Stored template'))
|
||||
|
||||
def function_changed(self, toWhat):
|
||||
name = str(self.function.itemData(toWhat))
|
||||
self.current_function_name = name = str(self.function.itemData(toWhat))
|
||||
self.source_code.clear()
|
||||
self.documentation.clear()
|
||||
self.func_type.clear()
|
||||
if name in self.all_functions:
|
||||
self.documentation.setPlainText(self.all_functions[name].doc)
|
||||
doc = self.all_functions[name].doc.strip()
|
||||
self.documentation.setHtml(self.docs_dsl.document_to_html(doc, name))
|
||||
if self.doc_viewer is not None:
|
||||
self.doc_viewer_widget.setHtml(self.docs_dsl.document_to_html(self.all_functions[name].doc, name))
|
||||
if name in self.builtins and name in self.builtin_source_dict:
|
||||
self.source_code.setPlainText(self.builtin_source_dict[name])
|
||||
else:
|
||||
@ -1009,6 +1067,8 @@ def evaluate(book, context):
|
||||
QDialog.accept(self)
|
||||
if self.dialog_number is not None:
|
||||
self.tester_closed.emit(txt, self.dialog_number)
|
||||
if self.doc_viewer is not None:
|
||||
self.doc_viewer.close()
|
||||
|
||||
def reject(self):
|
||||
self.save_geometry()
|
||||
@ -1024,6 +1084,8 @@ def evaluate(book, context):
|
||||
break
|
||||
if self.dialog_number is not None:
|
||||
self.tester_closed.emit(None, self.dialog_number)
|
||||
if self.doc_viewer is not None:
|
||||
self.doc_viewer.close()
|
||||
|
||||
|
||||
class BreakReporterItem(QTableWidgetItem):
|
||||
|
@ -660,11 +660,51 @@ you the value as well as all the local variables</p></string>
|
||||
<item row="30" column="0" colspan="3">
|
||||
<layout class="QGridLayout">
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Template Function Reference</string>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="tf_ref">
|
||||
<property name="text">
|
||||
<string>Template Function Reference</string>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="template_tutorial">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
@ -700,20 +740,40 @@ you the value as well as all the local variables</p></string>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>&Documentation:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>documentation</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
<layout class="QVBoxLayout" name="lo_34">
|
||||
<item>
|
||||
<widget class="QPushButton" name="doc_button">
|
||||
<property name="text">
|
||||
<string>&Documentation:</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Click this button to open the documentation is a separate dialog</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="some_label">
|
||||
<property name="text">
|
||||
<string>See tooltip for general information</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><p>When using functions in a Single Function Mode template,
|
||||
for example {title:uppercase()}, the first parameter 'value' is omitted. It is automatically replaced
|
||||
by the value of the specified field.</p> In all the other modes the value parameter
|
||||
must be supplied.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QPlainTextEdit" name="documentation">
|
||||
<widget class="QTextBrowser" name="documentation">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
@ -747,20 +807,6 @@ you the value as well as all the local variables</p></string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="27" column="1">
|
||||
<widget class="QLabel" name="template_tutorial">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="28" column="1">
|
||||
<widget class="QLabel" name="template_func_reference">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
516
src/calibre/utils/ffml_processor.py
Normal file
516
src/calibre/utils/ffml_processor.py
Normal file
@ -0,0 +1,516 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from enum import IntEnum
|
||||
from calibre import prepare_string_for_xml
|
||||
|
||||
class NodeKinds(IntEnum):
|
||||
DOCUMENT = -1
|
||||
CODE_TEXT = -2
|
||||
CODE_BLOCK = -3
|
||||
URL = -4
|
||||
BLANK_LINE = -5
|
||||
TEXT = -6
|
||||
LIST = -7
|
||||
END_LIST = -8
|
||||
LIST_ITEM = -9
|
||||
GUI_LABEL = -10
|
||||
ITALIC_TEXT = -11
|
||||
|
||||
|
||||
class Node:
|
||||
|
||||
def __init__(self, node_kind: NodeKinds):
|
||||
self._node_kind = node_kind
|
||||
self._children = []
|
||||
|
||||
def node_kind(self) -> NodeKinds:
|
||||
return self._node_kind
|
||||
|
||||
def add_child(self, node):
|
||||
self._children.append(node)
|
||||
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
def text(self):
|
||||
return self._text
|
||||
|
||||
def escaped_text(self):
|
||||
return prepare_string_for_xml(self._text) #.replace('<', 'LESS_THAN') #.replace('>', '>')
|
||||
|
||||
|
||||
class DocumentNode(Node):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(NodeKinds.DOCUMENT)
|
||||
self._children = []
|
||||
|
||||
|
||||
class TextNode(Node):
|
||||
|
||||
def __init__(self, text):
|
||||
super().__init__(NodeKinds.TEXT)
|
||||
self._text = text
|
||||
|
||||
|
||||
class CodeBlock(Node):
|
||||
|
||||
def __init__(self, code_text):
|
||||
super().__init__(NodeKinds.CODE_BLOCK)
|
||||
self._text = code_text
|
||||
|
||||
|
||||
class CodeText(Node):
|
||||
|
||||
def __init__(self, code_text):
|
||||
super().__init__(NodeKinds.CODE_TEXT)
|
||||
self._text = code_text
|
||||
|
||||
|
||||
class BlankLineNode(Node):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(NodeKinds.BLANK_LINE)
|
||||
|
||||
|
||||
class UrlNode(Node):
|
||||
|
||||
def __init__(self, label, url):
|
||||
super().__init__(NodeKinds.URL)
|
||||
self._label = label
|
||||
self._url = url
|
||||
|
||||
def label(self):
|
||||
return self._label
|
||||
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
|
||||
class ListNode(Node):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(NodeKinds.LIST)
|
||||
|
||||
|
||||
class ListItemNode(Node):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(NodeKinds.LIST_ITEM)
|
||||
|
||||
|
||||
class ItalicTextNode(Node):
|
||||
|
||||
def __init__(self, text):
|
||||
super().__init__(NodeKinds.ITALIC_TEXT)
|
||||
self._text = text
|
||||
|
||||
|
||||
class GuiLabelNode(Node):
|
||||
|
||||
def __init__(self, text):
|
||||
super().__init__(NodeKinds.GUI_LABEL)
|
||||
self._text = text
|
||||
|
||||
|
||||
|
||||
class FFMLProcessor:
|
||||
"""
|
||||
This class is parser for the Formatter Function Markup Language (FFML). It
|
||||
provides output methods for RST and HTML.
|
||||
|
||||
FFML is a basic markup language used to document formatter functions. It is
|
||||
based on a combination of RST used by sphinx and BBCODE used by many
|
||||
bulletin board systems such as MobileRead. It provides a way to specify:
|
||||
|
||||
- inline program code text: surround this text with `` as in ``foo``.
|
||||
|
||||
- italic text: surround this text with `, as in `foo`.
|
||||
|
||||
- text intended to reference a calibre GUI action. This uses RST syntax.
|
||||
Example: :guilabel:`Preferences->Advanced->Template functions`
|
||||
|
||||
- empty lines, indicated by two newlines in a row. A visible empty line
|
||||
in the FFMC will become an empty line in the output.
|
||||
|
||||
- URLs. The syntax is similar to BBCODE: [URL href="http..."]Link text[/URL].
|
||||
Example: [URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL]
|
||||
|
||||
- example program code text blocks. Surround the code block with [CODE]
|
||||
and [/CODE] tags. These tags must be first on a line. Example:
|
||||
[CODE]
|
||||
program:
|
||||
get_note('authors', 'Isaac Asimov', 1)
|
||||
[/CODE]
|
||||
|
||||
- bulleted lists, using BBCODE tags. Surround the list with [LIST] and
|
||||
[/LIST]. List items are indicated with [*]. All of the tags must be
|
||||
first on a line. Bulleted lists can be nested and can contain other FFML
|
||||
elements. Example: a two bullet list containing CODE blocks
|
||||
[LIST]
|
||||
[*]Return the HTML of the note attached to the tag `Fiction`:
|
||||
[CODE]
|
||||
program:
|
||||
get_note('tags', 'Fiction', '')
|
||||
[/CODE]
|
||||
[*]Return the plain text of the note attached to the author `Isaac Asimov`:
|
||||
[CODE]
|
||||
program:
|
||||
get_note('authors', 'Isaac Asimov', 1)
|
||||
[/CODE]
|
||||
[/LIST]
|
||||
|
||||
HTML output contains no CSS and does not start with a tag such as <DIV> or <P>.
|
||||
|
||||
RST output is not indented.
|
||||
|
||||
API example: generate documents for all builtin formatter functions
|
||||
--------------------
|
||||
from calibre.utils.ffml_processor import FFMLProcessor
|
||||
from calibre.utils.formatter_functions import formatter_functions
|
||||
from calibre.db.legacy import LibraryDatabase
|
||||
|
||||
# We need this to load the formatter functions
|
||||
db = LibraryDatabase('<path to some library>')
|
||||
|
||||
ffml = FFMLProcessor()
|
||||
funcs = formatter_functions().get_builtins()
|
||||
|
||||
with open('all_docs.html', 'w') as w:
|
||||
for name in sorted(funcs):
|
||||
w.write(f'\n<h2>{name}</h2>\n')
|
||||
w.write(ffml.document_to_html(funcs[name].doc, name))
|
||||
|
||||
with open('all_docs.rst', 'w') as w:
|
||||
for name in sorted(funcs):
|
||||
w.write(f"\n\n{name}\n{'^'*len(name)}\n\n")
|
||||
w.write(ffml.document_to_rst(funcs[name].doc, name))
|
||||
--------------------
|
||||
"""
|
||||
|
||||
# ====== API ======
|
||||
|
||||
def print_node_tree(self, node, indent=0):
|
||||
"""
|
||||
Pretty print a Formatter Function Markup Language (FFML) parse tree.
|
||||
|
||||
:param node: The root of the tree you want printed.
|
||||
:param indent: The indent level of the tree. The outermost root should
|
||||
have an indent of zero.
|
||||
"""
|
||||
if node.node_kind() in (NodeKinds.TEXT, NodeKinds.CODE_TEXT,
|
||||
NodeKinds.CODE_BLOCK, NodeKinds.ITALIC_TEXT,
|
||||
NodeKinds.GUI_LABEL):
|
||||
print(f'{" " * indent}{node.node_kind().name}:{node.text()}')
|
||||
elif node.node_kind() == NodeKinds.URL:
|
||||
print(f'{" " * indent}URL: label={node.label()}, URL={node.url()}')
|
||||
else:
|
||||
print(f'{" " * indent}{node.node_kind().name}')
|
||||
for n in node.children():
|
||||
self.print_node_tree(n, indent+1)
|
||||
|
||||
def parse_document(self, doc, name):
|
||||
"""
|
||||
Given a Formatter Function Markup Language (FFML) document, return
|
||||
a parse tree for that document.
|
||||
|
||||
:param doc: the document in FFML.
|
||||
:param name: the name of the document, used for generating errors. This
|
||||
is usually the name of the function.
|
||||
|
||||
:return: a parse tree for the document
|
||||
"""
|
||||
self.input = doc
|
||||
self.input_pos = 0
|
||||
self.document_name = name
|
||||
|
||||
node = DocumentNode()
|
||||
return self._parse_document(node)
|
||||
|
||||
def tree_to_html(self, tree, depth=0):
|
||||
"""
|
||||
Given a Formatter Function Markup Language (FFML) parse tree, return
|
||||
a string containing the HTML for that tree.
|
||||
|
||||
:param tree: the parsed FFML.
|
||||
:param depth: the recursion level. This is used for debugging.
|
||||
|
||||
:return: a string containing the HTML text
|
||||
"""
|
||||
result = ''
|
||||
if tree.node_kind() == NodeKinds.TEXT:
|
||||
result += tree.escaped_text()
|
||||
elif tree.node_kind() == NodeKinds.CODE_TEXT:
|
||||
result += f'<code>{tree.escaped_text()}</code>'
|
||||
elif tree.node_kind() == NodeKinds.CODE_BLOCK:
|
||||
result += f'<pre style="margin-left:2em"><code>{tree.escaped_text()}</code></pre>'
|
||||
elif tree.node_kind() == NodeKinds.ITALIC_TEXT:
|
||||
result += f'<i>{tree.escaped_text()}</i>'
|
||||
elif tree.node_kind() == NodeKinds.GUI_LABEL:
|
||||
result += f'<span style="font-family: Sans-Serif">{tree.escaped_text()}</span>'
|
||||
elif tree.node_kind() == NodeKinds.BLANK_LINE:
|
||||
result += '\n<br>\n<br>\n'
|
||||
elif tree.node_kind() == NodeKinds.URL:
|
||||
result += f'<a href="{tree.url()}">{tree.label()}</a>'
|
||||
elif tree.node_kind() == NodeKinds.LIST:
|
||||
result += '\n<ul>\n'
|
||||
for child in tree.children():
|
||||
result += '<li>\n'
|
||||
result += self.tree_to_html(child, depth+1)
|
||||
result += '</li>\n'
|
||||
result += '</ul>\n'
|
||||
elif tree.node_kind() in (NodeKinds.DOCUMENT, NodeKinds.LIST_ITEM):
|
||||
for child in tree.children():
|
||||
result += self.tree_to_html(child, depth+1)
|
||||
return result
|
||||
|
||||
def document_to_html(self, document, name):
|
||||
"""
|
||||
Given a document in the Formatter Function Markup Language (FFML), return
|
||||
that document in HTML format.
|
||||
|
||||
:param document: the text in FFML.
|
||||
:param name: the name of the document, used during error
|
||||
processing. It is usually the name of the function.
|
||||
|
||||
:return: a string containing the HTML
|
||||
|
||||
"""
|
||||
tree = self.parse_document(document, name)
|
||||
return self.tree_to_html(tree, 0)
|
||||
|
||||
def tree_to_rst(self, tree, indent, result=None):
|
||||
"""
|
||||
Given a Formatter Function Markup Language (FFML) parse tree, return
|
||||
a string containing the RST (sphinx reStructuredText) for that tree.
|
||||
|
||||
:param tree: the parsed FFML.
|
||||
:param indent: the indenting level of the items in the tree. This is
|
||||
usually zero, but can be greater than zero if you want
|
||||
the RST output indented.
|
||||
|
||||
:return: a string containing the RST text
|
||||
"""
|
||||
if result is None:
|
||||
result = ' ' * indent
|
||||
if tree.node_kind() == NodeKinds.TEXT:
|
||||
txt = tree.text()
|
||||
if not result:
|
||||
txt = txt.lstrip()
|
||||
elif result.endswith('\n'):
|
||||
txt = txt.lstrip()
|
||||
result += ' ' * indent
|
||||
result += txt
|
||||
elif tree.node_kind() == NodeKinds.CODE_TEXT:
|
||||
result += f'``{tree.text()}``'
|
||||
elif tree.node_kind() == NodeKinds.GUI_LABEL:
|
||||
result += f':guilabel:`{tree.text()}`'
|
||||
elif tree.node_kind() == NodeKinds.CODE_BLOCK:
|
||||
result += f"\n\n{' ' * indent}::\n\n"
|
||||
for line in tree.text().strip().split('\n'):
|
||||
result += f"{' ' * (indent+1)}{line}\n"
|
||||
result += '\n'
|
||||
elif tree.node_kind() == NodeKinds.BLANK_LINE:
|
||||
result += '\n\n'
|
||||
elif tree.node_kind() == NodeKinds.ITALIC_TEXT:
|
||||
result += f'`{tree.text()}`'
|
||||
elif tree.node_kind() == NodeKinds.URL:
|
||||
result += f'`{tree.label()} <{tree.url()}>`_'
|
||||
elif tree.node_kind() == NodeKinds.LIST:
|
||||
result += '\n\n'
|
||||
for child in tree.children():
|
||||
result += f"{' ' * (indent)}* "
|
||||
result = self.tree_to_rst(child, indent+1, result)
|
||||
result += '\n'
|
||||
result += '\n'
|
||||
elif tree.node_kind() in (NodeKinds.DOCUMENT, NodeKinds.LIST_ITEM):
|
||||
for child in tree.children():
|
||||
result = self.tree_to_rst(child, indent, result)
|
||||
return result
|
||||
|
||||
def document_to_rst(self, document, name):
|
||||
"""
|
||||
Given a document in the Formatter Function Markup Language (FFML), return
|
||||
that document in RST (sphinx reStructuredText) format.
|
||||
|
||||
:param document: the text in FFML.
|
||||
:param name: the name of the document, used during error
|
||||
processing. It is usually the name of the function.
|
||||
|
||||
:return: a string containing the RST text
|
||||
|
||||
"""
|
||||
return self.tree_to_rst(self.parse_document(document, name), 0)
|
||||
|
||||
# ============== Internal methods =================
|
||||
|
||||
keywords = {'``': NodeKinds.CODE_TEXT, # must be before '`'
|
||||
'`': NodeKinds.ITALIC_TEXT,
|
||||
':guilabel:': NodeKinds.GUI_LABEL,
|
||||
'[CODE]': NodeKinds.CODE_BLOCK,
|
||||
'[URL': NodeKinds.URL,
|
||||
'[LIST]': NodeKinds.LIST,
|
||||
'[/LIST]': NodeKinds.END_LIST,
|
||||
'[*]': NodeKinds.LIST_ITEM,
|
||||
'\n\n': NodeKinds.BLANK_LINE
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.document = DocumentNode()
|
||||
self.input = None
|
||||
self.input_pos = 0
|
||||
self.input_line = 1
|
||||
|
||||
def error(self, message):
|
||||
raise ValueError(f'{message} on line {self.input_line} in "{self.document_name}"')
|
||||
|
||||
def find(self, for_what):
|
||||
p = self.input.find(for_what, self.input_pos)
|
||||
return -1 if p < 0 else p - self.input_pos
|
||||
|
||||
def move_pos(self, to_where):
|
||||
for c in self.input[self.input_pos:self.input_pos+to_where]:
|
||||
if c == '\n':
|
||||
self.input_line += 1
|
||||
self.input_pos += to_where
|
||||
|
||||
def at_end(self):
|
||||
return self.input_pos >= len(self.input)
|
||||
|
||||
def text_to(self, end):
|
||||
return self.input[self.input_pos:self.input_pos+end]
|
||||
|
||||
def text_contains_newline(self, txt):
|
||||
return '\n' in txt
|
||||
|
||||
def text_to_no_newline(self, end, block_name):
|
||||
txt = self.input[self.input_pos:self.input_pos+end]
|
||||
if self.text_contains_newline(txt):
|
||||
self.error(f'Newline unexpected in {block_name}')
|
||||
return txt
|
||||
|
||||
def startswith(self, txt):
|
||||
return self.input.startswith(txt, self.input_pos)
|
||||
|
||||
def find_one_of(self):
|
||||
positions = []
|
||||
for s in self.keywords:
|
||||
p = self.find(s)
|
||||
if p == 0:
|
||||
return self.keywords[s]
|
||||
positions.append(self.find(s))
|
||||
positions = list(filter(lambda x: x >= 0, positions))
|
||||
if positions:
|
||||
return min(positions)
|
||||
return len(self.input)
|
||||
|
||||
def get_code_text(self):
|
||||
self.move_pos(len('``'))
|
||||
end = self.find('``')
|
||||
if end < 0:
|
||||
self.error(f'Missing closing "``" for CODE_TEXT')
|
||||
node = CodeText(self.text_to(end))
|
||||
self.move_pos(end + len('``'))
|
||||
return node
|
||||
|
||||
def get_italic_text(self):
|
||||
self.move_pos(1)
|
||||
end = self.find('`')
|
||||
if end < 0:
|
||||
self.error(f'Missing closing "`" for italics')
|
||||
node = ItalicTextNode(self.text_to(end))
|
||||
self.move_pos(end + 1)
|
||||
return node
|
||||
|
||||
def get_gui_label(self):
|
||||
self.move_pos(len(':guilabel:`'))
|
||||
end = self.find('`')
|
||||
if end < 0:
|
||||
self.error(f'Missing ` (backquote) for :guilabel:')
|
||||
node = GuiLabelNode(self.text_to_no_newline(end, 'GUI_LABEL (:guilabel:`)'))
|
||||
self.move_pos(end + len('`'))
|
||||
return node
|
||||
|
||||
def get_code_block(self):
|
||||
self.move_pos(len('[CODE]\n'))
|
||||
end = self.find('[/CODE]')
|
||||
if end < 0:
|
||||
self.error(f'Missing [/CODE] for block')
|
||||
node = CodeBlock(self.text_to(end))
|
||||
self.move_pos(end + len('[/CODE]'))
|
||||
if self.text_to(1) == '\n':
|
||||
self.move_pos(1)
|
||||
return node
|
||||
|
||||
def get_list(self):
|
||||
self.move_pos(len('[LIST]\n'))
|
||||
list_node = ListNode()
|
||||
while True:
|
||||
if self.startswith('[/LIST]'):
|
||||
break
|
||||
if not self.startswith('[*]'):
|
||||
self.error(f'Missing [*] in list near text:"{self.text_to(10)}"')
|
||||
self.move_pos(len('[*]'))
|
||||
node = self._parse_document(ListItemNode())
|
||||
list_node.add_child(node)
|
||||
self.move_pos(len('[/LIST]'))
|
||||
if self.text_to(1) == '\n':
|
||||
self.move_pos(1)
|
||||
return list_node
|
||||
|
||||
def get_url(self):
|
||||
self.move_pos(len('[URL'))
|
||||
hp = self.find('href="')
|
||||
if hp < 0:
|
||||
self.error(f'Missing href=" near text {self.text_to(10)}')
|
||||
self.move_pos(hp + len('href="'))
|
||||
close_quote = self.find('"]')
|
||||
if close_quote < 0:
|
||||
self.error(f'Missing closing "> for URL near text:"{self.text_to(10)}"')
|
||||
href = self.text_to_no_newline(close_quote, 'URL href')
|
||||
self.move_pos(close_quote + len('"]'))
|
||||
lp = self.find('[/URL]')
|
||||
if lp < 0:
|
||||
self.error(f'Missing closing [/URL] near text {self.text_to(10)}')
|
||||
label = self.text_to(lp).strip()
|
||||
label = label.replace('\n', ' ')
|
||||
node = UrlNode(label, href)
|
||||
self.move_pos(lp + len('[/URL]'))
|
||||
return node
|
||||
|
||||
def _parse_document(self, parent):
|
||||
while True:
|
||||
p = self.find_one_of()
|
||||
if p > 0:
|
||||
txt = self.text_to(p).replace('\n', ' ')
|
||||
parent.add_child(TextNode(txt))
|
||||
self.move_pos(p)
|
||||
elif p == NodeKinds.CODE_TEXT:
|
||||
parent.add_child(self.get_code_text())
|
||||
elif p == NodeKinds.CODE_BLOCK:
|
||||
parent.add_child(self.get_code_block())
|
||||
elif p == NodeKinds.LIST:
|
||||
parent.add_child(self.get_list())
|
||||
elif p == NodeKinds.LIST_ITEM:
|
||||
return parent
|
||||
elif p == NodeKinds.END_LIST:
|
||||
return parent
|
||||
elif p == NodeKinds.BLANK_LINE:
|
||||
parent.add_child(BlankLineNode())
|
||||
self.move_pos(2)
|
||||
elif p == NodeKinds.ITALIC_TEXT:
|
||||
parent.add_child(self.get_italic_text())
|
||||
elif p == NodeKinds.GUI_LABEL:
|
||||
parent.add_child(self.get_gui_label())
|
||||
elif p == NodeKinds.URL:
|
||||
parent.add_child(self.get_url())
|
||||
else:
|
||||
self.move_pos(p+1)
|
||||
if self.at_end():
|
||||
break
|
||||
return parent
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user