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