pep8 etc.
[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, Format
8 from xmlutils import Xmill, tag, tagged, ifoption, tag_open_close
9 from librarian import functions
10 import re
11 import random
12 from copy import deepcopy
13
14 IMAGE_THUMB_WIDTH = 300
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.activity_last = None
22         self.exercise_counter = 0
23
24         # text filters
25         def swap_endlines(txt):
26             if self.options['strofa']:
27                 txt = txt.replace("/\n", "<br/>\n")
28             return txt
29         self.register_text_filter(functions.substitute_entities)
30         self.register_escaped_text_filter(swap_endlines)
31
32     @tagged('div', 'stanza')
33     def handle_strofa(self, element):
34         self.options = {'strofa': True}
35         return "", ""
36
37     def handle_powiesc(self, element):
38         return u"""
39 <div class="module" id="book-text">
40 <!-- <span class="teacher-toggle">
41   <input type="checkbox" name="teacher-toggle" id="teacher-toggle"/>
42   <label for="teacher-toggle">Pokaż treść dla nauczyciela</label>
43  </span>-->
44
45 """, u"</div>"
46
47     handle_autor_utworu = tag("span", "author")
48     handle_dzielo_nadrzedne = tag("span", "collection")
49     handle_podtytul = tag("span", "subtitle")
50     handle_naglowek_akt = handle_naglowek_czesc = handle_srodtytul = tag("h2")
51     handle_naglowek_scena = tag('h2')
52     handle_naglowek_osoba = tag('h3')
53     handle_akap = handle_akap_dialog = handle_akap_cd = tag('p', 'paragraph')
54
55     handle_wyroznienie = tag('em')
56     handle_tytul_dziela = tag('em', 'title')
57     handle_slowo_obce = tag('em', 'foreign')
58
59     def naglowek_to_anchor(self, naglowek):
60         return self.options['urlmapper'].naglowek_to_anchor(naglowek)
61
62     def handle_nazwa_utworu(self, element):
63         toc = []
64         for naglowek in element.getparent().findall('.//naglowek_rozdzial'):
65             a = etree.Element("a")
66             a.attrib["href"] = "#" + self.naglowek_to_anchor(naglowek)
67             a.text = naglowek.text
68             atxt = etree.tostring(a, encoding=unicode)
69             toc.append("<li>%s</li>" % atxt)
70         toc = "<ul class='toc'>%s</ul>" % "".join(toc)
71         add_header = "Lekcja: " if self.options['wldoc'].book_info.type in ('course', 'synthetic') else ''
72         return "<h1 class='title' id='top'>%s" % add_header, "</h1>" + toc
73
74     def handle_naglowek_rozdzial(self, element):
75         return_to_top = u"<a href='#top' class='top-link'>wróć do spisu treści</a>"
76         pre, post = tag_open_close("h2", id=self.naglowek_to_anchor(element))
77         url = self.options['urlmapper'].get_help_url(element)
78         if url:
79             post = " <a class='help' href='%s'>?</a>" % (url,) + post
80         return return_to_top + pre, post
81
82     def handle_naglowek_podrozdzial(self, element):
83         self.activity_counter = 0
84         return tag('h3')(self, element)
85
86     def handle_uwaga(self, _e):
87         return None
88
89     def handle_aktywnosc(self, element):
90         self.activity_counter += 1
91         self.options = {
92             'activity': True,
93             'activity_counter': self.activity_counter,
94             }
95         submill = EduModule(dict(self.options.items() + {'sub_gen': True}.items()))
96
97         if element.xpath('opis'):
98             opis = submill.generate(element.xpath('opis')[0])
99         else:
100             opis = ''
101
102         n = element.xpath('wskazowki')
103         if n:
104             wskazowki = submill.generate(n[0])
105         else:
106             wskazowki = ''
107         n = element.xpath('pomoce')
108
109         if n:
110             pomoce = submill.generate(n[0])
111         else:
112             pomoce = ''
113
114         forma = ''.join(element.xpath('forma/text()'))
115         get_forma_url = self.options['urlmapper'].get_forma_url
116         forms = []
117         for form_name in forma.split(','):
118             name = form_name.strip()
119             url = get_forma_url(name)
120             if url:
121                 forms.append("<a href='%s'>%s</a>" % (url, name))
122             else:
123                 forms.append(name)
124         forma = ', '.join(forms)
125         if forma:
126             forma = '<section class="infobox kind"><h1>Metoda</h1><p>%s</p></section>' % forma
127
128         czas = ''.join(element.xpath('czas/text()'))
129         if czas:
130             czas = '<section class="infobox time"><h1>Czas</h1><p>%s min</p></section>' % czas
131
132         counter = self.activity_counter
133
134         if element.getnext().tag == 'aktywnosc' or self.activity_last.getnext() == element:
135             counter_html = """<span class="act_counter">%(counter)d.</span>""" % {'counter': counter}
136         else:
137             counter_html = ''
138
139         self.activity_last = element
140
141         return (
142             u"""
143 <div class="activity">
144   <div class="text">
145     %(counter_html)s
146     %(opis)s""" % {'counter_html': counter_html, 'opis': opis},
147             u"""%(wskazowki)s
148   </div>
149   <aside class="info">
150     %(czas)s
151     %(forma)s
152     %(pomoce)s
153   </aside>
154   <div class="clearboth"></div>
155 </div>
156 """ % {'wskazowki': wskazowki, 'czas': czas, 'forma': forms, 'pomoce': pomoce})
157
158     handle_opis = ifoption(sub_gen=True)(tag('div', 'description'))
159     handle_wskazowki = ifoption(sub_gen=True)(tag('div', ('hints', 'teacher')))
160
161     @ifoption(sub_gen=True)
162     @tagged('section', 'infobox materials')
163     def handle_pomoce(self, _):
164         return """<h1>Pomoce</h1>""", ""
165
166     def handle_czas(self, *_):
167         return
168
169     def handle_forma(self, *_):
170         return
171
172     def handle_cwiczenie(self, element):
173         exercise_handlers = {
174             'wybor': Wybor,
175             'uporzadkuj': Uporzadkuj,
176             'luki': Luki,
177             'zastap': Zastap,
178             'przyporzadkuj': Przyporzadkuj,
179             'prawdafalsz': PrawdaFalsz
180             }
181
182         typ = element.attrib['typ']
183         self.exercise_counter += 1
184         self.options = {'exercise_counter': self.exercise_counter}
185         handler = exercise_handlers[typ](self.options)
186         return handler.generate(element)
187
188     # Lists
189     def handle_lista(self, element, attrs=None):
190         if attrs is None:
191             attrs = {}
192         ltype = element.attrib.get('typ', 'punkt')
193         if not element.findall("punkt"):
194             if ltype == 'czytelnia':
195                 return '<p>W przygotowaniu.</p>'
196             else:
197                 return None
198         if ltype == 'slowniczek':
199             surl = element.attrib.get('src', None)
200             if surl is None:
201                 # print '** missing src on <slowniczek>, setting default'
202                 surl = 'http://edukacjamedialna.edu.pl/lekcje/slowniczek/'
203             sxml = etree.fromstring(self.options['provider'].by_uri(surl).get_string())
204
205             self.options = {'slowniczek': True, 'slowniczek_xml': sxml}
206             pre, post = '<div class="slowniczek">', '</div>'
207             if not self.options['wldoc'].book_info.url.slug.startswith('slowniczek'):
208                 post += u'<p class="see-more"><a href="%s">Zobacz cały słowniczek.</a></p>' % surl
209             return pre, post
210
211         listtag = {
212             'num': 'ol',
213             'punkt': 'ul',
214             'alfa': 'ul',
215             'czytelnia': 'ul'}[ltype]
216
217         classes = attrs.get('class', '')
218         if classes:
219             del attrs['class']
220
221         attrs_s = ' '.join(['%s="%s"' % kv for kv in attrs.items()])
222         if attrs_s:
223             attrs_s = ' ' + attrs_s
224
225         return '<%s class="lista %s %s"%s>' % (listtag, ltype, classes, attrs_s), '</%s>' % listtag
226
227     def handle_punkt(self, element):
228         if self.options['slowniczek']:
229             return '<dl>', '</dl>'
230         else:
231             return '<li>', '</li>'
232
233     def handle_definiendum(self, element):
234         nxt = element.getnext()
235         definiens_s = ''
236
237         if not element.text:
238             print "!! Empty <definiendum/>"
239             return None
240
241         # let's pull definiens from another document
242         if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
243             sxml = self.options['slowniczek_xml']
244             if "'" in (element.text or ''):
245                 defloc = sxml.xpath("//definiendum[text()=\"%s\"]" % (element.text or '').strip())
246             else:
247                 defloc = sxml.xpath("//definiendum[text()='%s']" % (element.text or '').strip())
248             if defloc:
249                 definiens = defloc[0].getnext()
250                 if definiens.tag == 'definiens':
251                     subgen = EduModule(self.options)
252                     definiens_s = subgen.generate(definiens)
253             else:
254                 print "!! Missing definiendum in source: '%s'" % element.text
255
256         return u"<dt id='%s'>" % self.naglowek_to_anchor(element), u"</dt>" + definiens_s
257
258     def handle_definiens(self, element):
259         return u"<dd>", u"</dd>"
260
261     def handle_podpis(self, element):
262         return u"""<div class="caption">""", u"</div>"
263
264     def handle_tabela(self, element):
265         has_frames = int(element.attrib.get("ramki", "0"))
266         frames_c = "framed" if has_frames else ""
267         return u"""<table class="%s">""" % frames_c, u"</table>"
268
269     def handle_wiersz(self, element):
270         return u"<tr>", u"</tr>"
271
272     def handle_kol(self, element):
273         return u"<td>", u"</td>"
274
275     def handle_rdf__RDF(self, _):
276         # ustal w opcjach  rzeczy :D
277         return
278
279     def handle_link(self, element):
280         if 'url' in element.attrib:
281             return tag('a', href=element.attrib['url'])(self, element)
282         elif 'material' in element.attrib:
283             material_err = u' [BRAKUJĄCY MATERIAŁ]'
284             slug = element.attrib['material']
285
286             def make_url(f):
287                 return self.options['urlmapper'].url_for_material(slug, f)
288
289             formats = self.options['urlmapper'].materials(slug)
290
291             try:
292                 def_href = make_url(formats[0][0])
293                 def_err = u""
294             except (IndexError, self.options['urlmapper'].MaterialNotFound):
295                 def_err = material_err
296                 def_href = u""
297             fmt_links = []
298             for f in formats[1:]:
299                 try:
300                     fmt_links.append(u'<a href="%s">%s</a>' % (make_url(f[0]), f[0].upper()))
301                 except self.options['urlmapper'].MaterialNotFound:
302                     fmt_links.append(u'<a>%s%s</a>' % (f[0].upper(), material_err))
303             more_links = u' (%s)' % u', '.join(fmt_links) if fmt_links else u''
304
305             return u"<a href='%s'>" % def_href, u'%s</a>%s' % (def_err, more_links)
306
307     def handle_obraz(self, element):
308         name = element.attrib.get('nazwa', '').strip()
309         if not name:
310             print '!! <obraz> missing "nazwa"'
311             return
312         alt = element.attrib.get('alt', '')
313         if not alt:
314             print '** <obraz> missing "alt"'
315         slug, ext = name.rsplit('.', 1)
316         url = self.options['urlmapper'].url_for_image(slug, ext)
317         thumb_url = self.options['urlmapper'].url_for_image(slug, ext, IMAGE_THUMB_WIDTH)
318         e = etree.Element("a", attrib={"href": url, "class": "image"})
319         e.append(etree.Element("img", attrib={
320             "src": thumb_url,
321             "alt": alt,
322             "width": str(IMAGE_THUMB_WIDTH)}))
323         return etree.tostring(e, encoding=unicode), u""
324
325     def handle_video(self, element):
326         url = element.attrib.get('url')
327         if not url:
328             print '!! <video> missing url'
329             return
330         m = re.match(r'(?:https?://)?(?:www.)?youtube.com/watch\?(?:.*&)?v=([^&]+)(?:$|&)', url)
331         if not m:
332             print '!! unknown <video> url scheme:', url
333             return
334         return """<iframe width="630" height="384" src="http://www.youtube.com/embed/%s"
335             frameborder="0" allowfullscreen></iframe>""" % m.group(1), ""
336
337
338 class Exercise(EduModule):
339     INSTRUCTION = ""
340
341     def __init__(self, *args, **kw):
342         self.question_counter = 0
343         super(Exercise, self).__init__(*args, **kw)
344         self.instruction_printed = False
345         self.piece_counter = None
346
347     @tagged('div', 'description')
348     def handle_opis(self, element):
349         return "", self.get_instruction()
350
351     def handle_rozw_kom(self, element):
352         return u"""<div style="display:none" class="comment">""", u"""</div>"""
353
354     def handle_cwiczenie(self, element):
355         self.options = {'exercise': element.attrib['typ']}
356         self.question_counter = 0
357         self.piece_counter = 0
358
359         pre = u"""
360 <div class="exercise %(typ)s" data-type="%(typ)s">
361 <form action="#" method="POST">
362 <h3>Zadanie %(exercies_counter)d</h3>
363 <div class="buttons">
364 <span class="message"></span>
365 <input type="button" class="check" value="sprawdź"/>
366 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
367 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
368 <input type="button" class="reset" value="reset"/>
369 </div>
370
371 """ % {'exercies_counter': self.options['exercise_counter'], 'typ': element.attrib['typ']}
372         post = u"""
373 <div class="buttons">
374 <span class="message"></span>
375 <input type="button" class="check" value="sprawdź"/>
376 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
377 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
378 <input type="button" class="reset" value="reset"/>
379 </div>
380 </form>
381 </div>
382 """
383         # Add a single <pytanie> tag if it's not there
384         if not element.xpath(".//pytanie"):
385             qpre, qpost = self.handle_pytanie(element)
386             pre += qpre
387             post = qpost + post
388         return pre, post
389
390     def handle_pytanie(self, element):
391         """This will handle <cwiczenie> element, when there is no <pytanie>
392         """
393         add_class = ""
394         self.question_counter += 1
395         self.piece_counter = 0
396         solution = element.attrib.get('rozw', None)
397         solution_s = ' data-solution="%s"' % solution if solution else ''
398
399         handles = element.attrib.get('uchwyty', None)
400         if handles:
401             add_class += ' handles handles-%s' % handles
402             self.options = {'handles': handles}
403
404         minimum = element.attrib.get('min', None)
405         minimum_s = ' data-minimum="%d"' % int(minimum) if minimum else ''
406
407         return '<div class="question%s" data-no="%d" %s>' %\
408             (add_class, self.question_counter, solution_s + minimum_s), \
409             "</div>"
410
411     def get_instruction(self):
412         if not self.instruction_printed:
413             self.instruction_printed = True
414             if self.INSTRUCTION:
415                 return u'<span class="instruction">%s</span>' % self.INSTRUCTION
416             else:
417                 return ""
418         else:
419             return ""
420
421
422 class Wybor(Exercise):
423     def handle_cwiczenie(self, element):
424         pre, post = super(Wybor, self).handle_cwiczenie(element)
425         is_single_choice = True
426         pytania = element.xpath(".//pytanie")
427         if not pytania:
428             pytania = [element]
429         for p in pytania:
430             solutions = re.split(r"[, ]+", p.attrib.get('rozw', ''))
431             if len(solutions) != 1:
432                 is_single_choice = False
433                 break
434             choices = p.xpath(".//*[@nazwa]")
435             uniq = set()
436             for n in choices:
437                 uniq.add(n.attrib.get('nazwa', ''))
438             if len(choices) != len(uniq):
439                 is_single_choice = False
440                 break
441
442         self.options = {'single': is_single_choice}
443         return pre, post
444
445     def handle_punkt(self, element):
446         if self.options['exercise'] and element.attrib.get('nazwa', None):
447             qc = self.question_counter
448             self.piece_counter += 1
449             no = self.piece_counter
450             eid = "q%(qc)d_%(no)d" % locals()
451             aname = element.attrib.get('nazwa', None)
452             if self.options['single']:
453                 return u"""
454 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
455 <input type="radio" name="q%(qc)d" id="%(eid)s" value="%(aname)s" />
456 <label for="%(eid)s">
457                 """ % locals(), u"</label></li>"
458             else:
459                 return u"""
460 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-name="%(aname)s">
461 <input type="checkbox" name="%(eid)s" id="%(eid)s" />
462 <label for="%(eid)s">
463 """ % locals(), u"</label></li>"
464
465         else:
466             return super(Wybor, self).handle_punkt(element)
467
468
469 class Uporzadkuj(Exercise):
470     INSTRUCTION = u"Kliknij wybraną odpowiedź i przeciągnij w nowe miejsce."
471
472     def handle_pytanie(self, element):
473         """
474 Overrides the returned content default handle_pytanie
475         """
476         # we ignore the result, returning our own
477         super(Uporzadkuj, self).handle_pytanie(element)
478         order_items = element.xpath(".//punkt/@rozw")
479
480         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
481             (','.join(order_items), self.question_counter), \
482             u"""</div>"""
483
484     def handle_punkt(self, element):
485         return """<li class="question-piece" data-pos="%(rozw)s">""" \
486             % element.attrib,\
487             "</li>"
488
489
490 class Luki(Exercise):
491     INSTRUCTION = u"Przeciągnij odpowiedzi i upuść w wybranym polu."
492
493     def find_pieces(self, question):
494         return question.xpath(".//luka")
495
496     def solution_html(self, piece):
497         piece = deepcopy(piece)
498         piece.tail = None
499         sub = EduModule()
500         return sub.generate(piece)
501
502     def handle_pytanie(self, element):
503         qpre, qpost = super(Luki, self).handle_pytanie(element)
504
505         luki = list(enumerate(self.find_pieces(element)))
506         luki_html = ""
507         i = 0
508         random.shuffle(luki)
509         for (i, luka) in luki:
510             i += 1
511             luka_html = self.solution_html(luka)
512             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
513         self.words_html = '<div class="words">%s</div>' % luki_html
514
515         return qpre, qpost
516
517     def handle_opis(self, element):
518         return '', self.words_html
519
520     def handle_luka(self, element):
521         self.piece_counter += 1
522         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
523
524
525 class Zastap(Luki):
526     INSTRUCTION = u"Przeciągnij odpowiedzi i upuść je na słowie lub wyrażeniu, które chcesz zastąpić."
527
528     def find_pieces(self, question):
529         return question.xpath(".//zastap")
530
531     def solution_html(self, piece):
532         return piece.attrib.get('rozw', '')
533
534     def handle_zastap(self, element):
535         self.piece_counter += 1
536         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
537             % self.piece_counter, '</span>'
538
539
540 class Przyporzadkuj(Exercise):
541     INSTRUCTION = [u"Przeciągnij odpowiedzi i upuść w wybranym polu.",
542                    u"Kliknij numer odpowiedzi, przeciągnij i upuść w wybranym polu."]
543
544     def get_instruction(self):
545         if not self.instruction_printed:
546             self.instruction_printed = True
547             return u'<span class="instruction">%s</span>' % self.INSTRUCTION[self.options['handles'] and 1 or 0]
548         else:
549             return ""
550
551     def handle_cwiczenie(self, element):
552         pre, post = super(Przyporzadkuj, self).handle_cwiczenie(element)
553         lista_with_handles = element.xpath(".//*[@uchwyty]")
554         if lista_with_handles:
555             self.options = {'handles': True}
556         return pre, post
557
558     def handle_pytanie(self, element):
559         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
560         minimum = element.attrib.get("min", None)
561         if minimum:
562             self.options = {"min": int(minimum)}
563         return pre, post
564
565     def handle_lista(self, lista):
566         if 'nazwa' in lista.attrib:
567             attrs = {
568                 'data-name': lista.attrib['nazwa'],
569                 'class': 'predicate'
570             }
571             self.options = {'predicate': True}
572         elif 'cel' in lista.attrib:
573             attrs = {
574                 'data-target': lista.attrib['cel'],
575                 'class': 'subject'
576             }
577             if lista.attrib.get('krotkie'):
578                 self.options = {'short': True}
579             self.options = {'subject': True}
580         else:
581             attrs = {}
582         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
583         return pre, post + '<br class="clr"/>'
584
585     def handle_punkt(self, element):
586         if self.options['subject']:
587             self.piece_counter += 1
588             if self.options['handles']:
589                 return (
590                     '<li><span data-solution="%s" data-no="%s" '
591                     'class="question-piece draggable handle add-li">%s</span>' % (
592                         element.attrib.get('rozw', ''),
593                         self.piece_counter,
594                         self.piece_counter),
595                     '</li>')
596             else:
597                 extra_class = ""
598                 if self.options['short']:
599                     extra_class += ' short'
600                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable%s">' % (
601                     element.attrib.get('rozw', ''),
602                     self.piece_counter, extra_class), '</li>'
603
604         elif self.options['predicate']:
605             if self.options['min']:
606                 placeholders = u'<li class="placeholder"></li>' * self.options['min']
607             else:
608                 placeholders = u'<li class="placeholder multiple"></li>'
609             return (
610                 '<li data-predicate="%s">' % element.attrib.get('nazwa', ''),
611                 '<ul class="subjects">' + placeholders + '</ul></li>')
612
613         else:
614             return super(Przyporzadkuj, self).handle_punkt(element)
615
616
617 class PrawdaFalsz(Exercise):
618     def handle_punkt(self, element):
619         if 'rozw' in element.attrib:
620             return u'''<li data-solution="%s" class="question-piece">
621             <span class="buttons">
622             <a href="#" data-value="true" class="true">Prawda</a>
623             <a href="#" data-value="false" class="false">Fałsz</a>
624         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
625         else:
626             return super(PrawdaFalsz, self).handle_punkt(element)
627
628
629 class EduModuleFormat(Format):
630     PRIMARY_MATERIAL_FORMATS = ('pdf', 'odt')
631
632     class MaterialNotFound(BaseException):
633         pass
634
635     def __init__(self, wldoc, **kwargs):
636         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
637         self.materials_by_slug = None
638
639     def build(self):
640         # Sort materials by slug.
641         self.materials_by_slug = {}
642         for name, att in self.wldoc.source.attachments.items():
643             parts = name.rsplit('.', 1)
644             if len(parts) == 1:
645                 continue
646             slug, ext = parts
647             if slug not in self.materials_by_slug:
648                 self.materials_by_slug[slug] = {}
649             self.materials_by_slug[slug][ext] = att
650
651         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self, 'wldoc': self.wldoc})
652
653         html = edumod.generate(self.wldoc.edoc.getroot())
654
655         return IOFile.from_string(html.encode('utf-8'))
656
657     def materials(self, slug):
658         """Returns a list of pairs: (ext, iofile)."""
659         order = {pmf: i for (i, pmf) in enumerate(self.PRIMARY_MATERIAL_FORMATS)}
660         mats = self.materials_by_slug.get(slug, {}).items()
661         if not mats:
662             print "!! Material missing: '%s'" % slug
663         return sorted(mats, key=lambda (x, y): order.get(x, x))
664
665     def url_for_material(self, slug, fmt):
666         return "%s.%s" % (slug, fmt)
667
668     # WTF: tutaj był błąd, ale nikomu to nie przeszkadzało?
669     def url_for_image(self, slug, fmt, width=None):
670         return self.url_for_material(slug, fmt)
671
672     def text_to_anchor(self, text):
673         return re.sub(r" +", " ", text)
674
675     def naglowek_to_anchor(self, naglowek):
676         return self.text_to_anchor(naglowek.text.strip())
677
678     def get_forma_url(self, forma):
679         return None
680
681     def get_help_url(self, naglowek):
682         return None
683
684
685 def transform(wldoc, stylesheet='edumed', options=None, flags=None, verbose=None):
686     """Transforms the WL document to XHTML.
687
688     If output_filename is None, returns an XML,
689     otherwise returns True if file has been written,False if it hasn't.
690     File won't be written if it has no content.
691     """
692     edumodfor = EduModuleFormat(wldoc)
693     return edumodfor.build()