Skip to content

Commit 67eb185

Browse files
authored
Merge pull request #17 from machow/feat-interlinks
Feat interlinks
2 parents 0fd8eb6 + 3893ad6 commit 67eb185

20 files changed

+476
-55
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ jobs:
7777
- name: Install dependencies
7878
run: |
7979
python -m pip install -r requirements-dev.txt
80+
python -m pip install .
8081
- uses: quarto-dev/quarto-actions/setup@v2
8182
- name: Build docs
8283
run: |

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ README.md: README.qmd
22
quarto render $<
33

44
docs-build:
5+
cd docs && quarto add --no-prompt ..
56
quarto render docs
7+
8+
requirements-dev.txt:
9+
pip-compile setup.cfg --extra dev -o $@
File renamed without changes.
File renamed without changes.

interlinks/_extensions/interlinks/_extension.yml renamed to _extensions/interlinks/_extension.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ version: 1.0.0
44
quarto-required: ">=1.2.0"
55
contributes:
66
filters:
7-
- interlinks.lua
7+
- interlinks.py

_extensions/interlinks/example.qmd

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
title: "Interlinks Example"
3+
filters:
4+
- interlinks.py
5+
interlinks:
6+
sources:
7+
siuba:
8+
url: https://siuba.org/
9+
inv: null
10+
fallback: objects_siuba.json
11+
vetiver:
12+
url: https://vetiver.rstudio.com/
13+
inv: null
14+
fallback: objects_vetiver.json
15+
---
16+
17+
## Heading {#sec-hello}
18+
19+
## Testing
20+
21+
22+
| style | syntax | output |
23+
| ----- | ------ | ------ |
24+
| md custom text | `[some explanation](`vetiver.SKLearnHandler`)` | [some explanation](`vetiver.SKLearnHandler`) |
25+
| md blank text | `[](`vetiver.SKLearnHandler`)` | [](`vetiver.SKLearnHandler`) |
26+
| md blank text (~shortened)| `[](`~vetiver.SKLearnHandler`)` | [](`~vetiver.SKLearnHandler`) |
27+
| rst custom text | `` :ref:`some explanation <vetiver.SKLearnHandler>` `` | :ref:`some explanation <vetiver.SKLearnHandler>` |
28+
| rst blank text | `` :ref:`vetiver.SKLearnHandler` `` | :ref:`vetiver.SKLearnHandler` |
29+
| rst blank text (~shortened) | `` :ref:`~vetiver.SKLearnHandler` `` | :ref:`~vetiver.SKLearnHandler` |
File renamed without changes.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import json
2+
import panflute as pf
3+
4+
from plum import dispatch
5+
6+
7+
inventory = {}
8+
9+
10+
class ConfigError(Exception):
11+
pass
12+
13+
14+
def load_mock_inventory(items: "dict[str, str]"):
15+
for k, v in items.items():
16+
inventory[k] = v
17+
18+
19+
def ref_to_anchor(ref: str, text: "str | pf.ListContainer | None"):
20+
"""Return a Link element based on ref in interlink format
21+
22+
Parameters
23+
----------
24+
ref:
25+
The interlink reference (e.g. "my_module.my_function").
26+
text:
27+
The text to be displayed for the link.
28+
29+
Examples
30+
--------
31+
32+
>>> url = "https://example.org/functools.partial.html"
33+
>>> load_mock_inventory({"functools.partial": {"full_uri": url, "name": "functools.partial"}})
34+
>>> ref_to_anchor("functools.partial")
35+
Link(Str(functools.partial); url='https://example.org/functools.partial.html')
36+
37+
>>> ref_to_anchor("~functools.partial")
38+
Link(Str(partial); url='https://example.org/functools.partial.html')
39+
"""
40+
# TODO: for now we just mutate el
41+
is_shortened = ref.startswith("~")
42+
43+
stripped = ref.lstrip("~")
44+
45+
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}")
50+
51+
if not text:
52+
if is_shortened:
53+
# shorten names from module.sub_module.func_name -> func_name
54+
name = pf.Str(entry["name"].split(".")[-1])
55+
else:
56+
name = pf.Str(entry["name"])
57+
else:
58+
# when the element is an Link, content is a ListContainer, but it has to be
59+
# *splatted back into Link?
60+
if isinstance(text, pf.ListContainer):
61+
return pf.Link(*text, url=dst_url)
62+
elif isinstance(text, str):
63+
return pf.Link(pf.Str(text), url=dst_url)
64+
else:
65+
raise TypeError(f"Unsupported type: {type(text)}")
66+
67+
return pf.Link(name, url=dst_url)
68+
69+
70+
def parse_rst_style_ref(full_text):
71+
"""
72+
Returns
73+
-------
74+
tuple
75+
The parsed title (None if no title specified), and corresponding reference.
76+
"""
77+
78+
import re
79+
80+
# pf.debug(full_text)
81+
82+
m = re.match(r"(?P<text>.+?)\<(?P<ref>[a-zA-Z\.\-: _]+)\>", full_text)
83+
if m is None:
84+
# TODO: print a warning or something
85+
return full_text, None
86+
87+
text, ref = m.groups()
88+
89+
return ref, text
90+
91+
92+
# Visitor ================================================================================
93+
94+
95+
@dispatch
96+
def visit(el, doc):
97+
return el
98+
# raise TypeError(f"Unsupported type: {type(el)}")
99+
100+
101+
@dispatch
102+
def visit(el: pf.MetaList, doc):
103+
meta = doc.get_metadata()
104+
105+
try:
106+
sources = meta["interlinks"]["sources"]
107+
except KeyError:
108+
raise ConfigError(
109+
"No interlinks.sources field detected in your metadata."
110+
"Please add this to your header:\n\n"
111+
"interlinks:"
112+
"\n sources:"
113+
"\n - <source_name>: {url: ..., inv: ..., fallback: ... }"
114+
)
115+
for doc_name, cfg in sources.items():
116+
json_data = json.load(open(cfg["fallback"]))
117+
118+
for item in json_data["items"]:
119+
# TODO: what are the rules for inventories with overlapping names?
120+
# it seems like this is where priority and using source name as an
121+
# optional prefix in references is useful (e.g. siuba:a.b.c).
122+
full_uri = cfg["url"] + item["uri"].replace("$", item["name"])
123+
enh_item = {**item, "full_uri": full_uri}
124+
inventory[item["name"]] = enh_item
125+
126+
return el
127+
128+
129+
@dispatch
130+
def visit(el: pf.Doc, doc):
131+
return el
132+
133+
134+
@dispatch
135+
def visit(el: pf.Plain, doc):
136+
cont = el.content
137+
if len(cont) == 2 and cont[0] == pf.Str(":ref:") and isinstance(cont[1], pf.Code):
138+
_, code = el.content
139+
140+
ref, title = parse_rst_style_ref(code.text)
141+
142+
return pf.Plain(ref_to_anchor(ref, title))
143+
144+
return el
145+
146+
147+
@dispatch
148+
def visit(el: pf.Link, doc):
149+
if el.url.startswith("%60") and el.url.endswith("%60"):
150+
url = el.url[3:-3]
151+
152+
# Get URL ----
153+
154+
# TODO: url can be form external+invname:domain:reftype:target
155+
# for now, assume it's simply <target>. e.g. siuba.dply.verbs.mutate
156+
return ref_to_anchor(url, el.content)
157+
158+
return el
159+
160+
161+
def main(doc=None):
162+
return pf.run_filter(visit, doc=None)
163+
164+
165+
if __name__ == "__main__":
166+
main()
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)