UNPKG

rot-js

Version:

A roguelike toolkit in JavaScript

183 lines (182 loc) 6.38 kB
/** * @namespace * Contains text tokenization and breaking routines */ const RE_COLORS = /%([bc]){([^}]*)}/g; // token types export const TYPE_TEXT = 0; export const TYPE_NEWLINE = 1; export const TYPE_FG = 2; export const TYPE_BG = 3; /** * Measure size of a resulting text block */ export function measure(str, maxWidth) { let result = { width: 0, height: 1 }; let tokens = tokenize(str, maxWidth); let lineWidth = 0; for (let i = 0; i < tokens.length; i++) { let token = tokens[i]; switch (token.type) { case TYPE_TEXT: lineWidth += token.value.length; break; case TYPE_NEWLINE: result.height++; result.width = Math.max(result.width, lineWidth); lineWidth = 0; break; } } result.width = Math.max(result.width, lineWidth); return result; } /** * Convert string to a series of a formatting commands */ export function tokenize(str, maxWidth) { let result = []; /* first tokenization pass - split texts and color formatting commands */ let offset = 0; str.replace(RE_COLORS, function (match, type, name, index) { /* string before */ let part = str.substring(offset, index); if (part.length) { result.push({ type: TYPE_TEXT, value: part }); } /* color command */ result.push({ type: (type == "c" ? TYPE_FG : TYPE_BG), value: name.trim() }); offset = index + match.length; return ""; }); /* last remaining part */ let part = str.substring(offset); if (part.length) { result.push({ type: TYPE_TEXT, value: part }); } return breakLines(result, maxWidth); } /* insert line breaks into first-pass tokenized data */ function breakLines(tokens, maxWidth) { if (!maxWidth) { maxWidth = Infinity; } let i = 0; let lineLength = 0; let lastTokenWithSpace = -1; while (i < tokens.length) { /* take all text tokens, remove space, apply linebreaks */ let token = tokens[i]; if (token.type == TYPE_NEWLINE) { /* reset */ lineLength = 0; lastTokenWithSpace = -1; } if (token.type != TYPE_TEXT) { /* skip non-text tokens */ i++; continue; } /* remove spaces at the beginning of line */ while (lineLength == 0 && token.value.charAt(0) == " ") { token.value = token.value.substring(1); } /* forced newline? insert two new tokens after this one */ let index = token.value.indexOf("\n"); if (index != -1) { token.value = breakInsideToken(tokens, i, index, true); /* if there are spaces at the end, we must remove them (we do not want the line too long) */ let arr = token.value.split(""); while (arr.length && arr[arr.length - 1] == " ") { arr.pop(); } token.value = arr.join(""); } /* token degenerated? */ if (!token.value.length) { tokens.splice(i, 1); continue; } if (lineLength + token.value.length > maxWidth) { /* line too long, find a suitable breaking spot */ /* is it possible to break within this token? */ let index = -1; while (1) { let nextIndex = token.value.indexOf(" ", index + 1); if (nextIndex == -1) { break; } if (lineLength + nextIndex > maxWidth) { break; } index = nextIndex; } if (index != -1) { /* break at space within this one */ token.value = breakInsideToken(tokens, i, index, true); } else if (lastTokenWithSpace != -1) { /* is there a previous token where a break can occur? */ let token = tokens[lastTokenWithSpace]; let breakIndex = token.value.lastIndexOf(" "); token.value = breakInsideToken(tokens, lastTokenWithSpace, breakIndex, true); i = lastTokenWithSpace; } else { /* force break in this token */ token.value = breakInsideToken(tokens, i, maxWidth - lineLength, false); } } else { /* line not long, continue */ lineLength += token.value.length; if (token.value.indexOf(" ") != -1) { lastTokenWithSpace = i; } } i++; /* advance to next token */ } tokens.push({ type: TYPE_NEWLINE }); /* insert fake newline to fix the last text line */ /* remove trailing space from text tokens before newlines */ let lastTextToken = null; for (let i = 0; i < tokens.length; i++) { let token = tokens[i]; switch (token.type) { case TYPE_TEXT: lastTextToken = token; break; case TYPE_NEWLINE: if (lastTextToken) { /* remove trailing space */ let arr = lastTextToken.value.split(""); while (arr.length && arr[arr.length - 1] == " ") { arr.pop(); } lastTextToken.value = arr.join(""); } lastTextToken = null; break; } } tokens.pop(); /* remove fake token */ return tokens; } /** * Create new tokens and insert them into the stream * @param {object[]} tokens * @param {int} tokenIndex Token being processed * @param {int} breakIndex Index within current token's value * @param {bool} removeBreakChar Do we want to remove the breaking character? * @returns {string} remaining unbroken token value */ function breakInsideToken(tokens, tokenIndex, breakIndex, removeBreakChar) { let newBreakToken = { type: TYPE_NEWLINE }; let newTextToken = { type: TYPE_TEXT, value: tokens[tokenIndex].value.substring(breakIndex + (removeBreakChar ? 1 : 0)) }; tokens.splice(tokenIndex + 1, 0, newBreakToken, newTextToken); return tokens[tokenIndex].value.substring(0, breakIndex); }