diff options
Diffstat (limited to 'src/data/composite')
133 files changed, 4232 insertions, 1139 deletions
diff --git a/src/data/composite/control-flow/exposeWhetherDependencyAvailable.js b/src/data/composite/control-flow/exposeWhetherDependencyAvailable.js new file mode 100644 index 00000000..a2fdd6b0 --- /dev/null +++ b/src/data/composite/control-flow/exposeWhetherDependencyAvailable.js @@ -0,0 +1,42 @@ +// Exposes true if a dependency is available, and false otherwise, +// or the reverse if the `negate` input is set true. +// +// See withResultOfAvailabilityCheck for {mode} options. + +import {input, templateCompositeFrom} from '#composite'; + +import inputAvailabilityCheckMode from './inputAvailabilityCheckMode.js'; +import withResultOfAvailabilityCheck from './withResultOfAvailabilityCheck.js'; + +export default templateCompositeFrom({ + annotation: `exposeWhetherDependencyAvailable`, + + compose: false, + + inputs: { + dependency: input({acceptsNull: true}), + + mode: inputAvailabilityCheckMode(), + + negate: input({type: 'boolean', defaultValue: false}), + }, + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('dependency'), + mode: input('mode'), + }), + + { + dependencies: ['#availability', input('negate')], + + compute: ({ + ['#availability']: availability, + [input('negate')]: negate, + }) => + (negate + ? !availability + : availability), + }, + ], +}); diff --git a/src/data/composite/control-flow/flipFilter.js b/src/data/composite/control-flow/flipFilter.js new file mode 100644 index 00000000..995bacad --- /dev/null +++ b/src/data/composite/control-flow/flipFilter.js @@ -0,0 +1,36 @@ +// Flips a filter, so that each true item becomes false, and vice versa. +// Overwrites the provided dependency. +// +// See also: +// - withAvailabilityFilter + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `flipFilter`, + + inputs: { + filter: input({type: 'array'}), + }, + + outputs: ({ + [input.staticDependency('filter')]: filterDependency, + }) => [filterDependency ?? '#flippedFilter'], + + steps: () => [ + { + dependencies: [ + input('filter'), + input.staticDependency('filter'), + ], + + compute: (continuation, { + [input('filter')]: filter, + [input.staticDependency('filter')]: filterDependency, + }) => continuation({ + [filterDependency ?? '#flippedFilter']: + filter.map(item => !item), + }), + }, + ], +}); 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 00000000..0e44ab59 --- /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 7fad88b2..778dc66b 100644 --- a/src/data/composite/control-flow/index.js +++ b/src/data/composite/control-flow/index.js @@ -9,6 +9,9 @@ export {default as exposeConstant} from './exposeConstant.js'; export {default as exposeDependency} from './exposeDependency.js'; export {default as exposeDependencyOrContinue} from './exposeDependencyOrContinue.js'; export {default as exposeUpdateValueOrContinue} from './exposeUpdateValueOrContinue.js'; +export {default as exposeWhetherDependencyAvailable} from './exposeWhetherDependencyAvailable.js'; +export {default as flipFilter} from './flipFilter.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/raiseOutputWithoutDependency.js b/src/data/composite/control-flow/raiseOutputWithoutDependency.js index 3d04f8a9..03d8036a 100644 --- a/src/data/composite/control-flow/raiseOutputWithoutDependency.js +++ b/src/data/composite/control-flow/raiseOutputWithoutDependency.js @@ -17,7 +17,7 @@ export default templateCompositeFrom({ outputs: ({ [input.staticValue('output')]: output, - }) => Object.keys(output ?? {}), + }) => Object.keys(output), steps: () => [ withResultOfAvailabilityCheck({ diff --git a/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js index ffa83a94..3c39f5ba 100644 --- a/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js +++ b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js @@ -16,7 +16,7 @@ export default templateCompositeFrom({ outputs: ({ [input.staticValue('output')]: output, - }) => Object.keys(output ?? {}), + }) => Object.keys(output), steps: () => [ withResultOfAvailabilityCheck({ diff --git a/src/data/composite/control-flow/withAvailabilityFilter.js b/src/data/composite/control-flow/withAvailabilityFilter.js new file mode 100644 index 00000000..fd93af71 --- /dev/null +++ b/src/data/composite/control-flow/withAvailabilityFilter.js @@ -0,0 +1,41 @@ +// 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: +// - flipFilter +// - 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 a6942014..c5221a62 100644 --- a/src/data/composite/control-flow/withResultOfAvailabilityCheck.js +++ b/src/data/composite/control-flow/withResultOfAvailabilityCheck.js @@ -17,15 +17,18 @@ // - exitWithoutUpdateValue // - exposeDependencyOrContinue // - exposeUpdateValueOrContinue +// - 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`, @@ -39,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/excludeFromList.js b/src/data/composite/data/excludeFromList.js index d798dcdc..2a3e818e 100644 --- a/src/data/composite/data/excludeFromList.js +++ b/src/data/composite/data/excludeFromList.js @@ -5,11 +5,6 @@ // See also: // - fillMissingListItems // -// More list utilities: -// - withFilteredList, withMappedList, withSortedList -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; import {empty} from '#sugar'; diff --git a/src/data/composite/data/fillMissingListItems.js b/src/data/composite/data/fillMissingListItems.js index 4f818a79..356b1119 100644 --- a/src/data/composite/data/fillMissingListItems.js +++ b/src/data/composite/data/fillMissingListItems.js @@ -4,11 +4,6 @@ // See also: // - excludeFromList // -// More list utilities: -// - withFilteredList, withMappedList, withSortedList -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js index 256c0490..05b59445 100644 --- a/src/data/composite/data/index.js +++ b/src/data/composite/data/index.js @@ -3,15 +3,34 @@ // Entries here may depend on entries in #composite/control-flow. // +// Utilities which act on generic objects + +export {default as withPropertiesFromObject} from './withPropertiesFromObject.js'; +export {default as withPropertyFromObject} from './withPropertyFromObject.js'; + +// Utilities which act on generic lists + export {default as excludeFromList} from './excludeFromList.js'; + export {default as fillMissingListItems} from './fillMissingListItems.js'; +export {default as withUniqueItemsOnly} from './withUniqueItemsOnly.js'; + export {default as withFilteredList} from './withFilteredList.js'; -export {default as withFlattenedList} from './withFlattenedList.js'; export {default as withMappedList} from './withMappedList.js'; -export {default as withPropertiesFromList} from './withPropertiesFromList.js'; -export {default as withPropertiesFromObject} from './withPropertiesFromObject.js'; -export {default as withPropertyFromList} from './withPropertyFromList.js'; -export {default as withPropertyFromObject} from './withPropertyFromObject.js'; export {default as withSortedList} from './withSortedList.js'; +export {default as withStretchedList} from './withStretchedList.js'; + +export {default as withLengthOfList} from './withLengthOfList.js'; +export {default as withPropertyFromList} from './withPropertyFromList.js'; +export {default as withPropertiesFromList} from './withPropertiesFromList.js'; + +export {default as withFlattenedList} from './withFlattenedList.js'; export {default as withUnflattenedList} from './withUnflattenedList.js'; -export {default as withUniqueItemsOnly} from './withUniqueItemsOnly.js'; + +export {default as withIndexInList} from './withIndexInList.js'; +export {default as withNearbyItemFromList} from './withNearbyItemFromList.js'; + +// Utilities which act on slightly more particular data forms +// (probably, containers of particular kinds of values) + +export {default as withSum} from './withSum.js'; diff --git a/src/data/composite/data/withFilteredList.js b/src/data/composite/data/withFilteredList.js index 82e56903..15ee3373 100644 --- a/src/data/composite/data/withFilteredList.js +++ b/src/data/composite/data/withFilteredList.js @@ -2,26 +2,14 @@ // corresponding items in a list. Items which correspond to a truthy value // are kept, and the rest are excluded from the output list. // -// 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 // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; @@ -43,7 +31,7 @@ export default templateCompositeFrom({ [input('filter')]: filter, }) => continuation({ '#filteredList': - list.filter((item, index) => filter[index]), + list.filter((_item, index) => filter[index]), }), }, ], diff --git a/src/data/composite/data/withFlattenedList.js b/src/data/composite/data/withFlattenedList.js index edfa3403..31b1a742 100644 --- a/src/data/composite/data/withFlattenedList.js +++ b/src/data/composite/data/withFlattenedList.js @@ -5,12 +5,6 @@ // See also: // - withUnflattenedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFilteredList, withMappedList, withSortedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/withIndexInList.js b/src/data/composite/data/withIndexInList.js new file mode 100644 index 00000000..b1af2033 --- /dev/null +++ b/src/data/composite/data/withIndexInList.js @@ -0,0 +1,38 @@ +// Gets the index of the provided item in the provided list. Note that this +// will output -1 if the item is not found, and this may be detected using +// any availability check with type: 'index'. If the list includes the item +// twice, the output index will be of the first match. +// +// Both the list and item must be provided. +// +// See also: +// - withNearbyItemFromList +// - exitWithoutDependency +// - raiseOutputWithoutDependency +// + +import {input, templateCompositeFrom} from '#composite'; + +export default templateCompositeFrom({ + annotation: `withIndexInList`, + + inputs: { + list: input({acceptsNull: false, type: 'array'}), + item: input({acceptsNull: false}), + }, + + outputs: ['#index'], + + steps: () => [ + { + dependencies: [input('list'), input('item')], + compute: (continuation, { + [input('list')]: list, + [input('item')]: item, + }) => continuation({ + ['#index']: + list.indexOf(item), + }), + }, + ], +}); diff --git a/src/data/composite/data/withLengthOfList.js b/src/data/composite/data/withLengthOfList.js new file mode 100644 index 00000000..e67aa887 --- /dev/null +++ b/src/data/composite/data/withLengthOfList.js @@ -0,0 +1,54 @@ +import {input, templateCompositeFrom} from '#composite'; + +function getOutputName({ + [input.staticDependency('list')]: list, +}) { + if (list && list.startsWith('#')) { + return `${list}.length`; + } else if (list) { + return `#${list}.length`; + } else { + return '#length'; + } +} + +export default templateCompositeFrom({ + annotation: `withMappedList`, + + inputs: { + list: input({type: 'array'}), + }, + + outputs: inputs => [getOutputName(inputs)], + + steps: () => [ + { + dependencies: [input.staticDependency('list')], + compute: (continuation, inputs) => + continuation({'#output': getOutputName(inputs)}), + }, + + { + dependencies: [input('list')], + compute: (continuation, { + [input('list')]: list, + }) => continuation({ + ['#value']: + (list === null + ? null + : list.length), + }), + }, + + { + dependencies: ['#output', '#value'], + + compute: (continuation, { + ['#output']: output, + ['#value']: value, + }) => continuation({ + [output]: value, + }), + }, + ], +}); diff --git a/src/data/composite/data/withMappedList.js b/src/data/composite/data/withMappedList.js index e0a700b2..cd32058e 100644 --- a/src/data/composite/data/withMappedList.js +++ b/src/data/composite/data/withMappedList.js @@ -1,18 +1,16 @@ // Applies a map function to each item in a list, just like a normal JavaScript // map. // +// Pass a filter (e.g. from withAvailabilityFilter) to process only items +// kept by the filter. Other items will be left as-is. +// // See also: // - withFilteredList // - withSortedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; export default templateCompositeFrom({ annotation: `withMappedList`, @@ -20,19 +18,31 @@ export default templateCompositeFrom({ inputs: { list: input({type: 'array'}), map: input({type: 'function'}), + + filter: input({ + type: 'array', + defaultValue: null, + }), }, outputs: ['#mappedList'], steps: () => [ { - dependencies: [input('list'), input('map')], + dependencies: [input('list'), input('map'), input('filter')], compute: (continuation, { [input('list')]: list, [input('map')]: mapFn, + [input('filter')]: filter, }) => continuation({ ['#mappedList']: - list.map(mapFn), + stitchArrays({ + item: list, + keep: filter ?? Array.from(list, () => true), + }).map(({item, keep}, index) => + (keep + ? mapFn(item, index, list) + : item)), }), }, ], diff --git a/src/data/composite/data/withNearbyItemFromList.js b/src/data/composite/data/withNearbyItemFromList.js new file mode 100644 index 00000000..5e165219 --- /dev/null +++ b/src/data/composite/data/withNearbyItemFromList.js @@ -0,0 +1,105 @@ +// Gets a nearby (typically adjacent) item in a list, meaning the item which is +// placed at a particular offset compared to the provided item. This is null if +// the provided list doesn't include the provided item at all, and also if the +// offset would read past either end of the list - except if configured: +// +// - If the 'wrap' input is provided (as true), the offset will loop around +// and continue from the opposing end. +// +// - If the 'valuePastEdge' input is provided, that value will be output +// instead of null. +// +// - If the 'filter' input is provided, corresponding items will be skipped, +// and only (repeating `offset`) the next included in the filter will be +// returned. +// +// Both the list and item must be provided. +// +// See also: +// - withIndexInList +// + +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withIndexInList from './withIndexInList.js'; + +export default templateCompositeFrom({ + annotation: `withNearbyItemFromList`, + + inputs: { + list: input({acceptsNull: false, type: 'array'}), + item: input({acceptsNull: false}), + offset: input({type: 'number'}), + + wrap: input({type: 'boolean', defaultValue: false}), + valuePastEdge: input({defaultValue: null}), + + filter: input({defaultValue: null, type: 'array'}), + }, + + outputs: ['#nearbyItem'], + + steps: () => [ + withIndexInList({ + list: input('list'), + item: input('item'), + }), + + raiseOutputWithoutDependency({ + dependency: '#index', + mode: input.value('index'), + + output: input.value({'#nearbyItem': null}), + }), + + { + dependencies: [ + input('list'), + input('offset'), + + input('wrap'), + input('valuePastEdge'), + + input('filter'), + + '#index', + ], + + compute: (continuation, { + [input('list')]: list, + [input('offset')]: offset, + + [input('wrap')]: wrap, + [input('valuePastEdge')]: valuePastEdge, + + [input('filter')]: filter, + + ['#index']: index, + }) => { + const startIndex = index; + + do { + index += offset; + + if (wrap) { + index = index % list.length; + } else if (index < 0) { + return continuation({'#nearbyItem': valuePastEdge}); + } else if (index >= list.length) { + return continuation({'#nearbyItem': valuePastEdge}); + } + + if (filter && !filter[index]) { + continue; + } + + return continuation({'#nearbyItem': list[index]}); + } while (index !== startIndex); + + return continuation({'#nearbyItem': null}); + }, + }, + ], +}); diff --git a/src/data/composite/data/withPropertiesFromList.js b/src/data/composite/data/withPropertiesFromList.js index 08907bab..fb4134bc 100644 --- a/src/data/composite/data/withPropertiesFromList.js +++ b/src/data/composite/data/withPropertiesFromList.js @@ -8,12 +8,6 @@ // - withPropertiesFromObject // - withPropertyFromList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFilteredList, withMappedList, withSortedList -// - withFlattenedList, withUnflattenedList -// import {input, templateCompositeFrom} from '#composite'; import {isString, validateArrayItems} from '#validators'; diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js index a2c66d77..760095c2 100644 --- a/src/data/composite/data/withPropertyFromList.js +++ b/src/data/composite/data/withPropertyFromList.js @@ -5,17 +5,15 @@ // original list are kept null here. Objects which don't have the specified // property are retained in-place as null. // +// If the `internal` input is true, this reads the CacheableObject update value +// of each object rather than its exposed value. +// // See also: // - withPropertiesFromList // - withPropertyFromObject // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFilteredList, withMappedList, withSortedList -// - withFlattenedList, withUnflattenedList -// +import CacheableObject from '#cacheable-object'; import {input, templateCompositeFrom} from '#composite'; function getOutputName({list, property, prefix}) { @@ -32,6 +30,7 @@ export default templateCompositeFrom({ list: input({type: 'array'}), property: input({type: 'string'}), prefix: input.staticValue({type: 'string', defaultValue: null}), + internal: input({type: 'boolean', defaultValue: false}), }, outputs: ({ @@ -43,13 +42,26 @@ export default templateCompositeFrom({ steps: () => [ { - dependencies: [input('list'), input('property')], + dependencies: [ + input('list'), + input('property'), + input('internal'), + ], + compute: (continuation, { [input('list')]: list, [input('property')]: property, + [input('internal')]: internal, }) => continuation({ ['#values']: - list.map(item => item[property] ?? null), + list.map(item => + (item === null + ? null + : internal + ? CacheableObject.getUpdateValue(item, property) + ?? null + : item[property] + ?? null)), }), }, diff --git a/src/data/composite/data/withPropertyFromObject.js b/src/data/composite/data/withPropertyFromObject.js index b31bab15..7b452b99 100644 --- a/src/data/composite/data/withPropertyFromObject.js +++ b/src/data/composite/data/withPropertyFromObject.js @@ -2,30 +2,42 @@ // If the object itself is null, or the object doesn't have the listed property, // the provided dependency will also be null. // +// If the `internal` input is true, this reads the CacheableObject update value +// of the object rather than its exposed value. +// // See also: // - withPropertiesFromObject // - withPropertyFromList // +import CacheableObject from '#cacheable-object'; import {input, templateCompositeFrom} from '#composite'; +function getOutputName({ + [input.staticDependency('object')]: object, + [input.staticValue('property')]: property, +}) { + if (object && property) { + if (object.startsWith('#')) { + return `${object}.${property}`; + } else { + return `#${object}.${property}`; + } + } else { + return '#value'; + } +} + export default templateCompositeFrom({ annotation: `withPropertyFromObject`, inputs: { object: input({type: 'object', acceptsNull: true}), property: input({type: 'string'}), + internal: input({type: 'boolean', defaultValue: false}), }, - outputs: ({ - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => - (object && property - ? (object.startsWith('#') - ? [`${object}.${property}`] - : [`#${object}.${property}`]) - : ['#value']), + outputs: inputs => [getOutputName(inputs)], steps: () => [ { @@ -34,35 +46,41 @@ export default templateCompositeFrom({ input.staticValue('property'), ], - compute: (continuation, { - [input.staticDependency('object')]: object, - [input.staticValue('property')]: property, - }) => continuation({ - '#output': - (object && property - ? (object.startsWith('#') - ? `${object}.${property}` - : `#${object}.${property}`) - : '#value'), - }), + compute: (continuation, inputs) => + continuation({'#output': getOutputName(inputs)}), }, { dependencies: [ - '#output', input('object'), input('property'), + input('internal'), ], compute: (continuation, { - ['#output']: output, [input('object')]: object, [input('property')]: property, + [input('internal')]: internal, }) => continuation({ - [output]: + '#value': (object === null ? null - : object[property] ?? null), + : internal + ? CacheableObject.getUpdateValue(object, property) + ?? null + : object[property] + ?? null), + }), + }, + + { + dependencies: ['#output', '#value'], + + compute: (continuation, { + ['#output']: output, + ['#value']: value, + }) => continuation({ + [output]: value, }), }, ], diff --git a/src/data/composite/data/withSortedList.js b/src/data/composite/data/withSortedList.js index dd810786..a7d21768 100644 --- a/src/data/composite/data/withSortedList.js +++ b/src/data/composite/data/withSortedList.js @@ -27,12 +27,6 @@ // - withFilteredList // - withMappedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFlattenedList, withUnflattenedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; diff --git a/src/data/composite/data/withStretchedList.js b/src/data/composite/data/withStretchedList.js new file mode 100644 index 00000000..46733064 --- /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/data/withSum.js b/src/data/composite/data/withSum.js new file mode 100644 index 00000000..484e9906 --- /dev/null +++ b/src/data/composite/data/withSum.js @@ -0,0 +1,33 @@ +// Gets the numeric total of adding all the values in a list together. +// Values that are false, null, or undefined are skipped over. + +import {input, templateCompositeFrom} from '#composite'; +import {isNumber, sparseArrayOf} from '#validators'; + +export default templateCompositeFrom({ + annotation: `withSum`, + + inputs: { + values: input({ + validate: sparseArrayOf(isNumber), + }), + }, + + outputs: ['#sum'], + + steps: () => [ + { + dependencies: [input('values')], + compute: (continuation, { + [input('values')]: values, + }) => continuation({ + ['#sum']: + values + .filter(item => typeof item === 'number') + .reduce( + (accumulator, value) => accumulator + value, + 0), + }), + }, + ], +}); diff --git a/src/data/composite/data/withUnflattenedList.js b/src/data/composite/data/withUnflattenedList.js index 39a666dc..820d628a 100644 --- a/src/data/composite/data/withUnflattenedList.js +++ b/src/data/composite/data/withUnflattenedList.js @@ -7,12 +7,6 @@ // See also: // - withFlattenedList // -// More list utilities: -// - excludeFromList -// - fillMissingListItems -// - withFilteredList, withMappedList, withSortedList -// - withPropertyFromList, withPropertiesFromList -// import {input, templateCompositeFrom} from '#composite'; import {isWholeNumber, validateArrayItems} from '#validators'; diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js index 8b5098f0..dfc6864f 100644 --- a/src/data/composite/things/album/index.js +++ b/src/data/composite/things/album/index.js @@ -1 +1,2 @@ +export {default as withHasCoverArt} from './withHasCoverArt.js'; export {default as withTracks} from './withTracks.js'; diff --git a/src/data/composite/things/album/withHasCoverArt.js b/src/data/composite/things/album/withHasCoverArt.js new file mode 100644 index 00000000..fd3f2894 --- /dev/null +++ b/src/data/composite/things/album/withHasCoverArt.js @@ -0,0 +1,64 @@ +// TODO: This shouldn't be coded as an Album-specific thing, +// or even really to do with cover artworks in particular, either. + +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {fillMissingListItems, withFlattenedList, withPropertyFromList} + from '#composite/data'; + +export default templateCompositeFrom({ + annotation: 'withHasCoverArt', + + outputs: ['#hasCoverArt'], + + steps: () => [ + withResultOfAvailabilityCheck({ + from: 'coverArtistContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability'], + compute: (continuation, { + ['#availability']: availability, + }) => + (availability + ? continuation.raiseOutput({ + ['#hasCoverArt']: true, + }) + : continuation()), + }, + + raiseOutputWithoutDependency({ + dependency: 'coverArtworks', + mode: input.value('empty'), + output: input.value({'#hasCoverArt': false}), + }), + + withPropertyFromList({ + list: 'coverArtworks', + property: input.value('artistContribs'), + internal: input.value(true), + }), + + // Since we're getting the update value for each artwork's artistContribs, + // it may not be set at all, and in that case won't be exposing as []. + fillMissingListItems({ + list: '#coverArtworks.artistContribs', + fill: input.value([]), + }), + + withFlattenedList({ + list: '#coverArtworks.artistContribs', + }), + + withResultOfAvailabilityCheck({ + from: '#flattenedList', + mode: input.value('empty'), + }).outputs({ + '#availability': '#hasCoverArt', + }), + ], +}); diff --git a/src/data/composite/things/album/withTracks.js b/src/data/composite/things/album/withTracks.js index 3fe6dd2e..835ee570 100644 --- a/src/data/composite/things/album/withTracks.js +++ b/src/data/composite/things/album/withTracks.js @@ -1,10 +1,8 @@ import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; - -import {exitWithoutDependency} from '#composite/control-flow'; import {withFlattenedList, withPropertyFromList} from '#composite/data'; -import {withResolvedReferenceList} from '#composite/wiki-data'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; export default templateCompositeFrom({ annotation: `withTracks`, @@ -12,16 +10,13 @@ export default templateCompositeFrom({ outputs: ['#tracks'], steps: () => [ - withResolvedReferenceList({ - list: 'trackSections', - data: 'ownTrackSectionData', - find: input.value(find.unqualifiedTrackSection), - }).outputs({ - ['#resolvedReferenceList']: '#trackSections', + raiseOutputWithoutDependency({ + dependency: 'trackSections', + output: input.value({'#tracks': []}), }), withPropertyFromList({ - list: '#trackSections', + list: 'trackSections', property: input.value('tracks'), }), diff --git a/src/data/composite/things/art-tag/index.js b/src/data/composite/things/art-tag/index.js new file mode 100644 index 00000000..bbd38293 --- /dev/null +++ b/src/data/composite/things/art-tag/index.js @@ -0,0 +1,2 @@ +export {default as withAllDescendantArtTags} from './withAllDescendantArtTags.js'; +export {default as withAncestorArtTagBaobabTree} from './withAncestorArtTagBaobabTree.js'; diff --git a/src/data/composite/things/art-tag/withAllDescendantArtTags.js b/src/data/composite/things/art-tag/withAllDescendantArtTags.js new file mode 100644 index 00000000..795f96cd --- /dev/null +++ b/src/data/composite/things/art-tag/withAllDescendantArtTags.js @@ -0,0 +1,44 @@ +// Gets all the art tags which descend from this one - that means its own direct +// descendants, but also all the direct and indirect desceands of each of those! +// The results aren't specially sorted, but they won't contain any duplicates +// (for example if two descendant tags both route deeper to end up including +// some of the same tags). + +import {input, templateCompositeFrom} from '#composite'; +import {unique} from '#sugar'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; +import {soupyFind} from '#composite/wiki-properties'; + +export default templateCompositeFrom({ + annotation: `withAllDescendantArtTags`, + + outputs: ['#allDescendantArtTags'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'directDescendantArtTags', + mode: input.value('empty'), + output: input.value({'#allDescendantArtTags': []}) + }), + + withResolvedReferenceList({ + list: 'directDescendantArtTags', + find: soupyFind.input('artTag'), + }), + + { + dependencies: ['#resolvedReferenceList'], + compute: (continuation, { + ['#resolvedReferenceList']: directDescendantArtTags, + }) => continuation({ + ['#allDescendantArtTags']: + unique([ + ...directDescendantArtTags, + ...directDescendantArtTags.flatMap(artTag => artTag.allDescendantArtTags), + ]), + }), + }, + ], +}) diff --git a/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js b/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js new file mode 100644 index 00000000..e084a42b --- /dev/null +++ b/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js @@ -0,0 +1,46 @@ +// Gets all the art tags which are ancestors of this one as a "baobab tree" - +// what you'd typically think of as roots are all up in the air! Since this +// really is backwards from the way that the art tag tree is written in data, +// chances are pretty good that there will be many of the exact same "leaf" +// nodes - art tags which don't themselves have any ancestors. In the actual +// data structure, each node is a Map, with keys for each ancestor and values +// for each ancestor's own baobab (thus a branching structure, just like normal +// trees in this regard). + +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withReverseReferenceList} from '#composite/wiki-data'; +import {soupyReverse} from '#composite/wiki-properties'; + +export default templateCompositeFrom({ + annotation: `withAncestorArtTagBaobabTree`, + + outputs: ['#ancestorArtTagBaobabTree'], + + steps: () => [ + withReverseReferenceList({ + reverse: soupyReverse.input('artTagsWhichDirectlyAncestor'), + }).outputs({ + ['#reverseReferenceList']: '#directAncestorArtTags', + }), + + raiseOutputWithoutDependency({ + dependency: '#directAncestorArtTags', + mode: input.value('empty'), + output: input.value({'#ancestorArtTagBaobabTree': new Map()}), + }), + + { + dependencies: ['#directAncestorArtTags'], + compute: (continuation, { + ['#directAncestorArtTags']: directAncestorArtTags, + }) => continuation({ + ['#ancestorArtTagBaobabTree']: + new Map( + directAncestorArtTags + .map(artTag => [artTag, artTag.ancestorArtTagBaobabTree])), + }), + }, + ], +}); diff --git a/src/data/composite/things/artist/artistTotalDuration.js b/src/data/composite/things/artist/artistTotalDuration.js new file mode 100644 index 00000000..b8a205fe --- /dev/null +++ b/src/data/composite/things/artist/artistTotalDuration.js @@ -0,0 +1,69 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency} from '#composite/control-flow'; +import {withFilteredList, withPropertyFromList} from '#composite/data'; +import {withContributionListSums, withReverseReferenceList} + from '#composite/wiki-data'; +import {soupyReverse} from '#composite/wiki-properties'; + +export default templateCompositeFrom({ + annotation: `artistTotalDuration`, + + compose: false, + + steps: () => [ + withReverseReferenceList({ + reverse: soupyReverse.input('trackArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#contributionsAsArtist', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('trackContributorContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#contributionsAsContributor', + }), + + { + dependencies: [ + '#contributionsAsArtist', + '#contributionsAsContributor', + ], + + compute: (continuation, { + ['#contributionsAsArtist']: artistContribs, + ['#contributionsAsContributor']: contributorContribs, + }) => continuation({ + ['#allContributions']: [ + ...artistContribs, + ...contributorContribs, + ], + }), + }, + + withPropertyFromList({ + list: '#allContributions', + property: input.value('thing'), + }), + + withPropertyFromList({ + list: '#allContributions.thing', + property: input.value('isMainRelease'), + }), + + withFilteredList({ + list: '#allContributions', + filter: '#allContributions.thing.isMainRelease', + }).outputs({ + '#filteredList': '#mainReleaseContributions', + }), + + withContributionListSums({ + list: '#mainReleaseContributions', + }), + + exposeDependency({ + dependency: '#contributionListDuration', + }), + ], +}); diff --git a/src/data/composite/things/artist/index.js b/src/data/composite/things/artist/index.js new file mode 100644 index 00000000..55514c71 --- /dev/null +++ b/src/data/composite/things/artist/index.js @@ -0,0 +1 @@ +export {default as artistTotalDuration} from './artistTotalDuration.js'; diff --git a/src/data/composite/things/artwork/index.js b/src/data/composite/things/artwork/index.js new file mode 100644 index 00000000..3693c10f --- /dev/null +++ b/src/data/composite/things/artwork/index.js @@ -0,0 +1,5 @@ +export {default as withAttachedArtwork} from './withAttachedArtwork.js'; +export {default as withContainingArtworkList} from './withContainingArtworkList.js'; +export {default as withContribsFromAttachedArtwork} from './withContribsFromAttachedArtwork.js'; +export {default as withDate} from './withDate.js'; +export {default as withPropertyFromAttachedArtwork} from './withPropertyFromAttachedArtwork.js'; diff --git a/src/data/composite/things/artwork/withAttachedArtwork.js b/src/data/composite/things/artwork/withAttachedArtwork.js new file mode 100644 index 00000000..d7c0d87b --- /dev/null +++ b/src/data/composite/things/artwork/withAttachedArtwork.js @@ -0,0 +1,43 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {flipFilter, raiseOutputWithoutDependency} + from '#composite/control-flow'; +import {withNearbyItemFromList, withPropertyFromList} from '#composite/data'; + +import withContainingArtworkList from './withContainingArtworkList.js'; + +export default templateCompositeFrom({ + annotaion: `withContribsFromMainArtwork`, + + outputs: ['#attachedArtwork'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'attachAbove', + mode: input.value('falsy'), + output: input.value({'#attachedArtwork': null}), + }), + + withContainingArtworkList(), + + withPropertyFromList({ + list: '#containingArtworkList', + property: input.value('attachAbove'), + }), + + flipFilter({ + filter: '#containingArtworkList.attachAbove', + }).outputs({ + '#containingArtworkList.attachAbove': '#filterNotAttached', + }), + + withNearbyItemFromList({ + list: '#containingArtworkList', + item: input.myself(), + offset: input.value(-1), + filter: '#filterNotAttached', + }).outputs({ + '#nearbyItem': '#attachedArtwork', + }), + ], +}); diff --git a/src/data/composite/things/artwork/withContainingArtworkList.js b/src/data/composite/things/artwork/withContainingArtworkList.js new file mode 100644 index 00000000..9c928ffd --- /dev/null +++ b/src/data/composite/things/artwork/withContainingArtworkList.js @@ -0,0 +1,46 @@ +// Gets the list of artworks which contains this one, which is functionally +// equivalent to `this.thing[this.thingProperty]`. If the exposed value is not +// a list at all (i.e. the property holds a single artwork), this composition +// outputs null. + +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withContainingArtworkList`, + + outputs: ['#containingArtworkList'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'thing', + output: input.value({'#containingArtworkList': null}), + }), + + raiseOutputWithoutDependency({ + dependency: 'thingProperty', + output: input.value({'#containingArtworkList': null}), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'thingProperty', + }).outputs({ + '#value': '#containingValue', + }), + + { + dependencies: ['#containingValue'], + compute: (continuation, { + ['#containingValue']: containingValue, + }) => continuation({ + ['#containingArtworkList']: + (Array.isArray(containingValue) + ? containingValue + : null), + }), + }, + ], +}); diff --git a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js new file mode 100644 index 00000000..e9425c95 --- /dev/null +++ b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js @@ -0,0 +1,27 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withRecontextualizedContributionList} from '#composite/wiki-data'; + +import withPropertyFromAttachedArtwork from './withPropertyFromAttachedArtwork.js'; + +export default templateCompositeFrom({ + annotaion: `withContribsFromAttachedArtwork`, + + outputs: ['#attachedArtwork.artistContribs'], + + steps: () => [ + withPropertyFromAttachedArtwork({ + property: input.value('artistContribs'), + }), + + raiseOutputWithoutDependency({ + dependency: '#attachedArtwork.artistContribs', + output: input.value({'#attachedArtwork.artistContribs': null}), + }), + + withRecontextualizedContributionList({ + list: '#attachedArtwork.artistContribs', + }), + ], +}); diff --git a/src/data/composite/things/artwork/withDate.js b/src/data/composite/things/artwork/withDate.js new file mode 100644 index 00000000..5e05b814 --- /dev/null +++ b/src/data/composite/things/artwork/withDate.js @@ -0,0 +1,41 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withDate`, + + inputs: { + from: input({ + defaultDependency: 'date', + acceptsNull: true, + }), + }, + + outputs: ['#date'], + + steps: () => [ + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: date, + }) => + (date + ? continuation.raiseOutput({'#date': date}) + : continuation()), + }, + + raiseOutputWithoutDependency({ + dependency: 'dateFromThingProperty', + output: input.value({'#date': null}), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'dateFromThingProperty', + }).outputs({ + ['#value']: '#date', + }), + ], +}) diff --git a/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js b/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js new file mode 100644 index 00000000..a2f954b9 --- /dev/null +++ b/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js @@ -0,0 +1,65 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +import withAttachedArtwork from './withAttachedArtwork.js'; + +function getOutputName({ + [input.staticValue('property')]: property, +}) { + if (property) { + return `#attachedArtwork.${property}`; + } else { + return '#value'; + } +} + +export default templateCompositeFrom({ + annotation: `withPropertyFromAttachedArtwork`, + + inputs: { + property: input({type: 'string'}), + }, + + outputs: inputs => [getOutputName(inputs)], + + steps: () => [ + { + dependencies: [input.staticValue('property')], + compute: (continuation, inputs) => + continuation({'#output': getOutputName(inputs)}), + }, + + withAttachedArtwork(), + + withResultOfAvailabilityCheck({ + from: '#attachedArtwork', + }), + + { + dependencies: ['#availability', '#output'], + compute: (continuation, { + ['#availability']: availability, + ['#output']: output, + }) => + (availability + ? continuation() + : continuation.raiseOutput({[output]: null})), + }, + + withPropertyFromObject({ + object: '#attachedArtwork', + property: input('property'), + }), + + { + dependencies: ['#value', '#output'], + compute: (continuation, { + ['#value']: value, + ['#output']: output, + }) => + continuation.raiseOutput({[output]: value}), + }, + ], +}); diff --git a/src/data/composite/things/content/contentArtists.js b/src/data/composite/things/content/contentArtists.js new file mode 100644 index 00000000..8d5db5a5 --- /dev/null +++ b/src/data/composite/things/content/contentArtists.js @@ -0,0 +1,40 @@ +import {input, templateCompositeFrom} from '#composite'; +import {validateReferenceList} from '#validators'; + +import {exitWithoutDependency, exposeDependency} + from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; +import {soupyFind} from '#composite/wiki-properties'; + +import withExpressedOrImplicitArtistReferences + from './helpers/withExpressedOrImplicitArtistReferences.js'; + +export default templateCompositeFrom({ + annotation: `contentArtists`, + + compose: false, + + update: { + validate: validateReferenceList('artist'), + }, + + steps: () => [ + withExpressedOrImplicitArtistReferences({ + from: input.updateValue(), + }), + + exitWithoutDependency({ + dependency: '#artistReferences', + value: input.value([]), + }), + + withResolvedReferenceList({ + list: '#artistReferences', + find: soupyFind.input('artist'), + }), + + exposeDependency({ + dependency: '#resolvedReferenceList', + }), + ], +}); diff --git a/src/data/composite/things/content/hasAnnotationPart.js b/src/data/composite/things/content/hasAnnotationPart.js new file mode 100644 index 00000000..83d175e3 --- /dev/null +++ b/src/data/composite/things/content/hasAnnotationPart.js @@ -0,0 +1,25 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency} from '#composite/control-flow'; + +import withHasAnnotationPart from './withHasAnnotationPart.js'; + +export default templateCompositeFrom({ + annotation: `hasAnnotationPart`, + + compose: false, + + inputs: { + part: input({type: 'string'}), + }, + + steps: () => [ + withHasAnnotationPart({ + part: input('part'), + }), + + exposeDependency({ + dependency: '#hasAnnotationPart', + }), + ], +}); diff --git a/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js new file mode 100644 index 00000000..62799d43 --- /dev/null +++ b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js @@ -0,0 +1,60 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withFilteredList, withMappedList} from '#composite/data'; +import {withContentNodes} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withExpressedOrImplicitArtistReferences`, + + inputs: { + from: input({type: 'array', acceptsNull: true}), + }, + + outputs: ['#artistReferences'], + + steps: () => [ + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: expressedArtistReferences, + }) => + (expressedArtistReferences + ? continuation.raiseOutput({'#artistReferences': expressedArtistReferences}) + : continuation()), + }, + + raiseOutputWithoutDependency({ + dependency: 'artistText', + output: input.value({'#artistReferences': null}), + }), + + withContentNodes({ + from: 'artistText', + }), + + withMappedList({ + list: '#contentNodes', + map: input.value(node => + node.type === 'tag' && + node.data.replacerKey?.data === 'artist'), + }).outputs({ + '#mappedList': '#artistTagFilter', + }), + + withFilteredList({ + list: '#contentNodes', + filter: '#artistTagFilter', + }).outputs({ + '#filteredList': '#artistTags', + }), + + withMappedList({ + list: '#artistTags', + map: input.value(node => + node.data.replacerValue[0].data), + }).outputs({ + '#mappedList': '#artistReferences', + }), + ], +}); diff --git a/src/data/composite/things/content/index.js b/src/data/composite/things/content/index.js new file mode 100644 index 00000000..4176337d --- /dev/null +++ b/src/data/composite/things/content/index.js @@ -0,0 +1,7 @@ +export {default as contentArtists} from './contentArtists.js'; +export {default as hasAnnotationPart} from './hasAnnotationPart.js'; +export {default as withAnnotationParts} from './withAnnotationParts.js'; +export {default as withHasAnnotationPart} from './withHasAnnotationPart.js'; +export {default as withSourceText} from './withSourceText.js'; +export {default as withSourceURLs} from './withSourceURLs.js'; +export {default as withWebArchiveDate} from './withWebArchiveDate.js'; diff --git a/src/data/composite/things/content/withAnnotationParts.js b/src/data/composite/things/content/withAnnotationParts.js new file mode 100644 index 00000000..6311b57a --- /dev/null +++ b/src/data/composite/things/content/withAnnotationParts.js @@ -0,0 +1,93 @@ +import {input, templateCompositeFrom} from '#composite'; +import {transposeArrays} from '#sugar'; +import {is} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; +import {splitContentNodesAround, withContentNodes} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withAnnotationParts`, + + inputs: { + mode: input({ + validate: is('strings', 'nodes'), + }), + }, + + outputs: ['#annotationParts'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'annotation', + output: input.value({'#annotationParts': []}), + }), + + withContentNodes({ + from: 'annotation', + }), + + splitContentNodesAround({ + nodes: '#contentNodes', + around: input.value(/, */g), + }), + + { + dependencies: ['#contentNodeLists', input('mode')], + compute: (continuation, { + ['#contentNodeLists']: nodeLists, + [input('mode')]: mode, + }) => + (mode === 'nodes' + ? continuation.raiseOutput({'#annotationParts': nodeLists}) + : continuation()), + }, + + { + dependencies: ['#contentNodeLists'], + + compute: (continuation, { + ['#contentNodeLists']: nodeLists, + }) => continuation({ + ['#firstNodes']: + nodeLists.map(list => list.at(0)), + + ['#lastNodes']: + nodeLists.map(list => list.at(-1)), + }), + }, + + withPropertyFromList({ + list: '#firstNodes', + property: input.value('i'), + }).outputs({ + '#firstNodes.i': '#startIndices', + }), + + withPropertyFromList({ + list: '#lastNodes', + property: input.value('iEnd'), + }).outputs({ + '#lastNodes.iEnd': '#endIndices', + }), + + { + dependencies: [ + 'annotation', + '#startIndices', + '#endIndices', + ], + + compute: (continuation, { + ['annotation']: annotation, + ['#startIndices']: startIndices, + ['#endIndices']: endIndices, + }) => continuation({ + ['#annotationParts']: + transposeArrays([startIndices, endIndices]) + .map(([start, end]) => + annotation.slice(start, end)), + }), + }, + ], +}); diff --git a/src/data/composite/things/content/withHasAnnotationPart.js b/src/data/composite/things/content/withHasAnnotationPart.js new file mode 100644 index 00000000..4af554f3 --- /dev/null +++ b/src/data/composite/things/content/withHasAnnotationPart.js @@ -0,0 +1,43 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withAnnotationParts from './withAnnotationParts.js'; + +export default templateCompositeFrom({ + annotation: `withHasAnnotationPart`, + + inputs: { + part: input({type: 'string'}), + }, + + outputs: ['#hasAnnotationPart'], + + steps: () => [ + withAnnotationParts({ + mode: input.value('strings'), + }), + + raiseOutputWithoutDependency({ + dependency: '#annotationParts', + output: input.value({'#hasAnnotationPart': false}), + }), + + { + dependencies: [ + input('part'), + '#annotationParts', + ], + + compute: (continuation, { + [input('part')]: search, + ['#annotationParts']: parts, + }) => continuation({ + ['#hasAnnotationPart']: + parts.some(part => + part.toLowerCase() === + search.toLowerCase()), + }), + }, + ], +}); diff --git a/src/data/composite/things/content/withSourceText.js b/src/data/composite/things/content/withSourceText.js new file mode 100644 index 00000000..292306b7 --- /dev/null +++ b/src/data/composite/things/content/withSourceText.js @@ -0,0 +1,53 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withAnnotationParts from './withAnnotationParts.js'; + +export default templateCompositeFrom({ + annotation: `withSourceText`, + + outputs: ['#sourceText'], + + steps: () => [ + withAnnotationParts({ + mode: input.value('nodes'), + }), + + raiseOutputWithoutDependency({ + dependency: '#annotationParts', + output: input.value({'#sourceText': null}), + }), + + { + dependencies: ['#annotationParts'], + compute: (continuation, { + ['#annotationParts']: annotationParts, + }) => continuation({ + ['#firstPartWithExternalLink']: + annotationParts + .find(nodes => nodes + .some(node => node.type === 'external-link')) ?? + null, + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#firstPartWithExternalLink', + output: input.value({'#sourceText': null}), + }), + + { + dependencies: ['annotation', '#firstPartWithExternalLink'], + compute: (continuation, { + ['annotation']: annotation, + ['#firstPartWithExternalLink']: nodes, + }) => continuation({ + ['#sourceText']: + annotation.slice( + nodes.at(0).i, + nodes.at(-1).iEnd), + }), + }, + ], +}); diff --git a/src/data/composite/things/content/withSourceURLs.js b/src/data/composite/things/content/withSourceURLs.js new file mode 100644 index 00000000..f85ff9ea --- /dev/null +++ b/src/data/composite/things/content/withSourceURLs.js @@ -0,0 +1,62 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withFilteredList, withMappedList} from '#composite/data'; + +import withAnnotationParts from './withAnnotationParts.js'; + +export default templateCompositeFrom({ + annotation: `withSourceURLs`, + + outputs: ['#sourceURLs'], + + steps: () => [ + withAnnotationParts({ + mode: input.value('nodes'), + }), + + raiseOutputWithoutDependency({ + dependency: '#annotationParts', + output: input.value({'#sourceURLs': []}), + }), + + { + dependencies: ['#annotationParts'], + compute: (continuation, { + ['#annotationParts']: annotationParts, + }) => continuation({ + ['#firstPartWithExternalLink']: + annotationParts + .find(nodes => nodes + .some(node => node.type === 'external-link')) ?? + null, + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#firstPartWithExternalLink', + output: input.value({'#sourceURLs': []}), + }), + + withMappedList({ + list: '#firstPartWithExternalLink', + map: input.value(node => node.type === 'external-link'), + }).outputs({ + '#mappedList': '#externalLinkFilter', + }), + + withFilteredList({ + list: '#firstPartWithExternalLink', + filter: '#externalLinkFilter', + }).outputs({ + '#filteredList': '#externalLinks', + }), + + withMappedList({ + list: '#externalLinks', + map: input.value(node => node.data.href), + }).outputs({ + '#mappedList': '#sourceURLs', + }), + ], +}); diff --git a/src/data/composite/things/content/withWebArchiveDate.js b/src/data/composite/things/content/withWebArchiveDate.js new file mode 100644 index 00000000..3aaa4f64 --- /dev/null +++ b/src/data/composite/things/content/withWebArchiveDate.js @@ -0,0 +1,41 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `withWebArchiveDate`, + + outputs: ['#webArchiveDate'], + + steps: () => [ + { + dependencies: ['annotation'], + + compute: (continuation, {annotation}) => + continuation({ + ['#dateText']: + annotation + ?.match(/https?:\/\/web.archive.org\/web\/([0-9]{8,8})[0-9]*\//) + ?.[1] ?? + null, + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#dateText', + output: input.value({['#webArchiveDate']: null}), + }), + + { + dependencies: ['#dateText'], + compute: (continuation, {['#dateText']: dateText}) => + continuation({ + ['#webArchiveDate']: + new Date( + dateText.slice(0, 4) + '/' + + dateText.slice(4, 6) + '/' + + dateText.slice(6, 8)), + }), + }, + ], +}); diff --git a/src/data/composite/things/contribution/index.js b/src/data/composite/things/contribution/index.js new file mode 100644 index 00000000..9b22be2e --- /dev/null +++ b/src/data/composite/things/contribution/index.js @@ -0,0 +1,7 @@ +export {default as inheritFromContributionPresets} from './inheritFromContributionPresets.js'; +export {default as thingPropertyMatches} from './thingPropertyMatches.js'; +export {default as thingReferenceTypeMatches} from './thingReferenceTypeMatches.js'; +export {default as withContainingReverseContributionList} from './withContainingReverseContributionList.js'; +export {default as withContributionArtist} from './withContributionArtist.js'; +export {default as withContributionContext} from './withContributionContext.js'; +export {default as withMatchingContributionPresets} from './withMatchingContributionPresets.js'; diff --git a/src/data/composite/things/contribution/inheritFromContributionPresets.js b/src/data/composite/things/contribution/inheritFromContributionPresets.js new file mode 100644 index 00000000..a74e6db3 --- /dev/null +++ b/src/data/composite/things/contribution/inheritFromContributionPresets.js @@ -0,0 +1,61 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromList} from '#composite/data'; + +import withMatchingContributionPresets + from './withMatchingContributionPresets.js'; + +export default templateCompositeFrom({ + annotation: `inheritFromContributionPresets`, + + inputs: { + property: input({type: 'string'}), + }, + + steps: () => [ + withMatchingContributionPresets().outputs({ + '#matchingContributionPresets': '#presets', + }), + + raiseOutputWithoutDependency({ + dependency: '#presets', + mode: input.value('empty'), + }), + + withPropertyFromList({ + list: '#presets', + property: input('property'), + }), + + { + dependencies: ['#values'], + + compute: (continuation, { + ['#values']: values, + }) => continuation({ + ['#index']: + values.findIndex(value => + value !== undefined && + value !== null), + }), + }, + + raiseOutputWithoutDependency({ + dependency: '#index', + mode: input.value('index'), + }), + + { + dependencies: ['#values', '#index'], + + compute: (continuation, { + ['#values']: values, + ['#index']: index, + }) => continuation({ + ['#value']: + values[index], + }), + }, + ], +}); diff --git a/src/data/composite/things/contribution/thingPropertyMatches.js b/src/data/composite/things/contribution/thingPropertyMatches.js new file mode 100644 index 00000000..a678c3f5 --- /dev/null +++ b/src/data/composite/things/contribution/thingPropertyMatches.js @@ -0,0 +1,45 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `thingPropertyMatches`, + + compose: false, + + inputs: { + value: input({type: 'string'}), + }, + + steps: () => [ + { + dependencies: ['thing', 'thingProperty'], + + compute: (continuation, {thing, thingProperty}) => + continuation({ + ['#thingProperty']: + (thing.constructor[Symbol.for('Thing.referenceType')] === 'artwork' + ? thing.artistContribsFromThingProperty + : thingProperty), + }), + }, + + exitWithoutDependency({ + dependency: '#thingProperty', + value: input.value(false), + }), + + { + dependencies: [ + '#thingProperty', + input('value'), + ], + + compute: ({ + ['#thingProperty']: thingProperty, + [input('value')]: value, + }) => + thingProperty === value, + }, + ], +}); diff --git a/src/data/composite/things/contribution/thingReferenceTypeMatches.js b/src/data/composite/things/contribution/thingReferenceTypeMatches.js new file mode 100644 index 00000000..4042e78f --- /dev/null +++ b/src/data/composite/things/contribution/thingReferenceTypeMatches.js @@ -0,0 +1,66 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `thingReferenceTypeMatches`, + + compose: false, + + inputs: { + value: input({type: 'string'}), + }, + + steps: () => [ + exitWithoutDependency({ + dependency: 'thing', + value: input.value(false), + }), + + withPropertyFromObject({ + object: 'thing', + property: input.value('constructor'), + }), + + { + dependencies: [ + '#thing.constructor', + input('value'), + ], + + compute: (continuation, { + ['#thing.constructor']: constructor, + [input('value')]: value, + }) => + (constructor[Symbol.for('Thing.referenceType')] === value + ? continuation.exit(true) + : constructor[Symbol.for('Thing.referenceType')] === 'artwork' + ? continuation() + : continuation.exit(false)), + }, + + withPropertyFromObject({ + object: 'thing', + property: input.value('thing'), + }), + + withPropertyFromObject({ + object: '#thing.thing', + property: input.value('constructor'), + }), + + { + dependencies: [ + '#thing.thing.constructor', + input('value'), + ], + + compute: ({ + ['#thing.thing.constructor']: constructor, + [input('value')]: value, + }) => + constructor[Symbol.for('Thing.referenceType')] === value, + }, + ], +}); diff --git a/src/data/composite/things/contribution/withContainingReverseContributionList.js b/src/data/composite/things/contribution/withContainingReverseContributionList.js new file mode 100644 index 00000000..175d6cbb --- /dev/null +++ b/src/data/composite/things/contribution/withContainingReverseContributionList.js @@ -0,0 +1,80 @@ +// Get the artist's contribution list containing this property. Although that +// list literally includes both dated and dateless contributions, here, if the +// current contribution is dateless, the list is filtered to only include +// dateless contributions from the same immediately nearby context. + +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +import withContributionArtist from './withContributionArtist.js'; + +export default templateCompositeFrom({ + annotation: `withContainingReverseContributionList`, + + inputs: { + artistProperty: input({ + defaultDependency: 'artistProperty', + acceptsNull: true, + }), + }, + + outputs: ['#containingReverseContributionList'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('artistProperty'), + output: input.value({ + ['#containingReverseContributionList']: + null, + }), + }), + + withContributionArtist(), + + withPropertyFromObject({ + object: '#artist', + property: input('artistProperty'), + }).outputs({ + ['#value']: '#list', + }), + + withResultOfAvailabilityCheck({ + from: 'date', + }).outputs({ + ['#availability']: '#hasDate', + }), + + { + dependencies: ['#hasDate', '#list'], + compute: (continuation, { + ['#hasDate']: hasDate, + ['#list']: list, + }) => + (hasDate + ? continuation.raiseOutput({ + ['#containingReverseContributionList']: + list.filter(contrib => contrib.date), + }) + : continuation({ + ['#list']: + list.filter(contrib => !contrib.date), + })), + }, + + { + dependencies: ['#list', 'thing'], + compute: (continuation, { + ['#list']: list, + ['thing']: thing, + }) => continuation({ + ['#containingReverseContributionList']: + (thing.album + ? list.filter(contrib => contrib.thing.album === thing.album) + : list), + }), + }, + ], +}); diff --git a/src/data/composite/things/contribution/withContributionArtist.js b/src/data/composite/things/contribution/withContributionArtist.js new file mode 100644 index 00000000..5f81c716 --- /dev/null +++ b/src/data/composite/things/contribution/withContributionArtist.js @@ -0,0 +1,26 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withResolvedReference} from '#composite/wiki-data'; +import {soupyFind} from '#composite/wiki-properties'; + +export default templateCompositeFrom({ + annotation: `withContributionArtist`, + + inputs: { + ref: input({ + type: 'string', + defaultDependency: 'artist', + }), + }, + + outputs: ['#artist'], + + steps: () => [ + withResolvedReference({ + ref: input('ref'), + find: soupyFind.input('artist'), + }).outputs({ + '#resolvedReference': '#artist', + }), + ], +}); diff --git a/src/data/composite/things/contribution/withContributionContext.js b/src/data/composite/things/contribution/withContributionContext.js new file mode 100644 index 00000000..3c1c31c0 --- /dev/null +++ b/src/data/composite/things/contribution/withContributionContext.js @@ -0,0 +1,45 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `withContributionContext`, + + outputs: [ + '#contributionTarget', + '#contributionProperty', + ], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'thing', + output: input.value({ + '#contributionTarget': null, + '#contributionProperty': null, + }), + }), + + raiseOutputWithoutDependency({ + dependency: 'thingProperty', + output: input.value({ + '#contributionTarget': null, + '#contributionProperty': null, + }), + }), + + { + dependencies: ['thing', 'thingProperty'], + + compute: (continuation, { + ['thing']: thing, + ['thingProperty']: thingProperty, + }) => continuation({ + ['#contributionTarget']: + thing.constructor[Symbol.for('Thing.referenceType')], + + ['#contributionProperty']: + thingProperty, + }), + }, + ], +}); diff --git a/src/data/composite/things/contribution/withMatchingContributionPresets.js b/src/data/composite/things/contribution/withMatchingContributionPresets.js new file mode 100644 index 00000000..09454164 --- /dev/null +++ b/src/data/composite/things/contribution/withMatchingContributionPresets.js @@ -0,0 +1,70 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withPropertyFromObject} from '#composite/data'; + +import withContributionContext from './withContributionContext.js'; + +export default templateCompositeFrom({ + annotation: `withMatchingContributionPresets`, + + outputs: ['#matchingContributionPresets'], + + steps: () => [ + withPropertyFromObject({ + object: 'thing', + property: input.value('wikiInfo'), + internal: input.value(true), + }), + + raiseOutputWithoutDependency({ + dependency: '#thing.wikiInfo', + output: input.value({ + '#matchingContributionPresets': null, + }), + }), + + withPropertyFromObject({ + object: '#thing.wikiInfo', + property: input.value('contributionPresets'), + }).outputs({ + '#thing.wikiInfo.contributionPresets': '#contributionPresets', + }), + + raiseOutputWithoutDependency({ + dependency: '#contributionPresets', + mode: input.value('empty'), + output: input.value({ + '#matchingContributionPresets': [], + }), + }), + + withContributionContext(), + + { + dependencies: [ + '#contributionPresets', + '#contributionTarget', + '#contributionProperty', + 'annotation', + ], + + compute: (continuation, { + ['#contributionPresets']: presets, + ['#contributionTarget']: target, + ['#contributionProperty']: property, + ['annotation']: annotation, + }) => continuation({ + ['#matchingContributionPresets']: + presets + .filter(preset => + preset.context[0] === target && + preset.context.slice(1).includes(property) && + // For now, only match if the annotation is a complete match. + // Partial matches (e.g. because the contribution includes "two" + // annotations, separated by commas) don't count. + preset.annotation === annotation), + }) + }, + ], +}); diff --git a/src/data/composite/things/flash-act/withFlashSide.js b/src/data/composite/things/flash-act/withFlashSide.js index 64daa1fb..e09f06e6 100644 --- a/src/data/composite/things/flash-act/withFlashSide.js +++ b/src/data/composite/things/flash-act/withFlashSide.js @@ -2,9 +2,10 @@ // If there's no side whose list of flash acts includes this act, the output // dependency will be null. -import {input, templateCompositeFrom} from '#composite'; +import {templateCompositeFrom} from '#composite'; import {withUniqueReferencingThing} from '#composite/wiki-data'; +import {soupyReverse} from '#composite/wiki-properties'; export default templateCompositeFrom({ annotation: `withFlashSide`, @@ -13,8 +14,7 @@ export default templateCompositeFrom({ steps: () => [ withUniqueReferencingThing({ - data: 'flashSideData', - list: input.value('acts'), + reverse: soupyReverse.input('flashSidesWhoseActsInclude'), }).outputs({ ['#uniqueReferencingThing']: '#flashSide', }), diff --git a/src/data/composite/things/flash/withFlashAct.js b/src/data/composite/things/flash/withFlashAct.js index 652b8bfb..87922aff 100644 --- a/src/data/composite/things/flash/withFlashAct.js +++ b/src/data/composite/things/flash/withFlashAct.js @@ -2,9 +2,10 @@ // If there's no flash whose list of flashes includes this flash, the output // dependency will be null. -import {input, templateCompositeFrom} from '#composite'; +import {templateCompositeFrom} from '#composite'; import {withUniqueReferencingThing} from '#composite/wiki-data'; +import {soupyReverse} from '#composite/wiki-properties'; export default templateCompositeFrom({ annotation: `withFlashAct`, @@ -13,8 +14,7 @@ export default templateCompositeFrom({ steps: () => [ withUniqueReferencingThing({ - data: 'flashActData', - list: input.value('flashes'), + reverse: soupyReverse.input('flashActsWhoseFlashesInclude'), }).outputs({ ['#uniqueReferencingThing']: '#flashAct', }), diff --git a/src/data/composite/things/track-section/index.js b/src/data/composite/things/track-section/index.js index 3202ed49..f11a2ab5 100644 --- a/src/data/composite/things/track-section/index.js +++ b/src/data/composite/things/track-section/index.js @@ -1 +1,3 @@ export {default as withAlbum} from './withAlbum.js'; +export {default as withContinueCountingFrom} from './withContinueCountingFrom.js'; +export {default as withStartCountingFrom} from './withStartCountingFrom.js'; diff --git a/src/data/composite/things/track-section/withAlbum.js b/src/data/composite/things/track-section/withAlbum.js index 608cc0cd..e257062e 100644 --- a/src/data/composite/things/track-section/withAlbum.js +++ b/src/data/composite/things/track-section/withAlbum.js @@ -1,10 +1,9 @@ -// Gets the track section's album. This will early exit if ownAlbumData is -// missing. If there's no album whose list of track sections includes this one, -// the output dependency will be null. +// Gets the track section's album. -import {input, templateCompositeFrom} from '#composite'; +import {templateCompositeFrom} from '#composite'; import {withUniqueReferencingThing} from '#composite/wiki-data'; +import {soupyReverse} from '#composite/wiki-properties'; export default templateCompositeFrom({ annotation: `withAlbum`, @@ -13,8 +12,7 @@ export default templateCompositeFrom({ steps: () => [ withUniqueReferencingThing({ - data: 'ownAlbumData', - list: input.value('trackSections'), + reverse: soupyReverse.input('albumsWhoseTrackSectionsInclude'), }).outputs({ ['#uniqueReferencingThing']: '#album', }), diff --git a/src/data/composite/things/track-section/withContinueCountingFrom.js b/src/data/composite/things/track-section/withContinueCountingFrom.js new file mode 100644 index 00000000..0ca52b6c --- /dev/null +++ b/src/data/composite/things/track-section/withContinueCountingFrom.js @@ -0,0 +1,25 @@ +import {templateCompositeFrom} from '#composite'; + +import withStartCountingFrom from './withStartCountingFrom.js'; + +export default templateCompositeFrom({ + annotation: `withContinueCountingFrom`, + + outputs: ['#continueCountingFrom'], + + steps: () => [ + withStartCountingFrom(), + + { + dependencies: ['#startCountingFrom', 'tracks'], + compute: (continuation, { + ['#startCountingFrom']: startCountingFrom, + ['tracks']: tracks, + }) => continuation({ + ['#continueCountingFrom']: + startCountingFrom + + tracks.length, + }), + }, + ], +}); diff --git a/src/data/composite/things/track-section/withStartCountingFrom.js b/src/data/composite/things/track-section/withStartCountingFrom.js new file mode 100644 index 00000000..ef345327 --- /dev/null +++ b/src/data/composite/things/track-section/withStartCountingFrom.js @@ -0,0 +1,64 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withNearbyItemFromList, withPropertyFromObject} from '#composite/data'; + +import withAlbum from './withAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withStartCountingFrom`, + + inputs: { + from: input({ + type: 'number', + defaultDependency: 'startCountingFrom', + acceptsNull: true, + }), + }, + + outputs: ['#startCountingFrom'], + + steps: () => [ + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: from, + }) => + (from === null + ? continuation() + : continuation.raiseOutput({'#startCountingFrom': from})), + }, + + withAlbum(), + + raiseOutputWithoutDependency({ + dependency: '#album', + output: input.value({'#startCountingFrom': 1}), + }), + + withPropertyFromObject({ + object: '#album', + property: input.value('trackSections'), + }), + + withNearbyItemFromList({ + list: '#album.trackSections', + item: input.myself(), + offset: input.value(-1), + }).outputs({ + '#nearbyItem': '#previousTrackSection', + }), + + raiseOutputWithoutDependency({ + dependency: '#previousTrackSection', + output: input.value({'#startCountingFrom': 1}), + }), + + withPropertyFromObject({ + object: '#previousTrackSection', + property: input.value('continueCountingFrom'), + }).outputs({ + '#previousTrackSection.continueCountingFrom': '#startCountingFrom', + }), + ], +}); diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js index 8959de9f..e789e736 100644 --- a/src/data/composite/things/track/index.js +++ b/src/data/composite/things/track/index.js @@ -1,12 +1,17 @@ export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js'; -export {default as inferredAdditionalNameList} from './inferredAdditionalNameList.js'; -export {default as inheritFromOriginalRelease} from './inheritFromOriginalRelease.js'; -export {default as sharedAdditionalNameList} from './sharedAdditionalNameList.js'; -export {default as trackReverseReferenceList} from './trackReverseReferenceList.js'; -export {default as withAlbum} from './withAlbum.js'; +export {default as inheritContributionListFromMainRelease} from './inheritContributionListFromMainRelease.js'; +export {default as inheritFromMainRelease} from './inheritFromMainRelease.js'; +export {default as withAllReleases} from './withAllReleases.js'; export {default as withAlwaysReferenceByDirectory} from './withAlwaysReferenceByDirectory.js'; export {default as withContainingTrackSection} from './withContainingTrackSection.js'; +export {default as withCoverArtistContribs} from './withCoverArtistContribs.js'; +export {default as withDate} from './withDate.js'; +export {default as withDirectorySuffix} from './withDirectorySuffix.js'; export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js'; +export {default as withMainRelease} from './withMainRelease.js'; export {default as withOtherReleases} from './withOtherReleases.js'; export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js'; -export {default as withPropertyFromOriginalRelease} from './withPropertyFromOriginalRelease.js'; +export {default as withPropertyFromMainRelease} from './withPropertyFromMainRelease.js'; +export {default as withSuffixDirectoryFromAlbum} from './withSuffixDirectoryFromAlbum.js'; +export {default as withTrackArtDate} from './withTrackArtDate.js'; +export {default as withTrackNumber} from './withTrackNumber.js'; diff --git a/src/data/composite/things/track/inferredAdditionalNameList.js b/src/data/composite/things/track/inferredAdditionalNameList.js deleted file mode 100644 index 58e8d2a1..00000000 --- a/src/data/composite/things/track/inferredAdditionalNameList.js +++ /dev/null @@ -1,67 +0,0 @@ -// Infers additional name entries from other releases that were titled -// differently; the corresponding releases are stored in eacn entry's "from" -// array, which will include multiple items, if more than one other release -// shares the same name differing from this one's. - -import {input, templateCompositeFrom} from '#composite'; -import {chunkByProperties} from '#sugar'; - -import {exitWithoutDependency} from '#composite/control-flow'; -import {withFilteredList, withPropertyFromList} from '#composite/data'; -import {withThingsSortedAlphabetically} from '#composite/wiki-data'; - -import withOtherReleases from './withOtherReleases.js'; - -export default templateCompositeFrom({ - annotation: `inferredAdditionalNameList`, - - compose: false, - - steps: () => [ - withOtherReleases(), - - exitWithoutDependency({ - dependency: '#otherReleases', - mode: input.value('empty'), - value: input.value([]), - }), - - withPropertyFromList({ - list: '#otherReleases', - property: input.value('name'), - }), - - { - dependencies: ['#otherReleases.name', 'name'], - compute: (continuation, { - ['#otherReleases.name']: releaseNames, - ['name']: ownName, - }) => continuation({ - ['#nameFilter']: - releaseNames.map(name => name !== ownName), - }), - }, - - withFilteredList({ - list: '#otherReleases', - filter: '#nameFilter', - }).outputs({ - '#filteredList': '#differentlyNamedReleases', - }), - - withThingsSortedAlphabetically({ - things: '#differentlyNamedReleases', - }).outputs({ - '#sortedThings': '#differentlyNamedReleases', - }), - - { - dependencies: ['#differentlyNamedReleases'], - compute: ({ - ['#differentlyNamedReleases']: releases, - }) => - chunkByProperties(releases, ['name']) - .map(({name, chunk}) => ({name, from: chunk})), - }, - ], -}); diff --git a/src/data/composite/things/track/inheritContributionListFromMainRelease.js b/src/data/composite/things/track/inheritContributionListFromMainRelease.js new file mode 100644 index 00000000..89252feb --- /dev/null +++ b/src/data/composite/things/track/inheritContributionListFromMainRelease.js @@ -0,0 +1,44 @@ +// Like inheritFromMainRelease, but tuned for contributions. +// Recontextualizes contributions for this track. + +import {input, templateCompositeFrom} from '#composite'; + +import {exposeDependency, raiseOutputWithoutDependency} + from '#composite/control-flow'; +import {withRecontextualizedContributionList, withRedatedContributionList} + from '#composite/wiki-data'; + +import withDate from './withDate.js'; +import withPropertyFromMainRelease + from './withPropertyFromMainRelease.js'; + +export default templateCompositeFrom({ + annotation: `inheritContributionListFromMainRelease`, + + steps: () => [ + withPropertyFromMainRelease({ + property: input.thisProperty(), + notFoundValue: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: '#isSecondaryRelease', + mode: input.value('falsy'), + }), + + withRecontextualizedContributionList({ + list: '#mainReleaseValue', + }), + + withDate(), + + withRedatedContributionList({ + list: '#mainReleaseValue', + date: '#date', + }), + + exposeDependency({ + dependency: '#mainReleaseValue', + }), + ], +}); diff --git a/src/data/composite/things/track/inheritFromOriginalRelease.js b/src/data/composite/things/track/inheritFromMainRelease.js index 38ab06be..b1cbb65e 100644 --- a/src/data/composite/things/track/inheritFromOriginalRelease.js +++ b/src/data/composite/things/track/inheritFromMainRelease.js @@ -1,9 +1,9 @@ // Early exits with the value for the same property as specified on the -// original release, if this track is a rerelease, and otherwise continues +// main release, if this track is a secondary release, and otherwise continues // without providing any further dependencies. // -// Like withOriginalRelease, this will early exit (with notFoundValue) if the -// original release is specified by reference and that reference doesn't +// Like withMainRelease, this will early exit (with notFoundValue) if the +// main release is specified by reference and that reference doesn't // resolve to anything. import {input, templateCompositeFrom} from '#composite'; @@ -11,11 +11,11 @@ import {input, templateCompositeFrom} from '#composite'; import {exposeDependency, raiseOutputWithoutDependency} from '#composite/control-flow'; -import withPropertyFromOriginalRelease - from './withPropertyFromOriginalRelease.js'; +import withPropertyFromMainRelease + from './withPropertyFromMainRelease.js'; export default templateCompositeFrom({ - annotation: `inheritFromOriginalRelease`, + annotation: `inheritFromMainRelease`, inputs: { notFoundValue: input({ @@ -24,18 +24,18 @@ export default templateCompositeFrom({ }, steps: () => [ - withPropertyFromOriginalRelease({ + withPropertyFromMainRelease({ property: input.thisProperty(), notFoundValue: input('notFoundValue'), }), raiseOutputWithoutDependency({ - dependency: '#isRerelease', + dependency: '#isSecondaryRelease', mode: input.value('falsy'), }), exposeDependency({ - dependency: '#originalValue', + dependency: '#mainReleaseValue', }), ], }); diff --git a/src/data/composite/things/track/sharedAdditionalNameList.js b/src/data/composite/things/track/sharedAdditionalNameList.js deleted file mode 100644 index 1806ec80..00000000 --- a/src/data/composite/things/track/sharedAdditionalNameList.js +++ /dev/null @@ -1,38 +0,0 @@ -// Compiles additional names directly provided by other releases. - -import {input, templateCompositeFrom} from '#composite'; - -import {exitWithoutDependency, exposeDependency} - from '#composite/control-flow'; -import {withFlattenedList, withPropertyFromList} from '#composite/data'; - -import withOtherReleases from './withOtherReleases.js'; - -export default templateCompositeFrom({ - annotation: `sharedAdditionalNameList`, - - compose: false, - - steps: () => [ - withOtherReleases(), - - exitWithoutDependency({ - dependency: '#otherReleases', - mode: input.value('empty'), - value: input.value([]), - }), - - withPropertyFromList({ - list: '#otherReleases', - property: input.value('additionalNames'), - }), - - withFlattenedList({ - list: '#otherReleases.additionalNames', - }), - - exposeDependency({ - dependency: '#flattenedList', - }), - ], -}); diff --git a/src/data/composite/things/track/trackAdditionalNameList.js b/src/data/composite/things/track/trackAdditionalNameList.js deleted file mode 100644 index 65a2263d..00000000 --- a/src/data/composite/things/track/trackAdditionalNameList.js +++ /dev/null @@ -1,38 +0,0 @@ -// Compiles additional names from various sources. - -import {input, templateCompositeFrom} from '#composite'; -import {isAdditionalNameList} from '#validators'; - -import withInferredAdditionalNames from './withInferredAdditionalNames.js'; -import withSharedAdditionalNames from './withSharedAdditionalNames.js'; - -export default templateCompositeFrom({ - annotation: `trackAdditionalNameList`, - - compose: false, - - update: {validate: isAdditionalNameList}, - - steps: () => [ - withInferredAdditionalNames(), - withSharedAdditionalNames(), - - { - dependencies: [ - '#inferredAdditionalNames', - '#sharedAdditionalNames', - input.updateValue(), - ], - - compute: ({ - ['#inferredAdditionalNames']: inferredAdditionalNames, - ['#sharedAdditionalNames']: sharedAdditionalNames, - [input.updateValue()]: providedAdditionalNames, - }) => [ - ...providedAdditionalNames ?? [], - ...sharedAdditionalNames, - ...inferredAdditionalNames, - ], - }, - ], -}); diff --git a/src/data/composite/things/track/trackReverseReferenceList.js b/src/data/composite/things/track/trackReverseReferenceList.js deleted file mode 100644 index 44940ae7..00000000 --- a/src/data/composite/things/track/trackReverseReferenceList.js +++ /dev/null @@ -1,38 +0,0 @@ -// Like a normal reverse reference list ("objects which reference this object -// under a specified property"), only excluding rereleases from the possible -// outputs. While it's useful to travel from a rerelease to the tracks it -// references, rereleases aren't generally relevant from the perspective of -// the tracks *being* referenced. Apart from hiding rereleases from lists on -// the site, it also excludes keeps them from relational data processing, such -// as on the "Tracks - by Times Referenced" listing page. - -import {input, templateCompositeFrom} from '#composite'; -import {withReverseReferenceList} from '#composite/wiki-data'; - -export default templateCompositeFrom({ - annotation: `trackReverseReferenceList`, - - compose: false, - - inputs: { - list: input({type: 'string'}), - }, - - steps: () => [ - withReverseReferenceList({ - data: 'trackData', - list: input('list'), - }), - - { - flags: {expose: true}, - expose: { - dependencies: ['#reverseReferenceList'], - compute: ({ - ['#reverseReferenceList']: reverseReferenceList, - }) => - reverseReferenceList.filter(track => !track.originalReleaseTrack), - }, - }, - ], -}); diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js deleted file mode 100644 index 03b840d4..00000000 --- a/src/data/composite/things/track/withAlbum.js +++ /dev/null @@ -1,22 +0,0 @@ -// Gets the track's album. This will early exit if albumData is missing. -// If there's no album whose list of tracks includes this track, the output -// dependency will be null. - -import {input, templateCompositeFrom} from '#composite'; - -import {withUniqueReferencingThing} from '#composite/wiki-data'; - -export default templateCompositeFrom({ - annotation: `withAlbum`, - - outputs: ['#album'], - - steps: () => [ - withUniqueReferencingThing({ - data: 'albumData', - list: input.value('tracks'), - }).outputs({ - ['#uniqueReferencingThing']: '#album', - }), - ], -}); diff --git a/src/data/composite/things/track/withAllReleases.js b/src/data/composite/things/track/withAllReleases.js new file mode 100644 index 00000000..891db102 --- /dev/null +++ b/src/data/composite/things/track/withAllReleases.js @@ -0,0 +1,46 @@ +// Gets all releases of the current track. All items of the outputs are +// distinct Track objects; one track is the main release; all else are +// secondary releases of that main release; and one item, which may be +// the main release or one of the secondary releases, is the current +// track. The results are sorted by date, and it is possible that the +// main release is not actually the earliest/first. + +import {input, templateCompositeFrom} from '#composite'; +import {sortByDate} from '#sort'; + +import {withPropertyFromObject} from '#composite/data'; + +import withMainRelease from './withMainRelease.js'; + +export default templateCompositeFrom({ + annotation: `withAllReleases`, + + outputs: ['#allReleases'], + + steps: () => [ + withMainRelease({ + selfIfMain: input.value(true), + notFoundValue: input.value([]), + }), + + // We don't talk about bruno no no + // Yes, this can perform a normal access equivalent to + // `this.secondaryReleases` from within a data composition. + // Oooooooooooooooooooooooooooooooooooooooooooooooo + withPropertyFromObject({ + object: '#mainRelease', + property: input.value('secondaryReleases'), + }), + + { + dependencies: ['#mainRelease', '#mainRelease.secondaryReleases'], + compute: (continuation, { + ['#mainRelease']: mainRelease, + ['#mainRelease.secondaryReleases']: secondaryReleases, + }) => continuation({ + ['#allReleases']: + sortByDate([mainRelease, ...secondaryReleases]), + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js index e01720b4..87edf21e 100644 --- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js +++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js @@ -16,6 +16,8 @@ import { exposeUpdateValueOrContinue, } from '#composite/control-flow'; +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + export default templateCompositeFrom({ annotation: `withAlwaysReferenceByDirectory`, @@ -26,19 +28,7 @@ export default templateCompositeFrom({ validate: input.value(isBoolean), }), - // withAlwaysReferenceByDirectory is sort of a fragile area - we can't - // find the track's album the normal way because albums' track lists - // recurse back into alwaysReferenceByDirectory! - withResolvedReference({ - ref: 'dataSourceAlbum', - data: 'albumData', - find: input.value(find.album), - }).outputs({ - '#resolvedReference': '#album', - }), - - withPropertyFromObject({ - object: '#album', + withPropertyFromAlbum({ property: input.value('alwaysReferenceTracksByDirectory'), }), @@ -51,7 +41,7 @@ export default templateCompositeFrom({ // Remaining code is for defaulting to true if this track is a rerelease of // another with the same name, so everything further depends on access to - // trackData as well as originalReleaseTrack. + // trackData as well as mainReleaseTrack. exitWithoutDependency({ dependency: 'trackData', @@ -60,45 +50,46 @@ export default templateCompositeFrom({ }), exitWithoutDependency({ - dependency: 'originalReleaseTrack', + dependency: 'mainReleaseTrack', value: input.value(false), }), - // It's necessary to use the custom trackOriginalReleasesOnly find function + // It's necessary to use the custom trackMainReleasesOnly find function // here, so as to avoid recursion issues - the find.track() function depends // on accessing each track's alwaysReferenceByDirectory, which means it'll // hit *this track* - and thus this step - and end up recursing infinitely. - // By definition, find.trackOriginalReleasesOnly excludes tracks which have - // an originalReleaseTrack update value set, which means even though it does + // By definition, find.trackMainReleasesOnly excludes tracks which have + // an mainReleaseTrack update value set, which means even though it does // still access each of tracks' `alwaysReferenceByDirectory` property, it // won't access that of *this* track - it will never proceed past the // `exitWithoutDependency` step directly above, so there's no opportunity // for recursion. withResolvedReference({ - ref: 'originalReleaseTrack', + ref: 'mainReleaseTrack', data: 'trackData', - find: input.value(find.trackOriginalReleasesOnly), + find: input.value(find.trackMainReleasesOnly), }).outputs({ - '#resolvedReference': '#originalRelease', + '#resolvedReference': '#mainRelease', }), exitWithoutDependency({ - dependency: '#originalRelease', + dependency: '#mainRelease', value: input.value(false), }), withPropertyFromObject({ - object: '#originalRelease', + object: '#mainRelease', property: input.value('name'), }), { - dependencies: ['name', '#originalRelease.name'], + dependencies: ['name', '#mainRelease.name'], compute: (continuation, { name, - ['#originalRelease.name']: originalName, + ['#mainRelease.name']: mainReleaseName, }) => continuation({ - ['#alwaysReferenceByDirectory']: name === originalName, + ['#alwaysReferenceByDirectory']: + name === mainReleaseName, }), }, ], diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js index eaac14de..3d4d081e 100644 --- a/src/data/composite/things/track/withContainingTrackSection.js +++ b/src/data/composite/things/track/withContainingTrackSection.js @@ -1,11 +1,9 @@ // Gets the track section containing this track from its album's track list. -import {input, templateCompositeFrom} from '#composite'; -import {is} from '#validators'; +import {templateCompositeFrom} from '#composite'; -import {raiseOutputWithoutDependency} from '#composite/control-flow'; - -import withPropertyFromAlbum from './withPropertyFromAlbum.js'; +import {withUniqueReferencingThing} from '#composite/wiki-data'; +import {soupyReverse} from '#composite/wiki-properties'; export default templateCompositeFrom({ annotation: `withContainingTrackSection`, @@ -13,30 +11,10 @@ export default templateCompositeFrom({ outputs: ['#trackSection'], steps: () => [ - withPropertyFromAlbum({ - property: input.value('trackSections'), - }), - - raiseOutputWithoutDependency({ - dependency: '#album.trackSections', - output: input.value({'#trackSection': null}), + withUniqueReferencingThing({ + reverse: soupyReverse.input('trackSectionsWhichInclude'), + }).outputs({ + ['#uniqueReferencingThing']: '#trackSection', }), - - { - dependencies: [ - input.myself(), - '#album.trackSections', - ], - - compute: (continuation, { - [input.myself()]: track, - [input('notFoundMode')]: notFoundMode, - ['#album.trackSections']: trackSections, - }) => continuation({ - ['#trackSection']: - trackSections.find(({tracks}) => tracks.includes(track)) - ?? null, - }), - }, ], }); diff --git a/src/data/composite/things/track/withCoverArtistContribs.js b/src/data/composite/things/track/withCoverArtistContribs.js new file mode 100644 index 00000000..9057cfeb --- /dev/null +++ b/src/data/composite/things/track/withCoverArtistContribs.js @@ -0,0 +1,73 @@ +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {exposeDependencyOrContinue} from '#composite/control-flow'; + +import { + withRecontextualizedContributionList, + withRedatedContributionList, + withResolvedContribs, +} from '#composite/wiki-data'; + +import exitWithoutUniqueCoverArt from './exitWithoutUniqueCoverArt.js'; +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; +import withTrackArtDate from './withTrackArtDate.js'; + +export default templateCompositeFrom({ + annotation: `withCoverArtistContribs`, + + inputs: { + from: input({ + defaultDependency: 'coverArtistContribs', + validate: isContributionList, + acceptsNull: true, + }), + }, + + outputs: ['#coverArtistContribs'], + + steps: () => [ + exitWithoutUniqueCoverArt({ + value: input.value([]), + }), + + withTrackArtDate(), + + withResolvedContribs({ + from: input('from'), + thingProperty: input.value('coverArtistContribs'), + artistProperty: input.value('trackCoverArtistContributions'), + date: '#trackArtDate', + }).outputs({ + '#resolvedContribs': '#coverArtistContribs', + }), + + exposeDependencyOrContinue({ + dependency: '#coverArtistContribs', + mode: input.value('empty'), + }), + + withPropertyFromAlbum({ + property: input.value('trackCoverArtistContribs'), + }), + + withRecontextualizedContributionList({ + list: '#album.trackCoverArtistContribs', + artistProperty: input.value('trackCoverArtistContributions'), + }), + + withRedatedContributionList({ + list: '#album.trackCoverArtistContribs', + date: '#trackArtDate', + }), + + { + dependencies: ['#album.trackCoverArtistContribs'], + compute: (continuation, { + ['#album.trackCoverArtistContribs']: coverArtistContribs, + }) => continuation({ + ['#coverArtistContribs']: coverArtistContribs, + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withDate.js b/src/data/composite/things/track/withDate.js new file mode 100644 index 00000000..b5a770e9 --- /dev/null +++ b/src/data/composite/things/track/withDate.js @@ -0,0 +1,34 @@ +// Gets the track's own date. This is either its dateFirstReleased property +// or, if unset, the album's date. + +import {input, templateCompositeFrom} from '#composite'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withDate`, + + outputs: ['#date'], + + steps: () => [ + { + dependencies: ['dateFirstReleased'], + compute: (continuation, {dateFirstReleased}) => + (dateFirstReleased + ? continuation.raiseOutput({'#date': dateFirstReleased}) + : continuation()), + }, + + withPropertyFromAlbum({ + property: input.value('date'), + }), + + { + dependencies: ['#album.date'], + compute: (continuation, {['#album.date']: albumDate}) => + (albumDate + ? continuation.raiseOutput({'#date': albumDate}) + : continuation.raiseOutput({'#date': null})), + }, + ], +}) diff --git a/src/data/composite/things/track/withDirectorySuffix.js b/src/data/composite/things/track/withDirectorySuffix.js new file mode 100644 index 00000000..c063e158 --- /dev/null +++ b/src/data/composite/things/track/withDirectorySuffix.js @@ -0,0 +1,36 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; +import withSuffixDirectoryFromAlbum from './withSuffixDirectoryFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withDirectorySuffix`, + + outputs: ['#directorySuffix'], + + steps: () => [ + withSuffixDirectoryFromAlbum(), + + raiseOutputWithoutDependency({ + dependency: '#suffixDirectoryFromAlbum', + mode: input.value('falsy'), + output: input.value({['#directorySuffix']: null}), + }), + + withPropertyFromAlbum({ + property: input.value('directorySuffix'), + }), + + { + dependencies: ['#album.directorySuffix'], + compute: (continuation, { + ['#album.directorySuffix']: directorySuffix, + }) => continuation({ + ['#directorySuffix']: + directorySuffix, + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js index 96078d5f..85d3b92a 100644 --- a/src/data/composite/things/track/withHasUniqueCoverArt.js +++ b/src/data/composite/things/track/withHasUniqueCoverArt.js @@ -5,11 +5,18 @@ // or a placeholder. (This property is named hasUniqueCoverArt instead of // the usual hasCoverArt to emphasize that it does not inherit from the // album.) +// +// withHasUniqueCoverArt is based only around the presence of *specified* +// cover artist contributions, not whether the references to artists on those +// contributions actually resolve to anything. It completely evades interacting +// with find/replace. import {input, templateCompositeFrom} from '#composite'; -import {empty} from '#sugar'; -import {withResolvedContribs} from '#composite/wiki-data'; +import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck} + from '#composite/control-flow'; +import {fillMissingListItems, withFlattenedList, withPropertyFromList} + from '#composite/data'; import withPropertyFromAlbum from './withPropertyFromAlbum.js'; @@ -29,33 +36,73 @@ export default templateCompositeFrom({ : continuation()), }, - withResolvedContribs({from: 'coverArtistContribs'}), + withResultOfAvailabilityCheck({ + from: 'coverArtistContribs', + mode: input.value('empty'), + }), { - dependencies: ['#resolvedContribs'], + dependencies: ['#availability'], compute: (continuation, { - ['#resolvedContribs']: contribsFromTrack, + ['#availability']: availability, }) => - (empty(contribsFromTrack) - ? continuation() - : continuation.raiseOutput({ + (availability + ? continuation.raiseOutput({ ['#hasUniqueCoverArt']: true, - })), + }) + : continuation()), }, withPropertyFromAlbum({ property: input.value('trackCoverArtistContribs'), + internal: input.value(true), + }), + + withResultOfAvailabilityCheck({ + from: '#album.trackCoverArtistContribs', + mode: input.value('empty'), }), { - dependencies: ['#album.trackCoverArtistContribs'], + dependencies: ['#availability'], compute: (continuation, { - ['#album.trackCoverArtistContribs']: contribsFromAlbum, + ['#availability']: availability, }) => - continuation.raiseOutput({ - ['#hasUniqueCoverArt']: - !empty(contribsFromAlbum), - }), + (availability + ? continuation.raiseOutput({ + ['#hasUniqueCoverArt']: true, + }) + : continuation()), }, + + raiseOutputWithoutDependency({ + dependency: 'trackArtworks', + mode: input.value('empty'), + output: input.value({'#hasUniqueCoverArt': false}), + }), + + withPropertyFromList({ + list: 'trackArtworks', + property: input.value('artistContribs'), + internal: input.value(true), + }), + + // Since we're getting the update value for each artwork's artistContribs, + // it may not be set at all, and in that case won't be exposing as []. + fillMissingListItems({ + list: '#trackArtworks.artistContribs', + fill: input.value([]), + }), + + withFlattenedList({ + list: '#trackArtworks.artistContribs', + }), + + withResultOfAvailabilityCheck({ + from: '#flattenedList', + mode: input.value('empty'), + }).outputs({ + '#availability': '#hasUniqueCoverArt', + }), ], }); diff --git a/src/data/composite/things/track/withOriginalRelease.js b/src/data/composite/things/track/withMainRelease.js index c7f49657..3a91edae 100644 --- a/src/data/composite/things/track/withOriginalRelease.js +++ b/src/data/composite/things/track/withMainRelease.js @@ -1,62 +1,54 @@ -// Just includes the original release of this track as a dependency. -// If this track isn't a rerelease, then it'll provide null, unless the -// {selfIfOriginal} option is set, in which case it'll provide this track -// itself. This will early exit (with notFoundValue) if the original release +// Just includes the main release of this track as a dependency. +// If this track isn't a secondary release, then it'll provide null, unless +// the {selfIfMain} option is set, in which case it'll provide this track +// itself. This will early exit (with notFoundValue) if the main release // is specified by reference and that reference doesn't resolve to anything. import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; -import {validateWikiData} from '#validators'; import {exitWithoutDependency, withResultOfAvailabilityCheck} from '#composite/control-flow'; import {withResolvedReference} from '#composite/wiki-data'; +import {soupyFind} from '#composite/wiki-properties'; export default templateCompositeFrom({ - annotation: `withOriginalRelease`, + annotation: `withMainRelease`, inputs: { - selfIfOriginal: input({type: 'boolean', defaultValue: false}), - - data: input({ - validate: validateWikiData({referenceType: 'track'}), - defaultDependency: 'trackData', - }), - + selfIfMain: input({type: 'boolean', defaultValue: false}), notFoundValue: input({defaultValue: null}), }, - outputs: ['#originalRelease'], + outputs: ['#mainRelease'], steps: () => [ withResultOfAvailabilityCheck({ - from: 'originalReleaseTrack', + from: 'mainReleaseTrack', }), { dependencies: [ input.myself(), - input('selfIfOriginal'), + input('selfIfMain'), '#availability', ], compute: (continuation, { [input.myself()]: track, - [input('selfIfOriginal')]: selfIfOriginal, + [input('selfIfMain')]: selfIfMain, '#availability': availability, }) => (availability ? continuation() : continuation.raiseOutput({ - ['#originalRelease']: - (selfIfOriginal ? track : null), + ['#mainRelease']: + (selfIfMain ? track : null), })), }, withResolvedReference({ - ref: 'originalReleaseTrack', - data: input('data'), - find: input.value(find.track), + ref: 'mainReleaseTrack', + find: soupyFind.input('track'), }), exitWithoutDependency({ @@ -71,7 +63,7 @@ export default templateCompositeFrom({ ['#resolvedReference']: resolvedReference, }) => continuation({ - ['#originalRelease']: resolvedReference, + ['#mainRelease']: resolvedReference, }), }, ], diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js index f8c1c3f0..bb3e8983 100644 --- a/src/data/composite/things/track/withOtherReleases.js +++ b/src/data/composite/things/track/withOtherReleases.js @@ -1,8 +1,9 @@ -import {input, templateCompositeFrom} from '#composite'; +// Gets all releases of the current track *except* this track itself; +// in other words, all other releases of the current track. -import {exitWithoutDependency} from '#composite/control-flow'; +import {input, templateCompositeFrom} from '#composite'; -import withOriginalRelease from './withOriginalRelease.js'; +import withAllReleases from './withAllReleases.js'; export default templateCompositeFrom({ annotation: `withOtherReleases`, @@ -10,31 +11,16 @@ export default templateCompositeFrom({ outputs: ['#otherReleases'], steps: () => [ - exitWithoutDependency({ - dependency: 'trackData', - mode: input.value('empty'), - }), - - withOriginalRelease({ - selfIfOriginal: input.value(true), - notFoundValue: input.value([]), - }), + withAllReleases(), { - dependencies: [input.myself(), '#originalRelease', 'trackData'], + dependencies: [input.myself(), '#allReleases'], compute: (continuation, { [input.myself()]: thisTrack, - ['#originalRelease']: originalRelease, - trackData, + ['#allReleases']: allReleases, }) => continuation({ ['#otherReleases']: - (originalRelease === thisTrack - ? [] - : [originalRelease]) - .concat(trackData.filter(track => - track !== originalRelease && - track !== thisTrack && - track.originalReleaseTrack === originalRelease)), + allReleases.filter(track => track !== thisTrack), }), }, ], diff --git a/src/data/composite/things/track/withPropertyFromAlbum.js b/src/data/composite/things/track/withPropertyFromAlbum.js index d41390fa..a203c2e7 100644 --- a/src/data/composite/things/track/withPropertyFromAlbum.js +++ b/src/data/composite/things/track/withPropertyFromAlbum.js @@ -2,17 +2,15 @@ // property name prefixed with '#album.' (by default). import {input, templateCompositeFrom} from '#composite'; -import {is} from '#validators'; import {withPropertyFromObject} from '#composite/data'; -import withAlbum from './withAlbum.js'; - export default templateCompositeFrom({ annotation: `withPropertyFromAlbum`, inputs: { property: input.staticValue({type: 'string'}), + internal: input({type: 'boolean', defaultValue: false}), }, outputs: ({ @@ -20,11 +18,21 @@ export default templateCompositeFrom({ }) => ['#album.' + property], steps: () => [ - withAlbum(), + // XXX: This is a ridiculous hack considering `defaultValue` above. + // If we were certain what was up, we'd just get around to fixing it LOL + { + dependencies: [input('internal')], + compute: (continuation, { + [input('internal')]: internal, + }) => continuation({ + ['#internal']: internal ?? false, + }), + }, withPropertyFromObject({ - object: '#album', + object: 'album', property: input('property'), + internal: '#internal', }), { diff --git a/src/data/composite/things/track/withPropertyFromOriginalRelease.js b/src/data/composite/things/track/withPropertyFromMainRelease.js index fd37f6de..393a4c63 100644 --- a/src/data/composite/things/track/withPropertyFromOriginalRelease.js +++ b/src/data/composite/things/track/withPropertyFromMainRelease.js @@ -1,8 +1,8 @@ -// Provides a value inherited from the original release, if applicable, and a -// flag indicating if this track is a rerelase or not. +// Provides a value inherited from the main release, if applicable, and a +// flag indicating if this track is a secondary release or not. // -// Like withOriginalRelease, this will early exit (with notFoundValue) if the -// original release is specified by reference and that reference doesn't +// Like withMainRelease, this will early exit (with notFoundValue) if the +// main release is specified by reference and that reference doesn't // resolve to anything. import {input, templateCompositeFrom} from '#composite'; @@ -10,10 +10,10 @@ import {input, templateCompositeFrom} from '#composite'; import {withResultOfAvailabilityCheck} from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; -import withOriginalRelease from './withOriginalRelease.js'; +import withMainRelease from './withMainRelease.js'; export default templateCompositeFrom({ - annotation: `inheritFromOriginalRelease`, + annotation: `inheritFromMainRelease`, inputs: { property: input({type: 'string'}), @@ -26,18 +26,18 @@ export default templateCompositeFrom({ outputs: ({ [input.staticValue('property')]: property, }) => - ['#isRerelease'].concat( + ['#isSecondaryRelease'].concat( (property - ? ['#original.' + property] - : ['#originalValue'])), + ? ['#mainRelease.' + property] + : ['#mainReleaseValue'])), steps: () => [ - withOriginalRelease({ + withMainRelease({ notFoundValue: input('notFoundValue'), }), withResultOfAvailabilityCheck({ - from: '#originalRelease', + from: '#mainRelease', }), { @@ -54,14 +54,14 @@ export default templateCompositeFrom({ ? continuation() : continuation.raiseOutput( Object.assign( - {'#isRerelease': false}, + {'#isSecondaryRelease': false}, (property - ? {['#original.' + property]: null} - : {'#originalValue': null})))), + ? {['#mainRelease.' + property]: null} + : {'#mainReleaseValue': null})))), }, withPropertyFromObject({ - object: '#originalRelease', + object: '#mainRelease', property: input('property'), }), @@ -77,10 +77,10 @@ export default templateCompositeFrom({ }) => continuation.raiseOutput( Object.assign( - {'#isRerelease': true}, + {'#isSecondaryRelease': true}, (property - ? {['#original.' + property]: value} - : {'#originalValue': value}))), + ? {['#mainRelease.' + property]: value} + : {'#mainReleaseValue': value}))), }, ], }); diff --git a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js new file mode 100644 index 00000000..7159a3f4 --- /dev/null +++ b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js @@ -0,0 +1,53 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; + +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withSuffixDirectoryFromAlbum`, + + inputs: { + flagValue: input({ + defaultDependency: 'suffixDirectoryFromAlbum', + acceptsNull: true, + }), + }, + + outputs: ['#suffixDirectoryFromAlbum'], + + steps: () => [ + withResultOfAvailabilityCheck({ + from: 'suffixDirectoryFromAlbum', + }), + + { + dependencies: [ + '#availability', + 'suffixDirectoryFromAlbum' + ], + + compute: (continuation, { + ['#availability']: availability, + ['suffixDirectoryFromAlbum']: flagValue, + }) => + (availability + ? continuation.raiseOutput({['#suffixDirectoryFromAlbum']: flagValue}) + : continuation()), + }, + + withPropertyFromAlbum({ + property: input.value('suffixTrackDirectories'), + }), + + { + dependencies: ['#album.suffixTrackDirectories'], + compute: (continuation, { + ['#album.suffixTrackDirectories']: suffixTrackDirectories, + }) => continuation({ + ['#suffixDirectoryFromAlbum']: + suffixTrackDirectories, + }), + }, + ], +}); diff --git a/src/data/composite/things/track/withTrackArtDate.js b/src/data/composite/things/track/withTrackArtDate.js new file mode 100644 index 00000000..9b7b61c7 --- /dev/null +++ b/src/data/composite/things/track/withTrackArtDate.js @@ -0,0 +1,60 @@ +import {input, templateCompositeFrom} from '#composite'; +import {isDate} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withDate from './withDate.js'; +import withHasUniqueCoverArt from './withHasUniqueCoverArt.js'; +import withPropertyFromAlbum from './withPropertyFromAlbum.js'; + +export default templateCompositeFrom({ + annotation: `withTrackArtDate`, + + inputs: { + from: input({ + validate: isDate, + defaultDependency: 'coverArtDate', + acceptsNull: true, + }), + }, + + outputs: ['#trackArtDate'], + + steps: () => [ + withHasUniqueCoverArt(), + + raiseOutputWithoutDependency({ + dependency: '#hasUniqueCoverArt', + mode: input.value('falsy'), + output: input.value({'#trackArtDate': null}), + }), + + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: from, + }) => + (from + ? continuation.raiseOutput({'#trackArtDate': from}) + : continuation()), + }, + + withPropertyFromAlbum({ + property: input.value('trackArtDate'), + }), + + { + dependencies: ['#album.trackArtDate'], + compute: (continuation, { + ['#album.trackArtDate']: albumTrackArtDate, + }) => + (albumTrackArtDate + ? continuation.raiseOutput({'#trackArtDate': albumTrackArtDate}) + : continuation()), + }, + + withDate().outputs({ + '#date': '#trackArtDate', + }), + ], +}); diff --git a/src/data/composite/things/track/withTrackNumber.js b/src/data/composite/things/track/withTrackNumber.js new file mode 100644 index 00000000..61428e8c --- /dev/null +++ b/src/data/composite/things/track/withTrackNumber.js @@ -0,0 +1,50 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withIndexInList, withPropertiesFromObject} from '#composite/data'; + +import withContainingTrackSection from './withContainingTrackSection.js'; + +export default templateCompositeFrom({ + annotation: `withTrackNumber`, + + outputs: ['#trackNumber'], + + steps: () => [ + withContainingTrackSection(), + + // Zero is the fallback, not one, but in most albums the first track + // (and its intended output by this composition) will be one. + raiseOutputWithoutDependency({ + dependency: '#trackSection', + output: input.value({'#trackNumber': 0}), + }), + + withPropertiesFromObject({ + object: '#trackSection', + properties: input.value(['tracks', 'startCountingFrom']), + }), + + withIndexInList({ + list: '#trackSection.tracks', + item: input.myself(), + }), + + raiseOutputWithoutDependency({ + dependency: '#index', + output: input.value({'#trackNumber': 0}), + }), + + { + dependencies: ['#trackSection.startCountingFrom', '#index'], + compute: (continuation, { + ['#trackSection.startCountingFrom']: startCountingFrom, + ['#index']: index, + }) => continuation({ + ['#trackNumber']: + startCountingFrom + + index, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/exitWithoutContribs.js b/src/data/composite/wiki-data/exitWithoutContribs.js index 2c8219fc..cf52950d 100644 --- a/src/data/composite/wiki-data/exitWithoutContribs.js +++ b/src/data/composite/wiki-data/exitWithoutContribs.js @@ -24,6 +24,7 @@ export default templateCompositeFrom({ steps: () => [ withResolvedContribs({ from: input('contribs'), + date: input.value(null), }), // TODO: Fairly certain exitWithoutDependency would be sufficient here. diff --git a/src/data/composite/wiki-data/gobbleSoupyFind.js b/src/data/composite/wiki-data/gobbleSoupyFind.js new file mode 100644 index 00000000..aec3f5b1 --- /dev/null +++ b/src/data/composite/wiki-data/gobbleSoupyFind.js @@ -0,0 +1,39 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withPropertyFromObject} from '#composite/data'; + +import inputSoupyFind, {getSoupyFindInputKey} from './inputSoupyFind.js'; + +export default templateCompositeFrom({ + annotation: `gobbleSoupyFind`, + + inputs: { + find: inputSoupyFind(), + }, + + outputs: ['#find'], + + steps: () => [ + { + dependencies: [input('find')], + compute: (continuation, { + [input('find')]: find, + }) => + (typeof find === 'function' + ? continuation.raiseOutput({ + ['#find']: find, + }) + : continuation({ + ['#key']: + getSoupyFindInputKey(find), + })), + }, + + withPropertyFromObject({ + object: 'find', + property: '#key', + }).outputs({ + '#value': '#find', + }), + ], +}); diff --git a/src/data/composite/wiki-data/gobbleSoupyReverse.js b/src/data/composite/wiki-data/gobbleSoupyReverse.js new file mode 100644 index 00000000..86a1061c --- /dev/null +++ b/src/data/composite/wiki-data/gobbleSoupyReverse.js @@ -0,0 +1,39 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {withPropertyFromObject} from '#composite/data'; + +import inputSoupyReverse, {getSoupyReverseInputKey} from './inputSoupyReverse.js'; + +export default templateCompositeFrom({ + annotation: `gobbleSoupyReverse`, + + inputs: { + reverse: inputSoupyReverse(), + }, + + outputs: ['#reverse'], + + steps: () => [ + { + dependencies: [input('reverse')], + compute: (continuation, { + [input('reverse')]: reverse, + }) => + (typeof reverse === 'function' + ? continuation.raiseOutput({ + ['#reverse']: reverse, + }) + : continuation({ + ['#key']: + getSoupyReverseInputKey(reverse), + })), + }, + + withPropertyFromObject({ + object: 'reverse', + property: '#key', + }).outputs({ + '#value': '#reverse', + }), + ], +}); diff --git a/src/data/composite/wiki-data/withDirectoryFromName.js b/src/data/composite/wiki-data/helpers/withDirectoryFromName.js index 034464e4..f85dae16 100644 --- a/src/data/composite/wiki-data/withDirectoryFromName.js +++ b/src/data/composite/wiki-data/helpers/withDirectoryFromName.js @@ -1,4 +1,4 @@ -// Compute a directory from a name - by default the current thing's own name. +// Compute a directory from a name. import {input, templateCompositeFrom} from '#composite'; @@ -13,7 +13,6 @@ export default templateCompositeFrom({ inputs: { name: input({ validate: isName, - defaultDependency: 'name', acceptsNull: true, }), }, diff --git a/src/data/composite/wiki-data/helpers/withResolvedReverse.js b/src/data/composite/wiki-data/helpers/withResolvedReverse.js new file mode 100644 index 00000000..818f60b7 --- /dev/null +++ b/src/data/composite/wiki-data/helpers/withResolvedReverse.js @@ -0,0 +1,40 @@ +// Actually execute a reverse function. + +import {input, templateCompositeFrom} from '#composite'; + +import inputWikiData from '../inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withReverseReferenceList`, + + inputs: { + data: inputWikiData({allowMixedTypes: true}), + reverse: input({type: 'function'}), + options: input({type: 'object', defaultValue: null}), + }, + + outputs: ['#resolvedReverse'], + + steps: () => [ + { + dependencies: [ + input.myself(), + input('data'), + input('reverse'), + input('options'), + ], + + compute: (continuation, { + [input.myself()]: myself, + [input('data')]: data, + [input('reverse')]: reverseFunction, + [input('options')]: opts, + }) => continuation({ + ['#resolvedReverse']: + (data + ? reverseFunction(myself, data, opts) + : reverseFunction(myself, opts)), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/helpers/withSimpleDirectory.js b/src/data/composite/wiki-data/helpers/withSimpleDirectory.js new file mode 100644 index 00000000..08ca3bfc --- /dev/null +++ b/src/data/composite/wiki-data/helpers/withSimpleDirectory.js @@ -0,0 +1,52 @@ +// A "simple" directory, based only on the already-provided directory, if +// available, or the provided name. + +import {input, templateCompositeFrom} from '#composite'; + +import {isDirectory, isName} from '#validators'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; + +import withDirectoryFromName from './withDirectoryFromName.js'; + +export default templateCompositeFrom({ + annotation: `withSimpleDirectory`, + + inputs: { + directory: input({ + validate: isDirectory, + defaultDependency: 'directory', + acceptsNull: true, + }), + + name: input({ + validate: isName, + acceptsNull: true, + }), + }, + + outputs: ['#directory'], + + steps: () => [ + withResultOfAvailabilityCheck({ + from: input('directory'), + }), + + { + dependencies: ['#availability', input('directory')], + compute: (continuation, { + ['#availability']: availability, + [input('directory')]: directory, + }) => + (availability + ? continuation.raiseOutput({ + ['#directory']: directory + }) + : continuation()), + }, + + withDirectoryFromName({ + name: input('name'), + }), + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js index 15ebaffa..38afc2ac 100644 --- a/src/data/composite/wiki-data/index.js +++ b/src/data/composite/wiki-data/index.js @@ -5,14 +5,25 @@ // export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as gobbleSoupyFind} from './gobbleSoupyFind.js'; +export {default as gobbleSoupyReverse} from './gobbleSoupyReverse.js'; +export {default as inputNotFoundMode} from './inputNotFoundMode.js'; +export {default as inputSoupyFind} from './inputSoupyFind.js'; +export {default as inputSoupyReverse} from './inputSoupyReverse.js'; export {default as inputWikiData} from './inputWikiData.js'; +export {default as splitContentNodesAround} from './splitContentNodesAround.js'; +export {default as withClonedThings} from './withClonedThings.js'; +export {default as withConstitutedArtwork} from './withConstitutedArtwork.js'; +export {default as withContentNodes} from './withContentNodes.js'; +export {default as withContributionListSums} from './withContributionListSums.js'; +export {default as withCoverArtDate} from './withCoverArtDate.js'; export {default as withDirectory} from './withDirectory.js'; -export {default as withDirectoryFromName} from './withDirectoryFromName.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 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 withReverseContributionList} from './withReverseContributionList.js'; export {default as withReverseReferenceList} from './withReverseReferenceList.js'; export {default as withThingsSortedAlphabetically} from './withThingsSortedAlphabetically.js'; export {default as withUniqueReferencingThing} from './withUniqueReferencingThing.js'; diff --git a/src/data/composite/wiki-data/inputNotFoundMode.js b/src/data/composite/wiki-data/inputNotFoundMode.js new file mode 100644 index 00000000..d16b2472 --- /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/inputSoupyFind.js b/src/data/composite/wiki-data/inputSoupyFind.js new file mode 100644 index 00000000..020f4990 --- /dev/null +++ b/src/data/composite/wiki-data/inputSoupyFind.js @@ -0,0 +1,28 @@ +import {input} from '#composite'; +import {anyOf, isFunction, isString} from '#validators'; + +function inputSoupyFind() { + return input({ + validate: + anyOf( + isFunction, + val => { + isString(val); + + if (!val.startsWith('_soupyFind:')) { + throw new Error(`Expected soupyFind.input() token`); + } + + return true; + }), + }); +} + +inputSoupyFind.input = key => + input.value('_soupyFind:' + key); + +export default inputSoupyFind; + +export function getSoupyFindInputKey(value) { + return value.slice('_soupyFind:'.length); +} diff --git a/src/data/composite/wiki-data/inputSoupyReverse.js b/src/data/composite/wiki-data/inputSoupyReverse.js new file mode 100644 index 00000000..0b0a23fe --- /dev/null +++ b/src/data/composite/wiki-data/inputSoupyReverse.js @@ -0,0 +1,32 @@ +import {input} from '#composite'; +import {anyOf, isFunction, isString} from '#validators'; + +function inputSoupyReverse() { + return input({ + validate: + anyOf( + isFunction, + val => { + isString(val); + + if (!val.startsWith('_soupyReverse:')) { + throw new Error(`Expected soupyReverse.input() token`); + } + + return true; + }), + }); +} + +inputSoupyReverse.input = key => + input.value('_soupyReverse:' + key); + +export default inputSoupyReverse; + +export function getSoupyReverseInputKey(value) { + return value.slice('_soupyReverse:'.length).replace(/\.unique$/, ''); +} + +export function doesSoupyReverseInputWantUnique(value) { + return value.endsWith('.unique'); +} diff --git a/src/data/composite/wiki-data/inputWikiData.js b/src/data/composite/wiki-data/inputWikiData.js index cf7a7c2c..b9021986 100644 --- a/src/data/composite/wiki-data/inputWikiData.js +++ b/src/data/composite/wiki-data/inputWikiData.js @@ -12,6 +12,6 @@ export default function inputWikiData({ } = {}) { return input({ validate: validateWikiData({referenceType, allowMixedTypes}), - acceptsNull: true, + defaultValue: null, }); } diff --git a/src/data/composite/wiki-data/raiseResolvedReferenceList.js b/src/data/composite/wiki-data/raiseResolvedReferenceList.js new file mode 100644 index 00000000..613b002b --- /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/splitContentNodesAround.js b/src/data/composite/wiki-data/splitContentNodesAround.js new file mode 100644 index 00000000..6648d8e1 --- /dev/null +++ b/src/data/composite/wiki-data/splitContentNodesAround.js @@ -0,0 +1,87 @@ +import {input, templateCompositeFrom} from '#composite'; +import {splitContentNodesAround} from '#replacer'; +import {anyOf, isFunction, validateInstanceOf} from '#validators'; + +import {withFilteredList, withMappedList, withUnflattenedList} + from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `splitContentNodesAround`, + + inputs: { + nodes: input({type: 'array'}), + + around: input({ + validate: + anyOf(isFunction, validateInstanceOf(RegExp)), + }), + }, + + outputs: ['#contentNodeLists'], + + steps: () => [ + { + dependencies: [input('nodes'), input('around')], + + compute: (continuation, { + [input('nodes')]: nodes, + [input('around')]: splitter, + }) => continuation({ + ['#nodes']: + Array.from(splitContentNodesAround(nodes, splitter)), + }), + }, + + withMappedList({ + list: '#nodes', + map: input.value(node => node.type === 'separator'), + }).outputs({ + '#mappedList': '#separatorFilter', + }), + + withMappedList({ + list: '#separatorFilter', + filter: '#separatorFilter', + map: input.value((_node, index) => index), + }), + + withFilteredList({ + list: '#mappedList', + filter: '#separatorFilter', + }).outputs({ + '#filteredList': '#separatorIndices', + }), + + { + dependencies: ['#nodes', '#separatorFilter'], + + compute: (continuation, { + ['#nodes']: nodes, + ['#separatorFilter']: separatorFilter, + }) => continuation({ + ['#nodes']: + nodes.map((node, index) => + (separatorFilter[index] + ? null + : node)), + }), + }, + + { + dependencies: ['#separatorIndices'], + compute: (continuation, { + ['#separatorIndices']: separatorIndices, + }) => continuation({ + ['#unflattenIndices']: + [0, ...separatorIndices], + }), + }, + + withUnflattenedList({ + list: '#nodes', + indices: '#unflattenIndices', + }).outputs({ + '#unflattenedList': '#contentNodeLists', + }), + ], +}); diff --git a/src/data/composite/wiki-data/withClonedThings.js b/src/data/composite/wiki-data/withClonedThings.js new file mode 100644 index 00000000..9af6aa84 --- /dev/null +++ b/src/data/composite/wiki-data/withClonedThings.js @@ -0,0 +1,68 @@ +// Clones all the things in a list. If the 'assign' input is provided, +// all new things are assigned the same specified properties. If the +// 'assignEach' input is provided, each new thing is assigned the +// corresponding properties. + +import CacheableObject from '#cacheable-object'; +import {input, templateCompositeFrom} from '#composite'; +import {isObject, sparseArrayOf} from '#validators'; + +import {withMappedList} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withClonedThings`, + + inputs: { + things: input({type: 'array'}), + + assign: input({ + type: 'object', + defaultValue: null, + }), + + assignEach: input({ + validate: sparseArrayOf(isObject), + defaultValue: null, + }), + }, + + outputs: ['#clonedThings'], + + steps: () => [ + { + dependencies: [input('assign'), input('assignEach')], + compute: (continuation, { + [input('assign')]: assign, + [input('assignEach')]: assignEach, + }) => continuation({ + ['#assignmentMap']: + (index) => + (assign && assignEach + ? {...assignEach[index] ?? {}, ...assign} + : assignEach + ? assignEach[index] ?? {} + : assign ?? {}), + }), + }, + + { + dependencies: ['#assignmentMap'], + compute: (continuation, { + ['#assignmentMap']: assignmentMap, + }) => continuation({ + ['#cloningMap']: + (thing, index) => + Object.assign( + CacheableObject.clone(thing), + assignmentMap(index)), + }), + }, + + withMappedList({ + list: input('things'), + map: '#cloningMap', + }).outputs({ + '#mappedList': '#clonedThings', + }), + ], +}); diff --git a/src/data/composite/wiki-data/withConstitutedArtwork.js b/src/data/composite/wiki-data/withConstitutedArtwork.js new file mode 100644 index 00000000..28d719e2 --- /dev/null +++ b/src/data/composite/wiki-data/withConstitutedArtwork.js @@ -0,0 +1,60 @@ +import {input, templateCompositeFrom} from '#composite'; +import thingConstructors from '#things'; + +export default templateCompositeFrom({ + annotation: `withConstitutedArtwork`, + + inputs: { + thingProperty: input({type: 'string', acceptsNull: true}), + dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}), + fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}), + dateFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsArtistProperty: input({type: 'string', acceptsNull: true}), + artTagsFromThingProperty: input({type: 'string', acceptsNull: true}), + referencedArtworksFromThingProperty: input({type: 'string', acceptsNull: true}), + }, + + outputs: ['#constitutedArtwork'], + + steps: () => [ + { + dependencies: [ + input.myself(), + input('thingProperty'), + input('dimensionsFromThingProperty'), + input('fileExtensionFromThingProperty'), + input('dateFromThingProperty'), + input('artistContribsFromThingProperty'), + input('artistContribsArtistProperty'), + input('artTagsFromThingProperty'), + input('referencedArtworksFromThingProperty'), + ], + + compute: (continuation, { + [input.myself()]: myself, + [input('thingProperty')]: thingProperty, + [input('dimensionsFromThingProperty')]: dimensionsFromThingProperty, + [input('fileExtensionFromThingProperty')]: fileExtensionFromThingProperty, + [input('dateFromThingProperty')]: dateFromThingProperty, + [input('artistContribsFromThingProperty')]: artistContribsFromThingProperty, + [input('artistContribsArtistProperty')]: artistContribsArtistProperty, + [input('artTagsFromThingProperty')]: artTagsFromThingProperty, + [input('referencedArtworksFromThingProperty')]: referencedArtworksFromThingProperty, + }) => continuation({ + ['#constitutedArtwork']: + Object.assign(new thingConstructors.Artwork, { + thing: myself, + thingProperty, + dimensionsFromThingProperty, + fileExtensionFromThingProperty, + artistContribsFromThingProperty, + artistContribsArtistProperty, + artTagsFromThingProperty, + dateFromThingProperty, + referencedArtworksFromThingProperty, + }), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withContentNodes.js b/src/data/composite/wiki-data/withContentNodes.js new file mode 100644 index 00000000..d014d43b --- /dev/null +++ b/src/data/composite/wiki-data/withContentNodes.js @@ -0,0 +1,25 @@ +import {input, templateCompositeFrom} from '#composite'; +import {parseContentNodes} from '#replacer'; + +export default templateCompositeFrom({ + annotation: `withContentNodes`, + + inputs: { + from: input({type: 'string', acceptsNull: false}), + }, + + outputs: ['#contentNodes'], + + steps: () => [ + { + dependencies: [input('from')], + + compute: (continuation, { + [input('from')]: string, + }) => continuation({ + ['#contentNodes']: + parseContentNodes(string), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withContributionListSums.js b/src/data/composite/wiki-data/withContributionListSums.js new file mode 100644 index 00000000..b4f36361 --- /dev/null +++ b/src/data/composite/wiki-data/withContributionListSums.js @@ -0,0 +1,95 @@ +// Gets the total duration and contribution count from a list of contributions, +// respecting their `countInContributionTotals` and `countInDurationTotals` +// flags. + +import {input, templateCompositeFrom} from '#composite'; + +import { + withFilteredList, + withPropertiesFromList, + withPropertyFromList, + withSum, + withUniqueItemsOnly, +} from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withContributionListSums`, + + inputs: { + list: input({type: 'array'}), + }, + + outputs: [ + '#contributionListCount', + '#contributionListDuration', + ], + + steps: () => [ + withPropertiesFromList({ + list: input('list'), + properties: input.value([ + 'countInContributionTotals', + 'countInDurationTotals', + ]), + }), + + withFilteredList({ + list: input('list'), + filter: '#list.countInContributionTotals', + }).outputs({ + '#filteredList': '#contributionsForCounting', + }), + + withFilteredList({ + list: input('list'), + filter: '#list.countInDurationTotals', + }).outputs({ + '#filteredList': '#contributionsForDuration', + }), + + { + dependencies: ['#contributionsForCounting'], + compute: (continuation, { + ['#contributionsForCounting']: contributionsForCounting, + }) => continuation({ + ['#count']: + contributionsForCounting.length, + }), + }, + + withPropertyFromList({ + list: '#contributionsForDuration', + property: input.value('thing'), + }), + + // Don't double-up the durations for a track where the artist has multiple + // contributions. + withUniqueItemsOnly({ + list: '#contributionsForDuration.thing', + }), + + withPropertyFromList({ + list: '#contributionsForDuration.thing', + property: input.value('duration'), + }).outputs({ + '#contributionsForDuration.thing.duration': '#durationValues', + }), + + withSum({ + values: '#durationValues', + }).outputs({ + '#sum': '#duration', + }), + + { + dependencies: ['#count', '#duration'], + compute: (continuation, { + ['#count']: count, + ['#duration']: duration, + }) => continuation({ + ['#contributionListCount']: count, + ['#contributionListDuration']: duration, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withCoverArtDate.js b/src/data/composite/wiki-data/withCoverArtDate.js new file mode 100644 index 00000000..a114d5ff --- /dev/null +++ b/src/data/composite/wiki-data/withCoverArtDate.js @@ -0,0 +1,51 @@ +import {input, templateCompositeFrom} from '#composite'; +import {isDate} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; + +import withResolvedContribs from './withResolvedContribs.js'; + +export default templateCompositeFrom({ + annotation: `withCoverArtDate`, + + inputs: { + from: input({ + validate: isDate, + defaultDependency: 'coverArtDate', + acceptsNull: true, + }), + }, + + outputs: ['#coverArtDate'], + + steps: () => [ + withResolvedContribs({ + from: 'coverArtistContribs', + date: input.value(null), + }), + + raiseOutputWithoutDependency({ + dependency: '#resolvedContribs', + mode: input.value('empty'), + output: input.value({'#coverArtDate': null}), + }), + + { + dependencies: [input('from')], + compute: (continuation, { + [input('from')]: from, + }) => + (from + ? continuation.raiseOutput({'#coverArtDate': from}) + : continuation()), + }, + + { + dependencies: ['date'], + compute: (continuation, {date}) => + (date + ? continuation({'#coverArtDate': date}) + : continuation({'#coverArtDate': null})), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withDirectory.js b/src/data/composite/wiki-data/withDirectory.js index b08b6153..f3bedf2e 100644 --- a/src/data/composite/wiki-data/withDirectory.js +++ b/src/data/composite/wiki-data/withDirectory.js @@ -7,9 +7,9 @@ import {input, templateCompositeFrom} from '#composite'; import {isDirectory, isName} from '#validators'; -import {withResultOfAvailabilityCheck} from '#composite/control-flow'; +import {raiseOutputWithoutDependency} from '#composite/control-flow'; -import withDirectoryFromName from './withDirectoryFromName.js'; +import withSimpleDirectory from './helpers/withSimpleDirectory.js'; export default templateCompositeFrom({ annotation: `withDirectory`, @@ -26,30 +26,37 @@ export default templateCompositeFrom({ defaultDependency: 'name', acceptsNull: true, }), + + suffix: input({ + validate: isDirectory, + defaultValue: null, + }), }, outputs: ['#directory'], steps: () => [ - withResultOfAvailabilityCheck({ - from: input('directory'), + withSimpleDirectory({ + directory: input('directory'), + name: input('name'), + }), + + raiseOutputWithoutDependency({ + dependency: '#directory', + output: input.value({['#directory']: null}), }), { - dependencies: ['#availability', input('directory')], + dependencies: ['#directory', input('suffix')], compute: (continuation, { - ['#availability']: availability, - [input('directory')]: directory, - }) => - (availability - ? continuation.raiseOutput({ - ['#directory']: directory - }) - : continuation()), + ['#directory']: directory, + [input('suffix')]: suffix, + }) => continuation({ + ['#directory']: + (suffix + ? directory + '-' + suffix + : directory), + }), }, - - withDirectoryFromName({ - name: input('name'), - }), ], }); diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js deleted file mode 100644 index f0404a5d..00000000 --- a/src/data/composite/wiki-data/withParsedCommentaryEntries.js +++ /dev/null @@ -1,179 +0,0 @@ -import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; -import {stitchArrays} from '#sugar'; -import {isCommentary} from '#validators'; -import {commentaryRegexCaseSensitive} from '#wiki-data'; - -import { - fillMissingListItems, - withFlattenedList, - withPropertiesFromList, - withUnflattenedList, -} from '#composite/data'; - -import withResolvedReferenceList from './withResolvedReferenceList.js'; - -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(commentaryRegexCaseSensitive)), - }), - }, - - 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([ - 'artistReferences', - 'artistDisplayText', - 'annotation', - 'date', - ]), - }), - - // 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: '#flattenedList', - data: 'artistData', - find: input.value(find.artist), - notFoundMode: input.value('null'), - }), - - withUnflattenedList({ - list: '#resolvedReferenceList', - }).outputs({ - '#unflattenedList': '#entries.artists', - }), - - fillMissingListItems({ - list: '#entries.artistDisplayText', - fill: input.value(null), - }), - - 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.artists', - '#entries.artistDisplayText', - '#entries.annotation', - '#entries.date', - '#entries.body', - ], - - compute: (continuation, { - ['#entries.artists']: artists, - ['#entries.artistDisplayText']: artistDisplayText, - ['#entries.annotation']: annotation, - ['#entries.date']: date, - ['#entries.body']: body, - }) => continuation({ - ['#parsedCommentaryEntries']: - stitchArrays({ - artists, - artistDisplayText, - annotation, - date, - body, - }), - }), - }, - ], -}); diff --git a/src/data/composite/wiki-data/withRecontextualizedContributionList.js b/src/data/composite/wiki-data/withRecontextualizedContributionList.js new file mode 100644 index 00000000..bcc6e486 --- /dev/null +++ b/src/data/composite/wiki-data/withRecontextualizedContributionList.js @@ -0,0 +1,100 @@ +// Clones all the contributions in a list, with thing and thingProperty both +// updated to match the current thing. Overwrites the provided dependency. +// Optionally updates artistProperty as well. Doesn't do anything if +// the provided dependency is null. +// +// See also: +// - withRedatedContributionList +// + +import {input, templateCompositeFrom} from '#composite'; +import {isStringNonEmpty} from '#validators'; + +import {withClonedThings} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withRecontextualizedContributionList`, + + inputs: { + list: input.staticDependency({ + type: 'array', + acceptsNull: true, + }), + + artistProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, + }), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list], + + steps: () => [ + // TODO: Is raiseOutputWithoutDependency workable here? + // Is it true that not specifying any output wouldn't overwrite + // the provided dependency? + { + dependencies: [ + input.staticDependency('list'), + input('list'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: dependency, + [input('list')]: list, + }) => + (list + ? continuation() + : continuation.raiseOutput({ + [dependency]: list, + })), + }, + + { + dependencies: [ + input.myself(), + input.thisProperty(), + input('artistProperty'), + ], + + compute: (continuation, { + [input.myself()]: myself, + [input.thisProperty()]: thisProperty, + [input('artistProperty')]: artistProperty, + }) => continuation({ + ['#assignment']: + Object.assign( + {thing: myself}, + {thingProperty: thisProperty}, + + (artistProperty + ? {artistProperty} + : {})), + }), + }, + + withClonedThings({ + things: input('list'), + assign: '#assignment', + }).outputs({ + '#clonedThings': '#newContributions', + }), + + { + dependencies: [ + input.staticDependency('list'), + '#newContributions', + ], + + compute: (continuation, { + [input.staticDependency('list')]: listDependency, + ['#newContributions']: newContributions, + }) => continuation({ + [listDependency]: + newContributions, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withRedatedContributionList.js b/src/data/composite/wiki-data/withRedatedContributionList.js new file mode 100644 index 00000000..12f3e16b --- /dev/null +++ b/src/data/composite/wiki-data/withRedatedContributionList.js @@ -0,0 +1,127 @@ +// Clones all the contributions in a list, with date updated to the provided +// value. Overwrites the provided dependency. Doesn't do anything if the +// provided dependency is null, or the provided date is null. +// +// If 'override' is true (the default), then so long as the provided date has +// a value at all, it's always written onto the (cloned) contributions. +// +// If 'override' is false, and any of the contributions were already dated, +// those will keep their existing dates. +// +// See also: +// - withRecontextualizedContributionList +// + +import {input, templateCompositeFrom} from '#composite'; +import {isDate} from '#validators'; + +import {withMappedList, withPropertyFromList} from '#composite/data'; +import {withClonedThings} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withRedatedContributionList`, + + inputs: { + list: input.staticDependency({ + type: 'array', + acceptsNull: true, + }), + + date: input({ + validate: isDate, + acceptsNull: true, + }), + + override: input({ + type: 'boolean', + defaultValue: true, + }), + }, + + outputs: ({ + [input.staticDependency('list')]: list, + }) => [list], + + steps: () => [ + // TODO: Is raiseOutputWithoutDependency workable here? + // Is it true that not specifying any output wouldn't overwrite + // the provided dependency? + { + dependencies: [ + input.staticDependency('list'), + input('list'), + input('date'), + ], + + compute: (continuation, { + [input.staticDependency('list')]: dependency, + [input('list')]: list, + [input('date')]: date, + }) => + (list && date + ? continuation() + : continuation.raiseOutput({ + [dependency]: list, + })), + }, + + withPropertyFromList({ + list: input('list'), + property: input.value('date'), + }).outputs({ + '#list.date': '#existingDates', + }), + + { + dependencies: [ + input('date'), + input('override'), + '#existingDates', + ], + + compute: (continuation, { + [input('date')]: date, + [input('override')]: override, + '#existingDates': existingDates, + }) => continuation({ + ['#assignmentMap']: + // TODO: Should be mapping over withIndicesFromList + (_, index) => + (!override && existingDates[index] + ? {date: existingDates[index]} + : date + ? {date} + : {}), + }), + }, + + withMappedList({ + list: input('list'), + map: '#assignmentMap', + }).outputs({ + '#mappedList': '#assignment', + }), + + withClonedThings({ + things: input('list'), + assignEach: '#assignment', + }).outputs({ + '#clonedThings': '#newContributions', + }), + + { + dependencies: [ + input.staticDependency('list'), + '#newContributions', + ], + + compute: (continuation, { + [input.staticDependency('list')]: listDependency, + ['#newContributions']: newContributions, + }) => continuation({ + [listDependency]: + newContributions, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js new file mode 100644 index 00000000..9cc52f29 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js @@ -0,0 +1,100 @@ +import {input, templateCompositeFrom} from '#composite'; +import {stitchArrays} from '#sugar'; +import {isObject, validateArrayItems} from '#validators'; + +import {withPropertyFromList} from '#composite/data'; + +import {raiseOutputWithoutDependency, withAvailabilityFilter} + from '#composite/control-flow'; + +import inputSoupyFind from './inputSoupyFind.js'; +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(isObject), + acceptsNull: true, + }), + + reference: input({type: 'string', defaultValue: 'reference'}), + annotation: input({type: 'string', defaultValue: 'annotation'}), + thing: input({type: 'string', defaultValue: 'thing'}), + + data: inputWikiData({allowMixedTypes: true}), + find: inputSoupyFind(), + + notFoundMode: inputNotFoundMode(), + }, + + outputs: ['#resolvedAnnotatedReferenceList'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedAnnotatedReferenceList']: [], + }), + }), + + withPropertyFromList({ + list: input('list'), + property: input('reference'), + }).outputs({ + ['#values']: '#references', + }), + + withPropertyFromList({ + list: input('list'), + property: input('annotation'), + }).outputs({ + ['#values']: '#annotations', + }), + + withResolvedReferenceList({ + list: '#references', + data: input('data'), + find: input('find'), + notFoundMode: input.value('null'), + }), + + { + dependencies: [ + input('thing'), + input('annotation'), + '#resolvedReferenceList', + '#annotations', + ], + + compute: (continuation, { + [input('thing')]: thingProperty, + [input('annotation')]: annotationProperty, + ['#resolvedReferenceList']: things, + ['#annotations']: annotations, + }) => continuation({ + ['#matches']: + stitchArrays({ + [thingProperty]: things, + [annotationProperty]: annotations, + }), + }), + }, + + withAvailabilityFilter({ + from: '#resolvedReferenceList', + }), + + raiseResolvedReferenceList({ + notFoundMode: input('notFoundMode'), + results: '#matches', + filter: '#availabilityFilter', + outputs: input.value('#resolvedAnnotatedReferenceList'), + }), + ], +}) diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js index 95266382..838c991f 100644 --- a/src/data/composite/wiki-data/withResolvedContribs.js +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -5,19 +5,16 @@ // any artist. import {input, templateCompositeFrom} from '#composite'; -import find from '#find'; import {filterMultipleArrays, stitchArrays} from '#sugar'; -import {is, isContributionList} from '#validators'; +import thingConstructors from '#things'; +import {isContributionList, isDate, isStringNonEmpty} from '#validators'; -import { - raiseOutputWithoutDependency, -} from '#composite/control-flow'; +import {raiseOutputWithoutDependency, withAvailabilityFilter} + from '#composite/control-flow'; +import {withPropertyFromList, withPropertiesFromList} from '#composite/data'; -import { - withPropertiesFromList, -} from '#composite/data'; - -import withResolvedReferenceList from './withResolvedReferenceList.js'; +import inputNotFoundMode from './inputNotFoundMode.js'; +import raiseResolvedReferenceList from './raiseResolvedReferenceList.js'; export default templateCompositeFrom({ annotation: `withResolvedContribs`, @@ -28,9 +25,21 @@ export default templateCompositeFrom({ acceptsNull: true, }), - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'null', + date: input({ + validate: isDate, + acceptsNull: true, + }), + + notFoundMode: inputNotFoundMode(), + + thingProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, + }), + + artistProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, }), }, @@ -45,34 +54,103 @@ export default templateCompositeFrom({ }), }), + { + dependencies: [ + input('thingProperty'), + input.staticDependency('from'), + ], + + compute: (continuation, { + [input('thingProperty')]: thingProperty, + [input.staticDependency('from')]: fromDependency, + }) => continuation({ + ['#thingProperty']: + (thingProperty + ? thingProperty + : !fromDependency?.startsWith('#') + ? fromDependency + : null), + }), + }, + withPropertiesFromList({ list: input('from'), properties: input.value(['artist', 'annotation']), prefix: input.value('#contribs'), }), - withResolvedReferenceList({ - list: '#contribs.artist', - data: 'artistData', - find: input.value(find.artist), - notFoundMode: input('notFoundMode'), - }).outputs({ - ['#resolvedReferenceList']: '#contribs.artist', - }), - { - dependencies: ['#contribs.artist', '#contribs.annotation'], + dependencies: [ + '#contribs.artist', + '#contribs.annotation', + input('date'), + ], compute(continuation, { ['#contribs.artist']: artist, ['#contribs.annotation']: annotation, + [input('date')]: date, }) { filterMultipleArrays(artist, annotation, (artist, _annotation) => artist); + return continuation({ - ['#resolvedContribs']: - stitchArrays({artist, annotation}), + ['#details']: + stitchArrays({artist, annotation}) + .map(details => ({ + ...details, + date: date ?? null, + })), }); }, }, + + { + dependencies: [ + '#details', + '#thingProperty', + input('artistProperty'), + input.myself(), + 'find', + ], + + compute: (continuation, { + ['#details']: details, + ['#thingProperty']: thingProperty, + [input('artistProperty')]: artistProperty, + [input.myself()]: myself, + ['find']: find, + }) => continuation({ + ['#contributions']: + details.map(details => { + const contrib = new thingConstructors.Contribution(); + + Object.assign(contrib, { + ...details, + thing: myself, + thingProperty: thingProperty, + artistProperty: artistProperty, + find: find, + }); + + return contrib; + }), + }), + }, + + withPropertyFromList({ + list: '#contributions', + property: input.value('artist'), + }), + + withAvailabilityFilter({ + from: '#contributions.artist', + }), + + raiseResolvedReferenceList({ + notFoundMode: input('notFoundMode'), + results: '#contributions', + filter: '#availabilityFilter', + outputs: input.value('#resolvedContribs'), + }), ], }); diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js index ea71707e..6f422194 100644 --- a/src/data/composite/wiki-data/withResolvedReference.js +++ b/src/data/composite/wiki-data/withResolvedReference.js @@ -1,16 +1,14 @@ // Resolves a reference by using the provided find function to match it -// within the provided thingData dependency. This will early exit if the -// data dependency is null. Otherwise, the data object is provided on the -// output dependency, or null, if the reference doesn't match anything or +// within the provided thingData dependency. The data object is provided on +// the output dependency, or null, if the reference doesn't match anything or // itself was null to begin with. import {input, templateCompositeFrom} from '#composite'; -import { - exitWithoutDependency, - raiseOutputWithoutDependency, -} from '#composite/control-flow'; +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import gobbleSoupyFind from './gobbleSoupyFind.js'; +import inputSoupyFind from './inputSoupyFind.js'; import inputWikiData from './inputWikiData.js'; export default templateCompositeFrom({ @@ -20,7 +18,7 @@ export default templateCompositeFrom({ ref: input({type: 'string', acceptsNull: true}), data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), + find: inputSoupyFind(), }, outputs: ['#resolvedReference'], @@ -33,24 +31,26 @@ export default templateCompositeFrom({ }), }), - exitWithoutDependency({ - dependency: input('data'), + gobbleSoupyFind({ + find: input('find'), }), { dependencies: [ input('ref'), input('data'), - input('find'), + '#find', ], compute: (continuation, { [input('ref')]: ref, [input('data')]: data, - [input('find')]: findFunction, + ['#find']: findFunction, }) => continuation({ ['#resolvedReference']: - findFunction(ref, data, {mode: 'quiet'}) ?? null, + (data + ? findFunction(ref, data, {mode: 'quiet'}) ?? null + : findFunction(ref, {mode: 'quiet'}) ?? null), }), }, ], diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js index 1d39e5b2..9dc960dd 100644 --- a/src/data/composite/wiki-data/withResolvedReferenceList.js +++ b/src/data/composite/wiki-data/withResolvedReferenceList.js @@ -1,18 +1,20 @@ // Resolves a list of references, with each reference matched with provided -// data in the same way as withResolvedReference. This will early exit if the -// data dependency is null (even if the reference list is empty). By default -// it will filter out references which don't match, but this can be changed -// to early exit ({notFoundMode: 'exit'}) or leave null in place ('null'). +// data in the same way as withResolvedReference. By default it will filter +// out references which don't match, but this can be changed 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, -} from '#composite/control-flow'; +import {raiseOutputWithoutDependency, withAvailabilityFilter} + from '#composite/control-flow'; +import {withMappedList} from '#composite/data'; +import gobbleSoupyFind from './gobbleSoupyFind.js'; +import inputNotFoundMode from './inputNotFoundMode.js'; +import inputSoupyFind from './inputSoupyFind.js'; import inputWikiData from './inputWikiData.js'; +import raiseResolvedReferenceList from './raiseResolvedReferenceList.js'; export default templateCompositeFrom({ annotation: `withResolvedReferenceList`, @@ -23,23 +25,15 @@ export default templateCompositeFrom({ acceptsNull: true, }), - data: inputWikiData({allowMixedTypes: false}), - find: input({type: 'function'}), + data: inputWikiData({allowMixedTypes: true}), + find: inputSoupyFind(), - notFoundMode: input({ - validate: is('exit', 'filter', 'null'), - defaultValue: 'filter', - }), + notFoundMode: inputNotFoundMode(), }, outputs: ['#resolvedReferenceList'], steps: () => [ - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - raiseOutputWithoutDependency({ dependency: input('list'), mode: input.value('empty'), @@ -48,54 +42,39 @@ export default templateCompositeFrom({ }), }), + gobbleSoupyFind({ + find: input('find'), + }), + { - dependencies: [input('list'), input('data'), input('find')], + dependencies: [input('data'), '#find'], compute: (continuation, { - [input('list')]: list, [input('data')]: data, - [input('find')]: findFunction, - }) => - continuation({ - '#matches': list.map(ref => findFunction(ref, data, {mode: 'quiet'})), - }), - }, - - { - dependencies: ['#matches'], - compute: (continuation, {'#matches': matches}) => - (matches.every(match => match) - ? continuation.raiseOutput({ - ['#resolvedReferenceList']: matches, - }) - : continuation()), + ['#find']: findFunction, + }) => continuation({ + ['#map']: + (data + ? ref => findFunction(ref, data, {mode: 'quiet'}) + : ref => findFunction(ref, {mode: 'quiet'})), + }), }, - { - 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), - }); + withMappedList({ + list: input('list'), + map: '#map', + }).outputs({ + '#mappedList': '#matches', + }), - 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/withReverseContributionList.js b/src/data/composite/wiki-data/withReverseContributionList.js deleted file mode 100644 index 91e125e4..00000000 --- a/src/data/composite/wiki-data/withReverseContributionList.js +++ /dev/null @@ -1,91 +0,0 @@ -// Analogous implementation for withReverseReferenceList, for contributions. -// This is all duplicate code and both should be ported to the same underlying -// data form later on. -// -// This implementation uses a global cache (via WeakMap) to attempt to speed -// up subsequent similar accesses. -// -// This has absolutely not been rigorously tested with altering properties of -// data objects in a wiki data array which is reused. If a new wiki data array -// is used, a fresh cache will always be created. - -import {input, templateCompositeFrom} from '#composite'; - -import {exitWithoutDependency, raiseOutputWithoutDependency} - from '#composite/control-flow'; - -import inputWikiData from './inputWikiData.js'; - -// Mapping of reference list property to WeakMap. -// Each WeakMap maps a wiki data array to another weak map, -// which in turn maps each referenced thing to an array of -// things referencing it. -const caches = new Map(); - -export default templateCompositeFrom({ - annotation: `withReverseContributionList`, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - outputs: ['#reverseContributionList'], - - steps: () => [ - // Early exit with an empty array if the data list isn't available. - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - // Raise an empty array (don't early exit) if the data list is empty. - raiseOutputWithoutDependency({ - dependency: input('data'), - mode: input.value('empty'), - output: input.value({'#reverseContributionList': []}), - }), - - { - dependencies: [input.myself(), input('data'), input('list')], - - compute: (continuation, { - [input.myself()]: myself, - [input('data')]: data, - [input('list')]: list, - }) => { - if (!caches.has(list)) { - caches.set(list, new WeakMap()); - } - - const cache = caches.get(list); - - if (!cache.has(data)) { - const cacheRecord = new WeakMap(); - - for (const referencingThing of data) { - const referenceList = referencingThing[list]; - - // Destructuring {artist} is the only unique part of the - // withReverseContributionList implementation, compared to - // withReverseReferneceList. - for (const {artist: referencedThing} of referenceList) { - if (cacheRecord.has(referencedThing)) { - cacheRecord.get(referencedThing).push(referencingThing); - } else { - cacheRecord.set(referencedThing, [referencingThing]); - } - } - } - - cache.set(data, cacheRecord); - } - - return continuation({ - ['#reverseContributionList']: - cache.get(data).get(myself) ?? [], - }); - }, - }, - ], -}); diff --git a/src/data/composite/wiki-data/withReverseReferenceList.js b/src/data/composite/wiki-data/withReverseReferenceList.js index 8cd540a5..906f5bc5 100644 --- a/src/data/composite/wiki-data/withReverseReferenceList.js +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -1,89 +1,36 @@ // Check out the info on reverseReferenceList! // This is its composable form. -// -// This implementation uses a global cache (via WeakMap) to attempt to speed -// up subsequent similar accesses. -// -// This has absolutely not been rigorously tested with altering properties of -// data objects in a wiki data array which is reused. If a new wiki data array -// is used, a fresh cache will always be created. -// -// Note that this implementation is mirrored in withReverseContributionList, -// so any changes should be reflected there (until these are combined). import {input, templateCompositeFrom} from '#composite'; -import {exitWithoutDependency, raiseOutputWithoutDependency} - from '#composite/control-flow'; - +import gobbleSoupyReverse from './gobbleSoupyReverse.js'; +import inputSoupyReverse from './inputSoupyReverse.js'; import inputWikiData from './inputWikiData.js'; -// Mapping of reference list property to WeakMap. -// Each WeakMap maps a wiki data array to another weak map, -// which in turn maps each referenced thing to an array of -// things referencing it. -const caches = new Map(); +import withResolvedReverse from './helpers/withResolvedReverse.js'; export default templateCompositeFrom({ annotation: `withReverseReferenceList`, inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), + data: inputWikiData({allowMixedTypes: true}), + reverse: inputSoupyReverse(), }, outputs: ['#reverseReferenceList'], steps: () => [ - // Early exit with an empty array if the data list isn't available. - exitWithoutDependency({ - dependency: input('data'), - value: input.value([]), - }), - - // Raise an empty array (don't early exit) if the data list is empty. - raiseOutputWithoutDependency({ - dependency: input('data'), - mode: input.value('empty'), - output: input.value({'#reverseReferenceList': []}), + gobbleSoupyReverse({ + reverse: input('reverse'), }), - { - dependencies: [input.myself(), input('data'), input('list')], - - compute: (continuation, { - [input.myself()]: myself, - [input('data')]: data, - [input('list')]: list, - }) => { - if (!caches.has(list)) { - caches.set(list, new WeakMap()); - } - - const cache = caches.get(list); + // TODO: Check that the reverse spec returns a list. - if (!cache.has(data)) { - const cacheRecord = new WeakMap(); - - for (const referencingThing of data) { - const referenceList = referencingThing[list]; - for (const referencedThing of referenceList) { - if (cacheRecord.has(referencedThing)) { - cacheRecord.get(referencedThing).push(referencingThing); - } else { - cacheRecord.set(referencedThing, [referencingThing]); - } - } - } - - cache.set(data, cacheRecord); - } - - return continuation({ - ['#reverseReferenceList']: - cache.get(data).get(myself) ?? [], - }); - }, - }, + withResolvedReverse({ + data: input('data'), + reverse: '#reverse', + }).outputs({ + '#resolvedReverse': '#reverseReferenceList', + }), ], }); diff --git a/src/data/composite/wiki-data/withUniqueReferencingThing.js b/src/data/composite/wiki-data/withUniqueReferencingThing.js index 61c10618..7c267038 100644 --- a/src/data/composite/wiki-data/withUniqueReferencingThing.js +++ b/src/data/composite/wiki-data/withUniqueReferencingThing.js @@ -4,48 +4,33 @@ import {input, templateCompositeFrom} from '#composite'; -import {exitWithoutDependency, raiseOutputWithoutDependency} - from '#composite/control-flow'; - +import gobbleSoupyReverse from './gobbleSoupyReverse.js'; +import inputSoupyReverse from './inputSoupyReverse.js'; import inputWikiData from './inputWikiData.js'; -import withReverseReferenceList from './withReverseReferenceList.js'; + +import withResolvedReverse from './helpers/withResolvedReverse.js'; export default templateCompositeFrom({ annotation: `withUniqueReferencingThing`, inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), + data: inputWikiData({allowMixedTypes: true}), + reverse: inputSoupyReverse(), }, outputs: ['#uniqueReferencingThing'], steps: () => [ - // Early exit with null (not an empty array) if the data list - // isn't available. - exitWithoutDependency({ - dependency: input('data'), + gobbleSoupyReverse({ + reverse: input('reverse'), }), - withReverseReferenceList({ + withResolvedReverse({ data: input('data'), - list: input('list'), + reverse: '#reverse', + options: input.value({unique: true}), + }).outputs({ + '#resolvedReverse': '#uniqueReferencingThing', }), - - raiseOutputWithoutDependency({ - dependency: '#reverseReferenceList', - mode: input.value('empty'), - output: input.value({'#uniqueReferencingThing': null}), - }), - - { - dependencies: ['#reverseReferenceList'], - compute: (continuation, { - ['#reverseReferenceList']: reverseReferenceList, - }) => continuation({ - ['#uniqueReferencingThing']: - reverseReferenceList[0], - }), - }, ], }); diff --git a/src/data/composite/wiki-properties/additionalFiles.js b/src/data/composite/wiki-properties/additionalFiles.js deleted file mode 100644 index 6760527a..00000000 --- a/src/data/composite/wiki-properties/additionalFiles.js +++ /dev/null @@ -1,30 +0,0 @@ -// This is a somewhat more involved data structure - it's for additional -// or "bonus" files associated with albums or tracks (or anything else). -// It's got this form: -// -// [ -// {title: 'Booklet', files: ['Booklet.pdf']}, -// { -// title: 'Wallpaper', -// description: 'Cool Wallpaper!', -// files: ['1440x900.png', '1920x1080.png'] -// }, -// {title: 'Alternate Covers', description: null, files: [...]}, -// ... -// ] -// - -import {isAdditionalFileList} from '#validators'; - -// TODO: Not templateCompositeFrom. - -export default function() { - return { - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }; -} diff --git a/src/data/composite/wiki-properties/additionalNameList.js b/src/data/composite/wiki-properties/additionalNameList.js deleted file mode 100644 index c5971d4a..00000000 --- a/src/data/composite/wiki-properties/additionalNameList.js +++ /dev/null @@ -1,14 +0,0 @@ -// A list of additional names! These can be used for a variety of purposes, -// e.g. providing extra searchable titles, localizations, romanizations or -// original titles, and so on. Each item has a name and, optionally, a -// descriptive annotation. - -import {isAdditionalNameList} from '#validators'; - -export default function() { - return { - flags: {update: true, expose: true}, - update: {validate: isAdditionalNameList}, - expose: {transform: value => value ?? []}, - }; -} diff --git a/src/data/composite/wiki-properties/annotatedReferenceList.js b/src/data/composite/wiki-properties/annotatedReferenceList.js new file mode 100644 index 00000000..8e6c96a1 --- /dev/null +++ b/src/data/composite/wiki-properties/annotatedReferenceList.js @@ -0,0 +1,64 @@ +import {input, templateCompositeFrom} from '#composite'; + +import { + isContentString, + optional, + validateArrayItems, + validateProperties, + validateReference, +} from '#validators'; + +import {exposeDependency} from '#composite/control-flow'; +import {inputSoupyFind, inputWikiData, withResolvedAnnotatedReferenceList} + from '#composite/wiki-data'; + +import {referenceListInputDescriptions, referenceListUpdateDescription} + from './helpers/reference-list-helpers.js'; + +export default templateCompositeFrom({ + annotation: `annotatedReferenceList`, + + compose: false, + + inputs: { + ...referenceListInputDescriptions(), + + data: inputWikiData({allowMixedTypes: true}), + find: inputSoupyFind(), + + reference: input.staticValue({type: 'string', defaultValue: 'reference'}), + annotation: input.staticValue({type: 'string', defaultValue: 'annotation'}), + thing: input.staticValue({type: 'string', defaultValue: 'thing'}), + }, + + update(staticInputs) { + const { + [input.staticValue('reference')]: referenceProperty, + [input.staticValue('annotation')]: annotationProperty, + } = staticInputs; + + return referenceListUpdateDescription({ + validateReferenceList: type => + validateArrayItems( + validateProperties({ + [referenceProperty]: validateReference(type), + [annotationProperty]: optional(isContentString), + })), + })(staticInputs); + }, + + steps: () => [ + withResolvedAnnotatedReferenceList({ + list: input.updateValue(), + + reference: input('reference'), + annotation: input('annotation'), + thing: input('thing'), + + data: input('data'), + find: input('find'), + }), + + exposeDependency({dependency: '#resolvedAnnotatedReferenceList'}), + ], +}); diff --git a/src/data/composite/wiki-properties/commentary.js b/src/data/composite/wiki-properties/commentary.js deleted file mode 100644 index cd6b7ac4..00000000 --- a/src/data/composite/wiki-properties/commentary.js +++ /dev/null @@ -1,30 +0,0 @@ -// Artist commentary! Generally present on tracks and albums. - -import {input, templateCompositeFrom} from '#composite'; -import {isCommentary} from '#validators'; - -import {exitWithoutDependency, exposeDependency} - from '#composite/control-flow'; -import {withParsedCommentaryEntries} from '#composite/wiki-data'; - -export default templateCompositeFrom({ - annotation: `commentary`, - - compose: false, - - steps: () => [ - exitWithoutDependency({ - dependency: input.updateValue({validate: isCommentary}), - mode: input.value('falsy'), - value: input.value(null), - }), - - withParsedCommentaryEntries({ - from: input.updateValue(), - }), - - exposeDependency({ - dependency: '#parsedCommentaryEntries', - }), - ], -}); diff --git a/src/data/composite/wiki-properties/commentatorArtists.js b/src/data/composite/wiki-properties/commentatorArtists.js index c5c14769..54d3e1a5 100644 --- a/src/data/composite/wiki-properties/commentatorArtists.js +++ b/src/data/composite/wiki-properties/commentatorArtists.js @@ -7,7 +7,6 @@ import {exitWithoutDependency, exposeDependency} from '#composite/control-flow'; import {withFlattenedList, withPropertyFromList, withUniqueItemsOnly} from '#composite/data'; -import {withParsedCommentaryEntries} from '#composite/wiki-data'; export default templateCompositeFrom({ annotation: `commentatorArtists`, @@ -21,19 +20,13 @@ export default templateCompositeFrom({ value: input.value([]), }), - withParsedCommentaryEntries({ - from: 'commentary', - }), - withPropertyFromList({ - list: '#parsedCommentaryEntries', + list: 'commentary', property: input.value('artists'), - }).outputs({ - '#parsedCommentaryEntries.artists': '#artistLists', }), withFlattenedList({ - list: '#artistLists', + list: '#commentary.artists', }).outputs({ '#flattenedList': '#artists', }), diff --git a/src/data/composite/wiki-properties/constitutibleArtwork.js b/src/data/composite/wiki-properties/constitutibleArtwork.js new file mode 100644 index 00000000..48f4211a --- /dev/null +++ b/src/data/composite/wiki-properties/constitutibleArtwork.js @@ -0,0 +1,70 @@ +// This composition does not actually inspect the values of any properties +// specified, so it's not responsible for determining whether a constituted +// artwork should exist at all. + +import {input, templateCompositeFrom} from '#composite'; +import {withEntries} from '#sugar'; +import Thing from '#thing'; +import {validateThing} from '#validators'; + +import {exposeDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; +import {withConstitutedArtwork} from '#composite/wiki-data'; + +const template = templateCompositeFrom({ + annotation: `constitutibleArtwork`, + + compose: false, + + inputs: { + thingProperty: input({type: 'string', acceptsNull: true}), + dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}), + fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}), + dateFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsArtistProperty: input({type: 'string', acceptsNull: true}), + artTagsFromThingProperty: input({type: 'string', acceptsNull: true}), + referencedArtworksFromThingProperty: input({type: 'string', acceptsNull: true}), + }, + + steps: () => [ + exposeUpdateValueOrContinue({ + validate: input.value( + validateThing({ + referenceType: 'artwork', + })), + }), + + withConstitutedArtwork({ + thingProperty: input('thingProperty'), + dimensionsFromThingProperty: input('dimensionsFromThingProperty'), + fileExtensionFromThingProperty: input('fileExtensionFromThingProperty'), + dateFromThingProperty: input('dateFromThingProperty'), + artistContribsFromThingProperty: input('artistContribsFromThingProperty'), + artistContribsArtistProperty: input('artistContribsArtistProperty'), + artTagsFromThingProperty: input('artTagsFromThingProperty'), + referencedArtworksFromThingProperty: input('referencedArtworksFromThingProperty'), + }), + + exposeDependency({ + dependency: '#constitutedArtwork', + }), + ], +}); + +template.fromYAMLFieldSpec = function(field) { + const {[Thing.yamlDocumentSpec]: documentSpec} = this; + + const {provide} = documentSpec.fields[field].transform; + + const inputs = + withEntries(provide, entries => + entries.map(([property, value]) => [ + property, + input.value(value), + ])); + + return template(inputs); +}; + +export default template; diff --git a/src/data/composite/wiki-properties/constitutibleArtworkList.js b/src/data/composite/wiki-properties/constitutibleArtworkList.js new file mode 100644 index 00000000..dad3a957 --- /dev/null +++ b/src/data/composite/wiki-properties/constitutibleArtworkList.js @@ -0,0 +1,72 @@ +// This composition does not actually inspect the values of any properties +// specified, so it's not responsible for determining whether a constituted +// artwork should exist at all. + +import {input, templateCompositeFrom} from '#composite'; +import {withEntries} from '#sugar'; +import Thing from '#thing'; +import {validateWikiData} from '#validators'; + +import {exposeUpdateValueOrContinue} from '#composite/control-flow'; +import {withConstitutedArtwork} from '#composite/wiki-data'; + +const template = templateCompositeFrom({ + annotation: `constitutibleArtworkList`, + + compose: false, + + inputs: { + thingProperty: input({type: 'string', acceptsNull: true}), + dimensionsFromThingProperty: input({type: 'string', acceptsNull: true}), + fileExtensionFromThingProperty: input({type: 'string', acceptsNull: true}), + dateFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsFromThingProperty: input({type: 'string', acceptsNull: true}), + artistContribsArtistProperty: input({type: 'string', acceptsNull: true}), + artTagsFromThingProperty: input({type: 'string', acceptsNull: true}), + referencedArtworksFromThingProperty: input({type: 'string', acceptsNull: true}), + }, + + steps: () => [ + exposeUpdateValueOrContinue({ + validate: input.value( + validateWikiData({ + referenceType: 'artwork', + })), + }), + + withConstitutedArtwork({ + thingProperty: input('thingProperty'), + dimensionsFromThingProperty: input('dimensionsFromThingProperty'), + fileExtensionFromThingProperty: input('fileExtensionFromThingProperty'), + dateFromThingProperty: input('dateFromThingProperty'), + artistContribsFromThingProperty: input('artistContribsFromThingProperty'), + artistContribsArtistProperty: input('artistContribsArtistProperty'), + artTagsFromThingProperty: input('artTagsFromThingProperty'), + referencedArtworksFromThingProperty: input('referencedArtworksFromThingProperty'), + }), + + { + dependencies: ['#constitutedArtwork'], + compute: ({ + ['#constitutedArtwork']: constitutedArtwork, + }) => [constitutedArtwork], + }, + ], +}); + +template.fromYAMLFieldSpec = function(field) { + const {[Thing.yamlDocumentSpec]: documentSpec} = this; + + const {provide} = documentSpec.fields[field].transform; + + const inputs = + withEntries(provide, entries => + entries.map(([property, value]) => [ + property, + input.value(value), + ])); + + return template(inputs); +}; + +export default template; diff --git a/src/data/composite/wiki-properties/contributionList.js b/src/data/composite/wiki-properties/contributionList.js index aad12a2d..d9a6b417 100644 --- a/src/data/composite/wiki-properties/contributionList.js +++ b/src/data/composite/wiki-properties/contributionList.js @@ -15,7 +15,7 @@ // import {input, templateCompositeFrom} from '#composite'; -import {isContributionList} from '#validators'; +import {isContributionList, isDate, isStringNonEmpty} from '#validators'; import {exposeConstant, exposeDependencyOrContinue} from '#composite/control-flow'; import {withResolvedContribs} from '#composite/wiki-data'; @@ -25,11 +25,34 @@ export default templateCompositeFrom({ compose: false, + inputs: { + date: input({ + validate: isDate, + acceptsNull: true, + }), + + artistProperty: input({ + validate: isStringNonEmpty, + defaultValue: null, + }), + }, + update: {validate: isContributionList}, steps: () => [ - withResolvedContribs({from: input.updateValue()}), - exposeDependencyOrContinue({dependency: '#resolvedContribs'}), - exposeConstant({value: input.value([])}), + withResolvedContribs({ + from: input.updateValue(), + thingProperty: input.thisProperty(), + artistProperty: input('artistProperty'), + date: input('date'), + }), + + exposeDependencyOrContinue({ + dependency: '#resolvedContribs', + }), + + exposeConstant({ + value: input.value([]), + }), ], }); diff --git a/src/data/composite/wiki-properties/directory.js b/src/data/composite/wiki-properties/directory.js index 41ce4b27..1756a8e5 100644 --- a/src/data/composite/wiki-properties/directory.js +++ b/src/data/composite/wiki-properties/directory.js @@ -18,12 +18,20 @@ export default templateCompositeFrom({ name: input({ validate: isName, defaultDependency: 'name', + acceptsNull: true, + }), + + suffix: input({ + validate: isDirectory, + defaultValue: null, }), }, steps: () => [ withDirectory({ directory: input.updateValue({validate: isDirectory}), + name: input('name'), + suffix: input('suffix'), }), exposeDependency({ 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 00000000..dfdc6b41 --- /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 89cb6838..e8f109d3 100644 --- a/src/data/composite/wiki-properties/index.js +++ b/src/data/composite/wiki-properties/index.js @@ -3,11 +3,11 @@ // 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 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'; +export {default as constitutibleArtwork} from './constitutibleArtwork.js'; +export {default as constitutibleArtworkList} from './constitutibleArtworkList.js'; export {default as contentString} from './contentString.js'; export {default as contribsPresent} from './contribsPresent.js'; export {default as contributionList} from './contributionList.js'; @@ -19,10 +19,15 @@ export {default as fileExtension} from './fileExtension.js'; export {default as flag} from './flag.js'; export {default as name} from './name.js'; export {default as referenceList} from './referenceList.js'; -export {default as reverseContributionList} from './reverseContributionList.js'; +export {default as referencedArtworkList} from './referencedArtworkList.js'; export {default as reverseReferenceList} from './reverseReferenceList.js'; export {default as simpleDate} from './simpleDate.js'; export {default as simpleString} from './simpleString.js'; export {default as singleReference} from './singleReference.js'; +export {default as soupyFind} from './soupyFind.js'; +export {default as soupyReverse} from './soupyReverse.js'; +export {default as thing} from './thing.js'; +export {default as thingList} from './thingList.js'; export {default as urls} from './urls.js'; +export {default as wallpaperParts} from './wallpaperParts.js'; export {default as wikiData} from './wikiData.js'; diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js index ebd5947c..4f8207b5 100644 --- a/src/data/composite/wiki-properties/referenceList.js +++ b/src/data/composite/wiki-properties/referenceList.js @@ -8,10 +8,14 @@ // 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 {inputSoupyFind, inputWikiData, withResolvedReferenceList} + from '#composite/wiki-data'; + +import {referenceListInputDescriptions, referenceListUpdateDescription} + from './helpers/reference-list-helpers.js'; export default templateCompositeFrom({ annotation: `referenceList`, @@ -19,33 +23,16 @@ export default templateCompositeFrom({ compose: false, inputs: { - class: input.staticValue({ - validate: isThingClass, - acceptsNull: true, - defaultValue: null, - }), - - referenceType: input.staticValue({ - type: 'string', - acceptsNull: true, - defaultValue: null, - }), - - data: inputWikiData({allowMixedTypes: false}), + ...referenceListInputDescriptions(), - find: input({type: 'function'}), + data: inputWikiData({allowMixedTypes: true}), + find: inputSoupyFind(), }, - 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 new file mode 100644 index 00000000..4f243493 --- /dev/null +++ b/src/data/composite/wiki-properties/referencedArtworkList.js @@ -0,0 +1,31 @@ +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; + +import annotatedReferenceList from './annotatedReferenceList.js'; + +export default templateCompositeFrom({ + annotation: `referencedArtworkList`, + + compose: false, + + steps: () => [ + { + compute: (continuation) => continuation({ + ['#find']: + find.mixed({ + track: find.trackPrimaryArtwork, + album: find.albumPrimaryArtwork, + }), + }), + }, + + annotatedReferenceList({ + referenceType: input.value(['album', 'track']), + + data: 'artworkData', + find: '#find', + + thing: input.value('artwork'), + }), + ], +}); diff --git a/src/data/composite/wiki-properties/reverseContributionList.js b/src/data/composite/wiki-properties/reverseContributionList.js deleted file mode 100644 index 7f3f9c81..00000000 --- a/src/data/composite/wiki-properties/reverseContributionList.js +++ /dev/null @@ -1,24 +0,0 @@ -import {input, templateCompositeFrom} from '#composite'; - -import {exposeDependency} from '#composite/control-flow'; -import {inputWikiData, withReverseContributionList} from '#composite/wiki-data'; - -export default templateCompositeFrom({ - annotation: `reverseContributionList`, - - compose: false, - - inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), - }, - - steps: () => [ - withReverseContributionList({ - data: input('data'), - list: input('list'), - }), - - exposeDependency({dependency: '#reverseContributionList'}), - ], -}); diff --git a/src/data/composite/wiki-properties/reverseReferenceList.js b/src/data/composite/wiki-properties/reverseReferenceList.js index 84ba67df..6d590a67 100644 --- a/src/data/composite/wiki-properties/reverseReferenceList.js +++ b/src/data/composite/wiki-properties/reverseReferenceList.js @@ -1,13 +1,13 @@ // Neat little shortcut for "reversing" the reference lists stored on other // things - for example, tracks specify a "referenced tracks" property, and // you would use this to compute a corresponding "referenced *by* tracks" -// property. Naturally, the passed ref list property is of the things in the -// wiki data provided, not the requesting Thing itself. +// property. import {input, templateCompositeFrom} from '#composite'; import {exposeDependency} from '#composite/control-flow'; -import {inputWikiData, withReverseReferenceList} from '#composite/wiki-data'; +import {inputSoupyReverse, inputWikiData, withReverseReferenceList} + from '#composite/wiki-data'; export default templateCompositeFrom({ annotation: `reverseReferenceList`, @@ -15,14 +15,14 @@ export default templateCompositeFrom({ compose: false, inputs: { - data: inputWikiData({allowMixedTypes: false}), - list: input({type: 'string'}), + data: inputWikiData({allowMixedTypes: true}), + reverse: inputSoupyReverse(), }, steps: () => [ withReverseReferenceList({ data: input('data'), - list: input('list'), + reverse: input('reverse'), }), exposeDependency({dependency: '#reverseReferenceList'}), diff --git a/src/data/composite/wiki-properties/singleReference.js b/src/data/composite/wiki-properties/singleReference.js index db4fc9f9..f532ebbe 100644 --- a/src/data/composite/wiki-properties/singleReference.js +++ b/src/data/composite/wiki-properties/singleReference.js @@ -11,7 +11,8 @@ import {input, templateCompositeFrom} from '#composite'; import {isThingClass, validateReference} from '#validators'; import {exposeDependency} from '#composite/control-flow'; -import {inputWikiData, withResolvedReference} from '#composite/wiki-data'; +import {inputSoupyFind, inputWikiData, withResolvedReference} + from '#composite/wiki-data'; export default templateCompositeFrom({ annotation: `singleReference`, @@ -21,8 +22,7 @@ export default templateCompositeFrom({ inputs: { class: input.staticValue({validate: isThingClass}), - find: input({type: 'function'}), - + find: inputSoupyFind(), data: inputWikiData({allowMixedTypes: false}), }, diff --git a/src/data/composite/wiki-properties/soupyFind.js b/src/data/composite/wiki-properties/soupyFind.js new file mode 100644 index 00000000..0f9a17e3 --- /dev/null +++ b/src/data/composite/wiki-properties/soupyFind.js @@ -0,0 +1,14 @@ +import {isObject} from '#validators'; + +import {inputSoupyFind} from '#composite/wiki-data'; + +function soupyFind() { + return { + flags: {update: true}, + update: {validate: isObject}, + }; +} + +soupyFind.input = inputSoupyFind.input; + +export default soupyFind; diff --git a/src/data/composite/wiki-properties/soupyReverse.js b/src/data/composite/wiki-properties/soupyReverse.js new file mode 100644 index 00000000..784a66b4 --- /dev/null +++ b/src/data/composite/wiki-properties/soupyReverse.js @@ -0,0 +1,37 @@ +import {isObject} from '#validators'; + +import {inputSoupyReverse} from '#composite/wiki-data'; + +function soupyReverse() { + return { + flags: {update: true}, + update: {validate: isObject}, + }; +} + +soupyReverse.input = inputSoupyReverse.input; + +soupyReverse.contributionsBy = + (bindTo, contributionsProperty) => ({ + bindTo, + + referencing: thing => thing[contributionsProperty], + referenced: contrib => [contrib.artist], + }); + +soupyReverse.artworkContributionsBy = + (bindTo, artworkProperty, {single = false} = {}) => ({ + bindTo, + + referencing: thing => + (single + ? (thing[artworkProperty] + ? thing[artworkProperty].artistContribs + : []) + : thing[artworkProperty] + .flatMap(artwork => artwork.artistContribs)), + + referenced: contrib => [contrib.artist], + }); + +export default soupyReverse; diff --git a/src/data/composite/wiki-properties/thing.js b/src/data/composite/wiki-properties/thing.js new file mode 100644 index 00000000..1f97a362 --- /dev/null +++ b/src/data/composite/wiki-properties/thing.js @@ -0,0 +1,40 @@ +// An individual Thing, provided directly rather than by reference. + +import {input, templateCompositeFrom} from '#composite'; +import {isThingClass, validateThing} from '#validators'; + +import {exposeConstant, exposeUpdateValueOrContinue} + from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `wikiData`, + + compose: false, + + inputs: { + class: input.staticValue({ + validate: isThingClass, + defaultValue: null, + }), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => ({ + validate: + validateThing({ + referenceType: + (thingClass + ? thingClass[Symbol.for('Thing.referenceType')] + : ''), + }), + }), + + steps: () => [ + exposeUpdateValueOrContinue(), + + exposeConstant({ + value: input.value(null), + }), + ], +}); diff --git a/src/data/composite/wiki-properties/thingList.js b/src/data/composite/wiki-properties/thingList.js new file mode 100644 index 00000000..f4c00e06 --- /dev/null +++ b/src/data/composite/wiki-properties/thingList.js @@ -0,0 +1,44 @@ +// A list of Things, provided directly rather than by reference. +// +// Essentially the same as wikiData, but exposes the list of things, +// instead of keeping it private. + +import {input, templateCompositeFrom} from '#composite'; +import {isThingClass, validateWikiData} from '#validators'; + +import {exposeConstant, exposeUpdateValueOrContinue} + from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `wikiData`, + + compose: false, + + inputs: { + class: input.staticValue({ + validate: isThingClass, + defaultValue: null, + }), + }, + + update: ({ + [input.staticValue('class')]: thingClass, + }) => ({ + validate: + validateWikiData({ + referenceType: + (thingClass + ? thingClass[Symbol.for('Thing.referenceType')] + : ''), + }), + }), + + steps: () => [ + exposeUpdateValueOrContinue(), + + exposeConstant({ + value: input.value([]), + }), + ], +}); + diff --git a/src/data/composite/wiki-properties/wallpaperParts.js b/src/data/composite/wiki-properties/wallpaperParts.js new file mode 100644 index 00000000..23049397 --- /dev/null +++ b/src/data/composite/wiki-properties/wallpaperParts.js @@ -0,0 +1,9 @@ +import {isWallpaperPartList} from '#validators'; + +export default function() { + return { + flags: {update: true, expose: true}, + update: {validate: isWallpaperPartList}, + expose: {transform: value => value ?? []}, + }; +} |