xml2rfc 3.26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xml2rfc/__init__.py +68 -0
- xml2rfc/boilerplate_id_guidelines.py +28 -0
- xml2rfc/boilerplate_rfc_7841.py +575 -0
- xml2rfc/boilerplate_tlp.py +258 -0
- xml2rfc/data/SVG-1.2-RFC.rnc +1666 -0
- xml2rfc/data/SVG-1.2-RFC.rng +7460 -0
- xml2rfc/data/Scripts.txt +2693 -0
- xml2rfc/data/metadata.js +218 -0
- xml2rfc/data/metadata.min.js +1 -0
- xml2rfc/data/reference.rnc +955 -0
- xml2rfc/data/reference.rng +2158 -0
- xml2rfc/data/referencegroup.rnc +16 -0
- xml2rfc/data/referencegroup.rng +2180 -0
- xml2rfc/data/rfc7991.rnc +955 -0
- xml2rfc/data/rfc7991.rng +2143 -0
- xml2rfc/data/v2.rnc +309 -0
- xml2rfc/data/v2.rng +720 -0
- xml2rfc/data/v3.rnc +1144 -0
- xml2rfc/data/v3.rng +2538 -0
- xml2rfc/data/v3compat.dtd +630 -0
- xml2rfc/data/v3compat.rnc +985 -0
- xml2rfc/data/xml2rfc-v3-emacs-nxml.rnc +1083 -0
- xml2rfc/data/xml2rfc.css +1171 -0
- xml2rfc/data/xml2rfc.js +7 -0
- xml2rfc/debug.py +221 -0
- xml2rfc/log.py +54 -0
- xml2rfc/parser.py +815 -0
- xml2rfc/run.py +801 -0
- xml2rfc/scripts.py +52 -0
- xml2rfc/strings.py +26 -0
- xml2rfc/templates/address_card.html +21 -0
- xml2rfc/templates/base.html +415 -0
- xml2rfc/templates/doc.xml +367 -0
- xml2rfc/templates/doc.yaml +2138 -0
- xml2rfc/templates/rfc2629-other.ent +61 -0
- xml2rfc/templates/rfc2629-xhtml.ent +170 -0
- xml2rfc/templates/rfc2629.dtd +328 -0
- xml2rfc/uniscripts/__init__.py +97 -0
- xml2rfc/uniscripts/unidata.py +306 -0
- xml2rfc/uniscripts/update/make_unidata.py +94 -0
- xml2rfc/util/__init__.py +7 -0
- xml2rfc/util/date.py +94 -0
- xml2rfc/util/fonts.py +53 -0
- xml2rfc/util/name.py +229 -0
- xml2rfc/util/num.py +119 -0
- xml2rfc/util/postal.py +285 -0
- xml2rfc/util/unicode.py +441 -0
- xml2rfc/utils.py +672 -0
- xml2rfc/walkpdf.py +113 -0
- xml2rfc/writers/__init__.py +25 -0
- xml2rfc/writers/base.py +2248 -0
- xml2rfc/writers/bib.py +43 -0
- xml2rfc/writers/doc.py +354 -0
- xml2rfc/writers/expand.py +51 -0
- xml2rfc/writers/expanded_xml.py +39 -0
- xml2rfc/writers/html.py +3110 -0
- xml2rfc/writers/legacy_html.py +860 -0
- xml2rfc/writers/nroff.py +345 -0
- xml2rfc/writers/paginated_txt.py +432 -0
- xml2rfc/writers/pdf.py +230 -0
- xml2rfc/writers/preptool.py +2428 -0
- xml2rfc/writers/raw_txt.py +1453 -0
- xml2rfc/writers/text.py +5000 -0
- xml2rfc/writers/unprep.py +187 -0
- xml2rfc/writers/v2v3.py +1150 -0
- xml2rfc-3.26.0.dist-info/LICENSE +29 -0
- xml2rfc-3.26.0.dist-info/METADATA +178 -0
- xml2rfc-3.26.0.dist-info/RECORD +71 -0
- xml2rfc-3.26.0.dist-info/WHEEL +5 -0
- xml2rfc-3.26.0.dist-info/entry_points.txt +2 -0
- xml2rfc-3.26.0.dist-info/top_level.txt +1 -0
xml2rfc/writers/base.py
ADDED
|
@@ -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
|