From b053fa14052aaa24883e73a3f899016f963b5d43 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 14 Nov 2023 21:45:08 -0400 Subject: data: expose CacheableObject directly via #cacheable-object import --- package.json | 1 + src/data/things/index.js | 5 ----- src/data/yaml.js | 8 +++----- src/find.js | 2 +- src/gen-thumbs.js | 2 +- src/repl.js | 3 ++- src/upd8.js | 2 +- test/unit/data/cacheable-object.js | 2 +- 8 files changed, 10 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 194c406..34b4ebf 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dev": "eslint src && node src/upd8.js" }, "imports": { + "#cacheable-object": "./src/data/things/cacheable-object.js", "#colors": "./src/util/colors.js", "#composite": "./src/data/things/composite.js", "#composite/control-flow": "./src/data/composite/control-flow/index.js", diff --git a/src/data/things/index.js b/src/data/things/index.js index 4ea1f00..d1143b0 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -22,11 +22,6 @@ import * as wikiInfoClasses from './wiki-info.js'; export {default as Thing} from './thing.js'; -export { - default as CacheableObject, - CacheableObjectPropertyValueError, -} from './cacheable-object.js'; - const allClassLists = { 'album.js': albumClasses, 'art-tag.js': artTagClasses, diff --git a/src/data/yaml.js b/src/data/yaml.js index 1d35bae..986f25d 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -7,15 +7,13 @@ import {inspect as nodeInspect} from 'node:util'; import yaml from 'js-yaml'; +import CacheableObject, {CacheableObjectPropertyValueError} + from '#cacheable-object'; import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import find, {bindFind} from '#find'; import {traverse} from '#node-utils'; -import T, { - CacheableObject, - CacheableObjectPropertyValueError, - Thing, -} from '#things'; +import T, {Thing} from '#things'; import { annotateErrorWithFile, diff --git a/src/find.js b/src/find.js index dfcaa9a..4d3e996 100644 --- a/src/find.js +++ b/src/find.js @@ -1,8 +1,8 @@ import {inspect} from 'node:util'; +import CacheableObject from '#cacheable-object'; import {colors, logWarn} from '#cli'; import {typeAppearance} from '#sugar'; -import {CacheableObject} from '#things'; function warnOrThrow(mode, message) { if (mode === 'error') { diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 1bbcb9c..e6c1f5c 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -101,8 +101,8 @@ import { import dimensionsOf from 'image-size'; +import CacheableObject from '#cacheable-object'; import {delay, empty, queue, unique} from '#sugar'; -import {CacheableObject} from '#things'; import {sortByName} from '#wiki-data'; import { diff --git a/src/repl.js b/src/repl.js index ead0156..26879be 100644 --- a/src/repl.js +++ b/src/repl.js @@ -11,7 +11,8 @@ import {generateURLs, urlSpec} from '#urls'; import {quickLoadAllFromYAML} from '#yaml'; import _find, {bindFind} from '#find'; -import thingConstructors, {CacheableObject} from '#things'; +import CacheableObject from '#cacheable-object'; +import thingConstructors from '#things'; import * as serialize from '#serialize'; import * as sugar from '#sugar'; import * as wikiDataUtils from '#wiki-data'; diff --git a/src/upd8.js b/src/upd8.js index 408ad88..3d7da80 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -38,12 +38,12 @@ import {fileURLToPath} from 'node:url'; import wrap from 'word-wrap'; +import CacheableObject from '#cacheable-object'; import {displayCompositeCacheAnalysis} from '#composite'; import {processLanguageFile} from '#language'; import {isMain, traverse} from '#node-utils'; import bootRepl from '#repl'; import {empty, showAggregate, withEntries} from '#sugar'; -import {CacheableObject} from '#things'; import {generateURLs, urlSpec} from '#urls'; import {sortByName} from '#wiki-data'; diff --git a/test/unit/data/cacheable-object.js b/test/unit/data/cacheable-object.js index 57e562d..5ed9a4a 100644 --- a/test/unit/data/cacheable-object.js +++ b/test/unit/data/cacheable-object.js @@ -1,6 +1,6 @@ import t from 'tap'; -import {CacheableObject} from '#things'; +import CacheableObject from '#cacheable-object'; function newCacheableObject(PD) { return new (class extends CacheableObject { -- cgit 1.3.0-6-gf8a5 From 764b6a3a2c89d0e4d948862bfeb546d33bbf45e6 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 14 Nov 2023 21:45:35 -0400 Subject: data: generic composite dependency comments --- src/data/composite/control-flow/index.js | 5 +++++ src/data/composite/data/index.js | 5 +++++ src/data/composite/wiki-data/index.js | 7 +++++++ src/data/composite/wiki-properties/index.js | 5 +++++ 4 files changed, 22 insertions(+) diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js index dfc53db..7fad88b 100644 --- a/src/data/composite/control-flow/index.js +++ b/src/data/composite/control-flow/index.js @@ -1,3 +1,8 @@ +// #composite/control-flow +// +// No entries depend on any other entries, except siblings in this directory. +// + export {default as exitWithoutDependency} from './exitWithoutDependency.js'; export {default as exitWithoutUpdateValue} from './exitWithoutUpdateValue.js'; export {default as exposeConstant} from './exposeConstant.js'; diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js index ecd0512..db1c37c 100644 --- a/src/data/composite/data/index.js +++ b/src/data/composite/data/index.js @@ -1,3 +1,8 @@ +// #composite/data +// +// Entries here may depend on entries in #composite/control-flow. +// + export {default as excludeFromList} from './excludeFromList.js'; export {default as fillMissingListItems} from './fillMissingListItems.js'; export {default as withFlattenedList} from './withFlattenedList.js'; diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js index 1d0400f..df50a2d 100644 --- a/src/data/composite/wiki-data/index.js +++ b/src/data/composite/wiki-data/index.js @@ -1,6 +1,13 @@ +// #composite/wiki-data +// +// Entries here may depend on entries in #composite/control-flow and in +// #composite/data. +// + export {default as exitWithoutContribs} from './exitWithoutContribs.js'; export {default as inputThingClass} from './inputThingClass.js'; export {default as inputWikiData} from './inputWikiData.js'; +export {default as withParsedCommentaryEntries} from './withParsedCommentaryEntries.js'; export {default as withResolvedContribs} from './withResolvedContribs.js'; export {default as withResolvedReference} from './withResolvedReference.js'; export {default as withResolvedReferenceList} from './withResolvedReferenceList.js'; diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js index 2462b04..3a8b51d 100644 --- a/src/data/composite/wiki-properties/index.js +++ b/src/data/composite/wiki-properties/index.js @@ -1,3 +1,8 @@ +// #composite/wiki-properties +// +// Entries here may depend on entries in #composite/control-flow, +// #composite/data, and #composite/wiki-data. + export {default as additionalFiles} from './additionalFiles.js'; export {default as color} from './color.js'; export {default as commentary} from './commentary.js'; -- cgit 1.3.0-6-gf8a5 From 6deea0629a3f3b9985d205d2f3a048893ea938c9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 14 Nov 2023 22:20:45 -0400 Subject: data, test: withParsedCommentaryEntries --- .../wiki-data/withParsedCommentaryEntries.js | 181 +++++++++++++++++++++ src/data/composite/wiki-properties/commentary.js | 32 +++- .../wiki-data/withParsedCommentaryEntries.js | 102 ++++++++++++ 3 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 src/data/composite/wiki-data/withParsedCommentaryEntries.js create mode 100644 test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js new file mode 100644 index 0000000..5bd72dc --- /dev/null +++ b/src/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -0,0 +1,181 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {stitchArrays} from '#sugar'; +import {isCommentary} from '#validators'; + +import {fillMissingListItems, withPropertiesFromList} from '#composite/data'; + +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +// Matches 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. +// +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 default templateCompositeFrom({ + annotation: `withParsedCommentaryEntries`, + + inputs: { + from: input({validate: isCommentary}), + }, + + outputs: ['#parsedCommentaryEntries'], + + steps: () => [ + { + dependencies: [input('from')], + + compute: (continuation, { + [input('from')]: commentaryText, + }) => continuation({ + ['#rawMatches']: + Array.from(commentaryText.matchAll(commentaryRegex)), + }), + }, + + withPropertiesFromList({ + list: '#rawMatches', + properties: input.value([ + '0', // The entire match as a string. + 'groups', + 'index', + ]), + }).outputs({ + '#rawMatches.0': '#rawMatches.text', + '#rawMatches.groups': '#rawMatches.groups', + '#rawMatches.index': '#rawMatches.startIndex', + }), + + { + dependencies: [ + '#rawMatches.text', + '#rawMatches.startIndex', + ], + + compute: (continuation, { + ['#rawMatches.text']: text, + ['#rawMatches.startIndex']: startIndex, + }) => continuation({ + ['#rawMatches.endIndex']: + stitchArrays({text, startIndex}) + .map(({text, startIndex}) => startIndex + text.length), + }), + }, + + { + dependencies: [ + input('from'), + '#rawMatches.startIndex', + '#rawMatches.endIndex', + ], + + compute: (continuation, { + [input('from')]: commentaryText, + ['#rawMatches.startIndex']: startIndex, + ['#rawMatches.endIndex']: endIndex, + }) => continuation({ + ['#entries.body']: + stitchArrays({startIndex, endIndex}) + .map(({endIndex}, index, stitched) => + (index === stitched.length - 1 + ? commentaryText.slice(endIndex) + : commentaryText.slice( + endIndex, + stitched[index + 1].startIndex))) + .map(body => body.trim()), + }), + }, + + withPropertiesFromList({ + list: '#rawMatches.groups', + prefix: input.value('#entries'), + properties: input.value([ + 'artistReference', + 'boldfaceArtist', + 'annotation', + 'date', + ]), + }), + + // The artistReference group will always have a value, since it's required + // for the line to match in the first place. + + withResolvedReferenceList({ + list: '#entries.artistReference', + data: 'artistData', + find: input.value(find.artist), + notFoundMode: input.value('null'), + }).outputs({ + '#resolvedReferenceList': '#entries.artist', + }), + + { + dependencies: ['#entries.boldfaceArtist'], + compute: (continuation, { + ['#entries.boldfaceArtist']: boldfaceArtist, + }) => continuation({ + ['#entries.boldfaceArtist']: + boldfaceArtist.map(boldface => boldface ? true : false), + }), + }, + + fillMissingListItems({ + list: '#entries.annotation', + fill: input.value(null), + }), + + { + dependencies: ['#entries.date'], + compute: (continuation, { + ['#entries.date']: date, + }) => continuation({ + ['#entries.date']: + date.map(date => date ? new Date(date) : null), + }), + }, + + { + dependencies: [ + '#entries.artist', + '#entries.boldfaceArtist', + '#entries.annotation', + '#entries.date', + '#entries.body', + ], + + compute: (continuation, { + ['#entries.artist']: artist, + ['#entries.boldfaceArtist']: boldfaceArtist, + ['#entries.annotation']: annotation, + ['#entries.date']: date, + ['#entries.body']: body, + }) => continuation({ + ['#parsedCommentaryEntries']: + stitchArrays({ + artist, + boldfaceArtist, + annotation, + date, + body, + }), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-properties/commentary.js b/src/data/composite/wiki-properties/commentary.js index fbea9d5..cd6b7ac 100644 --- a/src/data/composite/wiki-properties/commentary.js +++ b/src/data/composite/wiki-properties/commentary.js @@ -1,12 +1,30 @@ // Artist commentary! Generally present on tracks and albums. +import {input, templateCompositeFrom} from '#composite'; import {isCommentary} from '#validators'; -// TODO: Not templateCompositeFrom. +import {exitWithoutDependency, exposeDependency} + from '#composite/control-flow'; +import {withParsedCommentaryEntries} from '#composite/wiki-data'; -export default function() { - return { - flags: {update: true, expose: true}, - update: {validate: isCommentary}, - }; -} +export default templateCompositeFrom({ + annotation: `commentary`, + + compose: false, + + steps: () => [ + exitWithoutDependency({ + dependency: input.updateValue({validate: isCommentary}), + mode: input.value('falsy'), + value: input.value(null), + }), + + withParsedCommentaryEntries({ + from: input.updateValue(), + }), + + exposeDependency({ + dependency: '#parsedCommentaryEntries', + }), + ], +}); diff --git a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js new file mode 100644 index 0000000..7b45644 --- /dev/null +++ b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -0,0 +1,102 @@ +import t from 'tap'; + +import {compositeFrom, input} from '#composite'; +import thingConstructors from '#things'; + +import {exposeDependency} from '#composite/control-flow'; +import {withParsedCommentaryEntries} from '#composite/wiki-data'; + +const {Artist} = thingConstructors; + +const composite = compositeFrom({ + compose: false, + + steps: [ + withParsedCommentaryEntries({ + from: 'from', + }), + + exposeDependency({dependency: '#parsedCommentaryEntries'}), + ], +}); + +function stubArtist(artistName = `Test Artist`) { + const artist = new Artist(); + artist.name = artistName; + + return artist; +} + +t.test(`withParsedCommentaryEntries: basic behavior`, t => { + t.plan(3); + + const artist1 = stubArtist(`Mobius Trip`); + const artist2 = stubArtist(`Hadron Kaleido`); + + const artistData = [artist1, artist2]; + + t.match(composite, { + expose: { + dependencies: ['from', 'artistData'], + }, + }); + + t.match(composite.expose.compute({ + artistData, + from: + `Mobius Trip:\n` + + `Some commentary.\n` + + `Very cool.\n`, + }), [ + { + artist: artist1, + boldfaceArtist: false, + annotation: null, + date: null, + body: `Some commentary.\nVery cool.`, + }, + ]); + + t.match(composite.expose.compute({ + artistData, + from: + `Mobius Trip: (music, art, 12 January 2015)\n` + + `First commentary entry.\n` + + `Very cool.\n` + + `Hadron Kaleido: (moral support, 4/4/2022)\n` + + `Second commentary entry. Yes. So cool.\n` + + `Mystery Artist: (pingas)\n` + + `Oh no.. Oh dear...\n` + + `Mobius Trip:\n` + + `And back around we go.`, + }), [ + { + artist: artist1, + boldfaceArtist: false, + annotation: `music, art`, + date: new Date('12 January 2015'), + body: `First commentary entry.\nVery cool.`, + }, + { + artist: artist2, + boldfaceArtist: true, + annotation: `moral support`, + date: new Date('4 April 2022'), + body: `Second commentary entry. Yes. So cool.`, + }, + { + artist: null, + boldfaceArtist: false, + annotation: `pingas`, + date: null, + body: `Oh no.. Oh dear...`, + }, + { + artist: artist1, + boldfaceArtist: false, + annotation: null, + date: null, + body: `And back around we go.`, + }, + ]); +}); -- cgit 1.3.0-6-gf8a5 From 09a4af31a3f8207dfe114926b0dbf27eeddf7de9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 14 Nov 2023 22:26:25 -0400 Subject: data: commentatorArtists: use withParsedCommentaryEntries --- .../wiki-properties/commentatorArtists.js | 41 +++++++--------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js index 52aeb86..8720e66 100644 --- a/src/data/composite/wiki-properties/commentatorArtists.js +++ b/src/data/composite/wiki-properties/commentatorArtists.js @@ -1,13 +1,12 @@ -// This one's kinda tricky: it parses artist "references" from the -// commentary content, and finds the matching artist for each reference. +// List of artists referenced in commentary entries. // This is mostly useful for credits and listings on artist pages. import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; import {unique} from '#sugar'; import {exitWithoutDependency} from '#composite/control-flow'; -import {withResolvedReferenceList} from '#composite/wiki-data'; +import {withPropertyFromList} from '#composite/data'; +import {withParsedCommentaryEntries} from '#composite/wiki-data'; export default templateCompositeFrom({ annotation: `commentatorArtists`, @@ -21,35 +20,21 @@ export default templateCompositeFrom({ value: input.value([]), }), - { - dependencies: ['commentary'], - compute: (continuation, {commentary}) => - continuation({ - '#artistRefs': - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/(?.*?):<\/i>/g)) - .map(({groups: {who}}) => who), - }), - }, + withParsedCommentaryEntries({ + from: 'commentary', + }), - withResolvedReferenceList({ - list: '#artistRefs', - data: 'artistData', - find: input.value(find.artist), + withPropertyFromList({ + list: '#parsedCommentaryEntries', + property: input.value('artist'), }).outputs({ - '#resolvedReferenceList': '#artists', + '#parsedCommentaryEntries.artist': '#artists', }), { - flags: {expose: true}, - - expose: { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists), - }, + dependencies: ['#artists'], + compute: ({'#artists': artists}) => + unique(artists.filter(artist => artist !== null)), }, ], }); -- cgit 1.3.0-6-gf8a5 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 --- .../wiki-data/withParsedCommentaryEntries.js | 23 +-------- src/data/yaml.js | 55 ++++++++++++++++------ src/util/wiki-data.js | 25 ++++++++++ 3 files changed, 67 insertions(+), 36 deletions(-) diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js index 5bd72dc..9e33cda 100644 --- a/src/data/composite/wiki-data/withParsedCommentaryEntries.js +++ b/src/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -2,33 +2,12 @@ import {input, templateCompositeFrom} from '#composite'; import find from '#find'; import {stitchArrays} from '#sugar'; import {isCommentary} from '#validators'; +import {commentaryRegex} from '#wiki-data'; import {fillMissingListItems, withPropertiesFromList} from '#composite/data'; import withResolvedReferenceList from './withResolvedReferenceList.js'; -// Matches 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. -// -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 default templateCompositeFrom({ annotation: `withParsedCommentaryEntries`, diff --git a/src/data/yaml.js b/src/data/yaml.js index 986f25d..843e70b 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -28,6 +28,7 @@ import { } from '#sugar'; import { + commentaryRegex, sortAlbumsTracksChronologically, sortAlphabetically, sortChronologically, @@ -1616,6 +1617,7 @@ export function filterReferenceErrors(wikiData) { bannerArtistContribs: '_contrib', groups: 'group', artTags: 'artTag', + commentary: '_commentary', }], ['trackData', processTrackDocument, { @@ -1626,6 +1628,7 @@ export function filterReferenceErrors(wikiData) { sampledTracks: '_trackNotRerelease', artTags: 'artTag', originalReleaseTrack: '_trackNotRerelease', + commentary: '_commentary', }], ['groupCategoryData', processGroupCategoryDocument, { @@ -1675,7 +1678,19 @@ export function filterReferenceErrors(wikiData) { nest({message: `Reference errors in ${inspect(thing)}`}, ({nest, push, filter}) => { for (const [property, findFnKey] of Object.entries(propSpec)) { - const value = CacheableObject.getUpdateValue(thing, property); + let value = CacheableObject.getUpdateValue(thing, property); + let writeProperty = true; + + switch (findFnKey) { + case '_commentary': + if (value) { + value = + Array.from(value.matchAll(commentaryRegex)) + .map(({groups}) => groups.artistReference); + } + writeProperty = false; + break; + } if (value === undefined) { push(new TypeError(`Property ${colors.red(property)} isn't valid for ${colors.green(thing.constructor.name)}`)); @@ -1688,19 +1703,25 @@ export function filterReferenceErrors(wikiData) { let findFn; + const findArtistOrAlias = artistRef => { + const alias = find.artist(artistRef, wikiData.artistAliasData, {mode: 'quiet'}); + if (alias) { + // No need to check if the original exists here. Aliases are automatically + // created from a field on the original, so the original certainly exists. + const original = alias.aliasedArtist; + throw new Error(`Reference ${colors.red(artistRef)} is to an alias, should be ${colors.green(original.name)}`); + } + + return boundFind.artist(artistRef); + }; + switch (findFnKey) { - case '_contrib': - findFn = contribRef => { - const alias = find.artist(contribRef.who, wikiData.artistAliasData, {mode: 'quiet'}); - if (alias) { - // No need to check if the original exists here. Aliases are automatically - // created from a field on the original, so the original certainly exists. - const original = alias.aliasedArtist; - throw new Error(`Reference ${colors.red(contribRef.who)} is to an alias, should be ${colors.green(original.name)}`); - } + case '_commentary': + findFn = findArtistOrAlias; + break; - return boundFind.artist(contribRef.who); - }; + case '_contrib': + findFn = contribRef => findArtistOrAlias(contribRef.who); break; case '_homepageSourceGroup': @@ -1781,8 +1802,10 @@ export function filterReferenceErrors(wikiData) { ? `Reference errors` + fieldPropertyMessage + findFnMessage : `Reference error` + fieldPropertyMessage + findFnMessage); + let newPropertyValue = value; + if (Array.isArray(value)) { - thing[property] = filter( + newPropertyValue = filter( value, decorateErrorWithIndex(suppress(findFn)), {message: errorMessage}); @@ -1792,11 +1815,15 @@ export function filterReferenceErrors(wikiData) { try { call(findFn, value); } catch (error) { - thing[property] = null; + newPropertyValue = null; throw error; } })); } + + if (writeProperty) { + thing[property] = newPropertyValue; + } } }); } diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 0790ae9..5ab0122 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 --- .../wiki-data/withParsedCommentaryEntries.js | 21 ++++++++------------- src/util/wiki-data.js | 12 ++++++------ .../wiki-data/withParsedCommentaryEntries.js | 18 +++++++++--------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js index 9e33cda..7b1c948 100644 --- a/src/data/composite/wiki-data/withParsedCommentaryEntries.js +++ b/src/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -87,7 +87,7 @@ export default templateCompositeFrom({ prefix: input.value('#entries'), properties: input.value([ 'artistReference', - 'boldfaceArtist', + 'artistDisplayText', 'annotation', 'date', ]), @@ -105,15 +105,10 @@ export default templateCompositeFrom({ '#resolvedReferenceList': '#entries.artist', }), - { - dependencies: ['#entries.boldfaceArtist'], - compute: (continuation, { - ['#entries.boldfaceArtist']: boldfaceArtist, - }) => continuation({ - ['#entries.boldfaceArtist']: - boldfaceArtist.map(boldface => boldface ? true : false), - }), - }, + fillMissingListItems({ + list: '#entries.artistDisplayText', + fill: input.value(null), + }), fillMissingListItems({ list: '#entries.annotation', @@ -133,7 +128,7 @@ export default templateCompositeFrom({ { dependencies: [ '#entries.artist', - '#entries.boldfaceArtist', + '#entries.artistDisplayText', '#entries.annotation', '#entries.date', '#entries.body', @@ -141,7 +136,7 @@ export default templateCompositeFrom({ compute: (continuation, { ['#entries.artist']: artist, - ['#entries.boldfaceArtist']: boldfaceArtist, + ['#entries.artistDisplayText']: artistDisplayText, ['#entries.annotation']: annotation, ['#entries.date']: date, ['#entries.body']: body, @@ -149,7 +144,7 @@ export default templateCompositeFrom({ ['#parsedCommentaryEntries']: stitchArrays({ artist, - boldfaceArtist, + artistDisplayText, annotation, date, body, diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 5ab0122..75a141d 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 diff --git a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js index 7b45644..50570de 100644 --- a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js +++ b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -50,7 +50,7 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => { }), [ { artist: artist1, - boldfaceArtist: false, + artistDisplayText: null, annotation: null, date: null, body: `Some commentary.\nVery cool.`, @@ -60,40 +60,40 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => { t.match(composite.expose.compute({ artistData, from: - `Mobius Trip: (music, art, 12 January 2015)\n` + + `Mobius Trip|Moo-bius Trip: (music, art, 12 January 2015)\n` + `First commentary entry.\n` + `Very cool.\n` + - `Hadron Kaleido: (moral support, 4/4/2022)\n` + + `Hadron Kaleido|[[artist:hadron-kaleido|The Ol' Hadron]]: (moral support, 4/4/2022)\n` + `Second commentary entry. Yes. So cool.\n` + - `Mystery Artist: (pingas)\n` + + `Mystery Artist: (pingas, August 25, 2023)\n` + `Oh no.. Oh dear...\n` + `Mobius Trip:\n` + `And back around we go.`, }), [ { artist: artist1, - boldfaceArtist: false, + artistDisplayText: `Moo-bius Trip`, annotation: `music, art`, date: new Date('12 January 2015'), body: `First commentary entry.\nVery cool.`, }, { artist: artist2, - boldfaceArtist: true, + artistDisplayText: `[[artist:hadron-kaleido|The Ol' Hadron]]`, annotation: `moral support`, date: new Date('4 April 2022'), body: `Second commentary entry. Yes. So cool.`, }, { artist: null, - boldfaceArtist: false, + artistDisplayText: null, annotation: `pingas`, - date: null, + date: new Date('25 August 2023'), body: `Oh no.. Oh dear...`, }, { artist: artist1, - boldfaceArtist: false, + artistDisplayText: null, annotation: null, date: null, body: `And back around we go.`, -- cgit 1.3.0-6-gf8a5 From f2a31006efa7c4d9c7c15823adc70cc40c46dedd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 15 Nov 2023 10:50:54 -0400 Subject: data: fix commentary entry serialization --- src/data/serialize.js | 4 ++++ src/data/things/album.js | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/data/serialize.js b/src/data/serialize.js index 52aacb0..8cac330 100644 --- a/src/data/serialize.js +++ b/src/data/serialize.js @@ -19,6 +19,10 @@ export function toContribRefs(contribs) { return contribs?.map(({who, what}) => ({who: toRef(who), what})); } +export function toCommentaryRefs(entries) { + return entries?.map(({artist, ...props}) => ({artist: toRef(artist), ...props})); +} + // Interface export const serializeDescriptors = Symbol(); diff --git a/src/data/things/album.js b/src/data/things/album.js index af3eb04..63ec114 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -181,7 +181,8 @@ export class Album extends Thing { hasTrackArt: S.id, isListedOnHomepage: S.id, - commentary: S.id, + commentary: S.toCommentaryRefs, + additionalFiles: S.id, tracks: S.toRefs, -- cgit 1.3.0-6-gf8a5 From d019852fc5dcfa2a7686c17ec1bc9c4877ad5832 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 15 Nov 2023 11:27:22 -0400 Subject: content, css: generateCommentarySection{Entry}? --- src/content/dependencies/generateAlbumInfoPage.js | 22 ++----- .../dependencies/generateCommentarySection.js | 29 ++++++++ .../dependencies/generateCommentarySectionEntry.js | 77 ++++++++++++++++++++++ src/content/dependencies/generateTrackInfoPage.js | 22 ++----- src/static/site5.css | 11 ++++ src/strings-default.yaml | 20 ++++-- 6 files changed, 141 insertions(+), 40 deletions(-) create mode 100644 src/content/dependencies/generateCommentarySection.js create mode 100644 src/content/dependencies/generateCommentarySectionEntry.js diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index 5fe27ca..90a120c 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -17,6 +17,7 @@ export default { 'generateAlbumStyleRules', 'generateAlbumTrackList', 'generateChronologyLinks', + 'generateCommentarySection', 'generateContentHeading', 'generatePageLayout', 'linkAlbum', @@ -126,13 +127,8 @@ export default { // Section: Artist commentary if (album.commentary) { - const artistCommentary = sections.artistCommentary = {}; - - artistCommentary.heading = - relation('generateContentHeading'); - - artistCommentary.content = - relation('transformContent', album.commentary); + sections.artistCommentary = + relation('generateCommentarySection', album.commentary); } return relations; @@ -235,17 +231,7 @@ export default { sec.additionalFiles.additionalFilesList, ], - sec.artistCommentary && [ - sec.artistCommentary.heading - .slots({ - id: 'artist-commentary', - title: language.$('releaseInfo.artistCommentary') - }), - - html.tag('blockquote', - sec.artistCommentary.content - .slot('mode', 'multiline')), - ], + sec.artistCommentary, ], navLinkStyle: 'hierarchical', diff --git a/src/content/dependencies/generateCommentarySection.js b/src/content/dependencies/generateCommentarySection.js new file mode 100644 index 0000000..d08c3c9 --- /dev/null +++ b/src/content/dependencies/generateCommentarySection.js @@ -0,0 +1,29 @@ +export default { + contentDependencies: [ + 'transformContent', + 'generateCommentarySectionEntry', + 'generateContentHeading', + ], + + extraDependencies: ['html', 'language'], + + relations: (relation, entries) => ({ + heading: + relation('generateContentHeading'), + + entries: + entries.map(entry => + relation('generateCommentarySectionEntry', entry)), + }), + + generate: (relations, {html, language}) => + html.tags([ + relations.heading + .slots({ + id: 'artist-commentary', + title: language.$('misc.artistCommentary') + }), + + relations.entries, + ]), +}; diff --git a/src/content/dependencies/generateCommentarySectionEntry.js b/src/content/dependencies/generateCommentarySectionEntry.js new file mode 100644 index 0000000..22e8fd1 --- /dev/null +++ b/src/content/dependencies/generateCommentarySectionEntry.js @@ -0,0 +1,77 @@ +export default { + contentDependencies: ['linkArtist', 'transformContent'], + extraDependencies: ['html', 'language'], + + relations: (relation, entry) => ({ + artistLink: + (entry.artist && !entry.artistDisplayText + ? relation('linkArtist', entry.artist) + : null), + + artistsContent: + (entry.artistDisplayText + ? relation('transformContent', entry.artistDisplayText) + : null), + + annotationContent: + (entry.annotation + ? relation('transformContent', entry.annotation) + : null), + + bodyContent: + (entry.body + ? relation('transformContent', entry.body) + : null), + }), + + data: (entry) => ({ + date: entry.date, + }), + + generate(data, relations, {html, language}) { + const artistsSpan = + html.tag('span', {class: 'commentary-entry-artists'}, + (relations.artistsContent + ? relations.artistsContent.slot('mode', 'inline') + : relations.artistLink + ? relations.artistLink + : language.$('misc.artistCommentary.noArtist'))); + + const accentParts = ['misc.artistCommentary.entry.title.accent']; + const accentOptions = {}; + + if (relations.annotationContent) { + accentParts.push('withAnnotation'); + accentOptions.annotation = + relations.annotationContent.slot('mode', 'inline'); + } + + if (data.date) { + accentParts.push('withDate'); + accentOptions.date = + language.formatDate(data.date); + } + + const accent = + (accentParts.length > 1 + ? html.tag('span', {class: 'commentary-entry-accent'}, + language.$(...accentParts, accentOptions)) + : null); + + const titleParts = ['misc.artistCommentary.entry.title']; + const titleOptions = {artists: artistsSpan}; + + if (accent) { + titleParts.push('withAccent'); + titleOptions.accent = accent; + } + + return html.tags([ + html.tag('p', {class: 'commentary-entry-heading'}, + language.$(...titleParts, titleOptions)), + + html.tag('blockquote', {class: 'commentary-entry-body'}, + relations.bodyContent.slot('mode', 'multiline')), + ]); + }, +}; diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 9333494..200cf05 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -11,6 +11,7 @@ export default { 'generateAlbumSidebar', 'generateAlbumStyleRules', 'generateChronologyLinks', + 'generateCommentarySection', 'generateContentHeading', 'generateContributionList', 'generatePageLayout', @@ -268,13 +269,8 @@ export default { // Section: Artist commentary if (track.commentary) { - const artistCommentary = sections.artistCommentary = {}; - - artistCommentary.heading = - relation('generateContentHeading'); - - artistCommentary.content = - relation('transformContent', track.commentary); + sections.artistCommentary = + relation('generateCommentarySection', track.commentary); } return relations; @@ -491,17 +487,7 @@ export default { sec.additionalFiles.list, ], - sec.artistCommentary && [ - sec.artistCommentary.heading - .slots({ - id: 'artist-commentary', - title: language.$('releaseInfo.artistCommentary') - }), - - html.tag('blockquote', - sec.artistCommentary.content - .slot('mode', 'multiline')), - ], + sec.artistCommentary, ], navLinkStyle: 'hierarchical', diff --git a/src/static/site5.css b/src/static/site5.css index 014e6d2..1ffe504 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -537,6 +537,17 @@ p .current { margin-top: 5px; } +.commentary-entry-heading { + margin-left: 15px; + padding-left: 5px; + padding-bottom: 0.2em; + border-bottom: 1px dotted var(--primary-color); +} + +.commentary-entry-accent { + font-style: oblique; +} + .commentary-art { float: right; width: 30%; diff --git a/src/strings-default.yaml b/src/strings-default.yaml index a21758e..eccfc80 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -273,10 +273,6 @@ releaseInfo: _: "Read {LINK}." link: "artist commentary" - artistCommentary: - _: "Artist commentary:" - seeOriginalRelease: "See {ORIGINAL}!" - additionalFiles: heading: "View or download {ADDITIONAL_FILES}:" @@ -349,6 +345,22 @@ misc: artistAvatar: "artist avatar" flashArt: "flash art" + # artistCommentary: + + artistCommentary: + _: "Artist commentary:" + + entry: + title: + _: "{ARTISTS}:" + withAccent: "{ARTISTS}: {ACCENT}" + accent: + withAnnotation: "({ANNOTATION})" + withDate: ({DATE})" + withAnnotation.withDate: "({ANNOTATION}, {DATE})" + + seeOriginalRelease: "See {ORIGINAL}!" + # artistLink: # Artist links have special accents which are made conditionally # present in a variety of places across the wiki. -- cgit 1.3.0-6-gf8a5 From 4b62389403ef84497d5855e8ef1359ebf5784881 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 16 Nov 2023 14:12:06 -0400 Subject: content: generateCommentarySectionEntry -> generateCommentaryEntry --- .../dependencies/generateCommentaryEntry.js | 77 ++++++++++++++++++++++ .../dependencies/generateCommentarySection.js | 4 +- .../dependencies/generateCommentarySectionEntry.js | 77 ---------------------- 3 files changed, 79 insertions(+), 79 deletions(-) create mode 100644 src/content/dependencies/generateCommentaryEntry.js delete mode 100644 src/content/dependencies/generateCommentarySectionEntry.js diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js new file mode 100644 index 0000000..22e8fd1 --- /dev/null +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -0,0 +1,77 @@ +export default { + contentDependencies: ['linkArtist', 'transformContent'], + extraDependencies: ['html', 'language'], + + relations: (relation, entry) => ({ + artistLink: + (entry.artist && !entry.artistDisplayText + ? relation('linkArtist', entry.artist) + : null), + + artistsContent: + (entry.artistDisplayText + ? relation('transformContent', entry.artistDisplayText) + : null), + + annotationContent: + (entry.annotation + ? relation('transformContent', entry.annotation) + : null), + + bodyContent: + (entry.body + ? relation('transformContent', entry.body) + : null), + }), + + data: (entry) => ({ + date: entry.date, + }), + + generate(data, relations, {html, language}) { + const artistsSpan = + html.tag('span', {class: 'commentary-entry-artists'}, + (relations.artistsContent + ? relations.artistsContent.slot('mode', 'inline') + : relations.artistLink + ? relations.artistLink + : language.$('misc.artistCommentary.noArtist'))); + + const accentParts = ['misc.artistCommentary.entry.title.accent']; + const accentOptions = {}; + + if (relations.annotationContent) { + accentParts.push('withAnnotation'); + accentOptions.annotation = + relations.annotationContent.slot('mode', 'inline'); + } + + if (data.date) { + accentParts.push('withDate'); + accentOptions.date = + language.formatDate(data.date); + } + + const accent = + (accentParts.length > 1 + ? html.tag('span', {class: 'commentary-entry-accent'}, + language.$(...accentParts, accentOptions)) + : null); + + const titleParts = ['misc.artistCommentary.entry.title']; + const titleOptions = {artists: artistsSpan}; + + if (accent) { + titleParts.push('withAccent'); + titleOptions.accent = accent; + } + + return html.tags([ + html.tag('p', {class: 'commentary-entry-heading'}, + language.$(...titleParts, titleOptions)), + + html.tag('blockquote', {class: 'commentary-entry-body'}, + relations.bodyContent.slot('mode', 'multiline')), + ]); + }, +}; diff --git a/src/content/dependencies/generateCommentarySection.js b/src/content/dependencies/generateCommentarySection.js index d08c3c9..8ae1b2d 100644 --- a/src/content/dependencies/generateCommentarySection.js +++ b/src/content/dependencies/generateCommentarySection.js @@ -1,7 +1,7 @@ export default { contentDependencies: [ 'transformContent', - 'generateCommentarySectionEntry', + 'generateCommentaryEntry', 'generateContentHeading', ], @@ -13,7 +13,7 @@ export default { entries: entries.map(entry => - relation('generateCommentarySectionEntry', entry)), + relation('generateCommentaryEntry', entry)), }), generate: (relations, {html, language}) => diff --git a/src/content/dependencies/generateCommentarySectionEntry.js b/src/content/dependencies/generateCommentarySectionEntry.js deleted file mode 100644 index 22e8fd1..0000000 --- a/src/content/dependencies/generateCommentarySectionEntry.js +++ /dev/null @@ -1,77 +0,0 @@ -export default { - contentDependencies: ['linkArtist', 'transformContent'], - extraDependencies: ['html', 'language'], - - relations: (relation, entry) => ({ - artistLink: - (entry.artist && !entry.artistDisplayText - ? relation('linkArtist', entry.artist) - : null), - - artistsContent: - (entry.artistDisplayText - ? relation('transformContent', entry.artistDisplayText) - : null), - - annotationContent: - (entry.annotation - ? relation('transformContent', entry.annotation) - : null), - - bodyContent: - (entry.body - ? relation('transformContent', entry.body) - : null), - }), - - data: (entry) => ({ - date: entry.date, - }), - - generate(data, relations, {html, language}) { - const artistsSpan = - html.tag('span', {class: 'commentary-entry-artists'}, - (relations.artistsContent - ? relations.artistsContent.slot('mode', 'inline') - : relations.artistLink - ? relations.artistLink - : language.$('misc.artistCommentary.noArtist'))); - - const accentParts = ['misc.artistCommentary.entry.title.accent']; - const accentOptions = {}; - - if (relations.annotationContent) { - accentParts.push('withAnnotation'); - accentOptions.annotation = - relations.annotationContent.slot('mode', 'inline'); - } - - if (data.date) { - accentParts.push('withDate'); - accentOptions.date = - language.formatDate(data.date); - } - - const accent = - (accentParts.length > 1 - ? html.tag('span', {class: 'commentary-entry-accent'}, - language.$(...accentParts, accentOptions)) - : null); - - const titleParts = ['misc.artistCommentary.entry.title']; - const titleOptions = {artists: artistsSpan}; - - if (accent) { - titleParts.push('withAccent'); - titleOptions.accent = accent; - } - - return html.tags([ - html.tag('p', {class: 'commentary-entry-heading'}, - language.$(...titleParts, titleOptions)), - - html.tag('blockquote', {class: 'commentary-entry-body'}, - relations.bodyContent.slot('mode', 'multiline')), - ]); - }, -}; -- cgit 1.3.0-6-gf8a5 From 233e46c8d9099b4f6d2174e12f2b287478c54f40 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 16 Nov 2023 14:25:08 -0400 Subject: content: generateAlbumCommentaryPage: use updated commentary entries --- .../dependencies/generateAlbumCommentaryPage.js | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index e241551..5b00acf 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -7,12 +7,12 @@ export default { 'generateAlbumSidebarTrackSection', 'generateAlbumStyleRules', 'generateColorStyleVariables', + 'generateCommentaryEntry', 'generateContentHeading', 'generateTrackCoverArtwork', 'generatePageLayout', 'linkAlbum', 'linkTrack', - 'transformContent', ], extraDependencies: ['html', 'language'], @@ -38,8 +38,9 @@ export default { relation('generateAlbumCoverArtwork', album); } - relations.albumCommentaryContent = - relation('transformContent', album.commentary); + relations.albumCommentaryEntries = + album.commentary + .map(entry => relation('generateCommentaryEntry', entry)); } const tracksWithCommentary = @@ -61,9 +62,11 @@ export default { ? relation('generateTrackCoverArtwork', track) : null)); - relations.trackCommentaryContent = + relations.trackCommentaryEntries = tracksWithCommentary - .map(track => relation('transformContent', track.commentary)); + .map(track => + track.commentary + .map(entry => relation('generateCommentaryEntry', entry))); relations.trackCommentaryColorVariables = tracksWithCommentary @@ -163,10 +166,10 @@ export default { link: relations.trackCommentaryLinks, directory: data.trackCommentaryDirectories, cover: relations.trackCommentaryCovers, - content: relations.trackCommentaryContent, + entries: relations.trackCommentaryEntries, colorVariables: relations.trackCommentaryColorVariables, color: data.trackCommentaryColors, - }).map(({heading, link, directory, cover, content, colorVariables, color}) => [ + }).map(({heading, link, directory, cover, entries, colorVariables, color}) => [ heading.slots({ tag: 'h3', id: directory, @@ -175,11 +178,7 @@ export default { cover?.slots({mode: 'commentary'}), - html.tag('blockquote', - (color - ? {style: colorVariables.slot('color', color).content} - : {}), - content), + entries, ]), ], -- cgit 1.3.0-6-gf8a5 From 7a5b4465069d594a90c8e783e646a3248408ddaf Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 16 Nov 2023 14:25:37 -0400 Subject: content: generateCommentaryEntry: add color slot --- .../dependencies/generateAlbumCommentaryPage.js | 13 ++--------- .../dependencies/generateCommentaryEntry.js | 27 ++++++++++++++++++---- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 5b00acf..001003a 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -6,7 +6,6 @@ export default { 'generateAlbumNavAccent', 'generateAlbumSidebarTrackSection', 'generateAlbumStyleRules', - 'generateColorStyleVariables', 'generateCommentaryEntry', 'generateContentHeading', 'generateTrackCoverArtwork', @@ -68,13 +67,6 @@ export default { track.commentary .map(entry => relation('generateCommentaryEntry', entry))); - relations.trackCommentaryColorVariables = - tracksWithCommentary - .map(track => - (track.color === album.color - ? null - : relation('generateColorStyleVariables'))); - relations.sidebarAlbumLink = relation('linkAlbum', album); @@ -167,9 +159,8 @@ export default { directory: data.trackCommentaryDirectories, cover: relations.trackCommentaryCovers, entries: relations.trackCommentaryEntries, - colorVariables: relations.trackCommentaryColorVariables, color: data.trackCommentaryColors, - }).map(({heading, link, directory, cover, entries, colorVariables, color}) => [ + }).map(({heading, link, directory, cover, entries, color}) => [ heading.slots({ tag: 'h3', id: directory, @@ -178,7 +169,7 @@ export default { cover?.slots({mode: 'commentary'}), - entries, + entries.map(entry => entry.slot('color', color)), ]), ], diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index 22e8fd1..72e3065 100644 --- a/src/content/dependencies/generateCommentaryEntry.js +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -1,5 +1,10 @@ export default { - contentDependencies: ['linkArtist', 'transformContent'], + contentDependencies: [ + 'generateColorStyleVariables', + 'linkArtist', + 'transformContent', + ], + extraDependencies: ['html', 'language'], relations: (relation, entry) => ({ @@ -22,13 +27,20 @@ export default { (entry.body ? relation('transformContent', entry.body) : null), + + colorVariables: + relation('generateColorStyleVariables'), }), data: (entry) => ({ date: entry.date, }), - generate(data, relations, {html, language}) { + slots: { + color: {validate: v => v.isColor}, + }, + + generate(data, relations, slots, {html, language}) { const artistsSpan = html.tag('span', {class: 'commentary-entry-artists'}, (relations.artistsContent @@ -66,11 +78,18 @@ export default { titleOptions.accent = accent; } + const style = + (slots.color + ? relations.colorVariables + .slot('color', slots.color) + .content + : null); + return html.tags([ - html.tag('p', {class: 'commentary-entry-heading'}, + html.tag('p', {class: 'commentary-entry-heading', style}, language.$(...titleParts, titleOptions)), - html.tag('blockquote', {class: 'commentary-entry-body'}, + html.tag('blockquote', {class: 'commentary-entry-body', style}, relations.bodyContent.slot('mode', 'multiline')), ]); }, -- cgit 1.3.0-6-gf8a5 From c9c8ccfbeb6a013131dee429da0f12192364a8d0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 16 Nov 2023 14:27:15 -0400 Subject: content: generateCommentaryEntry: add missing noArtists string --- src/content/dependencies/generateCommentaryEntry.js | 2 +- src/strings-default.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index 72e3065..b265ed4 100644 --- a/src/content/dependencies/generateCommentaryEntry.js +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -47,7 +47,7 @@ export default { ? relations.artistsContent.slot('mode', 'inline') : relations.artistLink ? relations.artistLink - : language.$('misc.artistCommentary.noArtist'))); + : language.$('misc.artistCommentary.entry.title.noArtists'))); const accentParts = ['misc.artistCommentary.entry.title.accent']; const accentOptions = {}; diff --git a/src/strings-default.yaml b/src/strings-default.yaml index f83412e..5a3d172 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -353,6 +353,7 @@ misc: entry: title: _: "{ARTISTS}:" + noArtists: "Unknown artist" withAccent: "{ARTISTS}: {ACCENT}" accent: withAnnotation: "({ANNOTATION})" -- cgit 1.3.0-6-gf8a5 From fbcfb88ea407ab76d278c28f1f0711502ab2fbf9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 16 Nov 2023 17:38:17 -0400 Subject: content, css: quick visual fixes for commentary entries --- src/content/dependencies/transformContent.js | 4 +++- src/static/site5.css | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 3c2c352..dab8963 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -450,7 +450,9 @@ export default { // In inline mode, no further processing is needed! if (slots.mode === 'inline') { - return html.tags(contentFromNodes.map(node => node.data)); + return html.tags( + contentFromNodes.map(node => node.data), + {[html.joinChildren]: ''}); } // Multiline mode has a secondary processing stage where it's passed... diff --git a/src/static/site5.css b/src/static/site5.css index ba44ec3..dd16cbf 100644 --- a/src/static/site5.css +++ b/src/static/site5.css @@ -540,6 +540,7 @@ p .current { .commentary-entry-heading { margin-left: 15px; padding-left: 5px; + max-width: 625px; padding-bottom: 0.2em; border-bottom: 1px dotted var(--primary-color); } @@ -1700,7 +1701,7 @@ html[data-language-code="preview-en"][data-url-key="localized.home"] #content float: right; width: 40%; max-width: 400px; - margin: -60px 0 10px 10px; + margin: -60px 0 10px 20px; position: relative; z-index: 2; -- 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 --- .../dependencies/generateCommentaryEntry.js | 13 ++++--- .../wiki-data/withParsedCommentaryEntries.js | 41 +++++++++++++++++----- .../wiki-properties/commentatorArtists.js | 23 +++++++----- src/data/yaml.js | 24 ++++++++++--- src/util/wiki-data.js | 2 +- 5 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index b265ed4..0b2b255 100644 --- a/src/content/dependencies/generateCommentaryEntry.js +++ b/src/content/dependencies/generateCommentaryEntry.js @@ -1,3 +1,5 @@ +import {empty} from '#sugar'; + export default { contentDependencies: [ 'generateColorStyleVariables', @@ -8,9 +10,10 @@ export default { extraDependencies: ['html', 'language'], relations: (relation, entry) => ({ - artistLink: - (entry.artist && !entry.artistDisplayText - ? relation('linkArtist', entry.artist) + artistLinks: + (!empty(entry.artists) && !entry.artistDisplayText + ? entry.artists + .map(artist => relation('linkArtist', artist)) : null), artistsContent: @@ -45,8 +48,8 @@ export default { html.tag('span', {class: 'commentary-entry-artists'}, (relations.artistsContent ? relations.artistsContent.slot('mode', 'inline') - : relations.artistLink - ? relations.artistLink + : relations.artistLinks + ? language.formatConjunctionList(relations.artistLinks) : language.$('misc.artistCommentary.entry.title.noArtists'))); const accentParts = ['misc.artistCommentary.entry.title.accent']; diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js index 7b1c948..25c07a3 100644 --- a/src/data/composite/wiki-data/withParsedCommentaryEntries.js +++ b/src/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -4,7 +4,12 @@ import {stitchArrays} from '#sugar'; import {isCommentary} from '#validators'; import {commentaryRegex} from '#wiki-data'; -import {fillMissingListItems, withPropertiesFromList} from '#composite/data'; +import { + fillMissingListItems, + withFlattenedList, + withPropertiesFromList, + withUnflattenedList, +} from '#composite/data'; import withResolvedReferenceList from './withResolvedReferenceList.js'; @@ -86,23 +91,43 @@ export default templateCompositeFrom({ list: '#rawMatches.groups', prefix: input.value('#entries'), properties: input.value([ - 'artistReference', + 'artistReferences', 'artistDisplayText', 'annotation', 'date', ]), }), - // The artistReference group will always have a value, since it's required + // The artistReferences group will always have a value, since it's required // for the line to match in the first place. + { + dependencies: ['#entries.artistReferences'], + compute: (continuation, { + ['#entries.artistReferences']: artistReferenceTexts, + }) => continuation({ + ['#entries.artistReferences']: + artistReferenceTexts + .map(text => text.split(',').map(ref => ref.trim())), + }), + }, + + withFlattenedList({ + list: '#entries.artistReferences', + }), + withResolvedReferenceList({ - list: '#entries.artistReference', + list: '#flattenedList', data: 'artistData', find: input.value(find.artist), notFoundMode: input.value('null'), + }), + + withUnflattenedList({ + list: '#resolvedReferenceList', + filter: input.value(false), }).outputs({ - '#resolvedReferenceList': '#entries.artist', + '#unflattenedList': '#entries.artists', }), fillMissingListItems({ @@ -127,7 +152,7 @@ export default templateCompositeFrom({ { dependencies: [ - '#entries.artist', + '#entries.artists', '#entries.artistDisplayText', '#entries.annotation', '#entries.date', @@ -135,7 +160,7 @@ export default templateCompositeFrom({ ], compute: (continuation, { - ['#entries.artist']: artist, + ['#entries.artists']: artists, ['#entries.artistDisplayText']: artistDisplayText, ['#entries.annotation']: annotation, ['#entries.date']: date, @@ -143,7 +168,7 @@ export default templateCompositeFrom({ }) => continuation({ ['#parsedCommentaryEntries']: stitchArrays({ - artist, + artists, artistDisplayText, annotation, date, diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js index 8720e66..65ab146 100644 --- a/src/data/composite/wiki-properties/commentatorArtists.js +++ b/src/data/composite/wiki-properties/commentatorArtists.js @@ -4,8 +4,9 @@ import {input, templateCompositeFrom} from '#composite'; import {unique} from '#sugar'; -import {exitWithoutDependency} from '#composite/control-flow'; -import {withPropertyFromList} from '#composite/data'; +import {exitWithoutDependency, exposeDependency} + from '#composite/control-flow'; +import {withFlattenedList, withPropertyFromList} from '#composite/data'; import {withParsedCommentaryEntries} from '#composite/wiki-data'; export default templateCompositeFrom({ @@ -26,15 +27,19 @@ export default templateCompositeFrom({ withPropertyFromList({ list: '#parsedCommentaryEntries', - property: input.value('artist'), + property: input.value('artists'), }).outputs({ - '#parsedCommentaryEntries.artist': '#artists', + '#parsedCommentaryEntries.artists': '#artistLists', }), - { - dependencies: ['#artists'], - compute: ({'#artists': artists}) => - unique(artists.filter(artist => artist !== null)), - }, + withFlattenedList({ + list: '#artistLists', + }).outputs({ + '#flattenedList': '#artists', + }), + + exposeDependency({ + dependency: '#artists', + }), ], }); diff --git a/src/data/yaml.js b/src/data/yaml.js index 843e70b..0734d53 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -21,6 +21,7 @@ import { decorateErrorWithIndex, decorateErrorWithAnnotation, empty, + filterAggregate, filterProperties, openAggregate, showAggregate, @@ -1686,8 +1687,10 @@ export function filterReferenceErrors(wikiData) { if (value) { value = Array.from(value.matchAll(commentaryRegex)) - .map(({groups}) => groups.artistReference); + .map(({groups}) => groups.artistReferences) + .map(text => text.split(',').map(text => text.trim())); } + writeProperty = false; break; } @@ -1804,11 +1807,22 @@ export function filterReferenceErrors(wikiData) { let newPropertyValue = value; - if (Array.isArray(value)) { + if (findFnKey === '_commentary') { + // Commentary doesn't write a property value, so no need to set. + filter( + value, {message: errorMessage}, + decorateErrorWithIndex(refs => + (refs.length === 1 + ? suppress(findFn)(refs[0]) + : filterAggregate( + refs, {message: `Errors in entry's artist references`}, + decorateErrorWithIndex(suppress(findFn))) + .aggregate + .close()))); + } else if (Array.isArray(value)) { newPropertyValue = filter( - value, - decorateErrorWithIndex(suppress(findFn)), - {message: errorMessage}); + value, {message: errorMessage}, + decorateErrorWithIndex(suppress(findFn))); } else { nest({message: errorMessage}, suppress(({call}) => { diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 75a141d..4c7ec04 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 7da15227b8623a2fcef28c4f7988a2f89e5ab8b3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:22:08 -0400 Subject: content: transformContent: use marked for inline + own instances There's obviously lots of room to do more here, but this is a simple way to get typical inline contents passing through marked. --- src/content/dependencies/transformContent.js | 57 +++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index dab8963..7b2d057 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -1,7 +1,7 @@ import {bindFind} from '#find'; import {parseInput} from '#replacer'; -import {marked} from 'marked'; +import {Marked} from 'marked'; export const replacerSpec = { album: { @@ -147,6 +147,29 @@ const linkIndexRelationMap = { newsIndex: 'linkNewsIndex', }; +const commonMarkedOptions = { + headerIds: false, + mangle: false, +}; + +const multilineMarked = new Marked({ + ...commonMarkedOptions, +}); + +const inlineMarked = new Marked({ + ...commonMarkedOptions, + + renderer: { + paragraph(text) { + return text; + }, + }, +}); + +const lyricsMarked = new Marked({ + ...commonMarkedOptions, +}); + function getPlaceholder(node, content) { return {type: 'text', data: content.slice(node.i, node.iEnd)}; } @@ -447,21 +470,9 @@ export default { return link.data; } - // In inline mode, no further processing is needed! - - if (slots.mode === 'inline') { - return html.tags( - contentFromNodes.map(node => node.data), - {[html.joinChildren]: ''}); - } - - // Multiline mode has a secondary processing stage where it's passed... - // through marked! Rolling your own Markdown only gets you so far :D - - const markedOptions = { - headerIds: false, - mangle: false, - }; + // Content always goes through marked (i.e. parsing as Markdown). + // This does require some attention to detail, mostly to do with line + // breaks (in multiline mode) and extracting/re-inserting non-text nodes. // The content of non-text nodes can end up getting mangled by marked. // To avoid this, we replace them with mundane placeholders, then @@ -536,6 +547,16 @@ export default { return html.tags(tags, {[html.joinChildren]: ''}); }; + if (slots.mode === 'inline') { + const markedInput = + extractNonTextNodes(); + + const markedOutput = + inlineMarked.parse(markedInput); + + return reinsertNonTextNodes(markedOutput); + } + // This is separated into its own function just since we're gonna reuse // it in a minute if everything goes to heck in lyrics mode. const transformMultiline = () => { @@ -552,7 +573,7 @@ export default { .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n'); const markedOutput = - marked.parse(markedInput, markedOptions); + multilineMarked.parse(markedInput); return reinsertNonTextNodes(markedOutput); } @@ -602,7 +623,7 @@ export default { }); const markedOutput = - marked.parse(markedInput, markedOptions); + lyricsMarked.parse(markedInput); return reinsertNonTextNodes(markedOutput); } -- cgit 1.3.0-6-gf8a5 From 42a8275ff59d13757b0d6a5a19588d4f8393ab6f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:19:49 -0400 Subject: update min node to 20, canonize in package.json --- README.md | 2 +- package.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7fc582..2c99e3d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ HSMusic, short for the *Homestuck Music Wiki*, is a revitalization and reimagini Install dependencies: -- [Node.js](https://nodejs.org/en/) - we recommend using [nvm](https://github.com/nvm-sh/nvm) to install Node and keep easy track of any versions you've got installed; development is generally tested on latest but 16.x LTS should also work +- [Node.js](https://nodejs.org/en/) - we recommend using [nvm](https://github.com/nvm-sh/nvm) to install Node and keep easy track of any versions you've got installed; development is generally tested on latest but 20.x LTS should also work - [ImageMagick](https://imagemagick.org/) - check your package manager if it's available (e.g. apt or homebrew) or follow [installation info right from the official website](https://imagemagick.org/script/download.php) Make a new empty folder for storing all your HSMusic repositories, then clone 'em with git: diff --git a/package.json b/package.json index 34b4ebf..6f3323f 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,9 @@ "#wiki-data": "./src/util/wiki-data.js", "#yaml": "./src/data/yaml.js" }, + "engines": { + "node": ">= 20.9.0" + }, "dependencies": { "chroma-js": "^2.4.2", "command-exists": "^1.2.9", -- cgit 1.3.0-6-gf8a5 From f60c890fc4578879707bfdbb4e8219f78bd67b8b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:20:58 -0400 Subject: update marked from 5.0.2 to 10.0.0 --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6433ea1..9e8f4df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "he": "^1.2.0", "image-size": "^1.0.2", "js-yaml": "^4.1.0", - "marked": "^5.0.2", + "marked": "^10.0.0", "striptags": "^4.0.0-alpha.4", "word-wrap": "^1.2.3" }, @@ -3303,9 +3303,9 @@ } }, "node_modules/marked": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz", - "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-10.0.0.tgz", + "integrity": "sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA==", "bin": { "marked": "bin/marked.js" }, @@ -8096,9 +8096,9 @@ } }, "marked": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz", - "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-10.0.0.tgz", + "integrity": "sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA==" }, "mimic-fn": { "version": "2.1.0", diff --git a/package.json b/package.json index 6f3323f..719a5b8 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "he": "^1.2.0", "image-size": "^1.0.2", "js-yaml": "^4.1.0", - "marked": "^5.0.2", + "marked": "^10.0.0", "striptags": "^4.0.0-alpha.4", "word-wrap": "^1.2.3" }, -- 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(-) diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 4c7ec04..8a6d434 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(-) diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 8a6d434..09b3623 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(-) diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 09b3623..5e3182a 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 6fb9bbcb4d4806a4d31eb3d387dd229a598bc15d Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:38:33 -0400 Subject: test: withParsedCommentaryEntries: update + test multiple artists --- .../data/composite/wiki-data/withParsedCommentaryEntries.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js index 50570de..928a9c9 100644 --- a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js +++ b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -49,7 +49,7 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => { `Very cool.\n`, }), [ { - artist: artist1, + artists: [artist1], artistDisplayText: null, annotation: null, date: null, @@ -67,32 +67,32 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => { `Second commentary entry. Yes. So cool.\n` + `Mystery Artist: (pingas, August 25, 2023)\n` + `Oh no.. Oh dear...\n` + - `Mobius Trip:\n` + + `Mobius Trip, Hadron Kaleido:\n` + `And back around we go.`, }), [ { - artist: artist1, + artists: [artist1], artistDisplayText: `Moo-bius Trip`, annotation: `music, art`, date: new Date('12 January 2015'), body: `First commentary entry.\nVery cool.`, }, { - artist: artist2, + artists: [artist2], artistDisplayText: `[[artist:hadron-kaleido|The Ol' Hadron]]`, annotation: `moral support`, date: new Date('4 April 2022'), body: `Second commentary entry. Yes. So cool.`, }, { - artist: null, + artists: [], artistDisplayText: null, annotation: `pingas`, date: new Date('25 August 2023'), body: `Oh no.. Oh dear...`, }, { - artist: artist1, + artists: [artist1, artist2], artistDisplayText: null, annotation: null, date: null, -- cgit 1.3.0-6-gf8a5 From 9eef57746726faeeeb8e571b1794ec5c14023e97 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:53:35 -0400 Subject: test: withParsedCommentaryEntries: use stricter t.same() matching --- test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js index 928a9c9..babe4fa 100644 --- a/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js +++ b/test/unit/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -41,7 +41,7 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => { }, }); - t.match(composite.expose.compute({ + t.same(composite.expose.compute({ artistData, from: `Mobius Trip:\n` + @@ -57,7 +57,7 @@ t.test(`withParsedCommentaryEntries: basic behavior`, t => { }, ]); - t.match(composite.expose.compute({ + t.same(composite.expose.compute({ artistData, from: `Mobius Trip|Moo-bius Trip: (music, art, 12 January 2015)\n` + -- cgit 1.3.0-6-gf8a5 From 6cbd8ee458a25ed8608760f8867008aa8f55d5e3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:54:37 -0400 Subject: test: Track.commentatorArtists: update overall --- test/unit/data/things/track.js | 48 ++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/test/unit/data/things/track.js b/test/unit/data/things/track.js index 806efbf..571624a 100644 --- a/test/unit/data/things/track.js +++ b/test/unit/data/things/track.js @@ -214,7 +214,7 @@ t.test(`Track.color`, t => { }); t.test(`Track.commentatorArtists`, t => { - t.plan(6); + t.plan(8); const track = new Track(); const artist1 = stubArtist(`SnooPING`); @@ -226,47 +226,67 @@ t.test(`Track.commentatorArtists`, t => { artistData: [artist1, artist2, artist3], }); - track.commentary = + // Keep track of the last commentary string in a separate value, since + // the track.commentary property exposes as a completely different format + // (i.e. an array of objects, one for each entry), and so isn't compatible + // with the += operator on its own. + let commentary; + + track.commentary = commentary = `SnooPING:\n` + `Wow.\n`; t.same(track.commentatorArtists, [artist1], `Track.commentatorArtists #1: works with one commentator`); - track.commentary += + track.commentary = commentary += `ASUsual:\n` + `Yes!\n`; t.same(track.commentatorArtists, [artist1, artist2], `Track.commentatorArtists #2: works with two commentators`); - track.commentary += - `Icy:\n` + + track.commentary = commentary += + `Icy|Icy What You Did There:\n` + `Incredible.\n`; t.same(track.commentatorArtists, [artist1, artist2, artist3], - `Track.commentatorArtists #3: works with boldface name`); + `Track.commentatorArtists #3: works with custom artist text`); - track.commentary = + track.commentary = commentary = `Icy: (project manager)\n` + `Very good track.\n`; t.same(track.commentatorArtists, [artist3], - `Track.commentatorArtists #4: works with parenthical accent`); + `Track.commentatorArtists #4: works with annotation`); - track.commentary += - `SNooPING ASUsual Icy:\n` + - `WITH ALL THREE POWERS COMBINED...`; + track.commentary = commentary = + `Icy: (project manager, 08/15/2023)\n` + + `Very very good track.\n`; + + t.same(track.commentatorArtists, [artist3], + `Track.commentatorArtists #5: works with date`); + + track.commentary = commentary += + `Ohohohoho:\n` + + `OHOHOHOHOHOHO...\n`; t.same(track.commentatorArtists, [artist3], - `Track.commentatorArtists #5: ignores artist names not found`); + `Track.commentatorArtists #6: ignores artist names not found`); - track.commentary += + track.commentary = commentary += `Icy:\n` + `I'm back!\n`; t.same(track.commentatorArtists, [artist3], - `Track.commentatorArtists #6: ignores duplicate artist`); + `Track.commentatorArtists #7: ignores duplicate artist`); + + track.commentary = commentary += + `SNooPING, ASUsual, Icy:\n` + + `WITH ALL THREE POWERS COMBINED...`; + + t.same(track.commentatorArtists, [artist3, artist1, artist2], + `Track.commentatorArtists #8: works with more than one artist in one entry`); }); t.test(`Track.coverArtistContribs`, t => { -- cgit 1.3.0-6-gf8a5 From f82b74f62595c4def21b176c366c703da02c331e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 20:15:07 -0400 Subject: data, test: withUniqueItemsOnly (#composite/data) --- src/data/composite/data/index.js | 1 + src/data/composite/data/withUniqueItemsOnly.js | 40 +++++++++++ .../data/composite/data/withUniqueItemsOnly.js | 84 ++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 src/data/composite/data/withUniqueItemsOnly.js create mode 100644 test/unit/data/composite/data/withUniqueItemsOnly.js diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js index db1c37c..e2927af 100644 --- a/src/data/composite/data/index.js +++ b/src/data/composite/data/index.js @@ -11,3 +11,4 @@ export {default as withPropertiesFromObject} from './withPropertiesFromObject.js export {default as withPropertyFromList} from './withPropertyFromList.js'; export {default as withPropertyFromObject} from './withPropertyFromObject.js'; export {default as withUnflattenedList} from './withUnflattenedList.js'; +export {default as withUniqueItemsOnly} from './withUniqueItemsOnly.js'; diff --git a/src/data/composite/data/withUniqueItemsOnly.js b/src/data/composite/data/withUniqueItemsOnly.js new file mode 100644 index 0000000..7ee08b0 --- /dev/null +++ b/src/data/composite/data/withUniqueItemsOnly.js @@ -0,0 +1,40 @@ +// Excludes duplicate items from a list and provides the results, overwriting +// the list in-place, if possible. + +import {input, templateCompositeFrom} from '#composite'; +import {unique} from '#sugar'; + +export default templateCompositeFrom({ + annotation: `withUniqueItemsOnly`, + + inputs: { + list: input({type: 'array'}), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list ?? '#uniqueItems'], + + steps: () => [ + { + dependencies: [input('list')], + compute: (continuation, { + [input('list')]: list, + }) => continuation({ + ['#values']: + unique(list), + }), + }, + + { + dependencies: ['#values', input.staticDependency('list')], + compute: (continuation, { + '#values': values, + [input.staticDependency('list')]: list, + }) => continuation({ + [list ?? '#uniqueItems']: + values, + }), + }, + ], +}); diff --git a/test/unit/data/composite/data/withUniqueItemsOnly.js b/test/unit/data/composite/data/withUniqueItemsOnly.js new file mode 100644 index 0000000..965b14b --- /dev/null +++ b/test/unit/data/composite/data/withUniqueItemsOnly.js @@ -0,0 +1,84 @@ +import t from 'tap'; + +import {compositeFrom, input} from '#composite'; +import {exposeDependency} from '#composite/control-flow'; +import {withUniqueItemsOnly} from '#composite/data'; + +t.test(`withUniqueItemsOnly: basic behavior`, t => { + t.plan(3); + + const composite = compositeFrom({ + compose: false, + + steps: [ + withUniqueItemsOnly({ + list: 'list', + }), + + exposeDependency({dependency: '#list'}), + ], + }); + + t.match(composite, { + expose: { + dependencies: ['list'], + }, + }); + + t.same(composite.expose.compute({ + list: ['apple', 'banana', 'banana', 'banana', 'apple', 'watermelon'], + }), ['apple', 'banana', 'watermelon']); + + t.same(composite.expose.compute({ + list: [], + }), []); +}); + +t.test(`withUniqueItemsOnly: output shapes & values`, t => { + t.plan(2 * 3 ** 1); + + const dependencies = { + ['list_dependency']: + [1, 1, 2, 3, 3, 4, 'foo', false, false, 4], + [input('list_neither')]: + [8, 8, 7, 6, 6, 5, 'bar', true, true, 5], + }; + + const mapLevel1 = [ + ['list_dependency', { + '#list_dependency': [1, 2, 3, 4, 'foo', false], + }], + [input.value([-1, -1, 'interesting', 'very', 'interesting']), { + '#uniqueItems': [-1, 'interesting', 'very'], + }], + [input('list_neither'), { + '#uniqueItems': [8, 7, 6, 5, 'bar', true], + }], + ]; + + for (const [listInput, outputDict] of mapLevel1) { + const step = withUniqueItemsOnly({ + list: listInput, + }); + + quickCheckOutputs(step, outputDict); + } + + function quickCheckOutputs(step, outputDict) { + t.same( + Object.keys(step.toDescription().outputs), + Object.keys(outputDict)); + + const composite = compositeFrom({ + compose: false, + steps: [step, { + dependencies: Object.keys(outputDict), + compute: dependencies => dependencies, + }], + }); + + t.same( + composite.expose.compute(dependencies), + outputDict); + } +}); -- cgit 1.3.0-6-gf8a5 From 5b8060bb86d457a0d23b607aa866c4d7d6eb6f0f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 20:16:40 -0400 Subject: data: withParsedCommentaryEntries: filter out null artists --- src/data/composite/wiki-data/withParsedCommentaryEntries.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js index 25c07a3..edfc9e3 100644 --- a/src/data/composite/wiki-data/withParsedCommentaryEntries.js +++ b/src/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -125,7 +125,6 @@ export default templateCompositeFrom({ withUnflattenedList({ list: '#resolvedReferenceList', - filter: input.value(false), }).outputs({ '#unflattenedList': '#entries.artists', }), -- cgit 1.3.0-6-gf8a5 From e35d23f4e9492b497138dce3f21382872e329e71 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 20:16:57 -0400 Subject: data: commentatorArtists: filter out duplicate artists --- src/data/composite/wiki-properties/commentatorArtists.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js index 65ab146..f400bbf 100644 --- a/src/data/composite/wiki-properties/commentatorArtists.js +++ b/src/data/composite/wiki-properties/commentatorArtists.js @@ -6,7 +6,8 @@ import {unique} from '#sugar'; import {exitWithoutDependency, exposeDependency} from '#composite/control-flow'; -import {withFlattenedList, withPropertyFromList} from '#composite/data'; +import {withFlattenedList, withPropertyFromList, withUniqueItemsOnly} + from '#composite/data'; import {withParsedCommentaryEntries} from '#composite/wiki-data'; export default templateCompositeFrom({ @@ -38,6 +39,10 @@ export default templateCompositeFrom({ '#flattenedList': '#artists', }), + withUniqueItemsOnly({ + list: '#artists', + }), + exposeDependency({ dependency: '#artists', }), -- cgit 1.3.0-6-gf8a5 From ee02bc3efebf992c47694ec4065f658473b1f904 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 13:32:10 -0400 Subject: data: validateArrayItems: annotate multiline errors nicely --- package-lock.json | 14 ++++++++++++++ package.json | 1 + src/data/things/validators.js | 17 ++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 9e8f4df..bca46bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "image-size": "^1.0.2", "js-yaml": "^4.1.0", "marked": "^10.0.0", + "printable-characters": "^1.0.42", "striptags": "^4.0.0-alpha.4", "word-wrap": "^1.2.3" }, @@ -26,6 +27,9 @@ "chokidar": "^3.5.3", "tap": "^18.4.0", "tcompare": "^6.0.0" + }, + "engines": { + "node": ">= 20.9.0" } }, "node_modules/@alcalzone/ansi-tokenize": { @@ -3954,6 +3958,11 @@ "node": ">= 0.8.0" } }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" + }, "node_modules/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -8576,6 +8585,11 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, + "printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==" + }, "prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", diff --git a/package.json b/package.json index 719a5b8..b7467fd 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "image-size": "^1.0.2", "js-yaml": "^4.1.0", "marked": "^10.0.0", + "printable-characters": "^1.0.42", "striptags": "^4.0.0-alpha.4", "word-wrap": "^1.2.3" }, diff --git a/src/data/things/validators.js b/src/data/things/validators.js index f60c363..e213e93 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -1,5 +1,9 @@ import {inspect as nodeInspect} from 'node:util'; +// Heresy. +import printable_characters from 'printable-characters'; +const {strlen} = printable_characters; + import {colors, ENABLE_COLOR} from '#cli'; import {empty, typeAppearance, withAggregate} from '#sugar'; @@ -174,8 +178,19 @@ function validateArrayItemsHelper(itemValidator) { throw new Error(`Expected validator to return true`); } } catch (error) { - error.message = `(index: ${colors.yellow(`${index}`)}, item: ${inspect(item)}) ${error.message}`; + const annotation = `(index: ${colors.yellow(`${index}`)}, item: ${inspect(item)})`; + + error.message = + (error.message.includes('\n') || strlen(annotation) > 20 + ? annotation + '\n' + + error.message + .split('\n') + .map(line => ` ${line}`) + .join('\n') + : `${annotation} ${error}`); + error[Symbol.for('hsmusic.decorate.indexInSourceArray')] = index; + throw error; } }; -- cgit 1.3.0-6-gf8a5 From 2d58b70d0bd5bbc7cdd8789332a31a220c78da01 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 13:48:52 -0400 Subject: data: validateArrayItems (etc): pass through index, array --- src/data/things/validators.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/data/things/validators.js b/src/data/things/validators.js index e213e93..2893e7f 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -170,9 +170,9 @@ export function is(...values) { } function validateArrayItemsHelper(itemValidator) { - return (item, index) => { + return (item, index, array) => { try { - const value = itemValidator(item); + const value = itemValidator(item, index, array); if (value !== true) { throw new Error(`Expected validator to return true`); @@ -197,13 +197,15 @@ function validateArrayItemsHelper(itemValidator) { } export function validateArrayItems(itemValidator) { - const fn = validateArrayItemsHelper(itemValidator); + const helper = validateArrayItemsHelper(itemValidator); return (array) => { isArray(array); - withAggregate({message: 'Errors validating array items'}, ({wrap}) => { - array.forEach(wrap(fn)); + withAggregate({message: 'Errors validating array items'}, ({call}) => { + for (let index = 0; index < array.length; index++) { + call(helper, array[index], index, array); + } }); return true; @@ -215,12 +217,12 @@ export function strictArrayOf(itemValidator) { } export function sparseArrayOf(itemValidator) { - return validateArrayItems(item => { + return validateArrayItems((item, index, array) => { if (item === false || item === null) { return true; } - return itemValidator(item); + return itemValidator(item, index, array); }); } -- 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(+) diff --git a/src/util/sugar.js b/src/util/sugar.js index 9646be3..ad67691 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 9a35630d91ebd7227994a1009487794a65ec38e1 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 13:53:36 -0400 Subject: data: tidy yaml error message construction, cut long strings ...Using maxStringLength, which is more than a bit annoying, because this isn't the same cut() algorithm we just added, looks bulkier, and can't be customized. But that's the cost of using util.inspect() here. It's better than displaying the entire long string or handling line breaks poorly. FOR NOW. --- src/data/yaml.js | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/data/yaml.js b/src/data/yaml.js index 0734d53..49e0526 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -38,8 +38,8 @@ import { // --> General supporting stuff -function inspect(value) { - return nodeInspect(value, {colors: ENABLE_COLOR}); +function inspect(value, opts = {}) { + return nodeInspect(value, {colors: ENABLE_COLOR, ...opts}); } // --> YAML data repository structure constants @@ -308,7 +308,12 @@ export class FieldCombinationError extends Error { constructor(fields, message) { const fieldNames = Object.keys(fields); - const mainMessage = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; + const fieldNamesText = + fieldNames + .map(field => colors.red(field)) + .join(', '); + + const mainMessage = `Don't combine ${fieldNamesText}`; const causeMessage = (typeof message === 'function' @@ -330,7 +335,12 @@ export class FieldCombinationError extends Error { export class FieldValueAggregateError extends AggregateError { constructor(thingConstructor, errors) { - super(errors, `Errors processing field values for ${colors.green(thingConstructor.name)}`); + const constructorText = + colors.green(thingConstructor.name); + + super( + errors, + `Errors processing field values for ${constructorText}`); } } @@ -341,8 +351,17 @@ export class FieldValueError extends Error { ? caughtError.cause : caughtError); + const fieldText = + colors.green(`"${field}"`); + + const propertyText = + colors.green(property); + + const valueText = + inspect(value, {maxStringLength: 40}); + super( - `Failed to set ${colors.green(`"${field}"`)} field (${colors.green(property)}) to ${inspect(value)}`, + `Failed to set ${fieldText} field (${propertyText}) to ${valueText}`, {cause}); } } @@ -354,13 +373,18 @@ export class SkippedFieldsSummaryError extends Error { const lines = entries.map(([field, value]) => ` - ${field}: ` + - inspect(value) + inspect(value, {maxStringLength: 70}) .split('\n') .map((line, index) => index === 0 ? line : ` ${line}`) .join('\n')); + const numFieldsText = + (entries.length === 1 + ? `1 field` + : `${entries.length} fields`); + super( - colors.bright(colors.yellow(`Altogether, skipped ${entries.length === 1 ? `1 field` : `${entries.length} fields`}:\n`)) + + colors.bright(colors.yellow(`Altogether, skipped ${numFieldsText}:\n`)) + lines.join('\n') + '\n' + colors.bright(colors.yellow(`See above errors for details.`))); } -- 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/data/things/validators.js | 59 +++++++++++++++++++++++++++++++++++-------- src/util/wiki-data.js | 14 +++++++--- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 2893e7f..569a7b3 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -5,7 +5,8 @@ import printable_characters from 'printable-characters'; const {strlen} = printable_characters; import {colors, ENABLE_COLOR} from '#cli'; -import {empty, typeAppearance, withAggregate} from '#sugar'; +import {cut, empty, typeAppearance, withAggregate} from '#sugar'; +import {commentaryRegex} from '#wiki-data'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -248,18 +249,56 @@ export function isColor(color) { throw new TypeError(`Unknown color format`); } -export function isCommentary(commentary) { - isString(commentary); +export function isCommentary(commentaryText) { + isString(commentaryText); - const [firstLine] = commentary.match(/.*/); - if (!firstLine.replace(/<\/b>/g, '').includes(':')) { - throw new TypeError(`Missing commentary citation: "${ - firstLine.length > 40 - ? firstLine.slice(0, 40) + '...' - : firstLine - }"`); + const rawMatches = + Array.from(commentaryText.matchAll(commentaryRegex)); + + if (empty(rawMatches)) { + throw new TypeError(`Expected at least one commentary heading`); } + const niceMatches = + rawMatches.map(match => ({ + position: match.index, + length: match[0].length, + })); + + validateArrayItems(({position, length}, index) => { + if (index === 0 && position > 0) { + throw new TypeError(`Expected first commentary heading to be at top`); + } + + const ownInput = commentaryText.slice(position, position + length); + const restOfInput = commentaryText.slice(position + length); + const nextLineBreak = restOfInput.indexOf('\n'); + const upToNextLineBreak = restOfInput.slice(0, nextLineBreak); + + if (/\S/.test(upToNextLineBreak)) { + throw new TypeError( + `Expected commentary heading to occupy entire line, got extra text:\n` + + `${colors.green(`"${cut(ownInput, 40)}"`)} (<- heading)\n` + + `(extra on same line ->) ${colors.red(`"${cut(upToNextLineBreak, 30)}"`)}\n` + + `(Check for missing "|-" in YAML, or a misshapen annotation)`); + } + + const nextHeading = + (index === niceMatches.length - 1 + ? commentaryText.length + : niceMatches[index + 1].position); + + const upToNextHeading = + commentaryText.slice(position + length, nextHeading); + + if (!/\S/.test(upToNextHeading)) { + throw new TypeError( + `Expected commentary entry to have body text, only got a heading`); + } + + return true; + })(niceMatches); + return true; } diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 5e3182a..b5813c7 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/data/yaml.js | 9 +++++++-- src/util/sugar.js | 14 +++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/data/yaml.js b/src/data/yaml.js index 49e0526..dddf5fb 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -334,6 +334,8 @@ export class FieldCombinationError extends Error { } export class FieldValueAggregateError extends AggregateError { + [Symbol.for('hsmusic.aggregate.translucent')] = true; + constructor(thingConstructor, errors) { const constructorText = colors.green(thingConstructor.name); @@ -1162,7 +1164,10 @@ export async function loadAndProcessDataDocuments({dataPath}) { for (const dataStep of dataSteps) { await processDataAggregate.nestAsync( - {message: `Errors during data step: ${colors.bright(dataStep.title)}`}, + { + message: `Errors during data step: ${colors.bright(dataStep.title)}`, + translucent: true, + }, async ({call, callAsync, map, mapAsync, push}) => { const {documentMode} = dataStep; @@ -1407,7 +1412,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { switch (documentMode) { case documentModes.headerAndEntries: - map(yamlResults, {message: `Errors processing documents in data files`}, + map(yamlResults, {message: `Errors processing documents in data files`, translucent: true}, decorateErrorWithFile(({documents}) => { const headerDocument = documents[0]; const entryDocuments = documents.slice(1).filter(Boolean); diff --git a/src/util/sugar.js b/src/util/sugar.js index ad67691..6c41bca 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(-) diff --git a/src/util/sugar.js b/src/util/sugar.js index 6c41bca..c4d4c12 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 9f58ba688c8a6ac3acf7b4bc435e2ccaed20b011 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 24 Nov 2023 15:01:33 -0400 Subject: test: isCommentary: basic unit test update --- test/unit/data/things/validators.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/unit/data/things/validators.js b/test/unit/data/things/validators.js index bb33bf8..aa56a10 100644 --- a/test/unit/data/things/validators.js +++ b/test/unit/data/things/validators.js @@ -149,13 +149,18 @@ t.test('isColor', t => { }); t.test('isCommentary', t => { - t.plan(6); + t.plan(9); + + // TODO: Test specific error messages. t.ok(isCommentary(`Toby Fox:\ndogsong.mp3`)); - t.ok(isCommentary(`Technically, this works:`)); - t.ok(isCommentary(`Whodunnit:`)); - t.throws(() => isCommentary(123), TypeError); - t.throws(() => isCommentary(``), TypeError); - t.throws(() => isCommentary(`Toby Fox:`)); + t.ok(isCommentary(`Toby Fox: (music)\ndogsong.mp3`)); + t.throws(() => isCommentary(`dogsong.mp3\nToby Fox:\ndogsong.mp3`)); + t.throws(() => isCommentary(`Toby Fox: dogsong.mp3`)); + t.throws(() => isCommentary(`Toby Fox: (music) dogsong.mp3`)); + t.throws(() => isCommentary(`I Have Nothing To Say:`)); + t.throws(() => isCommentary(123)); + t.throws(() => isCommentary(``)); + t.throws(() => isCommentary(`Technically, ah, er:\nCorrect`)); }); t.test('isContribution', t => { -- cgit 1.3.0-6-gf8a5