# Copyright The IETF Trust 2018, All Rights Reserved
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function, division

import copy
import datetime
import inspect
import re
import sys
import six
import textwrap

from codecs import open
from collections import namedtuple
from kitchen.text.display import textual_width as displength
from lxml import etree

try:
    from xml2rfc import debug
    debug.debug = True
except ImportError:
    debug = None
    pass


from xml2rfc import strings
from xml2rfc.writers.base import default_options, BaseV3Writer, RfcWriterError
from xml2rfc import utils
from xml2rfc.uniscripts import is_script
from xml2rfc.util.date import extract_date, augment_date, get_expiry_date, format_date
from xml2rfc.util.name import short_author_name, short_author_ascii_name, short_author_name_parts, short_org_name_set

from xml2rfc.util.name import full_author_name_set
from xml2rfc.util.num import ol_style_formatter, num_width
from xml2rfc.util.unicode import expand_unicode_element, textwidth
from xml2rfc.util.postal import get_normalized_address_info, get_address_format_rules, address_field_mapping
from xml2rfc.utils import justify_inline, clean_text


IndexItem   = namedtuple('indexitem', ['item', 'subitem', 'anchor', 'page', ])
Joiner      = namedtuple('joiner', ['join', 'indent', 'hang', 'overlap', 'outdent'])
# Joiner parts:
#   join    string used to join a rendered element to preceding text or lines
#   indent  indentation of rendered element
#   hang    additional indentation of second and follwoing lines
#   overlap Join the last preceding and the first new line on one line, rather
#           than simply appending new lines (when processing lines).
#           Used to handle <dl newline="false"/"true"> and multiple emails
#   outdent If necessary to fit content within width, use a smaller indent than
#           indicated, in ljoin().  Used for <artwork>.

# We don't use namedtuple for Line, because the resulting objects would be immutable:
class Line(object):
    def __init__(self, text, elem):
        assert isinstance(text, six.text_type)
        self.text = text
        self.elem = elem
        self.page = None
        self.block = None
        self.keep = False               # keep this line with the previous one

# a couple of factory functions.  We may modify the resulting lines later,
# which is why we can't just use static instances.
def blankline():
    return [ Line('', None) ]
def pagefeed():
    return [ Line('\f', None) ]
def striplines(lines):
    while lines and lines[0].text.strip(stripspace) == '':
        lines = lines[1:]
    while lines and lines[-1].text.strip(stripspace) == '':
        lines = lines[:-1]
    return lines

class Block(object):
    " Used to hold line block information needed for pagination."
    def __init__(self, elem, prev, next=None, beg=None, end=None):
        self.prev = prev                # previous block
        self.next = next                # next block
        self.elem = elem                # the block's element
        self.beg  = beg                 # beginning line of block
        self.end  = end                 # ending line of block

wrapper = utils.TextWrapper(width=72)
splitter = utils.TextSplitter(width=67)
seen = set()

# This is not a complete list of whitespace characters, and isn't intended to be.  It's
# intended to be whitespace characters commonly occuring in XML input text which should be
# ignored at the beginning and end of text blocks:
stripspace = " \t\n\r\f\v"

base_joiners = {
            None:           Joiner('\n\n', 0, 0, False, False),
            etree.Comment:  Joiner('', 0, 0, False, False),
            etree.PI:       Joiner('', 0, 0, False, False),
        }

def set_joiners(kwargs, update):
    kwargs['joiners'] = copy.copy(base_joiners)
    kwargs['joiners'].update(update)

def indent(text, indent=3, hang=0):
    lines = []
    text = text.replace('\u2028', '\n')
    for l in text.split('\n'):
        if l.strip(stripspace):
            if lines:
                lines.append(' '*(indent+hang) + l)
            else:
                lines.append(' '*indent + l)
        else:
            lines.append('')
    return '\n'.join(lines)

def lindent(lines, indent=3, hang=0):
    for i, l in enumerate(lines):
        if l.text.strip(stripspace):
            if i == 0:
                lines[i].text = ' '*(indent+hang) + l.text
            else:
                lines[i].text = ' '*(indent) + l.text
    return lines

def fill(text, **kwargs):
    kwargs.pop('joiners', None)
    kwargs.pop('prev', None)
    #
    indent = kwargs.pop('indent', 0)
    hang   = kwargs.pop('hang', 0)
    first  = kwargs.pop('first', 0)
    keep   = kwargs.pop('keep_url', False)
    initial=' '*(first+indent)
    subsequent_indent = ' '*(indent+hang)
    if keep:
        text = utils.urlkeep(text, max=kwargs['width'])
    result = wrapper.fill(text, initial=initial, subsequent_indent=subsequent_indent, **kwargs)
    return result

def center(text, width, **kwargs):
    "Fold and center the given text"
    # avoid centered text extending all the way to the margins
    kwargs['width'] = width-4
    text = text.replace('\u2028', '\n')
    lines = text.split('\n')
    if max([ len(l) for l in lines ]+[0]) > width:
        # need to reflow
        lines = wrapper.wrap(text, **kwargs)
    for i, l in enumerate(lines):
        lines[i] = l.center(width).rstrip(stripspace)
    text = '\n'.join(lines).replace('\u00A0', ' ')
    return text

def align(lines, how, width):
    "Align the given text block left, center, or right, as a block"
    if not lines:
        return lines
    if   how == 'left':
        return lines
    w = max( len(l.text) for l in lines )
    if w >= width:
        return lines
    shift = width - w
    if how == 'center':
        for i, l in enumerate(lines):
            if l.text.strip(stripspace):
                lines[i].text = ' '*(shift//2)+l.text
    elif how == 'right':
        for i, l in enumerate(lines):
            if l.text.strip(stripspace):
                lines[i].text = ' '*(shift)+l.text
    else:
        # XXX TODO: Raise execption, catch in TextWriter, and emit error
        pass
    return lines

def mklines(arg, e):
    if isinstance(arg, six.text_type):
        # \u2028 and \u2029 are eliminated here, through splitlines()
        lines = [ Line(t, e) for t in arg.splitlines() ]
    else:
        lines = arg
    return lines

def mktextblock(arg):
    if isinstance(arg, six.text_type):
        text = arg
    else:
        text = '\u2028'.join([ l.text for l in arg ])
    return text

def mktext(arg):
    if isinstance(arg, six.text_type):
        text = arg
    else:
        text = '\n'.join([ l.text for l in arg ])
    return text

def minwidth(arg):
    text = mktext(arg)
    words = text.split()
    return min([ len(w) for w in words ]+[0])

def stripl(l):
    while l and l[0].text.strip(stripspace) == '':
        del l[0]
    while l and l[-1].text.strip(stripspace) == '':
        del l[-1]
    return l

def findblocks(lines):
    "Iterate through all lines, adding block beg/end and back/fwd links"
    elem = None                         # last seen element
    prev = None                         # previous block
    keep = False                        # True if previous keepWithNext was true
    block = None
    for n, l in enumerate(lines):
        if   l.elem == None:
            if block!=None and not keep and not block.end:
                block.end = n
        elif l.elem != elem:
            elem = l.elem
            if elem.tag not in ['t', 'dl', 'dt', 'figure', 'ol', 'table', 'ul', ]:
                keep = l.keep
            if not keep:
                block = Block(elem, prev, beg=n)
                if prev!=None:
                    prev.next = block
                    if not prev.end:
                        prev.end = n
                prev = block
            keep = (elem.get('keepWithNext') == 'true'
                    or (elem.getnext()!=None and elem.getnext().get('keepWithPrevious') == 'true')
                    or elem.tag == 'section')
            l.block = block
        else:
            l.block = block
    block.end = n
    return lines

def expand_ellipsis(text, width):
    if re.search(r'\u2026\d+$', text):
        head, tail = text.rsplit('\u2026', 1)   # split on ellipsis
        head += ' '
        if tail != '0000':
            tail = '%4s' % tail.lstrip('0')     # strip leading zeros
        last = head.split('\n')[-1]
        lack = width - (len(last) + len(tail))
        elip = (' .'*40)[-lack:]
        text = head + elip + tail
    return text

# ------------------------------------------------------------------------------
# Address formatting functions, based on i18naddress functions, but rewritten to
# suit the text output format.

def _format_address_line(line_format, address, rules):
    def _get_field(name):
        value = address.get(name, '')
        if name == 'name':
            role = address.get('role', '')
            if role:
                value += ' (%s)' % role
        return value

    replacements = {
        '%%%s' % code: _get_field(field_name)
        for code, field_name in address_field_mapping.items()}

    fields = re.split('(%.)', line_format)
    has_content = any([ replacements.get(f) for f in fields if (f.startswith('%') and f!= '%%') ])
    if not has_content:
        return ''
    values = [replacements.get(f, f) for f in fields]
    return ''.join(values).strip(stripspace).lstrip(', ')

def format_address(address, latin=False, normalize=False):
    def hasword(item):
        return re.search(r'\w', item, re.U) != None
    address_format, rules = get_address_format_rules(address, latin, normalize)
    address_line_formats = address_format.split('%n')
    address_lines = [
        _format_address_line(lf, address, rules)
        for lf in address_line_formats]
    address_lines = filter(hasword, address_lines)
    return '\n'.join(address_lines)

class TextWriter(BaseV3Writer):

    def __init__(self, xmlrfc, quiet=None, options=default_options, date=datetime.date.today()):
        super(TextWriter, self).__init__(xmlrfc, quiet=quiet, options=options, date=date)
        self.options.min_section_start_lines = 5
        self.refname_mapping = self.get_refname_mapping()
        self.rendered = None

    def process(self):
        if not self.rendered:
            joiners = base_joiners
            if self.options.pagination:
                self.add_pageno_placeholders()
            lines = self.render(self.root, width=72, joiners=joiners)

            if self.options.pagination:
                lines = findblocks(lines)
                lines = self.paginate(lines)
                lines = self.update_toc(lines)
            if self.options.debug:
                for i, l in enumerate(lines):
                    tag  = l.elem.tag  if l.elem!=None else '-'
                    page = l.elem.page if l.elem!=None else '-'
                    if l.block:
                        if six.PY2:
                            sys.stderr.write(("%3d %10s %3d-%3d [%4s] %s\n" % (i, tag, l.block.beg, l.block.end, page, l.text)).encode('utf8'))
                        else:
                            sys.stderr.write(("%3d %10s %3d-%3d [%4s] %s\n" % (i, tag, l.block.beg, l.block.end, page, l.text)))
                    else:
                        if six.PY2:
                            sys.stderr.write(("%3d %10s         [%4s] %s\n" % (i, tag,                           page, l.text)).encode('utf8'))
                        else:
                            sys.stderr.write(("%3d %10s         [%4s] %s\n" % (i, tag,                           page, l.text)))
            for i, l in enumerate(lines):
                length = len(l.text)
                if length > 72:
                    self.warn(l.elem, "Too long line found (L%s), %s characters longer than 72 characters: \n%s" %(i+1, length-72, l.text))

            text = ('\n'.join( l.text for l in lines )).rstrip(stripspace) + '\n'

            # Replace some code points whose utility has ended
            text = text.replace(u'\u00A0', u' ')
            text = text.replace(u'\u2011', u'-')
            text = text.replace(u'\u200B', u'')
            text = text.replace(u'\u2060', u'')
            assert text == text.replace(u'\u2028', u' ')
            assert text == text.replace(u'\uE060', u'')

            self.rendered = text

        return self.rendered

    def write(self, filename):
        """Write the document to a file """

        text = self.process()

        if self.errors:
            raise RfcWriterError("Not creating output file due to errors (see above)")

        encoding = 'utf-8-sig' if self.options.bom else 'utf-8'
        with open(filename, 'w', encoding=encoding) as file:
            file.write(text)

        if not self.options.quiet:
            self.log(' Created file %s' % filename)

    def render(self, e, width, **kw):
        if e.tag in (etree.PI, etree.Comment):
            return e.tail.lstrip(stripspace) if (e.tail and e.tail.strip(stripspace)) else ''
        kwargs = copy.deepcopy(kw)
        func_name = "render_%s" % (e.tag.lower(),)
        func = getattr(self, func_name, self.default_renderer)
        if func == self.default_renderer:
            if e.tag in self.__class__.deprecated_element_tags:
                self.warn(e, "Was asked to render a deprecated element: <%s>" % (e.tag, ))
            elif not e.tag in seen:
                self.warn(e, "No renderer for <%s> found" % (e.tag, ))
                seen.add(e.tag)
        res = func(e, width, **kwargs)
        return res

    def add_pageno_placeholders(self):
        toc = self.root.find('./front/toc/section')
        for e in toc.xpath('.//xref[2]'):
            e.set('pageno', '0000')

    def paginate(self, lines):
        """
        The maximum length of page text is 48 lines.  Above this there are 4 lines of
        top header, or 4 blank lines on the first page, below this there are 5 lines
        of footer, with ^L on the last line and the footer on the next-to-last line.
        """
        header = justify_inline(self.page_top_left(),
                                self.page_top_center(),
                                self.page_top_right())
        start_lineno = 0                # start of unbroken text
        break_target = 4+48             # projected next break
        page = 1
        textlen = len(lines)
        paginated = []
        while start_lineno < textlen:
            footer = justify_inline(self.page_bottom_left(),
                                    self.page_bottom_center(),
                                    "[Page %s]" % page)
            # if the current block ends 1 after break_target, we'll have a widow line on
            # the next page.  If the current block starts 1 before break_target, we'll
            # have an orphan line on this page.  In either case, we insert the
            # page break one line earlier, at break_target-1, and add a filler line.
            break_lineno = break_target
            pad = 0
            if break_lineno >= textlen:
                # The remaining text fits on the next page, this is the last page break
                pad = break_lineno - textlen
                break_target = textlen - 1                 # last line
            else:
                # See if we need to adjust break point to avoid break right after a section
                # heading, and avoid creating orphans or widows
                block = lines[break_target].block
                if block is None:
                    # check backwards for section start.  If we find one, check
                    # again for another, in case it's a subsection.
                    found = None
                    i = break_target
                    while i > break_target-12:
                        for j in range(1,4):
                            k = i - j
                            if lines[k].elem != None and lines[k].elem.tag == 'section':
                                found = True
                                i = k
                                break       # break for loop
                        else:
                            break           # break while loop
                    if found:
                        pad = break_target - i
                        break_lineno = i
                else:
                    # Look for orphan and widow cases
                    olen = break_target - block.beg # number of lines left at the end of this page
                    wlen = block.end - break_target # number of lines at the start of next page
                    blen = block.end - block.beg    # complete block length
                    elem = lines[block.beg].elem
                    if elem.tag == 'section':
                        tcount = 0
                        for r in range(block.beg, break_target):
                            if lines[r].elem!=None and lines[r].elem.tag != 'section':
                                tcount += 1
                        if wlen == 1 or tcount <= self.options.min_section_start_lines:
                            adj = break_lineno - block.beg
                            pad += adj
                            break_lineno -= adj
                    elif elem.tag in ['artset', 'artwork', 'figure', 'sourcecode', 'table', ]:
                        if blen < 48 or olen <= self.options.min_section_start_lines:
                            adj = break_lineno - block.beg
                            pad += adj
                            break_lineno -= adj
                        else:
                            pass
                    elif ( (olen in range(1, self.options.orphans+1) and blen > olen)
                        or (wlen in range(1, self.options.widows+1) and blen > wlen)):
                        break_lineno -= olen
                        pad += olen
                    else:
                        pass
            # Transfer lines to next page
            pagestart = len(paginated)
            if page > 1:
                paginated += pagefeed() + mklines(header, None) + blankline()*2
            paginated += lines[start_lineno:break_lineno]
            paginated += blankline() * pad
            paginated += blankline() * 3 + mklines(footer, None)
            # make note of each line's page
            for i in range(pagestart, len(paginated)):
                paginated[i].page = page
                if paginated[i].elem != None and not isinstance(paginated[i].elem, (etree._ProcessingInstruction, etree._Comment)):
                    paginated[i].elem.page = page
            # Set the next page start
            start_lineno = break_lineno
            # discard blank lines at the top of the next page, if any
            while start_lineno < textlen and lines[start_lineno].text.strip(stripspace) == '':
                start_lineno += 1
            # advance page end to the next potential page break
            break_target = start_lineno + 48
            page += 1

        return paginated

    def update_toc(self, lines):
        if self.root.get('tocInclude') != 'true':
            return lines
        toc = self.root.find('./front/toc/section')
        in_toc = False
        toc_start = None
        toc_end = None
        for i, l in enumerate(lines):
            if l.elem is None:
                continue
            elif l.elem == toc:
                in_toc = True
                toc_start = i
            elif in_toc and l.elem.tag == 'section':
                # end of toc
                in_toc = False
                toc_end = i
                break
            elif in_toc and l.elem.tag in ['li', 't']:
                xref = l.elem.find('.//xref[2]')
                if xref!= None:
                    id = xref.get('target')
                    target = self.get_element_from_id(id)
                    page = self.get_element_page(target)
                    xref.set('pageno', '%s'%page )
            elif in_toc and l.elem!=None:
                self.error(l.elem, "Unexpected condition. <%s> in toc" % (l.elem.tag))
            else:
                pass
        # new toc, to be used to replace the old one
        toclines = self.render(toc, width=72, joiners=base_joiners)
        if toc_start and toc_end:
            j = 2
            for i in range(toc_start+2, toc_end):
                old = lines[i]
                if old.elem is None:
                    continue
                new = toclines[j]
                lines[i].text = new.text
                j += 1
        return lines

    def tjoin(self, text, e, width, **kwargs):
        '''
        Render element e, then format and join it to text using the
        appropriate settings in joiners.
        '''
        assert isinstance(text, six.text_type)
        joiners = kwargs['joiners']
        j = joiners[e.tag] if e.tag in joiners else joiners[None]
        width -= j.indent + j.hang
        if width < minwidth(text):
            self.die(e, "Trying to render text in a too narrow column: width: %s, text: '%s'" % (width, text))
        kwargs['hang'] = j.hang
        etext = self.render(e, width, **kwargs)
        itext = indent(etext, j.indent, j.hang)
        if text:
            if '\n' in j.join:
                text += j.join + itext
            elif j.join.strip(stripspace) and not itext.strip(stripspace):
                # don't use non-empty joiners with empty content
                pass
            else:
                text += j.join + itext.lstrip(stripspace)
        else:
            text  = itext
        return text

    def ljoin(self, lines, e, width, **kwargs):
        '''
        Render element e, then format and join it to preceding text using the
        appropriate settings in joiners.
        '''
        assert isinstance(lines, list)
        assert not lines or isinstance(lines[0], Line)
        joiners = kwargs['joiners']
        j = joiners[e.tag] if e.tag in joiners else joiners[None]
        width -= j.indent
        kwargs['hang'] = j.hang
        res = mklines(self.render(e, width, **kwargs), e)
        if lines:
            for i in range(j.join.count('\n')-1):
                lines += blankline()
        reswidth = max(len(l.text) for l in res) if res else 0
        indent = j.indent
        if j.outdent and reswidth > width:
            indent -= min(indent, reswidth-width)
            self.warn(e, "%s too wide, reducing indentation from %s to %s" % (e.tag.capitalize(), j.indent, indent))
        nlines = lindent(res, indent, j.hang)
        if j.overlap and nlines:
            firstline = nlines[0]
            nlines = nlines[1:]
            if firstline.text.strip(stripspace):
                lines[-1].text += j.join + firstline.text.lstrip(stripspace)
        lines += nlines
        return lines


    def element(self, tag, line=None, **attribs):
        e = self.root.makeelement(tag, attrib=attribs)
        if line:
            e.sourceline = line
        return e

    def get_initials(self, author):
        """author is an rfc2629 author element.  Return the author initials,
        fixed up according to current flavour and policy."""
        initials = author.attrib.get('initials', '')

        initials_list = re.split("[. ]+", initials)
        try:
            initials_list.remove('')
        except:
            pass
        if len(initials_list) > 0:
            # preserve spacing, but make sure all parts have a trailing
            # period
            initials = initials.strip(stripspace)
            initials += '.' if not initials.endswith('.') else ''
            initials = re.sub('([^.]) ', r'\g<1>. ', initials)
        return initials

    # --- fallback rendering functions ------------------------------------------

    def default_renderer(self, e, width, **kwargs):
        # This is a fallback when a more specific function doesn't exist
        text = "<%s>:%s" % (e.tag, e.text or '')
        for c in e.getchildren():
            ctext = self.render(c, width, **kwargs)
            if isinstance(ctext, list):
                ctext = "\n\n".join(ctext)
            if ctext is None and debug:
                debug.show('e')
                debug.show('c')
            text += '\n' + ctext
        text += e.tail or ''
        return text

#     def parts_renderer(self, e, width, **kwargs):
#         text = ""
#         for c in e.getchildren():
#             text = self.tjoin(text, c, width, **kwargs)
#         return text

    def inner_text_renderer(self, e, width=None, **kwargs):
        text = e.text or ''
        for c in e.getchildren():
            try:
                text += self.render(c, width, **kwargs)
            except TypeError:
                debug.show('c')
                raise
        return text.strip(stripspace)
    
#     def text_renderer(self, e, width, **kwargs):
#         text = self.inner_text_renderer(e, **kwargs)
#         text += ' '+e.tail if e.tail else ''
#         return text

    def text_or_block_renderer(self, e, width, **kw):
        # This handles the case where the element has two alternative content
        # models, either text or block-level children; deal with them
        # separately.  Return text and whether this was plain text.
        kwargs = copy.deepcopy(kw)
        if utils.hastext(e):
            _tag = e.tag; e.tag = 't'
            text = mktext(self.ljoin([], e, width, **kwargs))
            e.tag = _tag
            return text, True
        else:
            lines = []
            for c in e.getchildren():
                lines = self.ljoin(lines, c, width, **kwargs)
                kwargs.pop('first', None)
            return lines, False
            

    def quote_renderer(self, e, width, prefix, by, cite, **kwargs):
        set_joiners(kwargs, {
            None:      Joiner('\n', 0, 0, False, False),
            't':       Joiner('\n\n', 0, 0, False, False),
        })
        width = width if width else 69
        text, plain = self.text_or_block_renderer(e, width-3, **kwargs)
        if plain:
            text = fill(text, width=width-3, **kwargs)
        lines = mklines(text, e)
        if by  or cite:
            lines += [ Line('', e) ]
        if by:
            lines += mklines("-- %s" % fill(by, width=width-6, hang=3), e)
        if cite:
            lines += mklines("   %s\n" % fill(cite, width=width-6, hang=3), e)
        for i, l in enumerate(lines):
            lines[i].text = prefix + '  '+l.text
        lines = lindent(lines, indent=kwargs.get('indent', 0))
        return lines

    def null_renderer(self, e, width, **kwargs):
        self.die(e, "Did not expect to be asked to render <%s> while in %s//%s" % (e.tag, self.part, e.getparent().tag))
        return None

    # --- element rendering functions ------------------------------------------

    # 2.1.  <abstract>
    # 
    #    Contains the Abstract of the document.  See [RFC7322] for more
    #    information on restrictions for the Abstract.
    #
    #    This element appears as a child element of <front> (Section 2.26).
    # 
    # ...
    # 
    # 2.1.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the Abstract.
    def render_abstract(self, e, width, **kwargs):
        kwargs['joiners'].update({ None:       Joiner('\n\n', 3, 0, False, False), })
        lines = [ Line("Abstract", e) ]
        for c in e.getchildren():
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines

    # 2.2.  <address>
    # 
    #    Provides address information for the author.
    # 
    #    This element appears as a child element of <author> (Section 2.7).
    def render_address(self, e, width, **kwargs):
        set_joiners(kwargs, {
            None:       Joiner('\n', 0, 0, False, False),
            'email':    Joiner('', 0, 0, True, False),
        })
        lines = []
        for c in e.getchildren():
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines

    # 2.3.  <annotation>
    # 
    #    Provides additional prose augmenting a bibliographic reference.  This
    #    text is intended to be shown after the rest of the generated
    #    reference text.
    # 
    #    This element appears as a child element of <reference>
    #    (Section 2.40).
    def render_annotation(self, e, width, **kwargs):
        text = fill(self.inner_text_renderer(e), width=width, **kwargs)
        return text

    # 2.4.  <area>
    # 
    #    Provides information about the IETF area to which this document
    #    relates (currently not used when generating documents).
    # 
    #    The value ought to be either the full name or the abbreviation of one
    #    of the IETF areas as listed on <http://www.ietf.org/iesg/area.html>.
    #    A list of full names and abbreviations will be kept by the RFC Series
    #    Editor.
    # 
    #    This element appears as a child element of <front> (Section 2.26).


    # 2.5.  <artwork>
    # 
    #    This element allows the inclusion of "artwork" in the document.
    #    <artwork> provides full control of horizontal whitespace and line
    #    breaks; thus, it is used for a variety of things, such as diagrams
    #    ("line art") and protocol unit diagrams.  Tab characters (U+0009)
    #    inside of this element are prohibited.
    # 
    #    Alternatively, the "src" attribute allows referencing an external
    #    graphics file, such as a vector drawing in SVG or a bitmap graphic
    #    file, using a URI.  In this case, the textual content acts as a
    #    fallback for output representations that do not support graphics;
    #    thus, it ought to contain either (1) a "line art" variant of the
    #    graphics or (2) prose that describes the included image in sufficient
    #    detail.
    # 
    #    In [RFC7749], the <artwork> element was also used for source code and
    #    formal languages; in v3, this is now done with <sourcecode>.
    # 
    #    There are at least five ways to include SVG in artwork in
    #    Internet-Drafts:
    # 
    #    o  Inline, by including all of the SVG in the content of the element,
    #       such as: <artwork type="svg"><svg xmlns="http://www.w3.org/2000/
    #       svg...">
    # 
    #    o  Inline, but using XInclude (see Appendix B.1), such as: <artwork
    #       type="svg"><xi:include href=...>
    # 
    #    o  As a data: URI, such as: <artwork type="svg" src="data:image/
    #       svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3...">
    # 
    #    o  As a URI to an external entity, such as: <artwork type="svg"
    #       src="http://www.example.com/...">
    # 
    #    o  As a local file, such as: <artwork type="svg" src="diagram12.svg">
    # 
    #    The use of SVG in Internet-Drafts and RFCs is covered in much more
    #    detail in [RFC7996].
    # 
    #    The above methods for inclusion of SVG art can also be used for
    #    including text artwork, but using a data: URI is probably confusing
    #    for text artwork.
    # 
    #    Formatters that do pagination should attempt to keep artwork on a
    #    single page.  This is to prevent artwork that is split across pages
    #    from looking like two separate pieces of artwork.
    # 
    #    See Section 5 for a description of how to deal with issues of using
    #    "&" and "<" characters in artwork.

    def render_artset(self, e, width, **kwargs):
        preflist = ['ascii-art', ]
        lines = []
        for t in preflist:
            for a in e.xpath('./artwork[@type="%s"]' % t):
                lines = self.ljoin(lines, a, width, **kwargs)
                return lines
        else:
            a = e[0]
            if e.text and e.text.strip(stripspace):
                lines = self.ljoin(lines, a, width, **kwargs)
            else:
                self.err(a, "Expected ascii-art text, but found none.")
        return lines

    def render_artwork(self, e, width, **kwargs):
        msg  = ( "(Artwork only available as %s: %s)"
                    % ( e.get('type', '(unknown type)'),
                        e.get('originalSrc') or e.get('src') or 'No external link available, see %s.html for artwork.'%self.root.get('docName')))
        msg  = fill(msg, width=width, **kwargs)
#        text = (e.text.strip(stripspace) and e.text.expandtabs()) or msg
#         text = text.strip('\n')
#         text = '\n'.join( [ l.rstrip(stripspace) for l in text.split('\n') ] )
        # We need this in order to deal with xml comments inside artwork:
        text = e.text + ''.join([ c.tail for c in e.getchildren() ])
        text = text.strip('\n')
        text = (text.strip(stripspace) and text.expandtabs()) or msg
        text = '\n'.join( [ l.rstrip(stripspace) for l in text.split('\n') ] )
        #
        lines = [ Line(t, e) for t in text.splitlines() ]
        lines = align(lines, e.get('align', 'left'), width)
        return lines 

    # 2.5.1.  "align" Attribute
    # 
    #    Controls whether the artwork appears left justified (default),
    #    centered, or right justified.  Artwork is aligned relative to the
    #    left margin of the document.
    # 
    #    Allowed values:
    # 
    #    o  "left" (default)
    # 
    #    o  "center"
    # 
    #    o  "right"


    # 2.5.2.  "alt" Attribute
    # 
    #    Alternative text description of the artwork (which is more than just
    #    a summary or caption).  When the art comes from the "src" attribute
    #    and the format of that artwork supports alternate text, the
    #    alternative text comes from the text of the artwork itself, not from
    #    this attribute.  The contents of this attribute are important to
    #    readers who are visually impaired, as well as those reading on
    #    devices that cannot show the artwork well, or at all.


    # 2.5.3.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this artwork.

    # 2.5.5.  "name" Attribute
    # 
    #    A filename suitable for the contents (such as for extraction to a
    #    local file).  This attribute can be helpful for other kinds of tools
    #    (such as automated syntax checkers, which work by extracting the
    #    artwork).  Note that the "name" attribute does not need to be unique
    #    for <artwork> elements in a document.  If multiple <artwork> elements
    #    have the same "name" attribute, a processing tool might assume that
    #    the elements are all fragments of a single file, and the tool can
    #    collect those fragments for later processing.  See Section 7 for a
    #    discussion of possible problems with the value of this attribute.

    # 2.5.6.  "src" Attribute
    # 
    #    The URI reference of a graphics file [RFC3986], or the name of a file
    #    on the local disk.  This can be a "data" URI [RFC2397] that contains
    #    the contents of the graphics file.  Note that the inclusion of art
    #    with the "src" attribute depends on the capabilities of the
    #    processing tool reading the XML document.  Tools need to be able to
    #    handle the file: URI, and they should be able to handle http: and
    #    https: URIs as well.  The prep tool will be able to handle reading
    #    the "src" attribute.
    # 
    #    If no URI scheme is given in the attribute, the attribute is
    #    considered to be a local filename relative to the current directory.
    #    Processing tools must be careful to not accept dangerous values for
    #    the filename, particularly those that contain absolute references
    #    outside the current directory.  Document creators should think hard
    #    before using relative URIs due to possible later problems if files
    #    move around on the disk.  Also, documents should most likely use
    #    explicit URI schemes wherever possible.
    # 
    #    In some cases, the prep tool may remove the "src" attribute after
    #    processing its value.  See [RFC7998] for a description of this.
    # 
    #    It is an error to have both a "src" attribute and content in the
    #    <artwork> element.

    # 2.5.7.  "type" Attribute
    # 
    #    Specifies the type of the artwork.  The value of this attribute is
    #    free text with certain values designated as preferred.
    # 
    #    The preferred values for <artwork> types are:
    # 
    #    o  ascii-art
    # 
    #    o  binary-art
    # 
    #    o  call-flow
    # 
    #    o  hex-dump
    # 
    #    o  svg
    # 
    #    The RFC Series Editor will maintain a complete list of the preferred
    #    values on the RFC Editor web site, and that list is expected to be
    #    updated over time.  Thus, a consumer of v3 XML should not cause a
    #    failure when it encounters an unexpected type or no type is
    #    specified.  The table will also indicate which type of art can appear
    #    in plain-text output (for example, type="svg" cannot).



    # 2.6.  <aside>
    # 
    #    This element is a container for content that is semantically less
    #    important or tangential to the content that surrounds it.
    # 
    #    This element appears as a child element of <section> (Section 2.46).
    #
    # 2.6.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this aside.
    def render_aside(self, e, width, **kwargs):
        kwargs['joiners'].update({ 't':       Joiner('\n\n', 0, 0, False, False), })
        prefix = '   |  '
        width -= len(prefix)
        text, plain = self.text_or_block_renderer(e, width, **kwargs)
        if plain:
            text = fill(text, width=width, **kwargs)
        lines = mklines(text, e)
        for i, l in enumerate(lines):
            lines[i].text = prefix + l.text
        lines = lindent(lines, indent=kwargs.get('indent', 0))
        return lines


    # 2.7.  <author>
    # 
    #    Provides information about a document's author.  This is used both
    #    for the document itself (at the beginning of the document) and for
    #    referenced documents.
    # 
    #    The <author> elements contained within the document's <front> element
    #    are used to fill the boilerplate and also to generate the "Author's
    #    Address" section (see [RFC7322]).
    # 
    #    Note that an "author" can also be just an organization (by not
    #    specifying any of the "name" attributes, but adding the
    #    <organization> child element).
    # 
    #    Furthermore, the "role" attribute can be used to mark an author as
    #    "editor".  This is reflected both on the front page and in the
    #    "Author's Address" section, as well as in bibliographic references.
    #    Note that this specification does not define a precise meaning for
    #    the term "editor".
    # 
    #    This element appears as a child element of <front> (Section 2.26).
    # 
    # ...
    # 
    # 2.7.1.  "asciiFullname" Attribute
    # 
    #    The ASCII equivalent of the author's full name.
    # 
    # 2.7.2.  "asciiInitials" Attribute
    # 
    #    The ASCII equivalent of the author's initials, to be used in
    #    conjunction with the separately specified asciiSurname.
    # 
    # 2.7.3.  "asciiSurname" Attribute
    # 
    #    The ASCII equivalent of the author's surname, to be used in
    #    conjunction with the separately specified asciiInitials.
    # 
    # 2.7.4.  "fullname" Attribute
    # 
    #    The full name (used in the automatically generated "Author's Address"
    #    section).  Although this attribute is optional, if one or more of the
    #    "asciiFullname", "asciiInitials", or "asciiSurname" attributes have
    #    values, the "fullname" attribute is required.
    # 
    # 2.7.5.  "initials" Attribute
    # 
    #    An abbreviated variant of the given name(s), to be used in
    #    conjunction with the separately specified surname.  It usually
    #    appears on the front page, in footers, and in references.
    # 
    #    Some processors will post-process the value -- for instance, when it
    #    only contains a single letter (in which case they might add a
    #    trailing dot).  Relying on this kind of post-processing can lead to
    #    results varying across formatters and thus ought to be avoided.
    # 
    # 2.7.6.  "role" Attribute
    # 
    #    Specifies the role the author had in creating the document.
    # 
    #    Allowed value:
    # 
    #    o  "editor"
    # 
    # 2.7.7.  "surname" Attribute
    # 
    #    The author's surname, to be used in conjunction with the separately
    #    specified initials.  It usually appears on the front page, in
    #    footers, and in references.
    def render_author(self, e, width, **kwargs):
        """
        Render one author entry for the Authors' Addresses section.
        """
        set_joiners(kwargs, {
            None:       Joiner('\n', 0, 0, False, False),  # default 
        })
        lines = []
        address = e.find('./address')
        if address is None:
            address = etree.Element('address')
            e.append(address)
        postal = e.find('./address/postal')
        if postal is None:
            # We render author name as part of postal, so make sure it's there
            address.insert(0, etree.Element('postal'))
        # ascii will be set only if name has codepoints not in the Latin script blocks
        name, ascii  = full_author_name_set(e)
        if ascii:
            for c in e.iterchildren('address'):
                lines = self.ljoin(lines, c, width, latin=True, **kwargs)
                lines = striplines(lines)
                lines += blankline()
                lines += [ Line( 'Additional contact information:', address) ]
                lines += blankline()
                lines += lindent(self.ljoin([], c, width, latin=False, **kwargs))
                lines = striplines(lines)
        else:
            for c in e.iterchildren('address'):
                lines = self.ljoin(lines, c, width, **kwargs)
                lines = striplines(lines)
        lines += blankline()
        return lines

    def render_author_name(self, e, width, **kwargs):
        text = ''
        organization = self.render_organization(e.find('organization'), width, **kwargs)
        fullname = e.attrib.get('fullname', '')
        if not fullname:
            surname = e.attrib.get('surname', '')
            if surname:
                initials = self.get_initials(e)
                fullname = '%s %s' % (initials, fullname)
        if fullname:
            text = fullname
            if e.attrib.get('role', '') == 'editor':
                text += ' (editor)'
            if organization:
                text += '\n'+ organization
        elif organization:
            # Use organization instead of name
            text = organization
        else:
            text = ''
        return text

    def render_contact(self, e, width, **kwargs):
        p = e.getparent()
        if   p.tag == 't':
            name, ascii = full_author_name_set(e)
            if ascii:
                contact = "%s (%s)" % (name, ascii)
            else:
                contact = name
            # Avoid sentence end space doubling
            contact = contact.replace('. ', '.\u00a0')
            return contact + (e.tail or '')
        elif p.tag == 'section':
            return self.render_author(e, width, **kwargs)
        else:
            return self.null_renderer(e, width, **kwargs)

    def render_author_front(self, e, **kwargs):
        name = short_author_name(e)
        if not is_script(name, 'Latin'):
            aname = short_author_ascii_name(e)
            name = '%s (%s)' % (name, aname)
        #
        o = e.find('./organization')
        
        if o != None:
            if o.get('showOnFrontPage') == 'true':
                organization = self.render_front_organization(o, **kwargs)
            else:
                organization = None
        else:
            organization = ''
        #
        if organization and not name:
            name = organization
            organization = None
        #
        if e.get('role') == 'editor':
            name += ', Ed.'
        return name, organization

    def render_authors(self, e, width, **kwargs):
        """
        Render authors for reference display.  This has to take into
        consideration the particular presentation of surnames and initials
        used by the RFC Editor.
        """
        buf = []
        authors = list(e.iterdescendants('author'))
        for i, author in enumerate(authors):
            if i == len(authors) - 1 and len(authors) > 1:
                buf.append('and ')
            organization = author.find('organization')
            initials, surname = short_author_name_parts(author)
            if surname:
                initials = initials or ''
                if i == len(authors) - 1 and len(authors) > 1:
                    # Last author is rendered in reverse
                    if len(initials) > 0:
                        buf.append(initials + ' ' + \
                                     surname)
                    else:
                        buf.append(surname)
                elif len(initials) > 0:
                    buf.append(surname + ', ' + initials)
                else:
                    buf.append(surname)
                if author.attrib.get('role', '') == 'editor':
                    buf.append(', Ed.')
            elif organization is not None and organization.text:
                # Use organization instead of name
                buf.append(organization.text.strip(stripspace))
            else:
                continue
            if len(authors) == 2 and i == 0:
                buf.append(' ')
            elif i < len(authors) - 1:
                buf.append(', ')
        return ''.join(buf)

    # 2.8.  <back>
    # 
    #    Contains the "back" part of the document: the references and
    #    appendices.  In <back>, <section> elements indicate appendices.
    # 
    #    This element appears as a child element of <rfc> (Section 2.45).
    def render_back(self, e, width, **kwargs):
        lines = []
        for c in e.getchildren():
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines


    # 2.9.  <bcp14>
    # 
    #    Marks text that are phrases defined in [BCP14] such as "MUST",
    #    "SHOULD NOT", and so on.  When shown in some of the output
    #    representations, the text in this element might be highlighted.  The
    #    use of this element is optional.
    # 
    #    This element is only to be used around the actual phrase from BCP 14,
    #    not the full definition of a requirement.  For example, it is correct
    #    to say "The packet <bcp14>MUST</bcp14> be dropped.", but it is not
    #    correct to say "<bcp14>The packet MUST be dropped.</bcp14>".
    # 
    #    This element appears as a child element of <annotation>
    #    (Section 2.3), <blockquote> (Section 2.10), <dd> (Section 2.18), <dt>
    #    (Section 2.21), <em> (Section 2.22), <li> (Section 2.29), <preamble>
    #    (Section 3.6), <refcontent> (Section 2.39), <strong> (Section 2.50),
    #    <sub> (Section 2.51), <sup> (Section 2.52), <t> (Section 2.53), <td>
    #    (Section 2.56), <th> (Section 2.58), and <tt> (Section 2.62).
    # 
    #    Content model: only text content.
    def render_bcp14(self, e, width, **kwargs):
        return (e.text or '') + (e.tail or '')

    # 2.10.  <blockquote>
    # 
    #    Specifies that a block of text is a quotation.
    # 
    #    This element appears as a child element of <section> (Section 2.46).
    # 
    # 2.10.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this quotation.
    # 
    # 2.10.2.  "cite" Attribute
    # 
    #    The source of the citation.  This must be a URI.  If the "quotedFrom"
    #    attribute is given, this URI will be used by processing tools as the
    #    link for the text of that attribute.
    # 
    # 2.10.3.  "quotedFrom" Attribute
    # 
    #    Name of person or document the text in this element is quoted from.
    #    A formatter should render this as visible text at the end of the
    #    quotation.
    def render_blockquote(self, e, width, **kwargs):
        by  = e.get('quotedFrom')
        cite = e.get('cite')
        return self.quote_renderer(e, width, '|', by, cite, **kwargs)

    # 2.11.  <boilerplate>
    # 
    #    Holds the boilerplate text for the document.  This element is filled
    #    in by the prep tool.
    # 
    #    This element contains <section> elements.  Every <section> element in
    #    this element must have the "numbered" attribute set to "false".
    # 
    #    This element appears as a child element of <front> (Section 2.26).
    def render_boilerplate(self, e, width, **kwargs):
        lines = []
        for c in e.getchildren():
            numbered = c.get('numbered')
            if not numbered == 'false':
                self.err(c, "Expected boilerplate section to have numbered='false', but found '%s'" % (numbered, ))
            keep_url = True if self.options.rfc else False
            lines = self.ljoin(lines, c, width, keep_url=keep_url, **kwargs)
        return lines

    # 2.12.  <br>
    # 
    #    Indicates that a line break should be inserted in the generated
    #    output by a formatting tool.  Multiple successive instances of this
    #    element are ignored.
    # 
    #    This element appears as a child element of <td> (Section 2.56) and
    #    <th> (Section 2.58).
    def render_br(self, e, width, **kwargs):
        return '\u2028' + (e.tail or '')

    # 2.13.  <city>
    # 
    #    Gives the city name in a postal address.
    # 
    #    This element appears as a child element of <postal> (Section 2.37).
    # 
    # 2.13.1.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the city name.
    render_city = null_renderer         # handled in render_address

    # 2.14.  <code>
    # 
    #    Gives the postal region code.
    # 
    #    This element appears as a child element of <postal> (Section 2.37).
    # 
    # 2.14.1.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the postal code.
    render_code = null_renderer         # handled in render_address

    # 2.15.  <country>
    # 
    #    Gives the country name or code in a postal address.
    # 
    #    This element appears as a child element of <postal> (Section 2.37).
    # 
    # 2.15.1.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the country name.
    render_country = null_renderer      # handled in render_address

    # 2.16.  <cref>
    # 
    #    Represents a comment.
    # 
    #    Comments can be used in a document while it is work in progress.
    #    They might appear either inline and visually highlighted, at the end
    #    of the document, or not at all, depending on the formatting tool.
    # 
    #    This element appears as a child element of <annotation>
    #    (Section 2.3), <blockquote> (Section 2.10), <c> (Section 3.1), <dd>
    #    (Section 2.18), <dt> (Section 2.21), <em> (Section 2.22), <li>
    #    (Section 2.29), <name> (Section 2.32), <postamble> (Section 3.5),
    #    <preamble> (Section 3.6), <strong> (Section 2.50), <sub>
    #    (Section 2.51), <sup> (Section 2.52), <t> (Section 2.53), <td>
    #    (Section 2.56), <th> (Section 2.58), <tt> (Section 2.62), and <ttcol>
    #    (Section 3.9).
    # 
    # 2.16.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this comment.
    # 
    # 2.16.2.  "display" Attribute
    # 
    #    Suggests whether or not the comment should be displayed by formatting
    #    tools.  This might be set to "false" if you want to keep a comment in
    #    a document after the contents of the comment have already been dealt
    #    with.
    # 
    #    Allowed values:
    # 
    #    o  "true" (default)
    # 
    #    o  "false"
    # 
    # 2.16.3.  "source" Attribute
    # 
    #    Holds the "source" of a comment, such as the name or the initials of
    #    the person who made the comment.
    def render_cref(self, e, width, **kwargs):
        display = e.get('display') == 'true'
        source = e.get('source')
        if display:
            text = '\u2028' + mktextblock(self.quote_renderer(e, width, '//', source, None, **kwargs))
            return text
        else:
            return ''

    # 2.17.  <date>
    # 
    #    Provides information about the publication date.  This element is
    #    used for two cases: the boilerplate of the document being produced,
    #    and inside bibliographic references that use the <front> element.
    # 
    #    Boilerplate for Internet-Drafts and RFCs:  This element defines the
    #       date of publication for the current document (Internet-Draft or
    #       RFC).  When producing Internet-Drafts, the prep tool uses this
    #       date to compute the expiration date (see [IDGUIDE]).  When one or
    #       more of "year", "month", or "day" are left out, the prep tool will
    #       attempt to use the current system date if the attributes that are
    #       present are consistent with that date.
    # 
    #       In dates in <rfc> elements, the month must be a number or a month
    #       in English.  The prep tool will silently change text month names
    #       to numbers.  Similarly, the year must be a four-digit number.
    # 
    #       When the prep tool is used to create Internet-Drafts, it will
    #       reject a submitted Internet-Draft that has a <date> element in the
    #       boilerplate for itself that is anything other than today.  That
    #       is, the tool will not allow a submitter to specify a date other
    #       than the day of submission.  To avoid this problem, authors might
    #       simply not include a <date> element in the boilerplate.
    # 
    #    Bibliographic references:  In dates in <reference> elements, the date
    #       information can have prose text for the month or year.  For
    #       example, vague dates (year="ca. 2000"), date ranges
    #       (year="2012-2013"), non-specific months (month="Second quarter"),
    #       and so on are allowed.
    # 
    #    This element appears as a child element of <front> (Section 2.26).
    # 
    # 2.17.1.  "day" Attribute
    # 
    #    The day of publication.
    # 
    # 2.17.2.  "month" Attribute
    # 
    #    The month or months of publication.
    # 
    # 2.17.3.  "year" Attribute
    # 
    #    The year or years of publication.
    def render_date(self, e, width, **kwargs):
        #pp = e.getparent().getparent()
        #if pp.tag == 'rfc':
        have_date = e.get('day') or e.get('month') or e.get('year')
        year, month, day = extract_date(e, self.date)
        p = e.getparent()
        if p==None or p.getparent().tag != 'reference':
            # don't touch the given date if we're rendering a reference
            year, month, day = augment_date(year, month, day, self.date)
        date = format_date(year, month, day, self.options.legacy_date_format)
        if e.text and have_date:
            date = "%s (%s)" % (e.text, date)
        elif e.text:
            date = e.text
        else:
            # date = date
            pass
        return date

    # 2.18.  <dd>
    # 
    #    The definition part of an entry in a definition list.
    # 
    #    This element appears as a child element of <dl> (Section 2.20).
    # 
    # 2.18.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this definition.
    def render_dd(self, e, width, **kwargs):
        dtwidth = kwargs.pop('dtwidth')
        j = kwargs['joiners']['dd']
        kwargs['first'] = dtwidth + len(j.join) - j.indent if j.overlap else 0
        r, foldable = self.text_or_block_renderer(e, width, **kwargs)
        lines = mklines(r, e) if foldable else r
        if lines:
            lines[0].keep = True            # keep first line of dd with preceding dt
        return lines

    # 2.19.  <displayreference>
    # 
    #    This element gives a mapping between the anchor of a reference and a
    #    name that will be displayed instead.  This allows authors to display
    #    more mnemonic anchor names for automatically included references.
    #    The mapping in this element only applies to <xref> elements whose
    #    format is "default".  For example, if the reference uses the anchor
    #    "RFC6949", the following would cause that anchor in the body of
    #    displayed documents to be "RFC-dev":
    # 
    #    <displayreference target="RFC6949" to="RFC-dev"/>
    # 
    #    If a reference section is sorted, this element changes the sort
    #    order.
    # 
    #    It is expected that this element will only be valid in input
    #    documents.  It will likely be removed by prep tools when preparing a
    #    final version after those tools have replaced all of the associated
    #    anchors, targets, and "derivedContent" attributes.
    # 
    #    This element appears as a child element of <back> (Section 2.8).
    # 
    # 2.19.1.  "target" Attribute (Mandatory)
    # 
    #    This attribute must be the name of an anchor in a <reference> or
    #    <referencegroup> element.
    # 
    # 2.19.2.  "to" Attribute (Mandatory)
    # 
    #    This attribute is a name that will be displayed as the anchor instead
    #    of the anchor that is given in the <reference> element.  The string
    #    given must start with one of the following characters: 0-9, a-z, or
    #    A-Z.  The other characters in the string must be 0-9, a-z, A-Z, "-",
    #    ".", or "_".
    def render_displayreference(self, e, width, **kwargs):
        return ''


    # 2.20.  <dl>
    # 
    #    A definition list.  Each entry has a pair of elements: a term (<dt>)
    #    and a definition (<dd>).  (This is slightly different and simpler
    #    than the model used in HTML, which allows for multiple terms for a
    #    single definition.)
    # 
    #    This element appears as a child element of <abstract> (Section 2.1),
    #    <aside> (Section 2.6), <blockquote> (Section 2.10), <dd>
    #    (Section 2.18), <li> (Section 2.29), <note> (Section 2.33), <section>
    #    (Section 2.46), <td> (Section 2.56), and <th> (Section 2.58).
    # 
    # 2.20.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the list.
    # 
    # 2.20.2.  "hanging" Attribute
    # 
    #    The "hanging" attribute defines whether or not the term appears on
    #    the same line as the definition.  hanging="true" indicates that the
    #    term is to the left of the definition, while hanging="false"
    #    indicates that the term will be on a separate line.
    # 
    #    Allowed values:
    # 
    #    o  "false"
    # 
    #    o  "true" (default)
    # 
    # 2.20.3.  "spacing" Attribute
    # 
    #    Defines whether or not there is a blank line between entries.
    #    spacing="normal" indicates a single blank line, while
    #    spacing="compact" indicates no space between.
    # 
    #    Allowed values:
    # 
    #    o  "normal" (default)
    # 
    #    o  "compact"
    def render_dl(self, e, width, **kwargs):
        newline = e.get('newline') == 'true'
        compact = e.get('spacing') == 'compact'
        tjoin  = '\n' if compact else '\n\n'
        #
        indent = e.get('indent', None)
        if indent and not indent.isdigit():
            self.warn(e, "Expected indent to have a numeric value, found '%s'" % indent)
            indent = None
        if indent is None:
            indent = 3
# Commented out: code to adapt the indentation to the longest term in the list        
#             for dt in e.iterchildren('dt'):
#                 dt._text = self.inner_text_renderer(dt)
#                 l = len(dt._text)
#                 if l+2 > indent:
#                     indent = l+2
#             if newline:
#                 if indent > 15:                   # XXX Somewhat arbitrary choice
#                     indent = 3
#             else:
#                 if indent > 24:                   # XXX Somewhat arbitrary choice
#                     indent = 3
        else:
            indent = int(indent)
        nljoin = Joiner('\n', indent, 0, False, False)
        spjoin = Joiner('  ', indent, 0, True, False)
        ddjoin  = nljoin if newline else spjoin
        set_joiners(kwargs, {
            None:       Joiner(tjoin, 0, 0, False, False),
            'dt':       Joiner(tjoin, 0, 0, False, False),
            'dd':       ddjoin,
        })
        # rendering
        lines = []
        text = ''
        dtwidth = indent
        for c in e.getchildren():
            if (not newline and c.tag == 'dd' and c.text and c.text.strip(stripspace)
                and (width - len('  ') - len(text)) < len(c.text.split(None, 1)[0]) ):
                # Add a newline if first word of dd text won't fit to the right of dt
                kwargs['joiners']['dd'] = nljoin
            else:
                kwargs['joiners']['dd'] = ddjoin
            #
            lines = self.ljoin(lines, c, width, dtwidth=dtwidth, **kwargs)
            #
            if c.tag == 'dt':
                dtwidth = len(lines[-1].text)
        return lines


    # 2.21.  <dt>
    # 
    #    The term being defined in a definition list.
    # 
    #    This element appears as a child element of <dl> (Section 2.20).
    # 
    # 2.21.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this term.
    def render_dt(self, e, width, **kwargs):
        kwargs.pop('dtwidth')        
        indent = kwargs['joiners']['dd'].indent
        join   = len(kwargs['joiners']['dd'].join)
        text = fill(self.inner_text_renderer(e), width=width-3, **kwargs)
        if len(text) < indent:
            text = text+' '*max(0, indent-join-len(text))
        return mklines(text, e)


    # 2.22.  <em>
    # 
    #    Indicates text that is semantically emphasized.  Text enclosed within
    #    this element will be displayed as italic after processing.  This
    #    element can be combined with other character formatting elements, and
    #    the formatting will be additive.
    def render_em(self, e, width, **kwargs):
        # Render text with leading and trailing '_'
        text = '_%s_' % self.inner_text_renderer(e)
        text += e.tail or ''
        return text

    # 2.23.  <email>
    # 
    #    Provides an email address.
    # 
    #    The value is expected to be the addr-spec defined in Section 2 of
    #    [RFC6068].
    # 
    #    This element appears as a child element of <address> (Section 2.2).
    # 
    #    Content model: only text content.
    # 
    # 2.23.1.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the author's email address.  This is only
    #    used if the email address has any internationalized components.
    def render_email(self, e, width, **kwargs):
        latin = kwargs.pop('latin', None)
        prev = e.getprevious()
        if prev!=None and prev.tag==e.tag:
            text = fill(", %s"%e.text, width=width, **kwargs) if e.text and latin!=False else ''
        else:
            text = '\n'+fill("Email: %s"%e.text, width=width, **kwargs) if e.text and latin!=False else ''
        return text

    # 2.24.  <eref>
    # 
    #    Represents an "external" link (as specified in the "target"
    #    attribute).  This is useful for embedding URIs in the body of a
    #    document.
    # 
    #    If the <eref> element has non-empty text content, formatters should
    #    use the content as the displayed text that is linked.  Otherwise, the
    #    formatter should use the value of the "target" attribute as the
    #    displayed text.  Formatters will link the displayed text to the value
    #    of the "target" attribute in a manner appropriate for the output
    #    format.
    # 
    #    For example, with an input of:
    # 
    #          This is described at
    #          <eref target="http://www.example.com/reports/r12.html"/>.
    # 
    #    An HTML formatter might generate:
    # 
    #          This is described at
    #          <a href="http://www.example.com/reports/r12.html">
    #          http://www.example.com/reports/r12.html</a>.
    # 
    #    With an input of:
    # 
    #          This is described
    #          <eref target="http://www.example.com/reports/r12.html">
    #          in this interesting report</eref>.
    # 
    #    An HTML formatter might generate:
    # 
    #          This is described
    #          <a href="http://www.example.com/reports/r12.html">
    #          in this interesting report</a>.
    # 
    #    This element appears as a child element of <annotation>
    #    (Section 2.3), <blockquote> (Section 2.10), <c> (Section 3.1), <cref>
    #    (Section 2.16), <dd> (Section 2.18), <dt> (Section 2.21), <em>
    #    (Section 2.22), <li> (Section 2.29), <name> (Section 2.32),
    #    <postamble> (Section 3.5), <preamble> (Section 3.6), <strong>
    #    (Section 2.50), <sub> (Section 2.51), <sup> (Section 2.52), <t>
    #    (Section 2.53), <td> (Section 2.56), <th> (Section 2.58), <tt>
    #    (Section 2.62), and <ttcol> (Section 3.9).
    # 
    #    Content model: only text content.
    # 
    # 2.24.1.  "target" Attribute (Mandatory)
    # 
    #    URI of the link target [RFC3986].  This must begin with a scheme name
    #    (such as "https://") and thus not be relative to the URL of the
    #    current document.
    def render_eref(self, e, width, **kwargs):
        target = e.get('target', '')
        brackets = e.get('brackets', self.attribute_defaults[e.tag]['brackets'])
        if not target:
            self.warn(e, "Expected the 'target' attribute to have a value, but found %s" % (etree.tostring(e), ))
        if   brackets == 'none':
            if e.text and target:
                target = "(%s)" % target
        elif brackets == 'angle':
            target = "<%s>" % target                
        else:
            self.warn(e, 'Unexpected attribute value in <eref>: brackets="%s"' % brackets)
        text = ' '.join([ t for t in [e.text, target] if t ])
        text += e.tail or ''
        return text
            

    # 2.25.  <figure>
    # 
    #    Contains a figure with a caption with the figure number.  If the
    #    element contains a <name> element, the caption will also show that
    #    name.
    # 
    #    This element appears as a child element of <aside> (Section 2.6),
    #    <blockquote> (Section 2.10), <dd> (Section 2.18), <li>
    #    (Section 2.29), <section> (Section 2.46), <td> (Section 2.56), and
    #    <th> (Section 2.58).
    # 
    #    Content model:
    # 
    #    In this order:
    # 
    #    1.  One optional <name> element (Section 2.32)
    # 
    #    2.  Optional <iref> elements (Section 2.27)
    # 
    #    3.  One optional <preamble> element (Section 3.6)
    # 
    #    4.  In any order, but at least one of:
    # 
    #        *  <artwork> elements (Section 2.5)
    # 
    #        *  <sourcecode> elements (Section 2.48)
    # 
    #    5.  One optional <postamble> element (Section 3.5)
    # 
    # 2.25.1.  "align" Attribute
    # 
    #    Deprecated.
    # 
    #    Note: does not affect title or <artwork> alignment.
    # 
    #    Allowed values:
    # 
    #    o  "left" (default)
    # 
    #    o  "center"
    # 
    #    o  "right"
    # 
    # 2.25.2.  "alt" Attribute
    # 
    #    Deprecated.  If the goal is to provide a single URI for a reference,
    #    use the "target" attribute in <reference> instead.
    # 
    # 2.25.3.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this figure.
    # 
    # 2.25.4.  "height" Attribute
    # 
    #    Deprecated.
    # 
    # 2.25.5.  "src" Attribute
    # 
    #    Deprecated.
    # 
    # 2.25.6.  "suppress-title" Attribute
    # 
    #    Deprecated.
    # 
    #    Allowed values:
    # 
    #    o  "true"
    # 
    #    o  "false" (default)
    # 
    # 2.25.7.  "title" Attribute
    # 
    #    Deprecated.  Use <name> instead.
    # 
    # 2.25.8.  "width" Attribute
    # 
    #    Deprecated.
    def render_figure(self, e, width, **kwargs):
        kwargs['joiners'].update({
            'name':         Joiner(': ', 0, 0, False, False),
            'artset':       Joiner('', 0, 0, False, False),
            'artwork':      Joiner('', 0, 0, False, False),
            'sourcecode':   Joiner('', 0, 0, False, False),
        })
        #
        pn = e.get('pn')
        num = pn.split('-')[1].capitalize()
        children = e.getchildren()
        title = "Figure %s" % (num, )
        if len(children) and children[0].tag == 'name':
            name = children[0]
            children = children[1:]
            title = self.tjoin(title, name, width, **kwargs)
        lines = []
        for c in children:
            lines = self.ljoin(lines, c, width, **kwargs)
        title = '\n'+center(title, width).rstrip(stripspace)
        lines += mklines(title, e)
        return lines

    # 2.26.  <front>
    # 
    #    Represents the "front matter": metadata (such as author information),
    #    the Abstract, and additional notes.
    # 
    #    A <front> element may have more than one <seriesInfo> element.  A
    #    <seriesInfo> element determines the document number (for RFCs) or
    #    name (for Internet-Drafts).  Another <seriesInfo> element determines
    #    the "maturity level" (defined in [RFC2026]), using values of "std"
    #    for "Standards Track", "bcp" for "BCP", "info" for "Informational",
    #    "exp" for "Experimental", and "historic" for "Historic".  The "name"
    #    attributes of those multiple <seriesInfo> elements interact as
    #    described in Section 2.47.
    # 
    #    This element appears as a child element of <reference> (Section 2.40)
    #    and <rfc> (Section 2.45).
    # 
    #    Content model:
    # 
    #    In this order:
    # 
    # ...
    # 
    def render_front(self, e, width, **kwargs):
        if e.getparent().tag == 'reference':
            return self.render_reference_front(e, width, **kwargs)
        else:
            text = '\n\n\n\n' + self.render_first_page_top(e, width, **kwargs) + '\n'
            lines = mklines(text, e)
            for c in e.getchildren():
                if c.tag in ['title', 'seriesInfo', 'author', 'date', 'area', 'workgroup', 'keyword', etree.PI, etree.Comment, ]:
                    # handled in render_first_page_top() or discarded
                    continue
                res = self.render(c, width, **kwargs)
                lines += blankline() + res
            return lines

    def render_first_page_top(self, e, width, **kwargs):
        def join_cols(left, right):
            "Join left and right columns of page top into page top text"
            l = max(len(left), len(right))
            left  += ['']*(l-len(left))
            right += ['']*(l-len(right))
            lines = []
            t = len(left)
            for i in range(t):
                l = left[i]
                r = right[i]
                #assert displength(l)+displength(r)<70
                w = 72-displength(l)-displength(r)
                lines.append(l+' '*w+r)
            return '\n'.join(lines).rstrip(stripspace)+'\n'
        #
        def wrap(label, items, left, right, suffix=''):
            line = '%s%s%s' % (label, items, suffix)
            ll = len(left)
            lr = len(right)
            width = 48 if ll >= lr else min(48, 72-4-len(right[ll]))
            wrapper = textwrap.TextWrapper(width=width, subsequent_indent=' '*len(label))
            return wrapper.wrap(line)
        #
        def normalize(t):
            return re.sub(r',\s*', ', ', t).strip(', ')

        def get_left(front, right):
            "Get front page top left column"
            #left_parts = ['source', 'seriesInfo', 'obsoletes', 'updates', 'category', 'issn', 'expires', ]
            left = []
            if self.root.get('ipr') == 'none':
                for group in front.xpath('./workgroup'):
                    if group.text and group.text.strip(stripspace):
                        found = True
                        left.append(group.text.strip(stripspace))
                return left
            if self.options.rfc:
                # 
                #    There is a set of additional information that is needed at the front
                #    of the RFC.  Historically, this has been presented with the
                #    information below in a left hand column, and the author-related
                #    information described above in the right.
                # 
                #    <document source>  This describes the area where the work originates.
                #       Historically, all RFCs were labeled "Network Working Group".
                #       Network Working Group refers to the original version of today's
                #       IETF when people from the original set of ARPANET sites and
                #       whomever else was interested -- the meetings were open -- got
                #       together to discuss, design, and document proposed protocols
                #       [RFC3].  Here, we obsolete the term "Network Working Group" in
                #       order to indicate the originating stream.
                # 
                #       The <document source> is the name of the RFC stream, as defined in
                #       [RFC4844] and its successors.  At the time of this publication,
                #       the streams, and therefore the possible entries are:
                # 
                #       *  Internet Engineering Task Force
                #       *  Internet Architecture Board
                #       *  Internet Research Task Force
                #       *  Independent Submission
                stream = self.root.get('submissionType')
                left.append(strings.stream_name[stream])
                #
                #    Request for Comments:  <RFC number>  This indicates the RFC number,
                #       assigned by the RFC Editor upon publication of the document.  This
                #       element is unchanged.
                for item in front.iter('seriesInfo'):
                    name  = item.get('name') 
                    value = item.get('value')
                    if name == 'RFC':
                        name = 'Request for Comments'
                    left.append("%s: %s" % (name, value))
                #    <subseries ID> <subseries number>  Some document categories are also
                #       labeled as a subseries of RFCs.  These elements appear as
                #       appropriate for such categories, indicating the subseries and the
                #       documents number within that series.  Currently, there are
                #       subseries for BCPs [RFC2026] and STDs [RFC1311].  These subseries
                #       numbers may appear in several RFCs.  For example, when a new RFC
                #       obsoletes or updates an old one, the same subseries number is
                #       used.  Also, several RFCs may be assigned the same subseries
                #       number: a single STD, for example, may be composed of several
                #       RFCs, each of which will bear the same STD number.  This element
                #       is unchanged.
                category = self.root.get('category', '')
                #
                ## The following code duplicates series info also generated from <seriesInfo>
                ## entries.  Commented out.
                #series_no = self.root.get('seriesNo')
                #if category and category in strings.series_name and series_no:
                #    left.append('%s: %s' % (strings.series_name[category], series_no))
                #else:
                #    pass
                #    [<RFC relation>:<RFC number[s]>]  Some relations between RFCs in the
                #       series are explicitly noted in the RFC header.  For example, a new
                #       RFC may update one or more earlier RFCs.  Currently two
                #       relationships are defined: "Updates" and "Obsoletes" [RFC7322].
                #       Variants like "Obsoleted by" are also used (e.g, in [RFC5143]).
                #       Other types of relationships may be defined by the RFC Editor and
                #       may appear in future RFCs.
                obsoletes = self.root.get('obsoletes')
                if obsoletes:
                    left += wrap('Obsoletes: ', normalize(obsoletes), left, right)
                updates = self.root.get('updates')
                if updates:
                    left += wrap('Updates: ', normalize(updates), left, right)
                
                #    Category: <category>  This indicates the initial RFC document
                #       category of the publication.  These are defined in [RFC2026].
                #       Currently, this is always one of: Standards Track, Best Current
                #       Practice, Experimental, Informational, or Historic.  This element
                #       is unchanged.
                if category:
                    if category in strings.category_name:
                        left.append('Category: %s' % (strings.category_name[category], ))
                    else:
                        self.warn(self.root, "Expected a known category, one of %s, but found '%s'" % (','.join(strings.category_name.keys()), category, ))
                else:
                    self.warn(self.root, "Expected a category, one of %s, but found none" % (','.join(strings.category_name.keys()), ))
                #
                left.append('ISSN: 2070-1721')
                #
            else:
                # Internet-Draft
                found = False
                for group in front.xpath('./workgroup'):
                    if group.text and group.text.strip(stripspace):
                        found = True
                        left.append(group.text.strip(stripspace))
                if not found:
                    left.append('Network Working Group')
                left.append('Internet-Draft')
                #
                category = self.root.get('category', '')
                #
                ## The following code duplicates series info also generated from <seriesInfo>
                ## entries.  Commented out.
                #series_no = self.root.get('seriesNo')
                #if category and series_no and category in strings.series_name:
                #    left.append('%s: %s (if approved)' % (strings.series_name[category], series_no))
                #else:
                #    pass
                #
                obsoletes = self.root.get('obsoletes')
                if obsoletes:
                    left += wrap('Obsoletes: ', normalize(obsoletes), left, right, suffix=' (if approved)')
                updates = self.root.get('updates')
                if updates:
                    left += wrap('Updates: ', normalize(updates), left, right, suffix=' (if approved)')
                #
                if category:
                    if category in strings.category_name:
                        left.append('Intended status: %s' % (strings.category_name[category], ))
                    else:
                        self.warn(self.root, "Expected a known category, one of %s, but found '%s'" % (','.join(strings.category_name.keys()), category, ))
                else:
                    self.warn(self.root, "Expected a category, one of %s, but found none" % (','.join(strings.category_name.keys()), ))
                #
                if self.root.get('ipr') != 'none':
                    exp = get_expiry_date(self.root, self.date)
                    left.append('Expires: %s' % format_date(exp.year, exp.month, exp.day, self.options.legacy_date_format))
            return left
        #
        def get_right(front):
            "Get front page top right column"
            # RFC 7841           RFC Streams, Headers, Boilerplates           May 2016
            # 
            # 3.1.  The Title Page Header
            # 
            #    The information at the front of the RFC includes the name and
            #    affiliation of the authors as well as the RFC publication month and
            #    year.
            # 
            #-------------------------------------------------------------------------
            # 
            # RFC 7322                     RFC Style Guide              September 2014
            # 
            # 4.1.2.  Organization
            # 
            #    The author's organization is indicated on the line following the
            #    author's name.
            # 
            #    For multiple authors, each author name appears on its own line,
            #    followed by that author's organization.  When more than one author is
            #    affiliated with the same organization, the organization can be
            #    "factored out," appearing only once following the corresponding
            #    Author lines.  However, such factoring is inappropriate when it would
            #    force an unacceptable reordering of author names.
            right = []
            auth = namedtuple('author', ['name', 'org'])
            prev = auth(None, '')
            authors = front.xpath('./author')
            for a in authors:
                this = auth(*self.render_author_front(a, **kwargs))
                if right and this.name and this.org and this.org == prev.org:
                    right[-1] = this.name
                    right.append(this.org or '')
                else:
                    if this.name:
                        right.append(this.name)
                    if this.org!=None:
                        right.append(this.org)
                prev = this
            # We don't need show a trailing blank line if the last author has a blank organization
            if prev.org == '':
                right = right[:-1]
            right.append(self.render_date(front.find('./date'), width, **kwargs))
            return right
        #
        # get right first, in order to limit the width of left lines as needed
        right = get_right(e)            
        left  = get_left(e, right)
        #
        first_page_header = join_cols(left, right)
        first_page_header += '\n\n'
        first_page_header += self.render_title_front(e.find('./title'), width, **kwargs)
        return first_page_header

    def render_reference_front(self, e, width, **kwargs):
        return self.default_renderer(e, width, **kwargs)

    # 2.27.  <iref>
    # 
    #    Provides terms for the document's index.
    # 
    #    Index entries can be either regular entries (when just the "item"
    #    attribute is given) or nested entries (by specifying "subitem" as
    #    well), grouped under a regular entry.
    # 
    #    Index entries generally refer to the exact place where the <iref>
    #    element occurred.  An exception is the occurrence as a child element
    #    of <section>, in which case the whole section is considered to be
    #    relevant for that index entry.  In some formats, index entries of
    #    this type might be displayed as ranges.
    # 
    #    When the prep tool is creating index content, it collects the items
    #    in a case-sensitive fashion for both the item and subitem level.
    # 
    #    This element appears as a child element of <annotation>
    #    (Section 2.3), <aside> (Section 2.6), <blockquote> (Section 2.10),
    #    <c> (Section 3.1), <dd> (Section 2.18), <dt> (Section 2.21), <em>
    #    (Section 2.22), <figure> (Section 2.25), <li> (Section 2.29),
    #    <postamble> (Section 3.5), <preamble> (Section 3.6), <section>
    #    (Section 2.46), <strong> (Section 2.50), <sub> (Section 2.51), <sup>
    #    (Section 2.52), <t> (Section 2.53), <table> (Section 2.54), <td>
    #    (Section 2.56), <th> (Section 2.58), <tt> (Section 2.62), and <ttcol>
    #    (Section 3.9).
    # 
    #    Content model: this element does not have any contents.
    def render_iref(self, e, width, **kwargs):
        p = e.getparent()
        self.index_items.append(IndexItem(e.get('item'), e.get('subitem'), p.get('pn'), None))
        return '' if p.tag in ['section', 'figure', 'table', ]  else e.tail or ''

    # 2.27.1.  "item" Attribute (Mandatory)
    # 
    #    The item to include.
    # 
    # 2.27.2.  "primary" Attribute
    # 
    #    Setting this to "true" declares the occurrence as "primary", which
    #    might cause it to be highlighted in the index.  There is no
    #    restriction on the number of occurrences that can be "primary".
    # 
    #    Allowed values:
    # 
    #    o  "true"
    # 
    #    o  "false" (default)
    # 
    # 2.27.3.  "subitem" Attribute
    # 
    #    The subitem to include.


    # 2.28.  <keyword>
    # 
    #    Specifies a keyword applicable to the document.
    # 
    #    Note that each element should only contain a single keyword; for
    #    multiple keywords, the element can simply be repeated.
    # 
    #    Keywords are used both in the RFC Index and in the metadata of
    #    generated document representations.
    # 
    #    This element appears as a child element of <front> (Section 2.26).
    # 
    #    Content model: only text content.


    # 2.29.  <li>
    # 
    #    A list element, used in <ol> and <ul>.
    # 
    #    This element appears as a child element of <ol> (Section 2.34) and
    #    <ul> (Section 2.63).
    # 
    #    Content model:
    # 
    #    Either:
    # 
    #       In any order, but at least one of:
    # 
    #       *  <artwork> elements (Section 2.5)
    # 
    #       *  <dl> elements (Section 2.20)
    # 
    #       *  <figure> elements (Section 2.25)
    # 
    #       *  <ol> elements (Section 2.34)
    # 
    #       *  <sourcecode> elements (Section 2.48)
    # 
    #       *  <t> elements (Section 2.53)
    # 
    #       *  <ul> elements (Section 2.63)
    # 
    #    Or:
    # 
    #       In any order, but at least one of:
    # 
    #       *  Text
    # 
    #       *  <bcp14> elements (Section 2.9)
    # 
    #       *  <cref> elements (Section 2.16)
    # 
    #       *  <em> elements (Section 2.22)
    # 
    #       *  <eref> elements (Section 2.24)
    # 
    #       *  <iref> elements (Section 2.27)
    # 
    #       *  <relref> elements (Section 2.44)
    # 
    #       *  <strong> elements (Section 2.50)
    # 
    #       *  <sub> elements (Section 2.51)
    # 
    #       *  <sup> elements (Section 2.52)
    # 
    #       *  <tt> elements (Section 2.62)
    # 
    #       *  <xref> elements (Section 2.66)
    # 
    # 2.29.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this list item.
    # Text
    def render_li(self, e, width, **kwargs):
        p = e.getparent()
        text = p._initial_text(e, p)
        tt, __ = self.text_or_block_renderer(e, width, **kwargs)
        if isinstance(tt, list):
            lines = stripl(tt)
            if lines and lines[0].elem.tag not in ['artwork', 'figure', 'sourcecode', 'li', ]:
                lines[0].text = text + lines[0].text.lstrip(stripspace)
        else:
            text += tt.lstrip(stripspace)
            lines = mklines(text, e)
        if self.options.pagination:
            for i, l in enumerate(lines):
                if '\u2026' in l.text:
                    lines[i].text = expand_ellipsis(l.text, width)
        return lines

    def get_ol_li_initial_text(self, e, p):
        text = p._format % p._int2str(p._counter)
        text += ' '*(p._padding-len(text))
        p._counter += 1
        return text

    def get_ul_li_initial_text(self, e, p):
        if p._bare:
            text = ''
        else:
            text = p._symbol + '  '
        return text

    # 2.30.  <link>
    # 
    #    A link to an external document that is related to the RFC.
    # 
    #    The following are the supported types of external documents that can
    #    be pointed to in a <link> element:
    # 
    #    o  The current International Standard Serial Number (ISSN) for the
    #       RFC Series.  The value for the "rel" attribute is "item".  The
    #       link should use the form "urn:issn:".
    # 
    #    o  The Digital Object Identifier (DOI) for this document.  The value
    #       for the "rel" attribute is "describedBy".  The link should use the
    #       form specified in [RFC7669]; this is expected to change in the
    #       future.
    # 
    #    o  The Internet-Draft that was submitted to the RFC Editor to become
    #       the published RFC.  The value for the "rel" attribute is
    #       "convertedFrom".  The link should be to an IETF-controlled web
    #       site that retains copies of Internet-Drafts.
    # 
    #    o  A representation of the document offered by the document author.
    #       The value for the "rel" attribute is "alternate".  The link can be
    #       to a personally run web site.
    # 
    #    In RFC production mode, the prep tool needs to check the values for
    #    <link> before an RFC is published.  In draft production mode, the
    #    prep tool might remove some <link> elements during the draft
    #    submission process.
    # 
    #    This element appears as a child element of <rfc> (Section 2.45).
    # 
    #    Content model: this element does not have any contents.
    def render_link(self, e, width, **kwargs):
        return ''

    # 2.30.1.  "href" Attribute (Mandatory)
    # 
    #    The URI of the external document.
    # 
    # 2.30.2.  "rel" Attribute
    # 
    #    The relationship of the external document to this one.  The
    #    relationships are taken from the "Link Relations" registry maintained
    #    by IANA [LINKRELATIONS].


    # 2.31.  <middle>
    # 
    #    Represents the main content of the document.
    # 
    #    This element appears as a child element of <rfc> (Section 2.45).
    # 
    #    Content model:
    # 
    #    One or more <section> elements (Section 2.46)
    def render_middle(self, e, width, **kwargs):
        kwargs['joiners'] = base_joiners
        lines = []
        for c in e.getchildren():
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines

    # 2.32.  <name>
    # 
    #    The name of the section, note, figure, or texttable.  This name can
    #    indicate markup of flowing text (for example, including references or
    #    making some characters use a fixed-width font).
    # 
    #    This element appears as a child element of <figure> (Section 2.25),
    #    <note> (Section 2.33), <references> (Section 2.42), <section>
    #    (Section 2.46), <table> (Section 2.54), and <texttable>
    #    (Section 3.8).
    # 
    #    Content model:
    # 
    #    In any order:
    # 
    #    o  Text
    # 
    #    o  <cref> elements (Section 2.16)
    # 
    #    o  <eref> elements (Section 2.24)
    # 
    #    o  <relref> elements (Section 2.44)
    # 
    #    o  <tt> elements (Section 2.62)
    # 
    #    o  <xref> elements (Section 2.66)
    def render_name(self, e, width, **kwargs):
        hang=kwargs['joiners'][e.tag].hang
        return fill(self.inner_text_renderer(e).strip(stripspace), width=width, hang=hang)

    # 2.33.  <note>
    # 
    #    Creates an unnumbered, titled block of text that appears after the
    #    Abstract.
    # 
    #    It is usually used for additional information to reviewers (Working
    #    Group information, mailing list, ...) or for additional publication
    #    information such as "IESG Notes".
    # 
    #    This element appears as a child element of <front> (Section 2.26).
    # 
    #    Content model:
    # 
    #    In this order:
    # 
    #    1.  One optional <name> element (Section 2.32)
    # 
    #    2.  In any order, but at least one of:
    # 
    #        *  <dl> elements (Section 2.20)
    # 
    #        *  <ol> elements (Section 2.34)
    # 
    #        *  <t> elements (Section 2.53)
    # 
    #        *  <ul> elements (Section 2.63)
    def render_note(self, e, width, **kwargs):
        kwargs['joiners'].update(
            {
                None:       Joiner('\n\n', 3, 0, False, False),
                'name':     Joiner(': ', 0, 0, False, False),
            }
        )
        lines = []
        if e[0].tag != 'name':
            lines.append(Line("Note", e))
        for c in e.getchildren():
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines

    # 2.33.1.  "removeInRFC" Attribute
    # 
    #    If set to "true", this note is marked in the prep tool with text
    #    indicating that it should be removed before the document is published
    #    as an RFC.  That text will be "This note is to be removed before
    #    publishing as an RFC."
    # 
    #    Allowed values:
    # 
    #    o  "true"
    # 
    #    o  "false" (default)
    # 
    # 2.33.2.  "title" Attribute
    # 
    #    Deprecated.  Use <name> instead.


    # 2.34.  <ol>
    # 
    #    An ordered list.  The labels on the items will be either a number or
    #    a letter, depending on the value of the style attribute.
    # 
    #    This element appears as a child element of <abstract> (Section 2.1),
    #    <aside> (Section 2.6), <blockquote> (Section 2.10), <dd>
    #    (Section 2.18), <li> (Section 2.29), <note> (Section 2.33), <section>
    #    (Section 2.46), <td> (Section 2.56), and <th> (Section 2.58).
    # 
    #    Content model:
    # 
    #    One or more <li> elements (Section 2.29)
    # 
    # 2.34.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the list.
    # 
    # 2.34.2.  "group" Attribute
    # 
    #    When the prep tool sees an <ol> element with a "group" attribute that
    #    has already been seen, it continues the numbering of the list from
    #    where the previous list with the same group name left off.  If an
    #    <ol> element has both a "group" attribute and a "start" attribute,
    #    the group's numbering is reset to the given start value.
    # 
    # 2.34.3.  "spacing" Attribute
    # 
    #    Defines whether or not there is a blank line between entries.
    #    spacing="normal" indicates a single blank line, while
    #    spacing="compact" indicates no space between.
    # 
    #    Allowed values:
    # 
    #    o  "normal" (default)
    # 
    #    o  "compact"
    # 
    # 2.34.4.  "start" Attribute
    # 
    #    The ordinal value at which to start the list.  This defaults to "1"
    #    and must be an integer of 0 or greater.
    # 
    # 2.34.5.  "type" Attribute
    # 
    #    The type of the labels on list items.  If the length of the type
    #    value is 1, the meaning is the same as it is for HTML:
    # 
    #    a  Lowercase letters (a, b, c, ...)
    # 
    #    A  Uppercase letters (A, B, C, ...)
    # 
    #    1  Decimal numbers (1, 2, 3, ...)
    # 
    #    i  Lowercase Roman numerals (i, ii, iii, ...)
    # 
    #    I  Uppercase Roman numerals (I, II, III, ...)
    # 
    #    For types "a" and "A", after the 26th entry, the numbering starts at
    #    "aa"/"AA", then "ab"/"AB", and so on.
    # 
    #    If the length of the type value is greater than 1, the value must
    #    contain a percent-encoded indicator and other text.  The value is a
    #    free-form text that allows counter values to be inserted using a
    #    "percent-letter" format.  For instance, "[REQ%d]" generates labels of
    #    the form "[REQ1]", where "%d" inserts the item number as a decimal
    #    number.
    # 
    #    The following formats are supported:
    # 
    #    %c Lowercase letters (a, b, c, ...)
    # 
    #    %C Uppercase letters (A, B, C, ...)
    # 
    #    %d Decimal numbers (1, 2, 3, ...)
    # 
    #    %i Lowercase Roman numerals (i, ii, iii, ...)
    # 
    #    %I Uppercase Roman numerals (I, II, III, ...)
    # 
    #    %% Represents a percent sign
    # 
    #    Other formats are reserved for future use.  Only one percent encoding
    #    other than "%%" is allowed in a type string.
    # 
    #    It is an error for the type string to be empty.  For bulleted lists,
    #    use the <ul> element.  For lists that have neither bullets nor
    #    numbers, use the <ul> element with the 'empty="true"' attribute.
    # 
    #    If no type attribute is given, the default type is the same as
    #    "type='%d.'".
    def render_ol(self, e, width, **kwargs):
        # setup and validation
        start = e.get('start')
        if not start.isdigit():
            self.warn(e, "Expected a numeric value for the 'start' attribute, but found %s" % (etree.tostring(e), ))
            start = '1'
        e._counter = int(start)
        #
        type = e.get('type')
        if not type:
            self.warn(e, "Expected the 'type' attribute to have a string value, but found %s" % (etree.tostring(e), ))
            type = '1'
        e._type = type
        if len(type) > 1:
            formspec = re.search('%([cCdiIoOxX])', type)
            if formspec:
                fchar = formspec.group(1)
                fspec = formspec.group(0)
                e._format = type.replace(fspec, '%s')
            else:
                self.err(e, "Expected an <ol> format specification of '%%' followed by upper- or lower-case letter, of one of c,d,i,o,x; but found '%s'" % (type, ))
                fchar = 'd'
                e._format = '%s'
        else:
            fchar = type
            e._format = '%s.'
        e._int2str = ol_style_formatter[fchar]
        e._initial_text = self.get_ol_li_initial_text
        #
        compact = e.get('spacing') == 'compact'
        ljoin  = '\n' if compact else '\n\n'
        #
        indent = len(e._format % (' '*num_width(fchar, len(list(e))))) + len('  ')
        indent = int( e.get('indent') or indent )
        e._padding = indent
        kwargs['joiners'].update({
            None:   Joiner(ljoin, indent, 0, False, False),
            'li':   Joiner(ljoin, 0, 0, False, False),
            't':    Joiner(ljoin, indent, 0, False, False),
        })
        # rendering
        lines = []
        for c in e.getchildren():
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines

    # 2.35.  <organization>
    # 
    #    Specifies the affiliation [RFC7322] of an author.
    # 
    #    This information appears both in the "Author's Address" section and
    #    on the front page (see [RFC7322] for more information).  If the value
    #    is long, an abbreviated variant can be specified in the "abbrev"
    #    attribute.
    # 
    #    This element appears as a child element of <author> (Section 2.7).
    # 
    #    Content model: only text content.
    # 
    # 2.35.1.  "abbrev" Attribute
    # 
    #    Abbreviated variant.
    # 
    # 2.35.2.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the organization's name.
    def render_front_organization(self, e, **kwargs):
        author = e.getparent()
        org, ascii = short_org_name_set(author)
        if ascii:
            org += ' (%s)' % ascii.strip(stripspace)
        return org

    def render_organization(self, e, width, **kwargs):
        text = ''
        if e != None:
            org = e.text or ''
            org = org.strip(stripspace)
            if org and not is_script(org, 'Latin'):
                ascii = e.get('ascii')
                if ascii and ascii != org:
                    org += ' (%s)' % ascii.strip(stripspace)
            text = fill(org, width=width, **kwargs)
        return text

    # 2.36.  <phone>
    # 
    #    Represents a phone number.
    # 
    #    The value is expected to be the scheme-specific part of a "tel" URI
    #    (and so does not include the prefix "tel:"), using the
    #    "global-number-digits" syntax.  See Section 3 of [RFC3966] for
    #    details.
    # 
    #    This element appears as a child element of <address> (Section 2.2).
    # 
    #    Content model: only text content.
    def render_phone(self, e, width, **kwargs):
        latin = kwargs.pop('latin', None)
        text = fill("Phone: %s"%e.text, width=width, **kwargs) if e.text and latin!=False else ''
        return text

    # 2.37.  <postal>
    # 
    #    Contains optional child elements providing postal information.  These
    #    elements will be displayed in an order that is specific to
    #    formatters.  A postal address can contain only a set of <street>,
    #    <city>, <region>, <code>, and <country> elements, or only an ordered
    #    set of <postalLine> elements, but not both.
    # 
    #    This element appears as a child element of <address> (Section 2.2).
    # 
    #    Content model:
    # 
    #    Either:
    # 
    #       In any order:
    # 
    #       *  <city> elements (Section 2.13)
    # 
    #       *  <code> elements (Section 2.14)
    # 
    #       *  <country> elements (Section 2.15)
    # 
    #       *  <region> elements (Section 2.43)
    # 
    #       *  <street> elements (Section 2.49)
    # 
    #    Or:
    # 
    #       One or more <postalLine> elements (Section 2.38)
    def render_postal(self, e, width, **kwargs):
        latin = kwargs.pop('latin', False)
        adr = get_normalized_address_info(self, e, latin=latin)
        for k in adr:
            if isinstance(adr[k], list):
                adr[k] = '\n'.join(adr[k])
        set_joiners(kwargs, { None: Joiner('\n', 0, 0, False, False), })
        if adr:
            if all(is_script(v, 'Latin') for v in adr.values() if v):
                latin = True
            try:
                text = format_address(adr, latin=latin)
                text = text.strip(stripspace)+'\n\n'
                return mklines(text, e)
            except:
                debug.pprint('adr')
                raise
        else:
            author = e.getparent().getparent()
            text = self.render_author_name(author, width, **kwargs)
            if e.find('./postalLine') != None:
                lines = []
                for c in e.getchildren():
                    lines = self.ljoin(lines, c, width, **kwargs)
            else:
                lines = []
                for street in e.findall('street'):
                    if street.text:
                        lines.append(Line(street.text, street))
                cityline = []
                city = e.find('city')
                if city is not None and city.text:
                    cityline.append(city.text)
                region = e.find('region')
                if region is not None and region.text:
                    if len(cityline) > 0: cityline.append(', ');
                    cityline.append(region.text)
                code = e.find('code')
                if code is not None and code.text:
                    if len(cityline) > 0: cityline.append('  ');
                    cityline.append(code.text)
                if len(cityline) > 0:
                    lines.append(Line(''.join(cityline), e))
                country = e.find('country')
                if country is not None and country.text:
                    lines.append(Line(country.text, country))
            lines.append( Line('', e) )
            return lines

    # 2.38.  <postalLine>
    # 
    #    Represents one line of a postal address.  When more than one
    #    <postalLine> is given, the prep tool emits them in the order given.
    # 
    #    This element appears as a child element of <postal> (Section 2.37).
    # 
    #    Content model: only text content.
    # 
    # 2.38.1.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the text in the address line.
    def render_postalline(self, e, width, **kwargs):
        text = fill(self.inner_text_renderer(e), width=width, **kwargs)
        return text

    # 2.39.  <refcontent>
    # 
    #    Text that should appear between the title and the date of a
    #    reference.  The purpose of this element is to prevent the need to
    #    abuse <seriesInfo> to get such text in a reference.
    # 
    #    For example:
    # 
    #    <reference anchor="April1">
    #      <front>
    #        <title>On Being A Fool</title>
    #        <author initials="K." surname="Phunny" fullname="Knot Phunny"/>
    #        <date year="2000" month="April"/>
    #      </front>
    #      <refcontent>Self-published pamphlet</refcontent>
    #    </reference>
    # 
    #    would render as:
    # 
    #       [April1]     Phunny, K., "On Being A Fool", Self-published
    #                    pamphlet, April 2000.
    # 
    #    This element appears as a child element of <reference>
    #    (Section 2.40).
    # 
    #    Content model:
    # 
    #    In any order:
    # 
    #    o  Text
    # 
    #    o  <bcp14> elements (Section 2.9)
    # 
    #    o  <em> elements (Section 2.22)
    # 
    #    o  <strong> elements (Section 2.50)
    # 
    #    o  <sub> elements (Section 2.51)
    # 
    #    o  <sup> elements (Section 2.52)
    # 
    #    o  <tt> elements (Section 2.62)
    def render_refcontent(self, e, width, **kwargs):
        text = self.inner_text_renderer(e)
        return text

    # 2.40.  <reference>
    # 
    #    Represents a bibliographic reference.
    # 
    #    This element appears as a child element of <referencegroup>
    #    (Section 2.41) and <references> (Section 2.42).
    # 
    #    Content model:
    # 
    #    In this order:
    # 
    #    1.  One <front> element (Section 2.26)
    # 
    #    2.  In any order:
    # 
    #        *  <annotation> elements (Section 2.3)
    # 
    #        *  <format> elements (Section 3.3)
    # 
    #        *  <refcontent> elements (Section 2.39)
    # 
    #        *  <seriesInfo> elements (Section 2.47; deprecated in this
    #           context)
    # 
    # 2.40.1.  "anchor" Attribute (Mandatory)
    # 
    #    Document-wide unique identifier for this reference.  Usually, this
    #    will be used both to "label" the reference in the "References"
    #    section and as an identifier in links to this reference entry.
    # 
    # 2.40.2.  "quoteTitle" Attribute
    # 
    #    Specifies whether or not the title in the reference should be quoted.
    #    This can be used to prevent quoting, such as on errata.
    # 
    #    Allowed values:
    # 
    #    o  "true" (default)
    # 
    #    o  "false"
    # 
    # 2.40.3.  "target" Attribute
    # 
    #    Holds the URI for the reference.
    def render_reference(self, e, width, **kwargs):
        # rendering order: authors, title, seriesInfo, date, target, annotation
        p = e.getparent()
        if p.tag == 'referencegroup':
            label = ''
        else:
            label = self.refname_mapping[e.get('anchor')]
            label = ('[%s]' % label).ljust(11)
        # ensure the desired ordering
        elements = []
        for ctag in ('title', 'refcontent', 'stream', 'seriesInfo', 'date',):
            for c in e.iterdescendants(ctag):
                if p.tag == 'referencegroup' and c.tag == 'seriesInfo' and c.get('name') == 'DOI':
                    # Don't render DOI within a reference group
                    continue              
                elements.append(c)
        if p.tag != 'referencegroup':
            target = e.get('target')
            if target:
                url = self.element('refcontent')
                url.text = '<%s>' % target
                elements.append(url)
        set_joiners(kwargs, {
            None:           Joiner(', ', 0, 0, False, False),
            'annotation':   Joiner('  ', 0, 0, False, False),
        })
        width = width-11
        text = self.render_authors(e, width, **kwargs)
        for c in elements:
            text = self.tjoin(text, c, width, **kwargs)
        text += '.'
        for ctag in ('annotation', ):
            for c in e.iterdescendants(ctag):
                text = self.tjoin(text, c, width, **kwargs)
        text = fill(text, width=width, fix_sentence_endings=False, keep_url=True, **kwargs).lstrip(stripspace)
        
        text = indent(text, 11, 0)
        if p.tag != 'referencegroup':
            if len(label.strip(stripspace)) > 10:
                label += '\n'
            else:
                text = text.lstrip(stripspace)
        text = label + text
        lines = mklines(text, e)
        return lines



    # 2.41.  <referencegroup>
    # 
    #    Represents a list of bibliographic references that will be
    #    represented as a single reference.  This is most often used to
    #    reference STDs and BCPs, where a single reference (such as "BCP 9")
    #    may encompass more than one RFC.
    # 
    #    This element appears as a child element of <references>
    #    (Section 2.42).
    # 
    #    Content model:
    # 
    #    One or more <reference> elements (Section 2.40)
    # 
    # 2.41.1.  "anchor" Attribute (Mandatory)
    # 
    #    Document-wide unique identifier for this reference group.  Usually,
    #    this will be used both to "label" the reference group in the
    #    "References" section and as an identifier in links to this reference
    #    entry.
    def render_referencegroup(self, e, width, **kwargs):
        kwargs['joiners'].update({
            'reference':    Joiner('\n\n', 0, 0, False, False),
            't':            Joiner('\n\n', 11, 0, False, False),
        })
        label = self.refname_mapping[e.get('anchor')]
        label = ('[%s]' % label).ljust(11)
        lines = []
        for c in e.getchildren():
            lines = self.ljoin(lines, c, width, **kwargs)
        target = e.get('target')
        if target:
            t = self.element('t')
            t.text = '<%s>' % target
            lines = self.ljoin(lines, t, width, **kwargs)
        if len(label.strip(stripspace)) > 10:
            lines = [ Line(label, e) ] + lines
        else:
            lines[0].text = label + lines[0].text.lstrip(stripspace)
        return lines

    # 2.42.  <references>
    # 
    #    Contains a set of bibliographic references.
    # 
    #    In the early days of the RFC Series, there was only one "References"
    #    section per RFC.  This convention was later changed to group
    #    references into two sets, "Normative" and "Informative", as described
    #    in [RFC7322].  This vocabulary supports the split with the <name>
    #    child element.  In general, the title should be either "Normative
    #    References" or "Informative References".
    # 
    #    This element appears as a child element of <back> (Section 2.8).
    # 
    #    Content model:
    # 
    #    In this order:
    # 
    #    1.  One optional <name> element (Section 2.32)
    # 
    #    2.  In any order:
    # 
    #        *  <reference> elements (Section 2.40)
    # 
    #        *  <referencegroup> elements (Section 2.41)
    # 
    # 2.42.1.  "anchor" Attribute
    # 
    #    An optional user-supplied identifier for this set of references.
    # 
    # 2.42.2.  "title" Attribute
    # 
    #    Deprecated.  Use <name> instead.
    def render_references(self, e, width, **kwargs):
        self.part = e.tag
        kwargs['joiners'].update({
            None:           Joiner('\n\n', 3, 0, False, False),
            'name':         Joiner('  '  , 0, 0, False, False),
            'reference':    Joiner('\n\n', 3, 0, False, False),
            'references':   Joiner('\n\n', 0, 0, False, False),
        })
        lines = []
        if e.find('name') != None:
            if self.check_refs_numbered():
                pn = e.get('pn')
                text = pn.split('-',1)[1].replace('-', ' ').title() +'.'
            else:
                text = ''
            lines += mklines(self.tjoin(text, e[0], width, **kwargs), e)
        for c in e[1:]:
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines


    # 2.43.  <region>
    # 
    #    Provides the region name in a postal address.
    # 
    #    This element appears as a child element of <postal> (Section 2.37).
    # 
    #    Content model: only text content.
    # 
    # 2.43.1.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the region name.
    render_region = null_renderer       # handled in render_address

    # 2.44.  <relref>
    # 
    #    Represents a link to a specific part of a document that appears in a
    #    <reference> element.  Formatters that have links (such as HTML and
    #    PDF) render <relref> elements as external hyperlinks to the specified
    #    part of the reference, creating the link target by combining the base
    #    URI from the <reference> element with the "relative" attribute from
    #    this element.  The "target" attribute is required, and it must be the
    #    anchor of a <reference> element.
    # 
    #    The "section" attribute is required, and the "relative" attribute is
    #    optional.  If the reference is not an RFC or Internet-Draft that is
    #    in the v3 format, the element needs to have a "relative" attribute;
    #    in this case, the value of the "section" attribute is ignored.
    # 
    #    An example of the <relref> element with text content might be:
    # 
    #          See
    #          <relref section="2.3" target="RFC9999" displayFormat="bare">
    #          the protocol overview</relref>
    #          for more information.
    # 
    #    An HTML formatter might generate:
    # 
    #          See
    #          <a href="http://www.rfc-editor.org/rfc/rfc9999.html#s-2.3">
    #          the protocol overview</a>
    #          for more information.
    # 
    #    Note that the URL in the above example might be different when the
    #    RFC Editor deploys the v3 format.
    # 
    #    This element appears as a child element of <annotation>
    #    (Section 2.3), <blockquote> (Section 2.10), <cref> (Section 2.16),
    #    <dd> (Section 2.18), <dt> (Section 2.21), <em> (Section 2.22), <li>
    #    (Section 2.29), <name> (Section 2.32), <preamble> (Section 3.6),
    #    <strong> (Section 2.50), <sub> (Section 2.51), <sup> (Section 2.52),
    #    <t> (Section 2.53), <td> (Section 2.56), <th> (Section 2.58), and
    #    <tt> (Section 2.62).
    # 
    #    Content model: only text content.
    # 
    # 2.44.1.  "displayFormat" Attribute
    # 
    #    This attribute is used to signal formatters what the desired format
    #    of the relative reference should be.  Formatters for document types
    #    that have linking capability should wrap each part of the displayed
    #    text in hyperlinks.  If there is content in the <relref> element,
    #    formatters will ignore the value of this attribute.
    # 
    #    "of"
    # 
    #       A formatter should display the relative reference as the word
    #       "Section" followed by a space, the contents of the "section"
    #       attribute followed by a space, the word "of", another space, and
    #       the value from the "target" attribute enclosed in square brackets.
    # 
    #       For example, with an input of:
    # 
    #          See
    #          <relref section="2.3" target="RFC9999" displayFormat="of"/>
    #          for an overview.
    # 
    #       An HTML formatter might generate:
    # 
    #          See
    #          <a href="http://www.rfc-editor.org/info/rfc9999#s-2.3">
    #          Section 2.3</a> of
    #          [<a href="#RFC9999">RFC9999</a>]
    #          for an overview.
    # 
    #       Note that "displayFormat='of'" is the default for <relref>, so it
    #       does not need to be given in a <relref> element if that format is
    #       desired.
    # 
    #    "comma"
    # 
    #       A formatter should display the relative reference as the value
    #       from the "target" attribute enclosed in square brackets, a comma,
    #       a space, the word "Section" followed by a space, and the "section"
    #       attribute.
    # 
    #       For example, with an input of:
    # 
    #          See
    #          <relref section="2.3" target="RFC9999" displayFormat="comma"/>,
    #          for an overview.
    # 
    #       An HTML formatter might generate:
    # 
    #          See
    #          [<a href="#RFC9999">RFC9999</a>],
    #          <a href="http://www.rfc-editor.org/info/rfc9999#s-2.3">
    #          Section 2.3</a>, for an overview.
    # 
    #    "parens"
    # 
    #       A formatter should display the relative reference as the value
    #       from the "target" attribute enclosed in square brackets, a space,
    #       a left parenthesis, the word "Section" followed by a space, the
    #       "section" attribute, and a right parenthesis.
    # 
    #       For example, with an input of:
    # 
    #          See
    #          <relref section="2.3" target="RFC9999" displayFormat="parens"/>
    #          for an overview.
    # 
    #       An HTML formatter might generate:
    # 
    #          See
    #          [<a href="#RFC9999">RFC9999</a>]
    #          (<a href="http://www.rfc-editor.org/info/rfc9999#s-2.3">
    #          Section 2.3</a>)
    #          for an overview.
    # 
    #    "bare"
    # 
    #       A formatter should display the relative reference as the contents
    #       of the "section" attribute and nothing else.  This is useful when
    #       there are multiple relative references to a single base reference.
    # 
    #       For example:
    # 
    #          See Sections
    #          <relref section="2.3" target="RFC9999" displayFormat="bare"/>
    #          and
    #          <relref section="2.4" target="RFC9999" displayFormat="of"/>
    #          for an overview.
    # 
    #       An HTML formatter might generate:
    # 
    #          See Sections
    #          <a href="http://www.rfc-editor.org/info/rfc9999#s-2.3">
    #          2.3</a>
    #          and
    #          <a href="http://www.rfc-editor.org/info/rfc9999#s-2.4">
    #          Section 2.4</a> of
    #          [<a href="#RFC9999">RFC9999</a>]
    #          for an overview.
    # 
    #    Allowed values:
    # 
    #    o  "of" (default)
    # 
    #    o  "comma"
    # 
    #    o  "parens"
    # 
    #    o  "bare"
    # 
    # 2.44.2.  "relative" Attribute
    # 
    #    Specifies a relative reference from the URI in the target reference.
    #    This value must include whatever leading character is needed to
    #    create the relative reference; typically, this is "#" for HTML
    #    documents.
    # 
    # 2.44.3.  "section" Attribute (Mandatory)
    # 
    #    Specifies a section of the target reference.  If the reference is not
    #    an RFC or Internet-Draft in the v3 format, it is an error.
    # 
    # 2.44.4.  "target" Attribute (Mandatory)
    # 
    #    The anchor of the reference for this element.  If this value is not
    #    an anchor to a <reference> or <referencegroup> element, it is an
    #    error.  If the reference at the target has no URI, it is an error.
    def render_relref(self, e, width, **kwargs):
        return self.render_xref(e, width, **kwargs)

    # 2.45.  <rfc>
    # 
    #    This is the root element of the xml2rfc vocabulary.
    # 
    #    Content model:
    # 
    #    In this order:
    # 
    #    1.  Optional <link> elements (Section 2.30)
    # 
    #    2.  One <front> element (Section 2.26)
    # 
    #    3.  One <middle> element (Section 2.31)
    # 
    #    4.  One optional <back> element (Section 2.8)
    def render_rfc(self, e, width, **kwargs):
        self.part = e.tag
        lines = []
        for c in e.getchildren():
            if c.tag in (etree.PI, etree.Comment):
                continue
            self.part = c.tag
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines

    # 2.45.1.  "category" Attribute
    # 
    #    Deprecated; instead, use the "name" attribute in <seriesInfo>.
    # 
    # 2.45.2.  "consensus" Attribute
    # 
    #    Affects the generated boilerplate.  Note that the values of "no" and
    #    "yes" are deprecated and are replaced by "false" (the default) and
    #    "true".
    # 
    #    See [RFC7841] for more information.
    # 
    #    Allowed values:
    # 
    #    o  "no"
    # 
    #    o  "yes"
    # 
    #    o  "false" (default)
    # 
    #    o  "true"
    # 
    # 2.45.3.  "docName" Attribute
    # 
    #    Deprecated; instead, use the "value" attribute in <seriesInfo>.
    # 
    # 2.45.4.  "indexInclude" Attribute
    # 
    #    Specifies whether or not a formatter is requested to include an index
    #    in generated files.  If the source file has no <iref> elements, an
    #    index is never generated.  This option is useful for generating
    #    documents where the source document has <iref> elements but the
    #    author no longer wants an index.
    # 
    #    Allowed values:
    # 
    #    o  "true" (default)
    # 
    #    o  "false"
    # 
    # 2.45.5.  "ipr" Attribute
    # 
    #    Represents the Intellectual Property status of the document.  See
    #    Appendix A.1 for details.
    # 
    # 2.45.6.  "iprExtract" Attribute
    # 
    #    Identifies a single section within the document for which extraction
    #    "as is" is explicitly allowed (only relevant for historic values of
    #    the "ipr" attribute).
    # 
    # 2.45.7.  "number" Attribute
    # 
    #    Deprecated; instead, use the "value" attribute in <seriesInfo>.
    # 
    # 2.45.8.  "obsoletes" Attribute
    # 
    #    A comma-separated list of RFC numbers or Internet-Draft names.
    # 
    #    The prep tool will parse the attribute value so that incorrect
    #    references can be detected.
    # 
    # 2.45.9.  "prepTime" Attribute
    # 
    #    The date that the XML was processed by a prep tool.  This is included
    #    in the XML file just before it is saved to disk.  The value is
    #    formatted using the "date-time" format defined in Section 5.6 of
    #    [RFC3339].  The "time-offset" should be "Z".
    # 
    # 2.45.10.  "seriesNo" Attribute
    # 
    #    Deprecated; instead, use the "value" attribute in <seriesInfo>.
    # 
    # 2.45.11.  "sortRefs" Attribute
    # 
    #    Specifies whether or not the prep tool will sort the references in
    #    each reference section.
    # 
    #    Allowed values:
    # 
    #    o  "true"
    # 
    #    o  "false" (default)
    # 
    # 2.45.12.  "submissionType" Attribute
    # 
    #    The document stream, as described in [RFC7841].  (The RFC Series
    #    Editor may change the list of allowed values in the future.)
    # 
    #    Allowed values:
    # 
    #    o  "IETF" (default)
    # 
    #    o  "IAB"
    # 
    #    o  "IRTF"
    # 
    #    o  "independent"
    # 
    # 2.45.13.  "symRefs" Attribute
    # 
    #    Specifies whether or not a formatter is requested to use symbolic
    #    references (such as "[RFC2119]").  If the value for this is "false",
    #    the references come out as numbers (such as "[3]").
    # 
    #    Allowed values:
    # 
    #    o  "true" (default)
    # 
    #    o  "false"
    # 
    # 2.45.14.  "tocDepth" Attribute
    # 
    #    Specifies the number of levels of headings that a formatter is
    #    requested to include in the table of contents; the default is "3".
    # 
    # 2.45.15.  "tocInclude" Attribute
    # 
    #    Specifies whether or not a formatter is requested to include a table
    #    of contents in generated files.
    # 
    #    Allowed values:
    # 
    #    o  "true" (default)
    # 
    #    o  "false"
    # 
    # 2.45.16.  "updates" Attribute
    # 
    #    A comma-separated list of RFC numbers or Internet-Draft names.
    # 
    #    The prep tool will parse the attribute value so that incorrect
    #    references can be detected.
    # 
    # 2.45.17.  "version" Attribute
    # 
    #    Specifies the version of xml2rfc syntax used in this document.  The
    #    only expected value (for now) is "3".


    # 2.46.  <section>
    # 
    #    Represents a section (when inside a <middle> element) or an appendix
    #    (when inside a <back> element).
    # 
    #    Subsections are created by nesting <section> elements inside
    #    <section> elements.  Sections are allowed to be empty.
    # 
    #    This element appears as a child element of <back> (Section 2.8),
    #    <boilerplate> (Section 2.11), <middle> (Section 2.31), and <section>
    #    (Section 2.46).
    # 
    #    Content model:
    # 
    #    In this order:
    # 
    #    1.  One optional <name> element (Section 2.32)
    # 
    #    2.  In any order:
    # 
    #        *  <artwork> elements (Section 2.5)
    # 
    #        *  <aside> elements (Section 2.6)
    # 
    #        *  <blockquote> elements (Section 2.10)
    # 
    #        *  <dl> elements (Section 2.20)
    # 
    #        *  <figure> elements (Section 2.25)
    # 
    #        *  <iref> elements (Section 2.27)
    # 
    #        *  <ol> elements (Section 2.34)
    # 
    #        *  <sourcecode> elements (Section 2.48)
    # 
    #        *  <t> elements (Section 2.53)
    # 
    #        *  <table> elements (Section 2.54)
    # 
    #        *  <texttable> elements (Section 3.8)
    # 
    #        *  <ul> elements (Section 2.63)
    # 
    #    3.  Optional <section> elements (Section 2.46)
    # 
    # 2.46.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this section.
    # 
    # 2.46.2.  "numbered" Attribute
    # 
    #    If set to "false", the formatter is requested to not display a
    #    section number.  The prep tool will verify that such a section is not
    #    followed by a numbered section in this part of the document and will
    #    verify that the section is a top-level section.
    # 
    #    Allowed values:
    # 
    #    o  "true" (default)
    # 
    #    o  "false"
    # 
    # 2.46.3.  "removeInRFC" Attribute
    # 
    #    If set to "true", this note is marked in the prep tool with text
    #    indicating that it should be removed before the document is published
    #    as an RFC.  That text will be "This note is to be removed before
    #    publishing as an RFC."
    # 
    #    Allowed values:
    # 
    #    o  "true"
    # 
    #    o  "false" (default)
    # 
    # 2.46.4.  "title" Attribute
    # 
    #    Deprecated.  Use <name> instead.
    # 
    # 2.46.5.  "toc" Attribute
    # 
    #    Indicates to a formatter whether or not the section is to be included
    #    in a table of contents, if such a table of contents is produced.
    #    This only takes effect if the level of the section would have
    #    appeared in the table of contents based on the "tocDepth" attribute
    #    of the <rfc> element, and of course only if the table of contents is
    #    being created based on the "tocInclude" attribute of the <rfc>
    #    element.  If this is set to "exclude", any section below this one
    #    will be excluded as well.  The "default" value indicates inclusion of
    #    the section if it would be included by the tocDepth attribute of the
    #    <rfc> element.
    # 
    #    Allowed values:
    # 
    #    o  "include"
    # 
    #    o  "exclude"
    # 
    #    o  "default" (default)
    def render_section(self, e, width, **kwargs):
        kwargs['joiners'].update({
            None:       Joiner('\n\n', 3, 0, False, False), # default
            't':        Joiner('\n\n', 3, 0, False, False),
            'name':     Joiner('  ', 0, 0, False, False),
            'iref':     Joiner('  ', 0, 0, False, False),
            'section':  Joiner('\n\n', 0, 0, False, False),
            'artset':   Joiner('\n\n', 0, 0, False, False),
            'artwork':  Joiner('\n\n', 3, 0, False, True),
            'sourcecode':  Joiner('\n\n', 3, 0, False, False),
        })
        text = ''
        pn = e.get('pn', 'unknown-unknown')
        if e.get('numbered') == 'true':
            text = pn.split('-',1)[1].replace('-', ' ').title() +'.'
            if text.startswith('Appendix'):
                text = text.replace('.', ' ', 1)
            kwargs['joiners'].update({
                'name':     Joiner('  ', len(text)+2, 0, False, False),
            })
        lines = []
        name = e.find('name')
        if name != None:
            lines += mklines(self.tjoin(text, name, width, **kwargs), e)
        for c in e.getchildren():
            if c.tag == 'name':
                continue
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines

    # 2.47.  <seriesInfo>
    # 
    #    Specifies the document series in which this document appears, and
    #    also specifies an identifier within that series.
    # 
    #    A processing tool determines whether it is working on an RFC or an
    #    Internet-Draft by inspecting the "name" attribute of a <seriesInfo>
    #    element inside the <front> element inside the <rfc> element, looking
    #    for "RFC" or "Internet-Draft".  (Specifying neither value in any of
    #    the <seriesInfo> elements can be useful for producing other types of
    #    documents but is out of scope for this specification.)
    # 
    #    It is invalid to have multiple <seriesInfo> elements inside the same
    #    <front> element containing the same "name" value.  Some combinations
    #    of <seriesInfo> "name" attribute values make no sense, such as having
    #    both <seriesInfo name="rfc"/> and <seriesInfo name="Internet-Draft"/>
    #    in the same <front> element.
    # 
    #    This element appears as a child element of <front> (Section 2.26) and
    #    <reference> (Section 2.40; deprecated in this context).
    # 
    #    Content model: this element does not have any contents.
    # 
    # 2.47.1.  "asciiName" Attribute
    # 
    #    The ASCII equivalent of the name field.
    # 
    # 2.47.2.  "asciiValue" Attribute
    # 
    #    The ASCII equivalent of the value field.
    # 
    # 2.47.3.  "name" Attribute (Mandatory)
    # 
    #    The name of the series.  The currently known values are "RFC",
    #    "Internet-Draft", and "DOI".  The RFC Series Editor may change this
    #    list in the future.
    # 
    #    Some of the values for "name" interact as follows:
    # 
    #    o  If a <front> element contains a <seriesInfo> element with a name
    #       of "Internet-Draft", it can also have at most one additional
    #       <seriesInfo> element with a "status" attribute whose value is of
    #       "standard", "full-standard", "bcp", "fyi", "informational",
    #       "experimental", or "historic" to indicate the intended status of
    #       this Internet-Draft, if it were to be later published as an RFC.
    #       If such an additional <seriesInfo> element has one of those
    #       statuses, the name needs to be "".
    # 
    #    o  If a <front> element contains a <seriesInfo> element with a name
    #       of "RFC", it can also have at most one additional <seriesInfo>
    #       element with a "status" attribute whose value is of
    #       "full-standard", "bcp", or "fyi" to indicate the current status of
    #       this RFC.  If such an additional <seriesInfo> element has one of
    #       those statuses, the "value" attribute for that name needs to be
    #       the number within that series.  That <front> element might also
    #       contain an additional <seriesInfo> element with the status of
    #       "info", "exp", or "historic" and a name of "" to indicate the
    #       status of the RFC.
    # 
    #    o  A <front> element that has a <seriesInfo> element that has the
    #       name "Internet-Draft" cannot also have a <seriesInfo> element that
    #       has the name "RFC".
    # 
    #    o  The <seriesInfo> element can contain the DOI for the referenced
    #       document.  This cannot be used when the <seriesInfo> element is an
    #       eventual child element of an <rfc> element -- only as an eventual
    #       child of a <reference> element.  The "value" attribute should use
    #       the form specified in [RFC7669].
    # 
    # 2.47.4.  "status" Attribute
    # 
    #    The status of this document.  The currently known values are
    #    "standard", "informational", "experimental", "bcp", "fyi", and
    #    "full-standard".  The RFC Series Editor may change this list in the
    #    future.
    # 
    # 2.47.5.  "stream" Attribute
    # 
    #    The stream (as described in [RFC7841]) that originated the document.
    #    (The RFC Series Editor may change this list in the future.)
    # 
    #    Allowed values:
    # 
    #    o  "IETF" (default)
    # 
    #    o  "IAB"
    # 
    #    o  "IRTF"
    # 
    #    o  "independent"
    # 
    # 2.47.6.  "value" Attribute (Mandatory)
    # 
    #    The identifier within the series specified by the "name" attribute.
    # 
    #    For BCPs, FYIs, RFCs, and STDs, this is the number within the series.
    #    For Internet-Drafts, it is the full draft name (ending with the
    #    two-digit version number).  For DOIs, the value is given, such as
    #    "10.17487/rfc1149", as described in [RFC7669].
    # 
    #    The name in the value should be the document name without any file
    #    extension.  For Internet-Drafts, the value for this attribute should
    #    be "draft-ietf-somewg-someprotocol-07", not
    #    "draft-ietf-somewg-someprotocol-07.txt".
    def render_seriesinfo(self, e, width, **kwargs):
        name = e.get('name')
        value = e.get('value')
        if name == 'Internet-Draft':
            return name + ', ' + value
        else:
            return name + '\u00A0' + value.replace('/', '/' + '\uE060')

    # 2.48.  <sourcecode>
    # 
    #    This element allows the inclusion of source code into the document.
    # 
    #    When rendered, source code is always shown in a monospace font.  When
    #    <sourcecode> is a child of <figure> or <section>, it provides full
    #    control of horizontal whitespace and line breaks.  When formatted, it
    #    is indented relative to the left margin of the enclosing element.  It
    #    is thus useful for source code and formal languages (such as ABNF
    #    [RFC5234] or the RNC notation used in this document).  (When
    #    <sourcecode> is a child of other elements, it flows with the text
    #    that surrounds it.)  Tab characters (U+0009) inside of this element
    #    are prohibited.
    # 
    #    For artwork such as character-based art, diagrams of message layouts,
    #    and so on, use the <artwork> element instead.
    # 
    #    Output formatters that do pagination should attempt to keep source
    #    code on a single page.  This is to prevent source code that is split
    #    across pages from looking like two separate pieces of code.
    # 
    #    See Section 5 for a description of how to deal with issues of using
    #    "&" and "<" characters in source code.
    # 
    #    This element appears as a child element of <blockquote>
    #    (Section 2.10), <dd> (Section 2.18), <figure> (Section 2.25), <li>
    #    (Section 2.29), <section> (Section 2.46), <td> (Section 2.56), and
    #    <th> (Section 2.58).
    # 
    #    Content model: only text content.
    # 
    # 2.48.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this source code.
    # 
    # 2.48.2.  "name" Attribute
    # 
    #    A filename suitable for the contents (such as for extraction to a
    #    local file).  This attribute can be helpful for other kinds of tools
    #    (such as automated syntax checkers, which work by extracting the
    #    source code).  Note that the "name" attribute does not need to be
    #    unique for <artwork> elements in a document.  If multiple
    #    <sourcecode> elements have the same "name" attribute, a formatter
    #    might assume that the elements are all fragments of a single file,
    #    and such a formatter can collect those fragments for later
    #    processing.
    # 
    # 2.48.3.  "src" Attribute
    # 
    #    The URI reference of a source file [RFC3986].
    # 
    #    It is an error to have both a "src" attribute and content in the
    #    <sourcecode> element.
    # 
    # 2.48.4.  "type" Attribute
    # 
    #    Specifies the type of the source code.  The value of this attribute
    #    is free text with certain values designated as preferred.
    # 
    #    The preferred values for <sourcecode> types are:
    # 
    #    o  abnf
    # 
    #    o  asn.1
    # 
    #    o  bash
    # 
    #    o  c++
    # 
    #    o  c
    # 
    #    o  cbor
    # 
    #    o  dtd
    # 
    #    o  java
    # 
    #    o  javascript
    # 
    #    o  json
    # 
    #    o  mib
    # 
    #    o  perl
    # 
    #    o  pseudocode
    # 
    #    o  python
    # 
    #    o  rnc
    # 
    #    o  xml
    # 
    #    o  yang
    # 
    #    The RFC Series Editor will maintain a complete list of the preferred
    #    values on the RFC Editor web site, and that list is expected to be
    #    updated over time.  Thus, a consumer of v3 XML should not cause a
    #    failure when it encounters an unexpected type or no type is
    #    specified.
    def render_sourcecode(self, e, width, **kwargs):
        markers = e.get('markers')
        lines = self.render_artwork(e, width, **kwargs)
        if markers == 'true':
            text = '<CODE BEGINS>'
            file = e.get('name')
            marker_lines = [ Line(text, e) ]
            if file:
                filetext = 'file "%s"' % file
                if len(filetext) > (width - len(text)):
                    marker_lines += [ Line('  ' + filetext, e) ]
                else:
                    marker_lines = [ Line(text + ' ' + filetext, e) ]
            lines = marker_lines + lines + [ Line('<CODE ENDS>', e) ]
        return lines


    def render_stream(self, e, width, **kwargs):
        text = e.text
        return text


    # 2.49.  <street>
    # 
    #    Provides a street address.
    # 
    #    This element appears as a child element of <postal> (Section 2.37).
    # 
    #    Content model: only text content.
    # 
    # 2.49.1.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the street address.
    render_street = null_renderer       # handled in render_address

    # 2.50.  <strong>
    # 
    #    Indicates text that is semantically strong.  Text enclosed within
    #    this element will be displayed as bold after processing.  This
    #    element can be combined with other character formatting elements, and
    #    the formatting will be additive.
    def render_strong(self, e, width, **kwargs):
        text = '*%s*' % self.inner_text_renderer(e)
        text += e.tail or ''
        return text


    # 2.51.  <sub>
    # 
    #    Causes the text to be displayed as subscript, approximately half a
    #    letter-height lower than normal text.  This element can be combined
    #    with other character formatting elements, and the formatting will be
    #    additive.
    def render_sub(self, e, width, **kwargs):
        text = '_(%s)' % self.inner_text_renderer(e)
        text += e.tail or ''
        return text


    # 2.52.  <sup>
    # 
    #    Causes the text to be displayed as superscript, approximately half a
    #    letter-height higher than normal text.  This element can be combined
    #    with other character formatting elements, and the formatting will be
    #    additive.
    def render_sup(self, e, width, **kwargs):
        text = '^(%s)' % self.inner_text_renderer(e)
        text += e.tail or ''
        return text


    # 2.53.  <t>
    # 
    #    Contains a paragraph of text.
    # 
    #    This element appears as a child element of <abstract> (Section 2.1),
    #    <aside> (Section 2.6), <blockquote> (Section 2.10), <dd>
    #    (Section 2.18), <li> (Section 2.29), <list> (Section 3.4), <note>
    #    (Section 2.33), <section> (Section 2.46), <td> (Section 2.56), and
    #    <th> (Section 2.58).
    # 
    #    Content model:
    # 
    #    In any order:
    # 
    #    o  Text
    # 
    #    o  <bcp14> elements (Section 2.9)
    # 
    #    o  <cref> elements (Section 2.16)
    # 
    #    o  <em> elements (Section 2.22)
    # 
    #    o  <eref> elements (Section 2.24)
    # 
    #    o  <iref> elements (Section 2.27)
    # 
    #    o  <list> elements (Section 3.4)
    # 
    #    o  <relref> elements (Section 2.44)
    # 
    #    o  <spanx> elements (Section 3.7)
    # 
    #    o  <strong> elements (Section 2.50)
    # 
    #    o  <sub> elements (Section 2.51)
    # 
    #    o  <sup> elements (Section 2.52)
    # 
    #    o  <tt> elements (Section 2.62)
    # 
    #    o  <vspace> elements (Section 3.10)
    # 
    #    o  <xref> elements (Section 2.66)
    # 
    # 2.53.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this paragraph.
    # 
    # 2.53.2.  "hangText" Attribute
    # 
    #    Deprecated.  Instead, use <dd> inside of a definition list (<dl>).
    # 
    # 2.53.3.  "keepWithNext" Attribute
    # 
    #    Acts as a hint to the output formatters that do pagination to do a
    #    best-effort attempt to keep the paragraph with the next element,
    #    whatever that happens to be.  For example, the HTML output @media
    #    print CSS ("CSS" refers to Cascading Style Sheets) might translate
    #    this to page-break-after: avoid.  For PDF, the paginator could
    #    attempt to keep the paragraph with the next element.  Note: this
    #    attribute is strictly a hint and not always actionable.
    # 
    #    Allowed values:
    # 
    #    o  "false" (default)
    # 
    #    o  "true"
    # 
    # 2.53.4.  "keepWithPrevious" Attribute
    # 
    #    Acts as a hint to the output formatters that do pagination to do a
    #    best-effort attempt to keep the paragraph with the previous element,
    #    whatever that happens to be.  For example, the HTML output @media
    #    print CSS might translate this to page-break-before: avoid.  For PDF,
    #    the paginator could attempt to keep the paragraph with the previous
    #    element.  Note: this attribute is strictly a hint and not always
    #    actionable.
    # 
    #    Allowed values:
    # 
    #    o  "false" (default)
    # 
    #    o  "true"
    def render_t(self, e, width, **kwargs):
        def rreplace(s, old, new, max):
            lst = s.rsplit(old, max)
            return new.join(lst)
        text = self.inner_text_renderer(e)
        if kwargs.pop('fill', True):
            text = fill(text, width=width, **kwargs)
            lines = mklines(text, e)
        else:
            if isinstance(text, six.binary_type):
                text = text.decode('utf-8')
            lines = [ Line(text, e) ]
        return lines


    # 2.54.  <table>
    # 
    #    Contains a table with a caption with the table number.  If the
    #    element contains a <name> element, the caption will also show that
    #    name.
    # 
    #    Inside the <table> element is, optionally, a <thead> element to
    #    contain the rows that will be the table's heading and, optionally, a
    #    <tfoot> element to contain the rows of the table's footer.  If the
    #    XML is converted to a representation that has page breaks (such as
    #    PDFs or printed HTML), the header and footer are meant to appear on
    #    each page.
    # 
    #    This element appears as a child element of <aside> (Section 2.6) and
    #    <section> (Section 2.46).
    # 
    #    Content model:
    # 
    #    In this order:
    # 
    #    1.  One optional <name> element (Section 2.32)
    # 
    #    2.  Optional <iref> elements (Section 2.27)
    # 
    #    3.  One optional <thead> element (Section 2.59)
    # 
    #    4.  One or more <tbody> elements (Section 2.55)
    # 
    #    5.  One optional <tfoot> element (Section 2.57)
    # 
    # 2.54.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for this table.
    def build_table(self, e, width, **kwargs):
        # variations on border characters for table styles
        style = self.get_relevant_pi(e, 'table_borders') or self.options.table_borders
        bchar_sets = {
                'full': { '=': '=',
                          '-': '-',
                          '+': '+',
                          '|': '|',},
                'light':{ '=': '-',
                          '-': None,
                          '+': '+',
                          '|': '|',},
                'min':  { '=': '-',
                          '-': None,
                          '+': ' ',
                          '|': ' ',},
            }
        bchar_sets['minimal'] = bchar_sets['min']
        bchar = bchar_sets[style]
        class Cell(object):
            type    = b'None'
            text    = None
            wrapped = []
            colspan = 1
            rowspan = 1
            width   = None
            minwidth= None
            height  = None
            element = None
            padding = 0
            foldable= True
            top     = ''
            bot     = ''

        def show(cells, attr='', note=''):
            debug.say('')
            debug.say('%s %s:' % (attr, note))
            for i in range(len(cells)):
                row = [ (c.type[1], getattr(c, attr)) if attr else c for c in cells[i] ]
                debug.say(str(row).replace('\u2028', '\u00a4'))

        def array(rows, cols, init):
            a = []
            for i in range(rows):
                a.append([])
                for j in range(cols):
                    if inspect.isclass(init):
                        a[i].append(init())
                    else:
                        a[i].append(init)
            return a

        def intattr(e, name):
            attr = e.get(name)
            if attr.isdigit():
                attr = int(attr)
            else:
                attr = 1
            return attr

        def get_dimensions(e):
            cols = 0
            rows = 0
            # Find the dimensions of the table
            for p in e.iterchildren(['thead', 'tbody', 'tfoot']):
                for r in p.iterchildren('tr'):
                    ccols = 0
                    crows = 0
                    extrarows = 0
                    for c in r.iterchildren('td', 'th'):
                        colspan = intattr(c, 'colspan')
                        ccols += colspan
                        rowspan = intattr(c, 'rowspan')
                        crows = max(crows, rowspan)
                    cols = max(cols, ccols)
                    extrarows = max(extrarows, crows)
                    extrarows -=1
                    rows += 1
            if extrarows > 0:
                rows += extrarows
            return rows, cols

        def justify(cell, line):
            align = cell.element.get('align')
            padding = cell.colwidth - textwidth(line)
            width = cell.colwidth - min(2, padding)
            if   align == 'left':
                text = line.ljust(width)
            elif align == 'center':
                text = line.center(width)
            elif align == 'right':
                text = line.rjust(width)
            if   align == 'right':
                if padding > 1:
                    text = text + ' ' 
                if padding > 0:
                    text = ' ' + text
            else:
                if padding > 1:
                    text = ' ' + text
                if padding > 0:
                    text = text + ' ' 
            return text

        def merge_border(c, d):
            border = {
                '=': { '=':'=', '-':'=', '+':'+', },
                '-': { '=':'=', '-':'-', '+':'+', },
                '+': { '=':'+', '-':'+', '+':'+', '|':'+', },
                '|': { '+':'+', '|':'|', },
            }
            if c in border and d in border[c]:
                return border[c][d]
            return c

        def build_line(cells, i, cols, next=True):
            def table(e):
                return list(e.iterancestors('table'))[0]
            line = ''
            e = cells[i][0].element
            for j in range(cols):
                k, l = cells[i][j].origin
                # skip colspan cells
                if k==i and l<j:
                    continue
                cell = cells[k][l]
                part = cell.wrapped[cell.m]
                if next:
                    cell.m += 1
                if line:
                    if bchar['|']:
                        line = line[:-1] + merge_border(line[-1], part[0]) + part[1:]
                    else:
                        line = line + part
                else:
                    line = part
            return Line(line, table(e))

        def find_minwidths(e, cells, hyphen_split=False):
            """
            Find the minimum column widths of regular cells
            """
            i = 0
            splitter = utils.TextSplitter(width=67, hyphen_split=hyphen_split)
            for p in e.iterchildren(['thead', 'tbody', 'tfoot']):
                for r in list(p.iterchildren('tr')):
                    j = 0
                    for c in r.iterchildren('td', 'th'):
                        # skip over cells belonging to an earlier row or column
                        while j < len(cells[i]) and cells[i][j].element != c:
                            j += 1
                        #
                        cell = cells[i][j]
                        if cell.foldable:
                            cell.text = cell.text.strip(stripspace)
                            cell.minwidth = max([0]+[ len(word) for word in splitter._split(cell.text) ]) if cell.text else 0
                        else:
                            cell.minwidth = max([0]+[ len(word) for line in cell.text.splitlines() for word in splitter._split(line) ])
                    i += 1

        def set_colwidths(cells, rows, cols):
            """
            Compute the adjusted cell widths; the same for all rows of each column
            """
            for j in range(cols):
                colmax = 0
                for i in range(rows):
                    cell = cells[i][j]
                    if cell.minwidth:
                        cw = cell.minwidth // cell.colspan
                        if cw > colmax:
                            colmax = cw
                for i in range(rows):
                    cells[i][j].colwidth = colmax

        # ----------------------------------------------------------------------
        rows, cols = get_dimensions(e)
        cells = array(rows, cols, Cell)

        # ----------------------------------------------------------------------
        # Iterate through tr and th/td elements, and annotate the cells array
        # with rowspan, colspan, and owning element and its origin
        i = 0
        for p in e.iterchildren(['thead', 'tbody', 'tfoot']):
            for r in list(p.iterchildren('tr')):
                j = 0
                for c in r.iterchildren('td', 'th'):
                    # skip over cells belonging to an earlier row or column
                    while j < len(cells[i]) and cells[i][j].element != None:
                        j += 1
                    #
                    cell = cells[i][j]
                    cell.colspan = intattr(c, 'colspan')
                    cell.rowspan = intattr(c, 'rowspan')
                    if len(c) == 1 and c[0].tag == 't':
                        cell.text, cell.foldable = self.text_or_block_renderer(c[0], width, fill=False, **kwargs) or ('', True)
                    else:
                        cell.text, cell.foldable = self.text_or_block_renderer(c, width, fill=False, **kwargs) or ('', True)
                    cell.text = mktextblock(cell.text)
                    if cell.foldable:
                        cell.text = cell.text.strip(stripspace)
                        cell.minwidth = max([0]+[ len(word) for word in splitter._split(cell.text) ]) if cell.text else 0
                    else:
                        cell.minwidth = max([0]+[ len(word) for line in cell.text.splitlines() for word in splitter._split(line) ])
                    cell.type = p.tag
                    if c.tag == 'th':
                        cell.top = bchar['=']
                        cell.bot = bchar['=']
                    else:
                        cell.top = bchar['-'] if not cell.top else cell.top
                        cell.bot = bchar['-'] if not cell.bot else cell.bot
                    for k in range(i, i+cell.rowspan):
                        for l in range(j, j+cell.colspan):
                            cells[k][l].element = c
                            cells[k][l].origin  = (i, j)
                i += 1
        # Ensure we have top and bottom borders
        for j in range(len(cells[0])):
            if hasattr(cells[0][j], 'origin'):
                k, l = cells[0][j].origin
                if not cells[k][l].top:
                    cells[k][l].top = bchar['=']
        for j in range(len(cells[-1])):
            if hasattr(cells[-1][j], 'origin'):
                k, l = cells[-1][j].origin
                if not cells[k][l].bot:
                    cells[k][l].bot = bchar['=']
            del k, l
        #show(cells, 'origin')

        # ----------------------------------------------------------------------
        # Find the minimum column widths of regular cells, and total width
        # per row.
        find_minwidths(e, cells, hyphen_split=self.options.table_hyphen_breaks)
        #show(cells, 'minwidth')
        #debug.pprint('totwidth')

        # ----------------------------------------------------------------------
        # Compute the adjusted cell widths; the same for all rows of each column
        set_colwidths(cells, rows, cols)
        reqwidth = sum([ c.colwidth for c in cells[0] ]) + cols + 1
        if reqwidth > width:
            # Try again, splitting cell content on hyphens this time
            find_minwidths(e, cells, hyphen_split=True)
            set_colwidths(cells, rows, cols)
        #show(cells, 'colwidth', 'after aligned cell widths')

        # ----------------------------------------------------------------------
        # Add padding if possible. Pad widest first.
        reqwidth = sum([ c.colwidth for c in cells[0] ]) + (cols + 1)*len(bchar['|'])
        if reqwidth > width:
            self.warn(e, "Total table width (%s) exceeds available width (%s)" % (reqwidth, width))
        excess = width - reqwidth
        #
        if excess > 0:
            widths = [ (c.colwidth, ic) for ic, c in enumerate(cells[0]) ]
            widths.sort()
            widths.reverse()
            for j in [ k for w, k in widths ]:
                pad = min(2, excess)
                excess -= pad
                for i in range(rows):
                    cells[i][j].colwidth += pad
                    cells[i][j].padding   = pad
        #show(cells, 'colwidth', 'after padding')

        # ----------------------------------------------------------------------
        # Set up initial cell.wrapped values
        for i in range(rows):
            for j in range(cols):
                cell = cells[i][j]
                if cell.text:
                    if cell.foldable:
                        cell.wrapped = fill(cell.text, width=cell.colwidth, fix_sentence_endings=True).splitlines()
                    else:
                        cell.wrapped = cell.text.splitlines()

        # ----------------------------------------------------------------------
        # Make columns wider, if possible
        while excess > 0:
            maxpos = (None, None)
            maxrows = 0
            for i in range(rows):
                for j in range(cols):
                    cell = cells[i][j]
                    if hasattr(cell, 'origin'):
                        if cell.origin == (i,j):
                            w = sum([ cells[i][k].colwidth for k in range(j, j+cell.colspan)])+ cell.colspan-1 - cell.padding
                            r = cell.rowspan
                            # this is simplified, and doesn't always account for the
                            # extra line from the missing border line in a rowspan cell:
                            if cell.text:
                                if cell.foldable:
                                    cell.wrapped = fill(cell.text, width=w, fix_sentence_endings=True).splitlines()
                                else:
                                    cell.wrapped = [ l.text for l in self.text_or_block_renderer(cell.element, width=w, fill=True, **kwargs)[0] ]
                                cell.height = len(cell.wrapped)
                                if maxrows < cell.height and cell.height > 1:
                                    maxrows = cell.height
                                    maxpos = (i, j)
                    else:
                        self.die(e, "Inconsistent table width: Found different row lengths in this table")

            # calculate a better width for the cell with the largest number
            # of text rows
            if maxpos != (None, None):
                i, j = maxpos
                cell = cells[i][j]
                w = sum([ cells[i][k].colwidth for k in range(j, j+cell.colspan)])+ cell.colspan-1 - cell.padding
                r = cell.rowspan
                h = cell.height
                for l in range(1, excess+1):
                    lines = fill(cell.text, width=w+l, fix_sentence_endings=True).splitlines()
                    if len(lines) < h:
                        cell.height = lines
                        excess -= l
                        c = h//r                        
                        for k in range(rows):
                            cells[k][j].colwidth += l
                        break
                else:
                    break
            else:
                break

        #show(cells, 'colwidth', 'after widening wide cells and re-wrapping lines')
        #show(cells, 'height')
        #show(cells, 'origin')

        # ----------------------------------------------------------------------
        # Normalize cell height and lines lists
        #show(cells, 'wrapped', 'before height normalization')
        #show(cells, 'rowspan', 'before height normalization')
        for i in range(rows):
            minspan = sys.maxsize
            for j in range(cols):
                cell = cells[i][j]
                k, l = cell.origin
                hspan = cell.rowspan+k-i if cell.rowspan else minspan
                if hspan > 0 and hspan < minspan:
                    minspan = hspan
            maxlines = 0
            for j in range(cols):
                cell = cells[i][j]
                k, l = cell.origin
                hspan = cell.rowspan+k-i if cell.rowspan else minspan
                lines = len(cell.wrapped) if cell.wrapped else 0
                if hspan == minspan and lines > maxlines:
                    maxlines = lines
            for j in range(cols):
                cells[i][j].lines = maxlines

        # ----------------------------------------------------------------------
        # Calculate total height for rowspan cells
        for i in range(rows):
            for j in range(cols):
                cells[i][j].m = None
                cells[i][j].height = None
                k, l = cells[i][j].origin
                cell = cells[k][l]
                if cell.m is None:
                    cell.m = 0
                    cell.height = sum([ cells[n][l].lines for n in range(k, k+cell.rowspan)]) + cell.rowspan-1

        # ----------------------------------------------------------------------
        # Calculate total width for colspan cells
        for i in range(rows):
            for j in range(cols):
                k, l = cells[i][j].origin
                cell = cells[k][l]
                if cell.origin == (i,j):
                    cell.colwidth = sum([ cells[i][n].colwidth for n in range(j, j+cell.colspan)]) + cell.colspan-1

        # ----------------------------------------------------------------------
        # Add cell borders
        x = bchar['+']
        l = bchar['|']
        for i in range(rows):
            for j in range(cols):
                cell = cells[i][j]
                if cell.origin == (i, j):
                    wrapped = (cell.wrapped + ['']*cell.height)[:cell.height]
                    lines = (  ([ x + cell.top*cell.colwidth + x ] if cell.top else [])
                             + ([ l + justify(cell, line) + l for line in wrapped ])
                             + ([ x + cell.bot*cell.colwidth + x ] if cell.bot else []) )
                    cell.wrapped = lines

        #show(cells, 'lines', 'before assembly')
        # ----------------------------------------------------------------------
        # Emit combined cell content, line by line
        lines = []
        prev_bottom_border_line = None
        for i in range(rows):
            # For each table row, render the top cell border (if any) and content.  The bottom
            # border will be merged with the next row's top border when processing that row.
            has_top_border = any( c.top for c in cells[i] if c.wrapped)
            has_bot_border = any( c.bot for c in cells[i] if c.wrapped)
            for n in range(min(len(c.wrapped) for c in cells[i] if c.wrapped)-int(has_bot_border) ):
                line = build_line(cells, i, cols)
                lines.append(line)
                if prev_bottom_border_line:
                    if has_top_border:
                        line = lines[-1]
                        lines[-1] = Line(''.join(merge_border(prev_bottom_border_line.text[c], line.text[c]) for c in range(len(line.text))), line.elem)
                    else:
                        line = lines[-1]
                        lines[-1] = prev_bottom_border_line
                        lines.append(line)
                prev_bottom_border_line = None
            # Get the next line, which will contain the bottom border for completed cells,
            # without incrementing the line count (we might have rowspan cells which might
            # not have been completely consumed yet):
            prev_bottom_border_line = build_line(cells, i, cols, next=False) if has_bot_border else None
        lines.append(prev_bottom_border_line)
        return lines

    def render_table(self, e, width, **kwargs):
        kwargs['joiners'].update({
            'name':     Joiner(': ', 0, 0, False, False),
            'dl':       Joiner('\n\n', 0, 0, False, False),
            'ol':       Joiner('\n\n', 0, 0, False, False),
            't':        Joiner('\n\n', 0, 0, False, False),
            'ul':       Joiner('\n\n', 0, 0, False, False),
        })
        #
        pn = e.get('pn')
        num = pn.split('-')[1].capitalize()
        children = e.getchildren()
        title = "Table %s" % (num, )
        if len(children) and children[0].tag == 'name':
            name = children[0]
            children = children[1:]
            title = self.tjoin(title, name, width, **kwargs)
        lines = self.build_table(e, width, **kwargs)
        table_width = min([ width, max( len(l.text) for l in lines ) ])
        min_title_width = min([ 26, len(title) ])
        if table_width < min_title_width:
            table_width = min_title_width
            lines = align(lines, 'center', table_width)
        title = '\n'+center(title, table_width).rstrip(stripspace)
        lines += mklines(title, e)
        lines = align(lines, e.get('align', 'center'), width)
        return lines


    # 2.55.  <tbody>
    # 
    #    A container for a set of body rows for a table.
    # 
    #    This element appears as a child element of <table> (Section 2.54).
    # 
    #    Content model:
    # 
    #    One or more <tr> elements (Section 2.61)
    # 
    # 2.55.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the tbody.
    render_tbody = null_renderer        # handled in build_table

    # 2.56.  <td>
    # 
    #    A cell in a table row.
    # 
    #    This element appears as a child element of <tr> (Section 2.61).
    # 
    #    Content model:
    # 
    #    Either:
    # 
    #       In any order, but at least one of:
    # 
    #       *  <artwork> elements (Section 2.5)
    # 
    #       *  <dl> elements (Section 2.20)
    # 
    #       *  <figure> elements (Section 2.25)
    # 
    #       *  <ol> elements (Section 2.34)
    # 
    #       *  <sourcecode> elements (Section 2.48)
    # 
    #       *  <t> elements (Section 2.53)
    # 
    #       *  <ul> elements (Section 2.63)
    # 
    #    Or:
    # 
    #       In any order:
    # 
    #       *  Text
    # 
    #       *  <bcp14> elements (Section 2.9)
    # 
    #       *  <br> elements (Section 2.12)
    # 
    #       *  <cref> elements (Section 2.16)
    # 
    #       *  <em> elements (Section 2.22)
    # 
    #       *  <eref> elements (Section 2.24)
    # 
    #       *  <iref> elements (Section 2.27)
    # 
    #       *  <relref> elements (Section 2.44)
    # 
    #       *  <strong> elements (Section 2.50)
    # 
    #       *  <sub> elements (Section 2.51)
    # 
    #       *  <sup> elements (Section 2.52)
    # 
    #       *  <tt> elements (Section 2.62)
    # 
    #       *  <xref> elements (Section 2.66)
    # 
    # 2.56.1.  "align" Attribute
    # 
    #    Controls whether the content of the cell appears left justified
    #    (default), centered, or right justified.  Note that "center" or
    #    "right" will probably only work well in cells with plain text; any
    #    other elements might make the contents render badly.
    # 
    #    Allowed values:
    # 
    #    o  "left" (default)
    # 
    #    o  "center"
    # 
    #    o  "right"
    # 
    # 2.56.2.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the cell.
    # 
    # 2.56.3.  "colspan" Attribute
    # 
    #    The number of columns that the cell is to span.  For example, setting
    #    "colspan='3'" indicates that the cell occupies the same horizontal
    #    space as three cells of a row without any "colspan" attributes.
    # 
    # 2.56.4.  "rowspan" Attribute
    # 
    #    The number of rows that the cell is to span.  For example, setting
    #    "rowspan='3'" indicates that the cell occupies the same vertical
    #    space as three rows.
    render_td = null_renderer           # handled in build_table


    # 2.57.  <tfoot>
    # 
    #    A container for a set of footer rows for a table.
    # 
    #    This element appears as a child element of <table> (Section 2.54).
    # 
    #    Content model:
    # 
    #    One or more <tr> elements (Section 2.61)
    # 
    # 2.57.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the tfoot.
    render_tfoot = null_renderer        # handled in build_table


    # 2.58.  <th>
    # 
    #    A cell in a table row.  When rendered, this will normally come out in
    #    boldface; other than that, there is no difference between this and
    #    the <td> element.
    # 
    #    This element appears as a child element of <tr> (Section 2.61).
    # 
    #    Content model:
    # 
    #    Either:
    # 
    #       In any order, but at least one of:
    # 
    #       *  <artwork> elements (Section 2.5)
    # 
    #       *  <dl> elements (Section 2.20)
    # 
    #       *  <figure> elements (Section 2.25)
    # 
    #       *  <ol> elements (Section 2.34)
    # 
    #       *  <sourcecode> elements (Section 2.48)
    # 
    #       *  <t> elements (Section 2.53)
    # 
    #       *  <ul> elements (Section 2.63)
    # 
    #    Or:
    # 
    #       In any order:
    # 
    #       *  Text
    # 
    #       *  <bcp14> elements (Section 2.9)
    # 
    #       *  <br> elements (Section 2.12)
    # 
    #       *  <cref> elements (Section 2.16)
    # 
    #       *  <em> elements (Section 2.22)
    # 
    #       *  <eref> elements (Section 2.24)
    # 
    #       *  <iref> elements (Section 2.27)
    # 
    #       *  <relref> elements (Section 2.44)
    # 
    #       *  <strong> elements (Section 2.50)
    # 
    #       *  <sub> elements (Section 2.51)
    # 
    #       *  <sup> elements (Section 2.52)
    # 
    #       *  <tt> elements (Section 2.62)
    # 
    #       *  <xref> elements (Section 2.66)
    # 
    # 2.58.1.  "align" Attribute
    # 
    #    Controls whether the content of the cell appears left justified
    #    (default), centered, or right justified.  Note that "center" or
    #    "right" will probably only work well in cells with plain text; any
    #    other elements might make the contents render badly.
    # 
    #    Allowed values:
    # 
    #    o  "left" (default)
    # 
    #    o  "center"
    # 
    #    o  "right"
    # 
    # 2.58.2.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the row.
    # 
    # 2.58.3.  "colspan" Attribute
    # 
    #    The number of columns that the cell is to span.  For example, setting
    #    "colspan='3'" indicates that the cell occupies the same horizontal
    #    space as three cells of a row without any "colspan" attributes.
    # 
    # 2.58.4.  "rowspan" Attribute
    # 
    #    The number of rows that the cell is to span.  For example, setting
    #    "rowspan='3'" indicates that the cell occupies the same vertical
    #    space as three rows.
    render_th = null_renderer           # handled in build_table


    # 2.59.  <thead>
    # 
    #    A container for a set of header rows for a table.
    # 
    #    This element appears as a child element of <table> (Section 2.54).
    # 
    #    Content model:
    # 
    #    One or more <tr> elements (Section 2.61)
    # 
    # 2.59.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the thead.
    render_thead = null_renderer        # handled in build_table


    # 2.60.  <title>
    # 
    #    Represents the document title.
    # 
    #    When this element appears in the <front> element of the current
    #    document, the title might also appear in page headers or footers.  If
    #    it is long (~40 characters), the "abbrev" attribute can be used to
    #    specify an abbreviated variant.
    # 
    #    This element appears as a child element of <front> (Section 2.26).
    # 
    #    Content model: only text content.
    def render_title(self, e, width, **kwargs):
        r = e.getparent().getparent()   # <reference>
        title = clean_text(' '.join(e.itertext()).strip(stripspace))
        quote_title = r.get('quoteTitle')
        if quote_title == 'true':
            title = '"%s"' % title
        return title

    def render_title_front(self, e, width, **kwargs):
        pp = e.getparent().getparent()
        title = '\u2028'.join(e.itertext()).strip(stripspace)
        title = fill(title, width=width, **kwargs)
        title = center(title, width)
        if self.options.rfc:
            return title
        else:
            if pp.tag == 'rfc':
                doc_name = self.root.get('docName')
                if doc_name:
                    title += '\n'+doc_name.strip(stripspace).center(width).rstrip(stripspace)
            return title

    # 2.60.1.  "abbrev" Attribute
    # 
    #    Specifies an abbreviated variant of the document title.
    # 
    # 2.60.2.  "ascii" Attribute
    # 
    #    The ASCII equivalent of the title.


    # 2.61.  <tr>
    # 
    #    A row of a table.
    # 
    #    This element appears as a child element of <tbody> (Section 2.55),
    #    <tfoot> (Section 2.57), and <thead> (Section 2.59).
    # 
    #    Content model:
    # 
    #    In any order, but at least one of:
    # 
    #    o  <td> elements (Section 2.56)
    # 
    #    o  <th> elements (Section 2.58)
    # 
    # 2.61.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the row.
    render_tr = null_renderer           # handled in build_table

    # <toc>
    def render_toc(self, e, width, **kwargs):
        lines = []
        for c in e.getchildren():
            numbered = c.get('numbered')
            if not numbered == 'false':
                self.err(c, "Expected toc section to have numbered='false', but found '%s'" % (numbered, ))
            keep_url = True if self.options.rfc else False
            lines = self.ljoin(lines, c, width, keep_url=keep_url, **kwargs)
        return lines


    # 2.62.  <tt>
    # 
    #    Causes the text to be displayed in a constant-width font.  This
    #    element can be combined with other character formatting elements, and
    #    the formatting will be additive.
    def render_tt(self, e, width, **kwargs):
        p = e.getparent()
        render_plain = list(p.iterancestors('table')) and not utils.hastext(p, ignore=['tt'])
        if render_plain:
            text = '%s' % self.inner_text_renderer(e)
        else:
            text = '"%s"' % self.inner_text_renderer(e)
        text += e.tail or ''
        return text


    # 2.63.  <ul>
    # 
    #    An unordered list.  The labels on the items will be symbols picked by
    #    the formatter.
    # 
    #    This element appears as a child element of <abstract> (Section 2.1),
    #    <aside> (Section 2.6), <blockquote> (Section 2.10), <dd>
    #    (Section 2.18), <li> (Section 2.29), <note> (Section 2.33), <section>
    #    (Section 2.46), <td> (Section 2.56), and <th> (Section 2.58).
    # 
    #    Content model:
    # 
    #    One or more <li> elements (Section 2.29)
    # 
    # 2.63.1.  "anchor" Attribute
    # 
    #    Document-wide unique identifier for the list.
    # 
    # 2.63.2.  "empty" Attribute
    # 
    #    Defines whether or not the label is empty.  empty="true" indicates
    #    that no label will be shown.
    # 
    #    Allowed values:
    # 
    #    o  "false" (default)
    # 
    #    o  "true"
    # 
    # 2.63.3.  "spacing" Attribute
    # 
    #    Defines whether or not there is a blank line between entries.
    #    spacing="normal" indicates a single blank line, while
    #    spacing="compact" indicates no space between.
    # 
    #    Allowed values:
    # 
    #    o  "normal" (default)
    # 
    #    o  "compact"
    def render_ul(self, e, width, **kwargs):
        # setup and validation
        empty = e.get('empty') == 'true'
        e._bare = empty and e.get('bare') == 'true'
        e._initial_text = self.get_ul_li_initial_text
        #
        compact = e.get('spacing') == 'compact'
        ljoin  = '\n' if compact else '\n\n'
        #
        depth = len([ a for a in e.iterancestors(e.tag) ])
        symbols = self.options.list_symbols
        e._symbol = ' ' if empty else symbols[depth%len(symbols)]

        #
        indent = len(e._symbol)+2
        if e._bare:
            first = mktextblock(self.render(e[-1], width, **kwargs))
            if first:
                indent = min(8, len(first.split()[0])+2)
        padding = indent
        indent = int( e.get('indent') or indent )
        hang = max(padding, indent) - indent
        #
        kwargs['joiners'].update({
            None:   Joiner(ljoin, indent, 0, False, False),
            'li':   Joiner(ljoin, 0, 0, False, False),
            't':    Joiner(ljoin, indent, hang, False, False),
        })
        # rendering
        lines = []
        for c in e.getchildren():
            lines = self.ljoin(lines, c, width, **kwargs)
        return lines
        

    def render_u(self, e, width, **kwargs):
        try:
            text = expand_unicode_element(e)
        except (RuntimeError, ValueError) as exception:
            text = ''
            self.err(e, exception)
        anchor = e.get('anchor')
        xref = self.root.find('.//xref[@target="%s"]'%anchor) if anchor else None
        if xref != None:
            # render only literal here
            text = e.text
        text += e.tail or ''
        return text

    # 2.64.  <uri>
    # 
    #    Contains a web address associated with the author.
    # 
    #    The contents should be a valid URI; this most likely will be an
    #    "http:" or "https:" URI.
    # 
    #    This element appears as a child element of <address> (Section 2.2).
    # 
    #    Content model: only text content.
    def render_uri(self, e, width, **kwargs):
        latin = kwargs.pop('latin', None)
        text = fill("URI:\u00a0\u00a0 %s"%e.text, width=width, **kwargs) if e.text and latin!=False else ''
        return text

    # 2.65.  <workgroup>
    # 
    #    This element is used to specify the Working Group (IETF) or Research
    #    Group (IRTF) from which the document originates, if any.  The
    #    recommended format is the official name of the Working Group (with
    #    some capitalization).
    # 
    #    In Internet-Drafts, this is used in the upper left corner of the
    #    boilerplate, replacing the "Network Working Group" string.
    #    Formatting software can append the words "Working Group" or "Research
    #    Group", depending on the "submissionType" property of the <rfc>
    #    element (Section 2.45.12).
    # 
    #    This element appears as a child element of <front> (Section 2.26).
    # 
    #    Content model: only text content.


    # 2.66.  <xref>
    # 
    #    A reference to an anchor in this document.  Formatters that have
    #    links (such as HTML and PDF) are likely to render <xref> elements as
    #    internal hyperlinks.  This element is useful for referring to
    #    references in the "References" section, to specific sections of this
    #    document, to specific figures, and so on.  The "target" attribute is
    #    required.
    # 
    #    This element appears as a child element of <annotation>
    #    (Section 2.3), <blockquote> (Section 2.10), <c> (Section 3.1), <cref>
    #    (Section 2.16), <dd> (Section 2.18), <dt> (Section 2.21), <em>
    #    (Section 2.22), <li> (Section 2.29), <name> (Section 2.32),
    #    <postamble> (Section 3.5), <preamble> (Section 3.6), <strong>
    #    (Section 2.50), <sub> (Section 2.51), <sup> (Section 2.52), <t>
    #    (Section 2.53), <td> (Section 2.56), <th> (Section 2.58), <tt>
    #    (Section 2.62), and <ttcol> (Section 3.9).
    # 
    #    Content model: only text content.
    def render_xref(self, e, width, **kwargs):
        target = e.get('target')
        section = e.get('section')
        format = e.get('format')
        reftext = e.get('derivedContent').strip(stripspace)
        exptext = self.inner_text_renderer(e, width, **kwargs)
        if exptext:
            # for later string formatting convenience, a trailing space if any text:
            exptext += ' '              
        content = clean_text(''.join(list(e.itertext())))
        if reftext is None:
            self.die(e, "Found an <xref> without derivedContent: %s" % (etree.tostring(e),))
        #
        if not section:
            if reftext:
                if target in self.refname_mapping and format != 'title':
                    if content and content != reftext:
                        text = "%s[%s]" % (exptext, reftext)
                    else:
                        text = "[%s]" % reftext
                else:
                    if content and content != reftext:
                        text = "%s(%s)" % (exptext, reftext)
                    else:
                        text = "%s" % (exptext or reftext)
            else:
                text = exptext.strip(stripspace)
            pageno = e.get('pageno')
            if pageno and pageno.isdigit():
                text += '\u2026' '%04d' % int(pageno)
        else:
            label = 'Section' if section[0].isdigit() else 'Appendix' if re.search(r'^[A-Z](\.|$)', section) else 'Part'
            sform  = e.get('sectionFormat')
            
            if   sform == 'of':
                text = '%s %s of %s[%s]' % (label, section, exptext, reftext)
            elif sform == 'comma':
                text = '%s[%s], %s %s' % (exptext, reftext, label, section)
            elif sform == 'parens':
                text = '%s[%s] (%s %s)' % (exptext, reftext, label, section)
            elif sform == 'bare':
                if exptext and exptext != section:
                    text = '%s (%s)' % (section, exptext.strip(stripspace))
                else:
                    text = '%s' % (section, )
            else:
                self.err(e, 'Unexpected value combination: section: %s  sectionFormat: %s' %(section, sform))

        # Prevent line breaking on dash
        text = text.replace('-', '\u2011')
        text += (e.tail or '')

        return text
        
    # 2.66.1.  "format" Attribute
    # 
    #    This attribute signals to formatters what the desired format of the
    #    reference should be.  Formatters for document types that have linking
    #    capability should wrap the displayed text in hyperlinks.
    # 
    #    "counter"
    # 
    #       The "derivedContent" attribute will contain just a counter.  This
    #       is used for targets that are <section>, <figure>, <table>, or
    #       items in an ordered list.  Using "format='counter'" where the
    #       target is any other type of element is an error.
    # 
    #       For example, with an input of:
    # 
    #          <section anchor="overview">Protocol Overview</section>
    #          . . .
    #          See Section <xref target="overview" format="counter"/>
    #          for an overview.
    # 
    #       An HTML formatter might generate:
    # 
    #          See Section <a href="#overview">1.7</a> for an overview.
    # 
    #    "default"
    # 
    #       If the element has no content, the "derivedContent" attribute will
    #       contain a text fragment that describes the referenced part
    #       completely, such as "XML" for a target that is a <reference>, or
    #       "Section 2" or "Table 4" for a target to a non-reference.  (If the
    #       element has content, the "derivedContent" attribute is filled with
    #       the content.)
    # 
    #       For example, with an input of:
    # 
    #          <section anchor="overview">Protocol Overview</section>
    #          . . .
    #          See <xref target="overview"/> for an overview.
    # 
    #       An HTML formatter might generate:
    # 
    #          See <a href="#overview">Section 1.7</a> for an overview.
    # 
    #    "none"
    # 
    #       Deprecated.
    # 
    #    "title"
    # 
    #       If the target is a <reference> element, the "derivedContent"
    #       attribute will contain the name of the reference, extracted from
    #       the <title> child of the <front> child of the reference.  Or, if
    #       the target element has a <name> child element, the
    #       "derivedContent" attribute will contain the text content of that
    #       <name> element concatenated with the text content of each
    #       descendant node of <name> (that is, stripping out all of the XML
    #       markup, leaving only the text).  Or, if the target element does
    #       not contain a <name> child element, the "derivedContent" attribute
    #       will contain the name of the "anchor" attribute of that element
    #       with no other adornment.
    # 
    #    Allowed values:
    # 
    #    o  "default" (default)
    # 
    #    o  "title"
    # 
    #    o  "counter"
    # 
    #    o  "none"
    # 
    # 2.66.2.  "pageno" Attribute
    # 
    #    Deprecated.
    # 
    #    Allowed values:
    # 
    #    o  "true"
    # 
    #    o  "false" (default)
    # 
    # 2.66.3.  "target" Attribute (Mandatory)
    # 
    #    Identifies the document component being referenced.  The value needs
    #    to match the value of the "anchor" attribute of an element in the
    #    document; otherwise, it is an error.

    # --- class variables ------------------------------------------------------

    element_tags = [
        'abstract',
        'address',
        'annotation',
        'artset',
        'artwork',
        'aside',
        'author',
        'back',
        'bcp14',
        'blockquote',
        'boilerplate',
        'br',
        'city',
        'code',
        'country',
        'cref',
        'date',
        'dd',
        'displayreference',
        'dl',
        'dt',
        'em',
        'email',
        'eref',
        'figure',
        'front',
        'iref',
        'li',
        'link',
        'middle',
        'name',
        'note',
        'ol',
        'organization',
        'phone',
        'postal',
        'postalLine',
        'refcontent',
        'reference',
        'referencegroup',
        'references',
        'region',
        'relref',
        'rfc',
        'section',
        'seriesInfo',
        'sourcecode',
        'street',
        'strong',
        'sub',
        'sup',
        't',
        'table',
        'tbody',
        'td',
        'tfoot',
        'th',
        'thead',
        'title',
        'toc',
        'tr',
        'tt',
        'ul',
        'uri',
        'xref',
    ]
    deprecated_element_tags = [
        'list',
        'spanx',
        'vspace',
        'c',
        'texttable',
        'ttcol',
        'facsimile',
        'format',
        'preamble',
        'postamble',
    ]
    unused_front_element_renderers = [
        'area',
        'keyword',
        'workgroup',
    ]
    all_element_tags = element_tags + deprecated_element_tags + unused_front_element_renderers
    deprecated_attributes = [
        # element, attrbute
        ('figure', 'align'),
        ('section', 'title'),
        ('note', 'title'),
        ('figure', 'title'),
        ('references', 'title'),
        ('texttable', 'title'),
        ('figure', 'src'),
        ('artwork', 'xml:space'),
        ('artwork', 'height'),
        ('artwork', 'width'),
        ('figure', 'height'),
        ('figure', 'width'),
        ('xref', 'pageno'),
    ]
