This commit is contained in:
Kovid Goyal 2024-11-11 08:51:10 +05:30
commit 47886804ae
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 1754 additions and 707 deletions

View File

@ -12,6 +12,8 @@ import sys
import traceback import traceback
from functools import partial from functools import partial
from qt.webengine import QWebEngineView
from qt.core import ( from qt.core import (
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
@ -24,14 +26,17 @@ from qt.core import (
QFontDatabase, QFontDatabase,
QFontInfo, QFontInfo,
QFontMetrics, QFontMetrics,
QHBoxLayout,
QIcon, QIcon,
QLineEdit, QLineEdit,
QPalette, QPalette,
QPushButton,
QSize, QSize,
QSyntaxHighlighter, QSyntaxHighlighter,
Qt, Qt,
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
QTextBrowser,
QTextCharFormat, QTextCharFormat,
QTextOption, QTextOption,
QToolButton, 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.library.coloring import color_row_key, displayable_columns
from calibre.utils.config_base import tweaks from calibre.utils.config_base import tweaks
from calibre.utils.date import DEFAULT_DATE 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 import PythonTemplateContext, StopException
from calibre.utils.formatter_functions import StoredObjectType, formatter_functions from calibre.utils.formatter_functions import StoredObjectType, formatter_functions
from calibre.utils.icu import lower as icu_lower from calibre.utils.icu import lower as icu_lower
@ -364,6 +370,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.setupUi(self) self.setupUi(self)
self.setWindowIcon(self.windowIcon()) self.setWindowIcon(self.windowIcon())
self.docs_dsl = FFMLProcessor()
self.dialog_number = dialog_number self.dialog_number = dialog_number
self.coloring = color_field is not None self.coloring = color_field is not None
self.iconing = icon_field_key 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.textbox.textChanged.connect(self.textbox_changed)
self.set_editor_font() self.set_editor_font()
self.doc_viewer = None
self.current_function_name = None
self.documentation.setReadOnly(True) self.documentation.setReadOnly(True)
self.source_code.setReadOnly(True) self.source_code.setReadOnly(True)
self.doc_button.clicked.connect(self.open_documentation_viewer)
if text is not None: if text is not None:
if text_is_placeholder: if text_is_placeholder:
@ -501,7 +511,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
'<a href="{}">{}</a>'.format( '<a href="{}">{}</a>'.format(
localize_user_manual_link('https://manual.calibre-ebook.com/template_lang.html'), tt)) localize_user_manual_link('https://manual.calibre-ebook.com/template_lang.html'), tt))
tt = _('Template function reference') tt = _('Template function reference')
self.template_func_reference.setText( self.tf_ref.setText(
'<a href="{}">{}</a>'.format( '<a href="{}">{}</a>'.format(
localize_user_manual_link('https://manual.calibre-ebook.com/generated/en/template_ref.html'), tt)) 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 # Now geometry
self.restore_geometry(gprefs, self.geometry_string('template_editor_dialog_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): def geometry_string(self, txt):
if self.dialog_number is None or self.dialog_number == 0: if self.dialog_number is None or self.dialog_number == 0:
return txt return txt
@ -947,12 +1002,15 @@ def evaluate(book, context):
return (_('Stored user defined GPM template') if longform else _('Stored template')) return (_('Stored user defined GPM template') if longform else _('Stored template'))
def function_changed(self, toWhat): 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.source_code.clear()
self.documentation.clear() self.documentation.clear()
self.func_type.clear() self.func_type.clear()
if name in self.all_functions: 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: if name in self.builtins and name in self.builtin_source_dict:
self.source_code.setPlainText(self.builtin_source_dict[name]) self.source_code.setPlainText(self.builtin_source_dict[name])
else: else:
@ -1009,6 +1067,8 @@ def evaluate(book, context):
QDialog.accept(self) QDialog.accept(self)
if self.dialog_number is not None: if self.dialog_number is not None:
self.tester_closed.emit(txt, self.dialog_number) self.tester_closed.emit(txt, self.dialog_number)
if self.doc_viewer is not None:
self.doc_viewer.close()
def reject(self): def reject(self):
self.save_geometry() self.save_geometry()
@ -1024,6 +1084,8 @@ def evaluate(book, context):
break break
if self.dialog_number is not None: if self.dialog_number is not None:
self.tester_closed.emit(None, self.dialog_number) self.tester_closed.emit(None, self.dialog_number)
if self.doc_viewer is not None:
self.doc_viewer.close()
class BreakReporterItem(QTableWidgetItem): class BreakReporterItem(QTableWidgetItem):

View File

@ -660,11 +660,51 @@ you the value as well as all the local variables&lt;/p&gt;</string>
<item row="30" column="0" colspan="3"> <item row="30" column="0" colspan="3">
<layout class="QGridLayout"> <layout class="QGridLayout">
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
<widget class="QLabel" name="label"> <layout class="QHBoxLayout">
<property name="text"> <item>
<string>Template Function Reference</string> <spacer>
</property> <property name="orientation">
</widget> <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>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
@ -700,20 +740,40 @@ you the value as well as all the local variables&lt;/p&gt;</string>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_2"> <layout class="QVBoxLayout" name="lo_34">
<property name="text"> <item>
<string>&amp;Documentation:</string> <widget class="QPushButton" name="doc_button">
</property> <property name="text">
<property name="alignment"> <string>&amp;Documentation:</string>
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> </property>
</property> <property name="toolTip">
<property name="buddy"> <string>Click this button to open the documentation is a separate dialog</string>
<cstring>documentation</cstring> </property>
</property> </widget>
</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>&lt;p&gt;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.&lt;/p&gt; In all the other modes the value parameter
must be supplied.</string>
</property>
</widget>
</item>
</layout>
</item> </item>
<item row="3" column="1"> <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"> <property name="maximumSize">
<size> <size>
<width>16777215</width> <width>16777215</width>
@ -747,20 +807,6 @@ you the value as well as all the local variables&lt;/p&gt;</string>
</item> </item>
</layout> </layout>
</item> </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> </layout>
</item> </item>
</layout> </layout>

View 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('>', '&gt;')
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