xml2rfc 3.26.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. xml2rfc/__init__.py +68 -0
  2. xml2rfc/boilerplate_id_guidelines.py +28 -0
  3. xml2rfc/boilerplate_rfc_7841.py +575 -0
  4. xml2rfc/boilerplate_tlp.py +258 -0
  5. xml2rfc/data/SVG-1.2-RFC.rnc +1666 -0
  6. xml2rfc/data/SVG-1.2-RFC.rng +7460 -0
  7. xml2rfc/data/Scripts.txt +2693 -0
  8. xml2rfc/data/metadata.js +218 -0
  9. xml2rfc/data/metadata.min.js +1 -0
  10. xml2rfc/data/reference.rnc +955 -0
  11. xml2rfc/data/reference.rng +2158 -0
  12. xml2rfc/data/referencegroup.rnc +16 -0
  13. xml2rfc/data/referencegroup.rng +2180 -0
  14. xml2rfc/data/rfc7991.rnc +955 -0
  15. xml2rfc/data/rfc7991.rng +2143 -0
  16. xml2rfc/data/v2.rnc +309 -0
  17. xml2rfc/data/v2.rng +720 -0
  18. xml2rfc/data/v3.rnc +1144 -0
  19. xml2rfc/data/v3.rng +2538 -0
  20. xml2rfc/data/v3compat.dtd +630 -0
  21. xml2rfc/data/v3compat.rnc +985 -0
  22. xml2rfc/data/xml2rfc-v3-emacs-nxml.rnc +1083 -0
  23. xml2rfc/data/xml2rfc.css +1171 -0
  24. xml2rfc/data/xml2rfc.js +7 -0
  25. xml2rfc/debug.py +221 -0
  26. xml2rfc/log.py +54 -0
  27. xml2rfc/parser.py +815 -0
  28. xml2rfc/run.py +801 -0
  29. xml2rfc/scripts.py +52 -0
  30. xml2rfc/strings.py +26 -0
  31. xml2rfc/templates/address_card.html +21 -0
  32. xml2rfc/templates/base.html +415 -0
  33. xml2rfc/templates/doc.xml +367 -0
  34. xml2rfc/templates/doc.yaml +2138 -0
  35. xml2rfc/templates/rfc2629-other.ent +61 -0
  36. xml2rfc/templates/rfc2629-xhtml.ent +170 -0
  37. xml2rfc/templates/rfc2629.dtd +328 -0
  38. xml2rfc/uniscripts/__init__.py +97 -0
  39. xml2rfc/uniscripts/unidata.py +306 -0
  40. xml2rfc/uniscripts/update/make_unidata.py +94 -0
  41. xml2rfc/util/__init__.py +7 -0
  42. xml2rfc/util/date.py +94 -0
  43. xml2rfc/util/fonts.py +53 -0
  44. xml2rfc/util/name.py +229 -0
  45. xml2rfc/util/num.py +119 -0
  46. xml2rfc/util/postal.py +285 -0
  47. xml2rfc/util/unicode.py +441 -0
  48. xml2rfc/utils.py +672 -0
  49. xml2rfc/walkpdf.py +113 -0
  50. xml2rfc/writers/__init__.py +25 -0
  51. xml2rfc/writers/base.py +2248 -0
  52. xml2rfc/writers/bib.py +43 -0
  53. xml2rfc/writers/doc.py +354 -0
  54. xml2rfc/writers/expand.py +51 -0
  55. xml2rfc/writers/expanded_xml.py +39 -0
  56. xml2rfc/writers/html.py +3110 -0
  57. xml2rfc/writers/legacy_html.py +860 -0
  58. xml2rfc/writers/nroff.py +345 -0
  59. xml2rfc/writers/paginated_txt.py +432 -0
  60. xml2rfc/writers/pdf.py +230 -0
  61. xml2rfc/writers/preptool.py +2428 -0
  62. xml2rfc/writers/raw_txt.py +1453 -0
  63. xml2rfc/writers/text.py +5000 -0
  64. xml2rfc/writers/unprep.py +187 -0
  65. xml2rfc/writers/v2v3.py +1150 -0
  66. xml2rfc-3.26.0.dist-info/LICENSE +29 -0
  67. xml2rfc-3.26.0.dist-info/METADATA +178 -0
  68. xml2rfc-3.26.0.dist-info/RECORD +71 -0
  69. xml2rfc-3.26.0.dist-info/WHEEL +5 -0
  70. xml2rfc-3.26.0.dist-info/entry_points.txt +2 -0
  71. xml2rfc-3.26.0.dist-info/top_level.txt +1 -0
xml2rfc/parser.py ADDED
@@ -0,0 +1,815 @@
1
+ # --------------------------------------------------
2
+ # Copyright The IETF Trust 2011, All Rights Reserved
3
+ # --------------------------------------------------
4
+
5
+ """ Public XML parser module """
6
+
7
+ import base64
8
+ import hashlib
9
+ import io
10
+ import lxml.etree
11
+ import os
12
+ import re
13
+ import requests
14
+ import shutil
15
+ import time
16
+ import xml2rfc.log
17
+ import xml2rfc.utils
18
+
19
+ from xml2rfc.writers import base
20
+
21
+ try:
22
+ from urllib.parse import urlparse, urljoin, urlsplit
23
+ except ImportError:
24
+ from urlparse import urlparse, urljoin, urlsplit
25
+
26
+ try:
27
+ from xml2rfc import debug
28
+ assert debug
29
+ except ImportError:
30
+ pass
31
+
32
+ __all__ = ['XmlRfcParser', 'XmlRfc', 'XmlRfcError']
33
+
34
+ class XmlRfcError(Exception):
35
+ """ Application XML errors with positional information
36
+
37
+ This class attempts to mirror the API of lxml's error class
38
+ """
39
+ def __init__(self, msg, filename=None, line_no=0):
40
+ self.msg = msg
41
+ # This mirrors lxml error behavior, but we can't capture column
42
+ self.position = (line_no, 0)
43
+ # Also match lxml.etree._LogEntry attributes:
44
+ self.message = msg
45
+ self.filename = filename
46
+ self.line = line_no
47
+
48
+ def __str__(self):
49
+ return self.msg
50
+
51
+ class CachingResolver(lxml.etree.Resolver):
52
+ """ Custom ENTITY request handler that uses a local cache """
53
+ def __init__(self, cache_path=None, library_dirs=None, source=None,
54
+ templates_path=base.default_options.template_dir, verbose=None, quiet=None,
55
+ no_network=None, network_locs= [
56
+ 'https://bib.ietf.org/public/rfc/',
57
+ ],
58
+ rfc_number=None, options=base.default_options):
59
+ self.quiet = quiet if quiet != None else options.quiet
60
+ self.verbose = verbose if verbose != None else options.verbose
61
+ self.no_network = no_network if no_network != None else options.no_network
62
+ self.cache_path = cache_path or options.cache
63
+ self.source = source
64
+ self.library_dirs = library_dirs
65
+ self.templates_path = templates_path
66
+ self.network_locs = network_locs
67
+ self.include = False
68
+ self.rfc_number = rfc_number
69
+ self.cache_refresh_secs = (60*60*24*14) # 14 days
70
+ self.options = options
71
+
72
+ # Get directory of source
73
+ if self.source:
74
+ if isinstance(self.source, str):
75
+ self.source_dir = os.path.abspath(os.path.dirname(self.source))
76
+ else:
77
+ self.source_dir = os.path.abspath(os.path.dirname(self.source.name))
78
+ else:
79
+ self.source_dir = None
80
+
81
+ # Determine cache directories to read/write to
82
+ self.read_caches = [ os.path.expanduser(path) for path in xml2rfc.CACHES ]
83
+ self.write_cache = None
84
+ if self.cache_path:
85
+ # Explicit directory given, set as first directory in read_caches
86
+ self.read_caches.insert(0, self.cache_path)
87
+ # Try to find a valid directory to write to by stepping through
88
+ # Read caches one by one
89
+ for dir in self.read_caches:
90
+ if os.path.exists(dir) and os.access(dir, os.W_OK):
91
+ self.write_cache = dir
92
+ break
93
+ else:
94
+ try:
95
+ os.makedirs(dir)
96
+ xml2rfc.log.note('Created cache directory at', dir)
97
+ self.write_cache = dir
98
+ except OSError:
99
+ # Can't write to this directory, try the next one
100
+ pass
101
+ if not self.write_cache:
102
+ xml2rfc.log.warn('Unable to find a suitable cache directory to '
103
+ 'write to, trying the following directories:\n ',
104
+ '\n '.join(self.read_caches),
105
+ '\nTry giving a specific directory with --cache.')
106
+ else:
107
+ # Create the prefix directory if it doesnt exist
108
+ pdir = os.path.join(self.write_cache, xml2rfc.CACHE_PREFIX)
109
+ if not os.path.exists(pdir):
110
+ os.makedirs(pdir)
111
+
112
+ self.sessions = {}
113
+
114
+ def delete_cache(self, path=None):
115
+ # Explicit path given?
116
+ caches = path and [path] or self.read_caches
117
+ for dir in caches:
118
+ path = os.path.join(dir, xml2rfc.CACHE_PREFIX)
119
+ if os.access(path, os.W_OK):
120
+ shutil.rmtree(path)
121
+ xml2rfc.log.note('Deleted cache directory at', path)
122
+
123
+
124
+ def resolve(self, request, public_id, context):
125
+ """ Called internally by lxml """
126
+ if not request:
127
+ # Not sure why but sometimes LXML will ask for an empty request,
128
+ # So lets give it an empty response.
129
+ return None
130
+ # If the source itself is requested, return as-is
131
+ if request == self.source:
132
+ return self.resolve_filename(request, context)
133
+ if request == u"internal:/rfc.number":
134
+ if self.rfc_number:
135
+ return self.resolve_string(self.rfc_number, context)
136
+ return self.resolve_string("XXXX", context)
137
+ # Warn if .ent file is referred.
138
+ if request.endswith('.ent'):
139
+ xml2rfc.log.warn('{} is no longer needed as the special processing of non-ASCII characters has been superseded by direct support for non-ASCII characters in RFCXML.'.format(request))
140
+ url = urlparse(request)
141
+ if not url.netloc or url.scheme == 'file':
142
+ if request.startswith("file://"):
143
+ request = request[7:]
144
+ if not self.options.allow_local_file_access:
145
+ path = self.getReferenceRequest(request)
146
+ abspath = os.path.dirname(os.path.abspath(path))
147
+ if not (abspath == self.options.template_dir or
148
+ abspath == self.source_dir):
149
+ error = 'Can not access local file: {}'.format(request)
150
+ xml2rfc.log.error(error)
151
+ raise XmlRfcError(error)
152
+ try:
153
+ path = self.getReferenceRequest(request)
154
+ return self.resolve_filename(path, context)
155
+ except Exception as e:
156
+ xml2rfc.log.error(str(e))
157
+ return None
158
+
159
+
160
+ def getReferenceRequest(self, request, include=False, line_no=0):
161
+ """ Returns the correct and most efficient path for an external request
162
+
163
+ To determine the path, the following algorithm is consulted:
164
+
165
+ If REQUEST ends with '.dtd' or '.ent' then
166
+ If REQUEST is an absolute path (local or network) then
167
+ Return REQUEST
168
+ Else
169
+ Try TEMPLATE_DIR + REQUEST, otherwise
170
+ Return SOURCE_DIR + REQUEST
171
+ Else
172
+ If REQUEST doesn't end with '.xml' then
173
+ append '.xml'
174
+ If REQUEST is an absolute path (local or network) then
175
+ Return REQUEST
176
+ Else
177
+ If REQUEST contains intermediate directories then
178
+ Try each directory in LOCAL_LIB_DIRS + REQUEST, otherwise
179
+ Try NETWORK + REQUEST
180
+ Else (REQUEST is simply a filename)
181
+ [Recursively] Try each directory in LOCAL_LIB_DIRS + REQUEST, otherwise
182
+ Try each explicit (bibxml, bibxml2...) subdirectory in NETWORK + REQUEST
183
+
184
+ Finally if the path returned is a network URL, use the cached
185
+ version or create a new cache.
186
+
187
+ - REQUEST refers to the full string of the file asked for,
188
+ - TEMPLATE_DIR refers to the applications 'templates' directory,
189
+ - SOURCE_DIR refers to the directory of the XML file being parsed
190
+ - LOCAL_LIB_DIRS refers to a list of local directories to consult,
191
+ on the CLI this is set by $XML_LIBRARY, defaulting to
192
+ ['/usr/share/xml2rfc']. On the GUI this can be configured
193
+ manually but has the same initial defaults.
194
+ - NETWORK refers to the online citation library.
195
+
196
+ The caches in read_dirs are consulted in sequence order to find the
197
+ request. If not found, the request will be cached at write_dir.
198
+
199
+ This method will throw an lxml.etree.XMLSyntaxError to be handled
200
+ by the application if the reference or referencegroup cannot be
201
+ properly resolved
202
+ """
203
+ self.include = include # include state
204
+ tried_cache = False
205
+ attempts = [] # Store the attempts
206
+ original = request # Used for the error message only
207
+ result = None # Our proper path
208
+ if request.endswith('.dtd') or request.endswith('.ent'):
209
+ if os.path.isabs(request) and os.path.exists(request):
210
+ # Absolute request, return as-is if it exists
211
+ attempts.append(request)
212
+ result = request
213
+ elif urlparse(request).netloc:
214
+ # Network request, return as-is
215
+ attempts.append(request)
216
+ result = request
217
+ else:
218
+ basename = os.path.basename(request)
219
+ # Look for dtd in templates directory
220
+ attempt = os.path.join(self.templates_path, basename)
221
+ attempts.append(attempt)
222
+ if os.path.exists(attempt):
223
+ result = attempt
224
+ else:
225
+ # Default to source directory
226
+ result = os.path.join(self.source_dir, basename)
227
+ attempts.append(result)
228
+ else:
229
+ if not request.endswith('.xml'):
230
+ paths = [ request + '.xml', request ]
231
+ else:
232
+ paths = [ request ]
233
+ if os.path.isabs(paths[0]):
234
+ # Absolute path, return as-is
235
+ for path in paths:
236
+ attempts.append(path)
237
+ result = path
238
+ if os.path.exists(path):
239
+ break
240
+ elif urlparse(paths[0]).netloc:
241
+ # URL requested, cache it
242
+ origloc = urlparse(paths[0]).netloc
243
+ if True in [ urlparse(loc).netloc == urlparse(paths[0]).netloc for loc in self.network_locs ]:
244
+ for loc in self.network_locs:
245
+ newloc = urlparse(loc).netloc
246
+ for path in paths:
247
+ path = path.replace(origloc, newloc)
248
+ attempts.append(path)
249
+ result = self.cache(path)
250
+ if result:
251
+ break
252
+ if result:
253
+ break
254
+ else:
255
+ for path in paths:
256
+ attempts.append(path)
257
+ result = self.cache(path)
258
+ if result:
259
+ break
260
+ if not result:
261
+ if self.options.vocabulary == 'v3' and not request.endswith('.xml'):
262
+ xml2rfc.log.warn("The v3 formatters require full explicit URLs of external resources. Did you forget to add '.xml' (or some other extension)?")
263
+ if self.no_network:
264
+ xml2rfc.log.warn("Document not found in cache, and --no-network specified -- couldn't resolve %s" % request)
265
+ tried_cache = True
266
+ else:
267
+ if os.path.dirname(paths[0]):
268
+ # Intermediate directories, only do flat searches
269
+ for dir in self.library_dirs:
270
+ # Try local library directories
271
+ for path in paths:
272
+ attempt = os.path.join(dir, path)
273
+ attempts.append(attempt)
274
+ if os.path.exists(attempt):
275
+ result = attempt
276
+ break
277
+ if not result:
278
+ # Try network location
279
+ for loc in self.network_locs:
280
+ for path in paths:
281
+ url = urljoin(loc, path)
282
+ attempts.append(url)
283
+ result = self.cache(url)
284
+ if result:
285
+ break
286
+ if result:
287
+ break
288
+ tried_cache = True
289
+ if not result and self.no_network:
290
+ xml2rfc.log.warn("Document not found in cache, and --no-network specified -- couldn't resolve %s" % request)
291
+
292
+ # if not result:
293
+ # # Document didn't exist, default to source dir
294
+ # result = os.path.join(self.source_dir, request)
295
+ # attempts.append(result)
296
+ else:
297
+ for dir in self.library_dirs:
298
+ # NOTE: Recursion can be implemented here
299
+ # Try local library directories
300
+ for path in paths:
301
+ attempt = os.path.join(dir, path)
302
+ attempts.append(attempt)
303
+ if os.path.exists(attempt):
304
+ result = attempt
305
+ break
306
+ if not result:
307
+ # Try network subdirs
308
+ for subdir in xml2rfc.NET_SUBDIRS:
309
+ for loc in self.network_locs:
310
+ for path in paths:
311
+ url = urljoin(loc, subdir + '/' + path)
312
+ attempts.append(url)
313
+ result = self.cache(url)
314
+ if result:
315
+ break
316
+ if result:
317
+ break
318
+ tried_cache = True
319
+ if result:
320
+ break
321
+ if not result and self.no_network:
322
+ xml2rfc.log.warn("Document not found in cache, and --no-network specified -- couldn't resolve %s" % request)
323
+ # if not result:
324
+ # # Default to source dir
325
+ # result = os.path.join(self.source_dir, request)
326
+ # attempts.append(result)
327
+
328
+ # Verify the result -- either raise exception or return it
329
+ if not result or (not os.path.exists(result) and not urlparse(original).netloc):
330
+ if os.path.isabs(original):
331
+ xml2rfc.log.warn('A reference was requested with an absolute path: "%s", but not found '
332
+ 'in that location. Removing the path component will cause xml2rfc to look for '
333
+ 'the file automatically in standard locations.' % original)
334
+ # Couldn't resolve. Throw an exception
335
+ error = XmlRfcError('Unable to resolve external request: '
336
+ + '"' + original + '"', line_no=line_no, filename=self.source)
337
+ if self.verbose and len(attempts) > 1:
338
+ # Reveal attemps
339
+ error.msg += ', trying the following location(s):\n ' + \
340
+ '\n '.join(attempts)
341
+ raise error
342
+ else:
343
+ if not tried_cache:
344
+ # Haven't printed a verbose message yet
345
+ typename = self.include and 'include' or 'entity'
346
+ xml2rfc.log.note('Resolving ' + typename + '...', result)
347
+ return result
348
+
349
+ def cache(self, url):
350
+ """ Return the path to a cached URL
351
+
352
+ Checks for the existence of the cache and creates it if necessary.
353
+ """
354
+ scheme, netloc, path, query, fragment = urlsplit(url)
355
+ root, ext = os.path.splitext(path)
356
+ hash = '-'+base64.urlsafe_b64encode(hashlib.sha1(query.encode()).digest()).decode() if query else ''
357
+ basename = os.path.basename(root+hash+ext)
358
+ typename = self.include and 'include' or 'entity'
359
+ # Try to load the URL from each cache in `read_cache`
360
+ for dir in self.read_caches:
361
+ cached_path = os.path.join(dir, xml2rfc.CACHE_PREFIX, basename)
362
+ if os.path.exists(cached_path):
363
+ if os.path.getmtime(cached_path) < (time.time() - self.cache_refresh_secs) and not self.no_network:
364
+ xml2rfc.log.note('Cached version at %s too old; will refresh cache for %s %s' % (cached_path, typename, url))
365
+ break
366
+ else:
367
+ xml2rfc.log.note('Resolving ' + typename + '...', url)
368
+ xml2rfc.log.note('Loaded from cache', cached_path)
369
+ xml = lxml.etree.parse(cached_path)
370
+ if xml.getroot().tag == 'reference':
371
+ if self.validate_ref(xml):
372
+ return cached_path
373
+ else:
374
+ xml2rfc.log.error('Failure validating reference xml from %s' % cached_path )
375
+ os.path.unlink(cached_path)
376
+ return url
377
+ elif xml.getroot().tag == 'referencegroup':
378
+ if self.validate_ref_group(xml):
379
+ return cached_path
380
+ else:
381
+ xml2rfc.log.error('Failure validating referencegroup xml from %s' % cached_path )
382
+ os.path.unlink(cached_path)
383
+ else:
384
+ return cached_path
385
+
386
+ xml2rfc.log.note('Resolving ' + typename + '...', url)
387
+ if not netloc in self.sessions:
388
+ self.sessions[netloc] = requests.Session()
389
+ session = self.sessions[netloc]
390
+ exc = None
391
+ for i in range(4):
392
+ try:
393
+ r = session.get(url)
394
+ break
395
+ except requests.exceptions.ConnectionError as e:
396
+ exc = e
397
+ xml2rfc.log.note(' retrying %s (%s)' % (url, e.args[0].args[0]))
398
+ else:
399
+ xml2rfc.log.error('Failure fetching URL %s (%s)' % (url, exc.args[0].args[0]))
400
+ return ''
401
+ for rr in r.history + [r, ]:
402
+ xml2rfc.log.note(' ... %s %s' % (rr.status_code, rr.url))
403
+ if r.status_code == 200:
404
+ if self.write_cache:
405
+ try:
406
+ xml = lxml.etree.fromstring(r.text.encode('utf8'))
407
+ if xml.tag == 'reference':
408
+ if self.validate_ref(xml):
409
+ return self.add_to_cache(r.url, xml, basename)
410
+ else:
411
+ xml2rfc.log.error('Failure validating reference xml from %s' % url )
412
+ return url
413
+ elif xml.tag == 'referencegroup':
414
+ if self.validate_ref_group(xml):
415
+ return self.add_to_cache(r.url, xml, basename)
416
+ else:
417
+ xml2rfc.log.error('Failure validating referencegroup xml from %s' % url )
418
+ return url
419
+ else:
420
+ return url
421
+ except Exception as e:
422
+ xml2rfc.log.error(str(e))
423
+ return url
424
+ else:
425
+ return url
426
+ else:
427
+ # Invalid URL -- Error will be displayed in getReferenceRequest
428
+ xml2rfc.log.note("URL retrieval failed with status code %s for '%s'" % (r.status_code, r.url))
429
+ return ''
430
+
431
+ def validate_ref(self, xml):
432
+ ref_rng_file = os.path.join(os.path.dirname(__file__), 'data', 'reference.rng')
433
+ ref_rng = lxml.etree.RelaxNG(file=ref_rng_file)
434
+ return ref_rng.validate(xml)
435
+
436
+ def validate_ref_group(self, xml):
437
+ ref_rng_file = os.path.join(os.path.dirname(__file__), 'data', 'referencegroup.rng')
438
+ ref_rng = lxml.etree.RelaxNG(file=ref_rng_file)
439
+ return ref_rng.validate(xml)
440
+
441
+ def add_to_cache(self, url, xml, basename):
442
+ xml.set('{%s}base'%xml2rfc.utils.namespaces['xml'], url)
443
+ text = lxml.etree.tostring(xml, encoding='utf-8')
444
+ write_path = os.path.join(self.write_cache,
445
+ xml2rfc.CACHE_PREFIX, basename)
446
+ with io.open(write_path, 'w', encoding='utf-8') as cache_file:
447
+ cache_file.write(text.decode('utf-8'))
448
+ xml2rfc.log.note('Added file to cache: ', write_path)
449
+ return write_path
450
+
451
+ class AnnotatedElement(lxml.etree.ElementBase):
452
+ pis = None
453
+ page = None
454
+ outdent = 0
455
+ def get(self, key, default=None):
456
+ value = super(AnnotatedElement, self).get(key, default)
457
+ if value == default:
458
+ return value
459
+ else:
460
+ return str(value)
461
+
462
+ class XmlRfcParser:
463
+
464
+ nsmap = {
465
+ b'xi': b'http://www.w3.org/2001/XInclude',
466
+ }
467
+
468
+
469
+ """ XML parser container with callbacks to construct an RFC tree """
470
+ def __init__(self, source, verbose=None, quiet=None, options=base.default_options,
471
+ cache_path=None, templates_path=base.default_options.template_dir, library_dirs=None, add_xmlns=False,
472
+ no_network=None, network_locs=[
473
+ 'https://bib.ietf.org/public/rfc/',
474
+ ]
475
+ ):
476
+ self.options = options
477
+ self.quiet = quiet if quiet != None else options.quiet
478
+ self.verbose = verbose if verbose != None else options.verbose
479
+ self.no_network = no_network if no_network != None else options.no_network
480
+ self.source = source
481
+ self.cache_path = cache_path or options.cache
482
+ self.network_locs = network_locs
483
+
484
+ if self.source:
485
+ with io.open(self.source, "rb", newline=None) as f:
486
+ self.text = f.read()
487
+
488
+ # Initialize templates directory
489
+ self.templates_path = templates_path
490
+
491
+ if options and options.vocabulary == 'v2':
492
+ self.default_dtd_path = os.path.join(self.templates_path, 'rfc2629.dtd')
493
+ else:
494
+ self.default_dtd_path = None
495
+
496
+ for prefix, value in self.nsmap.items():
497
+ lxml.etree.register_namespace(prefix, value)
498
+
499
+ # If library dirs werent explicitly set, like from the gui, then try:
500
+ # 1. $XML_LIBRARY environment variable as a delimited list
501
+ # 2. Default to /usr/share/xml2rfc
502
+ # Split on colon or semi-colon delimiters
503
+ if not library_dirs:
504
+ library_dirs = os.environ.get('XML_LIBRARY', '/usr/share/xml2rfc')
505
+ self.library_dirs = []
506
+ srcdir = os.path.abspath(os.path.dirname(self.source)) if source else ''
507
+ for raw_dir in re.split(':|;', library_dirs) + [ srcdir ]:
508
+ # Convert empty directory to source dir
509
+ if raw_dir == '':
510
+ raw_dir = srcdir
511
+ else:
512
+ raw_dir = os.path.normpath(os.path.expanduser(raw_dir))
513
+ # Add dir if its unique
514
+ if raw_dir not in self.library_dirs:
515
+ self.library_dirs.append(raw_dir)
516
+
517
+ # Initialize the caching system. We'll replace this later if parsing.
518
+ self.cachingResolver = CachingResolver(cache_path=self.cache_path,
519
+ library_dirs=self.library_dirs,
520
+ templates_path=self.templates_path,
521
+ source=self.source,
522
+ network_locs=self.network_locs,
523
+ verbose=self.verbose,
524
+ quiet=self.quiet,
525
+ options=options,
526
+ )
527
+
528
+ def delete_cache(self, path=None):
529
+ self.cachingResolver.delete_cache(path=path)
530
+
531
+ def parse(self, remove_comments=True, remove_pis=False, quiet=False, strip_cdata=True, normalize=False, add_xmlns=False):
532
+ """ Parses the source XML file and returns an XmlRfc instance """
533
+ xml2rfc.log.note('Parsing file', self.source)
534
+
535
+ # workaround for not being able to explicitly set namespaces on the
536
+ # xml root element in lxml: Insert it before we parse:
537
+ text = self.text
538
+ if add_xmlns and not self.nsmap[b'xi'] in text:
539
+ if re.search(b"""<rfc[^>]*(?P<xi>xmlns:xi=['"][^'"]+['"])""", text):
540
+ xml2rfc.log.error('Namespace prefix "xi" is defined, but not as the XInclude namespace "%s".' % self.nsmap[b'xi'])
541
+ else:
542
+ text = text.replace(b'<rfc ', b'<rfc xmlns:%s="%s" ' % (b'xi', self.nsmap[b'xi']), 1)
543
+
544
+ # Get an iterating parser object
545
+ with io.BytesIO(text) as file:
546
+ file.name = self.source
547
+ context = lxml.etree.iterparse(file,
548
+ dtd_validation=False,
549
+ load_dtd=True,
550
+ attribute_defaults=True,
551
+ no_network=self.no_network,
552
+ remove_comments=remove_comments,
553
+ remove_pis=remove_pis,
554
+ remove_blank_text=True,
555
+ resolve_entities=False,
556
+ strip_cdata=strip_cdata,
557
+ events=("start",),
558
+ tag="rfc",
559
+ )
560
+ # resolver without knowledge of rfc_number:
561
+ caching_resolver = CachingResolver(cache_path=self.cache_path,
562
+ library_dirs=self.library_dirs,
563
+ templates_path=self.templates_path,
564
+ source=self.source,
565
+ no_network=self.no_network,
566
+ network_locs=self.network_locs,
567
+ verbose=self.verbose,
568
+ quiet=self.quiet,
569
+ options=self.options,
570
+ )
571
+ context.resolvers.add(caching_resolver)
572
+
573
+ # Get hold of the rfc number (if any) in the rfc element, so we can
574
+ # later resolve the "&rfc.number;" entity.
575
+ self.rfc_number = None
576
+ self.format_version = None
577
+ for action, element in context:
578
+ if element.tag == "rfc":
579
+ self.rfc_number = element.attrib.get("number", None)
580
+ self.format_version = element.attrib.get("version", None)
581
+ break
582
+ if self.format_version == "3":
583
+ self.default_dtd_path = None
584
+
585
+ # now get a regular parser, and parse again, this time resolving entities
586
+ parser = lxml.etree.XMLParser(dtd_validation=False,
587
+ load_dtd=True,
588
+ attribute_defaults=True,
589
+ no_network=self.no_network,
590
+ remove_comments=remove_comments,
591
+ remove_pis=remove_pis,
592
+ remove_blank_text=False,
593
+ resolve_entities=True,
594
+ strip_cdata=strip_cdata)
595
+
596
+ # Initialize the caching system
597
+ self.cachingResolver = CachingResolver(cache_path=self.cache_path,
598
+ library_dirs=self.library_dirs,
599
+ templates_path=self.templates_path,
600
+ source=self.source,
601
+ no_network=self.no_network,
602
+ network_locs=self.network_locs,
603
+ verbose=self.verbose,
604
+ quiet=self.quiet,
605
+ rfc_number = self.rfc_number,
606
+ options=self.options,
607
+ )
608
+
609
+ # Add our custom resolver
610
+ parser.resolvers.add(self.cachingResolver)
611
+
612
+ # Use our custom element class, which holds the state of PI settings
613
+ # at this point in the xml tree
614
+ element_lookup = lxml.etree.ElementDefaultClassLookup(element=AnnotatedElement)
615
+ parser.set_element_class_lookup(element_lookup)
616
+
617
+ # Parse the XML file into a tree and create an rfc instance
618
+ with io.BytesIO(text) as file:
619
+ file.name = self.source
620
+ tree = lxml.etree.parse(file, parser)
621
+ xmlrfc = XmlRfc(tree, self.default_dtd_path, nsmap=self.nsmap)
622
+ xmlrfc.source = self.source
623
+
624
+ # Evaluate processing instructions before root element
625
+ xmlrfc._eval_pre_pi()
626
+
627
+ # Keep seen elements in a list, to force lxml to not discard (and
628
+ # recreate) the elements, as this would cause loss of our custom
629
+ # state, the PI settings at the time the element was parsed
630
+ # (in element.pis)
631
+ xmlrfc._elements_cache = []
632
+ # Process PIs and expand 'include' instructions
633
+ pis = xmlrfc.pis.copy()
634
+ for element in xmlrfc.getroot().iterdescendants():
635
+ if element.tag is lxml.etree.PI:
636
+ pidict = xmlrfc.parse_pi(element)
637
+ pis = xmlrfc.pis.copy()
638
+ if 'include' in pidict and pidict['include']:
639
+ request = pidict['include']
640
+ if xmlrfc.getroot().get('version', '3') in ['3', ]:
641
+ xml2rfc.log.warn(f'XInclude should be used instead of PI include for {request}')
642
+ path = self.cachingResolver.getReferenceRequest(request,
643
+ # Pass the line number in XML for error bubbling
644
+ include=True, line_no=getattr(element, 'sourceline', 0))
645
+ try:
646
+ # Parse the xml and attach it to the tree here
647
+ parser = lxml.etree.XMLParser(load_dtd=False,
648
+ no_network=False,
649
+ remove_comments=remove_comments,
650
+ remove_pis=remove_pis,
651
+ remove_blank_text=False,
652
+ resolve_entities=True,
653
+ strip_cdata=strip_cdata)
654
+ parser.set_element_class_lookup(element_lookup)
655
+ # parser.resolvers.add(self.cachingResolver) --- should this be done?
656
+ ref_root = lxml.etree.parse(path, parser).getroot()
657
+ ref_root.pis = pis
658
+ xmlrfc._elements_cache.append(ref_root)
659
+ for e in ref_root.iterdescendants():
660
+ e.pis = pis
661
+ xmlrfc._elements_cache.append(e)
662
+ parent = element.getparent()
663
+ parent.replace(element, ref_root)
664
+ except (lxml.etree.XMLSyntaxError, IOError) as e:
665
+ if e is lxml.etree.XMLSyntaxError:
666
+ xml2rfc.log.warn('The include file at', path,
667
+ 'contained an XML error and was '\
668
+ 'not expanded:', e.msg)
669
+ else:
670
+ xml2rfc.log.warn('Unable to load the include file at',
671
+ path)
672
+ else:
673
+ if isinstance(element, AnnotatedElement):
674
+ element.pis = pis
675
+ xmlrfc._elements_cache.append(element)
676
+
677
+ # Finally, do any extra formatting on the RFC before returning
678
+ if normalize:
679
+ xmlrfc._format_whitespace()
680
+
681
+ return xmlrfc
682
+
683
+ class XmlRfc(object):
684
+ """ Internal representation of an RFC document
685
+
686
+ Contains an lxml.etree.ElementTree, with some additional helper
687
+ methods to prepare the tree for output.
688
+
689
+ Accessing the rfc tree is done by getting the root node from getroot()
690
+ """
691
+
692
+ pis = {}
693
+ source = "(unknown source)"
694
+
695
+ def __init__(self, tree, default_dtd_path, source=None, nsmap={}):
696
+ self.default_dtd_path = default_dtd_path
697
+ self.tree = tree
698
+ self.nsmap = nsmap
699
+ if source:
700
+ self.source = source
701
+ # Pi default values
702
+ self.pis = {
703
+ "artworkdelimiter": None,
704
+ "artworklines": "0",
705
+ "authorship": "yes",
706
+ "autobreaks": "yes",
707
+ "background": "" ,
708
+ "colonspace": "no" ,
709
+ "comments": "no" ,
710
+ "docmapping": "no",
711
+ "editing": "no",
712
+ #"emoticonic": "no",
713
+ #"footer": Unset
714
+ "figurecount": "no",
715
+ #"header": Unset
716
+ "inline": "no",
717
+ #"iprnotified": "no",
718
+ "linkmailto": "yes",
719
+ #"linefile": Unset
720
+ "needLines": None,
721
+ "multiple-initials":"no",
722
+ #"notedraftinprogress": "yes",
723
+ "orphanlimit": "2",
724
+ "private": "",
725
+ "refparent": "References",
726
+ "rfcedstyle": "no",
727
+ #"rfcprocack": "no",
728
+ "sectionorphan": "4",
729
+ #"slides": "no",
730
+ "sortrefs": "yes",
731
+ #"strict": "no",
732
+ "symrefs": "yes",
733
+ "tablecount": "no",
734
+ "text-list-symbols": "o*+-",
735
+ "toc": "no",
736
+ "tocappendix": "yes",
737
+ "tocdepth": "3",
738
+ "tocindent": "yes",
739
+ "tocnarrow": "yes",
740
+ #"tocompact": "yes",
741
+ "tocpagebreak": "no",
742
+ "topblock": "yes",
743
+ #"typeout": Unset
744
+ #"useobject": "no" ,
745
+ "widowlimit": "2",
746
+ }
747
+ # Special cases:
748
+ self.pis["compact"] = self.pis["rfcedstyle"]
749
+ self.pis["subcompact"] = self.pis["compact"]
750
+
751
+ def getroot(self):
752
+ """ Wrapper method to get the root of the XML tree"""
753
+ return self.tree.getroot()
754
+
755
+ def getpis(self):
756
+ """ Returns a list of the XML processing instructions """
757
+ return self.pis.copy()
758
+
759
+ def validate(self, dtd_path=None):
760
+ """ Validate the document with its default dtd, or an optional one
761
+
762
+ Return a success bool along with a list of any errors
763
+ """
764
+ # Load dtd from alternate path, if it was specified
765
+ if dtd_path:
766
+ if os.path.exists(dtd_path):
767
+ try:
768
+ dtd = lxml.etree.DTD(dtd_path)
769
+ except lxml.etree.DTDParseError as e:
770
+ # The DTD itself has errors
771
+ xml2rfc.log.error('Could not parse the dtd file:',
772
+ dtd_path + '\n ', e.message)
773
+ return False, []
774
+ else:
775
+ # Invalid path given
776
+ xml2rfc.log.error('DTD file does not exist:', dtd_path)
777
+ return False, []
778
+
779
+ # Otherwise, use document's DTD declaration
780
+ else:
781
+ dtd = self.tree.docinfo.externalDTD
782
+
783
+ if not dtd and self.default_dtd_path:
784
+ # No explicit DTD filename OR declaration in document!
785
+ xml2rfc.log.warn('No DTD given, defaulting to', self.default_dtd_path)
786
+ return self.validate(dtd_path=self.default_dtd_path)
787
+
788
+ if not dtd or dtd.validate(self.getroot()):
789
+ # The document was valid
790
+ return True, []
791
+ else:
792
+ if len(dtd.error_log) == 0:
793
+ return True, []
794
+ else:
795
+ # The document was not valid
796
+ return False, dtd.error_log
797
+
798
+ def parse_pi(self, pi):
799
+ return xml2rfc.utils.parse_pi(pi, self.pis)
800
+
801
+ def _eval_pre_pi(self):
802
+ """ Evaluate pre-document processing instructions
803
+
804
+ This will look at all processing instructions before the root node
805
+ for initial document settings.
806
+ """
807
+ # Grab processing instructions from xml tree
808
+ element = self.tree.getroot().getprevious()
809
+ while element is not None:
810
+ if element.tag is lxml.etree.PI:
811
+ self.parse_pi(element)
812
+ element = element.getprevious()
813
+
814
+ def _format_whitespace(self):
815
+ xml2rfc.utils.formatXmlWhitespace(self.getroot())