refactor: simplify implementation

This commit is contained in:
crimx 2020-10-03 11:10:42 +08:00
parent 90d06a9b0b
commit 4a04a77f64
2 changed files with 203 additions and 134 deletions

View file

@ -9,8 +9,6 @@ A Hexo plugin that adds emoji support, using [Github Emojis API][ghemojis].
Check out the [Emoji Cheat Sheet](http://www.webpagefx.com/tools/emoji-cheat-sheet/) for all the emojis it supports.
V2 is not compatible with [V1](https://github.com/crimx/hexo-filter-github-emojis/tree/e52ceb8b18a7b06916b6cb0a887b218d49a7ab92). V1 replaces codepoints with `<img>` tags. While V2 makes the font transparent and displays emojis with `background-image`.
## Installation
``` bash
@ -30,43 +28,71 @@ githubEmojis:
customEmojis:
```
- **className** - Image class name. For example :sparkles: `:sparkles:` the filter will generate something like this:
- **enable** `boolean=true` - Enable `::` emoji parsing. If off the [tag](#tag) and [helper](#helper) still work.
- **className** `string="github-emoji"` - Emoji class name.
For example :sparkles: `:sparkles:` the filter will generate something like this:
```html
<span class="github-emoji" style="background-image:url(https://assets-cdn.github.com/images/icons/emoji/unicode/2728.png?v8)" data-src="https://assets-cdn.github.com/images/icons/emoji/unicode/2728.png?v8">&#x2728;</span>
<span class="github-emoji"><span>&#x2728;</span><img src="https://assets-cdn.github.com/images/icons/emoji/unicode/2728.png?v8"></span>
```
- **inject** - If true, the filter will inject proper inline styles and a script to fallback when image loading fails. If you can modify script files and style files, you may turn this off and add them yourself.
```html
<span class="github-emoji" style="color:transparent;background:no-repeat url(...) center/contain" ...>
```
- **inject** `boolean=true` - Inject emoji styles and fallback script.
If `true`, the filter will inject a `<style>` to document `<head>`.
If `false`, the filter will not inject any style. If you can modify source style files you may turn this off and add them yourself.
A script tag will be appended, the className changes according to the options:
Below is the injected `<style>`. The class name changes according to option.
```html
<script>
document.querySelectorAll('.github-emojis')
.forEach(el => {
if (!el.dataset.src) { return; }
const img = document.createElement('img');
img.style = 'display:none !important;';
img.src = el.dataset.src;
img.addEventListener('error', () => {
img.remove();
el.style.color = 'inherit';
el.style.backgroundImage = 'none';
el.style.background = 'none';
});
img.addEventListener('load', () => {
img.remove();
});
document.body.appendChild(img);
});
</script>
<style>
.github-emoji {
position: relative;
display: inline-block;
width: 1.2em;
min-height: 1.2em;
overflow: hidden;
vertical-align: top;
color: transparent;
}
.github-emoji > span {
position: relative;
z-index: 10;
}
.github-emoji img,
.github-emoji .fancybox {
margin: 0 !important;
padding: 0 !important;
border: none !important;
outline: none !important;
text-decoration: none !important;
user-select: none !important;
cursor: auto !important;
}
.github-emoji img {
height: 1.2em !important;
width: 1.2em !important;
position: absolute !important;
left: 50% !important;
top: 50% !important;
transform: translate(-50%, -50%) !important;
user-select: none !important;
cursor: auto !important;
}
.github-emoji-fallback {
color: inherit;
}
.github-emoji-fallback img {
opacity: 0 !important;
}
</style>
```
- **styles** - inline styles. For example:
- **styles** `object={}` - inline styles. For example:
```yaml
githubEmojis:
@ -78,10 +104,10 @@ githubEmojis:
outputs:
```html
<span class="github-emoji" style="font-size:2em;font-weight:bold;background-image:url(...)" ...>
<span class="github-emoji" style="font-size:2em;font-weight:bold" ...>
```
- **customEmojis** - You can specify your own list. An object or JSON string is valid. The filter will first check the `customEmojis` then fallback to the [Github Emojis][ghemojis] list.
- **customEmojis** `object={}` - You can specify your own list. An object or JSON string is valid. The filter will first check the `customEmojis` then fallback to the [Github Emojis][ghemojis] list.
For example:
@ -129,7 +155,7 @@ no-emoji: true
You can also render a GitHub emoji from a template using the `github_emoji` helper:
```html
<h1><% github_emoji('octocat') %></h1>
<h1><%- github_emoji('octocat') %></h1>
```
[ghemojis]: https://api.github.com/emojis

245
index.js
View file

@ -1,144 +1,187 @@
'use strict'
"use strict";
/* global hexo */
const _ = require('lodash')
const path = require('path')
const fs = require('fs')
const { JSDOM } = require('jsdom')
const _ = require("lodash");
const { JSDOM } = require("jsdom");
var options = _.assign({
enable: true,
inject: true,
version: 'latest',
className: 'github-emoji',
}, hexo.config.githubEmojis)
const options = Object.assign(
{
enable: true,
inject: true,
version: "latest",
className: "github-emoji",
},
hexo.theme.githubEmojis || {},
hexo.config.githubEmojis || {}
);
if (options.enable !== false) {
const emojis = _.assign(
{},
require('./emojis.json'),
loadCustomEmojis(options.customEmojis || options.localEmojis),
)
const emojis = Object.assign(
{},
require("./emojis.json"),
loadCustomEmojis(options.customEmojis || options.localEmojis)
);
fs.writeFile(
path.join(__dirname, 'emojis.json'),
JSON.stringify(emojis, null, ' '),
function (err) { err && console.warn(err) },
)
hexo.extend.helper.register("github_emoji", (name) =>
renderEmoji(emojis, name)
);
hexo.extend.filter.register('after_post_render', data => {
if (!options.inject && data['no-emoji']) { return data }
hexo.extend.tag.register("github_emoji", (args) =>
renderEmoji(emojis, args[0])
);
const $content = new JSDOM(data.content)
const $excerpt = new JSDOM(data.excerpt)
hexo.extend.filter.register("after_post_render", (data) => {
if (data["no-emoji"]) {
if (options.inject !== false) {
data.content = `<style>${getEmojiStyles()}</style>` + data.content;
}
} else {
const $content = new JSDOM(data.content);
const $excerpt = new JSDOM(data.excerpt);
if (options.inject) {
const $script = $content.window.document.createElement('script')
$script.innerHTML = `
document.querySelectorAll('.${options.className}')
.forEach(el => {
if (!el.dataset.src) { return; }
const img = document.createElement('img');
img.style = 'display:none !important;';
img.src = el.dataset.src;
img.addEventListener('error', () => {
img.remove();
el.style.color = 'inherit';
el.style.backgroundImage = 'none';
el.style.background = 'none';
});
img.addEventListener('load', () => {
img.remove();
});
document.body.appendChild(img);
});
`
$content.window.document.body.appendChild($script)
replaceColons($content.window.document.body, emojis);
replaceColons($excerpt.window.document.body, emojis);
if (options.inject !== false) {
const style = $content.window.document.createElement("style");
style.innerHTML = getEmojiStyles();
$content.window.document.body.insertBefore(
style,
$content.window.document.body.firstElementChild
);
}
if (!data['no-emoji']) {
replaceColons($content.window.document.body, emojis)
replaceColons($excerpt.window.document.body, emojis)
}
data.content = $content.window.document.body.innerHTML;
data.excerpt = $excerpt.window.document.body.innerHTML;
}
data.content = $content.window.document.body.innerHTML
data.excerpt = $excerpt.window.document.body.innerHTML
return data
})
return data;
});
hexo.extend.helper.register('github_emoji', name => renderEmoji(emojis, name))
hexo.extend.tag.register('github_emoji', args => renderEmoji(emojis, args[0]))
}
function replaceColons (node, emojis) {
if (!node || !node.childNodes) { return }
function replaceColons(node, emojis) {
if (!node || !node.childNodes) {
return;
}
for (let i = node.childNodes.length - 1; i >= 0; i--) {
const child = node.childNodes[i]
if (child.tagName === 'PRE' || child.tagName === 'CODE') { return }
const child = node.childNodes[i];
if (child.tagName === "PRE" || child.tagName === "CODE") {
return;
}
if (child.nodeType === 3) {
const content = child.data.replace(
/:(\w+):/ig,
(match, p1) => emojis[p1] ? renderEmoji(emojis, p1) : match,
)
const content = child.data.replace(/:(\w+):/gi, (match, p1) =>
emojis[p1] ? renderEmoji(emojis, p1) : match
);
if (content !== child.data) {
child.replaceWith(JSDOM.fragment(content))
child.replaceWith(JSDOM.fragment(content));
}
} else {
replaceColons(child, emojis)
replaceColons(child, emojis);
}
}
}
function loadCustomEmojis (customEmojis) {
function loadCustomEmojis(customEmojis) {
// JSON string
if (_.isString(customEmojis)) {
try {
customEmojis = JSON.parse(customEmojis)
Object.keys(customEmojis).forEach(name => {
customEmojis = JSON.parse(customEmojis);
Object.keys(customEmojis).forEach((name) => {
if (_.isString(customEmojis[name])) {
customEmojis[name] = {
src: customEmojis[name],
}
};
}
})
});
} catch (err) {
customEmojis = {}
console.warn('hexo-filter-github-emojis: Custom emojis not valid. Skipped.')
customEmojis = {};
console.warn(
"hexo-filter-github-emojis: Custom emojis not valid. Skipped."
);
}
}
if (!_.isObject(customEmojis)) {
customEmojis = {}
customEmojis = {};
}
Object.values(customEmojis).forEach(emoji => {
if (emoji.codepoints && !_.isArray(emoji.codepoints)) {
emoji.codepoints = emoji.codepoints.split(' ')
}
})
return Object.keys(customEmojis).reduce((emojis, name) => {
const emoji = customEmojis[name];
emojis[name] = Object.assign(
{},
emoji,
emoji.codepoints && !_.isArray(emoji.codepoints)
? { codepoints: emoji.codepoints.split(" ") }
: {}
);
return emojis;
}, {});
}
function renderEmoji (emojis, name) {
if (!emojis[name]) { return name }
function renderEmoji(emojis, name) {
if (!emojis[name]) return name;
const styles = _.isObject(options.styles)
? Object.keys(options.styles)
.filter(k => _.isString(options.styles[k]))
.map(k => k + ':' + options.styles[k])
: []
if (options.inject) {
styles.push(
'color: transparent',
`background:no-repeat url(${emojis[name].src}) center/contain`,
)
} else {
styles.push(`background-image:url(${emojis[name].src})`)
}
? ` style="${Object.keys(options.styles)
.map((k) => k + ":" + options.styles[k])
.join(";")}"`
: "";
const codepoints = emojis[name].codepoints
? emojis[name].codepoints.map(c => `&#x${c};`).join('')
: ' '
? emojis[name].codepoints.map((c) => `&#x${c};`).join("")
: " ";
return `<span class="${options.className}" style="${styles.join(';')}" data-src="${emojis[name].src}">${codepoints}</span>`
return (
`<span class="${options.className}" aria-hidden="true"${styles}>` +
`<span>${codepoints}</span>` +
`<img src="${emojis[name].src}" onerror="this.parent.classList.add('${options.className}-fallback')">` +
`</span>`
);
}
function getEmojiStyles() {
const rules = `.${options.className} {
position: relative;
display: inline-block;
width: 1.2em;
min-height: 1.2em;
overflow: hidden;
vertical-align: top;
color: transparent;
}
.${options.className} > span {
position: relative;
z-index: 10;
}
.${options.className} img,
.${options.className} .fancybox {
margin: 0 !important;
padding: 0 !important;
border: none !important;
outline: none !important;
text-decoration: none !important;
user-select: none !important;
cursor: auto !important;
}
.${options.className} img {
height: 1.2em !important;
width: 1.2em !important;
position: absolute !important;
left: 50% !important;
top: 50% !important;
transform: translate(-50%, -50%) !important;
user-select: none !important;
cursor: auto !important;
}
.${options.className}-fallback {
color: inherit;
}
.${options.className}-fallback img {
opacity: 0 !important;
}`;
return rules.replace(/^ +/gm, ' ').replace(/\n/g, '')
}