mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-12-01 16:11:57 -05:00
381 lines
23 KiB
JavaScript
381 lines
23 KiB
JavaScript
|
(function (root, factory) {
|
|||
|
'use strict';
|
|||
|
|
|||
|
if (typeof define === 'function' && define.amd) {
|
|||
|
// AMD. Register as an anonymous module.
|
|||
|
define([], factory);
|
|||
|
} else if (typeof exports === 'object') {
|
|||
|
// Node. Does not work with strict CommonJS, but
|
|||
|
// only CommonJS-like environments that support module.exports,
|
|||
|
// like Node.
|
|||
|
module.exports = factory();
|
|||
|
} else {
|
|||
|
// Browser globals (root is window)
|
|||
|
root.emojify = factory();
|
|||
|
}
|
|||
|
}(this, function () {
|
|||
|
'use strict';
|
|||
|
|
|||
|
var emojify = (function () {
|
|||
|
/**
|
|||
|
* NB!
|
|||
|
* The namedEmojiString variable is updated automatically by the
|
|||
|
* "update" gulp task. Do not remove the comment as this will
|
|||
|
* cause the gulp task to stop working.
|
|||
|
*/
|
|||
|
var namedEmojiString =
|
|||
|
/*##EMOJILIST*/"+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,blue_car,blue_heart,blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,boy,bread,bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,busstop,bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,couplekiss,cow,cow2,credit_card,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,interrobang,iphone,it,izakaya_lan
|
|||
|
|
|||
|
var namedEmoji = namedEmojiString.split(/,/);
|
|||
|
|
|||
|
/* A hash with the named emoji as keys */
|
|||
|
var namedMatchHash = namedEmoji.reduce(function(memo, v) {
|
|||
|
memo[v] = true;
|
|||
|
return memo;
|
|||
|
}, {});
|
|||
|
|
|||
|
var emoticonsProcessed;
|
|||
|
var emojiMegaRe;
|
|||
|
|
|||
|
function initEmoticonsProcessed() {
|
|||
|
/* List of emoticons used in the regular expression */
|
|||
|
var emoticons = {
|
|||
|
/* :..: */ named: /:([a-z0-9A-Z_-]+):/,
|
|||
|
/* :-) */ smile: /:-?\)/g,
|
|||
|
/* :-o */ scream: /:-o/gi,
|
|||
|
/* :-] */ smirk: /[:;]-?]/g,
|
|||
|
/* :-D */ grinning: /[:;]-?d/gi,
|
|||
|
/* X-D */ stuck_out_tongue_closed_eyes: /x-d/gi,
|
|||
|
/* ;-p */ stuck_out_tongue_winking_eye: /[:;]-?p/gi,
|
|||
|
/* :-[ / :-@ */ rage: /:-?[\[@]/g,
|
|||
|
/* :-( */ frowning: /:-?\(/g,
|
|||
|
/* :'-( */ sob: /:['’]-?\(|:'\(/g,
|
|||
|
/* :-* */ kissing_heart: /:-?\*/g,
|
|||
|
/* ;-) */ wink: /;-?\)/g,
|
|||
|
/* :-/ */ pensive: /:-?\//g,
|
|||
|
/* :-s */ confounded: /:-?s/gi,
|
|||
|
/* :-| */ flushed: /:-?\|/g,
|
|||
|
/* :-$ */ relaxed: /:-?\$/g,
|
|||
|
/* :-x */ mask: /:-x/gi,
|
|||
|
/* <3 */ heart: /<3|<3/g,
|
|||
|
/* </3 */ broken_heart: /<\/3|</3/g,
|
|||
|
/* :+1: */ thumbsup: /:\+1:/g,
|
|||
|
/* :-1: */ thumbsdown: /:\-1:/g
|
|||
|
};
|
|||
|
|
|||
|
if (defaultConfig.ignore_emoticons) {
|
|||
|
emoticons = {
|
|||
|
/* :..: */ named: /:([a-z0-9A-Z_-]+):/,
|
|||
|
/* :+1: */ thumbsup: /:\+1:/g,
|
|||
|
/* :-1: */ thumbsdown: /:\-1:/g
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
return Object.keys(emoticons).map(function(key) {
|
|||
|
return [emoticons[key], key];
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
function initMegaRe() {
|
|||
|
/* The source for our mega-regex */
|
|||
|
var mega = emoticonsProcessed
|
|||
|
.map(function(v) {
|
|||
|
var re = v[0];
|
|||
|
var val = re.source || re;
|
|||
|
val = val.replace(/(^|[^\[])\^/g, '$1');
|
|||
|
return "(" + val + ")";
|
|||
|
})
|
|||
|
.join('|');
|
|||
|
|
|||
|
/* The regex used to find emoji */
|
|||
|
return new RegExp(mega, "gi");
|
|||
|
}
|
|||
|
|
|||
|
var defaultConfig = {
|
|||
|
blacklist: {
|
|||
|
'ids': [],
|
|||
|
'classes': ['no-emojify'],
|
|||
|
'elements': ['script', 'textarea', 'a', 'pre', 'code']
|
|||
|
},
|
|||
|
tag_type: null,
|
|||
|
only_crawl_id: null,
|
|||
|
img_dir: 'images/emoji',
|
|||
|
ignore_emoticons: false,
|
|||
|
mode: 'img'
|
|||
|
};
|
|||
|
|
|||
|
/* Returns true if the given char is whitespace */
|
|||
|
function isWhitespace(s) {
|
|||
|
return s === ' ' || s === '\t' || s === '\r' || s === '\n' || s === '' || s === String.fromCharCode(160);
|
|||
|
}
|
|||
|
|
|||
|
var modeToElementTagType = {
|
|||
|
'img': 'img',
|
|||
|
'sprite': 'span',
|
|||
|
'data-uri': 'span'
|
|||
|
};
|
|||
|
|
|||
|
/* Given a match in a node, replace the text with an image */
|
|||
|
function insertEmojicon(args) {
|
|||
|
var emojiElement = null;
|
|||
|
|
|||
|
|
|||
|
if(args.replacer){
|
|||
|
emojiElement = args.replacer.apply({
|
|||
|
config: defaultConfig
|
|||
|
},
|
|||
|
[':' + args.emojiName + ':', args.emojiName]
|
|||
|
);
|
|||
|
}
|
|||
|
else {
|
|||
|
var elementType = defaultConfig.tag_type || modeToElementTagType[defaultConfig.mode];
|
|||
|
emojiElement = args.win.document.createElement(elementType);
|
|||
|
|
|||
|
if (elementType !== 'img') {
|
|||
|
emojiElement.setAttribute('class', 'emoji emoji-' + args.emojiName);
|
|||
|
} else {
|
|||
|
emojiElement.setAttribute('align', 'absmiddle');
|
|||
|
emojiElement.setAttribute('alt', ':' + args.emojiName + ':');
|
|||
|
emojiElement.setAttribute('class', 'emoji');
|
|||
|
emojiElement.setAttribute('src', defaultConfig.img_dir + '/' + args.emojiName + '.png');
|
|||
|
}
|
|||
|
|
|||
|
emojiElement.setAttribute('title', ':' + args.emojiName + ':');
|
|||
|
}
|
|||
|
|
|||
|
args.node.splitText(args.match.index);
|
|||
|
args.node.nextSibling.nodeValue = args.node.nextSibling.nodeValue.substr(
|
|||
|
args.match[0].length,
|
|||
|
args.node.nextSibling.nodeValue.length
|
|||
|
);
|
|||
|
emojiElement.appendChild(args.node.splitText(args.match.index));
|
|||
|
args.node.parentNode.insertBefore(emojiElement, args.node.nextSibling);
|
|||
|
}
|
|||
|
|
|||
|
/* Given an regex match, return the name of the matching emoji */
|
|||
|
function getEmojiNameForMatch(match) {
|
|||
|
/* Special case for named emoji */
|
|||
|
if(match[1] && match[2]) {
|
|||
|
var named = match[2];
|
|||
|
if(namedMatchHash[named]) { return named; }
|
|||
|
return;
|
|||
|
}
|
|||
|
for(var i = 3; i < match.length - 1; i++) {
|
|||
|
if(match[i]) {
|
|||
|
return emoticonsProcessed[i - 2][1];
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function defaultReplacer(emoji, name) {
|
|||
|
/*jshint validthis: true */
|
|||
|
var elementType = this.config.tag_type || modeToElementTagType[this.config.mode];
|
|||
|
if (elementType !== 'img') {
|
|||
|
return "<" + elementType +" class='emoji emoji-" + name + "' title=':" + name + ":'></" + elementType+ ">";
|
|||
|
} else {
|
|||
|
return "<img align='absmiddle' alt=':" + name + ":' class='emoji' src='" + this.config.img_dir + '/' + name + ".png' title=':" + name + ":' />";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function Validator() {
|
|||
|
this.lastEmojiTerminatedAt = -1;
|
|||
|
}
|
|||
|
|
|||
|
Validator.prototype = {
|
|||
|
validate: function(match, index, input) {
|
|||
|
var self = this;
|
|||
|
|
|||
|
/* Validator */
|
|||
|
var emojiName = getEmojiNameForMatch(match);
|
|||
|
if(!emojiName) { return; }
|
|||
|
|
|||
|
var m = match[0];
|
|||
|
var length = m.length;
|
|||
|
// var index = match.index;
|
|||
|
// var input = match.input;
|
|||
|
|
|||
|
function success() {
|
|||
|
self.lastEmojiTerminatedAt = length + index;
|
|||
|
return emojiName;
|
|||
|
}
|
|||
|
|
|||
|
/* At the beginning? */
|
|||
|
if(index === 0) { return success(); }
|
|||
|
|
|||
|
/* At the end? */
|
|||
|
if(input.length === m.length + index) { return success(); }
|
|||
|
|
|||
|
var hasEmojiBefore = this.lastEmojiTerminatedAt === index;
|
|||
|
if (hasEmojiBefore) { return success();}
|
|||
|
|
|||
|
/* Has a whitespace before? */
|
|||
|
if(isWhitespace(input.charAt(index - 1))) { return success(); }
|
|||
|
|
|||
|
var hasWhitespaceAfter = isWhitespace(input.charAt(m.length + index));
|
|||
|
/* Has a whitespace after? */
|
|||
|
if(hasWhitespaceAfter && hasEmojiBefore) { return success(); }
|
|||
|
|
|||
|
return;
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
function emojifyString (htmlString, replacer) {
|
|||
|
if(!htmlString) { return htmlString; }
|
|||
|
if(!replacer) { replacer = defaultReplacer; }
|
|||
|
|
|||
|
emoticonsProcessed = initEmoticonsProcessed();
|
|||
|
emojiMegaRe = initMegaRe();
|
|||
|
|
|||
|
var validator = new Validator();
|
|||
|
|
|||
|
return htmlString.replace(emojiMegaRe, function() {
|
|||
|
var matches = Array.prototype.slice.call(arguments, 0, -2);
|
|||
|
var index = arguments[arguments.length - 2];
|
|||
|
var input = arguments[arguments.length - 1];
|
|||
|
var emojiName = validator.validate(matches, index, input);
|
|||
|
if(emojiName) {
|
|||
|
return replacer.apply({
|
|||
|
config: defaultConfig
|
|||
|
},
|
|||
|
[arguments[0], emojiName]
|
|||
|
);
|
|||
|
}
|
|||
|
/* Did not validate, return the original value */
|
|||
|
return arguments[0];
|
|||
|
});
|
|||
|
|
|||
|
}
|
|||
|
function run(el, replacer) {
|
|||
|
|
|||
|
// Check if an element was not passed.
|
|||
|
// This will only work in the browser
|
|||
|
if(typeof el === 'undefined'){
|
|||
|
// Check if an element was configured. If not, default to the body.
|
|||
|
if (defaultConfig.only_crawl_id) {
|
|||
|
el = document.getElementById(defaultConfig.only_crawl_id);
|
|||
|
} else {
|
|||
|
el = document.body;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// Get the window object from the passed element.
|
|||
|
var doc = el.ownerDocument,
|
|||
|
win = doc.defaultView || doc.parentWindow;
|
|||
|
|
|||
|
var treeTraverse = function (parent, cb){
|
|||
|
var child;
|
|||
|
|
|||
|
if (parent.hasChildNodes()) {
|
|||
|
child = parent.firstChild;
|
|||
|
while(child){
|
|||
|
if(cb(child)) {
|
|||
|
treeTraverse(child, cb);
|
|||
|
}
|
|||
|
child = child.nextSibling;
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
var matchAndInsertEmoji = function(node) {
|
|||
|
var match;
|
|||
|
var matches = [];
|
|||
|
var validator = new Validator();
|
|||
|
|
|||
|
while ((match = emojiMegaRe.exec(node.data)) !== null) {
|
|||
|
if(validator.validate(match, match.index, match.input)) {
|
|||
|
matches.push(match);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
for (var i = matches.length; i-- > 0;) {
|
|||
|
/* Replace the text with the emoji */
|
|||
|
var emojiName = getEmojiNameForMatch(matches[i]);
|
|||
|
insertEmojicon({
|
|||
|
node: node,
|
|||
|
match: matches[i],
|
|||
|
emojiName: emojiName,
|
|||
|
replacer: replacer,
|
|||
|
win: win
|
|||
|
});
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
emoticonsProcessed = initEmoticonsProcessed();
|
|||
|
emojiMegaRe = initMegaRe();
|
|||
|
|
|||
|
var nodes = [];
|
|||
|
|
|||
|
var elementsBlacklist = new RegExp(defaultConfig.blacklist.elements.join('|'), 'i'),
|
|||
|
classesBlacklist = new RegExp(defaultConfig.blacklist.classes.join('|'), 'i');
|
|||
|
|
|||
|
if(typeof win.document.createTreeWalker !== 'undefined') {
|
|||
|
var nodeIterator = win.document.createTreeWalker(
|
|||
|
el,
|
|||
|
win.NodeFilter.SHOW_TEXT | win.NodeFilter.SHOW_ELEMENT,
|
|||
|
function(node) {
|
|||
|
if(node.nodeType !== 1) {
|
|||
|
/* Text Node? Good! */
|
|||
|
return win.NodeFilter.FILTER_ACCEPT;
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
if(node.tagName.match(elementsBlacklist) || node.className.match(classesBlacklist)) {
|
|||
|
return win.NodeFilter.FILTER_REJECT;
|
|||
|
}
|
|||
|
} catch(err) {
|
|||
|
return win.NodeFilter.FILTER_SKIP;
|
|||
|
}
|
|||
|
return win.NodeFilter.FILTER_SKIP;
|
|||
|
},
|
|||
|
false
|
|||
|
);
|
|||
|
|
|||
|
var node;
|
|||
|
|
|||
|
while((node = nodeIterator.nextNode()) !== null) {
|
|||
|
nodes.push(node);
|
|||
|
}
|
|||
|
}
|
|||
|
else {
|
|||
|
treeTraverse(el, function(node){
|
|||
|
if(
|
|||
|
(typeof node.tagName !== 'undefined' && node.tagName.match(elementsBlacklist)) ||
|
|||
|
(typeof node.className !== 'undefined' && node.className.match(classesBlacklist))
|
|||
|
){
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (node.nodeType === 1) {
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
nodes.push(node);
|
|||
|
return true;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
nodes.forEach(matchAndInsertEmoji);
|
|||
|
}
|
|||
|
|
|||
|
return {
|
|||
|
// Sane defaults
|
|||
|
defaultConfig: defaultConfig,
|
|||
|
emojiNames: namedEmoji,
|
|||
|
setConfig: function (newConfig) {
|
|||
|
Object.keys(defaultConfig).forEach(function(f) {
|
|||
|
if(f in newConfig) {
|
|||
|
defaultConfig[f] = newConfig[f];
|
|||
|
}
|
|||
|
});
|
|||
|
},
|
|||
|
|
|||
|
replace: emojifyString,
|
|||
|
|
|||
|
// Main method
|
|||
|
run: run
|
|||
|
};
|
|||
|
})();
|
|||
|
|
|||
|
return emojify;
|
|||
|
}
|
|||
|
));
|