From 362dc0619b93d74ad34df1bfbfd9ebc632fa5156 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 14 Nov 2023 22:49:51 -0400 Subject: data, yaml: catch commentary artist ref errors --- src/util/wiki-data.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'src/util') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 0790ae91..5ab01225 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -629,6 +629,31 @@ export function sortFlashesChronologically(data, { // Specific data utilities +// Matches heading details from commentary data in roughly the format: +// +// artistReference: (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 one of +// these formats: +// +// * "25 December 2019" - one or two number digits, followed by any text, +// followed by four number digits +// * "12/25/2019" - one or two number digits, a slash, one or two number +// digits, a slash, and two to four number digits +// +// The artist reference can optionally be boldface (in ), which will be +// captured as non-null in "boldfaceArtist". Otherwise it is all the characters +// between and and is captured in "artistReference" 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 = + /^(?)?(?.+):(?:<\/b>)?<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; + export function filterAlbumsByCommentary(albums) { return albums .filter((album) => [album, ...album.tracks].some((x) => x.commentary)); -- cgit 1.3.0-6-gf8a5 From f754a8d9187e435a761db31b5053aa2e7ba22e13 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 14 Nov 2023 23:36:37 -0400 Subject: data, test: boldfaceArtist -> artistDisplayText --- src/util/wiki-data.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src/util') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 5ab01225..75a141d3 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -629,9 +629,10 @@ export function sortFlashesChronologically(data, { // Specific data utilities -// Matches heading details from commentary data in roughly the format: +// Matches heading details from commentary data in roughly the formats: // // artistReference: (annotation, date) +// artistReference|artistDisplayText: (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 @@ -643,16 +644,15 @@ export function sortFlashesChronologically(data, { // * "12/25/2019" - one or two number digits, a slash, one or two number // digits, a slash, and two to four number digits // -// The artist reference can optionally be boldface (in ), which will be -// captured as non-null in "boldfaceArtist". Otherwise it is all the characters -// between and and is captured in "artistReference" and is either the -// name of an artist or an "artist:directory"-style reference. +// Capturing group "artistReference" is all the characters between and +// (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 = - /^(?)?(?.+):(?:<\/b>)?<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From a34b8d027866fbe858a4d2ff3543bc84c9d5983a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 17 Nov 2023 06:53:34 -0400 Subject: data, yaml, content: support multiple artists per commentary entry --- src/util/wiki-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/util') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 75a141d3..4c7ec043 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -652,7 +652,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From 4b7da4c1f8c359e5c82c4cc5e0cfb78f8204850f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:21:31 -0400 Subject: data: parse commentary heading contents to end of line --- src/util/wiki-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/util') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 4c7ec043..8a6d4345 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -652,7 +652,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?$/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From 522c982bf0b5a0bd39512eb56a9d0d8d8feea44e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:30:27 -0400 Subject: data: looser commentary date parsing + clearer regex explanation --- src/util/wiki-data.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src/util') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 8a6d4345..09b3623e 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -641,8 +641,10 @@ export function sortFlashesChronologically(data, { // // * "25 December 2019" - one or two number digits, followed by any text, // followed by four number digits -// * "12/25/2019" - one or two number digits, a slash, one or two number -// digits, a slash, and two to 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 (only valid formats are MM/DD/YYYY and YYYY/MM/DD) // // Capturing group "artistReference" is all the characters between and // (apart from the pipe and "artistDisplayText" text, if present), and is either @@ -652,7 +654,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?$/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[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 -- cgit 1.3.0-6-gf8a5 From e874f7e4d0df5fed25d4c359c8cb403e67061e59 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:33:38 -0400 Subject: data: support dash-style short dates in commentary dates --- src/util/wiki-data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/util') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 09b3623e..5e3182a9 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -644,7 +644,7 @@ export function sortFlashesChronologically(data, { // * "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 (only valid formats are MM/DD/YYYY and YYYY/MM/DD) +// by slashes or dashes (only valid orders are MM/DD/YYYY and YYYY/MM/DD) // // Capturing group "artistReference" is all the characters between and // (apart from the pipe and "artistDisplayText" text, if present), and is either @@ -654,7 +654,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[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; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[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 -- cgit 1.3.0-6-gf8a5 From 1de52e0937132eca0349460fdcc7b0383d2b3312 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 13:53:13 -0400 Subject: sugar: cut (string to length) --- src/util/sugar.js | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/util') diff --git a/src/util/sugar.js b/src/util/sugar.js index 9646be37..ad676914 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 -- cgit 1.3.0-6-gf8a5 From 87988954ad7314bee59932b0e5ef3474936ed33e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 13:59:13 -0400 Subject: data: update and revamp isCommentary validator --- src/util/wiki-data.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'src/util') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 5e3182a9..b5813c7a 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -636,8 +636,8 @@ export function sortFlashesChronologically(data, { // // 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 one of -// these formats: +// 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 @@ -646,6 +646,14 @@ export function sortFlashesChronologically(data, { // * "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 and // (apart from the pipe and "artistDisplayText" text, if present), and is either // the name of an artist or an "artist:directory"-style reference. @@ -654,7 +662,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[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; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)[^)]*$))*?)(?:,? ?(?[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 -- cgit 1.3.0-6-gf8a5 From f87fa920f91d36424e4613ac5da50f46418f4b19 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 14:31:58 -0400 Subject: data, util: principle "translucent errors" & applications --- src/util/sugar.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'src/util') diff --git a/src/util/sugar.js b/src/util/sugar.js index ad676914..6c41bca2 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -325,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 @@ -407,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; } }; -- cgit 1.3.0-6-gf8a5 From 08e18be60cf20ee6d69701c88676d3577c8f431e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 14:32:27 -0400 Subject: sugar: translucent errors implementation --- src/util/sugar.js | 122 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 27 deletions(-) (limited to 'src/util') diff --git a/src/util/sugar.js b/src/util/sugar.js index 6c41bca2..c4d4c129 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -592,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('') - ); + !line.includes('')); - 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)}`; } @@ -628,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') @@ -638,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); -- cgit 1.3.0-6-gf8a5 From 8b81a3aa4e266548ef2c8083391f6bb859915133 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 21 Nov 2023 07:22:40 -0400 Subject: sugar: fix async decorateError not providing calling arguments --- src/util/sugar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/util') diff --git a/src/util/sugar.js b/src/util/sugar.js index 9646be37..3f0eb2ea 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -685,7 +685,7 @@ export function asyncAdaptiveDecorateError(fn, callback) { try { return await fn(...args); } catch (caughtError) { - throw callback(caughtError); + throw callback(caughtError, ...args); } }; -- cgit 1.3.0-6-gf8a5 From 8f17782a5f2adbafd031b269195879eb7f79e05f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 11:16:48 -0400 Subject: data, content: extract external link parsing into nicer interface --- src/util/external-links.js | 308 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 src/util/external-links.js (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js new file mode 100644 index 00000000..8e1c3ca9 --- /dev/null +++ b/src/util/external-links.js @@ -0,0 +1,308 @@ +import {empty, stitchArrays} from '#sugar'; + +import { + is, + isStringNonEmpty, + optional, + validateArrayItems, + validateInstanceOf, + validateProperties, +} from '#validators'; + +export const externalLinkStyles = [ + 'normal', + 'compact', + 'icon-id', +]; + +export const isExternalLinkStyle = is(...externalLinkStyles); + +// This might need to be adjusted for YAML importing... +const isExternalLinkSpecRegex = + validateInstanceOf(RegExp); + +export const isExternalLinkHandleSpec = + validateProperties({ + prefix: optional(isStringNonEmpty), + + url: optional(isExternalLinkSpecRegex), + + // TODO: Don't allow specifying both of these (they're aliases) + domain: optional(isExternalLinkSpecRegex), + hostname: optional(isExternalLinkSpecRegex), + + // TODO: Don't allow specifying both of these (they're aliases) + path: optional(isExternalLinkSpecRegex), + pathname: optional(isExternalLinkSpecRegex), + }); + +export const isExternalLinkSpec = + validateArrayItems( + validateProperties({ + // TODO: Don't allow providing both of these, and require providing one + matchDomain: optional(isStringNonEmpty), + matchDomains: optional(validateArrayItems(isStringNonEmpty)), + + string: isStringNonEmpty, + + // TODO: Don't allow 'handle' options if handle isn't provided + normal: optional(is('domain', 'handle')), + compact: optional(is('domain', 'handle')), + icon: optional(isStringNonEmpty), + + handle: optional(isExternalLinkHandleSpec), + })); + +export const fallbackDescriptor = { + string: 'external', + + normal: 'domain', + compact: 'domain', + icon: 'globe', +}; + +// TODO: Define all this stuff in data as YAML! +export const externalLinkSpec = [ + { + matchDomain: 'bandcamp.com', + + string: 'bandcamp', + + compact: 'handle', + icon: 'bandcamp', + + handle: {domain: /^[^.]*/}, + }, + + { + matchDomains: ['bc.s3m.us', 'music.solatrux.com'], + + icon: 'bandcamp', + string: 'bandcamp', + + normal: 'domain', + compact: 'domain', + }, + + { + matchDomains: ['types.pl'], + + icon: 'mastodon', + string: 'mastodon', + + compact: 'domain', + }, + + { + matchDomains: ['youtube.com', 'youtu.be'], + + icon: 'youtube', + string: 'youtube', + + compact: 'handle', + + handle: { + pathname: /^(@.*?)\/?$/, + }, + }, + + { + matchDomain: 'soundcloud.com', + + icon: 'soundcloud', + string: 'soundcloud', + + compact: 'handle', + + handle: /[^/]*\/?$/, + }, + + { + matchDomain: 'tumblr.com', + + icon: 'tumblr', + string: 'tumblr', + + compact: 'handle', + + handle: {domain: /^[^.]*/}, + }, + + { + matchDomain: 'twitter.com', + + icon: 'twitter', + string: 'twitter', + + compact: 'handle', + + handle: { + prefix: '@', + pathname: /^@?.*\/?$/, + }, + }, + + { + matchDomain: 'deviantart.com', + + icon: 'deviantart', + string: 'deviantart', + }, + + { + matchDomain: 'instagram.com', + + icon: 'instagram', + string: 'instagram', + }, + + { + matchDomain: 'newgrounds.com', + + icon: 'newgrounds', + string: 'newgrounds', + }, +]; + +export function getMatchingDescriptorsForExternalLink(url, descriptors) { + const {hostname: domain} = new URL(url); + const compare = d => domain.includes(d); + + const matchingDescriptors = + descriptors.filter(spec => { + if (spec.matchDomain && compare(spec.matchDomain)) return true; + if (spec.matchDomains && spec.matchDomains.some(compare)) return true; + return false; + }); + + return [...matchingDescriptors, fallbackDescriptor]; +} + +export function getExternalLinkStringsFromDescriptor(url, descriptor, language) { + const prefix = 'misc.external'; + + const results = { + 'normal': null, + 'compact': null, + 'icon-id': null, + }; + + const {hostname: domain, pathname} = new URL(url); + + const place = language.$(prefix, descriptor.string); + + if (descriptor.icon) { + results['icon-id'] = descriptor.icon; + } + + if (descriptor.normal === 'domain') { + results['normal'] = language.$(prefix, 'withDomain', {place, domain}); + } + + if (descriptor.compact === 'domain') { + results['compact'] = language.sanitize(domain.replace(/^www\./, '')); + } + + let handle = null; + + if (descriptor.handle) { + let regexen = []; + let tests = []; + + let handlePrefix = ''; + + if (descriptor.handle instanceof RegExp) { + regexen.push(descriptor.handle); + tests.push(url); + } else { + for (const [key, value] of Object.entries(descriptor.handle)) { + switch (key) { + case 'prefix': + handlePrefix = value; + continue; + + case 'url': + tests.push(url); + break; + + case 'domain': + case 'hostname': + tests.push(domain); + break; + + case 'path': + case 'pathname': + tests.push(pathname.slice(1)); + break; + + default: + tests.push(''); + break; + } + + regexen.push(value); + } + } + + for (const {regex, test} of stitchArrays({ + regex: regexen, + test: tests, + })) { + const match = test.match(regex); + if (match) { + handle = handlePrefix + (match[1] ?? match[0]); + break; + } + } + } + + if (descriptor.compact === 'handle') { + results.compact = language.sanitize(handle); + } + + if (descriptor.normal === 'handle' && handle) { + results.normal = language.$(prefix, 'withHandle', {place, handle}); + } + + results.normal ??= language.$(prefix, descriptor.string); + + return results; +} + +export function getExternalLinkStringsFromDescriptors(url, descriptors, language) { + const results = { + 'normal': null, + 'compact': null, + 'icon-id': null, + }; + + const remainingKeys = + new Set(Object.keys(results)); + + const matchingDescriptors = + getMatchingDescriptorsForExternalLink(url, descriptors); + + for (const descriptor of matchingDescriptors) { + const descriptorResults = + getExternalLinkStringsFromDescriptor(url, descriptor, language); + + const descriptorKeys = + new Set( + Object.entries(descriptorResults) + .filter(entry => entry[1]) + .map(entry => entry[0])); + + for (const key of remainingKeys) { + if (descriptorKeys.has(key)) { + results[key] = descriptorResults[key]; + remainingKeys.delete(key); + } + } + + if (empty(remainingKeys)) { + return results; + } + } + + return results; +} -- cgit 1.3.0-6-gf8a5 From c5e02f9d314118a534fd0e942d87e74864674498 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 17:42:49 -0400 Subject: content: *mostly* port linkExternal to language.formatExternalLink --- src/util/external-links.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 8e1c3ca9..2047a720 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -12,6 +12,7 @@ import { export const externalLinkStyles = [ 'normal', 'compact', + 'platform', 'icon-id', ]; @@ -181,16 +182,15 @@ export function getMatchingDescriptorsForExternalLink(url, descriptors) { export function getExternalLinkStringsFromDescriptor(url, descriptor, language) { const prefix = 'misc.external'; - const results = { - 'normal': null, - 'compact': null, - 'icon-id': null, - }; + const results = + Object.fromEntries(externalLinkStyles.map(style => [style, null])); const {hostname: domain, pathname} = new URL(url); const place = language.$(prefix, descriptor.string); + results['platform'] = place; + if (descriptor.icon) { results['icon-id'] = descriptor.icon; } @@ -270,11 +270,8 @@ export function getExternalLinkStringsFromDescriptor(url, descriptor, language) } export function getExternalLinkStringsFromDescriptors(url, descriptors, language) { - const results = { - 'normal': null, - 'compact': null, - 'icon-id': null, - }; + const results = + Object.fromEntries(externalLinkStyles.map(style => [style, null])); const remainingKeys = new Set(Object.keys(results)); -- cgit 1.3.0-6-gf8a5 From 0ee5269cd196cd14f06aac6c586e7104159eac74 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 17:47:18 -0400 Subject: content: implement "local" links much more rudimentarily --- src/util/external-links.js | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 2047a720..7a34fa9e 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -64,6 +64,14 @@ export const fallbackDescriptor = { // TODO: Define all this stuff in data as YAML! export const externalLinkSpec = [ + { + matchDomain: 'hsmusic.wiki', + + string: 'local', + + icon: 'globe', + }, + { matchDomain: 'bandcamp.com', -- cgit 1.3.0-6-gf8a5 From cf08893d48db6f8082a176f54d0d92cb82716b3a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 18:50:59 -0400 Subject: external-links: general support for page-contextual formatting --- src/util/external-links.js | 263 +++++++++++++++++++++++++++++++++------------ 1 file changed, 196 insertions(+), 67 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 7a34fa9e..07f46bd3 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -18,31 +18,48 @@ export const externalLinkStyles = [ export const isExternalLinkStyle = is(...externalLinkStyles); +export const externalLinkContexts = [ + 'album', + 'artist', + 'flash', + 'generic', + 'group', + 'track', +]; + +export const isExternalLinkContext = is(...externalLinkContexts); + // This might need to be adjusted for YAML importing... -const isExternalLinkSpecRegex = +const isRegExp = validateInstanceOf(RegExp); export const isExternalLinkHandleSpec = validateProperties({ prefix: optional(isStringNonEmpty), - url: optional(isExternalLinkSpecRegex), - - // TODO: Don't allow specifying both of these (they're aliases) - domain: optional(isExternalLinkSpecRegex), - hostname: optional(isExternalLinkSpecRegex), - - // TODO: Don't allow specifying both of these (they're aliases) - path: optional(isExternalLinkSpecRegex), - pathname: optional(isExternalLinkSpecRegex), + url: optional(isRegExp), + domain: optional(isRegExp), + pathname: optional(isRegExp), }); export const isExternalLinkSpec = validateArrayItems( validateProperties({ - // TODO: Don't allow providing both of these, and require providing one - matchDomain: optional(isStringNonEmpty), - matchDomains: optional(validateArrayItems(isStringNonEmpty)), + match: validateProperties({ + // TODO: Don't allow providing both of these, and require providing one + domain: optional(isStringNonEmpty), + domains: optional(validateArrayItems(isStringNonEmpty)), + + // TODO: Don't allow providing both of these + pathname: optional(isRegExp), + pathnames: optional(validateArrayItems(isRegExp)), + + // TODO: Don't allow providing both of these + query: optional(isRegExp), + queries: optional(validateArrayItems(isRegExp)), + + context: optional(isExternalLinkContext), + }), string: isStringNonEmpty, @@ -64,27 +81,84 @@ export const fallbackDescriptor = { // TODO: Define all this stuff in data as YAML! export const externalLinkSpec = [ + // Special handling for album links + { - matchDomain: 'hsmusic.wiki', + match: { + context: 'album', + domain: 'youtube.com', + pathname: /^playlist/, + }, - string: 'local', + string: 'youtube.playlist', + icon: 'youtube', + }, - icon: 'globe', + { + match: { + context: 'album', + domain: 'youtube.com', + pathname: /^watch/, + }, + + string: 'youtube.fullAlbum', + icon: 'youtube', }, { - matchDomain: 'bandcamp.com', + match: { + context: 'album', + domain: 'youtu.be', + }, - string: 'bandcamp', + string: 'youtube.fullAlbum', + icon: 'youtube', + }, + + // Special handling for artist links + + { + match: { + context: 'artist', + domains: ['youtube.com', 'youtu.be'], + }, + + string: 'youtube', + icon: 'youtube', compact: 'handle', - icon: 'bandcamp', - handle: {domain: /^[^.]*/}, + handle: { + pathname: /^(@.*?)\/?$/, + }, + }, + + // Special handling for flash links + + { + match: { + context: 'flash', + domain: 'bgreco.net', + }, + + string: 'bgreco.flash', + icon: 'external', + }, + + { + match: { + context: 'flash', + domains: ['youtube.com', 'youtu.be'], + }, + + string: 'youtube.flash', + icon: 'youtube', }, + // Generic domains, sorted alphabetically (by string) + { - matchDomains: ['bc.s3m.us', 'music.solatrux.com'], + match: {domains: ['bc.s3m.us', 'music.solatrux.com']}, icon: 'bandcamp', string: 'bandcamp', @@ -94,7 +168,47 @@ export const externalLinkSpec = [ }, { - matchDomains: ['types.pl'], + match: {domain: 'bandcamp.com'}, + + string: 'bandcamp', + + compact: 'handle', + icon: 'bandcamp', + + handle: {domain: /^[^.]*/}, + }, + + { + match: {domain: 'deviantart.com'}, + + string: 'deviantart', + icon: 'deviantart', + }, + + { + match: {domain: 'instagram.com'}, + + string: 'instagram', + icon: 'instagram', + }, + + { + match: {domain: 'homestuck.com'}, + + string: 'homestuck', + icon: 'globe', // The horror! + }, + + { + match: {domain: 'hsmusic.wiki'}, + + string: 'local', + + icon: 'globe', + }, + + { + match: {domains: ['types.pl']}, icon: 'mastodon', string: 'mastodon', @@ -103,23 +217,17 @@ export const externalLinkSpec = [ }, { - matchDomains: ['youtube.com', 'youtu.be'], - - icon: 'youtube', - string: 'youtube', - - compact: 'handle', + match: {domain: 'newgrounds.com'}, - handle: { - pathname: /^(@.*?)\/?$/, - }, + string: 'newgrounds', + icon: 'newgrounds', }, { - matchDomain: 'soundcloud.com', + match: {domain: 'soundcloud.com'}, - icon: 'soundcloud', string: 'soundcloud', + icon: 'soundcloud', compact: 'handle', @@ -127,10 +235,10 @@ export const externalLinkSpec = [ }, { - matchDomain: 'tumblr.com', + match: {domain: 'tumblr.com'}, - icon: 'tumblr', string: 'tumblr', + icon: 'tumblr', compact: 'handle', @@ -138,62 +246,80 @@ export const externalLinkSpec = [ }, { - matchDomain: 'twitter.com', + match: {domain: 'twitter.com'}, - icon: 'twitter', string: 'twitter', + icon: 'twitter', compact: 'handle', handle: { prefix: '@', - pathname: /^@?.*\/?$/, + pathname: /^@?([a-zA-Z0-9_]*)\/?$/, }, }, { - matchDomain: 'deviantart.com', + match: {domains: ['youtube.com', 'youtu.be']}, - icon: 'deviantart', - string: 'deviantart', + string: 'youtube', + icon: 'youtube', }, +]; - { - matchDomain: 'instagram.com', - - icon: 'instagram', - string: 'instagram', - }, +function urlParts(url) { + const { + hostname: domain, + pathname, + search: query, + } = new URL(url); - { - matchDomain: 'newgrounds.com', + return {domain, pathname, query}; +} - icon: 'newgrounds', - string: 'newgrounds', - }, -]; +export function getMatchingDescriptorsForExternalLink(url, descriptors, { + context = 'generic', +} = {}) { + const {domain, pathname, query} = urlParts(url); -export function getMatchingDescriptorsForExternalLink(url, descriptors) { - const {hostname: domain} = new URL(url); - const compare = d => domain.includes(d); + const compareDomain = string => domain.includes(string); + const comparePathname = regex => regex.test(pathname.slice(1)); + const compareQuery = regex => regex.test(query.slice(1)); const matchingDescriptors = - descriptors.filter(spec => { - if (spec.matchDomain && compare(spec.matchDomain)) return true; - if (spec.matchDomains && spec.matchDomains.some(compare)) return true; - return false; - }); + descriptors + .filter(({match}) => { + if (match.domain) return compareDomain(match.domain); + if (match.domains) return match.domains.some(compareDomain); + return false; + }) + .filter(({match}) => { + if (match.context) return context === match.context; + return true; + }) + .filter(({match}) => { + if (match.pathname) return comparePathname(match.pathname); + if (match.pathnames) return match.pathnames.some(comparePathname); + return true; + }) + .filter(({match}) => { + if (match.query) return compareQuery(match.query); + if (match.queries) return match.quieries.some(compareQuery); + return true; + }); return [...matchingDescriptors, fallbackDescriptor]; } -export function getExternalLinkStringsFromDescriptor(url, descriptor, language) { +export function getExternalLinkStringsFromDescriptor(url, descriptor, { + language, +}) { const prefix = 'misc.external'; const results = Object.fromEntries(externalLinkStyles.map(style => [style, null])); - const {hostname: domain, pathname} = new URL(url); + const {domain, pathname, query} = urlParts(url); const place = language.$(prefix, descriptor.string); @@ -240,7 +366,7 @@ export function getExternalLinkStringsFromDescriptor(url, descriptor, language) case 'path': case 'pathname': - tests.push(pathname.slice(1)); + tests.push(pathname.slice(1) + query); break; default: @@ -277,7 +403,10 @@ export function getExternalLinkStringsFromDescriptor(url, descriptor, language) return results; } -export function getExternalLinkStringsFromDescriptors(url, descriptors, language) { +export function getExternalLinkStringsFromDescriptors(url, descriptors, { + language, + context = 'generic', +}) { const results = Object.fromEntries(externalLinkStyles.map(style => [style, null])); @@ -285,11 +414,11 @@ export function getExternalLinkStringsFromDescriptors(url, descriptors, language new Set(Object.keys(results)); const matchingDescriptors = - getMatchingDescriptorsForExternalLink(url, descriptors); + getMatchingDescriptorsForExternalLink(url, descriptors, {context}); for (const descriptor of matchingDescriptors) { const descriptorResults = - getExternalLinkStringsFromDescriptor(url, descriptor, language); + getExternalLinkStringsFromDescriptor(url, descriptor, {language}); const descriptorKeys = new Set( -- cgit 1.3.0-6-gf8a5 From ba6c4e043b3364481ac3beff1e2a141d1bfcf6fb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 20:47:34 -0400 Subject: external-links: cleaner per-style logic --- src/util/external-links.js | 340 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 266 insertions(+), 74 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 07f46bd3..a0301c9c 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -2,6 +2,7 @@ import {empty, stitchArrays} from '#sugar'; import { is, + isObject, isStringNonEmpty, optional, validateArrayItems, @@ -33,13 +34,14 @@ export const isExternalLinkContext = is(...externalLinkContexts); const isRegExp = validateInstanceOf(RegExp); -export const isExternalLinkHandleSpec = +export const isExternalLinkExtractSpec = validateProperties({ prefix: optional(isStringNonEmpty), url: optional(isRegExp), domain: optional(isRegExp), pathname: optional(isRegExp), + query: optional(isRegExp), }); export const isExternalLinkSpec = @@ -63,12 +65,16 @@ export const isExternalLinkSpec = string: isStringNonEmpty, - // TODO: Don't allow 'handle' options if handle isn't provided - normal: optional(is('domain', 'handle')), - compact: optional(is('domain', 'handle')), + // TODO: Don't allow 'handle' or 'custom' options if the corresponding + // properties aren't provided + normal: optional(is('domain', 'handle', 'custom')), + compact: optional(is('domain', 'handle', 'custom')), icon: optional(isStringNonEmpty), - handle: optional(isExternalLinkHandleSpec), + handle: optional(isExternalLinkExtractSpec), + + // TODO: This should validate each value with isExternalLinkExtractSpec. + custom: optional(isObject), })); export const fallbackDescriptor = { @@ -145,6 +151,38 @@ export const externalLinkSpec = [ icon: 'external', }, + // This takes precedence over the secretPage match below. + { + match: { + context: 'flash', + domain: 'homestuck.com', + pathname: /^story\/[0-9]+\/?$/, + }, + + platform: 'homestuck', + string: 'homestuck.page', + icon: 'globe', + + normal: 'custom', + + custom: { + page: { + pathname: /[0-9]+/, + }, + }, + }, + + { + match: { + context: 'flash', + domain: 'homestuck.com', + pathname: /^story\/.+\/?$/, + }, + + string: 'homestuck.secretPage', + icon: 'globe', + }, + { match: { context: 'flash', @@ -277,6 +315,10 @@ function urlParts(url) { return {domain, pathname, query}; } +function createEmptyResults() { + return Object.fromEntries(externalLinkStyles.map(style => [style, null])); +} + export function getMatchingDescriptorsForExternalLink(url, descriptors, { context = 'generic', } = {}) { @@ -311,107 +353,257 @@ export function getMatchingDescriptorsForExternalLink(url, descriptors, { return [...matchingDescriptors, fallbackDescriptor]; } -export function getExternalLinkStringsFromDescriptor(url, descriptor, { - language, -}) { - const prefix = 'misc.external'; +export function extractPartFromExternalLink(url, extract) { + const {domain, pathname, query} = urlParts(url); - const results = - Object.fromEntries(externalLinkStyles.map(style => [style, null])); + let regexen = []; + let tests = []; + let prefix = ''; + + if (extract instanceof RegExp) { + regexen.push(descriptor.handle); + tests.push(url); + } else { + for (const [key, value] of Object.entries(extract)) { + switch (key) { + case 'prefix': + prefix = value; + continue; + + case 'url': + tests.push(url); + break; + + case 'domain': + tests.push(domain); + break; + + case 'pathname': + tests.push(pathname.slice(1)); + break; + + case 'query': + tests.push(query.slice(1)); + + default: + tests.push(''); + break; + } - const {domain, pathname, query} = urlParts(url); + regexen.push(value); + } + } - const place = language.$(prefix, descriptor.string); + for (const {regex, test} of stitchArrays({ + regex: regexen, + test: tests, + })) { + const match = test.match(regex); + if (match) { + return prefix + (match[1] ?? match[0]); + } + } - results['platform'] = place; + return null; +} - if (descriptor.icon) { - results['icon-id'] = descriptor.icon; +export function extractAllCustomPartsFromExternalLink(url, custom) { + const customParts = {}; + + // All or nothing: if one part doesn't match, all results are scrapped. + for (const [key, value] of Object.entries(custom)) { + customParts[key] = extractPartFromExternalLink(url, value); + if (!customParts[key]) return null; } - if (descriptor.normal === 'domain') { - results['normal'] = language.$(prefix, 'withDomain', {place, domain}); + return customParts; +} + +const prefix = 'misc.external'; + +export function getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}) { + function getPlatform() { + if (descriptor.custom) { + return null; + } + + return language.$(prefix, descriptor.string); } - if (descriptor.compact === 'domain') { - results['compact'] = language.sanitize(domain.replace(/^www\./, '')); + function getDomain() { + return urlParts(url).domain; } - let handle = null; + function getCustom() { + if (!descriptor.custom) { + return null; + } - if (descriptor.handle) { - let regexen = []; - let tests = []; + const customParts = + extractAllCustomPartsFromExternalLink(url, descriptor.custom); - let handlePrefix = ''; + if (!customParts) { + return null; + } - if (descriptor.handle instanceof RegExp) { - regexen.push(descriptor.handle); - tests.push(url); - } else { - for (const [key, value] of Object.entries(descriptor.handle)) { - switch (key) { - case 'prefix': - handlePrefix = value; - continue; - - case 'url': - tests.push(url); - break; - - case 'domain': - case 'hostname': - tests.push(domain); - break; - - case 'path': - case 'pathname': - tests.push(pathname.slice(1) + query); - break; - - default: - tests.push(''); - break; - } - - regexen.push(value); + return language.$(prefix, descriptor.string, customParts); + } + + function getHandle() { + if (!descriptor.handle) { + return null; + } + + return extractPartFromExternalLink(url, descriptor.handle); + } + + function getNormal() { + if (descriptor.custom) { + if (descriptor.normal === 'custom') { + return getCustom(); + } else { + return null; + } + } + + if (descriptor.normal === 'domain') { + const platform = getPlatform(); + const domain = getDomain(); + + if (!platform || !domain) { + return null; } + + return language.$(prefix, 'withDomain', {platform, domain}); } - for (const {regex, test} of stitchArrays({ - regex: regexen, - test: tests, - })) { - const match = test.match(regex); - if (match) { - handle = handlePrefix + (match[1] ?? match[0]); - break; + if (descriptor.normal === 'handle') { + const platform = getPlatform(); + const handle = getHandle(); + + if (!platform || !handle) { + return null; } + + return language.$(prefix, 'withHandle', {platform, handle}); } + + return language.$(prefix, descriptor.string); } - if (descriptor.compact === 'handle') { - results.compact = language.sanitize(handle); + function getCompact() { + if (descriptor.custom) { + if (descriptor.compact === 'custom') { + return getCustom(); + } else { + return null; + } + } + + if (descriptor.compact === 'domain') { + const domain = getDomain(); + + if (!domain) { + return null; + } + + return language.sanitize(domain.replace(/^www\./, '')); + } + + if (descriptor.compact === 'handle') { + const handle = getHandle(); + + if (!handle) { + return null; + } + + return language.sanitize(handle); + } + } + + function getIconId() { + return descriptor.icon ?? null; } - if (descriptor.normal === 'handle' && handle) { - results.normal = language.$(prefix, 'withHandle', {place, handle}); + switch (style) { + case 'normal': return getNormal(); + case 'compact': return getCompact(); + case 'platform': return getPlatform(); + case 'icon-id': return getIconId(); } +} - results.normal ??= language.$(prefix, descriptor.string); +export function couldDescriptorSupportStyle(descriptor, style) { + if (style === 'platform') { + return !descriptor.custom; + } - return results; + if (style === 'icon-id') { + return !!descriptor.icon; + } + + if (style === 'normal') { + if (descriptor.custom) { + return descriptor.normal === 'custom'; + } else { + return true; + } + } + + if (style === 'compact') { + if (descriptor.custom) { + return descriptor.compact === 'custom'; + } else { + return !!descriptor.compact; + } + } } -export function getExternalLinkStringsFromDescriptors(url, descriptors, { +export function getExternalLinkStringOfStyleFromDescriptors(url, style, descriptors, { language, context = 'generic', }) { - const results = - Object.fromEntries(externalLinkStyles.map(style => [style, null])); + const matchingDescriptors = + getMatchingDescriptorsForExternalLink(url, descriptors, {context}); + + console.log('match-filtered:', matchingDescriptors); + + const styleFilteredDescriptors = + matchingDescriptors.filter(descriptor => + couldDescriptorSupportStyle(descriptor, style)); + + console.log('style-filtered:', styleFilteredDescriptors); + + for (const descriptor of styleFilteredDescriptors) { + const descriptorResult = + getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}); - const remainingKeys = - new Set(Object.keys(results)); + if (descriptorResult) { + return descriptorResult; + } + } + + return null; +} + +export function getExternalLinkStringsFromDescriptor(url, descriptor, {language}) { + const getStyle = style => + getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}); + + return { + 'normal': getStyle('normal'), + 'compact': getStyle('compact'), + 'platform': getStyle('platform'), + 'icon-id': getStyle('icon-id'), + }; +} + +export function getExternalLinkStringsFromDescriptors(url, descriptors, { + language, + context = 'generic', +}) { + const results = createEmptyResults(); + const remainingKeys = new Set(Object.keys(results)); const matchingDescriptors = getMatchingDescriptorsForExternalLink(url, descriptors, {context}); -- cgit 1.3.0-6-gf8a5 From 841daeb4a29657485488ac55a743492b010658de Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 21:03:17 -0400 Subject: external-links: spec in terms of platform + substring --- src/util/external-links.js | 115 +++++++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 52 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index a0301c9c..c8cb1670 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -63,7 +63,8 @@ export const isExternalLinkSpec = context: optional(isExternalLinkContext), }), - string: isStringNonEmpty, + platform: isStringNonEmpty, + substring: optional(isStringNonEmpty), // TODO: Don't allow 'handle' or 'custom' options if the corresponding // properties aren't provided @@ -78,7 +79,7 @@ export const isExternalLinkSpec = })); export const fallbackDescriptor = { - string: 'external', + platform: 'external', normal: 'domain', compact: 'domain', @@ -96,7 +97,9 @@ export const externalLinkSpec = [ pathname: /^playlist/, }, - string: 'youtube.playlist', + platform: 'youtube', + substring: 'playlist', + icon: 'youtube', }, @@ -107,7 +110,9 @@ export const externalLinkSpec = [ pathname: /^watch/, }, - string: 'youtube.fullAlbum', + platform: 'youtube', + substring: 'fullAlbum', + icon: 'youtube', }, @@ -117,7 +122,9 @@ export const externalLinkSpec = [ domain: 'youtu.be', }, - string: 'youtube.fullAlbum', + platform: 'youtube', + substring: 'fullAlbum', + icon: 'youtube', }, @@ -129,10 +136,11 @@ export const externalLinkSpec = [ domains: ['youtube.com', 'youtu.be'], }, - string: 'youtube', - icon: 'youtube', + platform: 'youtube', + normal: 'handle', compact: 'handle', + icon: 'youtube', handle: { pathname: /^(@.*?)\/?$/, @@ -147,7 +155,9 @@ export const externalLinkSpec = [ domain: 'bgreco.net', }, - string: 'bgreco.flash', + platform: 'bgreco', + substring: 'flash', + icon: 'external', }, @@ -160,10 +170,10 @@ export const externalLinkSpec = [ }, platform: 'homestuck', - string: 'homestuck.page', - icon: 'globe', + substring: 'page', normal: 'custom', + icon: 'globe', custom: { page: { @@ -179,7 +189,9 @@ export const externalLinkSpec = [ pathname: /^story\/.+\/?$/, }, - string: 'homestuck.secretPage', + platform: 'homestuck', + substring: 'secretPage', + icon: 'globe', }, @@ -189,7 +201,9 @@ export const externalLinkSpec = [ domains: ['youtube.com', 'youtu.be'], }, - string: 'youtube.flash', + platform: 'youtube', + substring: 'flash', + icon: 'youtube', }, @@ -198,17 +212,17 @@ export const externalLinkSpec = [ { match: {domains: ['bc.s3m.us', 'music.solatrux.com']}, - icon: 'bandcamp', - string: 'bandcamp', + platform: 'bandcamp', normal: 'domain', compact: 'domain', + icon: 'bandcamp', }, { match: {domain: 'bandcamp.com'}, - string: 'bandcamp', + platform: 'bandcamp', compact: 'handle', icon: 'bandcamp', @@ -219,28 +233,31 @@ export const externalLinkSpec = [ { match: {domain: 'deviantart.com'}, - string: 'deviantart', + platform: 'deviantart', + icon: 'deviantart', }, { match: {domain: 'instagram.com'}, - string: 'instagram', + platform: 'instagram', + icon: 'instagram', }, { match: {domain: 'homestuck.com'}, - string: 'homestuck', - icon: 'globe', // The horror! + platform: 'homestuck', + + icon: 'globe', }, { match: {domain: 'hsmusic.wiki'}, - string: 'local', + platform: 'local', icon: 'globe', }, @@ -248,37 +265,38 @@ export const externalLinkSpec = [ { match: {domains: ['types.pl']}, - icon: 'mastodon', - string: 'mastodon', + platform: 'mastodon', compact: 'domain', + icon: 'mastodon', }, { match: {domain: 'newgrounds.com'}, - string: 'newgrounds', + platform: 'newgrounds', + icon: 'newgrounds', }, { match: {domain: 'soundcloud.com'}, - string: 'soundcloud', - icon: 'soundcloud', + platform: 'soundcloud', compact: 'handle', + icon: 'soundcloud', - handle: /[^/]*\/?$/, + handle: /([^/]*)\/?$/, }, { match: {domain: 'tumblr.com'}, - string: 'tumblr', - icon: 'tumblr', + platform: 'tumblr', compact: 'handle', + icon: 'tumblr', handle: {domain: /^[^.]*/}, }, @@ -286,10 +304,10 @@ export const externalLinkSpec = [ { match: {domain: 'twitter.com'}, - string: 'twitter', - icon: 'twitter', + platform: 'twitter', compact: 'handle', + icon: 'twitter', handle: { prefix: '@', @@ -300,7 +318,8 @@ export const externalLinkSpec = [ { match: {domains: ['youtube.com', 'youtu.be']}, - string: 'youtube', + platform: 'youtube', + icon: 'youtube', }, ]; @@ -419,15 +438,11 @@ export function extractAllCustomPartsFromExternalLink(url, custom) { return customParts; } -const prefix = 'misc.external'; - export function getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}) { - function getPlatform() { - if (descriptor.custom) { - return null; - } + const prefix = 'misc.external'; - return language.$(prefix, descriptor.string); + function getPlatform() { + return language.$(prefix, descriptor.platform); } function getDomain() { @@ -446,7 +461,7 @@ export function getExternalLinkStringOfStyleFromDescriptor(url, style, descripto return null; } - return language.$(prefix, descriptor.string, customParts); + return language.$(prefix, descriptor.platform, descriptor.substring, customParts); } function getHandle() { @@ -488,7 +503,7 @@ export function getExternalLinkStringOfStyleFromDescriptor(url, style, descripto return language.$(prefix, 'withHandle', {platform, handle}); } - return language.$(prefix, descriptor.string); + return language.$(prefix, descriptor.platform, descriptor.substring); } function getCompact() { @@ -534,14 +549,6 @@ export function getExternalLinkStringOfStyleFromDescriptor(url, style, descripto } export function couldDescriptorSupportStyle(descriptor, style) { - if (style === 'platform') { - return !descriptor.custom; - } - - if (style === 'icon-id') { - return !!descriptor.icon; - } - if (style === 'normal') { if (descriptor.custom) { return descriptor.normal === 'custom'; @@ -557,6 +564,14 @@ export function couldDescriptorSupportStyle(descriptor, style) { return !!descriptor.compact; } } + + if (style === 'platform') { + return true; + } + + if (style === 'icon-id') { + return !!descriptor.icon; + } } export function getExternalLinkStringOfStyleFromDescriptors(url, style, descriptors, { @@ -566,14 +581,10 @@ export function getExternalLinkStringOfStyleFromDescriptors(url, style, descript const matchingDescriptors = getMatchingDescriptorsForExternalLink(url, descriptors, {context}); - console.log('match-filtered:', matchingDescriptors); - const styleFilteredDescriptors = matchingDescriptors.filter(descriptor => couldDescriptorSupportStyle(descriptor, style)); - console.log('style-filtered:', styleFilteredDescriptors); - for (const descriptor of styleFilteredDescriptors) { const descriptorResult = getExternalLinkStringOfStyleFromDescriptor(url, style, descriptor, {language}); -- cgit 1.3.0-6-gf8a5 From db786c25a9fafc4cac37b108b4ea433019741c07 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 23 Nov 2023 21:33:15 -0400 Subject: content, external-links: minor fixes --- src/util/external-links.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index c8cb1670..07a83bc1 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -133,7 +133,7 @@ export const externalLinkSpec = [ { match: { context: 'artist', - domains: ['youtube.com', 'youtu.be'], + domain: 'youtube.com', }, platform: 'youtube', @@ -158,7 +158,7 @@ export const externalLinkSpec = [ platform: 'bgreco', substring: 'flash', - icon: 'external', + icon: 'globe', }, // This takes precedence over the secretPage match below. -- cgit 1.3.0-6-gf8a5 From 803a17296249e1521089451c9d077cc524b4acf5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 24 Nov 2023 13:43:34 -0400 Subject: external-links: minor code fixes --- src/util/external-links.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index 07a83bc1..dee65cc5 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -380,7 +380,7 @@ export function extractPartFromExternalLink(url, extract) { let prefix = ''; if (extract instanceof RegExp) { - regexen.push(descriptor.handle); + regexen.push(extract); tests.push(url); } else { for (const [key, value] of Object.entries(extract)) { @@ -403,6 +403,7 @@ export function extractPartFromExternalLink(url, extract) { case 'query': tests.push(query.slice(1)); + break; default: tests.push(''); -- cgit 1.3.0-6-gf8a5 From ad1ae12ab182dd50cf3ca6ec653d371d77b5fabb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 24 Nov 2023 14:14:27 -0400 Subject: external-links: quick spec tweaks --- src/util/external-links.js | 77 +++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 22 deletions(-) (limited to 'src/util') diff --git a/src/util/external-links.js b/src/util/external-links.js index dee65cc5..0a4a77cf 100644 --- a/src/util/external-links.js +++ b/src/util/external-links.js @@ -4,6 +4,7 @@ import { is, isObject, isStringNonEmpty, + oneOf, optional, validateArrayItems, validateInstanceOf, @@ -60,7 +61,10 @@ export const isExternalLinkSpec = query: optional(isRegExp), queries: optional(validateArrayItems(isRegExp)), - context: optional(isExternalLinkContext), + context: + optional(oneOf( + isExternalLinkContext, + validateArrayItems(isExternalLinkContext))), }), platform: isStringNonEmpty, @@ -130,6 +134,21 @@ export const externalLinkSpec = [ // Special handling for artist links + { + match: { + domain: 'patreon.com', + context: 'artist', + }, + + platform: 'patreon', + + normal: 'handle', + compact: 'handle', + icon: 'globe', + + handle: /([^/]*)\/?$/, + }, + { match: { context: 'artist', @@ -210,7 +229,7 @@ export const externalLinkSpec = [ // Generic domains, sorted alphabetically (by string) { - match: {domains: ['bc.s3m.us', 'music.solatrux.com']}, + match: {domains: ['bc.s3m.us', 'music.solatrus.com']}, platform: 'bandcamp', @@ -220,7 +239,7 @@ export const externalLinkSpec = [ }, { - match: {domain: 'bandcamp.com'}, + match: {domain: '.bandcamp.com'}, platform: 'bandcamp', @@ -232,53 +251,56 @@ export const externalLinkSpec = [ { match: {domain: 'deviantart.com'}, - platform: 'deviantart', - icon: 'deviantart', }, - { - match: {domain: 'instagram.com'}, - - platform: 'instagram', - - icon: 'instagram', - }, - { match: {domain: 'homestuck.com'}, - platform: 'homestuck', - icon: 'globe', }, { match: {domain: 'hsmusic.wiki'}, - platform: 'local', - icon: 'globe', }, + { + match: {domain: 'instagram.com'}, + platform: 'instagram', + icon: 'instagram', + }, + { match: {domains: ['types.pl']}, platform: 'mastodon', + normal: 'domain', compact: 'domain', icon: 'mastodon', }, { match: {domain: 'newgrounds.com'}, - platform: 'newgrounds', - icon: 'newgrounds', }, + { + match: {domain: 'patreon.com'}, + platform: 'patreon', + icon: 'globe', + }, + + { + match: {domain: 'poetryfoundation.org'}, + platform: 'poetryFoundation', + icon: 'globe', + }, + { match: {domain: 'soundcloud.com'}, @@ -291,7 +313,13 @@ export const externalLinkSpec = [ }, { - match: {domain: 'tumblr.com'}, + match: {domain: 'spotify.com'}, + platform: 'spotify', + icon: 'globe', + }, + + { + match: {domain: '.tumblr.com'}, platform: 'tumblr', @@ -316,10 +344,14 @@ export const externalLinkSpec = [ }, { - match: {domains: ['youtube.com', 'youtu.be']}, + match: {domain: 'wikipedia.org'}, + platform: 'wikipedia', + icon: 'misc', + }, + { + match: {domains: ['youtube.com', 'youtu.be']}, platform: 'youtube', - icon: 'youtube', }, ]; @@ -355,6 +387,7 @@ export function getMatchingDescriptorsForExternalLink(url, descriptors, { return false; }) .filter(({match}) => { + if (Array.isArray(match.context)) return match.context.includes(context); if (match.context) return context === match.context; return true; }) -- cgit 1.3.0-6-gf8a5 From e3358d7c6993aa454019ebf58a53198b9e28087f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 28 Nov 2023 12:30:42 -0400 Subject: sugar: extract "getUsefulStackLine" logic & internals --- src/util/sugar.js | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) (limited to 'src/util') diff --git a/src/util/sugar.js b/src/util/sugar.js index eab44b75..cee3df12 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -589,6 +589,32 @@ export function _withAggregate(mode, aggregateOpts, fn) { } } +export const unhelpfulStackLines = [ + /sugar/, + /node:/, + //, +]; + +export function getUsefulStackLine(stack) { + if (!stack) return ''; + + function isUseful(stackLine) { + const trimmed = stackLine.trim(); + + if (!trimmed.startsWith('at')) + return false; + + if (unhelpfulStackLines.some(regex => regex.test(trimmed))) + return false; + + return true; + } + + const stackLines = stack.split('\n'); + const usefulStackLine = stackLines.find(isUseful); + return usefulStackLine ?? ''; +} + export function showAggregate(topError, { pathToFileURL = f => f, showTraces = true, @@ -670,15 +696,8 @@ export function showAggregate(topError, { : messagePart); if (showTraces) { - const stackLines = - stack?.split('\n'); - const stackLine = - stackLines?.find(line => - line.trim().startsWith('at') && - !line.includes('sugar') && - !line.includes('node:') && - !line.includes('')); + getUsefulStackLine(stack); const tracePart = (stackLine -- cgit 1.3.0-6-gf8a5