1 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
4 from collections import Counter
8 from xml.parsers.expat import ExpatError
10 from lxml.etree import XMLSyntaxError, XSLTApplyError
11 from librarian import ValidationError, NoDublinCore, ParseError, NoProvider
12 from librarian import RDFNS
13 from librarian.cover import make_cover
14 from librarian import dcparser
15 from .elements import WL_ELEMENTS
18 class WLElementLookup(etree.CustomElementClassLookup):
19 def lookup(self, node_type, document, namespace, name):
20 if node_type != 'element':
25 return WL_ELEMENTS[name]
30 parser = etree.XMLParser()
31 parser.set_element_class_lookup(
38 """Legacy class, to be replaced with documents.WLDocument."""
39 LINE_SWAP_EXPR = re.compile(r'/\s', re.MULTILINE | re.UNICODE)
42 def __init__(self, edoc, parse_dublincore=True, provider=None,
43 strict=False, meta_fallbacks=None):
45 self.provider = provider
47 root_elem = edoc.getroot()
49 dc_path = './/' + RDFNS('RDF')
51 if root_elem.tag != 'utwor':
52 raise ValidationError(
53 "Invalid root element. Found '%s', should be 'utwor'"
58 self.rdf_elem = root_elem.find(dc_path)
60 if self.rdf_elem is None:
62 "Document must have a '%s' element." % RDFNS('RDF')
65 self.book_info = dcparser.BookInfo.from_element(
66 self.rdf_elem, fallbacks=meta_fallbacks, strict=strict)
70 def get_statistics(self):
71 def count_text(text, counter, in_fn=False, stanza=False):
73 text = re.sub(r'\s+', ' ', text)
75 chars = len(text) if text.strip() else 0
76 words = len(text.split()) if text.strip() else 0
78 counter['chars_with_fn'] += chars
79 counter['words_with_fn'] += words
81 counter['chars'] += chars
82 counter['words'] += words
84 counter['chars_out_verse_with_fn'] += chars
86 counter['chars_out_verse'] += chars
88 def count(elem, counter, in_fn=False, stanza=False):
89 if elem.tag in (RDFNS('RDF'), 'nota_red', 'abstrakt', 'uwaga', 'ekstra'):
91 if not in_fn and elem.tag in ('pa', 'pe', 'pr', 'pt', 'motyw'):
93 if elem.tag == 'strofa':
95 verses = len(elem.findall('.//br')) + 1
96 counter['verses_with_fn'] += verses
98 counter['verses'] += verses
100 count_text(elem.text, counter, in_fn=in_fn, stanza=stanza)
102 count(child, counter, in_fn=in_fn, stanza=stanza)
103 count_text(child.tail, counter, in_fn=in_fn, stanza=stanza)
114 count(self.edoc.getroot(), data['self'])
115 for k, v in data['self'].items():
118 for part in self.parts(pass_part_errors=True):
119 if isinstance(part, Exception):
120 data['parts'].append((None, {}))
122 data['parts'].append((part, part.get_statistics()))
123 for k, v in data['parts'][-1][1]['total'].items():
124 data['total'][k] = data['total'].get(k, 0) + v
129 def from_bytes(cls, xml, *args, **kwargs):
130 return cls.from_file(io.BytesIO(xml), *args, **kwargs)
133 def from_file(cls, xmlfile, *args, **kwargs):
135 # first, prepare for parsing
136 if isinstance(xmlfile, str):
137 file = open(xmlfile, 'rb')
143 data = xmlfile.read()
145 if not isinstance(data, str):
146 data = data.decode('utf-8')
148 data = data.replace('\ufeff', '')
151 parser = etree.XMLParser(remove_blank_text=False)
152 tree = etree.parse(io.BytesIO(data.encode('utf-8')), parser)
154 return cls(tree, *args, **kwargs)
155 except (ExpatError, XMLSyntaxError, XSLTApplyError) as e:
158 def swap_endlines(self):
159 """Converts line breaks in stanzas into <br/> tags."""
160 # only swap inside stanzas
161 for elem in self.edoc.iter('strofa'):
162 for child in list(elem):
164 chunks = self.LINE_SWAP_EXPR.split(child.tail)
165 ins_index = elem.index(child) + 1
166 while len(chunks) > 1:
167 ins = etree.Element('br')
168 ins.tail = chunks.pop()
169 elem.insert(ins_index, ins)
170 child.tail = chunks.pop(0)
172 chunks = self.LINE_SWAP_EXPR.split(elem.text)
173 while len(chunks) > 1:
174 ins = etree.Element('br')
175 ins.tail = chunks.pop()
177 elem.text = chunks.pop(0)
179 def parts(self, pass_part_errors=False):
180 if self.provider is None:
181 raise NoProvider('No document provider supplied.')
182 if self.book_info is None:
183 raise NoDublinCore('No Dublin Core in document.')
184 for part_uri in self.book_info.parts:
186 with self.provider.by_slug(part_uri.slug) as f:
187 yield self.from_file(f, provider=self.provider)
188 except Exception as e:
194 def chunk(self, path):
195 # convert the path to XPath
196 expr = self.path_to_xpath(path)
197 elems = self.edoc.xpath(expr)
204 def path_to_xpath(self, path):
207 for part in path.split('/'):
208 match = re.match(r'([^\[]+)\[(\d+)\]', part)
212 tag, n = match.groups()
213 parts.append("*[%d][name() = '%s']" % (int(n)+1, tag))
218 return '/'.join(parts)
220 def transform(self, stylesheet, **options):
221 return self.edoc.xslt(stylesheet, **options)
225 parent = self.rdf_elem.getparent()
226 parent.replace(self.rdf_elem, self.book_info.to_etree(parent))
230 return etree.tostring(self.edoc, encoding='unicode', pretty_print=True)
232 def merge_chunks(self, chunk_dict):
235 for key, data in chunk_dict.iteritems():
237 xpath = self.path_to_xpath(key)
238 node = self.edoc.xpath(xpath)[0]
239 repl = etree.fromstring(
240 "<%s>%s</%s>" % (node.tag, data, node.tag)
242 node.getparent().replace(node, repl)
243 except Exception as e:
244 unmerged.append(repr((key, xpath, e)))
248 def clean_ed_note(self, note_tag='nota_red'):
249 """ deletes forbidden tags from nota_red """
251 for node in self.edoc.xpath('|'.join(
252 '//%s//%s' % (note_tag, tag) for tag in
253 ('pa', 'pe', 'pr', 'pt', 'begin', 'end', 'motyw'))):
259 def fix_pa_akap(self):
260 for pa in ('pa','pe','pr','pt'):
261 for akap in self.edoc.findall(f'//{pa}/akap'):
262 akap.getparent().set('blocks', 'true')
263 if not akap.getparent().index(akap):
264 akap.set('inline', 'true')
267 """Returns a set of all editors for book and its children.
269 :returns: set of dcparser.Person objects
271 if self.book_info is None:
272 raise NoDublinCore('No Dublin Core in document.')
273 persons = set(self.book_info.editors
274 + self.book_info.technical_editors)
275 for child in self.parts():
276 persons.update(child.editors())
283 def as_html(self, *args, **kwargs):
284 from librarian import html
285 return html.transform(self, *args, **kwargs)
287 def as_text(self, *args, **kwargs):
288 from librarian import text
289 return text.transform(self, *args, **kwargs)
291 def as_pdf(self, *args, **kwargs):
292 from librarian import pdf
293 return pdf.transform(self, *args, **kwargs)
295 def as_fb2(self, *args, **kwargs):
296 from librarian import fb2
297 return fb2.transform(self, *args, **kwargs)
299 def as_cover(self, cover_class=None, *args, **kwargs):
300 if cover_class is None:
301 cover_class = make_cover
302 return cover_class(self.book_info, *args, **kwargs).output_file()
305 def latex_dir(self, *args, **kwargs):
306 kwargs['latex_dir'] = True
307 from librarian import pdf
308 return pdf.transform(self, *args, **kwargs)
310 def save_output_file(self, output_file, output_path=None,
311 output_dir_path=None, make_author_dir=False,
314 save_path = output_dir_path
316 save_path = os.path.join(
318 str(self.book_info.author).encode('utf-8')
320 save_path = os.path.join(save_path, self.book_info.url.slug)
322 save_path += '.%s' % ext
324 save_path = output_path
326 output_file.save_as(save_path)