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.
- xml2rfc/__init__.py +68 -0
- xml2rfc/boilerplate_id_guidelines.py +28 -0
- xml2rfc/boilerplate_rfc_7841.py +575 -0
- xml2rfc/boilerplate_tlp.py +258 -0
- xml2rfc/data/SVG-1.2-RFC.rnc +1666 -0
- xml2rfc/data/SVG-1.2-RFC.rng +7460 -0
- xml2rfc/data/Scripts.txt +2693 -0
- xml2rfc/data/metadata.js +218 -0
- xml2rfc/data/metadata.min.js +1 -0
- xml2rfc/data/reference.rnc +955 -0
- xml2rfc/data/reference.rng +2158 -0
- xml2rfc/data/referencegroup.rnc +16 -0
- xml2rfc/data/referencegroup.rng +2180 -0
- xml2rfc/data/rfc7991.rnc +955 -0
- xml2rfc/data/rfc7991.rng +2143 -0
- xml2rfc/data/v2.rnc +309 -0
- xml2rfc/data/v2.rng +720 -0
- xml2rfc/data/v3.rnc +1144 -0
- xml2rfc/data/v3.rng +2538 -0
- xml2rfc/data/v3compat.dtd +630 -0
- xml2rfc/data/v3compat.rnc +985 -0
- xml2rfc/data/xml2rfc-v3-emacs-nxml.rnc +1083 -0
- xml2rfc/data/xml2rfc.css +1171 -0
- xml2rfc/data/xml2rfc.js +7 -0
- xml2rfc/debug.py +221 -0
- xml2rfc/log.py +54 -0
- xml2rfc/parser.py +815 -0
- xml2rfc/run.py +801 -0
- xml2rfc/scripts.py +52 -0
- xml2rfc/strings.py +26 -0
- xml2rfc/templates/address_card.html +21 -0
- xml2rfc/templates/base.html +415 -0
- xml2rfc/templates/doc.xml +367 -0
- xml2rfc/templates/doc.yaml +2138 -0
- xml2rfc/templates/rfc2629-other.ent +61 -0
- xml2rfc/templates/rfc2629-xhtml.ent +170 -0
- xml2rfc/templates/rfc2629.dtd +328 -0
- xml2rfc/uniscripts/__init__.py +97 -0
- xml2rfc/uniscripts/unidata.py +306 -0
- xml2rfc/uniscripts/update/make_unidata.py +94 -0
- xml2rfc/util/__init__.py +7 -0
- xml2rfc/util/date.py +94 -0
- xml2rfc/util/fonts.py +53 -0
- xml2rfc/util/name.py +229 -0
- xml2rfc/util/num.py +119 -0
- xml2rfc/util/postal.py +285 -0
- xml2rfc/util/unicode.py +441 -0
- xml2rfc/utils.py +672 -0
- xml2rfc/walkpdf.py +113 -0
- xml2rfc/writers/__init__.py +25 -0
- xml2rfc/writers/base.py +2248 -0
- xml2rfc/writers/bib.py +43 -0
- xml2rfc/writers/doc.py +354 -0
- xml2rfc/writers/expand.py +51 -0
- xml2rfc/writers/expanded_xml.py +39 -0
- xml2rfc/writers/html.py +3110 -0
- xml2rfc/writers/legacy_html.py +860 -0
- xml2rfc/writers/nroff.py +345 -0
- xml2rfc/writers/paginated_txt.py +432 -0
- xml2rfc/writers/pdf.py +230 -0
- xml2rfc/writers/preptool.py +2428 -0
- xml2rfc/writers/raw_txt.py +1453 -0
- xml2rfc/writers/text.py +5000 -0
- xml2rfc/writers/unprep.py +187 -0
- xml2rfc/writers/v2v3.py +1150 -0
- xml2rfc-3.26.0.dist-info/LICENSE +29 -0
- xml2rfc-3.26.0.dist-info/METADATA +178 -0
- xml2rfc-3.26.0.dist-info/RECORD +71 -0
- xml2rfc-3.26.0.dist-info/WHEEL +5 -0
- xml2rfc-3.26.0.dist-info/entry_points.txt +2 -0
- 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())
|