This commit is contained in:
Kovid Goyal 2024-11-21 07:48:49 +05:30
commit 681c50ed08
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 445 additions and 291 deletions

View File

@ -174,53 +174,53 @@ Notes on calling functions in Single Function Mode:
Functions are documented in :ref:`template_functions_reference`. The documentation tells you what arguments the functions require and what the functions do. For example, here is the documentation of the :ref:`ff_ifempty` function. Functions are documented in :ref:`template_functions_reference`. The documentation tells you what arguments the functions require and what the functions do. For example, here is the documentation of the :ref:`ff_ifempty` function.
:ffdoc:`ifempty` * :ffdoc:`ifempty`
You see that the function requires two arguments, ``value`` and ``text_if_empty``. However, because we are using Single Function Mode, we omit the ``value`` argument, passing only ``text_if_empty``. For example, this template:: You see that the function requires two arguments, ``value`` and ``text_if_empty``. However, because we are using Single Function Mode, we omit the ``value`` argument, passing only ``text_if_empty``. For example, this template::
{tags:ifempty(No tags on this book)} {tags:ifempty(No tags on this book)}
shows the tags for a book if it has any. If it has no tags then it show `No tags on this book`. shows the tags for a book, if any. If it has no tags then it show `No tags on this book`.
The following functions are usable in Single Function Mode because their first parameter is ``value``. The following functions are usable in Single Function Mode because their first parameter is ``value``.
* :ref:`ff_capitalize` * :ffsum:`ff_capitalize`
* :ref:`ff_ceiling` * :ffsum:`ff_ceiling`
* :ref:`ff_cmp` * :ffsum:`ff_cmp`
* :ref:`ff_contains` * :ffsum:`ff_contains`
* :ref:`ff_date_arithmetic` * :ffsum:`ff_date_arithmetic`
* :ref:`ff_floor` * :ffsum:`ff_floor`
* :ref:`ff_format_date` * :ffsum:`ff_format_date`
* :ref:`ff_format_number` * :ffsum:`ff_format_number`
* :ref:`ff_fractional_part` * :ffsum:`ff_fractional_part`
* :ref:`ff_human_readable` * :ffsum:`ff_human_readable`
* :ref:`ff_ifempty` * :ffsum:`ff_ifempty`
* :ref:`ff_language_strings` * :ffsum:`ff_language_strings`
* :ref:`ff_list_contains` * :ffsum:`ff_list_contains`
* :ref:`ff_list_count` * :ffsum:`ff_list_count`
* :ref:`ff_list_count_matching` * :ffsum:`ff_list_count_matching`
* :ref:`ff_list_item` * :ffsum:`ff_list_item`
* :ref:`ff_list_sort` * :ffsum:`ff_list_sort`
* :ref:`ff_lookup` * :ffsum:`ff_lookup`
* :ref:`ff_lowercase` * :ffsum:`ff_lowercase`
* :ref:`ff_mod` * :ffsum:`ff_mod`
* :ref:`ff_rating_to_stars` * :ffsum:`ff_rating_to_stars`
* :ref:`ff_re` * :ffsum:`ff_re`
* :ref:`ff_re_group` * :ffsum:`ff_re_group`
* :ref:`ff_round` * :ffsum:`ff_round`
* :ref:`ff_select` * :ffsum:`ff_select`
* :ref:`ff_shorten` * :ffsum:`ff_shorten`
* :ref:`ff_str_in_list` * :ffsum:`ff_str_in_list`
* :ref:`ff_subitems` * :ffsum:`ff_subitems`
* :ref:`ff_sublist` * :ffsum:`ff_sublist`
* :ref:`ff_substr` * :ffsum:`ff_substr`
* :ref:`ff_swap_around_articles` * :ffsum:`ff_swap_around_articles`
* :ref:`ff_swap_around_comma` * :ffsum:`ff_swap_around_comma`
* :ref:`ff_switch` * :ffsum:`ff_switch`
* :ref:`ff_test` * :ffsum:`ff_test`
* :ref:`ff_titlecase` * :ffsum:`ff_titlecase`
* :ref:`ff_transliterate` * :ffsum:`ff_transliterate`
* :ref:`ff_uppercase` * :ffsum:`ff_uppercase`
**Using functions and formatting in the same template** **Using functions and formatting in the same template**
@ -405,7 +405,7 @@ Example: This template computes an approximate duration in years, months, and da
**Relational operators** **Relational operators**
Relational operators return ``'1'`` if the comparison is true, otherwise the empty string (''). Relational operators return ``'1'`` if the comparison is true, otherwise the empty string (``''``).
There are two forms of relational operators: string comparisons and numeric comparisons. There are two forms of relational operators: string comparisons and numeric comparisons.
@ -418,17 +418,16 @@ The numeric comparison operators are ``==#``, ``!=#``, ``<#``, ``<=#``, ``>#``,
Examples: Examples:
* ``program: field('series') == 'foo'`` returns ``'1'`` if the book's series is 'foo', otherwise ``''``. * ``program: field('series') == 'foo'`` returns ``'1'`` if the book's series is `foo`, otherwise ``''``.
* ``program: 'f.o' in field('series')`` returns ``'1'`` if the book's series matches the regular expression ``f.o`` (e.g., `foo`, `Off Onyx`, etc.), otherwise ``''``. * ``program: 'f.o' in field('series')`` returns ``'1'`` if the book's series matches the regular expression ``f.o`` (e.g., `foo`, `Off Onyx`, etc.), otherwise ``''``.
* ``program: 'science' inlist $#genre`` returns ``'1'`` if any of the values retrieved from the book's genres match the regular expression ``science``, e.g., `Science`, `History of Science`, `Science Fiction` etc., otherwise ``''``. * ``program: 'science' inlist $#genre`` returns ``'1'`` if any of the values retrieved from the book's genres match the regular expression ``science``, e.g., `Science`, `History of Science`, `Science Fiction` etc., otherwise ``''``.
* ``program: '^science$' inlist $#genre`` returns ``'1'`` if any of the book's genres exactly match the regular expression ``^science$``, e.g., `Science`, otherwise ``''``. The genres `History of Science` and `Science Fiction` don't match. * ``program: '^science$' inlist $#genre`` returns ``'1'`` if any of the book's genres exactly match the regular expression ``^science$``, e.g., `Science`, otherwise ``''``. The genres `History of Science` and `Science Fiction` don't match.
* ``program: 'asimov' inlist $authors`` returns ``'1'`` if any author matches the regular expression ``asimov``, e.g., `Asimov, Isaac` or `Isaac Asimov`, otherwise ``''``. * ``program: 'asimov' inlist $authors`` returns ``'1'`` if any author matches the regular expression ``asimov``, e.g., `Asimov, Isaac` or `Isaac Asimov`, otherwise ``''``.
* ``program: 'asimov' inlist_field 'authors'`` returns ``'1'`` if any author matches the regular expression ``asimov``, e.g., `Asimov, Isaac` or `Isaac Asimov`, otherwise ``''``. * ``program: 'asimov' inlist_field 'authors'`` returns ``'1'`` if any author matches the regular expression ``asimov``, e.g., `Asimov, Isaac` or `Isaac Asimov`, otherwise ``''``.
* ``program: 'asimov$' inlist_field 'authors'`` returns ``'1'`` if any author matches the regular expression ``asimov$``, e.g., `Isaac Asimov`, otherwise ``''``. It doesn't match `Asimov, Isaac` because of the ``$`` anchor in the regular expression. * ``program: 'asimov$' inlist_field 'authors'`` returns ``'1'`` if any author matches the regular expression ``asimov$``, e.g., `Isaac Asimov`, otherwise ``''``. It doesn't match `Asimov, Isaac` because of the ``$`` anchor in the regular expression.
* ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns ``'bar'`` if the book's series is not ``foo``. Otherwise it returns ``'mumble'``. * ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns ``'bar'`` if the book's series is not `foo`. Otherwise it returns ``'mumble'``.
* ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns ``'bar'`` if the book's series is not ``foo``. Otherwise it returns ``'mumble'``. * ``program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi`` returns ``'yes'`` if series is either `foo` or `1632`, otherwise ``'no'``.
* ``program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi`` returns ``'yes'`` if series is either ``'foo'`` or ``'1632'``, otherwise ``'no'``. * ``program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi`` returns ``'yes'`` if series is either `foo` or `1632`, otherwise ``'no'``.
* ``program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi`` returns ``'yes'`` if series is either ``'foo'`` or ``'1632'``, otherwise ``'no'``.
* ``program: if 11 > 2 then 'yes' else 'no' fi`` returns ``'no'`` because the ``>`` operator does a lexical comparison. * ``program: if 11 > 2 then 'yes' else 'no' fi`` returns ``'no'`` because the ``>`` operator does a lexical comparison.
* ``program: if 11 ># 2 then 'yes' else 'no' fi`` returns ``'yes'`` because the ``>#`` operator does a numeric comparison. * ``program: if 11 ># 2 then 'yes' else 'no' fi`` returns ``'yes'`` because the ``>#`` operator does a numeric comparison.
@ -452,13 +451,18 @@ More complex programs in template expressions - Template Program Mode
Example: assume you want a template to show the series for a book if it has one, otherwise show Example: assume you want a template to show the series for a book if it has one, otherwise show
the value of a custom field #genre. You cannot do this in the :ref:`Single Function Mode <single_mode>` because you cannot make reference to another metadata field within a template expression. In `TPM` you can, as the following expression demonstrates:: the value of a custom field #genre. You cannot do this in the :ref:`Single Function Mode <single_mode>` because you cannot make reference to another metadata field within a template expression. In `TPM` you can, as the following expression demonstrates::
{#series:'ifempty($, field('#genre'))'} {series_index:0>7.1f:'ifempty($, -5)'}
The example shows several things: The example shows several things:
* `TPM` is used if the expression begins with ``:'`` and ends with ``'}``. Anything else is assumed to be in :ref:`Single Function Mode <single_mode>`. * `TPM` is used if the expression begins with ``:'`` and ends with ``'}``. Anything else is assumed to be in :ref:`Single Function Mode <single_mode>`.
* Functions must be given all their arguments. There is no default value. For example, the standard built-in functions must be given the initial parameter ``value``.
* The variable ``$`` is usable as the ``input`` argument and stands for the value of the field named in the template, ``#series`` in this case. If the template contains a prefix and suffix, the expression ends with ``'|`` where the ``|`` is the delimiter for the prefix. Example::
{series_index:0>7.1f:'ifempty($, -5)'|prefix | suffix}
* Functions must be given all their arguments. For example, the standard built-in functions must be given the initial parameter ``value``.
* The variable ``$`` is usable as the ``value`` argument and stands for the value of the field named in the template, ``series_index`` in this case.
* white space is ignored and can be used anywhere within the expression. * white space is ignored and can be used anywhere within the expression.
* constant strings are enclosed in matching quotes, either ``'`` or ``"``. * constant strings are enclosed in matching quotes, either ``'`` or ``"``.

View File

@ -45,7 +45,8 @@ from calibre import sanitize_file_name
from calibre.constants import config_dir, iswindows from calibre.constants import config_dir, iswindows
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.book.formatter import SafeFormat from calibre.ebooks.metadata.book.formatter import SafeFormat
from calibre.gui2 import choose_files, choose_save_file, error_dialog, gprefs, pixmap_to_data, question_dialog, safe_open_url from calibre.gui2 import (choose_files, choose_save_file, error_dialog, gprefs, info_dialog,
pixmap_to_data, question_dialog, safe_open_url)
from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog from calibre.gui2.dialogs.template_dialog_ui import Ui_TemplateDialog
from calibre.gui2.dialogs.template_general_info import GeneralInformationDialog from calibre.gui2.dialogs.template_general_info import GeneralInformationDialog
from calibre.gui2.widgets2 import Dialog, HTMLDisplay from calibre.gui2.widgets2 import Dialog, HTMLDisplay
@ -68,6 +69,8 @@ class DocViewer(Dialog):
self.builtins = builtins self.builtins = builtins
self.function_type_string = function_type_string_method self.function_type_string = function_type_string_method
self.last_operation = None self.last_operation = None
self.last_function = None
self.back_stack = []
super().__init__(title=_('Template function documentation'), name='template_editor_doc_viewer_dialog', super().__init__(title=_('Template function documentation'), name='template_editor_doc_viewer_dialog',
default_buttons=QDialogButtonBox.StandardButton.Close, parent=parent) default_buttons=QDialogButtonBox.StandardButton.Close, parent=parent)
@ -82,7 +85,7 @@ class DocViewer(Dialog):
e = self.doc_viewer_widget = HTMLDisplay(self) e = self.doc_viewer_widget = HTMLDisplay(self)
if iswindows: if iswindows:
e.setDefaultStyleSheet('pre { font-family: "Segoe UI Mono", "Consolas", monospace; }') e.setDefaultStyleSheet('pre { font-family: "Segoe UI Mono", "Consolas", monospace; }')
e.anchor_clicked.connect(safe_open_url) e.anchor_clicked.connect(self.url_clicked)
l.addWidget(e) l.addWidget(e)
bl = QHBoxLayout() bl = QHBoxLayout()
l.addLayout(bl) l.addLayout(bl)
@ -91,10 +94,41 @@ class DocViewer(Dialog):
cb.stateChanged.connect(self.english_cb_state_changed) cb.stateChanged.connect(self.english_cb_state_changed)
bl.addWidget(cb) bl.addWidget(cb)
bl.addWidget(self.bb) bl.addWidget(self.bb)
b = self.back_button = self.bb.addButton(_('&Back'), QDialogButtonBox.ButtonRole.ActionRole)
b.clicked.connect(self.back)
b.setToolTip((_('Displays the previously viewed function')))
b.setEnabled(False)
b = self.bb.addButton(_('Show &all functions'), QDialogButtonBox.ButtonRole.ActionRole) b = self.bb.addButton(_('Show &all functions'), QDialogButtonBox.ButtonRole.ActionRole)
b.clicked.connect(self.show_all_functions) b.clicked.connect(self.show_all_functions)
b.setToolTip((_('Shows a list of all built-in functions in alphabetic order'))) b.setToolTip((_('Shows a list of all built-in functions in alphabetic order')))
def back(self):
if not self.back_stack:
info_dialog(self, _('Go back'), _('No function to go back to'), show=True)
else:
place = self.back_stack.pop()
if not self.back_stack:
self.back_button.setEnabled(False)
if isinstance(place, int):
self.show_all_functions()
self.doc_viewer_widget.verticalScrollBar().setSliderPosition(place)
else:
self.show_function(place)
def url_clicked(self, qurl):
if qurl.scheme().startswith('http'):
safe_open_url(qurl)
else:
if self.last_function is not None:
self.back_stack.append(self.last_function)
self.back_button.setEnabled(True)
else:
self.back_stack.append(self.doc_viewer_widget.verticalScrollBar().sliderPosition())
self.back_button.setEnabled(True)
self.show_function(qurl.path())
def english_cb_state_changed(self): def english_cb_state_changed(self):
if self.last_operation is not None: if self.last_operation is not None:
self.last_operation() self.last_operation()
@ -113,10 +147,14 @@ class DocViewer(Dialog):
if fname not in self.builtins or not bif.doc: if fname not in self.builtins or not bif.doc:
self.set_html(self.header_line(fname) + ('No documentation provided')) self.set_html(self.header_line(fname) + ('No documentation provided'))
else: else:
self.last_function = fname
self.set_html(self.header_line(fname) + self.set_html(self.header_line(fname) +
self.ffml.document_to_html(self.get_doc(bif), fname)) self.ffml.document_to_html(self.get_doc(bif), fname))
def show_all_functions(self): def show_all_functions(self):
self.back_button.setEnabled(False)
self.back_stack = []
self.last_function = None
self.last_operation = self.show_all_functions self.last_operation = self.show_all_functions
result = [] result = []
a = result.append a = result.append
@ -127,7 +165,10 @@ class DocViewer(Dialog):
if not doc: if not doc:
a(_('No documentation provided')) a(_('No documentation provided'))
else: else:
a(self.ffml.document_to_html(doc.strip(), name)) html = self.ffml.document_to_html(doc.strip(), name)
paren = html.find('(')
html = f'<a href="ffdoc:{name}">{name}</a>{html[paren:]}'
a(html)
except Exception: except Exception:
print('Exception in', name) print('Exception in', name)
raise raise
@ -541,6 +582,8 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.doc_viewer = None self.doc_viewer = None
self.current_function_name = None self.current_function_name = None
self.documentation.setReadOnly(True) self.documentation.setReadOnly(True)
self.documentation.setOpenLinks(False)
self.documentation.anchorClicked.connect(self.url_clicked)
self.source_code.setReadOnly(True) self.source_code.setReadOnly(True)
self.doc_button.clicked.connect(self.open_documentation_viewer) self.doc_button.clicked.connect(self.open_documentation_viewer)
self.general_info_button.clicked.connect(self.open_general_info_dialog) self.general_info_button.clicked.connect(self.open_general_info_dialog)
@ -569,7 +612,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
except: except:
self.builtin_source_dict = {} self.builtin_source_dict = {}
func_names = sorted(self.all_functions) self.function_names = func_names = sorted(self.all_functions)
self.function.clear() self.function.clear()
self.function.addItem('') self.function.addItem('')
for f in func_names: for f in func_names:
@ -603,6 +646,15 @@ 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 url_clicked(self, qurl):
if qurl.scheme().startswith('http'):
safe_open_url(qurl)
elif qurl.scheme() == 'ffdoc':
name = qurl.path()
if name in self.function_names:
dex = self.function_names.index(name)
self.function.setCurrentIndex(dex+1)
def open_documentation_viewer(self): def open_documentation_viewer(self):
if self.doc_viewer is None: if self.doc_viewer is None:
dv = self.doc_viewer = DocViewer(self.ffml, self.all_functions, dv = self.doc_viewer = DocViewer(self.ffml, self.all_functions,

View File

@ -768,9 +768,6 @@ Selecting a function will show only that function's documentation</string>
</item> </item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="QTextBrowser" 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>

View File

@ -116,6 +116,13 @@ program:
\[/CODE] \[/CODE]
\[/LIST] \[/LIST]
[/CODE] [/CODE]
[*]End of summary marker. A summary is generated from the first characters of
the documentation. The summary includes text up to a \[/] tag. There is no
opening tag because the summary starts at the first character. If there is
no \[/] tag then all the document is used for the summary. The \[/] tag
is not replaced with white space or any other character.
[*]Escaped character: precede the character with a backslash. This is useful
to escape tags. For example to make the \[CODE] tag not a tag, use \\\[CODE].
[*]HTML output contains no CSS and does not start with a tag such as <DIV> or <P>. [*]HTML output contains no CSS and does not start with a tag such as <DIV> or <P>.
[/LIST] [/LIST]

View File

@ -14,16 +14,18 @@ class NodeKinds(IntEnum):
DOCUMENT = -1 DOCUMENT = -1
BLANK_LINE = -2 BLANK_LINE = -2
BOLD_TEXT = -3 BOLD_TEXT = -3
CODE_TEXT = -4 CHARACTER = -4
CODE_BLOCK = -5 CODE_TEXT = -5
END_LIST = -6 CODE_BLOCK = -6
GUI_LABEL = -7 END_LIST = -7
ITALIC_TEXT = -8 GUI_LABEL = -8
LIST = -9 ITALIC_TEXT = -9
LIST_ITEM = -10 LIST = -10
REF = -11 LIST_ITEM = -11
TEXT = -12 REF = -12
URL = -13 END_SUMMARY = -13
TEXT = -14
URL = -15
class Node: class Node:
@ -42,7 +44,7 @@ class Node:
return self._children return self._children
def text(self): def text(self):
return self._text.replace('\\', '') return self._text
def escaped_text(self): def escaped_text(self):
return prepare_string_for_xml(self.text()) return prepare_string_for_xml(self.text())
@ -61,6 +63,13 @@ class BoldTextNode(Node):
self._text = text self._text = text
class CharacterNode(Node):
def __init__(self, character):
super().__init__(NodeKinds.CHARACTER)
self._text = character
class CodeBlock(Node): class CodeBlock(Node):
def __init__(self, code_text): def __init__(self, code_text):
@ -82,6 +91,12 @@ class DocumentNode(Node):
self._children = [] self._children = []
class EndSummaryNode(Node):
def __init__(self):
super().__init__(NodeKinds.END_SUMMARY)
class GuiLabelNode(Node): class GuiLabelNode(Node):
def __init__(self, text): def __init__(self, text):
@ -202,6 +217,12 @@ class FFMLProcessor:
[/CODE] [/CODE]
[/LIST] [/LIST]
- end of summary marker. A summary is generated from the first characters of
the documentation. The summary includes text up to a \[/] tag. There is no
opening tag because the summary starts at the first character. If there is
no \[/] tag then all the document is used for the summary. The \[/] tag
is not replaced with white space or any other character.
- escaped character: precede the character with a backslash. This is useful - escaped character: precede the character with a backslash. This is useful
to escape tags. For example to make the [CODE] tag not a tag, use \[CODE]. to escape tags. For example to make the [CODE] tag not a tag, use \[CODE].
@ -241,7 +262,7 @@ class FFMLProcessor:
:param indent: The indent level of the tree. The outermost root should :param indent: The indent level of the tree. The outermost root should
have an indent of zero. have an indent of zero.
""" """
if node.node_kind() in (NodeKinds.TEXT, NodeKinds.CODE_TEXT, if node.node_kind() in (NodeKinds.TEXT, NodeKinds.CODE_TEXT, NodeKinds.CHARACTER,
NodeKinds.CODE_BLOCK, NodeKinds.ITALIC_TEXT, NodeKinds.CODE_BLOCK, NodeKinds.ITALIC_TEXT,
NodeKinds.GUI_LABEL, NodeKinds.BOLD_TEXT): NodeKinds.GUI_LABEL, NodeKinds.BOLD_TEXT):
print(f'{" " * indent}{node.node_kind().name}:{node.text()}') print(f'{" " * indent}{node.node_kind().name}:{node.text()}')
@ -288,10 +309,14 @@ class FFMLProcessor:
result += f'<b>{tree.escaped_text()}</b>' result += f'<b>{tree.escaped_text()}</b>'
elif tree.node_kind() == NodeKinds.BLANK_LINE: elif tree.node_kind() == NodeKinds.BLANK_LINE:
result += '\n<br>\n<br>\n' result += '\n<br>\n<br>\n'
elif tree.node_kind() == NodeKinds.CHARACTER:
result += tree.text()
elif tree.node_kind() == NodeKinds.CODE_TEXT: elif tree.node_kind() == NodeKinds.CODE_TEXT:
result += f'<code>{tree.escaped_text()}</code>' result += f'<code>{tree.escaped_text()}</code>'
elif tree.node_kind() == NodeKinds.CODE_BLOCK: elif tree.node_kind() == NodeKinds.CODE_BLOCK:
result += f'<pre style="margin-left:2em"><code>{tree.escaped_text().rstrip()}</code></pre>' result += f'<pre style="margin-left:2em"><code>{tree.escaped_text().rstrip()}</code></pre>'
elif tree.node_kind() == NodeKinds.END_SUMMARY:
pass
elif tree.node_kind() == NodeKinds.GUI_LABEL: elif tree.node_kind() == NodeKinds.GUI_LABEL:
result += f'<span style="font-family: Sans-Serif">{tree.escaped_text()}</span>' result += f'<span style="font-family: Sans-Serif">{tree.escaped_text()}</span>'
elif tree.node_kind() == NodeKinds.ITALIC_TEXT: elif tree.node_kind() == NodeKinds.ITALIC_TEXT:
@ -300,16 +325,16 @@ class FFMLProcessor:
result += '\n<ul>\n' result += '\n<ul>\n'
for child in tree.children(): for child in tree.children():
result += '<li>\n' result += '<li>\n'
result += self.tree_to_html(child, depth+1) result += self.tree_to_html(child, depth=depth+1)
result += '</li>\n' result += '</li>\n'
result += '</ul>\n' result += '</ul>\n'
elif tree.node_kind() == NodeKinds.REF: elif tree.node_kind() == NodeKinds.REF:
result += f'<code>{tree.escaped_text()}()</code>' result += f'<a href="ffdoc:{tree.text()}">{tree.text()}</a></a>'
elif tree.node_kind() == NodeKinds.URL: elif tree.node_kind() == NodeKinds.URL:
result += f'<a href="{tree.escaped_url()}">{tree.escaped_label()}</a>' result += f'<a href="{tree.escaped_url()}">{tree.escaped_label()}</a>'
elif tree.node_kind() in (NodeKinds.DOCUMENT, NodeKinds.LIST_ITEM): elif tree.node_kind() in (NodeKinds.DOCUMENT, NodeKinds.LIST_ITEM):
for child in tree.children(): for child in tree.children():
result += self.tree_to_html(child, depth+1) result += self.tree_to_html(child, depth=depth+1)
return result return result
def document_to_html(self, document, name): def document_to_html(self, document, name):
@ -327,6 +352,29 @@ class FFMLProcessor:
tree = self.parse_document(document, name) tree = self.parse_document(document, name)
return self.tree_to_html(tree, 0) return self.tree_to_html(tree, 0)
def document_to_summary_html(self, document, name):
"""
Given a document in the Formatter Function Markup Language (FFML), return
that document's summary 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
"""
document = document.strip()
sum_tag = document.find('[/]')
if sum_tag > 0:
document = document[0:sum_tag]
fname = document[0:document.find('(')].lstrip('`')
tree = self.parse_document(document, name)
result = self.tree_to_html(tree, depth=0)
paren = result.find('(')
result = f'<a href="ffdoc:{fname}">{fname}</a>{result[paren:]}'
return result
def tree_to_rst(self, tree, indent, result=None): def tree_to_rst(self, tree, indent, result=None):
""" """
Given a Formatter Function Markup Language (FFML) parse tree, return Given a Formatter Function Markup Language (FFML) parse tree, return
@ -356,6 +404,8 @@ class FFMLProcessor:
result += '\n\n' result += '\n\n'
elif tree.node_kind() == NodeKinds.BOLD_TEXT: elif tree.node_kind() == NodeKinds.BOLD_TEXT:
indent_text(f'**{tree.text()}**') indent_text(f'**{tree.text()}**')
elif tree.node_kind() == NodeKinds.CHARACTER:
result += tree.text()
elif tree.node_kind() == NodeKinds.CODE_BLOCK: elif tree.node_kind() == NodeKinds.CODE_BLOCK:
result += f"\n\n{' ' * indent}::\n\n" result += f"\n\n{' ' * indent}::\n\n"
for line in tree.text().strip().split('\n'): for line in tree.text().strip().split('\n'):
@ -363,6 +413,8 @@ class FFMLProcessor:
result += '\n' result += '\n'
elif tree.node_kind() == NodeKinds.CODE_TEXT: elif tree.node_kind() == NodeKinds.CODE_TEXT:
indent_text(f'``{tree.text()}``') indent_text(f'``{tree.text()}``')
elif tree.node_kind() == NodeKinds.END_SUMMARY:
pass
elif tree.node_kind() == NodeKinds.GUI_LABEL: elif tree.node_kind() == NodeKinds.GUI_LABEL:
indent_text(f':guilabel:`{tree.text()}`') indent_text(f':guilabel:`{tree.text()}`')
elif tree.node_kind() == NodeKinds.ITALIC_TEXT: elif tree.node_kind() == NodeKinds.ITALIC_TEXT:
@ -371,7 +423,7 @@ class FFMLProcessor:
result += '\n\n' result += '\n\n'
for child in tree.children(): for child in tree.children():
result += f"{' ' * (indent)}* " result += f"{' ' * (indent)}* "
result = self.tree_to_rst(child, indent+1, result) result = self.tree_to_rst(child, indent+1, result=result)
result += '\n' result += '\n'
result += '\n' result += '\n'
elif tree.node_kind() == NodeKinds.REF: elif tree.node_kind() == NodeKinds.REF:
@ -384,7 +436,7 @@ class FFMLProcessor:
indent_text(f'`{tree.label()} <{tree.url()}>`_') indent_text(f'`{tree.label()} <{tree.url()}>`_')
elif tree.node_kind() in (NodeKinds.DOCUMENT, NodeKinds.LIST_ITEM): elif tree.node_kind() in (NodeKinds.DOCUMENT, NodeKinds.LIST_ITEM):
for child in tree.children(): for child in tree.children():
result = self.tree_to_rst(child, indent, result) result = self.tree_to_rst(child, indent, result=result)
return result return result
def document_to_rst(self, document, name, indent=0, prefix=None): def document_to_rst(self, document, name, indent=0, prefix=None):
@ -410,19 +462,51 @@ class FFMLProcessor:
doc = prefix + doc.lstrip(' ' * indent) doc = prefix + doc.lstrip(' ' * indent)
return doc return doc
def document_to_summary_rst(self, document, name, indent=0, prefix=None):
"""
Given a document in the Formatter Function Markup Language (FFML), return
that document's summary 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.
: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.
:param prefix: string. if supplied, this string replaces the indent
on the first line of the output. This permits specifying
an RST block, for example a bullet list
:return: a string containing the RST text
"""
document = document.strip()
sum_tag = document.find('[/]')
if sum_tag > 0:
document = document[0:sum_tag]
fname = document[0:document.find('(')].lstrip('`')
doc = self.tree_to_rst(self.parse_document(document, name), indent)
lparen = doc.find('(')
doc = f':ref:`ff_{fname}`\\ ``{doc[lparen:]}'
if prefix is not None:
doc = prefix + doc.lstrip(' ' * indent)
return doc
# ============== Internal methods ================= # ============== Internal methods =================
keywords = {'``': NodeKinds.CODE_TEXT, # must be before '`' keywords = {'``': NodeKinds.CODE_TEXT, # must be before '`'
'`': NodeKinds.ITALIC_TEXT, '`': NodeKinds.ITALIC_TEXT,
'[B]': NodeKinds.BOLD_TEXT, '[B]': NodeKinds.BOLD_TEXT,
'[CODE]': NodeKinds.CODE_BLOCK, '[CODE]': NodeKinds.CODE_BLOCK,
'[/]': NodeKinds.END_SUMMARY,
':guilabel:': NodeKinds.GUI_LABEL, ':guilabel:': NodeKinds.GUI_LABEL,
'[LIST]': NodeKinds.LIST, '[LIST]': NodeKinds.LIST,
'[/LIST]': NodeKinds.END_LIST, '[/LIST]': NodeKinds.END_LIST,
':ref:': NodeKinds.REF, ':ref:': NodeKinds.REF,
'[URL': NodeKinds.URL, '[URL': NodeKinds.URL,
'[*]': NodeKinds.LIST_ITEM, '[*]': NodeKinds.LIST_ITEM,
'\n\n': NodeKinds.BLANK_LINE '\n\n': NodeKinds.BLANK_LINE,
'\\': NodeKinds.CHARACTER
} }
def __init__(self): def __init__(self):
@ -437,8 +521,6 @@ class FFMLProcessor:
p = self.input.find(for_what, self.input_pos) p = self.input.find(for_what, self.input_pos)
if p < 0: if p < 0:
return -1 return -1
while p > 0 and self.input[p-1] == '\\':
p = self.input.find(for_what, p+1)
return -1 if p < 0 else p - self.input_pos return -1 if p < 0 else p - self.input_pos
def move_pos(self, to_where): def move_pos(self, to_where):
@ -486,6 +568,12 @@ class FFMLProcessor:
self.move_pos(end + len('[/B]')) self.move_pos(end + len('[/B]'))
return node return node
def get_character(self):
self.move_pos(1)
node = CharacterNode(self.text_to(1))
self.move_pos(1)
return node
def get_code_block(self): def get_code_block(self):
self.move_pos(len('[CODE]')) self.move_pos(len('[CODE]'))
if self.text_to(1) == '\n': if self.text_to(1) == '\n':
@ -583,10 +671,15 @@ class FFMLProcessor:
self.move_pos(2) self.move_pos(2)
elif p == NodeKinds.BOLD_TEXT: elif p == NodeKinds.BOLD_TEXT:
parent.add_child(self.get_bold_text()) parent.add_child(self.get_bold_text())
elif p == NodeKinds.CHARACTER:
parent.add_child(self.get_character())
elif p == NodeKinds.CODE_TEXT: elif p == NodeKinds.CODE_TEXT:
parent.add_child(self.get_code_text()) parent.add_child(self.get_code_text())
elif p == NodeKinds.CODE_BLOCK: elif p == NodeKinds.CODE_BLOCK:
parent.add_child(self.get_code_block()) parent.add_child(self.get_code_block())
elif p == NodeKinds.END_SUMMARY:
parent.add_child(EndSummaryNode())
self.move_pos(3)
elif p == NodeKinds.GUI_LABEL: elif p == NodeKinds.GUI_LABEL:
parent.add_child(self.get_gui_label()) parent.add_child(self.get_gui_label())
elif p == NodeKinds.ITALIC_TEXT: elif p == NodeKinds.ITALIC_TEXT:

File diff suppressed because it is too large Load Diff