Skip to content

Commit 70aec12

Browse files
authored
feat: interlinks autolink (#395)
* chore: prettify interlinks filter * draft: autolinks * test: add autolink test qmd * feat: interlinks autolink and aliases * feat: interlinks auto qd-no-link class * docs: add autolinks to guide * fix: filter handles pandoc inlines better * feat(autolink): alias lists and shortening syntax * docs: autolink shortening and aliases * fix(autolink): remove shortening syntax from unmatched * docs: unmatched autolink example * feat: add ~~. autolink syntax * dev: autolinks do not log debug messages
1 parent b5a9518 commit 70aec12

File tree

7 files changed

+340
-29
lines changed

7 files changed

+340
-29
lines changed

_extensions/interlinks/interlinks.lua

Lines changed: 183 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
local inventory = {} -- sphinx inventories
2+
local autolink -- set in Meta
3+
local autolink_ignore_token = "qd-no-link"
4+
5+
local function _debug_log(text, debug)
6+
if debug then
7+
quarto.log.warning(text)
8+
end
9+
end
10+
111
local function read_inv_text(filename)
212
-- read file
313
local file = io.open(filename, "r")
@@ -11,16 +21,16 @@ local function read_inv_text(filename)
1121
local project = str:match("# Project: (%S+)")
1222
local version = str:match("# Version: (%S+)")
1323

14-
local data = {project = project, version = version, items = {}}
24+
local data = { project = project, version = version, items = {} }
1525

1626
local ptn_data =
1727
"^" ..
18-
"(.-)%s+" .. -- name
19-
"([%S:]-):" .. -- domain
20-
"([%S]+)%s+" .. -- role
21-
"(%-?%d+)%s+" .. -- priority
22-
"(%S*)%s+" .. -- uri
23-
"(.-)\r?$" -- dispname
28+
"(.-)%s+" .. -- name
29+
"([%S:]-):" .. -- domain
30+
"([%S]+)%s+" .. -- role
31+
"(%-?%d+)%s+" .. -- priority
32+
"(%S*)%s+" .. -- uri
33+
"(.-)\r?$" -- dispname
2434

2535

2636
-- Iterate through each line in the file content
@@ -48,7 +58,6 @@ local function read_inv_text(filename)
4858
end
4959

5060
local function read_json(filename)
51-
5261
local file = io.open(filename, "r")
5362
if file == nil then
5463
return nil
@@ -66,18 +75,15 @@ local function read_inv_text_or_json(base_name)
6675
-- TODO: refactors so we don't just close the file immediately
6776
io.close(file)
6877
json = read_inv_text(base_name .. ".txt")
69-
7078
else
7179
json = read_json(base_name .. ".json")
7280
end
7381

7482
return json
7583
end
7684

77-
local inventory = {}
78-
79-
local function lookup(search_object)
80-
85+
-- each inventory has entries: project, version, items
86+
local function lookup(search_object, debug)
8187
local results = {}
8288
for _, inv in ipairs(inventory) do
8389
for _, item in ipairs(inv.items) do
@@ -98,7 +104,7 @@ local function lookup(search_object)
98104
goto continue
99105
else
100106
if search_object.domain or item.domain == "py" then
101-
table.insert(results, item)
107+
table.insert(results, item)
102108
end
103109

104110
goto continue
@@ -112,23 +118,24 @@ local function lookup(search_object)
112118
return results[1]
113119
end
114120
if #results > 1 then
115-
quarto.log.warning("Found multiple matches for " .. search_object.name .. ", using the first match.")
121+
_debug_log("Found multiple matches for " .. search_object.name .. ", using the first match.", debug)
116122
return results[1]
117123
end
118124
if #results == 0 then
119-
quarto.log.warning("Found no matches for object:\n", search_object)
125+
_debug_log("Found no matches for object:\n", debug)
126+
_debug_log(search_object, debug)
120127
end
121128

122129
return nil
123130
end
124131

125-
local function mysplit (inputstr, sep)
132+
local function mysplit(inputstr, sep)
126133
if sep == nil then
127-
sep = "%s"
134+
sep = "%s"
128135
end
129-
local t={}
130-
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
131-
table.insert(t, str)
136+
local t = {}
137+
for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
138+
table.insert(t, str)
132139
end
133140
return t
134141
end
@@ -140,7 +147,84 @@ local function normalize_role(role)
140147
return role
141148
end
142149

143-
local function build_search_object(str)
150+
local function copy_replace(original, key, new_value)
151+
-- First create a copy of the table
152+
local copy = {}
153+
for k, v in pairs(original) do
154+
copy[k] = v
155+
end
156+
157+
-- Then replace the specific value
158+
copy[key] = new_value
159+
160+
return copy
161+
end
162+
163+
local function contains(list, value)
164+
-- check if list contains a value
165+
for i, v in ipairs(list) do
166+
if v == value then
167+
return true
168+
end
169+
end
170+
return false
171+
end
172+
173+
local function flatten_alias_list(list)
174+
-- flatten a list of lists into a single list,
175+
-- where each entry has the form {key, subvalue}}
176+
-- e.g.
177+
-- input: {key1 = {subval1, subval2}, key2 = subval3}
178+
-- output: {{key1, subval1}, {key1, subval2}, {key2, subval3}}
179+
local flat = {}
180+
for key, sublist in pairs(list) do
181+
if type(sublist) == "table" then
182+
for _, subvalue in ipairs(sublist) do
183+
table.insert(flat, { key, subvalue })
184+
end
185+
else
186+
table.insert(flat, { key, sublist })
187+
end
188+
end
189+
return flat
190+
end
191+
192+
local function prepend_aliases(flat_aliases)
193+
-- if str up to first period starts with an alias, then
194+
-- replace it with the full name.
195+
-- For example, suppose we have the alias quartodoc -> qd
196+
-- e.g. qd.Auto -> quartodoc.Auto
197+
-- e.g. qda.Auto -> qda.Auto
198+
199+
local new_inv = {}
200+
new_inv["project"] = "aliases"
201+
new_inv["version"] = "0.0.9999" -- I have not begun to think about version...
202+
new_inv["items"] = {}
203+
204+
for _, name_pair in pairs(flat_aliases) do
205+
local full = name_pair[1]
206+
local alias = name_pair[2]
207+
for _, inv in ipairs(inventory) do
208+
for _, item in ipairs(inv.items) do
209+
if string.sub(item.name, 1, string.len(full) + 1) == (full .. ".") then
210+
-- replace full .. "." with alias .. "."
211+
local prefix
212+
if not alias or pandoc.utils.stringify(alias) == "" then
213+
prefix = ""
214+
else
215+
-- TODO: ensure alias doesn't end with period
216+
prefix = pandoc.utils.stringify(alias) .. "."
217+
end
218+
local new_name = prefix .. string.sub(item.name, string.len(full) + 2)
219+
table.insert(new_inv.items, copy_replace(item, "name", new_name))
220+
end
221+
end
222+
end
223+
end
224+
table.insert(inventory, new_inv)
225+
end
226+
227+
local function build_search_object(str, debug)
144228
local starts_with_colon = str:sub(1, 1) == ":"
145229
local search = {}
146230
if starts_with_colon then
@@ -163,15 +247,15 @@ local function build_search_object(str)
163247
search.role = normalize_role(t[3])
164248
search.name = t[4]:match("%%60(.*)%%60")
165249
else
166-
quarto.log.warning("couldn't parse this link: " .. str)
250+
_debug_log("couldn't parse this link: " .. str, debug)
167251
return {}
168252
end
169253
else
170254
search.name = str:match("%%60(.*)%%60")
171255
end
172256

173257
if search.name == nil then
174-
quarto.log.warning("couldn't parse this link: " .. str)
258+
_debug_log("couldn't parse this link: " .. str, debug)
175259
return {}
176260
end
177261

@@ -220,7 +304,60 @@ function Link(link)
220304
return link
221305
end
222306

223-
local function fixup_json(json, prefix)
307+
function Code(code)
308+
if (not autolink) or contains(code.classes, autolink_ignore_token) then
309+
return code
310+
end
311+
312+
-- allow text for lookup to be simple function call
313+
-- and also support shortened syntax (~~ prefix)
314+
-- e.g. my_func() -> my_func
315+
-- e.g. a.b.call() -> a.b.call
316+
-- e.g. ~~my_func() -> my_func
317+
local text
318+
319+
-- detect and remove shortening syntax (~~ prefix)
320+
local is_shortened = code.text:sub(1, 2) == "~~"
321+
local is_short_dot = code.text:sub(1, 3) == "~~."
322+
local unprefixed = code.text:gsub("^~~%.?", "")
323+
if unprefixed:match("%(%s*%)") then
324+
text = unprefixed:gsub("%(%s*%)", "")
325+
else
326+
text = unprefixed
327+
end
328+
329+
330+
-- return code.attr
331+
local search = build_search_object("%60" .. text .. "%60")
332+
local item = lookup(search)
333+
334+
-- determine replacement, used if no link text specified ----
335+
if item == nil then
336+
code.text = unprefixed
337+
return code
338+
end
339+
340+
-- shorten text if shortening syntax used
341+
if is_shortened then
342+
-- keep text after last period (.)
343+
local split = mysplit(unprefixed, ".")
344+
if #split > 0 then
345+
local new_name = split[#split]
346+
if is_short_dot then
347+
-- if shortened with dot, keep the dot
348+
new_name = "." .. new_name
349+
end
350+
code.text = new_name
351+
else
352+
code.text = unprefixed
353+
end
354+
end
355+
356+
357+
return pandoc.Link(code, item.uri:gsub("%$$", search.name))
358+
end
359+
360+
local function fixup_json(json, prefix, attach)
224361
for _, item in ipairs(json.items) do
225362
item.uri = prefix .. item.uri
226363
end
@@ -232,6 +369,23 @@ return {
232369
Meta = function(meta)
233370
local json
234371
local prefix
372+
local aliases
373+
374+
-- set globals from config
375+
if meta.interlinks and meta.interlinks.autolink then
376+
autolink = true
377+
else
378+
autolink = false
379+
end
380+
381+
local aliases
382+
if meta.interlinks and meta.interlinks.aliases then
383+
aliases = meta.interlinks.aliases
384+
else
385+
aliases = {}
386+
end
387+
388+
-- process sources
235389
if meta.interlinks and meta.interlinks.sources then
236390
for k, v in pairs(meta.interlinks.sources) do
237391
local base_name = quarto.project.offset .. "/_inv/" .. k .. "_objects"
@@ -246,9 +400,12 @@ return {
246400
if json ~= nil then
247401
fixup_json(json, "/")
248402
end
403+
404+
prepend_aliases(flatten_alias_list(aliases))
249405
end
250406
},
251407
{
252-
Link = Link
408+
Link = Link,
409+
Code = Code
253410
}
254411
}

_extensions/interlinks/objects.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Sphinx inventory version 2
2+
# Project: quartodoc
3+
# Version: 0.0.9999
4+
# The remainder of this file is compressed using zlib.
5+
some_func py:function 1 api/some_func.html -
6+
a.b.c py:function 1 api/a.b.c.html -
7+
quartodoc.Auto py:class 1 api/Auto.html -

_extensions/interlinks/test.qmd

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
filters:
3+
- interlinks.lua
4+
interlinks:
5+
autolink: true
6+
aliases:
7+
quartodoc: null
8+
#sources:
9+
# test:
10+
# url: https://example.com
11+
---
12+
13+
* `some_func`
14+
* `some_func()`
15+
* `some_func(a=1)`
16+
* `some_func()`{.qd-no-link}
17+
* `some_func + some_func`
18+
* `a.b.c`
19+
* `~a.b.c`
20+
* `a.b.c()`
21+
* `quartodoc.Auto()`
22+
* `Auto()`

docs/_quarto.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ filters:
1616

1717
interlinks:
1818
fast: true
19+
autolink: true
20+
aliases:
21+
quartodoc: [null, qd]
1922
sources:
2023
python:
2124
url: https://docs.python.org/3/
@@ -56,10 +59,14 @@ website:
5659
- get-started/basic-content.qmd
5760
- get-started/basic-building.qmd
5861
- get-started/crossrefs.qmd
59-
- get-started/interlinks.qmd
6062
- get-started/sidebar.qmd
6163
- get-started/extending.qmd
6264

65+
- section: Interlinking
66+
contents:
67+
- get-started/interlinks.qmd
68+
- get-started/interlinks-autolink.qmd
69+
6370
- section: "Docstrings"
6471
contents:
6572
- get-started/docstring-style.qmd

0 commit comments

Comments
 (0)