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