add emphasis tags
[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['format'])
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     def handle_rozw_kom(self, element):
200         return u"""<div style="display:none" class="comment">""", u"""</div>"""
201
202     def handle_cwiczenie(self, element):
203         self.options = {'exercise': element.attrib['typ']}
204         self.question_counter = 0
205         self.piece_counter = 0
206
207         pre = u"""
208 <div class="exercise %(typ)s" data-type="%(typ)s">
209 <form action="#" method="POST">
210 """ % element.attrib
211         post = u"""
212 <div class="buttons">
213 <span class="message"></span>
214 <input type="button" class="check" value="sprawdź"/>
215 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
216 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
217 <input type="button" class="reset" value="reset"/>
218 </div>
219 </form>
220 </div>
221 """
222         # Add a single <pytanie> tag if it's not there
223         if not element.xpath(".//pytanie"):
224             qpre, qpost = self.handle_pytanie(element)
225             pre = pre + qpre
226             post = qpost + post
227         return pre, post
228
229     def handle_pytanie(self, element):
230         """This will handle <cwiczenie> element, when there is no <pytanie>
231         """
232         add_class = ""
233         self.question_counter += 1
234         self.piece_counter = 0
235         solution = element.attrib.get('rozw', None)
236         if solution: solution_s = ' data-solution="%s"' % solution
237         else: solution_s = ''
238
239         handles = element.attrib.get('uchwyty', None)
240         if handles:
241             add_class += ' handles handles-%s' % handles
242             self.options = {'handles': handles}
243
244         minimum = element.attrib.get('min', None)
245         if minimum: minimum_s = ' data-minimum="%d"' % int(minimum)
246         else: minimum_s = ''
247
248         return '<div class="question%s" data-no="%d" %s>' %\
249             (add_class, self.question_counter, solution_s + minimum_s), \
250             "</div>"
251
252
253 class Wybor(Exercise):
254     def handle_cwiczenie(self, element):
255         pre, post = super(Wybor, self).handle_cwiczenie(element)
256         is_single_choice = True
257         for p in element.xpath(".//pytanie"):
258             solutions = re.split(r"[, ]+", p.attrib['rozw'])
259             if len(solutions) != 1:
260                 is_single_choice = False
261                 break
262         self.options = {'single': is_single_choice}
263         return pre, post
264
265     def handle_punkt(self, element):
266         if self.options['exercise'] and element.attrib.get('nazwa', None):
267             qc = self.question_counter
268             self.piece_counter += 1
269             no = self.piece_counter
270             eid = "q%(qc)d_%(no)d" % locals()
271             aname = element.attrib.get('nazwa', None)
272             if self.options['single']:
273                 return u"""
274 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
275 <input type="radio" name="q%(qc)d" id="%(eid)s" value="%(aname)s" />
276 <label for="%(eid)s">
277                 """ % locals(), u"</label></li>"
278             else:
279                 return u"""
280 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
281 <input type="checkbox" name="%(eid)s" id="%(eid)s" />
282 <label for="%(eid)s">
283 """ % locals(), u"</label></li>"
284
285         else:
286             return super(Wybor, self).handle_punkt(element)
287
288
289 class Uporzadkuj(Exercise):
290     def handle_pytanie(self, element):
291         """
292 Overrides the returned content default handle_pytanie
293         """
294         # we ignore the result, returning our own
295         super(Uporzadkuj, self).handle_pytanie(element)
296         order_items = element.xpath(".//punkt/@rozw")
297
298         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
299             (','.join(order_items), self.question_counter), \
300             u"""</div>"""
301
302     def handle_punkt(self, element):
303         return """<li class="question-piece" data-pos="%(rozw)s"/>""" \
304             % element.attrib,\
305             "</li>"
306
307
308 class Luki(Exercise):
309     def find_pieces(self, question):
310         return question.xpath(".//luka")
311
312     def solution_html(self, piece):
313         return piece.text + ''.join(
314             [etree.tostring(n, encoding=unicode)
315              for n in piece])
316
317     def handle_pytanie(self, element):
318         qpre, qpost = super(Luki, self).handle_pytanie(element)
319
320         luki = list(enumerate(self.find_pieces(element)))
321         luki_html = ""
322         i = 0
323         random.shuffle(luki)
324         for (i, luka) in luki:
325             i += 1
326             luka_html = self.solution_html(luka)
327             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
328         self.words_html = '<div class="words">%s</div>' % luki_html
329
330         return qpre, qpost
331
332     def handle_opis(self, element):
333         return '', self.words_html
334
335     def handle_luka(self, element):
336         self.piece_counter += 1
337         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
338
339
340 class Zastap(Luki):
341     def find_pieces(self, question):
342         return question.xpath(".//zastap")
343
344     def solution_html(self, piece):
345         return piece.attrib['rozw']
346
347     def handle_zastap(self, element):
348         self.piece_counter += 1
349         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
350             % self.piece_counter, '</span>'
351
352
353 class Przyporzadkuj(Exercise):
354     def handle_pytanie(self, element):
355         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
356         minimum = element.attrib.get("min", None)
357         if minimum:
358             self.options = {"min": int(minimum)}
359         return pre, post
360
361     def handle_lista(self, lista):
362         if 'nazwa' in lista.attrib:
363             attrs = {
364                 'data-name': lista.attrib['nazwa'],
365                 'class': 'predicate'
366             }
367             self.options = {'predicate': True}
368         elif 'cel' in lista.attrib:
369             attrs = {
370                 'data-target': lista.attrib['cel'],
371                 'class': 'subject'
372             }
373             self.options = {'subject': True, 'handles': 'uchwyty' in lista.attrib}
374         else:
375             attrs = {}
376         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
377         return pre, post + '<br class="clr"/>'
378
379     def handle_punkt(self, element):
380         if self.options['subject']:
381             self.piece_counter += 1
382             if self.options['handles']:
383                 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>'
384             else:
385                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable">' % (element.attrib['rozw'], self.piece_counter), '</li>'
386
387         elif self.options['predicate']:
388             if self.options['min']:
389                 placeholders = u'<li class="placeholder"/>' * self.options['min']
390             else:
391                 placeholders = u'<li class="placeholder multiple"/>'
392             return '<li data-predicate="%(nazwa)s">' % element.attrib, '<ul class="subjects">' + placeholders + '</ul></li>'
393
394         else:
395             return super(Przyporzadkuj, self).handle_punkt(element)
396
397
398 class PrawdaFalsz(Exercise):
399     def handle_punkt(self, element):
400         if 'rozw' in element.attrib:
401             return u'''<li data-solution="%s" class="question-piece">
402             <span class="buttons">
403             <a href="#" data-value="true" class="true">Prawda</a>
404             <a href="#" data-value="false" class="false">Fałsz</a>
405         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
406         else:
407             return super(PrawdaFalsz, self).handle_punkt(element)
408
409
410 class EduModuleFormat(Format):
411     def __init__(self, wldoc, **kwargs):
412         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
413
414     def build(self):
415         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self})
416
417         html = edumod.generate(self.wldoc.edoc.getroot())
418
419         return IOFile.from_string(html.encode('utf-8'))
420
421     def url_for_material(self, slug, fmt=None):
422         # No briliant idea for an API here.
423         if fmt:
424             return "%s.%s" % (slug, fmt)
425         return slug
426
427
428 def transform(wldoc, stylesheet='edumed', options=None, flags=None):
429     """Transforms the WL document to XHTML.
430
431     If output_filename is None, returns an XML,
432     otherwise returns True if file has been written,False if it hasn't.
433     File won't be written if it has no content.
434     """
435     edumodfor = EduModuleFormat(wldoc)
436     return edumodfor.build()