handlers/hook_transformer: Actually fire USER.BACK

import IRCClient, priority from require 'lib.irc'
import Map, Option from require 'lib.std'
import Channel, User from require 'lib.models'

unpack = unpack or table.unpack

missing_nick_for = (command, nick)-> "Couldn't see nick for #{command}: #{nick}"
missing_channel_for = (command, channel)-> "Couldn't see channel for #{command}: #{channel}"

IRCClient\add_handler '005', (prefix, args)=>
	-- Capabilities
	isupport_caps = {select 2, unpack args}
	for cap in *isupport_caps
		local cap_name
		if cap\sub(1, 1) == "-"
			cap_name = cap\match "^%-([^=]*)"
			cap_value = @server.caps[cap_name]
			@server.caps[cap_name] = nil
			@fire_hook "ISUPPORT.#{cap_name}.DEL", cap_value
		else
			cap_name = cap\match "^([^=]*)"
			cap_value = cap\find"=" and cap\match"=(.+)$" or true
			@server.caps[cap_name] = cap_value
			@fire_hook "ISUPPORT.#{cap_name}.ADD", cap_value
		@fire_hook "ISUPPORT.#{cap_name}", cap_value

IRCClient\add_handler 'AWAY', (prefix, args)=>
	{:nick} = prefix
	user = @users\expect nick, missing_nick_for "AWAY", nick
	if args[1]
		user.away = Option args[1]
		@fire_hook "USER.AWAY", user, user.away
	else
		user.away = Option!
		@fire_hook "USER.BACK", user

IRCClient\add_handler 'ACCOUNT', (prefix, args)=>
	{:nick} = prefix
	user = @users\expect nick missing_nick_for "ACCOUNT", nick
	if args[1] == "*"
		user.account = Option!
		@fire_hook "USER.SIGNOUT", user
	else
		user.account = Option args[1]
		@fire_hook "USER.SIGNIN", user, args[1]

IRCClient\add_handler 'RENAME', (prefix, args)=>
	{old, new} = args
	old_channel = @channels\expect old, missing_channel_for "RENAME", old
	old_channel.name = new_channel
	for _, user in old_channel.users\iter!
		users.channels\set new, old_channel
		users.channels\remove old
	@channels\set new, old_channel
	@channels\remove old

	@fire_hook "CHANNEL.RENAME", old_channel, old, new

IRCClient\add_handler 'JOIN', (prefix, args, tags)=>
	local account, realname
	{:nick, :user, :host} = prefix
	channel = args[1]
	if @server.ircv3_caps['extended-join']
		account = args[2] != '*' and args[2] or nil
		realname = args[3]
	elseif @server.ircv3_caps['account-tag']
		account = tags.account

	-- Make sure user exists
	joined_user = @users\entry(nick)\or_insert_with ->
		User self, nick, user, host, :account, :realname

	-- Make sure channel exists
	unless @channels\contains_key channel -- Bot has just joined the channel
		-- Get a list of accounts for all channel users
		if @server.caps['WHOX'] and not @server.ircv3_caps['account-tag']
			@send_raw 'WHO', channel, '%nat,001'

		-- Get user and host for all channel users
		unless @server.ircv3_caps['userhost-in-names']
			@send_raw 'WHO', channel

		@channels\set channel, Channel self, channel, {
			users: {
				[nick]: joined_user
			}
		}
	else
		@channels\unwrap(channel).users\set nick, joined_user

	channel_object = @channels\unwrap channel

	-- Add this channel to the joined user's channel list
	joined_user.channels\set channel, channel_object
	@fire_hook "CHANNEL.JOIN", channel_object, joined_user
	@fire_hook "USER.JOIN", joined_user, channel_object

IRCClient\add_hook 'WHOX_001', (nick, account)=>
	@users\expect(nick, missing_nick_for "WHOX_001", nick).account = Option(account) if account != '0'

IRCClient\add_handler '353', (prefix, args)=>
	-- Result of NAMES, for JOIN
	target = args[3]
	statuses = @server.caps.PREFIX and @server.caps.PREFIX\match '%(.-%)(.+)' or "+@"
	statuses = "[" .. statuses\gsub("%p", "%%%1") .. "]"

	channel = @channels\expect target

	for text in args[#args]\gmatch '%S+'
		local status, pre, nick, ident, host
		if text\match statuses
			status, pre = text\match "^(#{statuses}*)(.-)$"
		else
			status, pre = nil, text
		if @server.ircv3_caps['userhost-in-names']
			nick, ident, host = pre\match '^(.-)!(.-)@(.-)$'
		else
			nick = pre

		channel.statuses\set nick, status

		if @users\contains_key nick -- NAMES not triggered from JOIN
			user = @users\expect nick
			user.user = ident if ident
			user.host = host if host
		else
			@users\set nick, User(self, nick, ident or "", host or "")

		-- Make sure channels and users both have links to each other
		channel.users\set nick, @users\expect nick
		@users\expect(nick).channels\set target, channel

IRCClient\add_handler '352', (prefix, args)=>
	-- Result of WHO, for JOIN
	_, user, host, _, nick, away = unpack args
	client = @users\entry(nick)\or_insert_with -> User self, nick, user, host
	client.user = user
	client.host = host
	client.away = Option away\sub(1, 1) == "G" and "No value set" or nil -- "H"ere or "G"one

IRCClient\add_handler 'NICK', (prefix, args)=>
	{nick: old} = prefix
	{new} = args
	user = @users\expect old, missing_nick_for "NICK", old
	for _, channel in user.channels\iter!
		channel.users\set new, user
		channel.users\remove old
	@users\set new, user
	@users\remove old
	@fire_hook "USER.NICK", user, old, new
	-- Tempted to put in a USER.RENAME here but it could be used further down
	-- the line for some other reason.

IRCClient\add_handler 'MODE', (prefix, args)=>
	-- User or bot called /mode
	-- This should be managed further down the line with events such as
	-- CHANNEL.OP, CHANNEL.DEOP, CHANNEL.MODE, etc. but it's not something
	-- I have all the stuff for right now. For now, we're just going to fire
	-- a generic CHANNEL.MODE or USER.MODE event.
	target = args[1]
	table.remove(args, 1)
	if target\match "^#"
		@send_raw "NAMES", target
		channel = @channels\expect target, missing_channel_for "MODE", target
		@fire_hook "CHANNEL.MODE", channel, unpack args
	else
		user = @users\expect target, missing_nick_for "MODE", target
		@fire_hook "USER.MODE", user, unpack args

IRCClient\add_handler 'CHGHOST', (prefix, args)=>
	{:nick} = prefix
	client = @users\expect nick, missing_nick_for "CHGHOST", nick
	client.user, client.host = unpack args
	@fire_hook "USER.CHGHOST", client, unpack args

IRCClient\add_handler 'KICK', (prefix, args)=>
	{channel, target, message} = args
	client = @users\expect target, missing_nick_for "KICK", target
	channel = @chanenls\expect channel, missing_channel_for "KICK", channel
	channel\remove client.nick
	client.channels\remove channel.name
	@users\remove client.nick unles client\is_visible!
	@fire_hook "CHANNEL.KICK", channel, client, message

IRCClient\add_handler 'PART', (prefix, args)=>
	{channel, target, message} = args
	client = @users\expect target, missing_nick_for "PART", target
	channel = @chanenls\expect channel, missing_channel_for "PART", channel
	channel\remove client.nick
	client.channels\remove channel.name
	@users\remove client.nick unles client\is_visible!
	@fire_hook "CHANNEL.PART", channel, client, message
	@fire_hook "USER.PART", client, channel, message

IRCClient\add_handler 'QUIT', (prefix, args)=>
	{:nick} = prefix
	user = @users\expect nick, missing_nick_for "QUIT", nick
	for _, channel in user.channels\iter!
		channel\remove nick
	@users\remove nick

import IRCClient, priority from require 'lib.irc'
import Map, Option from require 'lib.std'
import Channel, User from require 'lib.models'

unpack = unpack or table.unpack

missing_nick_for = (command, nick)-> "Couldn't see nick for #{command}: #{nick}"
missing_channel_for = (command, channel)-> "Couldn't see channel for #{command}: #{channel}"

IRCClient\add_handler '005', (prefix, args)=>
	-- Capabilities
	isupport_caps = {select 2, unpack args}
	for cap in *isupport_caps
		local cap_name
		if cap\sub(1, 1) == "-"
			cap_name = cap\match "^%-([^=]*)"
			cap_value = @server.caps[cap_name]
			@server.caps[cap_name] = nil
			@fire_hook "ISUPPORT.#{cap_name}.DEL", cap_value
		else
			cap_name = cap\match "^([^=]*)"
			cap_value = cap\find"=" and cap\match"=(.+)$" or true
			@server.caps[cap_name] = cap_value
			@fire_hook "ISUPPORT.#{cap_name}.ADD", cap_value
		@fire_hook "ISUPPORT.#{cap_name}", cap_value

IRCClient\add_handler 'AWAY', (prefix, args)=>
	{:nick} = prefix
	user = @users\expect nick, missing_nick_for "AWAY", nick
	if args[1]
		user.away = Option args[1]
		@fire_hook "USER.AWAY", user, user.away
	else
		user.away = Option!

IRCClient\add_handler 'ACCOUNT', (prefix, args)=>
	{:nick} = prefix
	user = @users\expect nick missing_nick_for "ACCOUNT", nick
	if args[1] == "*"
		user.account = Option!
		@fire_hook "USER.SIGNOUT", user
	else
		user.account = Option args[1]
		@fire_hook "USER.SIGNIN", user, args[1]

IRCClient\add_handler 'RENAME', (prefix, args)=>
	{old, new} = args
	old_channel = @channels\expect old, missing_channel_for "RENAME", old
	old_channel.name = new_channel
	for _, user in old_channel.users\iter!
		users.channels\set new, old_channel
		users.channels\remove old
	@channels\set new, old_channel
	@channels\remove old

	@fire_hook "CHANNEL.RENAME", old_channel, old, new

IRCClient\add_handler 'JOIN', (prefix, args, tags)=>
	local account, realname
	{:nick, :user, :host} = prefix
	channel = args[1]
	if @server.ircv3_caps['extended-join']
		account = args[2] != '*' and args[2] or nil
		realname = args[3]
	elseif @server.ircv3_caps['account-tag']
		account = tags.account

	-- Make sure user exists
	joined_user = @users\entry(nick)\or_insert_with ->
		User self, nick, user, host, :account, :realname

	-- Make sure channel exists
	unless @channels\contains_key channel -- Bot has just joined the channel
		-- Get a list of accounts for all channel users
		if @server.caps['WHOX'] and not @server.ircv3_caps['account-tag']
			@send_raw 'WHO', channel, '%nat,001'

		-- Get user and host for all channel users
		unless @server.ircv3_caps['userhost-in-names']
			@send_raw 'WHO', channel

		@channels\set channel, Channel self, channel, {
			users: {
				[nick]: joined_user
			}
		}
	else
		@channels\unwrap(channel).users\set nick, joined_user

	channel_object = @channels\unwrap channel

	-- Add this channel to the joined user's channel list
	joined_user.channels\set channel, channel_object
	@fire_hook "CHANNEL.JOIN", channel_object, joined_user
	@fire_hook "USER.JOIN", joined_user, channel_object

IRCClient\add_hook 'WHOX_001', (nick, account)=>
	@users\expect(nick, missing_nick_for "WHOX_001", nick).account = Option(account) if account != '0'

IRCClient\add_handler '353', (prefix, args)=>
	-- Result of NAMES, for JOIN
	target = args[3]
	statuses = @server.caps.PREFIX and @server.caps.PREFIX\match '%(.-%)(.+)' or "+@"
	statuses = "[" .. statuses\gsub("%p", "%%%1") .. "]"

	channel = @channels\expect target

	for text in args[#args]\gmatch '%S+'
		local status, pre, nick, ident, host
		if text\match statuses
			status, pre = text\match "^(#{statuses}*)(.-)$"
		else
			status, pre = nil, text
		if @server.ircv3_caps['userhost-in-names']
			nick, ident, host = pre\match '^(.-)!(.-)@(.-)$'
		else
			nick = pre

		channel.statuses\set nick, status

		if @users\contains_key nick -- NAMES not triggered from JOIN
			user = @users\expect nick
			user.user = ident if ident
			user.host = host if host
		else
			@users\set nick, User(self, nick, ident or "", host or "")

		-- Make sure channels and users both have links to each other
		channel.users\set nick, @users\expect nick
		@users\expect(nick).channels\set target, channel

IRCClient\add_handler '352', (prefix, args)=>
	-- Result of WHO, for JOIN
	_, user, host, _, nick, away = unpack args
	client = @users\entry(nick)\or_insert_with -> User self, nick, user, host
	client.user = user
	client.host = host
	client.away = Option away\sub(1, 1) == "G" and "No value set" or nil -- "H"ere or "G"one

IRCClient\add_handler 'NICK', (prefix, args)=>
	{nick: old} = prefix
	{new} = args
	user = @users\expect old, missing_nick_for "NICK", old
	for _, channel in user.channels\iter!
		channel.users\set new, user
		channel.users\remove old
	@users\set new, user
	@users\remove old
	@fire_hook "USER.NICK", user, old, new
	-- Tempted to put in a USER.RENAME here but it could be used further down
	-- the line for some other reason.

IRCClient\add_handler 'MODE', (prefix, args)=>
	-- User or bot called /mode
	-- This should be managed further down the line with events such as
	-- CHANNEL.OP, CHANNEL.DEOP, CHANNEL.MODE, etc. but it's not something
	-- I have all the stuff for right now. For now, we're just going to fire
	-- a generic CHANNEL.MODE or USER.MODE event.
	target = args[1]
	table.remove(args, 1)
	if target\match "^#"
		@send_raw "NAMES", target
		channel = @channels\expect target, missing_channel_for "MODE", target
		@fire_hook "CHANNEL.MODE", channel, unpack args
	else
		user = @users\expect target, missing_nick_for "MODE", target
		@fire_hook "USER.MODE", user, unpack args

IRCClient\add_handler 'CHGHOST', (prefix, args)=>
	{:nick} = prefix
	client = @users\expect nick, missing_nick_for "CHGHOST", nick
	client.user, client.host = unpack args
	@fire_hook "USER.CHGHOST", client, unpack args

IRCClient\add_handler 'KICK', (prefix, args)=>
	{channel, target, message} = args
	client = @users\expect target, missing_nick_for "KICK", target
	channel = @chanenls\expect channel, missing_channel_for "KICK", channel
	channel\remove client.nick
	client.channels\remove channel.name
	@users\remove client.nick unles client\is_visible!
	@fire_hook "CHANNEL.KICK", channel, client, message

IRCClient\add_handler 'PART', (prefix, args)=>
	{channel, target, message} = args
	client = @users\expect target, missing_nick_for "PART", target
	channel = @chanenls\expect channel, missing_channel_for "PART", channel
	channel\remove client.nick
	client.channels\remove channel.name
	@users\remove client.nick unles client\is_visible!
	@fire_hook "CHANNEL.PART", channel, client, message
	@fire_hook "USER.PART", client, channel, message

IRCClient\add_handler 'QUIT', (prefix, args)=>
	{:nick} = prefix
	user = @users\expect nick, missing_nick_for "QUIT", nick
	for _, channel in user.channels\iter!
		channel\remove nick
	@users\remove nick


utf8: remove extra space

utf8: remove extra space

Former-commit-id: 7dd597dcd2a6af8dd9f5101ecfb99618b57f0ae3 [formerly f8e2daa41cd7599e164fa9e763f229a31ab1593c]
Former-commit-id: 02a3321fca17e7eede43f7244210bfd0a7055b02

html2unicode = require'html'
math = require'math'

an = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.?!"'`()[]{}<>&_]]
charmaps = {
ci: 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl: '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'
ud: [[∀BƆDƎℲפHIſK˥WNOԀQRS┴∩ΛMX⅄Zɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz0ƖᄅƐㄣϛ9ㄥ86'˙¿¡,,,)(][}{><⅋‾]]
nc: [[🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩⓿]]
sq: [[🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉0123456789,⊡]]
ns: [[🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉]]
ds: [[𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡]]
bo: [[𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗]]
bi: [[𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛0123456789]]
bs: [[𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃]]
pt: [[⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵0⑴⑵⑶⑷⑸⑹⑺⑻⑼]]
}
codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

-- Construct a table which can be used for lookup replacement later
-- Iterate over normal ascii and find counterparts in the weirdo strings as substrings
maps = {}
for charmap, chars in pairs charmaps
  i = 1
  maps[charmap] = {}
  for uchar in string.gfind(chars, "([%z\1-\127\194-\244][\128-\191]*)")
    maps[charmap][an\sub(i,i)] = uchar
    i = i +1

unichr = (n) ->
  html2unicode('&#x%x;'\format(n))

wireplace = (offset, arg) ->
    s = arg or ''
    t = {}
    for i = 1, #s
      bc = string.byte(s, i, i)
      -- Replace space width ideographic space for fullwidth offset
      if bc == 32 and offset == 0xFEE0
        t[#t + 1] = '\227\128\128'
      elseif bc == 32
        t[#t + 1] = ' '
      elseif bc < 0x80 then
        t[#t + 1] = html2unicode("&#" .. (offset + bc) .. ";")
      else
        t[#t + 1] = s\sub(i, i)

    table.concat(t, "")

remap = (map, s) ->
  table.concat [map[s\sub(i,i)] or s\sub(i,i) for i=1, #s], ''

zalgo = (text, intensity=50) ->
  zalgo_chars = {}
  for i=0x0300, 0x036f
    zalgo_chars[i-0x2ff] = unichr(i)

  zalgo_chars[#zalgo_chars + 1] = unichr(0x0488)
  zalgo_chars[#zalgo_chars + 0] = unichr(0x0489)

  zalgoized = {}
  for letter in codepoints(text)
    zalgoized[#zalgoized + 1] = letter
    zalgo_num = math.random(1, intensity)
    for i=1, zalgo_num
      zalgoized[#zalgoized + 1] = zalgo_chars[math.random(1, #zalgo_chars)]
  table.concat(zalgoized)

PRIVMSG:
  '^%pwide (.+)$': (source, destination, arg) =>
    say wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    say remap(maps.bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    say remap(maps.ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    say zalgo(arg, 7)
  '^%pupsidedown (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pflip (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pparanthesized (.+)$': (source, destination, arg) =>
    say remap(maps.pt, arg)
  '^%pnegcircle (.+)$': (source, destination, arg) =>
    say remap(maps['nc'], arg)
  '^%psquare (.+)$': (source, destination, arg) =>
    say remap(maps.sq, arg)
  '^%pnegsquare (.+)$': (source, destination, arg) =>
    say remap(maps.ns, arg)
  '^%pdoublestruck (.+)$': (source, destination, arg) =>
    say remap(maps.ds, arg)
  '^%pubold (.+)$': (source, destination, arg) =>
    say remap(maps.bo, arg)
  '^%pbolditalic (.+)$': (source, destination, arg) =>
    say remap(maps.bi, arg)
  '^%pboldscript (.+)$': (source, destination, arg) =>
    say remap(maps.bs, arg)

html2unicode = require'html'
math = require'math'

an = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.?!"'`()[]{}<>&_]]
charmaps = {
ci: 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl: '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'
ud: [[∀BƆDƎℲפHIſK˥WNOԀQRS┴∩ΛMX⅄Zɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz0ƖᄅƐㄣϛ9ㄥ86'˙¿¡,,,)(][}{><⅋‾]]
nc: [[🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩⓿]]
sq: [[🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉0123456789,⊡]]
ns: [[🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉]]
ds: [[𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡]]
bo: [[𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗]]
bi: [[𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛0123456789]]
bs: [[𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃]]
pt: [[ ⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵0⑴⑵⑶⑷⑸⑹⑺⑻⑼]]
}
codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

-- Construct a table which can be used for lookup replacement later
-- Iterate over normal ascii and find counterparts in the weirdo strings as substrings
maps = {}
for charmap, chars in pairs charmaps
  i = 1
  maps[charmap] = {}
  for uchar in string.gfind(chars, "([%z\1-\127\194-\244][\128-\191]*)")
    maps[charmap][an\sub(i,i)] = uchar
    i = i +1

unichr = (n) ->
  html2unicode('&#x%x;'\format(n))

wireplace = (offset, arg) ->
    s = arg or ''
    t = {}
    for i = 1, #s
      bc = string.byte(s, i, i)
      -- Replace space width ideographic space for fullwidth offset
      if bc == 32 and offset == 0xFEE0
        t[#t + 1] = '\227\128\128'
      elseif bc == 32
        t[#t + 1] = ' '
      elseif bc < 0x80 then
        t[#t + 1] = html2unicode("&#" .. (offset + bc) .. ";")
      else
        t[#t + 1] = s\sub(i, i)

    table.concat(t, "")

remap = (map, s) ->
  table.concat [map[s\sub(i,i)] or s\sub(i,i) for i=1, #s], ''

zalgo = (text, intensity=50) ->
  zalgo_chars = {}
  for i=0x0300, 0x036f
    zalgo_chars[i-0x2ff] = unichr(i)

  zalgo_chars[#zalgo_chars + 1] = unichr(0x0488)
  zalgo_chars[#zalgo_chars + 0] = unichr(0x0489)

  zalgoized = {}
  for letter in codepoints(text)
    zalgoized[#zalgoized + 1] = letter
    zalgo_num = math.random(1, intensity)
    for i=1, zalgo_num
      zalgoized[#zalgoized + 1] = zalgo_chars[math.random(1, #zalgo_chars)]
  table.concat(zalgoized)

PRIVMSG:
  '^%pwide (.+)$': (source, destination, arg) =>
    say wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    say remap(maps.bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    say remap(maps.ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    say zalgo(arg, 7)
  '^%pupsidedown (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pflip (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pparanthesized (.+)$': (source, destination, arg) =>
    say remap(maps.pt, arg)
  '^%pnegcircle (.+)$': (source, destination, arg) =>
    say remap(maps['nc'], arg)
  '^%psquare (.+)$': (source, destination, arg) =>
    say remap(maps.sq, arg)
  '^%pnegsquare (.+)$': (source, destination, arg) =>
    say remap(maps.ns, arg)
  '^%pdoublestruck (.+)$': (source, destination, arg) =>
    say remap(maps.ds, arg)
  '^%pubold (.+)$': (source, destination, arg) =>
    say remap(maps.bo, arg)
  '^%pbolditalic (.+)$': (source, destination, arg) =>
    say remap(maps.bi, arg)
  '^%pboldscript (.+)$': (source, destination, arg) =>
    say remap(maps.bs, arg)


Fix imports

Fix imports

-------------------------------------------------------------------------------
-- Defines the shape of characters that can be drawn onto the screen.
-------------------------------------------------------------------------------
-- @classmod yae.Font

FreeTypeFontGenerator = sauce3.java.require "com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator"
GlyphLayout = sauce3.java.require "com.badlogic.gdx.graphics.g2d.GlyphLayout"
Constants = require "sauce3.wrappers"

class
  new: (filename, size=16, filetype) =>
    file = nil

    if filename == nil
      file = yae.File("sauce3/font.ttf", filetype)
    else
      file = yae.File(filename, filetype)

    generator = yae.java.new FreeTypeFontGenerator, file.file
    @font = generator\generateFont size
    @fontTexture = @font\getRegion(0)\getTexture!
    @fontTexture\setFilter Constants.filters["linear"], Constants.filters["linear"]
    @glyphLayout = yae.java.new GlyphLayout

  get_ascent: =>
    @font\getAscent!

  get_descent: =>
    @font\getDescent!

  get_line_height: =>
    @font\getLineHeight!

  get_bounds: (text) =>
    @glyphLayout\setText text
    @glyphLayout.width, @glyphLayout.height

  get_filter: =>
    min_filter = @fontTexture\getMinFilter!
    mag_filter = @fontTexture\getMagFilter!
    Constants.filtercodes[min_filter], Constants.filtercodes[mag_filter]

  get_height: (text) =>
    _, h = @getBounds text
    h

  get_width: (text) =>
    w, _ = @getBounds text
    w

  has_glyphs: (...) =>
    args = table.pack ...
    found = true

    for i = 1, args.n
      found = found and @font\containsCharacter args[i]

    found

  set_filter: (min, mag) =>
    @fontTexture\setFilter Constants.filters[min], Constants.filters[mag]

  set_line_height: (height) =>
    @font\getData()\setLineHeight height

  free: =>
    @font\dispose!

-------------------------------------------------------------------------------
-- Defines the shape of characters that can be drawn onto the screen.
-------------------------------------------------------------------------------
-- @classmod yae.Font

FreeTypeFontGenerator = yae.java.require "com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator"
GlyphLayout = yae.java.require "com.badlogic.gdx.graphics.g2d.GlyphLayout"
Constants = require "sauce.wrappers"

class
  new: (filename, size=16, filetype) =>
    file = nil

    if filename == nil
      file = yae.File("sauce3/font.ttf", filetype)
    else
      file = yae.File(filename, filetype)

    generator = yae.java.new FreeTypeFontGenerator, file.file
    @font = generator\generateFont size
    @fontTexture = @font\getRegion(0)\getTexture!
    @fontTexture\setFilter Constants.filters["linear"], Constants.filters["linear"]
    @glyphLayout = yae.java.new GlyphLayout

  get_ascent: =>
    @font\getAscent!

  get_descent: =>
    @font\getDescent!

  get_line_height: =>
    @font\getLineHeight!

  get_bounds: (text) =>
    @glyphLayout\setText text
    @glyphLayout.width, @glyphLayout.height

  get_filter: =>
    min_filter = @fontTexture\getMinFilter!
    mag_filter = @fontTexture\getMagFilter!
    Constants.filtercodes[min_filter], Constants.filtercodes[mag_filter]

  get_height: (text) =>
    _, h = @getBounds text
    h

  get_width: (text) =>
    w, _ = @getBounds text
    w

  has_glyphs: (...) =>
    args = table.pack ...
    found = true

    for i = 1, args.n
      found = found and @font\containsCharacter args[i]

    found

  set_filter: (min, mag) =>
    @fontTexture\setFilter Constants.filters[min], Constants.filters[mag]

  set_line_height: (height) =>
    @font\getData()\setLineHeight height

  free: =>
    @font\dispose!