From f0f17eda020eb2a51f6d196d0c5043db8131ef39 Mon Sep 17 00:00:00 2001 From: meem <75212565+meemofcourse@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:42:55 -0300 Subject: [PATCH] mitigates paper fields lagging out a lot (#2738) ## About The Pull Request Finishes porting https://github.com/tgstation/tgstation/pull/73628 ## Why It's Good For The Game paperlag bad ## Changelog :cl: Timberpoes fix: Papercode has been significantly improved and trivially filled paper forms should no longer lag or crash players' game clients. /:cl: --- tgui/packages/tgui/interfaces/PaperSheet.tsx | 112 +++++++++++++++++-- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/tgui/packages/tgui/interfaces/PaperSheet.tsx b/tgui/packages/tgui/interfaces/PaperSheet.tsx index 1151c7ce9a59d..beda23bd4a0ac 100644 --- a/tgui/packages/tgui/interfaces/PaperSheet.tsx +++ b/tgui/packages/tgui/interfaces/PaperSheet.tsx @@ -430,10 +430,83 @@ export class PreviewView extends Component { // Array containing cache of HTMLInputElements that are enabled. enabledInputFieldCache: { [key: string]: HTMLInputElement } = {}; + // State checking variables. Used to determine whether or not to use cache. + lastReadOnly: boolean = true; + lastDMInputCount: number = 0; + lastFieldCount: number = 0; + lastFieldInputCount: number = 0; + + // Cache variables for fully parsed text. Workaround for marked.js not being + // super fast on the BYOND/IE js engine. + parsedDMCache: string = ''; + parsedTextBoxCache: string = ''; + constructor(props, context) { super(props, context); + this.configureMarked(); } + configureMarked = (): void => { + // This is an extension for marked defining a complete custom tokenizer. + // This tokenizer should run before the the non-custom ones, and gives us + // the ability to handle [_____] fields before the em/strong tokenizers + // mangle them, since underscores are used for italic/bold. + // This massively improves the order of operations, allowing us to run + // marked, THEN sanitise the output (much safer) and finally insert fields + // manually afterwards. + const inputField = { + name: 'inputField', + level: 'inline', + + start(src) { + return src.match(/\[/)?.index; + }, + + tokenizer(src: string) { + const rule = /^\[_+\]/; + const match = src.match(rule); + if (match) { + const token = { + type: 'inputField', + raw: match[0], + }; + return token; + } + }, + + renderer(token) { + return `${token.raw}`; + }, + }; + + // Override function, any links and images should + // kill any other marked tokens we don't want here + const walkTokens = (token) => { + switch (token.type) { + case 'url': + case 'autolink': + case 'reflink': + case 'link': + case 'image': + token.type = 'text'; + // Once asset system is up change to some default image + // or rewrite for icon images + token.href = ''; + break; + } + }; + + marked.use({ + extensions: [inputField], + breaks: true, + gfm: true, + smartypants: true, + walkTokens: walkTokens, + // Once assets are fixed might need to change this for them + baseUrl: 'thisshouldbreakhttp', + }); + }; + // Extracts the paper field "counter" from a full ID. getHeaderID = (header: string): string => { return header.replace('paperfield_', ''); @@ -457,6 +530,7 @@ export class PreviewView extends Component { // Skip text area input. if (input.nodeName !== 'INPUT') { + this.parsedTextBoxCache = ''; return; } @@ -494,6 +568,7 @@ export class PreviewView extends Component { createPreviewFromDM = (): { text: string; newFieldCount: number } => { const { data } = useBackend(this.context); const { + raw_field_input, raw_text_input, default_pen_font, default_pen_color, @@ -506,6 +581,19 @@ export class PreviewView extends Component { const readOnly = !canEdit(held_item_details); + // If readonly is the same (input field writiability state hasn't changed) + // And the input stats are the same (no new text inputs since last time) + // Then use any cached values. + if ( + this.lastReadOnly === readOnly && + this.lastDMInputCount === raw_text_input?.length && + this.lastFieldInputCount === raw_field_input?.length + ) { + return { text: this.parsedDMCache, newFieldCount: this.lastFieldCount }; + } + + this.lastReadOnly = readOnly; + raw_text_input?.forEach((value) => { let rawText = value.raw_text.trim(); if (!rawText.length) { @@ -533,6 +621,11 @@ export class PreviewView extends Component { fieldCount = processingOutput.nextCounter; }); + this.lastDMInputCount = raw_text_input?.length || 0; + this.lastFieldInputCount = raw_field_input?.length || 0; + this.lastFieldCount = fieldCount; + this.parsedDMCache = output; + return { text: output, newFieldCount: fieldCount }; }; @@ -548,6 +641,11 @@ export class PreviewView extends Component { } = data; const { textArea } = this.props; + // Use the cache if one exists. + if (this.parsedTextBoxCache) { + return this.parsedTextBoxCache; + } + const readOnly = true; const fontColor = held_item_details?.color || default_pen_color; @@ -564,6 +662,8 @@ export class PreviewView extends Component { readOnly ); + this.parsedTextBoxCache = processingOutput.text; + return processingOutput.text; }; @@ -630,17 +730,7 @@ export class PreviewView extends Component { }, }; - // marked.use({ tokenizer }); - marked.use({ extensions: [inputField] }); - - return marked.parse(rawText, { - breaks: true, - smartypants: true, - smartLists: true, - walkTokens, - // Once assets are fixed might need to change this for them - baseUrl: 'thisshouldbreakhttp', - }); + return marked.parse(rawText); }; // Fully formats, sanitises and parses the provided raw text and wraps it