1
+ import itertools
1
2
import json
2
3
import panflute as pf
3
4
5
+ from quartodoc .inventory import Ref , RefSyntaxError
6
+ from pathlib import Path
4
7
from plum import dispatch
5
8
6
9
7
- inventory = {}
10
+ # Hold all inventory items in a singleton -------------------------------------
11
+
12
+ # TODO: make entries into dataclass
13
+ # has fields: name, domain, role, priority, invname, full_uri
14
+
15
+
16
+ class InvLookupError (Exception ):
17
+ pass
18
+
19
+
20
+ class Inventories :
21
+ def __init__ (self ):
22
+ self .registry = {}
23
+
24
+ def items (self ):
25
+ return itertools .chain (* self .registry .values ())
26
+
27
+ def load_inventory (self , inventory , url , invname ):
28
+ all_items = []
29
+ for item in inventory ["items" ]:
30
+ # TODO: what are the rules for inventories with overlapping names?
31
+ # it seems like this is where priority and using source name as an
32
+ # optional prefix in references is useful (e.g. siuba:a.b.c).
33
+ full_uri = url + item ["uri" ].replace ("$" , item ["name" ])
34
+ enh_item = {** item , "invname" : invname , "full_uri" : full_uri }
35
+ all_items .append (enh_item )
36
+
37
+ self .registry [invname ] = all_items
38
+
39
+ def lookup_reference (self , ref : Ref ):
40
+ # return global_inventory[ref]
41
+
42
+ crnt_items = self .items ()
43
+ for field in ["name" , "role" , "domain" , "invname" ]:
44
+ if field == "name" :
45
+ # target may have ~ option in front, so we strip it off
46
+ field_value = ref .target .lstrip ("~" )
47
+ else :
48
+ field_value = getattr (ref , field )
49
+
50
+ crnt_items = _filter_by_field (crnt_items , field , field_value )
51
+
52
+ results = list (crnt_items )
53
+ if not results :
54
+ raise InvLookupError (
55
+ f"Cross reference not found in an inventory file: `{ ref } `"
56
+ )
57
+
58
+ if len (results ) > 1 :
59
+ raise InvLookupError (
60
+ f"Cross reference matches multiple entries.\n "
61
+ f"Matching entries: { len (results )} \n "
62
+ f"Reference: { ref } \n "
63
+ f"Top 2 matches: \n * { results [0 ]} \n * { results [1 ]} "
64
+ )
65
+
66
+ return results [0 ]
67
+
68
+
69
+ global_inventory = Inventories ()
70
+
71
+
72
+ # Utility functions -----------------------------------------------------------
8
73
9
74
10
75
class ConfigError (Exception ):
11
76
pass
12
77
13
78
14
- def load_mock_inventory (items : "dict[str, str]" ):
15
- for k , v in items .items ():
16
- inventory [k ] = v
79
+ def get_path_to_root ():
80
+ # I have no idea how to get the documentation root,
81
+ # except to get the path the extension script, which
82
+ # lives in <root>/_extensions/interlinks, and work back
83
+ return Path (__file__ ).parent .parent .parent
84
+
85
+
86
+ def load_inventories (interlinks : dict ):
87
+ p_root = get_path_to_root ()
88
+
89
+ sources = interlinks ["sources" ]
90
+ cache = interlinks .get ("cache" , "_inv" )
91
+
92
+ # load this sites inventory ----
93
+ site_inv = interlinks .get ("site_inv" , "objects.json" )
17
94
95
+ json_data = json .load (open (p_root / site_inv ))
96
+ global_inventory .load_inventory (json_data , url = "/" , invname = "" )
18
97
19
- def ref_to_anchor (ref : str , text : "str | pf.ListContainer | None" ):
98
+ # load other inventories ----
99
+ for doc_name , cfg in sources .items ():
100
+
101
+ fname = doc_name + "_objects.json"
102
+ inv_path = p_root / Path (cache ) / fname
103
+
104
+ json_data = json .load (open (inv_path ))
105
+
106
+ global_inventory .load_inventory (json_data , url = cfg ["url" ], invname = doc_name )
107
+
108
+
109
+ def _filter_by_field (items , field_name : str , value : "str | None" = None ):
110
+ if value is None :
111
+ return items
112
+
113
+ return (item for item in items if item [field_name ] == value )
114
+
115
+
116
+ def ref_to_anchor (raw : str , text : "str | pf.ListContainer | None" ):
20
117
"""Return a Link element based on ref in interlink format
21
118
22
119
Parameters
@@ -38,17 +135,17 @@ def ref_to_anchor(ref: str, text: "str | pf.ListContainer | None"):
38
135
Link(Str(partial); url='https://example.org/functools.partial.html')
39
136
"""
40
137
# TODO: for now we just mutate el
41
- is_shortened = ref .startswith ("~" )
42
-
43
- stripped = ref .lstrip ("~" )
44
138
45
139
try :
46
- entry = inventory [stripped ]
47
- dst_url = entry ["full_uri" ]
48
- except KeyError :
49
- raise KeyError (f"Cross reference not found in an inventory file: { stripped } " )
140
+ ref = Ref .from_string (raw )
141
+ except RefSyntaxError as e :
142
+ pf .debug ("WARNING: " , str (e ))
143
+
144
+ is_shortened = ref .target .startswith ("~" )
145
+
146
+ entry = global_inventory .lookup_reference (ref )
147
+ dst_url = entry ["full_uri" ]
50
148
51
- pf .debug (f"TEXT IS: { text } " )
52
149
if not text :
53
150
name = entry ["name" ] if entry ["dispname" ] == "-" else entry ["dispname" ]
54
151
if is_shortened :
@@ -79,8 +176,6 @@ def parse_rst_style_ref(full_text):
79
176
80
177
import re
81
178
82
- # pf.debug(full_text)
83
-
84
179
m = re .match (r"(?P<text>.+?)\<(?P<ref>[a-zA-Z\.\-: _]+)\>" , full_text )
85
180
if m is None :
86
181
# TODO: print a warning or something
@@ -105,7 +200,7 @@ def visit(el: pf.MetaList, doc):
105
200
meta = doc .get_metadata ()
106
201
107
202
try :
108
- sources = meta ["interlinks" ][ "sources " ]
203
+ interlinks = meta ["interlinks" ]
109
204
except KeyError :
110
205
raise ConfigError (
111
206
"No interlinks.sources field detected in your metadata."
@@ -114,16 +209,8 @@ def visit(el: pf.MetaList, doc):
114
209
"\n sources:"
115
210
"\n - <source_name>: {url: ..., inv: ..., fallback: ... }"
116
211
)
117
- for doc_name , cfg in sources .items ():
118
- json_data = json .load (open (cfg ["fallback" ]))
119
212
120
- for item in json_data ["items" ]:
121
- # TODO: what are the rules for inventories with overlapping names?
122
- # it seems like this is where priority and using source name as an
123
- # optional prefix in references is useful (e.g. siuba:a.b.c).
124
- full_uri = cfg ["url" ] + item ["uri" ].replace ("$" , item ["name" ])
125
- enh_item = {** item , "full_uri" : full_uri }
126
- inventory [item ["name" ]] = enh_item
213
+ load_inventories (interlinks )
127
214
128
215
return el
129
216
@@ -133,29 +220,32 @@ def visit(el: pf.Doc, doc):
133
220
return el
134
221
135
222
136
- @dispatch
137
- def visit (el : pf .Plain , doc ):
138
- cont = el .content
139
- if len (cont ) == 2 and cont [0 ] == pf .Str (":ref:" ) and isinstance (cont [1 ], pf .Code ):
140
- _ , code = el .content
141
-
142
- ref , title = parse_rst_style_ref (code .text )
143
-
144
- return pf .Plain (ref_to_anchor (ref , title ))
145
-
146
- return el
223
+ # TODO: the syntax :ref:`target` is not trivial to implement. The pandoc AST
224
+ # often embeds it in a list of Plain with other elements. Currently, we only
225
+ # support the syntax inside of links.
226
+ #
227
+ # @dispatch
228
+ # def visit(el: pf.Plain, doc):
229
+ # cont = el.content
230
+ # if len(cont) == 2 and cont[0] == pf.Str(":ref:") and isinstance(cont[1], pf.Code):
231
+ # _, code = el.content
232
+ #
233
+ # ref, title = parse_rst_style_ref(code.text)
234
+ #
235
+ # return pf.Plain(ref_to_anchor(ref, title))
236
+ #
237
+ # return el
147
238
148
239
149
240
@dispatch
150
241
def visit (el : pf .Link , doc ):
151
- if el .url .startswith ("%60" ) and el .url .endswith ("%60" ):
152
- url = el .url [3 :- 3 ]
153
-
242
+ url = el .url
243
+ if (url .startswith ("%60" ) or url .startswith (":" )) and url .endswith ("%60" ):
154
244
# Get URL ----
155
-
156
- # TODO: url can be form external+invname:domain:reftype:target
157
- # for now, assume it's simply <target>. e.g. siuba.dply.verbs.mutate
158
- return ref_to_anchor ( url , el . content )
245
+ try :
246
+ return ref_to_anchor ( url . replace ( "%60" , "`" ), el . content )
247
+ except InvLookupError as e :
248
+ pf . debug ( "WARNING: " + str ( e ) )
159
249
160
250
return el
161
251
0 commit comments