This commit is contained in:
Kovid Goyal 2024-11-14 08:21:57 +05:30
commit e035674bdb
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 162 additions and 19 deletions

View File

@ -11,7 +11,8 @@ Created on 12 Nov 2024
@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
@ -139,12 +140,18 @@ class FFDocEditor(Dialog):
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()

View File

@ -47,6 +47,7 @@ from calibre.ebooks.metadata.book.base import Metadata
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
@ -542,6 +543,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
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:
@ -615,6 +617,9 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
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

View File

@ -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>

View File

@ -0,0 +1,114 @@
#!/usr/bin/env python
__copyright__ = '2024, Kovid Goyal kovid@kovidgoyal.net'
__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]
'''

View File

@ -43,6 +43,7 @@ from calibre.gui2.auto_add import AutoAdder
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
@ -329,6 +330,13 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
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)
@ -462,6 +470,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
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:

View File

@ -41,10 +41,10 @@ class Node:
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):
@ -248,6 +248,7 @@ class FFMLProcessor:
:return: a parse tree for the document
"""
self.input_line = 1
self.input = doc
self.input_pos = 0
self.document_name = name
@ -404,13 +405,16 @@ class FFMLProcessor:
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):
@ -450,7 +454,9 @@ class FFMLProcessor:
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')
@ -469,6 +475,11 @@ class FFMLProcessor:
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('`')