~ubuntu-branches/debian/sid/calibre/sid

« back to all changes in this revision

Viewing changes to src/calibre/gui2/tweak_book/editor/smart/html.py

  • Committer: Package Import Robot
  • Author(s): Martin Pitt
  • Date: 2014-02-27 07:48:06 UTC
  • mto: This revision was merged to the branch mainline in revision 74.
  • Revision ID: package-import@ubuntu.com-20140227074806-64wdebb3ptosxhhx
Tags: upstream-1.25.0+dfsg
ImportĀ upstreamĀ versionĀ 1.25.0+dfsg

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# vim:fileencoding=utf-8
 
3
from __future__ import (unicode_literals, division, absolute_import,
 
4
                        print_function)
 
5
 
 
6
__license__ = 'GPL v3'
 
7
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
 
8
 
 
9
import sys, re
 
10
from operator import itemgetter
 
11
from . import NullSmarts
 
12
 
 
13
from PyQt4.Qt import QTextEdit
 
14
 
 
15
from calibre.gui2 import error_dialog
 
16
 
 
17
get_offset = itemgetter(0)
 
18
PARAGRAPH_SEPARATOR = '\u2029'
 
19
 
 
20
class Tag(object):
 
21
 
 
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
 
28
        self.name = tag
 
29
        self.self_closing = self_closing
 
30
 
 
31
def next_tag_boundary(block, offset, forward=True):
 
32
    while block.isValid():
 
33
        ud = block.userData()
 
34
        if ud is not None:
 
35
            tags = sorted(ud.tags, key=get_offset, reverse=not forward)
 
36
            for boundary in tags:
 
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
 
43
    return None, None
 
44
 
 
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)
 
50
 
 
51
    block, boundary = prev_tag_boundary(block, offset)
 
52
    if block is None:
 
53
        return None
 
54
    if boundary.is_start:
 
55
        # We are inside a tag, therefore the containing tag is the parent tag of
 
56
        # this tag
 
57
        return find_closest_containing_tag(block, boundary.offset)
 
58
    stack = []
 
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:
 
63
            break
 
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/>
 
67
            pass  # Ignore it
 
68
        else:  # An opening tag, hurray
 
69
            try:
 
70
                prefix, name = stack.pop()
 
71
            except IndexError:
 
72
                prefix = name = None
 
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)
 
78
        max_tags -= 1
 
79
    return None  # Could not find a containing tag
 
80
 
 
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. '''
 
85
 
 
86
    stack = []
 
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:
 
91
            break
 
92
        endblock, tag_end = next_tag_boundary(block, tag_start.offset)
 
93
        if endblock is None or tag_end.is_start:
 
94
            break
 
95
        if tag_start.closing:
 
96
            try:
 
97
                prefix, name = stack.pop()
 
98
            except IndexError:
 
99
                prefix = name = None
 
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:
 
103
            pass
 
104
        else:
 
105
            stack.append((tag_start.prefix, tag_start.name))
 
106
        block, offset = endblock, tag_end.offset
 
107
        max_tags -= 1
 
108
    return None
 
109
 
 
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')
 
114
 
 
115
def rename_tag(cursor, opening_tag, closing_tag, new_name, insert=False):
 
116
    cursor.beginEditBlock()
 
117
    text = select_tag(cursor, closing_tag)
 
118
    if insert:
 
119
        text = '</%s>%s' % (new_name, text)
 
120
    else:
 
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)
 
124
    if insert:
 
125
        text += '<%s>' % new_name
 
126
    else:
 
127
        text = re.sub(r'^<\s*[a-zA-Z0-9]+', '<%s' % new_name, text)
 
128
    cursor.insertText(text)
 
129
    cursor.endEditBlock()
 
130
 
 
131
class HTMLSmarts(NullSmarts):
 
132
 
 
133
    def get_extra_selections(self, editor):
 
134
        ans = []
 
135
 
 
136
        def add_tag(tag):
 
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)
 
141
            ans.append(a)
 
142
 
 
143
        c = editor.textCursor()
 
144
        block, offset = c.block(), c.positionInBlock()
 
145
        tag = find_closest_containing_tag(block, offset, max_tags=2000)
 
146
        if tag is not None:
 
147
            add_tag(tag)
 
148
            tag = find_closing_tag(tag, max_tags=4000)
 
149
            if tag is not None:
 
150
                add_tag(tag)
 
151
        return ans
 
152
 
 
153
    def rename_block_tag(self, editor, new_name):
 
154
        c = editor.textCursor()
 
155
        block, offset = c.block(), c.positionInBlock()
 
156
        tag = None
 
157
 
 
158
        while True:
 
159
            tag = find_closest_containing_tag(block, offset)
 
160
            if tag is None:
 
161
                break
 
162
            block, offset = tag.start_block, tag.start_offset
 
163
            if tag.name in {
 
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',
 
168
                    'td', 'th'}:
 
169
                break
 
170
            tag = None
 
171
 
 
172
        if tag is not None:
 
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'})
 
179
        else:
 
180
            return error_dialog(editor, _('No found'), _(
 
181
                'No suitable block level tag was found to rename'), show=True)
 
182
 
 
183