helpers/html/perWordClasses.js
'use strict';
const striptags = require('striptags'),
speakingurl = require('speakingurl'),
he = require('he'),
_map = require('lodash/map'),
_last = require('lodash/last'),
_isEmpty = require('lodash/isEmpty'),
_isString = require('lodash/isString');
/**
* Removes all unicode from string
* mostly used for stripping various curly quotes
* @param {string} str
* @returns {string}
*/
function stripUnicode(str) {
return str.replace(/[^A-Za-z 0-9\.,\?!@#\$%\^&\*\(\)-_=\+;:<>\/\\\|\}\{\[\]~]*/g, '');
}
/**
* slugify text
* @param {string} word
* @return {string}
*/
function toSlug(word) {
// strip unicode (mostly curly quotes)
// strip html tags (like <b>)
// decode html entities (like &)
// generate slugified version
return speakingurl(he.decode(striptags(stripUnicode(word))), {custom: {_: '-'}});
}
/**
* generate the class for the word
* an underscore prefix is added to all words for consistency
* (css classes cannot begin with a number or other non-alpha character)
* @param {string} word
* @returns {string}
*/
function generateWordClass(word) {
return `_${toSlug(word)}`;
}
/**
* wraps each letter in a span
* @param {string} letter
* @param {integer} index (array index)
* @returns {string}
*/
function addCharSpan(letter, index) {
return `<span class="_char${index}">${letter}</span>`;
}
/**
* generate mark-up where each letter of a word is wrapped in a span
* @param {string} word
* @returns {string}
*/
function generateLetterClasses(word) {
const letters = word.split('');
return _map(letters, addCharSpan).join('');
}
/**
* wrap a word in a class
* @param {string} word
* @param {number} index
* @param {array} array
* @param {boolean} hasLetterClasses
* @returns {string}
*/
function wrapWord(word, index, array, hasLetterClasses) {
const isLastWord = _last(array) === word,
suffixSpace = isLastWord ? '' : ' ',
finalWord = hasLetterClasses ? generateLetterClasses(word) : word;
// note: we add the space after each word (except the last word) INSIDE the span
// because otherwise browsers will display them weirdly if they're styled differently
// than the words
return `<span class="${generateWordClass(word)}">${finalWord}${suffixSpace}</span>`;
}
/**
* wraps each word in spans with classes allowing designers and devs to target individual words with css
* @param {string} html to add classes to
* @param {object} options
* @param {boolean} [options.hash.perLetter] if you want an extra span wrapping each letter. defaults to true
* @returns {string} words wrapped in classes
*/
module.exports = function (html, options) {
let words, hasLetterClasses;
if (_isEmpty(html) || !_isString(html)) {
return ''; // fail gracefully
}
if (options.hash.perLetter !== false) {
// if it's not EXPLICITLY false, add letter classes
hasLetterClasses = true;
} else {
hasLetterClasses = false;
}
// replace nonbreaking spaces before splitting the text
words = html.split(' ');
return _map(words, (word, index, array) => wrapWord(word, index, array, hasLetterClasses)).join('');
};
// for testing
module.exports.toSlug = toSlug;
module.exports.example = {
code: '{{{ perWordClasses "One two three" perLetter=false }}}',
result: '<span class="_one">One</span> <span class="_two">two</span> <span class="_three">three</span>'
};