mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge branch 'master' of https://github.com/cbhaley/calibre
This commit is contained in:
commit
e035674bdb
@ -11,7 +11,8 @@ Created on 12 Nov 2024
|
|||||||
@author: chaley
|
@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.constants import iswindows
|
||||||
from calibre.gui2 import gprefs
|
from calibre.gui2 import gprefs
|
||||||
@ -139,12 +140,18 @@ class FFDocEditor(Dialog):
|
|||||||
name = self.functions_box.currentText()
|
name = self.functions_box.currentText()
|
||||||
if name and self.doc_show_formatted_cb.isVisible() and self.doc_show_formatted_cb.isChecked():
|
if name and self.doc_show_formatted_cb.isVisible() and self.doc_show_formatted_cb.isChecked():
|
||||||
doc = self.builtins[name].doc
|
doc = self.builtins[name].doc
|
||||||
self.editable_text_result.setHtml(
|
try:
|
||||||
self.ffml.document_to_html(doc.format_again(
|
self.editable_text_result.setHtml(
|
||||||
self.editable_text_widget.toPlainText()), 'edited text'))
|
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:
|
else:
|
||||||
self.editable_text_result.setHtml(
|
try:
|
||||||
self.ffml.document_to_html(self.editable_text_widget.toPlainText(), 'edited text'))
|
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):
|
def fill_in_top_row(self):
|
||||||
to_show = self.show_original_cb.isChecked()
|
to_show = self.show_original_cb.isChecked()
|
||||||
|
@ -47,6 +47,7 @@ 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, 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.widgets2 import Dialog, HTMLDisplay
|
from calibre.gui2.widgets2 import Dialog, HTMLDisplay
|
||||||
from calibre.library.coloring import color_row_key, displayable_columns
|
from calibre.library.coloring import color_row_key, displayable_columns
|
||||||
from calibre.utils.config_base import tweaks
|
from calibre.utils.config_base import tweaks
|
||||||
@ -542,6 +543,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
|||||||
self.documentation.setReadOnly(True)
|
self.documentation.setReadOnly(True)
|
||||||
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)
|
||||||
|
|
||||||
if text is not None:
|
if text is not None:
|
||||||
if text_is_placeholder:
|
if text_is_placeholder:
|
||||||
@ -615,6 +617,9 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
|||||||
def doc_viewer_finished(self):
|
def doc_viewer_finished(self):
|
||||||
self.doc_viewer = None
|
self.doc_viewer = None
|
||||||
|
|
||||||
|
def open_general_info_dialog(self):
|
||||||
|
GeneralInformationDialog().exec()
|
||||||
|
|
||||||
def geometry_string(self, txt):
|
def geometry_string(self, txt):
|
||||||
if self.dialog_number is None or self.dialog_number == 0:
|
if self.dialog_number is None or self.dialog_number == 0:
|
||||||
return txt
|
return txt
|
||||||
|
@ -754,18 +754,13 @@ Selecting a function will show only that function's documentation</string>
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="some_label">
|
<widget class="QPushButton" name="general_info_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>See tooltip for general information</string>
|
<string>General &Information</string>
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><p>When using functions in a Single function mode template,
|
<string>Click this button to see general help about using template functions
|
||||||
for example {title:uppercase()}, the first parameter 'value' is omitted. It is automatically replaced
|
and how the are documented.</string>
|
||||||
by the value of the specified field.</p> <p>In all the other modes the value parameter
|
|
||||||
must be supplied.</p></string>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
114
src/calibre/gui2/dialogs/template_general_info.py
Normal file
114
src/calibre/gui2/dialogs/template_general_info.py
Normal 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]
|
||||||
|
'''
|
@ -43,6 +43,7 @@ from calibre.gui2.auto_add import AutoAdder
|
|||||||
from calibre.gui2.changes import handle_changes
|
from calibre.gui2.changes import handle_changes
|
||||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||||
from calibre.gui2.device import DeviceMixin
|
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.dialogs.message_box import JobError
|
||||||
from calibre.gui2.ebook_download import EbookDownloadMixin
|
from calibre.gui2.ebook_download import EbookDownloadMixin
|
||||||
from calibre.gui2.email import EmailMixin
|
from calibre.gui2.email import EmailMixin
|
||||||
@ -329,6 +330,13 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
action=self.alt_esc_action)
|
action=self.alt_esc_action)
|
||||||
self.alt_esc_action.triggered.connect(self.clear_additional_restriction)
|
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 ########################
|
# ###################### Start spare job server ########################
|
||||||
QTimer.singleShot(1000, self.create_spare_pool)
|
QTimer.singleShot(1000, self.create_spare_pool)
|
||||||
|
|
||||||
@ -462,6 +470,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
def esc(self, *args):
|
def esc(self, *args):
|
||||||
self.search.clear()
|
self.search.clear()
|
||||||
|
|
||||||
|
def open_ff_doc_editor(self):
|
||||||
|
FFDocEditor(False).exec()
|
||||||
|
|
||||||
def focus_current_view(self):
|
def focus_current_view(self):
|
||||||
view = self.current_view()
|
view = self.current_view()
|
||||||
if view is self.library_view:
|
if view is self.library_view:
|
||||||
|
@ -41,10 +41,10 @@ class Node:
|
|||||||
return self._children
|
return self._children
|
||||||
|
|
||||||
def text(self):
|
def text(self):
|
||||||
return self._text
|
return self._text.replace('\\', '')
|
||||||
|
|
||||||
def escaped_text(self):
|
def escaped_text(self):
|
||||||
return prepare_string_for_xml(self._text)
|
return prepare_string_for_xml(self.text())
|
||||||
|
|
||||||
|
|
||||||
class BlankLineNode(Node):
|
class BlankLineNode(Node):
|
||||||
@ -248,6 +248,7 @@ class FFMLProcessor:
|
|||||||
|
|
||||||
:return: a parse tree for the document
|
:return: a parse tree for the document
|
||||||
"""
|
"""
|
||||||
|
self.input_line = 1
|
||||||
self.input = doc
|
self.input = doc
|
||||||
self.input_pos = 0
|
self.input_pos = 0
|
||||||
self.document_name = name
|
self.document_name = name
|
||||||
@ -404,13 +405,16 @@ class FFMLProcessor:
|
|||||||
self.document = DocumentNode()
|
self.document = DocumentNode()
|
||||||
self.input = None
|
self.input = None
|
||||||
self.input_pos = 0
|
self.input_pos = 0
|
||||||
self.input_line = 1
|
|
||||||
|
|
||||||
def error(self, message):
|
def error(self, message):
|
||||||
raise ValueError(f'{message} on line {self.input_line} in "{self.document_name}"')
|
raise ValueError(f'{message} on line {self.input_line} in "{self.document_name}"')
|
||||||
|
|
||||||
def find(self, for_what):
|
def find(self, for_what):
|
||||||
p = self.input.find(for_what, self.input_pos)
|
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
|
return -1 if p < 0 else p - self.input_pos
|
||||||
|
|
||||||
def move_pos(self, to_where):
|
def move_pos(self, to_where):
|
||||||
@ -450,7 +454,9 @@ class FFMLProcessor:
|
|||||||
return len(self.input)
|
return len(self.input)
|
||||||
|
|
||||||
def get_code_block(self):
|
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]')
|
end = self.find('[/CODE]')
|
||||||
if end < 0:
|
if end < 0:
|
||||||
self.error('Missing [/CODE] for block')
|
self.error('Missing [/CODE] for block')
|
||||||
@ -469,6 +475,11 @@ class FFMLProcessor:
|
|||||||
self.move_pos(end + len('``'))
|
self.move_pos(end + len('``'))
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
def get_escaped_char(self):
|
||||||
|
node = EscapedCharNode(self.text_to(1))
|
||||||
|
self.move_pos(1)
|
||||||
|
return node
|
||||||
|
|
||||||
def get_gui_label(self):
|
def get_gui_label(self):
|
||||||
self.move_pos(len(':guilabel:`'))
|
self.move_pos(len(':guilabel:`'))
|
||||||
end = self.find('`')
|
end = self.find('`')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user