Skip to content

Commit

Permalink
Refactor code-style
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Sep 6, 2023
1 parent ab496fd commit a21dda5
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 83 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules/
*.d.ts
*.log
yarn.lock
!/index.d.ts
34 changes: 34 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type {Options} from './lib/index.js'

export {default} from './lib/index.js'

// Add custom data supported when `retext-emoji` is added.
declare module 'nlcst-emoticon-modifier' {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface EmoticonData {
/**
* Emoji of emoticon node (example: `'😀'`).
*
* Registered by `retext-emoji`.
*/
emoji?: string
/**
* Short description of emoticon node (example: `'grinning face'`).
*
* Registered by `retext-emoji`.
*/
description?: string
/**
* Name of emoji (example: `['grinning']`).
*
* Registered by `retext-emoji`.
*/
names?: string[]
/**
* Tags of emoji (example: `['smile', 'happy']`).
*
* Registered by `retext-emoji`.
*/
tags?: string[]
}
}
5 changes: 1 addition & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
/**
* @typedef {import('./lib/index.js').Options} Options
*/

// Note: types exposed from `index.d.ts`.
export {default} from './lib/index.js'
160 changes: 87 additions & 73 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,61 @@
/**
* @typedef {import('gemoji').Gemoji} Gemoji
* @typedef {import('nlcst').Root} Root
*
* @typedef {import('nlcst-emoticon-modifier')} DoNotTouchAsThisImportIncludesEmoticonsInTree
* @typedef {import('unified').Processor} Processor
*/

/**
* @callback Convert
* Convert.
* @param {string} value
* Input.
* @returns {string}
* Output.
*
* @typedef Options
* Configuration (optional).
* @property {'encode'|'decode'} [convert]
* If, and *how* to convert (`'encode'` or `'decode'`, optional).
*
* When `encode` is given, converts short-codes and emoticons to their unicode
* equivalent (`:heart:` and `<3` to `❤️`).
*
* When `decode` is given, converts unicode emoji and emoticons to their short-code
* equivalent (`❤️` and `<3` to `:heart:`).
* @property {'decode' | 'encode' | null | undefined} [convert]
* Whether to decode (`❤️` and `<3` to `:heart:`), encode (`:heart:` and `<3`
* to `❤️`), or do nothing (optional).
*/

import {emoticon} from 'emoticon'
import {gemoji} from 'gemoji'
import {emojiModifier} from 'nlcst-emoji-modifier'
import {affixEmoticonModifier} from 'nlcst-affix-emoticon-modifier'
import {emoticonModifier} from 'nlcst-emoticon-modifier'
import {emojiModifier} from 'nlcst-emoji-modifier'
import {toString} from 'nlcst-to-string'
import {visit} from 'unist-util-visit'
import {emoticon} from 'emoticon'
import {gemoji} from 'gemoji'

const own = {}.hasOwnProperty

const vs16 = 0xfe_0f

/** @type {Record<string, gemoji[number]>} */
const emoji2info = {}
/** @type {Record<string, Gemoji>} */
const emojiToInfo = {}
/** @type {Record<string, string>} */
const emoticon2emoji = {}
const emoticonToEmoji = {}
/** @type {Record<string, string>} */
const gemoji2emoji = {}

/**
* Map of visitors.
*
* @type {Record<string, (value: string) => string>}
*/
const fns = {
/**
* Change to an emoji.
*
* @param {string} emoji
* @returns {string}
*/
encode: (emoji) => emoji,
/**
* Change to a GitHub emoji short-code.
*
* @param {string} emoji
* @returns {string}
*/
decode: (emoji) => ':' + emoji2info[emoji].names[0] + ':'
}
const gemojiToEmoji = {}

init()

/** @type {Readonly<Options>} */
const emptyOptions = {}

/**
* Plugin to support emoji, gemoji, and emoticons.
*
* @type {import('unified').Plugin<[Options?], Root>}
* @this {Processor}
* Processor.
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
export default function retextEmoji(options) {
const convert = (options || {}).convert
/** @type {fns[keyof fns]|undefined} */
let fn
const settings = options || emptyOptions
const convert = settings.convert

// Register extensions to parser.
let sentence = this.data('nlcstSentenceExtensions')
let paragraph = this.data('nlcstParagraphExtensions')

Expand All @@ -82,38 +70,47 @@ export default function retextEmoji(options) {
sentence.push(emojiModifier, emoticonModifier)
paragraph.push(affixEmoticonModifier)

if (convert !== null && convert !== undefined) {
if (!Object.keys(fns).includes(convert)) {
throw new TypeError(
'Invalid `convert` value `' +
convert +
"`, expected `'encode'` or `'decode'`"
)
}

fn = fns[convert]
/** @type {Convert | undefined} */
let convertFn

if (convert === null || convert === undefined) {
// Empty.
} else if (convert === 'decode') {
convertFn = decode
} else if (convert === 'encode') {
convertFn = identity
} else {
throw new TypeError(
'Invalid `convert` value `' +
convert +
"`, expected `'decode'` or `'encode'`"
)
}

return (node) => {
visit(node, 'EmoticonNode', (node) => {
/**
* Transform.
*
* @param {Root} tree
* Tree.
* @returns {undefined}
* Nothing.
*/
return function (tree) {
visit(tree, 'EmoticonNode', function (node) {
const emoji = parse(toString(node))

if (!emoji) return

if (fn) {
node.value = fn(emoji)
if (convertFn) {
node.value = convertFn(emoji)
}

const info = emoji2info[emoji]
const info = emojiToInfo[emoji]
const data = node.data || (node.data = {})
// @ts-expect-error: to do: register extra data.
data.emoji = info.emoji
// @ts-expect-error: to do: register extra data.
data.description = info.description
// @ts-expect-error: to do: register extra data.
data.names = info.names.concat()
// @ts-expect-error: to do: register extra data.
data.tags = info.tags.concat()
data.names = [...info.names]
data.tags = [...info.tags]
})
}
}
Expand All @@ -122,15 +119,18 @@ export default function retextEmoji(options) {
* Map a value to an emoji.
*
* @param {string} value
* Input.
* @returns {string | undefined}
* Output.
*/
function parse(value) {
if (own.call(emoji2info, value)) return value
if (own.call(emoticon2emoji, value)) return emoticon2emoji[value]
if (own.call(gemoji2emoji, value)) return gemoji2emoji[value]
if (Object.hasOwn(emojiToInfo, value)) return value
if (Object.hasOwn(emoticonToEmoji, value)) return emoticonToEmoji[value]
if (Object.hasOwn(gemojiToEmoji, value)) return gemojiToEmoji[value]

if (value.charCodeAt(value.length - 1) === vs16) {
const without = value.slice(0, -1)
if (own.call(emoji2info, without)) return without
if (Object.hasOwn(emojiToInfo, without)) return without
}
}

Expand All @@ -143,10 +143,10 @@ function init() {
const values = info.names
let offset = -1

emoji2info[info.emoji] = info
emojiToInfo[info.emoji] = info

while (++offset < values.length) {
gemoji2emoji[':' + values[offset] + ':'] = info.emoji
gemojiToEmoji[':' + values[offset] + ':'] = info.emoji
}
}

Expand All @@ -157,7 +157,21 @@ function init() {
let offset = -1

while (++offset < info.emoticons.length) {
emoticon2emoji[info.emoticons[offset]] = info.emoji
emoticonToEmoji[info.emoticons[offset]] = info.emoji
}
}
}

/**
* @type {Convert}
*/
function decode(value) {
return ':' + emojiToInfo[value].names[0] + ':'
}

/**
* @type {Convert}
*/
function identity(value) {
return value
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
"xo": {
"prettier": true,
"rules": {
"unicorn/prefer-code-point": "off"
"unicorn/prefer-code-point": "off",
"unicorn/prefer-switch": "off"
}
}
}
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ import {visit} from 'unist-util-visit'
/** @type {import('nlcst').Root} */
const tree = getNlcstNodeSomeHow()

visit(tree, (node) => {
visit(tree, function (node) {
// `node` can now be `Emoticon`.
})
```
Expand Down
10 changes: 8 additions & 2 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @typedef {import('nlcst-emoticon-modifier').Emoticon} Emoticon
*/

import assert from 'node:assert/strict'
import test from 'node:test'
import {retext} from 'retext'
Expand All @@ -9,9 +13,9 @@ test('retext-emoji', async function (t) {

await t.test('should throw when given invalid `convert`', async function () {
assert.throws(function () {
// @ts-expect-error: runtime.
// @ts-expect-error: check how the runtime handles invalid `convert`.
retext().use(retextEmoji, {convert: false}).freeze()
}, /Invalid `convert` value `false`, expected `'encode'` or `'decode'`/)
}, /Invalid `convert` value `false`, expected `'decode'` or `'encode'`/)
})

await t.test('should classify emoticons', async function () {
Expand Down Expand Up @@ -236,6 +240,8 @@ test('retext-emoji', async function (t) {
})

await t.test('should not overwrite existing data', async function () {
/** @type {Emoticon} */
// @ts-expect-error: unregistered data.
const emoji = u('EmoticonNode', {data: {alpha: true, bravo: 2}}, ':+1:')

await retext()
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
"declaration": true,
"emitDeclarationOnly": true,
"exactOptionalPropertyTypes": true,
"lib": ["es2020"],
"lib": ["es2022"],
"module": "node16",
"strict": true,
"target": "es2020"
},
"exclude": ["coverage/", "node_modules/"],
"include": ["**/*.js"]
"include": ["**/*.js", "index.d.ts"]
}

0 comments on commit a21dda5

Please sign in to comment.