xml2rfc 3.26.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. xml2rfc/__init__.py +68 -0
  2. xml2rfc/boilerplate_id_guidelines.py +28 -0
  3. xml2rfc/boilerplate_rfc_7841.py +575 -0
  4. xml2rfc/boilerplate_tlp.py +258 -0
  5. xml2rfc/data/SVG-1.2-RFC.rnc +1666 -0
  6. xml2rfc/data/SVG-1.2-RFC.rng +7460 -0
  7. xml2rfc/data/Scripts.txt +2693 -0
  8. xml2rfc/data/metadata.js +218 -0
  9. xml2rfc/data/metadata.min.js +1 -0
  10. xml2rfc/data/reference.rnc +955 -0
  11. xml2rfc/data/reference.rng +2158 -0
  12. xml2rfc/data/referencegroup.rnc +16 -0
  13. xml2rfc/data/referencegroup.rng +2180 -0
  14. xml2rfc/data/rfc7991.rnc +955 -0
  15. xml2rfc/data/rfc7991.rng +2143 -0
  16. xml2rfc/data/v2.rnc +309 -0
  17. xml2rfc/data/v2.rng +720 -0
  18. xml2rfc/data/v3.rnc +1144 -0
  19. xml2rfc/data/v3.rng +2538 -0
  20. xml2rfc/data/v3compat.dtd +630 -0
  21. xml2rfc/data/v3compat.rnc +985 -0
  22. xml2rfc/data/xml2rfc-v3-emacs-nxml.rnc +1083 -0
  23. xml2rfc/data/xml2rfc.css +1171 -0
  24. xml2rfc/data/xml2rfc.js +7 -0
  25. xml2rfc/debug.py +221 -0
  26. xml2rfc/log.py +54 -0
  27. xml2rfc/parser.py +815 -0
  28. xml2rfc/run.py +801 -0
  29. xml2rfc/scripts.py +52 -0
  30. xml2rfc/strings.py +26 -0
  31. xml2rfc/templates/address_card.html +21 -0
  32. xml2rfc/templates/base.html +415 -0
  33. xml2rfc/templates/doc.xml +367 -0
  34. xml2rfc/templates/doc.yaml +2138 -0
  35. xml2rfc/templates/rfc2629-other.ent +61 -0
  36. xml2rfc/templates/rfc2629-xhtml.ent +170 -0
  37. xml2rfc/templates/rfc2629.dtd +328 -0
  38. xml2rfc/uniscripts/__init__.py +97 -0
  39. xml2rfc/uniscripts/unidata.py +306 -0
  40. xml2rfc/uniscripts/update/make_unidata.py +94 -0
  41. xml2rfc/util/__init__.py +7 -0
  42. xml2rfc/util/date.py +94 -0
  43. xml2rfc/util/fonts.py +53 -0
  44. xml2rfc/util/name.py +229 -0
  45. xml2rfc/util/num.py +119 -0
  46. xml2rfc/util/postal.py +285 -0
  47. xml2rfc/util/unicode.py +441 -0
  48. xml2rfc/utils.py +672 -0
  49. xml2rfc/walkpdf.py +113 -0
  50. xml2rfc/writers/__init__.py +25 -0
  51. xml2rfc/writers/base.py +2248 -0
  52. xml2rfc/writers/bib.py +43 -0
  53. xml2rfc/writers/doc.py +354 -0
  54. xml2rfc/writers/expand.py +51 -0
  55. xml2rfc/writers/expanded_xml.py +39 -0
  56. xml2rfc/writers/html.py +3110 -0
  57. xml2rfc/writers/legacy_html.py +860 -0
  58. xml2rfc/writers/nroff.py +345 -0
  59. xml2rfc/writers/paginated_txt.py +432 -0
  60. xml2rfc/writers/pdf.py +230 -0
  61. xml2rfc/writers/preptool.py +2428 -0
  62. xml2rfc/writers/raw_txt.py +1453 -0
  63. xml2rfc/writers/text.py +5000 -0
  64. xml2rfc/writers/unprep.py +187 -0
  65. xml2rfc/writers/v2v3.py +1150 -0
  66. xml2rfc-3.26.0.dist-info/LICENSE +29 -0
  67. xml2rfc-3.26.0.dist-info/METADATA +178 -0
  68. xml2rfc-3.26.0.dist-info/RECORD +71 -0
  69. xml2rfc-3.26.0.dist-info/WHEEL +5 -0
  70. xml2rfc-3.26.0.dist-info/entry_points.txt +2 -0
  71. xml2rfc-3.26.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1453 @@
1
+ # --------------------------------------------------
2
+ # Copyright The IETF Trust 2011, All Rights Reserved
3
+ # --------------------------------------------------
4
+
5
+ # Python libs
6
+ import textwrap
7
+ import lxml
8
+ import re
9
+
10
+ try:
11
+ from xml2rfc import debug
12
+ assert debug
13
+ except ImportError:
14
+ pass
15
+
16
+ # Local lib
17
+ from xml2rfc.writers.base import BaseRfcWriter, default_options
18
+ from xml2rfc.util.unicode import expand_unicode_element
19
+ import xml2rfc.utils
20
+
21
+
22
+ class RawTextRfcWriter(BaseRfcWriter):
23
+ """ Writes to a text file, unpaginated, no headers or footers.
24
+
25
+ The page width is controlled by the *width* parameter.
26
+ """
27
+
28
+ def __init__(self, xmlrfc, width=72, margin=3, quiet=None, options=default_options, date=None):
29
+ if not quiet is None:
30
+ options.quiet = quiet
31
+ BaseRfcWriter.__init__(self, xmlrfc, options=options, date=date)
32
+ # Document processing data
33
+ self.width = width # Page width
34
+ self.margin = margin # Page margin
35
+ self.buf = [] # Main buffer during processing
36
+ self.output = [] # Final buffer that gets written to disk
37
+ self.toc_marker = 0 # Line number in buffer to write toc to
38
+ self.iref_marker = 0 # Line number in buffer to write index to
39
+ self.list_counters = {} # Maintain counters for 'format' type lists
40
+ self.edit_counter = 0 # Counter for edit marks
41
+ # Set this to False to permit utf+8 output:
42
+ self.ascii = not options.utf8 # Enable ascii flag
43
+ self.cref_counter = 0 # Counter for cref anchors
44
+ self.cref_list = []
45
+
46
+ # Marks position of iref elements
47
+ self.iref_marks = {}
48
+
49
+ # Text lookups
50
+ self.inline_tags = ['xref', 'eref', 'iref', 'cref', 'spanx', 'u', ]
51
+
52
+ # Custom textwrapper object
53
+ self.wrapper = xml2rfc.utils.TextWrapper(width=self.width,
54
+ fix_sentence_endings=True)
55
+
56
+ def _length(self, text):
57
+ return len(text)
58
+
59
+ def _lb(self, buf=None, text='', num=1):
60
+ """ Write a blank line to the file, with optional filler text
61
+
62
+ Filler text is usually used by editing marks
63
+ """
64
+ if num > 0:
65
+ if buf is None:
66
+ buf = self.buf
67
+ buf.extend([text] * num)
68
+
69
+ def _vspace(self, num=0):
70
+ """ <vspace> line break wrapper to allow for overrides """
71
+ return self._lb(num=num)
72
+
73
+ def write_text(self, string, indent=0, sub_indent=0, bullet='',
74
+ align='left', leading_blankline=False, buf=None,
75
+ strip=True, edit=False, wrap_urls=True,
76
+ fix_sentence_endings=True, source_line=None,
77
+ fix_doublespace=True):
78
+ """ Writes a line or multiple lines of text to the buffer.
79
+
80
+ Several parameters are included here. All of the API calls
81
+ for text writers use this as the underlying method to write data
82
+ to the buffer, with the exception of write_raw() that handles
83
+ preserving of whitespace.
84
+ """
85
+ if buf is None:
86
+ buf = self.buf
87
+
88
+ if leading_blankline:
89
+ if edit and self.pis['editing'] == 'yes':
90
+ # Render an editing mark
91
+ self.edit_counter += 1
92
+ self._lb(buf=buf, text=str('<' + str(self.edit_counter) + '>'))
93
+ else:
94
+ self._lb(buf=buf)
95
+
96
+ # We can take advantage of textwrap's initial_indent by using a bullet
97
+ # parameter and treating it separately. We still need to indent it.
98
+ subsequent = ' ' * (indent + sub_indent)
99
+ if bullet:
100
+ initial = ' ' * indent
101
+ bullet_parts = self.wrapper.wrap(bullet, initial=initial,
102
+ fix_doublespace=False, drop_whitespace=False)
103
+ if len(bullet_parts) > 1:
104
+ buf.extend(bullet_parts[:-1])
105
+ initial = bullet_parts[-1]
106
+ if not sub_indent:
107
+ # Use bullet length for subsequent indents
108
+ subsequent = ' ' * len(initial)
109
+ else:
110
+ # No bullet, so combine indent and sub_indent
111
+ initial = subsequent
112
+
113
+ if string:
114
+ if strip:
115
+ # Strip initial whitespace
116
+ string = string.lstrip()
117
+ if wrap_urls:
118
+ par = self.wrapper.wrap(string, initial=initial, subsequent_indent=subsequent,
119
+ fix_sentence_endings=fix_sentence_endings,
120
+ fix_doublespace=fix_doublespace)
121
+ else:
122
+ par = self.wrapper.wrap(xml2rfc.utils.urlkeep(string), initial=initial,
123
+ subsequent_indent=subsequent,
124
+ fix_sentence_endings=fix_sentence_endings,
125
+ fix_doublespace=fix_doublespace)
126
+ if len(par) == 0 and bullet:
127
+ par = [ initial ]
128
+ if align == 'left':
129
+ buf.extend(par)
130
+ elif align == 'center':
131
+ for line in par:
132
+ margin = ' ' * indent
133
+ if line.startswith(margin):
134
+ line = line.rstrip()
135
+ centered = ' ' * ((self.width - len(line.rstrip()))//2)
136
+ centered += line
137
+ # centered = margin + line[len(margin):].center(self.width-indent-4).rstrip()
138
+ buf.append(centered)
139
+ else:
140
+ buf.append(line.center(self.width).rstrip())
141
+ elif align == 'right':
142
+ for line in par:
143
+ buf.append(line.rjust(self.width))
144
+ elif bullet:
145
+ # If the string is empty but a bullet was declared, just
146
+ # print the bullet
147
+ buf.append(initial)
148
+
149
+ def write_list(self, list, level=0, indent=3):
150
+ """ Writes a <list> element """
151
+ bullet = ' '
152
+ hangIndent = None
153
+ # style comes from the node if one exists
154
+ style = list.attrib.get('style', '')
155
+ if not style:
156
+ # otherwise look for the nearest list parent with a style and use it
157
+ for parent in list.iterancestors():
158
+ if parent.tag == 'list':
159
+ style = parent.attrib.get('style', '')
160
+ if style:
161
+ break
162
+ if not style:
163
+ style = 'empty'
164
+ listlength = len(list.findall('t'))
165
+ if style == 'hanging' or style.startswith('format'):
166
+ # Check for optional hangIndent
167
+ try:
168
+ hangIndent = list.attrib.get('hangIndent', None)
169
+ if hangIndent != None:
170
+ hangIndent = int(hangIndent)
171
+ except (ValueError, TypeError):
172
+ xml2rfc.log.error("hangIndent value '%s' is not an integer" % hangIndent)
173
+ hangIndent = 6
174
+ format_str = None
175
+ counter_index = None
176
+ if style.startswith('format'):
177
+ format_str = style.partition('format ')[2]
178
+ allowed_formats = ('%c', '%C', '%d', '%i', '%I', '%o', '%x', '%X')
179
+ if not any(map(lambda format: format in format_str, allowed_formats)):
180
+ xml2rfc.log.warn('Invalid format specified: %s '
181
+ '(Must be one of %s)' % (style,
182
+ ', '.join(allowed_formats)))
183
+ counter_index = list.attrib.get('counter', None)
184
+ if not counter_index:
185
+ counter_index = 'temp' + str(level)
186
+ self.list_counters[counter_index] = 0
187
+ elif counter_index not in self.list_counters:
188
+ # Initialize if we need to
189
+ self.list_counters[counter_index] = 0
190
+ t_count = 0
191
+ for element in list:
192
+ # Check for PI
193
+ if element.tag is lxml.etree.PI:
194
+ pidict = self.parse_pi(element)
195
+ if pidict and "needLines" in pidict:
196
+ self.needLines(pidict["needLines"])
197
+ elif element.tag == 't':
198
+ # Disable linebreak if subcompact=yes AND not first list element
199
+ leading_blankline = True
200
+ if t_count > 0 and element.pis['subcompact'] == 'yes':
201
+ leading_blankline = False
202
+ if style == 'symbols':
203
+ bullet = element.pis['text-list-symbols'][level % len(element.pis['text-list-symbols'])]
204
+ bullet += ' '
205
+ elif style == 'numbers':
206
+ bullet = self._format_counter("%d.", t_count+1, listlength)
207
+ elif style == 'letters':
208
+ letter_style = "%C." if (level % 2) else "%c."
209
+ bullet = self._format_counter(letter_style, t_count+1, listlength)
210
+ elif style == 'hanging':
211
+ bullet = element.attrib.get('hangText', '')
212
+ if hangIndent is None:
213
+ hangIndent = 3
214
+ if len(bullet) < hangIndent:
215
+ # Insert whitespace up to hangIndent
216
+ bullet = bullet.ljust(hangIndent)
217
+ else:
218
+ # Insert a single space
219
+ bullet += ' '
220
+ # Add an extra space in front of colon if colonspace enabled
221
+ if bullet.endswith(':') and \
222
+ element.pis['colonspace'] == 'yes':
223
+ bullet+= ' '
224
+ if element.text and len(bullet) > self.width//2:
225
+ # extra check of remaining space if the bullet is
226
+ # very long
227
+ first_word = self.wrapper._split(element.text)[0]
228
+ if len(first_word) > (self.width - len(bullet) - indent):
229
+ self.write_text('', bullet=bullet, indent=indent,
230
+ leading_blankline=leading_blankline)
231
+ self._vspace()
232
+ leading_blankline=False
233
+ indent = hangIndent
234
+ bullet = ''
235
+ elif style.startswith('format'):
236
+ self.list_counters[counter_index] += 1
237
+ count = self.list_counters[counter_index]
238
+ bullet = self._format_counter(format_str, count, listlength)
239
+ if hangIndent:
240
+ sub_indent = hangIndent
241
+ else:
242
+ sub_indent = len(bullet)
243
+ anchor = element.attrib.get('anchor')
244
+ if anchor and self.indexmode:
245
+ self._indexListParagraph(t_count+1, anchor)
246
+ self.write_t_rec(element, bullet=bullet, indent=indent, \
247
+ level=level + 1, \
248
+ sub_indent=sub_indent, leading_blankline=leading_blankline)
249
+ t_count += 1
250
+
251
+
252
+ def pre_write_toc(self):
253
+ return ['', 'Table of Contents', '']
254
+
255
+ def post_write_toc(self):
256
+ return []
257
+
258
+ def write_toc(self, paging=False):
259
+ """ Write table of contents to a temporary buffer and return """
260
+ if self.toc_marker < 1:
261
+ # Toc is either disabled, or the pointer was messed up
262
+ return ['']
263
+ tmpbuf = []
264
+ tmpbuf.extend(self.pre_write_toc())
265
+ # Retrieve toc from the index
266
+ tocindex = self._getTocIndex()
267
+ tocdepth = self.pis['tocdepth']
268
+ try:
269
+ tocdepth = int(tocdepth)
270
+ except ValueError:
271
+ xml2rfc.log.warn('Invalid tocdepth specified, must be integer:', \
272
+ tocdepth)
273
+ tocdepth = 3
274
+ indent_scale = 2
275
+ if self.pis['tocnarrow'] == 'no':
276
+ indent_scale = 3
277
+ for item in tocindex:
278
+ # Add decoration to counter if it exists, otherwise leave empty
279
+ if item.level <= tocdepth:
280
+ counter = ''
281
+ if item.counter:
282
+ counter = item.counter + '. '
283
+ # Extra space on single digit counters
284
+ if len(item.counter.rsplit('.')[-1]) == 1:
285
+ counter += ' '
286
+ # Get item depth based on its section 'level' attribute
287
+ depth = item.level - 1
288
+ if depth < 0 or self.pis['tocindent'] == 'no':
289
+ depth = 0
290
+ # Prepend appendix at first level
291
+ if item.level == 1 and item.appendix:
292
+ counter = "Appendix " + counter
293
+ bullet = ' ' * (depth * indent_scale) + counter
294
+ indent = 3
295
+ sub_indent = indent + len(bullet)
296
+ pagestr = '%4s' % item.page
297
+ lines = textwrap.wrap(bullet + (item.title.strip() if item.title else ""),
298
+ self.width - len(pagestr),
299
+ initial_indent=' ' * indent,
300
+ subsequent_indent=' ' * sub_indent)
301
+ if paging:
302
+ # Construct dots
303
+ dots = len(lines[-1]) % 2 and ' ' or ''
304
+ dots += ' .' * int((self.width - len(lines[-1]) - len(dots) + 1)//2)
305
+ lines[-1] += dots
306
+ # Insert page
307
+ lines[-1] = lines[-1][:0 - len(pagestr)] + pagestr
308
+ tmpbuf.extend(lines)
309
+ tmpbuf.extend(self.post_write_toc())
310
+ return tmpbuf
311
+
312
+ def pre_write_iref_index(self):
313
+ return ['', 'Index']
314
+
315
+ def write_iref_index(self):
316
+ """ Write iref index to a temporary buffer and return """
317
+ def pagelist(pages):
318
+ pages = list(set(pages))
319
+ pages.sort()
320
+ # find ranges
321
+ items = []
322
+ prev = None
323
+ for page in pages:
324
+ if prev and (page - prev == 1):
325
+ items[-1].append(page)
326
+ else:
327
+ items.append([page])
328
+ prev = page
329
+ pagelist = []
330
+ for item in items:
331
+ if len(item) == 1:
332
+ pagelist.append("%d"%item[0])
333
+ else:
334
+ pagelist.append("%d-%d"%(item[0], item[-1]))
335
+ return ", ".join(pagelist)
336
+
337
+ if self.iref_marker < 1:
338
+ # iref is either disabled, or the pointer was messed up
339
+ return ['']
340
+ tmpbuf = self.pre_write_iref_index()
341
+ # Sort iref items alphabetically, store by first letter
342
+ alpha_bucket = {}
343
+ keys = list(self._iref_index.keys())
344
+ keys.sort(key=str.upper)
345
+ for key in keys:
346
+ letter = key[0].upper()
347
+ if letter in alpha_bucket:
348
+ alpha_bucket[letter].append(key)
349
+ else:
350
+ alpha_bucket[letter] = [key]
351
+ for letter in sorted(alpha_bucket.keys()):
352
+ # Write letter
353
+ self.write_text(letter, indent=3, leading_blankline=True, buf=tmpbuf)
354
+ for item in sorted(alpha_bucket[letter]):
355
+ pages = self._iref_index[item].pages
356
+ # Write item
357
+ self.write_text(item + ' ' + pagelist(pages)
358
+ , indent=6, buf=tmpbuf, fix_doublespace=False)
359
+ subkeys = list(self._iref_index[item].subitems.keys())
360
+ subkeys.sort(key=str.upper)
361
+ for subitem in subkeys:
362
+ pages = set(self._iref_index[item].subitems[subitem].pages)
363
+ # Write subitem
364
+ self.write_text(subitem + ' ' + pagelist(pages)
365
+ , indent=9, buf=tmpbuf, fix_doublespace=False)
366
+ return tmpbuf
367
+
368
+ def _expand_xref(self, xref):
369
+ """ Returns the proper text representation of an xref element """
370
+ target = xref.attrib.get('target', '')
371
+ format = xref.attrib.get('format', self.defaults['xref_format'])
372
+ item = self._getItemByAnchor(target)
373
+ if not self.indexmode:
374
+ if not item:
375
+ xml2rfc.log.warn("Can't resolve xref target %s" % target)
376
+ else:
377
+ item.used = True
378
+ if not item:
379
+ target_text = '[' + target + ']'
380
+ elif format == 'none':
381
+ if xref.text:
382
+ return xref.text.rstrip()
383
+ return ''
384
+ elif format == 'counter':
385
+ target_text = item.counter
386
+ elif format == 'title':
387
+ target_text = item.title.strip()
388
+ else: #Default
389
+ target_text = item.autoName
390
+ target_text = re.sub("-", u"\u2011", target_text) # switch to non-breaking hyphens
391
+ target_text = re.sub(" ", u"\u00A0", target_text) # switch to non-breaking space
392
+ if xref.text:
393
+ if not target_text.startswith('['):
394
+ target_text = '(' + target_text + ')'
395
+ return xref.text.rstrip() + ' ' + target_text
396
+ else:
397
+ return target_text
398
+
399
+ def _expand_u(self, u):
400
+ try:
401
+ text = expand_unicode_element(u)
402
+ except (RuntimeError, ValueError) as exc:
403
+ text = ''
404
+ xml2rfc.log.error('%s'%exc)
405
+ return text
406
+
407
+ def write_ref_element(self, key, text, sub_indent, source_line=None):
408
+ """ Render a single reference element """
409
+ # Use an empty first line if key is too long
410
+ min_spacing = 2
411
+ if len(key) + min_spacing > sub_indent:
412
+ self.write_text(key, indent=3, leading_blankline=True, wrap_urls=False, fix_sentence_endings=False, source_line=source_line)
413
+ self.write_text(text, indent=3 + sub_indent, wrap_urls=False, fix_sentence_endings=False, source_line=source_line)
414
+ else:
415
+ # Fill space to sub_indent in the bullet
416
+ self.write_text(text, indent=3, bullet=key.ljust(sub_indent), \
417
+ sub_indent=sub_indent, leading_blankline=True, wrap_urls=False, fix_sentence_endings=False, source_line=source_line)
418
+
419
+ def _combine_inline_elements(self, elements):
420
+ """ Shared function for <t> and <c> elements
421
+
422
+ Aggregates all the rendered text of the following elements:
423
+ - xref
424
+ - eref
425
+ - iref
426
+ - cref
427
+ - spanx
428
+
429
+ Plus their tails. If an element is encountered that isn't one
430
+ of these (such as a list, figure, etc) then the function
431
+ returns.
432
+
433
+ This function takes a list of elements as its argument.
434
+
435
+ This function returns TWO arguments, the aggregated text, and
436
+ a list containing the rest of the elements that were not processed,
437
+ so that the calling function can deal with them.
438
+ """
439
+ line = ['']
440
+ for i, element in enumerate(elements):
441
+ if element.tag not in self.inline_tags:
442
+ # Not an inline element, exit
443
+ return ''.join(line), elements[i:]
444
+
445
+ if element.tag == 'xref':
446
+ line.append(self._expand_xref(element))
447
+ elif element.tag == 'eref':
448
+ if element.text and len(element.text)>0:
449
+ line.append(element.text.strip())
450
+ self.eref_count += 1
451
+ line.append(' [' + str(self.eref_count) + ']')
452
+ if self.indexmode:
453
+ self.eref_list.append([self.eref_count, element])
454
+ else:
455
+ line.append('<' + element.attrib['target'].strip() + '>')
456
+ elif element.tag == 'iref':
457
+ self._add_iref_to_index(element)
458
+ elif element.tag == 'cref':
459
+ if element.pis['comments'] == 'yes':
460
+ # Render if processing instruction is enabled
461
+ self.cref_counter += 1
462
+ anchor = element.attrib.get('anchor', None)
463
+ if anchor is None:
464
+ anchor = 'CREF' + str(self.cref_counter)
465
+ element.attrib['anchor'] = anchor
466
+ self._indexCref(self.cref_counter, anchor)
467
+ if element.pis['inline'] == 'yes':
468
+ if anchor:
469
+ anchor = anchor + ': '
470
+ source = element.attrib.get('source', "")
471
+ if source:
472
+ source = " --" + source
473
+ if element.text:
474
+ line.append('[[' + anchor + element.text + source + ']]')
475
+ else:
476
+ line.append('[' + anchor + ']')
477
+ self.cref_list.append(element)
478
+ elif element.tag == 'spanx':
479
+ style = element.attrib.get('style', 'emph')
480
+ edgechar = '_' # default to emph because the spanx element exists
481
+ if style == 'emph':
482
+ edgechar = '_'
483
+ elif style == 'strong':
484
+ edgechar = '*'
485
+ elif style == 'verb':
486
+ edgechar = '"'
487
+ text = ''
488
+ if element.text:
489
+ text = element.text
490
+ line.append(edgechar + text + edgechar)
491
+ elif element.tag == 'u':
492
+ line.append(self._expand_u(element))
493
+ else:
494
+ xml2rfc.log.error("Found unexpected inline element: <%s>" % element.tag)
495
+
496
+ # Add tail text before next element
497
+ if element.tail:
498
+ line.append(element.tail)
499
+
500
+ # Go to next sibling
501
+ element = element.getnext()
502
+
503
+ # Went through all elements, return text with an empty list
504
+ return ''.join(line), []
505
+
506
+ def _check_long_lines(self, buf_line, source_line):
507
+ long_lines = [ (num, line) for num, line in enumerate(self.buf[buf_line:]) if (len(line) > self.width) ]
508
+ for num, line in long_lines:
509
+ if source_line:
510
+ xml2rfc.log.warn("Output line (from source around line %s) is %s characters; longer than %s. Excess characters: '%s':\n '%s'\n"
511
+ % (source_line+num, len(line), self.width, line[self.width:], line))
512
+ else:
513
+ xml2rfc.log.warn("Output line (from source around line %s) is %s characters; longer than %s. Excess characters: '%s':\n '%s'\n"
514
+ % (buf_line+num, len(line), self.width, line[self.width:], line))
515
+
516
+ # ---------------------------------------------------------
517
+ # Base writer overrides
518
+ # ---------------------------------------------------------
519
+
520
+ def insert_toc(self):
521
+ """ Marks buffer position for post-writing table of contents """
522
+ self.toc_marker = len(self.buf)
523
+
524
+ def insert_iref_index(self):
525
+ """ Marks buffer position for post-writing index """
526
+ self.iref_marker = len(self.buf)
527
+
528
+ def write_raw(self, text, indent=3, align='left', blanklines=0, \
529
+ delimiter=None, leading_blankline=True, source_line=None):
530
+ """ Writes a raw stream of characters, preserving space and breaks """
531
+
532
+ if text:
533
+ if leading_blankline:
534
+ # Start with a newline
535
+ self._lb()
536
+ # Delimiter?
537
+ if delimiter:
538
+ self.buf.append(delimiter)
539
+ # Additional blank lines?
540
+ self.buf.extend([''] * blanklines)
541
+ start_line = len(self.buf)
542
+ # Format the input
543
+ if "\t" in text:
544
+ xml2rfc.log.warn("Text %scontains tab characters. These will be expanded, assuming a tab-size of 8." %
545
+ (("around line %s "%source_line) if source_line else ""))
546
+ lines = [line.rstrip() for line in text.expandtabs().split('\n')]
547
+ # Outdent if it helps anything
548
+ longest_line = max(len(line.rstrip()) for line in lines)
549
+ if (longest_line > self.width-indent):
550
+ new_indent = max(self.width - longest_line, 0)
551
+ if not self.indexmode:
552
+ xml2rfc.log.warn('artwork outdented %s characters to avoid overrunning right margin around input line %s)' % (indent - new_indent, source_line if source_line else 0))
553
+ indent = new_indent
554
+
555
+ # Trim first and last lines if they are blank, whitespace is handled
556
+ # by the `blanklines` and `delimiter` arguments
557
+ if len(lines) > 1:
558
+ if lines[0] == '':
559
+ lines.pop(0)
560
+ if lines[-1] == '':
561
+ lines.pop(-1)
562
+ if align == 'center':
563
+ # Find the longest line, and use that as a fixed center.
564
+ center_indent = indent + ((self.width - indent - longest_line) // 2)
565
+ indent_str = center_indent > indent and ' ' * center_indent or \
566
+ ' ' * indent
567
+ for line in lines:
568
+ self.buf.append(indent_str + line.rstrip())
569
+ elif align == 'right':
570
+ indent_str = ' ' * (self.width - longest_line)
571
+ for line in lines:
572
+ self.buf.append(indent_str + line.rstrip())
573
+ else: # align == left
574
+ indent_str = ' ' * indent
575
+ for line in lines:
576
+ self.buf.append(indent_str + line.rstrip())
577
+ # Additional blank lines?
578
+ self.buf.extend([''] * blanklines)
579
+ # Delimiter?
580
+ if delimiter:
581
+ self.buf.append(delimiter)
582
+ if not self.indexmode:
583
+ self._check_long_lines(start_line, source_line)
584
+
585
+ def write_label(self, text, type='figure', source_line=None):
586
+ """ Writes a centered label """
587
+ self.write_text(text, indent=3, align='center', leading_blankline=True, source_line=source_line)
588
+
589
+ def write_title(self, title, docName=None, source_line=None):
590
+ """ Write the document title and (optional) name """
591
+ self.write_text(title, leading_blankline=True, align='center', source_line=source_line)
592
+ if docName and not self.rfcnumber:
593
+ self.write_text(docName, align='center')
594
+
595
+ def write_heading(self, text, bullet='', autoAnchor=None, anchor=None, \
596
+ level=1):
597
+ """ Write a generic header """
598
+ if bullet:
599
+ bullet += ' '
600
+ self.write_text(text, bullet=bullet, indent=0, leading_blankline=True)
601
+
602
+ def write_paragraph(self, text, align='left', autoAnchor=None):
603
+ """ Write a generic paragraph of text. Used for boilerplate. """
604
+ text = xml2rfc.utils.urlkeep(text)
605
+ self.write_text(text, indent=3, align=align, leading_blankline=True)
606
+
607
+ def write_t_rec(self, t, indent=3, sub_indent=0, bullet='',
608
+ autoAnchor=None, align='left', level=0, leading_blankline=True):
609
+ """ Recursively writes a <t> element """
610
+ # Grab any initial text in <t>
611
+ current_text = t.text or ''
612
+ if bullet and current_text == '':
613
+ current_text = ' '
614
+
615
+ # Render child elements
616
+ remainder = t.getchildren()
617
+ while len(remainder) > 0 or current_text:
618
+ # Process any inline elements
619
+ inline_text, remainder = self._combine_inline_elements(remainder)
620
+ current_text += inline_text
621
+ if (current_text and not current_text.isspace()) or bullet:
622
+ # Attempt to write a paragraph of inline text
623
+ self.write_text(current_text, indent=indent,
624
+ leading_blankline=leading_blankline,
625
+ sub_indent=sub_indent, bullet=bullet,
626
+ edit=True, align=align, source_line=t.sourceline)
627
+ # Clear text
628
+ current_text = ''
629
+
630
+ # Handle paragraph-based elements (list, figure, vspace)
631
+ if len(remainder) > 0:
632
+ # Get front element
633
+ element = remainder.pop(0)
634
+
635
+ if element.tag == 'list':
636
+ if sub_indent > 0:
637
+ new_indent = sub_indent + indent
638
+ else:
639
+ new_indent = len(bullet) + indent
640
+ # Call sibling function to construct list
641
+ self.write_list(element, indent=new_indent, level=level)
642
+ # Auto-break for tail paragraph
643
+ leading_blankline = True
644
+ bullet = ''
645
+
646
+ elif element.tag == 'figure':
647
+ self.write_figure(element)
648
+ # Auto-break for tail paragraph
649
+ leading_blankline = True
650
+ bullet = ''
651
+
652
+ elif element.tag == 'vspace':
653
+ # Insert `blankLines` blank lines into document
654
+ self._vspace(num=int(element.attrib.get('blankLines',
655
+ self.defaults['vspace_blanklines'])))
656
+ # Don't auto-break for tail paragraph
657
+ leading_blankline = False
658
+ # Keep indentation
659
+ bullet = ' ' * sub_indent
660
+
661
+ # Set tail of element as input text of next paragraph
662
+ if element.tail:
663
+ current_text = element.tail
664
+
665
+
666
+ def write_top(self, left_header, right_header):
667
+ """ Combines left and right lists to write a document heading """
668
+ # Begin with a blank line on the first page. We'll add additional
669
+ # blank lines at the top later, but those won't be counted as part
670
+ # of the page linecount.
671
+ #self._lb(num=3)
672
+ self._lb()
673
+ heading = []
674
+ for i in range(max(len(left_header), len(right_header))):
675
+ if i < len(left_header):
676
+ left = left_header[i]
677
+ else:
678
+ left = ''
679
+ if i < len(right_header):
680
+ right = right_header[i]
681
+ else:
682
+ right = ''
683
+ heading.append(xml2rfc.utils.justify_inline(left, '', right, \
684
+ self.width))
685
+ self.write_raw('\n'.join(heading), align='left', indent=0,
686
+ leading_blankline=False, source_line=None)
687
+ # Extra blank line underneath top block
688
+ self._lb()
689
+
690
+ def write_address_card(self, author):
691
+ """ Writes a simple address card with no line breaks """
692
+ lines = []
693
+ if 'role' in author.attrib:
694
+ lines.append("%s (%s)" % (author.attrib.get('fullname', ''),
695
+ author.attrib.get('role', '')))
696
+ else:
697
+ lines.append(author.attrib.get('fullname', ''))
698
+ organization = author.find('organization')
699
+ if organization is not None and organization.text:
700
+ lines.append(organization.text.strip())
701
+ address = author.find('address')
702
+ if address is not None:
703
+ postal = address.find('postal')
704
+ if postal is not None:
705
+ for street in postal.findall('street'):
706
+ if street.text:
707
+ lines.append(street.text)
708
+ cityline = []
709
+ city = postal.find('city')
710
+ if city is not None and city.text:
711
+ cityline.append(city.text)
712
+ region = postal.find('region')
713
+ if region is not None and region.text:
714
+ if len(cityline) > 0: cityline.append(', ');
715
+ cityline.append(region.text)
716
+ code = postal.find('code')
717
+ if code is not None and code.text:
718
+ if len(cityline) > 0: cityline.append(' ');
719
+ cityline.append(code.text)
720
+ if len(cityline) > 0:
721
+ lines.append(''.join(cityline))
722
+ country = postal.find('country')
723
+ if country is not None and country.text:
724
+ lines.append(country.text)
725
+ lines.append('')
726
+ phone = address.find('phone')
727
+ if phone is not None and phone.text:
728
+ lines.append('Phone: ' + phone.text)
729
+ facsimile = address.find('facsimile')
730
+ if facsimile is not None and facsimile.text:
731
+ lines.append('Fax: ' + facsimile.text)
732
+ email = address.find('email')
733
+ if email is not None and email.text:
734
+ label = self.pis['rfcedstyle'] == 'yes' and 'EMail' or 'Email'
735
+ lines.append('%s: %s' % (label, email.text))
736
+ uri = address.find('uri')
737
+ if uri is not None and uri.text:
738
+ lines.append('URI: ' + uri.text)
739
+ self.write_raw('\n'.join(lines), indent=self.margin)
740
+ self._lb()
741
+
742
+ def write_reference_list(self, list):
743
+ """ Writes a formatted list of <reference> elements """
744
+ refdict = {}
745
+ annotationdict = {}
746
+ refkeys = []
747
+ refsource = {}
748
+ # [surname, initial.,] "title", (STD), (BCP), (RFC), (Month) Year.
749
+ i = 0
750
+ for i, ref in enumerate(list.findall('reference')):
751
+ refstring = []
752
+ authors = ref.findall('front/author')
753
+ author_string = self._format_author_string(authors)
754
+ refstring.append(author_string)
755
+ if len(author_string):
756
+ refstring.append(', ')
757
+ title = ref.find('front/title')
758
+ if title is not None and title.text:
759
+ if ref.attrib.get("quote-title", "true") == "true": # attribute default value: yes
760
+ refstring.append('"' + title.text.strip() + '"')
761
+ else:
762
+ refstring.append(title.text.strip())
763
+ else:
764
+ xml2rfc.log.warn('No title specified in reference',
765
+ ref.attrib.get('anchor', ''))
766
+ for seriesInfo in ref.findall('seriesInfo'):
767
+ if seriesInfo.attrib['name'] == "Internet-Draft":
768
+ refstring.append(', '+seriesInfo.attrib['value'] + ' (work in progress)')
769
+ else:
770
+ refstring.append(', '+seriesInfo.attrib['name'] + u'\u00A0' +
771
+ seriesInfo.attrib['value'].replace('/', '/' + u'\uE060'))
772
+ date = ref.find('front/date')
773
+ if date is not None:
774
+ month = date.attrib.get('month', '')
775
+ year = date.attrib.get('year', '')
776
+ if month or year:
777
+ if month:
778
+ month += ' '
779
+ refstring.append(', '+month + year)
780
+ # Target?
781
+ target = ref.attrib.get('target')
782
+ if target:
783
+ refstring.append(', <' + target + '>')
784
+ refstring.append('.')
785
+ annotation = ref.find('annotation')
786
+ # Use anchor or num depending on PI
787
+ if self.pis['symrefs'] == 'yes':
788
+ key = ref.attrib.get('anchor', str(i + self.ref_start))
789
+ else:
790
+ key = str(i + self.ref_start)
791
+ refdict[key] = ''.join(refstring)
792
+ refsource[key] = ref.sourceline
793
+ refkeys.append(key)
794
+ # Add annotation if it exists to a separate dict
795
+ if annotation is not None and annotation.text:
796
+ # Render annotation as a separate paragraph
797
+ remainder = annotation.getchildren()
798
+ annotationdict[key] = annotation.text
799
+ if len(remainder) > 0:
800
+ inline_txt, remainder = self._combine_inline_elements(remainder)
801
+ annotationdict[key] += inline_txt
802
+ self.ref_start += i + 1
803
+ # Don't sort if we're doing numbered refs; they are already in
804
+ # numeric order, and if we sort, they will be sorted alphabetically,
805
+ # rather than numerically ... ( i.e., [10], [11], ... [19], [1], ... )
806
+ if self.pis['sortrefs'] == 'yes' and self.pis['symrefs'] == 'yes' :
807
+ refkeys.sort(key=str.lower)
808
+ # Hard coded indentation amount
809
+ refindent = 11
810
+ for key in refkeys:
811
+ reflabel = '['+key+']'
812
+ self.write_ref_element(reflabel, refdict[key], refindent, source_line=refsource[key])
813
+ # Render annotation as a separate paragraph
814
+ if key in annotationdict:
815
+ self.write_text(annotationdict[key], indent=refindent + 3,
816
+ leading_blankline=True, source_line=refsource[key])
817
+
818
+ def write_erefs(self, refs_counter, refs_subsection):
819
+ bullet=str(refs_counter) + '.' + str(refs_subsection)
820
+ self.write_heading("URIs", bullet=bullet +'.',
821
+ level = 2, autoAnchor='rfc.references.' + str(refs_subsection))
822
+ for i, element in self.eref_list:
823
+ self.write_text(element.attrib['target'], bullet='['+str(i)+'] ',
824
+ leading_blankline=True, indent=3)
825
+
826
+ def write_crefs(self):
827
+ """ If we have any of these because the are not inline, then
828
+ write them out here
829
+ """
830
+ if self.cref_list:
831
+ self.write_heading('Editorial Comments')
832
+ for cref in self.cref_list:
833
+ body = cref.text
834
+ source = cref.attrib.get('source', None)
835
+ if source:
836
+ body = source + ': ' + body
837
+ self.write_text(body, bullet='['+cref.attrib['anchor']+'] ',
838
+ leading_blankline=True)
839
+
840
+ def draw_table(self, table, table_num=None):
841
+ # First construct a 2d matrix from the table
842
+ matrix = []
843
+ matrix.append([])
844
+ row = 0
845
+ column_aligns = []
846
+ ttcol_width_attrs = []
847
+ style = table.attrib.get('style', self.defaults['table_style'])
848
+
849
+ # Extract information from the ttcol elements about each column
850
+ for ttcol in table.findall('ttcol'):
851
+ column_aligns.append(ttcol.attrib.get('align', self.defaults['ttcol_align']))
852
+ ttcol_width_attrs.append(ttcol.attrib.get('width', ''))
853
+
854
+ text = ttcol.text or ''
855
+ if len(ttcol) > 0:
856
+ # <c> has children, render their text and add to line
857
+ inline_text, null = \
858
+ self._combine_inline_elements(ttcol.getchildren())
859
+ text += inline_text
860
+ matrix[row].append(text)
861
+
862
+ num_columns = len(matrix[0])
863
+
864
+ # Get the text for each cell into the matrix
865
+ # Insert appropriate blank lines for filler at this time.
866
+ for i, cell in enumerate(table.findall('c')):
867
+ if i % num_columns == 0:
868
+ # Insert blank row if PI 'compact' is 'no'
869
+ if table.pis["compact"] == "no":
870
+ if (style == 'none' and row == 0) or (style in ['headers', 'full'] and row != 0):
871
+ row += 1
872
+ matrix.append(['']*num_columns)
873
+ pass
874
+ row += 1
875
+ matrix.append([])
876
+
877
+
878
+ text = cell.text or ''
879
+ if len(cell) > 0:
880
+ # <c> has children, render their text and add to line
881
+ inline_text, null = \
882
+ self._combine_inline_elements(cell.getchildren())
883
+ text += inline_text
884
+ matrix[row].append(text)
885
+
886
+ # Get table style and determine maximum width of table
887
+ if style in ['none', 'headers']:
888
+ table_max_chars = self.width - 3 - num_columns + 1
889
+ else:
890
+ table_max_chars = self.width - 3 - 3 * num_columns - 1 # indent+border
891
+
892
+ if table_max_chars < 0:
893
+ xml2rfc.log.error("too many <ttcol>s to even build <texttable> frame")
894
+
895
+ # Find the longest line and longest word in each column
896
+ longest_lines = [0] * num_columns
897
+ longest_words = [0] * num_columns
898
+ for col in range(num_columns):
899
+ for row in matrix:
900
+ if col < len(row) and len(row[col]) > 0: # Column exists
901
+ # Longest line
902
+ if self._length(row[col]) > longest_lines[col]:
903
+ longest_lines[col] = self._length(row[col])
904
+ # Longest word
905
+ if row[col].strip():
906
+ word = max(xml2rfc.utils.ascii_split(row[col]), key=self._length)
907
+ if self._length(word) > longest_words[col]:
908
+ longest_words[col] = self._length(word)
909
+
910
+ min_width = sum(longest_words)
911
+
912
+ # Assume one character per non-empty column - are there too many columns?
913
+ inf = []
914
+ for col in longest_words:
915
+ col = (col != 0)
916
+ inf.append(col)
917
+ if table_max_chars < sum(inf):
918
+ xml2rfc.log.error("too many non-empty columns in <texttable> to fit in page width")
919
+
920
+ # Translate width specs given available space
921
+ cols = []
922
+ rel_total = 0
923
+ ttcol_width_unspec = []
924
+
925
+ for col in ttcol_width_attrs:
926
+ # what to do with the unspecified ones is touchy.
927
+ u = 0
928
+ if not col:
929
+ x = longest_words[len(cols)]
930
+ u = 1
931
+ elif col == '*':
932
+ x = -1
933
+ rel_total += x
934
+ else:
935
+ m = re.match("^0*([0-9]+)(|em|[%*])$", col)
936
+ if not m:
937
+ xml2rfc.log.warn("Width attribute not in correct format: %s" & col)
938
+ x = longest_words[len(cols)]
939
+ u = 1
940
+ if m.group(2) == 'em':
941
+ x = int(m.group(1));
942
+ elif m.group(2) == '%':
943
+ x = (int(m.group(1)) * table_max_chars + 99)//100
944
+ elif m.group(2) == '*':
945
+ x = -int(m.group(1))
946
+ rel_total += x
947
+
948
+ cols.append(x)
949
+ ttcol_width_unspec.append(u)
950
+
951
+ ttcol_width_attrs = cols
952
+
953
+ # Compute final cell widths ( and their totals )
954
+
955
+ width = table_max_chars
956
+ if table_max_chars == sum(longest_words):
957
+ # Use the minimum widths if that is all we have
958
+ ttcol_widths = longest_words
959
+ elif table_max_chars == sum(inf):
960
+ # ??????????????
961
+ ttcol_widths = inf
962
+ elif table_max_chars < sum(longest_words):
963
+ if not self.indexmode:
964
+ xml2rfc.log.warn("so many <ttcol>s in <texttable> that some words need to be split near line %s" % table.sourceline)
965
+
966
+ excess = sum(longest_words) - table_max_chars
967
+
968
+ # First, try the mins that are greater than what they requested.
969
+
970
+ rel_total = -rel_total
971
+ tol = []
972
+ fnum = 1000 if (rel_total > 0) else 0
973
+ fden = 1
974
+ for s, n in zip(ttcol_width_attrs, longest_words):
975
+ t = 0
976
+ if s >= 0:
977
+ s += (s == 0)
978
+ y = n - s
979
+ if y > 0:
980
+ t = y
981
+ elif fnum > 0:
982
+ # Prepare in case we need to try the relative widths.
983
+ n -= 1
984
+ s = -s
985
+ # We keep the smallest acceptable factor to all as a mixed number.
986
+ if n <= 0:
987
+ fnum = 0
988
+ elif n * fden < s * fnum:
989
+ fnum = n
990
+ fden = s
991
+
992
+ tol.append(t)
993
+
994
+ excess, ttcol_widths = self.shave_cols_excess(longest_words, excess, tol, sum(tol))
995
+
996
+ # Second, try the unspecified widths.
997
+ if excess > 0:
998
+ tol = []
999
+ for w,u in zip(ttcol_widths, ttcol_width_unspec):
1000
+ t = (w-1) if (u and w > 0) else 0
1001
+ tol.append(t)
1002
+
1003
+ excess, ttcol_widths = self.shave_cols_excess(ttcol_widths, excess, tol, sum(tol))
1004
+
1005
+ # Third, try the relative widths (to shrink, not expand).
1006
+ if excess > 0 and fnum > 0:
1007
+ if excess * fden < rel_total * fnum:
1008
+ fnum = excess
1009
+ fden = rel_total
1010
+
1011
+ tol = []
1012
+ for s in ttcol_width_attrs:
1013
+ t = ((fnum * -(s)) // fden) if (s < 0) else 0
1014
+ tol.append(t)
1015
+
1016
+ excess, ttcol_widths = self.shave_cols_excess(ttcol_widths, excess, tol, sum(tol))
1017
+
1018
+ # Finally, try anything that's left, leaving as little as one column.
1019
+ # Start with largest.
1020
+
1021
+ p = 1
1022
+ for w in ttcol_widths:
1023
+ if w > p:
1024
+ p = w
1025
+
1026
+ while excess > 0:
1027
+ if p == 1:
1028
+ xml2rfc.log.error("bug in reducing <texttable> column widths")
1029
+
1030
+ # Find second largest.
1031
+ q = 1
1032
+ for w in ttcol_widths:
1033
+ if w > q and w < p:
1034
+ q = w
1035
+
1036
+ tol = []
1037
+ for w in ttcol_widths:
1038
+ t = (w - q) if (w > q) else 0
1039
+ tol.append(t)
1040
+
1041
+ excess, ttcol_widths = self.shave_cols_excess(ttcol_widths, excess, tol, sum(tol))
1042
+
1043
+ p = q
1044
+
1045
+ else:
1046
+ # There is room beyond the minimum space
1047
+ # First, try and give everyone what they want or need
1048
+ # longest_words + ttcol_width_attrs + longest_lines(all)
1049
+ ttcol_widths = []
1050
+ cols_width = 0
1051
+
1052
+ # longest_words + ttcol_width_attrs
1053
+ mnsa = []
1054
+
1055
+ # longest_words + ttcol_width_attrs + longest_lines(rel)
1056
+ mnsb = []
1057
+
1058
+ # longest_words + ttcol_width_attrs + longest_lines(rel + ttcol_width_unspec)
1059
+ mnsc = []
1060
+
1061
+ for m, n, s, u in zip(longest_lines, longest_words, ttcol_width_attrs, ttcol_width_unspec):
1062
+ t = n
1063
+ if (t < s):
1064
+ t = s
1065
+ mnsa.append( t )
1066
+
1067
+ if (s < 0):
1068
+ t = m
1069
+ mnsb.append( t )
1070
+
1071
+ if u:
1072
+ t = m
1073
+ mnsc.append(t)
1074
+
1075
+ if (t < m):
1076
+ t = m
1077
+ ttcol_widths.append(t)
1078
+
1079
+ cols_width = sum(ttcol_widths)
1080
+
1081
+ # In decreasing order by width
1082
+ if table_max_chars >= cols_width:
1083
+ width = cols_width
1084
+
1085
+ elif table_max_chars > sum(mnsc):
1086
+ # partially expand columns with greater maximal width from mnsc
1087
+ target = table_max_chars - sum(mnsc)
1088
+ target, ttcol_widths = self.expand_cols_by_height(mnsc, target, ttcol_widths)
1089
+
1090
+ elif table_max_chars == sum(mnsc):
1091
+ ttcol_widths = mnsc
1092
+
1093
+ elif table_max_chars > sum(mnsb):
1094
+ # Partially expand columns with unspecified width from mnsb
1095
+ target = table_max_chars - sum(mnsb)
1096
+ target, ttcol_widths = self.expand_cols_by_height(mnsb, target, ttcol_widths)
1097
+
1098
+ elif table_max_chars == sum(mnsb):
1099
+ ttcol_widths = mnsb
1100
+
1101
+ elif table_max_chars > sum(mnsa):
1102
+ #Partially expand columns with relative width from mnsa.
1103
+
1104
+ ttcol_widths = mnsa
1105
+ target = table_max_chars - sum(mnsa)
1106
+ while target > 0:
1107
+ rel = []
1108
+ num_rel = 0
1109
+ rel_total = 0
1110
+ fnum = 1000
1111
+ fden = 1
1112
+ for m,s,w in zip(longest_lines, ttcol_width_attrs, ttcol_widths):
1113
+ x = 0
1114
+ if s < 0:
1115
+ d = m - w
1116
+ if d > 0:
1117
+ x = -s
1118
+ if d * fden < x * fnum:
1119
+ fnum = d
1120
+ fden = x
1121
+ num_rel += 1;
1122
+ rel_total += x
1123
+ rel.append(x)
1124
+
1125
+ if target * fden < rel_total * fnum:
1126
+ fnum = target
1127
+ fden = rel_total
1128
+
1129
+ plus_width = 0
1130
+ if (num_rel > 0) and (fnum > 0):
1131
+ kols = []
1132
+ for r,w in zip(rel, ttcol_widths):
1133
+ p = r*fnum//fden
1134
+ kols.append(w + p)
1135
+ plus_width += p
1136
+
1137
+ if plus_width > 0:
1138
+ ttcol_widths = kols
1139
+ target += -plus_width
1140
+
1141
+ elif num_rel > 0:
1142
+ dd = target
1143
+ kols = []
1144
+ for r,w in zip(rel, ttcol_widths):
1145
+ if r > 0:
1146
+ # prefer expanding the rightmost ones
1147
+ if dd >= num_rel:
1148
+ w += 1
1149
+ target -= 1
1150
+ dd += 1
1151
+ kols.append(w)
1152
+ ttcol_widths = kols
1153
+
1154
+ else:
1155
+ xml2rfc.log.error("bug in expanding <texttable> columns with relative widths")
1156
+
1157
+ elif table_max_chars == sum(mnsa):
1158
+ ttcol_widths = mnsa
1159
+ else:
1160
+ # Partially expand comns with greater specified width from longest_words
1161
+ target = table_max_chars - min_width
1162
+ plus = []
1163
+ for a,b in zip(mnsa, longest_words):
1164
+ plus.append(a-b)
1165
+ target, ttcol_widths = self.expand_cols(longest_words, target, plus)
1166
+
1167
+ # Ensure we don't have any zero-width columns; it breaks textwrap
1168
+ ttcol_widths = [ k or 1 for k in ttcol_widths ]
1169
+ # Now construct the cells using textwrap against ttcol_widths
1170
+ cell_lines = [
1171
+ [ textwrap.wrap(cell, ttcol_widths[j]) or [''] for j, cell in enumerate(matrix[i]) ]
1172
+ for i in range(0, len(matrix))
1173
+ ]
1174
+
1175
+ output = []
1176
+ # Create the border
1177
+ if style == 'none':
1178
+ pass
1179
+ elif style == 'headers':
1180
+ borderstring = []
1181
+ for i in range(num_columns):
1182
+ borderstring.append('-' * ttcol_widths[i])
1183
+ borderstring.append(' ')
1184
+ else:
1185
+ borderstring = ['+']
1186
+ for i in range(num_columns):
1187
+ borderstring.append('-' * (ttcol_widths[i] + 2))
1188
+ borderstring.append('+')
1189
+ output.append(''.join(borderstring))
1190
+
1191
+ # Draw the table
1192
+ for i, cell_line in enumerate(cell_lines):
1193
+ if i==0 and cell_line == [['']]*num_columns:
1194
+ if style in ['headers']:
1195
+ # This is the header row, append the header decoration
1196
+ output.append(''.join(borderstring))
1197
+ continue
1198
+ # produce as many output rows as the number of wrapped
1199
+ # text lines in the cell with most lines, but at least 1
1200
+ for row in range(0, max(map(len, cell_line))):
1201
+ if style == 'headers' or style == 'none':
1202
+ line = ['']
1203
+ else:
1204
+ line = ['|']
1205
+ for col, cell in enumerate(cell_line):
1206
+ align = column_aligns[col]
1207
+ width = ttcol_widths[col]
1208
+ if row < len(cell):
1209
+ # width = width + len(cell[row]) - self._length(cell[row])
1210
+ if align == 'center':
1211
+ text = cell[row].center(width)
1212
+ elif align == 'right':
1213
+ text = cell[row].rjust(width)
1214
+ else: # align == left
1215
+ text = cell[row].ljust(width)
1216
+ if style == 'headers' or style == 'none':
1217
+ line.append(text)
1218
+ line.append(' ')
1219
+ else:
1220
+ line.append(' ')
1221
+ line.append(text)
1222
+ line.append(' |')
1223
+ else:
1224
+ if style == 'headers' or style == 'none':
1225
+ line.append(' ' * (ttcol_widths[col] + 1))
1226
+ else:
1227
+ line.append(' ' * (ttcol_widths[col] + 2) + '|')
1228
+ output.append(''.join(line))
1229
+ if style in ['headers', 'full']:
1230
+ if i == 0:
1231
+ # This is the header row, append the header decoration
1232
+ output.append(''.join(borderstring))
1233
+ if style in ['full']:
1234
+ if i == len(cell_lines)-1:
1235
+ output.append(''.join(borderstring))
1236
+ if style in ['all']:
1237
+ output.append(''.join(borderstring))
1238
+
1239
+
1240
+ # Finally, write the table to the buffer with proper alignment
1241
+ align = table.attrib.get('align', self.defaults['table_align'])
1242
+ self.write_raw('\n'.join(output), align=align, indent=self.margin,
1243
+ source_line=table.sourceline)
1244
+
1245
+
1246
+ def expand_cols_by_height(self, cols, target, bound):
1247
+ # cols - current column width
1248
+ # target - expand by this amount
1249
+ # bound - this is the lower boundary of the column width
1250
+
1251
+ while target > 0:
1252
+ # Compute the current width and boundary width for those where current is less than boundary
1253
+ m_width = 0
1254
+ w_width = 0
1255
+ for m,w in zip(bound, cols):
1256
+ if (m > w):
1257
+ # Only cover those columns below the bound.
1258
+ m_width += m
1259
+ w_width += w
1260
+
1261
+ # This mixed number is the average height we could achieve.
1262
+
1263
+ fnum = m_width
1264
+ fden = w_width + target
1265
+ plus_num = 0
1266
+ plus = []
1267
+ plus_width = 0
1268
+ kols = []
1269
+ for m,w in zip(bound, cols):
1270
+ p = 0
1271
+ k = w
1272
+ if m > w:
1273
+ x = m * fden
1274
+ y = w * fnum
1275
+ # Seek columns of above average height.
1276
+ if x >= y:
1277
+ # Nearly reduce height to average by augmenting width
1278
+ p = 1
1279
+ k = x // (fnum + fden - 1)
1280
+ if k < w:
1281
+ # but no smaller than before.
1282
+ k = w
1283
+ if k > w + target:
1284
+ k = w + target
1285
+ q = k - w
1286
+ plus_num += 1
1287
+ plus_width += q
1288
+
1289
+ plus.append(p)
1290
+ kols.append(k)
1291
+
1292
+ if plus_width > 0:
1293
+ cols = kols
1294
+ target += (-plus_width)
1295
+
1296
+ elif plus_num > 0:
1297
+ # No progress due to integer division. Make some.
1298
+ dd = target
1299
+ cols = []
1300
+ for p,w in zip(plus, kols):
1301
+ if p > 0:
1302
+ # Prefer expanding the rightmost ones.
1303
+ if dd >= plus_num:
1304
+ w += 1
1305
+ target -= 1
1306
+ dd += 1
1307
+ cols.append(w)
1308
+
1309
+ else:
1310
+ # Should not happen, but just in case we were given a bad target
1311
+ cols = cols
1312
+ break;
1313
+
1314
+ return [target, cols]
1315
+
1316
+ def shave_cols_excess (self, cols, excess, give, give_width=-1):
1317
+ if give_width < 0:
1318
+ give_width =sum(give)
1319
+
1320
+ if excess <= 0 or give_width <= 0:
1321
+ # Nothing to do.
1322
+ pass
1323
+ elif excess >= give_width:
1324
+ # Take everything; it may still not be enough.
1325
+ plus = []
1326
+ for a,b in zip(cols, give):
1327
+ plus.append(a-b)
1328
+ cols = plus
1329
+ excess += -(give_width)
1330
+ else:
1331
+ # Take only what we need, in relative proportion.
1332
+ dd = 0
1333
+ kols = []
1334
+ for c, g in zip(cols, give):
1335
+ if g > 0:
1336
+ d =(g * excess) // give_width
1337
+ c += -(d)
1338
+ dd += d
1339
+ kols.append( c )
1340
+ excess += -(dd)
1341
+
1342
+ # Do the ones we missed because of integer division.
1343
+ if excess > 0:
1344
+ cols = []
1345
+ for k, g in zip(kols, give):
1346
+ # Prefer shaving the leftmost ones.
1347
+ if excess > 0 and g > 0:
1348
+ k += -1
1349
+ excess += -1
1350
+ cols.append( k )
1351
+ else:
1352
+ cols = kols
1353
+
1354
+ return [excess, cols]
1355
+
1356
+ def expand_cols(self, cols, target, plus, plus_width=-1):
1357
+ if plus_width < 0:
1358
+ plus_width =sum(plus)
1359
+
1360
+ if target <= 0 or plus_width <= 0:
1361
+ # Nothing to do.
1362
+ pass
1363
+ elif target >= plus_width:
1364
+ # Take everything; it may still not be enough.
1365
+ tmp = []
1366
+ for a,b in zip(cols, plus):
1367
+ tmp.append(a+b)
1368
+ cols = tmp
1369
+ target += -(plus_width)
1370
+ else:
1371
+ # Take only what we need, in relative proportion.
1372
+ dd = 0
1373
+ dr = 0
1374
+ kols = []
1375
+ for c,p in zip(cols, plus):
1376
+ if p > 0:
1377
+ d = (p * target) // plus_width
1378
+ c += d
1379
+ dd += d
1380
+ dr += 1
1381
+ kols.append(c)
1382
+
1383
+ target += -(dd)
1384
+
1385
+ # Do the ones we missed because of integer division.
1386
+ if target > 0:
1387
+ dd = target
1388
+ cols = []
1389
+ for k, p in zip(kols, plus):
1390
+ if p > 0:
1391
+ # Prefer expanding the rightmost ones.
1392
+ if dd >= dr:
1393
+ k += 1
1394
+ target -= 1
1395
+ dd += 1
1396
+ cols.append( k )
1397
+ else:
1398
+ cols = kols
1399
+
1400
+ return [target, cols]
1401
+
1402
+ def insert_anchor(self, text):
1403
+ # No anchors for text
1404
+ pass
1405
+
1406
+ def pre_indexing(self):
1407
+ pass
1408
+
1409
+ def pre_rendering(self):
1410
+ # Discard buffer from indexing pass
1411
+ self.buf = []
1412
+
1413
+ # Reset document counters from indexing pass
1414
+ self.list_counters = {}
1415
+ self.edit_counter = 0 # Counter for edit marks
1416
+ self.cref_counter = 0
1417
+ self.cref_list = []
1418
+
1419
+ def post_rendering(self):
1420
+ # Insert the TOC and IREF into the main buffer
1421
+ self.output = self.buf[:self.toc_marker] + \
1422
+ self.write_toc()
1423
+ if self.iref_marker > 0:
1424
+ self.output += self.buf[self.toc_marker:self.iref_marker] + \
1425
+ self.write_iref_index() + \
1426
+ self.buf[self.iref_marker:]
1427
+ else:
1428
+ self.output += self.buf[self.toc_marker:]
1429
+
1430
+ def post_process_lines(self, lines):
1431
+ output = []
1432
+ for line in lines:
1433
+ if isinstance(line, type(u'')):
1434
+ # line = line.replace(u"\u8209", '-')
1435
+ line = line.replace(u'\u00A0', u' ')
1436
+ line = line.replace(u'\u2011', u'-')
1437
+ line = line.replace(u'\u200B', u'')
1438
+ line = line.replace(u'\u2028', u' ')
1439
+ line = line.replace(u'\u2060', u'')
1440
+ assert line == line.replace(u'\uE060', u'')
1441
+ output.append(line);
1442
+ return output
1443
+
1444
+ def write_to_file(self, file):
1445
+ """ Writes the buffer to the specified file """
1446
+ # write initial blank lines, not counted against page size...
1447
+ if self.draft:
1448
+ file.write("\n"*3)
1449
+ else:
1450
+ file.write("\n"*5)
1451
+ for line in self.post_process_lines(self.output):
1452
+ file.write(line.rstrip(u" \t"))
1453
+ file.write("\n")