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