mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
681c50ed08
@ -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 ``"``.
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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]
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user