def rule_to_html(self, kind, col, rule): trans_kind = 'not found' if kind == 'color': trans_kind = _('color') else: for tt, t in icon_rule_kinds: if kind == t: trans_kind = tt break if not isinstance(rule, Rule): if kind == 'color': return _(''' <p>Advanced Rule for column <b>%(col)s</b>: <pre>%(rule)s</pre> ''')%dict(col=col, rule=prepare_string_for_xml(rule)) else: return _(''' <p>Advanced Rule: set <b>%(typ)s</b> for column <b>%(col)s</b>: <pre>%(rule)s</pre> ''')%dict(col=col, typ=trans_kind, rule=prepare_string_for_xml(rule)) conditions = [self.condition_to_html(c) for c in rule.conditions] return _('''\ <p>Set the <b>%(kind)s</b> of <b>%(col)s</b> to <b>%(color)s</b> if the following conditions are met:</p> <ul>%(rule)s</ul> ''') % dict(kind=trans_kind, col=col, color=rule.color, rule=''.join(conditions))
def create_book(mi, path, fmt='epub', opf_name='metadata.opf', html_name='start.xhtml', toc_name='toc.ncx'): ''' Create an empty book in the specified format at the specified location. ''' path = os.path.abspath(path) lang = 'und' opf = metadata_to_opf(mi, as_string=False) for l in opf.xpath('//*[local-name()="language"]'): if l.text: lang = l.text break lang = lang_as_iso639_1(lang) or lang opfns = OPF_NAMESPACES['opf'] m = opf.makeelement('{%s}manifest' % opfns) opf.insert(1, m) i = m.makeelement('{%s}item' % opfns, href=html_name, id='start') i.set('media-type', guess_type('a.xhtml')) m.append(i) i = m.makeelement('{%s}item' % opfns, href=toc_name, id='ncx') i.set('media-type', guess_type(toc_name)) m.append(i) s = opf.makeelement('{%s}spine' % opfns, toc="ncx") opf.insert(2, s) i = s.makeelement('{%s}itemref' % opfns, idref='start') s.append(i) CONTAINER = '''\ <?xml version="1.0"?> <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> <rootfiles> <rootfile full-path="{0}" media-type="application/oebps-package+xml"/> </rootfiles> </container> '''.format(prepare_string_for_xml(opf_name, True)).encode('utf-8') HTML = P('templates/new_book.html', data=True).decode('utf-8').replace( '_LANGUAGE_', prepare_string_for_xml(lang, True) ).replace( '_TITLE_', prepare_string_for_xml(mi.title) ).replace( '_AUTHORS_', prepare_string_for_xml(authors_to_string(mi.authors)) ).encode('utf-8') h = parse(HTML) pretty_html_tree(None, h) HTML = serialize(h, 'text/html') ncx = etree.tostring(create_toc(mi, opf, html_name, lang), encoding='utf-8', xml_declaration=True, pretty_print=True) pretty_xml_tree(opf) opf = etree.tostring(opf, encoding='utf-8', xml_declaration=True, pretty_print=True) if fmt == 'azw3': with TemporaryDirectory('create-azw3') as tdir, CurrentDir(tdir): for name, data in ((opf_name, opf), (html_name, HTML), (toc_name, ncx)): with open(name, 'wb') as f: f.write(data) c = Container(os.path.dirname(os.path.abspath(opf_name)), opf_name, DevNull()) opf_to_azw3(opf_name, path, c) else: with ZipFile(path, 'w', compression=ZIP_STORED) as zf: zf.writestr('mimetype', b'application/epub+zip', compression=ZIP_STORED) zf.writestr('META-INF/', b'', 0755) zf.writestr('META-INF/container.xml', CONTAINER) zf.writestr(opf_name, opf) zf.writestr(html_name, HTML) zf.writestr(toc_name, ncx)
def count_message(action, count, show_diff=False): msg = _('%(action)s %(num)s occurrences of %(query)s' % dict(num=count, query=errfind, action=action)) if show_diff and count > 0: d = MessageBox(MessageBox.INFO, _('Searching done'), prepare_string_for_xml(msg), parent=gui_parent, show_copy_button=False) d.diffb = b = d.bb.addButton(_('See what &changed'), d.bb.ActionRole) b.setIcon(QIcon(I('diff.png'))), d.set_details(None), b.clicked.connect(d.accept) b.clicked.connect(partial(show_current_diff, allow_revert=True)) d.exec_() else: info_dialog(gui_parent, _('Searching done'), prepare_string_for_xml(msg), show=True)
def insert_link(self, *args): link, name = self.ask_link() if not link: return url = self.parse_link(unicode(link)) if url.isValid(): url = unicode(url.toString()) if name: self.exec_command('insertHTML', '<a href="%s">%s</a>'%(prepare_string_for_xml(url, True), prepare_string_for_xml(name))) else: self.exec_command('createLink', url)
def make_highlighted_text(emph, text, positions): positions = sorted(set(positions) - {-1}) if positions: parts = [] pos = 0 for p in positions: ch = get_char(text, p) parts.append(prepare_string_for_xml(text[pos:p])) parts.append('<span style="%s">%s</span>' % (emph, prepare_string_for_xml(ch))) pos = p + len(ch) parts.append(prepare_string_for_xml(text[pos:])) return ''.join(parts) return text
def dump_text(self, elem, stylizer, page): ''' @elem: The element in the etree that we are working on. @stylizer: The style information attached to the element. ''' # We can only processes tags. If there isn't a tag return any text. if not isinstance(elem.tag, string_or_bytes) \ or namespace(elem.tag) != XHTML_NS: p = elem.getparent() if p is not None and isinstance(p.tag, string_or_bytes) and namespace(p.tag) == XHTML_NS \ and elem.tail: return [elem.tail] return [''] # Setup our variables. text = [''] tags = [] tag = barename(elem.tag) attribs = elem.attrib if tag == 'body': tag = 'div' tags.append(tag) # Remove attributes we won't want. if 'style' in attribs: del attribs['style'] # Turn the rest of the attributes into a string we can write with the tag. at = '' for k, v in attribs.items(): at += ' %s="%s"' % (k, prepare_string_for_xml(v, attribute=True)) # Write the tag. text.append('<%s%s' % (tag, at)) if tag in SELF_CLOSING_TAGS: text.append(' />') else: text.append('>') # Process tags that contain text. if hasattr(elem, 'text') and elem.text: text.append(self.prepare_string_for_html(elem.text)) # Recurse down into tags within the tag we are in. for item in elem: text += self.dump_text(item, stylizer, page) # Close all open tags. tags.reverse() for t in tags: if t not in SELF_CLOSING_TAGS: text.append('</%s>' % t) # Add the text that is outside of the tag. if hasattr(elem, 'tail') and elem.tail: text.append(self.prepare_string_for_html(elem.tail)) return text
def manifest_item_for_name(self, name): href = self.name_to_href(name, os.path.dirname(self.opf_name)) q = prepare_string_for_xml(href, attribute = True) existing = self.opf.xpath('//opf:manifest/opf:item[@href="{0}"]'.format(q), namespaces = self.namespaces) if not existing: return None return existing[0]
def prepare_string_for_html(self, raw): raw = prepare_string_for_xml(raw) raw = raw.replace(u'\u00ad', '­') raw = raw.replace(u'\u2014', '—') raw = raw.replace(u'\u2013', '–') raw = raw.replace(u'\u00a0', ' ') return raw
def current_index_changed(self, item): n = self.current_search_name if n: t = self.searches[n] else: t = '' self.desc.setText('<p><b>{}</b>: '.format(_('Search expression')) + prepare_string_for_xml(t))
def create_cover_page(self, input_fmt): templ = ''' <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head><style> html, body, img { height: 100%%; display: block; margin: 0; padding: 0; border-width: 0; } img { width: auto; margin-left:auto; margin-right: auto; } </style></head><body><img src="%s"/></body></html> ''' if input_fmt == 'epub': def cover_path(action, data): if action == 'write_image': data.write(BLANK_JPEG) return set_epub_cover(self, cover_path, (lambda *a: None), options={'template':templ}) raster_cover_name = find_cover_image(self, strict=True) if raster_cover_name is None: item = self.generate_item(name='cover.jpeg', id_prefix='cover') raster_cover_name = self.href_to_name(item.get('href'), self.opf_name) with self.open(raster_cover_name, 'wb') as dest: dest.write(BLANK_JPEG) item = self.generate_item(name='titlepage.html', id_prefix='titlepage') titlepage_name = self.href_to_name(item.get('href'), self.opf_name) raw = templ % prepare_string_for_xml(self.name_to_href(raster_cover_name, titlepage_name), True) with self.open(titlepage_name, 'wb') as f: f.write(raw.encode('utf-8')) spine = self.opf_xpath('//opf:spine')[0] ref = spine.makeelement(OPF('itemref'), idref=item.get('id')) self.insert_into_xml(spine, ref, index=0) self.dirty(self.opf_name) return raster_cover_name, titlepage_name
def get_resource(self, url, rtype='img', use_cache=True, timeout=default_timeout): ''' Download a resource (image/stylesheet/script). The resource is downloaded by visiting an simple HTML page that contains only that resource. The resource is then returned from the cache (therefore, to use this method you must not disable the cache). If use_cache is True then the cache is queried before loading the resource. This can result in a stale object if the resource has changed on the server, however, it is a big performance boost in the common case, by avoiding a roundtrip to the server. The resource is returned as a bytestring or None if it could not be loaded. ''' if not hasattr(self.nam, 'cache'): raise RuntimeError('Cannot get resources when the cache is disabled') if use_cache: ans = self.get_cached(url) if ans is not None: return ans try: tag = { 'img': '<img src="%s">', 'link': '<link href="%s"></link>', 'script': '<script src="%s"></script>', }[rtype] % prepare_string_for_xml(url, attribute=True) except KeyError: raise ValueError('Unknown resource type: %s' % rtype) self.page.mainFrame().setHtml( '''<!DOCTYPE html><html><body><div>{0}</div></body></html>'''.format(tag)) self._wait_for_load(timeout) ans = self.get_cached(url) if ans is not None: return ans
def make_highlighted_text(emph, text, positions): positions = sorted(set(positions) - {-1}, reverse=True) text = prepare_string_for_xml(text) for p in positions: ch = get_char(text, p) text = '%s<span style="%s">%s</span>%s' % (text[:p], emph, ch, text[p + len(ch) :]) return text
def prepare_pml(self, pml): # Give Chapters the form \\*='text'text\\*. This is used for generating # the TOC later. pml = re.sub(r'(?msu)(?P<c>\\x)(?P<text>.*?)(?P=c)', lambda match: '%s="%s"%s%s' % (match.group('c'), self.strip_pml(match.group('text')), match.group('text'), match.group('c')), pml) pml = re.sub(r'(?msu)(?P<c>\\X[0-4])(?P<text>.*?)(?P=c)', lambda match: '%s="%s"%s%s' % (match.group('c'), self.strip_pml(match.group('text')), match.group('text'), match.group('c')), pml) # Remove comments pml = re.sub(r'(?mus)\\v(?P<text>.*?)\\v', '', pml) # Remove extra white spaces. pml = re.sub(r'(?mus)[ ]{2,}', ' ', pml) pml = re.sub(r'(?mus)^[ ]*(?=.)', '', pml) pml = re.sub(r'(?mus)(?<=.)[ ]*$', '', pml) pml = re.sub(r'(?mus)^[ ]*$', '', pml) # Footnotes and Sidebars. pml = re.sub(r'(?mus)<footnote\s+id="(?P<target>.+?)">\s*(?P<text>.*?)\s*</footnote>', lambda match: '\\FN="%s"%s\\FN' % (match.group('target'), match.group('text')) if match.group('text') else '', pml) pml = re.sub(r'(?mus)<sidebar\s+id="(?P<target>.+?)">\s*(?P<text>.*?)\s*</sidebar>', lambda match: '\\SB="%s"%s\\SB' % (match.group('target'), match.group('text')) if match.group('text') else '', pml) # Convert &'s into entities so & in the text doesn't get turned into # &. It will display as & pml = pml.replace('&', '&') # Replace \\a and \\U with either the unicode character or the entity. pml = re.sub(r'\\a(?P<num>\d{3})', lambda match: '&#%s;' % match.group('num'), pml) pml = re.sub(r'\\U(?P<num>[0-9a-f]{4})', lambda match: '%s' % my_unichr(int(match.group('num'), 16)), pml) pml = prepare_string_for_xml(pml) return pml
def condition_to_html(self, condition): col, a, v = condition dt = self.fm[col]['datatype'] c = self.fm[col]['name'] action_name = a if col in ConditionEditor.ACTION_MAP: # look for a column-name-specific label for trans, ac in ConditionEditor.ACTION_MAP[col]: if ac == a: action_name = trans break elif dt in ConditionEditor.ACTION_MAP: # Look for a type-specific label for trans, ac in ConditionEditor.ACTION_MAP[dt]: if ac == a: action_name = trans break else: # Wasn't a type-specific or column-specific label. Look for a text-type for dt in ['single', 'multiple']: for trans, ac in ConditionEditor.ACTION_MAP[dt]: if ac == a: action_name = trans break else: continue break if action_name == Rule.INVALID_CONDITION: return ( _('<li>The condition using column <b>%(col)s</b> is <b>invalid</b>') % dict(col=c)) return ( _('<li>If the <b>%(col)s</b> column <b>%(action)s</b> value: <b>%(val)s</b>') % dict( col=c, action=action_name, val=prepare_string_for_xml(v)))
def run_text_search(search, current_editor, current_editor_name, searchable_names, gui_parent, show_editor, edit_file): try: pat = get_search_regex(search) except InvalidRegex as e: return error_dialog(gui_parent, _('Invalid regex'), '<p>' + _( 'The regular expression you entered is invalid: <pre>{0}</pre>With error: {1}').format( prepare_string_for_xml(e.regex), error_message(e)), show=True) editor, where, files, do_all, marked = initialize_search_request(search, 'count', current_editor, current_editor_name, searchable_names) with BusyCursor(): if editor is not None: if editor.find_text(pat): return True if not files and editor.find_text(pat, wrap=True): return True for fname, syntax in iteritems(files): ed = editors.get(fname, None) if ed is not None: if ed.find_text(pat, complete=True): show_editor(fname) return True else: root = current_container().parsed(fname) if hasattr(root, 'xpath'): raw = tostring(root, method='text', encoding='unicode', with_tail=True) else: raw = current_container().raw_data(fname) if pat.search(raw) is not None: edit_file(fname, syntax) if editors[fname].find_text(pat, complete=True): return True msg = '<p>' + _('No matches were found for %s') % ('<pre style="font-style:italic">' + prepare_string_for_xml(search['find']) + '</pre>') return error_dialog(gui_parent, _('Not found'), msg, show=True)
def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False): c = self.textCursor() template, alt = "url(%s)", "" left = min(c.position(), c.anchor) if self.syntax == "html": left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, c.KeepAnchor) href = prepare_string_for_xml(href, True) if fullpage: template = """\ <div style="page-break-before:always; page-break-after:always; page-break-inside:avoid">\ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \ version="1.1" width="100%%" height="100%%" viewBox="0 0 1200 1600" preserveAspectRatio="{}">\ <image width="1200" height="1600" xlink:href="%s"/>\ </svg></div>""".format( "xMidYMid meet" if preserve_aspect_ratio else "none" ) else: alt = _("Image") template = '<img alt="{0}" src="%s" />'.format(alt) text = template % href c.insertText(text) if self.syntax == "html" and not fullpage: c.setPosition(left + 10) c.setPosition(c.position() + len(alt), c.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), c.KeepAnchor) self.setTextCursor(c)
def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False, width=-1, height=-1): if width <= 0: width = 1200 if height <= 0: height = 1600 c = self.textCursor() template, alt = 'url(%s)', '' left = min(c.position(), c.anchor) if self.syntax == 'html': left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, c.KeepAnchor) href = prepare_string_for_xml(href, True) if fullpage: template = '''\ <div style="page-break-before:always; page-break-after:always; page-break-inside:avoid">\ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \ version="1.1" width="100%%" height="100%%" viewBox="0 0 {w} {h}" preserveAspectRatio="{a}">\ <image width="{w}" height="{h}" xlink:href="%s"/>\ </svg></div>'''.format(w=width, h=height, a='xMidYMid meet' if preserve_aspect_ratio else 'none') else: alt = _('Image') template = '<img alt="{0}" src="%s" />'.format(alt) text = template % href c.insertText(text) if self.syntax == 'html' and not fullpage: c.setPosition(left + 10) c.setPosition(c.position() + len(alt), c.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), c.KeepAnchor) self.setTextCursor(c)
def extract_content(self, output_dir): txt = '' self.log.info('Decompressing text...') for i in range(1, self.header_record.num_records + 1): self.log.debug('\tDecompressing text section %i' % i) title = self.header_record.chapter_titles[i-1] lines = [] title_added = False for line in self.decompress_text(i).splitlines(): line = fix_punct(line) line = line.strip() if not title_added and title in line: line = u'<h1 class="chapter">' + line + u'</h1>\n' title_added = True else: line = prepare_string_for_xml(line) lines.append(u'<p>%s</p>' % line) if not title_added: lines.insert(0, u'<h1 class="chapter">' + title + u'</h1>\n') txt += '\n'.join(lines) self.log.info('Converting text to OEB...') html = HTML_TEMPLATE % (self.header_record.title, txt) with open(os.path.join(output_dir, 'index.html'), 'wb') as index: index.write(html.encode('utf-8')) mi = self.get_metadata() manifest = [('index.html', None)] spine = ['index.html'] opf_writer(output_dir, 'metadata.opf', manifest, spine, mi) return os.path.join(output_dir, 'metadata.opf')
def __init__(self, name, namespace): BaseError.__init__(self, _('Invalid or missing namespace'), name) self.HELP = prepare_string_for_xml(_( 'This file has {0}. Its namespace must be {1}. Set the namespace by defining the xmlns' ' attribute on the <html> element, like this <html xmlns="{1}">').format( (_('incorrect namespace %s') % namespace) if namespace else _('no namespace'), XHTML_NS))
def template_for(syntax): mi = current_container().mi data = { 'TITLE':mi.title, 'AUTHOR': ' & '.join(mi.authors), } template = DEFAULT_TEMPLATES.get(syntax, '') return template.format(**{k:prepare_string_for_xml(v, True) for k, v in data.iteritems()})
def template_for(syntax): mi = current_container().mi data = { 'TITLE':mi.title, 'AUTHOR': ' & '.join(mi.authors), } return raw_template_for(syntax).format( **{k:prepare_string_for_xml(v, True) for k, v in data.iteritems()})
def index(ctx, rd): default_library = ctx.library_info(rd)[1] return rd.generate_static_output('/', partial( get_html, 'content-server/index.html', getattr(rd.opts, 'auto_reload_port', 0), ENTRY_POINT='book list', LOADING_MSG=prepare_string_for_xml(_('Loading library, please wait')), DEFAULT_LIBRARY=json_dumps(default_library) ))
def browse_book(self, id=None, category_sort=None): try: id_ = int(id) except: raise cherrypy.HTTPError(404, 'invalid id: %r'%id) ans = self.browse_render_details(id_, add_title=True) return self.browse_template('').format( title=prepare_string_for_xml(self.db.title(id_, index_is_id=True)), script='book();', main=ans)
def tag_text(elem): ans = tt_cache.get(elem) if ans is None: tag = elem.tag.rpartition('}')[-1] if elem.attrib: attribs = ' '.join('%s="%s"' % (k, prepare_string_for_xml(elem.get(k, ''), True)) for k in elem.keys()) return '<%s %s>' % (tag, attribs) ans = tt_cache[elem] = '<%s>' % tag
def no_match(): QApplication.restoreOverrideCursor() msg = '<p>' + _('No matches were found for %s.') % prepare_string_for_xml(state['find']) if not state['wrap']: msg += '<p>' + _('You have turned off search wrapping, so all text might not have been searched.' ' Try the search again, with wrapping enabled. Wrapping is enabled via the' ' "Wrap" checkbox at the bottom of the search panel.') return error_dialog( self.gui, _('Not found'), msg, show=True)
def manifest_item_for_name(self, name): href = self.name_to_href(name, posixpath.dirname(self.opf_name)) q = prepare_string_for_xml(href, attribute=True) existing = self.opf.xpath('//opf:manifest/opf:item[@href="%s"]'%q, namespaces={'opf':OPF_NS}) if not existing: return None return existing[0]
def browse_random(self, *args, **kwargs): import random try: book_id = random.choice(self.search_for_books('')) except IndexError: raise cherrypy.HTTPError(404, 'This library has no books') ans = self.browse_render_details(book_id, add_random_button=True, add_title=True) return self.browse_template('').format( title=prepare_string_for_xml(self.db.title(book_id, index_is_id=True)), script='book();', main=ans)
def rule_to_html(self, kind, col, rule): trans_kind = 'not found' if kind == 'color': trans_kind = _('color') else: for tt, t in icon_rule_kinds: if kind == t: trans_kind = tt break if not isinstance(rule, Rule): if kind == 'color': return _(''' <p>Advanced rule for column <b>%(col)s</b>: <pre>%(rule)s</pre> ''')%dict(col=col, rule=prepare_string_for_xml(rule)) elif self.rule_kind == 'emblem': return _(''' <p>Advanced rule: <pre>%(rule)s</pre> ''')%dict(rule=prepare_string_for_xml(rule)) else: return _(''' <p>Advanced rule: set <b>%(typ)s</b> for column <b>%(col)s</b>: <pre>%(rule)s</pre> ''')%dict(col=col, typ=trans_kind, rule=prepare_string_for_xml(rule)) conditions = [self.condition_to_html(c) for c in rule.conditions] sample = '' if kind != 'color' else ( _('(<span style="color: %s;">sample</span>)') % rule.color) if kind == 'emblem': return _('<p>Add the emblem <b>{0}</b> to the cover if the following conditions are met:</p>' '\n<ul>{1}</ul>').format(rule.color, ''.join(conditions)) return _('''\ <p>Set the <b>%(kind)s</b> of <b>%(col)s</b> to <b>%(color)s</b> %(sample)s if the following conditions are met:</p> <ul>%(rule)s</ul> ''') % dict(kind=trans_kind, col=col, color=rule.color, sample=sample, rule=''.join(conditions))
def insert_hyperlink(self, editor, target): c = editor.textCursor() if c.hasSelection(): c.insertText("") # delete any existing selected text ensure_not_within_tag_definition(c) c.insertText('<a href="%s">' % prepare_string_for_xml(target, True)) p = c.position() c.insertText("</a>") c.setPosition(p) # ensure cursor is positioned inside the newly created tag editor.setTextCursor(c)
def get_metadata(stream, extract_cover=True): """ Return metadata as a L{MetaInfo} object """ mi = MetaInformation(_('Unknown'), [_('Unknown')]) stream.seek(0) pml = '' if stream.name.endswith('.pmlz'): with TemporaryDirectory('_unpmlz') as tdir: zf = ZipFile(stream) zf.extractall(tdir) pmls = glob.glob(os.path.join(tdir, '*.pml')) for p in pmls: with open(p, 'r+b') as p_stream: pml += p_stream.read() if extract_cover: mi.cover_data = get_cover(os.path.splitext(os.path.basename(stream.name))[0], tdir, True) else: pml = stream.read() if extract_cover: mi.cover_data = get_cover(os.path.splitext(os.path.basename(stream.name))[0], os.path.abspath(os.path.dirname(stream.name))) for comment in re.findall(r'(?mus)\\v.*?\\v', pml): m = re.search(r'TITLE="(.*?)"', comment) if m: mi.title = re.sub('[\x00-\x1f]', '', prepare_string_for_xml(m.group(1).strip().decode('cp1252', 'replace'))) m = re.search(r'AUTHOR="(.*?)"', comment) if m: if mi.authors == [_('Unknown')]: mi.authors = [] mi.authors.append(re.sub('[\x00-\x1f]', '', prepare_string_for_xml(m.group(1).strip().decode('cp1252', 'replace')))) m = re.search(r'PUBLISHER="(.*?)"', comment) if m: mi.publisher = re.sub('[\x00-\x1f]', '', prepare_string_for_xml(m.group(1).strip().decode('cp1252', 'replace'))) m = re.search(r'COPYRIGHT="(.*?)"', comment) if m: mi.rights = re.sub('[\x00-\x1f]', '', prepare_string_for_xml(m.group(1).strip().decode('cp1252', 'replace'))) m = re.search(r'ISBN="(.*?)"', comment) if m: mi.isbn = re.sub('[\x00-\x1f]', '', prepare_string_for_xml(m.group(1).strip().decode('cp1252', 'replace'))) return mi
def insert_hyperlink(self, editor, target, text): editor.highlighter.join() c = editor.textCursor() if c.hasSelection(): c.insertText('') # delete any existing selected text ensure_not_within_tag_definition(c) c.insertText('<a href="%s">' % prepare_string_for_xml(target, True)) p = c.position() c.insertText('</a>') c.setPosition( p) # ensure cursor is positioned inside the newly created tag if text: c.insertText(text) editor.setTextCursor(c)
def rule_to_html(self, kind, col, rule): trans_kind = 'not found' if kind == 'color': trans_kind = _('color') else: for tt, t in icon_rule_kinds: if kind == t: trans_kind = tt break if not isinstance(rule, Rule): if kind == 'color': return _(''' <p>Advanced Rule for column <b>%(col)s</b>: <pre>%(rule)s</pre> ''') % dict(col=col, rule=prepare_string_for_xml(rule)) else: return _(''' <p>Advanced Rule: set <b>%(typ)s</b> for column <b>%(col)s</b>: <pre>%(rule)s</pre> ''') % dict( col=col, typ=trans_kind, rule=prepare_string_for_xml(rule)) conditions = [self.condition_to_html(c) for c in rule.conditions] sample = '' if kind != 'color' else ( _('(<span style="color: %s;">sample</span>)') % rule.color) return _('''\ <p>Set the <b>%(kind)s</b> of <b>%(col)s</b> to <b>%(color)s</b> %(sample)s if the following conditions are met:</p> <ul>%(rule)s</ul> ''') % dict(kind=trans_kind, col=col, color=rule.color, sample=sample, rule=''.join(conditions))
def prepare_pml(self, pml): # Give Chapters the form \\*='text'text\\*. This is used for generating # the TOC later. pml = re.sub( r'(?msu)(?P<c>\\x)(?P<text>.*?)(?P=c)', lambda match: '%s="%s"%s%s' % (match.group('c'), self.strip_pml(match.group('text')), match.group('text'), match.group('c')), pml) pml = re.sub( r'(?msu)(?P<c>\\X[0-4])(?P<text>.*?)(?P=c)', lambda match: '%s="%s"%s%s' % (match.group('c'), self.strip_pml(match.group('text')), match.group('text'), match.group('c')), pml) # Remove comments pml = re.sub(r'(?mus)\\v(?P<text>.*?)\\v', '', pml) # Remove extra white spaces. pml = re.sub(r'(?mus)[ ]{2,}', ' ', pml) pml = re.sub(r'(?mus)^[ ]*(?=.)', '', pml) pml = re.sub(r'(?mus)(?<=.)[ ]*$', '', pml) pml = re.sub(r'(?mus)^[ ]*$', '', pml) # Footnotes and Sidebars. pml = re.sub( r'(?mus)<footnote\s+id="(?P<target>.+?)">\s*(?P<text>.*?)\s*</footnote>', lambda match: '\\FN="%s"%s\\FN' % (match.group('target'), match.group('text')) if match.group('text') else '', pml) pml = re.sub( r'(?mus)<sidebar\s+id="(?P<target>.+?)">\s*(?P<text>.*?)\s*</sidebar>', lambda match: '\\SB="%s"%s\\SB' % (match.group('target'), match.group('text')) if match.group('text') else '', pml) # Convert &'s into entities so & in the text doesn't get turned into # &. It will display as & pml = pml.replace('&', '&') # Replace \\a and \\U with either the unicode character or the entity. pml = re.sub(r'\\a(?P<num>\d{3})', lambda match: '&#%s;' % match.group('num'), pml) pml = re.sub( r'\\U(?P<num>[0-9a-f]{4})', lambda match: '%s' % my_unichr(int(match.group('num'), 16)), pml) pml = prepare_string_for_xml(pml) return pml
def get_static_text(self, otext, positions): st = self.rendered_text_cache.get(otext) if st is None: text = (otext or '').ljust(self.max_text_length + 1, '\xa0') text = make_highlighted_text('color: magenta', text, positions) desc = self.descriptions.get(otext) if desc: text += ' - <i>%s</i>' % prepare_string_for_xml(desc) color = self.palette().color(QPalette.ColorRole.Text).name() text = '<span style="color: %s">%s</span>' % (color, text) st = self.rendered_text_cache[otext] = QStaticText(text) st.setTextOption(self.text_option) st.setTextFormat(Qt.TextFormat.RichText) st.prepare(font=self.parent().font()) return st
def insert_hyperlink(self, editor, target, text, template=None): template = template or DEFAULT_LINK_TEMPLATE template = template.replace('_TARGET_', prepare_string_for_xml(target, True)) offset = template.find('_TEXT_') editor.highlighter.join() c = editor.textCursor() if c.hasSelection(): c.insertText('') # delete any existing selected text ensure_not_within_tag_definition(c) p = c.position() + offset c.insertText(template.replace('_TEXT_', text or '')) c.setPosition( p) # ensure cursor is positioned inside the newly created tag editor.setTextCursor(c)
def text_from_rule(rule, parent): try: query = elided_text(rule['query'], font=parent.font(), width=200, pos='right') text = _('If the tag <b>{match_type}</b> <b>{query}</b>').format( match_type=MATCH_TYPE_MAP[rule['match_type']].text, query=prepare_string_for_xml(query)) for action in rule['actions']: text += '<br>' + ACTION_MAP[action['type']].short_text if action.get('data'): ad = elided_text(action['data'], font=parent.font(), width=200, pos='right') text += f' <code>{prepare_string_for_xml(ad)}</code>' except Exception: import traceback traceback.print_exc() text = _('This rule is invalid, please remove it') return text
def mlize_spine(self, oeb_book): output = [ u'<html><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" /><title>%s</title></head><body>' % (prepare_string_for_xml(self.book_title)) ] for item in oeb_book.spine: self.log.debug('Converting %s to HTML...' % item.href) self.rewrite_ids(item.data, item) rewrite_links(item.data, partial(self.rewrite_link, page=item)) stylizer = Stylizer(item.data, item.href, oeb_book, self.opts) output += self.dump_text(item.data.find(XHTML('body')), stylizer, item) output.append('\n\n') output.append('</body></html>') return ''.join(output)
def to_doc(self, index): data = index.data(Qt.ItemDataRole.UserRole) if data is None: html = _('<b>This shortcut no longer exists</b>') elif data.is_shortcut: shortcut = data.data # Shortcut keys = [ unicode_type(k.toString(k.NativeText)) for k in shortcut['keys'] ] if not keys: keys = _('None') else: keys = ', '.join(keys) html = '<b>%s</b><br>%s: %s' % (prepare_string_for_xml( shortcut['name']), _('Shortcuts'), prepare_string_for_xml(keys)) else: # Group html = data.data doc = QTextDocument() doc.setHtml(html) return doc
def search(self, text, index, backwards=False): text = prepare_string_for_xml(text.lower()) pmap = [(i, path) for i, path in enumerate(self.spine)] if backwards: pmap.reverse() for i, path in pmap: if (backwards and i < index) or (not backwards and i > index): with open(path, 'rb') as f: raw = f.read().decode(path.encoding) try: raw = xml_replace_entities(raw) except: pass if text in raw.lower(): return i
def run_text_search(search, current_editor, current_editor_name, searchable_names, gui_parent, show_editor, edit_file): try: pat = get_search_regex(search) except InvalidRegex as e: return error_dialog( gui_parent, _('Invalid regex'), '<p>' + _('The regular expression you entered is invalid: <pre>{0}</pre>With error: {1}' ).format(prepare_string_for_xml(e.regex), error_message(e)), show=True) editor, where, files, do_all, marked = initialize_search_request( search, 'count', current_editor, current_editor_name, searchable_names) with BusyCursor(): if editor is not None: if editor.find_text(pat): return True if not files and editor.find_text(pat, wrap=True): return True for fname, syntax in iteritems(files): ed = editors.get(fname, None) if ed is not None: if ed.find_text(pat, complete=True): show_editor(fname) return True else: if file_matches_pattern(fname, pat): edit_file(fname, syntax) if editors[fname].find_text(pat, complete=True): return True msg = '<p>' + _('No matches were found for %s') % ( '<pre style="font-style:italic">' + prepare_string_for_xml(search['find']) + '</pre>') return error_dialog(gui_parent, _('Not found'), msg, show=True)
def find(self, query): if not query: return try: idx = self._model.find(query) except ParseException: self.search.search_done(False) return self.search.search_done(True) if not idx.isValid(): info_dialog(self, _('No matches'), _('Could not find any shortcuts matching <i>{}</i>').format(prepare_string_for_xml(query)), show=True, show_copy_button=False) return self.highlight_index(idx)
def __call__(self, text='Testing message popup', show_undo=True, timeout=5000, has_markup=False): text = '<p>' + (text if has_markup else prepare_string_for_xml(text)) if show_undo: text += '\xa0\xa0<a style="text-decoration: none" href="undo://me.com">{}</a>'.format( _('Undo')) text += f'\xa0\xa0<a style="text-decoration: none; color: {self.color}" href="close://me.com">✖</a>' self.setText(text) self.resize(self.sizeHint()) self.position_in_parent() self.show() self.raise_() self.close_timer.start(timeout)
def mlize_spine(self, oeb_book): output = [] for item in oeb_book.spine: self.log.debug('Converting %s to HTML...' % item.href) self.rewrite_ids(item.data, item) rewrite_links(item.data, partial(self.rewrite_link, page=item)) stylizer = Stylizer(item.data, item.href, oeb_book, self.opts) output += self.dump_text(item.data.find(XHTML('body')), stylizer, item) output.append('\n\n') if self.opts.htmlz_class_style == 'external': css = '<link href="style.css" rel="stylesheet" type="text/css" />' else: css = '<style type="text/css">' + self.get_css(oeb_book) + '</style>' title = '<title>%s</title>' % prepare_string_for_xml(self.book_title) output = ['<html><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'] + \ [css] + [title, '</head><body>'] + output + ['</body></html>'] return ''.join(output)
def insert_image(self, href): c = self.textCursor() template, alt = 'url(%s)', '' left = min(c.position(), c.anchor) if self.syntax == 'html': left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, c.KeepAnchor) alt = _('Image') template = '<img alt="{0}" src="%s" />'.format(alt) href = prepare_string_for_xml(href, True) text = template % href c.insertText(text) if self.syntax == 'html': c.setPosition(left + 10) c.setPosition(c.position() + len(alt), c.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), c.KeepAnchor) self.setTextCursor(c)
def __str__(self): s = u'' open_containers = collections.deque() for c in self.content: if isinstance(c, string_or_bytes): s += prepare_string_for_xml(c).replace('\0', '') elif c is None: if open_containers: p = open_containers.pop() s += u'</%s>'%(p.name,) else: s += unicode_type(c) if not c.self_closing: open_containers.append(c) if len(open_containers) > 0: if len(open_containers) == 1: s += u'</%s>'%(open_containers[0].name,) else: raise LRFParseError('Malformed text stream %s'%([i.name for i in open_containers if isinstance(i, Text.TextTag)],)) return s
def insert_image(self, href, fullpage=False, preserve_aspect_ratio=False, width=-1, height=-1): if width <= 0: width = 1200 if height <= 0: height = 1600 c = self.textCursor() template, alt = 'url(%s)', '' left = min(c.position(), c.anchor()) if self.syntax == 'html': left, right = self.get_range_inside_tag() c.setPosition(left) c.setPosition(right, QTextCursor.MoveMode.KeepAnchor) href = prepare_string_for_xml(href, True) if fullpage: template = '''\ <div style="page-break-before:always; page-break-after:always; page-break-inside:avoid">\ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \ version="1.1" width="100%%" height="100%%" viewBox="0 0 {w} {h}" preserveAspectRatio="{a}">\ <image width="{w}" height="{h}" xlink:href="%s"/>\ </svg></div>'''.format(w=width, h=height, a='xMidYMid meet' if preserve_aspect_ratio else 'none') else: alt = _('Image') template = '<img alt="{0}" src="%s" />'.format(alt) text = template % href c.insertText(text) if self.syntax == 'html' and not fullpage: c.setPosition(left + 10) c.setPosition(c.position() + len(alt), QTextCursor.MoveMode.KeepAnchor) else: c.setPosition(left) c.setPosition(left + len(text), QTextCursor.MoveMode.KeepAnchor) self.setTextCursor(c)
def create_cover_page(self, input_fmt): templ = ''' <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head><style> html, body, img { height: 100vh; display: block; margin: 0; padding: 0; border-width: 0; } img { width: auto; height: auto; margin-left: auto; margin-right: auto; max-width: 100vw; max-height: 100vh } </style></head><body><img src="%s"/></body></html> ''' if input_fmt == 'epub': def cover_path(action, data): if action == 'write_image': data.write(BLANK_JPEG) return set_epub_cover(self, cover_path, (lambda *a: None), options={'template': templ}) raster_cover_name = find_cover_image(self, strict=True) if raster_cover_name is None: item = self.generate_item(name='cover.jpeg', id_prefix='cover') raster_cover_name = self.href_to_name(item.get('href'), self.opf_name) with self.open(raster_cover_name, 'wb') as dest: dest.write(BLANK_JPEG) item = self.generate_item(name='titlepage.html', id_prefix='titlepage') titlepage_name = self.href_to_name(item.get('href'), self.opf_name) raw = templ % prepare_string_for_xml( self.name_to_href(raster_cover_name, titlepage_name), True) with self.open(titlepage_name, 'wb') as f: f.write(raw.encode('utf-8')) spine = self.opf_xpath('//opf:spine')[0] ref = spine.makeelement(OPF('itemref'), idref=item.get('id')) self.insert_into_xml(spine, ref, index=0) self.dirty(self.opf_name) return raster_cover_name, titlepage_name
def get_resource(self, url, rtype='img', use_cache=True, timeout=default_timeout): ''' Download a resource (image/stylesheet/script). The resource is downloaded by visiting an simple HTML page that contains only that resource. The resource is then returned from the cache (therefore, to use this method you must not disable the cache). If use_cache is True then the cache is queried before loading the resource. This can result in a stale object if the resource has changed on the server, however, it is a big performance boost in the common case, by avoiding a roundtrip to the server. The resource is returned as a bytestring or None if it could not be loaded. ''' if not hasattr(self.nam, 'cache'): raise RuntimeError( 'Cannot get resources when the cache is disabled') if use_cache: ans = self.get_cached(url) if ans is not None: return ans try: tag = { 'img': '<img src="%s">', 'link': '<link href="%s"></link>', 'script': '<script src="%s"></script>', }[rtype] % prepare_string_for_xml(url, attribute=True) except KeyError: raise ValueError('Unknown resource type: %s' % rtype) self.page.mainFrame().setHtml( '''<!DOCTYPE html><html><body><div>{0}</div></body></html>'''. format(tag)) self._wait_for_load(timeout) ans = self.get_cached(url) if ans is not None: return ans
def convert_basic(txt, title='', epub_split_size_kb=0): ''' Converts plain text to html by putting all paragraphs in <p> tags. It condense and retains blank lines when necessary. Requires paragraphs to be in single line format. ''' txt = clean_txt(txt) txt = split_txt(txt, epub_split_size_kb) lines = [] blank_count = 0 # Split into paragraphs based on having a blank line between text. for line in txt.split('\n'): if line.strip(): blank_count = 0 lines.append(u'<p>%s</p>' % prepare_string_for_xml(line.replace('\n', ' '))) else: blank_count += 1 if blank_count == 2: lines.append(u'<p> </p>') return HTML_TEMPLATE % (title, u'\n'.join(lines))
def unhandled_exception(self, exc_type, value, tb): if exc_type is KeyboardInterrupt: return import traceback try: sio = PolyglotStringIO(errors='replace') try: from calibre.debug import print_basic_debug_info print_basic_debug_info(out=sio) except: pass traceback.print_exception(exc_type, value, tb, file=sio) if getattr(value, 'locking_debug_msg', None): prints(value.locking_debug_msg, file=sio) fe = sio.getvalue() msg = '<b>%s</b>:'%exc_type.__name__ + prepare_string_for_xml(as_unicode(value)) error_dialog(self, _('Unhandled exception'), msg, det_msg=fe, show=True) prints(fe, file=sys.stderr) except BaseException: pass except: pass
def condition_to_html(self, condition): col, a, v = condition dt = self.fm[col]['datatype'] c = self.fm[col]['name'] action_name = a if col in ConditionEditor.ACTION_MAP: # look for a column-name-specific label for trans, ac in ConditionEditor.ACTION_MAP[col]: if ac == a: action_name = trans break elif dt in ConditionEditor.ACTION_MAP: # Look for a type-specific label for trans, ac in ConditionEditor.ACTION_MAP[dt]: if ac == a: action_name = trans break else: # Wasn't a type-specific or column-specific label. Look for a text-type for dt in ['single', 'multiple']: for trans, ac in ConditionEditor.ACTION_MAP[dt]: if ac == a: action_name = trans break else: continue break if action_name == Rule.INVALID_CONDITION: return (_( '<li>The condition using column <b>%(col)s</b> is <b>invalid</b>' ) % dict(col=c)) return (_( '<li>If the <b>%(col)s</b> column <b>%(action)s</b> %(val_label)s<b>%(val)s</b>' ) % dict(col=c, action=action_name, val=prepare_string_for_xml(v), val_label=_('value: ') if v else ''))
def __init__(self, parent=None): BasicSettings.__init__(self, parent) self.dictionaries_changed = self.snippets_changed = False self.l = l = QFormLayout(self) self.setLayout(l) fc = FontFamilyChooser(self) self('editor_font_family', widget=fc, getter=attrgetter('font_family'), setter=lambda x, val: setattr(x, 'font_family', val)) fc.family_changed.connect(self.emit_changed) l.addRow(_('Editor font family:'), fc) fs = self('editor_font_size') fs.setMinimum(8), fs.setSuffix(' pt'), fs.setMaximum(50) l.addRow(_('Editor font &size:'), fs) choices = self.theme_choices() theme = self.choices_widget('editor_theme', choices, 'auto', 'auto') self.custom_theme_button = b = QPushButton(_('Create/edit &custom color schemes')) b.clicked.connect(self.custom_theme) h = QHBoxLayout() h.addWidget(theme), h.addWidget(b) l.addRow(_('&Color scheme:'), h) l.labelForField(h).setBuddy(theme) tw = self('editor_tab_stop_width') tw.setMinimum(2), tw.setSuffix(_(' characters')), tw.setMaximum(20) l.addRow(_('W&idth of tabs:'), tw) self.tb = b = QPushButton(_('Change &templates')) l.addRow(_('Templates for new files:'), b) connect_lambda(b.clicked, self, lambda self: TemplatesDialog(self).exec_()) lw = self('editor_line_wrap') lw.setText(_('&Wrap long lines in the editor')) l.addRow(lw) lw = self('replace_entities_as_typed') lw.setText(_('&Replace HTML entities as they are typed')) lw.setToolTip('<p>' + _( 'With this option, every time you type in a complete html entity, such as &hellip;' ' it is automatically replaced by its corresponding character. The replacement' ' happens only when the trailing semi-colon is typed.')) l.addRow(lw) lw = self('auto_close_tags') lw.setText(_('Auto close t&ags when typing </')) lw.setToolTip('<p>' + prepare_string_for_xml(_( 'With this option, every time you type </ the current HTML closing tag is auto-completed'))) l.addRow(lw) lw = self('editor_show_char_under_cursor') lw.setText(_('Show the &name of the current character before the cursor along with the line and column number')) l.addRow(lw) lw = self('pretty_print_on_open') lw.setText(_('Beautify individual &files automatically when they are opened')) lw.setToolTip('<p>' + _( 'This will cause the beautify current file action to be performed automatically every' ' time you open a HTML/CSS/etc. file for editing.')) l.addRow(lw) lw = self('inline_spell_check') lw.setText(_('Show &misspelled words underlined in the code view')) lw.setToolTip('<p>' + _( 'This will cause spelling errors to be highlighted in the code view' ' for easy correction as you type.')) l.addRow(lw) lw = self('editor_accepts_drops') lw.setText(_('Allow drag and drop &editing of text')) lw.setToolTip('<p>' + _( 'Allow using drag and drop to move text around in the editor.' ' It can be useful to turn this off if you have a misbehaving touchpad.')) l.addRow(lw) self.dictionaries = d = QPushButton(_('Manage &spelling dictionaries'), self) d.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) d.clicked.connect(self.manage_dictionaries) l.addRow(d) self.snippets = s = QPushButton(_('Manage sni&ppets'), self) s.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) s.clicked.connect(self.manage_snippets) l.addRow(s)
def create_cover_page(self, input_fmt): templ = ''' <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head><style> html, body, img { height: 100vh; display: block; margin: 0; padding: 0; border-width: 0; } img { width: 100%%; height: 100%%; object-fit: contain; margin-left: auto; margin-right: auto; max-width: 100vw; max-height: 100vh; top: 50vh; transform: translateY(-50%%); position: relative; } body.cover-fill img { object-fit: fill; } </style></head><body><img src="%s"/></body></html> ''' def generic_cover(): if self.book_metadata is not None: from calibre.ebooks.covers import create_cover mi = self.book_metadata return create_cover(mi.title, mi.authors, mi.series, mi.series_index) return BLANK_JPEG if input_fmt == 'epub': def image_callback(cover_image, wrapped_image): if cover_image: image_callback.cover_data = self.raw_data(cover_image, decode=False) if wrapped_image and not getattr(image_callback, 'cover_data', None): image_callback.cover_data = self.raw_data(wrapped_image, decode=False) def cover_path(action, data): if action == 'write_image': cdata = getattr(image_callback, 'cover_data', None) or generic_cover() data.write(cdata) raster_cover_name, titlepage_name = set_epub_cover( self, cover_path, (lambda *a: None), options={'template': templ}, image_callback=image_callback) else: raster_cover_name = find_cover_image(self, strict=True) if raster_cover_name is None: item = self.generate_item(name='cover.jpeg', id_prefix='cover') raster_cover_name = self.href_to_name(item.get('href'), self.opf_name) with self.open(raster_cover_name, 'wb') as dest: dest.write(generic_cover()) item = self.generate_item(name='titlepage.html', id_prefix='titlepage') titlepage_name = self.href_to_name(item.get('href'), self.opf_name) raw = templ % prepare_string_for_xml( self.name_to_href(raster_cover_name, titlepage_name), True) with self.open(titlepage_name, 'wb') as f: f.write(raw.encode('utf-8')) spine = self.opf_xpath('//opf:spine')[0] ref = spine.makeelement(OPF('itemref'), idref=item.get('id')) self.insert_into_xml(spine, ref, index=0) self.dirty(self.opf_name) return raster_cover_name, titlepage_name
def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=True, rating_font='Liberation Serif', rtl=False): if field_list is None: field_list = get_field_list(mi) ans = [] comment_fields = [] isdevice = not hasattr(mi, 'id') row = u'<td class="title">%s</td><td class="value">%s</td>' p = prepare_string_for_xml a = partial(prepare_string_for_xml, attribute=True) book_id = getattr(mi, 'id', 0) for field in (field for field, display in field_list if display): try: metadata = mi.metadata_for_field(field) except: continue if not metadata: continue if field == 'sort': field = 'title_sort' if metadata['is_custom'] and metadata['datatype'] in {'bool', 'int', 'float'}: isnull = mi.get(field) is None else: isnull = mi.is_null(field) if isnull: continue name = metadata['name'] if not name: name = field name += ':' if metadata['datatype'] == 'comments' or field == 'comments': val = getattr(mi, field) if val: val = force_unicode(val) comment_fields.append(comments_to_html(val)) elif metadata['datatype'] == 'rating': val = getattr(mi, field) if val: val = val/2.0 ans.append((field, u'<td class="title">%s</td><td class="rating value" ' 'style=\'font-family:"%s"\'>%s</td>'%( name, rating_font, u'\u2605'*int(val)))) elif metadata['datatype'] == 'composite': val = getattr(mi, field) if val: val = force_unicode(val) if metadata['display'].get('contains_html', False): ans.append((field, row % (name, comments_to_html(val)))) else: if not metadata['is_multiple']: val = '<a href="%s" title="%s">%s</a>' % ( search_href(field, val), _('Click to see books with {0}: {1}').format(metadata['name'], a(val)), p(val)) else: all_vals = [v.strip() for v in val.split(metadata['is_multiple']['list_to_ui']) if v.strip()] links = ['<a href="%s" title="%s">%s</a>' % ( search_href(field, x), _('Click to see books with {0}: {1}').format( metadata['name'], a(x)), p(x)) for x in all_vals] val = metadata['is_multiple']['list_to_ui'].join(links) ans.append((field, row % (name, val))) elif field == 'path': if mi.path: path = force_unicode(mi.path, filesystem_encoding) scheme = u'devpath' if isdevice else u'path' url = prepare_string_for_xml(path if isdevice else unicode(book_id), True) pathstr = _('Click to open') extra = '' if isdevice: durl = url if durl.startswith('mtp:::'): durl = ':::'.join((durl.split(':::'))[2:]) extra = '<br><span style="font-size:smaller">%s</span>'%( prepare_string_for_xml(durl)) link = u'<a href="%s:%s" title="%s">%s</a>%s' % (scheme, url, prepare_string_for_xml(path, True), pathstr, extra) ans.append((field, row % (name, link))) elif field == 'formats': if isdevice: continue path = mi.path or '' bpath = '' if path: h, t = os.path.split(path) bpath = os.sep.join((os.path.basename(h), t)) data = ({ 'fmt':x, 'path':a(path or ''), 'fname':a(mi.format_files.get(x, '')), 'ext':x.lower(), 'id':book_id, 'bpath':bpath, 'sep':os.sep } for x in mi.formats) fmts = [u'<a data-full-path="{path}{sep}{fname}.{ext}" title="{bpath}{sep}{fname}.{ext}" href="format:{id}:{fmt}">{fmt}</a>'.format(**x) for x in data] ans.append((field, row % (name, u', '.join(fmts)))) elif field == 'identifiers': urls = urls_from_identifiers(mi.identifiers) links = [u'<a href="%s" title="%s:%s" data-item="%s">%s</a>' % (a(url), a(id_typ), a(id_val), a(item_data(field, id_typ, book_id)), p(namel)) for namel, id_typ, id_val, url in urls] links = u', '.join(links) if links: ans.append((field, row % (_('Ids')+':', links))) elif field == 'authors' and not isdevice: authors = [] formatter = EvalFormatter() for aut in mi.authors: link = '' if mi.author_link_map[aut]: link = lt = mi.author_link_map[aut] elif default_author_link: if default_author_link == 'search-calibre': link = search_href('authors', aut) lt = a(_('Search the calibre library for books by %s') % aut) else: vals = {'author': aut.replace(' ', '+')} try: vals['author_sort'] = mi.author_sort_map[aut].replace(' ', '+') except: vals['author_sort'] = aut.replace(' ', '+') link = lt = a(formatter.safe_format(default_author_link, vals, '', vals)) aut = p(aut) if link: authors.append(u'<a calibre-data="authors" title="%s" href="%s">%s</a>'%(lt, link, aut)) else: authors.append(aut) ans.append((field, row % (name, u' & '.join(authors)))) elif field == 'languages': if not mi.languages: continue names = filter(None, map(calibre_langcode_to_name, mi.languages)) ans.append((field, row % (name, u', '.join(names)))) elif field == 'publisher': if not mi.publisher: continue val = '<a href="%s" title="%s" data-item="%s">%s</a>' % ( search_href('publisher', mi.publisher), _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)), a(item_data('publisher', mi.publisher, book_id)), p(mi.publisher)) ans.append((field, row % (name, val))) elif field == 'title': # otherwise title gets metadata['datatype'] == 'text' # treatment below with a click to search link (which isn't # too bad), and a right-click 'Delete' option to delete # the title (which is bad). val = mi.format_field(field)[-1] ans.append((field, row % (name, val))) else: val = mi.format_field(field)[-1] if val is None: continue val = p(val) if metadata['datatype'] == 'series': sidx = mi.get(field+'_index') if sidx is None: sidx = 1.0 try: st = metadata['search_terms'][0] except Exception: st = field series = getattr(mi, field) val = _( '%(sidx)s of <a href="%(href)s" title="%(tt)s" data-item="%(data)s">' '<span class="%(cls)s">%(series)s</span></a>') % dict( sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls="series_name", series=p(series), href=search_href(st, series), data=a(item_data(field, series, book_id)), tt=p(_('Click to see books in this series'))) elif metadata['datatype'] == 'datetime': aval = getattr(mi, field) if is_date_undefined(aval): continue elif metadata['datatype'] == 'text' and metadata['is_multiple']: try: st = metadata['search_terms'][0] except Exception: st = field all_vals = mi.get(field) if field == 'tags': all_vals = sorted(all_vals, key=sort_key) links = ['<a href="%s" title="%s" data-item="%s">%s</a>' % ( search_href(st, x), _('Click to see books with {0}: {1}').format( metadata['name'], a(x)), a(item_data(field, x, book_id)), p(x)) for x in all_vals] val = metadata['is_multiple']['list_to_ui'].join(links) elif metadata['datatype'] == 'text' or metadata['datatype'] == 'enumeration': # text/is_multiple handled above so no need to add the test to the if try: st = metadata['search_terms'][0] except Exception: st = field val = '<a href="%s" title="%s" data-item="%s">%s</a>' % ( search_href(st, val), a(_('Click to see books with {0}: {1}').format(metadata['name'], val)), a(item_data(field, val, book_id)), p(val)) ans.append((field, row % (name, val))) dc = getattr(mi, 'device_collections', []) if dc: dc = u', '.join(sorted(dc, key=sort_key)) ans.append(('device_collections', row % (_('Collections')+':', dc))) def classname(field): try: dt = mi.metadata_for_field(field)['datatype'] except: dt = 'text' return 'datatype_%s'%dt ans = [u'<tr id="%s" class="%s">%s</tr>'%(fieldl.replace('#', '_'), classname(fieldl), html) for fieldl, html in ans] # print '\n'.join(ans) direction = 'rtl' if rtl else 'ltr' margin = 'left' if rtl else 'right' return u'<table class="fields" style="direction: %s; margin-%s:auto">%s</table>'%(direction, margin, u'\n'.join(ans)), comment_fields
def search_href(search_term, value): search = '%s:"=%s"' % (search_term, value.replace('"', '\\"')) return prepare_string_for_xml('search:' + hexlify(search.encode('utf-8')), True)
def create_book(mi, path, fmt='epub', opf_name='metadata.opf', html_name='start.xhtml', toc_name='toc.ncx'): ''' Create an empty book in the specified format at the specified location. ''' if fmt not in valid_empty_formats: raise ValueError('Cannot create empty book in the %s format' % fmt) if fmt == 'txt': with open(path, 'wb') as f: if not mi.is_null('title'): f.write(as_bytes(mi.title)) return if fmt == 'docx': from calibre.ebooks.conversion.plumber import Plumber from calibre.ebooks.docx.writer.container import DOCX from calibre.utils.logging import default_log p = Plumber('a.docx', 'b.docx', default_log) p.setup_options() # Use the word default of one inch page margins for x in 'left right top bottom'.split(): setattr(p.opts, 'margin_' + x, 72) DOCX(p.opts, default_log).write(path, mi, create_empty_document=True) return path = os.path.abspath(path) lang = 'und' opf = metadata_to_opf(mi, as_string=False) for l in opf.xpath('//*[local-name()="language"]'): if l.text: lang = l.text break lang = lang_as_iso639_1(lang) or lang opfns = OPF_NAMESPACES['opf'] m = opf.makeelement('{%s}manifest' % opfns) opf.insert(1, m) i = m.makeelement('{%s}item' % opfns, href=html_name, id='start') i.set('media-type', guess_type('a.xhtml')) m.append(i) i = m.makeelement('{%s}item' % opfns, href=toc_name, id='ncx') i.set('media-type', guess_type(toc_name)) m.append(i) s = opf.makeelement('{%s}spine' % opfns, toc="ncx") opf.insert(2, s) i = s.makeelement('{%s}itemref' % opfns, idref='start') s.append(i) CONTAINER = '''\ <?xml version="1.0"?> <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"> <rootfiles> <rootfile full-path="{0}" media-type="application/oebps-package+xml"/> </rootfiles> </container> '''.format(prepare_string_for_xml(opf_name, True)).encode('utf-8') HTML = P('templates/new_book.html', data=True).decode('utf-8').replace( '_LANGUAGE_', prepare_string_for_xml(lang, True)).replace( '_TITLE_', prepare_string_for_xml(mi.title)).replace( '_AUTHORS_', prepare_string_for_xml(authors_to_string( mi.authors))).encode('utf-8') h = parse(HTML) pretty_html_tree(None, h) HTML = serialize(h, 'text/html') ncx = etree.tostring(create_toc(mi, opf, html_name, lang), encoding='utf-8', xml_declaration=True, pretty_print=True) pretty_xml_tree(opf) opf = etree.tostring(opf, encoding='utf-8', xml_declaration=True, pretty_print=True) if fmt == 'azw3': with TemporaryDirectory('create-azw3') as tdir, CurrentDir(tdir): for name, data in ((opf_name, opf), (html_name, HTML), (toc_name, ncx)): with open(name, 'wb') as f: f.write(data) c = Container(os.path.dirname(os.path.abspath(opf_name)), opf_name, DevNull()) opf_to_azw3(opf_name, path, c) else: with ZipFile(path, 'w', compression=ZIP_STORED) as zf: zf.writestr('mimetype', b'application/epub+zip', compression=ZIP_STORED) zf.writestr('META-INF/', b'', 0o755) zf.writestr('META-INF/container.xml', CONTAINER) zf.writestr(opf_name, opf) zf.writestr(html_name, HTML) zf.writestr(toc_name, ncx)
def __enter__(self, processed=False, only_input_plugin=False, run_char_count=True, read_anchor_map=True, extract_embedded_fonts_for_qt=False): ''' Convert an ebook file into an exploded OEB book suitable for display in viewers/preprocessing etc. ''' from calibre.ebooks.conversion.plumber import Plumber, create_oebbook self.delete_on_exit = [] self._tdir = TemporaryDirectory('_ebook_iter') self.base = self._tdir.__enter__() plumber = Plumber(self.pathtoebook, self.base, self.log) plumber.setup_options() if self.pathtoebook.lower().endswith('.opf'): plumber.opts.dont_package = True if hasattr(plumber.opts, 'no_process'): plumber.opts.no_process = True plumber.input_plugin.for_viewer = True with plumber.input_plugin, open(plumber.input, 'rb') as inf: self.pathtoopf = plumber.input_plugin(inf, plumber.opts, plumber.input_fmt, self.log, {}, self.base) if not only_input_plugin: # Run the HTML preprocess/parsing from the conversion pipeline as # well if (processed or plumber.input_fmt.lower() in {'pdb', 'pdf', 'rb'} and not hasattr(self.pathtoopf, 'manifest')): if hasattr(self.pathtoopf, 'manifest'): self.pathtoopf = write_oebbook(self.pathtoopf, self.base) self.pathtoopf = create_oebbook(self.log, self.pathtoopf, plumber.opts) if hasattr(self.pathtoopf, 'manifest'): self.pathtoopf = write_oebbook(self.pathtoopf, self.base) self.book_format = os.path.splitext(self.pathtoebook)[1][1:].upper() if getattr(plumber.input_plugin, 'is_kf8', False): self.book_format = 'KF8' self.opf = getattr(plumber.input_plugin, 'optimize_opf_parsing', None) if self.opf is None: self.opf = OPF(self.pathtoopf, os.path.dirname(self.pathtoopf)) self.language = self.opf.language if self.language: self.language = self.language.lower() ordered = [i for i in self.opf.spine if i.is_linear] + \ [i for i in self.opf.spine if not i.is_linear] self.spine = [] Spiny = partial(SpineItem, read_anchor_map=read_anchor_map, run_char_count=run_char_count) is_comic = plumber.input_fmt.lower() in {'cbc', 'cbz', 'cbr', 'cb7'} for i in ordered: spath = i.path mt = None if i.idref is not None: mt = self.opf.manifest.type_for_id(i.idref) if mt is None: mt = guess_type(spath)[0] try: self.spine.append(Spiny(spath, mime_type=mt)) if is_comic: self.spine[-1].is_single_page = True except: self.log.warn('Missing spine item:', repr(spath)) cover = self.opf.cover if cover and self.ebook_ext in { 'lit', 'mobi', 'prc', 'opf', 'fb2', 'azw', 'azw3' }: cfile = os.path.join(self.base, 'calibre_iterator_cover.html') rcpath = os.path.relpath(cover, self.base).replace(os.sep, '/') chtml = (TITLEPAGE % prepare_string_for_xml(rcpath, True)).encode('utf-8') with open(cfile, 'wb') as f: f.write(chtml) self.spine[0:0] = [Spiny(cfile, mime_type='application/xhtml+xml')] self.delete_on_exit.append(cfile) if self.opf.path_to_html_toc is not None and \ self.opf.path_to_html_toc not in self.spine: try: self.spine.append(Spiny(self.opf.path_to_html_toc)) except: import traceback traceback.print_exc() sizes = [i.character_count for i in self.spine] self.pages = [ math.ceil(i / float(self.CHARACTERS_PER_PAGE)) for i in sizes ] for p, s in zip(self.pages, self.spine): s.pages = p start = 1 for s in self.spine: s.start_page = start start += s.pages s.max_page = s.start_page + s.pages - 1 self.toc = self.opf.toc if read_anchor_map: create_indexing_data(self.spine, self.toc) self.read_bookmarks() if extract_embedded_fonts_for_qt: from calibre.ebooks.oeb.iterator.extract_fonts import extract_fonts try: extract_fonts(self.opf, self.log) except: ol = self.log.filter_level self.log.filter_level = self.log.DEBUG self.log.exception('Failed to extract fonts') self.log.filter_level = ol return self
def fb2_header(self): from calibre.ebooks.oeb.base import OPF metadata = {} metadata['title'] = self.oeb_book.metadata.title[0].value metadata['appname'] = __appname__ metadata['version'] = __version__ metadata['date'] = '%i.%i.%i' % ( datetime.now().day, datetime.now().month, datetime.now().year) if self.oeb_book.metadata.language: lc = lang_as_iso639_1(self.oeb_book.metadata.language[0].value) if not lc: lc = self.oeb_book.metadata.language[0].value metadata['lang'] = lc or 'en' else: metadata['lang'] = u'en' metadata['id'] = None metadata['cover'] = self.get_cover() metadata['genre'] = self.opts.fb2_genre metadata['author'] = '' for auth in self.oeb_book.metadata.creator: author_first = '' author_middle = '' author_last = '' author_parts = auth.value.split(' ') if len(author_parts) == 1: author_last = author_parts[0] elif len(author_parts) == 2: author_first = author_parts[0] author_last = author_parts[1] else: author_first = author_parts[0] author_middle = ' '.join(author_parts[1:-1]) author_last = author_parts[-1] metadata['author'] += '<author>' metadata[ 'author'] += '<first-name>%s</first-name>' % prepare_string_for_xml( author_first) if author_middle: metadata[ 'author'] += '<middle-name>%s</middle-name>' % prepare_string_for_xml( author_middle) metadata[ 'author'] += '<last-name>%s</last-name>' % prepare_string_for_xml( author_last) metadata['author'] += '</author>' if not metadata['author']: metadata[ 'author'] = '<author><first-name></first-name><last-name></last-name></author>' metadata['keywords'] = '' tags = list(map(unicode_type, self.oeb_book.metadata.subject)) if tags: tags = ', '.join(prepare_string_for_xml(x) for x in tags) metadata['keywords'] = '<keywords>%s</keywords>' % tags metadata['sequence'] = '' if self.oeb_book.metadata.series: index = '1' if self.oeb_book.metadata.series_index: index = self.oeb_book.metadata.series_index[0] metadata['sequence'] = '<sequence name="%s" number="%s"/>' % ( prepare_string_for_xml( '%s' % self.oeb_book.metadata.series[0]), index) year = publisher = isbn = '' identifiers = self.oeb_book.metadata['identifier'] for x in identifiers: if x.get(OPF('scheme'), None).lower() == 'uuid' or unicode_type( x).startswith('urn:uuid:'): metadata['id'] = unicode_type(x).split(':')[-1] break if metadata['id'] is None: self.log.warn('No UUID identifier found') metadata['id'] = unicode_type(uuid.uuid4()) try: date = self.oeb_book.metadata['date'][0] except IndexError: pass else: year = '<year>%s</year>' % prepare_string_for_xml( date.value.partition('-')[0]) try: publisher = self.oeb_book.metadata['publisher'][0] except IndexError: pass else: publisher = '<publisher>%s</publisher>' % prepare_string_for_xml( publisher.value) for x in identifiers: if x.get(OPF('scheme'), None).lower() == 'isbn': isbn = '<isbn>%s</isbn>' % prepare_string_for_xml(x.value) metadata['year'], metadata['isbn'], metadata[ 'publisher'] = year, isbn, publisher for key, value in metadata.items(): if key not in ('author', 'cover', 'sequence', 'keywords', 'year', 'publisher', 'isbn'): metadata[key] = prepare_string_for_xml(value) try: comments = self.oeb_book.metadata['description'][0] except Exception: metadata['comments'] = '' else: from calibre.utils.html2text import html2text metadata['comments'] = '<annotation><p>{}</p></annotation>'.format( prepare_string_for_xml(html2text(comments.value).strip())) # Keep the indentation level of the description the same as the body. header = textwrap.dedent('''\ <FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink"> <description> <title-info> <genre>%(genre)s</genre> %(author)s <book-title>%(title)s</book-title> %(cover)s <lang>%(lang)s</lang> %(keywords)s %(sequence)s %(comments)s </title-info> <document-info> %(author)s <program-used>%(appname)s %(version)s</program-used> <date>%(date)s</date> <id>%(id)s</id> <version>1.0</version> </document-info> <publish-info> %(publisher)s %(year)s %(isbn)s </publish-info> </description>''') % metadata # Remove empty lines. return '\n'.join(filter(unicode_type.strip, header.splitlines()))
def index(ctx, rd): return rd.generate_static_output('/', partial( get_html, 'content-server/index.html', getattr(rd.opts, 'auto_reload_port', 0), ENTRY_POINT='book list', LOADING_MSG=prepare_string_for_xml(_('Loading library, please wait'))))
def dump_text(self, elem_tree, stylizer, page, tag_stack=[]): ''' This function is intended to be used in a recursive manner. dump_text will run though all elements in the elem_tree and call itself on each element. self.image_hrefs will be populated by calling this function. @param elem_tree: etree representation of XHTML content to be transformed. @param stylizer: Used to track the style of elements within the tree. @param page: OEB page used to determine absolute urls. @param tag_stack: List of open FB2 tags to take into account. @return: List of string representing the XHTML converted to FB2 markup. ''' from calibre.ebooks.oeb.base import XHTML_NS, barename, namespace elem = elem_tree # Ensure what we are converting is not a string and that the fist tag is part of the XHTML namespace. if not isinstance(elem_tree.tag, string_or_bytes) or namespace( elem_tree.tag) != XHTML_NS: p = elem.getparent() if p is not None and isinstance(p.tag, string_or_bytes) and namespace(p.tag) == XHTML_NS \ and elem.tail: return [elem.tail] return [] style = stylizer.style(elem_tree) if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \ or style['visibility'] == 'hidden': if hasattr(elem, 'tail') and elem.tail: return [elem.tail] return [] # FB2 generated output. fb2_out = [] # FB2 tags in the order they are opened. This will be used to close the tags. tags = [] # First tag in tree tag = barename(elem_tree.tag) # Number of blank lines above tag try: ems = int(round((float(style.marginTop) / style.fontSize) - 1)) if ems < 0: ems = 0 except: ems = 0 # Convert TOC entries to <title>s and add <section>s if self.opts.sectionize == 'toc': # A section cannot be a child of any other element than another section, # so leave the tag alone if there are parents if not tag_stack: # There are two reasons to start a new section here: the TOC pointed to # this page (then we use the first non-<body> on the page as a <title>), or # the TOC pointed to a specific element newlevel = 0 toc_entry = self.toc.get(page.href, None) if toc_entry is not None: if None in toc_entry: if tag != 'body' and hasattr( elem_tree, 'text') and elem_tree.text: newlevel = 1 self.toc[page.href] = None if not newlevel and elem_tree.attrib.get('id', None) is not None: newlevel = toc_entry.get( elem_tree.attrib.get('id', None), None) # Start a new section if necessary if newlevel: while newlevel <= self.section_level: fb2_out.append('</section>') self.section_level -= 1 fb2_out.append('<section>') self.section_level += 1 fb2_out.append('<title>') tags.append('title') if self.section_level == 0: # If none of the prior processing made a section, make one now to be FB2 spec compliant fb2_out.append('<section>') self.section_level += 1 # Process the XHTML tag and styles. Converted to an FB2 tag. # Use individual if statement not if else. There can be # only one XHTML tag but it can have multiple styles. if tag == 'img' and elem_tree.attrib.get('src', None): # Only write the image tag if it is in the manifest. ihref = urlnormalize(page.abshref(elem_tree.attrib['src'])) if ihref in self.oeb_book.manifest.hrefs: if ihref not in self.image_hrefs: self.image_hrefs[ihref] = 'img_%s' % len(self.image_hrefs) p_txt, p_tag = self.ensure_p() fb2_out += p_txt tags += p_tag fb2_out.append('<image l:href="#%s"/>' % self.image_hrefs[ihref]) else: self.log.warn(u'Ignoring image not in manifest: %s' % ihref) if tag in ('br', 'hr') or ems >= 1: if ems < 1: multiplier = 1 else: multiplier = ems if self.in_p: closed_tags = [] open_tags = tag_stack + tags open_tags.reverse() for t in open_tags: fb2_out.append('</%s>' % t) closed_tags.append(t) if t == 'p': break fb2_out.append('<empty-line/>' * multiplier) closed_tags.reverse() for t in closed_tags: fb2_out.append('<%s>' % t) else: fb2_out.append('<empty-line/>' * multiplier) if tag in ('div', 'li', 'p'): p_text, added_p = self.close_open_p(tag_stack + tags) fb2_out += p_text if added_p: tags.append('p') if tag == 'a' and elem_tree.attrib.get('href', None): # Handle only external links for now if urlparse(elem_tree.attrib['href']).netloc: p_txt, p_tag = self.ensure_p() fb2_out += p_txt tags += p_tag fb2_out.append('<a l:href="%s">' % urlnormalize(elem_tree.attrib['href'])) tags.append('a') if tag == 'b' or style['font-weight'] in ('bold', 'bolder'): s_out, s_tags = self.handle_simple_tag('strong', tag_stack + tags) fb2_out += s_out tags += s_tags if tag == 'i' or style['font-style'] == 'italic': s_out, s_tags = self.handle_simple_tag('emphasis', tag_stack + tags) fb2_out += s_out tags += s_tags if tag in ('del', 'strike') or style['text-decoration'] == 'line-through': s_out, s_tags = self.handle_simple_tag('strikethrough', tag_stack + tags) fb2_out += s_out tags += s_tags if tag == 'sub': s_out, s_tags = self.handle_simple_tag('sub', tag_stack + tags) fb2_out += s_out tags += s_tags if tag == 'sup': s_out, s_tags = self.handle_simple_tag('sup', tag_stack + tags) fb2_out += s_out tags += s_tags # Process element text. if hasattr(elem_tree, 'text') and elem_tree.text: if not self.in_p: fb2_out.append('<p>') fb2_out.append(prepare_string_for_xml(elem_tree.text)) if not self.in_p: fb2_out.append('</p>') # Process sub-elements. for item in elem_tree: fb2_out += self.dump_text(item, stylizer, page, tag_stack + tags) # Close open FB2 tags. tags.reverse() fb2_out += self.close_tags(tags) # Process element text that comes after the close of the XHTML tag but before the next XHTML tag. if hasattr(elem_tree, 'tail') and elem_tree.tail: if not self.in_p: fb2_out.append('<p>') fb2_out.append(prepare_string_for_xml(elem_tree.tail)) if not self.in_p: fb2_out.append('</p>') return fb2_out