<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<!-- Saved on Wednesday, 22 December 2010, 4:25 PM -->
<!-- MuClient version 4.61 -->
<muclient>
<plugin
name="Hyperlink_URL2"
author="Sketch"
id="520bc4f29806f7af0017985f"
language="Lua"
purpose="Changes URLs on a line into hyperlinks."
date_written="2006-04-01"
date_modified="2010-12-22"
requires="4.42"
version="3.1">
<description trim="y">
<![CDATA[
Detects URLs and turns them into hyperlinks.
]]>
</description>
</plugin>
<!-- Triggers -->
<triggers>
<trigger
enabled="y"
match="(?:https?://|mailto:)\S*[\w/=@#\-\?]"
omit_from_output="y"
ignore_case="y"
regexp="y"
script="onURL"
sequence="100"
>
</trigger>
</triggers>
<!-- Script -->
<script>
<![CDATA[
--[[
-- What it is --
A URL hyperlinker for MUSHclient.
-- Why it's needed --
We want to hyperlink all the URLs in a line, while preserving the original style and color of the line. However, due to how wildcard matching and triggers work, we can't make a trigger to match each URL on a line and hyperlink it.
What do we do?
-- How it works --
MUSHclient provides the plaintext of the whole line to scripts called from triggers, as the argument "line". MUSHclient also provides an array called "styles" to Lua scripts called from triggers. The styles array contains a structure (or "dictionary") for each styled segment of the whole line the trigger matched. The structure is: {textcolour, backcolour, style, text, length} where textcolour and backcolour are the foreground/background color, style is a set of OR-ed flags bold=1, underline=2, blink=4, text is the text in that segment, and length is the length of the segment. It's worth noting that the 'styles' array is contiguous, and the segments it contains span the whole line. Even segments with no special styling are included, and each style begins immediately after the previous one ends.
Using these two arguments, we can compose a line with all URLs hyperlinked, while preserving colors and other formatting.
Short breakdown:
The script takes a matched trigger line and breaks it up in two different ways: An array of segments based on styling is made, and an array of segments based on URLs is made. Using those two arrays, the segments of the line are pieced back together, placing the styling on the hyperlinks.
Long breakdown: (function names in parentheses)
1) Trigger on a line that contains at least one URL-like item. The 'styles' array is passed into the script, as well as the plaintext line.
2) Find all the URLs in the plaintext line, number them, and record their start/end points and text. We now have an array of (to-be) hyperlinks numbered 1-N, with start/end positions for each.
(findURLs)
3) Merge the start and end points of all the styles and hyperlinks into a sorted set. We now have a list of all points where either or both occurs:
* A style changes (even to/from 'no styling').
* A hyperlink begins or ends.
(getpoints)
4) Convert the start and end points into 'slices' of the text line. Disregarding the text itself, each of these slices is homogenous: the style is the same through the whole slice, and the slice is either not part of any hyperlink, or is part of only one hyperlink.
(getslices)
5) Iterate over the slices, recording style/hyperlink number/text for each slice
into an array named 'reformatted'.
(reformat)
6) Iterate over the 'reformatted' array. If the slice contains part of a hyperlink,
print it as such. Although the slice may contain only part of a hyperlink, the
whole URL was stored in the 'hyperlinks' array (in step 2), and can be looked
up by the slice's stored hyperlink number.
You now have a line with detected URLs changed into hyperlinks, and the original styling preserved.
--]]
-- CODE SECTION --
-- Returns an array {start, end, text}
function findURLs(text)
local URLs = {}
local start, position = 0, 0
-- "rex" is a table supplied by MUSHclient for PCRE functionality.
local re = rex.new("(?:https?://|mailto:)\\S*[\\w/=@#\\-\\?]", rex.flags().CASELESS)
re:gmatch(text,
function (link, _)
start, position = string.find(text, link, position, true)
table.insert(URLs, {start=start, stop=position, text=link})
end
)
return URLs
end -- function findURL
-- Returns a table of points where formatting should change in the new string.
function getpoints(styles, hyperlinks)
local points = {}
local unique_points = {}
local pos = 1
for _,v in pairs(styles) do
table.insert(points, pos)
table.insert(points, pos + v.length)
pos = pos + v.length
end
-- The last value of points is now 1 past the end of the string.
for _,v in pairs(hyperlinks) do
table.insert(points, v.start)
table.insert(points, v.stop + 1)
-- The hyperlink itself is at v.stop. v.stop+1 is where the change is.
end
table.sort(points)
return unique_array(points)
end -- function getpoints
-- Returns an array with consecutive duplicate items removed.
function unique_array(a)
local uniq, current = {}, nil
for _,v in pairs(a) do
if v ~= current then
table.insert(uniq, v)
current = v
end
end
return uniq
end -- function unique_array
-- Given an array of numbers, return an array of pairs, to be used in string.sub().
-- Each pair starts at the original array's key, and ends before the next key.
-- Example: [1, 5, 9, 13] --> [{1,4},{5,8},{9,12}]
function getslices(points)
local newpoints = {}
for i = 1, #points - 1, 1 do
table.insert(newpoints, {start=points[i], stop=points[i+1] - 1})
end
return newpoints
end -- function getslices
-- Returns an array of
-- {startpos, endpos, textcolour, backcolour, style, hyperlink_number}
function reformat(slices, styles, hyperlinks)
local styles_i, hyperlinks_i = 1, 1
local hyperlink_number = 0
local reformatted = {}
-- nextstyle is set at the character where the next style begins.
local nextstyle = styles[1].length + 1
-- Add a fake hyperlink past the end of the string at the end of the array.
-- This way, we don't have to keep checking (hyperlinks_i > #hyperlinks).
table.insert(hyperlinks,{start=slices[#slices].stop + 1,stop=slices[#slices].stop + 1,text=""})
for _,v in pairs(slices) do
-- v.start is our 'current position'.
-- Make sure we're using the correct style
if v.start >= nextstyle then
nextstyle = v.start + styles[styles_i + 1].length
styles_i = styles_i + 1
end
-- If we've passed the hyperlink marked by hyperlinks_i, increment it.
if v.start > hyperlinks[hyperlinks_i].stop then
hyperlinks_i = hyperlinks_i + 1
end
-- The hyperlink_number is set to the hyperlink we're checking for if
-- we're at or past its starting position. (In other words, inside it)
if v.start >= hyperlinks[hyperlinks_i].start then
hyperlink_number = hyperlinks_i
else
hyperlink_number = 0
end
table.insert(reformatted,
{startpoint = v.start
,endpoint = v.stop
,textcolour = styles[styles_i].textcolour
,backcolour = styles[styles_i].backcolour
,style = styles[styles_i].style
,hyperlink_number = hyperlink_number}
)
end
return reformatted
end -- function reformat
-- Line: Whole line that contains the trigger, in plaintext.
-- Styles: [{textcolour, backcolour, text, length, style},...]
function onURL (name, line, wildcards, styles)
local hyperlinks = findURLs(line)
local reformatted = reformat(getslices(getpoints(styles,hyperlinks)), styles, hyperlinks)
for _,v in pairs(reformatted) do
NoteStyle(v.style) -- Set style for the segment, regardless of type.
if v.hyperlink_number ~= 0 then
Hyperlink(
hyperlinks[v.hyperlink_number].text -- Hyperlink
,string.sub(line, v.startpoint, v.endpoint) -- Displayed text
,"Go to " .. hyperlinks[v.hyperlink_number].text -- Hover text
,RGBColourToName(v.textcolour) -- Foreground color
,RGBColourToName(v.backcolour) -- Background color
,1 -- Boolean: Open as a URL?
)
else
ColourTell(
RGBColourToName(v.textcolour) -- Foreground color
,RGBColourToName(v.backcolour) -- Background color
,string.sub(line, v.startpoint, v. endpoint) -- Displayed text
)
end
end
Note ("") -- Insert a newline at the end of the string.
end -- function onURL
]]>
</script>
</muclient>
Edit: Works in 3.80 and above, but has color bleeding before version 4.42, so I changed the required version.
[EDIT] Amended 25 December 2010 to be version 3.1 as described below.
[EDIT] Edited 16 May 2011, to be case-insensitive. |