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
@@ -0,0 +1,2428 @@
1
+ # Copyright The IETF Trust 2017, All Rights Reserved
2
+ # -*- coding: utf-8 -*-
3
+ from __future__ import unicode_literals, print_function
4
+
5
+ import copy
6
+ import datetime
7
+ import os
8
+ import re
9
+ import sys
10
+ import traceback as tb
11
+ import unicodedata
12
+
13
+ from codecs import open
14
+ from collections import defaultdict, namedtuple
15
+ from contextlib import closing
16
+
17
+ try:
18
+ from xml2rfc import debug
19
+ debug.debug = True
20
+ except ImportError:
21
+ pass
22
+
23
+ from urllib.parse import urlsplit, urlunsplit, urljoin, urlparse
24
+ from urllib.request import urlopen
25
+
26
+ from lxml import etree
27
+
28
+
29
+ from xml2rfc import strings, log
30
+ from xml2rfc.boilerplate_id_guidelines import boilerplate_draft_status_of_memo
31
+ from xml2rfc.boilerplate_rfc_7841 import boilerplate_rfc_status_of_memo
32
+ from xml2rfc.boilerplate_tlp import boilerplate_tlp
33
+ from xml2rfc.scripts import get_scripts
34
+ from xml2rfc.uniscripts import is_script
35
+ from xml2rfc.util.date import get_expiry_date, format_date, normalize_month
36
+ from xml2rfc.util.name import full_author_name_expansion
37
+ from xml2rfc.util.num import ol_style_formatter
38
+ from xml2rfc.util.unicode import (
39
+ unicode_content_tags, unicode_attributes, expand_unicode_element,
40
+ isascii, latinscript_attributes, is_svg)
41
+ from xml2rfc.utils import build_dataurl, namespaces, sdict, clean_text
42
+ from xml2rfc.writers.base import default_options, BaseV3Writer, RfcWriterError
43
+
44
+
45
+ pnprefix = {
46
+ # tag: prefix
47
+ 'abstract': 'section',
48
+ 'boilerplate': 'section',
49
+ 'toc': 'section',
50
+ 'figure': 'figure',
51
+ 'iref': 'iref',
52
+ 'note': 'section',
53
+ 'references': 'section',
54
+ 'section': 'section',
55
+ 'table': 'table',
56
+ 'u': 'u',
57
+ }
58
+
59
+ index_item = namedtuple('index_item', ['item', 'sub', 'anchor', 'anchor_tag', 'iref', ])
60
+
61
+ re_spaces = re.compile(r'\s+')
62
+
63
+ def uniq(l):
64
+ seen = set()
65
+ ll = []
66
+ for i in l:
67
+ if not i in seen:
68
+ seen.add(i)
69
+ ll.append(i)
70
+ return ll
71
+
72
+
73
+ class PrepToolWriter(BaseV3Writer):
74
+ """ Writes an XML file where the input has been modified according to RFC 7998"""
75
+
76
+ def __init__(self, xmlrfc, quiet=None, options=default_options, date=None, liberal=None, keep_pis=['v3xml2rfc']):
77
+ super(PrepToolWriter, self).__init__(xmlrfc, quiet=quiet, options=options, date=date)
78
+ if not quiet is None:
79
+ options.quiet = quiet
80
+ self.rfcnumber = self.root.get('number')
81
+ self.liberal = liberal if liberal != None else options.accept_prepped
82
+ self.keep_pis = keep_pis
83
+ #
84
+ self.ol_counts = {}
85
+ self.attribute_defaults = {}
86
+ #
87
+ self.boilerplate_section_number = 0
88
+ self.toc_section_number = 0
89
+ self.note_number = 0
90
+ self.middle_section_number = [0, ]
91
+ self.table_number = 0
92
+ self.figure_number = 0
93
+ self.unicode_number = 0
94
+ self.references_number = [0, ]
95
+ self.back_section_number = [0, ]
96
+ self.paragraph_number = [0, ]
97
+ self.iref_number = 0
98
+ #
99
+ self.prev_section_level = 0
100
+ self.prev_references_level = 0
101
+ self.prev_paragraph_section = None
102
+ #
103
+ self.prepped = self.root.get('prepTime')
104
+ #
105
+ self.index_entries = []
106
+ #
107
+ self.spacer = '\u00a0\u00a0'
108
+ #
109
+ self.boilerplate_https_date = datetime.date(year=2017, month=8, day=21)
110
+ #
111
+ self._seen_slugs = set() # This is used to enforce global uniqueness on slugs:
112
+
113
+ def get_attribute_names(self, tag):
114
+ attr = self.schema.xpath("/x:grammar/x:define/x:element[@name='%s']//x:attribute" % tag, namespaces=namespaces)
115
+ names = [ a.get('name') for a in attr ]
116
+ return names
117
+
118
+ def get_attribute_defaults(self, tag):
119
+ if not tag in self.attribute_defaults:
120
+ ignored_attributes = set(['keepWithNext', 'keepWithPrevious', 'toc', 'pageno', ])
121
+ attr = self.schema.xpath("/x:grammar/x:define/x:element[@name='%s']//x:attribute" % tag, namespaces=namespaces)
122
+ defaults = dict( (a.get('name'), a.get("{%s}defaultValue"%namespaces['a'], None)) for a in attr )
123
+ keys = set(defaults.keys()) - ignored_attributes
124
+ self.attribute_defaults[tag] = sdict(dict( (k, defaults[k]) for k in keys if defaults[k] ))
125
+ return copy.copy(self.attribute_defaults[tag])
126
+
127
+ def set_element_pn(self, elt, value):
128
+ existing_value = elt.get('pn', None)
129
+ if existing_value is None:
130
+ if self.prepped:
131
+ self.warn(elt, 'pn not set on element in prepped input, setting to {}'.format(value))
132
+ elt.set('pn', value)
133
+ elif existing_value != value:
134
+ self.warn(elt, 'using existing pn ({}) instead of generated pn ({})'.format(existing_value, value))
135
+
136
+ def element(self, tag, line=None, **kwargs):
137
+ attrib = self.get_attribute_defaults(tag)
138
+ attrib.update(kwargs)
139
+ e = etree.Element(tag, **sdict(attrib))
140
+ if line:
141
+ e.sourceline = line
142
+ elif self.options.debug:
143
+ filename, lineno, caller, code = tb.extract_stack()[-2]
144
+ e.base = os.path.basename(filename)
145
+ e.sourceline = lineno
146
+ return e
147
+
148
+ def slugify_name(self, name):
149
+ name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii')
150
+ slug = re.sub(r'[^\w\s/-]', '', name).strip().lower()
151
+ slug = re.sub(r'[-\s/]+', '-', slug)
152
+ # limit slug length
153
+ n = 32
154
+ m = 2
155
+ while slug[:n] in self._seen_slugs and n < len(slug) and n < 40:
156
+ n += 1
157
+ while slug[:n] + '-%s' % m in self._seen_slugs and m < 99:
158
+ m += 1
159
+ if m == 99 and slug[:n] + '-%s' % m in self._seen_slugs:
160
+ raise RuntimeError(
161
+ "Too many overlapping <name> content instances; cannot create a sensible slugifiedName attribute")
162
+ if slug[:n] in self._seen_slugs:
163
+ slug = slug[:n] + '-%s' % m
164
+ else:
165
+ slug = slug[:n]
166
+ self._seen_slugs.add(slug)
167
+ return slug
168
+
169
+ def validate(self, when, warn=False):
170
+ return super(PrepToolWriter, self).validate(when='%s running preptool'%when, warn=warn)
171
+
172
+ def write(self, filename):
173
+ """ Public method to write the XML document to a file """
174
+
175
+ if not self.options.quiet:
176
+ self.log(' Prepping %s' % self.xmlrfc.source)
177
+ self.prep()
178
+ if self.errors:
179
+ raise RfcWriterError("Not creating output file due to errors (see above)")
180
+
181
+ # remove the processing instructions
182
+ self.remove_pis()
183
+
184
+ with open(filename, 'w', encoding='utf-8') as file:
185
+ # Use lxml's built-in serialization
186
+ text = etree.tostring(self.root.getroottree(),
187
+ xml_declaration=True,
188
+ encoding='utf-8',
189
+ pretty_print=True)
190
+ file.write(text.decode('utf-8'))
191
+
192
+ if not self.options.quiet:
193
+ self.log(' Created file %s' % filename)
194
+
195
+ def normalize_whitespace(self, e):
196
+ lines = e.text.split('\n')
197
+ for i, line in enumerate(lines):
198
+ # This strips unnecessary whitespace, and also works around an issue in
199
+ # WeasyPrint where trailing whitespace in artwork can trigger something
200
+ # akin to line wrapping even if rendered in <pre>:
201
+ line = line.rstrip()
202
+ if '\t' in line:
203
+ self.warn(e, "Found tab on line %d of <%s>: \n %s" % (i+1, e.tag, line))
204
+ line = line.expandtabs()
205
+ lines[i] = line
206
+ e.text = '\n'.join(lines)
207
+
208
+ def prep(self):
209
+ self._seen_slugs = set() # Reset cache before prepping
210
+ self.xinclude()
211
+ # Set up reference mapping for later use. Done here, and not earlier,
212
+ # to capture any references pulled in by the XInclude we just did.
213
+ self.refname_mapping = self.get_refname_mapping()
214
+ self.remove_dtd()
215
+ tree = self.dispatch(self.selectors)
216
+ log.note(" Completed preptool run")
217
+ return tree
218
+
219
+ ## Selector notation: Some selectors below have a handler annotation,
220
+ ## with the selector and the annotation separated by a semicolon (;).
221
+ ## Everything from the semicolon to the end of the string is stripped
222
+ ## before the selector is used as an XPath selector.
223
+ selectors = [
224
+ './/keyword', # 2.28. Keyword
225
+ '.;check_unnumbered_sections()', # 2.46.2 "numbered" Attribute
226
+ # 5.1.1. XInclude Processing
227
+ # 5.1.2. DTD Removal
228
+ '//processing-instruction();removal()', # 5.1.3. Processing Instruction Removal
229
+ '.;validate_before()', # 5.1.4. Validity Check
230
+ '/rfc;check_attribute_values()',
231
+ '.;check_attribute_values()', #
232
+ '.;check_ascii_text()',
233
+ '.;normalize_text_items()',
234
+ './/bcp14;check_key_words()',
235
+ './/*[@anchor]', # 5.1.5. Check "anchor"
236
+ '.;insert_version()', # 5.2.1. "version" Insertion
237
+ './front;insert_series_info()', # 5.2.2. "seriesInfo" Insertion
238
+ './front;insert_date())', # 5.2.3. <date> Insertion
239
+ '.;insert_preptime()', # 5.2.4. "prepTime" Insertion
240
+ './/ol[@group]', # 5.2.5. <ol> Group "start" Insertion
241
+ '//*;insert_attribute_defaults()', # 5.2.6. Attribute Default Value Insertion
242
+ './/relref;to_xref()',
243
+ './/section', # 5.2.7. Section "toc" attribute
244
+ './/note[@removeInRFC="true"]', # 5.2.8. "removeInRFC" Warning Paragraph
245
+ './/section[@removeInRFC="true"]',
246
+ '//*[@*="yes" or @*="no"]', # convert old attribute false/true
247
+ './front/date', # 5.3.1. "month" Attribute
248
+ './/*[@ascii]', # 5.3.2. ASCII Attribute Processing
249
+ './front/author',
250
+ './/contact',
251
+ './/*[@title]', # 5.3.3. "title" Conversion
252
+ './/*[@keepWithPrevious="true"]', # 5.3.4. "keepWithPrevious" Conversion
253
+ '.;fill_in_expires_date()', # 5.4.1. "expiresDate" Insertion
254
+ './front;insert_boilerplate()', # 5.4.2. <boilerplate> Insertion
255
+ './front;insert_toc()',
256
+ '.;check_series_and_submission_type()', # 5.4.2.1. Compare <rfc> "submissionType" and <seriesInfo> "stream"
257
+ './/boilerplate;insert_status_of_memo()', # 5.4.2.2. "Status of This Memo" Insertion
258
+ './/boilerplate;insert_copyright_notice()', # 5.4.2.3. "Copyright Notice" Insertion
259
+ './/boilerplate//section', # 5.2.7. Section "toc" attribute
260
+ './/reference;insert_target()', # 5.4.3. <reference> "target" Insertion
261
+ './/referencegroup;insert_target()', # <referencegroup> "target" Insertion
262
+ './/reference;insert_work_in_progress()',
263
+ './/reference;sort_series_info()', # <reference> sort <seriesInfo>
264
+ './/name;insert_slugified_name()', # 5.4.4. <name> Slugification
265
+ './/references;sort()', # 5.4.5. <reference> Sorting
266
+ './/references;add_derived_anchor()',
267
+ './/references;check_usage()',
268
+ './/*;insert_attribute_defaults()', # 5.2.6. Attribute Default Value Insertion
269
+ # 5.4.6. "pn" Numbering
270
+ './/boilerplate//section;add_number()',
271
+ './front//abstract;add_number()',
272
+ './/front//note;add_number()',
273
+ './/middle//section;add_number()',
274
+ './/table;add_number()',
275
+ './/figure;add_number()',
276
+ './/references;add_number()',
277
+ './/back//section;add_number()',
278
+ '.;paragraph_add_numbers()',
279
+ './/iref;add_number()', # 5.4.7. <iref> Numbering
280
+ './/u;add_number()',
281
+ './/ol;add_counter()',
282
+ './/artset', # <artwork> Processing
283
+ './/artwork', # 5.5.1. <artwork> Processing
284
+ './/sourcecode', # 5.5.2. <sourcecode> Processing
285
+ #
286
+ './back;insert_index()',
287
+ './/xref', # 5.4.8. <xref> Processing
288
+ # Relref processing be handled under .//xref:
289
+ # 5.4.9. <relref> Processing
290
+ './back;insert_author_address()',
291
+ './/toc;insert_table_of_contents()',
292
+ './/*[@removeInRFC="true"]', # 5.6.1. <note> Removal
293
+ './/cref;removal()', # 5.6.2. <cref> Removal
294
+ # 5.6.3. <link> Processing
295
+ './/link[@rel="alternate"];removal()',
296
+ '.;check_links_required()',
297
+ './/comment();removal()', # 5.6.4. XML Comment Removal
298
+ '.;attribute_removal()', # 5.6.5. "xml:base" and "originalSrc" Removal
299
+ '.;validate_after()', # 5.6.6. Compliance Check
300
+ '.;insert_scripts()', # 5.7.1. "scripts" Insertion
301
+ #'.;final_pi_removal()', # Done in write(). Keep PIs when prep() is called interally
302
+ '.;pretty_print_prep()', # 5.7.2. Pretty-Format
303
+ ]
304
+
305
+ # ----------------------------------------------------------------
306
+ # 2.28. <keyword>
307
+ #
308
+ # Specifies a keyword applicable to the document.
309
+ #
310
+ # Note that each element should only contain a single keyword; for
311
+ # multiple keywords, the element can simply be repeated.
312
+ #
313
+ # Keywords are used both in the RFC Index and in the metadata of
314
+ # generated document representations.
315
+ #
316
+ # This element appears as a child element of <front> (Section 2.26).
317
+ #
318
+ # Content model: only text content.
319
+ def element_keyword(self, e, p):
320
+ # if ',' in e.text or ' ' in e.text:
321
+ # self.warn(e, "Expected a single keyword in the <keyword/> element, but found '%s'" % (e.text, ))
322
+ pass
323
+
324
+ # ----------------------------------------------------------------
325
+ # 2.46.2. "numbered" Attribute
326
+ #
327
+ # If set to "false", the formatter is requested to not display a
328
+ # section number. The prep tool will verify that such a section is not
329
+ # followed by a numbered section in this part of the document and will
330
+ # verify that the section is a top-level section.
331
+ def check_unnumbered_sections(self, e, p):
332
+ def check_child_sections(e, unnumbered_parent=None):
333
+ unnumbered_seen = None
334
+ for s in e.iterchildren('section'):
335
+ numbered = s.get('numbered', 'true')
336
+ if numbered == 'false':
337
+ unnumbered_seen = s
338
+ elif unnumbered_parent != None:
339
+ self.err(s, "Did not expect a numbered section under an unnumbered parent section (seen on line %s)" % unnumbered_parent.sourceline)
340
+ elif numbered == 'true':
341
+ if unnumbered_seen != None:
342
+ self.err(s, "Did not expect a numbered section after an unnumbered section (seen on line %s)" % unnumbered_seen.sourceline)
343
+ check_child_sections(s, unnumbered_parent=(s if numbered=='false' else None))
344
+ #
345
+ for tag in ['front', 'middle', 'back' ]:
346
+ e = self.root.find(tag)
347
+ if e != None:
348
+ check_child_sections(e)
349
+
350
+ # ----------------------------------------------------------------
351
+
352
+ # 5.1.3. Processing Instruction Removal
353
+ #
354
+ # Remove processing instructions.
355
+
356
+ def processing_instruction_removal(self, e, p):
357
+ if p != None:
358
+ if not e.target in self.keep_pis:
359
+ self.remove(p, e)
360
+
361
+ def final_pi_removal(self, e, p):
362
+ self.keep_pis = []
363
+ for i in e.xpath('.//processing-instruction()'):
364
+ self.processing_instruction_removal(i, i.getparent())
365
+
366
+ def relref_to_xref(self, e, p):
367
+ e.attrib['sectionFormat'] = e.attrib['displayFormat']
368
+ del e.attrib['displayFormat']
369
+ e.tag = 'xref'
370
+ self.note(e, "Changed <relref> to <xref>, which now supports equivalent functionality.")
371
+
372
+ # 5.1.4. Validity Check
373
+ #
374
+ # Check the input against the RELAX NG (RNG) in [RFC7991]. If the
375
+ # input is not valid, give an error.
376
+
377
+ # implemented in parent class: validate_before(self, e, p):
378
+
379
+
380
+ def rfc_check_attribute_values(self, e, p):
381
+ doc_name = e.get('docName')
382
+ if self.root.get('ipr') == '':
383
+ # If the <rfc> ipr attribute is blank, it's a non-I*TF document
384
+ if doc_name and doc_name.strip():
385
+ if '.' in doc_name:
386
+ self.warn(e, "The 'docName' attribute of the <rfc/> element should not contain any filename extension: docName=\"draft-foo-bar-02\".")
387
+ if not re.search(r'-\d\d$', doc_name):
388
+ self.warn(e, "The 'docName' attribute of the <rfc/> element should have a revision number as the last component: docName=\"draft-foo-bar-02\".")
389
+ elif not self.options.rfc:
390
+ self.warn(e, "Expected a 'docName' attribute in the <rfc/> element, but found none.")
391
+
392
+ def check_attribute_values(self, e, p):
393
+ # attribute names
394
+ attribute_names = (
395
+ ('quote-title', 'quoteTitle'),
396
+ )
397
+ for old, new in attribute_names:
398
+ for o in self.root.findall('.//*[@%s]'%old):
399
+ a = o.get(old)
400
+ if a:
401
+ o.set(new, a)
402
+ del o.attrib[old]
403
+
404
+
405
+ # Integer attributes
406
+ integer_attributes = {
407
+ 'rfc': ('number', 'version', ),
408
+ 'date': ('year', 'day', ),
409
+ 'dl': ('indent', ),
410
+ 'format': ('octets', ),
411
+ 'ol': ('indent', ),
412
+ 't': ('indent', ),
413
+ 'ul': ('indent', ),
414
+ }
415
+ tags = integer_attributes.keys()
416
+ for c in e.iter(tags):
417
+ for a in integer_attributes[c.tag]:
418
+ i = c.get(a)
419
+ if i and not i.isdigit() and not i==self.get_attribute_defaults(c.tag).get(a):
420
+ self.err(c, 'Expected <%s> attribute "%s" to be a non-negative integer, but found "%s"' % (c.tag, a, i))
421
+
422
+
423
+ # Attributes that may have leading or trailing space
424
+ space_attributes = {
425
+ ('u', 'format'),
426
+ ('iref', 'item'),
427
+ ('iref', 'subitem'),
428
+ }
429
+ for c in e.iter():
430
+ if c.tag == etree.PI:
431
+ continue
432
+ for a in c.attrib:
433
+ v = c.get(a)
434
+ if not (c.tag, a) in unicode_attributes:
435
+ if (c.tag, a) in latinscript_attributes:
436
+ if not is_script(v, 'Latin'):
437
+ self.err(c, 'Found non-Latin-script content in <%s> attribute value %s="%s"' % (c.tag, a, v))
438
+ if self.options.warn_bare_unicode:
439
+ if not isascii(v):
440
+ self.warn(c, f'Found non-ASCII content in {c.tag} attribute value {a}="{v}" that should be inspected to ensure it is intentional.')
441
+ if not (c.tag, a) in space_attributes:
442
+ vv = v.strip()
443
+ if vv != v:
444
+ self.note(c, 'Stripped extra whitespace in <%s> attribute value %s="%s"' % (c.tag, a, v))
445
+ c.set(a, vv)
446
+
447
+
448
+ # Some attribute values we should check before we default set them,
449
+ # such as some of the attributes on <rfc>:
450
+ self.attribute_yes_no(e, p)
451
+ #
452
+ stream = self.root.get('submissionType')
453
+ category = self.root.get('category')
454
+ consensus = self.root.get('consensus')
455
+ workgroup = self.root.find('./front/workgroup')
456
+ #
457
+ rfc_defaults = self.get_attribute_defaults('rfc')
458
+ #
459
+ stream_values = boilerplate_rfc_status_of_memo.keys()
460
+ stream_default = rfc_defaults['submissionType']
461
+ if not stream in stream_values:
462
+ self.warn(self.root, "Expected a valid submissionType (stream) setting, one of %s, but found %s. Will use '%s'" %
463
+ (', '.join(stream_values), stream, stream_default))
464
+ stream = stream_default
465
+ #
466
+ category_values = boilerplate_rfc_status_of_memo[stream].keys()
467
+ if not category in category_values:
468
+ self.die(self.root, "Expected a valid category for submissionType='%s', one of %s, but found '%s'" %
469
+ (stream, ','.join(category_values), category, ))
470
+ #
471
+ consensus_values = list(boilerplate_rfc_status_of_memo[stream][category].keys())
472
+ consensus_default = rfc_defaults['consensus']
473
+ if stream == 'IRTF' and workgroup == None:
474
+ if consensus:
475
+ self.err(self.root, "Expected no consensus setting for IRTF stream and no workgroup, but found '%s'. Ignoring it." % consensus)
476
+ consensus = 'n/a'
477
+ elif stream == 'independent' or stream == 'editorial':
478
+ if consensus:
479
+ self.err(self.root, "Expected no consensus setting for %s stream, but found '%s'. Ignoring it." % (stream, consensus))
480
+ consensus = 'n/a'
481
+ elif consensus != None:
482
+ pass
483
+ else:
484
+ if len(consensus_values) > 1:
485
+ consensus = consensus_default
486
+ else:
487
+ consensus = consensus_values[0]
488
+ if consensus != consensus_default:
489
+ self.warn(self.root, 'Setting consensus="%s" for %s %s document (this is not the schema default, but is the only value permitted for this type of document)' % (consensus, stream, category.upper()))
490
+ if not consensus in consensus_values:
491
+ if not consensus_default in consensus_values:
492
+ self.die(self.root, "No valid consensus setting available.")
493
+ else:
494
+ self.warn(self.root, "Expected a valid consensus setting (one of %s), but found %s. Will use '%s'" %
495
+ (', '.join(consensus_values), consensus, consensus_default))
496
+ consensus = consensus_default
497
+ if consensus in ('true', 'false'):
498
+ self.root.set('consensus', consensus)
499
+ #
500
+
501
+ def check_ascii_text(self, e, p):
502
+ for c in self.root.iter():
503
+ if is_svg(c):
504
+ continue
505
+ if c.text and not isascii(c.text) and c.tag not in unicode_content_tags:
506
+ show = c.text.encode('ascii', errors='replace')
507
+ if self.options.warn_bare_unicode:
508
+ self.warn(c, 'Found non-ascii characters in an element that should be inspected to ensure they are intentional, in <%s>: %s' % (c.tag, show))
509
+
510
+ def normalize_text_items(self, e, p):
511
+ """
512
+ Normalize the text content of tags expected to hold a word, name,
513
+ or similar, in order to get rid of newlines, double spaces etc.
514
+ inserted just for editing convenience.
515
+ """
516
+ tags = [ 'area', 'bcp14', 'cityarea', 'code', 'country', 'date', 'email', 'extaddr',
517
+ 'keyword', 'organization', 'phone', 'pobox', 'postalLine', 'sortingcode',
518
+ 'street', 'uri', 'workgroup', 'region', 'title', ]
519
+ for c in self.root.iter(tags):
520
+ if c.text:
521
+ t = c.text
522
+ t = re.sub(r'[ \t\n\r\f\v]', ' ', t) # convert \n\t etc. to space (not &nbsp; etc., though)
523
+ t = re.sub(r'\. +', '. ', t) # normalize period followed by more than 2 spaces
524
+ t = re.sub(r'([^.]) +', r'\1 ', t) # normalize non-period followed by more than one space
525
+ t = t.strip() # strip leading and trailing spaces
526
+ c.text = t
527
+
528
+ def bcp14_check_key_words(self, e, p):
529
+ # according to RFC 2119 and 8174
530
+ permitted_words = [ "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
531
+ "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", "OPTIONAL", ]
532
+ text = re.sub(r'(\s|\u00a0)+', ' ', e.text, flags=re.UNICODE).strip()
533
+ if not text in permitted_words:
534
+ self.warn(e, "Expected one of the permitted words or phrases from RFC 2119 and RFC 8174 in <bcp14/>, "
535
+ "but found '%s'." % (etree.tostring(e).strip()))
536
+
537
+ # 5.1.5. Check "anchor"
538
+ #
539
+ # Check all elements for "anchor" attributes. If any "anchor"
540
+ # attribute begins with "s-", "f-", "t-", or "i-", give an error.
541
+ #
542
+ ## modified to use "section-", "figure-", "table-", "index-"
543
+
544
+ def attribute_anchor(self, e, p):
545
+ if not self.prepped:
546
+ reserved = set([ '%s-'%v for v in pnprefix.values() ])
547
+ k = 'anchor'
548
+ if k in e.keys():
549
+ v = e.get(k)
550
+ for prefix in reserved:
551
+ if v.startswith(prefix):
552
+ self.err(e, "Reserved anchor name: %s. Don't use anchor names beginning with one of %s" % (v, ', '.join(reserved)))
553
+
554
+ #
555
+ # 5.2. Defaults
556
+ #
557
+ # These steps will ensure that all default values have been filled in
558
+ # to the XML, in case the defaults change at a later date. Steps in
559
+ # this section will not overwrite existing values in the input file.
560
+ #
561
+ # 5.2.1. "version" Insertion
562
+ #
563
+ # If the <rfc> element has a "version" attribute with a value other
564
+ # than "3", give an error. If the <rfc> element has no "version"
565
+ # attribute, add one with the value "3".
566
+
567
+ def insert_version(self, e, p):
568
+ version = e.get('version', None)
569
+ if version and version != '3':
570
+ self.err(e, "Expected <rfc version='3'>, found version='%s'" % (version,))
571
+ e.set('version', '3')
572
+
573
+ # 5.2.2. "seriesInfo" Insertion
574
+ #
575
+ # If the <front> element of the <rfc> element does not already have a
576
+ # <seriesInfo> element, add a <seriesInfo> element with the name
577
+ # attribute based on the mode in which the prep tool is running
578
+ # ("Internet-Draft" for Draft mode and "RFC" for RFC production mode)
579
+ # and a value that is the input filename minus any extension for
580
+ # Internet-Drafts, and is a number specified by the RFC Editor for
581
+ # RFCs.
582
+ def front_insert_series_info(self, e, p):
583
+ series = e.xpath('seriesInfo')
584
+ if self.root.get('ipr') == 'none':
585
+ return
586
+ if series == None:
587
+ title = e.find('title')
588
+ if title != None:
589
+ pos = e.index(title)+1
590
+ else:
591
+ pos = 0
592
+ path, base = os.path.split(self.options.output_filename)
593
+ name, ext = base.split('.', 1)
594
+ if self.options.rfc:
595
+ if not name.startswith('rfc'):
596
+ self.die(e, "Expected a filename starting with 'rfc' in RFC mode, but found '%s'" % (name, ))
597
+ num = name[3:]
598
+ if not num.isdigit():
599
+ self.die(e, "Expected to find the RFC number in the file name in --rfc mode, but found '%s'" % (num, ))
600
+ e.insert(pos, self.element('seriesInfo', name='RFC', value=self.rfcnumber))
601
+ else:
602
+ e.insert(pos, self.element('seriesInfo', name='Internet-Draft', value=name))
603
+ else:
604
+ if self.options.rfc:
605
+ rfcinfo = e.find('./seriesInfo[@name="RFC"]')
606
+ if rfcinfo is None:
607
+ if self.rfcnumber:
608
+ self.warn(e, "Expected a <seriesInfo> element giving the RFC number in --rfc mode, but found none")
609
+ else:
610
+ self.die(e, "Expected a <seriesInfo> element giving the RFC number in --rfc mode, but found none")
611
+ rfc_number = self.root.get('number')
612
+ if rfc_number and rfcinfo!=None and rfc_number != rfcinfo.get('value'):
613
+ self.die(e, 'Mismatch between <rfc number="%s" ...> and <seriesInfo name="RFC" value="%s">' % (rfc_number, rfcinfo.get('value')))
614
+ if not self.rfcnumber.isdigit():
615
+ self.die(rfcinfo, "Expected a numeric RFC number, but found '%s'" % (self.rfcnumber, ))
616
+
617
+
618
+ # 5.2.3. <date> Insertion
619
+ #
620
+ # If the <front> element in the <rfc> element does not contain a <date>
621
+ # element, add it and fill in the "day", "month", and "year" attributes
622
+ # from the current date. If the <front> element in the <rfc> element
623
+ # has a <date> element with "day", "month", and "year" attributes, but
624
+ # the date indicated is more than three days in the past or is in the
625
+ # future, give a warning. If the <front> element in the <rfc> element
626
+ # has a <date> element with some but not all of the "day", "month", and
627
+ # "year" attributes, give an error.
628
+ ## This tries to change the RFC Editor policy on publication dates
629
+ ## (normally only indicating year and month, not day, of publication. It
630
+ ## also changes the heuristics that have been found helpful in v2.
631
+ ## Disregarding most of this, and going with what seems to make sense.
632
+ def front_insert_date(self, e, p):
633
+ d = e.find('date')
634
+ today = datetime.date.today()
635
+
636
+ if d != None:
637
+ year = d.get('year')
638
+ month = d.get('month')
639
+ day = d.get('day')
640
+ if month and not month.isdigit():
641
+ if len(month) < 3:
642
+ self.err(e, "Expected a month name with at least 3 letters, found '%s'" % (month, ))
643
+ month = normalize_month(month)
644
+ if not year:
645
+ year = str(today.year)
646
+ if not month:
647
+ if year != str(today.year):
648
+ self.warn(e, "Expected <date> to have the current year when month is missing, but found '%s'" % (d.get('year')))
649
+ month = today.strftime('%m')
650
+ day = today.strftime('%d')
651
+ datestr = "%s-%s-%s" %(year, month, day or '01')
652
+ try:
653
+ date = datetime.datetime.strptime(datestr, "%Y-%m-%d").date()
654
+ except ValueError:
655
+ self.die(e, '<date> {} is invalid'.format(datestr))
656
+ if not self.rfcnumber and abs(date - datetime.date.today()) > datetime.timedelta(days=3):
657
+ self.warn(d, "The document date (%s) is more than 3 days away from today's date" % date)
658
+ n = self.element('date', year=year, month=month, line=d.sourceline)
659
+ if day:
660
+ n.set('day', day)
661
+ e.replace(d, n)
662
+ else:
663
+ preceding = e.xpath('title|seriesInfo|author')
664
+ pos = max(e.index(i) for i in preceding)+1
665
+ date = self.options.date or datetime.date.today()
666
+ year = str(date.year)
667
+ month = date.strftime('%m')
668
+ day = date.strftime('%d')
669
+ e.insert(pos, self.element('date', year=year, month=month, day=day))
670
+ self.date = datetime.date(year=int(year), month=int(month), day=int(day or '01'))
671
+
672
+ # 5.2.4. "prepTime" Insertion
673
+ #
674
+ # If the input document includes a "prepTime" attribute of <rfc>, exit
675
+ # with an error.
676
+ #
677
+ # Fill in the "prepTime" attribute of <rfc> with the current datetime.
678
+ def insert_preptime(self, e, p):
679
+ if 'prepTime' in e.attrib:
680
+ if self.liberal:
681
+ self.note(e, "Scanning already prepped source dated %s" % (e.get('prepTime'), ))
682
+ else:
683
+ self.die(e, "Did not expect a prepTime= attribute for <rfc>, but found '%s'" % (e.get('prepTime')))
684
+ else:
685
+ pn = e.xpath('.//*[@pn]')
686
+ if pn and not self.liberal:
687
+ self.die(e, "Inconsistent input. Found pn numbers but no prepTime. Cannot continue.")
688
+ e.set('prepTime', datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'))
689
+
690
+ # 5.2.5. <ol> Group "start" Insertion
691
+ #
692
+ # Add a "start" attribute to every <ol> element containing a group that
693
+ # does not already have a start.
694
+
695
+ def attribute_ol_group(self, e, p):
696
+ group = e.get('group')
697
+ start = e.get('start')
698
+ if start and not start.isdigit():
699
+ self.warn(e, 'Expected the "start" attribute to have a numeric value, but found "%s". Using "1"' %(start,))
700
+ start = "1"
701
+ if not group in self.ol_counts:
702
+ self.ol_counts[group] = 1
703
+ if not start:
704
+ start = self.ol_counts[group]
705
+ e.set('start', str(start))
706
+ else:
707
+ self.ol_counts[group] = int(start)
708
+ self.ol_counts[group] += len(list(e.iterchildren('li')))
709
+
710
+ # 5.2.6. Attribute Default Value Insertion
711
+ #
712
+ # Fill in any default values for attributes on elements, except
713
+ # "keepWithNext" and "keepWithPrevious" of <t>, and "toc" of <section>.
714
+ # Some default values can be found in the RELAX NG schema, while others
715
+ # can be found in the prose describing the elements in [RFC7991].
716
+ def insert_attribute_defaults(self, e, p):
717
+ g = p.getparent() if p != None else None
718
+ ptag = p.tag if p != None else None
719
+ gtag = g.tag if g != None else None
720
+ if not (gtag in ['reference', ] or ptag in ['reference', ]):
721
+ defaults = self.get_attribute_defaults(e.tag)
722
+ for k in defaults:
723
+ if not k in e.attrib:
724
+ if (e.tag, k) in [('rfc', 'consensus')]:
725
+ continue
726
+ #debug.say('L%-5s: Setting <%s %s="%s">' % (e.sourceline, e.tag, k, defaults[k]))
727
+ if ':' in k:
728
+ ns, tag = k.split(':',1)
729
+ q = etree.QName(namespaces[ns], tag)
730
+ e.set(q, defaults[k])
731
+ else:
732
+ e.set(k, defaults[k])
733
+
734
+ # 5.2.7. Section "toc" attribute
735
+ #
736
+ # For each <section>, modify the "toc" attribute to be either "include"
737
+ # or "exclude":
738
+ #
739
+ # o for sections that have an ancestor of <boilerplate>, use "exclude"
740
+ #
741
+ # o else for sections that have a descendant that has toc="include",
742
+ # use "include". If the ancestor section has toc="exclude" in the
743
+ # input, this is an error.
744
+ #
745
+ # o else for sections that are children of a section with
746
+ # toc="exclude", use "exclude".
747
+ #
748
+ # o else for sections that are deeper than rfc/@tocDepth, use
749
+ # "exclude"
750
+ #
751
+ # o else use "include"
752
+ def element_section(self, e, p):
753
+ # we will process .//boilerplate//section elements correctly later,
754
+ # so ignore that condition for now.
755
+ etoc = e.get('toc')
756
+ ptoc = p.get('toc')
757
+ included_descendants = e.xpath('.//section[@toc="include"]')
758
+ edepth = len([ a for a in e.iterancestors('section') ])+1
759
+ if etoc == 'include':
760
+ pass
761
+ elif etoc in [ None, 'default' ]:
762
+ if included_descendants:
763
+ e.set('toc', 'include')
764
+ elif ptoc == 'exclude':
765
+ e.set('toc', 'exclude')
766
+ else:
767
+ tocDepth = self.root.get('tocDepth', '3')
768
+ if tocDepth.isdigit():
769
+ if edepth <= int(tocDepth):
770
+ e.set('toc', 'include')
771
+ else:
772
+ e.set('toc', 'exclude')
773
+ else:
774
+ self.err(self.root, "Expected tocDepth to be an integer, but found '%s'" % (tocDepth))
775
+ elif etoc == 'exclude':
776
+ if included_descendants:
777
+ self.err(e, 'Expected <section> to have toc="include", to match child section attribute, but found toc="exclude"')
778
+ else:
779
+ self.err(e, "Expected the toc attribute to be one of 'include', 'exclude', or 'default', but found '%s'" % etoc)
780
+
781
+
782
+ # 5.2.8. "removeInRFC" Warning Paragraph
783
+ #
784
+ # In I-D mode, if there is a <note> or <section> element with a
785
+ # "removeInRFC" attribute that has the value "true", add a paragraph to
786
+ # the top of the element with the text "This note is to be removed
787
+ # before publishing as an RFC." or "This section...", unless a
788
+ # paragraph consisting of that exact text already exists.
789
+ def attribute_note_removeinrfc_true(self, e, p):
790
+ if not self.options.rfc:
791
+ warning_text = "This %s is to be removed before publishing as an RFC." % e.tag
792
+ # Ignore the <name> element, if present. It must be the first child.
793
+ if len(e) > 0 and e[0].tag == 'name':
794
+ pos = 1
795
+ else:
796
+ pos = 0
797
+ # Check whether the first child (after optional <name>) is a <t>
798
+ first_child_tag = e[pos].tag if len(e) > pos else None
799
+ top_para = e[pos] if first_child_tag == 't' else None
800
+ # If no top_para, or if it is not the warning, then insert the warning
801
+ if top_para is None or top_para.text != warning_text:
802
+ t = self.element('t')
803
+ t.text = warning_text
804
+ e.insert(pos, t)
805
+
806
+ def attribute_section_removeinrfc_true(self, e, p):
807
+ self.attribute_note_removeinrfc_true(e, p)
808
+
809
+
810
+
811
+ # 5.3. Normalization
812
+ #
813
+ # These steps will ensure that ideas that can be expressed in multiple
814
+ # different ways in the input document are only found in one way in the
815
+ # prepared document.
816
+
817
+ # The following is not specified or required, but will make life easier
818
+ # later:
819
+ def attribute_yes_no(self, e, p):
820
+ for k,v in e.attrib.items():
821
+ if v == 'yes':
822
+ e.set(k, 'true')
823
+ elif v == 'no':
824
+ e.set(k, 'false')
825
+
826
+ # 5.3.1. "month" Attribute
827
+ #
828
+ # Normalize the values of "month" attributes in all <date> elements in
829
+ # <front> elements in <rfc> elements to numeric values.
830
+ def element_front_date(self, e, p):
831
+ month = e.get('month')
832
+ if not month.isdigit():
833
+ e.set('month', normalize_month(month))
834
+
835
+ # 5.3.2. ASCII Attribute Processing
836
+ #
837
+ # In every <email>, <organization>, <street>, <city>, <region>,
838
+ # <country>, and <code> element, if there is an "ascii" attribute and
839
+ # the value of that attribute is the same as the content of the
840
+ # element, remove the "ascii" element and issue a warning about the
841
+ # removal.
842
+ def attribute_ascii(self, e, p):
843
+ if e.text.strip() == e.get('ascii').strip():
844
+ del e.attrib['ascii']
845
+ self.warn(e, "Removed a redundant ascii= attribute from <%s>" % (e.tag))
846
+
847
+ # In every <author> element, if there is an "asciiFullname",
848
+ # "asciiInitials", or "asciiSurname" attribute, check the content of
849
+ # that element against its matching "fullname", "initials", or
850
+ # "surname" element (respectively). If the two are the same, remove
851
+ # the "ascii*" element and issue a warning about the removal.
852
+ def element_front_author(self, e, p):
853
+ fullname = e.get('fullname')
854
+ initials = e.get('initials')
855
+ surname = e.get('surname')
856
+ organization = e.find('organization')
857
+ org = organization.text if organization != None and organization.text else None
858
+ if not ((initials and surname) or fullname or org):
859
+ self.err(e, "Expected <author> to have initials, surname and fullname or organization")
860
+ if (initials or surname) and not fullname:
861
+ e.set('fullname', ' '.join( n for n in [initials, surname] if n ))
862
+ for a in ['fullname', 'initials', 'surname']:
863
+ aa = 'ascii'+a.capitalize()
864
+ keys = e.keys()
865
+ if aa in keys and not a in keys:
866
+ self.err(e, "Expected a %s= attribute to match the %s= attribute, but found none" % (a, aa))
867
+ elif a in keys and not aa in keys:
868
+ if not is_script(e.get(a), 'Latin'):
869
+ self.err(e, "Expected an %s= attribute to match the unicode %s value" % (aa, a))
870
+ elif a in keys and aa in keys:
871
+ if e.get(a).strip() == e.get(aa).strip():
872
+ del e.attrib[aa]
873
+ self.warn(e, "Removed a redundant %s= attribute from <%s>" % (aa, e.tag))
874
+
875
+ element_contact = element_front_author
876
+
877
+ # 5.3.3. "title" Conversion
878
+ #
879
+ # For every <section>, <note>, <figure>, <references>, and <texttable>
880
+ # element that has a (deprecated) "title" attribute, remove the "title"
881
+ # attribute and insert a <name> element with the title from the
882
+ # attribute.
883
+ def attribute_title(self, e, p):
884
+ title = e.get('title')
885
+ del e.attrib['title']
886
+ name = e.find('name')
887
+ if title:
888
+ if name is None:
889
+ name = self.element('name', line=e.sourceline)
890
+ name.text = title
891
+ e.insert(0, name)
892
+ else:
893
+ self.warn(e, "Found both title attribute and <name> element for section; dropping the title.")
894
+
895
+ # 5.3.4. "keepWithPrevious" Conversion
896
+ #
897
+ # For every element that has a (deprecated) "keepWithPrevious" attribute,
898
+ # remove the "keepWithPrevious" attribute, and if its value is "true",
899
+ # and the previous element can have a "keepWithNext" element, remove
900
+ # "keepWithPrevious" and replace it with a "keepWithNext" attribute set
901
+ # to "true" on the previous element.
902
+ def attribute_keepwithprevious_true(self, e, p):
903
+ value = e.get('keepWithPrevious')
904
+ # XXX TODO: This is too simple. We need to find the previous
905
+ # content-generating element of relevant types, probably something
906
+ # like ['t', 'dl', 'ol', 'figure', 'table', ], and update that:
907
+ prev = e.getprevious()
908
+ if prev is None:
909
+ return
910
+ prev_attrs = self.get_attribute_names(prev.tag)
911
+ if value=='true' and prev!=None and 'keepWithNext' in prev_attrs:
912
+ prev.set('keepWithNext', value)
913
+ del e.attrib['keepWithPrevious']
914
+
915
+
916
+ # 5.4. Generation
917
+ #
918
+ # These steps will generate new content, overriding existing similar
919
+ # content in the input document. Some of these steps are important
920
+ # enough that they specify a warning to be generated when the content
921
+ # being overwritten does not match the new content.
922
+ #
923
+ # 5.4.1. "expiresDate" Insertion
924
+ #
925
+ # If in I-D mode, fill in "expiresDate" attribute of <rfc> based on the
926
+ # <date> element of the document's <front> element.
927
+ def fill_in_expires_date(self, e, p):
928
+ return
929
+ if not self.options.rfc:
930
+ d = e.find('./front/date')
931
+ date = datetime.date(year=int(d.get('year')), month=int(d.get('month')), day=int(d.get('day')))
932
+ old_exp = e.get('expiresDate')
933
+ new_exp = (date + datetime.timedelta(days=185)).strftime('%Y-%m-%d')
934
+ if old_exp != new_exp:
935
+ e.set('expiresDate', new_exp)
936
+ if old_exp:
937
+ self.warn(e, "Changed the rfc expiresDate attribute to correspond with the <front><date> element: '%s'" % (new_exp, ))
938
+
939
+ #
940
+ # 5.4.2. <boilerplate> Insertion
941
+ #
942
+ # Create a <boilerplate> element if it does not exist. If there are
943
+ # any children of the <boilerplate> element, produce a warning that
944
+ # says "Existing boilerplate being removed. Other tools, specifically
945
+ # the draft submission tool, will treat this condition as an error" and
946
+ # remove the existing children.
947
+ #
948
+ def front_insert_boilerplate(self, e, p):
949
+ if self.root.get('ipr') == 'none':
950
+ # no boilerplate
951
+ return
952
+ old_bp = e.find('boilerplate')
953
+ new_bp = self.element('boilerplate')
954
+ if old_bp != None:
955
+ if not self.liberal and not self.prepped:
956
+ children = old_bp.getchildren()
957
+ if len(children):
958
+ self.warn(old_bp, "Expected no <boilerplate> element, but found one. Replacing the content with new boilerplate")
959
+ for c in children:
960
+ old_bp.remove(c)
961
+ elif not self.rfcnumber:
962
+ for c in old_bp.getchildren():
963
+ old_bp.remove(c)
964
+ else:
965
+ e.append(new_bp)
966
+
967
+ def front_insert_toc(self, e, p):
968
+ old_toc = e.find('toc')
969
+ new_toc = self.element('toc')
970
+ if old_toc != None:
971
+ if not self.liberal and not self.prepped:
972
+ children = old_toc.getchildren()
973
+ if len(children):
974
+ self.warn(old_toc, "Expected no <toc> element, but found one. Replacing the content with new toc")
975
+ for c in children:
976
+ old_toc.remove(c)
977
+ elif not self.rfcnumber:
978
+ for c in old_toc.getchildren():
979
+ old_toc.remove(c)
980
+ else:
981
+ e.append(new_toc)
982
+
983
+ # 5.4.2.1. Compare <rfc> "submissionType" and <seriesInfo> "stream"
984
+ #
985
+ # Verify that <rfc> "submissionType" and <seriesInfo> "stream" are the
986
+ # same if they are both present. If either is missing, add it. Note
987
+ # that both have a default value of "IETF".
988
+ def check_series_and_submission_type(self, e, p):
989
+ submission_type = e.get('submissionType')
990
+ series_info_list = e.xpath('./front/seriesInfo')
991
+ streams = [ i.get('stream') for i in series_info_list if i.get('stream') ] + [ submission_type ]
992
+ if len(set(streams)) > 1:
993
+ if submission_type:
994
+ self.err(series_info_list[0], "The stream setting of <seriesInfo> is inconsistent with the submissionType of <rfc>. Found %s" % (', '.join(streams)))
995
+ else:
996
+ self.err(series_info_list[0], "The stream settings of the <seriesInfo> elements are inconsistent. Found %s" % (', '.join(streams)))
997
+ else:
998
+ stream = list(streams)[0]
999
+ if stream:
1000
+ e.set('submissionType', stream)
1001
+ for i in series_info_list:
1002
+ i.set('stream', stream)
1003
+
1004
+ # 5.4.2.2. "Status of This Memo" Insertion
1005
+ #
1006
+ # Add the "Status of This Memo" section to the <boilerplate> element
1007
+ # with current values. The application will use the "submissionType",
1008
+ # and "consensus" attributes of the <rfc> element, the <workgroup>
1009
+ # element, and the "status" and "stream" attributes of the <seriesInfo>
1010
+ # element, to determine which boilerplate from [RFC7841] to include, as
1011
+ # described in Appendix A of [RFC7991].
1012
+ def boilerplate_insert_status_of_memo(self, e, p):
1013
+ if self.prepped and self.rfcnumber:
1014
+ return
1015
+ if self.root.get('ipr') == 'none':
1016
+ return
1017
+ # submissionType: "IETF" | "IAB" | "IRTF" | "independent" | "editorial"
1018
+ # consensus: "false" | "true"
1019
+ # category: "std" | "bcp" | "exp" | "info" | "historic"
1020
+ b = e.xpath('./section/name[text()="Status of This Memo"]')
1021
+ existing_status_of_memo = b[0] if b else None
1022
+ if existing_status_of_memo:
1023
+ if self.liberal:
1024
+ self.note(e, "Boilerplate 'Status of this Memo' section exists, leaving it in place")
1025
+ return
1026
+ else:
1027
+ e.remove(existing_status_of_memo)
1028
+ #
1029
+ section = self.element('section', numbered='false', toc='exclude', anchor='status-of-memo')
1030
+ name = self.element('name')
1031
+ name.text = "Status of This Memo"
1032
+ section.append(name)
1033
+ format_dict = {}
1034
+ format_dict['scheme'] = 'http' if self.date < self.boilerplate_https_date else 'https'
1035
+ #
1036
+ if self.options.rfc:
1037
+ stream = self.root.get('submissionType')
1038
+ category = self.root.get('category')
1039
+ consensus = self.root.get('consensus')
1040
+ workgroup = self.root.find('./front/workgroup')
1041
+ if not category in strings.category_name:
1042
+ self.err(self.root, "Expected a known category, one of %s, but found '%s'" % (','.join(strings.category_name.keys()), category, ))
1043
+ #
1044
+ group = workgroup.text if workgroup != None else None
1045
+ format_dict.update({ 'rfc_number': self.rfcnumber })
1046
+ if group:
1047
+ format_dict['group_name'] = group
1048
+ #
1049
+ if stream == 'IRTF' and workgroup == None:
1050
+ consensus = 'n/a'
1051
+ elif stream == 'independent' or stream == 'editorial':
1052
+ consensus = 'n/a'
1053
+ try:
1054
+ for para in boilerplate_rfc_status_of_memo[stream][category][consensus]:
1055
+ para = para.format(**format_dict).strip()
1056
+ t = etree.fromstring(para)
1057
+ section.append(t)
1058
+ except KeyError as exception:
1059
+ if str(exception) in ["'rfc_number'", "'group_name'"]:
1060
+ # Error in string expansion
1061
+ self.die(p, 'Expected to have a value for %s when expanding the "Status of This Memo" boilerplate, but found none.' % str(exception))
1062
+ else:
1063
+ # Error in boilerplate dictionary indexes
1064
+ self.die(self.root, 'Unexpected attribute combination(%s): <rfc submissionType="%s" category="%s" consensus="%s">' % (exception, stream, category, consensus))
1065
+
1066
+ else:
1067
+ exp = get_expiry_date(self.tree, self.date)
1068
+ format_dict['expiration_date'] = format_date(exp.year, exp.month, exp.day, legacy=self.options.legacy_date_format)
1069
+ for para in boilerplate_draft_status_of_memo:
1070
+ para = para.format(**format_dict).strip()
1071
+ t = etree.fromstring(para)
1072
+ section.append(t)
1073
+ e.append(section)
1074
+
1075
+ # 5.4.2.3. "Copyright Notice" Insertion
1076
+ #
1077
+ # Add the "Copyright Notice" section to the <boilerplate> element. The
1078
+ # application will use the "ipr" and "submissionType" attributes of the
1079
+ # <rfc> element and the <date> element to determine which portions and
1080
+ # which version of the Trust Legal Provisions (TLP) to use, as
1081
+ # described in A.1 of [RFC7991].
1082
+ def boilerplate_insert_copyright_notice(self, e, p):
1083
+ if self.prepped and self.rfcnumber:
1084
+ return
1085
+ ipr = self.root.get('ipr', '').lower()
1086
+ if ipr == 'none':
1087
+ return
1088
+ b = e.xpath('./section/name[text()="Copyright Notice"]')
1089
+ existing_copyright_notice = b[0] if b else None
1090
+ if existing_copyright_notice:
1091
+ if self.liberal:
1092
+ self.note(e, "Boilerplate 'Copyright Notice' section exists, leaving it in place")
1093
+ return
1094
+ else:
1095
+ e.remove(existing_copyright_notice)
1096
+ #
1097
+ tlp_2_start_date = datetime.date(year=2009, month=2, day=15)
1098
+ tlp_3_start_date = datetime.date(year=2009, month=9, day=12)
1099
+ tlp_4_start_date = datetime.date(year=2009, month=12, day=28)
1100
+ ipr = self.root.get('ipr', '').lower()
1101
+ subtype = self.root.get('submissionType')
1102
+ if not ipr:
1103
+ self.die(self.root, "Missing ipr attribute on <rfc> element.")
1104
+ if not ipr.endswith('trust200902'):
1105
+ self.die(self.root, "Unknown ipr attribute: %s" % (self.root.get('ipr'), ))
1106
+ #
1107
+ if self.date < tlp_2_start_date:
1108
+ self.die(e, "Cannot insert copyright statements earlier than TLP2.0, effective %s" % (tlp_2_start_date))
1109
+ elif self.date < tlp_3_start_date:
1110
+ tlp = "2.0"
1111
+ stream = "n/a"
1112
+ elif self.date < tlp_4_start_date:
1113
+ tlp = "3.0"
1114
+ stream = "n/a"
1115
+ else:
1116
+ # The only difference between 4.0 and 5.0 is selective URL scheme,
1117
+ # which we handle using the http cutover date, through interpolation:
1118
+ tlp = "5.0"
1119
+ stream = 'IETF' if subtype == 'IETF' else 'alt'
1120
+ format_dict = {'year': self.date.year, }
1121
+ format_dict['scheme'] = 'http' if self.date < self.boilerplate_https_date else 'https'
1122
+ section = self.element('section', numbered='false', toc='exclude', anchor='copyright')
1123
+ name = self.element('name')
1124
+ name.text = "Copyright Notice"
1125
+ section.append(name)
1126
+ paras = []
1127
+ paras += boilerplate_tlp[tlp][stream][:]
1128
+ if ipr.startswith('nomodification'):
1129
+ paras += boilerplate_tlp[tlp]['noModification'][:]
1130
+ elif ipr.startswith('noderivatives'):
1131
+ paras += boilerplate_tlp[tlp]['noDerivatives'][:]
1132
+ elif ipr.startswith('pre5378'):
1133
+ paras += boilerplate_tlp[tlp]['pre5378'][:]
1134
+ for para in paras:
1135
+ para = para.format(**format_dict).strip()
1136
+ t = etree.fromstring(para)
1137
+ section.append(t)
1138
+ e.append(section)
1139
+
1140
+ # 5.2.7. Section "toc" attribute
1141
+ #
1142
+ # ...
1143
+ # o for sections that have an ancestor of <boilerplate>, use "exclude"
1144
+
1145
+ def element_boilerplate_section(self, e, p):
1146
+ e.set('toc', 'exclude')
1147
+
1148
+ # 5.4.3. <reference> "target" Insertion
1149
+ #
1150
+ # For any <reference> element that does not already have a "target"
1151
+ # attribute, fill the target attribute in if the element has one or
1152
+ # more <seriesinfo> child element(s) and the "name" attribute of the
1153
+ # <seriesinfo> element is "RFC", "Internet-Draft", or "DOI" or other
1154
+ # value for which it is clear what the "target" should be. The
1155
+ # particular URLs for RFCs, Internet-Drafts, and Digital Object
1156
+ # Identifiers (DOIs) for this step will be specified later by the RFC
1157
+ # Editor and the IESG. These URLs might also be different before and
1158
+ # after the v3 format is adopted.
1159
+ def reference_insert_target(self, e, p):
1160
+ target_pattern = {
1161
+ "RFC": os.path.join(self.options.rfc_base_url, 'rfc{value}'),
1162
+ "Internet-Draft": os.path.join(self.options.id_base_url, '{value}'),
1163
+ "DOI": os.path.join(self.options.doi_base_url, '{value}'),
1164
+ }
1165
+ # datatracker.ietf.org has code to deal with draft urls (text, html, and pdf) lacking
1166
+ # extension, but others may not. www.ietf.org/archive/id/ only has .txt versions:
1167
+ if urlparse(self.options.id_base_url).netloc != 'datatracker.ietf.org':
1168
+ target_pattern["Internet-Draft"] = os.path.join(self.options.id_base_url, '{value}.txt')
1169
+
1170
+ if not e.get('target'):
1171
+ for c in e.xpath('.//seriesInfo'):
1172
+ series_name = c.get('name')
1173
+ if series_name in target_pattern.keys():
1174
+ series_value=c.get('value')
1175
+ if series_value:
1176
+ e.set('target', target_pattern[series_name].format(value=series_value))
1177
+ break
1178
+ else:
1179
+ self.err(c, 'Expected a value= attribute value for <seriesInfo name="%s">, but found none' % (series_name, ))
1180
+
1181
+ def reference_insert_work_in_progress(self, e, p):
1182
+ if self.options.id_is_work_in_progress:
1183
+ for c in e.xpath('.//seriesInfo'):
1184
+ series_name = c.get('name')
1185
+ if series_name in ['Internet-Draft', ]:
1186
+ for refcontent in e.xpath('.//refcontent'):
1187
+ if refcontent.text == "Work in Progress":
1188
+ break
1189
+ else:
1190
+ refcontent = self.element('refcontent')
1191
+ refcontent.text = "Work in Progress"
1192
+ e.append(refcontent)
1193
+
1194
+ def reference_sort_series_info(self, e, p):
1195
+ def series_order(s):
1196
+ name = s.get('name')
1197
+ series_order = { 'STD': 1, 'BCP': 2, 'FYI': 3, 'RFC': 4, 'DOI': 5, }
1198
+ if name in series_order:
1199
+ return series_order[name]
1200
+ else:
1201
+ return sys.maxsize
1202
+ front = e.find('front')
1203
+ series_info = front.xpath('./seriesInfo')
1204
+ series_info.sort(key=lambda x: series_order(x) )
1205
+ pos = sys.maxsize
1206
+ for s in series_info:
1207
+ i = front.index(s)
1208
+ if i < pos:
1209
+ pos = i
1210
+ front.remove(s)
1211
+ for s in series_info:
1212
+ front.insert(pos, s)
1213
+ pos += 1
1214
+
1215
+ def referencegroup_insert_target(self, e, p):
1216
+ target_pattern = {
1217
+ "BCP": os.path.join(self.options.info_base_url, 'bcp{value}'),
1218
+ "STD": os.path.join(self.options.info_base_url, 'std{value}'),
1219
+ "FYI": os.path.join(self.options.info_base_url, 'fyi{value}'),
1220
+ }
1221
+
1222
+ if not e.get('target'):
1223
+ for c in e.xpath('.//seriesInfo'):
1224
+ series_name = c.get('name')
1225
+ if series_name in target_pattern.keys():
1226
+ series_value=c.get('value')
1227
+ if series_value:
1228
+ e.set('target', target_pattern[series_name].format(value=series_value))
1229
+ break
1230
+ else:
1231
+ self.err(c, 'Expected a value= attribute value for <seriesInfo name="%s">, but found none' % (series_name, ))
1232
+
1233
+ #
1234
+ # 5.4.4. <name> Slugification
1235
+ #
1236
+ # Add a "slugifiedName" attribute to each <name> element that does not
1237
+ # contain one; replace the attribute if it contains a value that begins
1238
+ # with "n-".
1239
+ def name_insert_slugified_name(self, e, p):
1240
+ text = ' '.join(list(e.itertext()))
1241
+ slug = self.slugify_name('name-'+text) if text else None
1242
+ if slug:
1243
+ e.set('slugifiedName', slug)
1244
+
1245
+ #
1246
+ # 5.4.5. <reference> Sorting
1247
+ #
1248
+ # If the "sortRefs" attribute of the <rfc> element is true, sort the
1249
+ # <reference> and <referencegroup> elements lexically by the value of
1250
+ # the "anchor" attribute, as modified by the "to" attribute of any
1251
+ # <displayreference> element. The RFC Editor needs to determine what
1252
+ # the rules for lexical sorting are. The authors of this document
1253
+ # acknowledge that getting consensus on this will be a difficult task.
1254
+ def references_sort(self, e, p):
1255
+ sort_refs = (self.root.get('sortRefs', 'true') == 'true') and (self.root.get('symRefs', 'true') == 'true')
1256
+ if sort_refs:
1257
+ children = e.xpath('./reference') + e.xpath('./referencegroup')
1258
+ children.sort(key=lambda x: self.refname_mapping[x.get('anchor')].upper() )
1259
+ for c in children:
1260
+ e.remove(c)
1261
+ if len(e):
1262
+ e[-1].tail = ''
1263
+ for c in children:
1264
+ e.append(c)
1265
+
1266
+ def references_add_derived_anchor(self, e, p):
1267
+ children = e.xpath('./reference') + e.xpath('./referencegroup')
1268
+ for c in children:
1269
+ anchor = c.get('anchor')
1270
+ c.set('derivedAnchor', self.refname_mapping[anchor])
1271
+
1272
+ def references_check_usage(self, e, p):
1273
+ children = e.xpath('./reference') + e.xpath('./referencegroup')
1274
+ for c in children:
1275
+ anchor = c.get('anchor')
1276
+ if not ( self.root.xpath('.//xref[@target="%s"]'%anchor)
1277
+ or self.root.xpath('.//relref[@target="%s"]'%anchor)):
1278
+ x = c if getattr(c, 'base') == getattr(e, 'base') else c.getparent()
1279
+ self.warn(x, "Unused reference: There seems to be no reference to [%s] in the document" % anchor)
1280
+
1281
+ # 5.4.6. "pn" Numbering
1282
+ #
1283
+ # Add "pn" attributes for all parts. Parts are:
1284
+ #
1285
+ # o <section> in <middle>: pn='s-1.4.2'
1286
+ #
1287
+ # o <references>: pn='s-12' or pn='s-12.1'
1288
+ #
1289
+ # o <abstract>: pn='s-abstract'
1290
+ #
1291
+ # o <note>: pn='s-note-2'
1292
+ #
1293
+ # o <section> in <boilerplate>: pn='s-boilerplate-1'
1294
+ #
1295
+ # o <table>: pn='t-3'
1296
+ #
1297
+ # o <figure>: pn='f-4'
1298
+ #
1299
+ # o <artwork>, <aside>, <blockquote>, <dt>, <li>, <sourcecode>, <t>:
1300
+ # pn='p-[section]-[counter]'
1301
+ #
1302
+ # N.B., the pn formats have changed from the above comment.
1303
+ #
1304
+ def boilerplate_section_add_number(self, e, p):
1305
+ self.boilerplate_section_number += 1
1306
+ self.set_element_pn(e, '%s-boilerplate.%s' % (pnprefix[e.tag], self.boilerplate_section_number, ))
1307
+
1308
+ def toc_section_add_number(self, e, p):
1309
+ self.toc_section_number += 1
1310
+ self.set_element_pn(e, '%s-toc.%s' % (pnprefix[e.tag], self.toc_section_number, ))
1311
+
1312
+ def front_abstract_add_number(self, e, p):
1313
+ self.set_element_pn(e, '%s-abstract' % pnprefix[e.tag])
1314
+
1315
+ def front_note_add_number(self, e, p):
1316
+ self.note_number += 1
1317
+ self.set_element_pn(e, '%s-note.%s' % (pnprefix[e.tag], self.note_number, ))
1318
+
1319
+ def middle_section_add_number(self, e, p):
1320
+ level = len(list(e.iterancestors('section')))
1321
+ if level > self.prev_section_level:
1322
+ self.middle_section_number.append(0)
1323
+ self.middle_section_number[level] += 1
1324
+ if level < self.prev_section_level:
1325
+ self.middle_section_number = self.middle_section_number[:level+1]
1326
+ self.set_element_pn(e, '%s-%s' % (pnprefix[e.tag], '.'.join([ str(n) for n in self.middle_section_number ]), ))
1327
+ self.prev_section_level = level
1328
+ self.references_number[0] = self.middle_section_number[0]
1329
+
1330
+ def table_add_number(self, e, p):
1331
+ self.table_number += 1
1332
+ self.set_element_pn(e, '%s-%s' % (pnprefix[e.tag], self.table_number, ))
1333
+
1334
+ def figure_add_number(self, e, p):
1335
+ self.figure_number += 1
1336
+ self.set_element_pn(e, '%s-%s' % (pnprefix[e.tag], self.figure_number, ))
1337
+
1338
+ def u_add_number(self, e, p):
1339
+ self.unicode_number += 1
1340
+ self.set_element_pn(e, '%s-%s' % (pnprefix[e.tag], self.unicode_number, ))
1341
+
1342
+ def references_add_number(self, e, p):
1343
+ level = len(list(e.iterancestors('references')))
1344
+ if level > self.prev_references_level:
1345
+ self.references_number.append(0)
1346
+ self.references_number[level] += 1
1347
+ if level < self.prev_references_level:
1348
+ self.references_number = self.references_number[:level+1]
1349
+ self.set_element_pn(e, '%s-%s' % (pnprefix[e.tag], '.'.join([ str(n) for n in self.references_number ]), ))
1350
+ self.prev_references_level = level
1351
+
1352
+ def back_section_add_number(self, e, p):
1353
+ level = len(list(e.iterancestors('section')))
1354
+ if level > self.prev_section_level:
1355
+ self.back_section_number.append(0)
1356
+ self.back_section_number[level] += 1
1357
+ if level < self.prev_section_level:
1358
+ self.back_section_number = self.back_section_number[:level+1]
1359
+ section_number = self.back_section_number[:]
1360
+ if section_number[0] > 26:
1361
+ # avoid running off the end of the alphabet when assigning appendix letters
1362
+ self.err(e, '<back> has at least %s sections, only 26 supported' % section_number[0])
1363
+ section_number[0] = chr(0x60+section_number[0])
1364
+ self.set_element_pn(e, '%s-appendix.%s' % (pnprefix[e.tag], '.'.join([ str(n) for n in section_number ]), ))
1365
+ self.prev_section_level = level
1366
+
1367
+ def paragraph_add_numbers(self, e, p):
1368
+ # In this case, we need to keep track of separate paragraph number
1369
+ # sequences as we descend to child levels and return. Handle this by
1370
+ # recursive descent.
1371
+ def child(s, prefix=None, num=[]):
1372
+ def num2str(num):
1373
+ return '.'.join([ str(n) for n in num ])
1374
+ #
1375
+ para_tags = ['artset', 'artwork', 'aside', 'blockquote', 'list', 'dl', 'dd', 'ol', 'ul', 'dt', 'li', 'sourcecode', 't', 'figure', 'table', ]
1376
+ bloc_tags = ['thead', 'tbody', 'tfoot', 'tr', 'td', ]
1377
+ sect_tags = ['abstract', 'note', 'section', 'references' ]
1378
+ skip_tags = ['reference', ]
1379
+ if s.tag in sect_tags:
1380
+ if not s.get('pn'):
1381
+ self.warn(s, "Expected a pn number, found none in <%s>" % (s.tag, ))
1382
+ prefix = s.get('pn', 'unknown-unknown')+'-'
1383
+ num = [0, ]
1384
+ for c in s:
1385
+ if c.tag in para_tags:
1386
+ if prefix is None:
1387
+ debug.show('s.tag')
1388
+ debug.show('s.items()')
1389
+ debug.show('c.tag')
1390
+ debug.show('c.items()')
1391
+ num[-1] += 1
1392
+ if c.tag not in ['figure', 'table']:
1393
+ self.set_element_pn(c, prefix+num2str(num))
1394
+ elif c.tag in bloc_tags:
1395
+ num[-1] += 1
1396
+ if c.tag in sect_tags:
1397
+ child(c, num)
1398
+ elif c.tag in skip_tags:
1399
+ pass
1400
+ else:
1401
+ num.append(0)
1402
+ num = child(c, prefix, num)
1403
+ num = num[:-1]
1404
+ return num
1405
+ child(e)
1406
+
1407
+ # 5.4.7. <iref> Numbering
1408
+ #
1409
+ # In every <iref> element, create a document-unique "pn" attribute.
1410
+ # The value of the "pn" attribute will start with 'i-', and use the
1411
+ # item attribute, the subitem attribute (if it exists), and a counter
1412
+ # to ensure uniqueness. For example, the first instance of "<iref
1413
+ # item='foo' subitem='bar'>" will have the "irefid" attribute set to
1414
+ # 'i-foo-bar-1'.
1415
+ def iref_add_number(self, e, p):
1416
+ def get_anchor(parent):
1417
+ # Walk up the tree until an anchored or numbered element is encountered
1418
+ while parent is not None:
1419
+ # <reference> elements inside a <referencegroup> are not labeled with their
1420
+ # own anchor, so skip up to the <referencegroup> element.
1421
+ if parent.tag == 'reference':
1422
+ grandparent = parent.getparent()
1423
+ if grandparent.tag == 'referencegroup':
1424
+ parent = grandparent
1425
+ anchor = parent.get('anchor') or parent.get('pn')
1426
+ if anchor:
1427
+ return anchor, parent.tag
1428
+ parent = parent.getparent()
1429
+ return None, None
1430
+
1431
+ item = e.get('item')
1432
+ sub = e.get('subitem')
1433
+ self.iref_number += 1
1434
+ if not item:
1435
+ self.err(e, "Expected <iref> to have an item= attribute, but found none")
1436
+ else:
1437
+ if sub:
1438
+ pn = self.slugify_name('%s-%s-%s-%s' % (pnprefix[e.tag], item, sub, self.iref_number))
1439
+ else:
1440
+ pn = self.slugify_name('%s-%s-%s' % (pnprefix[e.tag], item, self.iref_number))
1441
+ self.set_element_pn(e, pn)
1442
+ anchor, anchor_tag = get_anchor(p)
1443
+ if not anchor:
1444
+ self.err(e, "Did not find an anchor to use for <iref item='%s'> in <%s>" % (item, p.tag))
1445
+ else:
1446
+ self.index_entries.append(index_item(re_spaces.sub(' ', item), sub, anchor, anchor_tag, e))
1447
+
1448
+ def ol_add_counter(self, e, p):
1449
+ start = e.get('start')
1450
+ if not start.isdigit():
1451
+ self.warn(e, "Expected a numeric value for the 'start' attribute, but found %s" % (etree.tostring(e), ))
1452
+ start = '1'
1453
+ counter = int(start)
1454
+ #
1455
+ type = e.get('type')
1456
+ if not type:
1457
+ self.warn(e, "Expected the 'type' attribute to have a string value, but found %s" % (etree.tostring(e), ))
1458
+ type = '1'
1459
+ #
1460
+ if len(type) > 1:
1461
+ if '%p' in type:
1462
+ pcounter = None
1463
+ for p in e.iterancestors('li'):
1464
+ pcounter = p.get('derivedCounter')
1465
+ if pcounter:
1466
+ type = type.replace('%p', pcounter )
1467
+ break
1468
+ if not pcounter:
1469
+ self.err(e, "Expected an outer list to fetch the '%p' parent counter value from, but found none")
1470
+ formspec = re.search('%([cCdiIoOxX])', type)
1471
+ if formspec:
1472
+ fchar = formspec.group(1)
1473
+ fspec = formspec.group(0)
1474
+ format = type.replace(fspec, '%s')
1475
+ else:
1476
+ self.err(e, "Expected an <ol> format specification of '%%' followed by upper- or lower-case letter, of one of c,d,i,o,x; but found '%s'" % (type, ))
1477
+ fchar = 'd'
1478
+ format = '%s'
1479
+ else:
1480
+ fchar = type
1481
+ format = '%s.'
1482
+ #
1483
+ int2str = ol_style_formatter[fchar]
1484
+ for c in e.getchildren():
1485
+ if c.tag == 'li': # not PI or Comment
1486
+ label = format % int2str(counter)
1487
+ counter += 1
1488
+ c.set('derivedCounter', label)
1489
+
1490
+ # 5.4.8. <xref> Processing
1491
+ #
1492
+ # 5.4.8.1. "derivedContent" Insertion (with Content)
1493
+ #
1494
+ # For each <xref> element that has content, fill the "derivedContent"
1495
+ # with the element content, having first trimmed the whitespace from
1496
+ # ends of content text. Issue a warning if the "derivedContent"
1497
+ # attribute already exists and has a different value from what was
1498
+ # being filled in.
1499
+ #
1500
+ # 5.4.8.2. "derivedContent" Insertion (without Content)
1501
+ #
1502
+ # For each <xref> element that does not have content, fill the
1503
+ # "derivedContent" attribute based on the "format" attribute.
1504
+ #
1505
+ # o For a value of "counter", the "derivedContent" is set to the
1506
+ # section, figure, table, or ordered list number of the element with
1507
+ # an anchor equal to the <xref> target.
1508
+ #
1509
+ # o For format='default' and the "target" attribute points to a
1510
+ # <reference> or <referencegroup> element, the "derivedContent" is
1511
+ # the value of the "target" attribute (or the "to" attribute of a
1512
+ # <displayreference> element for the targeted <reference>).
1513
+ #
1514
+ # o For format='default' and the "target" attribute points to a
1515
+ # <section>, <figure>, or <table>, the "derivedContent" is the name
1516
+ # of the thing pointed to, such as "Section 2.3", "Figure 12", or
1517
+ # "Table 4".
1518
+ #
1519
+ # o For format='title', if the target is a <reference> element, the
1520
+ # "derivedContent" attribute is the name of the reference, extracted
1521
+ # from the <title> child of the <front> child of the reference.
1522
+ #
1523
+ # o For format='title', if the target element has a <name> child
1524
+ # element, the "derivedContent" attribute is the text content of
1525
+ # that <name> element concatenated with the text content of each
1526
+ # descendant node of <name> (that is, stripping out all of the XML
1527
+ # markup, leaving only the text).
1528
+ #
1529
+ # o For format='title', if the target element does not contain a
1530
+ # <name> child element, the "derivedContent" attribute is the value
1531
+ # of the "target" attribute with no other adornment. Issue a
1532
+ # warning if the "derivedContent" attribute already exists and has a
1533
+ # different value from what was being filled in.
1534
+ def build_derived_content(self, e):
1535
+ def split_pn(t, pn):
1536
+ if pn is None:
1537
+ self.die(e, "Expected to find a pn= attribute on <%s anchor='%s'> when processing <xref>, but found none" % (t.tag, t.get('anchor')), trace=True)
1538
+ type, num, para = self.split_pn(pn)
1539
+ if self.is_appendix(pn):
1540
+ type = 'appendix'
1541
+ return type.capitalize(), num.title(), para
1542
+ #
1543
+ def get_name(t):
1544
+ """Get target element name or None"""
1545
+ name = t if t.tag == 'name' else t.find('./name')
1546
+ return None if name is None else clean_text(''.join(list(name.itertext())))
1547
+ #
1548
+ target = e.get('target')
1549
+ if not target:
1550
+ self.die(e, "Expected <xref> to have a target= attribute, but found none")
1551
+ t = self.root.find('.//*[@anchor="%s"]'%(target, ))
1552
+ if t is None:
1553
+ t = self.root.find('.//*[@pn="%s"]'%(target, ))
1554
+ if t is None:
1555
+ t = self.root.find('.//*[@slugifiedName="%s"]'%(target, ))
1556
+ if t is None:
1557
+ self.die(e, "Found no element to match the <xref> target attribute '%s'" % (target, ))
1558
+ #
1559
+ p = t
1560
+ pn = None
1561
+ while p != None and pn == None:
1562
+ pn = p.get('pn')
1563
+ p = p.getparent()
1564
+
1565
+ #
1566
+ format = e.get('format', 'default')
1567
+ content = ''
1568
+ if format == 'counter':
1569
+ if not t.tag in ['section', 'table', 'figure', 'li', 'reference', 'references', 't', 'dt', ]:
1570
+ self.die(e, "Using <xref> format='%s' with a <%s> target is not supported" % (format, t.tag, ))
1571
+ elif t.tag == 'reference':
1572
+ if not e.get('section'):
1573
+ self.die(e, "Using <xref> format='%s' with a <%s> target requires a section attribute" % (format, t.tag, ))
1574
+ _, num, _ = split_pn(t, pn)
1575
+ if t.tag == 'li':
1576
+ parent = t.getparent()
1577
+ if not parent.tag == 'ol':
1578
+ self.die(e, "Using <xref> format='counter' with a <%s><%s> target is not supported" %(parent.tag, t.tag, ))
1579
+ content = t.get('derivedCounter').rstrip('.')
1580
+ elif t.tag == 'reference':
1581
+ content = e.get('section')
1582
+ else:
1583
+ content = num
1584
+ elif format == 'default':
1585
+ if t.tag in [ 'reference', 'referencegroup' ]:
1586
+ anchor = t.get('anchor')
1587
+ content = '%s' % self.refname_mapping[anchor] if anchor in self.refname_mapping else anchor
1588
+ elif t.tag in [ 't', 'ul', 'ol', 'dl', ]:
1589
+ type, num, para = split_pn(t, pn)
1590
+ content = "%s %s, Paragraph %s" % (type, num, para)
1591
+ elif t.tag in [ 'li', ]:
1592
+ type, num, para = split_pn(t, pn)
1593
+ para, item = para.split('.', 1)
1594
+ content = "%s %s, Paragraph %s, Item %s" % (type, num, para, item)
1595
+ elif t.tag == 'u':
1596
+ try:
1597
+ content = expand_unicode_element(t, bare=True)
1598
+ except (RuntimeError, ValueError) as exc:
1599
+ self.err(t, '%s' % exc)
1600
+ elif t.tag in ['author', 'contact']:
1601
+ content = full_author_name_expansion(t)
1602
+ elif t.tag in ['abstract']:
1603
+ content = t.tag.capitalize()
1604
+ elif t.tag in ['cref']:
1605
+ content = "Comment %s" % target
1606
+ else:
1607
+ # If section is numbered, refer by number, otherwise use section name if available
1608
+ numbered = t.get('numbered') != 'false'
1609
+ type, num, _ = split_pn(t, pn)
1610
+ if numbered:
1611
+ label = num
1612
+ else:
1613
+ label = '"%s"' % (get_name(t) or target)
1614
+ content = "%s %s" % (type, label)
1615
+ elif format == 'title':
1616
+ if t.tag in ['u', 'author', 'contact', ]:
1617
+ self.die(e, "Using <xref> format='%s' with a <%s> target is not supported" % (format, t.tag, ))
1618
+ elif t.tag == 'reference':
1619
+ title = t.find('./front/title')
1620
+ if title is None:
1621
+ self.err(t, "Expected a <title> element when processing <xref> to <%s>, but found none" % (t.tag, ))
1622
+ content = clean_text(title.text)
1623
+ elif t.tag in ['abstract']:
1624
+ content = t.tag.capitalize()
1625
+ else:
1626
+ name = get_name(t)
1627
+ content = name if name is not None else target
1628
+ elif format == 'none':
1629
+ if self.options.verbose and not e.text:
1630
+ self.warn(e, 'Expected <%s format="none"> to have text content, but found none. There will be no text rendered for this element.' % (e.tag, ))
1631
+ else:
1632
+ self.err(e, "Expected format to be one of 'default', 'title', 'counter' or 'none', but found '%s'" % (format, ) )
1633
+ return t, content
1634
+
1635
+ def element_xref(self, e, p):
1636
+ section = e.get('section')
1637
+ relative = e.get('relative')
1638
+ t, content = self.build_derived_content(e)
1639
+ is_toc = p.get('pn', '').startswith('section-toc')
1640
+ if not (section or relative):
1641
+ attr = e.get('derivedContent')
1642
+ if self.options.verbose and attr and attr != content:
1643
+ self.err(e, "When processing <xref>, found derivedContent='%s' when trying to set it to '%s'" % (attr, content))
1644
+ if not is_toc:
1645
+ e.set('derivedContent', content)
1646
+ else:
1647
+ if relative != None and section is None:
1648
+ self.err(e, "Cannot render an <%s> with a relative= attribute without also having a section= attribute." % (e.tag))
1649
+ if t.tag != 'reference':
1650
+ self.err(e, "Expected the target of an <%s> with a section= attribute to be a <reference>, found <%s>" % (e.tag, t.tag, ))
1651
+ if relative is None:
1652
+ for s in t.xpath('.//seriesInfo'):
1653
+ if s.get('name') in ['RFC', 'Internet-Draft']:
1654
+ if section[0].isdigit():
1655
+ relative = '#section-%s' % section.title()
1656
+ else:
1657
+ relative = '#appendix-%s' % section.title()
1658
+ break
1659
+ if not relative:
1660
+ self.err(e, 'Cannot build a href for <%s target="%s"> with a section= attribute without also having a relative= attribute.' % (e.tag, e.get('target')))
1661
+ if relative:
1662
+ url = t.get('target')
1663
+ if self.options.rfc_reference_base_url:
1664
+ ss = t.xpath('.//seriesInfo[@name="RFC"]')
1665
+ if ss:
1666
+ num = ss[0].get('value')
1667
+ url = urljoin(self.options.rfc_reference_base_url, "rfc%s" % num)
1668
+ if self.options.id_reference_base_url:
1669
+ ss = t.xpath('.//seriesInfo[@name="Internet-Draft"]')
1670
+ if ss:
1671
+ name = ss[0].get('value')
1672
+ url = urljoin(self.options.id_reference_base_url, name)
1673
+ if url is None:
1674
+ self.err(e, "Cannot build a href for <reference anchor='%s'> without having a target= attribute giving the URL." % (t.get('anchor'), ))
1675
+ link = urljoin(url, relative, allow_fragments=True)
1676
+ e.set('derivedLink', link)
1677
+ attr = e.get('derivedContent')
1678
+ if attr and attr != content:
1679
+ self.err(e, "When processing <xref>, found derivedContent='%s' when trying to set it to '%s'" % (attr, content))
1680
+ e.set('derivedContent', content)
1681
+ #
1682
+ sform = e.get('sectionFormat')
1683
+ if sform in ['of', 'comma', 'parens', ]:
1684
+ if not content:
1685
+ self.err(e, 'Found sectionFormat="%s" with blank derivedContent' % sform)
1686
+ elif sform == 'bare':
1687
+ format = e.get('format')
1688
+ if format in ['title', 'counter', 'none', ]:
1689
+ self.warn(e, 'Unexpected format="%s" used with sectionFormat="bare". Setting format has no effect with sectionFormat="bare"' % (format, ))
1690
+ else:
1691
+ self.err(e, 'Unexpected sectionFormat: "%s"' % (sform, ))
1692
+
1693
+ # 5.4.9. <relref> Processing
1694
+ #
1695
+ # If any <relref> element's "target" attribute refers to anything but a
1696
+ # <reference> element, give an error.
1697
+ #
1698
+ # For each <relref> element, fill in the "derivedLink" attribute.
1699
+
1700
+
1701
+ # 5.5. Inclusion
1702
+ #
1703
+ # These steps will include external files into the output document.
1704
+ #
1705
+ # 5.5.1. <artwork> Processing
1706
+ #
1707
+ def check_src_scheme(self, e, src):
1708
+ permitted_schemes = ['file', 'http', 'https', 'data', ]
1709
+ scheme, netloc, path, query, fragment = urlsplit(src)
1710
+ if scheme == '':
1711
+ scheme = 'file'
1712
+ if not scheme in permitted_schemes:
1713
+ self.err(e, "Expected an <%s> src scheme of '%s:', but found '%s:'" % (e.tag, scheme, ":', '".join(permitted_schemes)))
1714
+ return (scheme, netloc, path, query, fragment)
1715
+
1716
+ def check_src_file_path(self, e, scheme, netloc, path, query, fragment):
1717
+ shellmeta = re.compile("[><*[`$|;&(#]")
1718
+ #
1719
+ dir = os.path.abspath(os.path.dirname(self.xmlrfc.source))
1720
+ path = os.path.abspath(os.path.join(dir, path))
1721
+ if not path.startswith(dir):
1722
+ self.err(e, "Expected an <%s> src= file located beside or below the .xml source (in %s), but found a reference to %s" % (e.tag, dir, path))
1723
+ return None
1724
+ src = urlunsplit((scheme, '', path, '', ''))
1725
+ if shellmeta.search(src):
1726
+ self.err(e, "Found disallowed shell meta-characters in the src='file:...' attribute")
1727
+ return None
1728
+ if not os.path.exists(path):
1729
+ self.err(e, "Expected an <%s> src= file at '%s', but no such file exists" % (e.tag, path, ))
1730
+ return None
1731
+ #
1732
+ e.set('src', src)
1733
+ return src
1734
+
1735
+ def element_artset(self, e, p):
1736
+ anchors = [ w.get('anchor') for w in e.xpath('./artwork[@anchor]') ]
1737
+ if anchors:
1738
+ anchors.sort()
1739
+ if not e.get('anchor'):
1740
+ e.set('anchor', anchors[0])
1741
+ self.warn(e, "Moved anchor '%s' on <artset><artwork> up to <artset>" % anchors[0])
1742
+ if len(anchors) > 1:
1743
+ if e.get('anchor') == anchors[0]:
1744
+ self.warn(e, "Found multiple anchors on <artwork> within <artset>. Please use an anchor on <artset> instead.")
1745
+ self.warn(e, "-- Keeping anchor '%s', discarding '%s'." % (anchors[0], "', '".join(anchors[1:])))
1746
+ else:
1747
+ self.warn(e, "Found an anchor on <artset>, but also multiple anchors on <artwork> within <artset>. Please use only the one on <artset>.")
1748
+ self.warn(e, "-- Discarding anchors '%s'." % ("', '".join(anchors)))
1749
+ for w in e.xpath('./artwork[@anchor]'):
1750
+ del w.attrib['anchor']
1751
+
1752
+ def element_artwork(self, e, p):
1753
+
1754
+ # 1. If an <artwork> element has a "src" attribute where no scheme is
1755
+ # specified, copy the "src" attribute value to the "originalSrc"
1756
+ # attribute, and replace the "src" value with a URI that uses the
1757
+ # "file:" scheme in a path relative to the file being processed.
1758
+ # See Section 7 for warnings about this step. This will likely be
1759
+ # one of the most common authoring approaches.
1760
+
1761
+ src = e.get('src','').strip()
1762
+ if src:
1763
+ original_src = src
1764
+ data = None
1765
+ scheme, netloc, path, query, fragment = self.check_src_scheme(e, src)
1766
+
1767
+ # 2. If an <artwork> element has a "src" attribute with a "file:"
1768
+ # scheme, and if processing the URL would cause the processor to
1769
+ # retrieve a file that is not in the same directory, or a
1770
+ # subdirectory, as the file being processed, give an error. If the
1771
+ # "src" has any shellmeta strings (such as "`", "$USER", and so on)
1772
+ # that would be processed, give an error. Replace the "src"
1773
+ # attribute with a URI that uses the "file:" scheme in a path
1774
+ # relative to the file being processed. This rule attempts to
1775
+ # prevent <artwork src='file:///etc/passwd'> and similar security
1776
+ # issues. See Section 7 for warnings about this step.
1777
+ if scheme == 'file':
1778
+ src = self.check_src_file_path(e, scheme, netloc, path, query, fragment)
1779
+
1780
+ if src != original_src:
1781
+ e.set('originalSrc', original_src)
1782
+
1783
+ # 3. If an <artwork> element has a "src" attribute, and the element
1784
+ # has content, give an error.
1785
+
1786
+ if e.text and e.text.strip():
1787
+ self.err(e, "Found <artwork> with both a 'src' attribute and content. Please use <artset> with multiple <artwork> instances instead.")
1788
+ e.text = None
1789
+
1790
+ # 4. If an <artwork> element has type='svg' and there is an "src"
1791
+ # attribute, the data needs to be moved into the content of the
1792
+ # <artwork> element.
1793
+
1794
+ if src: # Test again, after check_src_file_path()
1795
+ awtype = e.get('type')
1796
+ if awtype is None:
1797
+ self.warn(e, "No 'type' attribute value provided for <artwork>, cannot process source %s" % src)
1798
+ elif awtype == 'svg':
1799
+
1800
+ # * If the "src" URI scheme is "data:", fill the content of the
1801
+ # <artwork> element with that data and remove the "src"
1802
+ # attribute.
1803
+
1804
+ # * If the "src" URI scheme is "file:", "http:", or "https:", fill
1805
+ # the content of the <artwork> element with the resolved XML
1806
+ # from the URI in the "src" attribute. If there is no
1807
+ # "originalSrc" attribute, add an "originalSrc" attribute with
1808
+ # the value of the URI and remove the "src" attribute.
1809
+
1810
+ if scheme in ['file', 'http', 'https', 'data']:
1811
+ with closing(urlopen(src)) as f:
1812
+ data = f.read()
1813
+ svg = etree.fromstring(data)
1814
+ e.append(svg)
1815
+ del e.attrib['src']
1816
+ else:
1817
+ self.err(e, "Unexpected <artwork> src scheme: '%s'" % scheme)
1818
+
1819
+ # * If the <artwork> element has an "alt" attribute, and the SVG
1820
+ # does not have a <desc> element, add the <desc> element with
1821
+ # the contents of the "alt" attribute.
1822
+ alt = e.get('alt')
1823
+ if alt and svg != None:
1824
+ if not svg.xpath('.//desc'):
1825
+ desc = self.element('{%s}desc'%namespaces['svg'], line=e.sourceline)
1826
+ desc.text = alt
1827
+ desc.tail = '\n'
1828
+ desc.sourceline = e.sourceline
1829
+ svg.insert(0, desc)
1830
+ del e.attrib['alt']
1831
+
1832
+ # 5. If an <artwork> element has type='binary-art', the data needs to
1833
+ # be in an "src" attribute with a URI scheme of "data:". If the
1834
+ # "src" URI scheme is "file:", "http:", or "https:", resolve the
1835
+ # URL. Replace the "src" attribute with a "data:" URI, and add an
1836
+ # "originalSrc" attribute with the value of the URI. For the
1837
+ # "http:" and "https:" URI schemes, the mediatype of the "data:"
1838
+ # URI will be the Content-Type of the HTTP response. For the
1839
+ # "file:" URI scheme, the mediatype of the "data:" URI needs to be
1840
+ # guessed with heuristics (this is possibly a bad idea). This also
1841
+ # fails for content that includes binary images but uses a type
1842
+ # other than "binary-art". Note: since this feature can't be used
1843
+ # for RFCs at the moment, this entire feature might be
1844
+ elif awtype == 'binary-art':
1845
+ # keep svg in src attribute
1846
+ if scheme in ['http', 'https']:
1847
+ with closing(urlopen(src)) as f:
1848
+ data = f.read()
1849
+ mediatype = f.info().get_content_type()
1850
+ src = build_dataurl(mediatype, data)
1851
+ e.set('src', src)
1852
+ elif scheme == 'file':
1853
+ self.err(e, "Won't guess media-type of file '%s', please use the data: scheme instead." % (src, ))
1854
+
1855
+ # 6. If an <artwork> element does not have type='svg' or
1856
+ # type='binary-art' and there is an "src" attribute, the data needs
1857
+ # to be moved into the content of the <artwork> element. Note that
1858
+ # this step assumes that all of the preferred types other than
1859
+ # "binary-art" are text, which is possibly wrong.
1860
+ #
1861
+ # * If the "src" URI scheme is "data:", fill the content of the
1862
+ # <artwork> element with the correctly escaped form of that data
1863
+ # and remove the "src" attribute.
1864
+ #
1865
+ # * If the "src" URI scheme is "file:", "http:", or "https:", fill
1866
+ # the content of the <artwork> element with the correctly
1867
+ # escaped form of the resolved text from the URI in the "src"
1868
+ # attribute. If there is no "originalSrc" attribute, add an
1869
+ # "originalSrc" attribute with the value of the URI and remove
1870
+ # the "src" attribute.
1871
+ else:
1872
+ if scheme in ['file', 'http', 'https', 'data']:
1873
+ try:
1874
+ with closing(urlopen(src)) as f:
1875
+ data = f.read().decode('utf-8')
1876
+ e.text = data
1877
+ except Exception as ex:
1878
+ self.err(e, "Discarded unexpected <artwork> content with type='%s': '%s'" % (awtype, ex))
1879
+ del e.attrib['src']
1880
+ else:
1881
+ self.err(e, "Unexpected <artwork> src scheme: '%s'" % scheme)
1882
+
1883
+ if src and not data:
1884
+ self.warn(e, "No image data found in source %s" % src)
1885
+
1886
+ if e.get('type') == 'ascii-art' and e.text:
1887
+ self.normalize_whitespace(e)
1888
+
1889
+ # 5.5.2. <sourcecode> Processing
1890
+
1891
+ def element_sourcecode(self, e, p):
1892
+ # 1. If a <sourcecode> element has a "src" attribute where no scheme
1893
+ # is specified, copy the "src" attribute value to the "originalSrc"
1894
+ # attribute and replace the "src" value with a URI that uses the
1895
+ # "file:" scheme in a path relative to the file being processed.
1896
+ # See Section 7 for warnings about this step. This will likely be
1897
+ # one of the most common authoring approaches.
1898
+ src = e.get('src','').strip()
1899
+ if src:
1900
+ original_src = src
1901
+ scheme, netloc, path, query, fragment = self.check_src_scheme(e, src)
1902
+
1903
+
1904
+ # 2. If a <sourcecode> element has a "src" attribute with a "file:"
1905
+ # scheme, and if processing the URL would cause the processor to
1906
+ # retrieve a file that is not in the same directory, or a
1907
+ # subdirectory, as the file being processed, give an error. If the
1908
+ # "src" has any shellmeta strings (such as "`", "$USER", and so on)
1909
+ # that would be processed, give an error. Replace the "src"
1910
+ # attribute with a URI that uses the "file:" scheme in a path
1911
+ # relative to the file being processed. This rule attempts to
1912
+ # prevent <sourcecode src='file:///etc/passwd'> and similar
1913
+ # security issues. See Section 7 for warnings about this step.
1914
+ if scheme == 'file':
1915
+ src = self.check_src_file_path(e, scheme, netloc, path, query, fragment)
1916
+ if src != original_src:
1917
+ e.set('originalSrc', original_src)
1918
+
1919
+ # 3. If a <sourcecode> element has a "src" attribute, and the element
1920
+ # has content, give an error.
1921
+ srctext = (' '.join(list(e.itertext()))).strip()
1922
+ if srctext:
1923
+ self.err(e, "Expected either external src= content, or element content for <%s>, but found both" % (e.tag, ))
1924
+
1925
+ # 4. If a <sourcecode> element has a "src" attribute, the data needs
1926
+ # to be moved into the content of the <sourcecode> element.
1927
+ #
1928
+ # * If the "src" URI scheme is "data:", fill the content of the
1929
+ # <sourcecode> element with that data and remove the "src"
1930
+ # attribute.
1931
+ #
1932
+ # * If the "src" URI scheme is "file:", "http:", or "https:", fill
1933
+ # the content of the <sourcecode> element with the resolved XML
1934
+ # from the URI in the "src" attribute. If there is no
1935
+ # "originalSrc" attribute, add an "originalSrc" attribute with
1936
+ # the value of the URI and remove the "src" attribute.
1937
+
1938
+ if src: # Test again, after check_src_file_path()
1939
+ with closing(urlopen(src)) as f:
1940
+ data = f.read()
1941
+ e.text = data
1942
+ del e.attrib['src']
1943
+
1944
+ if e.text:
1945
+ self.normalize_whitespace(e)
1946
+
1947
+ #
1948
+ # 5.4.2.4 "Table of Contents" Insertion
1949
+ # 5.4.2.4 "Table of Contents" Insertion
1950
+ def toc_insert_table_of_contents(self, e, p):
1951
+ if self.prepped and self.rfcnumber:
1952
+ return
1953
+ self.keep_toc_lines = 3
1954
+ # Number of lines to keep as a block at the start of the ToC
1955
+ def copy_reduce(e):
1956
+ """
1957
+ <xref> content may not contain all elements permitted in <name>,
1958
+ so we need to reduce any child elements of <name> which are not
1959
+ permitted in <xref> to plain text.
1960
+ """
1961
+ ee = copy.deepcopy(e)
1962
+ for c in ee.iterdescendants():
1963
+ if not c.tag in self.xref_tags:
1964
+ # bcp14 cref eref iref xref
1965
+ # The following is simplified. both <xref> and <eref> could do with more
1966
+ # sophisticated rendering.
1967
+ prev = c.getprevious()
1968
+ if prev != None:
1969
+ if c.tag == 'xref' and not c.text:
1970
+ prev.tail = (prev.tail or '') + c.get('derivedContent', '') + (c.tail or '')
1971
+ elif c.tag == 'eref' and not c.text:
1972
+ prev.tail = (prev.tail or '') + c.get('target', '') + (c.tail or '')
1973
+ else:
1974
+ prev.tail = (prev.tail or '') + (c.text or '') + (c.tail or '')
1975
+ else:
1976
+ prnt = c.getparent()
1977
+ if c.tag == 'xref' and not c.text:
1978
+ prnt.text = (prnt.text or '') + c.get('derivedContent', '') + (c.tail or '')
1979
+ else:
1980
+ prnt.text = (prnt.text or '') + (c.text or '') + (c.tail or '')
1981
+ c.getparent().remove(c)
1982
+ return ee
1983
+
1984
+ def toc_entry_t(s):
1985
+ name = s.find('./name')
1986
+ if name is None:
1987
+ self.die(s, "No name entry found for section, can't continue")
1988
+ numbered = s.get('numbered')=='true' or (self.check_refs_numbered() if s.tag == 'references' else False)
1989
+
1990
+ pn = s.get('pn')
1991
+ if not pn:
1992
+ self.warn(s, "Expected a pn number, found none in <%s>" % (s.tag, ))
1993
+ self.die(s, "No no pn entry found for section, can't continue: %s" % (etree.tostring(s)))
1994
+
1995
+ if not numbered:
1996
+ num = ''
1997
+ num_format = 'none'
1998
+ else:
1999
+ # numbered
2000
+ _, num, _ = self.split_pn(pn)
2001
+ num = num.title()
2002
+ if self.is_appendix(pn) and self.is_top_level_section(num):
2003
+ num_format = 'default'
2004
+ num = 'Appendix %s' % num
2005
+ else:
2006
+ num_format = 'counter'
2007
+ #
2008
+ if self.keep_toc_lines > 0:
2009
+ t = self.element('t', keepWithNext='true')
2010
+ self.keep_toc_lines -= 1
2011
+ else:
2012
+ t = self.element('t')
2013
+
2014
+ xref = self.element('xref', target=pn, format=num_format, derivedContent=num)
2015
+ xref.tail = ('.'+self.spacer) if num.strip() else ( self.spacer if num else '')
2016
+ t.append(xref)
2017
+ # <xref> can only contain text, not markup. so we need to reduce
2018
+ # the name content to plain text:
2019
+ text = clean_text(''.join(name.itertext()))
2020
+ if text:
2021
+ slug = name.get('slugifiedName')
2022
+ if not slug:
2023
+ self.warn(name, "Internal error: missing slugifiedName for %s" % (etree.tostring(name)))
2024
+ slug = self.slugify_name('name-'+text)
2025
+ name.set('slugifiedName', slug)
2026
+ xref = self.element('xref', target=slug, format='title', derivedContent='')
2027
+ cc = copy_reduce(name)
2028
+ xref.text = cc.text
2029
+ xref.extend(cc.getchildren())
2030
+ t.append(xref)
2031
+ return t
2032
+ def toc_entries(e):
2033
+ toc = e.get('toc')
2034
+ if toc == 'include' or e.tag in ['rfc', 'front', 'middle', 'back', 'references', ]:
2035
+ sub = []
2036
+ for c in e:
2037
+ l = toc_entries(c)
2038
+ if l:
2039
+ sub += l
2040
+ if sub:
2041
+ li = sub[0]
2042
+ m = len(li.find('t').find('xref').get('derivedContent'))
2043
+ for li in sub:
2044
+ xref = li.find('t').find('xref')
2045
+ num = xref.get('derivedContent')
2046
+ l = len(num)
2047
+ if (l > m
2048
+ and xref.tail[-1] in [' ', '\u00a0', ] and xref.tail[-2] in [' ', '\u00a0', ]
2049
+ and not num.startswith('Appendix')):
2050
+ xref.tail = xref.tail[:-1]
2051
+ if e.tag in ['section', 'references', ]:
2052
+ li = self.element('li')
2053
+ li.append(toc_entry_t(e))
2054
+ if sub:
2055
+ ul = self.element('ul', empty='true', spacing='compact', bare="true", indent="2")
2056
+ for s in sub:
2057
+ ul.append(s)
2058
+ li.append(ul)
2059
+ return [ li ]
2060
+ else:
2061
+ return sub
2062
+ return []
2063
+ if self.root.get('tocInclude') == 'true':
2064
+ toc = self.element('section', numbered='false', toc='exclude', anchor='toc')
2065
+ name = self.element('name')
2066
+ name.text = "Table of Contents"
2067
+ toc.append(name)
2068
+ self.name_insert_slugified_name(name, toc)
2069
+ ul = self.element('ul', empty='true', spacing='compact', bare="true", indent="2")
2070
+ toc.append(ul)
2071
+ for s in toc_entries(self.root):
2072
+ ul.append(s)
2073
+ e.append(toc)
2074
+ #
2075
+ self.toc_section_add_number(toc, e)
2076
+ self.paragraph_add_numbers(toc, e)
2077
+
2078
+ #
2079
+ def back_insert_index(self, e, p):
2080
+ oldix = self.root.find('./back/section/t[@anchor="rfc.index.index"]')
2081
+ if oldix != None:
2082
+ self.warn(e, "Found an existing Index section, not inserting another one")
2083
+ return
2084
+
2085
+ # Helper methods
2086
+ def mkxref(self, text, **kwargs):
2087
+ kwargs = sdict(kwargs)
2088
+ xref = self.element('xref', **kwargs)
2089
+ if text:
2090
+ xref.text = text
2091
+ return xref
2092
+
2093
+ def bin_item_entries(item_entries, bin_by='item'):
2094
+ """Bin together item entries with like attributes
2095
+
2096
+ Returns a dict whose keys are items and values are an ordered list of entries.
2097
+ Items with no value for the attr will appear with '' as the key.
2098
+ """
2099
+ binned = defaultdict(lambda: []) # missing values initialized to empty lists
2100
+ for entry in item_entries:
2101
+ binned[getattr(entry, bin_by) or ''].append(entry)
2102
+ return binned
2103
+
2104
+ def letter_li(letter, letter_entries):
2105
+ """Generate <li> element for a letter's entries
2106
+
2107
+ <li>
2108
+ <t><xref>A</xref></t>
2109
+ <ul><li>
2110
+ <dl>
2111
+ <dt>item 1</dt><dd>[references]</dd>
2112
+ <dt/><dd> [only if there are subitems]
2113
+ [contents from sub_dl()]
2114
+ </dd>
2115
+ <dt>item 2</dt><dd>[references]</dd>
2116
+ [...]
2117
+ </dl>
2118
+ </li></ul>
2119
+ </li>
2120
+ """
2121
+ binned_entries = bin_item_entries(letter_entries)
2122
+ anchor = letter_anchor(letter)
2123
+ li = self.element('li')
2124
+ t = self.element('t', anchor=anchor, keepWithNext='true')
2125
+ t.text = '\n' + ' ' * 12
2126
+ xref = mkxref(self, letter, target=anchor, format='none')
2127
+ xref.tail = (xref.tail or '') + '\n'
2128
+ t.append(xref)
2129
+ li.append(t)
2130
+ #
2131
+ ul = self.element('ul', empty='true', spacing='compact')
2132
+ li.append(ul)
2133
+ li2 = self.element('li')
2134
+ ul.append(li2)
2135
+ dl = self.element('dl', spacing='compact')
2136
+ li2.append(dl)
2137
+ # sort entries - py36 does not guarantee order and later versions use LIFO order
2138
+ for item in sorted(binned_entries, key=str.casefold):
2139
+ regular_entries = []
2140
+ sub_entries = []
2141
+ for i in binned_entries[item]:
2142
+ if i.sub is None:
2143
+ regular_entries.append(i)
2144
+ else:
2145
+ sub_entries.append(i)
2146
+ # first add the item heading and any regular entries
2147
+ for elt in entry_dt_dd(item, regular_entries, 18):
2148
+ dl.append(elt)
2149
+ # now add any sub-entries
2150
+ if len(sub_entries) > 0:
2151
+ dl.append(self.element('dt'))
2152
+ sub_dd = self.element('dd')
2153
+ dl.append(sub_dd)
2154
+ sub_dd.append(sub_dl(sub_entries))
2155
+ return li
2156
+
2157
+ def sub_dl(sub_elements):
2158
+ """Generate <dl> for a subitem entry
2159
+
2160
+ <dl>
2161
+ <dt>subitem 1</dt>
2162
+ <dd>[references]</dd>
2163
+ [...]
2164
+ </dl>
2165
+ """
2166
+ binned_subs = bin_item_entries(sub_elements, bin_by='sub')
2167
+ sub_dl = self.element('dl', spacing='compact')
2168
+ for sub in sorted(binned_subs):
2169
+ entries = binned_subs[sub]
2170
+ for elt in entry_dt_dd(sub, entries, 22):
2171
+ sub_dl.append(elt)
2172
+ return sub_dl
2173
+
2174
+ def entry_dt_dd(label, entries, indent):
2175
+ """Definition list <dt>/<dd> pair for either a list item or subitem
2176
+
2177
+ <dt>item</dt>
2178
+ and
2179
+ <dd>
2180
+ <t>
2181
+ <xref target="first entry"/>
2182
+ <xref target="second entry"/>
2183
+ [...]
2184
+ </t>
2185
+ </dd>
2186
+ """
2187
+ dt = self.element('dt')
2188
+ dt.text = label
2189
+ dd = self.element('dd')
2190
+ if len(entries) > 0:
2191
+ t = self.element('t')
2192
+ t.text = '\n' + ' ' * (indent + 2)
2193
+ dd.append(t)
2194
+ anchored_entries = [e for e in entries if e.anchor]
2195
+ for index, ent in enumerate(anchored_entries):
2196
+ xr = mkxref(
2197
+ self,
2198
+ text='Reference' if ent.anchor_tag in ['reference', 'referencegroup'] else None,
2199
+ target=ent.anchor,
2200
+ )
2201
+ if ent.iref.get('primary') == 'true':
2202
+ xr_em = self.element('em')
2203
+ xr_em.append(xr)
2204
+ xr = self.element('strong')
2205
+ xr.append(xr_em)
2206
+ t.append(xr)
2207
+ if index < len(anchored_entries) - 1:
2208
+ xr.tail = '; '
2209
+ xr.tail = (xr.tail or '') + '\n'
2210
+ return dt, dd
2211
+
2212
+ def index_letter(entry):
2213
+ return entry.item[0].upper()
2214
+
2215
+ def letter_anchor(letter):
2216
+ """Get an anchor string for a letter
2217
+
2218
+ The anchor must be a valid XML name, restricted to US ASCII A-Z,a-z,0-9,_,-,.,:,
2219
+ with 0-9, ., and - disallowed for leading characters. To guarantee this, even for
2220
+ non-alphanumeric "letters," the letter character is encoded to UTF8 bytes and
2221
+ its decimal representation is used in the anchor string.
2222
+ """
2223
+ return 'rfc.index.u{}'.format(int.from_bytes(letter.encode(), byteorder='big'))
2224
+
2225
+ def index_sort(letters):
2226
+ """Sort letters for the index
2227
+
2228
+ Sorts symbols ahead of alphanumerics to keep ASCII symbols together.
2229
+ """
2230
+ return sorted(
2231
+ letters,
2232
+ key=lambda letter: (letter.isalnum(), letter)
2233
+ )
2234
+
2235
+ # done defining helpers, resume back_insert_index() flow
2236
+ if self.index_entries and self.root.get('indexInclude') == 'true':
2237
+ # remove duplicate entries
2238
+ entries = {}
2239
+ for entry in self.index_entries:
2240
+ uniq_key = (entry.item, entry.anchor)
2241
+ entries.setdefault(uniq_key, entry) # keeps only the first for each key
2242
+ self.index_entries = list(entries.values())
2243
+ #
2244
+ index = self.element('section', numbered='false', toc='include')
2245
+ name = self.element('name')
2246
+ name.text = 'Index'
2247
+ index.append(name)
2248
+ self.name_insert_slugified_name(name, index)
2249
+ #
2250
+ index_index = self.element('t', anchor='rfc.index.index')
2251
+ index_index.text = '\n'+' '*8
2252
+ index.append(index_index)
2253
+ # sort the index entries
2254
+ self.index_entries.sort(key=lambda i: '%s~%s' % (i.item, i.sub or ''))
2255
+ # get the first letters
2256
+ letters = index_sort(uniq([ index_letter(i) for i in self.index_entries ]))
2257
+ # set up the index index
2258
+ for letter in letters:
2259
+ xref = mkxref(self, letter, target=letter_anchor(letter), format='none')
2260
+ xref.tail = '\n'
2261
+ index_index.append(xref)
2262
+ # one letter entry per letter
2263
+ index_ul = self.element('ul', empty='true', spacing='normal')
2264
+ index.append(index_ul)
2265
+ for letter in letters:
2266
+ letter_entries = [ i for i in self.index_entries if index_letter(i) == letter ]
2267
+ index_ul.append(letter_li(letter, letter_entries))
2268
+ #
2269
+ e.append(index)
2270
+ #
2271
+ self.back_section_add_number(index, e)
2272
+ self.paragraph_add_numbers(index, e)
2273
+
2274
+
2275
+ def back_insert_author_address(self, e, p):
2276
+ if self.prepped:
2277
+ return
2278
+ old = self.root.find('./back/section[@anchor="authors-addresses"]')
2279
+ if old != None:
2280
+ self.warn(e, "Found an existing Authors' Addresses section, not inserting another one")
2281
+ return
2282
+ authors = self.root.findall('./front/author')
2283
+ # Exclude contributors
2284
+ authors = [ a for a in authors if a.get('role') != 'contributor' ]
2285
+ back = self.root.find('./back')
2286
+ line = back[-1].sourceline or back.sourceline if len(back) else back.sourceline
2287
+ s = self.element('section', toc='include', numbered='false', anchor='authors-addresses', line=line)
2288
+ n = self.element('name', line=line+1)
2289
+ if len(authors) > 1:
2290
+ n.text = "Authors' Addresses"
2291
+ else:
2292
+ n.text = "Author's Address"
2293
+ n.set('slugifiedName', self.slugify_name('name-'+n.text))
2294
+ s.append(n)
2295
+ for e in self.root.find('./front'):
2296
+ if e.tag in ['author', etree.PI, ]:
2297
+ s.append(copy.copy(e))
2298
+ back.append(s)
2299
+ #
2300
+ self.back_section_add_number(s, e)
2301
+ self.paragraph_add_numbers(s, e)
2302
+
2303
+
2304
+ # 5.6. RFC Production Mode Cleanup
2305
+ #
2306
+ # These steps provide extra cleanup of the output document in RFC
2307
+ # production mode.
2308
+ #
2309
+ # 5.6.1. <note> Removal
2310
+ #
2311
+ # In RFC production mode, if there is a <note> or <section> element
2312
+ # with a "removeInRFC" attribute that has the value "true", remove the
2313
+ # element.
2314
+ def attribute_removeinrfc_true(self, e, p):
2315
+ if self.options.rfc:
2316
+ p.remove(e)
2317
+
2318
+ # 5.6.2. <cref> Removal
2319
+ #
2320
+ # If in RFC production mode, remove all <cref> elements.
2321
+ def cref_removal(self, e, p):
2322
+ if self.options.rfc:
2323
+ self.remove(p, e)
2324
+
2325
+ # 5.6.3. <link> Processing
2326
+ def attribute_link_rel_alternate_removal(self, e, p):
2327
+ if self.options.rfc:
2328
+ # 1. If in RFC production mode, remove all <link> elements whose "rel"
2329
+ # attribute has the value "alternate".
2330
+ if e.get('rel') == 'alternate':
2331
+ p.remove(e)
2332
+ return
2333
+
2334
+ # 2. If in RFC production mode, check if there is a <link> element
2335
+ # with the current ISSN for the RFC series (2070-1721); if not, add
2336
+ # <link rel="item" href="urn:issn:2070-1721">.
2337
+ def check_links_required(self, e, p):
2338
+ if self.options.rfc:
2339
+ item_href = "urn:issn:2070-1721"
2340
+ urnlink = e.find('.//link[@rel="item"][@href="%s"]' % (item_href, ))
2341
+ if urnlink is None :
2342
+ e.insert(0, self.element('link', href=item_href, rel='alternate'))
2343
+ # 3. If in RFC production mode, check if there is a <link> element
2344
+ # with a DOI for this RFC; if not, add one of the form <link
2345
+ # rel="describedBy" href="https://dx.doi.org/10.17487/rfcdd"> where
2346
+ # "dd" is the number of the RFC, such as
2347
+ # "https://dx.doi.org/10.17487/rfc2109". The URI is described in
2348
+ # [RFC7669]. If there was already a <link> element with a DOI for
2349
+ # this RFC, check that the "href" value has the right format. The
2350
+ # content of the href attribute is expected to change in the
2351
+ # future.
2352
+ doi_href = "https://dx.doi.org/10.17487/rfc%s" % self.rfcnumber
2353
+ doilink = e.find('.//link[@href="%s"]' % (doi_href, ))
2354
+ if doilink is None:
2355
+ e.insert(0, self.element('link', href=doi_href, rel='alternate'))
2356
+
2357
+ #
2358
+ # 4. If in RFC production mode, check if there is a <link> element
2359
+ # with the file name of the Internet-Draft that became this RFC the
2360
+ # form <link rel="convertedFrom"
2361
+ # href="https://datatracker.ietf.org/doc/draft-tttttttttt/">. If
2362
+ # one does not exist, give an error.
2363
+ converted_from = e.find('.//link[@rel="prev"]')
2364
+ if converted_from is None:
2365
+ docName = self.root.get("docName")
2366
+ if docName:
2367
+ e.insert(0, self.element('link', rel='prev', href="https://datatracker.ietf.org/doc/%s"%(docName, ), line=e.sourceline))
2368
+ else:
2369
+ self.warn(e, 'Expected a <link> with rel="prev" providing the datatracker url for the origin draft,'
2370
+ ' or alternatively a "docName" attribute on <rfc> from which to construct such a <link> element.'
2371
+ )
2372
+ else:
2373
+ converted_from_href = converted_from.get('href', '')
2374
+ if not converted_from_href.startswith("https://datatracker.ietf.org/doc/draft-"):
2375
+ self.err(converted_from, "Expected the <link rel='prev'> href= to have the form 'https://datatracker.ietf.org/doc/draft-...', but found '%s'" % (converted_from_href, ))
2376
+
2377
+ # 5.6.4. XML Comment Removal
2378
+ #
2379
+ # If in RFC production mode, remove XML comments.
2380
+ def comment_removal(self, e, p):
2381
+ if self.options.rfc:
2382
+ self.remove(p, e)
2383
+
2384
+ # 5.6.5. "xml:base" and "originalSrc" Removal
2385
+ #
2386
+ # If in RFC production mode, remove all "xml:base" or "originalSrc"
2387
+ # attributes from all elements.
2388
+ def attribute_removal(self, e, p):
2389
+ if self.options.rfc:
2390
+ for c in e.iterfind('.//*[@xml:base]', namespaces=namespaces):
2391
+ del c.attrib['{http://www.w3.org/XML/1998/namespace}base']
2392
+ for c in e.iterfind('.//*[@originalSrc]'):
2393
+ del c.attrib['originalSrc']
2394
+
2395
+ # 5.6.6. Compliance Check
2396
+ #
2397
+ # If in RFC production mode, ensure that the result is in full
2398
+ # compliance to the v3 schema, without any deprecated elements or
2399
+ # attributes and give an error if any issues are found.
2400
+
2401
+ # implemented in parent class: validate_after(self, e, p):
2402
+
2403
+ # 5.7. Finalization
2404
+ #
2405
+ # These steps provide the finishing touches on the output document.
2406
+
2407
+ # 5.7.1. "scripts" Insertion
2408
+ #
2409
+ # Determine all the characters used in the document and fill in the
2410
+ # "scripts" attribute for <rfc>.
2411
+ def insert_scripts(self, e, p):
2412
+ text = ' '.join(e.itertext())
2413
+ scripts = ','.join(sorted(list(get_scripts(text))))
2414
+ e.set('scripts', scripts)
2415
+
2416
+ # 5.7.2. Pretty-Format
2417
+ #
2418
+ # Pretty-format the XML output. (Note: there are many tools that do an
2419
+ # adequate job.)
2420
+
2421
+ # ------------------------------------------------------------------------
2422
+
2423
+ # Other post-processing
2424
+
2425
+ def remove_pis(self):
2426
+ self.keep_pis = []
2427
+ for i in self.root.xpath('.//processing-instruction()'):
2428
+ self.processing_instruction_removal(i, i.getparent())