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):
 
 124         identifiers = self.get_dc(element, 'subject.curriculum')
 
 128             from curriculum.templatetags.curriculum_tags import curriculum
 
 129             curr_elements = curriculum(identifiers)
 
 131             curr_elements = {'identifiers': identifiers}
 
 132         items = ['Podstawa programowa:']
 
 133         newline = '<ctrl ch="\\"/>\n'
 
 134         if 'currset' in curr_elements:
 
 135             for (course, level), types in curr_elements['currset'].iteritems():
 
 136                 lines = [u'%s, %s poziom edukacyjny' % (course, level)]
 
 137                 for type, currs in types.iteritems():
 
 139                     lines += [curr.title for curr in currs]
 
 140                 items.append(newline.join(lines))
 
 143         return '\n<cmd name="vspace"><parm>.6em</parm></cmd>\n'.join(
 
 144             '<cmd name="akap"><parm>%s</parm></cmd>' % item for item in items)
 
 146     def handle_utwor(self, element):
 
 149                 <TeXML xmlns="http://getfo.sourceforge.net/texml/ns1">
 
 151                 \\documentclass[%s]{wl}
 
 152                 \\usepackage{style}''' % self.options['customization_str'],
 
 153             self.options['has_cover'] and '\usepackage{makecover}',
 
 154             (self.options['morefloats'] == 'new' and '\usepackage[maxfloats=64]{morefloats}') or
 
 155             (self.options['morefloats'] == 'old' and '\usepackage{morefloats}') or
 
 156             (self.options['morefloats'] == 'none' and
 
 157                 u'''\\IfFileExists{morefloats.sty}{
 
 158                 \\usepackage{morefloats}
 
 160             u'''\\def\\authors{%s}''' % self.get_authors(element),
 
 161             u'''\\def\\authorsexpert{%s}''' % self.get_authors(element, 'expert'),
 
 162             u'''\\def\\authorsscenario{%s}''' % self.get_authors(element, 'scenario'),
 
 163             u'''\\def\\authorstextbook{%s}''' % self.get_authors(element, 'textbook'),
 
 164             u'''\\def\\description{%s}''' % self.get_description(element),
 
 166             u'''\\author{\\authors}''',
 
 167             u'''\\title{%s}''' % self.get_title(element),
 
 168             u'''\\def\\bookurl{%s}''' % self.options['wldoc'].book_info.url.canonical(),
 
 169             u'''\\def\\rightsinfo{%s}''' % self.get_rightsinfo(element),
 
 170             u'''\\def\\curriculum{%s}''' % self.get_curriculum(element),
 
 174         return u"".join(filter(None, lines)), u'</TeXML>'
 
 177     def handle_powiesc(self, element):
 
 179     <env name="document">
 
 180     <cmd name="maketitle"/>
 
 181     """, """<cmd name="editorialsection" /></env>"""
 
 184     def handle_texcommand(self, element):
 
 185         cmd = functions.texcommand(element.tag)
 
 186         return u'<TeXML escape="1"><cmd name="%s"><parm>' % cmd, u'</parm></cmd></TeXML>'
 
 190         handle_akap_dialog = \
 
 191         handle_autor_utworu = \
 
 193         handle_didaskalia = \
 
 194         handle_didask_tekst = \
 
 195         handle_dlugi_cytat = \
 
 196         handle_dzielo_nadrzedne = \
 
 197         handle_lista_osoba = \
 
 199         handle_miejsce_czas = \
 
 201         handle_motto_podpis = \
 
 202         handle_naglowek_akt = \
 
 203         handle_naglowek_czesc = \
 
 204         handle_naglowek_listy = \
 
 205         handle_naglowek_osoba = \
 
 206         handle_naglowek_scena = \
 
 207         handle_nazwa_utworu = \
 
 213         handle_poezja_cyt = \
 
 216         handle_sekcja_asterysk = \
 
 217         handle_sekcja_swiatlo = \
 
 218         handle_separator_linia = \
 
 219         handle_slowo_obce = \
 
 221         handle_tytul_dziela = \
 
 222         handle_wyroznienie = \
 
 226     def handle_naglowek_rozdzial(self, element):
 
 227         if not self.options['teacher']:
 
 228             if element.text.startswith((u'Wiedza', u'Zadania', u'Słowniczek', u'Dla ucznia')):
 
 229                 self.state['mute'] = False
 
 231                 self.state['mute'] = True
 
 233         return self.handle_texcommand(element)
 
 234     handle_naglowek_rozdzial.unmuter = True
 
 236     def handle_naglowek_podrozdzial(self, element):
 
 237         self.activity_counter = 0
 
 238         if not self.options['teacher']:
 
 239             if element.text.startswith(u'Dla ucznia'):
 
 240                 self.state['mute'] = False
 
 242             elif element.text.startswith(u'Dla nauczyciela'):
 
 243                 self.state['mute'] = True
 
 245             elif self.state['mute']:
 
 247         return self.handle_texcommand(element)
 
 248     handle_naglowek_podrozdzial.unmuter = True
 
 250     def handle_uwaga(self, _e):
 
 253     def handle_extra(self, _e):
 
 256     def handle_nbsp(self, _e):
 
 257         return '<spec cat="tilde" />'
 
 259     _handle_strofa = cmd("strofa")
 
 261     def handle_strofa(self, element):
 
 262         self.options = {'strofa': True}
 
 263         return self._handle_strofa(element)
 
 265     def handle_aktywnosc(self, element):
 
 266         self.activity_counter += 1
 
 269             'activity_counter': self.activity_counter,
 
 272         submill = EduModule(self.options, self.state)
 
 274         if element.xpath('opis'):
 
 275             opis = submill.generate(element.xpath('opis')[0])
 
 279         n = element.xpath('wskazowki')
 
 281             wskazowki = submill.generate(n[0])
 
 284         n = element.xpath('pomoce')
 
 287             pomoce = submill.generate(n[0])
 
 291         forma = ''.join(element.xpath('forma/text()'))
 
 293         czas = ''.join(element.xpath('czas/text()'))
 
 295         counter = self.activity_counter
 
 297         if element.getnext().tag == 'aktywnosc' or (len(self.activity_last) and self.activity_last.getnext() == element):
 
 298             counter_tex = """<cmd name="activitycounter"><parm>%(counter)d.</parm></cmd>""" % locals()
 
 302         self.activity_last = element
 
 305 <cmd name="noindent" />
 
 307 <cmd name="activityinfo"><parm>
 
 308  <cmd name="activitytime"><parm>%(czas)s</parm></cmd>
 
 309  <cmd name="activityform"><parm>%(forma)s</parm></cmd>
 
 310  <cmd name="activitytools"><parm>%(pomoce)s</parm></cmd>
 
 319     handle_opis = ifoption(sub_gen=True)(lambda s, e: ('', ''))
 
 320     handle_wskazowki = ifoption(sub_gen=True)(lambda s, e: ('', ''))
 
 322     @ifoption(sub_gen=True)
 
 323     def handle_pomoce(self, _):
 
 324         return "Pomoce: ", ""
 
 326     def handle_czas(self, *_):
 
 329     def handle_forma(self, *_):
 
 332     def handle_lista(self, element, attrs=None):
 
 333         ltype = element.attrib.get('typ', 'punkt')
 
 334         if not element.findall("punkt"):
 
 335             if ltype == 'czytelnia':
 
 336                 return 'W przygotowaniu.'
 
 339         if ltype == 'slowniczek':
 
 340             surl = element.attrib.get('src', None)
 
 342                 # print '** missing src on <slowniczek>, setting default'
 
 343                 surl = 'http://edukacjamedialna.edu.pl/lekcje/slowniczek/'
 
 344             sxml = etree.fromstring(self.options['wldoc'].provider.by_uri(surl).get_string())
 
 345             self.options = {'slowniczek': True, 'slowniczek_xml': sxml}
 
 351             'slowniczek': 'itemize',
 
 352             'czytelnia': 'itemize'
 
 355         return u'<env name="%s">' % listcmd, u'</env>'
 
 357     def handle_punkt(self, element):
 
 358         return '<cmd name="item"/>', ''
 
 360     def handle_cwiczenie(self, element):
 
 361         exercise_handlers = {
 
 363             'uporzadkuj': Uporzadkuj,
 
 366             'przyporzadkuj': Przyporzadkuj,
 
 367             'prawdafalsz': PrawdaFalsz
 
 370         typ = element.attrib['typ']
 
 371         self.exercise_counter += 1
 
 372         if typ not in exercise_handlers:
 
 373             return '(no handler)'
 
 374         self.options = {'exercise_counter': self.exercise_counter}
 
 375         handler = exercise_handlers[typ](self.options, self.state)
 
 376         return handler.generate(element)
 
 378     # XXX this is copied from pyhtml.py, except for return and
 
 379     # should be refactored for no code duplication
 
 380     def handle_definiendum(self, element):
 
 381         nxt = element.getnext()
 
 384         # let's pull definiens from another document
 
 385         if self.options['slowniczek_xml'] is not None and (nxt is None or nxt.tag != 'definiens'):
 
 386             sxml = self.options['slowniczek_xml']
 
 387             assert element.text != ''
 
 388             if "'" in (element.text or ''):
 
 389                 defloc = sxml.xpath("//definiendum[text()=\"%s\"]" % (element.text or '').strip())
 
 391                 defloc = sxml.xpath("//definiendum[text()='%s']" % (element.text or '').strip())
 
 393                 definiens = defloc[0].getnext()
 
 394                 if definiens.tag == 'definiens':
 
 395                     subgen = EduModule(self.options, self.state)
 
 396                     definiens_s = subgen.generate(definiens)
 
 398         return u'<cmd name="textbf"><parm>', u"</parm></cmd>: " + definiens_s
 
 400     def handle_definiens(self, element):
 
 403     def handle_podpis(self, element):
 
 404         return u"""<env name="figure">""", u"</env>"
 
 406     def handle_tabela(self, element):
 
 408         for w in element.xpath("wiersz"):
 
 410             if max_col < len(ks):
 
 412         self.options = {'columnts': max_col}
 
 414         #     has_frames = int(element.attrib.get("ramki", "0"))
 
 415         #     if has_frames: frames_c = "framed"
 
 416         #     else: frames_c = ""
 
 417         #     return u"""<table class="%s">""" % frames_c, u"</table>"
 
 419 <cmd name="begin"><parm>tabular</parm><parm>%s</parm></cmd>
 
 420     ''' % ('l' * max_col), u'''<cmd name="end"><parm>tabular</parm></cmd>'''
 
 423     def handle_wiersz(self, element):
 
 424         return u"", u'<ctrl ch="\\"/>'
 
 427     def handle_kol(self, element):
 
 428         if element.getnext() is not None:
 
 429             return u"", u'<spec cat="align" />'
 
 432     def handle_link(self, element):
 
 433         if element.attrib.get('url'):
 
 434             url = element.attrib.get('url')
 
 435             if url == element.text:
 
 436                 return cmd('url')(self, element)
 
 438                 return cmd('href', parms=[element.attrib['url']])(self, element)
 
 440             return cmd('emph')(self, element)
 
 442     def handle_obraz(self, element):
 
 443         frmt = self.options['format']
 
 444         name = element.attrib.get('nazwa', '').strip()
 
 445         image = frmt.get_image(name.strip())
 
 446         name = image.get_filename().rsplit('/', 1)[-1]
 
 447         img_path = "obraz/%s" % name.replace("_", "")
 
 448         frmt.attachments[img_path] = image
 
 449         return cmd("obraz", parms=[img_path])(self)
 
 451     def handle_video(self, element):
 
 452         url = element.attrib.get('url')
 
 454             print '!! <video> missing url'
 
 456         m = re.match(r'(?:https?://)?(?:www.)?youtube.com/watch\?(?:.*&)?v=([^&]+)(?:$|&)', url)
 
 458             print '!! unknown <video> url scheme:', url
 
 461         thumb = IOFile.from_string(urlopen("http://img.youtube.com/vi/%s/0.jpg" % name).read())
 
 462         img_path = "video/%s.jpg" % name.replace("_", "")
 
 463         self.options['format'].attachments[img_path] = thumb
 
 464         canon_url = "https://www.youtube.com/watch?v=%s" % name
 
 465         return cmd("video", parms=[img_path, canon_url])(self)
 
 468 class Exercise(EduModule):
 
 469     def __init__(self, *args, **kw):
 
 470         self.question_counter = 0
 
 471         super(Exercise, self).__init__(*args, **kw)
 
 472         self.piece_counter = None
 
 474     handle_rozw_kom = ifoption(teacher=True)(cmd('akap'))
 
 476     def handle_cwiczenie(self, element):
 
 478             'exercise': element.attrib['typ'],
 
 481         self.question_counter = 0
 
 482         self.piece_counter = 0
 
 484         header = etree.Element("parm")
 
 485         header_cmd = etree.Element("cmd", name="naglowekpodrozdzial")
 
 486         header_cmd.append(header)
 
 487         header.text = u"Zadanie %d." % self.options['exercise_counter']
 
 489         pre = etree.tostring(header_cmd, encoding=unicode)
 
 491         # Add a single <pytanie> tag if it's not there
 
 492         if not element.xpath(".//pytanie"):
 
 493             qpre, qpost = self.handle_pytanie(element)
 
 498     def handle_pytanie(self, element):
 
 499         """This will handle <cwiczenie> element, when there is no <pytanie>
 
 501         self.question_counter += 1
 
 502         self.piece_counter = 0
 
 504         if self.options['teacher'] and element.attrib.get('rozw'):
 
 505             post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
 
 508     def handle_punkt(self, element):
 
 509         pre, post = super(Exercise, self).handle_punkt(element)
 
 510         if self.options['teacher'] and element.attrib.get('rozw'):
 
 511             post += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
 
 514     def solution_header(self):
 
 515         par = etree.Element("cmd", name="par")
 
 516         parm = etree.Element("parm")
 
 517         parm.text = u"Rozwiązanie:"
 
 519         return etree.tostring(par)
 
 521     def explicit_solution(self):
 
 522         if self.options['solution']:
 
 523             par = etree.Element("cmd", name="par")
 
 524             parm = etree.Element("parm")
 
 525             parm.text = self.options['solution']
 
 527             return self.solution_header() + etree.tostring(par)
 
 530 class Wybor(Exercise):
 
 531     def handle_cwiczenie(self, element):
 
 532         pre, post = super(Wybor, self).handle_cwiczenie(element)
 
 533         is_single_choice = True
 
 534         pytania = element.xpath(".//pytanie")
 
 538             solutions = re.split(r"[, ]+", p.attrib.get('rozw', ''))
 
 539             if len(solutions) != 1:
 
 540                 is_single_choice = False
 
 542             choices = p.xpath(".//*[@nazwa]")
 
 545                 uniq.add(n.attrib.get('nazwa', ''))
 
 546             if len(choices) != len(uniq):
 
 547                 is_single_choice = False
 
 550         self.options = {'single': is_single_choice}
 
 553     def handle_punkt(self, element):
 
 554         if self.options['exercise'] and element.attrib.get('nazwa', None):
 
 555             cmd = 'radio' if self.options['single'] else 'checkbox'
 
 556             return u'<cmd name="%s"/>' % cmd, ''
 
 558             return super(Wybor, self).handle_punkt(element)
 
 561 class Uporzadkuj(Exercise):
 
 562     def handle_pytanie(self, element):
 
 563         order_items = element.xpath(".//punkt/@rozw")
 
 564         return super(Uporzadkuj, self).handle_pytanie(element)
 
 567 class Przyporzadkuj(Exercise):
 
 568     def handle_lista(self, lista):
 
 569         header = etree.Element("parm")
 
 570         header_cmd = etree.Element("cmd", name="par")
 
 571         header_cmd.append(header)
 
 572         if 'nazwa' in lista.attrib:
 
 573             header.text = u"Kategorie:"
 
 574         elif 'cel' in lista.attrib:
 
 575             header.text = u"Elementy do przyporządkowania:"
 
 577             header.text = u"Lista:"
 
 578         pre, post = super(Przyporzadkuj, self).handle_lista(lista)
 
 579         pre = etree.tostring(header_cmd, encoding=unicode) + pre
 
 583 class Luki(Exercise):
 
 584     def find_pieces(self, question):
 
 585         return question.xpath(".//luka")
 
 587     def solution(self, piece):
 
 588         piece = deepcopy(piece)
 
 591         return sub.generate(piece)
 
 593     def handle_pytanie(self, element):
 
 594         qpre, qpost = super(Luki, self).handle_pytanie(element)
 
 596         luki = self.find_pieces(element)
 
 598         self.words = u"<env name='itemize'>%s</env>" % (
 
 599             "".join("<cmd name='item'/>%s" % self.solution(luka) for luka in luki)
 
 603     def handle_opis(self, element):
 
 604         return '', self.words
 
 606     def handle_luka(self, element):
 
 608         if self.options['teacher']:
 
 609             piece = deepcopy(element)
 
 612             text = sub.generate(piece)
 
 613             luka += u" [rozwiązanie: %s]" % text
 
 618     def find_pieces(self, question):
 
 619         return question.xpath(".//zastap")
 
 621     def solution(self, piece):
 
 622         return piece.attrib.get('rozw', '')
 
 624     def list_header(self):
 
 625         return u"Elementy do wstawienia"
 
 627     def handle_zastap(self, element):
 
 628         piece = deepcopy(element)
 
 631         text = sub.generate(piece)
 
 632         if self.options['teacher'] and element.attrib.get('rozw'):
 
 633             text += u" [rozwiązanie: %s]" % element.attrib.get('rozw')
 
 637 class PrawdaFalsz(Exercise):
 
 638     def handle_punkt(self, element):
 
 639         pre, post = super(PrawdaFalsz, self).handle_punkt(element)
 
 640         if 'rozw' in element.attrib:
 
 641             post += u" [Prawda/Fałsz]"
 
 646     lists = tree.xpath(".//lista")
 
 663 class EduModulePDFFormat(PDFFormat):
 
 664     style = get_resource('res/styles/edumed/pdf/edumed.sty')
 
 667         substitute_hyphens(self.wldoc.edoc)
 
 668         fix_hanging(self.wldoc.edoc)
 
 670         self.attachments = {}
 
 674             "teacher": self.customization.get('teacher'),
 
 676         texml = edumod.generate(fix_lists(self.wldoc.edoc.getroot())).encode('utf-8')
 
 678         open("/tmp/texml.xml", "w").write(texml)
 
 681     def get_tex_dir(self):
 
 682         temp = super(EduModulePDFFormat, self).get_tex_dir()
 
 683         shutil.copy(get_resource('res/styles/edumed/logo.png'), temp)
 
 684         for name, iofile in self.attachments.items():
 
 685             iofile.save_as(os.path.join(temp, name))
 
 688     def get_image(self, name):
 
 689         return self.wldoc.source.attachments[name]