reference_resolver.py revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
1# Copyright (c) 2012 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from file_system import FileNotFoundError 6import logging 7import re 8import string 9 10def _ClassifySchemaNode(node_name, api): 11 """Attempt to classify |node_name| in an API, determining whether |node_name| 12 refers to a type, function, event, or property in |api|. 13 """ 14 if '.' in node_name: 15 node_name, rest = node_name.split('.', 1) 16 else: 17 rest = None 18 for key, group in [('types', 'type'), 19 ('functions', 'method'), 20 ('events', 'event'), 21 ('properties', 'property')]: 22 for item in api.get(key, []): 23 if item['name'] == node_name: 24 if rest is not None: 25 ret = _ClassifySchemaNode(rest, item) 26 if ret is not None: 27 return ret 28 else: 29 return group, node_name 30 return None 31 32def _MakeKey(namespace, ref, title): 33 return '%s.%s.%s' % (namespace, ref, title) 34 35class ReferenceResolver(object): 36 """Resolves references to $ref's by searching through the APIs to find the 37 correct node. 38 39 $ref's have two forms: 40 41 $ref:api.node - Replaces the $ref with a link to node on the API page. The 42 title is set to the name of the node. 43 44 $ref:[api.node The Title] - Same as the previous form but title is set to 45 "The Title". 46 """ 47 48 # Matches after a $ref: that doesn't have []s. 49 _bare_ref = re.compile('\w+(\.\w+)*') 50 51 class Factory(object): 52 def __init__(self, 53 api_data_source_factory, 54 api_list_data_source_factory, 55 object_store_creator_factory): 56 self._api_data_source_factory = api_data_source_factory 57 self._api_list_data_source_factory = api_list_data_source_factory 58 self._object_store_creator_factory = object_store_creator_factory 59 60 def Create(self): 61 return ReferenceResolver( 62 self._api_data_source_factory.Create(None, disable_refs=True), 63 self._api_list_data_source_factory.Create(), 64 self._object_store_creator_factory.Create(ReferenceResolver).Create()) 65 66 def __init__(self, api_data_source, api_list_data_source, object_store): 67 self._api_data_source = api_data_source 68 self._api_list_data_source = api_list_data_source 69 self._object_store = object_store 70 71 def _GetRefLink(self, ref, api_list, namespace, title): 72 # Check nodes within each API the ref might refer to. 73 parts = ref.split('.') 74 for i, part in enumerate(parts): 75 api_name = '.'.join(parts[:i]) 76 if api_name not in api_list: 77 continue 78 try: 79 api = self._api_data_source.get(api_name) 80 except FileNotFoundError: 81 continue 82 name = '.'.join(parts[i:]) 83 # Attempt to find |name| in the API. 84 node_info = _ClassifySchemaNode(name, api) 85 if node_info is None: 86 # Check to see if this ref is a property. If it is, we want the ref to 87 # the underlying type the property is referencing. 88 for prop in api.get('properties', []): 89 # If the name of this property is in the ref text, replace the 90 # property with its type, and attempt to classify it. 91 if prop['name'] in name and 'link' in prop: 92 name_as_prop_type = name.replace(prop['name'], prop['link']['name']) 93 node_info = _ClassifySchemaNode(name_as_prop_type, api) 94 if node_info is not None: 95 name = name_as_prop_type 96 text = ref.replace(prop['name'], prop['link']['name']) 97 break 98 if node_info is None: 99 continue 100 else: 101 text = ref 102 category, node_name = node_info 103 if namespace is not None and text.startswith('%s.' % namespace): 104 text = text[len('%s.' % namespace):] 105 return { 106 'href': '%s.html#%s-%s' % (api_name, category, name.replace('.', '-')), 107 'text': title if title else text, 108 'name': node_name 109 } 110 111 # If it's not a reference to an API node it might just be a reference to an 112 # API. Check this last so that links within APIs take precedence over links 113 # to other APIs. 114 if ref in api_list: 115 return { 116 'href': '%s.html' % ref, 117 'text': title if title else ref, 118 'name': ref 119 } 120 121 return None 122 123 def GetLink(self, ref, namespace=None, title=None): 124 """Resolve $ref |ref| in namespace |namespace| if not None, returning None 125 if it cannot be resolved. 126 """ 127 link = self._object_store.Get(_MakeKey(namespace, ref, title)).Get() 128 if link is not None: 129 return link 130 131 api_list = self._api_list_data_source.GetAllNames() 132 link = self._GetRefLink(ref, api_list, namespace, title) 133 134 if link is None and namespace is not None: 135 # Try to resolve the ref in the current namespace if there is one. 136 link = self._GetRefLink('%s.%s' % (namespace, ref), 137 api_list, 138 namespace, 139 title) 140 141 if link is not None: 142 self._object_store.Set(_MakeKey(namespace, ref, title), link) 143 return link 144 145 def SafeGetLink(self, ref, namespace=None, title=None): 146 """Resolve $ref |ref| in namespace |namespace|, or globally if None. If it 147 cannot be resolved, pretend like it is a link to a type. 148 """ 149 ref_data = self.GetLink(ref, namespace=namespace, title=title) 150 if ref_data is not None: 151 return ref_data 152 logging.error('$ref %s could not be resolved in namespace %s.' % 153 (ref, namespace)) 154 type_name = ref.rsplit('.', 1)[-1] 155 return { 156 'href': '#type-%s' % type_name, 157 'text': title if title else ref, 158 'name': ref 159 } 160 161 def ResolveAllLinks(self, text, namespace=None): 162 """This method will resolve all $ref links in |text| using namespace 163 |namespace| if not None. Any links that cannot be resolved will be replaced 164 using the default link format that |SafeGetLink| uses. 165 """ 166 if text is None or '$ref:' not in text: 167 return text 168 split_text = text.split('$ref:') 169 # |split_text| is an array of text chunks that all start with the 170 # argument to '$ref:'. 171 formatted_text = [split_text[0]] 172 for ref_and_rest in split_text[1:]: 173 title = None 174 if ref_and_rest.startswith('[') and ']' in ref_and_rest: 175 # Text was '$ref:[foo.bar maybe title] other stuff'. 176 ref_with_title, rest = ref_and_rest[1:].split(']', 1) 177 ref_with_title = ref_with_title.split(None, 1) 178 if len(ref_with_title) == 1: 179 # Text was '$ref:[foo.bar] other stuff'. 180 ref = ref_with_title[0] 181 else: 182 # Text was '$ref:[foo.bar title] other stuff'. 183 ref, title = ref_with_title 184 else: 185 # Text was '$ref:foo.bar other stuff'. 186 match = self._bare_ref.match(ref_and_rest) 187 if match is None: 188 ref = '' 189 rest = ref_and_rest 190 else: 191 ref = match.group() 192 rest = ref_and_rest[match.end():] 193 194 ref_dict = self.SafeGetLink(ref, namespace=namespace, title=title) 195 formatted_text.append('<a href="%(href)s">%(text)s</a>%(rest)s' % 196 { 'href': ref_dict['href'], 'text': ref_dict['text'], 'rest': rest }) 197 return ''.join(formatted_text) 198