diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index f2fdc5b6c..e969de8d7 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -1,4 +1,4 @@ -import {empty, unique} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; export default { contentDependencies: [ @@ -13,6 +13,7 @@ export default { 'generatePageLayout', 'linkArtistGallery', 'linkExternal', + 'linkGroup', 'transformContent', ], @@ -48,6 +49,16 @@ export default { hasGallery: !empty(artist.albumCoverArtistContributions) || !empty(artist.trackCoverArtistContributions), + + aliasLinkedGroups: + artist.closelyLinkedGroups + .filter(({annotation}) => + annotation === 'alias'), + + generalLinkedGroups: + artist.closelyLinkedGroups + .filter(({annotation}) => + annotation !== 'alias'), }), relations: (relation, query, artist) => ({ @@ -68,6 +79,16 @@ export default { contextNotes: relation('transformContent', artist.contextNotes), + closeGroupLinks: + query.generalLinkedGroups + .map(({thing: group}) => + relation('linkGroup', group)), + + aliasGroupLinks: + query.aliasLinkedGroups + .map(({thing: group}) => + relation('linkGroup', group)), + visitLinks: artist.urls .map(url => relation('linkExternal', url)), @@ -111,6 +132,10 @@ export default { ? artist.avatarFileExtension : null), + closeGroupAnnotations: + query.generalLinkedGroups + .map(({annotation}) => annotation), + totalTrackCount: query.allTracks.length, @@ -146,6 +171,49 @@ export default { relations.contextNotes), ]), + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + language.encapsulate(pageCapsule, 'closelyLinkedGroups', capsule => [ + language.encapsulate(capsule, capsule => { + const [workingCapsule, option] = + (relations.closeGroupLinks.length === 0 + ? [null, null] + : relations.closeGroupLinks.length === 1 + ? [language.encapsulate(capsule, 'one'), 'group'] + : [language.encapsulate(capsule, 'multiple'), 'groups']); + + if (!workingCapsule) return html.blank(); + + return language.$(workingCapsule, { + [option]: + language.formatUnitList( + stitchArrays({ + link: relations.closeGroupLinks, + annotation: data.closeGroupAnnotations, + }).map(({link, annotation}) => + language.encapsulate(capsule, 'group', workingCapsule => { + const workingOptions = {group: link}; + + if (annotation) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = annotation; + } + + return language.$(workingCapsule, workingOptions); + }))), + }); + }), + + language.$(capsule, 'alias', { + [language.onlyIfOptions]: ['groups'], + + groups: + language.formatConjunctionList(relations.aliasGroupLinks), + }), + ])), + html.tag('p', {[html.onlyIfContent]: true}, diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js index 87f35656e..90cbb9705 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -1,10 +1,14 @@ +import {stitchArrays} from '#sugar'; + export default { contentDependencies: [ + 'generateColorStyleAttribute', 'generateGroupInfoPageAlbumsSection', 'generateGroupNavLinks', 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', + 'linkArtist', 'linkExternal', 'transformContent', ], @@ -14,9 +18,24 @@ export default { sprawl: ({wikiInfo}) => ({ enableGroupUI: wikiInfo.enableGroupUI, + + wikiColor: + wikiInfo.color, + }), + + query: (_sprawl, group) => ({ + aliasLinkedArtists: + group.closelyLinkedArtists + .filter(({annotation}) => + annotation === 'alias'), + + generalLinkedArtists: + group.closelyLinkedArtists + .filter(({annotation}) => + annotation !== 'alias'), }), - relations: (relation, sprawl, group) => ({ + relations: (relation, query, sprawl, group) => ({ layout: relation('generatePageLayout'), @@ -33,6 +52,19 @@ export default { ? relation('generateGroupSidebar', group) : null), + wikiColorAttribute: + relation('generateColorStyleAttribute', sprawl.wikiColor), + + closeArtistLinks: + query.generalLinkedArtists + .map(({thing: artist}) => + relation('linkArtist', artist)), + + aliasArtistLinks: + query.aliasLinkedArtists + .map(({thing: artist}) => + relation('linkArtist', artist)), + visitLinks: group.urls .map(url => relation('linkExternal', url)), @@ -44,12 +76,16 @@ export default { relation('generateGroupInfoPageAlbumsSection', group), }), - data: (_sprawl, group) => ({ + data: (query, _sprawl, group) => ({ name: group.name, color: group.color, + + closeArtistAnnotations: + query.generalLinkedArtists + .map(({annotation}) => annotation), }), generate: (data, relations, {html, language}) => @@ -60,6 +96,58 @@ export default { color: data.color, mainContent: [ + html.tag('p', + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + language.encapsulate(pageCapsule, 'closelyLinkedArtists', capsule => [ + language.encapsulate(capsule, capsule => { + const [workingCapsule, option] = + (relations.closeArtistLinks.length === 0 + ? [null, null] + : relations.closeArtistLinks.length === 1 + ? [language.encapsulate(capsule, 'one'), 'artist'] + : [language.encapsulate(capsule, 'multiple'), 'artists']); + + if (!workingCapsule) return html.blank(); + + return language.$(workingCapsule, { + [option]: + language.formatUnitList( + stitchArrays({ + link: relations.closeArtistLinks, + annotation: data.closeArtistAnnotations, + }).map(({link, annotation}) => + language.encapsulate(capsule, 'artist', workingCapsule => { + const workingOptions = {}; + + workingOptions.artist = + link.slots({ + attributes: [relations.wikiColorAttribute], + }); + + if (annotation) { + workingCapsule += '.withAnnotation'; + workingOptions.annotation = annotation; + } + + return language.$(workingCapsule, workingOptions); + }))), + }); + }), + + language.$(capsule, 'aliases', { + [language.onlyIfOptions]: ['aliases'], + + aliases: + language.formatConjunctionList( + relations.aliasArtistLinks.map(link => + link.slots({ + attributes: [relations.wikiColorAttribute], + }))), + }), + ])), + html.tag('p', {[html.onlyIfContent]: true}, diff --git a/src/data/composite/control-flow/helpers/performAvailabilityCheck.js b/src/data/composite/control-flow/helpers/performAvailabilityCheck.js new file mode 100644 index 000000000..0e44ab59d --- /dev/null +++ b/src/data/composite/control-flow/helpers/performAvailabilityCheck.js @@ -0,0 +1,19 @@ +import {empty} from '#sugar'; + +export default function performAvailabilityCheck(value, mode) { + switch (mode) { + case 'null': + return value !== undefined && value !== null; + + case 'empty': + return value !== undefined && !empty(value); + + case 'falsy': + return !!value && (!Array.isArray(value) || !empty(value)); + + case 'index': + return typeof value === 'number' && value >= 0; + } + + return undefined; +} diff --git a/src/data/composite/control-flow/index.js b/src/data/composite/control-flow/index.js index 6148d4656..7e137a14a 100644 --- a/src/data/composite/control-flow/index.js +++ b/src/data/composite/control-flow/index.js @@ -12,4 +12,5 @@ export {default as exposeUpdateValueOrContinue} from './exposeUpdateValueOrConti export {default as exposeWhetherDependencyAvailable} from './exposeWhetherDependencyAvailable.js'; export {default as raiseOutputWithoutDependency} from './raiseOutputWithoutDependency.js'; export {default as raiseOutputWithoutUpdateValue} from './raiseOutputWithoutUpdateValue.js'; +export {default as withAvailabilityFilter} from './withAvailabilityFilter.js'; export {default as withResultOfAvailabilityCheck} from './withResultOfAvailabilityCheck.js'; diff --git a/src/data/composite/control-flow/withAvailabilityFilter.js b/src/data/composite/control-flow/withAvailabilityFilter.js new file mode 100644 index 000000000..cfea998e9 --- /dev/null +++ b/src/data/composite/control-flow/withAvailabilityFilter.js @@ -0,0 +1,40 @@ +// Performs the same availability check across all items of a list, providing +// a list that's suitable anywhere a filter is expected. +// +// Accepts the same mode options as withResultOfAvailabilityCheck. +// +// See also: +// - withFilteredList +// - withResultOfAvailabilityCheck +// + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; + +import performAvailabilityCheck from './helpers/performAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `withAvailabilityFilter`, + + inputs: { + from: input({type: 'array'}), + mode: inputAvailabilityCheckMode(), + }, + + outputs: ['#availabilityFilter'], + + steps: () => [ + { + dependencies: [input('from'), input('mode')], + compute: (continuation, { + [input('from')]: list, + [input('mode')]: mode, + }) => continuation({ + ['#availabilityFilter']: + list.map(value => + performAvailabilityCheck(value, mode)), + }), + }, + ], +}); diff --git a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js index 1d90b3241..c5221a62d 100644 --- a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -20,13 +20,15 @@ // - exposeWhetherDependencyAvailable // - raiseOutputWithoutDependency // - raiseOutputWithoutUpdateValue +// - withAvailabilityFilter // import {input, templateCompositeFrom} from '#composite'; -import {empty} from '#sugar'; import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import performAvailabilityCheck from './helpers/performAvailabilityCheck.js'; + export default templateCompositeFrom({ annotation: `withResultOfAvailabilityCheck`, @@ -40,33 +42,13 @@ export default templateCompositeFrom({ steps: () => [ { dependencies: [input('from'), input('mode')], - compute: (continuation, { [input('from')]: value, [input('mode')]: mode, - }) => { - let availability; - - switch (mode) { - case 'null': - availability = value !== undefined && value !== null; - break; - - case 'empty': - availability = value !== undefined && !empty(value); - break; - - case 'falsy': - availability = !!value && (!Array.isArray(value) || !empty(value)); - break; - - case 'index': - availability = typeof value === 'number' && value >= 0; - break; - } - - return continuation({'#availability': availability}); - }, + }) => continuation({ + ['#availability']: + performAvailabilityCheck(value, mode), + }), }, ], }); diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js index c80bb3503..46a3dc812 100644 --- a/src/data/composite/data/index.js +++ b/src/data/composite/data/index.js @@ -18,6 +18,7 @@ export {default as withUniqueItemsOnly} from './withUniqueItemsOnly.js'; export {default as withFilteredList} from './withFilteredList.js'; export {default as withMappedList} from './withMappedList.js'; export {default as withSortedList} from './withSortedList.js'; +export {default as withStretchedList} from './withStretchedList.js'; export {default as withPropertyFromList} from './withPropertyFromList.js'; export {default as withPropertiesFromList} from './withPropertiesFromList.js'; diff --git a/src/data/composite/data/withFilteredList.js b/src/data/composite/data/withFilteredList.js index 1dbbd3af5..44c1661dc 100644 --- a/src/data/composite/data/withFilteredList.js +++ b/src/data/composite/data/withFilteredList.js @@ -5,17 +5,11 @@ // If the flip option is set, only items corresponding with a *falsy* value in // the filter are kept. // -// TODO: It would be neat to apply an availability check here, e.g. to allow -// not providing a filter at all and performing the check on the contents of -// the list (though on the filter, if present, is fine too). But that's best -// done by some shmancy-fancy mapping support in composite.js, so a bit out -// of reach for now (apart from proving uses built on top of a more boring -// implementation). -// // TODO: There should be two outputs - one for the items included according to // the filter, and one for the items excluded. // // See also: +// - withAvailabilityFilter // - withMappedList // - withSortedList // diff --git a/src/data/composite/data/withStretchedList.js b/src/data/composite/data/withStretchedList.js new file mode 100644 index 000000000..467330644 --- /dev/null +++ b/src/data/composite/data/withStretchedList.js @@ -0,0 +1,36 @@ +// Repeats each item in a list in-place by a corresponding length. + +import {input, templateCompositeFrom} from '#composite'; +import {repeat, stitchArrays} from '#sugar'; +import {isNumber, validateArrayItems} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withStretchedList`, + + inputs: { + list: input({type: 'array'}), + + lengths: input({ + validate: validateArrayItems(isNumber), + }), + }, + + outputs: ['#stretchedList'], + + steps: () => [ + { + dependencies: [input('list'), input('lengths')], + compute: (continuation, { + [input('list')]: list, + [input('lengths')]: lengths, + }) => continuation({ + ['#stretchedList']: + stitchArrays({ + item: list, + length: lengths, + }).map(({item, length}) => repeat(length, [item])) + .flat(), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js index f99a1a148..51d073842 100644 --- a/src/data/composite/wiki-data/index.js +++ b/src/data/composite/wiki-data/index.js @@ -5,6 +5,7 @@ // export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as inputNotFoundMode} from './inputNotFoundMode.js'; export {default as inputWikiData} from './inputWikiData.js'; export {default as withClonedThings} from './withClonedThings.js'; export {default as withContributionListSums} from './withContributionListSums.js'; @@ -13,11 +14,12 @@ export {default as withDirectory} from './withDirectory.js'; export {default as withParsedCommentaryEntries} from './withParsedCommentaryEntries.js'; export {default as withRecontextualizedContributionList} from './withRecontextualizedContributionList.js'; export {default as withRedatedContributionList} from './withRedatedContributionList.js'; -export {default as withResolvedArtworkReferenceList} from './withResolvedArtworkReferenceList.js'; +export {default as withResolvedAnnotatedReferenceList} from './withResolvedAnnotatedReferenceList.js'; export {default as withResolvedContribs} from './withResolvedContribs.js'; export {default as withResolvedReference} from './withResolvedReference.js'; export {default as withResolvedReferenceList} from './withResolvedReferenceList.js'; export {default as withResolvedSeriesList} from './withResolvedSeriesList.js'; +export {default as withReverseAnnotatedReferenceList} from './withReverseAnnotatedReferenceList.js'; export {default as withReverseContributionList} from './withReverseContributionList.js'; export {default as withReverseReferenceList} from './withReverseReferenceList.js'; export {default as withReverseSingleReferenceList} from './withReverseSingleReferenceList.js'; diff --git a/src/data/composite/wiki-data/inputNotFoundMode.js b/src/data/composite/wiki-data/inputNotFoundMode.js new file mode 100644 index 000000000..d16b24722 --- /dev/null +++ b/src/data/composite/wiki-data/inputNotFoundMode.js @@ -0,0 +1,9 @@ +import {input} from '#composite'; +import {is} from '#validators'; + +export default function inputNotFoundMode() { + return input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'filter', + }); +} diff --git a/src/data/composite/wiki-data/raiseResolvedReferenceList.js b/src/data/composite/wiki-data/raiseResolvedReferenceList.js new file mode 100644 index 000000000..613b002b6 --- /dev/null +++ b/src/data/composite/wiki-data/raiseResolvedReferenceList.js @@ -0,0 +1,96 @@ +// Concludes compositions like withResolvedReferenceList, which share behavior +// in processing the resolved results before continuing further. + +import {input, templateCompositeFrom} from '#composite'; + +import {withFilteredList} from '#composite/data'; + +import inputNotFoundMode from './inputNotFoundMode.js'; + +export default templateCompositeFrom({ + inputs: { + notFoundMode: inputNotFoundMode(), + + results: input({type: 'array'}), + filter: input({type: 'array'}), + + exitValue: input({defaultValue: []}), + + outputs: input.staticValue({type: 'string'}), + }, + + outputs: ({ + [input.staticValue('outputs')]: outputs, + }) => [outputs], + + steps: () => [ + { + dependencies: [ + input('results'), + input('filter'), + input('outputs'), + ], + + compute: (continuation, { + [input('results')]: results, + [input('filter')]: filter, + [input('outputs')]: outputs, + }) => + (filter.every(keep => keep) + ? continuation.raiseOutput({[outputs]: results}) + : continuation()), + }, + + { + dependencies: [ + input('notFoundMode'), + input('exitValue'), + ], + + compute: (continuation, { + [input('notFoundMode')]: notFoundMode, + [input('exitValue')]: exitValue, + }) => + (notFoundMode === 'exit' + ? continuation.exit(exitValue) + : continuation()), + }, + + { + dependencies: [ + input('results'), + input('notFoundMode'), + input('outputs'), + ], + + compute: (continuation, { + [input('results')]: results, + [input('notFoundMode')]: notFoundMode, + [input('outputs')]: outputs, + }) => + (notFoundMode === 'null' + ? continuation.raiseOutput({[outputs]: results}) + : continuation()), + }, + + withFilteredList({ + list: input('results'), + filter: input('filter'), + }), + + { + dependencies: [ + '#filteredList', + input('outputs'), + ], + + compute: (continuation, { + ['#filteredList']: filteredList, + [input('outputs')]: outputs, + }) => continuation({ + [outputs]: + filteredList, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js new file mode 100644 index 000000000..ac6b15fa3 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js @@ -0,0 +1,100 @@ +import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; +import {isString, optional, validateArrayItems, validateProperties} + from '#validators'; + +import {withPropertiesFromList} from '#composite/data'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, + withAvailabilityFilter, +} from '#composite/control-flow'; + +import inputNotFoundMode from './inputNotFoundMode.js'; +import inputWikiData from './inputWikiData.js'; +import raiseResolvedReferenceList from './raiseResolvedReferenceList.js'; +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedAnnotatedReferenceList`, + + inputs: { + list: input({ + validate: + validateArrayItems( + validateProperties({ + reference: isString, + annotation: optional(isString), + })), + + acceptsNull: true, + }), + + data: inputWikiData({allowMixedTypes: true}), + find: input({type: 'function'}), + + notFoundMode: inputNotFoundMode(), + }, + + outputs: ['#resolvedAnnotatedReferenceList'], + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedAnnotatedReferenceList']: [], + }), + }), + + withPropertiesFromList({ + list: input('list'), + properties: input.value([ + 'reference', + 'annotation', + ]), + }), + + withResolvedReferenceList({ + list: '#list.reference', + data: input('data'), + find: input('find'), + notFoundMode: input.value('null'), + }), + + { + dependencies: [ + '#resolvedReferenceList', + '#list.annotation', + ], + + compute: (continuation, { + ['#resolvedReferenceList']: thing, + ['#list.annotation']: annotation, + }) => continuation({ + ['#matches']: + stitchArrays({ + thing, + annotation, + }), + }), + }, + + withAvailabilityFilter({ + from: '#resolvedReferenceList', + }), + + raiseResolvedReferenceList({ + notFoundMode: input('notFoundMode'), + results: '#matches', + filter: '#availabilityFilter', + outputs: input.value('#resolvedAnnotatedReferenceList'), + }), + ], +}) diff --git a/src/data/composite/wiki-data/withResolvedArtworkReferenceList.js b/src/data/composite/wiki-data/withResolvedArtworkReferenceList.js deleted file mode 100644 index 08c45ec86..000000000 --- a/src/data/composite/wiki-data/withResolvedArtworkReferenceList.js +++ /dev/null @@ -1,127 +0,0 @@ -import {input, templateCompositeFrom} from '#composite'; -import {stitchArrays} from '#sugar'; -import {is, isString, optional, validateArrayItems, validateProperties} - from '#validators'; - -import {withFilteredList, withMappedList, withPropertiesFromList} - from '#composite/data'; - -import inputWikiData from './inputWikiData.js'; -import withResolvedReferenceList from './withResolvedReferenceList.js'; - -export default templateCompositeFrom({ - annotation: `withResolvedArtworkReferenceList`, - - inputs: { - list: input({ - validate: - validateArrayItems( - validateProperties({ - reference: isString, - annotation: optional(isString), - })), - - acceptsNull: true, - }), - - data: inputWikiData({allowMixedTypes: true}), - find: input({type: 'function'}), - - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'filter', - }), - }, - - outputs: ['#resolvedArtworkReferenceList'], - - steps: () => [ - withPropertiesFromList({ - list: input('list'), - properties: input.value([ - 'reference', - 'annotation', - ]), - }), - - withResolvedReferenceList({ - list: '#list.reference', - data: input('data'), - find: input('find'), - notFoundMode: input.value('null'), - }), - - { - dependencies: [ - '#resolvedReferenceList', - '#list.annotation', - ], - - compute: (continuation, { - ['#resolvedReferenceList']: thing, - ['#list.annotation']: annotation, - }) => continuation({ - ['#matches']: - stitchArrays({ - thing, - annotation, - }), - }), - }, - - { - dependencies: ['#matches'], - compute: (continuation, {'#matches': matches}) => - (matches.every(match => match) - ? continuation.raiseOutput({ - ['#resolvedArtworkReferenceList']: - matches, - }) - : continuation()), - }, - - { - dependencies: [input('notFoundMode')], - compute: (continuation, { - [input('notFoundMode')]: notFoundMode, - }) => - (notFoundMode === 'exit' - ? continuation.exit([]) - : continuation()), - }, - - { - dependencies: ['#matches', input('notFoundMode')], - compute: (continuation, { - ['#matches']: matches, - [input('notFoundMode')]: notFoundMode, - }) => - (notFoundMode === 'null' - ? continuation.raiseOutput({ - ['#resolvedArtworkReferenceList']: - matches, - }) - : continuation()), - }, - - withMappedList({ - list: '#resolvedReferenceList', - map: input.value(thing => thing !== null), - }), - - withFilteredList({ - list: '#matches', - filter: '#mappedList', - }), - - { - dependencies: ['#filteredList'], - compute: (continuation, { - ['#filteredList']: filteredList, - }) => continuation({ - ['#resolvedArtworkReferenceList']: - filteredList, - }), - }, - ], -}) diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js index b5d7255ba..b4119604c 100644 --- a/src/data/composite/wiki-data/withResolvedContribs.js +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -5,13 +5,16 @@ // any artist. import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; import {filterMultipleArrays, stitchArrays} from '#sugar'; import thingConstructors from '#things'; -import {is, isContributionList, isDate, isStringNonEmpty} from '#validators'; +import {isContributionList, isDate, isStringNonEmpty} from '#validators'; -import {raiseOutputWithoutDependency} from '#composite/control-flow'; -import {withPropertiesFromList} from '#composite/data'; +import {raiseOutputWithoutDependency, withAvailabilityFilter} + from '#composite/control-flow'; +import {withPropertyFromList, withPropertiesFromList} from '#composite/data'; + +import inputNotFoundMode from './inputNotFoundMode.js'; +import raiseResolvedReferenceList from './raiseResolvedReferenceList.js'; export default templateCompositeFrom({ annotation: `withResolvedContribs`, @@ -27,10 +30,7 @@ export default templateCompositeFrom({ acceptsNull: true, }), - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'null', - }), + notFoundMode: inputNotFoundMode(), thingProperty: input({ validate: isStringNonEmpty, @@ -134,16 +134,20 @@ export default templateCompositeFrom({ }), }, - { - dependencies: ['#contributions'], + withPropertyFromList({ + list: '#contributions', + property: input.value('thing'), + }), - compute: (continuation, { - ['#contributions']: contributions, - }) => continuation({ - ['#resolvedContribs']: - contributions - .filter(contrib => contrib.artist), - }), - }, + withAvailabilityFilter({ + from: '#contributions.thing', + }), + + raiseResolvedReferenceList({ + notFoundMode: input('notFoundMode'), + results: '#contributions', + filter: '#availabilityFilter', + outputs: input.value('#resolvedContribs'), + }), ], }); diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js index 42f0e1758..790a962f9 100644 --- a/src/data/composite/wiki-data/withResolvedReferenceList.js +++ b/src/data/composite/wiki-data/withResolvedReferenceList.js @@ -5,14 +5,17 @@ // to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). import {input, templateCompositeFrom} from '#composite'; -import {is, isString, validateArrayItems} from '#validators'; +import {isString, validateArrayItems} from '#validators'; import { exitWithoutDependency, raiseOutputWithoutDependency, + withAvailabilityFilter, } from '#composite/control-flow'; +import inputNotFoundMode from './inputNotFoundMode.js'; import inputWikiData from './inputWikiData.js'; +import raiseResolvedReferenceList from './raiseResolvedReferenceList.js'; export default templateCompositeFrom({ annotation: `withResolvedReferenceList`, @@ -26,10 +29,7 @@ export default templateCompositeFrom({ data: inputWikiData({allowMixedTypes: true}), find: input({type: 'function'}), - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'filter', - }), + notFoundMode: inputNotFoundMode(), }, outputs: ['#resolvedReferenceList'], @@ -60,42 +60,15 @@ export default templateCompositeFrom({ }), }, - { - dependencies: ['#matches'], - compute: (continuation, {'#matches': matches}) => - (matches.every(match => match) - ? continuation.raiseOutput({ - ['#resolvedReferenceList']: matches, - }) - : continuation()), - }, - - { - dependencies: ['#matches', input('notFoundMode')], - compute(continuation, { - ['#matches']: matches, - [input('notFoundMode')]: notFoundMode, - }) { - switch (notFoundMode) { - case 'exit': - return continuation.exit([]); - - case 'filter': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.filter(match => match), - }); - - case 'null': - return continuation.raiseOutput({ - ['#resolvedReferenceList']: - matches.map(match => match ?? null), - }); + withAvailabilityFilter({ + from: '#matches', + }), - default: - throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); - } - }, - }, + raiseResolvedReferenceList({ + notFoundMode: input('notFoundMode'), + results: '#matches', + filter: '#availabilityFilter', + outputs: input.value('#resolvedReferenceList'), + }), ], }); diff --git a/src/data/composite/wiki-data/withReverseAnnotatedReferenceList.js b/src/data/composite/wiki-data/withReverseAnnotatedReferenceList.js new file mode 100644 index 000000000..168b68c05 --- /dev/null +++ b/src/data/composite/wiki-data/withReverseAnnotatedReferenceList.js @@ -0,0 +1,81 @@ +// Analogous implementation for withReverseReferenceList, for annotated +// references. +// +// Unlike withReverseContributionList, this composition is responsible for +// "flipping" the directionality of references: in a forward reference list, +// `thing` points to the thing being referenced, while here, it points to the +// referencing thing. + +import withReverseList_template from './helpers/withReverseList-template.js'; + +import {input} from '#composite'; +import {stitchArrays} from '#sugar'; + +import { + withFlattenedList, + withMappedList, + withPropertyFromList, + withStretchedList, +} from '#composite/data'; + +export default withReverseList_template({ + annotation: `withReverseAnnotatedReferenceList`, + + propertyInputName: 'list', + outputName: '#reverseAnnotatedReferenceList', + + customCompositionSteps: () => [ + withPropertyFromList({ + list: input('data'), + property: input('list'), + }).outputs({ + '#values': '#referenceLists', + }), + + withPropertyFromList({ + list: '#referenceLists', + property: input.value('length'), + }), + + withFlattenedList({ + list: '#referenceLists', + }).outputs({ + '#flattenedList': '#references', + }), + + withStretchedList({ + list: input('data'), + lengths: '#referenceLists.length', + }).outputs({ + '#stretchedList': '#things', + }), + + withPropertyFromList({ + list: '#references', + property: input.value('annotation'), + }).outputs({ + '#references.annotation': '#annotations', + }), + + { + dependencies: ['#things', '#annotations'], + compute: (continuation, { + ['#things']: things, + ['#annotations']: annotations, + }) => continuation({ + ['#referencingThings']: + stitchArrays({ + thing: things, + annotation: annotations, + }), + }), + }, + + withMappedList({ + list: '#references', + map: input.value(reference => [reference.thing]), + }).outputs({ + '#mappedList': '#referencedThings', + }), + ], +}); diff --git a/src/data/composite/wiki-data/withReverseContributionList.js b/src/data/composite/wiki-data/withReverseContributionList.js index dcf33c391..2396c3b4f 100644 --- a/src/data/composite/wiki-data/withReverseContributionList.js +++ b/src/data/composite/wiki-data/withReverseContributionList.js @@ -4,7 +4,8 @@ import withReverseList_template from './helpers/withReverseList-template.js'; import {input} from '#composite'; -import {withFlattenedList, withMappedList} from '#composite/data'; +import {withFlattenedList, withMappedList, withPropertyFromList} + from '#composite/data'; export default withReverseList_template({ annotation: `withReverseContributionList`, @@ -13,21 +14,11 @@ export default withReverseList_template({ outputName: '#reverseContributionList', customCompositionSteps: () => [ - { - dependencies: [input('list')], - compute: (continuation, { - [input('list')]: list, - }) => continuation({ - ['#contributionListMap']: - thing => thing[list], - }), - }, - - withMappedList({ + withPropertyFromList({ list: input('data'), - map: '#contributionListMap', + property: input('list'), }).outputs({ - '#mappedList': '#contributionLists', + '#values': '#contributionLists', }), withFlattenedList({ diff --git a/src/data/composite/wiki-properties/annotatedReferenceList.js b/src/data/composite/wiki-properties/annotatedReferenceList.js new file mode 100644 index 000000000..e8e5ac8c5 --- /dev/null +++ b/src/data/composite/wiki-properties/annotatedReferenceList.js @@ -0,0 +1,39 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {validateAnnotatedReferenceList} from '#validators'; +import {combineWikiDataArrays} from '#wiki-data'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputWikiData, withResolvedAnnotatedReferenceList} + from '#composite/wiki-data'; + +import {referenceListInputDescriptions, referenceListUpdateDescription} + from './helpers/reference-list-helpers.js'; + +export default templateCompositeFrom({ + annotation: `referencedArtworkList`, + + compose: false, + + inputs: { + ...referenceListInputDescriptions(), + + data: inputWikiData({allowMixedTypes: true}), + find: input({type: 'function'}), + }, + + update: + referenceListUpdateDescription({ + validateReferenceList: validateAnnotatedReferenceList, + }), + + steps: () => [ + withResolvedAnnotatedReferenceList({ + list: input.updateValue(), + data: input('data'), + find: input('find'), + }), + + exposeDependency({dependency: '#resolvedAnnotatedReferenceList'}), + ], +}); diff --git a/src/data/composite/wiki-properties/helpers/reference-list-helpers.js b/src/data/composite/wiki-properties/helpers/reference-list-helpers.js new file mode 100644 index 000000000..dfdc6b41e --- /dev/null +++ b/src/data/composite/wiki-properties/helpers/reference-list-helpers.js @@ -0,0 +1,44 @@ +import {input} from '#composite'; +import {anyOf, isString, isThingClass, validateArrayItems} from '#validators'; + +export function referenceListInputDescriptions() { + return { + class: input.staticValue({ + validate: + anyOf( + isThingClass, + validateArrayItems(isThingClass)), + + acceptsNull: true, + defaultValue: null, + }), + + referenceType: input.staticValue({ + validate: + anyOf( + isString, + validateArrayItems(isString)), + + acceptsNull: true, + defaultValue: null, + }), + }; +} + +export function referenceListUpdateDescription({ + validateReferenceList, +}) { + return ({ + [input.staticValue('class')]: thingClass, + [input.staticValue('referenceType')]: referenceType, + }) => ({ + validate: + validateReferenceList( + (Array.isArray(thingClass) + ? thingClass.map(thingClass => + thingClass[Symbol.for('Thing.referenceType')]) + : thingClass + ? thingClass[Symbol.for('Thing.referenceType')] + : referenceType)), + }); +} diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js index d39bff3a0..e98c89fcf 100644 --- a/src/data/composite/wiki-properties/index.js +++ b/src/data/composite/wiki-properties/index.js @@ -5,6 +5,7 @@ export {default as additionalFiles} from './additionalFiles.js'; export {default as additionalNameList} from './additionalNameList.js'; +export {default as annotatedReferenceList} from './annotatedReferenceList.js'; export {default as color} from './color.js'; export {default as commentary} from './commentary.js'; export {default as commentatorArtists} from './commentatorArtists.js'; @@ -20,6 +21,7 @@ export {default as flag} from './flag.js'; export {default as name} from './name.js'; export {default as referenceList} from './referenceList.js'; export {default as referencedArtworkList} from './referencedArtworkList.js'; +export {default as reverseAnnotatedReferenceList} from './reverseAnnotatedReferenceList.js'; export {default as reverseContributionList} from './reverseContributionList.js'; export {default as reverseReferenceList} from './reverseReferenceList.js'; export {default as reverseSingleReferenceList} from './reverseSingleReferenceList.js'; diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js index bf7185518..4d4cb1068 100644 --- a/src/data/composite/wiki-properties/referenceList.js +++ b/src/data/composite/wiki-properties/referenceList.js @@ -8,44 +8,30 @@ // import {input, templateCompositeFrom} from '#composite'; -import {isThingClass, validateReferenceList} from '#validators'; +import {validateReferenceList} from '#validators'; import {exposeDependency} from '#composite/control-flow'; import {inputWikiData, withResolvedReferenceList} from '#composite/wiki-data'; +import {referenceListInputDescriptions, referenceListUpdateDescription} + from './helpers/reference-list-helpers.js'; + export default templateCompositeFrom({ annotation: `referenceList`, compose: false, inputs: { - class: input.staticValue({ - validate: isThingClass, - acceptsNull: true, - defaultValue: null, - }), - - referenceType: input.staticValue({ - type: 'string', - acceptsNull: true, - defaultValue: null, - }), + ...referenceListInputDescriptions(), data: inputWikiData({allowMixedTypes: true}), - find: input({type: 'function'}), }, - update: ({ - [input.staticValue('class')]: thingClass, - [input.staticValue('referenceType')]: referenceType, - }) => ({ - validate: - validateReferenceList( - (thingClass - ? thingClass[Symbol.for('Thing.referenceType')] - : referenceType)), - }), + update: + referenceListUpdateDescription({ + validateReferenceList: validateReferenceList, + }), steps: () => [ withResolvedReferenceList({ diff --git a/src/data/composite/wiki-properties/referencedArtworkList.js b/src/data/composite/wiki-properties/referencedArtworkList.js index 251944e5c..747904436 100644 --- a/src/data/composite/wiki-properties/referencedArtworkList.js +++ b/src/data/composite/wiki-properties/referencedArtworkList.js @@ -1,21 +1,13 @@ import {input, templateCompositeFrom} from '#composite'; import find from '#find'; -import {validateAnnotatedReferenceList} from '#validators'; import {combineWikiDataArrays} from '#wiki-data'; -import {exposeDependency} from '#composite/control-flow'; -import {withResolvedArtworkReferenceList} from '#composite/wiki-data'; +import annotatedReferenceList from './annotatedReferenceList.js'; export default templateCompositeFrom({ annotation: `referencedArtworkList`, - update: ({ - [input.staticValue('class')]: thingClass, - [input.staticValue('referenceType')]: referenceType, - }) => ({ - validate: - validateAnnotatedReferenceList(['album', 'track']), - }), + compose: false, steps: () => [ { @@ -46,12 +38,10 @@ export default templateCompositeFrom({ }), }, - withResolvedArtworkReferenceList({ - list: input.updateValue(), + annotatedReferenceList({ + referenceType: input.value(['album', 'track']), data: '#data', find: '#find', }), - - exposeDependency({dependency: '#resolvedArtworkReferenceList'}), ], }); diff --git a/src/data/composite/wiki-properties/reverseAnnotatedReferenceList.js b/src/data/composite/wiki-properties/reverseAnnotatedReferenceList.js new file mode 100644 index 000000000..d18e53577 --- /dev/null +++ b/src/data/composite/wiki-properties/reverseAnnotatedReferenceList.js @@ -0,0 +1,25 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputWikiData, withReverseAnnotatedReferenceList} + from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `reverseAnnotatedReferenceList`, + + compose: false, + + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, + + steps: () => [ + withReverseAnnotatedReferenceList({ + data: input('data'), + list: input('list'), + }), + + exposeDependency({dependency: '#reverseAnnotatedReferenceList'}), + ], +}); diff --git a/src/data/things/album.js b/src/data/things/album.js index 6bfec68cf..b6bd13130 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -16,10 +16,10 @@ import {isColor, isDate, isDirectory, validateWikiData} from '#validators'; import { parseAdditionalFiles, parseAdditionalNames, + parseAnnotatedReferences, parseContributors, parseDate, parseDimensions, - parseReferencedArtworks, } from '#yaml'; import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} @@ -414,7 +414,7 @@ export class Album extends Thing { 'Referenced Artworks': { property: 'referencedArtworks', - transform: parseReferencedArtworks, + transform: parseAnnotatedReferences, }, 'Franchises': {ignore: true}, diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 6d5e33c0e..c6ee222a2 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -21,6 +21,7 @@ import { fileExtension, flag, name, + reverseAnnotatedReferenceList, reverseContributionList, reverseReferenceList, singleReference, @@ -34,7 +35,7 @@ export class Artist extends Thing { static [Thing.referenceType] = 'artist'; static [Thing.wikiDataArray] = 'artistData'; - static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Flash, Group, Track}) => ({ // Update & expose name: name('Unnamed Artist'), @@ -74,6 +75,10 @@ export class Artist extends Thing { class: input.value(Flash), }), + groupData: wikiData({ + class: input.value(Group), + }), + trackData: wikiData({ class: input.value(Track), }), @@ -135,6 +140,11 @@ export class Artist extends Thing { list: input.value('commentatorArtists'), }), + closelyLinkedGroups: reverseAnnotatedReferenceList({ + data: 'groupData', + list: input.value('closelyLinkedArtists'), + }), + totalDuration: artistTotalDuration(), }); diff --git a/src/data/things/group.js b/src/data/things/group.js index e000e4070..4a7901409 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -3,9 +3,10 @@ export const GROUP_DATA_FILE = 'groups.yaml'; import {input} from '#composite'; import find from '#find'; import Thing from '#thing'; -import {parseSerieses} from '#yaml'; +import {parseAnnotatedReferences, parseSerieses} from '#yaml'; import { + annotatedReferenceList, color, contentString, directory, @@ -19,7 +20,7 @@ import { export class Group extends Thing { static [Thing.referenceType] = 'group'; - static [Thing.getPropertyDescriptors] = ({Album}) => ({ + static [Thing.getPropertyDescriptors] = ({Album, Artist}) => ({ // Update & expose name: name('Unnamed Group'), @@ -29,6 +30,12 @@ export class Group extends Thing { urls: urls(), + closelyLinkedArtists: annotatedReferenceList({ + class: input.value(Artist), + find: input.value(find.artist), + data: 'artistData', + }), + featuredAlbums: referenceList({ class: input.value(Album), find: input.value(find.album), @@ -45,6 +52,10 @@ export class Group extends Thing { class: input.value(Album), }), + artistData: wikiData({ + class: input.value(Artist), + }), + groupCategoryData: wikiData({ class: input.value(GroupCategory), }), @@ -110,6 +121,11 @@ export class Group extends Thing { 'Description': {property: 'description'}, 'URLs': {property: 'urls'}, + 'Closely Linked Artists': { + property: 'closelyLinkedArtists', + transform: parseAnnotatedReferences, + }, + 'Featured Albums': {property: 'featuredAlbums'}, 'Series': { diff --git a/src/data/things/track.js b/src/data/things/track.js index d86c76355..5c3161aab 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -11,11 +11,11 @@ import {isBoolean, isColor, isContributionList, isDate, isFileExtension} import { parseAdditionalFiles, parseAdditionalNames, + parseAnnotatedReferences, parseContributors, parseDate, parseDimensions, parseDuration, - parseReferencedArtworks, } from '#yaml'; import {withPropertyFromObject} from '#composite/data'; @@ -526,7 +526,7 @@ export class Track extends Thing { 'Referenced Artworks': { property: 'referencedArtworks', - transform: parseReferencedArtworks, + transform: parseAnnotatedReferences, }, 'Franchises': {ignore: true}, diff --git a/src/data/yaml.js b/src/data/yaml.js index b21436252..37d6daf0a 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -569,7 +569,7 @@ export function parseContributionPresets(list) { }); } -export function parseReferencedArtworks(entries) { +export function parseAnnotatedReferences(entries) { return parseArrayEntries(entries, item => { if (typeof item === 'object' && item['References']) return { @@ -1224,6 +1224,7 @@ export function linkWikiDataArrays(wikiData) { 'albumData', 'artistData', 'flashData', + 'groupData', 'trackData', ]], @@ -1245,6 +1246,7 @@ export function linkWikiDataArrays(wikiData) { [wikiData.groupData, [ 'albumData', + 'artistData', 'groupCategoryData', ]], diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 5838aa7dd..5f82a1a12 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -1073,6 +1073,16 @@ artistPage: nav: artist: "Artist: {ARTIST}" + closelyLinkedGroups: + one: "This artist has a group page: {GROUP}." + multiple: "This artist has group pages: {GROUPS}." + + group: + _: "{GROUP}" + withAnnotation: "{GROUP} ({ANNOTATION})" + + alias: "This artist is an alias of {GROUPS}." + creditList: # album: @@ -1306,6 +1316,16 @@ groupPage: groupInfoPage: title: "{GROUP}" + closelyLinkedArtists: + one: "View artist page: {ARTIST}." + multiple: "View artist pages: {ARTISTS}." + + aliases: "View alias pages: {ALIASES}." + + artist: + _: "{ARTIST}" + withAnnotation: "{ARTIST} ({ANNOTATION})" + viewAlbumGallery: _: >- View {LINK}! Or browse the list: