Package: release.debian.org Severity: normal Tags: trixie X-Debbugs-Cc: [email protected], [email protected] Control: affects -1 + src:calibre User: [email protected] Usertags: pu
[ Reason ] Fix these CVEs. * CVE-2026-25635 * CVE-2026-25636 * CVE-2026-25731 [ Impact ] * CVE-2026-25635: path traversal vulnerability * CVE-2026-25636: path traversal vulnerability * CVE-2026-25731: Server-Side Template Injection (SSTI) vulnerability [ Tests ] Automated build-time test was successful. [ Risks ] Not well tested on real machine. [ Checklist ] [x] *all* changes are documented in the d/changelog [x] I reviewed all changes and I approve them [x] attach debdiff against the package in (old)stable [x] the issue is verified as fixed in unstable [ Changes ] Backport and apply upstream fixes. [ Other info ] Fixes are examine from online: https://github.com/debian- calibre/calibre/compare/debian/8.5.0+ds-1+deb13u1...debian/trixie
diff -Nru calibre-8.5.0+ds/debian/calibre.install calibre-8.5.0+ds/debian/calibre.install --- calibre-8.5.0+ds/debian/calibre.install 2025-09-22 00:51:57.000000000 +0900 +++ calibre-8.5.0+ds/debian/calibre.install 2026-02-08 23:10:50.000000000 +0900 @@ -7,7 +7,6 @@ # usr/lib/calibre/odf usr/lib/calibre/qt -usr/lib/calibre/templite usr/lib/calibre/tinycss usr/lib/calibre/css_selectors usr/lib/calibre/polyglot diff -Nru calibre-8.5.0+ds/debian/changelog calibre-8.5.0+ds/debian/changelog --- calibre-8.5.0+ds/debian/changelog 2025-11-09 16:06:24.000000000 +0900 +++ calibre-8.5.0+ds/debian/changelog 2026-02-08 23:19:22.000000000 +0900 @@ -1,3 +1,12 @@ +calibre (8.5.0+ds-1+deb13u2) trixie; urgency=medium + + * CVE-2026-25635: CHM Input: Ignore internal files that have paths that end up outside the container + * CVE-2026-25636: DRYer + * CVE-2026-25731: ZIP Output: Change the template engine used for HTML templating from templite to Mustache, for greater safety and performance. Note that this is a breaking change if you use custom templates with ZIP output. + * Use pystache instead of templite to fix CVE-2026-25731 + + -- YOKOTA Hiroshi <[email protected]> Sun, 08 Feb 2026 23:19:22 +0900 + calibre (8.5.0+ds-1+deb13u1) trixie; urgency=medium * Fix CVE-2025-64486 diff -Nru calibre-8.5.0+ds/debian/control calibre-8.5.0+ds/debian/control --- calibre-8.5.0+ds/debian/control 2025-11-09 16:06:24.000000000 +0900 +++ calibre-8.5.0+ds/debian/control 2026-02-08 23:06:45.000000000 +0900 @@ -72,6 +72,7 @@ python3-pyqt6.qttexttospeech, python3-pyqt6.qtwebengine, python3-pyqtbuild, + python3-pystache, python3-pyzstd, python3-regex, python3-requests-toolbelt, diff -Nru calibre-8.5.0+ds/debian/patches/series calibre-8.5.0+ds/debian/patches/series --- calibre-8.5.0+ds/debian/patches/series 2025-11-09 16:06:24.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/series 2026-02-08 23:06:17.000000000 +0900 @@ -80,3 +80,6 @@ pykakasi/0080-Revert-Fix-a-regression-that-caused-incorrect-Englis.patch 0081-Revert-Update-7zip-wrapper-code-for-removal-of-read-.patch upstream/0082-Fix-CVE-2025-64486.patch +upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch +upstream/0084-CVE-2026-25636-DRYer.patch +upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch calibre-8.5.0+ds/debian/patches/upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch --- calibre-8.5.0+ds/debian/patches/upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0083-CVE-2026-25635-CHM-Input-Ignore-internal-files-that-.patch 2026-02-08 23:06:17.000000000 +0900 @@ -0,0 +1,81 @@ +From: Kovid Goyal <[email protected]> +Date: Wed, 4 Feb 2026 09:39:54 +0530 +Subject: CVE-2026-25635: CHM Input: Ignore internal files that have paths + that end up outside the container + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-32vh-whvh-9fxr +Origin: backport, https://github.com/kovidgoyal/calibre/commit/9739232fcb029ac15dfe52ccd4fdb4a07ebb6ce9 + +Also, allow extraction of long filenames + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/chm/reader.py | 33 ++++++++++++++++----------------- + 1 file changed, 16 insertions(+), 17 deletions(-) + +diff --git a/src/calibre/ebooks/chm/reader.py b/src/calibre/ebooks/chm/reader.py +index d456171..397ef02 100644 +--- a/src/calibre/ebooks/chm/reader.py ++++ b/src/calibre/ebooks/chm/reader.py +@@ -15,6 +15,7 @@ from calibre.constants import filesystem_encoding, iswindows + from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString + from calibre.ebooks.chardet import xml_to_unicode + from calibre.ebooks.metadata.toc import TOC ++from calibre.utils.filenames import make_long_path_useable + from polyglot.builtins import as_unicode + + +@@ -181,37 +182,35 @@ class CHMReader(CHMFile): + + def ExtractFiles(self, output_dir=os.getcwd(), debug_dump=False): + html_files = set() ++ base = output_dir = os.path.abspath(output_dir) ++ if not base.endswith(os.sep): ++ base += os.sep + for path in self.Contents(): +- fpath = path +- lpath = os.path.join(output_dir, fpath) ++ fpath = path.partition(';')[0] # fix file names with ";<junk>" at the end, see _reformat() ++ fpath = fpath.replace('/', os.sep) ++ lpath = os.path.abspath(os.path.join(output_dir, fpath)) ++ if os.path.commonprefix((lpath, base)) != base: ++ self.log.warn(f'{path!r} outside container, skipping') ++ continue + self._ensure_dir(lpath) + try: + data = self.GetFile(path) + except: + self.log.exception(f'Failed to extract {path} from CHM, ignoring') + continue +- if lpath.find(';') != -1: +- # fix file names with ";<junk>" at the end, see _reformat() +- lpath = lpath.split(';')[0] ++ with open(make_long_path_useable(lpath), 'wb') as f: ++ f.write(data) + try: +- with open(lpath, 'wb') as f: +- f.write(data) +- try: +- if 'html' in guess_mimetype(path)[0]: +- html_files.add(lpath) +- except: +- pass ++ if 'html' in guess_mimetype(os.path.basename(lpath))[0]: ++ html_files.add(lpath) + except: +- if iswindows and len(lpath) > 250: +- self.log.warn(f'{path!r} filename too long, skipping') +- continue +- raise ++ pass + + if debug_dump: + import shutil + shutil.copytree(output_dir, os.path.join(debug_dump, 'debug_dump')) + for lpath in html_files: +- with open(lpath, 'r+b') as f: ++ with open(make_long_path_useable(lpath), 'r+b') as f: + data = f.read() + data = self._reformat(data, lpath) + if isinstance(data, str): diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch --- calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0084-CVE-2026-25636-DRYer.patch 2026-02-08 23:06:17.000000000 +0900 @@ -0,0 +1,37 @@ +From: Kovid Goyal <[email protected]> +Date: Mon, 2 Feb 2026 11:25:09 +0530 +Subject: CVE-2026-25636: DRYer + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-8r26-m7j5-hm29 +Origin: backport, https://github.com/kovidgoyal/calibre/commit/9484ea82c6ab226c18e6ca5aa000fa16de598726 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + src/calibre/ebooks/conversion/plugins/epub_input.py | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py +index 2505169..edfebee 100644 +--- a/src/calibre/ebooks/conversion/plugins/epub_input.py ++++ b/src/calibre/ebooks/conversion/plugins/epub_input.py +@@ -66,15 +66,17 @@ class EPUBInput(InputFormatPlugin): + + try: + root = etree.parse(encfile) ++ base = os.path.dirname(encfile) ++ container_base = os.path.dirname(base) + for em in root.xpath('descendant::*[contains(name(), "EncryptionMethod")]'): + algorithm = em.get('Algorithm', '') + if algorithm not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}: + return False + cr = em.getparent().xpath('descendant::*[contains(name(), "CipherReference")]')[0] + uri = cr.get('URI') +- path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/'))) ++ path = os.path.abspath(os.path.join(base, '..', *uri.split('/'))) + tkey = (key if algorithm == ADOBE_OBFUSCATION else idpf_key) +- if (tkey and os.path.exists(path)): ++ if (tkey and is_existing_subpath(path, container_base)): + self._encrypted_font_uris.append(uri) + decrypt_font(tkey, path, algorithm) + return True diff -Nru calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch --- calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch 1970-01-01 09:00:00.000000000 +0900 +++ calibre-8.5.0+ds/debian/patches/upstream/0085-CVE-2026-25731-ZIP-Output-Change-the-template-engine.patch 2026-02-08 23:06:17.000000000 +0900 @@ -0,0 +1,588 @@ +From: Kovid Goyal <[email protected]> +Date: Thu, 5 Feb 2026 14:21:25 +0530 +Subject: CVE-2026-25731: ZIP Output: Change the template engine used for HTML + templating from templite to Mustache, + for greater safety and performance. Note that this is a breaking change if + you use custom templates with ZIP output. + +Forwarded: not-needed +Bug: https://github.com/kovidgoyal/calibre/security/advisories/GHSA-xrh9-w7qx-3gcc +Origin: backport, https://github.com/kovidgoyal/calibre/commit/f0649b27512e987b95fcab2e1e0a3bcdafc23379 + +Signed-off-by: YOKOTA Hiroshi <[email protected]> +--- + COPYRIGHT | 6 -- + pyproject.toml | 5 +- + resources/templates/html_export_default.mustache | 70 ++++++++++++++++ + resources/templates/html_export_default.tmpl | 74 ----------------- + .../templates/html_export_default_index.mustache | 55 +++++++++++++ + resources/templates/html_export_default_index.tmpl | 61 -------------- + .../ebooks/conversion/plugins/html_output.py | 61 ++++++++------ + src/templite/__init__.py | 96 ---------------------- + 8 files changed, 164 insertions(+), 264 deletions(-) + create mode 100644 resources/templates/html_export_default.mustache + delete mode 100644 resources/templates/html_export_default.tmpl + create mode 100644 resources/templates/html_export_default_index.mustache + delete mode 100644 resources/templates/html_export_default_index.tmpl + delete mode 100644 src/templite/__init__.py + +diff --git a/COPYRIGHT b/COPYRIGHT +index a44d756..09e1308 100644 +--- a/COPYRIGHT ++++ b/COPYRIGHT +@@ -12,12 +12,6 @@ Files: resources/rapydscript/* + Copyright: Various + License: BSD + +-Files: src/templite/* +-Copyright: Copyright (c) 2009 joonis new media, Thimo Kraemer +-License: GPL-2+ +- The full text of the GPL is distributed as in +- /usr/share/common-licenses/GPL-2 on Debian systems. +- + Files: src/calibre/devices/bambook/* + Copyright: 2010, Li Fanxi + License: GPL-3 +diff --git a/pyproject.toml b/pyproject.toml +index b3697bd..f7ef5cb 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -26,7 +26,6 @@ exclude = [ + "setup/linux-installer.py", + "src/css_selectors/*", + "src/polyglot/*", +- "src/templite/*", + "src/tinycss/*", + ] + preview = true +@@ -71,7 +70,7 @@ unfixable = ['PIE794', 'ISC001'] + detect-same-package = true + extra-standard-library = ["aes", "elementmaker", "encodings"] + known-first-party = ["calibre_extensions", "calibre_plugins", "polyglot"] +-known-third-party = ["odf", "qt", "templite", "tinycss", "css_selectors"] ++known-third-party = ["odf", "qt", "tinycss", "css_selectors"] + relative-imports-order = "closest-to-furthest" + split-on-trailing-comma = false + section-order = ['__python__', "future", "standard-library", "third-party", "first-party", "local-folder"] +@@ -189,7 +188,6 @@ skip = [ + "./setup/linux-installer.py", + "./src/css_selectors/*", + "./src/polyglot/*", +- "./src/templite/*", + "./src/tinycss/*", + "./src/unicode_names/*", + ] +@@ -205,7 +203,6 @@ exclude = [ + "src/calibre/gui2/store/stores/", + "src/css_selectors/", + "src/polyglot/", +- "src/templite/", + "src/tinycss/", + ] + +diff --git a/resources/templates/html_export_default.mustache b/resources/templates/html_export_default.mustache +new file mode 100644 +index 0000000..1c8691a +--- /dev/null ++++ b/resources/templates/html_export_default.mustache +@@ -0,0 +1,70 @@ ++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> ++<html xmlns="http://www.w3.org/1999/xhtml"> ++<head> ++{{{head_content}}} ++ ++<link href="{{css_link}}" type="text/css" rel="stylesheet" /> ++ ++</head> ++<body> ++ ++<div class="calibreMeta"> ++ <div class="calibreMetaTitle"> ++ {{#meta.titles}} ++ {{#is_first}} ++ <h1><a href="{{toc_url}}">{{title}}</a> </h1> ++ {{/is_first}} ++ {{^is_first}} ++ <div class="calibreMetaSubtitle">{{title}}</div> ++ {{/is_first}} ++ {{/meta.titles}} ++ </div> ++ <div class="calibreMetaAuthor">{{meta.creators}}</div> ++</div> ++ ++<div class="calibreMain"> ++ ++ <div class="calibreEbookContent"> ++ {{#has_link}} ++ <div class="calibreEbNavTop"> ++ {{#prev_link}} ++ <a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a> ++ {{/prev_link}} ++ {{^prev_link}} ++ <a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a> ++ {{/prev_link}} ++ {{#next_link}} ++ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> ++ {{/next_link}} ++ </div> ++ {{/has_link}} ++ ++ {{{ebook_content}}} ++ </div> ++ ++ {{#has_toc}} ++ <div class="calibreToc"> ++ <h2><a href="{{toc_url}}">{{table_of_contents}}</a></h2> ++ {{{toc}}} ++ </div> ++ {{/has_toc}} ++ ++ <div class="calibreEbNav"> ++ {{#prev_link}} ++ <a href="{{prev_link}}" class="calibreAPrev">{{prev_page}}</a> ++ {{/prev_link}} ++ {{^prev_link}} ++ <a href="{{toc_url}}" class="calibreAPrev">{{prev_page}}</a> ++ {{/prev_link}} ++ ++ <a href="{{toc_url}}" class="calibreAHome">{{start}}</a> ++ ++ {{#next_link}} ++ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> ++ {{/next_link}} ++ </div> ++ ++</div> ++ ++</body> ++</html> +diff --git a/resources/templates/html_export_default.tmpl b/resources/templates/html_export_default.tmpl +deleted file mode 100644 +index 7aac247..0000000 +--- a/resources/templates/html_export_default.tmpl ++++ /dev/null +@@ -1,74 +0,0 @@ +-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +-<html xmlns="http://www.w3.org/1999/xhtml"> +-<head> +-${head_content}$ +- +-<link href="${cssLink}$" type="text/css" rel="stylesheet" /> +- +-</head> +-<body> +- +-<div class="calibreMeta"> +- <div class="calibreMetaTitle"> +- ${pos1=1}$ +- ${for title in meta.titles():}$ +- ${if pos1:}$ +- <h1> +- <a href="${tocUrl}$">${print(title)}$</a> +- </h1> +- ${:else:}$ +- <div class="calibreMetaSubtitle">${print(title)}$</div> +- ${:endif}$ +- ${pos1=0}$ +- ${:endfor}$ +- </div> +- <div class="calibreMetaAuthor"> +- ${print(', '.join(meta.creators()))}$ +- </div> +-</div> +- +-<div class="calibreMain"> +- +- <div class="calibreEbookContent"> +- ${if prevLink or nextLink:}$ +- <div class="calibreEbNavTop"> +- ${if prevLink:}$ +- <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a> +- ${:else:}$ +- <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a> +- ${:endif}$ +- +- ${if nextLink:}$ +- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> +- ${:endif}$ +- </div> +- ${:endif}$ +- +- ${ebookContent}$ +- </div> +- +- ${if has_toc:}$ +- <div class="calibreToc"> +- <h2><a href="${tocUrl}$">${print( _('Table of contents'))}$</a></h2> +- ${print(toc())}$ +- </div> +- ${:endif}$ +- +- <div class="calibreEbNav"> +- ${if prevLink:}$ +- <a href="${prevLink}$" class="calibreAPrev">${print(_('previous page'))}$</a> +- ${:else:}$ +- <a href="${tocUrl}$" class="calibreAPrev">${print(_('previous page'))}$</a> +- ${:endif}$ +- +- <a href="${tocUrl}$" class="calibreAHome">${print(_('start'))}$</a> +- +- ${if nextLink:}$ +- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> +- ${:endif}$ +- </div> +- +-</div> +- +-</body> +-</html> +diff --git a/resources/templates/html_export_default_index.mustache b/resources/templates/html_export_default_index.mustache +new file mode 100644 +index 0000000..aa1bc4d +--- /dev/null ++++ b/resources/templates/html_export_default_index.mustache +@@ -0,0 +1,55 @@ ++<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> ++<html xmlns="http://www.w3.org/1999/xhtml"> ++<head> ++<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> ++ ++<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> ++<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" /> ++ ++<title>{{meta.creators}} - {{meta.first_title}}</title> ++ ++{{#meta.items}} ++ <meta name="DC.{{name}}" content="{{value}}" /> ++{{/meta.items}} ++ ++<link href="{{css_link}}" type="text/css" rel="stylesheet" /> ++</head> ++<body> ++ ++<div class="calibreMeta"> ++ <div class="calibreMetaTitle"> ++ {{#meta.titles}} ++ {{#is_first}} ++ <h1><a href="{{toc_url}}">{{title}}</a> </h1> ++ {{/is_first}} ++ {{^is_first}} ++ <div class="calibreMetaSubtitle">{{title}}</div> ++ {{/is_first}} ++ {{/meta.titles}} ++ </div> ++ <div class="calibreMetaAuthor">{{meta.creators}}</div> ++</div> ++ ++<div class="calibreMain"> ++ <div class="calibreEbookContent"> ++ {{#has_toc}} ++ <div class="calibreTocIndex"> ++ <h2>{{table_of_contents}}</h2> ++ {{{toc}}} ++ </div> ++ {{/has_toc}} ++ {{^has_toc}} ++ <h2>{{no_toc}}</h2> ++ <div><strong><a href="{{next_link}}">{{begin_to_read}}</a></strong></div> ++ {{/has_toc}} ++ </div> ++ ++ <div class="calibreEbNav"> ++ {{#next_link}} ++ <a href="{{next_link}}" class="calibreANext">{{next_page}}</a> ++ {{/next_link}} ++ </div> ++</div> ++ ++</body> ++</html> +diff --git a/resources/templates/html_export_default_index.tmpl b/resources/templates/html_export_default_index.tmpl +deleted file mode 100644 +index f0665ad..0000000 +--- a/resources/templates/html_export_default_index.tmpl ++++ /dev/null +@@ -1,61 +0,0 @@ +-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +-<html xmlns="http://www.w3.org/1999/xhtml"> +-<head> +-<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +- +-<link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" /> +-<link rel="schema.DCTERMS" href="http://purl.org/dc/terms/" /> +- +-<title>${print(', '.join(meta.creators()))}$ - ${print(next(meta.titles())); print(meta.titles().close())}$</title> +- +-${for item in meta:}$ +- <meta ${print('name="DC.'+item['name']+'"')}$ ${print('content="'+item['value']+'"')}$ /> +-${:endfor}$ +- +-<link href="${cssLink}$" type="text/css" rel="stylesheet" /> +-</head> +-<body> +- +-<div class="calibreMeta"> +- <div class="calibreMetaTitle"> +- ${pos1=1}$ +- ${for title in meta.titles():}$ +- ${if pos1:}$ +- <h1> +- <a href="${tocUrl}$">${print(title)}$</a> +- </h1> +- ${:else:}$ +- <div class="calibreMetaSubtitle">${print(title)}$</div> +- ${:endif}$ +- ${pos1=0}$ +- ${:endfor}$ +- </div> +- <div class="calibreMetaAuthor"> +- ${print(', '.join(meta.creators()))}$ +- </div> +-</div> +- +-<div class="calibreMain"> +- <div class="calibreEbookContent"> +- +- ${if has_toc:}$ +- <div class="calibreTocIndex"> +- <h2>${print(_('Table of contents'))}$</h2> +- ${toc}$ +- </div> +- ${:else:}$ +- <h2>${print(_('No table of contents present'))}$</h2> +- <div><strong><a href="${nextLink}$">${print(_('begin to read'))}$</a></strong></div> +- ${:endif}$ +- +- </div> +- +- <div class="calibreEbNav"> +- ${if nextLink:}$ +- <a href="${nextLink}$" class="calibreANext">${print(_('next page'))}$</a> +- ${:endif}$ +- </div> +-</div> +- +-</body> +-</html> +diff --git a/src/calibre/ebooks/conversion/plugins/html_output.py b/src/calibre/ebooks/conversion/plugins/html_output.py +index ea64c70..5573d04 100644 +--- a/src/calibre/ebooks/conversion/plugins/html_output.py ++++ b/src/calibre/ebooks/conversion/plugins/html_output.py +@@ -27,13 +27,13 @@ class HTMLOutput(OutputFormatPlugin): + + options = { + OptionRecommendation(name='template_css', +- help=_('CSS file used for the output instead of the default file')), ++ help=_('CSS file used for the output instead of the default CSS.')), + + OptionRecommendation(name='template_html_index', +- help=_('Template used for generation of the HTML index file instead of the default file')), ++ help=_('Template used for generation of the HTML index file instead of the default template. In Mustache format.')), + + OptionRecommendation(name='template_html', +- help=_('Template used for the generation of the HTML contents of the book instead of the default file')), ++ help=_('Template used for the generation of the HTML contents of the book instead of the default template. In Mustache format.')), + + OptionRecommendation(name='extract_to', + help=_('Extract the contents of the generated ZIP file to the ' +@@ -85,8 +85,8 @@ class HTMLOutput(OutputFormatPlugin): + xml_declaration=False) + + def convert(self, oeb_book, output_path, input_plugin, opts, log): ++ import pystache + from lxml import etree +- from templite import Templite + + from calibre.ebooks.html.meta import EasyMeta + from calibre.utils import zipfile +@@ -97,7 +97,7 @@ class HTMLOutput(OutputFormatPlugin): + with open(opts.template_html_index, 'rb') as f: + template_html_index_data = f.read() + else: +- template_html_index_data = P('templates/html_export_default_index.tmpl', data=True) ++ template_html_data = P('templates/html_export_default.mustache', data=True) + + if opts.template_html is not None: + with open(opts.template_html, 'rb') as f: +@@ -111,9 +111,10 @@ class HTMLOutput(OutputFormatPlugin): + else: + template_css_data = P('templates/html_export_default.css', data=True) + +- template_html_index_data = template_html_index_data.decode('utf-8') +- template_html_data = template_html_data.decode('utf-8') ++ template_html_index = pystache.parse(template_html_index_data.decode('utf-8')) ++ template_html = pystache.parse(template_html_data.decode('utf-8')) + template_css_data = template_css_data.decode('utf-8') ++ has_toc = bool(oeb_book.toc.count()) + + self.log = log + self.opts = opts +@@ -130,18 +131,31 @@ class HTMLOutput(OutputFormatPlugin): + css_path = output_dir+os.sep+'calibreHtmlOutBasicCss.css' + with open(css_path, 'wb') as f: + f.write(template_css_data.encode('utf-8')) ++ meta_dict = { ++ 'titles': [{'title': x, 'is_first': i == 0} for i, x in enumerate(meta.titles())], ++ 'creators': authors_to_string(tuple(meta.creators())), ++ 'items': list(meta), ++ } ++ meta_dict['first_title'] = meta_dict['titles'][0]['title'] if meta_dict['titles'] else '' ++ basic_template_vars = { ++ 'meta': meta_dict, 'has_toc': has_toc, ++ 'table_of_contents': _('Table of contents'), 'no_toc': _('No table of contents present'), ++ 'begin_to_read': _('begin to read'), 'start': _('start'), ++ 'prev_page': _('previous page'), 'next_page': _('next page'), ++ } + + with open(output_file, 'wb') as f: +- html_toc = self.generate_html_toc(oeb_book, output_file, output_dir) +- templite = Templite(template_html_index_data) + nextLink = oeb_book.spine[0].href + nextLink = relpath(output_dir+os.sep+nextLink, dirname(output_file)) + cssLink = relpath(abspath(css_path), dirname(output_file)) + tocUrl = relpath(output_file, dirname(output_file)) +- t = templite.render(has_toc=bool(oeb_book.toc.count()), +- toc=html_toc, meta=meta, nextLink=nextLink, +- tocUrl=tocUrl, cssLink=cssLink, +- firstContentPageLink=nextLink) ++ toc_as_html = self.generate_html_toc(oeb_book, output_file, output_dir) if has_toc else '' ++ v = basic_template_vars.copy() ++ v.update({ ++ 'toc': toc_as_html, 'css_link': cssLink, 'toc_url': tocUrl, 'next_link': nextLink, ++ 'first_content_page_link': nextLink, ++ }) ++ t = pystache.render(template_html_index, v) + if isinstance(t, str): + t = t.encode('utf-8') + f.write(t) +@@ -197,17 +211,18 @@ class HTMLOutput(OutputFormatPlugin): + firstContentPageLink = oeb_book.spine[0].href + + # render template +- templite = Templite(template_html_data) +- + def toc(): +- return self.generate_html_toc(oeb_book, path, output_dir) +- t = templite.render(ebookContent=ebook_content, +- prevLink=prevLink, nextLink=nextLink, +- has_toc=bool(oeb_book.toc.count()), toc=toc, +- tocUrl=tocUrl, head_content=head_content, +- meta=meta, cssLink=cssLink, +- firstContentPageLink=firstContentPageLink) +- ++ return ++ toc_as_html = self.generate_html_toc(oeb_book, path, output_dir) if has_toc else '' ++ v = basic_template_vars.copy() ++ v.update({ ++ 'has_link': prevLink or nextLink, ++ 'prev_link': prevLink, 'next_link': nextLink, 'toc_url': tocUrl, ++ 'head_content': head_content, 'ebook_content': ebook_content, ++ 'css_link': cssLink, 'toc': toc_as_html, ++ 'first_content_page_link': firstContentPageLink, ++ }) ++ t = pystache.render(template_html, v) + # write html to file + with open(path, 'wb') as f: + f.write(t.encode('utf-8')) +diff --git a/src/templite/__init__.py b/src/templite/__init__.py +deleted file mode 100644 +index 8723d0d..0000000 +--- a/src/templite/__init__.py ++++ /dev/null +@@ -1,96 +0,0 @@ +-#!/usr/bin/env python +-# +-# Templite+ +-# A light-weight, fully functional, general purpose templating engine +-# +-# Copyright (c) 2009 joonis new media +-# Author: Thimo Kraemer <[email protected]> +-# +-# Based on Templite - Tomer Filiba +-# http://code.activestate.com/recipes/496702/ +-# +-# This program is free software; you can redistribute it and/or modify +-# it under the terms of the GNU General Public License as published by +-# the Free Software Foundation; either version 2 of the License, or +-# (at your option) any later version. +-# +-# This program is distributed in the hope that it will be useful, +-# but WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-# GNU General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +-# MA 02110-1301, USA. +-# +- +-import re +-import sys +- +-from polyglot.builtins import unicode_type +- +- +-class Templite: +- auto_emit = re.compile(r'''(^['"])|(^[a-zA-Z0-9_[\]'"]+$)''') +- +- def __init__(self, template, start='${', end='}$'): +- if len(start) != 2 or len(end) != 2: +- raise ValueError('each delimiter must be two characters long') +- delimiter = re.compile('%s(.*?)%s' % (re.escape(start), re.escape(end)), re.DOTALL) +- offset = 0 +- tokens = [] +- for i, part in enumerate(delimiter.split(template)): +- part = part.replace('\\'.join(list(start)), start) +- part = part.replace('\\'.join(list(end)), end) +- if i % 2 == 0: +- if not part: +- continue +- part = part.replace('\\', '\\\\').replace('"', '\\"') +- part = '\t' * offset + 'emit("""%s""")' % part +- else: +- part = part.rstrip() +- if not part: +- continue +- if part.lstrip().startswith(':'): +- if not offset: +- raise SyntaxError('no block statement to terminate: ${%s}$' % part) +- offset -= 1 +- part = part.lstrip()[1:] +- if not part.endswith(':'): +- continue +- elif self.auto_emit.match(part.lstrip()): +- part = 'emit(%s)' % part.lstrip() +- lines = part.splitlines() +- margin = min(len(l) - len(l.lstrip()) for l in lines if l.strip()) +- part = '\n'.join('\t' * offset + l[margin:] for l in lines) +- if part.endswith(':'): +- offset += 1 +- tokens.append(part) +- if offset: +- raise SyntaxError('%i block statement(s) not terminated' % offset) +- self.__code = compile('\n'.join(tokens), '<templite %r>' % template[:20], 'exec') +- +- def render(self, __namespace=None, **kw): +- """ +- renders the template according to the given namespace. +- __namespace - a dictionary serving as a namespace for evaluation +- **kw - keyword arguments which are added to the namespace +- """ +- namespace = {} +- if __namespace: +- namespace.update(__namespace) +- if kw: +- namespace.update(kw) +- namespace['emit'] = self.write +- +- __stdout = sys.stdout +- sys.stdout = self +- self.__output = [] +- eval(self.__code, namespace) +- sys.stdout = __stdout +- return ''.join(self.__output) +- +- def write(self, *args): +- for a in args: +- self.__output.append(unicode_type(a))

