From db21395639e2b753681939691db0cb353f77603a Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Fri, 9 Jun 2023 16:59:45 +0000 Subject: [PATCH] Split out pure functions and types into utils --- source/assets/js/playground.ts | 162 +++---------------- source/assets/js/playground/console-utils.ts | 74 +++++++++ source/assets/js/playground/utils.ts | 61 +++++++ 3 files changed, 156 insertions(+), 141 deletions(-) create mode 100644 source/assets/js/playground/console-utils.ts create mode 100644 source/assets/js/playground/utils.ts diff --git a/source/assets/js/playground.ts b/source/assets/js/playground.ts index 5caeed7..8e6ada6 100644 --- a/source/assets/js/playground.ts +++ b/source/assets/js/playground.ts @@ -1,60 +1,18 @@ -import { Diagnostic, setDiagnostics } from '@codemirror/lint'; +import { setDiagnostics } from '@codemirror/lint'; import { Text } from '@codemirror/state'; import { EditorView } from 'codemirror'; import debounce from 'lodash.debounce'; -import { - compileString, - Exception, - Logger, - OutputStyle, - SourceSpan, - Syntax, -} from 'sass'; +import { compileString, Logger, OutputStyle, Syntax } from 'sass'; +import { displayForConsoleLog } from './playground/console-utils.js'; import { editorSetup, outputSetup } from './playground/editor-setup.js'; - -type ConsoleLogDebug = { - options: { - span: SourceSpan; - }; - message: string; - type: 'debug'; -}; - -type ConsoleLogWarning = { - options: { - deprecation: boolean; - span?: SourceSpan | undefined; - stack?: string | undefined; - }; - message: string; - type: 'warn'; -}; -type ConsoleLogError = { - type: 'error'; - error: Exception | unknown; -}; -type ConsoleLog = ConsoleLogDebug | ConsoleLogWarning | ConsoleLogError; - -type PlaygroundState = { - inputFormat: Syntax; - outputFormat: OutputStyle; - inputValue: string; - compilerHasError: boolean; - debugOutput: ConsoleLog[]; -}; - -/** - * Encode the HTML in a user-submitted string to print safely using innerHTML - * Adapted from https://vanillajstoolkit.com/helpers/encodehtml/ - * @param {string} str The user-submitted string - * @return {string} The sanitized string - */ -function encodeHTML(str: string): string { - return str.replace(/[^\w-_. ]/gi, function (c) { - return `&#${c.charCodeAt(0)};`; - }); -} +import { + base64ToState, + errorToDiagnostic, + ParseResult, + PlaygroundState, + stateToBase64, +} from './playground/utils.js'; function setupPlayground() { const hashState = base64ToState(location.hash); @@ -67,6 +25,7 @@ function setupPlayground() { debugOutput: [], }; + // Proxy intercepts setters and triggers side effects const playgroundState = new Proxy(initialState, { set(state: PlaygroundState, prop: keyof PlaygroundState, ...rest) { // Set state first so called functions have access @@ -86,6 +45,7 @@ function setupPlayground() { }, }); + // Setup input sass view const editor = new EditorView({ doc: playgroundState.inputValue, extensions: [ @@ -122,6 +82,7 @@ function setupPlayground() { setting: 'outputFormat'; }; function attachListeners() { + // Settings buttons handlers function clickHandler(event: Event) { if (event.currentTarget instanceof HTMLElement) { const settings = event.currentTarget.dataset as TabbarItemDataset; @@ -137,6 +98,7 @@ function setupPlayground() { option.addEventListener('click', clickHandler); }); + // Copy URL handlers const copyURLButton = document.getElementById('playground-copy-url'); const copiedAlert = document.getElementById('playground-copied-alert'); @@ -181,6 +143,13 @@ function setupPlayground() { playgroundState.compilerHasError.toString(); } + /** + * updateDebugOutput + * Applies debug output state + * Called at end of updateCSS, and not by a debugOutput setter + * debugOutput may be updated multiple times during the sass compilation, + * so the output is collected through the compilation and the display updated just once. + */ function updateDebugOutput() { const console = document.querySelector('.console') as HTMLDivElement; console.innerHTML = playgroundState.debugOutput @@ -188,45 +157,6 @@ function setupPlayground() { .join('\n'); } - function lineNumberFormatter(number?: number): string { - if (typeof number === 'undefined') return ''; - number = number + 1; - return `${number} `; - } - - function displayForConsoleLog(item: ConsoleLog): string { - const data: { type: string; lineNumber?: number; message: string } = { - type: item.type, - lineNumber: undefined, - message: '', - }; - if (item.type === 'error') { - if (item.error instanceof Exception) { - data.lineNumber = item.error.span.start.line; - } - data.message = item.error?.toString() || ''; - } else if (['debug', 'warn'].includes(item.type)) { - data.message = item.message; - let lineNumber = item.options.span?.start?.line; - if (typeof lineNumber === 'undefined') { - const stack = 'stack' in item.options ? item.options.stack : ''; - const needleFromStackRegex = /^- (\d+):/; - const match = stack?.match(needleFromStackRegex); - if (match && match[1]) { - // Stack trace starts at 1, all others come from span, which starts at 0, so adjust before formatting. - lineNumber = parseInt(match[1]) - 1; - } - } - data.lineNumber = lineNumber; - } - - return `

@${ - data.type - }:${lineNumberFormatter(data.lineNumber)} ${encodeHTML( - data.message, - )}

`; - } - function updateCSS() { playgroundState.debugOutput = []; const result = parse(playgroundState.inputValue); @@ -270,10 +200,6 @@ function setupPlayground() { }, }; - type ParseResultSuccess = { css: string }; - type ParseResultError = { error: Exception | unknown }; - type ParseResult = ParseResultSuccess | ParseResultError; - function parse(css: string): ParseResult { try { const result = compileString(css, { @@ -287,57 +213,11 @@ function setupPlayground() { } } - function errorToDiagnostic(error: Exception | unknown): Diagnostic { - if (error instanceof Exception) { - return { - from: error.span.start.offset, - to: error.span.end.offset, - severity: 'error', - message: error.toString(), - }; - } else { - let errorString = 'Unknown compilation error'; - if (typeof error === 'string') errorString = error; - else if (typeof error?.toString() === 'string') - errorString = error.toString(); - return { - from: 0, - to: 0, - severity: 'error', - message: errorString, - }; - } - } - function updateURL() { const hash = stateToBase64(playgroundState); history.replaceState('playground', '', `#${hash}`); } - // State is persisted to the URL's hash format in the following format: - // [inputFormat, outputFormat, ...inputValue] = hash; - // inputFormat: 0=indented 1=scss - // outputFormat: 0=compressed 1=expanded - function stateToBase64(state: PlaygroundState): string { - const inputFormatChar = state.inputFormat === 'scss' ? 1 : 0; - const outputFormatChar = state.outputFormat === 'expanded' ? 1 : 0; - const persistedState = `${inputFormatChar}${outputFormatChar}${state.inputValue}`; - return btoa(encodeURIComponent(persistedState)); - } - - function base64ToState(string: string): Partial { - const state: Partial = {}; - // Remove hash - const decoded = decodeURIComponent(atob(string.slice(1))); - - if (!/\d\d.*/.test(decoded)) return {}; - state.inputFormat = decoded.charAt(0) === '1' ? 'scss' : 'indented'; - state.outputFormat = decoded.charAt(1) === '1' ? 'expanded' : 'compressed'; - state.inputValue = decoded.slice(2); - - return state; - } - attachListeners(); applyInitialState(); } diff --git a/source/assets/js/playground/console-utils.ts b/source/assets/js/playground/console-utils.ts new file mode 100644 index 0000000..e5fdfa6 --- /dev/null +++ b/source/assets/js/playground/console-utils.ts @@ -0,0 +1,74 @@ +import { Exception, SourceSpan } from 'sass'; +type ConsoleLogDebug = { + options: { + span: SourceSpan; + }; + message: string; + type: 'debug'; +}; + +type ConsoleLogWarning = { + options: { + deprecation: boolean; + span?: SourceSpan | undefined; + stack?: string | undefined; + }; + message: string; + type: 'warn'; +}; +type ConsoleLogError = { + type: 'error'; + error: Exception | unknown; +}; +export type ConsoleLog = ConsoleLogDebug | ConsoleLogWarning | ConsoleLogError; + +/** + * Encode the HTML in a user-submitted string to print safely using innerHTML + * Adapted from https://vanillajstoolkit.com/helpers/encodehtml/ + * @param {string} str The user-submitted string + * @return {string} The sanitized string + */ +function encodeHTML(str: string): string { + return str.replace(/[^\w-_. ]/gi, function (c) { + return `&#${c.charCodeAt(0)};`; + }); +} + +function lineNumberFormatter(number?: number): string { + if (typeof number === 'undefined') return ''; + number = number + 1; + return `${number} `; +} + +export function displayForConsoleLog(item: ConsoleLog): string { + const data: { type: string; lineNumber?: number; message: string } = { + type: item.type, + lineNumber: undefined, + message: '', + }; + if (item.type === 'error') { + if (item.error instanceof Exception) { + data.lineNumber = item.error.span.start.line; + } + data.message = item.error?.toString() || ''; + } else if (['debug', 'warn'].includes(item.type)) { + data.message = item.message; + let lineNumber = item.options.span?.start?.line; + if (typeof lineNumber === 'undefined') { + const stack = 'stack' in item.options ? item.options.stack : ''; + const needleFromStackRegex = /^- (\d+):/; + const match = stack?.match(needleFromStackRegex); + if (match && match[1]) { + // Stack trace starts at 1, all others come from span, which starts at 0, so adjust before formatting. + lineNumber = parseInt(match[1]) - 1; + } + } + data.lineNumber = lineNumber; + } + + return `

@${ + data.type + }:${lineNumberFormatter(data.lineNumber)} ${encodeHTML( + data.message, + )}

`; +} diff --git a/source/assets/js/playground/utils.ts b/source/assets/js/playground/utils.ts new file mode 100644 index 0000000..f7c2d0c --- /dev/null +++ b/source/assets/js/playground/utils.ts @@ -0,0 +1,61 @@ +import { Diagnostic } from '@codemirror/lint'; +import { Exception, OutputStyle, Syntax } from 'sass'; + +import { ConsoleLog } from './console-utils'; +export type PlaygroundState = { + inputFormat: Syntax; + outputFormat: OutputStyle; + inputValue: string; + compilerHasError: boolean; + debugOutput: ConsoleLog[]; +}; + +// State is persisted to the URL's hash format in the following format: +// [inputFormat, outputFormat, ...inputValue] = hash; +// inputFormat: 0=indented 1=scss +// outputFormat: 0=compressed 1=expanded +export function stateToBase64(state: PlaygroundState): string { + const inputFormatChar = state.inputFormat === 'scss' ? 1 : 0; + const outputFormatChar = state.outputFormat === 'expanded' ? 1 : 0; + const persistedState = `${inputFormatChar}${outputFormatChar}${state.inputValue}`; + return btoa(encodeURIComponent(persistedState)); +} + +export function base64ToState(string: string): Partial { + const state: Partial = {}; + // Remove hash + const decoded = decodeURIComponent(atob(string.slice(1))); + + if (!/\d\d.*/.test(decoded)) return {}; + state.inputFormat = decoded.charAt(0) === '1' ? 'scss' : 'indented'; + state.outputFormat = decoded.charAt(1) === '1' ? 'expanded' : 'compressed'; + state.inputValue = decoded.slice(2); + + return state; +} + +type ParseResultSuccess = { css: string }; +type ParseResultError = { error: Exception | unknown }; +export type ParseResult = ParseResultSuccess | ParseResultError; + +export function errorToDiagnostic(error: Exception | unknown): Diagnostic { + if (error instanceof Exception) { + return { + from: error.span.start.offset, + to: error.span.end.offset, + severity: 'error', + message: error.toString(), + }; + } else { + let errorString = 'Unknown compilation error'; + if (typeof error === 'string') errorString = error; + else if (typeof error?.toString() === 'string') + errorString = error.toString(); + return { + from: 0, + to: 0, + severity: 'error', + message: errorString, + }; + } +}