238 lines
7.1 KiB
Python
238 lines
7.1 KiB
Python
# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
|
# may not use this file except in compliance with the License. A copy of
|
|
# the License is located at
|
|
#
|
|
# http://aws.amazon.com/apache2.0/
|
|
#
|
|
# or in the "license" file accompanying this file. This file is
|
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
|
# ANY KIND, either express or implied. See the License for the specific
|
|
# language governing permissions and limitations under the License.
|
|
from html.parser import HTMLParser
|
|
|
|
PRIORITY_PARENT_TAGS = ('code', 'a')
|
|
OMIT_NESTED_TAGS = ('span', 'i', 'code', 'a')
|
|
OMIT_SELF_TAGS = ('i', 'b')
|
|
|
|
|
|
class DocStringParser(HTMLParser):
|
|
"""
|
|
A simple HTML parser. Focused on converting the subset of HTML
|
|
that appears in the documentation strings of the JSON models into
|
|
simple ReST format.
|
|
"""
|
|
|
|
def __init__(self, doc):
|
|
self.tree = None
|
|
self.doc = doc
|
|
HTMLParser.__init__(self)
|
|
|
|
def reset(self):
|
|
HTMLParser.reset(self)
|
|
self.tree = HTMLTree(self.doc)
|
|
|
|
def feed(self, data):
|
|
# HTMLParser is an old style class, so the super() method will not work.
|
|
HTMLParser.feed(self, data)
|
|
self.tree.write()
|
|
self.tree = HTMLTree(self.doc)
|
|
|
|
def close(self):
|
|
HTMLParser.close(self)
|
|
# Write if there is anything remaining.
|
|
self.tree.write()
|
|
self.tree = HTMLTree(self.doc)
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
self.tree.add_tag(tag, attrs=attrs)
|
|
|
|
def handle_endtag(self, tag):
|
|
self.tree.add_tag(tag, is_start=False)
|
|
|
|
def handle_data(self, data):
|
|
self.tree.add_data(data)
|
|
|
|
|
|
class HTMLTree:
|
|
"""
|
|
A tree which handles HTML nodes. Designed to work with a python HTML parser,
|
|
meaning that the current_node will be the most recently opened tag. When
|
|
a tag is closed, the current_node moves up to the parent node.
|
|
"""
|
|
|
|
def __init__(self, doc):
|
|
self.doc = doc
|
|
self.head = StemNode()
|
|
self.current_node = self.head
|
|
self.unhandled_tags = []
|
|
|
|
def add_tag(self, tag, attrs=None, is_start=True):
|
|
if not self._doc_has_handler(tag, is_start):
|
|
self.unhandled_tags.append(tag)
|
|
return
|
|
|
|
if is_start:
|
|
if tag == 'li':
|
|
node = LineItemNode(attrs)
|
|
else:
|
|
node = TagNode(tag, attrs)
|
|
self.current_node.add_child(node)
|
|
self.current_node = node
|
|
else:
|
|
self.current_node = self.current_node.parent
|
|
|
|
def _doc_has_handler(self, tag, is_start):
|
|
if is_start:
|
|
handler_name = 'start_%s' % tag
|
|
else:
|
|
handler_name = 'end_%s' % tag
|
|
|
|
return hasattr(self.doc.style, handler_name)
|
|
|
|
def add_data(self, data):
|
|
self.current_node.add_child(DataNode(data))
|
|
|
|
def write(self):
|
|
self.head.write(self.doc)
|
|
|
|
|
|
class Node:
|
|
def __init__(self, parent=None):
|
|
self.parent = parent
|
|
|
|
def write(self, doc):
|
|
raise NotImplementedError
|
|
|
|
|
|
class StemNode(Node):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.children = []
|
|
|
|
def add_child(self, child):
|
|
child.parent = self
|
|
self.children.append(child)
|
|
|
|
def write(self, doc):
|
|
self._write_children(doc)
|
|
|
|
def _write_children(self, doc):
|
|
for index, child in enumerate(self.children):
|
|
if isinstance(child, TagNode) and index + 1 < len(self.children):
|
|
# Provide a look ahead for TagNodes when one exists
|
|
next_child = self.children[index + 1]
|
|
child.write(doc, next_child)
|
|
else:
|
|
child.write(doc)
|
|
|
|
|
|
class TagNode(StemNode):
|
|
"""
|
|
A generic Tag node. It will verify that handlers exist before writing.
|
|
"""
|
|
|
|
def __init__(self, tag, attrs=None, parent=None):
|
|
super().__init__(parent)
|
|
self.attrs = attrs
|
|
self.tag = tag
|
|
|
|
def _has_nested_tags(self):
|
|
# Returns True if any children are TagNodes and False otherwise.
|
|
return any(isinstance(child, TagNode) for child in self.children)
|
|
|
|
def write(self, doc, next_child=None):
|
|
prioritize_nested_tags = (
|
|
self.tag in OMIT_SELF_TAGS and self._has_nested_tags()
|
|
)
|
|
prioritize_parent_tag = (
|
|
isinstance(self.parent, TagNode)
|
|
and self.parent.tag in PRIORITY_PARENT_TAGS
|
|
and self.tag in OMIT_NESTED_TAGS
|
|
)
|
|
if prioritize_nested_tags or prioritize_parent_tag:
|
|
self._write_children(doc)
|
|
return
|
|
|
|
self._write_start(doc)
|
|
self._write_children(doc)
|
|
self._write_end(doc, next_child)
|
|
|
|
def _write_start(self, doc):
|
|
handler_name = 'start_%s' % self.tag
|
|
if hasattr(doc.style, handler_name):
|
|
getattr(doc.style, handler_name)(self.attrs)
|
|
|
|
def _write_end(self, doc, next_child):
|
|
handler_name = 'end_%s' % self.tag
|
|
if hasattr(doc.style, handler_name):
|
|
if handler_name == 'end_a':
|
|
# We use lookahead to determine if a space is needed after a link node
|
|
getattr(doc.style, handler_name)(next_child)
|
|
else:
|
|
getattr(doc.style, handler_name)()
|
|
|
|
|
|
class LineItemNode(TagNode):
|
|
def __init__(self, attrs=None, parent=None):
|
|
super().__init__('li', attrs, parent)
|
|
|
|
def write(self, doc, next_child=None):
|
|
self._lstrip(self)
|
|
super().write(doc, next_child)
|
|
|
|
def _lstrip(self, node):
|
|
"""
|
|
Traverses the tree, stripping out whitespace until text data is found
|
|
:param node: The node to strip
|
|
:return: True if non-whitespace data was found, False otherwise
|
|
"""
|
|
for child in node.children:
|
|
if isinstance(child, DataNode):
|
|
child.lstrip()
|
|
if child.data:
|
|
return True
|
|
else:
|
|
found = self._lstrip(child)
|
|
if found:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
class DataNode(Node):
|
|
"""
|
|
A Node that contains only string data.
|
|
"""
|
|
|
|
def __init__(self, data, parent=None):
|
|
super().__init__(parent)
|
|
if not isinstance(data, str):
|
|
raise ValueError("Expecting string type, %s given." % type(data))
|
|
self.data = data
|
|
|
|
def lstrip(self):
|
|
self.data = self.data.lstrip()
|
|
|
|
def write(self, doc):
|
|
if not self.data:
|
|
return
|
|
|
|
if self.data.isspace():
|
|
str_data = ' '
|
|
if isinstance(self.parent, TagNode) and self.parent.tag == 'code':
|
|
# Inline markup content may not start or end with whitespace.
|
|
# When provided <code> Test </code>, we want to
|
|
# generate ``Test`` instead of `` Test ``.
|
|
str_data = ''
|
|
else:
|
|
end_space = self.data[-1].isspace()
|
|
words = self.data.split()
|
|
words = doc.translate_words(words)
|
|
str_data = ' '.join(words)
|
|
if end_space:
|
|
str_data += ' '
|
|
|
|
doc.handle_data(str_data)
|