UNPKG

tm-text

Version:

Trackmania and Maniaplanet text parser and formatter

244 lines (243 loc) 10.1 kB
"use strict"; 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', '&#13;') .replaceAll('\t', '&#9;'); }; exports.htmlify = htmlify; exports.default = exports.htmlify;