diff --git a/src/calibre/gui2/dialogs/ff_doc_editor.py b/src/calibre/gui2/dialogs/ff_doc_editor.py index 33deee24e0..eb43c5f44d 100644 --- a/src/calibre/gui2/dialogs/ff_doc_editor.py +++ b/src/calibre/gui2/dialogs/ff_doc_editor.py @@ -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() diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py index 1d507b3e33..26249945bd 100644 --- a/src/calibre/gui2/dialogs/template_dialog.py +++ b/src/calibre/gui2/dialogs/template_dialog.py @@ -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 diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui index 8443a8bbeb..c8254dc9a1 100644 --- a/src/calibre/gui2/dialogs/template_dialog.ui +++ b/src/calibre/gui2/dialogs/template_dialog.ui @@ -754,18 +754,13 @@ Selecting a function will show only that function's documentation - + - See tooltip for general information - - - true + General &Information - <p>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.</p> <p>In all the other modes the value parameter -must be supplied.</p> + Click this button to see general help about using template functions + and how the are documented. diff --git a/src/calibre/gui2/dialogs/template_general_info.py b/src/calibre/gui2/dialogs/template_general_info.py new file mode 100644 index 0000000000..1133340cb4 --- /dev/null +++ b/src/calibre/gui2/dialogs/template_general_info.py @@ -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() `` +[*]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
or

. +[/LIST] +[/LIST] +''' \ No newline at end of file diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index ca0fc26927..d3c0d8377b 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -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: diff --git a/src/calibre/utils/ffml_processor.py b/src/calibre/utils/ffml_processor.py index cf948bbe95..a8fc7143c9 100644 --- a/src/calibre/utils/ffml_processor.py +++ b/src/calibre/utils/ffml_processor.py @@ -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('`')