1 # -*- coding: utf-8 -*-
3 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
4 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
6 """PDF creation library.
8 Creates one big XML from the book and its children, converts it to LaTeX
9 with TeXML, then runs it by XeLaTeX.
12 from copy import deepcopy
17 from urllib2 import urlopen
19 from lxml import etree
21 from xmlutils import Xmill, tag, tagged, ifoption, tag_open_close
22 from librarian.dcparser import Person
23 from librarian import DCNS, get_resource, IOFile
24 from librarian import functions
25 from pdf import PDFFormat, substitute_hyphens, fix_hanging
30 def _wrap(*args, **kw):
31 value = f(*args, **kw)
33 prefix = (u'<TeXML escape="%d">' % (really and 1 or 0))
35 if isinstance(value, list):
36 import pdb; pdb.set_trace()
37 if isinstance(value, tuple):
38 return prefix + value[0], value[1] + postfix
40 return prefix + value + postfix
45 def cmd(name, parms=None):
46 def wrap(self, element=None):
47 pre, post = tag_open_close('cmd', name=name)
51 e = etree.Element("parm")
53 pre += etree.tostring(e)
54 if element is not None:
56 post = "</parm>" + post
63 def mark_alien_characters(text):
64 text = re.sub(ur"([\u0400-\u04ff]+)", ur"<alien>\1</alien>", text)
68 class EduModule(Xmill):
69 def __init__(self, options=None):
70 super(EduModule, self).__init__(options)
71 self.activity_counter = 0
72 self.exercise_counter = 0
74 def swap_endlines(txt):
75 if self.options['strofa']:
76 txt = txt.replace("/\n", '<ctrl ch="\\"/>')
78 self.register_text_filter(swap_endlines)
79 self.register_text_filter(functions.substitute_entities)
80 self.register_text_filter(mark_alien_characters)
82 def get_dc(self, element, dc_field, single=False):
83 values = map(lambda t: t.text, element.xpath("//dc:%s" % dc_field, namespaces={'dc': DCNS.uri}))
88 def handle_rdf__RDF(self, _):
89 "skip metadata in generation"
93 def get_rightsinfo(self, element):
94 rights_lic = self.get_dc(element, 'rights.license', True)
95 return u'<cmd name="rightsinfostr">' + \
96 (rights_lic and u'<opt>%s</opt>' % rights_lic or '') +\
97 u'<parm>%s</parm>' % self.get_dc(element, 'rights', True) +\
101 def get_authors(self, element, which=None):
102 dc = self.options['wldoc'].book_info
104 authors = dc.authors_textbook + \
105 dc.authors_scenario + \
108 authors = getattr(dc, "authors_%s" % which)
109 return u', '.join(author.readable() for author in authors)
112 def get_title(self, element):
113 return self.get_dc(element, 'title', True)
115 def handle_utwor(self, element):
118 <TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
120 \\documentclass[%s]{wl}
121 \\usepackage{style}''' % self.options['customization_str'],
122 self.options['has_cover'] and '\usepackage{makecover}',
123 (self.options['morefloats'] == 'new' and '\usepackage[maxfloats=64]{morefloats}') or
124 (self.options['morefloats'] == 'old' and '\usepackage{morefloats}') or
125 (self.options['morefloats'] == 'none' and
126 u'''\\IfFileExists{morefloats.sty}{
127 \\usepackage{morefloats}
129 u'''\\def\\authors{%s}''' % self.get_authors(element),
130 u'''\\def\\authorsexpert{%s}''' % self.get_authors(element, 'expert'),
131 u'''\\def\\authorsscenario{%s}''' % self.get_authors(element, 'scenario'),
132 u'''\\def\\authorstextbook{%s}''' % self.get_authors(element, 'textbook'),
134 u'''\\author{\\authors}''',
135 u'''\\title{%s}''' % self.get_title(element),
136 u'''\\def\\bookurl{%s}''' % self.options['wldoc'].book_info.url.canonical(),
137 u'''\\def\\rightsinfo{%s}''' % self.get_rightsinfo(element),
140 return u"".join(filter(None, lines)), u'</TeXML>'
144 def handle_powiesc(self, element):
146 <env name="document">
147 <cmd name="maketitle"/>
148 """, """<cmd name="editorialsection" /></env>"""
151 def handle_texcommand(self, element):
152 cmd = functions.texcommand(element.tag)
153 return u'<TeXML escape="1"><cmd name="%s"><parm>' % cmd, u'</parm></cmd></TeXML>'
159 handle_akap_dialog = \
160 handle_akap_dialog = \
161 handle_autor_utworu = \
163 handle_didaskalia = \
164 handle_didask_tekst = \
165 handle_dlugi_cytat = \
166 handle_dzielo_nadrzedne = \
167 handle_lista_osoba = \
169 handle_miejsce_czas = \
171 handle_motto_podpis = \
172 handle_naglowek_akt = \
173 handle_naglowek_czesc = \
174 handle_naglowek_listy = \
175 handle_naglowek_osoba = \
176 handle_naglowek_podrozdzial = \
177 handle_naglowek_podrozdzial = \
178 handle_naglowek_rozdzial = \
179 handle_naglowek_rozdzial = \
180 handle_naglowek_scena = \
181 handle_nazwa_utworu = \
187 handle_poezja_cyt = \
190 handle_sekcja_asterysk = \
191 handle_sekcja_swiatlo = \
192 handle_separator_linia = \
193 handle_slowo_obce = \
195 handle_tytul_dziela = \
196 handle_wyroznienie = \
200 def handle_uwaga(self, _e):
202 def handle_extra(self, _e):
205 def handle_nbsp(self, _e):
206 return '<spec cat="tilde" />'
208 _handle_strofa = cmd("strofa")
210 def handle_strofa(self, element):
211 self.options = {'strofa': True}
212 return self._handle_strofa(element)
214 def handle_aktywnosc(self, element):
215 self.activity_counter += 1
218 'activity_counter': self.activity_counter,
221 submill = EduModule(self.options)
223 opis = submill.generate(element.xpath('opis')[0])
225 n = element.xpath('wskazowki')
226 if n: wskazowki = submill.generate(n[0])
229 n = element.xpath('pomoce')
231 if n: pomoce = submill.generate(n[0])
234 forma = ''.join(element.xpath('forma/text()'))
236 czas = ''.join(element.xpath('czas/text()'))
238 counter = self.activity_counter
241 <cmd name="noindent" />
242 <cmd name="activitycounter"><parm>%(counter)d.</parm></cmd>
243 <cmd name="activityinfo"><parm>
244 <cmd name="activitytime"><parm>%(czas)s</parm></cmd>
245 <cmd name="activityform"><parm>%(forma)s</parm></cmd>
246 <cmd name="activitytools"><parm>%(pomoce)s</parm></cmd>
255 handle_opis = ifoption(sub_gen=True)(lambda s, e: ('', ''))
256 handle_wskazowki = ifoption(sub_gen=True)(lambda s, e: ('', ''))
258 @ifoption(sub_gen=True)
259 def handle_pomoce(self, _):
260 return "Pomoce: ", ""
262 def handle_czas(self, *_):
265 def handle_forma(self, *_):
268 def handle_lista(self, element, attrs={}):
269 ltype = element.attrib.get('typ', 'punkt')
270 if not element.findall("punkt"):
271 if ltype == 'czytelnia':
272 return 'W przygotowaniu.'
275 if ltype == 'slowniczek':
276 surl = element.attrib.get('src', None)
278 # print '** missing src on <slowniczek>, setting default'
279 surl = 'http://edukacjamedialna.edu.pl/lekcje/slowniczek/'
282 sxml = etree.fromstring(self.options['wldoc'].provider.by_uri(surl).get_string())
283 self.options = {'slowniczek': True, 'slowniczek_xml': sxml }
285 listcmd = {'num': 'enumerate',
288 'slowniczek': 'itemize',
289 'czytelnia': 'itemize'}[ltype]
291 return u'<env name="%s">' % listcmd, u'</env>'
293 def handle_punkt(self, element):
294 return '<cmd name="item"/>', ''
296 def handle_cwiczenie(self, element):
297 exercise_handlers = {
299 'uporzadkuj': Uporzadkuj,
302 'przyporzadkuj': Przyporzadkuj,
303 'prawdafalsz': PrawdaFalsz
306 typ = element.attrib['typ']
307 self.exercise_counter += 1
308 if not typ in exercise_handlers:
309 return '(no handler)'
310 self.options = {'exercise_counter': self.exercise_counter}
311 handler = exercise_handlers[typ](self.options)
312 return handler.generate(element)
314 # XXX this is copied from pyhtml.py, except for return and
315 # should be refactored for no code duplication
316 def handle_definiendum(self, element):
317 nxt = element.getnext()
320 # let's pull definiens from another document
321 if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
322 sxml = self.options['slowniczek_xml']
323 assert element.text != ''
324 defloc = sxml.xpath("//definiendum[text()='%s']" % element.text)
326 definiens = defloc[0].getnext()
327 if definiens.tag == 'definiens':
328 subgen = EduModule(self.options)
329 definiens_s = subgen.generate(definiens)
331 return u'<cmd name="textbf"><parm>', u"</parm></cmd>: " + definiens_s
333 def handle_definiens(self, element):
336 def handle_podpis(self, element):
337 return u"""<env name="figure">""", u"</env>"
339 def handle_tabela(self, element):
341 for w in element.xpath("wiersz"):
343 if max_col < len(ks):
345 self.options = {'columnts': max_col}
347 # has_frames = int(element.attrib.get("ramki", "0"))
348 # if has_frames: frames_c = "framed"
349 # else: frames_c = ""
350 # return u"""<table class="%s">""" % frames_c, u"</table>"
352 <cmd name="begin"><parm>tabular</parm><parm>%s</parm></cmd>
353 ''' % ('l' * max_col), \
354 u'''<cmd name="end"><parm>tabular</parm></cmd>'''
357 def handle_wiersz(self, element):
358 return u"", u'<ctrl ch="\\"/>'
361 def handle_kol(self, element):
362 if element.getnext() is not None:
363 return u"", u'<spec cat="align" />'
366 def handle_link(self, element):
367 if element.attrib.get('url'):
368 url = element.attrib.get('url')
369 if url == element.text:
370 return cmd('url')(self, element)
372 return cmd('href', parms=[element.attrib['url']])(self, element)
374 return cmd('emph')(self, element)
376 def handle_obraz(self, element):
377 frmt = self.options['format']
378 name = element.attrib['nazwa'].strip()
379 image = frmt.get_image(name.strip())
380 img_path = "obraz/%s" % name.replace("_", "")
381 frmt.attachments[img_path] = image
382 return cmd("obraz", parms=[img_path])(self)
384 def handle_video(self, element):
385 url = element.attrib.get('url')
387 print '!! <video> missing url'
389 m = re.match(r'(?:https?://)?(?:www.)?youtube.com/watch\?(?:.*&)?v=([^&]+)(?:$|&)', url)
391 print '!! unknown <video> url scheme:', url
394 thumb = IOFile.from_string(urlopen
395 ("http://img.youtube.com/vi/%s/0.jpg" % name).read())
396 img_path = "video/%s.jpg" % name.replace("_", "")
397 self.options['format'].attachments[img_path] = thumb
398 canon_url = "https://www.youtube.com/watch?v=%s" % name
399 return cmd("video", parms=[img_path, canon_url])(self)
402 class Exercise(EduModule):
403 def __init__(self, *args, **kw):
404 self.question_counter = 0
405 super(Exercise, self).__init__(*args, **kw)
407 handle_rozw_kom = ifoption(teacher=True)(cmd('akap'))
409 def handle_cwiczenie(self, element):
411 'exercise': element.attrib['typ'],
414 self.question_counter = 0
415 self.piece_counter = 0
417 header = etree.Element("parm")
418 header_cmd = etree.Element("cmd", name="naglowekpodrozdzial")
419 header_cmd.append(header)
420 header.text = u"Zadanie %d." % self.options['exercise_counter']
422 pre = etree.tostring(header_cmd, encoding=unicode)
424 # Add a single <pytanie> tag if it's not there
425 if not element.xpath(".//pytanie"):
426 qpre, qpost = self.handle_pytanie(element)
431 def handle_pytanie(self, element):
432 """This will handle <cwiczenie> element, when there is no <pytanie>
434 self.question_counter += 1
435 self.piece_counter = 0
437 if self.options['teacher'] and element.attrib.get('rozw'):
438 post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
441 def handle_punkt(self, element):
442 pre, post = super(Exercise, self).handle_punkt(element)
443 if self.options['teacher'] and element.attrib.get('rozw'):
444 post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
447 def solution_header(self):
448 par = etree.Element("cmd", name="par")
449 parm = etree.Element("parm")
450 parm.text = u"Rozwiązanie:"
452 return etree.tostring(par)
454 def explicit_solution(self):
455 if self.options['solution']:
456 par = etree.Element("cmd", name="par")
457 parm = etree.Element("parm")
458 parm.text = self.options['solution']
460 return self.solution_header() + etree.tostring(par)
464 class Wybor(Exercise):
465 def handle_cwiczenie(self, element):
466 pre, post = super(Wybor, self).handle_cwiczenie(element)
467 is_single_choice = True
468 pytania = element.xpath(".//pytanie")
472 solutions = re.split(r"[, ]+", p.attrib['rozw'])
473 if len(solutions) != 1:
474 is_single_choice = False
476 choices = p.xpath(".//*[@nazwa]")
478 for n in choices: uniq.add(n.attrib['nazwa'])
479 if len(choices) != len(uniq):
480 is_single_choice = False
483 self.options = {'single': is_single_choice}
486 def handle_punkt(self, element):
487 if self.options['exercise'] and element.attrib.get('nazwa', None):
488 cmd = 'radio' if self.options['single'] else 'checkbox'
489 return u'<cmd name="%s"/>' % cmd, ''
491 return super(Wybor, self).handle_punkt(element)
494 class Uporzadkuj(Exercise):
495 def handle_pytanie(self, element):
496 order_items = element.xpath(".//punkt/@rozw")
497 return super(Uporzadkuj, self).handle_pytanie(element)
500 class Przyporzadkuj(Exercise):
501 def handle_lista(self, lista):
502 header = etree.Element("parm")
503 header_cmd = etree.Element("cmd", name="par")
504 header_cmd.append(header)
505 if 'nazwa' in lista.attrib:
506 header.text = u"Kategorie:"
507 elif 'cel' in lista.attrib:
508 header.text = u"Elementy do przyporządkowania:"
510 header.text = u"Lista:"
511 pre, post = super(Przyporzadkuj, self).handle_lista(lista)
512 pre = etree.tostring(header_cmd, encoding=unicode) + pre
516 class Luki(Exercise):
517 def find_pieces(self, question):
518 return question.xpath(".//luka")
520 def solution(self, piece):
521 piece = deepcopy(piece)
524 return sub.generate(piece)
526 def handle_pytanie(self, element):
527 qpre, qpost = super(Luki, self).handle_pytanie(element)
529 luki = self.find_pieces(element)
531 self.words = u"<env name='itemize'>%s</env>" % (
532 "".join("<cmd name='item'/>%s" % self.solution(luka) for luka in luki)
536 def handle_opis(self, element):
537 return '', self.words
539 def handle_luka(self, element):
541 if self.options['teacher']:
542 piece = deepcopy(element)
545 text = sub.generate(piece)
546 luka += u" [rozwiązanie: %s]" % text
551 def find_pieces(self, question):
552 return question.xpath(".//zastap")
554 def solution(self, piece):
555 return piece.attrib['rozw']
557 def list_header(self):
558 return u"Elementy do wstawienia"
560 def handle_zastap(self, element):
561 piece = deepcopy(element)
564 text = sub.generate(piece)
565 if self.options['teacher'] and element.attrib.get('rozw'):
566 text += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
570 class PrawdaFalsz(Exercise):
571 def handle_punkt(self, element):
572 pre, post = super(PrawdaFalsz, self).handle_punkt(element)
573 if 'rozw' in element.attrib:
574 post += u" [Prawda/Fałsz]"
580 lists = tree.xpath(".//lista")
585 if p.tail is None: p.tail = ''
589 if p.text is None: p.text = ''
595 class EduModulePDFFormat(PDFFormat):
596 style = get_resource('res/styles/edumed/pdf/edumed.sty')
599 substitute_hyphens(self.wldoc.edoc)
600 fix_hanging(self.wldoc.edoc)
602 self.attachments = {}
606 "teacher": self.customization.get('teacher'),
608 texml = edumod.generate(fix_lists(self.wldoc.edoc.getroot())).encode('utf-8')
610 open("/tmp/texml.xml", "w").write(texml)
613 def get_tex_dir(self):
614 temp = super(EduModulePDFFormat, self).get_tex_dir()
615 shutil.copy(get_resource('res/styles/edumed/logo.png'), temp)
616 for name, iofile in self.attachments.items():
617 iofile.save_as(os.path.join(temp, name))
620 def get_image(self, name):
621 return self.wldoc.source.attachments[name]