endline handling in strofa
[librarian.git] / librarian / pyhtml.py
1 # -*- coding: utf-8 -*-
2 #
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
5 #
6 from lxml import etree
7 from librarian import IOFile, RDFNS, DCNS, Format
8 from xmlutils import Xmill, tag, tagged, ifoption, tag_open_close
9 from librarian import functions
10 import re
11 import random
12
13
14 class EduModule(Xmill):
15     def __init__(self, options=None):
16         super(EduModule, self).__init__(options)
17         self.activity_counter = 0
18
19         # text filters
20         def swap_endlines(txt):
21             if self.options['strofa']:
22                 txt = txt.replace("/\n", "<br/>\n")
23             return txt
24         self.register_text_filter(functions.substitute_entities)
25         self.register_text_filter(swap_endlines)
26
27     @tagged('div', 'stanza')
28     def handle_strofa(self, element):
29         self.options = {'strofa': True}
30         return "", ""
31         
32     def handle_powiesc(self, element):
33         return u"""
34 <div class="module" id="book-text">
35 <!-- <span class="teacher-toggle">
36   <input type="checkbox" name="teacher-toggle" id="teacher-toggle"/>
37   <label for="teacher-toggle">Pokaż treść dla nauczyciela</label>
38  </span>-->
39
40 """, u"</div>"
41
42     handle_autor_utworu = tag("span", "author")
43     handle_dzielo_nadrzedne = tag("span", "collection")
44     handle_podtytul = tag("span", "subtitle")
45     handle_naglowek_akt = handle_naglowek_czesc = handle_srodtytul = tag("h2")
46     handle_naglowek_scena = tag('h2')
47     handle_naglowek_osoba = handle_naglowek_podrozdzial = tag('h3')
48     handle_akap = handle_akap_dialog = handle_akap_cd = tag('p', 'paragraph')
49
50     handle_wyroznienie = tag('em')
51     handle_tytul_dziela = tag('em', 'title')
52     handle_slowo_obce = tag('em', 'foreign')
53
54     def handle_nazwa_utworu(self, element):
55         toc = []
56         for naglowek in element.getparent().findall('.//naglowek_rozdzial'):
57             a = etree.Element("a")
58             a.attrib["href"] = "#" + naglowek.text
59             a.text = naglowek.text
60             atxt = etree.tostring(a, encoding=unicode)
61             toc.append("<li>%s</li>" % atxt)
62         toc = "<ul class='toc'>%s</ul>" % "".join(toc)
63         return "<h1 class='title'>Lekcja: ", "</h1>" + toc
64
65     @tagged("h2")
66     def handle_naglowek_rozdzial(self, element):
67         return "", "".join(tag_open_close("a", name=element.text))
68
69     def handle_uwaga(self, _e):
70         return None
71
72     def handle_aktywnosc(self, element):
73         self.activity_counter += 1
74         self.options = {
75             'activity': True,
76             'activity_counter': self.activity_counter,
77             }
78         submill = EduModule(dict(self.options.items() + {'sub_gen': True}.items()))
79
80         opis = submill.generate(element.xpath('opis')[0])
81
82         n = element.xpath('wskazowki')
83         if n: wskazowki = submill.generate(n[0])
84
85         else: wskazowki = ''
86         n = element.xpath('pomoce')
87
88         if n: pomoce = submill.generate(n[0])
89         else: pomoce = ''
90
91         forma = ''.join(element.xpath('forma/text()'))
92
93         czas = ''.join(element.xpath('czas/text()'))
94
95         counter = self.activity_counter
96
97         return u"""
98 <div class="activity">
99  <div class="text">
100   <span class="act_counter">%(counter)d.</span>
101   %(opis)s""" % locals(), \
102 u"""%(wskazowki)s
103  </div>
104  <aside class="info">
105   <section class="infobox time"><h1>Czas</h1><p>%(czas)s min</p></section>
106   <section class="infobox kind"><h1>Metoda</h1><p>%(forma)s</p></section>
107   %(pomoce)s
108  </aside>
109  <div class="clearboth"></div>
110 </div>
111 """ % locals()
112
113     handle_opis = ifoption(sub_gen=True)(tag('div', 'description'))
114     handle_wskazowki = ifoption(sub_gen=True)(tag('div', ('hints', 'teacher')))
115
116     @ifoption(sub_gen=True)
117     @tagged('section', 'infobox materials')
118     def handle_pomoce(self, _):
119         return """<h1>Pomoce</h1>""", ""
120
121     def handle_czas(self, *_):
122         return
123
124     def handle_forma(self, *_):
125         return
126
127     def handle_cwiczenie(self, element):
128         exercise_handlers = {
129             'wybor': Wybor,
130             'uporzadkuj': Uporzadkuj,
131             'luki': Luki,
132             'zastap': Zastap,
133             'przyporzadkuj': Przyporzadkuj,
134             'prawdafalsz': PrawdaFalsz
135             }
136
137         typ = element.attrib['typ']
138         handler = exercise_handlers[typ](self.options)
139         return handler.generate(element)
140
141     # Lists
142     def handle_lista(self, element, attrs={}):
143         ltype = element.attrib.get('typ', 'punkt')
144         if ltype == 'slowniczek':
145             surl = element.attrib.get('href', None)
146             sxml = None
147             if surl:
148                 sxml = etree.fromstring(self.options['provider'].by_uri(surl).get_string())
149             self.options = {'slowniczek': True, 'slowniczek_xml': sxml }
150             return '<div class="slowniczek">', '</div>'
151
152         listtag = {'num': 'ol',
153                'punkt': 'ul',
154                'alfa': 'ul',
155                'czytelnia': 'ul'}[ltype]
156
157         classes = attrs.get('class', '')
158         if classes: del attrs['class']
159
160         attrs_s = ' '.join(['%s="%s"' % kv for kv in attrs.items()])
161         if attrs_s: attrs_s = ' ' + attrs_s
162
163         return '<%s class="lista %s %s"%s>' % (listtag, ltype, classes, attrs_s), '</%s>' % listtag
164
165     def handle_punkt(self, element):
166         if self.options['slowniczek']:
167             return '<dl>', '</dl>'
168         else:
169             return '<li>', '</li>'
170
171     def handle_definiendum(self, element):
172         nxt = element.getnext()
173         definiens_s = ''
174
175         # let's pull definiens from another document
176         if self.options['slowniczek_xml'] and (not nxt or nxt.tag != 'definiens'):
177             sxml = self.options['slowniczek_xml']
178             assert element.text != ''
179             defloc = sxml.xpath("//definiendum[text()='%s']" % element.text)
180             if defloc:
181                 definiens = defloc[0].getnext()
182                 if definiens.tag == 'definiens':
183                     subgen = EduModule(self.options)
184                     definiens_s = subgen.generate(definiens)
185
186         return u"<dt>", u"</dt>" + definiens_s
187
188     def handle_definiens(self, element):
189         return u"<dd>", u"</dd>"
190
191     def handle_podpis(self, element):
192         return u"""<div class="caption">""", u"</div>"
193
194     def handle_tabela(self, element):
195         has_frames = int(element.attrib.get("ramki", "0"))
196         if has_frames: frames_c = "framed"
197         else: frames_c = ""
198         return u"""<table class="%s">""" % frames_c, u"</table>"
199
200     def handle_wiersz(self, element):
201         return u"<tr>", u"</tr>"
202
203     def handle_kol(self, element):
204         return u"<td>", u"</td>"
205
206     def handle_rdf__RDF(self, _):
207         # ustal w opcjach  rzeczy :D
208         return
209
210     def handle_link(self, element):
211         if 'url' in element.attrib:
212             return tag('a', href=element.attrib['url'])(self, element)
213         elif 'material' in element.attrib:
214             material_err = u' [BRAKUJĄCY MATERIAŁ]'
215             make_url = lambda f: self.options['urlmapper'] \
216               .url_for_material(element.attrib['material'], f)
217
218             if 'format' in element.attrib:
219                 formats = re.split(r"[, ]+",
220                                element.attrib['format'])
221             else:
222                 formats = [None]
223
224             try:
225                 def_href = make_url(formats[0])
226                 def_err = u""
227             except self.options['urlmapper'].MaterialNotFound:
228                 def_err = material_err
229                 def_href = u""
230             fmt_links = []
231             for f in formats[1:]:
232                 try:
233                     fmt_links.append(u'<a href="%s">%s</a>' % (make_url(f), f.upper()))
234                 except self.options['urlmapper'].MaterialNotFound:
235                     fmt_links.append(u'<a>%s%s</a>' % (f.upper(), material_err))
236             more_links = u' (%s)' % u', '.join(fmt_links) if fmt_links else u''
237
238             return u"<a href='%s'>" % def_href, u'%s</a>%s' % (def_err, more_links)
239
240
241 class Exercise(EduModule):
242     def __init__(self, *args, **kw):
243         self.question_counter = 0
244         super(Exercise, self).__init__(*args, **kw)
245
246     handle_opis = tag('div', 'description')
247
248     def handle_rozw_kom(self, element):
249         return u"""<div style="display:none" class="comment">""", u"""</div>"""
250
251     def handle_cwiczenie(self, element):
252         self.options = {'exercise': element.attrib['typ']}
253         self.question_counter = 0
254         self.piece_counter = 0
255
256         pre = u"""
257 <div class="exercise %(typ)s" data-type="%(typ)s">
258 <form action="#" method="POST">
259 """ % element.attrib
260         post = u"""
261 <div class="buttons">
262 <span class="message"></span>
263 <input type="button" class="check" value="sprawdź"/>
264 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
265 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
266 <input type="button" class="reset" value="reset"/>
267 </div>
268 </form>
269 </div>
270 """
271         # Add a single <pytanie> tag if it's not there
272         if not element.xpath(".//pytanie"):
273             qpre, qpost = self.handle_pytanie(element)
274             pre = pre + qpre
275             post = qpost + post
276         return pre, post
277
278     def handle_pytanie(self, element):
279         """This will handle <cwiczenie> element, when there is no <pytanie>
280         """
281         add_class = ""
282         self.question_counter += 1
283         self.piece_counter = 0
284         solution = element.attrib.get('rozw', None)
285         if solution: solution_s = ' data-solution="%s"' % solution
286         else: solution_s = ''
287
288         handles = element.attrib.get('uchwyty', None)
289         if handles:
290             add_class += ' handles handles-%s' % handles
291             self.options = {'handles': handles}
292
293         minimum = element.attrib.get('min', None)
294         if minimum: minimum_s = ' data-minimum="%d"' % int(minimum)
295         else: minimum_s = ''
296
297         return '<div class="question%s" data-no="%d" %s>' %\
298             (add_class, self.question_counter, solution_s + minimum_s), \
299             "</div>"
300
301
302 class Wybor(Exercise):
303     def handle_cwiczenie(self, element):
304         pre, post = super(Wybor, self).handle_cwiczenie(element)
305         is_single_choice = True
306         pytania = element.xpath(".//pytanie")
307         if not pytania:
308             pytania = [element]
309         for p in pytania:
310             solutions = re.split(r"[, ]+", p.attrib['rozw'])
311             if len(solutions) != 1:
312                 is_single_choice = False
313                 break
314             choices = p.xpath(".//*[@nazwa]")
315             uniq = set()
316             for n in choices: uniq.add(n.attrib['nazwa'])
317             if len(choices) != len(uniq):
318                 is_single_choice = False
319                 break
320
321         self.options = {'single': is_single_choice}
322         return pre, post
323
324     def handle_punkt(self, element):
325         if self.options['exercise'] and element.attrib.get('nazwa', None):
326             qc = self.question_counter
327             self.piece_counter += 1
328             no = self.piece_counter
329             eid = "q%(qc)d_%(no)d" % locals()
330             aname = element.attrib.get('nazwa', None)
331             if self.options['single']:
332                 return u"""
333 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
334 <input type="radio" name="q%(qc)d" id="%(eid)s" value="%(aname)s" />
335 <label for="%(eid)s">
336                 """ % locals(), u"</label></li>"
337             else:
338                 return u"""
339 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
340 <input type="checkbox" name="%(eid)s" id="%(eid)s" />
341 <label for="%(eid)s">
342 """ % locals(), u"</label></li>"
343
344         else:
345             return super(Wybor, self).handle_punkt(element)
346
347
348 class Uporzadkuj(Exercise):
349     def handle_pytanie(self, element):
350         """
351 Overrides the returned content default handle_pytanie
352         """
353         # we ignore the result, returning our own
354         super(Uporzadkuj, self).handle_pytanie(element)
355         order_items = element.xpath(".//punkt/@rozw")
356
357         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
358             (','.join(order_items), self.question_counter), \
359             u"""</div>"""
360
361     def handle_punkt(self, element):
362         return """<li class="question-piece" data-pos="%(rozw)s"/>""" \
363             % element.attrib,\
364             "</li>"
365
366
367 class Luki(Exercise):
368     def find_pieces(self, question):
369         return question.xpath(".//luka")
370
371     def solution_html(self, piece):
372         sub = EduModule()
373         return sub.generate(piece)
374         # print piece.text
375         # return piece.text + ''.join(
376         #     [etree.tostring(n, encoding=unicode)
377         #      for n in piece])
378
379     def handle_pytanie(self, element):
380         qpre, qpost = super(Luki, self).handle_pytanie(element)
381
382         luki = list(enumerate(self.find_pieces(element)))
383         luki_html = ""
384         i = 0
385         random.shuffle(luki)
386         for (i, luka) in luki:
387             i += 1
388             luka_html = self.solution_html(luka)
389             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
390         self.words_html = '<div class="words">%s</div>' % luki_html
391
392         return qpre, qpost
393
394     def handle_opis(self, element):
395         return '', self.words_html
396
397     def handle_luka(self, element):
398         self.piece_counter += 1
399         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
400
401
402 class Zastap(Luki):
403     def find_pieces(self, question):
404         return question.xpath(".//zastap")
405
406     def solution_html(self, piece):
407         return piece.attrib['rozw']
408
409     def handle_zastap(self, element):
410         self.piece_counter += 1
411         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
412             % self.piece_counter, '</span>'
413
414
415 class Przyporzadkuj(Exercise):
416     def handle_pytanie(self, element):
417         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
418         minimum = element.attrib.get("min", None)
419         if minimum:
420             self.options = {"min": int(minimum)}
421         return pre, post
422
423     def handle_lista(self, lista):
424         if 'nazwa' in lista.attrib:
425             attrs = {
426                 'data-name': lista.attrib['nazwa'],
427                 'class': 'predicate'
428             }
429             self.options = {'predicate': True}
430         elif 'cel' in lista.attrib:
431             attrs = {
432                 'data-target': lista.attrib['cel'],
433                 'class': 'subject'
434             }
435             self.options = {'subject': True, 'handles': 'uchwyty' in lista.attrib}
436         else:
437             attrs = {}
438         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
439         return pre, post + '<br class="clr"/>'
440
441     def handle_punkt(self, element):
442         if self.options['subject']:
443             self.piece_counter += 1
444             if self.options['handles']:
445                 return '<li><span data-solution="%s" data-no="%s" class="question-piece draggable handle add-li">%s</span>' % (element.attrib['rozw'], self.piece_counter, self.piece_counter), '</li>'
446             else:
447                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
448
449         elif self.options['predicate']:
450             if self.options['min']:
451                 placeholders = u'<li class="placeholder"/>' * self.options['min']
452             else:
453                 placeholders = u'<li class="placeholder multiple"/>'
454             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul class="subjects">' + placeholders + '</ul></li>'
455
456         else:
457             return super(Przyporzadkuj, self).handle_punkt(element)
458
459
460 class PrawdaFalsz(Exercise):
461     def handle_punkt(self, element):
462         if 'rozw' in element.attrib:
463             return u'''<li data-solution="%s" class="question-piece">
464             <span class="buttons">
465             <a href="#" data-value="true" class="true">Prawda</a>
466             <a href="#" data-value="false" class="false">Fałsz</a>
467         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
468         else:
469             return super(PrawdaFalsz, self).handle_punkt(element)
470
471
472 class EduModuleFormat(Format):
473     DEFAULT_MATERIAL_FORMAT = 'odt'
474
475     class MaterialNotFound(BaseException):
476         pass
477
478     def __init__(self, wldoc, **kwargs):
479         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
480
481     def build(self):
482         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self})
483
484         html = edumod.generate(self.wldoc.edoc.getroot())
485
486         return IOFile.from_string(html.encode('utf-8'))
487
488     def url_for_material(self, slug, fmt=None):
489         if fmt is None:
490             fmt = self.DEFAULT_MATERIAL_FORMAT
491         # No briliant idea for an API here.
492         if fmt:
493             return "%s.%s" % (slug, fmt)
494         return slug
495
496
497 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
498     """Transforms the WL document to XHTML.
499
500     If output_filename is None, returns an XML,
501     otherwise returns True if file has been written,False if it hasn't.
502     File won't be written if it has no content.
503     """
504     edumodfor = EduModuleFormat(wldoc)
505     return edumodfor.build()