tm-text
Version:
Trackmania and Maniaplanet text parser and formatter
244 lines (243 loc) • 10.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.htmlify = exports.KIND_TO_CSS_MAP = void 0;
const object_entries_1 = require("../utils/object-entries");
const options_1 = require("../utils/options");
const syntax_1 = require("../utils/syntax");
const tokenize_1 = require("./tokenize");
exports.KIND_TO_CSS_MAP = {
[syntax_1.TOKEN.BOLD]: 'font-weight:700',
[syntax_1.TOKEN.COLOR]: 'color:<value>',
[syntax_1.TOKEN.ITALIC]: 'font-style:italic',
[syntax_1.TOKEN.SHADOW]: 'text-shadow:1px 1px 1px #000',
[syntax_1.TOKEN.UPPERCASE]: 'text-transform:uppercase',
[syntax_1.TOKEN.WIDTH_NARROW]: 'display:inline-block;margin-right:<value>;transform:scaleX(0.64);transform-origin:0 100%',
[syntax_1.TOKEN.WIDTH_WIDE]: 'display:inline-block;margin-right:<value>;transform:scaleX(1.57);transform-origin:0 100%',
};
const createTag = (tagName, content, attributes) => {
let output = `<${tagName}`;
(0, object_entries_1.objectEntries)(attributes).forEach(([key, value]) => {
if (value === null || value === void 0 ? void 0 : value.length) {
output += ` ${key}="${value}"`;
}
});
output += `>${content}</${tagName}>`;
return output;
};
const hasNextWordSameState = (word, nextWord) => !(0, object_entries_1.objectEntries)(word.blockState).some(([key, value]) => (nextWord === null || nextWord === void 0 ? void 0 : nextWord.blockState[key]) !== value);
const getCharWidth = (char, font) => {
if (!document) {
throw new Error('A document must be available to htmlify width tags.');
}
const context = document.createElement('canvas').getContext('2d');
if (!context) {
return 0;
}
context.font = font;
return context.measureText(char).width;
};
const getCanvasFont = (options) => {
if (!window) {
throw new Error('A window must be available to htmlify width tags.');
}
let family;
let size;
let weight;
if (options.font instanceof HTMLElement) {
const computedStyle = window.getComputedStyle(options.font);
family = computedStyle.fontFamily || 'Times New Roman';
size = computedStyle.fontSize || '16px';
weight = computedStyle.fontWeight || 'normal';
}
else {
({ family, weight, size } = options.font);
}
return `${weight} ${size} ${family}`;
};
const renderWord = (content, blockState, options) => {
const css = [];
(0, object_entries_1.objectEntries)(blockState).forEach(([key, value]) => {
const cssForKind = exports.KIND_TO_CSS_MAP[key];
if (!cssForKind || key === 'linkKind' || key === 'linkHref') {
return;
}
if (key === syntax_1.TOKEN.COLOR && typeof value === 'string') {
css.push(cssForKind.replace('<value>', value));
}
else if (value) {
if (key === syntax_1.TOKEN.WIDTH_NARROW) {
const marginRight = -getCharWidth(content, getCanvasFont(options)) * 0.37;
css.push(cssForKind.replace('<value>', `${marginRight.toFixed(3)}px`));
}
else if (key === syntax_1.TOKEN.WIDTH_WIDE) {
const marginRight = getCharWidth(content, getCanvasFont(options)) * 0.57;
css.push(cssForKind.replace('<value>', `${marginRight.toFixed(3)}px`));
}
else {
css.push(cssForKind);
}
}
});
return createTag('span', content, {
style: css.join(';'),
});
};
const transformHref = (href, kind, options) => {
if (kind === syntax_1.TOKEN.LINK_EXTERNAL) {
return href.startsWith('http://') || href.startsWith('https://')
? href
: `${options.scheme}://${href}`;
}
let theHref = href;
if (options.syntax === syntax_1.SYNTAX.MANIAPLANET && !href.startsWith('maniaplanet://')) {
theHref = `maniaplanet://${href}`;
}
else if (options.syntax !== syntax_1.SYNTAX.MANIAPLANET && !href.startsWith('tmtp://')) {
theHref = `tmtp://${href}`;
}
if (options.syntax === syntax_1.SYNTAX.FOREVER && kind === syntax_1.TOKEN.LINK_INTERNAL_WITH_PARAMS) {
const params = new URLSearchParams();
(0, object_entries_1.objectEntries)(options.playerParameters).forEach(([key, value]) => {
if (value) {
params.set(key, value);
}
});
const queryString = params.toString();
if (queryString.length) {
theHref += `?${queryString}`;
}
}
return theHref;
};
const getLinkedWords = (word, startIndex, words) => {
const linkedWords = [word];
for (let i = startIndex + 1; i <= words.length; i += 1) {
const nextWord = words.at(i);
if (!(nextWord === null || nextWord === void 0 ? void 0 : nextWord.blockState.linkKind) || nextWord.blockState.linkHref !== word.blockState.linkHref) {
break;
}
linkedWords.push(nextWord);
}
return linkedWords;
};
const htmlify = (input, options) => {
const opts = (0, options_1.withDefaultOptions)(options);
const tokens = typeof input === 'string' ? (0, tokenize_1.tokenize)(input, opts) : input;
const words = [];
let currentWordContent = '';
let currentLinkContent = '';
let wordsToSkip = 0;
let html = '';
const blockStates = [{
linkKind: false,
linkHref: false,
[syntax_1.TOKEN.BOLD]: false,
[syntax_1.TOKEN.COLOR]: false,
[syntax_1.TOKEN.ITALIC]: false,
[syntax_1.TOKEN.SHADOW]: false,
[syntax_1.TOKEN.UPPERCASE]: false,
[syntax_1.TOKEN.WIDTH_NARROW]: false,
[syntax_1.TOKEN.WIDTH_NORMAL]: false,
[syntax_1.TOKEN.WIDTH_WIDE]: false,
}];
tokens.forEach((token, index) => {
const currentBlockState = blockStates.at(-1);
if (!currentBlockState) {
throw new Error('The blockStates array should at least contain one object');
}
if (token.kind === syntax_1.TOKEN.WORD || token.kind === syntax_1.TOKEN.NEWLINE || token.kind === syntax_1.TOKEN.TAB) {
let { content } = token;
if (token.kind === syntax_1.TOKEN.NEWLINE) {
content = '\n';
}
else if (token.kind === syntax_1.TOKEN.TAB) {
content = '\t';
}
words.push({
content,
blockState: Object.assign({}, currentBlockState),
});
}
else if (token.kind === syntax_1.TOKEN.BLOCK_START) {
blockStates.push(Object.assign({}, currentBlockState));
}
else if (token.kind === syntax_1.TOKEN.BLOCK_END) {
if (blockStates.length > 1) {
blockStates.pop();
}
}
else if ((0, tokenize_1.isCssKind)(token.kind)) {
if (token.kind === syntax_1.TOKEN.COLOR) {
currentBlockState.COLOR = token.content.replace('$', '#');
}
else if ((0, tokenize_1.isWidthKind)(token.kind)) {
currentBlockState.WIDTH_NORMAL = token.kind === syntax_1.TOKEN.WIDTH_NORMAL;
currentBlockState.WIDTH_NARROW = token.kind === syntax_1.TOKEN.WIDTH_NARROW;
currentBlockState.WIDTH_WIDE = token.kind === syntax_1.TOKEN.WIDTH_WIDE;
}
else {
currentBlockState[token.kind] = !currentBlockState[token.kind];
}
}
else if (token.kind === syntax_1.TOKEN.RESET_ALL || token.kind === syntax_1.TOKEN.RESET_COLOR) {
currentBlockState.COLOR = false;
if (token.kind === syntax_1.TOKEN.RESET_ALL) {
currentBlockState.linkKind = false;
currentBlockState.linkHref = false;
currentBlockState.BOLD = false;
currentBlockState.ITALIC = false;
currentBlockState.SHADOW = false;
currentBlockState.UPPERCASE = false;
}
}
else if ((0, tokenize_1.isLinkKind)(token.kind)) {
if (currentBlockState.linkKind) {
currentBlockState.linkKind = false;
currentBlockState.linkHref = false;
return;
}
const nextButOneToken = tokens.at(index + 2);
if ((nextButOneToken === null || nextButOneToken === void 0 ? void 0 : nextButOneToken.kind) === syntax_1.TOKEN.HREF_CONTENT) {
currentBlockState.linkHref = nextButOneToken.content;
}
currentBlockState.linkKind = token.kind;
}
});
words.forEach((word, index) => {
if (!word.blockState.linkKind) {
if (hasNextWordSameState(word, words.at(index + 1))) {
currentWordContent += word.content;
}
else {
html += renderWord(currentWordContent + word.content, word.blockState, opts);
currentWordContent = '';
}
return;
}
if (wordsToSkip > 0) {
wordsToSkip -= 1;
return;
}
const linkedWords = getLinkedWords(word, index, words);
const linkHref = word.blockState.linkHref || linkedWords.reduce((acc, { content }) => acc + content, '');
const linkHtml = linkedWords.reduce((result, linkedWord, i) => {
if (hasNextWordSameState(word, linkedWords.at(i + 1))) {
currentLinkContent += linkedWord.content;
return result;
}
let theResult = result;
theResult += renderWord(currentLinkContent + linkedWord.content, linkedWord.blockState, opts);
currentLinkContent = '';
return theResult;
}, '');
wordsToSkip = linkedWords.length - 1;
html += createTag('a', linkHtml, {
href: transformHref(linkHref, word.blockState.linkKind, opts),
});
});
return html
.replaceAll('\n', ' ')
.replaceAll('\t', '	');
};
exports.htmlify = htmlify;
exports.default = exports.htmlify;