mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Added CLI support search_replace by changing serialization and deserialization of option.
This commit is contained in:
parent
7ea167eec1
commit
f0708f779e
@ -628,7 +628,17 @@ OptionRecommendation(name='sr3_replace',
|
|||||||
|
|
||||||
OptionRecommendation(name='search_replace',
|
OptionRecommendation(name='search_replace',
|
||||||
recommended_value='[]', level=OptionRecommendation.LOW,
|
recommended_value='[]', level=OptionRecommendation.LOW,
|
||||||
help=_('Modify the document text and structure using user defined patterns.')),
|
help=_('Modify the document text and structure using user defined patterns.'
|
||||||
|
'This option accepts parameters in two forms:\n'
|
||||||
|
'1.file:<path to search/replace definitions file>\n'
|
||||||
|
'The file should contain alternating lines or search/replace strings:\n'
|
||||||
|
' <search>\n'
|
||||||
|
' <replace>\n'
|
||||||
|
' <search>\n'
|
||||||
|
' <replace>\n'
|
||||||
|
'Files saved through the user interface dialog can be used with this option.\n'
|
||||||
|
'2.json:<json encoded list containg [search, replace] touples:\n'
|
||||||
|
' json:[["search","replace"],["search","replace"]]\n')),
|
||||||
]
|
]
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import functools, re, json
|
import functools, re, search_replace_option
|
||||||
|
|
||||||
from calibre import entity_to_unicode, as_unicode
|
from calibre import entity_to_unicode, as_unicode
|
||||||
|
|
||||||
@ -515,8 +515,8 @@ class HTMLPreProcessor(object):
|
|||||||
if not getattr(self.extra_opts, 'keep_ligatures', False):
|
if not getattr(self.extra_opts, 'keep_ligatures', False):
|
||||||
html = _ligpat.sub(lambda m:LIGATURES[m.group()], html)
|
html = _ligpat.sub(lambda m:LIGATURES[m.group()], html)
|
||||||
|
|
||||||
search_replace = json.loads(getattr(self.extra_opts, 'search_replace', '[]'))
|
# Function for processing search and replace
|
||||||
for search_pattern, replace_txt in search_replace:
|
def do_search_replace(search_pattern, replace_txt):
|
||||||
if search_pattern:
|
if search_pattern:
|
||||||
try:
|
try:
|
||||||
search_re = re.compile(search_pattern)
|
search_re = re.compile(search_pattern)
|
||||||
@ -528,6 +528,17 @@ class HTMLPreProcessor(object):
|
|||||||
self.log.error('Failed to parse %r regexp because %s' %
|
self.log.error('Failed to parse %r regexp because %s' %
|
||||||
(search, as_unicode(e)))
|
(search, as_unicode(e)))
|
||||||
|
|
||||||
|
#search / replace using the sr?_search / sr?_replace options
|
||||||
|
for search, replace in [['sr3_search', 'sr3_replace'], ['sr2_search', 'sr2_replace'], ['sr1_search', 'sr1_replace']]:
|
||||||
|
search_pattern = getattr(self.extra_opts, search, '')
|
||||||
|
replace_txt = getattr(self.extra_opts, replace, '')
|
||||||
|
do_search_replace(search_pattern, replace_txt)
|
||||||
|
|
||||||
|
# multi-search / replace using the search_replace option
|
||||||
|
search_replace = search_replace_option.decode(getattr(self.extra_opts, 'search_replace', '[]'))
|
||||||
|
for search_pattern, replace_txt in search_replace:
|
||||||
|
do_search_replace(search_pattern, replace_txt)
|
||||||
|
|
||||||
end_rules = []
|
end_rules = []
|
||||||
# delete soft hyphens - moved here so it's executed after header/footer removal
|
# delete soft hyphens - moved here so it's executed after header/footer removal
|
||||||
if is_pdftohtml:
|
if is_pdftohtml:
|
||||||
|
50
src/calibre/ebooks/conversion/search_replace_option.py
Normal file
50
src/calibre/ebooks/conversion/search_replace_option.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2012, Eli Algranti <idea00@hotmail.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import json
|
||||||
|
from itertools import izip
|
||||||
|
|
||||||
|
def encodeJson(definition):
|
||||||
|
'''
|
||||||
|
Encode a search/replace definition using json.
|
||||||
|
'''
|
||||||
|
return 'json:' + json.dumps(definition)
|
||||||
|
|
||||||
|
def encodeFile(definition, filename):
|
||||||
|
'''
|
||||||
|
Encode a search/replace definition into a file
|
||||||
|
'''
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
for search,replace in definition:
|
||||||
|
f.write(search + '\n')
|
||||||
|
f.write(replace + '\n')
|
||||||
|
|
||||||
|
return 'file:'+filename
|
||||||
|
|
||||||
|
|
||||||
|
def decode(definition):
|
||||||
|
'''
|
||||||
|
Decodes a search/replace definition
|
||||||
|
'''
|
||||||
|
if definition.startswith('json:'):
|
||||||
|
return json.loads(definition[len('json:'):])
|
||||||
|
elif definition.startswith('file:'):
|
||||||
|
with open(definition[len('file:'):], 'r') as f:
|
||||||
|
ans = []
|
||||||
|
for search, replace in izip(f, f):
|
||||||
|
ans.append([search.rstrip('\n\r'), replace.rstrip('\n\r')])
|
||||||
|
return ans
|
||||||
|
raise Exception('Invalid definition')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
__license__ = 'GPL 3'
|
__license__ = 'GPL 3'
|
||||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>, 2012 Eli Algranti <idea00@hotmail.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re, json
|
import re
|
||||||
|
from calibre.ebooks.conversion import search_replace_option
|
||||||
|
|
||||||
from PyQt4.QtCore import SIGNAL, Qt
|
from PyQt4.QtCore import SIGNAL, Qt
|
||||||
from PyQt4.QtGui import QTableWidget, QTableWidgetItem, QFileDialog
|
from PyQt4.QtGui import QTableWidget, QTableWidgetItem, QFileDialog, QMessageBox
|
||||||
from calibre.gui2.convert.search_and_replace_ui import Ui_Form
|
from calibre.gui2.convert.search_and_replace_ui import Ui_Form
|
||||||
from calibre.gui2.convert import Widget
|
from calibre.gui2.convert import Widget
|
||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
@ -41,7 +42,7 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
|||||||
self.search_replace.setColumnCount(2)
|
self.search_replace.setColumnCount(2)
|
||||||
self.search_replace.setColumnWidth(0, 300)
|
self.search_replace.setColumnWidth(0, 300)
|
||||||
self.search_replace.setColumnWidth(1, 300)
|
self.search_replace.setColumnWidth(1, 300)
|
||||||
self.search_replace.setHorizontalHeaderLabels(['Search Expression', 'Replacement'])
|
self.search_replace.setHorizontalHeaderLabels([_('Search Regular Expression'), _('Replacement Text')])
|
||||||
|
|
||||||
self.connect(self.sr_add, SIGNAL('clicked()'), self.sr_add_clicked)
|
self.connect(self.sr_add, SIGNAL('clicked()'), self.sr_add_clicked)
|
||||||
self.connect(self.sr_change, SIGNAL('clicked()'), self.sr_change_clicked)
|
self.connect(self.sr_change, SIGNAL('clicked()'), self.sr_change_clicked)
|
||||||
@ -82,18 +83,14 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
|||||||
self.search_replace.setCurrentCell(row-1, 0)
|
self.search_replace.setCurrentCell(row-1, 0)
|
||||||
|
|
||||||
def sr_load_clicked(self):
|
def sr_load_clicked(self):
|
||||||
filename = QFileDialog.getOpenFileName(self, 'Load Calibre Search-Replace definitions file', '.', 'Calibre Search-Replace definitions file (*.csr)')
|
filename = QFileDialog.getOpenFileName(self, _('Load Calibre Search-Replace definitions file'), '.', _('Calibre Search-Replace definitions file (*.csr)'))
|
||||||
if filename:
|
if filename:
|
||||||
with open(filename, 'r') as f:
|
self.set_value_handler(self.opt_search_replace, 'file:'+unicode(filename))
|
||||||
val = f.read()
|
|
||||||
self.set_value(self.search_replace, val)
|
|
||||||
|
|
||||||
def sr_save_clicked(self):
|
def sr_save_clicked(self):
|
||||||
filename = QFileDialog.getSaveFileName(self, 'Save Calibre Search-Replace definitions file', '.', 'Calibre Search-Replace definitions file (*.csr)')
|
filename = QFileDialog.getSaveFileName(self, _('Save Calibre Search-Replace definitions file'), '.', _('Calibre Search-Replace definitions file (*.csr)'))
|
||||||
if filename:
|
if filename:
|
||||||
with open(filename, 'w') as f:
|
search_replace_option.encodeFile(self.get_definitions(), unicode(filename))
|
||||||
val = self.get_value(self.search_replace)
|
|
||||||
f.write(val)
|
|
||||||
|
|
||||||
def sr_currentCellChanged(self, row, column, previousRow, previousColumn) :
|
def sr_currentCellChanged(self, row, column, previousRow, previousColumn) :
|
||||||
if row >= 0:
|
if row >= 0:
|
||||||
@ -122,14 +119,40 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
|||||||
self.sr_search.set_doc(doc)
|
self.sr_search.set_doc(doc)
|
||||||
|
|
||||||
def pre_commit_check(self):
|
def pre_commit_check(self):
|
||||||
for row in xrange(0, self.search_replace.rowCount()):
|
|
||||||
|
|
||||||
|
definitions = self.get_definitions()
|
||||||
|
|
||||||
|
# Verify the search/replace in the edit widgets has been
|
||||||
|
# included to the list of search/replace definitions
|
||||||
|
|
||||||
|
edit_search = self.sr_search.regex
|
||||||
|
|
||||||
|
if edit_search:
|
||||||
|
edit_replace = unicode(self.sr_replace.text())
|
||||||
|
found = False
|
||||||
|
for search, replace in definitions:
|
||||||
|
if search == edit_search and replace == edit_replace:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
msgBox = QMessageBox(self)
|
||||||
|
msgBox.setText(_('The search / replace definition being edited has not been added to the list of definitions'))
|
||||||
|
msgBox.setInformativeText(_('Do you wish to continue with the conversion (the definition will not be used)?'))
|
||||||
|
msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
||||||
|
msgBox.setDefaultButton(QMessageBox.No)
|
||||||
|
if msgBox.exec_() != QMessageBox.Yes:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verify all search expressions are valid
|
||||||
|
for search, replace in definitions:
|
||||||
try:
|
try:
|
||||||
pat = unicode(self.search_replace.item(row,0).text())
|
re.compile(search)
|
||||||
re.compile(pat)
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
error_dialog(self, _('Invalid regular expression'),
|
error_dialog(self, _('Invalid regular expression'),
|
||||||
_('Invalid regular expression: %s')%err, show=True)
|
_('Invalid regular expression: %s')%err, show=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
@ -171,14 +194,16 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
|||||||
def get_value_handler(self, g):
|
def get_value_handler(self, g):
|
||||||
if g != self.opt_search_replace:
|
if g != self.opt_search_replace:
|
||||||
return None
|
return None
|
||||||
|
return search_replace_option.encodeJson(self.get_definitions())
|
||||||
|
|
||||||
|
def get_definitions(self):
|
||||||
ans = []
|
ans = []
|
||||||
for row in xrange(0, self.search_replace.rowCount()):
|
for row in xrange(0, self.search_replace.rowCount()):
|
||||||
colItems = []
|
colItems = []
|
||||||
for col in xrange(0, self.search_replace.columnCount()):
|
for col in xrange(0, self.search_replace.columnCount()):
|
||||||
colItems.append(unicode(self.search_replace.item(row, col).text()))
|
colItems.append(unicode(self.search_replace.item(row, col).text()))
|
||||||
ans.append(colItems)
|
ans.append(colItems)
|
||||||
return json.dumps(ans)
|
return ans
|
||||||
|
|
||||||
def set_value_handler(self, g, val):
|
def set_value_handler(self, g, val):
|
||||||
if g != self.opt_search_replace:
|
if g != self.opt_search_replace:
|
||||||
@ -186,7 +211,7 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rowItems = json.loads(val)
|
rowItems = search_replace_option.decode(val)
|
||||||
if not isinstance(rowItems, list):
|
if not isinstance(rowItems, list):
|
||||||
rowItems = []
|
rowItems = []
|
||||||
except:
|
except:
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
</sizepolicy>
|
</sizepolicy>
|
||||||
</property>
|
</property>
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>First expression</string>
|
<string>Search/Replace Definition Edit</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_2">
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
<property name="sizeConstraint">
|
<property name="sizeConstraint">
|
||||||
@ -147,7 +147,7 @@
|
|||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string><p>Search and replace uses <i>regular expressions</i>. See the <a href="http://manual.calibre-ebook.com/regexp.html">regular expressions tutorial</a> to get started with regular expressions. Also clicking the wizard buttons below will allow you to test your regular expression against the current input document.</string>
|
<string><p>Search and replace uses <i>regular expressions</i>. See the <a href="http://manual.calibre-ebook.com/regexp.html">regular expressions tutorial</a> to get started with regular expressions. Also clicking the wizard button below will allow you to test your regular expression against the current input document.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user