Skip to content

Commit

Permalink
Changes and bug fixes:
Browse files Browse the repository at this point in the history
- Add an action to ui.py to define a possible shortcut for opening the template documentation editor.
- Better exception handling in the template doc editor
- Add a dialog giving general information about formatter functions. Use FFML to test it.
- Add a button to template_dialog.py to open the general information dialog.
- Add escaping characters to FFML. Characters are escaped with backslash. Useful if one wants to enter something that looks like FFML but is supposed to be text. Example: escaping the opening bracket in \[CODE] turns it into a string instead of a tag.
  • Loading branch information
cbhaley committed Nov 13, 2024
1 parent d061851 commit 0253793
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 19 deletions.
19 changes: 13 additions & 6 deletions src/calibre/gui2/dialogs/ff_doc_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
@author: chaley
'''

from qt.core import QApplication, QCheckBox, QComboBox, QFrame, QGridLayout, QHBoxLayout, QLabel, QPlainTextEdit, QPushButton, QSize, QTimer
from qt.core import (QApplication, QCheckBox, QComboBox, QFrame, QLabel, QGridLayout,
QHBoxLayout, QPlainTextEdit, QPushButton, QSize, QTimer)

from calibre.constants import iswindows
from calibre.gui2 import gprefs
Expand Down Expand Up @@ -139,12 +140,18 @@ def _editable_box_changed(self):
name = self.functions_box.currentText()
if name and self.doc_show_formatted_cb.isVisible() and self.doc_show_formatted_cb.isChecked():
doc = self.builtins[name].doc
self.editable_text_result.setHtml(
self.ffml.document_to_html(doc.format_again(
self.editable_text_widget.toPlainText()), 'edited text'))
try:
self.editable_text_result.setHtml(
self.ffml.document_to_html(doc.format_again(
self.editable_text_widget.toPlainText()), 'edited text'))
except Exception as e:
self.editable_text_result.setHtml(str(e))
else:
self.editable_text_result.setHtml(
self.ffml.document_to_html(self.editable_text_widget.toPlainText(), 'edited text'))
try:
self.editable_text_result.setHtml(
self.ffml.document_to_html(self.editable_text_widget.toPlainText(), 'edited text'))
except Exception as e:
self.editable_text_result.setHtml(str(e))

def fill_in_top_row(self):
to_show = self.show_original_cb.isChecked()
Expand Down
5 changes: 5 additions & 0 deletions src/calibre/gui2/dialogs/template_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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.dialogs.template_dialog_ui import Ui_TemplateDialog
from calibre.gui2.dialogs.template_general_info import GeneralInformationDialog
from calibre.gui2.widgets2 import Dialog, HTMLDisplay
from calibre.library.coloring import color_row_key, displayable_columns
from calibre.utils.config_base import tweaks
Expand Down Expand Up @@ -542,6 +543,7 @@ def __init__(self, parent, text, mi=None, fm=None, color_field=None,
self.documentation.setReadOnly(True)
self.source_code.setReadOnly(True)
self.doc_button.clicked.connect(self.open_documentation_viewer)
self.general_info_button.clicked.connect(self.open_general_info_dialog)

if text is not None:
if text_is_placeholder:
Expand Down Expand Up @@ -615,6 +617,9 @@ def open_documentation_viewer(self):
def doc_viewer_finished(self):
self.doc_viewer = None

def open_general_info_dialog(self):
GeneralInformationDialog().exec()

def geometry_string(self, txt):
if self.dialog_number is None or self.dialog_number == 0:
return txt
Expand Down
13 changes: 4 additions & 9 deletions src/calibre/gui2/dialogs/template_dialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -754,18 +754,13 @@ Selecting a function will show only that function's documentation</string>
</widget>
</item>
<item>
<widget class="QLabel" name="some_label">
<widget class="QPushButton" name="general_info_button">
<property name="text">
<string>See tooltip for general information</string>
</property>
<property name="wordWrap">
<bool>true</bool>
<string>General &amp;Information</string>
</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; &lt;p&gt;In all the other modes the value parameter
must be supplied.&lt;/p&gt;</string>
<string>Click this button to see general help about using template functions
and how the are documented.</string>
</property>
</widget>
</item>
Expand Down
114 changes: 114 additions & 0 deletions src/calibre/gui2/dialogs/template_general_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env python


__copyright__ = '2024, Kovid Goyal [email protected]'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'

'''
@author: Charles Haley
'''

from qt.core import (QDialogButtonBox, QVBoxLayout)

from calibre.constants import iswindows
from calibre.gui2.widgets2 import Dialog, HTMLDisplay
from calibre.utils.ffml_processor import FFMLProcessor

class GeneralInformationDialog(Dialog):

def __init__(self, parent=None):
super().__init__(title=_('Template function general information'), name='template_editor_gen_info_dialog',
default_buttons=QDialogButtonBox.StandardButton.Close, parent=parent)

def setup_ui(self):
l = QVBoxLayout(self)
e = HTMLDisplay(self)
l.addWidget(e)
if iswindows:
e.setDefaultStyleSheet('pre { font-family: "Segoe UI Mono", "Consolas", monospace; }')
l.addWidget(self.bb)
e.setHtml(FFMLProcessor().document_to_html(information, 'Template Information'))


information = '''
[LIST]
[*]`Functions in Single Function Mode templates`
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.
In all the other modes the value parameter must be supplied.
[*]`Editor for asssting with template function documentation`
An editor is available for helping write template function documentation. Given a document
in the Formatter Function Markup Language, it show the resulting HTML. The HTML is updated as you edit.
This editor is available in two ways:
[LIST]
[*]Using the command
[CODE]
calibre-debug -c "from calibre.gui2.dialogs.ff_doc_editor import main; main()"[/CODE]
all on one line.
[*]By defining a keyboard shortcut in calibre for the action `Open the template
documenation editor` in the `Miscellaneous` section. There is no default shortcut.
[/LIST]
[*]The `Template Function Markup Language`
Format Function Markup Language (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:
[LIST]
[*]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`.
[*]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`
[*]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]`` produces [URL href="https://en.wikipedia.org/wiki/ISO_8601"]ISO[/URL]
[*]Internal function reference links. These are links to formatter function
documentation. The syntax is the same as guilabel. Example: ``:ref:`get_note` ``.
The characters '()' are automatically added to the function name when
displayed. For HTML it generates the same as the inline program code text
operator (\`\`) with no link. Example: ``:ref:`add` `` produces ``add()``.
For RST it generates a ``:ref:`` reference that works only in an RST document
containing formatter function documentation. Example: ``:ref:`get_note` ``
generates ``:ref:`get_note() <ff_get_note>``
[*]Example program code text blocks. Surround the code block with ``[CODE]``
and ``[/CODE]`` tags. These tags must be first on a line. Example:
[CODE]
\[CODE]program:
get_note('authors', 'Isaac Asimov', 1)
\[/CODE]
[/CODE]
produces
[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
[CODE]
\[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]
[/CODE]
[*]HTML output contains no CSS and does not start with a tag such as <DIV> or <P>.
[/LIST]
[/LIST]
'''
11 changes: 11 additions & 0 deletions src/calibre/gui2/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from calibre.gui2.changes import handle_changes
from calibre.gui2.cover_flow import CoverFlowMixin
from calibre.gui2.device import DeviceMixin
from calibre.gui2.dialogs.ff_doc_editor import FFDocEditor
from calibre.gui2.dialogs.message_box import JobError
from calibre.gui2.ebook_download import EbookDownloadMixin
from calibre.gui2.email import EmailMixin
Expand Down Expand Up @@ -329,6 +330,13 @@ def initialize(self, library_path, db, actions, show_gui=True):
action=self.alt_esc_action)
self.alt_esc_action.triggered.connect(self.clear_additional_restriction)

self.ff_doc_editor_action = QAction(self)
self.addAction(self.ff_doc_editor_action)
self.keyboard.register_shortcut('open ff document editor',
_('Open the template documentation editor'), default_keys=(''),
action=self.ff_doc_editor_action)
self.ff_doc_editor_action.triggered.connect(self.open_ff_doc_editor)

# ###################### Start spare job server ########################
QTimer.singleShot(1000, self.create_spare_pool)

Expand Down Expand Up @@ -462,6 +470,9 @@ def show_gui_debug_msg(self):
def esc(self, *args):
self.search.clear()

def open_ff_doc_editor(self):
FFDocEditor(False).exec()

def focus_current_view(self):
view = self.current_view()
if view is self.library_view:
Expand Down
19 changes: 15 additions & 4 deletions src/calibre/utils/ffml_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ def children(self):
return self._children

def text(self):
return self._text
return self._text.replace('\\', '')

def escaped_text(self):
return prepare_string_for_xml(self._text)
return prepare_string_for_xml(self.text())


class BlankLineNode(Node):
Expand Down Expand Up @@ -248,6 +248,7 @@ def parse_document(self, doc, name):
:return: a parse tree for the document
"""
self.input_line = 1
self.input = doc
self.input_pos = 0
self.document_name = name
Expand Down Expand Up @@ -404,13 +405,16 @@ 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)
if p < 0:
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

def move_pos(self, to_where):
Expand Down Expand Up @@ -450,7 +454,9 @@ def find_one_of(self):
return len(self.input)

def get_code_block(self):
self.move_pos(len('[CODE]\n'))
self.move_pos(len('[CODE]'))
if self.text_to(1) == '\n':
self.move_pos(1)
end = self.find('[/CODE]')
if end < 0:
self.error('Missing [/CODE] for block')
Expand All @@ -469,6 +475,11 @@ def get_code_text(self):
self.move_pos(end + len('``'))
return node

def get_escaped_char(self):
node = EscapedCharNode(self.text_to(1))
self.move_pos(1)
return node

def get_gui_label(self):
self.move_pos(len(':guilabel:`'))
end = self.find('`')
Expand Down

0 comments on commit 0253793

Please sign in to comment.