Stable version 1.2.5.
[librarian.git] / librarian / html.py
1 # -*- coding: utf-8 -*-
2 import os
3 import cStringIO
4 import re
5 import copy
6
7 from lxml import etree
8 from librarian.parser import WLDocument
9 from librarian import XHTMLNS, ParseError
10
11 from lxml.etree import XMLSyntaxError, XSLTApplyError
12
13 ENTITY_SUBSTITUTIONS = [
14     (u'---', u'—'),
15     (u'--', u'–'),
16     (u'...', u'…'),
17     (u',,', u'„'),
18     (u'"', u'”'),
19 ]
20
21 STYLESHEETS = {
22     'legacy': 'book2html.xslt',
23     'full': 'wl2html_full.xslt',
24     'partial': 'wl2html_partial.xslt'
25 }
26
27 def get_stylesheet(name):
28     return os.path.join(os.path.dirname(__file__), STYLESHEETS[name])
29
30 def substitute_entities(context, text):
31     """XPath extension function converting all entites in passed text."""
32     if isinstance(text, list):
33         text = ''.join(text)
34     for entity, substitutution in ENTITY_SUBSTITUTIONS:
35         text = text.replace(entity, substitutution)
36     return text
37
38 # Register substitute_entities function with lxml
39 ns = etree.FunctionNamespace('http://wolnelektury.pl/functions')
40 ns['substitute_entities'] = substitute_entities
41
42 def transform(input, output_filename=None, is_file=True, \
43     parse_dublincore=True, stylesheet='legacy', options={}):
44     """Transforms file input_filename in XML to output_filename in XHTML."""
45     # Parse XSLT
46     try:
47         style_filename = get_stylesheet(stylesheet)
48         style = etree.parse(style_filename)
49
50         if is_file:
51             document = WLDocument.from_file(input, True, \
52                 parse_dublincore=parse_dublincore)
53         else:
54             document = WLDocument.from_string(input, True, \
55                 parse_dublincore=parse_dublincore)
56
57         result = document.transform(style, **options)
58         del document # no longer needed large object :)        
59         
60         if etree.ETXPath('//p|//{%s}p' % str(XHTMLNS))(result) is not None:
61             add_anchors(result.getroot())
62             add_table_of_contents(result.getroot())
63         
64             if output_filename is not None:
65                 result.write(output_filename, xml_declaration=False, pretty_print=True, encoding='utf-8')
66             else:
67                 return result
68             return True
69         else:
70             print "[Librarian] didn't find any paragraphs"
71             return "<empty />"
72     except KeyError:
73         raise ValueError("'%s' is not a valid stylesheet.")
74     except (XMLSyntaxError, XSLTApplyError), e:
75         raise ParseError(e)
76
77 class Fragment(object):
78     def __init__(self, id, themes):
79         super(Fragment, self).__init__()
80         self.id = id
81         self.themes = themes
82         self.events = []
83
84     def append(self, event, element):
85         self.events.append((event, element))
86
87     def closed_events(self):
88         stack = []
89         for event, element in self.events:
90             if event == 'start':
91                 stack.append(('end', element))
92             elif event == 'end':
93                 try:
94                     stack.pop()
95                 except IndexError:
96                     print 'CLOSED NON-OPEN TAG:', element
97
98         stack.reverse()
99         return self.events + stack
100
101     def to_string(self):
102         result = []
103         for event, element in self.closed_events():
104             if event == 'start':
105                 result.append(u'<%s %s>' % (element.tag, ' '.join('%s="%s"' % (k, v) for k, v in element.attrib.items())))
106                 if element.text:
107                     result.append(element.text)
108             elif event == 'end':
109                 result.append(u'</%s>' % element.tag)
110                 if element.tail:
111                     result.append(element.tail)
112             else:
113                 result.append(element)
114
115         return ''.join(result)
116
117     def __unicode__(self):
118         return self.to_string()
119
120
121 def extract_fragments(input_filename):
122     """Extracts theme fragments from input_filename."""
123     open_fragments = {}
124     closed_fragments = {}
125
126     for event, element in etree.iterparse(input_filename, events=('start', 'end')):
127         # Process begin and end elements
128         if element.get('class', '') in ('theme-begin', 'theme-end'):
129             if not event == 'end': continue # Process elements only once, on end event
130
131             # Open new fragment
132             if element.get('class', '') == 'theme-begin':
133                 fragment = Fragment(id=element.get('fid'), themes=element.text)
134
135                 # Append parents
136                 if element.getparent().get('id', None) != 'book-text':
137                     parents = [element.getparent()]
138                     while parents[-1].getparent().get('id', None) != 'book-text':
139                         parents.append(parents[-1].getparent())
140
141                     parents.reverse()
142                     for parent in parents:
143                         fragment.append('start', parent)
144
145                 open_fragments[fragment.id] = fragment
146
147             # Close existing fragment
148             else:
149                 try:
150                     fragment = open_fragments[element.get('fid')]
151                 except KeyError:
152                     print '%s:closed not open fragment #%s' % (input_filename, element.get('fid'))
153                 else:
154                     closed_fragments[fragment.id] = fragment
155                     del open_fragments[fragment.id]
156
157             # Append element tail to lost_text (we don't want to lose any text)
158             if element.tail:
159                 for fragment_id in open_fragments:
160                     open_fragments[fragment_id].append('text', element.tail)
161
162
163         # Process all elements except begin and end
164         else:
165             # Omit annotation tags
166             if len(element.get('name', '')) or element.get('class', '') == 'annotation':
167                 if event == 'end' and element.tail:
168                     for fragment_id in open_fragments:
169                         open_fragments[fragment_id].append('text', element.tail)
170             else:
171                 for fragment_id in open_fragments:
172                     open_fragments[fragment_id].append(event, copy.copy(element))
173
174     return closed_fragments, open_fragments
175
176
177 def add_anchor(element, prefix, with_link=True, with_target=True, link_text=None):
178     if with_link:
179         if link_text is None:
180             link_text = prefix
181         anchor = etree.Element('a', href='#%s' % prefix)
182         anchor.set('class', 'anchor')
183         anchor.text = unicode(link_text)
184         if element.text:
185             anchor.tail = element.text
186             element.text = u''
187         element.insert(0, anchor)
188     
189     if with_target:
190         anchor_target = etree.Element('a', name='%s' % prefix)
191         anchor_target.set('class', 'target')
192         anchor_target.text = u' '
193         if element.text:
194             anchor_target.tail = element.text
195             element.text = u''
196         element.insert(0, anchor_target)
197
198
199 def any_ancestor(element, test):
200     for ancestor in element.iterancestors():
201         if test(ancestor):
202             return True
203     return False
204
205
206 def add_anchors(root):
207     counter = 1
208     for element in root.iterdescendants():
209         if any_ancestor(element, lambda e: e.get('class') in ('note', 'motto', 'motto_podpis', 'dedication')
210         or e.tag == 'blockquote'):
211             continue
212         
213         if element.tag == 'p' and 'verse' in element.get('class', ''):
214             if counter == 1 or counter % 5 == 0:
215                 add_anchor(element, "f%d" % counter, link_text=counter)
216             counter += 1
217         elif 'paragraph' in element.get('class', ''):
218             add_anchor(element, "f%d" % counter, link_text=counter)
219             counter += 1
220
221
222 def add_table_of_contents(root):
223     sections = []
224     counter = 1
225     for element in root.iterdescendants():
226         if element.tag in ('h2', 'h3'):
227             if any_ancestor(element, lambda e: e.get('id') in ('footnotes',) or e.get('class') in ('person-list',)):
228                 continue
229             
230             if element.tag == 'h3' and len(sections) and sections[-1][1] == 'h2':
231                 sections[-1][3].append((counter, element.tag, ''.join(element.xpath('text()')), []))
232             else:
233                 sections.append((counter, element.tag, ''.join(element.xpath('text()')), []))
234             add_anchor(element, "s%d" % counter, with_link=False)
235             counter += 1
236     
237     toc = etree.Element('div')
238     toc.set('id', 'toc')
239     toc_header = etree.SubElement(toc, 'h2')
240     toc_header.text = u'Spis treści'
241     toc_list = etree.SubElement(toc, 'ol')
242
243     for n, section, text, subsections in sections:
244         section_element = etree.SubElement(toc_list, 'li')
245         add_anchor(section_element, "s%d" % n, with_target=False, link_text=text)
246         
247         if len(subsections):
248             subsection_list = etree.SubElement(section_element, 'ol')
249             for n, subsection, text, _ in subsections:
250                 subsection_element = etree.SubElement(subsection_list, 'li')
251                 add_anchor(subsection_element, "s%d" % n, with_target=False, link_text=text)
252     
253     root.insert(0, toc)
254