Register Guidelines E-Books Today's Posts Search

Go Back   MobileRead Forums > E-Book Software > Calibre > Plugins

Notices

Reply
 
Thread Tools Search this Thread
Old 04-29-2024, 12:21 PM   #91
thiago.eec
Wizard
thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.
 
Posts: 1,208
Karma: 1419583
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite, Kindle Oasis
Quote:
Originally Posted by capink View Post
I uploaded a new version that should fix both issues. Please try and report back.
All good now. Thanks.
thiago.eec is offline   Reply With Quote
Old 07-02-2024, 07:55 PM   #92
zencricket
Junior Member
zencricket began at the beginning.
 
Posts: 4
Karma: 10
Join Date: Jul 2024
Device: kindle paperwhite
Thank you, @capink, for your plugins.

I have some editor chain actions defined and working from the editor, but when I try to get them to run from the action chain plugin nothing happens.

I did get one action to work by defining a new editor chain action in the action chain UI, but nothing happens if I use an event to trigger it. The new editor chain action also does not show up in the editor chain plugin. I don't want to have to maintain two separate definitions.

I just started using the plugins today, so I'm pretty sure I'm doing something wrong.

Using Calibre 7.13 on an M1 MacBook Air.
zencricket is offline   Reply With Quote
Advert
Old 07-03-2024, 03:22 AM   #93
capink
Wizard
capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.
 
Posts: 1,194
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
Quote:
Originally Posted by zencricket View Post
I have some editor chain actions defined and working from the editor, but when I try to get them to run from the action chain plugin nothing happens.
Not all editor chains can be run from action chains. This depends on the actions as well as the settings of individual actions. This happens mainly when an action depends on elements of Editor GUI which won't be available when run from Action Chains.

For example, a chain containing an action like "Mark Cursor Tag" will not run at all because that action always depends on elements of the editor GUI, which are not available in Action Chains.

Another example of this, is an action (e.g. Search And Replace), configured with a scope that depends on the editor GUI, e.g. "current file" or "selected files". You can run the same action from Action Chains with a scope that does not depend on the Editor's GUI, e.g. "text files"

Quote:
Originally Posted by zencricket View Post
I did get one action to work by defining a new editor chain action in the action chain UI, but nothing happens if I use an event to trigger it.
Run calibre in debug mode and post the output.
Quote:
Originally Posted by zencricket View Post
The new editor chain action also does not show up in the editor chain plugin. I don't want to have to maintain two separate definitions.
If you want to have only one version, you must define it in the editor. Editor Chains cannot be aware of chains defined outside it.

Last edited by capink; 07-03-2024 at 03:53 AM.
capink is offline   Reply With Quote
Old 07-03-2024, 04:19 PM   #94
zencricket
Junior Member
zencricket began at the beginning.
 
Posts: 4
Karma: 10
Join Date: Jul 2024
Device: kindle paperwhite
Quote:
Originally Posted by capink View Post
Not all editor chains can be run from action chains. This depends on the actions as well as the settings of individual actions. This happens mainly when an action depends on elements of Editor GUI which won't be available when run from Action Chains.

For example, a chain containing an action like "Mark Cursor Tag" will not run at all because that action always depends on elements of the editor GUI, which are not available in Action Chains.

Another example of this, is an action (e.g. Search And Replace), configured with a scope that depends on the editor GUI, e.g. "current file" or "selected files". You can run the same action from Action Chains with a scope that does not depend on the Editor's GUI, e.g. "text files"



Run calibre in debug mode and post the output.


If you want to have only one version, you must define it in the editor. Editor Chains cannot be aware of chains defined outside it.
Got the actions working. I changed to scope to "text files" and it runs when triggered manually. When I tie it to the FanFicFare event I see the action run in the logs, but no changes are made when it is complete. Any ideas?

thx!

Calibre Debug Log:
Code:
ApplicationPaletteChange event ignored
Using calibre Qt style: True
2024-07-03 15:03:57.716 calibre-debug[77060:25970649] WARNING: Secure coding is not enabled for restorable state! Enable secure coding by implementing NSApplicationDelegate.applicationSupportsSecureRestorableState: and returning YES.
Failed to load resource: 'commit.txt' from the plugin zip file: /Users/danielgovier/Library/Preferences/calibre/plugins/OverDrive Libby.zip
Traceback (most recent call last):
  File "calibre/customize/zipplugin.py", line 46, in get_resources
  File "zipfile.py", line 1510, in read
  File "zipfile.py", line 1547, in open
  File "zipfile.py", line 1476, in getinfo
KeyError: "There is no item named 'commit.txt' in the archive"
[overdrive_libby/0.1.9] Loaded 0 items from file cache /Users/danielgovier/Library/Preferences/calibre/plugins/overdrive_libby.libraries.json
[overdrive_libby/0.1.9] Loaded 0 items from file cache /Users/danielgovier/Library/Preferences/calibre/plugins/overdrive_libby.media.json
EpubMerge: DEBUG: 2024-07-03 15:03:58,896: calibre_plugins.epubmerge.epubmerge_plugin(156): macmenuhack file_path:/Users/danielgovier/Library/Preferences/calibre/plugins/fanficfare_macmenuhack.txt
FFF: DEBUG: 2024-07-03 15:03:58,900: calibre_plugins.fanficfare_plugin.fff_plugin(217): Plugin FanFicFare macmenuhack file_path:/Users/danielgovier/Library/Preferences/calibre/plugins/fanficfare_macmenuhack.txt
calibre Debug log
calibre 7.13  embedded-python: True
macOS-14.5-arm64-arm-64bit Darwin ('64bit', '')
('Darwin', '23.5.0', 'Darwin Kernel Version 23.5.0: Wed May  1 20:16:51 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T8103')
Python 3.11.5
OSX: ('14.5', ('', '', ''), 'arm64')
Interface language: None
EXE path: /Applications/calibre.app/Contents/MacOS/calibre-debug
Successfully initialized third party plugins: Action Chains (1, 20, 7) && Count Pages (1, 13, 6) && Editor Chains (1, 1, 7) && EpubMerge (2, 18, 0) && EpubSplit (3, 7, 0) && FanFicFare (4, 36, 0) && Generate Cover (2, 3, 4) && OverDrive Libby (0, 1, 9) && Overdrive Link (2, 57, 0) && Reading List (1, 15, 4) && ePub Extended Metadata {Writer} (0, 11, 2) && ePub Extended Metadata {Reader} (0, 11, 2) && ePub Extended Metadata (0, 11, 2)
calibre 7.13  embedded-python: True
macOS-14.5-arm64-arm-64bit Darwin ('64bit', '')
('Darwin', '23.5.0', 'Darwin Kernel Version 23.5.0: Wed May  1 20:16:51 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T8103')
Python 3.11.5
OSX: ('14.5', ('', '', ''), 'arm64')
Interface language: None
EXE path: /Applications/calibre.app/Contents/MacOS/calibre-debug
Successfully initialized third party plugins: Action Chains (1, 20, 7) && Count Pages (1, 13, 6) && Editor Chains (1, 1, 7) && EpubMerge (2, 18, 0) && EpubSplit (3, 7, 0) && FanFicFare (4, 36, 0) && Generate Cover (2, 3, 4) && OverDrive Libby (0, 1, 9) && Overdrive Link (2, 57, 0) && Reading List (1, 15, 4) && ePub Extended Metadata {Writer} (0, 11, 2) && ePub Extended Metadata {Reader} (0, 11, 2) && ePub Extended Metadata (0, 11, 2)
QPA platform: cocoa
devicePixelRatio: 2.0
logicalDpi: 72.0 x 72.0
physicalDpi: 127.50000191485789 x 127.50000191485789
[0.00] Starting up...
[0.01] Showing splash screen...
[0.16] splash screen shown
[0.16] Initializing db...
[0.20] db initialized
[0.20] Constructing main UI...
[1.16] GUI main window shown
[1.21] main UI initialized...
[1.21] Hiding splash screen
Action chains: _get_plugins_resources(): start
Editor chains: _get_plugins_resources(): start
Editor Chains: _get_plugins_resources(): finished
Editor Chains: actions that does not run headless: ['Mark Cursor Tag', 'Insert Before Cursor Tag', 'Insert After Cursor Tag']
calling method (on_modules_update) of obj (Tag Actions)
Editor Chains: Tag Actions: running on_modules_update()
calling method (on_modules_update) of obj (Style Actions)
Editor Chains: Style Actions: running on_modules_update()
Editor chains: _call_plugins_on_modules_update(): start
Editor Chains: _call_plugins_on_modules_update(): finished
Action Chains: _get_plugins_resources(): finished
Action chains: formulas: running on_templates_update()
Action chains: _call_plugins_on_modules_update(): start
Action Chains: _call_plugins_on_modules_update(): finished
FFF: DEBUG: 2024-07-03 15:04:16,694: adapter_literotica.py(40): LiteroticaComAdapter:__init__ - url='https://www.literotica.com/s/because-8'
FFF: DEBUG: 2024-07-03 15:04:22,492: calibre_plugins.fanficfare_plugin.dialogs(493): self.extraoptions['anthology_url']:NOT FOUND
FFF: DEBUG: 2024-07-03 15:04:22,497: adapter_literotica.py(40): LiteroticaComAdapter:__init__ - url='https://www.literotica.com/s/because-8'
FFF: DEBUG: 2024-07-03 15:04:22,498: calibre_plugins.fanficfare_plugin.fff_plugin(1151): FanFicFare v4.36.0
Failed to request permission for showing notification: The operation couldn’t be completed. (UNErrorDomain error 1.)
FFF: INFO: 2024-07-03 15:04:22,604: calibre_plugins.fanficfare_plugin.prefs(216): Using default settings
FFF: DEBUG: 2024-07-03 15:04:22,615: adapter_literotica.py(40): LiteroticaComAdapter:__init__ - url='https://www.literotica.com/s/because-8'
FFF: DEBUG: 2024-07-03 15:04:22,617: configurable.py(1080): use_browser_cache:
FFF: DEBUG: 2024-07-03 15:04:22,617: configurable.py(1100): use_basic_cache:true
FFF: DEBUG: 2024-07-03 15:04:22,617: adapter_literotica.py(110): Chapter/Story URL: <https://www.literotica.com/s/because-8> 
FFF: DEBUG: 2024-07-03 15:04:22,618: cache_basic.py(116): 
========== MISS (GET) BasicCache
https://www.literotica.com/s/because-8
FFF: DEBUG: 2024-07-03 15:04:22,618: fetcher_requests.py(114): 
---------- REQ (GET) RequestsFetcher
https://www.literotica.com/s/because-8
FFF: DEBUG: 2024-07-03 15:04:22,966: fetcher_requests.py(127): response code:200
FFF: DEBUG: 2024-07-03 15:04:22,966: decorators.py(112): fromcache:False
FFF: DEBUG: 2024-07-03 15:04:22,966: requestable.py(55): Encoding:utf8
FFF: DEBUG: 2024-07-03 15:04:22,968: adapter_literotica.py(119): set opened url:https://www.literotica.com/s/because-8
FFF: DEBUG: 2024-07-03 15:04:23,034: adapter_literotica.py(141): One-shot
FFF: DEBUG: 2024-07-03 15:04:23,069: calibre_plugins.fanficfare_plugin.fff_plugin(1468): from URL(https://www.literotica.com/s/because-8)
FFF: DEBUG: 2024-07-03 15:04:23,091: calibre_plugins.fanficfare_plugin.fff_plugin(1673): title:Because
FFF: DEBUG: 2024-07-03 15:04:23,092: calibre_plugins.fanficfare_plugin.fff_plugin(1674): outfile:/var/folders/xb/cm77xljj4qd0vb6sglr_12sh0000gn/C/calibre_7.13.0_tmp_k5vxfe6b/fanficfare_3jzhtftt/Because-Harddaysknight-3f4mp2t8.epub
Failed to request permission for showing notification: The operation couldn’t be completed. (UNErrorDomain error 1.)
Failed to request permission for showing notification: The operation couldn’t be completed. (UNErrorDomain error 1.)
FFF: DEBUG: 2024-07-03 15:04:31,561: calibre_plugins.fanficfare_plugin.fff_plugin(1810): add/update Because https://www.literotica.com/s/because-8 id(None)
FFF: DEBUG: 2024-07-03 15:04:31,582: calibre_plugins.fanficfare_plugin.fff_plugin(1825): Attempting metadata update
FFF: DEBUG: 2024-07-03 15:04:31,598: adapter_literotica.py(40): LiteroticaComAdapter:__init__ - url='https://www.literotica.com/s/because-8'
FFF: DEBUG: 2024-07-03 15:04:31,600: calibre_plugins.fanficfare_plugin.fff_plugin(2495): has link_map:True
FFF: INFO: 2024-07-03 15:04:31,600: calibre_plugins.fanficfare_plugin.fff_plugin(2509): cover_image:
FFF: DEBUG: 2024-07-03 15:04:31,741: calibre_plugins.fanficfare_plugin.fff_plugin(1909): Finished Adding/Updating 1 books.
FFF: DEBUG: 2024-07-03 15:04:31,742: calibre_plugins.fanficfare_plugin.fff_plugin(1912): removed tdir
Failed to request permission for showing notification: The operation couldn’t be completed. (UNErrorDomain error 1.)
Failed to request permission for showing notification: The operation couldn’t be completed. (UNErrorDomain error 1.)
FFF: DEBUG: 2024-07-03 15:04:31,870: calibre_plugins.fanficfare_plugin.fff_plugin(1940): Starting auto conversion of 1 books.
Failed to request permission for showing notification: The operation couldn’t be completed. (UNErrorDomain error 1.)
Failed to request permission for showing notification: The operation couldn’t be completed. (UNErrorDomain error 1.)
zencricket is offline   Reply With Quote
Old 07-03-2024, 07:26 PM   #95
capink
Wizard
capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.
 
Posts: 1,194
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
Quote:
Originally Posted by zencricket View Post
When I tie it to the FanFicFare event I see the action run in the logs, but no changes are made when it is complete. Any ideas?
Cannot tell exactly without knowing seeing the chains and the files they are acting on.

First question that jumps to mind, does your chain select which books to act on? I don't use FFF plugin myself, so I cannot really help you much with that. I presume the plugin downloads new epubs, in which case you can use "Selection Modifier" action to select books modified since the start of the chain.

Edit: This won't work because the books are modified before the start of the chain. Ask in the FFF thread for help on how to select books.

Last edited by capink; 07-04-2024 at 05:13 AM.
capink is offline   Reply With Quote
Advert
Old 06-14-2025, 09:42 AM   #96
thiago.eec
Wizard
thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.
 
Posts: 1,208
Karma: 1419583
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite, Kindle Oasis
Just came back to say again: Thanks for the great plugin. I use it a lot.

With it, I was able to create:
1) 'Clean-up' chain that:
- Create a folder structure;
- Remove unwanted files, embedded calibre metadata, Sigil and KindleUnpack artifacts;
- Fix all HTMLs;
- Use full page SVGs for covers;
- Convert NAV landmarks and OPF guide to pt-BR;
- Remove 'lang' and 'xml:lang' from <body> tag;
- Fix missing 'alt' attribute on <img> tags;
- Remove 'linear="no"' from OPF;
- Customizable 'Pretty print all files' (adpted from calibre code, to avoid certain things, like adding spaces between the lines);
- Run 'Check Book' at the end
2) Merge splited files automatically (thanks for the chain!)
3) Run 'Check Book' and fix all errors automatically
4) Replace invalids IDs
5) A customizable 'Upgrade to EPUB3' based on calibre orginal tool, that can keep the NCX and the <guide> element (in the OPF). This gives me maximum backward compatibility

I can't thank you enough for this plugin. Made my life a lot easier. =)
thiago.eec is offline   Reply With Quote
Old 06-14-2025, 04:26 PM   #97
capink
Wizard
capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.
 
Posts: 1,194
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
Quote:
Originally Posted by thiago.eec View Post
Just came back to say again: Thanks for the great plugin. I use it a lot.

With it, I was able to create:
1) 'Clean-up' chain that:
- Create a folder structure;
- Remove unwanted files, embedded calibre metadata, Sigil and KindleUnpack artifacts;
- Fix all HTMLs;
- Use full page SVGs for covers;
- Convert NAV landmarks and OPF guide to pt-BR;
- Remove 'lang' and 'xml:lang' from <body> tag;
- Fix missing 'alt' attribute on <img> tags;
- Remove 'linear="no"' from OPF;
- Customizable 'Pretty print all files' (adpted from calibre code, to avoid certain things, like adding spaces between the lines);
- Run 'Check Book' at the end
2) Merge splited files automatically (thanks for the chain!)
3) Run 'Check Book' and fix all errors automatically
4) Replace invalids IDs
5) A customizable 'Upgrade to EPUB3' based on calibre orginal tool, that can keep the NCX and the <guide> element (in the OPF). This gives me maximum backward compatibility

I can't thank you enough for this plugin. Made my life a lot easier. =)
My pleasure. Thanks for the kind words and for taking the time to write the post. A lot of the actions in the plugin are heavily based on code written by Kovid, without whom none of this would have been possible. So a big thank you to Kovid for his work.

If it is not much trouble for you, please export your chain and post it here, so any one who might be interested can use it to his benefit.

You talked about two customized actions: 'Pretty print all files' and 'Upgrade to EPUB3'. These can be done using either the "Run python code" action, or can be done as separate modules. If it is the former, the code will be available in the exported chain, if it is the latter, please post the code of the modules. When I have the time, I will see if I can use the code to modify the plugin actions to incorporate them as configurable options. This is a busy period for me, so I am not sure when I will be able to do it. Thanks.
capink is offline   Reply With Quote
Old 06-14-2025, 04:32 PM   #98
thiago.eec
Wizard
thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.
 
Posts: 1,208
Karma: 1419583
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite, Kindle Oasis
Quote:
Originally Posted by capink View Post
A lot of the actions in the plugin are heavily based on code written by Kovid, without whom none of this would have been possible. So a big thank you to Kovid for his work.
That goes without saying. @Kovid rocks and calibre is my most used tool on my daily routine.

Quote:
Originally Posted by capink View Post
If it is not much trouble for you, please export your chain and post it here, so any one who might be interested can use it to his benefit.
Sure, I'll send them in next post.
thiago.eec is offline   Reply With Quote
Old 06-14-2025, 04:53 PM   #99
thiago.eec
Wizard
thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.
 
Posts: 1,208
Karma: 1419583
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite, Kindle Oasis
My modules:
  1. Show all editor actions [module name: editor_actions](by @capink)
    Spoiler:

    Code:
    #!/usr/bin/env python
    # ~*~ coding: utf-8 ~*~
    from __future__ import (unicode_literals, division, absolute_import,
                            print_function)
    
    import re
    
    # python 3 compatibility
    from six import text_type as unicode
    
    from PyQt5.Qt import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout,
                          QDialog, QDialogButtonBox, QMenu, QPainter, QPoint, QPixmap,
                          QSize, QIcon, QTreeWidgetItem, QTreeWidget, QAbstractItemView,
                          QGroupBox, QCheckBox, QLabel, QAction)
    
    from calibre import prints
    from calibre.constants import DEBUG
    from calibre.gui2 import error_dialog
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    from calibre_plugins.editor_chains.common_utils import get_icon
    
    try:
        load_translations()
    except NameError:
        prints("EditorChains::actions/editor_actions.py - exception when loading translations")
    
    
    EXCLUDED_GROUPS = [
        _('Editor Chains'),
        _('Editor actions'),
        _('Windows'),
        _('Preview'),
        _('Search')
    ]
    
    ICON_SIZE = 32
    
    
    def group_unique_names(gui, group):
        menu_actions = [x for x in gui.__dict__.values() if isinstance(x, QAction)]
        menu_unique_names = [re.sub(r'^action\-', r'', x.objectName()) for x in menu_actions]
        if group == 'Plugins':
            unique_names = [x for x in gui.keyboard.groups['Plugins']]
        else:
            unique_names = [x for x in gui.keyboard.groups[group] if x in menu_unique_names]
        
        return unique_names
    
    def get_qaction_from_unique_name(gui, unique_name):
        shortcut = gui.keyboard.shortcuts[unique_name]
        ac = shortcut['action']
        return ac
    
    
    def allowed_unique_names(gui):
        unique_names = []
        for group in gui.keyboard.groups.keys():
            unique_names.extend(group_unique_names(gui, group))
        unique_names += [x for x in gui.keyboard.groups['Plugins']]
        return unique_names
    
    class Item(QTreeWidgetItem):
        pass
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self.gui = plugin_action.gui
            self.all_nodes = {}
            # used to keep track of selected item and allow only one selection
            self.checked_item = None
            
            self._init_controls()
    
        def _init_controls(self):
            self.blank_icon = QIcon(I('blank.png'))
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
    #        opts_groupbox = QGroupBox()
    #        l.addWidget(opts_groupbox)
    #        opts_groupbox_layout = QVBoxLayout()
    #        opts_groupbox.setLayout(opts_groupbox_layout)
    #        cursor_chk = self.cursor_chk = QCheckBox(_('Disable wait cursor for this action.'))
    #        opts_groupbox_layout.addWidget(cursor_chk)
    #        cursor_chk.setChecked(False)
            
            self.tv = QTreeWidget(self.gui)
            self.tv.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
            self.tv.header().hide()
            self.tv.setSelectionMode(QAbstractItemView.SingleSelection)
            l.addWidget(self.tv, 1)
            self.tv.itemChanged.connect(self._tree_item_changed)
    
            self._populate_actions_tree()
    
        def _get_scaled_icon(self, icon):
            if icon.isNull():
                return self.blank_icon
            # We need the icon scaled to 16x16
            src = icon.pixmap(ICON_SIZE, ICON_SIZE)
            if src.width() == ICON_SIZE and src.height() == ICON_SIZE:
                return icon
            # Need a new version of the icon
            pm = QPixmap(ICON_SIZE, ICON_SIZE)
            pm.fill(Qt.transparent)
            p = QPainter(pm)
            p.drawPixmap(QPoint(int((ICON_SIZE - src.width()) / 2), int((ICON_SIZE - src.height()) / 2)), src)
            p.end()
            return QIcon(pm)
    
        def set_checked_state(self, item, col, state):
            item.setCheckState(col, state)
            if state == Qt.Checked:
                self.checked_item = item
            else:
                self.checked_item = None
    
        def _populate_actions_tree(self):
    
            self.top_level_items_map = {}
            for group in sorted(self.gui.keyboard.groups.keys()):
                if group in EXCLUDED_GROUPS: continue
                
                # Create a node for our top level plugin name
                tl = Item()
                tl.setText(0, group)
              
                # Normal top-level checkable plugin iaction handling
                tl.setFlags(Qt.ItemIsEnabled)
                
                #unique_names = self.gui.keyboard.groups[group]
                unique_names = group_unique_names(self.gui, group)
                
                # Iterate through all the children of this node
                self._populate_action_children(unique_names, tl)
                self.tv.addTopLevelItem(tl)
    
        def _populate_action_children(self, unique_names, parent):
            for unique_name in unique_names:
                ac = get_qaction_from_unique_name(self.gui, unique_name)
                text = ac.text().replace('&', '')
    
                it = Item(parent)
                it.setText(0, text)
                it.setData(0, Qt.UserRole, unique_name)
                it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
                it.setCheckState(0, Qt.Unchecked)
                it.setIcon(0, self._get_scaled_icon(ac.icon()))
    
                self.all_nodes[unique_name] = it
    
        def _tree_item_changed(self, item, column):
            # Checkstate has been changed
    
            is_checked = item.checkState(column) == Qt.Checked
            #text = unicode(item.text(column)).replace('&', '')
            unique_name = item.data(column, Qt.UserRole)
    
            if is_checked:
                # un-check previously checked item if any
                if self.checked_item:
                    self.tv.blockSignals(True)
                    self.checked_item.setCheckState(0, Qt.Unchecked)
                    self.tv.blockSignals(False)
                
                self.checked_item = item
            else:
                self.checked_item = None
    
        def _repopulate_actions_tree(self):
            self.tv.clear()
            self._populate_actions_tree()
    
        def load_settings(self, settings):
            if settings:
                #self.cursor_chk.setChecked(settings.get('disable_busy_cursor', False))
                #self._repopulate_actions_tree()
                self.set_unique_name_checked(settings['unique_name'])
    
        def save_settings(self):
            settings = {}
            settings['unique_name'] = self.checked_item.data(0, Qt.UserRole)
            #settings['disable_busy_cursor'] = self.cursor_chk.isChecked()
            return settings
    
        def set_unique_name_checked(self, unique_name):
            it = self.all_nodes.get(unique_name)
            if it:
                self.set_checked_state(it, 0, Qt.Checked)
                self.tv.setCurrentItem(it)
    
    class EditorActions(EditorAction):
    
        name = 'Editor Actions'
        #_is_builtin = True
    
        def run(self, chain, settings):
            
            ac = get_qaction_from_unique_name(chain.gui, settings['unique_name'])
            if ac:
                if DEBUG:
                    prints('Editor Chains: Editor Actions: Found action: {}'.format(ac.text().replace('&', ''))) 
    
                cursor = Qt.WaitCursor
                if settings.get('disable_busy_cursor'):
                    cursor = Qt.ArrowCursor
                
                QApplication.setOverrideCursor(cursor)
                try:
                    ac.trigger()
                       
                finally:
                    QApplication.restoreOverrideCursor()
                
            if DEBUG:
                prints('Editor Chains: Editor Actions: Finishing run action') 
    
        def validate(self, settings):
            gui = self.plugin_action.gui
            if not settings:
                return (_('Settings Error'), _('You must configure this action before running it'))
            if settings['unique_name'] not in allowed_unique_names(gui):
                return (_('Settings Error'), _('Action cannot be found: {}'.format(settings['unique_name'])))
            return True
    
        def config_widget(self):
            return ConfigWidget
  2. Run 'Check book' and fix all fixable errors [module name: fix_all]
    Spoiler:

    Code:
    from qt.core import (Qt, QApplication)
    
    from calibre.ebooks.oeb.polish.check.main import fix_errors, run_checks
    from calibre_plugins.editor_chains.actions.base import EditorAction
    
    class FixAll(EditorAction):
    
        name = 'Fix all errors'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            container = chain.current_container
            QApplication.setOverrideCursor(Qt.WaitCursor)
            QApplication.processEvents()
            errors = run_checks(container)
            fix_errors(container, errors)
            QApplication.restoreOverrideCursor()
  3. Merge files [module name: merge_files](by @capink)
    Spoiler:

    Code:
    import re
    
    from qt.core import (QWidget, QVBoxLayout, QGroupBox)
    
    from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
    from calibre.ebooks.oeb.polish.split import merge
    from calibre.ebooks.oeb.polish.replace import rename_files
    from calibre.gui2.tweak_book import editors
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    from calibre_plugins.editor_chains.scope import scope_names, ScopeWidget, validate_scope, scope_is_headless
    
    
    def spine_index(container, name):
        idx = 0
        for spine_name, is_linear in container.spine_names:
            idx +=1
            if name == spine_name:
                return idx
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self._init_controls()
    
        def _init_controls(self):
    
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
            scope_groupbox = QGroupBox(_('Files to merge'))
            l.addWidget(scope_groupbox)
            scope_l = QVBoxLayout()
            scope_groupbox.setLayout(scope_l)
            self.where_box = ScopeWidget(self, self.plugin_action, orientation='vertical',
                                         headless=self.plugin_action.gui is None,
                                         hide=['selected-text','open','current'])
            scope_l.addWidget(self.where_box)
    
            l.addStretch(1)
            self.resize(self.sizeHint())
    
        def load_settings(self, settings):
            if settings:
                self.where_box.where = settings['where']
    
        def save_settings(self):
            settings = {}
            settings['where'] = self.where_box.where
            return settings
    
    class MergeFiles(EditorAction):
    
        name = 'Merge Files'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            names = scope_names(chain, settings['where'])
            print(f'DEBUG: Merging Files\n       names: {names}')
            if len(names) < 2:
                print('Error: You need to select at least 2 files to merge')
                return
            container = chain.current_container
            spine_names = [name for name, is_linear in container.spine_names]
            if names[0] in set(container.manifest_items_of_type(OEB_DOCS)):
                category = 'text'
            elif names[0] in set(container.manifest_items_of_type(OEB_STYLES)):
                category = 'styles'
            else:
                print('Error: Can only merge files of categories: text or styles')
                return
            if category == 'text':
                if not set(names).issubset(set(container.manifest_items_of_type(OEB_DOCS))):
                    print('Error: some files are your trying to merge are not of category text')
                    return
                if not set(names).issubset(set(spine_names)):
                    print('Error: Some files you are trying to merge not in spine')
                    return
                names = sorted(names, key=lambda name: spine_index(container, name))
            else:
                if not set(names).issubset(set(container.manifest_items_of_type(OEB_STYLES))):
                    print('Error: some files are your trying to merge are not of category styles')
                    return
                names = sorted(names)
            master = names[0]
            print(f'DEBUG: Merging Files\n       master: {master}')
    
            # Create groups with the same prefix before merging
            while names:
                master = names[0]
                group = []
                for name in names[:]:
                    master_prefix =  re.search('.*?_split_[^d+]', master)[0]
                    if re.search('.*?_split_[^d+]', name)[0] == master_prefix:
                        group.append(name)
                        names.remove(name)
                merge(container, category, group, master)
                new_name = re.sub('_split_.*?\d+', '', master)
                rename_files(container, {master: new_name})
                #if new_name in editors:
                #    chain.boss.show_editor(new_name)
    
        def validate(self, settings):
            scope_ok = validate_scope(settings['where'])
            if scope_ok is not True:
                return scope_ok
            return True
    
        def config_widget(self):
            return ConfigWidget
    
        def is_headless(self, settings):
            return scope_is_headless(settings['where'])
  4. Pretty print all [module name: pretty_print_all]
    Spoiler:

    Code:
    from qt.core import (Qt, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QCheckBox)
    
    from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
    from calibre.ebooks.oeb.polish import pretty
    from calibre_plugins.editor_chains.actions.base import EditorAction
    
    
    def pretty_block(parent, settings, level=2, indent='  ', indent_level=1):
        ''' Surround block tags with blank lines and recurse into child block tags
        that contain only other block tags '''
        if not parent.text or pretty.isspace(parent.text):
            parent.text = ''
        if settings['block_spaces']:
            nn = '\n' if hasattr(parent.tag, 'strip') and pretty.barename(parent.tag) in {'tr', 'td', 'th'} else '\n\n'
        else:
            nn = '\n'
        parent.text = parent.text + nn + (indent * indent_level * level)
        for i, child in enumerate(parent):
            if pretty.isblock(child) and pretty.has_only_blocks(child):
                pretty_block(child, settings, level=level+1, indent=indent * indent_level)
            elif child.tag == pretty.SVG_TAG:
                pretty_xml_tree(child, level=level, indent=indent, indent_level=indent_level)
            l = level
            if i == len(parent) - 1:
                l -= 1
            if not child.tail or pretty.isspace(child.tail):
                child.tail = ''
            child.tail = child.tail + nn + (indent * indent_level * l)
    
    
    def pretty_xml_tree(elem, level=1, indent='  ', indent_level=1):
        ''' XML beautifier, assumes that elements that have children do not have
        textual content.  Also assumes that there is no text immediately after
        closing tags. These are true for opf/ncx and container.xml files. If either
        of the assumptions are violated, there should be no data loss, but pretty
        printing won't produce optimal results.'''
        if (not elem.text and len(elem) > 0) or (elem.text and pretty.isspace(elem.text)):
            elem.text = '\n' + (indent * indent_level * (level+1))
        for i, child in enumerate(elem):
            pretty_xml_tree(child, level=level+1, indent=indent, indent_level=indent_level)
            if not child.tail or pretty.isspace(child.tail):
                l = level + 1
                if i == len(elem) - 1:
                    l -= 1
                child.tail = '\n' + (indent * indent_level * l)
    
    
    def pretty_html_tree(container, root, settings):
        indent = '  '
        indent_level = settings['indent']
        nn = '\n\n' if settings['root_spaces'] else '\n'
        root.text = nn + indent * indent_level
        for child in root:
            if hasattr(child.tag, 'endswith') and child.tag.endswith('}head'):
                pretty_xml_tree(child, indent_level=indent_level)
                child.tail = nn + indent * indent_level
            else:
                child.tail = nn
    
        for body in root.findall('h:body', namespaces=pretty.XPNSMAP):
            pretty_block(body, settings, indent_level=indent_level)
            # Special case the handling of a body that contains a single block tag
            # with all content. In this case we prettify the containing block tag
            # even if it has non block children.
            if (len(body) == 1 and not callable(body[0].tag) and
            pretty.isblock(body[0]) and
            not pretty.has_only_blocks(body[0]) and
            pretty.barename(body[0].tag) not in ('pre', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6') and
            len(body[0]) > 0):
                pretty_block(body[0], settings, level=2)
    
        if container is not None:
            # Handle <script> and <style> tags
            for child in root.xpath('//*[local-name()="script" or local-name()="style"]'):
                pretty.pretty_script_or_style(container, child)
    
    
    def pretty_all(container, settings):
        ' Pretty print all HTML/CSS/XML files in the container '
        xml_types = {pretty.guess_type('a.ncx'), pretty.guess_type('a.xml'), pretty.guess_type('a.svg')}
        for name, mt in pretty.iteritems(container.mime_map):
            prettied = False
            if mt in OEB_DOCS:
                pretty_html_tree(container, container.parsed(name), settings)
                prettied = True
            elif mt in OEB_STYLES:
                container.parsed(name)
                prettied = True
            elif name == container.opf_name:
                root = container.parsed(name)
                pretty.pretty_opf(root)
                pretty.pretty_xml_tree(root)
                prettied = True
            elif mt in xml_types:
                pretty.pretty_xml_tree(container.parsed(name))
                prettied = True
            if prettied:
                container.dirty(name)
    
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self._init_controls()
    
        def _init_controls(self):
    
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
            h = self.h = QHBoxLayout()
            l.addLayout(h)
            self.label = QLabel(_('Indentation level:'))
            h.addWidget(self.label)
    
            self.indent_level = QSpinBox()
            self.indent_level.setRange(1, 10)
            h.addWidget(self.indent_level)
    
            self.root_spaces = QCheckBox(_('Add spaces for head/body'))
            l.addWidget(self.root_spaces)
    
            self.block_spaces = QCheckBox(_('Add spaces between blocks'))
            l.addWidget(self.block_spaces)
    
            #l.addStretch(1)
    
            #self.setMinimumSize(300,110)
            self.resize(self.sizeHint())
    
        def load_settings(self, settings):
            if settings:
                self.indent_level.setValue(settings['indent'])
                self.root_spaces.setChecked(settings['root_spaces'])
                self.block_spaces.setChecked(settings['block_spaces'])
    
        def save_settings(self):
            settings = {}
            settings['indent'] = self.indent_level.value()
            settings['root_spaces'] = self.root_spaces.isChecked()
            settings['block_spaces'] = self.block_spaces.isChecked()
            return settings
    
    
    class BeautifyAll(EditorAction):
    
        name = 'Pretty print ALL'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            container = chain.current_container
            QApplication.setOverrideCursor(Qt.WaitCursor)
            QApplication.processEvents()
            pretty_all(container, settings)
            QApplication.restoreOverrideCursor()
    
        def validate(self, settings):
            if not settings:
                return _('No settings'), _('You must first configure this action')
            return True
    
        def config_widget(self):
            return ConfigWidget
  5. Replace invalid IDs [module name: replace_ids]
    Spoiler:

    Code:
    from qt.core import (Qt, QApplication)
    
    from calibre.ebooks.oeb.polish.check import parsing
    from calibre.ebooks.oeb.polish.check.main import fix_errors
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    
    class ReplaceIDs(EditorAction):
    
        name = 'Replace IDs'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            container = chain.current_container
            QApplication.setOverrideCursor(Qt.WaitCursor)
            QApplication.processEvents()
            errors = parsing.check_ids(container)
            changed = fix_errors(container, errors)
            QApplication.restoreOverrideCursor()
  6. Select files: this is used to convert Editor Chain's 'escope' to a selection [module name: select_files] (by @capink)
    Spoiler:

    Code:
    import re
    
    from qt.core import (QWidget, QVBoxLayout, QGroupBox)
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    from calibre_plugins.editor_chains.scope import scope_names, ScopeWidget, validate_scope, scope_is_headless
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self._init_controls()
    
        def _init_controls(self):
    
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
            scope_groupbox = QGroupBox(_('Files to select'))
            l.addWidget(scope_groupbox)
            scope_l = QVBoxLayout()
            scope_groupbox.setLayout(scope_l)
            self.where_box = ScopeWidget(self, self.plugin_action, orientation='vertical',
                                         headless=self.plugin_action.gui is None,
                                         hide=['selected-text','open','current'])
            scope_l.addWidget(self.where_box)
    
            l.addStretch(1)
            self.setMinimumSize(400,200)
    
        def load_settings(self, settings):
            if settings:
                self.where_box.where = settings['where']
    
        def save_settings(self):
            settings = {}
            settings['where'] = self.where_box.where
            return settings
    
    class SelectFiles(EditorAction):
    
        name = 'Select Files'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            names = scope_names(chain, settings['where'])
            chain.gui.file_list.select_names(group)
    
        def validate(self, settings):
            scope_ok = validate_scope(settings['where'])
            if scope_ok is not True:
                return scope_ok
            return True
    
        def config_widget(self):
            return ConfigWidget
    
        def is_headless(self, settings):
            return scope_is_headless(settings['where'])
  7. Upgrade to EPUB3 [module name: upgrade]
    Spoiler:

    Code:
    from qt.core import (QApplication, Qt, QWidget, QVBoxLayout,
                         QCheckBox, QLabel)
    
    from calibre import prints
    from calibre.constants import DEBUG
    from calibre.ebooks.oeb.polish.main import tweak_polish
    from calibre.ebooks.oeb.polish import upgrade
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    
    
    try:
        load_translations()
    except NameError:
        prints("EditorChains::actions/upgrade_internals.py - exception when loading translations")
    
    
    def epub_2_to_3(container, previous_nav=None, remove_ncx=False, remove_guide=False):
        upgrade.upgrade_metadata(container.opf)
        upgrade.collect_properties(container)
        toc = upgrade.get_toc(container)
        toc_name = upgrade.find_existing_ncx_toc(container)
        if toc_name and remove_ncx:
            container.remove_item(toc_name)
            container.opf_xpath('./opf:spine')[0].attrib.pop('toc', None)
        landmarks = upgrade.get_landmarks(container)
        if remove_guide:
            for guide in container.opf_xpath('./opf:guide'):
                guide.getparent().remove(guide)
        upgrade.create_nav(container, toc, landmarks, previous_nav)
        container.opf.set('version', '3.0')
        if upgrade.fix_font_mime_types(container):
            container.refresh_mime_map()
        upgrade.migrate_obfuscated_fonts(container)
        container.dirty(container.opf_name)
    
    
    def upgrade_book(container, remove_ncx=False, remove_guide=False):
        if container.book_type != 'epub' or container.opf_version_parsed.major >= 3:
            return False
        epub_2_to_3(container, remove_ncx=remove_ncx, remove_guide=remove_guide)
        return True
    
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action, chain_name, chains_config, *args, **kwargs):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self.chain_name = chain_name
            self.chains_config = chains_config
            self._init_controls()
    
        def _init_controls(self):
    
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
            self.remove_ncx = QCheckBox(_('Remove the legacy Table of Contents in NCX form'))
            l.addWidget(self.remove_ncx)
    
            self.remove_guide = QCheckBox(_('Remove the the OPF Guide element'))
            l.addWidget(self.remove_guide)
    
            l.addStretch(1)
    
            self.resize(self.sizeHint())
    
        def load_settings(self, settings):
            if settings:
                self.remove_ncx.setChecked(settings['remove_ncx'])
                self.remove_guide.setChecked(settings['remove_guide'])
    
        def save_settings(self):
            settings = {}
            settings['remove_ncx'] = self.remove_ncx.isChecked()
            settings['remove_guide'] = self.remove_guide.isChecked()
            return settings
    
    class UpgradeInternals(EditorAction):
    
        name = 'Upgrade to EPUB3'
        _is_builtin_ = True
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            if settings is None:
                return
            upgrade_book(chain.current_container, settings['remove_ncx'], settings['remove_guide'])
    
        def validate(self, settings):
            if not settings:
                return _('No settings'), _('You must first configure this action')
            return True
    
        def config_widget(self):
            return ConfigWidget

The chains are attatched.

_______________________
Edit:

I noticed that the regex functions are not included with the chain. Most of them are used to convert items to pt-BR, so not relevant for most users. There is one, though, that is important:
Code:
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    if 'alt=' not in match[0]:
        ans = match.group().replace(match.group(1), match.group(1) + ' alt=""')
    else:
        ans = match[0]
    return ans
This is used for fixing missing 'alt' attributes, inside the 'Clean up' chain.
Attached Files
File Type: zip Clean up.zip (3.5 KB, 21 views)
File Type: zip Fix all errors.zip (2.0 KB, 18 views)
File Type: zip Merge splitted.zip (839 Bytes, 13 views)
File Type: zip Pretty print all.zip (3.1 KB, 16 views)
File Type: zip Replace IDs.zip (1,008 Bytes, 15 views)
File Type: zip Upgrade to EPUB3.zip (178.8 KB, 19 views)

Last edited by thiago.eec; 06-14-2025 at 05:20 PM.
thiago.eec is offline   Reply With Quote
Old 06-15-2025, 04:48 AM   #100
Terisa de morgan
Grand Sorcerer
Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.
 
Terisa de morgan's Avatar
 
Posts: 6,626
Karma: 12595249
Join Date: Jun 2009
Location: Madrid, Spain
Device: Kobo Clara/Aura One/Forma,XiaoMI 5, iPad, Huawei MediaPad, YotaPhone 2
Quote:
Originally Posted by thiago.eec View Post
My modules:
  1. Show all editor actions [module name: editor_actions](by @capink)
    Spoiler:

    Code:
    #!/usr/bin/env python
    # ~*~ coding: utf-8 ~*~
    from __future__ import (unicode_literals, division, absolute_import,
                            print_function)
    
    import re
    
    # python 3 compatibility
    from six import text_type as unicode
    
    from PyQt5.Qt import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout,
                          QDialog, QDialogButtonBox, QMenu, QPainter, QPoint, QPixmap,
                          QSize, QIcon, QTreeWidgetItem, QTreeWidget, QAbstractItemView,
                          QGroupBox, QCheckBox, QLabel, QAction)
    
    from calibre import prints
    from calibre.constants import DEBUG
    from calibre.gui2 import error_dialog
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    from calibre_plugins.editor_chains.common_utils import get_icon
    
    try:
        load_translations()
    except NameError:
        prints("EditorChains::actions/editor_actions.py - exception when loading translations")
    
    
    EXCLUDED_GROUPS = [
        _('Editor Chains'),
        _('Editor actions'),
        _('Windows'),
        _('Preview'),
        _('Search')
    ]
    
    ICON_SIZE = 32
    
    
    def group_unique_names(gui, group):
        menu_actions = [x for x in gui.__dict__.values() if isinstance(x, QAction)]
        menu_unique_names = [re.sub(r'^action\-', r'', x.objectName()) for x in menu_actions]
        if group == 'Plugins':
            unique_names = [x for x in gui.keyboard.groups['Plugins']]
        else:
            unique_names = [x for x in gui.keyboard.groups[group] if x in menu_unique_names]
        
        return unique_names
    
    def get_qaction_from_unique_name(gui, unique_name):
        shortcut = gui.keyboard.shortcuts[unique_name]
        ac = shortcut['action']
        return ac
    
    
    def allowed_unique_names(gui):
        unique_names = []
        for group in gui.keyboard.groups.keys():
            unique_names.extend(group_unique_names(gui, group))
        unique_names += [x for x in gui.keyboard.groups['Plugins']]
        return unique_names
    
    class Item(QTreeWidgetItem):
        pass
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self.gui = plugin_action.gui
            self.all_nodes = {}
            # used to keep track of selected item and allow only one selection
            self.checked_item = None
            
            self._init_controls()
    
        def _init_controls(self):
            self.blank_icon = QIcon(I('blank.png'))
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
    #        opts_groupbox = QGroupBox()
    #        l.addWidget(opts_groupbox)
    #        opts_groupbox_layout = QVBoxLayout()
    #        opts_groupbox.setLayout(opts_groupbox_layout)
    #        cursor_chk = self.cursor_chk = QCheckBox(_('Disable wait cursor for this action.'))
    #        opts_groupbox_layout.addWidget(cursor_chk)
    #        cursor_chk.setChecked(False)
            
            self.tv = QTreeWidget(self.gui)
            self.tv.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
            self.tv.header().hide()
            self.tv.setSelectionMode(QAbstractItemView.SingleSelection)
            l.addWidget(self.tv, 1)
            self.tv.itemChanged.connect(self._tree_item_changed)
    
            self._populate_actions_tree()
    
        def _get_scaled_icon(self, icon):
            if icon.isNull():
                return self.blank_icon
            # We need the icon scaled to 16x16
            src = icon.pixmap(ICON_SIZE, ICON_SIZE)
            if src.width() == ICON_SIZE and src.height() == ICON_SIZE:
                return icon
            # Need a new version of the icon
            pm = QPixmap(ICON_SIZE, ICON_SIZE)
            pm.fill(Qt.transparent)
            p = QPainter(pm)
            p.drawPixmap(QPoint(int((ICON_SIZE - src.width()) / 2), int((ICON_SIZE - src.height()) / 2)), src)
            p.end()
            return QIcon(pm)
    
        def set_checked_state(self, item, col, state):
            item.setCheckState(col, state)
            if state == Qt.Checked:
                self.checked_item = item
            else:
                self.checked_item = None
    
        def _populate_actions_tree(self):
    
            self.top_level_items_map = {}
            for group in sorted(self.gui.keyboard.groups.keys()):
                if group in EXCLUDED_GROUPS: continue
                
                # Create a node for our top level plugin name
                tl = Item()
                tl.setText(0, group)
              
                # Normal top-level checkable plugin iaction handling
                tl.setFlags(Qt.ItemIsEnabled)
                
                #unique_names = self.gui.keyboard.groups[group]
                unique_names = group_unique_names(self.gui, group)
                
                # Iterate through all the children of this node
                self._populate_action_children(unique_names, tl)
                self.tv.addTopLevelItem(tl)
    
        def _populate_action_children(self, unique_names, parent):
            for unique_name in unique_names:
                ac = get_qaction_from_unique_name(self.gui, unique_name)
                text = ac.text().replace('&', '')
    
                it = Item(parent)
                it.setText(0, text)
                it.setData(0, Qt.UserRole, unique_name)
                it.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
                it.setCheckState(0, Qt.Unchecked)
                it.setIcon(0, self._get_scaled_icon(ac.icon()))
    
                self.all_nodes[unique_name] = it
    
        def _tree_item_changed(self, item, column):
            # Checkstate has been changed
    
            is_checked = item.checkState(column) == Qt.Checked
            #text = unicode(item.text(column)).replace('&', '')
            unique_name = item.data(column, Qt.UserRole)
    
            if is_checked:
                # un-check previously checked item if any
                if self.checked_item:
                    self.tv.blockSignals(True)
                    self.checked_item.setCheckState(0, Qt.Unchecked)
                    self.tv.blockSignals(False)
                
                self.checked_item = item
            else:
                self.checked_item = None
    
        def _repopulate_actions_tree(self):
            self.tv.clear()
            self._populate_actions_tree()
    
        def load_settings(self, settings):
            if settings:
                #self.cursor_chk.setChecked(settings.get('disable_busy_cursor', False))
                #self._repopulate_actions_tree()
                self.set_unique_name_checked(settings['unique_name'])
    
        def save_settings(self):
            settings = {}
            settings['unique_name'] = self.checked_item.data(0, Qt.UserRole)
            #settings['disable_busy_cursor'] = self.cursor_chk.isChecked()
            return settings
    
        def set_unique_name_checked(self, unique_name):
            it = self.all_nodes.get(unique_name)
            if it:
                self.set_checked_state(it, 0, Qt.Checked)
                self.tv.setCurrentItem(it)
    
    class EditorActions(EditorAction):
    
        name = 'Editor Actions'
        #_is_builtin = True
    
        def run(self, chain, settings):
            
            ac = get_qaction_from_unique_name(chain.gui, settings['unique_name'])
            if ac:
                if DEBUG:
                    prints('Editor Chains: Editor Actions: Found action: {}'.format(ac.text().replace('&', ''))) 
    
                cursor = Qt.WaitCursor
                if settings.get('disable_busy_cursor'):
                    cursor = Qt.ArrowCursor
                
                QApplication.setOverrideCursor(cursor)
                try:
                    ac.trigger()
                       
                finally:
                    QApplication.restoreOverrideCursor()
                
            if DEBUG:
                prints('Editor Chains: Editor Actions: Finishing run action') 
    
        def validate(self, settings):
            gui = self.plugin_action.gui
            if not settings:
                return (_('Settings Error'), _('You must configure this action before running it'))
            if settings['unique_name'] not in allowed_unique_names(gui):
                return (_('Settings Error'), _('Action cannot be found: {}'.format(settings['unique_name'])))
            return True
    
        def config_widget(self):
            return ConfigWidget
  2. Run 'Check book' and fix all fixable errors [module name: fix_all]
    Spoiler:

    Code:
    from qt.core import (Qt, QApplication)
    
    from calibre.ebooks.oeb.polish.check.main import fix_errors, run_checks
    from calibre_plugins.editor_chains.actions.base import EditorAction
    
    class FixAll(EditorAction):
    
        name = 'Fix all errors'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            container = chain.current_container
            QApplication.setOverrideCursor(Qt.WaitCursor)
            QApplication.processEvents()
            errors = run_checks(container)
            fix_errors(container, errors)
            QApplication.restoreOverrideCursor()
  3. Merge files [module name: merge_files](by @capink)
    Spoiler:

    Code:
    import re
    
    from qt.core import (QWidget, QVBoxLayout, QGroupBox)
    
    from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
    from calibre.ebooks.oeb.polish.split import merge
    from calibre.ebooks.oeb.polish.replace import rename_files
    from calibre.gui2.tweak_book import editors
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    from calibre_plugins.editor_chains.scope import scope_names, ScopeWidget, validate_scope, scope_is_headless
    
    
    def spine_index(container, name):
        idx = 0
        for spine_name, is_linear in container.spine_names:
            idx +=1
            if name == spine_name:
                return idx
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self._init_controls()
    
        def _init_controls(self):
    
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
            scope_groupbox = QGroupBox(_('Files to merge'))
            l.addWidget(scope_groupbox)
            scope_l = QVBoxLayout()
            scope_groupbox.setLayout(scope_l)
            self.where_box = ScopeWidget(self, self.plugin_action, orientation='vertical',
                                         headless=self.plugin_action.gui is None,
                                         hide=['selected-text','open','current'])
            scope_l.addWidget(self.where_box)
    
            l.addStretch(1)
            self.resize(self.sizeHint())
    
        def load_settings(self, settings):
            if settings:
                self.where_box.where = settings['where']
    
        def save_settings(self):
            settings = {}
            settings['where'] = self.where_box.where
            return settings
    
    class MergeFiles(EditorAction):
    
        name = 'Merge Files'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            names = scope_names(chain, settings['where'])
            print(f'DEBUG: Merging Files\n       names: {names}')
            if len(names) < 2:
                print('Error: You need to select at least 2 files to merge')
                return
            container = chain.current_container
            spine_names = [name for name, is_linear in container.spine_names]
            if names[0] in set(container.manifest_items_of_type(OEB_DOCS)):
                category = 'text'
            elif names[0] in set(container.manifest_items_of_type(OEB_STYLES)):
                category = 'styles'
            else:
                print('Error: Can only merge files of categories: text or styles')
                return
            if category == 'text':
                if not set(names).issubset(set(container.manifest_items_of_type(OEB_DOCS))):
                    print('Error: some files are your trying to merge are not of category text')
                    return
                if not set(names).issubset(set(spine_names)):
                    print('Error: Some files you are trying to merge not in spine')
                    return
                names = sorted(names, key=lambda name: spine_index(container, name))
            else:
                if not set(names).issubset(set(container.manifest_items_of_type(OEB_STYLES))):
                    print('Error: some files are your trying to merge are not of category styles')
                    return
                names = sorted(names)
            master = names[0]
            print(f'DEBUG: Merging Files\n       master: {master}')
    
            # Create groups with the same prefix before merging
            while names:
                master = names[0]
                group = []
                for name in names[:]:
                    master_prefix =  re.search('.*?_split_[^d+]', master)[0]
                    if re.search('.*?_split_[^d+]', name)[0] == master_prefix:
                        group.append(name)
                        names.remove(name)
                merge(container, category, group, master)
                new_name = re.sub('_split_.*?\d+', '', master)
                rename_files(container, {master: new_name})
                #if new_name in editors:
                #    chain.boss.show_editor(new_name)
    
        def validate(self, settings):
            scope_ok = validate_scope(settings['where'])
            if scope_ok is not True:
                return scope_ok
            return True
    
        def config_widget(self):
            return ConfigWidget
    
        def is_headless(self, settings):
            return scope_is_headless(settings['where'])
  4. Pretty print all [module name: pretty_print_all]
    Spoiler:

    Code:
    from qt.core import (Qt, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QCheckBox)
    
    from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
    from calibre.ebooks.oeb.polish import pretty
    from calibre_plugins.editor_chains.actions.base import EditorAction
    
    
    def pretty_block(parent, settings, level=2, indent='  ', indent_level=1):
        ''' Surround block tags with blank lines and recurse into child block tags
        that contain only other block tags '''
        if not parent.text or pretty.isspace(parent.text):
            parent.text = ''
        if settings['block_spaces']:
            nn = '\n' if hasattr(parent.tag, 'strip') and pretty.barename(parent.tag) in {'tr', 'td', 'th'} else '\n\n'
        else:
            nn = '\n'
        parent.text = parent.text + nn + (indent * indent_level * level)
        for i, child in enumerate(parent):
            if pretty.isblock(child) and pretty.has_only_blocks(child):
                pretty_block(child, settings, level=level+1, indent=indent * indent_level)
            elif child.tag == pretty.SVG_TAG:
                pretty_xml_tree(child, level=level, indent=indent, indent_level=indent_level)
            l = level
            if i == len(parent) - 1:
                l -= 1
            if not child.tail or pretty.isspace(child.tail):
                child.tail = ''
            child.tail = child.tail + nn + (indent * indent_level * l)
    
    
    def pretty_xml_tree(elem, level=1, indent='  ', indent_level=1):
        ''' XML beautifier, assumes that elements that have children do not have
        textual content.  Also assumes that there is no text immediately after
        closing tags. These are true for opf/ncx and container.xml files. If either
        of the assumptions are violated, there should be no data loss, but pretty
        printing won't produce optimal results.'''
        if (not elem.text and len(elem) > 0) or (elem.text and pretty.isspace(elem.text)):
            elem.text = '\n' + (indent * indent_level * (level+1))
        for i, child in enumerate(elem):
            pretty_xml_tree(child, level=level+1, indent=indent, indent_level=indent_level)
            if not child.tail or pretty.isspace(child.tail):
                l = level + 1
                if i == len(elem) - 1:
                    l -= 1
                child.tail = '\n' + (indent * indent_level * l)
    
    
    def pretty_html_tree(container, root, settings):
        indent = '  '
        indent_level = settings['indent']
        nn = '\n\n' if settings['root_spaces'] else '\n'
        root.text = nn + indent * indent_level
        for child in root:
            if hasattr(child.tag, 'endswith') and child.tag.endswith('}head'):
                pretty_xml_tree(child, indent_level=indent_level)
                child.tail = nn + indent * indent_level
            else:
                child.tail = nn
    
        for body in root.findall('h:body', namespaces=pretty.XPNSMAP):
            pretty_block(body, settings, indent_level=indent_level)
            # Special case the handling of a body that contains a single block tag
            # with all content. In this case we prettify the containing block tag
            # even if it has non block children.
            if (len(body) == 1 and not callable(body[0].tag) and
            pretty.isblock(body[0]) and
            not pretty.has_only_blocks(body[0]) and
            pretty.barename(body[0].tag) not in ('pre', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6') and
            len(body[0]) > 0):
                pretty_block(body[0], settings, level=2)
    
        if container is not None:
            # Handle <script> and <style> tags
            for child in root.xpath('//*[local-name()="script" or local-name()="style"]'):
                pretty.pretty_script_or_style(container, child)
    
    
    def pretty_all(container, settings):
        ' Pretty print all HTML/CSS/XML files in the container '
        xml_types = {pretty.guess_type('a.ncx'), pretty.guess_type('a.xml'), pretty.guess_type('a.svg')}
        for name, mt in pretty.iteritems(container.mime_map):
            prettied = False
            if mt in OEB_DOCS:
                pretty_html_tree(container, container.parsed(name), settings)
                prettied = True
            elif mt in OEB_STYLES:
                container.parsed(name)
                prettied = True
            elif name == container.opf_name:
                root = container.parsed(name)
                pretty.pretty_opf(root)
                pretty.pretty_xml_tree(root)
                prettied = True
            elif mt in xml_types:
                pretty.pretty_xml_tree(container.parsed(name))
                prettied = True
            if prettied:
                container.dirty(name)
    
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self._init_controls()
    
        def _init_controls(self):
    
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
            h = self.h = QHBoxLayout()
            l.addLayout(h)
            self.label = QLabel(_('Indentation level:'))
            h.addWidget(self.label)
    
            self.indent_level = QSpinBox()
            self.indent_level.setRange(1, 10)
            h.addWidget(self.indent_level)
    
            self.root_spaces = QCheckBox(_('Add spaces for head/body'))
            l.addWidget(self.root_spaces)
    
            self.block_spaces = QCheckBox(_('Add spaces between blocks'))
            l.addWidget(self.block_spaces)
    
            #l.addStretch(1)
    
            #self.setMinimumSize(300,110)
            self.resize(self.sizeHint())
    
        def load_settings(self, settings):
            if settings:
                self.indent_level.setValue(settings['indent'])
                self.root_spaces.setChecked(settings['root_spaces'])
                self.block_spaces.setChecked(settings['block_spaces'])
    
        def save_settings(self):
            settings = {}
            settings['indent'] = self.indent_level.value()
            settings['root_spaces'] = self.root_spaces.isChecked()
            settings['block_spaces'] = self.block_spaces.isChecked()
            return settings
    
    
    class BeautifyAll(EditorAction):
    
        name = 'Pretty print ALL'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            container = chain.current_container
            QApplication.setOverrideCursor(Qt.WaitCursor)
            QApplication.processEvents()
            pretty_all(container, settings)
            QApplication.restoreOverrideCursor()
    
        def validate(self, settings):
            if not settings:
                return _('No settings'), _('You must first configure this action')
            return True
    
        def config_widget(self):
            return ConfigWidget
  5. Replace invalid IDs [module name: replace_ids]
    Spoiler:

    Code:
    from qt.core import (Qt, QApplication)
    
    from calibre.ebooks.oeb.polish.check import parsing
    from calibre.ebooks.oeb.polish.check.main import fix_errors
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    
    class ReplaceIDs(EditorAction):
    
        name = 'Replace IDs'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            container = chain.current_container
            QApplication.setOverrideCursor(Qt.WaitCursor)
            QApplication.processEvents()
            errors = parsing.check_ids(container)
            changed = fix_errors(container, errors)
            QApplication.restoreOverrideCursor()
  6. Select files: this is used to convert Editor Chain's 'escope' to a selection [module name: select_files] (by @capink)
    Spoiler:

    Code:
    import re
    
    from qt.core import (QWidget, QVBoxLayout, QGroupBox)
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    from calibre_plugins.editor_chains.scope import scope_names, ScopeWidget, validate_scope, scope_is_headless
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self._init_controls()
    
        def _init_controls(self):
    
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
            scope_groupbox = QGroupBox(_('Files to select'))
            l.addWidget(scope_groupbox)
            scope_l = QVBoxLayout()
            scope_groupbox.setLayout(scope_l)
            self.where_box = ScopeWidget(self, self.plugin_action, orientation='vertical',
                                         headless=self.plugin_action.gui is None,
                                         hide=['selected-text','open','current'])
            scope_l.addWidget(self.where_box)
    
            l.addStretch(1)
            self.setMinimumSize(400,200)
    
        def load_settings(self, settings):
            if settings:
                self.where_box.where = settings['where']
    
        def save_settings(self):
            settings = {}
            settings['where'] = self.where_box.where
            return settings
    
    class SelectFiles(EditorAction):
    
        name = 'Select Files'
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            names = scope_names(chain, settings['where'])
            chain.gui.file_list.select_names(group)
    
        def validate(self, settings):
            scope_ok = validate_scope(settings['where'])
            if scope_ok is not True:
                return scope_ok
            return True
    
        def config_widget(self):
            return ConfigWidget
    
        def is_headless(self, settings):
            return scope_is_headless(settings['where'])
  7. Upgrade to EPUB3 [module name: upgrade]
    Spoiler:

    Code:
    from qt.core import (QApplication, Qt, QWidget, QVBoxLayout,
                         QCheckBox, QLabel)
    
    from calibre import prints
    from calibre.constants import DEBUG
    from calibre.ebooks.oeb.polish.main import tweak_polish
    from calibre.ebooks.oeb.polish import upgrade
    
    from calibre_plugins.editor_chains.actions.base import EditorAction
    
    
    try:
        load_translations()
    except NameError:
        prints("EditorChains::actions/upgrade_internals.py - exception when loading translations")
    
    
    def epub_2_to_3(container, previous_nav=None, remove_ncx=False, remove_guide=False):
        upgrade.upgrade_metadata(container.opf)
        upgrade.collect_properties(container)
        toc = upgrade.get_toc(container)
        toc_name = upgrade.find_existing_ncx_toc(container)
        if toc_name and remove_ncx:
            container.remove_item(toc_name)
            container.opf_xpath('./opf:spine')[0].attrib.pop('toc', None)
        landmarks = upgrade.get_landmarks(container)
        if remove_guide:
            for guide in container.opf_xpath('./opf:guide'):
                guide.getparent().remove(guide)
        upgrade.create_nav(container, toc, landmarks, previous_nav)
        container.opf.set('version', '3.0')
        if upgrade.fix_font_mime_types(container):
            container.refresh_mime_map()
        upgrade.migrate_obfuscated_fonts(container)
        container.dirty(container.opf_name)
    
    
    def upgrade_book(container, remove_ncx=False, remove_guide=False):
        if container.book_type != 'epub' or container.opf_version_parsed.major >= 3:
            return False
        epub_2_to_3(container, remove_ncx=remove_ncx, remove_guide=remove_guide)
        return True
    
    
    class ConfigWidget(QWidget):
        def __init__(self, plugin_action, chain_name, chains_config, *args, **kwargs):
            QWidget.__init__(self)
            self.plugin_action = plugin_action
            self.chain_name = chain_name
            self.chains_config = chains_config
            self._init_controls()
    
        def _init_controls(self):
    
            l = self.l = QVBoxLayout()
            self.setLayout(l)
    
            self.remove_ncx = QCheckBox(_('Remove the legacy Table of Contents in NCX form'))
            l.addWidget(self.remove_ncx)
    
            self.remove_guide = QCheckBox(_('Remove the the OPF Guide element'))
            l.addWidget(self.remove_guide)
    
            l.addStretch(1)
    
            self.resize(self.sizeHint())
    
        def load_settings(self, settings):
            if settings:
                self.remove_ncx.setChecked(settings['remove_ncx'])
                self.remove_guide.setChecked(settings['remove_guide'])
    
        def save_settings(self):
            settings = {}
            settings['remove_ncx'] = self.remove_ncx.isChecked()
            settings['remove_guide'] = self.remove_guide.isChecked()
            return settings
    
    class UpgradeInternals(EditorAction):
    
        name = 'Upgrade to EPUB3'
        _is_builtin_ = True
        headless = True
    
        def run(self, chain, settings, *args, **kwargs):
            if settings is None:
                return
            upgrade_book(chain.current_container, settings['remove_ncx'], settings['remove_guide'])
    
        def validate(self, settings):
            if not settings:
                return _('No settings'), _('You must first configure this action')
            return True
    
        def config_widget(self):
            return ConfigWidget

The chains are attatched.

_______________________
Edit:

I noticed that the regex functions are not included with the chain. Most of them are used to convert items to pt-BR, so not relevant for most users. There is one, though, that is important:
Code:
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    if 'alt=' not in match[0]:
        ans = match.group().replace(match.group(1), match.group(1) + ' alt=""')
    else:
        ans = match[0]
    return ans
This is used for fixing missing 'alt' attributes, inside the 'Clean up' chain.
Thank you very much! With this, I'll be able to automate some of the options I do manually! And thanks to capink for the plugin and kovid for the program!
Terisa de morgan is offline   Reply With Quote
Old 06-15-2025, 06:14 AM   #101
thiago.eec
Wizard
thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.
 
Posts: 1,208
Karma: 1419583
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite, Kindle Oasis
Quote:
Originally Posted by Terisa de morgan View Post
Thank you very much! With this, I'll be able to automate some of the options I do manually! And thanks to capink for the plugin and kovid for the program!
Glad I could help. Let me know if everything worked as expected.
thiago.eec is offline   Reply With Quote
Old 06-15-2025, 06:30 AM   #102
Terisa de morgan
Grand Sorcerer
Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.Terisa de morgan ought to be getting tired of karma fortunes by now.
 
Terisa de morgan's Avatar
 
Posts: 6,626
Karma: 12595249
Join Date: Jun 2009
Location: Madrid, Spain
Device: Kobo Clara/Aura One/Forma,XiaoMI 5, iPad, Huawei MediaPad, YotaPhone 2
Quote:
Originally Posted by thiago.eec View Post
Glad I could help. Let me know if everything worked as expected.
I'll do it... and I'm sure I'll have to ask you help as, instead of upgrade to epub3 I want to downgrade to epub2
Terisa de morgan is offline   Reply With Quote
Old 06-15-2025, 06:37 AM   #103
thiago.eec
Wizard
thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.thiago.eec ought to be getting tired of karma fortunes by now.
 
Posts: 1,208
Karma: 1419583
Join Date: Dec 2016
Location: Goiânia - Brazil
Device: iPad, Kindle Paperwhite, Kindle Oasis
Quote:
Originally Posted by Terisa de morgan View Post
I'll do it... and I'm sure I'll have to ask you help as, instead of upgrade to epub3 I want to downgrade to epub2
That's out of my league.

The Editor already has a tool to upgrade to epub3. I just made a small modification to preserve the <guide> element. Also, to be automated, I added the options to the chain config dialog, this way it can be configured beforehand.

Unfortunately, the Editor has no tool to downgrade from epub3 to epub2, so I can't help you there.
thiago.eec is offline   Reply With Quote
Old 06-17-2025, 09:50 AM   #104
nephtys59
Enthusiast
nephtys59 began at the beginning.
 
nephtys59's Avatar
 
Posts: 33
Karma: 10
Join Date: Feb 2011
Device: Android Tablet
Dumb question, but I try: is there away to create a shortcut to a couple of saved searches, but without replacing anything?
nephtys59 is offline   Reply With Quote
Old 06-17-2025, 11:18 AM   #105
theducks
Well trained by Cats
theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.theducks ought to be getting tired of karma fortunes by now.
 
theducks's Avatar
 
Posts: 31,012
Karma: 60358908
Join Date: Aug 2009
Location: The Central Coast of California
Device: Kobo Libra2,Kobo Aura2v1, K4NT(Fixed: New Bat.), Galaxy Tab A
Quote:
Originally Posted by nephtys59 View Post
Dumb question, but I try: is there away to create a shortcut to a couple of saved searches, but without replacing anything?
Is this an Editor saved search?
Even if you did a change, just abort (Exit without save).
I really want to test out most complex S&R over more than 1 case.

BUT ....

You can just fill out the S&R box . Right-click in the FIND box: Save current search
theducks is online now   Reply With Quote
Reply


Forum Jump

Similar Threads
Thread Thread Starter Forum Replies Last Post
[Editor Plugin] EpubCheck Doitsu Plugins 215 05-22-2025 01:45 AM
[Editor Plugin] LanguageTool Doitsu Plugins 17 04-20-2024 02:21 PM
[Editor Plugin] - Enabling 'Customize plugin' dialog directly from the Editor thiago.eec Development 7 01-09-2019 08:05 PM
Sample Plugin for the Editor DiapDealer Editor 77 12-10-2014 07:16 AM
Editor plugin question DiapDealer Development 2 07-28-2014 10:23 PM


All times are GMT -4. The time now is 08:50 PM.


MobileRead.com is a privately owned, operated and funded community.