2
# vim:fileencoding=utf-8
3
from __future__ import (unicode_literals, division, absolute_import,
7
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
10
from operator import itemgetter
11
from . import NullSmarts
13
from PyQt4.Qt import QTextEdit
15
from calibre.gui2 import error_dialog
17
get_offset = itemgetter(0)
18
PARAGRAPH_SEPARATOR = '\u2029'
22
def __init__(self, start_block, tag_start, end_block, tag_end, self_closing=False):
23
self.start_block, self.end_block = start_block, end_block
24
self.start_offset, self.end_offset = tag_start.offset, tag_end.offset
25
tag = tag_start.name or tag_start.prefix
26
if tag_start.name and tag_start.prefix:
27
tag = tag_start.prefix + ':' + tag
29
self.self_closing = self_closing
31
def next_tag_boundary(block, offset, forward=True):
32
while block.isValid():
35
tags = sorted(ud.tags, key=get_offset, reverse=not forward)
37
if forward and boundary.offset > offset:
38
return block, boundary
39
if not forward and boundary.offset < offset:
40
return block, boundary
41
block = block.next() if forward else block.previous()
42
offset = -1 if forward else sys.maxint
45
def find_closest_containing_tag(block, offset, max_tags=sys.maxint):
46
''' Find the closest containing tag. To find it, we search for the first
47
opening tag that does not have a matching closing tag before the specified
48
position. Search through at most max_tags. '''
49
prev_tag_boundary = lambda b, o: next_tag_boundary(b, o, forward=False)
51
block, boundary = prev_tag_boundary(block, offset)
55
# We are inside a tag, therefore the containing tag is the parent tag of
57
return find_closest_containing_tag(block, boundary.offset)
59
block, tag_end = block, boundary
60
while block is not None and max_tags > 0:
61
sblock, tag_start = prev_tag_boundary(block, tag_end.offset)
62
if sblock is None or not tag_start.is_start:
64
if tag_start.closing: # A closing tag of the form </a>
65
stack.append((tag_start.prefix, tag_start.name))
66
elif tag_end.self_closing: # A self closing tag of the form <a/>
68
else: # An opening tag, hurray
70
prefix, name = stack.pop()
73
if (prefix, name) != (tag_start.prefix, tag_start.name):
74
# Either we have an unbalanced opening tag or a syntax error, in
75
# either case terminate
76
return Tag(sblock, tag_start, block, tag_end)
77
block, tag_end = prev_tag_boundary(sblock, tag_start.offset)
79
return None # Could not find a containing tag
81
def find_closing_tag(tag, max_tags=sys.maxint):
82
''' Find the closing tag corresponding to the specified tag. To find it we
83
search for the first closing tag after the specified tag that does not
84
match a previous opening tag. Search through at most max_tags. '''
87
block, offset = tag.end_block, tag.end_offset
88
while block.isValid() and max_tags > 0:
89
block, tag_start = next_tag_boundary(block, offset)
90
if block is None or not tag_start.is_start:
92
endblock, tag_end = next_tag_boundary(block, tag_start.offset)
93
if endblock is None or tag_end.is_start:
97
prefix, name = stack.pop()
100
if (prefix, name) != (tag_start.prefix, tag_start.name):
101
return Tag(block, tag_start, endblock, tag_end)
102
elif tag_end.self_closing:
105
stack.append((tag_start.prefix, tag_start.name))
106
block, offset = endblock, tag_end.offset
110
def select_tag(cursor, tag):
111
cursor.setPosition(tag.start_block.position() + tag.start_offset)
112
cursor.setPosition(tag.end_block.position() + tag.end_offset + 1, cursor.KeepAnchor)
113
return unicode(cursor.selectedText()).replace(PARAGRAPH_SEPARATOR, '\n').rstrip('\0')
115
def rename_tag(cursor, opening_tag, closing_tag, new_name, insert=False):
116
cursor.beginEditBlock()
117
text = select_tag(cursor, closing_tag)
119
text = '</%s>%s' % (new_name, text)
121
text = re.sub(r'^<\s*/\s*[a-zA-Z0-9]+', '</%s' % new_name, text)
122
cursor.insertText(text)
123
text = select_tag(cursor, opening_tag)
125
text += '<%s>' % new_name
127
text = re.sub(r'^<\s*[a-zA-Z0-9]+', '<%s' % new_name, text)
128
cursor.insertText(text)
129
cursor.endEditBlock()
131
class HTMLSmarts(NullSmarts):
133
def get_extra_selections(self, editor):
137
a = QTextEdit.ExtraSelection()
138
a.cursor, a.format = editor.textCursor(), editor.match_paren_format
139
a.cursor.setPosition(tag.start_block.position() + tag.start_offset)
140
a.cursor.setPosition(tag.end_block.position() + tag.end_offset + 1, a.cursor.KeepAnchor)
143
c = editor.textCursor()
144
block, offset = c.block(), c.positionInBlock()
145
tag = find_closest_containing_tag(block, offset, max_tags=2000)
148
tag = find_closing_tag(tag, max_tags=4000)
153
def rename_block_tag(self, editor, new_name):
154
c = editor.textCursor()
155
block, offset = c.block(), c.positionInBlock()
159
tag = find_closest_containing_tag(block, offset)
162
block, offset = tag.start_block, tag.start_offset
164
'address', 'article', 'aside', 'blockquote', 'center',
165
'dir', 'fieldset', 'isindex', 'menu', 'noframes', 'hgroup',
166
'noscript', 'pre', 'section', 'h1', 'h2', 'h3', 'h4', 'h5',
167
'h6', 'header', 'p', 'div', 'dd', 'dl', 'ul', 'ol', 'li', 'body',
173
closing_tag = find_closing_tag(tag)
174
if closing_tag is None:
175
return error_dialog(editor, _('Invalid HTML'), _(
176
'There is an unclosed %s tag. You should run the Fix HTML tool'
177
' before trying to rename tags.') % tag.name, show=True)
178
rename_tag(c, tag, closing_tag, new_name, insert=tag.name in {'body', 'td', 'th', 'li'})
180
return error_dialog(editor, _('No found'), _(
181
'No suitable block level tag was found to rename'), show=True)