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