1 # -*- coding: utf-8 -*-
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
10 from lxml import etree
11 from librarian import XHTMLNS, ParseError, OutputFile
12 from librarian import functions
14 from lxml.etree import XMLSyntaxError, XSLTApplyError
16 functions.reg_substitute_entities()
17 functions.reg_person_name()
20 'legacy': 'xslt/book2html.xslt',
21 'full': 'xslt/wl2html_full.xslt',
22 'partial': 'xslt/wl2html_partial.xslt'
25 def get_stylesheet(name):
26 return os.path.join(os.path.dirname(__file__), STYLESHEETS[name])
28 def html_has_content(text):
29 return etree.ETXPath('//p|//{%(ns)s}p|//h1|//{%(ns)s}h1' % {'ns': str(XHTMLNS)})(text)
31 def transform(wldoc, stylesheet='legacy', options=None, flags=None):
32 """Transforms the WL document to XHTML.
34 If output_filename is None, returns an XML,
35 otherwise returns True if file has been written,False if it hasn't.
36 File won't be written if it has no content.
40 style_filename = get_stylesheet(stylesheet)
41 style = etree.parse(style_filename)
43 document = copy.deepcopy(wldoc)
45 document.swap_endlines()
49 document.edoc.getroot().set(flag, 'yes')
51 document.clean_ed_note()
55 result = document.transform(style, **options)
56 del document # no longer needed large object :)
58 if html_has_content(result):
59 add_anchors(result.getroot())
60 add_table_of_themes(result.getroot())
61 add_table_of_contents(result.getroot())
63 return OutputFile.from_string(etree.tostring(result, method='html',
64 xml_declaration=False, pretty_print=True, encoding='utf-8'))
68 raise ValueError("'%s' is not a valid stylesheet.")
69 except (XMLSyntaxError, XSLTApplyError), e:
72 class Fragment(object):
73 def __init__(self, id, themes):
74 super(Fragment, self).__init__()
79 def append(self, event, element):
80 self.events.append((event, element))
82 def closed_events(self):
84 for event, element in self.events:
86 stack.append(('end', element))
91 print 'CLOSED NON-OPEN TAG:', element
94 return self.events + stack
98 for event, element in self.closed_events():
100 result.append(u'<%s %s>' % (element.tag, ' '.join('%s="%s"' % (k, v) for k, v in element.attrib.items())))
102 result.append(element.text)
104 result.append(u'</%s>' % element.tag)
106 result.append(element.tail)
108 result.append(element)
110 return ''.join(result)
112 def __unicode__(self):
113 return self.to_string()
116 def extract_fragments(input_filename):
117 """Extracts theme fragments from input_filename."""
119 closed_fragments = {}
121 # iterparse would die on a HTML document
122 parser = etree.HTMLParser(encoding='utf-8')
123 buf = cStringIO.StringIO()
124 buf.write(etree.tostring(etree.parse(input_filename, parser).getroot()[0][0], encoding='utf-8'))
127 for event, element in etree.iterparse(buf, events=('start', 'end')):
128 # Process begin and end elements
129 if element.get('class', '') in ('theme-begin', 'theme-end'):
130 if not event == 'end': continue # Process elements only once, on end event
133 if element.get('class', '') == 'theme-begin':
134 fragment = Fragment(id=element.get('fid'), themes=element.text)
137 if element.getparent().get('id', None) != 'book-text':
138 parents = [element.getparent()]
139 while parents[-1].getparent().get('id', None) != 'book-text':
140 parents.append(parents[-1].getparent())
143 for parent in parents:
144 fragment.append('start', parent)
146 open_fragments[fragment.id] = fragment
148 # Close existing fragment
151 fragment = open_fragments[element.get('fid')]
153 print '%s:closed not open fragment #%s' % (input_filename, element.get('fid'))
155 closed_fragments[fragment.id] = fragment
156 del open_fragments[fragment.id]
158 # Append element tail to lost_text (we don't want to lose any text)
160 for fragment_id in open_fragments:
161 open_fragments[fragment_id].append('text', element.tail)
164 # Process all elements except begin and end
166 # Omit annotation tags
167 if (len(element.get('name', '')) or
168 element.get('class', '') in ('annotation', 'anchor')):
169 if event == 'end' and element.tail:
170 for fragment_id in open_fragments:
171 open_fragments[fragment_id].append('text', element.tail)
173 for fragment_id in open_fragments:
174 open_fragments[fragment_id].append(event, copy.copy(element))
176 return closed_fragments, open_fragments
179 def add_anchor(element, prefix, with_link=True, with_target=True, link_text=None):
180 parent = element.getparent()
181 index = parent.index(element)
184 if link_text is None:
186 anchor = etree.Element('a', href='#%s' % prefix)
187 anchor.set('class', 'anchor')
188 anchor.text = unicode(link_text)
189 parent.insert(index, anchor)
192 anchor_target = etree.Element('a', name='%s' % prefix)
193 anchor_target.set('class', 'target')
194 anchor_target.text = u' '
195 parent.insert(index, anchor_target)
198 def any_ancestor(element, test):
199 for ancestor in element.iterancestors():
205 def add_anchors(root):
207 for element in root.iterdescendants():
208 if any_ancestor(element, lambda e: e.get('class') in ('note', 'motto', 'motto_podpis', 'dedication')
209 or e.get('id') == 'nota_red'
210 or e.tag == 'blockquote'):
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)
217 elif 'paragraph' in element.get('class', ''):
218 add_anchor(element, "f%d" % counter, link_text=counter)
222 def raw_printable_text(element):
223 working = copy.deepcopy(element)
224 for e in working.findall('a'):
225 if e.get('class') in ('annotation', 'theme-begin'):
227 return etree.tostring(working, method='text', encoding=unicode).strip()
230 def add_table_of_contents(root):
233 for element in root.iterdescendants():
234 if element.tag in ('h2', 'h3'):
235 if any_ancestor(element, lambda e: e.get('id') in ('footnotes', 'nota_red') or e.get('class') in ('person-list',)):
238 element_text = raw_printable_text(element)
239 if element.tag == 'h3' and len(sections) and sections[-1][1] == 'h2':
240 sections[-1][3].append((counter, element.tag, element_text, []))
242 sections.append((counter, element.tag, element_text, []))
243 add_anchor(element, "s%d" % counter, with_link=False)
246 toc = etree.Element('div')
248 toc_header = etree.SubElement(toc, 'h2')
249 toc_header.text = u'Spis treści'
250 toc_list = etree.SubElement(toc, 'ol')
252 for n, section, text, subsections in sections:
253 section_element = etree.SubElement(toc_list, 'li')
254 add_anchor(section_element, "s%d" % n, with_target=False, link_text=text)
257 subsection_list = etree.SubElement(section_element, 'ol')
258 for n, subsection, text, _ in subsections:
259 subsection_element = etree.SubElement(subsection_list, 'li')
260 add_anchor(subsection_element, "s%d" % n, with_target=False, link_text=text)
265 def add_table_of_themes(root):
267 from sortify import sortify
269 sortify = lambda x: x
272 for fragment in root.findall('.//a[@class="theme-begin"]'):
273 if not fragment.text:
275 theme_names = [s.strip() for s in fragment.text.split(',')]
276 for theme_name in theme_names:
277 book_themes.setdefault(theme_name, []).append(fragment.get('name'))
278 book_themes = book_themes.items()
279 book_themes.sort(key=lambda s: sortify(s[0]))
280 themes_div = etree.Element('div', id="themes")
281 themes_ol = etree.SubElement(themes_div, 'ol')
282 for theme_name, fragments in book_themes:
283 themes_li = etree.SubElement(themes_ol, 'li')
284 themes_li.text = "%s: " % theme_name
285 for i, fragment in enumerate(fragments):
286 item = etree.SubElement(themes_li, 'a', href="#%s" % fragment)
287 item.text = str(i + 1)
289 root.insert(0, themes_div)
293 def extract_annotations(html_path):
294 """For each annotation, yields a tuple: anchor, text, html."""
295 parser = etree.HTMLParser(encoding='utf-8')
296 tree = etree.parse(html_path, parser)
297 footnotes = tree.find('//*[@id="footnotes"]')
298 if footnotes is not None:
299 for footnote in footnotes.findall('div'):
300 anchor = footnote.find('a[@name]').get('name')
302 text_str = etree.tostring(footnote, method='text', encoding='utf-8').strip()
303 html_str = etree.tostring(footnote, method='html', encoding='utf-8')
304 yield anchor, text_str, html_str