nodebb-plugin-rainbows
Version:
Add colors and gradients to your posts, tags, and more. Smile!
378 lines (318 loc) • 12 kB
JavaScript
// nodebb-plugin-rainbows
// library.js
const NodeBB = module => require.main.require(module)
const async = NodeBB('async')
const meta = NodeBB('./src/meta')
const user = NodeBB('./src/user')
const topics = NodeBB('./src/topics')
const plugins = NodeBB('./src/plugins')
const Settings = NodeBB('./src/settings')
const SocketAdmin = NodeBB('./src/socket.io/admin')
const SocketPlugins = NodeBB('./src/socket.io/plugins')
const RainbowVis = require('./lib/rainbowvis.js')
// Themes in use
let themes = { }
// Default settings
const defaultSettings = {
themes: [
{ name: 'flutter', value: 'magenta,yellow' },
{ name: 'dashie', value: 'red,orange,yellow,green,blue,purple' },
{ name: 'sunbutt', value: 'lightblue,lightpink,lightgreen' }
],
postsEnabled: 1,
tagsEnabled: 1,
topicsEnabled: 1,
postsModsOnly: 0,
topicsModsOnly: 0,
hueModifier: 0,
lumModifier: 40
}
// Settings object
let settings
// Capture rainbow format
let regex = /-=[^\0]+?=-|~\[[^\0]+?\]~(?:\([\w\d:,# ]*?\))?/gm
exports.onLoad = function (params, cb) {
let router = params.router
let middleware = params.middleware
settings = new Settings('rainbows', '1.2.0', defaultSettings, loadSettings)
function renderAdmin (req, res, next) {
res.render('admin/plugins/rainbows', {})
}
router.get('/admin/plugins/rainbows', middleware.admin.buildHeader, renderAdmin)
router.get('/api/admin/plugins/rainbows', renderAdmin)
SocketAdmin.settings.syncRainbows = function () {
settings.sync(loadSettings)
}
SocketPlugins.rainbows = {
colorPost: function (socket, data, next) {
plugins.fireHook('filter:parse.raw', data, next)
},
colorTopic: function (socket, data, next) {
if (data.uid === -1) next(null, remove(data.title))
if (data.uid === -2) next(null, parse(data.title))
parseTopicTitle(data.uid, data.cid, data.title, next)
},
colorTopics: function (socket, data, next) {
parseTopics(data.topics, next)
}
}
function loadSettings () {
themes = {}
settings.get('themes').forEach(function (theme) {
themes[theme.name] = theme.value
})
settings.set('themes', themes)
}
cb()
}
function hasPerms (uid, cid, cb) {
async.parallel({
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, uid),
isModerator: async.apply(user.isModerator, uid, cid)
}, function (err, results) {
cb(err, results ? (results.isAdminOrGlobalMod || results.isModerator) : false)
})
}
function readOption (options, option) {
option = option.split(':')
if (option.length > 1) {
switch (option[0]) {
case 'range':
case 'length':
options.range = Math.floor(parseInt(option[1]))
options.range = options.range > -1 && options.range < 9000 ? options.range : 0
break
case 'theme':
if (themes.hasOwnProperty(option[1]) && !!themes[option[1]]) {
themes[option[1]].replace(/ */g, '').split(',').forEach(function (val) {
if (val !== '') {
readOption(options, val)
}
})
}
break
case 'bg':
case 'bgcolor':
options.bgcolor = option[1]
break
default:
break
}
} else {
if (option[0] === 'mirror') {
options.mirror = true
} else {
options.colors.push(option[0])
}
}
}
exports.adminHeader = function (custom_header, cb) {
custom_header.plugins.push({
'route': '/plugins/rainbows',
'icon': '',
'name': '<div class="rb-nav"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAj5JREFUeNqkVEtrE1EYPZM7j7ya2KaJSW3AWgxiMUWCgojaaLNy4cKVIP0D1a1QUNSfYf+AuBBx3YURdGOTjRu1SogNmATa0OadzON6JtRFi7aKA2eymHse3+NGkVLifx5FZjL/ct4gNMJ1HaJQMNW/IHmIyT04UNFGAh0ch+l+PEpggjg/cp7GLq6giXMQiEBx0x8lME9cQhgD3CL5IvodTWjrCKlrGPeU4FWeHyJwnVjEWUZdwk5nXAxfItp7gZhehyagTwj4p/+YYIG4iwvo8+0peX3WI8yYX5RAH+G5LmI5HfqsQFv9rcAp4h6dBcmdojeEFczYDT1qI3lnADHfRRltveqo0VpLwdLYPgF3PCuseYqx+3QWJCsNX5Ky921UIwP/Z6uXLtZaqa/bmrdnebCU2Sdwg7jKhg1Ys8bYKp1VkgXKEZz40LIX8uX+se6weTIA43SCKQ+UsMxR+dltjQ0zWLPB2DqddZLF4loJcWmZ2Sm0J3U0Do4xTVzmnB2OSme3LYTmDNZsMLZBZ51k7WYC8Ats8ewmsfNry9wnRymDS+LlnH0cVQDxXBDfEU4X6xHGjmdjSJIc4tnXbyvF2tP3q6NLpGY3NvAmlbrG9dS4YZJLIjlntnTW0X84Dhtms+YAYwdJfPjk3Wo9v1kcuT7GM6j5VitSMc0zSlAKpwZ8dCwM/AZk55sMFZrYqpXkMCLletWs3H714NN2b9dd7+7oMvFuuI3wE2OEOGStHcIizD2i+2vzr0D+FGAARenLmDXrT3oAAAAASUVORK5CYII="><b style="color:#ff0000">R</b><b style="color:#ff6000">a</b><b style="color:#ffbf00">i</b><b style="color:#dfff00">n</b><b style="color:#7fff00">b</b><b style="color:#20ff00">o</b><b style="color:#00bf40">w</b><b style="color:#00609f">s</b></div>'
})
// Image is rainbow-icon.png from:
// http://p.yusukekamiyamane.com/
// CC-BY
cb(null, custom_header)
}
function parse (text) {
if (!text) return text
let matches = text.match(regex) || []
if (matches.length === 0) return text
matches.forEach(matchedString => {
const rainbow = new RainbowVis()
let matchedParts
let matchedContent
let matchedOptions
let unsafe = false
let characters
let options = {
range: 0,
colors: [],
bgcolor: '',
mirror: false
}
if (matchedString.match(/^-=/)) {
unsafe = true
matchedParts = matchedString.slice(2, matchedString.length - 2)
matchedParts = matchedParts.match(/^(?:\(([\w\d:,# ]+?)\))?(.*)$/m)
matchedContent = matchedParts[2]
matchedOptions = matchedParts[1]
} else {
matchedParts = matchedString.slice(1)
matchedParts = matchedParts.match(/^\[([^\0]+?)\]~(?:\(([\w\d:,# ]*?)\))?$/m)
matchedContent = matchedParts[1]
matchedOptions = matchedParts[2]
}
if (matchedOptions) {
matchedOptions = matchedOptions.replace(/ +/g, '').split(',')
matchedOptions.forEach(option => {
if (option !== '') {
readOption(options, option)
}
})
}
characters = matchedContent.replace(/<[^>]*>/gm, '').replace(/(\r\n|\n|\r| |\t)*/gm, '').length
if (!characters) return
if (options.colors.length > 0) {
if (options.colors.length === 1) options.colors[1] = options.colors[0]
if (options.range === 0) {
options.range = characters > 1 ? characters : 2
} else {
options.range = options.range > options.colors.length ? options.range : options.colors.length
}
} else {
options.colors = [ 'red', 'orange', 'gold', 'lime', 'deepskyblue', 'blue', 'blueviolet', 'magenta']
if (options.range === 0) {
options.range = characters > 8 ? characters : 8
} else {
options.range = options.range > 8 ? options.range : 8
}
}
try {
rainbow.setSpectrumByArray(options.colors)
} catch (e) {
return
}
rainbow.setNumberRange(0, options.range - 1)
let parsed = ''
let offset = 0
let trimmed = matchedContent
while (trimmed.length) {
let c = trimmed.charAt(0)
if (c.match(/(\n|\r)/)) {
parsed += '<br>'
trimmed = trimmed.substr(1)
} else if (c.match(/ |\t/)) {
parsed += ' '
trimmed = trimmed.substr(1)
} else if (trimmed.match(/^<\/p><p>/)) {
parsed += '<br /><br />'
trimmed = trimmed.replace(/^<\/p><p>/, '')
} else if (trimmed.match(/^<[^>]*>/)) {
parsed += trimmed.match(/^<[^>]*>/)[0]
trimmed = trimmed.replace(/^<[^>]*>/, '')
} else {
parsed += '<span style="color:#' + rainbow.colourAt(offset % options.range) + '">' + trimmed.charAt(0) + '</span>'
trimmed = trimmed.substr(1)
offset++
}
}
if (options.bgcolor) parsed = '<span style="background-color:' + options.bgcolor + '">' + parsed + '</span>'
parsed = '<span class="rainbowified">' + parsed + '</span>'
text = text.replace(matchedString, parsed)
})
return text
}
function remove (content) {
let arr
while ((arr = regex.exec(content)) !== null) {
content = content.replace(arr[0], arr[2])
}
return content
}
exports.parseRaw = function (content, cb) {
cb(null, parse(content))
}
exports.parseSignature = function (data, cb) {
if (settings.get('postsEnabled')) {
data.userData.signature = parse(data.userData.signature)
} else {
data.userData.signature = remove(data.userData.signature)
}
cb(null, data)
}
exports.parsePost = function (data, cb) {
topics.getTopicField(data.tid, 'cid', function (err, cid) {
parsePost(data.postData.uid, cid, data.postData.content, function (err, content) {
data.postData.content = content
cb(null, data)
})
})
}
function parsePost (uid, cid, content, cb) {
if (!settings.get('postsEnabled')) return cb(new Error('Not allowed to post colors.'), remove(content))
if (!settings.get('postsModsOnly')) return cb(null, parse(content))
hasPerms(uid, cid, function (err, hasPerms) {
cb(hasPerms ? null : new Error('Not allowed post colors.'), hasPerms ? parse(content) : remove(content))
})
}
exports.parseTopic = function (data, cb) {
parseTopicTitle(data.templateData.uid, data.templateData.cid, data.templateData.title, function (err, title) {
data.templateData.title = title
cb(null, data)
})
}
// Pass back an array of parsed topics only.
function parseTopics (topicsData, cb) {
if (!settings.get('topicsEnabled')) return cb()
async.map(topicsData, function (topic, next) {
topics.getTopicFields(topic.tid, ['cid', 'uid'], function (err, fields) {
parseTopicTitle(fields.uid, fields.cid, topic.title, function (err, title) {
topic.uid = fields.uid
topic.cid = fields.cid
topic.title = title
next(null, topic)
})
})
}, function (err, topicsData) {
cb(null, {topics: topicsData})
})
};
function parseTopicsAll (topicsData, cb) {
if (!settings.get('topicsEnabled')) return cb()
async.map(topicsData, function (topic, next) {
topics.getTopicFields(topic.tid, ['cid', 'uid'], function (err, fields) {
parseTopicTitle(fields.uid, fields.cid, topic.title, function (err, title) {
topic.title = title
next(null, topic)
})
})
}, cb)
};
function parseTopicTitle (uid, cid, title, cb) {
if (!settings.get('topicsEnabled')) return cb(new Error('Not allowed to use colors in topic titles.'), remove(title))
if (!settings.get('topicsModsOnly')) return cb(null, parse(title))
hasPerms(uid, cid, function (err, hasPerms) {
cb(hasPerms ? null : new Error('Not allowed to use colors in topic titles.'), hasPerms ? parse(title) : remove(title))
})
}
exports.configGet = function (data, next) {
data.rainbows = {}
data.rainbows.postsEnabled = parseInt(settings.get('postsEnabled'), 10) === 1
data.rainbows.postsModOnly = parseInt(settings.get('postsModOnly'), 10) === 1
data.rainbows.tagsEnabled = parseInt(settings.get('tagsEnabled'), 10) === 1
data.rainbows.topicsEnabled = parseInt(settings.get('topicsEnabled'), 10) === 1
data.rainbows.topicsModsOnly = parseInt(settings.get('topicsModsOnly'), 10) === 1
data.rainbows.hueModifier = parseInt(settings.get('hueModifier'), 10) || 0
data.rainbows.lumModifier = parseInt(settings.get('lumModifier'), 10) || 40
next(null, data)
}
exports.renderHeader = function (data, cb) {
data.templateValues.browserTitle = remove(data.templateValues.browserTitle)
cb(null, data)
}
// Doesn't work.
exports.getTopic = function (data, cb) {
// data.topic.title = remove(data.topic.title)
cb(null, data)
}
exports.parseController = function (data, cb) {
if (!data) return cb(null, data)
if (data.templateData && data.templateData.topics) {
async.each(data.templateData.topics, function (topic, next) {
parseTopicTitle(topic.uid, topic.cid, topic.title, function (err, title) {
topic.title = title
next()
})
}, function () {
cb(null, data)
})
} else if (typeof data === 'string') {
cb(null, remove(data))
} else {
cb(null, data)
}
}