mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
HTML Output: Convert the ebook to a colelction of HTML files suitable for displaying as a website
This commit is contained in:
commit
8367f875a5
60
resources/templates/html_export_default.css
Normal file
60
resources/templates/html_export_default.css
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
body{
|
||||||
|
margin:0px;
|
||||||
|
padding: 0.5em;
|
||||||
|
background-color:#F6F3E9;
|
||||||
|
font-size:12px;
|
||||||
|
font-family:Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreMeta{
|
||||||
|
background-color:#39322B;
|
||||||
|
color:white;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreMeta a, .calibreEbNav a, .calibreEbNavTop a, .calibreToc a{
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreMeta h1{
|
||||||
|
margin:0px;
|
||||||
|
font-size:18px;
|
||||||
|
background-color:#39322B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreEbookContent{
|
||||||
|
padding:20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreEbNav, .calibreEbNavTop{
|
||||||
|
clear:both;
|
||||||
|
background-color:#39322B;
|
||||||
|
color:white;
|
||||||
|
padding:10px;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreEbNavTop{
|
||||||
|
margin-bottom:20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreEbNav a, .calibreEbNavTop a{
|
||||||
|
padding:0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreTocIndex{
|
||||||
|
line-height:18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calibreToc{
|
||||||
|
float:left;
|
||||||
|
margin:20px;
|
||||||
|
width:300px;
|
||||||
|
background-color:#39322B;
|
||||||
|
color:white;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
.calibreEbookContent{
|
||||||
|
width:600px;
|
||||||
|
float:left;
|
||||||
|
}
|
74
resources/templates/html_export_default.tmpl
Normal file
74
resources/templates/html_export_default.tmpl
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<!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>
|
61
resources/templates/html_export_default_index.tmpl
Normal file
61
resources/templates/html_export_default_index.tmpl
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<!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 meta.titles().next(); 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>
|
@ -446,6 +446,7 @@ from calibre.ebooks.rb.output import RBOutput
|
|||||||
from calibre.ebooks.rtf.output import RTFOutput
|
from calibre.ebooks.rtf.output import RTFOutput
|
||||||
from calibre.ebooks.tcr.output import TCROutput
|
from calibre.ebooks.tcr.output import TCROutput
|
||||||
from calibre.ebooks.txt.output import TXTOutput
|
from calibre.ebooks.txt.output import TXTOutput
|
||||||
|
from calibre.ebooks.html.output import HTMLOutput
|
||||||
from calibre.ebooks.snb.output import SNBOutput
|
from calibre.ebooks.snb.output import SNBOutput
|
||||||
|
|
||||||
from calibre.customize.profiles import input_profiles, output_profiles
|
from calibre.customize.profiles import input_profiles, output_profiles
|
||||||
@ -525,6 +526,7 @@ plugins += [
|
|||||||
RTFOutput,
|
RTFOutput,
|
||||||
TCROutput,
|
TCROutput,
|
||||||
TXTOutput,
|
TXTOutput,
|
||||||
|
HTMLOutput,
|
||||||
SNBOutput,
|
SNBOutput,
|
||||||
]
|
]
|
||||||
# Order here matters. The first matched device is the one used.
|
# Order here matters. The first matched device is the one used.
|
||||||
@ -893,4 +895,3 @@ plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions,
|
|||||||
Email, Server, Plugins, Tweaks, Misc]
|
Email, Server, Plugins, Tweaks, Misc]
|
||||||
|
|
||||||
#}}}
|
#}}}
|
||||||
|
|
||||||
|
33
src/calibre/ebooks/html/meta.py
Normal file
33
src/calibre/ebooks/html/meta.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2010, Fabian Grassl <fg@jusmeum.de>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.ebooks.oeb.base import namespace, barename, DC11_NS
|
||||||
|
|
||||||
|
class EasyMeta(object):
|
||||||
|
|
||||||
|
def __init__(self, meta):
|
||||||
|
self.meta = meta
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
meta = self.meta
|
||||||
|
for item_name in meta.items:
|
||||||
|
for item in meta[item_name]:
|
||||||
|
if namespace(item.term) == DC11_NS:
|
||||||
|
yield { 'name': barename(item.term), 'value': item.value }
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
count = 0
|
||||||
|
for item in self:
|
||||||
|
count = count+1
|
||||||
|
return count
|
||||||
|
|
||||||
|
def titles(self):
|
||||||
|
for item in self.meta['title']:
|
||||||
|
yield item.value
|
||||||
|
|
||||||
|
def creators(self):
|
||||||
|
for item in self.meta['creator']:
|
||||||
|
yield item.value
|
201
src/calibre/ebooks/html/output.py
Normal file
201
src/calibre/ebooks/html/output.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
from __future__ import with_statement
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2010, Fabian Grassl <fg@jusmeum.de>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, re, shutil
|
||||||
|
|
||||||
|
from os.path import dirname, abspath, relpath, exists
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from templite import Templite
|
||||||
|
|
||||||
|
from calibre.ebooks.oeb.base import element
|
||||||
|
from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation
|
||||||
|
from calibre import CurrentDir
|
||||||
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
|
from calibre.utils.zipfile import ZipFile
|
||||||
|
|
||||||
|
from urllib import unquote
|
||||||
|
|
||||||
|
from calibre.ebooks.html.meta import EasyMeta
|
||||||
|
|
||||||
|
class HTMLOutput(OutputFormatPlugin):
|
||||||
|
|
||||||
|
name = 'HTML Output'
|
||||||
|
author = 'Fabian Grassl'
|
||||||
|
file_type = 'zip'
|
||||||
|
|
||||||
|
options = set([
|
||||||
|
OptionRecommendation(name='template_css',
|
||||||
|
help=_('CSS file used for the output instead of the default file')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='template_html_index',
|
||||||
|
help=_('Template used for generation of the html index file instead of the default file')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='template_html',
|
||||||
|
help=_('Template used for the generation of the html contents of the book instead of the default file')),
|
||||||
|
|
||||||
|
OptionRecommendation(name='extract_to',
|
||||||
|
help=_('Extract the contents of the generated ZIP file to the directory of the generated ZIP file')
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
|
||||||
|
|
||||||
|
def generate_toc(self, oeb_book, ref_url, output_dir):
|
||||||
|
'''
|
||||||
|
Generate table of contents
|
||||||
|
'''
|
||||||
|
with CurrentDir(output_dir):
|
||||||
|
def build_node(current_node, parent=None):
|
||||||
|
if parent is None:
|
||||||
|
parent = etree.Element('ul')
|
||||||
|
elif len(current_node.nodes):
|
||||||
|
parent = element(parent, ('ul'))
|
||||||
|
for node in current_node.nodes:
|
||||||
|
point = element(parent, 'li')
|
||||||
|
href = relpath(abspath(unquote(node.href)), dirname(ref_url))
|
||||||
|
link = element(point, 'a', href=href)
|
||||||
|
title = node.title
|
||||||
|
if title:
|
||||||
|
title = re.sub(r'\s+', ' ', title)
|
||||||
|
link.text=title
|
||||||
|
build_node(node, point)
|
||||||
|
return parent
|
||||||
|
wrap = etree.Element('div')
|
||||||
|
wrap.append(build_node(oeb_book.toc))
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
def generate_html_toc(self, oeb_book, ref_url, output_dir):
|
||||||
|
root = self.generate_toc(oeb_book, ref_url, output_dir)
|
||||||
|
return etree.tostring(root, pretty_print=True, encoding='utf-8',
|
||||||
|
xml_declaration=True)
|
||||||
|
|
||||||
|
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||||
|
|
||||||
|
# read template files
|
||||||
|
if opts.template_html_index is not None:
|
||||||
|
template_html_index_data = open(opts.template_html_index, 'rb').read()
|
||||||
|
else:
|
||||||
|
template_html_index_data = P('templates/html_export_default_index.tmpl', data=True)
|
||||||
|
|
||||||
|
if opts.template_html is not None:
|
||||||
|
template_html_data = open(opts.template_html, 'rb').read()
|
||||||
|
else:
|
||||||
|
template_html_data = P('templates/html_export_default.tmpl', data=True)
|
||||||
|
|
||||||
|
if opts.template_css is not None:
|
||||||
|
template_css_data = open(opts.template_css, 'rb').read()
|
||||||
|
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_css_data = template_css_data.decode('utf-8')
|
||||||
|
|
||||||
|
self.log = log
|
||||||
|
self.opts = opts
|
||||||
|
meta = EasyMeta(oeb_book.metadata)
|
||||||
|
|
||||||
|
tempdir = PersistentTemporaryDirectory()
|
||||||
|
output_file = os.path.join(tempdir,
|
||||||
|
os.path.basename(re.sub(r'\.zip', '', output_path)+'.html'))
|
||||||
|
output_dir = re.sub(r'\.html', '', output_file)+'_files'
|
||||||
|
|
||||||
|
if not exists(output_dir):
|
||||||
|
os.makedirs(output_dir)
|
||||||
|
|
||||||
|
css_path = output_dir+os.sep+'calibreHtmlOutBasicCss.css'
|
||||||
|
with open(css_path, 'wb') as f:
|
||||||
|
f.write(template_css_data.encode('utf-8'))
|
||||||
|
|
||||||
|
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)
|
||||||
|
f.write(t)
|
||||||
|
|
||||||
|
with CurrentDir(output_dir):
|
||||||
|
for item in oeb_book.manifest:
|
||||||
|
path = abspath(unquote(item.href))
|
||||||
|
dir = dirname(path)
|
||||||
|
if not exists(dir):
|
||||||
|
os.makedirs(dir)
|
||||||
|
if item.spine_position is not None:
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(str(item))
|
||||||
|
item.unload_data_from_memory(memory=path)
|
||||||
|
|
||||||
|
for item in oeb_book.spine:
|
||||||
|
path = abspath(unquote(item.href))
|
||||||
|
dir = dirname(path)
|
||||||
|
root = item.data.getroottree()
|
||||||
|
|
||||||
|
# get & clean HTML <HEAD>-data
|
||||||
|
head = root.xpath('//h:head', namespaces={'h': 'http://www.w3.org/1999/xhtml'})[0]
|
||||||
|
head_content = etree.tostring(head, pretty_print=True, encoding='utf-8')
|
||||||
|
head_content = re.sub(r'\<\/?head.*\>', '', head_content)
|
||||||
|
head_content = re.sub(re.compile(r'\<style.*\/style\>', re.M|re.S), '', head_content)
|
||||||
|
|
||||||
|
# get & clean HTML <BODY>-data
|
||||||
|
body = root.xpath('//h:body', namespaces={'h': 'http://www.w3.org/1999/xhtml'})[0]
|
||||||
|
ebook_content = etree.tostring(body, pretty_print=True, encoding='utf-8')
|
||||||
|
ebook_content = re.sub(r'\<\/?body.*\>', '', ebook_content)
|
||||||
|
|
||||||
|
# generate link to next page
|
||||||
|
if item.spine_position+1 < len(oeb_book.spine):
|
||||||
|
nextLink = oeb_book.spine[item.spine_position+1].href
|
||||||
|
nextLink = relpath(abspath(nextLink), dir)
|
||||||
|
else:
|
||||||
|
nextLink = None
|
||||||
|
|
||||||
|
# generate link to previous page
|
||||||
|
if item.spine_position > 0:
|
||||||
|
prevLink = oeb_book.spine[item.spine_position-1].href
|
||||||
|
prevLink = relpath(abspath(prevLink), dir)
|
||||||
|
else:
|
||||||
|
prevLink = None
|
||||||
|
|
||||||
|
cssLink = relpath(abspath(css_path), dir)
|
||||||
|
tocUrl = relpath(output_file, dir)
|
||||||
|
|
||||||
|
# render template
|
||||||
|
templite = Templite(template_html_data)
|
||||||
|
toc = lambda: 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)
|
||||||
|
|
||||||
|
# write html to file
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(t)
|
||||||
|
item.unload_data_from_memory(memory=path)
|
||||||
|
|
||||||
|
zfile = ZipFile(output_path, "w")
|
||||||
|
zfile.add_dir(output_dir)
|
||||||
|
|
||||||
|
if opts.extract_to:
|
||||||
|
if os.path.exists(opts.extract_to):
|
||||||
|
shutil.rmtree(opts.extract_to)
|
||||||
|
os.makedirs(opts.extract_to)
|
||||||
|
zfile.extractall(opts.extract_to)
|
||||||
|
self.log('Zip file extracted to', opts.extract_to)
|
||||||
|
|
||||||
|
zfile.close()
|
||||||
|
|
||||||
|
# cleanup temp dir
|
||||||
|
shutil.rmtree(tempdir)
|
||||||
|
|
||||||
|
|
@ -49,5 +49,3 @@ class OEBOutput(OutputFormatPlugin):
|
|||||||
with open(path, 'wb') as f:
|
with open(path, 'wb') as f:
|
||||||
f.write(str(item))
|
f.write(str(item))
|
||||||
item.unload_data_from_memory(memory=path)
|
item.unload_data_from_memory(memory=path)
|
||||||
|
|
||||||
|
|
||||||
|
87
src/templite/__init__.py
Normal file
87
src/templite/__init__.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
#
|
||||||
|
# Templite+
|
||||||
|
# A light-weight, fully functional, general purpose templating engine
|
||||||
|
#
|
||||||
|
# Copyright (c) 2009 joonis new media
|
||||||
|
# Author: Thimo Kraemer <thimo.kraemer@joonis.de>
|
||||||
|
#
|
||||||
|
# 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 sys, re
|
||||||
|
|
||||||
|
class Templite(object):
|
||||||
|
auto_emit = re.compile('(^[\'\"])|(^[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(str(a))
|
Loading…
x
Reference in New Issue
Block a user