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