import cheerio from 'cheerio';
import { MbNode, NodeOrText } from '../utils/node.js';
import * as logger from '../utils/logger.js';
import { processIconString } from '../lib/markdown-it/plugins/markdown-it-icons.js';
import { emojiData as emojiDictionary } from '../lib/markdown-it/patches/markdown-it-emoji-fixed.js';
interface EmojiData {
[key: string]: string;
}
const emojiData = emojiDictionary as unknown as EmojiData;
const ICON_ATTRIBUTES
= ['icon', 'i-width', 'i-height', 'i-size', 'i-class', 'i-spacing', 'text', 't-size', 't-class', 'once'];
interface IconAttributes {
icon?: string;
iconClassName?: string;
iconSize?: string;
width?: string;
height?: string;
text?: string;
textClassName?: string;
textSize?: string;
spacing?: string;
once?: boolean;
}
class TextsManager {
texts: string[] = [];
nextTextPointer: number = 0;
constructor() {
this.texts = [];
}
isInUse() {
return this.texts.length > 0;
}
stopUsage() {
this.texts = [];
this.nextTextPointer = 0;
}
next(): string {
if (this.texts.length === 0) {
throw new Error('No texts');
}
const next = this.texts[this.nextTextPointer];
if (this.nextTextPointer < this.texts.length - 1) {
this.nextTextPointer += 1;
}
return next;
}
resetTexts(texts: string[]) {
this.texts = texts;
this.nextTextPointer = 0;
}
}
type IconAttributeDetail = {
isFirst: boolean;
addIcons: boolean;
textsManager: TextsManager;
iconAttrs: IconAttributes | null;
};
function classifyIcon(icon: string) {
const isEmoji = Object.prototype.hasOwnProperty.call(emojiData, icon);
return {
isEmoji,
unicodeEmoji: isEmoji
? emojiData[icon]
: undefined,
};
}
function createTextSpan(iconAttrs: IconAttributes): cheerio.Cheerio | null {
if (iconAttrs.text === undefined || iconAttrs.text.length === 0) {
return null;
}
const spanNode = cheerio(``)
.css({
'font-size': iconAttrs.textSize,
}).addClass(iconAttrs.textClassName || '');
const iconSpacing = iconAttrs.spacing || '0.35em';
return spanNode.css({
'line-height': 'unset',
'margin-inline-end': iconSpacing,
'align-self': 'flex-start',
'flex-shrink': '0',
});
}
function createIconSpan(iconAttrs: IconAttributes): cheerio.Cheerio | null {
if (iconAttrs.icon === undefined || iconAttrs.icon.length === 0) {
return null;
}
let spanContent;
const {
isEmoji,
unicodeEmoji,
} = classifyIcon(iconAttrs.icon!);
if (isEmoji) {
spanContent = ``;
} else {
spanContent = processIconString(iconAttrs.icon);
}
let spanNode;
if (spanContent === null && iconAttrs.icon !== undefined) {
const img = cheerio(``)
.css({ width: iconAttrs.width, height: iconAttrs.height, display: 'inline-block' })
.addClass(iconAttrs.iconClassName || '');
img.append('\u200B');
spanContent = cheerio('').append(img).css({
'padding-bottom': '0.3em',
'padding-top': '0.3em',
});
spanNode = cheerio(spanContent).css({ 'font-size': 'unset', 'min-width': '16px' });
} else {
spanNode = cheerio(spanContent).css({ 'font-size': 'unset', 'min-width': '16px' });
spanNode = spanNode.css({ 'font-size': iconAttrs.iconSize }).addClass(iconAttrs.iconClassName || '');
}
// Add invisible character to avoid the element from being empty
spanNode.append('\u200B');
const iconSpacing = iconAttrs.text ? '0.35em' : iconAttrs.spacing || '0.35em';
return spanNode.css({
'line-height': 'unset',
'margin-inline-end': iconSpacing,
'align-self': 'flex-start',
'flex-shrink': '0',
});
}
function updateNodeStyle(node: NodeOrText) {
const nodeCheerio = cheerio(node);
nodeCheerio.css({
'list-style-type': 'none',
'padding-inline-start': '0px',
});
}
// If an item has a specified icon, that icon and its attributes will be saved and used
// for it and for subsequent items at that level to prevent duplication of icons
// attribute declarations.
// If once is true, its icons and/or attributes will only be used for that item.
// Items with once icons/attributes do not overwrite the previously saved icon/
// attributes, meaning that subsequent items will still use the last saved
// icon/attributes.
const getIconAttributes = (node: MbNode, renderMdInline: (text: string) => string,
iconAttrsSoFar?: IconAttributes):
IconAttributes | null => {
if (iconAttrsSoFar?.icon === undefined && node.attribs.icon === undefined
&& iconAttrsSoFar?.text === undefined && node.attribs.text === undefined) {
return null;
}
return {
icon: node.attribs.icon !== undefined ? node.attribs.icon : iconAttrsSoFar?.icon,
width: node.attribs['i-width'] !== undefined ? node.attribs['i-width'] : iconAttrsSoFar?.width,
height: node.attribs['i-height'] !== undefined ? node.attribs['i-height'] : iconAttrsSoFar?.height,
iconSize: node.attribs['i-size'] !== undefined ? node.attribs['i-size'] : iconAttrsSoFar?.iconSize,
iconClassName: node.attribs['i-class'] !== undefined
? node.attribs['i-class']
: iconAttrsSoFar?.iconClassName,
text: node.attribs.text !== undefined ? renderMdInline(node.attribs.text) : iconAttrsSoFar?.text,
textClassName: node.attribs['t-class'] !== undefined
? node.attribs['t-class']
: iconAttrsSoFar?.textClassName,
textSize: node.attribs['t-size'] !== undefined ? node.attribs['t-size'] : iconAttrsSoFar?.textSize,
spacing: node.attribs['i-spacing'] !== undefined ? node.attribs['i-spacing'] : iconAttrsSoFar?.spacing,
once: (node.attribs.once === true || node.attribs.once === 'true'),
};
};
const deleteAttributes = (node: MbNode, attributes: string[]) => {
attributes.forEach((attr) => {
delete node.attribs[attr];
});
};
function updateLi(node: MbNode, iconAttributes: IconAttributes, renderMdInline: (text: string) => string) {
const curLiIcon = getIconAttributes(node, renderMdInline, iconAttributes);
deleteAttributes(node, ICON_ATTRIBUTES);
// Create a new div and span
const div = cheerio('