add option to save html dir (debug html to pdf)
[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 (len(self.activity_last) and 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': forma, '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).encode('utf-8')
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 extra_attributes(self):
355         return {}
356
357     def handle_cwiczenie(self, element):
358         self.options = {'exercise': element.attrib['typ']}
359         self.question_counter = 0
360         self.piece_counter = 0
361
362         extra_attrs = self.extra_attributes()
363
364         pre = u"""
365 <div class="exercise %(typ)s" data-type="%(typ)s"%(extra_attrs)s>
366 <form action="#" method="POST">
367 <h3>Zadanie %(exercies_counter)d</h3>
368 <div class="buttons">
369 <span class="message"></span>
370 <input type="button" class="check" value="sprawdź"/>
371 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
372 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
373 <input type="button" class="reset" value="reset"/>
374 </div>
375
376 """ % {
377             'exercies_counter': self.options['exercise_counter'],
378             'typ': element.attrib['typ'],
379             'extra_attrs': ' ' + ' '.join(
380                 'data-%s="%s"' % item for item in extra_attrs.iteritems()) if extra_attrs else '',
381         }
382         post = u"""
383 <div class="buttons">
384 <span class="message"></span>
385 <input type="button" class="check" value="sprawdź"/>
386 <input type="button" class="retry" style="display:none" value="spróbuj ponownie"/>
387 <input type="button" class="solutions" value="pokaż rozwiązanie"/>
388 <input type="button" class="reset" value="reset"/>
389 </div>
390 </form>
391 </div>
392 """
393         # Add a single <pytanie> tag if it's not there
394         if not element.xpath(".//pytanie"):
395             qpre, qpost = self.handle_pytanie(element)
396             pre += qpre
397             post = qpost + post
398         return pre, post
399
400     def handle_pytanie(self, element):
401         """This will handle <cwiczenie> element, when there is no <pytanie>
402         """
403         add_class = ""
404         self.question_counter += 1
405         self.piece_counter = 0
406         solution = element.attrib.get('rozw', None)
407         solution_s = ' data-solution="%s"' % solution if solution else ''
408
409         handles = element.attrib.get('uchwyty', None)
410         if handles:
411             add_class += ' handles handles-%s' % handles
412             self.options = {'handles': handles}
413
414         minimum = element.attrib.get('min', None)
415         minimum_s = ' data-minimum="%d"' % int(minimum) if minimum else ''
416
417         return '<div class="question%s" data-no="%d" %s>' %\
418             (add_class, self.question_counter, solution_s + minimum_s), \
419             "</div>"
420
421     def get_instruction(self):
422         if not self.instruction_printed:
423             self.instruction_printed = True
424             if self.INSTRUCTION:
425                 return u'<span class="instruction">%s</span>' % self.INSTRUCTION
426             else:
427                 return ""
428         else:
429             return ""
430
431
432 class Wybor(Exercise):
433     def extra_attributes(self):
434         return {'subtype': 'single' if self.options['single'] else 'multiple'}
435
436     def handle_cwiczenie(self, element):
437         is_single_choice = True
438         pytania = element.xpath(".//pytanie")
439         if not pytania:
440             pytania = [element]
441         for p in pytania:
442             solutions = p.xpath(".//punkt[@rozw='prawda']")
443             if len(solutions) != 1:
444                 is_single_choice = False
445                 break
446
447         self.options = {'single': is_single_choice}
448         return super(Wybor, self).handle_cwiczenie(element)
449
450     def handle_punkt(self, element):
451         if self.options['exercise'] and element.attrib.get('rozw', None):
452             qc = self.question_counter
453             self.piece_counter += 1
454             no = self.piece_counter
455             eid = "q%(qc)d_%(no)d" % locals()
456             sol = element.attrib.get('rozw', None)
457             params = {'qc': qc, 'no': no, 'sol': sol, 'eid': eid}
458             if self.options['single']:
459                 input_tag = u'<input type="radio" name="q%(qc)d" id="%(eid)s" value="%(eid)s" />'
460             else:
461                 input_tag = u'<input type="checkbox" name="%(eid)s" id="%(eid)s" />'
462             return (u"""
463 <li class="question-piece" data-qc="%(qc)d" data-no="%(no)d" data-sol="%(sol)s">
464                 """ + input_tag + u"""
465 <label for="%(eid)s">""") % params, u"</label></li>"
466         else:
467             return super(Wybor, self).handle_punkt(element)
468
469
470 class Uporzadkuj(Exercise):
471     INSTRUCTION = u"Kliknij wybraną odpowiedź i przeciągnij w nowe miejsce."
472
473     def handle_pytanie(self, element):
474         """
475 Overrides the returned content default handle_pytanie
476         """
477         # we ignore the result, returning our own
478         super(Uporzadkuj, self).handle_pytanie(element)
479         order_items = element.xpath(".//punkt/@rozw")
480
481         return u"""<div class="question" data-original="%s" data-no="%s">""" % \
482             (','.join(order_items), self.question_counter), \
483             u"""</div>"""
484
485     def handle_punkt(self, element):
486         return """<li class="question-piece" data-pos="%(rozw)s">""" \
487             % element.attrib,\
488             "</li>"
489
490
491 class Luki(Exercise):
492     INSTRUCTION = u"Przeciągnij odpowiedzi i upuść w wybranym polu."
493
494     def find_pieces(self, question):
495         return question.xpath(".//luka")
496
497     def solution_html(self, piece):
498         piece = deepcopy(piece)
499         piece.tail = None
500         sub = EduModule()
501         return sub.generate(piece)
502
503     def handle_pytanie(self, element):
504         qpre, qpost = super(Luki, self).handle_pytanie(element)
505
506         luki = list(enumerate(self.find_pieces(element)))
507         luki_html = ""
508         i = 0
509         random.shuffle(luki)
510         for (i, luka) in luki:
511             i += 1
512             luka_html = self.solution_html(luka)
513             luki_html += u'<span class="draggable question-piece" data-no="%d">%s</span>' % (i, luka_html)
514         self.words_html = '<div class="words">%s</div>' % luki_html
515
516         return qpre, qpost
517
518     def handle_opis(self, element):
519         return '', self.words_html
520
521     def handle_luka(self, element):
522         self.piece_counter += 1
523         return '<span class="placeholder" data-solution="%d"></span>' % self.piece_counter
524
525
526 class Zastap(Luki):
527     INSTRUCTION = u"Przeciągnij odpowiedzi i upuść je na słowie lub wyrażeniu, które chcesz zastąpić."
528
529     def find_pieces(self, question):
530         return question.xpath(".//zastap")
531
532     def solution_html(self, piece):
533         return piece.attrib.get('rozw', '')
534
535     def handle_zastap(self, element):
536         self.piece_counter += 1
537         return '<span class="placeholder zastap question-piece" data-solution="%d">' \
538             % self.piece_counter, '</span>'
539
540
541 class Przyporzadkuj(Exercise):
542     INSTRUCTION = [u"Przeciągnij odpowiedzi i upuść w wybranym polu.",
543                    u"Kliknij numer odpowiedzi, przeciągnij i upuść w wybranym polu."]
544
545     def get_instruction(self):
546         if not self.instruction_printed:
547             self.instruction_printed = True
548             return u'<span class="instruction">%s</span>' % self.INSTRUCTION[self.options['handles'] and 1 or 0]
549         else:
550             return ""
551
552     def handle_cwiczenie(self, element):
553         pre, post = super(Przyporzadkuj, self).handle_cwiczenie(element)
554         lista_with_handles = element.xpath(".//*[@uchwyty]")
555         if lista_with_handles:
556             self.options = {'handles': True}
557         return pre, post
558
559     def handle_pytanie(self, element):
560         pre, post = super(Przyporzadkuj, self).handle_pytanie(element)
561         minimum = element.attrib.get("min", None)
562         if minimum:
563             self.options = {"min": int(minimum)}
564         return pre, post
565
566     def handle_lista(self, lista):
567         if 'nazwa' in lista.attrib:
568             attrs = {
569                 'data-name': lista.attrib['nazwa'],
570                 'class': 'predicate'
571             }
572             self.options = {'predicate': True}
573         elif 'cel' in lista.attrib:
574             attrs = {
575                 'data-target': lista.attrib['cel'],
576                 'class': 'subject'
577             }
578             if lista.attrib.get('krotkie'):
579                 self.options = {'short': True}
580             self.options = {'subject': True}
581         else:
582             attrs = {}
583         pre, post = super(Przyporzadkuj, self).handle_lista(lista, attrs)
584         return pre, post + '<br class="clr"/>'
585
586     def handle_punkt(self, element):
587         if self.options['subject']:
588             self.piece_counter += 1
589             if self.options['handles']:
590                 return (
591                     '<li><span data-solution="%s" data-no="%s" '
592                     'class="question-piece draggable handle add-li">%s</span>' % (
593                         element.attrib.get('rozw', ''),
594                         self.piece_counter,
595                         self.piece_counter),
596                     '</li>')
597             else:
598                 extra_class = ""
599                 if self.options['short']:
600                     extra_class += ' short'
601                 return '<li data-solution="%s" data-no="%s" class="question-piece draggable%s">' % (
602                     element.attrib.get('rozw', ''),
603                     self.piece_counter, extra_class), '</li>'
604
605         elif self.options['predicate']:
606             if self.options['min']:
607                 placeholders = u'<li class="placeholder"></li>' * self.options['min']
608             else:
609                 placeholders = u'<li class="placeholder multiple"></li>'
610             return (
611                 '<li data-predicate="%s">' % element.attrib.get('nazwa', ''),
612                 '<ul class="subjects">' + placeholders + '</ul></li>')
613
614         else:
615             return super(Przyporzadkuj, self).handle_punkt(element)
616
617
618 class PrawdaFalsz(Exercise):
619     def handle_punkt(self, element):
620         if 'rozw' in element.attrib:
621             return u'''<li data-solution="%s" class="question-piece">
622             <span class="buttons">
623             <a href="#" data-value="true" class="true">Prawda</a>
624             <a href="#" data-value="false" class="false">Fałsz</a>
625         </span>''' % {'prawda': 'true', 'falsz': 'false'}[element.attrib['rozw']], '</li>'
626         else:
627             return super(PrawdaFalsz, self).handle_punkt(element)
628
629
630 class EduModuleFormat(Format):
631     PRIMARY_MATERIAL_FORMATS = ('pdf', 'odt')
632
633     class MaterialNotFound(BaseException):
634         pass
635
636     def __init__(self, wldoc, **kwargs):
637         super(EduModuleFormat, self).__init__(wldoc, **kwargs)
638         self.materials_by_slug = None
639
640     def build(self):
641         # Sort materials by slug.
642         self.materials_by_slug = {}
643         for name, att in self.wldoc.source.attachments.items():
644             parts = name.rsplit('.', 1)
645             if len(parts) == 1:
646                 continue
647             slug, ext = parts
648             if slug not in self.materials_by_slug:
649                 self.materials_by_slug[slug] = {}
650             self.materials_by_slug[slug][ext] = att
651
652         edumod = EduModule({'provider': self.wldoc.provider, 'urlmapper': self, 'wldoc': self.wldoc})
653
654         html = edumod.generate(self.wldoc.edoc.getroot())
655
656         return IOFile.from_string(html.encode('utf-8'))
657
658     def materials(self, slug):
659         """Returns a list of pairs: (ext, iofile)."""
660         order = {pmf: i for (i, pmf) in enumerate(self.PRIMARY_MATERIAL_FORMATS)}
661         mats = self.materials_by_slug.get(slug, {}).items()
662         if not mats:
663             print ("!! Material missing: '%s'" % slug).encode('utf-8')
664         return sorted(mats, key=lambda (x, y): order.get(x, x))
665
666     def url_for_material(self, slug, fmt):
667         return "%s.%s" % (slug, fmt)
668
669     # WTF: tutaj był błąd, ale nikomu to nie przeszkadzało?
670     def url_for_image(self, slug, fmt, width=None):
671         return self.url_for_material(slug, fmt)
672
673     def text_to_anchor(self, text):
674         return re.sub(r" +", " ", text)
675
676     def naglowek_to_anchor(self, naglowek):
677         return self.text_to_anchor(naglowek.text.strip())
678
679     def get_forma_url(self, forma):
680         return None
681
682     def get_help_url(self, naglowek):
683         return None
684
685
686 def transform(wldoc, stylesheet='edumed', options=None, flags=None, verbose=None):
687     """Transforms the WL document to XHTML.
688
689     If output_filename is None, returns an XML,
690     otherwise returns True if file has been written,False if it hasn't.
691     File won't be written if it has no content.
692     """
693     edumodfor = EduModuleFormat(wldoc)
694     return edumodfor.build()