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 --- 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 +- 6 files changed, 8 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/data/things/index.js b/src/data/things/index.js index 4ea1f007..d1143b0a 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 1d35bae8..986f25d1 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 dfcaa9aa..4d3e996a 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 1bbcb9c1..e6c1f5c2 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 ead01567..26879be4 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 408ad884..3d7da800 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'; -- 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(+) (limited to 'src') diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js index dfc53db7..7fad88b2 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 ecd05129..db1c37cc 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 1d0400fc..df50a2db 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 2462b047..3a8b51d5 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 +++- 2 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 src/data/composite/wiki-data/withParsedCommentaryEntries.js (limited to 'src') diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js new file mode 100644 index 00000000..5bd72dc9 --- /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 fbea9d5c..cd6b7ac4 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', + }), + ], +}); -- 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(-) (limited to 'src') diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js index 52aeb868..8720e66d 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(-) (limited to 'src') diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js index 5bd72dc9..9e33cdac 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 986f25d1..843e70b3 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 0790ae91..5ab01225 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -629,6 +629,31 @@ export function sortFlashesChronologically(data, { // Specific data utilities +// Matches heading details from commentary data in roughly the format: +// +// artistReference: (annotation, date) +// +// where capturing group "annotation" can be any text at all, except that the +// last entry (past a comma or the only content within parentheses), if parsed +// as a date, is the capturing group "date". "Parsing as a date" means one of +// these formats: +// +// * "25 December 2019" - one or two number digits, followed by any text, +// followed by four number digits +// * "12/25/2019" - one or two number digits, a slash, one or two number +// digits, a slash, and two to four number digits +// +// The artist reference can optionally be boldface (in ), which will be +// captured as non-null in "boldfaceArtist". Otherwise it is all the characters +// between and and is captured in "artistReference" and is either the +// name of an artist or an "artist:directory"-style reference. +// +// This regular expression *doesn't* match bodies, which will need to be parsed +// out of the original string based on the indices matched using this. +// +export const commentaryRegex = + /^(?)?(?.+):(?:<\/b>)?<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; + export function filterAlbumsByCommentary(albums) { return albums .filter((album) => [album, ...album.tracks].some((x) => x.commentary)); -- cgit 1.3.0-6-gf8a5 From f754a8d9187e435a761db31b5053aa2e7ba22e13 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 14 Nov 2023 23:36:37 -0400 Subject: data, test: boldfaceArtist -> artistDisplayText --- .../wiki-data/withParsedCommentaryEntries.js | 21 ++++++++------------- src/util/wiki-data.js | 12 ++++++------ 2 files changed, 14 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js index 9e33cdac..7b1c9484 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 5ab01225..75a141d3 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -629,9 +629,10 @@ export function sortFlashesChronologically(data, { // Specific data utilities -// Matches heading details from commentary data in roughly the format: +// Matches heading details from commentary data in roughly the formats: // // artistReference: (annotation, date) +// artistReference|artistDisplayText: (annotation, date) // // where capturing group "annotation" can be any text at all, except that the // last entry (past a comma or the only content within parentheses), if parsed @@ -643,16 +644,15 @@ export function sortFlashesChronologically(data, { // * "12/25/2019" - one or two number digits, a slash, one or two number // digits, a slash, and two to four number digits // -// The artist reference can optionally be boldface (in ), which will be -// captured as non-null in "boldfaceArtist". Otherwise it is all the characters -// between and and is captured in "artistReference" and is either the -// name of an artist or an "artist:directory"-style reference. +// Capturing group "artistReference" is all the characters between and +// (apart from the pipe and "artistDisplayText" text, if present), and is either +// the name of an artist or an "artist:directory"-style reference. // // This regular expression *doesn't* match bodies, which will need to be parsed // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?)?(?.+):(?:<\/b>)?<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From 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(-) (limited to 'src') diff --git a/src/data/serialize.js b/src/data/serialize.js index 52aacb07..8cac3309 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 af3eb042..63ec1140 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 (limited to 'src') diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index 5fe27caf..90a120ca 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 00000000..d08c3c90 --- /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 00000000..22e8fd1e --- /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 93334948..200cf054 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 014e6d25..1ffe5044 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 a21758e7..eccfc80c 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 (limited to 'src') diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js new file mode 100644 index 00000000..22e8fd1e --- /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 d08c3c90..8ae1b2d0 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 22e8fd1e..00000000 --- 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(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index e2415516..5b00acf7 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(-) (limited to 'src') diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index 5b00acf7..001003ae 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 22e8fd1e..72e30657 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(-) (limited to 'src') diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index 72e30657..b265ed41 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 f83412e9..5a3d172f 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(-) (limited to 'src') diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 3c2c3521..dab89630 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 ba44ec37..dd16cbf9 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(-) (limited to 'src') diff --git a/src/content/dependencies/generateCommentaryEntry.js b/src/content/dependencies/generateCommentaryEntry.js index b265ed41..0b2b2558 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 7b1c9484..25c07a37 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 8720e66d..65ab1466 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 843e70b3..0734d539 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 75a141d3..4c7ec043 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -652,7 +652,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From 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(-) (limited to 'src') diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index dab89630..7b2d0573 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 4b7da4c1f8c359e5c82c4cc5e0cfb78f8204850f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:21:31 -0400 Subject: data: parse commentary heading contents to end of line --- src/util/wiki-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 4c7ec043..8a6d4345 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -652,7 +652,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=[,)]))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?$/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From 522c982bf0b5a0bd39512eb56a9d0d8d8feea44e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:30:27 -0400 Subject: data: looser commentary date parsing + clearer regex explanation --- src/util/wiki-data.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 8a6d4345..09b3623e 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -641,8 +641,10 @@ export function sortFlashesChronologically(data, { // // * "25 December 2019" - one or two number digits, followed by any text, // followed by four number digits -// * "12/25/2019" - one or two number digits, a slash, one or two number -// digits, a slash, and two to four number digits +// * "December 25, 2019" - one all-letters word, a space, one or two number +// digits, a comma, and four number digits +// * "12/25/2019" etc - three sets of one to four number digits, separated +// by slashes (only valid formats are MM/DD/YYYY and YYYY/MM/DD) // // Capturing group "artistReference" is all the characters between and // (apart from the pipe and "artistDisplayText" text, if present), and is either @@ -652,7 +654,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}))?\))?$/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}\/[0-9]{1,4}\/[0-9]{1,4}))?\))?$/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From e874f7e4d0df5fed25d4c359c8cb403e67061e59 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 18 Nov 2023 19:33:38 -0400 Subject: data: support dash-style short dates in commentary dates --- src/util/wiki-data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index 09b3623e..5e3182a9 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -644,7 +644,7 @@ export function sortFlashesChronologically(data, { // * "December 25, 2019" - one all-letters word, a space, one or two number // digits, a comma, and four number digits // * "12/25/2019" etc - three sets of one to four number digits, separated -// by slashes (only valid formats are MM/DD/YYYY and YYYY/MM/DD) +// by slashes or dashes (only valid orders are MM/DD/YYYY and YYYY/MM/DD) // // Capturing group "artistReference" is all the characters between and // (apart from the pipe and "artistDisplayText" text, if present), and is either @@ -654,7 +654,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}\/[0-9]{1,4}\/[0-9]{1,4}))?\))?$/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4}))?\))?$/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From 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 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/data/composite/data/withUniqueItemsOnly.js (limited to 'src') diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js index db1c37cc..e2927afd 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 00000000..7ee08b08 --- /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, + }), + }, + ], +}); -- 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(-) (limited to 'src') diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js index 25c07a37..edfc9e3c 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(-) (limited to 'src') diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js index 65ab1466..f400bbfc 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 --- src/data/things/validators.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index f60c363c..e213e933 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(-) (limited to 'src') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index e213e933..2893e7fd 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(+) (limited to 'src') diff --git a/src/util/sugar.js b/src/util/sugar.js index 9646be37..ad676914 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -250,6 +250,16 @@ export function typeAppearance(value) { return typeof value; } +// Limits a string to the desired length, filling in an ellipsis at the end +// if it cuts any text off. +export function cut(text, length = 40) { + if (text.length >= length) { + return text.slice(0, Math.max(1, length - 3)) + '...'; + } else { + return text; + } +} + // Binds default values for arguments in a {key: value} type function argument // (typically the second argument, but may be overridden by providing a // [bindOpts.bindIndex] argument). Typically useful for preparing a function for -- cgit 1.3.0-6-gf8a5 From 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(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index 0734d539..49e05266 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(-) (limited to 'src') diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 2893e7fd..569a7b34 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 5e3182a9..b5813c7a 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -636,8 +636,8 @@ export function sortFlashesChronologically(data, { // // where capturing group "annotation" can be any text at all, except that the // last entry (past a comma or the only content within parentheses), if parsed -// as a date, is the capturing group "date". "Parsing as a date" means one of -// these formats: +// as a date, is the capturing group "date". "Parsing as a date" means matching +// one of these formats: // // * "25 December 2019" - one or two number digits, followed by any text, // followed by four number digits @@ -646,6 +646,14 @@ export function sortFlashesChronologically(data, { // * "12/25/2019" etc - three sets of one to four number digits, separated // by slashes or dashes (only valid orders are MM/DD/YYYY and YYYY/MM/DD) // +// Note that the annotation and date are always wrapped by one opening and one +// closing parentheses. The whole heading does NOT need to match the entire +// line it occupies (though it does always start at the first position on that +// line), and if there is more than one closing parenthesis on the line, the +// annotation will always cut off only at the last parenthesis, or a comma +// preceding a date and then the last parenthesis. This is to ensure that +// parentheses can be part of the actual annotation content. +// // Capturing group "artistReference" is all the characters between and // (apart from the pipe and "artistDisplayText" text, if present), and is either // the name of an artist or an "artist:directory"-style reference. @@ -654,7 +662,7 @@ export function sortFlashesChronologically(data, { // out of the original string based on the indices matched using this. // export const commentaryRegex = - /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4}))?\))?$/gm; + /^(?.+?)(?:\|(?.+))?:<\/i>(?: \((?(?:.*?(?=,|\)[^)]*$))*?)(?:,? ?(?[a-zA-Z]+ [0-9]{1,2}, [0-9]{4,4}|[0-9]{1,2} [^,]*[0-9]{4,4}|[0-9]{1,4}[-/][0-9]{1,4}[-/][0-9]{1,4}))?\))?/gm; export function filterAlbumsByCommentary(albums) { return albums -- cgit 1.3.0-6-gf8a5 From f87fa920f91d36424e4613ac5da50f46418f4b19 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 14:31:58 -0400 Subject: data, util: principle "translucent errors" & applications --- src/data/yaml.js | 9 +++++++-- src/util/sugar.js | 14 +++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/data/yaml.js b/src/data/yaml.js index 49e05266..dddf5fb2 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 ad676914..6c41bca2 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -325,6 +325,12 @@ export function openAggregate({ // constructed. message = '', + // Optional flag to indicate that this layer of the aggregate error isn't + // generally useful outside of developer debugging purposes - it will be + // skipped by default when using showAggregate, showing contained errors + // inline with other children of this aggregate's parent. + translucent = false, + // Value to return when a provided function throws an error. If this is a // function, it will be called with the arguments given to the function. // (This is primarily useful when wrapping a function and then providing it @@ -407,7 +413,13 @@ export function openAggregate({ aggregate.close = () => { if (errors.length) { - throw Reflect.construct(errorClass, [errors, message]); + const error = Reflect.construct(errorClass, [errors, message]); + + if (translucent) { + error[Symbol.for(`hsmusic.aggregate.translucent`)] = true; + } + + throw error; } }; -- cgit 1.3.0-6-gf8a5 From 08e18be60cf20ee6d69701c88676d3577c8f431e Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 20 Nov 2023 14:32:27 -0400 Subject: sugar: translucent errors implementation --- src/util/sugar.js | 122 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/util/sugar.js b/src/util/sugar.js index 6c41bca2..c4d4c129 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -592,34 +592,101 @@ export function _withAggregate(mode, aggregateOpts, fn) { export function showAggregate(topError, { pathToFileURL = f => f, showTraces = true, + showTranslucent = showTraces, print = true, } = {}) { - const recursive = (error, {level}) => { - let headerPart = showTraces - ? `[${error.constructor.name || 'unnamed'}] ${ - error.message || '(no message)' - }` - : error instanceof AggregateError - ? `[${error.message || '(no message)'}]` - : error.message || '(no message)'; + const translucentSymbol = Symbol.for('hsmusic.aggregate.translucent'); + + const determineCause = error => { + let cause = error.cause; + if (showTranslucent) return cause ?? null; + + while (cause) { + if (!cause[translucentSymbol]) return cause; + cause = cause.cause; + } + + return null; + }; + + const determineErrors = parentError => { + if (!parentError.errors) return null; + if (showTranslucent) return parentError.errors; + + const errors = []; + for (const error of parentError.errors) { + if (!error[translucentSymbol]) { + errors.push(error); + continue; + } + + if (error.cause) { + errors.push(determineCause(error)); + } + + if (error.errors) { + errors.push(...determineErrors(error)); + } + } + + return errors; + }; + + const flattenErrorStructure = (error, level = 0) => { + const cause = determineCause(error); + const errors = determineErrors(error); + + return { + level, + + kind: error.constructor.name, + message: error.message, + stack: error.stack, + + cause: + (cause + ? flattenErrorStructure(cause, level + 1) + : null), + + errors: + (errors + ? errors.map(error => flattenErrorStructure(error, level + 1)) + : null), + }; + }; + + const recursive = ({level, kind, message, stack, cause, errors}) => { + const messagePart = + message || `(no message)`; + + const kindPart = + kind || `unnamed kind`; + + let headerPart = + (showTraces + ? `[${kindPart}] ${messagePart}` + : errors + ? `[${messagePart}]` + : messagePart); if (showTraces) { - const stackLines = error.stack?.split('\n'); + const stackLines = + stack?.split('\n'); - const stackLine = stackLines?.find( - (line) => + const stackLine = + stackLines?.find(line => line.trim().startsWith('at') && !line.includes('sugar') && !line.includes('node:') && - !line.includes('') - ); + !line.includes('')); - const tracePart = stackLine - ? '- ' + - stackLine - .trim() - .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match)) - : '(no stack trace)'; + const tracePart = + (stackLine + ? '- ' + + stackLine + .trim() + .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match)) + : '(no stack trace)'); headerPart += ` ${colors.dim(tracePart)}`; } @@ -628,8 +695,8 @@ export function showAggregate(topError, { const bar1 = ' '; const causePart = - (error.cause - ? recursive(error.cause, {level: level + 1}) + (cause + ? recursive(cause) .split('\n') .map((line, i) => i === 0 ? ` ${head1} ${line}` : ` ${bar1} ${line}`) .join('\n') @@ -638,19 +705,20 @@ export function showAggregate(topError, { const head2 = level % 2 === 0 ? '\u257f' : colors.dim('\u257f'); const bar2 = level % 2 === 0 ? '\u2502' : colors.dim('\u254e'); - const aggregatePart = - (error instanceof AggregateError - ? error.errors - .map(error => recursive(error, {level: level + 1})) + const errorsPart = + (errors + ? errors + .map(error => recursive(error)) .flatMap(str => str.split('\n')) .map((line, i) => i === 0 ? ` ${head2} ${line}` : ` ${bar2} ${line}`) .join('\n') : ''); - return [headerPart, causePart, aggregatePart].filter(Boolean).join('\n'); + return [headerPart, causePart, errorsPart].filter(Boolean).join('\n'); }; - const message = recursive(topError, {level: 0}); + const structure = flattenErrorStructure(topError); + const message = recursive(structure); if (print) { console.error(message); -- cgit 1.3.0-6-gf8a5