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