Add swappable CSS for HTML previews.
[librarian.git] / librarian / parser.py
old mode 100755 (executable)
new mode 100644 (file)
index 225000b..efe6e95
@@ -1,45 +1,37 @@
 # -*- coding: utf-8 -*-
 #
 # -*- coding: utf-8 -*-
 #
-#    This file is part of Librarian.
+# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 #
-#    Copyright © 2008,2009,2010 Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>
-#    
-#    For full list of contributors see AUTHORS file. 
-#
-#    This program is free software: you can redistribute it and/or modify
-#    it under the terms of the GNU Affero General Public License as published by
-#    the Free Software Foundation, either version 3 of the License, or
-#    (at your option) any later version.
-#
-#    This program is distributed in the hope that it will be useful,
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#    GNU Affero General Public License for more details.
-#
-#    You should have received a copy of the GNU Affero General Public License
-#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-from librarian import ValidationError, NoDublinCore,  ParseError
+from __future__ import unicode_literals
+
+from librarian import ValidationError, NoDublinCore,  ParseError, NoProvider
 from librarian import RDFNS
 from librarian import RDFNS
+from librarian.cover import make_cover
 from librarian import dcparser
 
 from xml.parsers.expat import ExpatError
 from lxml import etree
 from lxml.etree import XMLSyntaxError, XSLTApplyError
 
 from librarian import dcparser
 
 from xml.parsers.expat import ExpatError
 from lxml import etree
 from lxml.etree import XMLSyntaxError, XSLTApplyError
 
+import os
 import re
 import re
-from StringIO import StringIO
+import six
+
 
 class WLDocument(object):
 
 class WLDocument(object):
-    LINE_SWAP_EXPR = re.compile(r'/\s', re.MULTILINE | re.UNICODE);
+    LINE_SWAP_EXPR = re.compile(r'/\s', re.MULTILINE | re.UNICODE)
+    provider = None
 
 
-    def __init__(self, edoc, parse_dublincore=True):
+    def __init__(self, edoc, parse_dublincore=True, provider=None, 
+                 strict=False, meta_fallbacks=None):
         self.edoc = edoc
         self.edoc = edoc
+        self.provider = provider
 
         root_elem = edoc.getroot()
 
         root_elem = edoc.getroot()
-       
+
         dc_path = './/' + RDFNS('RDF')
         dc_path = './/' + RDFNS('RDF')
-        
+
         if root_elem.tag != 'utwor':
             raise ValidationError("Invalid root element. Found '%s', should be 'utwor'" % root_elem.tag)
 
         if root_elem.tag != 'utwor':
             raise ValidationError("Invalid root element. Found '%s', should be 'utwor'" % root_elem.tag)
 
@@ -47,21 +39,22 @@ class WLDocument(object):
             self.rdf_elem = root_elem.find(dc_path)
 
             if self.rdf_elem is None:
             self.rdf_elem = root_elem.find(dc_path)
 
             if self.rdf_elem is None:
-                raise NoDublinCore('Document has no DublinCore - which is required.')
-            
-            self.book_info = dcparser.BookInfo.from_element(self.rdf_elem)
+                raise NoDublinCore("Document must have a '%s' element." % RDFNS('RDF'))
+
+            self.book_info = dcparser.BookInfo.from_element(
+                    self.rdf_elem, fallbacks=meta_fallbacks, strict=strict)
         else:
             self.book_info = None
         else:
             self.book_info = None
-    
+
     @classmethod
     @classmethod
-    def from_string(cls, xml, swap_endlines=False, parse_dublincore=True):
-        return cls.from_file(StringIO(xml), swap_endlines, parse_dublincore=parse_dublincore)
+    def from_bytes(cls, xml, *args, **kwargs):
+        return cls.from_file(six.BytesIO(xml), *args, **kwargs)
 
     @classmethod
 
     @classmethod
-    def from_file(cls, xmlfile, swap_endlines=False, parse_dublincore=True):
+    def from_file(cls, xmlfile, *args, **kwargs):
 
         # first, prepare for parsing
 
         # first, prepare for parsing
-        if isinstance(xmlfile, basestring):
+        if isinstance(xmlfile, six.text_type):
             file = open(xmlfile, 'rb')
             try:
                 data = file.read()
             file = open(xmlfile, 'rb')
             try:
                 data = file.read()
@@ -70,26 +63,56 @@ class WLDocument(object):
         else:
             data = xmlfile.read()
 
         else:
             data = xmlfile.read()
 
-        if not isinstance(data, unicode):
+        if not isinstance(data, six.text_type):
             data = data.decode('utf-8')
 
             data = data.decode('utf-8')
 
-        if swap_endlines:
-            data = cls.LINE_SWAP_EXPR.sub(u'<br />\n', data)
-    
+        data = data.replace(u'\ufeff', '')
+
         try:
             parser = etree.XMLParser(remove_blank_text=False)
         try:
             parser = etree.XMLParser(remove_blank_text=False)
-            return cls(etree.parse(StringIO(data), parser), parse_dublincore=parse_dublincore)
-        except (ExpatError, XMLSyntaxError, XSLTApplyError), e:
-            raise ParseError(e)                  
+            tree = etree.parse(six.BytesIO(data.encode('utf-8')), parser)
+
+            return cls(tree, *args, **kwargs)
+        except (ExpatError, XMLSyntaxError, XSLTApplyError) as e:
+            raise ParseError(e)
+
+    def swap_endlines(self):
+        """Converts line breaks in stanzas into <br/> tags."""
+        # only swap inside stanzas
+        for elem in self.edoc.iter('strofa'):
+            for child in list(elem):
+                if child.tail:
+                    chunks = self.LINE_SWAP_EXPR.split(child.tail)
+                    ins_index = elem.index(child) + 1
+                    while len(chunks) > 1:
+                        ins = etree.Element('br')
+                        ins.tail = chunks.pop()
+                        elem.insert(ins_index, ins)
+                    child.tail = chunks.pop(0)
+            if elem.text:
+                chunks = self.LINE_SWAP_EXPR.split(elem.text)
+                while len(chunks) > 1:
+                    ins = etree.Element('br')
+                    ins.tail = chunks.pop()
+                    elem.insert(0, ins)
+                elem.text = chunks.pop(0)
+
+    def parts(self):
+        if self.provider is None:
+            raise NoProvider('No document provider supplied.')
+        if self.book_info is None:
+            raise NoDublinCore('No Dublin Core in document.')
+        for part_uri in self.book_info.parts:
+            yield self.from_file(self.provider.by_uri(part_uri), provider=self.provider)
 
     def chunk(self, path):
 
     def chunk(self, path):
-        # convert the path to XPath        
+        # convert the path to XPath
         expr = self.path_to_xpath(path)
         elems = self.edoc.xpath(expr)
 
         if len(elems) == 0:
             return None
         expr = self.path_to_xpath(path)
         elems = self.edoc.xpath(expr)
 
         if len(elems) == 0:
             return None
-        else:        
+        else:
             return elems[0]
 
     def path_to_xpath(self, path):
             return elems[0]
 
     def path_to_xpath(self, path):
@@ -101,7 +124,7 @@ class WLDocument(object):
                 parts.append(part)
             else:
                 tag, n = match.groups()
                 parts.append(part)
             else:
                 tag, n = match.groups()
-                parts.append("*[%d][name() = '%s']" % (int(n)+1, tag) )
+                parts.append("*[%d][name() = '%s']" % (int(n)+1, tag))
 
         if parts[0] == '.':
             parts[0] = ''
 
         if parts[0] == '.':
             parts[0] = ''
@@ -114,11 +137,11 @@ class WLDocument(object):
     def update_dc(self):
         if self.book_info:
             parent = self.rdf_elem.getparent()
     def update_dc(self):
         if self.book_info:
             parent = self.rdf_elem.getparent()
-            parent.replace( self.rdf_elem, self.book_info.to_etree(parent) )
+            parent.replace(self.rdf_elem, self.book_info.to_etree(parent))
 
     def serialize(self):
         self.update_dc()
 
     def serialize(self):
         self.update_dc()
-        return etree.tostring(self.edoc, encoding=unicode, pretty_print=True)
+        return etree.tostring(self.edoc, encoding='unicode', pretty_print=True)
 
     def merge_chunks(self, chunk_dict):
         unmerged = []
 
     def merge_chunks(self, chunk_dict):
         unmerged = []
@@ -127,9 +150,83 @@ class WLDocument(object):
             try:
                 xpath = self.path_to_xpath(key)
                 node = self.edoc.xpath(xpath)[0]
             try:
                 xpath = self.path_to_xpath(key)
                 node = self.edoc.xpath(xpath)[0]
-                repl = etree.fromstring(u"<%s>%s</%s>" %(node.tag, data, node.tag) )
-                node.getparent().replace(node, repl);
-            except Exception, e:
-                unmerged.append( repr( (key, xpath, e) ) )
+                repl = etree.fromstring(u"<%s>%s</%s>" % (node.tag, data, node.tag))
+                node.getparent().replace(node, repl)
+            except Exception as e:
+                unmerged.append(repr((key, xpath, e)))
+
+        return unmerged
+
+    def clean_ed_note(self, note_tag='nota_red'):
+        """ deletes forbidden tags from nota_red """
+
+        for node in self.edoc.xpath('|'.join('//%s//%s' % (note_tag, tag) for tag in
+                                    ('pa', 'pe', 'pr', 'pt', 'begin', 'end', 'motyw'))):
+            tail = node.tail
+            node.clear()
+            node.tag = 'span'
+            node.tail = tail
+
+    def editors(self):
+        """Returns a set of all editors for book and its children.
+
+        :returns: set of dcparser.Person objects
+        """
+        if self.book_info is None:
+            raise NoDublinCore('No Dublin Core in document.')
+        persons = set(self.book_info.editors + self.book_info.technical_editors)
+        for child in self.parts():
+            persons.update(child.editors())
+        if None in persons:
+            persons.remove(None)
+        return persons
+
+    # Converters
+
+    def as_html(self, *args, **kwargs):
+        from librarian import html
+        return html.transform(self, *args, **kwargs)
+
+    def as_text(self, *args, **kwargs):
+        from librarian import text
+        return text.transform(self, *args, **kwargs)
+
+    def as_epub(self, *args, **kwargs):
+        from librarian import epub
+        return epub.transform(self, *args, **kwargs)
+
+    def as_pdf(self, *args, **kwargs):
+        from librarian import pdf
+        return pdf.transform(self, *args, **kwargs)
+
+    def as_mobi(self, *args, **kwargs):
+        from librarian import mobi
+        return mobi.transform(self, *args, **kwargs)
+
+    def as_fb2(self, *args, **kwargs):
+        from librarian import fb2
+        return fb2.transform(self, *args, **kwargs)
+
+    def as_cover(self, cover_class=None, *args, **kwargs):
+        if cover_class is None:
+            cover_class = make_cover
+        return cover_class(self.book_info, *args, **kwargs).output_file()
+
+    # for debugging only
+    def latex_dir(self, *args, **kwargs):
+        kwargs['latex_dir'] = True
+        from librarian import pdf
+        return pdf.transform(self, *args, **kwargs)
+
+    def save_output_file(self, output_file, output_path=None, output_dir_path=None, make_author_dir=False, ext=None):
+        if output_dir_path:
+            save_path = output_dir_path
+            if make_author_dir:
+                save_path = os.path.join(save_path, six.text_type(self.book_info.author).encode('utf-8'))
+            save_path = os.path.join(save_path, self.book_info.url.slug)
+            if ext:
+                save_path += '.%s' % ext
+        else:
+            save_path = output_path
 
 
-        return unmerged
\ No newline at end of file
+        output_file.save_as(save_path)