Added importing metabooks (books with relation.hasPart in Dublin Core metadata).
[wolnelektury.git] / lib / librarian / html.py
1 # -*- coding: utf-8 -*-
2 import os
3 import cStringIO
4 import re
5 import copy
6 import pkgutil
7
8 from lxml import etree
9
10
11 ENTITY_SUBSTITUTIONS = [
12     (u'---', u'—'),
13     (u'--', u'–'),
14     (u'...', u'…'),
15     (u',,', u'„'),
16     (u'"', u'”'),
17 ]
18
19
20 def substitute_entities(context, text):
21     """XPath extension function converting all entites in passed text."""
22     if isinstance(text, list):
23         text = ''.join(text)
24     for entity, substitutution in ENTITY_SUBSTITUTIONS:
25         text = text.replace(entity, substitutution)
26     return text
27
28
29 # Register substitute_entities function with lxml
30 ns = etree.FunctionNamespace('http://wolnelektury.pl/functions')
31 ns['substitute_entities'] = substitute_entities
32
33
34 def transform(input_filename, output_filename):
35     """Transforms file input_filename in XML to output_filename in XHTML."""
36     # Parse XSLT
37     style_filename = os.path.join(os.path.dirname(__file__), 'book2html.xslt')
38     style = etree.parse(style_filename)
39
40     doc_file = cStringIO.StringIO()
41     expr = re.compile(r'/\s', re.MULTILINE | re.UNICODE);
42
43     f = open(input_filename, 'r')
44     for line in f:
45         line = line.decode('utf-8')
46         line = expr.sub(u'<br/>\n', line)
47         doc_file.write(line.encode('utf-8'))
48     f.close()
49
50     doc_file.seek(0);
51
52     parser = etree.XMLParser(remove_blank_text=True)
53     doc = etree.parse(doc_file, parser)
54
55     result = doc.xslt(style)
56     result.write(output_filename, xml_declaration=True, pretty_print=True, encoding='utf-8')
57
58
59 class Fragment(object):
60     def __init__(self, id, themes):
61         super(Fragment, self).__init__()
62         self.id = id
63         self.themes = themes
64         self.events = []
65
66     def append(self, event, element):
67         self.events.append((event, element))
68
69     def closed_events(self):
70         stack = []
71         for event, element in self.events:
72             if event == 'start':
73                 stack.append(('end', element))
74             elif event == 'end':
75                 try:
76                     stack.pop()
77                 except IndexError:
78                     print 'CLOSED NON-OPEN TAG:', element
79
80         stack.reverse()
81         return self.events + stack
82
83     def to_string(self):
84         result = []
85         for event, element in self.closed_events():
86             if event == 'start':
87                 result.append(u'<%s %s>' % (element.tag, ' '.join('%s="%s"' % (k, v) for k, v in element.attrib.items())))
88                 if element.text:
89                     result.append(element.text)
90             elif event == 'end':
91                 result.append(u'</%s>' % element.tag)
92                 if element.tail:
93                     result.append(element.tail)
94             else:
95                 result.append(element)
96
97         return ''.join(result)
98
99     def __unicode__(self):
100         return self.to_string()
101
102
103 def extract_fragments(input_filename):
104     """Extracts theme fragments from input_filename."""
105     open_fragments = {}
106     closed_fragments = {}
107
108     for event, element in etree.iterparse(input_filename, events=('start', 'end')):
109         # Process begin and end elements
110         if element.tag == 'span' and element.get('class', '') in ('theme-begin', 'theme-end'):
111             if not event == 'end': continue # Process elements only once, on end event
112
113             # Open new fragment
114             if element.get('class', '') == 'theme-begin':
115                 fragment = Fragment(id=element.get('fid'), themes=element.text)
116
117                 # Append parents
118                 if element.getparent().tag != 'body':
119                     parents = [element.getparent()]
120                     while parents[-1].getparent().tag != 'body':
121                         parents.append(parents[-1].getparent())
122
123                     parents.reverse()
124                     for parent in parents:
125                         fragment.append('start', parent)
126
127                 open_fragments[fragment.id] = fragment
128
129             # Close existing fragment
130             else:
131                 try:
132                     fragment = open_fragments[element.get('fid')]
133                 except KeyError:
134                     print '%s:closed not open fragment #%s' % (input_filename, element.get('fid'))
135                 else:
136                     closed_fragments[fragment.id] = fragment
137                     del open_fragments[fragment.id]
138
139             # Append element tail to lost_text (we don't want to lose any text)
140             if element.tail:
141                 for fragment_id in open_fragments:
142                     open_fragments[fragment_id].append('text', element.tail)
143
144
145         # Process all elements except begin and end
146         else:
147             # Omit annotation tags
148             if len(element.get('name', '')) or element.get('class', '') == 'annotation':
149                 if event == 'end' and element.tail:
150                     for fragment_id in open_fragments:
151                         open_fragments[fragment_id].append('text', element.tail)
152             else:
153                 for fragment_id in open_fragments:
154                     open_fragments[fragment_id].append(event, copy.copy(element))
155
156     return closed_fragments, open_fragments
157