6a4ad5e27a9c7a08116bd4dcb040641eebfb1d28
[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     print result
57     if result.find('//div') is not None:
58         add_anchors(result.getroot())
59         add_table_of_contents(result.getroot())
60         result.write(output_filename, xml_declaration=False, pretty_print=True, encoding='utf-8')
61         return True
62     else:
63         return False
64
65
66 class Fragment(object):
67     def __init__(self, id, themes):
68         super(Fragment, self).__init__()
69         self.id = id
70         self.themes = themes
71         self.events = []
72
73     def append(self, event, element):
74         self.events.append((event, element))
75
76     def closed_events(self):
77         stack = []
78         for event, element in self.events:
79             if event == 'start':
80                 stack.append(('end', element))
81             elif event == 'end':
82                 try:
83                     stack.pop()
84                 except IndexError:
85                     print 'CLOSED NON-OPEN TAG:', element
86
87         stack.reverse()
88         return self.events + stack
89
90     def to_string(self):
91         result = []
92         for event, element in self.closed_events():
93             if event == 'start':
94                 result.append(u'<%s %s>' % (element.tag, ' '.join('%s="%s"' % (k, v) for k, v in element.attrib.items())))
95                 if element.text:
96                     result.append(element.text)
97             elif event == 'end':
98                 result.append(u'</%s>' % element.tag)
99                 if element.tail:
100                     result.append(element.tail)
101             else:
102                 result.append(element)
103
104         return ''.join(result)
105
106     def __unicode__(self):
107         return self.to_string()
108
109
110 def extract_fragments(input_filename):
111     """Extracts theme fragments from input_filename."""
112     open_fragments = {}
113     closed_fragments = {}
114
115     for event, element in etree.iterparse(input_filename, events=('start', 'end')):
116         # Process begin and end elements
117         if element.get('class', '') in ('theme-begin', 'theme-end'):
118             if not event == 'end': continue # Process elements only once, on end event
119
120             # Open new fragment
121             if element.get('class', '') == 'theme-begin':
122                 fragment = Fragment(id=element.get('fid'), themes=element.text)
123
124                 # Append parents
125                 if element.getparent().get('id', None) != 'book-text':
126                     parents = [element.getparent()]
127                     while parents[-1].getparent().get('id', None) != 'book-text':
128                         parents.append(parents[-1].getparent())
129
130                     parents.reverse()
131                     for parent in parents:
132                         fragment.append('start', parent)
133
134                 open_fragments[fragment.id] = fragment
135
136             # Close existing fragment
137             else:
138                 try:
139                     fragment = open_fragments[element.get('fid')]
140                 except KeyError:
141                     print '%s:closed not open fragment #%s' % (input_filename, element.get('fid'))
142                 else:
143                     closed_fragments[fragment.id] = fragment
144                     del open_fragments[fragment.id]
145
146             # Append element tail to lost_text (we don't want to lose any text)
147             if element.tail:
148                 for fragment_id in open_fragments:
149                     open_fragments[fragment_id].append('text', element.tail)
150
151
152         # Process all elements except begin and end
153         else:
154             # Omit annotation tags
155             if len(element.get('name', '')) or element.get('class', '') == 'annotation':
156                 if event == 'end' and element.tail:
157                     for fragment_id in open_fragments:
158                         open_fragments[fragment_id].append('text', element.tail)
159             else:
160                 for fragment_id in open_fragments:
161                     open_fragments[fragment_id].append(event, copy.copy(element))
162
163     return closed_fragments, open_fragments
164
165
166 def add_anchor(element, prefix, with_link=True, with_target=True, link_text=None):
167     if with_link:
168         if link_text is None:
169             link_text = prefix
170         anchor = etree.Element('a', href='#%s' % prefix)
171         anchor.set('class', 'anchor')
172         anchor.text = unicode(link_text)
173         if element.text:
174             anchor.tail = element.text
175             element.text = u''
176         element.insert(0, anchor)
177     
178     if with_target:
179         anchor_target = etree.Element('a', name='%s' % prefix)
180         anchor_target.set('class', 'target')
181         anchor_target.text = u' '
182         if element.text:
183             anchor_target.tail = element.text
184             element.text = u''
185         element.insert(0, anchor_target)
186
187
188 def any_ancestor(element, test):
189     for ancestor in element.iterancestors():
190         if test(ancestor):
191             return True
192     return False
193
194
195 def add_anchors(root):
196     counter = 1
197     for element in root.iterdescendants():
198         if any_ancestor(element, lambda e: e.get('class') in ('note', 'motto', 'motto_podpis', 'dedication')
199         or e.tag == 'blockquote'):
200             continue
201         
202         if element.tag == 'p' and 'verse' in element.get('class', ''):
203             if counter == 1 or counter % 5 == 0:
204                 add_anchor(element, "f%d" % counter, link_text=counter)
205             counter += 1
206         elif 'paragraph' in element.get('class', ''):
207             add_anchor(element, "f%d" % counter, link_text=counter)
208             counter += 1
209
210
211 def add_table_of_contents(root):
212     sections = []
213     counter = 1
214     for element in root.iterdescendants():
215         if element.tag in ('h2', 'h3'):
216             if any_ancestor(element, lambda e: e.get('id') in ('footnotes',) or e.get('class') in ('person-list',)):
217                 continue
218             
219             if element.tag == 'h3' and len(sections) and sections[-1][1] == 'h2':
220                 sections[-1][3].append((counter, element.tag, ''.join(element.xpath('text()')), []))
221             else:
222                 sections.append((counter, element.tag, ''.join(element.xpath('text()')), []))
223             add_anchor(element, "s%d" % counter, with_link=False)
224             counter += 1
225     
226     toc = etree.Element('div')
227     toc.set('id', 'toc')
228     toc_header = etree.SubElement(toc, 'h2')
229     toc_header.text = u'Spis treści'
230     toc_list = etree.SubElement(toc, 'ol')
231
232     for n, section, text, subsections in sections:
233         section_element = etree.SubElement(toc_list, 'li')
234         add_anchor(section_element, "s%d" % n, with_target=False, link_text=text)
235         
236         if len(subsections):
237             subsection_list = etree.SubElement(section_element, 'ol')
238             for n, subsection, text, _ in subsections:
239                 subsection_element = etree.SubElement(subsection_list, 'li')
240                 add_anchor(subsection_element, "s%d" % n, with_target=False, link_text=text)
241     
242     root.insert(0, toc)
243