1# Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7#     http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13import logging
14
15from botocore.compat import OrderedDict
16from awscli.bcdoc.docstringparser import DocStringParser
17from awscli.bcdoc.style import ReSTStyle
18
19LOG = logging.getLogger('bcdocs')
20
21
22class ReSTDocument(object):
23
24    def __init__(self, target='man'):
25        self.style = ReSTStyle(self)
26        self.target = target
27        self.parser = DocStringParser(self)
28        self.keep_data = True
29        self.do_translation = False
30        self.translation_map = {}
31        self.hrefs = {}
32        self._writes = []
33        self._last_doc_string = None
34
35    def _write(self, s):
36        if self.keep_data and s is not None:
37            self._writes.append(s)
38
39    def write(self, content):
40        """
41        Write content into the document.
42        """
43        self._write(content)
44
45    def writeln(self, content):
46        """
47        Write content on a newline.
48        """
49        self._write('%s%s\n' % (self.style.spaces(), content))
50
51    def peek_write(self):
52        """
53        Returns the last content written to the document without
54        removing it from the stack.
55        """
56        return self._writes[-1]
57
58    def pop_write(self):
59        """
60        Removes and returns the last content written to the stack.
61        """
62        return self._writes.pop()
63
64    def push_write(self, s):
65        """
66        Places new content on the stack.
67        """
68        self._writes.append(s)
69
70    def getvalue(self):
71        """
72        Returns the current content of the document as a string.
73        """
74        if self.hrefs:
75            self.style.new_paragraph()
76            for refname, link in self.hrefs.items():
77                self.style.link_target_definition(refname, link)
78        return ''.join(self._writes).encode('utf-8')
79
80    def translate_words(self, words):
81        return [self.translation_map.get(w, w) for w in words]
82
83    def handle_data(self, data):
84        if data and self.keep_data:
85            self._write(data)
86
87    def include_doc_string(self, doc_string):
88        if doc_string:
89            try:
90                start = len(self._writes)
91                self.parser.feed(doc_string)
92                self.parser.close()
93                end = len(self._writes)
94                self._last_doc_string = (start, end)
95            except Exception:
96                LOG.debug('Error parsing doc string', exc_info=True)
97                LOG.debug(doc_string)
98
99    def remove_last_doc_string(self):
100        # Removes all writes inserted by last doc string
101        if self._last_doc_string is not None:
102            start, end = self._last_doc_string
103            del self._writes[start:end]
104
105
106class DocumentStructure(ReSTDocument):
107    def __init__(self, name, section_names=None, target='man', context=None):
108        """Provides a Hierarichial structure to a ReSTDocument
109
110        You can write to it similiar to as you can to a ReSTDocument but
111        has an innate structure for more orginaztion and abstraction.
112
113        :param name: The name of the document
114        :param section_names: A list of sections to be included
115            in the document.
116        :param target: The target documentation of the Document structure
117        :param context: A dictionary of data to store with the strucuture. These
118            are only stored per section not the entire structure.
119        """
120        super(DocumentStructure, self).__init__(target=target)
121        self._name = name
122        self._structure = OrderedDict()
123        self._path = [self._name]
124        self._context = {}
125        if context is not None:
126            self._context = context
127        if section_names is not None:
128            self._generate_structure(section_names)
129
130    @property
131    def name(self):
132        """The name of the document structure"""
133        return self._name
134
135    @property
136    def path(self):
137        """
138        A list of where to find a particular document structure in the
139        overlying document structure.
140        """
141        return self._path
142
143    @path.setter
144    def path(self, value):
145        self._path = value
146
147    @property
148    def available_sections(self):
149        return list(self._structure)
150
151    @property
152    def context(self):
153        return self._context
154
155    def _generate_structure(self, section_names):
156        for section_name in section_names:
157            self.add_new_section(section_name)
158
159    def add_new_section(self, name, context=None):
160        """Adds a new section to the current document structure
161
162        This document structure will be considered a section to the
163        current document structure but will in itself be an entirely
164        new document structure that can be written to and have sections
165        as well
166
167        :param name: The name of the section.
168        :param context: A dictionary of data to store with the strucuture. These
169            are only stored per section not the entire structure.
170        :rtype: DocumentStructure
171        :returns: A new document structure to add to but lives as a section
172            to the document structure it was instantiated from.
173        """
174        # Add a new section
175        section = self.__class__(name=name, target=self.target,
176                                 context=context)
177        section.path = self.path + [name]
178        # Indent the section apporpriately as well
179        section.style.indentation = self.style.indentation
180        section.translation_map = self.translation_map
181        section.hrefs = self.hrefs
182        self._structure[name] = section
183        return section
184
185    def get_section(self, name):
186        """Retrieve a section"""
187        return self._structure[name]
188
189    def delete_section(self, name):
190        """Delete a section"""
191        del self._structure[name]
192
193    def flush_structure(self):
194        """Flushes a doc structure to a ReSTructed string
195
196        The document is flushed out in a DFS style where sections and their
197        subsections' values are added to the string as they are visited.
198        """
199        # We are at the root flush the links at the beginning of the
200        # document
201        if len(self.path) == 1:
202            if self.hrefs:
203                self.style.new_paragraph()
204                for refname, link in self.hrefs.items():
205                    self.style.link_target_definition(refname, link)
206        value = self.getvalue()
207        for name, section in self._structure.items():
208            value += section.flush_structure()
209        return value
210
211    def getvalue(self):
212        return ''.join(self._writes).encode('utf-8')
213
214    def remove_all_sections(self):
215        self._structure = OrderedDict()
216
217    def clear_text(self):
218        self._writes = []
219