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))

Reply via email to