This commit is contained in:
Kovid Goyal 2024-11-15 07:11:31 +05:30
commit 2cb7b68714
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 86 additions and 45 deletions

View File

@ -32,15 +32,20 @@ class GeneralInformationDialog(Dialog):
e.setHtml(FFMLProcessor().document_to_html(information, 'Template Information')) e.setHtml(FFMLProcessor().document_to_html(information, 'Template Information'))
information = ''' information = r'''
[LIST] [LIST]
[*]`Functions in Single Function Mode templates` [*]`Functions in Single Function Mode templates`
[LIST]
When using functions in a Single function mode template, [*]When using functions in a Single function mode template,
for example ``{title:uppercase()}``, the first parameter ``value`` is omitted. for example ``{title:uppercase()}``, the first parameter ``value`` is omitted.
It is automatically replaced by the value of the specified field. It is automatically replaced by the value of the specified field.
In all the other modes the value parameter must be supplied. In all the other modes the value parameter must be supplied.
[*]Do not use subtemplates "(`{...}`)" as function arguments because they will often not work.
Instead, use Template Program Mode and General Program Mode.
[*]Do not use subtemplates "(`{...}`)" or functions (see below) in the prefix or the suffix
for the same reason as above; they will often not work.
[/LIST]
[*]`Editor for asssting with template function documentation` [*]`Editor for asssting with template function documentation`
An editor is available for helping write template function documentation. Given a document An editor is available for helping write template function documentation. Given a document
@ -62,28 +67,29 @@ 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 and BBCODE used by many bulletin board systems such as MobileRead. It provides a
way to specify: way to specify:
[LIST] [LIST]
[*]Inline program code text: surround this text with \\`\\` as in \\`\\`foo\\`\\`. Tags inside the text are ignored. [*]Inline program code text: surround this text with \`\` as in \`\`foo\`\`. Tags inside the text are ignored.
[*]Italic text: surround this text with \\`. Example \\`foo\\` produces `foo`. [*]Italic text: surround this text with \`. Example: \`foo\` produces `foo`.
[*]Bold text: surround this text with \[B]text\[\B] tags. Example: \[B]foo\[/B] produces [B]foo[/B].
[*]Text intended to reference a calibre GUI action. This uses RST syntax.\ [*]Text intended to reference a calibre GUI action. This uses RST syntax.\
Example: \\:guilabel\\:\\`Preferences->Advanced->Template functions\\`. For HTML the produced text is in a different font, as in: :guilabel:`Some text` Example: \:guilabel\:\`Preferences->Advanced->Template functions\`. For HTML the produced text is in a different font, as in: :guilabel:`Some text`
[*]Empty lines, indicated by two newlines in a row. A visible empty line in the FFMC will become an empty line in the output. [*]Empty lines, indicated by two newlines in a row. A visible empty line in the FFML
will become an empty line in the output.
[*]URLs. The syntax is similar to BBCODE: ``[URL href="http..."]Link text[/URL]``.\ [*]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]`` produces [URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL] Example: ``[URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL]`` produces [URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL]
[*]Internal function reference links. These are links to formatter function [*]Internal function reference links. These are links to formatter function
documentation. The syntax is the same as guilabel. Example: ``:ref:`get_note` ``. documentation. The syntax is the same as guilabel. Example: ``:ref:\`get_note\```.
The characters '()' are automatically added to the function name when The characters '()' are automatically added to the function name when
displayed. For HTML it generates the same as the inline program code text displayed. For HTML it generates the same as the inline program code text
operator (\\`\\`) with no link. Example: ``:ref:`add` `` produces ``add()``. operator (\`\`) with no link. Example: ``:ref:`add` `` produces ``add()``.
For RST it generates a ``:ref:`` reference that works only in an RST document For RST it generates a ``:ref:`` reference that works only in an RST document
containing formatter function documentation. Example: ``:ref:`get_note` `` containing formatter function documentation. Example: ``:ref:\`get_note\```
generates \\:ref\\:\\`get_note() <ff_get_note>\\` generates \:ref\:\`get_note() <ff_get_note>\`
[*]Example program code text blocks. Surround the code block with ``[CODE]`` [*]Example program code text blocks. Surround the code block with ``[CODE]``
and ``[/CODE]`` tags. These tags must be first on a line. Example: and ``[/CODE]`` tags. These tags must be first on a line. Example:
[CODE] [CODE]
\\[CODE]program: \[CODE]program:
get_note('authors', 'Isaac Asimov', 1) get_note('authors', 'Isaac Asimov', 1)
\\[/CODE] \[/CODE]
[/CODE] [/CODE]
produces produces
[CODE] [CODE]
@ -97,18 +103,18 @@ elements.
Example: a two bullet list containing CODE blocks Example: a two bullet list containing CODE blocks
[CODE] [CODE]
\\[LIST] \[LIST]
[*]Return the HTML of the note attached to the tag `Fiction`: [*]Return the HTML of the note attached to the tag `Fiction`:
\\[CODE] \[CODE]
program: program:
get_note('tags', 'Fiction', '') get_note('tags', 'Fiction', '')
\\[/CODE] \[/CODE]
[*]Return the plain text of the note attached to the author `Isaac Asimov`: [*]Return the plain text of the note attached to the author `Isaac Asimov`:
\\[CODE] \[CODE]
program: program:
get_note('authors', 'Isaac Asimov', 1) get_note('authors', 'Isaac Asimov', 1)
\\[/CODE] \[/CODE]
\\[/LIST] \[/LIST]
[/CODE] [/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>.

View File

@ -13,16 +13,17 @@ from calibre import prepare_string_for_xml
class NodeKinds(IntEnum): class NodeKinds(IntEnum):
DOCUMENT = -1 DOCUMENT = -1
BLANK_LINE = -2 BLANK_LINE = -2
CODE_TEXT = -3 BOLD_TEXT = -3
CODE_BLOCK = -4 CODE_TEXT = -4
END_LIST = -5 CODE_BLOCK = -5
GUI_LABEL = -6 END_LIST = -6
ITALIC_TEXT = -7 GUI_LABEL = -7
LIST = -8 ITALIC_TEXT = -8
LIST_ITEM = -9 LIST = -9
REF = -10 LIST_ITEM = -10
TEXT = -11 REF = -11
URL = -12 TEXT = -12
URL = -13
class Node: class Node:
@ -53,6 +54,13 @@ class BlankLineNode(Node):
super().__init__(NodeKinds.BLANK_LINE) super().__init__(NodeKinds.BLANK_LINE)
class BoldTextNode(Node):
def __init__(self, text):
super().__init__(NodeKinds.BOLD_TEXT)
self._text = text
class CodeBlock(Node): class CodeBlock(Node):
def __init__(self, code_text): def __init__(self, code_text):
@ -146,7 +154,11 @@ class FFMLProcessor:
- inline program code text: surround this text with `` as in ``foo``. - inline program code text: surround this text with `` as in ``foo``.
- italic text: surround this text with `, as in `foo`. - bold text: surrond this text with [B] [/B], as in [B]foo[/B]. Note: bold
italics don't work in RST.
- italic text: surround this text with `, as in `foo`. Note: bold italics
don't work in RST.
- text intended to reference a calibre GUI action. This uses RST syntax. - text intended to reference a calibre GUI action. This uses RST syntax.
Example: :guilabel:`Preferences->Advanced->Template functions` Example: :guilabel:`Preferences->Advanced->Template functions`
@ -190,6 +202,9 @@ class FFMLProcessor:
[/CODE] [/CODE]
[/LIST] [/LIST]
- 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>.
API example: generate documents for all builtin formatter functions API example: generate documents for all builtin formatter functions
@ -228,7 +243,7 @@ class FFMLProcessor:
""" """
if node.node_kind() in (NodeKinds.TEXT, NodeKinds.CODE_TEXT, if node.node_kind() in (NodeKinds.TEXT, NodeKinds.CODE_TEXT,
NodeKinds.CODE_BLOCK, NodeKinds.ITALIC_TEXT, NodeKinds.CODE_BLOCK, NodeKinds.ITALIC_TEXT,
NodeKinds.GUI_LABEL): NodeKinds.GUI_LABEL, NodeKinds.BOLD_TEXT):
print(f'{" " * indent}{node.node_kind().name}:{node.text()}') print(f'{" " * indent}{node.node_kind().name}:{node.text()}')
elif node.node_kind() == NodeKinds.URL: elif node.node_kind() == NodeKinds.URL:
print(f'{" " * indent}URL: label={node.label()}, URL={node.url()}') print(f'{" " * indent}URL: label={node.label()}, URL={node.url()}')
@ -269,12 +284,14 @@ class FFMLProcessor:
result = '' result = ''
if tree.node_kind() == NodeKinds.TEXT: if tree.node_kind() == NodeKinds.TEXT:
result += tree.escaped_text() result += tree.escaped_text()
if tree.node_kind() == NodeKinds.BOLD_TEXT:
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.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()}</code></pre>' result += f'<pre style="margin-left:2em"><code>{tree.escaped_text().rstrip()}</code></pre>'
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:
@ -322,22 +339,34 @@ class FFMLProcessor:
:return: a string containing the RST text :return: a string containing the RST text
""" """
def indent_text(txt):
nonlocal result
if not result:
txt = txt.lstrip()
elif result.endswith('\n'):
txt = txt.lstrip()
result += ' ' * indent
result += txt.replace('\n', ' ' * indent)
if result is None: if result is None:
result = ' ' * indent result = ' ' * indent
if tree.node_kind() == NodeKinds.BLANK_LINE: if tree.node_kind() == NodeKinds.BLANK_LINE:
result += '\n\n' result += '\n\n'
elif tree.node_kind() == NodeKinds.BOLD_TEXT:
indent_text(f'**{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'):
result += f"{' ' * (indent+1)}{line}\n" result += f"{' ' * (indent+1)}{line}\n"
result += '\n' result += '\n'
elif tree.node_kind() == NodeKinds.CODE_TEXT: elif tree.node_kind() == NodeKinds.CODE_TEXT:
result += f'``{tree.text()}``' indent_text(f'``{tree.text()}``')
elif tree.node_kind() == NodeKinds.GUI_LABEL: elif tree.node_kind() == NodeKinds.GUI_LABEL:
result += f':guilabel:`{tree.text()}`' indent_text(f':guilabel:`{tree.text()}`')
elif tree.node_kind() == NodeKinds.ITALIC_TEXT: elif tree.node_kind() == NodeKinds.ITALIC_TEXT:
result += f'`{tree.text()}`' indent_text(f'`{tree.text()}`')
elif tree.node_kind() == NodeKinds.LIST: elif tree.node_kind() == NodeKinds.LIST:
result += '\n\n' result += '\n\n'
for child in tree.children(): for child in tree.children():
@ -348,17 +377,11 @@ class FFMLProcessor:
elif tree.node_kind() == NodeKinds.REF: elif tree.node_kind() == NodeKinds.REF:
if (rname := tree.text()).endswith('()'): if (rname := tree.text()).endswith('()'):
rname = rname[:-2] rname = rname[:-2]
result += f':ref:`{rname}() <ff_{rname}>`' indent_text(f':ref:`{rname}() <ff_{rname}>`')
elif tree.node_kind() == NodeKinds.TEXT: elif tree.node_kind() == NodeKinds.TEXT:
txt = tree.text() indent_text(tree.text())
if not result:
txt = txt.lstrip()
elif result.endswith('\n'):
txt = txt.lstrip()
result += ' ' * indent
result += txt
elif tree.node_kind() == NodeKinds.URL: elif tree.node_kind() == NodeKinds.URL:
result += 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)
@ -391,6 +414,7 @@ class FFMLProcessor:
keywords = {'``': NodeKinds.CODE_TEXT, # must be before '`' keywords = {'``': NodeKinds.CODE_TEXT, # must be before '`'
'`': NodeKinds.ITALIC_TEXT, '`': NodeKinds.ITALIC_TEXT,
'[B]': NodeKinds.BOLD_TEXT,
'[CODE]': NodeKinds.CODE_BLOCK, '[CODE]': NodeKinds.CODE_BLOCK,
':guilabel:': NodeKinds.GUI_LABEL, ':guilabel:': NodeKinds.GUI_LABEL,
'[LIST]': NodeKinds.LIST, '[LIST]': NodeKinds.LIST,
@ -453,6 +477,15 @@ class FFMLProcessor:
return min(positions) return min(positions)
return len(self.input) return len(self.input)
def get_bold_text(self):
self.move_pos(len('[B]'))
end = self.find('[/B]')
if end < 0:
self.error('Missing closing "[/B]" for bold')
node = BoldTextNode(self.text_to(end))
self.move_pos(end + len('[/B]'))
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':
@ -553,6 +586,8 @@ class FFMLProcessor:
elif p == NodeKinds.BLANK_LINE: elif p == NodeKinds.BLANK_LINE:
parent.add_child(BlankLineNode()) parent.add_child(BlankLineNode())
self.move_pos(2) self.move_pos(2)
elif p == NodeKinds.BOLD_TEXT:
parent.add_child(self.get_bold_text())
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: