diff options
Diffstat (limited to 'src/data/composite/wiki-data')
11 files changed, 831 insertions, 0 deletions
diff --git a/src/data/composite/wiki-data/exitWithoutContribs.js b/src/data/composite/wiki-data/exitWithoutContribs.js new file mode 100644 index 0000000..2c8219f --- /dev/null +++ b/src/data/composite/wiki-data/exitWithoutContribs.js @@ -0,0 +1,47 @@ +// Shorthand for exiting if the contribution list (usually a property's update +// value) resolves to empty - ensuring that the later computed results are only +// returned if these contributions are present. + +import {input, templateCompositeFrom} from '#composite'; +import {isContributionList} from '#validators'; + +import {withResultOfAvailabilityCheck} from '#composite/control-flow'; + +import withResolvedContribs from './withResolvedContribs.js'; + +export default templateCompositeFrom({ + annotation: `exitWithoutContribs`, + + inputs: { + contribs: input({ + validate: isContributionList, + acceptsNull: true, + }), + + value: input({defaultValue: null}), + }, + + steps: () => [ + withResolvedContribs({ + from: input('contribs'), + }), + + // TODO: Fairly certain exitWithoutDependency would be sufficient here. + + withResultOfAvailabilityCheck({ + from: '#resolvedContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability', input('value')], + compute: (continuation, { + ['#availability']: availability, + [input('value')]: value, + }) => + (availability + ? continuation() + : continuation.exit(value)), + }, + ], +}); diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js new file mode 100644 index 0000000..b4cf6d1 --- /dev/null +++ b/src/data/composite/wiki-data/index.js @@ -0,0 +1,16 @@ +// #composite/wiki-data +// +// Entries here may depend on entries in #composite/control-flow and in +// #composite/data. +// + +export {default as exitWithoutContribs} from './exitWithoutContribs.js'; +export {default as inputWikiData} from './inputWikiData.js'; +export {default as withParsedCommentaryEntries} from './withParsedCommentaryEntries.js'; +export {default as withResolvedContribs} from './withResolvedContribs.js'; +export {default as withResolvedReference} from './withResolvedReference.js'; +export {default as withResolvedReferenceList} from './withResolvedReferenceList.js'; +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/inputWikiData.js b/src/data/composite/wiki-data/inputWikiData.js new file mode 100644 index 0000000..cf7a7c2 --- /dev/null +++ b/src/data/composite/wiki-data/inputWikiData.js @@ -0,0 +1,17 @@ +import {input} from '#composite'; +import {validateWikiData} from '#validators'; + +// TODO: This doesn't access a class's own ThingSubclass[Thing.referenceType] +// value because classes aren't initialized by when templateCompositeFrom gets +// called (see: circular imports). So the reference types have to be hard-coded, +// which somewhat defeats the point of storing them on the class in the first +// place... +export default function inputWikiData({ + referenceType = '', + allowMixedTypes = false, +} = {}) { + return input({ + validate: validateWikiData({referenceType, allowMixedTypes}), + acceptsNull: true, + }); +} diff --git a/src/data/composite/wiki-data/withParsedCommentaryEntries.js b/src/data/composite/wiki-data/withParsedCommentaryEntries.js new file mode 100644 index 0000000..f0404a5 --- /dev/null +++ b/src/data/composite/wiki-data/withParsedCommentaryEntries.js @@ -0,0 +1,179 @@ +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/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js new file mode 100644 index 0000000..77b0f96 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -0,0 +1,76 @@ +// Resolves the contribsByRef contained in the provided dependency, +// providing (named by the second argument) the result. "Resolving" +// means mapping the "who" reference of each contribution to an artist +// object, and filtering out those whose "who" doesn't match any artist. + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {filterMultipleArrays, stitchArrays} from '#sugar'; +import {is, isContributionList} from '#validators'; + +import { + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import { + withPropertiesFromList, +} from '#composite/data'; + +import withResolvedReferenceList from './withResolvedReferenceList.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedContribs`, + + inputs: { + from: input({ + validate: isContributionList, + acceptsNull: true, + }), + + notFoundMode: input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'null', + }), + }, + + outputs: ['#resolvedContribs'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('from'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedContribs']: [], + }), + }), + + withPropertiesFromList({ + list: input('from'), + properties: input.value(['who', 'what']), + prefix: input.value('#contribs'), + }), + + withResolvedReferenceList({ + list: '#contribs.who', + data: 'artistData', + find: input.value(find.artist), + notFoundMode: input('notFoundMode'), + }).outputs({ + ['#resolvedReferenceList']: '#contribs.who', + }), + + { + dependencies: ['#contribs.who', '#contribs.what'], + + compute(continuation, { + ['#contribs.who']: who, + ['#contribs.what']: what, + }) { + filterMultipleArrays(who, what, (who, _what) => who); + return continuation({ + ['#resolvedContribs']: stitchArrays({who, what}), + }); + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js new file mode 100644 index 0000000..ea71707 --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedReference.js @@ -0,0 +1,57 @@ +// 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 +// itself was null to begin with. + +import {input, templateCompositeFrom} from '#composite'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedReference`, + + inputs: { + ref: input({type: 'string', acceptsNull: true}), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + }, + + outputs: ['#resolvedReference'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('ref'), + output: input.value({ + ['#resolvedReference']: null, + }), + }), + + exitWithoutDependency({ + dependency: input('data'), + }), + + { + dependencies: [ + input('ref'), + input('data'), + input('find'), + ], + + compute: (continuation, { + [input('ref')]: ref, + [input('data')]: data, + [input('find')]: findFunction, + }) => continuation({ + ['#resolvedReference']: + findFunction(ref, data, {mode: 'quiet'}) ?? null, + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js new file mode 100644 index 0000000..1d39e5b --- /dev/null +++ b/src/data/composite/wiki-data/withResolvedReferenceList.js @@ -0,0 +1,101 @@ +// 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'). + +import {input, templateCompositeFrom} from '#composite'; +import {is, isString, validateArrayItems} from '#validators'; + +import { + exitWithoutDependency, + raiseOutputWithoutDependency, +} from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; + +export default templateCompositeFrom({ + annotation: `withResolvedReferenceList`, + + inputs: { + list: input({ + validate: validateArrayItems(isString), + acceptsNull: true, + }), + + data: inputWikiData({allowMixedTypes: false}), + find: input({type: 'function'}), + + notFoundMode: input({ + validate: is('exit', 'filter', 'null'), + defaultValue: 'filter', + }), + }, + + outputs: ['#resolvedReferenceList'], + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + }), + + raiseOutputWithoutDependency({ + dependency: input('list'), + mode: input.value('empty'), + output: input.value({ + ['#resolvedReferenceList']: [], + }), + }), + + { + dependencies: [input('list'), input('data'), input('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()), + }, + + { + dependencies: ['#matches', input('notFoundMode')], + compute(continuation, { + ['#matches']: matches, + [input('notFoundMode')]: notFoundMode, + }) { + switch (notFoundMode) { + case 'exit': + return continuation.exit([]); + + case 'filter': + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.filter(match => match), + }); + + case 'null': + return continuation.raiseOutput({ + ['#resolvedReferenceList']: + matches.map(match => match ?? null), + }); + + default: + throw new TypeError(`Expected notFoundMode to be exit, filter, or null`); + } + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withReverseContributionList.js b/src/data/composite/wiki-data/withReverseContributionList.js new file mode 100644 index 0000000..eccb58b --- /dev/null +++ b/src/data/composite/wiki-data/withReverseContributionList.js @@ -0,0 +1,83 @@ +// 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} 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: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + mode: input.value('empty'), + }), + + { + 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 {who} is the only unique part of the + // withReverseContributionList implementation, compared to + // withReverseReferneceList. + for (const {who: 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 new file mode 100644 index 0000000..2d7a421 --- /dev/null +++ b/src/data/composite/wiki-data/withReverseReferenceList.js @@ -0,0 +1,81 @@ +// 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} 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: `withReverseReferenceList`, + + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, + + outputs: ['#reverseReferenceList'], + + steps: () => [ + exitWithoutDependency({ + dependency: input('data'), + value: input.value([]), + mode: input.value('empty'), + }), + + { + 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]; + 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) ?? [], + }); + }, + }, + ], +}); diff --git a/src/data/composite/wiki-data/withThingsSortedAlphabetically.js b/src/data/composite/wiki-data/withThingsSortedAlphabetically.js new file mode 100644 index 0000000..5e85fa6 --- /dev/null +++ b/src/data/composite/wiki-data/withThingsSortedAlphabetically.js @@ -0,0 +1,122 @@ +// Sorts a list of live, generic wiki data objects alphabetically. +// Note that this uses localeCompare but isn't specialized to a particular +// language; where localization is concerned (in content), a follow-up, locale- +// specific sort should be performed. But this function does serve to organize +// a list so same-name entries are beside each other. + +import {input, templateCompositeFrom} from '#composite'; +import {compareCaseLessSensitive, normalizeName} from '#sort'; +import {validateWikiData} from '#validators'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withMappedList, withSortedList, withPropertiesFromList} + from '#composite/data'; + +export default templateCompositeFrom({ + annotation: `withThingsSortedAlphabetically`, + + inputs: { + things: input({validate: validateWikiData}), + }, + + outputs: ['#sortedThings'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: input('things'), + mode: input.value('empty'), + output: input.value({'#sortedThings': []}), + }), + + withPropertiesFromList({ + list: input('things'), + properties: input.value(['name', 'directory']), + }).outputs({ + '#list.name': '#names', + '#list.directory': '#directories', + }), + + withMappedList({ + list: '#names', + map: input.value(normalizeName), + }).outputs({ + '#mappedList': '#normalizedNames', + }), + + withSortedList({ + list: '#normalizedNames', + sort: input.value(compareCaseLessSensitive), + }).outputs({ + '#unstableSortIndices': '#normalizedNameSortIndices', + }), + + withSortedList({ + list: '#names', + sort: input.value(compareCaseLessSensitive), + }).outputs({ + '#unstableSortIndices': '#nonNormalizedNameSortIndices', + }), + + withSortedList({ + list: '#directories', + sort: input.value(compareCaseLessSensitive), + }).outputs({ + '#unstableSortIndices': '#directorySortIndices', + }), + + // TODO: No primitive for the next two-three steps, yet... + + { + dependencies: [input('things')], + compute: (continuation, { + [input('things')]: things, + }) => continuation({ + ['#combinedSortIndices']: + Array.from( + {length: things.length}, + (_item, index) => index), + }), + }, + + { + dependencies: [ + '#combinedSortIndices', + '#normalizedNameSortIndices', + '#nonNormalizedNameSortIndices', + '#directorySortIndices', + ], + + compute: (continuation, { + ['#combinedSortIndices']: combined, + ['#normalizedNameSortIndices']: normalized, + ['#nonNormalizedNameSortIndices']: nonNormalized, + ['#directorySortIndices']: directory, + }) => continuation({ + ['#combinedSortIndices']: + combined.sort((index1, index2) => { + if (normalized[index1] !== normalized[index2]) + return normalized[index1] - normalized[index2]; + + if (nonNormalized[index1] !== nonNormalized[index2]) + return nonNormalized[index1] - nonNormalized[index2]; + + if (directory[index1] !== directory[index2]) + return directory[index1] - directory[index2]; + + return 0; + }), + }), + }, + + { + dependencies: [input('things'), '#combinedSortIndices'], + compute: (continuation, { + [input('things')]: things, + ['#combinedSortIndices']: combined, + }) => continuation({ + ['#sortedThings']: + combined.map(index => things[index]), + }), + }, + ], +}); diff --git a/src/data/composite/wiki-data/withUniqueReferencingThing.js b/src/data/composite/wiki-data/withUniqueReferencingThing.js new file mode 100644 index 0000000..ce04f83 --- /dev/null +++ b/src/data/composite/wiki-data/withUniqueReferencingThing.js @@ -0,0 +1,52 @@ +// Like withReverseReferenceList, but this is specifically for special "unique" +// references, meaning this thing is referenced by exactly one or zero things +// in the data list. + +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency, raiseOutputWithoutDependency} + from '#composite/control-flow'; + +import inputWikiData from './inputWikiData.js'; +import withReverseReferenceList from './withReverseReferenceList.js'; + +export default templateCompositeFrom({ + annotation: `withUniqueReferencingThing`, + + inputs: { + data: inputWikiData({allowMixedTypes: false}), + list: input({type: 'string'}), + }, + + outputs: ['#uniqueReferencingThing'], + + steps: () => [ + // withReverseRefernceList does this check too, but it early exits with + // an empty array. That's no good here! + exitWithoutDependency({ + dependency: input('data'), + mode: input.value('empty'), + }), + + withReverseReferenceList({ + data: input('data'), + list: input('list'), + }), + + raiseOutputWithoutDependency({ + dependency: '#reverseReferenceList', + mode: input.value('empty'), + output: input.value({'#uniqueReferencingThing': null}), + }), + + { + dependencies: ['#reverseReferenceList'], + compute: (continuation, { + ['#reverseReferenceList']: reverseReferenceList, + }) => continuation({ + ['#uniqueReferencingThing']: + reverseReferenceList[0], + }), + }, + ], +}); |