c75dd6b56c9a8e63988b42a7416b90a0bf347b28
[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     add_anchors(result.getroot())
57     result.write(output_filename, xml_declaration=True, pretty_print=True, encoding='utf-8')
58
59
60 class Fragment(object):
61     def __init__(self, id, themes):
62         super(Fragment, self).__init__()
63         self.id = id
64         self.themes = themes
65         self.events = []
66
67     def append(self, event, element):
68         self.events.append((event, element))
69
70     def closed_events(self):
71         stack = []
72         for event, element in self.events:
73             if event == 'start':
74                 stack.append(('end', element))
75             elif event == 'end':
76                 try:
77                     stack.pop()
78                 except IndexError:
79                     print 'CLOSED NON-OPEN TAG:', element
80
81         stack.reverse()
82         return self.events + stack
83
84     def to_string(self):
85         result = []
86         for event, element in self.closed_events():
87             if event == 'start':
88                 result.append(u'<%s %s>' % (element.tag, ' '.join('%s="%s"' % (k, v) for k, v in element.attrib.items())))
89                 if element.text:
90                     result.append(element.text)
91             elif event == 'end':
92                 result.append(u'</%s>' % element.tag)
93                 if element.tail:
94                     result.append(element.tail)
95             else:
96                 result.append(element)
97
98         return ''.join(result)
99
100     def __unicode__(self):
101         return self.to_string()
102
103
104 def extract_fragments(input_filename):
105     """Extracts theme fragments from input_filename."""
106     open_fragments = {}
107     closed_fragments = {}
108
109     for event, element in etree.iterparse(input_filename, events=('start', 'end')):
110         # Process begin and end elements
111         if element.tag == 'span' and element.get('class', '') in ('theme-begin', 'theme-end'):
112             if not event == 'end': continue # Process elements only once, on end event
113
114             # Open new fragment
115             if element.get('class', '') == 'theme-begin':
116                 fragment = Fragment(id=element.get('fid'), themes=element.text)
117
118                 # Append parents
119                 if element.getparent().tag != 'body':
120                     parents = [element.getparent()]
121                     while parents[-1].getparent().tag != 'body':
122                         parents.append(parents[-1].getparent())
123
124                     parents.reverse()
125                     for parent in parents:
126                         fragment.append('start', parent)
127
128                 open_fragments[fragment.id] = fragment
129
130             # Close existing fragment
131             else:
132                 try:
133                     fragment = open_fragments[element.get('fid')]
134                 except KeyError:
135                     print '%s:closed not open fragment #%s' % (input_filename, element.get('fid'))
136                 else:
137                     closed_fragments[fragment.id] = fragment
138                     del open_fragments[fragment.id]
139
140             # Append element tail to lost_text (we don't want to lose any text)
141             if element.tail:
142                 for fragment_id in open_fragments:
143                     open_fragments[fragment_id].append('text', element.tail)
144
145
146         # Process all elements except begin and end
147         else:
148             # Omit annotation tags
149             if len(element.get('name', '')) or element.get('class', '') == 'annotation':
150                 if event == 'end' and element.tail:
151                     for fragment_id in open_fragments:
152                         open_fragments[fragment_id].append('text', element.tail)
153             else:
154                 for fragment_id in open_fragments:
155                     open_fragments[fragment_id].append(event, copy.copy(element))
156
157     return closed_fragments, open_fragments
158
159
160 def add_anchor(element, number):
161     anchor = etree.Element('a', href='#f%d' % number)
162     anchor.set('class', 'anchor')
163     anchor.text = str(number)
164     if element.text:
165         anchor.tail = element.text
166         element.text = u''
167     element.insert(0, anchor)
168     
169     anchor_target = etree.Element('a', name='f%d' % number)
170     element.insert(0, anchor_target)
171
172
173 def add_anchors(root):
174     counter = 1
175     for element in root.iterdescendants():
176         if element.getparent().tag in 'div' and 'note' in element.getparent().get('class', ''):
177             continue
178         if element.getparent().tag in 'blockquote':
179             continue
180         
181         if element.tag == 'p' and 'verse' in element.get('class', ''):
182             if counter == 1 or counter % 5 == 0:
183                 add_anchor(element, counter)
184             counter += 1
185         elif 'paragraph' in element.get('class', ''):
186             add_anchor(element, counter)
187             counter += 1
188
189