blob: fabd05bfde3a46d93cdc4015199c73589a3abe9d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
-- cgit commit-filter that turns git object names and configurable text
-- references in commit messages into links. Use it with the commit-filter or
-- repo.commit-filter setting and the lua: prefix.
--
--     commit-filter=lua:/path/to/link-commits.lua
--
-- cgit hands the filter the message already HTML-escaped, so this only wraps
-- matches in anchors. No external dependencies. Runs on Lua 5.1 through 5.4
-- and LuaJIT.
--
-- Two kinds of thing are linked, object names (runs of hex that look like git
-- hashes) and any number of text-reference rules you define, each a pattern
-- and a URL. Both are configured among the values below. All matches are resolved
-- in a single left-to-right pass, so nothing is ever linked twice.


-- Object names (git hashes). Handled specially, because the length rule cannot
-- be written as a plain Lua pattern.
--
-- Recognition is by shape, since a commit-filter cannot ask the repository
-- whether a hash is real. Any hex run within the length bounds is linked,
-- whatever mix of digits and letters it has, so abbreviated and all-digit
-- hashes are both caught. The cost is that a long hex-looking number can now
-- and then link to an object that does not exist, which cgit renders as a
-- harmless "bad object name" page. Shape matching is inherently approximate,
-- the length bounds are the only filter.
local objects = {
	-- Set false to stop linking bare hashes.
	enabled = true,
	-- A hex run within these lengths is linked. Git abbreviations run about 7
	-- to 12 characters, full names are 40 (sha1) or 64 (sha256).
	min_length = 7,
	max_length = 64,
	-- Link target, %s is replaced with the matched hash. "./?id=%s" is relative
	-- to the current page and works for the common virtual-root layout.
	url = "./?id=%s",
}

-- Text-reference rules. Each rule is a Lua pattern with ONE capture and a URL
-- where %s is replaced by that capture, percent-encoded. The whole match is
-- shown, the capture is what goes in the URL. Rules are tried in order and the
-- leftmost match on the line wins, so put more specific patterns first. Leave
-- the list empty to link only object names.
--
-- Lua patterns are not regular expressions. There is no alternation and no
-- {n,m} repetition. %d is a digit, %a a letter, %w a letter or digit, %x a hex
-- digit, and a literal magic character is escaped with %, so a literal '-' is
-- '%-'. Reference: https://www.lua.org/manual/5.1/manual.html#5.4.1
--
-- Patterns run against the escaped message, so '&', '<' and '>' reach them as
-- '&amp;', '&lt;' and '&gt;'. Match those entity spellings rather than the
-- bare character, and keep a pattern from ending part way through one, since
-- the matched run is what gets wrapped in the anchor.
local rules = {
	{ pattern = "#(%d+)", url = "https://bugs.example.com/?bug=%s" },
	-- { pattern = "CVE%-(%d%d%d%d%-%d+)", url = "https://www.cve.org/CVERecord?id=CVE-%s" },
	-- { pattern = "!(%d+)",               url = "https://gitlab.example.com/group/repo/-/merge_requests/%s" },
	-- { pattern = "RFC%s?(%d+)",          url = "https://www.rfc-editor.org/rfc/rfc%s" },
}


local chunks = {}

-- Percent-encode everything but the URL-unreserved characters, so a captured
-- value cannot break out of the href attribute or the URL.
local function url_encode(s)
	return (string.gsub(s, "[^%w._~-]", function(c)
		return string.format("%%%02X", string.byte(c))
	end))
end

-- Build one anchor. url_template has %s where the encoded capture goes, display
-- is the text shown. A function replacement is used so a '%' in the encoded
-- value is not treated as a gsub reference.
local function make_link(url_template, capture, display)
	local encoded = url_encode(capture)
	local href = string.gsub(url_template, "%%s", function() return encoded end)
	return '<a href="' .. href .. '">' .. display .. '</a>'
end

-- Collect every candidate match as {s, e, pri, link}. A lower pri wins a tie on
-- the same start position.
local function collect(text)
	local cands = {}
	for pri, rule in ipairs(rules) do
		-- A malformed pattern is an operator error, skip that rule rather than
		-- failing the whole page.
		pcall(function()
			local init = 1
			while init <= #text do
				local s, e, cap = string.find(text, rule.pattern, init)
				if not s then break end
				if cap == nil then
					cap = string.sub(text, s, e)
				end
				cands[#cands + 1] = {
					s = s, e = e, pri = pri,
					link = make_link(rule.url, cap, string.sub(text, s, e)),
				}
				init = (e >= s) and e + 1 or s + 1
			end
		end)
	end
	if objects.enabled then
		local objpri = #rules + 1
		local init = 1
		while init <= #text do
			local s, e, run = string.find(text, "%f[%w](%x+)%f[%W]", init)
			if not s then break end
			if #run >= objects.min_length and #run <= objects.max_length then
				cands[#cands + 1] = {
					s = s, e = e, pri = objpri,
					link = make_link(objects.url, run, run),
				}
			end
			init = e + 1
		end
	end
	return cands
end

function filter_open(...)
	chunks = {}
end

function filter_write(str)
	chunks[#chunks + 1] = str
end

function filter_close()
	local text = table.concat(chunks)
	local cands = collect(text)
	table.sort(cands, function(a, b)
		if a.s ~= b.s then
			return a.s < b.s
		end
		return a.pri < b.pri
	end)
	local out = {}
	local i = 1
	for _, c in ipairs(cands) do
		-- Skip a candidate that overlaps one already emitted.
		if c.s >= i then
			out[#out + 1] = string.sub(text, i, c.s - 1)
			out[#out + 1] = c.link
			i = c.e + 1
		end
	end
	out[#out + 1] = string.sub(text, i)
	html(table.concat(out))
	return 0
end