diff options
Diffstat (limited to 'src/util')
-rw-r--r-- | src/util/sugar.js | 146 | ||||
-rw-r--r-- | src/util/wiki-data.js | 35 |
2 files changed, 153 insertions, 28 deletions
diff --git a/src/util/sugar.js b/src/util/sugar.js index 3f0eb2ea..eab44b75 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -250,6 +250,16 @@ export function typeAppearance(value) { return typeof value; } +// Limits a string to the desired length, filling in an ellipsis at the end +// if it cuts any text off. +export function cut(text, length = 40) { + if (text.length >= length) { + return text.slice(0, Math.max(1, length - 3)) + '...'; + } else { + return text; + } +} + // Binds default values for arguments in a {key: value} type function argument // (typically the second argument, but may be overridden by providing a // [bindOpts.bindIndex] argument). Typically useful for preparing a function for @@ -315,6 +325,12 @@ export function openAggregate({ // constructed. message = '', + // Optional flag to indicate that this layer of the aggregate error isn't + // generally useful outside of developer debugging purposes - it will be + // skipped by default when using showAggregate, showing contained errors + // inline with other children of this aggregate's parent. + translucent = false, + // Value to return when a provided function throws an error. If this is a // function, it will be called with the arguments given to the function. // (This is primarily useful when wrapping a function and then providing it @@ -397,7 +413,13 @@ export function openAggregate({ aggregate.close = () => { if (errors.length) { - throw Reflect.construct(errorClass, [errors, message]); + const error = Reflect.construct(errorClass, [errors, message]); + + if (translucent) { + error[Symbol.for(`hsmusic.aggregate.translucent`)] = true; + } + + throw error; } }; @@ -570,34 +592,101 @@ export function _withAggregate(mode, aggregateOpts, fn) { export function showAggregate(topError, { pathToFileURL = f => f, showTraces = true, + showTranslucent = showTraces, print = true, } = {}) { - const recursive = (error, {level}) => { - let headerPart = showTraces - ? `[${error.constructor.name || 'unnamed'}] ${ - error.message || '(no message)' - }` - : error instanceof AggregateError - ? `[${error.message || '(no message)'}]` - : error.message || '(no message)'; + const translucentSymbol = Symbol.for('hsmusic.aggregate.translucent'); + + const determineCause = error => { + let cause = error.cause; + if (showTranslucent) return cause ?? null; + + while (cause) { + if (!cause[translucentSymbol]) return cause; + cause = cause.cause; + } + + return null; + }; + + const determineErrors = parentError => { + if (!parentError.errors) return null; + if (showTranslucent) return parentError.errors; + + const errors = []; + for (const error of parentError.errors) { + if (!error[translucentSymbol]) { + errors.push(error); + continue; + } + + if (error.cause) { + errors.push(determineCause(error)); + } + + if (error.errors) { + errors.push(...determineErrors(error)); + } + } + + return errors; + }; + + const flattenErrorStructure = (error, level = 0) => { + const cause = determineCause(error); + const errors = determineErrors(error); + + return { + level, + + kind: error.constructor.name, + message: error.message, + stack: error.stack, + + cause: + (cause + ? flattenErrorStructure(cause, level + 1) + : null), + + errors: + (errors + ? errors.map(error => flattenErrorStructure(error, level + 1)) + : null), + }; + }; + + const recursive = ({level, kind, message, stack, cause, errors}) => { + const messagePart = + message || `(no message)`; + + const kindPart = + kind || `unnamed kind`; + + let headerPart = + (showTraces + ? `[${kindPart}] ${messagePart}` + : errors + ? `[${messagePart}]` + : messagePart); if (showTraces) { - const stackLines = error.stack?.split('\n'); + const stackLines = + stack?.split('\n'); - const stackLine = stackLines?.find( - (line) => + const stackLine = + stackLines?.find(line => line.trim().startsWith('at') && !line.includes('sugar') && !line.includes('node:') && - !line.includes('<anonymous>') - ); + !line.includes('<anonymous>')); - const tracePart = stackLine - ? '- ' + - stackLine - .trim() - .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match)) - : '(no stack trace)'; + const tracePart = + (stackLine + ? '- ' + + stackLine + .trim() + .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match)) + : '(no stack trace)'); headerPart += ` ${colors.dim(tracePart)}`; } @@ -606,8 +695,8 @@ export function showAggregate(topError, { const bar1 = ' '; const causePart = - (error.cause - ? recursive(error.cause, {level: level + 1}) + (cause + ? recursive(cause) .split('\n') .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`) .join('\n') @@ -616,19 +705,20 @@ export function showAggregate(topError, { const head2 = level % 2 === 0 ? '\u257f' : colors.dim('\u257f'); const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e'); - const aggregatePart = - (error instanceof AggregateError - ? error.errors - .map(error => recursive(error, {level: level + 1})) + const errorsPart = + (errors + ? errors + .map(error => recursive(error)) .flatMap(str => str.split('\n')) .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`) .join('\n') : ''); - return [headerPart, causePart, aggregatePart].filter(Boolean).join('\n'); + return [headerPart, causePart, errorsPart].filter(Boolean).join('\n'); }; - const message = recursive(topError, {level: 0}); + const structure = flattenErrorStructure(topError); + const message = recursive(structure); if (print) { console.error(message); diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 0790ae91..b5813c7a 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -629,6 +629,41 @@ export function sortFlashesChronologically(data, { // Specific data utilities +// Matches heading details from commentary data in roughly the formats: +// +// <i>artistReference:</i> (annotation, date) +// <i>artistReference|artistDisplayText:</i> (annotation, date) +// +// where capturing group "annotation" can be any text at all, except that the +// last entry (past a comma or the only content within parentheses), if parsed +// as a date, is the capturing group "date". "Parsing as a date" means matching +// one of these formats: +// +// * "25 December 2019" - one or two number digits, followed by any text, +// followed by four number digits +// * "December 25, 2019" - one all-letters word, a space, one or two number +// digits, a comma, and four number digits +// * "12/25/2019" etc - three sets of one to four number digits, separated +// by slashes or dashes (only valid orders are MM/DD/YYYY and YYYY/MM/DD) +// +// Note that the annotation and date are always wrapped by one opening and one +// closing parentheses. The whole heading does NOT need to match the entire +// line it occupies (though it does always start at the first position on that +// line), and if there is more than one closing parenthesis on the line, the +// annotation will always cut off only at the last parenthesis, or a comma +// preceding a date and then the last parenthesis. This is to ensure that +// parentheses can be part of the actual annotation content. +// +// Capturing group "artistReference" is all the characters between <i> and </i> +// (apart from the pipe and "artistDisplayText" text, if present), and is either +// the name of an artist or an "artist:directory"-style reference. +// +// This regular expression *doesn't* match bodies, which will need to be parsed +// out of the original string based on the indices matched using this. +// +export const commentaryRegex = + /^<i>(?<artistReferences>.+?)(?:\|(?<artistDisplayText>.+))?:<\/i>(?: \((?<annotation>(?:.*?(?=,|\)[^)]*$))*?)(?:,? ?(?<date>[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4}))?\))?/gm; + export function filterAlbumsByCommentary(albums) { return albums .filter((album) => [album, ...album.tracks].some((x) => x.commentary)); |