diff --git a/lib/bot.js b/lib/bot.js index c943d674..ba3f668f 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -4,6 +4,7 @@ import logger from 'winston'; import discord from 'discord.js'; import { ConfigurationError } from './errors'; import { validateChannelMapping } from './validators'; +import { formatFromDiscordToIRC, formatFromIRCToDiscord } from './formatting'; const REQUIRED_FIELDS = ['server', 'nickname', 'channelMapping', 'discordToken']; const NICK_COLORS = ['light_blue', 'dark_blue', 'light_red', 'dark_red', 'light_green', @@ -173,6 +174,9 @@ class Bot { this.ircClient.say(ircChannel, text); } else { if (text !== '') { + // Convert formatting + text = formatFromDiscordToIRC(text); + text = `<${displayUsername}> ${text}`; logger.debug('Sending message to IRC', ircChannel, text); this.ircClient.say(ircChannel, text); @@ -203,7 +207,10 @@ class Bot { return; } - const withMentions = text.replace(/@[^\s]+\b/g, (match) => { + // Convert text formatting (bold, italics, underscore) + const withFormat = formatFromIRCToDiscord(text); + + const withMentions = withFormat.replace(/@[^\s]+\b/g, (match) => { const search = match.substring(1); const guild = discordChannel.guild; const nickUser = guild.members.find('nickname', search); diff --git a/lib/formatting.js b/lib/formatting.js new file mode 100644 index 00000000..78e6022c --- /dev/null +++ b/lib/formatting.js @@ -0,0 +1,47 @@ +import ircFormatting from 'irc-formatting'; +import SimpleMarkdown from 'simple-markdown'; +import colors from 'irc-colors'; + +function mdNodeToIRC(node) { + let content = node.content; + if (Array.isArray(content)) content = content.map(mdNodeToIRC).join(''); + if (node.type === 'em') return colors.italic(content); + if (node.type === 'strong') return colors.bold(content); + if (node.type === 'u') return colors.underline(content); + return content; +} + +export function formatFromDiscordToIRC(text) { + const markdownAST = SimpleMarkdown.defaultInlineParse(text); + return markdownAST.map(mdNodeToIRC).join(''); +} + +export function formatFromIRCToDiscord(text) { + const blocks = ircFormatting.parse(text).map(block => ({ + // Consider reverse as italic, some IRC clients use that + ...block, + italic: block.italic || block.reverse + })); + let mdText = ''; + + for (let i = 0; i <= blocks.length; i += 1) { + // Default to unstyled blocks when index out of range + const block = blocks[i] || {}; + const prevBlock = blocks[i - 1] || {}; + + // Add start markers when style turns from false to true + if (!prevBlock.italic && block.italic) mdText += '*'; + if (!prevBlock.bold && block.bold) mdText += '**'; + if (!prevBlock.underline && block.underline) mdText += '__'; + + // Add end markers when style turns from true to false + // (and apply in reverse order to maintain nesting) + if (prevBlock.underline && !block.underline) mdText += '__'; + if (prevBlock.bold && !block.bold) mdText += '**'; + if (prevBlock.italic && !block.italic) mdText += '*'; + + mdText += block.text || ''; + } + + return mdText; +} diff --git a/package.json b/package.json index 777ce810..ddc0993c 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,10 @@ "commander": "2.9.0", "discord.js": "11.0.0", "irc": "0.5.2", + "irc-colors": "^1.3.2", + "irc-formatting": "^1.0.0-rc3", "lodash": "^4.17.4", + "simple-markdown": "^0.2.1", "strip-json-comments": "2.0.1", "winston": "2.3.1" }, diff --git a/test/formatting.test.js b/test/formatting.test.js new file mode 100644 index 00000000..d14f518f --- /dev/null +++ b/test/formatting.test.js @@ -0,0 +1,64 @@ +/* eslint-disable prefer-arrow-callback */ + +import chai from 'chai'; +import { formatFromDiscordToIRC, formatFromIRCToDiscord } from '../lib/formatting'; + +chai.should(); + +describe('Formatting', () => { + describe('Discord to IRC', () => { + it('should convert bold markdown', () => { + formatFromDiscordToIRC('**text**').should.equal('\x02text\x02'); + }); + + it('should convert italic markdown', () => { + formatFromDiscordToIRC('*text*').should.equal('\x16text\x16'); + formatFromDiscordToIRC('_text_').should.equal('\x16text\x16'); + }); + + it('should convert underline markdown', () => { + formatFromDiscordToIRC('__text__').should.equal('\x1ftext\x1f'); + }); + + it('should ignore strikethrough markdown', () => { + formatFromDiscordToIRC('~~text~~').should.equal('text'); + }); + + it('should convert nested markdown', () => { + formatFromDiscordToIRC('**bold *italics***') + .should.equal('\x02bold \x16italics\x16\x02'); + }); + }); + + describe('IRC to Discord', () => { + it('should convert bold IRC format', () => { + formatFromIRCToDiscord('\x02text\x02').should.equal('**text**'); + }); + + it('should convert reverse IRC format', () => { + formatFromIRCToDiscord('\x16text\x16').should.equal('*text*'); + }); + + it('should convert italic IRC format', () => { + formatFromIRCToDiscord('\x1dtext\x1d').should.equal('*text*'); + }); + + it('should convert underline IRC format', () => { + formatFromIRCToDiscord('\x1ftext\x1f').should.equal('__text__'); + }); + + it('should ignore color IRC format', () => { + formatFromIRCToDiscord('\x0306,08text\x03').should.equal('text'); + }); + + it('should convert nested IRC format', () => { + formatFromIRCToDiscord('\x02bold \x16italics\x16\x02') + .should.equal('**bold *italics***'); + }); + + it('should convert nested IRC format', () => { + formatFromIRCToDiscord('\x02bold \x1funderline\x1f\x02') + .should.equal('**bold __underline__**'); + }); + }); +});