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,2248 @@
1
+ # --------------------------------------------------
2
+ # Copyright The IETF Trust 2011, All Rights Reserved
3
+ # --------------------------------------------------
4
+
5
+ import calendar
6
+ import codecs
7
+ import copy
8
+ import datetime
9
+ import textwrap
10
+ import os
11
+ import re
12
+ import xml2rfc.log
13
+ import xml2rfc.util
14
+ import xml2rfc.utils
15
+
16
+ from lxml import etree
17
+ from argparse import Namespace
18
+ from urllib.parse import urlparse
19
+
20
+ try:
21
+ from xml2rfc import debug
22
+ debug.debug = True
23
+ except ImportError:
24
+ pass
25
+
26
+ from xml2rfc import strings, log
27
+ from xml2rfc.util.date import extract_date, augment_date, format_date, get_expiry_date
28
+ from xml2rfc.util.name import short_author_ascii_name_parts, full_author_name_expansion, short_author_name_parts
29
+ from xml2rfc.util.unicode import is_svg
30
+ from xml2rfc.utils import namespaces, find_duplicate_ids, slugify
31
+
32
+
33
+ SUBSERIES = {
34
+ 'STD': 'Internet Standard',
35
+ 'BCP': 'Best Current Practice',
36
+ 'FYI': 'For Your Information',
37
+ }
38
+
39
+ DEADLY_ERRORS = [
40
+ 'Element svg has extra content: script',
41
+ 'Did not expect element script there',
42
+ ]
43
+
44
+ default_silenced_messages = [
45
+ # ".*[Pp]ostal address",
46
+ ]
47
+
48
+
49
+ default_options = Namespace()
50
+ default_options.__dict__ = {
51
+ 'accept_prepped': None,
52
+ 'add_xinclude': None,
53
+ 'allow_local_file_access': False,
54
+ 'basename': None,
55
+ 'bom': False,
56
+ 'cache': None,
57
+ 'clear_cache': False,
58
+ 'css': None,
59
+ 'config_file': None,
60
+ 'country_help': False,
61
+ 'date': None,
62
+ 'datestring': None,
63
+ 'debug': False,
64
+ 'docfile': False,
65
+ 'doc_template': None,
66
+ 'doi_base_url': 'https://doi.org/',
67
+ 'draft_revisions': False,
68
+ 'dtd': None,
69
+ 'expand': False,
70
+ 'external_css': False,
71
+ 'external_js': False,
72
+ 'filename': None,
73
+ 'first_page_author_org': True,
74
+ 'html': False,
75
+ 'id_base_url': 'https://datatracker.ietf.org/doc/html/',
76
+ 'id_html_archive_url': 'https://www.ietf.org/archive/id/',
77
+ 'id_reference_base_url': 'https://datatracker.ietf.org/doc/html/',
78
+ 'id_is_work_in_progress': True,
79
+ 'image_svg': False,
80
+ 'indent': 2,
81
+ 'info': False,
82
+ 'info_base_url': 'https://www.rfc-editor.org/info/',
83
+ 'inline_version_info': True,
84
+ 'legacy': False,
85
+ 'legacy_date_format': False,
86
+ 'legacy_list_symbols': False,
87
+ 'list_symbols': ('*', '-', 'o', '+'),
88
+ 'manpage': False,
89
+ 'metadata_js_url': 'metadata.min.js',
90
+ 'no_css': False,
91
+ 'no_dtd': None,
92
+ 'no_network': False,
93
+ 'nroff': False,
94
+ 'omit_headers': None,
95
+ 'orphans': 2,
96
+ 'output_filename': None,
97
+ 'output_path': None,
98
+ 'pagination': True,
99
+ 'pi_help': False,
100
+ 'pdf': False,
101
+ 'pdf_help': False,
102
+ 'preptool': False,
103
+ 'quiet': False,
104
+ 'remove_pis': False,
105
+ 'raw': False,
106
+ 'rfc': None,
107
+ 'rfc_base_url': 'https://www.rfc-editor.org/rfc/',
108
+ 'rfc_html_archive_url': 'https://www.rfc-editor.org/rfc/',
109
+ 'rfc_local': True,
110
+ 'rfc_reference_base_url': 'https://rfc-editor.org/rfc/',
111
+ 'silence': default_silenced_messages,
112
+ 'skip_config_files': False,
113
+ 'source': None,
114
+ 'strict': False,
115
+ 'table_hyphen_breaks': False,
116
+ 'table_borders': 'full',
117
+ 'template_dir': os.path.join(os.path.dirname(os.path.dirname(__file__)), 'templates'),
118
+ 'text': True,
119
+ 'unprep': False,
120
+ 'use_bib': False,
121
+ 'utf8': False,
122
+ 'values': False,
123
+ 'verbose': False,
124
+ 'version': False,
125
+ 'v2v3': False,
126
+ 'v3': True,
127
+ 'vocabulary': 'v2',
128
+ 'widows': 2,
129
+ 'warn_bare_unicode': False,
130
+ }
131
+
132
+ class _RfcItem:
133
+ """ A unique ID object for an anchored RFC element.
134
+
135
+ Anchored elements are the following: Automatic sections, user (middle)
136
+ sections, paragraphs, references, appendices, figures, and tables.
137
+
138
+ RfcItems are collected into an index list in the base writer
139
+ """
140
+ def __init__(self, autoName, autoAnchor, counter='', title='', anchor='',
141
+ toc=True, level=1, appendix=False):
142
+ self.counter = str(counter)
143
+ self.autoName = autoName
144
+ self.autoAnchor = autoAnchor
145
+ self.title = title
146
+ self.anchor = anchor
147
+ self.toc = toc
148
+ self.level = level
149
+ self.appendix = appendix
150
+ self.page = 0 # This will be set after buffers are complete!
151
+ self.used = False
152
+
153
+
154
+ class _IrefItem:
155
+ """ A unique ID object for an iref element """
156
+ def __init__(self, anchor=None):
157
+ self.pages = []
158
+ self.subitems = {}
159
+ self.anchor = anchor
160
+
161
+
162
+ class RfcWriterError(Exception):
163
+ """ Exception class for errors during document writing """
164
+ def __init__(self, msg):
165
+ self.msg = msg
166
+
167
+
168
+ class BaseRfcWriter:
169
+ """ Base class for all writers
170
+
171
+ All public methods need to be overridden for a writer implementation.
172
+ """
173
+
174
+ # -------------------------------------------------------------------------
175
+ # Attribute default values
176
+ #
177
+ # These will mainly come into play if DTD validation was disabled, and
178
+ # processing that happens to rely on DTD populated attributes require a lookup
179
+ defaults = {
180
+ 'section_toc': 'default',
181
+ 'xref_pageno': 'false',
182
+ 'xref_format': 'default',
183
+ 'iref_primary': 'false',
184
+ 'spanx_style': 'emph',
185
+ 'figure_suppress-title': 'false',
186
+ 'figure_title': '',
187
+ 'figure_align': 'left',
188
+ 'vspace_blanklines': 0,
189
+ 'table_suppress-title': 'false',
190
+ 'table_title': '',
191
+ 'table_align': 'center',
192
+ 'table_style': 'full',
193
+ 'ttcol_align': 'left',
194
+ 'ipr': 'trust200902',
195
+ 'submissionType': 'IETF',
196
+ 'consensus': 'no',
197
+ }
198
+
199
+ # -------------------------------------------------------------------------
200
+ # Boilerplate text
201
+ boilerplate = {}
202
+
203
+ # The RFC Editor's switchover date for https URLs
204
+ boilerplate_https_date = datetime.date(year=2017, month=8, day=21)
205
+
206
+ # Document stream names
207
+ boilerplate['document_stream'] = {}
208
+ boilerplate['document_stream']['IETF'] = \
209
+ 'Internet Engineering Task Force (IETF)'
210
+ boilerplate['document_stream']['IAB'] = \
211
+ 'Internet Architecture Board (IAB)'
212
+ boilerplate['document_stream']['IRTF'] = \
213
+ 'Internet Research Task Force (IRTF)'
214
+ boilerplate['document_stream']['independent'] = \
215
+ 'Independent Submission'
216
+
217
+ # Draft workgroup name
218
+ boilerplate['draft_workgroup'] = 'Network Working Group'
219
+
220
+ # Category names
221
+ boilerplate['std'] = 'Standards Track'
222
+ boilerplate['bcp'] = 'Best Current Practice'
223
+ boilerplate['exp'] = 'Experimental'
224
+ boilerplate['info'] = 'Informational'
225
+ boilerplate['historic'] = 'Historic'
226
+
227
+ # Series type
228
+ boilerplate['series_name'] = {}
229
+ boilerplate['series_name']['std'] = 'STD'
230
+ boilerplate['series_name']['bcp'] = 'BCP'
231
+ boilerplate['series_name']['info'] = 'FYI'
232
+
233
+ # ISSN
234
+ boilerplate['issn'] = '2070-1721'
235
+
236
+ # 'Status of this Memo' boilerplate for RFCs
237
+ boilerplate['status'] = {
238
+ 'std': {},
239
+ 'bcp': {},
240
+ 'exp': {},
241
+ 'info': {},
242
+ 'historic': {},
243
+ }
244
+ # 'Status of this Memo' boilerplate for RFCs
245
+ boilerplate['status_5741'] = {
246
+ }
247
+
248
+ # Paragraph 1
249
+ boilerplate['status']['std']['p1'] = \
250
+ 'This is an Internet Standards Track document.'
251
+ boilerplate['status']['bcp']['p1'] = \
252
+ 'This memo documents an Internet Best Current Practice.'
253
+ boilerplate['status']['exp']['p1'] = \
254
+ 'This document is not an Internet Standards Track specification; ' \
255
+ 'it is published for examination, experimental implementation, and ' \
256
+ 'evaluation.'
257
+ boilerplate['status']['info']['p1'] = \
258
+ 'This document is not an Internet Standards Track specification; ' \
259
+ 'it is published for informational purposes.'
260
+ boilerplate['status']['historic']['p1'] = \
261
+ 'This document is not an Internet Standards Track specification; ' \
262
+ 'it is published for the historical record.'
263
+
264
+ # Paragraph 2 header
265
+ boilerplate['status']['exp']['p2'] = \
266
+ 'This document defines an Experimental Protocol for the Internet ' \
267
+ 'community.'
268
+ boilerplate['status']['historic']['p2'] = \
269
+ 'This document defines a Historic Document for the Internet ' \
270
+ 'community.'
271
+
272
+ # Paragraph 2 body
273
+ boilerplate['status']['IETF'] = \
274
+ 'This document is a product of the Internet Engineering Task Force ' \
275
+ '(IETF). It has been approved for publication by the Internet ' \
276
+ 'Engineering Steering Group (IESG).'
277
+ boilerplate['status']['IETF_consensus'] = \
278
+ 'This document is a product of the Internet Engineering Task Force ' \
279
+ '(IETF). It represents the consensus of the IETF community. It has ' \
280
+ 'received public review and has been approved for publication by ' \
281
+ 'the Internet Engineering Steering Group (IESG).'
282
+ boilerplate['status']['IRTF'] = \
283
+ 'This document is a product of the Internet Research Task Force ' \
284
+ '(IRTF). The IRTF publishes the results of Internet-related ' \
285
+ 'research and development activities. These results might not be ' \
286
+ 'suitable for deployment.'
287
+ boilerplate['status']['IRTF_workgroup'] = \
288
+ 'This RFC represents the individual opinion(s) ' \
289
+ 'of one or more members of the %s Research Group of the Internet ' \
290
+ 'Research Task Force (IRTF).'
291
+ boilerplate['status']['IRTF_workgroup_consensus'] = \
292
+ 'This RFC represents the consensus of the ' \
293
+ '%s Research Group of the Internet Research Task Force (IRTF).'
294
+ boilerplate['status']['IAB'] = \
295
+ 'This document is a product of the Internet Architecture Board ' \
296
+ '(IAB) and represents information that the IAB has deemed valuable ' \
297
+ 'to provide for permanent record.'
298
+ boilerplate['status']['IAB_consensus'] = (
299
+ 'This document is a product of the Internet Architecture Board '
300
+ '(IAB) and represents information that the IAB has deemed valuable '
301
+ 'to provide for permanent record. It represents the consensus of '
302
+ 'the Internet Architecture Board (IAB).')
303
+ boilerplate['status']['independent'] = \
304
+ 'This is a contribution to the RFC Series, independently of any ' \
305
+ 'other RFC stream. The RFC Editor has chosen to publish this ' \
306
+ 'document at its discretion and makes no statement about its value ' \
307
+ 'for implementation or deployment.'
308
+
309
+ # Paragraph 2 last sentence
310
+ boilerplate['status']['p2end_ietf_std'] = \
311
+ 'Further information on Internet Standards is available ' \
312
+ 'in Section 2 of RFC 7841.'
313
+ boilerplate['status']['p2end_ietf_bcp'] = \
314
+ 'Further information on BCPs is available in Section 2 of RFC 7841.'
315
+ boilerplate['status']['p2end_ietf_other'] = \
316
+ 'Not all documents approved by the IESG are candidates for any ' \
317
+ 'level of Internet Standard; see Section 2 of RFC 7841.'
318
+ boilerplate['status']['p2end_other'] = \
319
+ 'Documents approved for publication by the %s are not ' \
320
+ 'candidates for any level of Internet Standard; see Section 2 of RFC ' \
321
+ '7841.'
322
+
323
+ boilerplate['status_5741']['p2end_ietf_std'] = \
324
+ 'Further information on Internet Standards is available ' \
325
+ 'in Section 2 of RFC 5741.'
326
+ boilerplate['status_5741']['p2end_ietf_bcp'] = \
327
+ 'Further information on BCPs is available in Section 2 of RFC 5741.'
328
+ boilerplate['status_5741']['p2end_ietf_other'] = \
329
+ 'Not all documents approved by the IESG are candidates for any ' \
330
+ 'level of Internet Standard; see Section 2 of RFC 5741.'
331
+ boilerplate['status_5741']['p2end_other'] = \
332
+ 'Documents approved for publication by the %s are not ' \
333
+ 'candidates for any level of Internet Standard; see Section 2 of RFC ' \
334
+ '5741.'
335
+
336
+ # Paragraph 3
337
+ boilerplate['status']['p3'] = (
338
+ 'Information about the current status of this document, any errata, '
339
+ 'and how to provide feedback on it may be obtained at '
340
+ 'http://www.rfc-editor.org/info/rfc%s.')
341
+ boilerplate['status']['p3_s'] = (
342
+ 'Information about the current status of this document, any errata, '
343
+ 'and how to provide feedback on it may be obtained at '
344
+ 'https://www.rfc-editor.org/info/rfc%s.')
345
+
346
+ # 'Status of this Memo' boilerplate for drafts
347
+ boilerplate['status']['draft'] = [
348
+ 'Internet-Drafts are working documents of the Internet Engineering '
349
+ 'Task Force (IETF). Note that other groups may also distribute '
350
+ 'working documents as Internet-Drafts. The list of current Internet-'
351
+ 'Drafts is at http://datatracker.ietf.org/drafts/current/.',
352
+ #
353
+ 'Internet-Drafts are draft documents valid for a maximum of six months '
354
+ 'and may be updated, replaced, or obsoleted by other documents at any '
355
+ 'time. It is inappropriate to use Internet-Drafts as reference '
356
+ 'material or to cite them other than as "work in progress."']
357
+ boilerplate['status']['draft_s'] = [
358
+ 'Internet-Drafts are working documents of the Internet Engineering '
359
+ 'Task Force (IETF). Note that other groups may also distribute '
360
+ 'working documents as Internet-Drafts. The list of current Internet-'
361
+ 'Drafts is at https://datatracker.ietf.org/drafts/current/.',
362
+ #
363
+ 'Internet-Drafts are draft documents valid for a maximum of six months '
364
+ 'and may be updated, replaced, or obsoleted by other documents at any '
365
+ 'time. It is inappropriate to use Internet-Drafts as reference '
366
+ 'material or to cite them other than as "work in progress."']
367
+
368
+ boilerplate['draft_expire'] = \
369
+ 'This Internet-Draft will expire on %s.'
370
+
371
+ # IPR status boilerplate
372
+ boilerplate['ipr_200902_status'] = \
373
+ 'This Internet-Draft is submitted in full conformance ' \
374
+ 'with the provisions of BCP 78 and BCP 79.'
375
+ boilerplate['ipr_200811_status'] = \
376
+ 'This Internet-Draft is submitted to IETF in full conformance ' \
377
+ 'with the provisions of BCP 78 and BCP 79.'
378
+
379
+ # Copyright boilerplate
380
+ boilerplate['base_copyright_header'] = \
381
+ 'Copyright (c) %s IETF Trust and the persons identified as the ' \
382
+ 'document authors. All rights reserved.'
383
+ boilerplate['base_copyright_body'] = (
384
+ 'This document is subject to BCP 78 and the IETF Trust\'s Legal '
385
+ 'Provisions Relating to IETF Documents '
386
+ '(http://trustee.ietf.org/license-info) in effect on the date of '
387
+ 'publication of this document. Please review these documents '
388
+ 'carefully, as they describe your rights and restrictions with respect '
389
+ 'to this document.')
390
+ boilerplate['base_copyright_body_s'] = (
391
+ 'This document is subject to BCP 78 and the IETF Trust\'s Legal '
392
+ 'Provisions Relating to IETF Documents '
393
+ '(https://trustee.ietf.org/license-info) in effect on the date of '
394
+ 'publication of this document. Please review these documents '
395
+ 'carefully, as they describe your rights and restrictions with respect '
396
+ 'to this document.')
397
+
398
+ # IPR values which append things to copyright
399
+ boilerplate['ipr_200902_copyright_ietfbody'] = \
400
+ 'Code Components extracted from this document must ' \
401
+ 'include Simplified BSD License text as described in Section 4.e of ' \
402
+ 'the Trust Legal Provisions and are provided without warranty as ' \
403
+ 'described in the Simplified BSD License.'
404
+ boilerplate['ipr_noModification_copyright'] = \
405
+ 'This document may not be modified, and derivative works of it may ' \
406
+ 'not be created, except to format it for publication as an RFC or ' \
407
+ 'to translate it into languages other than English.'
408
+ boilerplate['ipr_noDerivatives_copyright'] = \
409
+ 'This document may not be modified, and derivative works of it may ' \
410
+ 'not be created, and it may not be published except as an ' \
411
+ 'Internet-Draft.'
412
+ boilerplate['ipr_pre5378Trust200902_copyright'] = \
413
+ 'This document may contain material from IETF Documents or IETF ' \
414
+ 'Contributions published or made publicly available before ' \
415
+ 'November 10, 2008. The person(s) controlling the copyright in some ' \
416
+ 'of this material may not have granted the IETF Trust the right to ' \
417
+ 'allow modifications of such material outside the IETF Standards ' \
418
+ 'Process. Without obtaining an adequate license from the person(s) ' \
419
+ 'controlling the copyright in such materials, this document may not ' \
420
+ 'be modified outside the IETF Standards Process, and derivative ' \
421
+ 'works of it may not be created outside the IETF Standards Process, ' \
422
+ 'except to format it for publication as an RFC or to translate it ' \
423
+ 'into languages other than English.'
424
+
425
+ # Any extra boilerplate
426
+ # Disabled. See issue #123, http://trac.tools.ietf.org/tools/xml2rfc/trac/ticket/123
427
+ ## boilerplate['iprnotified'] = \
428
+ ## 'The IETF has been notified of intellectual property rights ' \
429
+ ## 'claimed in regard to some or all of the specification contained ' \
430
+ ## 'in this document. For more information consult the online list ' \
431
+ ## 'of claimed rights.'
432
+
433
+ # Stream approvers
434
+ approvers = {
435
+ 'IAB': 'IAB',
436
+ 'IRTF': 'IRSG',
437
+ 'independent': 'RFC Editor',
438
+ }
439
+
440
+ # Valid IPR attributes
441
+ supported_ipr = [
442
+ 'trust200902',
443
+ 'noModificationTrust200902',
444
+ 'noDerivativesTrust200902',
445
+ 'pre5378Trust200902',
446
+ 'trust200811',
447
+ 'noModificationTrust200811',
448
+ 'noDerivativesTrust200811',
449
+ 'none',
450
+ ]
451
+
452
+ # -------------------------------------------------------------------------
453
+
454
+ def __init__(self, xmlrfc, quiet=None, options=default_options, date=None):
455
+ if not quiet is None:
456
+ options.quiet = quiet
457
+ self.options = options
458
+ self.date = date if date is not None else datetime.date.today()
459
+ self.expire_string = ''
460
+ self.ascii = False
461
+ self.nbws_cond = u'\u00A0'
462
+ self.eref_list = []
463
+ self.xmlrfc = xmlrfc
464
+ self.pis = self.xmlrfc.getpis()
465
+
466
+ # We will refer to the XmlRfc document root as 'r'
467
+ self.xmlrfc = xmlrfc
468
+ self.r = xmlrfc.getroot()
469
+
470
+ # Document counters
471
+ self.ref_start = 1 # Start of reference counters
472
+ self.refs_start = 1 # Start of references sections
473
+ self.figure_count = 0
474
+ self.table_count = 0
475
+ self.eref_count = 0
476
+
477
+ # Set RFC number and draft flag
478
+ self.rfcnumber = self.r.attrib.get('number', '')
479
+ self.draft = bool(not self.rfcnumber)
480
+
481
+ # Used for two-pass indexing
482
+ self.indexmode = False
483
+
484
+ # Item Indicies
485
+ self._index = []
486
+ self._iref_index = {}
487
+
488
+ def _make_iref(self, item, subitem=None, anchor=None):
489
+ """ Create an iref ID object if it doesnt exist yet """
490
+ last = None
491
+ if item not in self._iref_index:
492
+ self._iref_index[item] = _IrefItem()
493
+ last = self._iref_index[item]
494
+ if subitem and subitem not in self._iref_index[item].subitems:
495
+ self._iref_index[item].subitems[subitem] = _IrefItem()
496
+ last = self._iref_index[item].subitems[subitem]
497
+ if last and anchor:
498
+ last.anchor = anchor
499
+
500
+ def _add_iref_to_index(self, element):
501
+ item = element.attrib.get('item', None)
502
+ if item:
503
+ subitem = element.attrib.get('subitem', None)
504
+ self._make_iref(item, subitem)
505
+ # Store the buffer position for pagination data later
506
+ pos = len(self.buf)
507
+ if not self.indexmode:
508
+ if pos not in self.iref_marks:
509
+ self.iref_marks[pos] = []
510
+ self.iref_marks[pos].append((item, subitem))
511
+
512
+ def _indexParagraph(self, counter, p_counter, anchor=None, toc=False):
513
+ counter = str(counter) # This is the section counter
514
+ p_counter = str(p_counter) # This is the paragraph counter
515
+ autoName = 'Section ' + counter + ', Paragraph ' + p_counter
516
+ autoAnchor = 'rfc.section.' + counter + '.p.' + p_counter
517
+ item = _RfcItem(autoName, autoAnchor, anchor=anchor, toc=toc, counter=p_counter)
518
+ self._index.append(item)
519
+ return item
520
+
521
+ def _indexListParagraph(self, p_counter, anchor, toc=False):
522
+ p_counter = str(p_counter)
523
+ autoName = 'Paragraph ' + p_counter
524
+ item = _RfcItem(autoName, '', counter=p_counter, anchor=anchor, toc=toc)
525
+ self._index.append(item)
526
+ return item
527
+
528
+ def _indexSection(self, counter, title=None, anchor=None, toc=True, \
529
+ level=1, appendix=False, numbered=True):
530
+ counter = str(counter)
531
+ if numbered:
532
+ if appendix:
533
+ autoName = 'Appendix' + self.nbws_cond + counter
534
+ autoAnchor = 'rfc.appendix.' + counter
535
+ else:
536
+ autoName = 'Section' + self.nbws_cond + counter
537
+ autoAnchor = 'rfc.section.' + counter
538
+ item = _RfcItem(autoName, autoAnchor, counter=counter, title=title,
539
+ anchor=anchor, toc=toc, level=level, appendix=appendix)
540
+ else:
541
+ if not title:
542
+ raise RfcWriterError("No title available when trying to insert index item for anchor %s" % anchor)
543
+ autoAnchor = 'rfc.' + re.sub('[^A-Za-z0-9]+', '_', title).lower()
544
+ item = _RfcItem(title, autoAnchor, title=title)
545
+ self._index.append(item)
546
+ return item
547
+
548
+ def _indexReferences(self, counter, title=None, anchor=None, toc=True, \
549
+ subCounter=0, level=1):
550
+ if subCounter < 1:
551
+ autoName = 'References'
552
+ autoAnchor = 'rfc.references'
553
+ else:
554
+ subCounter = str(subCounter)
555
+ autoName = 'References' + self.nbws_cond + subCounter
556
+ autoAnchor = 'rfc.references.' + subCounter
557
+ item = _RfcItem(autoName, autoAnchor, counter=counter, title=title, \
558
+ anchor=anchor, toc=toc, level=level)
559
+ self._index.append(item)
560
+ return item
561
+
562
+ def _indexFigure(self, counter, title=None, anchor=None, toc=False):
563
+ counter = str(counter)
564
+ autoName = 'Figure' + self.nbws_cond + counter
565
+ autoAnchor = 'rfc.figure.' + counter
566
+ item = _RfcItem(autoName, autoAnchor, counter=counter, title=title, anchor=anchor, \
567
+ toc=toc)
568
+ self._index.append(item)
569
+ return item
570
+
571
+ def _indexTable(self, counter, title=None, anchor=None, toc=False):
572
+ counter = str(counter)
573
+ autoName = 'Table' + self.nbws_cond + counter
574
+ autoAnchor = 'rfc.table.' + counter
575
+ item = _RfcItem(autoName, autoAnchor, counter=counter, title=title, anchor=anchor, toc=toc)
576
+ self._index.append(item)
577
+ return item
578
+
579
+ def _indexRef(self, counter, title=None, anchor=None, toc=False):
580
+ counter = str(counter)
581
+ if self.pis['symrefs'] == 'yes':
582
+ autoName = '[' + (anchor or counter ) +']'
583
+ else:
584
+ autoName = '['+counter+']'
585
+ autoAnchor = 'rfc.ref.' + counter
586
+ item = _RfcItem(autoName, autoAnchor, counter=counter, title=title, anchor=anchor, toc=toc)
587
+ self._index.append(item)
588
+ return item
589
+
590
+ def _indexCref(self, counter, anchor):
591
+ counter = str(counter)
592
+ autoName = 'Comment' + self.nbws_cond + anchor
593
+ autoAnchor = 'rfc.comment.' + counter
594
+ item = _RfcItem(autoName, autoAnchor, counter=counter, anchor=anchor, toc=False)
595
+ self._index.append(item)
596
+ return item
597
+
598
+ def _indexAuthor(self, counter, anchor, name):
599
+ autoName = name
600
+ autoAnchor = 'rfc.author.%s' % counter
601
+ item = _RfcItem(autoName, autoAnchor, counter=counter, anchor=anchor, toc=False)
602
+ self._index.append(item)
603
+ return item
604
+
605
+ def get_initials(self, author):
606
+ """author is an rfc2629 author element. Return the author initials,
607
+ fixed up according to current flavour and policy."""
608
+ initials = author.attrib.get('initials', '')
609
+ multiple = author.pis["multiple-initials"] == "yes"
610
+ initials_list = re.split("[. ]+", initials)
611
+ try:
612
+ initials_list.remove('')
613
+ except:
614
+ pass
615
+ if len(initials_list) > 0:
616
+ if multiple:
617
+ # preserve spacing, but make sure all parts have a trailing
618
+ # period
619
+ initials = initials.strip()
620
+ initials += '.' if not initials.endswith('.') else ''
621
+ initials = re.sub(r'([^.]) ', r'\g<1>. ', initials)
622
+ else:
623
+ initials = initials_list[0] + "."
624
+ return initials
625
+
626
+ def parse_pi(self, pi):
627
+ return xml2rfc.utils.parse_pi(pi, self.pis)
628
+
629
+ def get_numeric_pi(self, key, default):
630
+ num = self.pis.get(key, None)
631
+ if num is None:
632
+ return default
633
+ if not num.isdigit():
634
+ xml2rfc.log.warn('Expected a numeric value for the %s PI, found "%s"' % (key, num))
635
+ return default
636
+ return int(num)
637
+
638
+ def _getTocIndex(self):
639
+ return [item for item in self._index if item.toc]
640
+
641
+ def _getItemByAnchor(self, anchor):
642
+ for item in self._index:
643
+ if item.autoAnchor == anchor or item.anchor == anchor:
644
+ return item
645
+ return None
646
+
647
+ def _validate_ipr(self):
648
+ """ Ensure the application has boilerplate for the ipr attribute given """
649
+ ipr = self.r.attrib.get('ipr', self.defaults['ipr'])
650
+ if not ipr in self.supported_ipr:
651
+ raise RfcWriterError('No boilerplate text available for '
652
+ 'ipr: \'%s\'. Acceptable values are: ' % ipr + \
653
+ ', '.join(self.supported_ipr))
654
+
655
+ def is_numbered(self, node):
656
+ attr = node.attrib.get('numbered', 'true')
657
+ return attr in ['yes', 'true', ]
658
+
659
+ def _format_date(self):
660
+ """ Fix the date data """
661
+ today = self.date
662
+ date = self.r.find('front/date')
663
+ assert date is not None, "Bug in schema validation: no date element in document"
664
+ year = date.attrib.get('year')
665
+ if not year:
666
+ year = str(self.date.year)
667
+ date.set('year', year)
668
+ if not year.isdigit():
669
+ xml2rfc.log.error("Expected a numeric year, found '%s'" % (year, ))
670
+ year = int(year)
671
+ #
672
+ month = date.attrib.get('month')
673
+ if not month:
674
+ if year != today.year:
675
+ xml2rfc.log.error("Cannot handle a <date> with year different than this year, and no month. Using today's date.")
676
+ year = today.year
677
+ month = today.month
678
+ date.set('month', str(month))
679
+ else:
680
+ if not month.isdigit():
681
+ month = xml2rfc.util.date.normalize_month(month)
682
+ month = int(month)
683
+ #
684
+ day = date.attrib.get('day')
685
+ if day is None:
686
+ temp_date = datetime.date(year=year, month=month, day=1)
687
+ if today.year == year and today.month == month:
688
+ day = today.day
689
+ elif abs(today - temp_date) < datetime.timedelta(days=34):
690
+ if datetime.date(year=year, month=month, day=1) < today:
691
+ # wrong month, and the first day of that month is earlier
692
+ # than today. Use the last day of the month
693
+ day = calendar.monthrange(year, month)[1]
694
+ else:
695
+ # wrong month, later than this month. Use the first day.
696
+ day = 1
697
+ else:
698
+ day = 1
699
+ else:
700
+ day = int(day)
701
+
702
+ self.date = datetime.date(year=year, month=month, day=day)
703
+
704
+ # Setup the expiration string for drafts as published date + six months
705
+ if self.draft:
706
+ if not date.get('day'):
707
+ date.set('day', str(day))
708
+ if not date.get('month'):
709
+ date.set('month', str(month))
710
+ expire_date = self.date + datetime.timedelta(185)
711
+ self.expire_string = expire_date.strftime('%B %d, %Y').replace(' 0', ' ')
712
+
713
+ def _format_counter(self, text, count, list_length=1):
714
+ """ Return a proper string for a formatted list bullet. Allowed types:
715
+ %c: Lowercase chars
716
+ %C: Uppercase chars
717
+ %d: Digits
718
+ %i: Lowercase roman numerals
719
+ %I: Uppercase roman numerals
720
+ %o: octal
721
+ %x: Lowercase hex
722
+ %X: Uppercase hex
723
+ """
724
+ import math
725
+ roman_widths = { 1:1, 2:2, 3:3, 4:2, 5:1, 6:2, 7:3, 8:4, 9:2,
726
+ 10:1, 11:2, 12:3, 13:4, 14:3, 15:2, 16:3, 17:4, 18:5, 19:3,
727
+ 20:2, 21:3, 22:4, 23:5, 24:4, 25:3, 26:4, 27:5, 28:6, 29:4, }
728
+ #
729
+ decimal_width = int(math.log(list_length, 10))
730
+ roman_width = roman_widths.get(list_length, 6)
731
+ letter_width = int(math.log(list_length, 26))
732
+ hex_width = int(math.log(list_length, 16))
733
+ octal_width = int(math.log(list_length, 8))
734
+ extra_width = len(text)+1
735
+ if '%d' in text:
736
+ text = text.replace(r'%d', str(count)).ljust(decimal_width+extra_width)
737
+ elif '%c' in text:
738
+ text = text.replace(r'%c', xml2rfc.util.num.int2letter(count)).ljust(letter_width+extra_width)
739
+ elif '%C' in text:
740
+ text = text.replace(r'%C', xml2rfc.util.num.int2letter(count).upper()).ljust(letter_width+extra_width)
741
+ elif '%i' in text:
742
+ text = text.replace(r'%i', xml2rfc.util.num.int2roman(count)).ljust(roman_width+extra_width)
743
+ elif '%I' in text:
744
+ text = text.replace(r'%I', xml2rfc.util.num.int2roman(count).upper()).ljust(roman_width+extra_width)
745
+ elif '%o' in text:
746
+ text = text.replace(r'%o', oct(count).replace("0","",1)).replace("o","",1).ljust(octal_width+extra_width)
747
+ elif '%x' in text:
748
+ text = text.replace(r'%x', hex(count).replace("0x","",1)).ljust(hex_width+extra_width)
749
+ elif '%X' in text:
750
+ text = text.replace(r'%X', hex(count).replace("0x","",1).upper()).ljust(hex_width+extra_width)
751
+ return text
752
+
753
+ def _format_author_string(self, authors):
754
+ """ Given a list of <author> elements, return a readable string of names """
755
+ buf = []
756
+ for i, author in enumerate(authors):
757
+ organization = author.find('organization')
758
+ initials, surname = short_author_name_parts(author)
759
+ if i == len(authors) - 1 and len(authors) > 1:
760
+ buf.append('and ')
761
+ if surname:
762
+ initials = self.get_initials(author) or initials or ''
763
+ if i == len(authors) - 1 and len(authors) > 1:
764
+ # Last author is rendered in reverse
765
+ if len(initials) > 0:
766
+ buf.append(initials + ' ' + \
767
+ surname)
768
+ else:
769
+ buf.append(surname)
770
+ elif len(initials) > 0:
771
+ buf.append(surname + ', ' + initials)
772
+ else:
773
+ buf.append(surname)
774
+ if author.attrib.get('role', '') == 'editor':
775
+ buf.append(', Ed.')
776
+ elif organization is not None and organization.text:
777
+ # Use organization instead of name
778
+ buf.append(organization.text.strip())
779
+ else:
780
+ continue
781
+ if len(authors) == 2 and i == 0:
782
+ buf.append(' ')
783
+ elif i < len(authors) - 1:
784
+ buf.append(', ')
785
+ return ''.join(buf)
786
+
787
+ def _prepare_top_left(self):
788
+ """ Returns a lines of lines for the top left header """
789
+ lines = []
790
+ # Document stream / workgroup
791
+ if not self.pis['private']:
792
+ if self.draft:
793
+ workgroup = self.r.find('front/workgroup')
794
+ if workgroup is not None and workgroup.text:
795
+ lines.append(workgroup.text)
796
+ else:
797
+ lines.append(self.boilerplate['draft_workgroup'])
798
+ else:
799
+ # Determine 'workgroup' from submissionType
800
+ subtype = self.r.attrib.get('submissionType',
801
+ self.defaults['submissionType'])
802
+ docstream = self.boilerplate['document_stream'].get(subtype)
803
+ lines.append(docstream)
804
+
805
+ # RFC number
806
+ if not self.draft:
807
+ lines.append('Request for Comments: ' + self.rfcnumber)
808
+ elif not self.pis['private']:
809
+ lines.append('Internet-Draft')
810
+
811
+ # Series number
812
+ category = self.r.attrib.get('category', '')
813
+ seriesNo = self.r.attrib.get('seriesNo')
814
+ if seriesNo is not None and category in self.boilerplate['series_name']:
815
+ lines.append('%s: %s' % (self.boilerplate['series_name'][category],
816
+ seriesNo))
817
+
818
+ # RFC relation notice
819
+ approved_text = self.draft and '(if approved)' or ''
820
+ obsoletes = self.r.attrib.get('obsoletes')
821
+ if obsoletes:
822
+ wrapper = textwrap.TextWrapper(width=40, subsequent_indent=' '*len('Obsoletes: '))
823
+ line = 'Obsoletes: %s %s' % (obsoletes, approved_text)
824
+ lines += wrapper.wrap(line)
825
+ updates = self.r.attrib.get('updates')
826
+ if updates:
827
+ wrapper = textwrap.TextWrapper(width=40, subsequent_indent=' '*len('Updates: '))
828
+ line = 'Updates: %s %s' % (updates, approved_text)
829
+ lines += wrapper.wrap(line)
830
+
831
+ # Category
832
+ if category:
833
+ cat_text = self.boilerplate[category]
834
+ if self.draft:
835
+ lines.append('Intended status: ' + cat_text)
836
+ else:
837
+ lines.append('Category: ' + cat_text)
838
+ else:
839
+ xml2rfc.log.warn('No category specified for document.')
840
+
841
+ # Expiration notice for drafts
842
+ if self.expire_string and not self.pis['private']:
843
+ lines.append('Expires: ' + self.expire_string)
844
+
845
+ # ISSN identifier
846
+ if not self.draft:
847
+ lines.append('ISSN: %s' % self.boilerplate['issn'])
848
+
849
+ # Strip any whitespace from XML to make header as neat as possible
850
+ lines = [ l.rstrip() for l in lines ]
851
+ return lines
852
+
853
+ def _prepare_top_right(self):
854
+ """ Returns a list of lines for the top right header """
855
+ lines = []
856
+ # Keep track of previous organization and remove if redundant.
857
+ last_org = None
858
+ last_pos = None
859
+ authors = self.r.findall('front/author')
860
+ authors = [ a for a in authors if a.get('role') != 'contributor' ]
861
+ for author in authors:
862
+ role = author.attrib.get('role', '')
863
+ if role == 'editor':
864
+ role = ', Ed.'
865
+ initials = self.get_initials(author)
866
+ lines.append(initials + ' ' + author.attrib.\
867
+ get('surname', '') + role)
868
+ organization = author.find('organization')
869
+ org_name = ''
870
+ if self.options.first_page_author_org:
871
+ if organization is not None:
872
+ abbrev = organization.attrib.get("abbrev", None)
873
+ if abbrev != None and abbrev.strip() != '':
874
+ org_name = abbrev.strip()
875
+ elif organization.text and organization.text.strip() != '':
876
+ org_name = organization.text.strip()
877
+ if org_name == '':
878
+ lines.append('')
879
+ else:
880
+ if org_name == last_org:
881
+ # Remove redundant organization
882
+ del lines[last_pos]
883
+ lines.append(org_name)
884
+ last_org = org_name
885
+ last_pos = len(lines)-1
886
+ # remove blank lines between authors and date
887
+ if lines[last_pos] == '':
888
+ del lines[last_pos]
889
+ last_pos = len(lines)-1
890
+
891
+ date = self.r.find('front/date')
892
+ if date is not None:
893
+ year = date.attrib.get('year', '')
894
+ month = date.attrib.get('month', '')
895
+ day = date.attrib.get('day', '')
896
+ if month:
897
+ if month.isdigit():
898
+ month = calendar.month_name[int(month)]
899
+ month = month + ' '
900
+ if day:
901
+ day = day + ', '
902
+ lines.append(month + day + year)
903
+ # Strip any whitespace from XML to make header as neat as possible
904
+ lines = [ l.strip() for l in lines ]
905
+ return lines
906
+
907
+ def write_figure(self, figure):
908
+ """ Writes <figure> elements """
909
+ figure_align = figure.attrib.get('align', self.defaults['figure_align'])
910
+ anchor = figure.attrib.get('anchor')
911
+ title = figure.attrib.get('title', self.defaults['figure_title'])
912
+ suppress_title = figure.attrib.get('suppress-title', 'false')
913
+
914
+ # Keep track of count if there is an anchor, or PI was enabled
915
+ if anchor or self.pis['figurecount'] == 'yes':
916
+ self.figure_count += 1
917
+
918
+ if anchor:
919
+ # Insert anchor(s) for the figure
920
+ self.insert_anchor('rfc.figure.' + str(self.figure_count))
921
+ self.insert_anchor(anchor)
922
+ if self.indexmode:
923
+ # Add figure to the index, inserting any anchors necessary
924
+ self._indexFigure(self.figure_count, anchor=anchor, title=title)
925
+
926
+ # Write preamble
927
+ preamble = figure.find('preamble')
928
+ if preamble is not None:
929
+ self.write_t_rec(preamble, align=figure_align)
930
+
931
+ # iref
932
+ for element in figure:
933
+ if element.tag == 'iref':
934
+ self._add_iref_to_index(element)
935
+
936
+ # Write figure with optional delimiter
937
+ delimiter = figure.pis['artworkdelimiter']
938
+ artwork = figure.find('artwork')
939
+ artwork_align = artwork.attrib.get('align', figure_align)
940
+ blanklines = int(figure.pis['artworklines'])
941
+ self.write_raw(artwork.text, align=artwork_align,
942
+ blanklines=blanklines, delimiter=delimiter,
943
+ source_line=figure.sourceline)
944
+
945
+ # Write postamble
946
+ postamble = figure.find('postamble')
947
+ if postamble is not None:
948
+ self.write_t_rec(postamble, align=figure_align)
949
+
950
+ # Write label
951
+ title = figure.attrib.get('title', '')
952
+ if anchor or self.pis['figurecount'] == 'yes':
953
+ if suppress_title == 'false':
954
+ if title:
955
+ title = 'Figure ' + str(self.figure_count) + ': ' + title
956
+ else:
957
+ title = 'Figure ' + str(self.figure_count)
958
+ if title:
959
+ self.write_label(title, type='figure', source_line=figure.sourceline)
960
+
961
+ def write_table(self, table):
962
+ """ Writes <texttable> elements """
963
+ align = table.attrib.get('align', self.defaults['table_align'])
964
+ anchor = table.attrib.get('anchor')
965
+ title = table.attrib.get('title', self.defaults['table_title'])
966
+ suppress_title = table.attrib.get('suppress-title', 'false')
967
+
968
+ # Keep track of count if there is an anchor, or PI was enabled
969
+ if anchor or self.pis['tablecount'] == 'yes':
970
+ self.table_count += 1
971
+
972
+ if anchor:
973
+ # Insert anchor(s) for the table
974
+ self.insert_anchor('rfc.table.' + str(self.table_count))
975
+ self.insert_anchor(anchor)
976
+ if self.indexmode:
977
+ # Add table to the index, inserting any anchors necessary
978
+ self._indexTable(self.table_count, anchor=anchor, title=title)
979
+
980
+ # Write preamble
981
+ preamble = table.find('preamble')
982
+ if preamble is not None:
983
+ self.write_t_rec(preamble, align=align)
984
+
985
+ # Write table
986
+ self.draw_table(table, table_num=self.table_count)
987
+
988
+ # Write postamble
989
+ postamble = table.find('postamble')
990
+ if postamble is not None:
991
+ self.write_t_rec(postamble, align=align)
992
+
993
+ # Write label if anchor is set or PI figurecount = yes
994
+ if anchor or self.pis['tablecount'] == 'yes':
995
+ title = table.attrib.get('title', '')
996
+ if suppress_title == 'false':
997
+ if title:
998
+ title = 'Table ' + str(self.table_count) + ': ' + title
999
+ else:
1000
+ title = 'Table ' + str(self.table_count)
1001
+ if title:
1002
+ self.write_label(title, type='table', source_line=table.sourceline)
1003
+
1004
+ def _index_t_rec(self, element):
1005
+ """ Traverse a <t> element only performing indexing operations """
1006
+ pass
1007
+
1008
+
1009
+ def numToAlpha(self, n):
1010
+ str = ""
1011
+ az = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1012
+ while n > 0:
1013
+ r = (n-1) % 26
1014
+ n = int((n-1)/26)
1015
+ str = az[r] + str
1016
+ return str
1017
+
1018
+ def write_section_rec(self, section, count_str="1.", appendix=False,
1019
+ level=0, numbered=True, emit_numbered="all", s_count=1):
1020
+ """ Recursively writes <section> elements
1021
+
1022
+ We use the self.indexmode flag to determine whether or not we
1023
+ render actual text at this point or just lookup header information
1024
+ to construct the index
1025
+ """
1026
+ if level > 0:
1027
+ anchor = section.attrib.get('anchor')
1028
+ title = section.attrib.get('title')
1029
+ include_toc = section.attrib.get('toc', self.defaults['section_toc']) != 'exclude' \
1030
+ and (not appendix or self.pis['tocappendix'] == 'yes')
1031
+ if level == 1:
1032
+ numbered = numbered and self.is_numbered(section)
1033
+ else:
1034
+ numbered = self.is_numbered(section)
1035
+ if level > 1 and not numbered:
1036
+ if not self.indexmode:
1037
+ xml2rfc.log.warn('Unnumbered subsections are not permitted: found section "%s" with attribute numbered="no" or "false"' % (title, ))
1038
+ numbered = True
1039
+ if self.indexmode:
1040
+ # Add section to the index
1041
+ self._indexSection(count_str, title=title, anchor=anchor,
1042
+ toc=include_toc, level=level,
1043
+ appendix=appendix, numbered=numbered)
1044
+ else:
1045
+ # Write the section heading
1046
+ if numbered:
1047
+ autoAnchor = 'rfc.' + ('section.' if not appendix else 'appendix.') + count_str
1048
+ else:
1049
+ autoAnchor = 'rfc.' + re.sub('[^A-Za-z0-9]+', '_', title).lower()
1050
+ bullet = appendix and level == 1 and 'Appendix %s' % count_str or count_str
1051
+ self.write_heading(title, bullet=bullet + '.' if numbered else "",
1052
+ autoAnchor=autoAnchor,
1053
+ anchor=anchor, level=level)
1054
+ else:
1055
+ # Must be <middle> or <back> element -- no title or index.
1056
+ count_str = ''
1057
+ numbered = True
1058
+
1059
+ p_count = 1 # Paragraph counter
1060
+ for element in section:
1061
+ # Check for a PI
1062
+ if element.tag is etree.PI:
1063
+ pidict = self.parse_pi(element)
1064
+ if pidict and "needLines" in pidict:
1065
+ self.needLines(pidict["needLines"])
1066
+ # Write elements in XML document order
1067
+ if element.tag == 't':
1068
+ anchor = element.attrib.get('anchor')
1069
+ if self.indexmode:
1070
+ self._indexParagraph(count_str, p_count, anchor=anchor)
1071
+ autoAnchor = 'rfc.section.' + count_str + '.p.' + str(p_count)
1072
+ self.write_t_rec(element, autoAnchor=autoAnchor)
1073
+ p_count += 1
1074
+ elif element.tag == 'figure':
1075
+ self.write_figure(element)
1076
+ elif element.tag == 'texttable':
1077
+ self.write_table(element)
1078
+ elif element.tag == 'iref':
1079
+ self._add_iref_to_index(element)
1080
+
1081
+ # Append a dot to separate sub counters
1082
+ if count_str:
1083
+ count_str += '.'
1084
+
1085
+ # Recurse on sections
1086
+ for child_sec in section.findall('section'):
1087
+ if level == 0:
1088
+ if numbered:
1089
+ numbered = self.is_numbered(child_sec)
1090
+ elif self.is_numbered(child_sec):
1091
+ title = child_sec.attrib.get('title')
1092
+ if not self.indexmode:
1093
+ xml2rfc.log.warn('Numbered sections are not permitted after unnumbered sections: found section "%s" without attribute numbered="no"' % (title,))
1094
+ numbered = False
1095
+ if emit_numbered=="only" and not numbered:
1096
+ continue
1097
+ if emit_numbered=="no" and numbered:
1098
+ continue
1099
+ if appendix == True and not count_str:
1100
+ if s_count == 1 and self.pis["rfcedstyle"] == "yes":
1101
+ self.needLines(-1)
1102
+ # Use an alphabetic counter for first-level appendix
1103
+ self.write_section_rec(child_sec, self.numToAlpha(s_count),
1104
+ level=level + 1, appendix=True, numbered=numbered)
1105
+ else:
1106
+ # Use a numeric counter
1107
+ self.write_section_rec(child_sec, count_str + str(s_count),
1108
+ level=level + 1, appendix=appendix, numbered=numbered)
1109
+
1110
+ s_count += 1
1111
+
1112
+ # Set the ending index number so we know where to begin references
1113
+ if count_str == '' and appendix == False:
1114
+ self.refs_start = s_count
1115
+
1116
+ return s_count
1117
+
1118
+ def write_status_section(self):
1119
+ """ Writes the 'Status of this Memo' section """
1120
+
1121
+ self.write_heading('Status of This Memo', autoAnchor='rfc.status')
1122
+ if not self.draft: #RFC
1123
+ # Status boilerplate is determined by category/submissionType/consensus
1124
+ category = self.r.attrib.get('category', 'none')
1125
+ stream = self.r.attrib.get('submissionType',
1126
+ self.defaults['submissionType'])
1127
+ # Consensus is assumed 'yes' for Standards Track documents and BCPs
1128
+ consensus = category in ['std', 'bcp'] and 'yes' or \
1129
+ self.r.attrib.get('consensus', self.defaults['consensus'])
1130
+ workgroup = ''
1131
+ wg_element = self.r.find('front/workgroup')
1132
+ if wg_element is not None and wg_element.text:
1133
+ workgroup = wg_element.text
1134
+
1135
+ # Write first paragraph
1136
+ if category in self.boilerplate['status']:
1137
+ self.write_paragraph(self.boilerplate['status'][category].get('p1', ''))
1138
+
1139
+ # Build second paragraph
1140
+ p2 = []
1141
+ if category in self.boilerplate['status']:
1142
+ p2.append(self.boilerplate['status'][category].get('p2', ''))
1143
+ if stream == 'IETF':
1144
+ if consensus == 'yes':
1145
+ p2.append(self.boilerplate['status']['IETF_consensus'])
1146
+ else:
1147
+ p2.append(self.boilerplate['status']['IETF'])
1148
+ elif stream == 'IRTF':
1149
+ p2.append(self.boilerplate['status']['IRTF'])
1150
+ if workgroup:
1151
+ if consensus == 'yes':
1152
+ p2.append(self.boilerplate['status']['IRTF_workgroup_consensus'] \
1153
+ % workgroup)
1154
+ else:
1155
+ p2.append(self.boilerplate['status']['IRTF_workgroup'] \
1156
+ % workgroup)
1157
+ elif stream == 'IAB':
1158
+ if consensus == 'yes':
1159
+ p2.append(self.boilerplate['status']['IAB_consensus'])
1160
+ else:
1161
+ p2.append(self.boilerplate['status']['IAB'])
1162
+ else:
1163
+ p2.append(self.boilerplate['status'].get(stream, ''))
1164
+
1165
+ # Last sentence of p2
1166
+ if self.date < datetime.date(year=2016, month=6, day=1):
1167
+ status="status_5741"
1168
+ else:
1169
+ status="status" # Current boilerplate
1170
+ if stream == 'IETF' and category == 'std':
1171
+ p2.append(self.boilerplate[status]['p2end_ietf_std'])
1172
+ elif stream == 'IETF' and category == 'bcp':
1173
+ p2.append(self.boilerplate[status]['p2end_ietf_bcp'])
1174
+ elif stream == 'IETF':
1175
+ p2.append(self.boilerplate[status]['p2end_ietf_other'])
1176
+ else:
1177
+ p2.append(self.boilerplate[status]['p2end_other'] \
1178
+ % self.approvers.get(stream, ''))
1179
+
1180
+
1181
+ # Write second paragraph
1182
+ self.write_paragraph(' '.join(p2))
1183
+
1184
+ # Write third paragraph
1185
+ key = 'p3' if self.date < self.boilerplate_https_date else 'p3_s'
1186
+ self.write_paragraph(self.boilerplate['status'][key] % self.rfcnumber)
1187
+
1188
+ else: # Draft
1189
+ # Start by checking for an ipr header
1190
+ ipr = self.r.attrib.get('ipr', self.defaults['ipr'])
1191
+ if '200902' in ipr:
1192
+ self.write_paragraph(self.boilerplate['ipr_200902_status'])
1193
+ elif '200811' in ipr:
1194
+ self.write_paragraph(self.boilerplate['ipr_200811_status'])
1195
+
1196
+ # Write the standard draft status
1197
+ key = 'draft' if self.date < self.boilerplate_https_date else 'draft_s'
1198
+ for par in self.boilerplate['status'][key]:
1199
+ self.write_paragraph(par)
1200
+
1201
+ # Write expiration string, if it was generated
1202
+ if self.expire_string:
1203
+ self.write_paragraph( \
1204
+ self.boilerplate['draft_expire'] % self.expire_string)
1205
+
1206
+ def write_copyright(self):
1207
+ """ Writes the 'Copyright' section """
1208
+ self.write_heading('Copyright Notice', autoAnchor='rfc.copyrightnotice')
1209
+
1210
+ # Write header line with year
1211
+ date = self.r.find('front/date')
1212
+ year = ''
1213
+ if date is not None:
1214
+ year = date.attrib.get('year', self.date.year)
1215
+ self.write_paragraph(self.boilerplate['base_copyright_header'] % year)
1216
+
1217
+ # Write next paragraph which may be modified by ipr
1218
+ ipr = self.r.attrib.get('ipr', self.defaults['ipr'])
1219
+ key = 'base_copyright_body' if self.date < self.boilerplate_https_date else 'base_copyright_body_s'
1220
+ body = self.boilerplate[key]
1221
+ if '200902' in ipr and self.r.attrib.get('submissionType',
1222
+ self.defaults['submissionType']) == 'IETF':
1223
+ body += ' ' + self.boilerplate['ipr_200902_copyright_ietfbody']
1224
+ self.write_paragraph(body)
1225
+
1226
+ # Write any additional paragraphs
1227
+ if 'noModification' in ipr:
1228
+ self.write_paragraph(self.boilerplate['ipr_noModification_copyright'])
1229
+ elif 'noDerivatives' in ipr:
1230
+ self.write_paragraph(self.boilerplate['ipr_noDerivatives_copyright'])
1231
+ elif ipr == 'pre5378Trust200902':
1232
+ self.write_paragraph(self.boilerplate['ipr_pre5378Trust200902_copyright'])
1233
+
1234
+ def _build_index(self):
1235
+ self.indexmode = True
1236
+ # Reset document counters
1237
+ self.ref_start = 1 # Start of reference counters
1238
+ self.refs_start = 1 # Start of references sections
1239
+ self.figure_count = 0
1240
+ self.table_count = 0
1241
+ self.eref_count = 0
1242
+ self.pis = self.xmlrfc.getpis()
1243
+
1244
+ # Abstract
1245
+ abstract = self.r.find('front/abstract')
1246
+ if abstract is not None:
1247
+ self.write_heading('Abstract', autoAnchor='rfc.abstract')
1248
+ for t in abstract.findall('t'):
1249
+ self.write_t_rec(t)
1250
+
1251
+ # Authors
1252
+ count = 0
1253
+ authors = self.r.findall('front/author')
1254
+ for author in authors:
1255
+ count += 1
1256
+ autoName = full_author_name_expansion(author)
1257
+ self._indexAuthor(count, author.get('anchor'), autoName)
1258
+
1259
+ # Optional notes
1260
+ for note in self.r.findall('front/note'):
1261
+ self.write_heading(note.attrib.get('title', 'Note'))
1262
+ for t in note.findall('t'):
1263
+ self.write_t_rec(t)
1264
+
1265
+ # Middle sections
1266
+ middle = self.r.find('middle')
1267
+ if middle is not None:
1268
+ self.write_section_rec(middle, None)
1269
+
1270
+ # References sections
1271
+ # Treat references as nested only if there is more than one
1272
+ ref_counter = 0
1273
+ refs_counter = str(self.refs_start)
1274
+ references = self.r.findall('back/references')
1275
+ # Write root level references header
1276
+ refs_title = self.pis['refparent']
1277
+ if len(references) == 1 and not self.eref_list:
1278
+ refs_title = references[0].attrib.get('title', refs_title)
1279
+
1280
+ if len(references) > 0:
1281
+ self._indexReferences(refs_counter, title=refs_title)
1282
+
1283
+ if len(references) > 1 or self.eref_list:
1284
+ for i, reference_list in enumerate(references):
1285
+ refs_newcounter = refs_counter + '.' + str(i + 1)
1286
+ refs_title = reference_list.attrib.get('title', self.pis["refparent"])
1287
+ self._indexReferences(refs_newcounter, title=refs_title, \
1288
+ subCounter=i+1, level=2)
1289
+ if self.eref_list:
1290
+ refs_newcounter = refs_counter + '.' + str(len(references)+1)
1291
+ self._indexReferences(refs_newcounter, title="URIs", level=2, subCounter=len(references)+1)
1292
+
1293
+
1294
+ for ref in self.r.xpath('.//references//reference'):
1295
+ if len(ref):
1296
+ ref_counter += 1
1297
+ title = ref.find("front/title")
1298
+ if title != None:
1299
+ if 'anchor' in ref.attrib:
1300
+ self._indexRef(ref_counter, title=title.text, anchor=ref.attrib["anchor"])
1301
+ else:
1302
+ raise RfcWriterError("Reference is missing an anchor: %s" % etree.tostring(ref))
1303
+
1304
+ # Appendix sections
1305
+ back = self.r.find('back')
1306
+ if back is not None:
1307
+ s_count = self.write_section_rec(back, None, appendix=True, emit_numbered="only")
1308
+
1309
+ # Index section, disable if there are no irefs
1310
+ if len(self._iref_index) > 0:
1311
+ # Add explicitly to index
1312
+ title = 'Index'
1313
+ autoAnchor = 'rfc.index'
1314
+ item = _RfcItem(title, autoAnchor, title=title)
1315
+ self._index.append(item)
1316
+
1317
+ if back is not None:
1318
+ self.write_section_rec(back, None, appendix=True, emit_numbered="no", s_count=s_count)
1319
+
1320
+ # Authors addresses section
1321
+
1322
+ if self.pis['authorship'] == 'yes':
1323
+ authors = self.r.findall('front/author')
1324
+ authors = [ a for a in authors if a.get('role') != 'contributor' ]
1325
+ autoAnchor = 'rfc.authors'
1326
+ if len(authors) > 1:
1327
+ title = "Authors' Addresses"
1328
+ else:
1329
+ title = "Author's Address"
1330
+ # Add explicitly to index
1331
+ item = _RfcItem(title, autoAnchor, title=title)
1332
+ self._index.append(item)
1333
+
1334
+ def _build_document(self):
1335
+ self.indexmode = False
1336
+ # Reset document counters
1337
+ self.ref_start = 1 # Start of reference counters
1338
+ self.refs_start = 1 # Start of references sections
1339
+ self.figure_count = 0
1340
+ self.table_count = 0
1341
+ self.eref_count = 0
1342
+ self.pis = self.xmlrfc.getpis()
1343
+
1344
+ # Block header
1345
+ topblock = self.pis['topblock']
1346
+ if topblock == 'yes':
1347
+ self.write_top(self._prepare_top_left(), \
1348
+ self._prepare_top_right())
1349
+
1350
+ # Title & Optional docname
1351
+ title = self.r.find('front/title')
1352
+ if title is not None:
1353
+ docName = self.r.attrib.get('docName', None)
1354
+ rfcnum = self.r.attrib.get('number', None)
1355
+ if (not docName or not docName.strip()) and not rfcnum:
1356
+ xml2rfc.log.warn("No (or empty) 'docName' attribute in the <rfc/> element -- can't insert draft name on first page.")
1357
+ if docName and '.' in docName:
1358
+ xml2rfc.log.warn("The 'docName' attribute of the <rfc/> element should not contain any filename extension: docName=\"draft-foo-bar-02\".")
1359
+ if docName and not rfcnum and not re.search(r'-\d\d$', docName):
1360
+ xml2rfc.log.warn("The 'docName' attribute of the <rfc/> element should have a revision number as the last component: docName=\"draft-foo-bar-02\".")
1361
+ self.write_title(title.text, docName, title.sourceline)
1362
+
1363
+ # Abstract
1364
+ abstract = self.r.find('front/abstract')
1365
+ if abstract is not None:
1366
+ self.write_heading('Abstract', autoAnchor='rfc.abstract')
1367
+ for t in abstract.findall('t'):
1368
+ self.write_t_rec(t)
1369
+
1370
+ # Optional notified boilerplate
1371
+ # Disabled. See issue #123, http://trac.tools.ietf.org/tools/xml2rfc/trac/ticket/123
1372
+ ## if self.pis['iprnotified'] == 'yes':
1373
+ ## self.write_paragraph(BaseRfcWriter.boilerplate['iprnotified'])
1374
+
1375
+ # Optional notes
1376
+ for note in self.r.findall('front/note'):
1377
+ self.write_heading(note.attrib.get('title', 'Note'))
1378
+ for t in note.findall('t'):
1379
+ self.write_t_rec(t)
1380
+
1381
+ if not self.pis['private']:
1382
+ # Verify that 'ipr' attribute is valid before continuing
1383
+ self._validate_ipr()
1384
+
1385
+ # "Status of this Memo" section
1386
+ self.write_status_section()
1387
+
1388
+ # Copyright section
1389
+ self.write_copyright()
1390
+
1391
+ # Insert the table of contents marker at this position
1392
+ toc_enabled = self.pis['toc']
1393
+ if toc_enabled == 'yes':
1394
+ self.insert_toc()
1395
+
1396
+ # Middle sections
1397
+ middle = self.r.find('middle')
1398
+ if middle is not None:
1399
+ self.write_section_rec(middle, None)
1400
+
1401
+ # References sections
1402
+ # Treat references as nested only if there is more than one
1403
+ refs_counter = str(self.refs_start)
1404
+ references = self.r.findall('back/references')
1405
+ # Write root level references header
1406
+ refs_title = self.pis['refparent']
1407
+ if len(references) == 1 and not self.eref_list:
1408
+ refs_title = references[0].attrib.get('title', refs_title)
1409
+
1410
+ if len(references) > 0 or self.eref_list:
1411
+ self.write_heading(refs_title, bullet=refs_counter + '.', \
1412
+ autoAnchor='rfc.references')
1413
+ if len(references) > 1:
1414
+ for i, reference_list in enumerate(references):
1415
+ refs_newcounter = refs_counter + '.' + str(i + 1)
1416
+ refs_title = reference_list.attrib.get('title', self.pis['refparent'])
1417
+ autoAnchor = 'rfc.references.' + str(i + 1)
1418
+ self.write_heading(refs_title, bullet=refs_newcounter + '.',\
1419
+ autoAnchor=autoAnchor, level=2)
1420
+ self.write_reference_list(reference_list)
1421
+ elif len(references) == 1:
1422
+ if self.eref_list:
1423
+ refs_newcounter = refs_counter + '.1'
1424
+ refs_title = references[0].attrib.get('title', self.pis['refparent'])
1425
+ autoAnchor = 'rfc.references.1'
1426
+ self.write_heading(refs_title, bullet=refs_newcounter + '.',\
1427
+ autoAnchor=autoAnchor, level=2)
1428
+ self.write_reference_list(references[0])
1429
+
1430
+ if self.eref_list:
1431
+ self.write_erefs(refs_counter, len(references)+1)
1432
+
1433
+ # Appendix sections
1434
+ back = self.r.find('back')
1435
+ if back is not None:
1436
+ s_count = self.write_section_rec(back, None, appendix=True, emit_numbered="only")
1437
+
1438
+ # Index section, disable if there are no irefs
1439
+ if len(self._iref_index) > 0:
1440
+ self.insert_iref_index()
1441
+
1442
+ if back is not None:
1443
+ self.write_section_rec(back, None, appendix=True, emit_numbered="no", s_count=s_count)
1444
+
1445
+ self.write_crefs()
1446
+
1447
+ # Authors addresses section
1448
+ if self.pis['authorship'] == 'yes':
1449
+ authors = self.r.findall('front/author')
1450
+ authors = [ a for a in authors if a.get('role') != 'contributor' ]
1451
+ autoAnchor = 'rfc.authors'
1452
+ if len(authors) > 1:
1453
+ title = "Authors' Addresses"
1454
+ else:
1455
+ title = "Author's Address"
1456
+ self.write_heading(title, autoAnchor=autoAnchor)
1457
+ for author in authors:
1458
+ self.write_address_card(author)
1459
+
1460
+ self.check_for_unused_references()
1461
+
1462
+ def write(self, filename, tmpfile=None):
1463
+ """ Public method to write the RFC document to a file. """
1464
+ # If the ascii flag is enabled, replace unicode with ascii in the tree
1465
+ if self.ascii:
1466
+ xml2rfc.utils.safeReplaceUnicode(self.r)
1467
+
1468
+ # Protect any words with slashes that are on the preserve list
1469
+ xml2rfc.utils.safeTagSlashedWords(self.r)
1470
+
1471
+ # Do any pre processing necessary, such as inserting metadata
1472
+ self.pre_indexing()
1473
+ # Make two passes over the document, the first pass we run in
1474
+ # 'index mode' to construct the internal index and other things,
1475
+ # the second pass will assemble a buffer and render the actual text
1476
+ self._build_index()
1477
+ # Format the date properly
1478
+ self._format_date()
1479
+ # Do any pre-build processing necessary, such as inserting metadata
1480
+ self.pre_rendering()
1481
+ # Build the document
1482
+ self._build_document()
1483
+ # Primary buffer is finished -- apply any post processing
1484
+ self.post_rendering()
1485
+
1486
+ # Finished processing, write to file
1487
+ # Override file with keyword argument if passed in, ignoring filename.
1488
+ # Warning -- make sure file is open and ready for writing!
1489
+ if not tmpfile:
1490
+ if self.ascii:
1491
+ file = open(filename, 'w')
1492
+ else:
1493
+ # Open as unicode
1494
+ file = codecs.open(filename, 'w', encoding='utf-8')
1495
+ self.write_to_file(file)
1496
+ file.close()
1497
+ else:
1498
+ self.write_to_file(tmpfile)
1499
+
1500
+ if not self.options.quiet and filename:
1501
+ xml2rfc.log.write(' Created file', filename)
1502
+
1503
+ def write_erefs(self, refs_counter, refs_subsection):
1504
+ """ Only text versions do this so provide a default that does nothing
1505
+ """
1506
+ pass
1507
+
1508
+ def write_crefs(self):
1509
+ """ Only text versions do this so provide a default that does nothing
1510
+ """
1511
+ pass
1512
+
1513
+ def check_for_unused_references(self):
1514
+ """ If this is a reference and it is not used - then warn me
1515
+ """
1516
+ if not self.indexmode:
1517
+ for item in self._index:
1518
+ if item.autoAnchor.startswith("rfc.ref.") and not item.used:
1519
+ xml2rfc.log.warn("No <xref> in <rfc> targets <reference anchor='%s'>" % item.anchor)
1520
+
1521
+ def needLines(self, count):
1522
+ """ Deal with the needLines PI """
1523
+ pass
1524
+
1525
+ # -----------------------------------------
1526
+ # Base writer interface methods to override
1527
+ # -----------------------------------------
1528
+
1529
+ def insert_toc(self):
1530
+ """ Marks the current buffer position to insert ToC at """
1531
+ raise NotImplementedError('insert_toc() needs to be overridden')
1532
+
1533
+ def insert_iref_index(self):
1534
+ """ Marks the current buffer position to insert the index at """
1535
+ raise NotImplementedError('insert_iref_index() needs to be '
1536
+ 'overridden')
1537
+
1538
+ def write_raw(self, text, indent=3, align='left', blanklines=0,
1539
+ delimiter=None, source_line=None):
1540
+ """ Writes a block of text that preserves all whitespace """
1541
+ raise NotImplementedError('write_raw() needs to be overridden')
1542
+
1543
+ def write_label(self, text, type='figure', source_line=None):
1544
+ """ Writes a table or figure label """
1545
+ raise NotImplementedError('write_label() needs to be overridden')
1546
+
1547
+ def write_title(self, title, docName=None, source_line=None):
1548
+ """ Writes the document title """
1549
+ raise NotImplementedError('write_title() needs to be overridden')
1550
+
1551
+ def write_heading(self, text, bullet='', autoAnchor=None, anchor=None,
1552
+ level=1, breakNeed=0):
1553
+ """ Writes a section heading """
1554
+ raise NotImplementedError('write_heading() needs to be overridden')
1555
+
1556
+ def write_paragraph(self, text, align='left', autoAnchor=None):
1557
+ """ Writes a paragraph of text """
1558
+ raise NotImplementedError('write_paragraph() needs to be overridden')
1559
+
1560
+ def write_t_rec(self, t, align='left', autoAnchor=None):
1561
+ """ Recursively writes <t> elements """
1562
+ raise NotImplementedError('write_t_rec() needs to be overridden')
1563
+
1564
+ def write_top(self, left_header, right_header):
1565
+ """ Writes the main document header
1566
+
1567
+ Takes two list arguments, one for each margin, and combines them
1568
+ so that they exist on the same lines of text
1569
+ """
1570
+ raise NotImplementedError('write_top() needs to be overridden')
1571
+
1572
+ def write_address_card(self, author):
1573
+ """ Writes the address information for an <author> element """
1574
+ raise NotImplementedError('write_address_card() needs to be ' \
1575
+ 'overridden')
1576
+
1577
+ def write_reference_list(self, list):
1578
+ """ Writes a <references> element """
1579
+ raise NotImplementedError('write_reference_list() needs to be ' \
1580
+ 'overridden')
1581
+
1582
+ def insert_anchor(self, text):
1583
+ """ Inserts a document anchor for internal links """
1584
+ raise NotImplementedError('insert_anchor() needs to be overridden')
1585
+
1586
+ def draw_table(self, table, table_num=None):
1587
+ """ Draws a formatted table from a <texttable> element
1588
+
1589
+ For HTML nothing is really 'drawn' since we can use <table>
1590
+ """
1591
+ raise NotImplementedError('draw_table() needs to be overridden')
1592
+
1593
+ def pre_indexing(self):
1594
+ """ First method that is called before traversing the XML RFC tree for indexing"""
1595
+ raise NotImplementedError('pre_indexing() needs to be overridden')
1596
+
1597
+ def pre_rendering(self):
1598
+ """ First method that is called before traversing the XML RFC tree for rendering"""
1599
+ raise NotImplementedError('pre_rendering() needs to be overridden')
1600
+
1601
+ def post_rendering(self):
1602
+ """ Last method that is called after traversing the XML RFC tree """
1603
+ raise NotImplementedError('post_rendering() needs to be overridden')
1604
+
1605
+ def write_to_file(self, file):
1606
+ """ Writes the finished buffer to a file """
1607
+ raise NotImplementedError('write_to_file() needs to be overridden')
1608
+
1609
+ # --------------------------------------------------------------------------------------------------
1610
+ # Sets of various kinds of tags in the v3 schema
1611
+
1612
+ deprecated_element_tags = set([
1613
+ 'c',
1614
+ 'facsimile',
1615
+ 'format',
1616
+ 'list',
1617
+ 'postamble',
1618
+ 'preamble',
1619
+ 'relref',
1620
+ 'spanx',
1621
+ 'texttable',
1622
+ 'ttcol',
1623
+ 'vspace',
1624
+ ])
1625
+
1626
+ deprecated_attributes = [
1627
+ # element, attribute
1628
+ ('artwork', 'height'),
1629
+ ('artwork', 'width'),
1630
+ ('artwork', 'xml:space'),
1631
+ ('figure', 'align'),
1632
+ ('figure', 'alt'),
1633
+ ('figure', 'height'),
1634
+ ('figure', 'src'),
1635
+ ('figure', 'suppress-title'),
1636
+ ('figure', 'title'),
1637
+ ('figure', 'width'),
1638
+ ('note', 'title'),
1639
+ ('references', 'title'),
1640
+ ('section', 'title'),
1641
+ ('seriesInfo', 'status'),
1642
+ ('seriesInfo', 'stream'),
1643
+ ('t', 'hangText'),
1644
+ ('texttable', 'title'),
1645
+ ('xref', 'pageno'),
1646
+ ]
1647
+
1648
+
1649
+ v3_rnc_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'v3.rnc')
1650
+ v3_rng_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'v3.rng')
1651
+ v3_schema = etree.ElementTree(file=v3_rng_file)
1652
+
1653
+ def get_element_tags():
1654
+ tags = set()
1655
+ elements = v3_schema.xpath("/x:grammar/x:define/x:element", namespaces=namespaces)
1656
+ for element in elements:
1657
+ name = element.get('name')
1658
+ if not name in tags:
1659
+ tags.add(name)
1660
+ return tags
1661
+
1662
+ def get_meta_tags():
1663
+ "Get tags that can have text content but don't occur in regular text"
1664
+ tags = set()
1665
+ for elem in ['author', 'address', 'front', 'postal', 'reference' ]:
1666
+ refs = v3_schema.xpath("/x:grammar/x:define[@name='%s']//x:ref"%elem, namespaces=namespaces)
1667
+ for r in refs:
1668
+ name = r.get('name')
1669
+ if not name in tags:
1670
+ tags.add(name)
1671
+ return tags
1672
+
1673
+ def get_text_tags():
1674
+ "Get tags that can have text content from the schema"
1675
+ tags = set()
1676
+ texts = v3_schema.xpath("/x:grammar/x:define/x:element//x:text", namespaces=namespaces)
1677
+ for t in texts:
1678
+ element = list(t.iterancestors('{*}element'))[0]
1679
+ name = element.get('name')
1680
+ if not name in tags:
1681
+ tags.add(name)
1682
+ return tags
1683
+
1684
+ def get_text_or_block_tags():
1685
+ "Get tags that can have either text or block contents from the schema"
1686
+ tags = set()
1687
+ texts = v3_schema.xpath("/x:grammar/x:define/x:element/x:choice/*/x:choice//x:text", namespaces=namespaces)
1688
+ for t in texts:
1689
+ element = list(t.iterancestors('{*}element'))[0]
1690
+ name = element.get('name')
1691
+ if not name in tags:
1692
+ tags.add(name)
1693
+ return tags
1694
+
1695
+ def get_inline_tags():
1696
+ "Get tags that can occur within text from the schema"
1697
+ tags = set()
1698
+ referenced = v3_schema.xpath("/x:grammar/x:define/x:element//x:ref", namespaces=namespaces)
1699
+ for ref in referenced:
1700
+ name = ref.get('name')
1701
+ if not name in tags:
1702
+ p = ref.getparent()
1703
+ text = p.find('x:text', namespaces=namespaces)
1704
+ if text != None:
1705
+ tags.add(name)
1706
+ return tags
1707
+
1708
+ def get_xref_tags():
1709
+ "Get tags that can occur as direct children of <xref>"
1710
+ tags = set()
1711
+ refs = v3_schema.xpath("/x:grammar/x:define/x:element[@name='xref']//x:ref", namespaces=namespaces)
1712
+ tags = set([ c.get('name') for c in refs ])
1713
+ return tags
1714
+
1715
+
1716
+ element_tags= get_element_tags()
1717
+ meta_tags = get_meta_tags() - deprecated_element_tags
1718
+ text_tags = get_text_tags() - deprecated_element_tags
1719
+ all_inline_tags = get_inline_tags()
1720
+ inline_tags = all_inline_tags - deprecated_element_tags
1721
+ block_tags = element_tags - inline_tags - deprecated_element_tags - set(['t'])
1722
+ mixed_tags = get_text_or_block_tags() - deprecated_element_tags
1723
+ xref_tags = get_xref_tags()
1724
+
1725
+ # --------------------------------------------------------------------------------------------------
1726
+
1727
+ class BaseV3Writer(object):
1728
+
1729
+ def __init__(self, xmlrfc, quiet=None, options=default_options, date=None):
1730
+ global v3_rnc_file, v3_rng_file, v3_schema
1731
+ self.xmlrfc = xmlrfc
1732
+ self.tree = xmlrfc.tree if xmlrfc else None
1733
+ self.root = self.tree.getroot() if xmlrfc else None
1734
+ self.options = options
1735
+ self.date = date if date is not None else datetime.date.today()
1736
+ self.v3_rnc_file = v3_rnc_file
1737
+ self.v3_rng_file = v3_rng_file
1738
+ self.v3_rng = etree.RelaxNG(file=self.v3_rng_file)
1739
+ self.v3_schema = v3_schema
1740
+ self.schema = v3_schema
1741
+ self.index_items = []
1742
+ self.meta_tags = set(meta_tags)
1743
+ self.text_tags = set(text_tags)
1744
+ self.inline_tags = set(inline_tags)
1745
+ self.mixed_tags = set(mixed_tags)
1746
+ self.xref_tags = set(xref_tags)
1747
+ self.attribute_defaults = self.get_all_attribute_defaults()
1748
+ #
1749
+ #
1750
+ self.errors = []
1751
+ # if options.debug:
1752
+ # found_handlers = []
1753
+ # missing_handlers = []
1754
+ # for tag in (self.get_element_tags() - deprecated_element_tags):
1755
+ # func_name = "render_%s" % (tag.lower(),)
1756
+ # if getattr(self, func_name, False):
1757
+ # found_handlers.append(func_name)
1758
+ # else:
1759
+ # missing_handlers.append(func_name)
1760
+ # debug.pprint('found_handlers')
1761
+ # debug.show('len(found_handlers)')
1762
+ # debug.pprint('missing_handlers')
1763
+ # debug.show('len(missing_handlers)')
1764
+
1765
+ def log(self, msg):
1766
+ xml2rfc.log.write(msg)
1767
+
1768
+ def msg(self, e, label, text):
1769
+ if e != None:
1770
+ lnum = getattr(e, 'sourceline', None)
1771
+ file = getattr(e, 'base', None)
1772
+ if lnum:
1773
+ msg = "%s(%s): %s %s" % (file or self.xmlrfc.source, lnum, label, text, )
1774
+ else:
1775
+ msg = "(No source line available): %s %s" % (label, text, )
1776
+ else:
1777
+ msg = "%s %s" % (label, text)
1778
+ return msg
1779
+
1780
+ def get_relevant_pis(self, e):
1781
+ pis = []
1782
+ if e != None:
1783
+ # directly inside element
1784
+ for c in e.getchildren():
1785
+ if c.tag == etree.PI and c.target == xml2rfc.V3_PI_TARGET:
1786
+ pis.append(c)
1787
+ # siblings before element
1788
+ for s in e.itersiblings(preceding=True):
1789
+ if s.tag == etree.PI and s.target == xml2rfc.V3_PI_TARGET:
1790
+ pis.append(s)
1791
+ # ancestor's earlier siblings
1792
+ for a in e.iterancestors():
1793
+ for s in a.itersiblings(preceding=True):
1794
+ if s.tag == etree.PI and s.target == xml2rfc.V3_PI_TARGET:
1795
+ pis.append(s)
1796
+ # before root elements
1797
+ p = self.root.getprevious()
1798
+ while p != None:
1799
+ if p.tag == etree.PI and p.target == xml2rfc.V3_PI_TARGET:
1800
+ pis.append(p)
1801
+ p = p.getprevious()
1802
+ return pis
1803
+
1804
+ def get_relevant_pi(self, e, name):
1805
+ pis = self.get_relevant_pis(e)
1806
+ pi_list = list(filter(None, [ pi.get(name) for pi in pis ]))
1807
+ return pi_list[-1] if pi_list else None
1808
+
1809
+ def silenced(self, e, text):
1810
+ text = text.strip()
1811
+ pis = self.get_relevant_pis(e)
1812
+ silenced = filter(None, self.options.silence[:] + [ pi.get('silence') for pi in pis ])
1813
+ for s in silenced:
1814
+ for label in ['Note: ', 'Warning: ']:
1815
+ if s.startswith(label):
1816
+ s = s[len(label):]
1817
+ if text.startswith(s):
1818
+ return True
1819
+ try:
1820
+ if re.match(s, text):
1821
+ return True
1822
+ except re.error:
1823
+ pass
1824
+ return False
1825
+
1826
+ def note(self, e, text, label='Note:'):
1827
+ if self.options.verbose:
1828
+ if not self.silenced(e, text):
1829
+ self.log(self.msg(e, label, text))
1830
+
1831
+ def warn(self, e, text, label='Warning:'):
1832
+ if self.options.verbose or not self.silenced(e, text):
1833
+ self.log(self.msg(e, label, text))
1834
+
1835
+ def err(self, e, text, trace=False):
1836
+ msg = self.msg(e, 'Error:', text)
1837
+ self.errors.append(msg)
1838
+ if trace or self.options.debug:
1839
+ raise RuntimeError(msg)
1840
+ else:
1841
+ self.log(msg)
1842
+
1843
+ def die(self, e, text, trace=False):
1844
+ msg = self.msg(e, 'Error:', text)
1845
+ self.errors.append(msg)
1846
+ raise RfcWriterError(msg)
1847
+
1848
+ def xinclude(self):
1849
+ ## From RFC7998:
1850
+ ##
1851
+ # 5.1.1. XInclude Processing
1852
+ #
1853
+ # Process all <x:include> elements. Note: XML <x:include> elements may
1854
+ # include more <x:include> elements (with relative references resolved
1855
+ # against the base URI potentially modified by a previously inserted
1856
+ # xml:base attribute). The tool may be configurable with a limit on
1857
+ # the depth of recursion.
1858
+ try:
1859
+ if not self.options.allow_local_file_access:
1860
+ self.check_includes()
1861
+ self.tree.xinclude()
1862
+ except etree.XIncludeError as e:
1863
+ self.die(None, "XInclude processing failed: %s" % e)
1864
+
1865
+ def check_includes(self):
1866
+ # Check for <xi:include> elements with local filesystem references
1867
+ ns = {'xi': b'http://www.w3.org/2001/XInclude'}
1868
+ xincludes = self.root.xpath('//xi:include', namespaces=ns)
1869
+ for xinclude in xincludes:
1870
+ href = urlparse(xinclude.get('href'))
1871
+ if not href.netloc or href.scheme == 'file':
1872
+ error = 'XInclude processing failed: Can not access local file: {}'.format(xinclude.get('href'))
1873
+ self.die(None, error)
1874
+
1875
+ def remove_dtd(self):
1876
+ #
1877
+ # 5.1.2. DTD Removal
1878
+ #
1879
+ # Fully process any Document Type Definitions (DTDs) in the input
1880
+ # document, then remove the DTD. At a minimum, this entails processing
1881
+ # the entity references and includes for external files.
1882
+
1883
+ ## Entities has been resolved as part of the initial parsing. Remove
1884
+ ## docinfo and PIs outside the <rfc/> element by copying the root
1885
+ ## element and creating a new tree.
1886
+ root = copy.deepcopy(self.root)
1887
+ self.tree = root.getroottree()
1888
+ self.root = root
1889
+
1890
+ def get_refname_mapping(self):
1891
+ reflist = self.root.xpath('.//references/reference|.//references/referencegroup')
1892
+ if self.root.get('symRefs', 'true') == 'true':
1893
+ refname_mapping = dict( (e.get('anchor'), e.get('anchor')) for e in reflist )
1894
+ else:
1895
+ refname_mapping = (dict( (e.get('anchor'), str(i+1)) for i,e in enumerate(reflist) ))
1896
+ refname_mapping.update(dict( (e.get('target'), e.get('to')) for e in self.root.xpath('.//displayreference') ))
1897
+ return refname_mapping
1898
+
1899
+ def dispatch(self, selectors):
1900
+ """
1901
+ Process selectors, extracting an XPath selector and generating a method name
1902
+ from each entry in self.selectors, and calling the method with all elements
1903
+ matching the XPath expression, in order to process self.tree.
1904
+ """
1905
+ # Setup
1906
+ selector_visits = dict( (s, 0) for s in selectors)
1907
+ # Check for duplicate <displayreference> 'to' values:
1908
+ seen = {}
1909
+ for e in self.root.xpath('.//displayreference'):
1910
+ to = e.get('to')
1911
+ if to in set(seen.keys()):
1912
+ self.die(e, 'Found duplicate displayreference value: "%s" has already been used in %s' % (to, etree.tostring(seen[to]).strip()))
1913
+ else:
1914
+ seen[to] = e
1915
+ del seen
1916
+ ## Do remaining processing by xpath selectors (listed above)
1917
+ for s in selectors:
1918
+ slug = slugify(s.replace('self::', '').replace(' or ','_').replace(';','_'))
1919
+ if '@' in s:
1920
+ func_name = 'attribute_%s' % slug
1921
+ elif "()" in s:
1922
+ func_name = slug
1923
+ else:
1924
+ if not slug:
1925
+ slug = 'rfc'
1926
+ func_name = 'element_%s' % slug
1927
+ # get rid of selector annotation
1928
+ ss = s.split(';')[0]
1929
+ func = getattr(self, func_name, None)
1930
+ if func:
1931
+ if self.options.debug:
1932
+ self.note(None, "Calling %s()" % func_name)
1933
+ for e in self.tree.xpath(ss):
1934
+ func(e, e.getparent())
1935
+ selector_visits[s] += 1
1936
+ else:
1937
+ self.warn(None, "No handler %s() found" % (func_name, ))
1938
+ if self.options.debug:
1939
+ for s in selectors:
1940
+ if selector_visits[s] == 0:
1941
+ self.note(None, "Selector '%s' has not matched" % (s))
1942
+ if self.errors:
1943
+ raise RfcWriterError("Not creating output file due to errors (see above)")
1944
+ return self.tree
1945
+
1946
+ def get_all_attribute_defaults(self):
1947
+ defaults = {}
1948
+ elements = self.schema.xpath("/x:grammar/x:define/x:element", namespaces=namespaces)
1949
+ for tag in [ e.get('name') for e in elements ]:
1950
+ attr = self.schema.xpath("/x:grammar/x:define/x:element[@name='%s']//x:attribute" % tag, namespaces=namespaces)
1951
+ defaults[tag] = dict( (a.get('name'), a.get("{%s}defaultValue"%namespaces['a'], None)) for a in attr )
1952
+ return defaults
1953
+
1954
+ def check_refs_numbered(self):
1955
+ # see if references should be numbered. This is True unless the last
1956
+ # top-level section of <middle/> had numbered='false'.
1957
+ if not hasattr(self, '_refs_numbered'):
1958
+ last_middle_section = ([None, ] + list(self.root.find('middle').iterchildren('section')))[-1]
1959
+ self._refs_numbered = last_middle_section.get('numbered', 'true') == 'true'
1960
+ return self._refs_numbered
1961
+
1962
+ # methods operating on the xml tree
1963
+
1964
+ def get_element_from_id(self, id):
1965
+ for a in 'anchor', 'pn', 'slugifiedName':
1966
+ elem = self.root.find('.//*[@%s="%s"]'%(a, id, ))
1967
+ if elem != None:
1968
+ break
1969
+ return elem
1970
+
1971
+ def get_element_page(self, e):
1972
+ page = getattr(e, 'page', None)
1973
+ if not page:
1974
+ for a in e.iterancestors():
1975
+ page = getattr(a, 'page', None)
1976
+ if page != None:
1977
+ break
1978
+ return page
1979
+
1980
+ # def get_valid_child_tags(self, tag):
1981
+ # refs = self.schema.xpath("/x:grammar/x:define/x:element[@name='%s']//x:ref" % tag, namespaces=utils.namespaces)
1982
+ # names = set([ r.get('name') for r in refs ])
1983
+ # return names
1984
+
1985
+ def write(self, filename):
1986
+ raise NotImplementedError()
1987
+
1988
+ # methods for use in setting up page headers and footers
1989
+
1990
+ def page_top_left(self):
1991
+ if self.root.get('ipr') == 'none':
1992
+ return ''
1993
+ number = self.root.get('number')
1994
+ if number != None:
1995
+ return 'RFC %s' % number
1996
+ info = self.root.find('./front/seriesInfo[@name="RFC"]')
1997
+ if info != None:
1998
+ return 'RFC %s' % info.get('value')
1999
+ return 'Internet-Draft'
2000
+
2001
+ def page_top_center(self):
2002
+ title = self.root.find('./front/title')
2003
+ text = title.get('abbrev') or ' '.join(title.itertext())
2004
+ if len(text) > 40:
2005
+ self.warn(title, "Expected a title or title abbreviation of not more than 40 character for the page header, found %s characters" % len(text))
2006
+ return text[:40]
2007
+
2008
+ def full_page_top_center(self):
2009
+ title = self.root.find('./front/title')
2010
+ text = title.get('abbrev') or ' '.join(title.itertext())
2011
+ if len(text) > 80:
2012
+ self.note(title, "Found a title or title abbreviation of length %s; check that it fits the running page header." % len(text))
2013
+ return text
2014
+
2015
+ def page_top_right(self):
2016
+ date = self.root.find('./front/date')
2017
+ year, month, day = extract_date(date, self.options.date)
2018
+ year, month, day = augment_date(year, month, day, self.options.date)
2019
+ text = format_date(year, month, None, legacy=True)
2020
+ return text
2021
+
2022
+ def page_bottom_left(self):
2023
+ authors = self.root.findall('front/author')
2024
+ authors = [ a for a in authors if a.get('role') != 'contributor' ]
2025
+ surnames = [ n for n in [ short_author_ascii_name_parts(a)[1] for a in authors ] if n ]
2026
+ text = ''
2027
+ if len(surnames) == 1:
2028
+ text = surnames[0]
2029
+ elif len(surnames) == 2:
2030
+ text = '%s & %s' % (surnames[0], surnames[1])
2031
+ elif len(surnames) > 2:
2032
+ text = '%s, et al.' % surnames[0]
2033
+ return text
2034
+
2035
+ def page_bottom_center(self):
2036
+ # Either expiry date or category
2037
+ if self.options.rfc or self.root.get('ipr') == 'none':
2038
+ cat = self.root.get('category')
2039
+ text = strings.category_name[cat]
2040
+ else:
2041
+ date = get_expiry_date(self.tree, self.options.date)
2042
+ parts = date.year, date.month, date.day
2043
+ text = 'Expires %s' % format_date(*parts, legacy=self.options.legacy_date_format)
2044
+ return text
2045
+
2046
+ def pretty_print_prep(self, e, p):
2047
+ ind = self.options.indent
2048
+ ## The actual printing is done in self.write()
2049
+ def indent(e, i):
2050
+ if e.tag in (etree.CDATA, ):
2051
+ return
2052
+ if e.tag in (etree.Comment, etree.PI, ):
2053
+ if not e.tail:
2054
+ if e.getnext() != None:
2055
+ e.tail = '\n'+' '*i
2056
+ else:
2057
+ e.tail = '\n'+' '*(i-ind)
2058
+ return
2059
+ #
2060
+ if e.tag not in text_tags:
2061
+ if len(e) and (e.text is None or e.text.strip()==''):
2062
+ e.text = '\n'+' '*(i+ind)
2063
+ elif e.tag in ['blockquote', 'li', 'dd', 'td', 'th' ]: # mixed content
2064
+ pass
2065
+ if len(e) and e[0] not in inline_tags and (e.text is None or e.text.strip()==''):
2066
+ e.text = '\n'+' '*(i+ind)
2067
+ elif e.tag in ['artwork', 'sourcecode', ]:
2068
+ pass
2069
+ else:
2070
+ # inline tag
2071
+ if e.tail == '':
2072
+ e.tail = None
2073
+ if len(e):
2074
+ z = e[-1]
2075
+ if z.tail and re.search(r'\n[ \t]+$', z.tail):
2076
+ z.tail = re.sub(r'\n[ \t]+$', '\n'+' '*(i), z.tail)
2077
+ else:
2078
+ if e.text and re.search(r'\n[ \t]+$', e.text):
2079
+ e.text = re.sub(r'\n[ \t]+$', '\n'+' '*(i), e.text)
2080
+
2081
+ for c in e:
2082
+ indent(c, i+ind)
2083
+
2084
+ if e.tag not in all_inline_tags:
2085
+ if e.tail is None or e.tail.strip()=='':
2086
+ if e.getnext() != None:
2087
+ e.tail = '\n'+' '*i
2088
+ else:
2089
+ e.tail = '\n'+' '*(i-ind)
2090
+ elif not is_svg(e):
2091
+ self.warn(e, 'Unexpected text content after block tag: <%s>%s' % (e.tag, e.tail))
2092
+ else:
2093
+ if e.tail != None and e.tail.strip()=='' and '\n' in e.tail:
2094
+ if e.getnext() != None:
2095
+ e.tail = '\n'+' '*i
2096
+ else:
2097
+ e.tail = '\n'+' '*(i-ind)
2098
+ indent(e, 0)
2099
+ e.tail = None
2100
+
2101
+ def deadly_error(self, error):
2102
+ # errors that xml2rfc must not allow to continue
2103
+
2104
+ if error.message in DEADLY_ERRORS:
2105
+ if self.options.verbose:
2106
+ msg = "%s(%s): Error: Can not continue further with error: %s" % (self.xmlrfc.source, error.line, error.message)
2107
+ self.log(msg)
2108
+ return True
2109
+
2110
+ def validate(self, when='', warn=False):
2111
+ # Note: Our schema doesn't permit xi:include elements, so the document
2112
+ # must have had XInclude processing done before calling validate()
2113
+
2114
+ # The lxml Relax NG validator checks that xsd:ID values are unique,
2115
+ # but unfortunately the error messages are completely unhelpful (lxml
2116
+ # 4.1.1, libxml 2.9.1): "Element li has extra content: t" when 't' has
2117
+ # a duplicate xsd:ID attribute. So we check all attributes with
2118
+ # content specified as xsd:ID first, and give better messages:
2119
+
2120
+ # Get the attributes we need to check
2121
+ if when and not when.startswith(' '):
2122
+ when = ' '+when
2123
+ dups = find_duplicate_ids(self.schema, self.tree)
2124
+ for attr, id, e in dups:
2125
+ self.warn(e, 'Duplicate xsd:ID attribute %s="%s" found. This will cause validation failure.' % (attr, id, ))
2126
+
2127
+ try:
2128
+ # Use a deepcopy to avoid any memory issues.
2129
+ tree = copy.deepcopy(self.tree)
2130
+ self.v3_rng.assertValid(tree)
2131
+ return True
2132
+ except Exception as e:
2133
+ deadly = False
2134
+ if hasattr(e, 'error_log'):
2135
+ for error in e.error_log:
2136
+ path = getattr(error, 'path', '')
2137
+ msg = "%s(%s): %s: %s, at %s" % (self.xmlrfc.source, error.line, error.level_name.title(), error.message, path)
2138
+ self.log(msg)
2139
+ if not deadly:
2140
+ deadly = self.deadly_error(error)
2141
+ if error.message.startswith("Did not expect text"):
2142
+ items = self.tree.xpath(error.path + '/text()')
2143
+ for item in items:
2144
+ item = item.strip()
2145
+ if item:
2146
+ nl = '' if len(item) < 60 else '\n '
2147
+ self.log(' Unexpected text:%s "%s"' % (nl, item))
2148
+
2149
+ else:
2150
+ log.warn('\nInvalid document: %s' % (e,))
2151
+ if warn and not deadly:
2152
+ self.warn(self.root, 'Invalid document%s.' % (when, ))
2153
+ return False
2154
+ else:
2155
+ self.die(self.root, 'Invalid document%s.' % (when, ))
2156
+
2157
+ def validate_before(self, e, p):
2158
+ version = self.root.get('version', '3')
2159
+ if version not in ['3', ]:
2160
+ self.die(self.root, 'Expected <rfc> version="3", but found "%s"' % version)
2161
+ if not self.validate('before'):
2162
+ self.note(None, "Schema validation failed for input document")
2163
+
2164
+ self.validate_draft_name()
2165
+
2166
+ def validate_draft_name(self):
2167
+ if not self.root.attrib.get('number', False):
2168
+ docName = self.root.attrib.get('docName', None)
2169
+ info = self.root.find('./front/seriesInfo[@name="Internet-Draft"]')
2170
+ si_draft_name = info.get('value') if info != None else None
2171
+
2172
+ if all([docName, si_draft_name]) and docName != si_draft_name:
2173
+ self.die(self.root, 'docName and value in <seriesInfo name="Internet-Draft" ..> must match.')
2174
+
2175
+ return True
2176
+
2177
+ def validate_after(self, e, p):
2178
+ # XXX: There is an issue with exponential increase in validation time
2179
+ # as a function of the number of attributes on the root element, on
2180
+ # the order of minutes for the full set of possible attribute values.
2181
+ # See https://bugzilla.gnome.org/show_bug.cgi?id=133736 . In our
2182
+ # schema, there is no dependency in the underlying schema on the root
2183
+ # element attributes. In order to avoid very long validation times, we
2184
+ # strip the root attributes before validation, and put them back
2185
+ # afterwards.
2186
+ attrib = copy.deepcopy(e.attrib)
2187
+ e.attrib.clear()
2188
+ #
2189
+ if not self.validate('after', warn=True):
2190
+ self.note(None, "Schema validation failed for input document")
2191
+ else:
2192
+ self.root.set('version', '3')
2193
+ #
2194
+ keys = list(attrib.keys())
2195
+ keys.sort()
2196
+ e.attrib.update(attrib)
2197
+
2198
+ def remove(self, p, e):
2199
+ # Element.remove(child) removes both the child and its tail, so in
2200
+ # order not to loose text when removing comments or other elements,
2201
+ # we need to handle that case:
2202
+ if e.tail:
2203
+ s = e.getprevious()
2204
+ if s != None:
2205
+ s.tail = (s.tail or '') + e.tail
2206
+ else:
2207
+ p.text = (p.text or '') + e.tail
2208
+ p.remove(e)
2209
+
2210
+ @staticmethod
2211
+ def split_pn(pn):
2212
+ """Split a pn into meaningful parts
2213
+
2214
+ Returns a tuple (element_type, section_number, paragraph_number).
2215
+ If there is no paragraph number, it will be None.
2216
+ """
2217
+ parts = pn.split('-', 2)
2218
+ elt_type = parts[0]
2219
+ sect_num = parts[1]
2220
+ paragraph_num = parts[2] if len(parts) > 2 else None
2221
+
2222
+ # see if section num needs special handling
2223
+ components = sect_num.split('.', 1)
2224
+ if components[0] in ['appendix', 'boilerplate', 'note', 'toc']:
2225
+ sect_num = components[1] # these always have a second component
2226
+ return elt_type, sect_num, paragraph_num
2227
+
2228
+ @staticmethod
2229
+ def level_of_section_num(num):
2230
+ """Determine hierarchy level of a section number
2231
+
2232
+ Top level items have level 1. N.B., num is a string.
2233
+ """
2234
+ num_with_no_trailing_dot = num.rstrip('.')
2235
+ components = num_with_no_trailing_dot.split('.')
2236
+ if any([len(cpt) == 0 for cpt in components]):
2237
+ log.warn('Empty section number component in "{}"'.format(num))
2238
+ return len(components)
2239
+
2240
+ @classmethod
2241
+ def is_top_level_section(cls, num):
2242
+ return cls.level_of_section_num(num) == 1
2243
+
2244
+ appendix_pn_re = re.compile(r'^section-[a-z]\.|^section-appendix\.')
2245
+ @classmethod
2246
+ def is_appendix(cls, pn):
2247
+ """Is a section with this number an appendix?"""
2248
+ return cls.appendix_pn_re.match(pn) is not None