1"""Implement some common transforms on parsed AST."""
2
3import os
4import re
5
6from docutils import nodes, transforms
7from docutils.statemachine import StringList
8from docutils.parsers.rst import Parser
9from docutils.utils import new_document
10from sphinx import addnodes
11
12from .states import DummyStateMachine
13
14
15class AutoStructify(transforms.Transform):
16
17    """Automatically try to transform blocks to sphinx directives.
18
19    This class is designed to handle AST generated by CommonMarkParser.
20    """
21
22    def __init__(self, *args, **kwargs):
23        transforms.Transform.__init__(self, *args, **kwargs)
24        self.reporter = self.document.reporter
25        self.config = self.default_config.copy()
26        try:
27            new_cfg = self.document.settings.env.config.recommonmark_config
28            self.config.update(new_cfg)
29        except AttributeError:
30            pass
31
32        # Deprecation notices
33        # TODO move this check to an extension pattern, and only call once
34        if self.config.get('enable_auto_doc_ref', False):
35            self.reporter.warning(
36                'AutoStructify option "enable_auto_doc_ref" is deprecated')
37
38    # set to a high priority so it can be applied first for markdown docs
39    default_priority = 1
40    suffix_set = set(['md', 'rst'])
41
42    default_config = {
43        'enable_auto_doc_ref': False,
44        'auto_toc_tree_section': None,
45        'enable_auto_toc_tree': True,
46        'enable_eval_rst': True,
47        'enable_math': True,
48        'enable_inline_math': True,
49        'commonmark_suffixes': ['.md'],
50        'url_resolver': lambda x: x,
51    }
52
53    def parse_ref(self, ref):
54        """Analyze the ref block, and return the information needed.
55
56        Parameters
57        ----------
58        ref : nodes.reference
59
60        Returns
61        -------
62        result : tuple of (str, str, str)
63            The returned result is tuple of (title, uri, docpath).
64            title is the display title of the ref.
65            uri is the html uri of to the ref after resolve.
66            docpath is the absolute document path to the document, if
67            the target corresponds to an internal document, this can bex None
68        """
69        title = None
70        if len(ref.children) == 0:
71            title = ref['name'] if 'name' in ref else None
72        elif isinstance(ref.children[0], nodes.Text):
73            title = ref.children[0].astext()
74        uri = ref['refuri']
75        if uri.find('://') != -1:
76            return (title, uri, None)
77        anchor = None
78        arr = uri.split('#')
79        if len(arr) == 2:
80            anchor = arr[1]
81        if len(arr) > 2 or len(arr[0]) == 0:
82            return (title, uri, None)
83        uri = arr[0]
84
85        abspath = os.path.abspath(os.path.join(self.file_dir, uri))
86        relpath = os.path.relpath(abspath, self.root_dir)
87        suffix = abspath.rsplit('.', 1)
88        if len(suffix) == 2 and suffix[1] in AutoStructify.suffix_set and (
89                os.path.exists(abspath) and abspath.startswith(self.root_dir)):
90            # replace the path separator if running on non-UNIX environment
91            if os.path.sep != '/':
92                relpath = relpath.replace(os.path.sep, '/')
93            docpath = '/' + relpath.rsplit('.', 1)[0]
94            # rewrite suffix to html, this is suboptimal
95            uri = docpath + '.html'
96            if anchor is None:
97                return (title, uri, docpath)
98            else:
99                return (title, uri + '#' + anchor, None)
100        else:
101            # use url resolver
102            if self.url_resolver:
103                uri = self.url_resolver(relpath)
104            if anchor:
105                uri += '#' + anchor
106            return (title, uri, None)
107
108    def auto_toc_tree(self, node):  # pylint: disable=too-many-branches
109        """Try to convert a list block to toctree in rst.
110
111        This function detects if the matches the condition and return
112        a converted toc tree node. The matching condition:
113        The list only contains one level, and only contains references
114
115        Parameters
116        ----------
117        node: nodes.Sequential
118            A list node in the doctree
119
120        Returns
121        -------
122        tocnode: docutils node
123            The converted toc tree node, None if conversion is not possible.
124        """
125        if not self.config['enable_auto_toc_tree']:
126            return None
127        # when auto_toc_tree_section is set
128        # only auto generate toctree under the specified section title
129        sec = self.config['auto_toc_tree_section']
130        if sec is not None:
131            if node.parent is None:
132                return None
133            title = None
134            if isinstance(node.parent, nodes.section):
135                child = node.parent.first_child_matching_class(nodes.title)
136                if child is not None:
137                    title = node.parent.children[child]
138            elif isinstance(node.parent, nodes.paragraph):
139                child = node.parent.parent.first_child_matching_class(nodes.title)
140                if child is not None:
141                    title = node.parent.parent.children[child]
142            if not title:
143                return None
144            if title.astext().strip() != sec:
145                return None
146
147        numbered = None
148        if isinstance(node, nodes.bullet_list):
149            numbered = 0
150        elif isinstance(node, nodes.enumerated_list):
151            numbered = 1
152
153        if numbered is None:
154            return None
155        refs = []
156        for nd in node.children[:]:
157            assert isinstance(nd, nodes.list_item)
158            if len(nd.children) != 1:
159                return None
160            par = nd.children[0]
161            if not isinstance(par, nodes.paragraph):
162                return None
163            if len(par.children) != 1:
164                return None
165            ref = par.children[0]
166            if isinstance(ref, addnodes.pending_xref):
167                ref = ref.children[0]
168            if not isinstance(ref, nodes.reference):
169                return None
170            title, uri, docpath = self.parse_ref(ref)
171            if title is None or uri.startswith('#'):
172                return None
173            if docpath:
174                refs.append((title, docpath))
175            else:
176                refs.append((title, uri))
177        self.state_machine.reset(self.document,
178                                 node.parent,
179                                 self.current_level)
180        return self.state_machine.run_directive(
181            'toctree',
182            options={'maxdepth': 1, 'numbered': numbered},
183            content=['%s <%s>' % (k, v) for k, v in refs])
184
185    def auto_inline_code(self, node):
186        """Try to automatically generate nodes for inline literals.
187
188        Parameters
189        ----------
190        node : nodes.literal
191            Original codeblock node
192        Returns
193        -------
194        tocnode: docutils node
195            The converted toc tree node, None if conversion is not possible.
196        """
197        assert isinstance(node, nodes.literal)
198        if len(node.children) != 1:
199            return None
200        content = node.children[0]
201        if not isinstance(content, nodes.Text):
202            return None
203        content = content.astext().strip()
204        if content.startswith('$') and content.endswith('$'):
205            if not self.config['enable_inline_math']:
206                return None
207            content = content[1:-1]
208            self.state_machine.reset(self.document,
209                                     node.parent,
210                                     self.current_level)
211            return self.state_machine.run_role('math', content=content)
212        else:
213            return None
214
215    def auto_code_block(self, node):
216        """Try to automatically generate nodes for codeblock syntax.
217
218        Parameters
219        ----------
220        node : nodes.literal_block
221            Original codeblock node
222        Returns
223        -------
224        tocnode: docutils node
225            The converted toc tree node, None if conversion is not possible.
226        """
227        assert isinstance(node, nodes.literal_block)
228        original_node = node
229        if 'language' not in node:
230            return None
231        self.state_machine.reset(self.document,
232                                 node.parent,
233                                 self.current_level)
234        content = node.rawsource.split('\n')
235        language = node['language']
236        if language == 'math':
237            if self.config['enable_math']:
238                return self.state_machine.run_directive(
239                    'math', content=content)
240        elif language == 'eval_rst':
241            if self.config['enable_eval_rst']:
242                # allow embed non section level rst
243                node = nodes.section()
244                self.state_machine.state.nested_parse(
245                    StringList(content, source=original_node.source),
246                    0, node=node, match_titles=True)
247                return node.children[:]
248        else:
249            match = re.search('[ ]?[\w_-]+::.*', language)
250            if match:
251                parser = Parser()
252                new_doc = new_document(None, self.document.settings)
253                newsource = u'.. ' + match.group(0) + '\n' + node.rawsource
254                parser.parse(newsource, new_doc)
255                return new_doc.children[:]
256            else:
257                return self.state_machine.run_directive(
258                    'code-block', arguments=[language],
259                    content=content)
260        return None
261
262    def find_replace(self, node):
263        """Try to find replace node for current node.
264
265        Parameters
266        ----------
267        node : docutil node
268            Node to find replacement for.
269
270        Returns
271        -------
272        nodes : node or list of node
273            The replacement nodes of current node.
274            Returns None if no replacement can be found.
275        """
276        newnode = None
277        if isinstance(node, nodes.Sequential):
278            newnode = self.auto_toc_tree(node)
279        elif isinstance(node, nodes.literal_block):
280            newnode = self.auto_code_block(node)
281        elif isinstance(node, nodes.literal):
282            newnode = self.auto_inline_code(node)
283        return newnode
284
285    def traverse(self, node):
286        """Traverse the document tree rooted at node.
287
288        node : docutil node
289            current root node to traverse
290        """
291        old_level = self.current_level
292        if isinstance(node, nodes.section):
293            if 'level' in node:
294                self.current_level = node['level']
295        to_visit = []
296        to_replace = []
297        for c in node.children[:]:
298            newnode = self.find_replace(c)
299            if newnode is not None:
300                to_replace.append((c, newnode))
301            else:
302                to_visit.append(c)
303
304        for oldnode, newnodes in to_replace:
305            node.replace(oldnode, newnodes)
306
307        for child in to_visit:
308            self.traverse(child)
309        self.current_level = old_level
310
311    def apply(self):
312        """Apply the transformation by configuration."""
313        source = self.document['source']
314
315        self.reporter.info('AutoStructify: %s' % source)
316
317        # only transform markdowns
318        if not source.endswith(tuple(self.config['commonmark_suffixes'])):
319            return
320
321        self.url_resolver = self.config['url_resolver']
322        assert callable(self.url_resolver)
323
324        self.state_machine = DummyStateMachine()
325        self.current_level = 0
326        self.file_dir = os.path.abspath(os.path.dirname(self.document['source']))
327        self.root_dir = os.path.abspath(self.document.settings.env.srcdir)
328        self.traverse(self.document)
329