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