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 from __future__ import with_statement
 
  10 from StringIO import StringIO
 
  11 from tempfile import mkdtemp
 
  13 from copy import deepcopy
 
  14 from subprocess import call, PIPE
 
  18 from Texml.processor import process
 
  19 from lxml import etree
 
  20 from lxml.etree import XMLSyntaxError, XSLTApplyError
 
  22 from librarian.dcparser import Person
 
  23 from librarian.parser import WLDocument
 
  24 from librarian import ParseError, DCNS, get_resource
 
  25 from librarian import functions
 
  28 functions.reg_substitute_entities()
 
  30 functions.reg_starts_white()
 
  31 functions.reg_ends_white()
 
  32 functions.reg_texcommand()
 
  35     'wl2tex': 'xslt/wl2tex.xslt',
 
  39 def insert_tags(doc, split_re, tagname, exclude=None):
 
  40     """ inserts <tagname> for every occurence of `split_re' in text nodes in the `doc' tree 
 
  42     >>> t = etree.fromstring('<a><b>A-B-C</b>X-Y-Z</a>');
 
  43     >>> insert_tags(t, re.compile('-'), 'd');
 
  44     >>> print etree.tostring(t)
 
  45     <a><b>A<d/>B<d/>C</b>X<d/>Y<d/>Z</a>
 
  48     for elem in doc.iter(tag=etree.Element):
 
  49         if exclude and elem.tag in exclude:
 
  52             chunks = split_re.split(elem.text)
 
  53             while len(chunks) > 1:
 
  54                 ins = etree.Element(tagname)
 
  55                 ins.tail = chunks.pop()
 
  57             elem.text = chunks.pop(0)
 
  59             chunks = split_re.split(elem.tail)
 
  60             parent = elem.getparent()
 
  61             ins_index = parent.index(elem) + 1
 
  62             while len(chunks) > 1:
 
  63                 ins = etree.Element(tagname)
 
  64                 ins.tail = chunks.pop()
 
  65                 parent.insert(ins_index, ins)
 
  66             elem.tail = chunks.pop(0)
 
  69 def substitute_hyphens(doc):
 
  71                 re.compile("(?<=[^-\s])-(?=[^-\s])"),
 
  73                 exclude=[DCNS("identifier.url"), DCNS("rights.license")]
 
  79                 re.compile("(?<=\s\w)\s+"),
 
  81                 exclude=[DCNS("identifier.url"), DCNS("rights.license")]
 
  85 def move_motifs_inside(doc):
 
  86     """ moves motifs to be into block elements """
 
  87     for master in doc.xpath('//powiesc|//opowiadanie|//liryka_l|//liryka_lp|//dramat_wierszowany_l|//dramat_wierszowany_lp|//dramat_wspolczesny'):
 
  88         for motif in master.xpath('motyw'):
 
  89             for sib in motif.itersiblings():
 
  90                 if sib.tag not in ('sekcja_swiatlo', 'sekcja_asterysk', 'separator_linia', 'begin', 'end', 'motyw', 'extra', 'uwaga'):
 
  91                     # motif shouldn't have a tail - it would be untagged text
 
  93                     motif.getparent().remove(motif)
 
  99     """ dirty hack for the marginpar-creates-orphans LaTeX problem
 
 100     see http://www.latex-project.org/cgi-bin/ltxbugs2html?pr=latex/2304
 
 102     moves motifs in stanzas from first verse to second
 
 103     and from next to last to last, then inserts negative vspace before them
 
 105     for motif in doc.findall('//strofa//motyw'):
 
 106         # find relevant verse-level tag
 
 107         verse, stanza = motif, motif.getparent()
 
 108         while stanza is not None and stanza.tag != 'strofa':
 
 109             verse, stanza = stanza, stanza.getparent()
 
 110         breaks_before = sum(1 for i in verse.itersiblings('br', preceding=True))
 
 111         breaks_after = sum(1 for i in verse.itersiblings('br'))
 
 112         if (breaks_before == 0 and breaks_after > 0) or breaks_after == 1:
 
 114             if breaks_after == 2:
 
 116             moved_motif = deepcopy(motif)
 
 119             moved_motif.tail = None
 
 120             moved_motif.set('moved', str(move_by))
 
 122             for br in verse.itersiblings('br'):
 
 126                 br.addnext(moved_motif)
 
 130 def parse_creator(doc):
 
 131     """ find all dc:creator tags and add dc:creator_parsed with forenames first """
 
 132     for creator in doc.findall('//'+DCNS('creator')):
 
 133         p = Person.from_text(creator.text)
 
 134         creator_parsed = deepcopy(creator)
 
 135         creator_parsed.tag = DCNS('creator_parsed')
 
 136         creator_parsed.text = ' '.join(p.first_names + (p.last_name,))
 
 137         creator.getparent().insert(0, creator_parsed)
 
 140 def get_stylesheet(name):
 
 141     return get_resource(STYLESHEETS[name])
 
 144 def package_available(package, args='', verbose=False):
 
 145     """ check if a verion of a latex package accepting given args is available """  
 
 146     tempdir = mkdtemp('-wl2pdf-test')
 
 147     fpath = os.path.join(tempdir, 'test.tex')
 
 154         """ % (args, package))
 
 157         p = call(['xelatex', '-output-directory', tempdir, fpath])
 
 159         p = call(['xelatex', '-interaction=batchmode', '-output-directory', tempdir, fpath], stdout=PIPE, stderr=PIPE)
 
 160     shutil.rmtree(tempdir)
 
 164 def transform(provider, slug=None, file_path=None, 
 
 165               output_file=None, output_dir=None, make_dir=False, verbose=False, save_tex=None, morefloats=None):
 
 166     """ produces a PDF file with XeLaTeX
 
 168     provider: a DocProvider
 
 169     slug: slug of file to process, available by provider
 
 170     file_path can be provided instead of a slug
 
 171     output_file: file-like object or path to output file
 
 172     output_dir: path to directory to save output file to; either this or output_file must be present
 
 173     make_dir: writes output to <output_dir>/<author>/<slug>.pdf istead of <output_dir>/<slug>.pdf
 
 174     verbose: prints all output from LaTeX
 
 175     save_tex: path to save the intermediary LaTeX file to
 
 176     morefloats (old/new/none): force specific morefloats
 
 183                 raise ValueError('slug or file_path should be specified, not both')
 
 184             document = load_including_children(provider, file_path=file_path)
 
 187                 raise ValueError('either slug or file_path should be specified')
 
 188             document = load_including_children(provider, slug=slug)
 
 190         # check for LaTeX packages
 
 192             document.edoc.getroot().set('morefloats', morefloats.lower())
 
 193         elif package_available('morefloats', 'maxfloats=19'):
 
 194             document.edoc.getroot().set('morefloats', 'new')
 
 197         move_motifs_inside(document.edoc)
 
 198         hack_motifs(document.edoc)
 
 199         parse_creator(document.edoc)
 
 200         substitute_hyphens(document.edoc)
 
 201         fix_hanging(document.edoc)
 
 204         if make_dir and output_dir is not None:
 
 205             author = unicode(document.book_info.author)
 
 206             output_dir = os.path.join(output_dir, author)
 
 209         style_filename = get_stylesheet("wl2tex")
 
 210         style = etree.parse(style_filename)
 
 211         texml = document.transform(style)
 
 212         del document # no longer needed large object :)
 
 215         temp = mkdtemp('-wl2pdf')
 
 216         tex_path = os.path.join(temp, 'doc.tex')
 
 217         fout = open(tex_path, 'w')
 
 218         process(StringIO(texml), fout, 'utf-8')
 
 223             shutil.copy(tex_path, save_tex)
 
 226         shutil.copy(get_resource('pdf/wl.sty'), temp)
 
 227         shutil.copy(get_resource('pdf/wl-logo.png'), temp)
 
 233             p = call(['xelatex', tex_path])
 
 235             p = call(['xelatex', '-interaction=batchmode', tex_path], stdout=PIPE, stderr=PIPE)
 
 237             raise ParseError("Error parsing .tex file")
 
 242         pdf_path = os.path.join(temp, 'doc.pdf')
 
 243         if output_dir is not None:
 
 245                 os.makedirs(output_dir)
 
 249                 output_path = os.path.join(output_dir, '%s.pdf' % slug)
 
 251                 output_path = os.path.join(output_dir, os.path.splitext(os.path.basename(file_path))[0] + '.pdf')
 
 252             shutil.move(pdf_path, output_path)
 
 254             if hasattr(output_file, 'write'):
 
 256                 with open(pdf_path) as f:
 
 257                     output_file.write(f.read())
 
 260                 # path to output file
 
 261                 shutil.copy(pdf_path, output_file)
 
 264     except (XMLSyntaxError, XSLTApplyError), e:
 
 268 def load_including_children(provider, slug=None, uri=None, file_path=None):
 
 269     """ makes one big xml file with children inserted at end 
 
 270     either slug or uri must be provided
 
 274         f = provider.by_uri(uri)
 
 278         f = open(file_path, 'r')
 
 280         raise ValueError('Neither slug, URI nor file path provided for a book.')
 
 282     text = f.read().decode('utf-8')
 
 283     text = re.sub(ur"([\u0400-\u04ff]+)", ur"<alien>\1</alien>", text)
 
 285     document = WLDocument.from_string(text, True,
 
 286         parse_dublincore=True)
 
 290     for child_uri in document.book_info.parts:
 
 291         child = load_including_children(provider, uri=child_uri)
 
 292         document.edoc.getroot().append(child.edoc.getroot())