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, ifoption, tag_open_close
22 from librarian import DCNS, get_resource, IOFile
23 from librarian import functions
24 from pdf import PDFFormat, substitute_hyphens, fix_hanging
29 def _wrap(*args, **kw):
30 value = f(*args, **kw)
32 prefix = (u'<TeXML escape="%d">' % (1 if really else 0))
34 if isinstance(value, list):
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, state=None):
70 super(EduModule, self).__init__(options, state)
71 self.activity_counter = 0
72 self.activity_last = None
73 self.exercise_counter = 0
75 def swap_endlines(txt):
76 if self.options['strofa']:
77 txt = txt.replace("/\n", '<ctrl ch="\\"/>')
79 self.register_text_filter(swap_endlines)
80 self.register_text_filter(functions.substitute_entities)
81 self.register_text_filter(mark_alien_characters)
83 def get_dc(self, element, dc_field, single=False):
84 values = map(lambda t: t.text, element.xpath("//dc:%s" % dc_field, namespaces={'dc': DCNS.uri}))
86 return values[0] if len(values) else ''
89 def handle_rdf__RDF(self, _):
90 """skip metadata in generation"""
94 def get_rightsinfo(self, element):
95 rights_lic = self.get_dc(element, 'rights.license', True)
96 return u'<cmd name="rightsinfostr">' + (u'<opt>%s</opt>' % rights_lic if rights_lic else '') + \
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 if author)
112 def get_title(self, element):
113 return self.get_dc(element, 'title', True)
116 def get_description(self, element):
117 desc = self.get_dc(element, 'description', single=True)
119 print '!! no description'
123 def get_curriculum(self, element):
125 for dc_tag, new in [('subject.curriculum', False), ('subject.curriculum.new', True)]:
126 identifiers = self.get_dc(element, dc_tag)
130 from curriculum.templatetags.curriculum_tags import curriculum
131 curr_elements = curriculum(identifiers)
133 curr_elements = {'identifiers': identifiers}
134 items = ['Nowa podstawa programowa:' if new else 'Podstawa programowa:']
135 newline = '<ctrl ch="\\"/>\n'
136 if 'currset' in curr_elements:
137 for (course, level), types in curr_elements['currset'].iteritems():
138 lines = [u'%s, %s' % (course, level)]
139 for type, currs in types.iteritems():
141 lines += [curr.title for curr in currs]
142 items.append(newline.join(lines))
145 ret.append('\n<cmd name="vspace"><parm>.6em</parm></cmd>\n'.join(
146 '<cmd name="akap"><parm>%s</parm></cmd>' % item for item in items))
147 return '\n<cmd name="vspace"><parm>1em</parm></cmd>\n'.join(ret)
149 def handle_utwor(self, element):
152 <TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
154 \\documentclass[%s]{wl}
155 \\usepackage{style}''' % self.options['customization_str'],
156 self.options['has_cover'] and '\usepackage{makecover}',
157 (self.options['morefloats'] == 'new' and '\usepackage[maxfloats=64]{morefloats}') or
158 (self.options['morefloats'] == 'old' and '\usepackage{morefloats}') or
159 (self.options['morefloats'] == 'none' and
160 u'''\\IfFileExists{morefloats.sty}{
161 \\usepackage{morefloats}
163 u'''\\def\\authors{%s}''' % self.get_authors(element),
164 u'''\\def\\authorsexpert{%s}''' % self.get_authors(element, 'expert'),
165 u'''\\def\\authorsscenario{%s}''' % self.get_authors(element, 'scenario'),
166 u'''\\def\\authorstextbook{%s}''' % self.get_authors(element, 'textbook'),
167 u'''\\def\\description{%s}''' % self.get_description(element),
169 u'''\\author{\\authors}''',
170 u'''\\title{%s}''' % self.get_title(element),
171 u'''\\def\\bookurl{%s}''' % self.options['wldoc'].book_info.url.canonical(),
172 u'''\\def\\rightsinfo{%s}''' % self.get_rightsinfo(element),
173 u'''\\def\\curriculum{%s}''' % self.get_curriculum(element),
177 return u"".join(filter(None, lines)), u'</TeXML>'
180 def handle_powiesc(self, element):
182 <env name="document">
183 <cmd name="maketitle"/>
184 """, """<cmd name="editorialsection" /></env>"""
187 def handle_texcommand(self, element):
188 cmd = functions.texcommand(element.tag)
189 return u'<TeXML escape="1"><cmd name="%s"><parm>' % cmd, u'</parm></cmd></TeXML>'
193 handle_akap_dialog = \
194 handle_autor_utworu = \
196 handle_didaskalia = \
197 handle_didask_tekst = \
198 handle_dlugi_cytat = \
199 handle_dzielo_nadrzedne = \
200 handle_lista_osoba = \
202 handle_miejsce_czas = \
204 handle_motto_podpis = \
205 handle_naglowek_akt = \
206 handle_naglowek_czesc = \
207 handle_naglowek_listy = \
208 handle_naglowek_osoba = \
209 handle_naglowek_scena = \
210 handle_nazwa_utworu = \
216 handle_poezja_cyt = \
219 handle_sekcja_asterysk = \
220 handle_sekcja_swiatlo = \
221 handle_separator_linia = \
222 handle_slowo_obce = \
224 handle_tytul_dziela = \
225 handle_wyroznienie = \
229 def handle_naglowek_rozdzial(self, element):
230 if not self.options['teacher']:
231 if element.text.startswith((u'Wiedza', u'Zadania', u'Słowniczek', u'Dla ucznia')):
232 self.state['mute'] = False
234 self.state['mute'] = True
236 return self.handle_texcommand(element)
237 handle_naglowek_rozdzial.unmuter = True
239 def handle_naglowek_podrozdzial(self, element):
240 self.activity_counter = 0
241 if not self.options['teacher']:
242 if element.text.startswith(u'Dla ucznia'):
243 self.state['mute'] = False
245 elif element.text.startswith(u'Dla nauczyciela'):
246 self.state['mute'] = True
248 elif self.state['mute']:
250 return self.handle_texcommand(element)
251 handle_naglowek_podrozdzial.unmuter = True
253 def handle_uwaga(self, _e):
256 def handle_extra(self, _e):
259 def handle_nbsp(self, _e):
260 return '<spec cat="tilde" />'
262 _handle_strofa = cmd("strofa")
264 def handle_strofa(self, element):
265 self.options = {'strofa': True}
266 return self._handle_strofa(element)
268 def handle_aktywnosc(self, element):
269 self.activity_counter += 1
272 'activity_counter': self.activity_counter,
275 submill = EduModule(self.options, self.state)
277 if element.xpath('opis'):
278 opis = submill.generate(element.xpath('opis')[0])
282 n = element.xpath('wskazowki')
284 wskazowki = submill.generate(n[0])
287 n = element.xpath('pomoce')
290 pomoce = submill.generate(n[0])
294 forma = ''.join(element.xpath('forma/text()'))
296 czas = ''.join(element.xpath('czas/text()'))
298 counter = self.activity_counter
300 if element.getnext().tag == 'aktywnosc' or (len(self.activity_last) and self.activity_last.getnext() == element):
301 counter_tex = """<cmd name="activitycounter"><parm>%(counter)d.</parm></cmd>""" % locals()
305 self.activity_last = element
308 <cmd name="noindent" />
310 <cmd name="activityinfo"><parm>
311 <cmd name="activitytime"><parm>%(czas)s</parm></cmd>
312 <cmd name="activityform"><parm>%(forma)s</parm></cmd>
313 <cmd name="activitytools"><parm>%(pomoce)s</parm></cmd>
322 handle_opis = ifoption(sub_gen=True)(lambda s, e: ('', ''))
323 handle_wskazowki = ifoption(sub_gen=True)(lambda s, e: ('', ''))
325 @ifoption(sub_gen=True)
326 def handle_pomoce(self, _):
327 return "Pomoce: ", ""
329 def handle_czas(self, *_):
332 def handle_forma(self, *_):
335 def handle_lista(self, element, attrs=None):
336 ltype = element.attrib.get('typ', 'punkt')
337 if not element.findall("punkt"):
338 if ltype == 'czytelnia':
339 return 'W przygotowaniu.'
342 if ltype == 'slowniczek':
343 surl = element.attrib.get('src', None)
345 # print '** missing src on <slowniczek>, setting default'
346 surl = 'http://edukacjamedialna.edu.pl/lekcje/slowniczek/'
347 sxml = etree.fromstring(self.options['wldoc'].provider.by_uri(surl).get_string())
348 self.options = {'slowniczek': True, 'slowniczek_xml': sxml}
354 'slowniczek': 'itemize',
355 'czytelnia': 'itemize'
358 return u'<env name="%s">' % listcmd, u'</env>'
360 def handle_punkt(self, element):
361 return '<cmd name="item"/>', ''
363 def handle_cwiczenie(self, element):
364 exercise_handlers = {
366 'uporzadkuj': Uporzadkuj,
369 'przyporzadkuj': Przyporzadkuj,
370 'prawdafalsz': PrawdaFalsz
373 typ = element.attrib['typ']
374 self.exercise_counter += 1
375 if typ not in exercise_handlers:
376 return '(no handler)'
377 self.options = {'exercise_counter': self.exercise_counter}
378 handler = exercise_handlers[typ](self.options, self.state)
379 return handler.generate(element)
381 # XXX this is copied from pyhtml.py, except for return and
382 # should be refactored for no code duplication
383 def handle_definiendum(self, element):
384 nxt = element.getnext()
387 # let's pull definiens from another document
388 if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
389 sxml = self.options['slowniczek_xml']
390 assert element.text != ''
391 if "'" in (element.text or ''):
392 defloc = sxml.xpath("//definiendum[text()=\"%s\"]" % (element.text or '').strip())
394 defloc = sxml.xpath("//definiendum[text()='%s']" % (element.text or '').strip())
396 definiens = defloc[0].getnext()
397 if definiens.tag == 'definiens':
398 subgen = EduModule(self.options, self.state)
399 definiens_s = subgen.generate(definiens)
401 return u'<cmd name="textbf"><parm>', u"</parm></cmd>: " + definiens_s
403 def handle_definiens(self, element):
406 def handle_podpis(self, element):
407 return u"""<env name="figure">""", u"</env>"
409 def handle_tabela(self, element):
411 for w in element.xpath("wiersz"):
413 if max_col < len(ks):
415 self.options = {'columnts': max_col}
417 # has_frames = int(element.attrib.get("ramki", "0"))
418 # if has_frames: frames_c = "framed"
419 # else: frames_c = ""
420 # return u"""<table class="%s">""" % frames_c, u"</table>"
422 <cmd name="begin"><parm>tabular</parm><parm>%s</parm></cmd>
423 ''' % ('l' * max_col), u'''<cmd name="end"><parm>tabular</parm></cmd>'''
426 def handle_wiersz(self, element):
427 return u"", u'<ctrl ch="\\"/>'
430 def handle_kol(self, element):
431 if element.getnext() is not None:
432 return u"", u'<spec cat="align" />'
435 def handle_link(self, element):
436 if element.attrib.get('url'):
437 url = element.attrib.get('url')
438 if url == element.text:
439 return cmd('url')(self, element)
441 return cmd('href', parms=[element.attrib['url']])(self, element)
443 return cmd('emph')(self, element)
445 def handle_obraz(self, element):
446 frmt = self.options['format']
447 name = element.attrib.get('nazwa', '').strip()
448 image = frmt.get_image(name.strip())
449 name = image.get_filename().rsplit('/', 1)[-1]
450 img_path = "obraz/%s" % name.replace("_", "")
451 frmt.attachments[img_path] = image
452 return cmd("obraz", parms=[img_path])(self)
454 def handle_video(self, element):
455 url = element.attrib.get('url')
457 print '!! <video> missing url'
459 m = re.match(r'(?:https?://)?(?:www.)?youtube.com/watch\?(?:.*&)?v=([^&]+)(?:$|&)', url)
461 print '!! unknown <video> url scheme:', url
464 thumb = IOFile.from_string(urlopen("http://img.youtube.com/vi/%s/0.jpg" % name).read())
465 img_path = "video/%s.jpg" % name.replace("_", "")
466 self.options['format'].attachments[img_path] = thumb
467 canon_url = "https://www.youtube.com/watch?v=%s" % name
468 return cmd("video", parms=[img_path, canon_url])(self)
471 class Exercise(EduModule):
472 def __init__(self, *args, **kw):
473 self.question_counter = 0
474 super(Exercise, self).__init__(*args, **kw)
475 self.piece_counter = None
477 handle_rozw_kom = ifoption(teacher=True)(cmd('akap'))
479 def handle_cwiczenie(self, element):
481 'exercise': element.attrib['typ'],
484 self.question_counter = 0
485 self.piece_counter = 0
487 header = etree.Element("parm")
488 header_cmd = etree.Element("cmd", name="naglowekpodrozdzial")
489 header_cmd.append(header)
490 header.text = u"Zadanie %d." % self.options['exercise_counter']
492 pre = etree.tostring(header_cmd, encoding=unicode)
494 # Add a single <pytanie> tag if it's not there
495 if not element.xpath(".//pytanie"):
496 qpre, qpost = self.handle_pytanie(element)
501 def handle_pytanie(self, element):
502 """This will handle <cwiczenie> element, when there is no <pytanie>
504 self.question_counter += 1
505 self.piece_counter = 0
507 if self.options['teacher'] and element.attrib.get('rozw'):
508 post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
511 def handle_punkt(self, element):
512 pre, post = super(Exercise, self).handle_punkt(element)
513 if self.options['teacher'] and element.attrib.get('rozw'):
514 post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
517 def solution_header(self):
518 par = etree.Element("cmd", name="par")
519 parm = etree.Element("parm")
520 parm.text = u"Rozwiązanie:"
522 return etree.tostring(par)
524 def explicit_solution(self):
525 if self.options['solution']:
526 par = etree.Element("cmd", name="par")
527 parm = etree.Element("parm")
528 parm.text = self.options['solution']
530 return self.solution_header() + etree.tostring(par)
533 class Wybor(Exercise):
534 def handle_cwiczenie(self, element):
535 pre, post = super(Wybor, self).handle_cwiczenie(element)
536 is_single_choice = True
537 pytania = element.xpath(".//pytanie")
541 solutions = p.xpath(".//punkt[@rozw='prawda']")
542 if len(solutions) != 1:
543 is_single_choice = False
546 self.options = {'single': is_single_choice}
549 def handle_punkt(self, element):
550 if self.options['exercise'] and element.attrib.get('rozw', None):
551 cmd = 'radio' if self.options['single'] else 'checkbox'
552 if self.options['teacher'] and element.attrib['rozw'] == 'prawda':
554 return u'<cmd name="%s"/>' % cmd, ''
556 return super(Wybor, self).handle_punkt(element)
559 class Uporzadkuj(Exercise):
560 def handle_pytanie(self, element):
561 order_items = element.xpath(".//punkt/@rozw")
562 return super(Uporzadkuj, self).handle_pytanie(element)
565 class Przyporzadkuj(Exercise):
566 def handle_lista(self, lista):
567 header = etree.Element("parm")
568 header_cmd = etree.Element("cmd", name="par")
569 header_cmd.append(header)
570 if 'nazwa' in lista.attrib:
571 header.text = u"Kategorie:"
572 elif 'cel' in lista.attrib:
573 header.text = u"Elementy do przyporządkowania:"
575 header.text = u"Lista:"
576 pre, post = super(Przyporzadkuj, self).handle_lista(lista)
577 pre = etree.tostring(header_cmd, encoding=unicode) + pre
581 class Luki(Exercise):
582 def find_pieces(self, question):
583 return question.xpath(".//luka")
585 def solution(self, piece):
586 piece = deepcopy(piece)
589 return sub.generate(piece)
591 def handle_pytanie(self, element):
592 qpre, qpost = super(Luki, self).handle_pytanie(element)
594 luki = self.find_pieces(element)
596 self.words = u"<env name='itemize'>%s</env>" % (
597 "".join("<cmd name='item'/>%s" % self.solution(luka) for luka in luki)
601 def handle_opis(self, element):
602 return '', self.words
604 def handle_luka(self, element):
606 if self.options['teacher']:
607 piece = deepcopy(element)
610 text = sub.generate(piece)
611 luka += u" [rozwiązanie: %s]" % text
616 def find_pieces(self, question):
617 return question.xpath(".//zastap")
619 def solution(self, piece):
620 return piece.attrib.get('rozw', '')
622 def list_header(self):
623 return u"Elementy do wstawienia"
625 def handle_zastap(self, element):
626 piece = deepcopy(element)
629 text = sub.generate(piece)
630 if self.options['teacher'] and element.attrib.get('rozw'):
631 text += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
635 class PrawdaFalsz(Exercise):
636 def handle_punkt(self, element):
637 pre, post = super(PrawdaFalsz, self).handle_punkt(element)
638 if 'rozw' in element.attrib:
639 post += u" [Prawda/Fałsz]"
644 lists = tree.xpath(".//lista")
661 class EduModulePDFFormat(PDFFormat):
662 style = get_resource('res/styles/edumed/pdf/edumed.sty')
665 substitute_hyphens(self.wldoc.edoc)
666 fix_hanging(self.wldoc.edoc)
668 self.attachments = {}
672 "teacher": self.customization.get('teacher'),
674 texml = edumod.generate(fix_lists(self.wldoc.edoc.getroot())).encode('utf-8')
676 open("/tmp/texml.xml", "w").write(texml)
679 def get_tex_dir(self):
680 temp = super(EduModulePDFFormat, self).get_tex_dir()
681 shutil.copy(get_resource('res/styles/edumed/logo.png'), temp)
682 for name, iofile in self.attachments.items():
683 iofile.save_as(os.path.join(temp, name))
686 def get_image(self, name):
687 return self.wldoc.source.attachments[name]