Skip to content

Commit 634acd5

Browse files
feat: render footnote text using superscripts
## Details Request: #241 When the contents of a link starts with the ^ symbol we'll now assume this is related to a footnote. When the footnote pattern is spotted we replace the link contents with a superscript equivalent when possible. There are some interesting gaps in the superscript glyphs, I do not know why they exist, but they exist for parens, 0-9, most of a-z, and also most of A-Z but both character sets have at least one missing glyph. This behavior can be disabled by setting `link.footnote.superscript` to `false`. Since the translation between base character and superscript is not based on a shared offset or anything easy like that we maintain a complete mapping of base character to superscript. If there are any missing characters in the mapping then we skip doing any sort of superscript rendering.
1 parent 3a319cd commit 634acd5

File tree

12 files changed

+196
-18
lines changed

12 files changed

+196
-18
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,11 @@ require('render-markdown').setup({
507507
link = {
508508
-- Turn on / off inline link icon rendering
509509
enabled = true,
510+
-- How to handle footnote links, start with a '^'
511+
footnote = {
512+
-- Replace value with superscript equivalent
513+
superscript = true,
514+
},
510515
-- Inlined with 'image' elements
511516
image = '󰥶 ',
512517
-- Inlined with 'email_autolink' elements
@@ -1050,6 +1055,11 @@ require('render-markdown').setup({
10501055
link = {
10511056
-- Turn on / off inline link icon rendering
10521057
enabled = true,
1058+
-- How to handle footnote links, start with a '^'
1059+
footnote = {
1060+
-- Replace value with superscript equivalent
1061+
superscript = true,
1062+
},
10531063
-- Inlined with 'image' elements
10541064
image = '󰥶 ',
10551065
-- Inlined with 'email_autolink' elements

benches/readme_spec.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local util = require('benches.util')
44

55
describe('README.md', function()
66
it('default', function()
7-
local base_marks = 100
7+
local base_marks = 114
88
util.less_than(util.setup('README.md'), 60)
99
util.num_marks(base_marks)
1010

doc/render-markdown.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,11 @@ Default Configuration ~
554554
link = {
555555
-- Turn on / off inline link icon rendering
556556
enabled = true,
557+
-- How to handle footnote links, start with a '^'
558+
footnote = {
559+
-- Replace value with superscript equivalent
560+
superscript = true,
561+
},
557562
-- Inlined with 'image' elements
558563
image = '󰥶 ',
559564
-- Inlined with 'email_autolink' elements
@@ -1077,6 +1082,11 @@ Link Configuration ~
10771082
link = {
10781083
-- Turn on / off inline link icon rendering
10791084
enabled = true,
1085+
-- How to handle footnote links, start with a '^'
1086+
footnote = {
1087+
-- Replace value with superscript equivalent
1088+
superscript = true,
1089+
},
10801090
-- Inlined with 'image' elements
10811091
image = '󰥶 ',
10821092
-- Inlined with 'email_autolink' elements

lua/render-markdown/health.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local state = require('render-markdown.state')
44
local M = {}
55

66
---@private
7-
M.version = '7.6.1'
7+
M.version = '7.6.2'
88

99
function M.check()
1010
M.start('version')

lua/render-markdown/init.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,12 @@ local M = {}
5454
---@field public icon? string
5555
---@field public highlight? string
5656

57+
---@class (exact) render.md.UserFootnote
58+
---@field public superscript? boolean
59+
5760
---@class (exact) render.md.UserLink
5861
---@field public enabled? boolean
62+
---@field public footnote? render.md.UserFootnote
5963
---@field public image? string
6064
---@field public email? string
6165
---@field public hyperlink? string
@@ -625,6 +629,11 @@ M.default_config = {
625629
link = {
626630
-- Turn on / off inline link icon rendering
627631
enabled = true,
632+
-- How to handle footnote links, start with a '^'
633+
footnote = {
634+
-- Replace value with superscript equivalent
635+
superscript = true,
636+
},
628637
-- Inlined with 'image' elements
629638
image = '󰥶 ',
630639
-- Inlined with 'email_autolink' elements

lua/render-markdown/lib/converter.lua

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---@class render.md.Converter
2+
local M = {}
3+
4+
---@private
5+
M.superscripts = {
6+
[' '] = ' ',
7+
['('] = '',
8+
[')'] = '',
9+
10+
['0'] = '',
11+
['1'] = '¹',
12+
['2'] = '²',
13+
['3'] = '³',
14+
['4'] = '',
15+
['5'] = '',
16+
['6'] = '',
17+
['7'] = '',
18+
['8'] = '',
19+
['9'] = '',
20+
21+
['a'] = '',
22+
['b'] = '',
23+
['c'] = '',
24+
['d'] = '',
25+
['e'] = '',
26+
['f'] = '',
27+
['g'] = '',
28+
['h'] = 'ʰ',
29+
['i'] = '',
30+
['j'] = 'ʲ',
31+
['k'] = '',
32+
['l'] = 'ˡ',
33+
['m'] = '',
34+
['n'] = '',
35+
['o'] = '',
36+
['p'] = '',
37+
['q'] = nil,
38+
['r'] = 'ʳ',
39+
['s'] = 'ˢ',
40+
['t'] = '',
41+
['u'] = '',
42+
['v'] = '',
43+
['w'] = 'ʷ',
44+
['x'] = 'ˣ',
45+
['y'] = 'ʸ',
46+
['z'] = '',
47+
48+
['A'] = '',
49+
['B'] = '',
50+
['C'] = nil,
51+
['D'] = '',
52+
['E'] = '',
53+
['F'] = nil,
54+
['G'] = '',
55+
['H'] = '',
56+
['I'] = '',
57+
['J'] = '',
58+
['K'] = '',
59+
['L'] = '',
60+
['M'] = '',
61+
['N'] = '',
62+
['O'] = '',
63+
['P'] = '',
64+
['Q'] = nil,
65+
['R'] = 'ᴿ',
66+
['S'] = nil,
67+
['T'] = '',
68+
['U'] = '',
69+
['V'] = '',
70+
['W'] = '',
71+
['X'] = nil,
72+
['Y'] = nil,
73+
['Z'] = nil,
74+
}
75+
76+
---@param s string
77+
---@return string?
78+
function M.to_superscript(s)
79+
local chars = {}
80+
for char in s:gmatch('.') do
81+
char = M.superscripts[char]
82+
if char == nil then
83+
return nil
84+
end
85+
table.insert(chars, char)
86+
end
87+
return table.concat(chars)
88+
end
89+
90+
return M

lua/render-markdown/render/inline_highlight.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function Render:render()
1919
---@type integer|nil
2020
local index = 1
2121
while index ~= nil do
22-
local start_index, end_index = self.node.text:find('(=)=[^=]+=(=)', index)
22+
local start_index, end_index = self.node.text:find('==[^=]+==', index)
2323
if start_index == nil or end_index == nil then
2424
index = nil
2525
else

lua/render-markdown/render/shortcut.lua

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
local Base = require('render-markdown.render.base')
2+
local Converter = require('render-markdown.lib.converter')
23
local Str = require('render-markdown.lib.str')
34

45
---@class render.md.render.Shortcut: render.md.Renderer
6+
---@field private link render.md.Link
57
local Render = setmetatable({}, Base)
68
Render.__index = Render
79

810
---@return boolean
911
function Render:setup()
12+
self.link = self.config.link
1013
return true
1114
end
1215

@@ -28,6 +31,12 @@ function Render:render()
2831
self:wiki_link()
2932
return
3033
end
34+
35+
local _, _, text = self.node.text:find('^%[%^(.+)%]$')
36+
if text ~= nil then
37+
self:footnote(text)
38+
return
39+
end
3140
end
3241

3342
---@private
@@ -85,13 +94,13 @@ end
8594

8695
---@private
8796
function Render:wiki_link()
88-
if not self.config.link.enabled then
97+
if not self.link.enabled then
8998
return
9099
end
91100

92101
local parts = Str.split(self.node.text:sub(2, -2), '|')
93102
local link_component = self:link_component(parts[1])
94-
local icon, highlight = self.config.link.wiki.icon, self.config.link.wiki.highlight
103+
local icon, highlight = self.link.wiki.icon, self.link.wiki.highlight
95104
if link_component ~= nil then
96105
icon, highlight = link_component.icon, link_component.highlight
97106
end
@@ -103,4 +112,23 @@ function Render:wiki_link()
103112
}, { 0, -1, 0, 1 })
104113
end
105114

115+
---@private
116+
---@param text string
117+
function Render:footnote(text)
118+
if not self.link.enabled or not self.link.footnote.superscript then
119+
return
120+
end
121+
122+
local value = Converter.to_superscript('(' .. text .. ')')
123+
if value == nil then
124+
return
125+
end
126+
127+
self.marks:add_over('link', self.node, {
128+
virt_text = { { value, self.link.highlight } },
129+
virt_text_pos = 'inline',
130+
conceal = '',
131+
})
132+
end
133+
106134
return Render

lua/render-markdown/state.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ function M.validate()
194194
:nested('checkbox', function(checkbox)
195195
checkbox
196196
:type('enabled', 'boolean')
197-
:type('custom', 'table')
198197
:one_of('position', { 'overlay', 'inline' })
199198
:nested({ 'unchecked', 'checked' }, function(box)
200199
box:type({ 'icon', 'highlight' }, 'string'):type('scope_highlight', { 'string', 'nil' }):check()
@@ -235,7 +234,9 @@ function M.validate()
235234
:nested('link', function(link)
236235
link:type('enabled', 'boolean')
237236
:type({ 'image', 'email', 'hyperlink', 'highlight' }, 'string')
238-
:type({ 'wiki', 'custom' }, 'table')
237+
:nested('footnote', function(footnote)
238+
footnote:type('superscript', 'boolean'):check()
239+
end)
239240
:nested('wiki', function(wiki)
240241
wiki:type({ 'icon', 'highlight' }, 'string'):check()
241242
end)

lua/render-markdown/types.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@
4141
---@field public icon string
4242
---@field public highlight string
4343

44+
---@class (exact) render.md.Footnote
45+
---@field public superscript boolean
46+
4447
---@class (exact) render.md.Link
4548
---@field public enabled boolean
49+
---@field public footnote render.md.Footnote
4650
---@field public image string
4751
---@field public email string
4852
---@field public hyperlink string

tests/ad_hoc_spec.lua

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,21 @@ local function setext_heading(start_row, end_row, level)
3030
end
3131

3232
---@param row integer
33-
---@param length integer
33+
---@param start_col integer
34+
---@param end_col integer
3435
---@param link_text string
3536
---@param highlight 'Link'|'UncycloLink'
3637
---@param conceal string?
37-
---@return render.md.MarkInfo[]
38-
local function bullet_link(row, length, link_text, highlight, conceal)
38+
---@return render.md.MarkInfo
39+
local function link(row, start_col, end_col, link_text, highlight, conceal)
3940
---@type render.md.MarkInfo
40-
local link = {
41+
return {
4142
row = { row, row },
42-
col = { 2, 2 + length },
43+
col = { start_col, end_col },
4344
virt_text = { { link_text, util.hl(highlight) } },
4445
virt_text_pos = 'inline',
4546
conceal = conceal,
4647
}
47-
return { util.bullet(row, 0, 1), link }
4848
end
4949

5050
describe('ad_hoc.md', function()
@@ -63,12 +63,32 @@ describe('ad_hoc.md', function()
6363

6464
vim.list_extend(expected, setext_heading(row:increment(2), row:increment(2), 2))
6565

66+
vim.list_extend(expected, { util.bullet(row:increment(2), 0, 1) })
67+
68+
vim.list_extend(expected, {
69+
util.bullet(row:increment(), 0, 1),
70+
link(row:get(), 2, 15, '󱗖 Basic One', 'UncycloLink', ''),
71+
})
72+
73+
vim.list_extend(expected, {
74+
util.bullet(row:increment(), 0, 1),
75+
link(row:get(), 2, 25, '󱗖 With Alias', 'UncycloLink', ''),
76+
})
77+
78+
vim.list_extend(expected, {
79+
util.bullet(row:increment(), 0, 1),
80+
link(row:get(), 2, 20, '󰀓 [email protected]', 'Link', ''),
81+
})
82+
83+
vim.list_extend(expected, {
84+
util.bullet(row:increment(), 0, 1),
85+
link(row:get(), 2, 61, '', 'Link', nil),
86+
})
87+
6688
vim.list_extend(expected, {
67-
util.bullet(row:increment(2), 0, 1),
68-
bullet_link(row:increment(), 13, '󱗖 Basic One', 'UncycloLink', ''),
69-
bullet_link(row:increment(), 23, '󱗖 With Alias', 'UncycloLink', ''),
70-
bullet_link(row:increment(), 18, '󰀓 [email protected]', 'Link', ''),
71-
bullet_link(row:increment(), 59, '', 'Link', nil),
89+
util.bullet(row:increment(), 0, 1),
90+
link(row:get(), 16, 25, '⁽¹ ᴵⁿᶠᵒ⁾', 'Link', ''),
91+
link(row:increment(2), 0, 9, '⁽¹ ᴵⁿᶠᵒ⁾', 'Link', ''),
7292
})
7393

7494
util.assert_view(expected, {
@@ -83,6 +103,9 @@ describe('ad_hoc.md', function()
83103
' 9 ● 󱗖 With Alias Something important',
84104
' 10 ● 󰀓 [email protected] Email',
85105
' 11 ●  Youtube Link',
106+
' 12 ● Footnote Link ⁽¹ ᴵⁿᶠᵒ⁾',
107+
' 13',
108+
' 14 ⁽¹ ᴵⁿᶠᵒ⁾: Some Info',
86109
})
87110
end)
88111
end)

tests/data/ad_hoc.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ Heading 2 Line 2
99
- [[Nickname|With Alias]] Something important
1010
1111
- [Youtube Link](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
12+
- Footnote Link [^1 Info]
13+
14+
[^1 Info]: Some Info

0 commit comments

Comments
 (0)