diff options
Diffstat (limited to 'src')
47 files changed, 742 insertions, 499 deletions
diff --git a/src/content/dependencies/generateArtistCredit.js b/src/content/dependencies/generateArtistCredit.js index 600d73d9..35603610 100644 --- a/src/content/dependencies/generateArtistCredit.js +++ b/src/content/dependencies/generateArtistCredit.js @@ -5,10 +5,10 @@ export default { const query = {}; const featuringFilter = contribution => - contribution.annotation === 'featuring'; + contribution.isFeaturingCredit; const wikiEditFilter = contribution => - contribution.annotation?.startsWith('edits for wiki'); + contribution.isEditsForWikiCredit; const normalFilter = contribution => !featuringFilter(contribution) && @@ -104,8 +104,6 @@ export default { showChronology: {type: 'boolean', default: false}, showWikiEdits: {type: 'boolean', default: false}, - trimAnnotation: {type: 'boolean', default: false}, - chunkwrap: {type: 'boolean', default: true}, chronologyKind: {type: 'string'}, @@ -123,28 +121,13 @@ export default { ...relations.featuringContributionLinks, ]) { link.setSlots({ + showAnnotation: slots.showAnnotation, showExternalLinks: slots.showExternalLinks, showChronology: slots.showChronology, - trimAnnotation: slots.trimAnnotation, chronologyKind: slots.chronologyKind, }); } - for (const link of relations.normalContributionLinks) { - link.setSlots({ - showAnnotation: slots.showAnnotation, - }); - } - - for (const link of relations.featuringContributionLinks) { - link.setSlots({ - showAnnotation: - (slots.featuringStringKey || slots.normalFeaturingStringKey - ? false - : slots.showAnnotation), - }); - } - let formattedArtistList = null; if (!html.isBlank(relations.formatText)) { diff --git a/src/content/dependencies/generateArtistCreditWikiEditsPart.js b/src/content/dependencies/generateArtistCreditWikiEditsPart.js index 4178928d..7b3d8b1a 100644 --- a/src/content/dependencies/generateArtistCreditWikiEditsPart.js +++ b/src/content/dependencies/generateArtistCreditWikiEditsPart.js @@ -38,7 +38,6 @@ export default { relations.contributionLinks.map(link => link.slots({ showAnnotation: slots.showAnnotation, - trimAnnotation: true, preventTooltip: true, preventWrapping: true, }))), diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js index f53e0f81..3d6e274b 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js @@ -14,7 +14,7 @@ export default { contribs .some(contrib => contrib.thingProperty === 'artistContribs' && - contrib.annotation !== 'featuring'); + !contrib.isFeaturingCredit); const creditedAsContributor = contribs @@ -22,9 +22,7 @@ export default { const annotatedContribs = contribs - .filter(contrib => - contrib.annotation && - contrib.annotation !== 'featuring'); + .filter(contrib => !empty(contrib.annotationParts)); const annotatedArtistContribs = annotatedContribs @@ -117,10 +115,10 @@ export default { duration: query.track.duration, - contribAnnotations: + contribAnnotationParts: (query.displayedContributions ? query.displayedContributions - .map(contrib => contrib.annotation) + .flatMap(contrib => contrib.annotationParts) : null), }), @@ -137,8 +135,8 @@ export default { firstReleaseTooltip: relations.firstReleaseTooltip, annotation: - (data.contribAnnotations - ? language.formatUnitList(data.contribAnnotations) + (data.contribAnnotationParts + ? language.formatUnitList(data.contribAnnotationParts) : html.blank()), content: diff --git a/src/content/dependencies/generateCoverArtworkOriginDetails.js b/src/content/dependencies/generateCoverArtworkOriginDetails.js index e489eea6..c8f51368 100644 --- a/src/content/dependencies/generateCoverArtworkOriginDetails.js +++ b/src/content/dependencies/generateCoverArtworkOriginDetails.js @@ -75,8 +75,6 @@ export default { showChronology: true, showWikiEdits: true, - trimAnnotation: false, - chronologyKind: 'coverArt', normalStringKey: workingCapsule, diff --git a/src/content/dependencies/generateReleaseInfoContributionsLine.js b/src/content/dependencies/generateReleaseInfoContributionsLine.js index 4353ccf4..da54f8b8 100644 --- a/src/content/dependencies/generateReleaseInfoContributionsLine.js +++ b/src/content/dependencies/generateReleaseInfoContributionsLine.js @@ -20,8 +20,6 @@ export default { showChronology: true, showWikiEdits: true, - trimAnnotation: false, - chronologyKind: slots.chronologyKind, normalStringKey: slots.stringKey, diff --git a/src/content/dependencies/linkContribution.js b/src/content/dependencies/linkContribution.js index aa9bdef9..f9c27724 100644 --- a/src/content/dependencies/linkContribution.js +++ b/src/content/dependencies/linkContribution.js @@ -11,7 +11,8 @@ export default { }), data: (contribution) => ({ - annotation: contribution.annotation, + artistText: contribution.artistText, + annotationParts: contribution.annotationParts, urls: contribution.artist.urls, }), @@ -22,8 +23,6 @@ export default { showExternalLinks: {type: 'boolean', default: false}, showChronology: {type: 'boolean', default: false}, - trimAnnotation: {type: 'boolean', default: false}, - preventWrapping: {type: 'boolean', default: false}, preventTooltip: {type: 'boolean', default: false}, @@ -53,6 +52,12 @@ export default { chronologyKind: slots.chronologyKind, }); + if (data.artistText) { + relations.artistLink.setSlots({ + content: language.sanitize(data.artistText), + }); + } + workingOptions.artist = (html.isBlank(relations.tooltip) || slots.preventTooltip ? relations.artistLink @@ -69,11 +74,9 @@ export default { })); const annotation = - (slots.trimAnnotation - ? data.annotation?.replace(/^edits for wiki(: )?/, '') - : data.annotation); + language.formatUnitList(data.annotationParts); - if (slots.showAnnotation && annotation) { + if (slots.showAnnotation && !html.isBlank(annotation)) { workingCapsule += '.withContribution'; workingOptions.contrib = annotation; } diff --git a/src/data/composite/things/contribution/hasAnnotationFront.js b/src/data/composite/things/contribution/hasAnnotationFront.js new file mode 100644 index 00000000..6969268b --- /dev/null +++ b/src/data/composite/things/contribution/hasAnnotationFront.js @@ -0,0 +1,29 @@ +import {input, templateCompositeFrom} from '#composite'; + +import {exitWithoutDependency} from '#composite/control-flow'; + +export default templateCompositeFrom({ + annotation: `hasAnnotationFront`, + + inputs: { + front: input({type: 'string'}), + }, + + compose: false, + + steps: () => [ + exitWithoutDependency({ + dependency: 'annotationFront', + value: input.value(false), + }), + + { + dependencies: ['annotationFront', input('front')], + compute: ({ + ['annotationFront']: present, + [input('front')]: expected, + }) => + present === expected, + }, + ], +}); diff --git a/src/data/composite/things/contribution/index.js b/src/data/composite/things/contribution/index.js index 2bbf994d..b03ebfd2 100644 --- a/src/data/composite/things/contribution/index.js +++ b/src/data/composite/things/contribution/index.js @@ -1,3 +1,4 @@ +export {default as hasAnnotationFront} from './hasAnnotationFront.js'; export {default as inheritFromContributionPresets} from './inheritFromContributionPresets.js'; export {default as withContainingReverseContributionList} from './withContainingReverseContributionList.js'; export {default as withContributionContext} from './withContributionContext.js'; diff --git a/src/data/composite/wiki-data/withClonedThings.js b/src/data/composite/wiki-data/withClonedThings.js index 9af6aa84..36c3ba54 100644 --- a/src/data/composite/wiki-data/withClonedThings.js +++ b/src/data/composite/wiki-data/withClonedThings.js @@ -3,9 +3,9 @@ // '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 Thing from '#thing'; +import {isObject, isThingClass, sparseArrayOf} from '#validators'; import {withMappedList} from '#composite/data'; @@ -15,6 +15,16 @@ export default templateCompositeFrom({ inputs: { things: input({type: 'array'}), + reclass: input({ + validate: isThingClass, + defaultValue: null, + }), + + reclassUnder: input({ + validate: isThingClass, + defaultValue: null, + }), + assign: input({ type: 'object', defaultValue: null, @@ -46,15 +56,29 @@ export default templateCompositeFrom({ }, { - dependencies: ['#assignmentMap'], + dependencies: [input('reclass'), input('reclassUnder')], + compute: (continuation, { + [input('reclass')]: reclass, + [input('reclassUnder')]: reclassUnder, + }) => continuation({ + ['#cloneOperation']: + (reclassUnder && reclass + ? source => reclassUnder.clone(source, {as: reclass}) + : reclass + ? source => Thing.clone(source, {as: reclass}) + : source => Thing.clone(source)), + }), + }, + + { + dependencies: ['#assignmentMap', '#cloneOperation'], compute: (continuation, { ['#assignmentMap']: assignmentMap, + ['#cloneOperation']: cloneOperation, }) => continuation({ ['#cloningMap']: (thing, index) => - Object.assign( - CacheableObject.clone(thing), - assignmentMap(index)), + Object.assign(cloneOperation(thing), assignmentMap(index)), }), }, diff --git a/src/data/composite/wiki-data/withRecontextualizedContributionList.js b/src/data/composite/wiki-data/withRecontextualizedContributionList.js index bcc6e486..66ac056a 100644 --- a/src/data/composite/wiki-data/withRecontextualizedContributionList.js +++ b/src/data/composite/wiki-data/withRecontextualizedContributionList.js @@ -1,14 +1,15 @@ // 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. +// Optionally updates artistProperty, and optionally reclasses as another +// kind of contribution. Does nothing if the provided dependency is null. // // See also: // - withRedatedContributionList // import {input, templateCompositeFrom} from '#composite'; -import {isStringNonEmpty} from '#validators'; +import thingConstructors from '#thing'; +import {isStringNonEmpty, isThingClass} from '#validators'; import {withClonedThings} from '#composite/wiki-data'; @@ -21,6 +22,11 @@ export default templateCompositeFrom({ acceptsNull: true, }), + reclass: input({ + validate: isThingClass, + defaultValue: null, + }), + artistProperty: input({ validate: isStringNonEmpty, defaultValue: null, @@ -77,6 +83,8 @@ export default templateCompositeFrom({ withClonedThings({ things: input('list'), + reclass: input('reclass'), + reclassUnder: input.value(thingConstructors.Contribution), assign: '#assignment', }).outputs({ '#clonedThings': '#newContributions', diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js index 0d96f1b7..3bbe1f81 100644 --- a/src/data/composite/wiki-data/withResolvedContribs.js +++ b/src/data/composite/wiki-data/withResolvedContribs.js @@ -82,27 +82,33 @@ export default templateCompositeFrom({ withPropertiesFromList({ list: input('from'), - properties: input.value(['artist', 'annotation']), + properties: input.value(['artist', 'artistText', 'annotation']), prefix: input.value('#contribs'), }), { dependencies: [ '#contribs.artist', + '#contribs.artistText', '#contribs.annotation', input('date'), ], compute(continuation, { ['#contribs.artist']: artist, + ['#contribs.artistText']: artistText, ['#contribs.annotation']: annotation, [input('date')]: date, }) { - filterMultipleArrays(artist, annotation, (artist, _annotation) => artist); + filterMultipleArrays( + artist, + artistText, + annotation, + (artist, _artistText, _annotation) => artist); return continuation({ ['#details']: - stitchArrays({artist, annotation}) + stitchArrays({artist, artistText, annotation}) .map(details => ({ ...details, date: date ?? null, diff --git a/src/data/files/album.js b/src/data/files/album.js new file mode 100644 index 00000000..84cda226 --- /dev/null +++ b/src/data/files/album.js @@ -0,0 +1,74 @@ +import * as path from 'node:path'; + +import {traverse} from '#node-utils'; +import {sortAlbumsTracksChronologically, sortChronologically} from '#sort'; +import {empty} from '#sugar'; + +export default ({ + documentModes: {headerAndEntries}, + thingConstructors: {Album, Track, TrackSection}, +}) => ({ + title: `Process album files`, + + files: dataPath => + traverse(path.join(dataPath, 'album'), { + filterFile: name => path.extname(name) === '.yaml', + prefixPath: 'album', + }), + + documentMode: headerAndEntries, + headerDocumentThing: Album, + entryDocumentThing: document => + ('Section' in document + ? TrackSection + : Track), + + connect({header: album, entries}) { + const trackSections = []; + + let currentTrackSection = new TrackSection(); + let currentTrackSectionTracks = []; + + Object.assign(currentTrackSection, { + name: `Default Track Section`, + isDefaultTrackSection: true, + }); + + const closeCurrentTrackSection = () => { + if ( + currentTrackSection.isDefaultTrackSection && + empty(currentTrackSectionTracks) + ) { + return; + } + + currentTrackSection.tracks = currentTrackSectionTracks; + currentTrackSection.album = album; + + trackSections.push(currentTrackSection); + }; + + for (const entry of entries) { + if (entry instanceof TrackSection) { + closeCurrentTrackSection(); + currentTrackSection = entry; + currentTrackSectionTracks = []; + continue; + } + + entry.album = album; + entry.trackSection = currentTrackSection; + + currentTrackSectionTracks.push(entry); + } + + closeCurrentTrackSection(); + + album.trackSections = trackSections; + }, + + sort({albumData, trackData}) { + sortChronologically(albumData); + sortAlbumsTracksChronologically(trackData); + }, +}); diff --git a/src/data/files/art-tag.js b/src/data/files/art-tag.js new file mode 100644 index 00000000..67a22ca7 --- /dev/null +++ b/src/data/files/art-tag.js @@ -0,0 +1,32 @@ +import {readFile} from 'node:fs/promises'; +import * as path from 'node:path'; + +import {traverse} from '#node-utils'; +import {sortAlphabetically} from '#sort'; + +export default ({ + documentModes: {allTogether}, + thingConstructors: {ArtTag}, +}) => ({ + title: `Process art tags file`, + + files: dataPath => + Promise.allSettled([ + readFile(path.join(dataPath, 'tags.yaml')) + .then(() => ['tags.yaml']), + + traverse(path.join(dataPath, 'art-tags'), { + filterFile: name => path.extname(name) === '.yaml', + prefixPath: 'art-tags', + }), + ]).then(results => results + .filter(({status}) => status === 'fulfilled') + .flatMap(({value}) => value)), + + documentMode: allTogether, + documentThing: ArtTag, + + sort({artTagData}) { + sortAlphabetically(artTagData); + }, +}); diff --git a/src/data/files/artist.js b/src/data/files/artist.js new file mode 100644 index 00000000..ef971171 --- /dev/null +++ b/src/data/files/artist.js @@ -0,0 +1,16 @@ +import {sortAlphabetically} from '#sort'; + +export default ({ + documentModes: {allInOne}, + thingConstructors: {Artist}, +}) => ({ + title: `Process artists file`, + file: 'artists.yaml', + + documentMode: allInOne, + documentThing: Artist, + + sort({artistData}) { + sortAlphabetically(artistData); + }, +}); diff --git a/src/data/files/flash.js b/src/data/files/flash.js new file mode 100644 index 00000000..3e4f750f --- /dev/null +++ b/src/data/files/flash.js @@ -0,0 +1,78 @@ +import {sortFlashesChronologically} from '#sort'; + +export default ({ + documentModes: {allInOne}, + thingConstructors: {Flash, FlashAct, FlashSide}, +}) => ({ + title: `Process flashes file`, + file: 'flashes.yaml', + + documentMode: allInOne, + documentThing: document => + ('Side' in document + ? FlashSide + : 'Act' in document + ? FlashAct + : Flash), + + connect(results) { + let thing, i; + + for (i = 0; thing = results[i]; i++) { + if (thing.isFlashSide) { + const side = thing; + const acts = []; + + for (i++; thing = results[i]; i++) { + if (thing.isFlashAct) { + const act = thing; + const flashes = []; + + for (i++; thing = results[i]; i++) { + if (thing.isFlash) { + const flash = thing; + + flash.act = act; + flashes.push(flash); + + continue; + } + + i--; + break; + } + + act.side = side; + act.flashes = flashes; + acts.push(act); + + continue; + } + + if (thing.isFlash) { + throw new Error(`Flashes must be under an act`); + } + + i--; + break; + } + + side.acts = acts; + + continue; + } + + if (thing.isFlashAct) { + throw new Error(`Acts must be under a side`); + } + + if (thing.isFlash) { + throw new Error(`Flashes must be under a side and act`); + } + } + }, + + sort({flashData}) { + sortFlashesChronologically(flashData); + }, +}); diff --git a/src/data/files/group.js b/src/data/files/group.js new file mode 100644 index 00000000..c10cbf98 --- /dev/null +++ b/src/data/files/group.js @@ -0,0 +1,45 @@ +import Thing from '#thing'; + +export default ({ + documentModes: {allInOne}, + thingConstructors: {Group, GroupCategory}, +}) => ({ + title: `Process groups file`, + file: 'groups.yaml', + + documentMode: allInOne, + documentThing: document => + ('Category' in document + ? GroupCategory + : Group), + + connect(results) { + let groupCategory; + let groupRefs = []; + + if (results[0] && !(results[0] instanceof GroupCategory)) { + throw new Error(`Expected a category at top of group data file`); + } + + for (const thing of results) { + if (thing instanceof GroupCategory) { + if (groupCategory) { + Object.assign(groupCategory, {groups: groupRefs}); + } + + groupCategory = thing; + groupRefs = []; + } else { + groupRefs.push(Thing.getReference(thing)); + } + } + + if (groupCategory) { + Object.assign(groupCategory, {groups: groupRefs}); + } + }, + + // Groups aren't sorted at all, always preserving the order in the data + // file as-is. + sort: null, +}); diff --git a/src/data/files/homepage-layout.js b/src/data/files/homepage-layout.js new file mode 100644 index 00000000..646beff6 --- /dev/null +++ b/src/data/files/homepage-layout.js @@ -0,0 +1,87 @@ +import {empty} from '#sugar'; + +export default ({ + documentModes: {allInOne}, + thingConstructors: { + HomepageLayout, + HomepageLayoutActionsRow, + HomepageLayoutAlbumCarouselRow, + HomepageLayoutAlbumGridRow, + HomepageLayoutRow, + HomepageLayoutSection, + }, +}) => ({ + title: `Process homepage layout file`, + file: 'homepage.yaml', + + documentMode: allInOne, + documentThing: document => { + if (document['Homepage']) { + return HomepageLayout; + } + + if (document['Section']) { + return HomepageLayoutSection; + } + + if (document['Row']) { + switch (document['Row']) { + case 'actions': + return HomepageLayoutActionsRow; + case 'album carousel': + return HomepageLayoutAlbumCarouselRow; + case 'album grid': + return HomepageLayoutAlbumGridRow; + default: + throw new TypeError(`Unrecognized row type ${document['Row']}`); + } + } + + return null; + }, + + connect(results) { + if (!empty(results) && !(results[0] instanceof HomepageLayout)) { + throw new Error(`Expected 'Homepage' document at top of homepage layout file`); + } + + const homepageLayout = results[0]; + const sections = []; + + let currentSection = null; + let currentSectionRows = []; + + const closeCurrentSection = () => { + if (currentSection) { + for (const row of currentSectionRows) { + row.section = currentSection; + } + + currentSection.rows = currentSectionRows; + sections.push(currentSection); + + currentSection = null; + currentSectionRows = []; + } + }; + + for (const entry of results.slice(1)) { + if (entry instanceof HomepageLayout) { + throw new Error(`Expected only one 'Homepage' document in total`); + } else if (entry instanceof HomepageLayoutSection) { + closeCurrentSection(); + currentSection = entry; + } else if (entry instanceof HomepageLayoutRow) { + if (currentSection) { + currentSectionRows.push(entry); + } else { + throw new Error(`Expected a 'Section' document to add following rows into`); + } + } + } + + closeCurrentSection(); + + homepageLayout.sections = sections; + }, +}); diff --git a/src/data/files/index.js b/src/data/files/index.js new file mode 100644 index 00000000..f3efebad --- /dev/null +++ b/src/data/files/index.js @@ -0,0 +1,10 @@ +export {default as getAlbumLoadingSpec} from './album.js'; +export {default as getArtistLoadingSpec} from './artist.js'; +export {default as getArtTagLoadingSpec} from './art-tag.js'; +export {default as getFlashLoadingSpec} from './flash.js'; +export {default as getGroupLoadingSpec} from './group.js'; +export {default as getHomepageLayoutLoadingSpec} from './homepage-layout.js'; +export {default as getNewsLoadingSpec} from './news.js'; +export {default as getSortingRuleLoadingSpec} from './sorting-rule.js'; +export {default as getStaticPageLoadingSpec} from './static-page.js'; +export {default as getWikiInfoLoadingSpec} from './wiki-info.js'; diff --git a/src/data/files/news.js b/src/data/files/news.js new file mode 100644 index 00000000..5b4a3029 --- /dev/null +++ b/src/data/files/news.js @@ -0,0 +1,16 @@ +import {sortChronologically} from '#sort'; + +export default ({ + documentModes: {allInOne}, + thingConstructors: {NewsEntry}, +}) => ({ + title: `Process news data file`, + file: 'news.yaml', + + documentMode: allInOne, + documentThing: NewsEntry, + + sort({newsData}) { + sortChronologically(newsData, {latestFirst: true}); + }, +}); diff --git a/src/data/files/sorting-rule.js b/src/data/files/sorting-rule.js new file mode 100644 index 00000000..61e1df23 --- /dev/null +++ b/src/data/files/sorting-rule.js @@ -0,0 +1,13 @@ +export default ({ + documentModes: {allInOne}, + thingConstructors: {DocumentSortingRule}, +}) => ({ + title: `Process sorting rules file`, + file: 'sorting-rules.yaml', + + documentMode: allInOne, + documentThing: document => + (document['Sort Documents'] + ? DocumentSortingRule + : null), +}); diff --git a/src/data/files/static-page.js b/src/data/files/static-page.js new file mode 100644 index 00000000..c7622bc8 --- /dev/null +++ b/src/data/files/static-page.js @@ -0,0 +1,24 @@ +import * as path from 'node:path'; + +import {traverse} from '#node-utils'; +import {sortAlphabetically} from '#sort'; + +export default ({ + documentModes: {onePerFile}, + thingConstructors: {StaticPage}, +}) => ({ + title: `Process static page files`, + + files: dataPath => + traverse(path.join(dataPath, 'static-page'), { + filterFile: name => path.extname(name) === '.yaml', + prefixPath: 'static-page', + }), + + documentMode: onePerFile, + documentThing: StaticPage, + + sort({staticPageData}) { + sortAlphabetically(staticPageData); + }, +}); diff --git a/src/data/files/wiki-info.js b/src/data/files/wiki-info.js new file mode 100644 index 00000000..a466ab0b --- /dev/null +++ b/src/data/files/wiki-info.js @@ -0,0 +1,10 @@ +export default ({ + documentModes: {oneDocumentTotal}, + thingConstructors: {WikiInfo}, +}) => ({ + title: `Process wiki info file`, + file: 'wiki-info.yaml', + + documentMode: oneDocumentTotal, + documentThing: WikiInfo, +}); diff --git a/src/data/thing.js b/src/data/thing.js index 32eff4d1..0a6e3be4 100644 --- a/src/data/thing.js +++ b/src/data/thing.js @@ -23,7 +23,6 @@ export default class Thing extends CacheableObject { static reverseSpecs = Symbol.for('Thing.reverseSpecs'); static yamlDocumentSpec = Symbol.for('Thing.yamlDocumentSpec'); - static getYamlLoadingSpec = Symbol.for('Thing.getYamlLoadingSpec'); static yamlSourceFilename = Symbol.for('Thing.yamlSourceFilename'); static yamlSourceDocument = Symbol.for('Thing.yamlSourceDocument'); @@ -82,9 +81,37 @@ export default class Thing extends CacheableObject { (reference ? ` (${reference})` : '')); } + static clone(source, {as = null} = {}) { + if (!(source instanceof this)) { + throw new TypeError( + `Passed thing is ${source.constructor.name}, ` + + `which is not a subclass of ${this.name}`); + } + + if (as && !(as.prototype instanceof this)) { + throw new TypeError( + `Passed constructor is ${as.name}, ` + + `which is not a subclass of ${this.name}`); + } + + let clone; + + if (as) { + clone = Reflect.construct(as, []); + } else { + clone = Reflect.construct(source.constructor, []); + } + + CacheableObject.copyUpdateValuesOnto(source, clone); + + return clone; + } + static getReference(thing) { if (!thing.constructor[Thing.referenceType]) { - throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); + throw TypeError( + `Passed Thing is ${thing.constructor.name}, ` + + `which provides no [Thing.referenceType]`); } if (!thing.directory) { diff --git a/src/data/things/ArtTag.js b/src/data/things/ArtTag.js index 91248f77..9d35f54d 100644 --- a/src/data/things/ArtTag.js +++ b/src/data/things/ArtTag.js @@ -1,12 +1,4 @@ -const DATA_ART_TAGS_DIRECTORY = 'art-tags'; -const ART_TAG_DATA_FILE = 'tags.yaml'; - -import {readFile} from 'node:fs/promises'; -import * as path from 'node:path'; - import {input, V} from '#composite'; -import {traverse} from '#node-utils'; -import {sortAlphabetically} from '#sort'; import Thing from '#thing'; import {unique} from '#sugar'; import {isName} from '#validators'; @@ -200,31 +192,4 @@ export class ArtTag extends Thing { }, }, }; - - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allTogether}, - thingConstructors: {ArtTag}, - }) => ({ - title: `Process art tags file`, - - files: dataPath => - Promise.allSettled([ - readFile(path.join(dataPath, ART_TAG_DATA_FILE)) - .then(() => [ART_TAG_DATA_FILE]), - - traverse(path.join(dataPath, DATA_ART_TAGS_DIRECTORY), { - filterFile: name => path.extname(name) === '.yaml', - prefixPath: DATA_ART_TAGS_DIRECTORY, - }), - ]).then(results => results - .filter(({status}) => status === 'fulfilled') - .flatMap(({value}) => value)), - - documentMode: allTogether, - documentThing: ArtTag, - - sort({artTagData}) { - sortAlphabetically(artTagData); - }, - }); } diff --git a/src/data/things/Artist.js b/src/data/things/Artist.js index 85bdc006..f518e31e 100644 --- a/src/data/things/Artist.js +++ b/src/data/things/Artist.js @@ -1,5 +1,3 @@ -const ARTIST_DATA_FILE = 'artists.yaml'; - import {inspect} from 'node:util'; import CacheableObject from '#cacheable-object'; @@ -11,7 +9,6 @@ import {parseArtistAliases, parseArtwork} from '#yaml'; import { sortAlbumsTracksChronologically, sortArtworksChronologically, - sortAlphabetically, sortContributionsChronologically, } from '#sort'; @@ -325,21 +322,6 @@ export class Artist extends Thing { }, }; - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allInOne}, - thingConstructors: {Artist}, - }) => ({ - title: `Process artists file`, - file: ARTIST_DATA_FILE, - - documentMode: allInOne, - documentThing: Artist, - - sort({artistData}) { - sortAlphabetically(artistData); - }, - }); - [inspect.custom]() { const parts = []; diff --git a/src/data/things/Artwork.js b/src/data/things/Artwork.js index c1aafa8f..7beb3567 100644 --- a/src/data/things/Artwork.js +++ b/src/data/things/Artwork.js @@ -73,7 +73,10 @@ export class Artwork extends Thing { // 'artistContribs', // from attached artwork or thing ]; - static [Thing.getPropertyDescriptors] = ({ArtTag}) => ({ + static [Thing.getPropertyDescriptors] = ({ + ArtTag, + ArtworkArtistContribution, + }) => ({ // Update & expose unqualifiedDirectory: directory({ @@ -128,6 +131,12 @@ export class Artwork extends Thing { artistContribs: [ withResolvedContribs({ from: input.updateValue({validate: isContributionList}), + + // XXX: All artwork artist contributions, as resolved from update value + // (*not* those constituted from thing), are generic artwork contribs. + // The class should be specified by whatever the artwork is placed on!! + class: input.value(ArtworkArtistContribution), + date: 'date', thingProperty: input.thisProperty(), artistProperty: 'artistContribsArtistProperty', diff --git a/src/data/things/NewsEntry.js b/src/data/things/NewsEntry.js index 65fd125b..7cbbfc4b 100644 --- a/src/data/things/NewsEntry.js +++ b/src/data/things/NewsEntry.js @@ -1,7 +1,4 @@ -const NEWS_DATA_FILE = 'news.yaml'; - import {V} from '#composite'; -import {sortChronologically} from '#sort'; import Thing from '#thing'; import {parseDate} from '#yaml'; @@ -58,19 +55,4 @@ export class NewsEntry extends Thing { 'Content': {property: 'content'}, }, }; - - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allInOne}, - thingConstructors: {NewsEntry}, - }) => ({ - title: `Process news data file`, - file: NEWS_DATA_FILE, - - documentMode: allInOne, - documentThing: NewsEntry, - - sort({newsData}) { - sortChronologically(newsData, {latestFirst: true}); - }, - }); } diff --git a/src/data/things/StaticPage.js b/src/data/things/StaticPage.js index daa77a7e..5ddddb9d 100644 --- a/src/data/things/StaticPage.js +++ b/src/data/things/StaticPage.js @@ -1,10 +1,4 @@ -const DATA_STATIC_PAGE_DIRECTORY = 'static-page'; - -import * as path from 'node:path'; - import {V} from '#composite'; -import {traverse} from '#node-utils'; -import {sortAlphabetically} from '#sort'; import Thing from '#thing'; import {isName} from '#validators'; @@ -67,24 +61,4 @@ export class StaticPage extends Thing { 'Review Points': {ignore: true}, }, }; - - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {onePerFile}, - thingConstructors: {StaticPage}, - }) => ({ - title: `Process static page files`, - - files: dataPath => - traverse(path.join(dataPath, DATA_STATIC_PAGE_DIRECTORY), { - filterFile: name => path.extname(name) === '.yaml', - prefixPath: DATA_STATIC_PAGE_DIRECTORY, - }), - - documentMode: onePerFile, - documentThing: StaticPage, - - sort({staticPageData}) { - sortAlphabetically(staticPageData); - }, - }); } diff --git a/src/data/things/Track.js b/src/data/things/Track.js index d776581f..36e073b6 100644 --- a/src/data/things/Track.js +++ b/src/data/things/Track.js @@ -116,6 +116,7 @@ export class Track extends Thing { LyricsEntry, MusicVideo, ReferencingSourcesEntry, + TrackArtistContribution, TrackSection, WikiInfo, }) => ({ @@ -262,6 +263,7 @@ export class Track extends Thing { artistContribs: [ withResolvedContribs({ from: input.updateValue({validate: isContributionList}), + class: input.value(TrackArtistContribution), date: 'date', thingProperty: input.thisProperty(), artistProperty: input.value('trackArtistContributions'), @@ -1254,9 +1256,6 @@ export class Track extends Thing { }, }; - // Track YAML loading is handled in album.js. - static [Thing.getYamlLoadingSpec] = null; - getOwnAdditionalFilePath(_file, filename) { if (!this.album) return null; diff --git a/src/data/things/WikiInfo.js b/src/data/things/WikiInfo.js index 1d1f90e6..ffb18cd8 100644 --- a/src/data/things/WikiInfo.js +++ b/src/data/things/WikiInfo.js @@ -1,5 +1,3 @@ -export const WIKI_INFO_FILE = 'wiki-info.yaml'; - import {input, V} from '#composite'; import Thing from '#thing'; import {parseContributionPresets, parseWallpaperParts} from '#yaml'; @@ -156,14 +154,5 @@ export class WikiInfo extends Thing { }, }; - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {oneDocumentTotal}, - thingConstructors: {WikiInfo}, - }) => ({ - title: `Process wiki info file`, - file: WIKI_INFO_FILE, - documentMode: oneDocumentTotal, - documentThing: WikiInfo, - }); } diff --git a/src/data/things/album/Album.js b/src/data/things/album/Album.js index d5fd1682..48d52223 100644 --- a/src/data/things/album/Album.js +++ b/src/data/things/album/Album.js @@ -1,10 +1,4 @@ -export const DATA_ALBUM_DIRECTORY = 'album'; - -import * as path from 'node:path'; - import {input, V} from '#composite'; -import {traverse} from '#node-utils'; -import {sortAlbumsTracksChronologically, sortChronologically} from '#sort'; import {empty} from '#sugar'; import Thing from '#thing'; import {is, isContributionList, isDate, isDirectory, isNumber} @@ -74,11 +68,15 @@ export class Album extends Thing { static [Thing.getPropertyDescriptors] = ({ AdditionalFile, AdditionalName, + AlbumArtistContribution, + AlbumBannerArtistContribution, + AlbumWallpaperArtistContribution, ArtTag, Artwork, CommentaryEntry, CreditingSourcesEntry, Group, + TrackArtistContribution, TrackSection, WikiInfo, }) => ({ @@ -125,6 +123,7 @@ export class Album extends Thing { // > Update & expose - Credits and contributors artistContribs: contributionList({ + class: input.value(AlbumArtistContribution), artistProperty: input.value('albumArtistContributions'), }), @@ -133,6 +132,7 @@ export class Album extends Thing { trackArtistContribs: [ withResolvedContribs({ from: input.updateValue({validate: isContributionList}), + class: input.value(TrackArtistContribution), thingProperty: input.thisProperty(), artistProperty: input.value('albumTrackArtistContributions'), }).outputs({ @@ -142,6 +142,7 @@ export class Album extends Thing { exposeDependencyOrContinue('#trackArtistContribs', V('empty')), withRecontextualizedContributionList('artistContribs', { + reclass: input.value(TrackArtistContribution), artistProperty: input.value('albumTrackArtistContributions'), }), @@ -267,6 +268,7 @@ export class Album extends Thing { ], wallpaperArtistContribs: contributionList({ + class: input.value(AlbumWallpaperArtistContribution), date: 'coverArtDate', artistProperty: input.value('albumWallpaperArtistContributions'), }), @@ -309,6 +311,7 @@ export class Album extends Thing { ], bannerArtistContribs: contributionList({ + class: input.value(AlbumBannerArtistContribution), date: 'coverArtDate', artistProperty: input.value('albumBannerArtistContributions'), }), @@ -794,75 +797,6 @@ export class Album extends Thing { ], }; - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {headerAndEntries}, - thingConstructors: {Album, Track, TrackSection}, - }) => ({ - title: `Process album files`, - - files: dataPath => - traverse(path.join(dataPath, DATA_ALBUM_DIRECTORY), { - filterFile: name => path.extname(name) === '.yaml', - prefixPath: DATA_ALBUM_DIRECTORY, - }), - - documentMode: headerAndEntries, - headerDocumentThing: Album, - entryDocumentThing: document => - ('Section' in document - ? TrackSection - : Track), - - connect({header: album, entries}) { - const trackSections = []; - - let currentTrackSection = new TrackSection(); - let currentTrackSectionTracks = []; - - Object.assign(currentTrackSection, { - name: `Default Track Section`, - isDefaultTrackSection: true, - }); - - const closeCurrentTrackSection = () => { - if ( - currentTrackSection.isDefaultTrackSection && - empty(currentTrackSectionTracks) - ) { - return; - } - - currentTrackSection.tracks = currentTrackSectionTracks; - currentTrackSection.album = album; - - trackSections.push(currentTrackSection); - }; - - for (const entry of entries) { - if (entry instanceof TrackSection) { - closeCurrentTrackSection(); - currentTrackSection = entry; - currentTrackSectionTracks = []; - continue; - } - - entry.album = album; - entry.trackSection = currentTrackSection; - - currentTrackSectionTracks.push(entry); - } - - closeCurrentTrackSection(); - - album.trackSections = trackSections; - }, - - sort({albumData, trackData}) { - sortChronologically(albumData); - sortAlbumsTracksChronologically(trackData); - }, - }); - getOwnAdditionalFilePath(_file, filename) { return [ 'media.albumAdditionalFile', diff --git a/src/data/things/contrib/AlbumArtistContribution.js b/src/data/things/contrib/AlbumArtistContribution.js new file mode 100644 index 00000000..7b6bc9da --- /dev/null +++ b/src/data/things/contrib/AlbumArtistContribution.js @@ -0,0 +1,12 @@ +import {V} from '#composite'; +import Thing from '#thing'; + +import {exposeConstant} from '#composite/control-flow'; + +import {MusicalArtistContribution} from './MusicalArtistContribution.js'; + +export class AlbumArtistContribution extends MusicalArtistContribution { + static [Thing.getPropertyDescriptors] = () => ({ + isAlbumArtistContribution: exposeConstant(V(true)), + }); +} diff --git a/src/data/things/contrib/AlbumAssetArtworkArtistContribution.js b/src/data/things/contrib/AlbumAssetArtworkArtistContribution.js new file mode 100644 index 00000000..fbc3f719 --- /dev/null +++ b/src/data/things/contrib/AlbumAssetArtworkArtistContribution.js @@ -0,0 +1,12 @@ +import {V} from '#composite'; +import Thing from '#thing'; + +import {exposeConstant} from '#composite/control-flow'; + +import {ArtworkArtistContribution} from './ArtworkArtistContribution.js'; + +export class AlbumAssetArtworkArtistContribution extends ArtworkArtistContribution { + static [Thing.getPropertyDescriptors] = () => ({ + isAlbumAssetArtworkArtistContribution: exposeConstant(V(true)), + }); +} diff --git a/src/data/things/contrib/AlbumBannerArtistContribution.js b/src/data/things/contrib/AlbumBannerArtistContribution.js new file mode 100644 index 00000000..16f1c9bb --- /dev/null +++ b/src/data/things/contrib/AlbumBannerArtistContribution.js @@ -0,0 +1,12 @@ +import {V} from '#composite'; +import Thing from '#thing'; + +import {exposeConstant} from '#composite/control-flow'; + +import {AlbumAssetArtworkArtistContribution} from './AlbumAssetArtworkArtistContribution.js'; + +export class AlbumBannerArtistContribution extends AlbumAssetArtworkArtistContribution { + static [Thing.getPropertyDescriptors] = () => ({ + isAlbumBannerArtistContribution: exposeConstant(V(true)), + }); +} diff --git a/src/data/things/contrib/AlbumWallpaperArtistContribution.js b/src/data/things/contrib/AlbumWallpaperArtistContribution.js new file mode 100644 index 00000000..acd29cf8 --- /dev/null +++ b/src/data/things/contrib/AlbumWallpaperArtistContribution.js @@ -0,0 +1,12 @@ +import {V} from '#composite'; +import Thing from '#thing'; + +import {exposeConstant} from '#composite/control-flow'; + +import {AlbumAssetArtworkArtistContribution} from './AlbumAssetArtworkArtistContribution.js'; + +export class AlbumWallpaperArtistContribution extends AlbumAssetArtworkArtistContribution { + static [Thing.getPropertyDescriptors] = () => ({ + isAlbumWallpaperArtistContribution: exposeConstant(V(true)), + }); +} diff --git a/src/data/things/contrib/ArtworkArtistContribution.js b/src/data/things/contrib/ArtworkArtistContribution.js new file mode 100644 index 00000000..a47f2391 --- /dev/null +++ b/src/data/things/contrib/ArtworkArtistContribution.js @@ -0,0 +1,20 @@ +import {V} from '#composite'; +import Thing from '#thing'; + +import {exposeConstant} from '#composite/control-flow'; + +import {hasAnnotationFront} from '#composite/things/contribution'; + +import {Contribution} from './Contribution.js'; + +export class ArtworkArtistContribution extends Contribution { + static [Thing.getPropertyDescriptors] = () => ({ + isArtworkArtistContribution: exposeConstant(V(true)), + + recognizedAnnotationFronts: + exposeConstant(V(['edits for wiki'])), + + isEditsForWikiCredit: + hasAnnotationFront(V('edits for wiki')), + }); +} diff --git a/src/data/things/Contribution.js b/src/data/things/contrib/Contribution.js index 4048709b..4352b58a 100644 --- a/src/data/things/Contribution.js +++ b/src/data/things/contrib/Contribution.js @@ -7,7 +7,7 @@ import {empty} from '#sugar'; import Thing from '#thing'; import {isBoolean, isStringNonEmpty, isThing} from '#validators'; -import {simpleDate, singleReference, soupyFind} +import {simpleDate, singleReference, simpleString, soupyFind} from '#composite/wiki-properties'; import { @@ -56,6 +56,8 @@ export class Contribution extends Thing { find: soupyFind.input('artist'), }), + artistText: simpleString(), + annotation: { flags: {update: true, expose: true}, update: {validate: isStringNonEmpty}, @@ -121,16 +123,51 @@ export class Contribution extends Thing { isContribution: exposeConstant(V(true)), - annotationParts: { - flags: {expose: true}, - expose: { - dependencies: ['annotation'], - compute: ({annotation}) => - (annotation - ? annotation.split(',').map(part => part.trim()) - : []), + recognizedAnnotationFronts: exposeConstant(V([])), + + annotationFront: [ + exitWithoutDependency('annotation'), + + { + dependencies: ['recognizedAnnotationFronts', 'annotation'], + compute: ({recognizedAnnotationFronts, annotation}) => + recognizedAnnotationFronts + .find(front => + annotation.startsWith(front) && ( + annotation === front || + annotation.at(front.length) === ':' || + annotation.at(front.length) === ',' + )) ?? null, }, - }, + ], + + annotationBack: [ + exitWithoutDependency('annotation'), + + exitWithoutDependency({ + dependency: 'annotationFront', + value: 'annotation', + }), + + { + dependencies: ['annotation', 'annotationFront'], + compute: ({annotation, annotationFront}) => + annotation.slice(annotationFront.length + 1).trim() + || null, + }, + ], + + annotationParts: [ + exitWithoutDependency('annotationBack', V([])), + + { + dependencies: ['annotationBack'], + compute: ({annotationBack}) => + annotationBack + .split(',') + .map(part => part.trim()), + }, + ], context: [ withContributionContext(), diff --git a/src/data/things/contrib/MusicalArtistContribution.js b/src/data/things/contrib/MusicalArtistContribution.js new file mode 100644 index 00000000..df26850b --- /dev/null +++ b/src/data/things/contrib/MusicalArtistContribution.js @@ -0,0 +1,20 @@ +import {V} from '#composite'; +import Thing from '#thing'; + +import {exposeConstant} from '#composite/control-flow'; + +import {hasAnnotationFront} from '#composite/things/contribution'; + +import {Contribution} from './Contribution.js'; + +export class MusicalArtistContribution extends Contribution { + static [Thing.getPropertyDescriptors] = () => ({ + isMusicalArtistContribution: exposeConstant(V(true)), + + recognizedAnnotationFronts: + exposeConstant(V(['featuring'])), + + isFeaturingCredit: + hasAnnotationFront(V('featuring')), + }); +} diff --git a/src/data/things/contrib/TrackArtistContribution.js b/src/data/things/contrib/TrackArtistContribution.js new file mode 100644 index 00000000..ecbe9b34 --- /dev/null +++ b/src/data/things/contrib/TrackArtistContribution.js @@ -0,0 +1,12 @@ +import {V} from '#composite'; +import Thing from '#thing'; + +import {exposeConstant} from '#composite/control-flow'; + +import {MusicalArtistContribution} from './MusicalArtistContribution.js'; + +export class TrackArtistContribution extends MusicalArtistContribution { + static [Thing.getPropertyDescriptors] = () => ({ + isTrackArtistContribution: exposeConstant(V(true)), + }); +} diff --git a/src/data/things/contrib/index.js b/src/data/things/contrib/index.js new file mode 100644 index 00000000..187ddb2c --- /dev/null +++ b/src/data/things/contrib/index.js @@ -0,0 +1,11 @@ +export * from './Contribution.js'; + +export * from './MusicalArtistContribution.js'; +export * from './AlbumArtistContribution.js'; +export * from './TrackArtistContribution.js'; + +export * from './ArtworkArtistContribution.js'; + +export * from './AlbumAssetArtworkArtistContribution.js'; +export * from './AlbumBannerArtistContribution.js'; +export * from './AlbumWallpaperArtistContribution.js'; diff --git a/src/data/things/flash/FlashSide.js b/src/data/things/flash/FlashSide.js index 72782bdd..5e2ea3de 100644 --- a/src/data/things/flash/FlashSide.js +++ b/src/data/things/flash/FlashSide.js @@ -1,7 +1,4 @@ -const FLASH_DATA_FILE = 'flashes.yaml'; - import {V} from '#composite'; -import {sortFlashesChronologically} from '#sort'; import Thing from '#thing'; import {exposeConstant} from '#composite/control-flow'; @@ -56,81 +53,4 @@ export class FlashSide extends Thing { referenced: flashSide => flashSide.acts, }, }; - - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allInOne}, - thingConstructors: {Flash, FlashAct}, - }) => ({ - title: `Process flashes file`, - file: FLASH_DATA_FILE, - - documentMode: allInOne, - documentThing: document => - ('Side' in document - ? FlashSide - : 'Act' in document - ? FlashAct - : Flash), - - connect(results) { - let thing, i; - - for (i = 0; thing = results[i]; i++) { - if (thing.isFlashSide) { - const side = thing; - const acts = []; - - for (i++; thing = results[i]; i++) { - if (thing.isFlashAct) { - const act = thing; - const flashes = []; - - for (i++; thing = results[i]; i++) { - if (thing.isFlash) { - const flash = thing; - - flash.act = act; - flashes.push(flash); - - continue; - } - - i--; - break; - } - - act.side = side; - act.flashes = flashes; - acts.push(act); - - continue; - } - - if (thing.isFlash) { - throw new Error(`Flashes must be under an act`); - } - - i--; - break; - } - - side.acts = acts; - - continue; - } - - if (thing.isFlashAct) { - throw new Error(`Acts must be under a side`); - } - - if (thing.isFlash) { - throw new Error(`Flashes must be under a side and act`); - } - } - }, - - sort({flashData}) { - sortFlashesChronologically(flashData); - }, - }); } diff --git a/src/data/things/group/Group.js b/src/data/things/group/Group.js index b065f9a3..6f698682 100644 --- a/src/data/things/group/Group.js +++ b/src/data/things/group/Group.js @@ -1,5 +1,3 @@ -const GROUP_DATA_FILE = 'groups.yaml'; - import {input, V} from '#composite'; import Thing from '#thing'; import {isBoolean} from '#validators'; @@ -185,48 +183,4 @@ export class Group extends Thing { 'Review Points': {ignore: true}, }, }; - - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allInOne}, - thingConstructors: {Group, GroupCategory}, - }) => ({ - title: `Process groups file`, - file: GROUP_DATA_FILE, - - documentMode: allInOne, - documentThing: document => - ('Category' in document - ? GroupCategory - : Group), - - connect(results) { - let groupCategory; - let groupRefs = []; - - if (results[0] && !(results[0] instanceof GroupCategory)) { - throw new Error(`Expected a category at top of group data file`); - } - - for (const thing of results) { - if (thing instanceof GroupCategory) { - if (groupCategory) { - Object.assign(groupCategory, {groups: groupRefs}); - } - - groupCategory = thing; - groupRefs = []; - } else { - groupRefs.push(Thing.getReference(thing)); - } - } - - if (groupCategory) { - Object.assign(groupCategory, {groups: groupRefs}); - } - }, - - // Groups aren't sorted at all, always preserving the order in the data - // file as-is. - sort: null, - }); } diff --git a/src/data/things/homepage-layout/HomepageLayout.js b/src/data/things/homepage-layout/HomepageLayout.js index e144bf80..1c432b53 100644 --- a/src/data/things/homepage-layout/HomepageLayout.js +++ b/src/data/things/homepage-layout/HomepageLayout.js @@ -1,8 +1,5 @@ -const HOMEPAGE_LAYOUT_DATA_FILE = 'homepage.yaml'; - import {V} from '#composite'; import Thing from '#thing'; -import {empty} from '#sugar'; import {isStringNonEmpty, validateArrayItems} from '#validators'; import {exposeConstant} from '#composite/control-flow'; @@ -39,90 +36,4 @@ export class HomepageLayout extends Thing { 'Navbar Links': {property: 'navbarLinks'}, }, }; - - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allInOne}, - thingConstructors: { - HomepageLayout, - HomepageLayoutActionsRow, - HomepageLayoutAlbumCarouselRow, - HomepageLayoutAlbumGridRow, - HomepageLayoutRow, - HomepageLayoutSection, - }, - }) => ({ - title: `Process homepage layout file`, - file: HOMEPAGE_LAYOUT_DATA_FILE, - - documentMode: allInOne, - documentThing: document => { - if (document['Homepage']) { - return HomepageLayout; - } - - if (document['Section']) { - return HomepageLayoutSection; - } - - if (document['Row']) { - switch (document['Row']) { - case 'actions': - return HomepageLayoutActionsRow; - case 'album carousel': - return HomepageLayoutAlbumCarouselRow; - case 'album grid': - return HomepageLayoutAlbumGridRow; - default: - throw new TypeError(`Unrecognized row type ${document['Row']}`); - } - } - - return null; - }, - - connect(results) { - if (!empty(results) && !(results[0] instanceof HomepageLayout)) { - throw new Error(`Expected 'Homepage' document at top of homepage layout file`); - } - - const homepageLayout = results[0]; - const sections = []; - - let currentSection = null; - let currentSectionRows = []; - - const closeCurrentSection = () => { - if (currentSection) { - for (const row of currentSectionRows) { - row.section = currentSection; - } - - currentSection.rows = currentSectionRows; - sections.push(currentSection); - - currentSection = null; - currentSectionRows = []; - } - }; - - for (const entry of results.slice(1)) { - if (entry instanceof HomepageLayout) { - throw new Error(`Expected only one 'Homepage' document in total`); - } else if (entry instanceof HomepageLayoutSection) { - closeCurrentSection(); - currentSection = entry; - } else if (entry instanceof HomepageLayoutRow) { - if (currentSection) { - currentSectionRows.push(entry); - } else { - throw new Error(`Expected a 'Section' document to add following rows into`); - } - } - } - - closeCurrentSection(); - - homepageLayout.sections = sections; - }, - }); } diff --git a/src/data/things/index.js b/src/data/things/index.js index bf8a5a33..3773864b 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -2,6 +2,7 @@ export * from './album/index.js'; export * from './content/index.js'; +export * from './contrib/index.js'; export * from './flash/index.js'; export * from './group/index.js'; export * from './homepage-layout/index.js'; @@ -12,7 +13,6 @@ export * from './AdditionalName.js'; export * from './ArtTag.js'; export * from './Artist.js'; export * from './Artwork.js'; -export * from './Contribution.js'; export * from './Language.js'; export * from './MusicVideo.js'; export * from './NewsEntry.js'; diff --git a/src/data/things/sorting-rule/SortingRule.js b/src/data/things/sorting-rule/SortingRule.js index 4ce9d97a..5d4bba99 100644 --- a/src/data/things/sorting-rule/SortingRule.js +++ b/src/data/things/sorting-rule/SortingRule.js @@ -1,5 +1,3 @@ -const SORTING_RULE_DATA_FILE = 'sorting-rules.yaml'; - import {V} from '#composite'; import {unique} from '#sugar'; import Thing from '#thing'; @@ -34,20 +32,6 @@ export class SortingRule extends Thing { }, }; - static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allInOne}, - thingConstructors: {DocumentSortingRule}, - }) => ({ - title: `Process sorting rules file`, - file: SORTING_RULE_DATA_FILE, - - documentMode: allInOne, - documentThing: document => - (document['Sort Documents'] - ? DocumentSortingRule - : null), - }); - check(opts) { return this.constructor.check(this, opts); } diff --git a/src/data/yaml.js b/src/data/yaml.js index 908d42c6..2afaffb5 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -7,6 +7,7 @@ import {inspect as nodeInspect} from 'node:util'; import yaml from 'js-yaml'; +import * as fileLoadingSpecs from '#files'; import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import {parseContentNodes, splitContentNodesAround} from '#replacer'; import {sortByName} from '#sort'; @@ -645,6 +646,9 @@ export const extractAccentRegex = export const extractPrefixAccentRegex = /^(?:\((?<accent>.*)\) )?(?<main>.*?)$/; +export const asNameRegex = + /^as (?<name>\S.+?)(?:(?<=\S)[,:] | +- |$)(?: *(?<annotation>.*))?$/; + // TODO: Should this fit better within actual YAML loading infrastructure?? export function parseArrayEntries(entries, mapFn) { // If this isn't something we can parse, just return it as-is. @@ -678,12 +682,14 @@ export function parseContributors(entries) { if (typeof item === 'object' && item['Who']) return { artist: item['Who'], + artistText: item['As'] ?? null, annotation: item['What'] ?? null, }; if (typeof item === 'object' && item['Artist']) return { artist: item['Artist'], + artistText: item['Artist Text'] ?? null, annotation: item['Annotation'] ?? null, countInContributionTotals: item['Count In Contribution Totals'] ?? null, @@ -692,13 +698,28 @@ export function parseContributors(entries) { if (typeof item !== 'string') return item; - const match = item.match(extractAccentRegex); + let match; + + match = item.match(extractAccentRegex); if (!match) return item; - return { - artist: match.groups.main, - annotation: match.groups.accent ?? null, - }; + const {accent} = match.groups; + + let artist = match.groups.main; + let artistText = null; + let annotation = null; + + if (accent) { + match = accent.match(asNameRegex); + if (match) { + artistText = match.groups.name; + annotation = match.groups.annotation ?? null; + } else { + annotation = accent; + } + } + + return {artist, artistText, annotation}; }); } @@ -1157,17 +1178,7 @@ export function getAllDataSteps() { const steps = []; - const seenLoadingFns = new Set(); - - for (const thingConstructor of Object.values(thingConstructors)) { - const getSpecFn = thingConstructor[Thing.getYamlLoadingSpec]; - if (!getSpecFn) continue; - - // Subclasses can expose literally the same static properties - // by inheritence. We don't want to double-count those! - if (seenLoadingFns.has(getSpecFn)) continue; - seenLoadingFns.add(getSpecFn); - + for (const getSpecFn of Object.values(fileLoadingSpecs)) { steps.push(getSpecFn({ documentModes, thingConstructors, @@ -1837,6 +1848,7 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) { for (const thing of things) { if (thing === undefined) continue; + if (thing === null) continue; let hasFind; if (constructorHasFindMap.has(thing.constructor)) { diff --git a/src/validators.js b/src/validators.js index 63268ded..625df307 100644 --- a/src/validators.js +++ b/src/validators.js @@ -678,6 +678,7 @@ export function isThing(thing) { export const isContribution = validateProperties({ artist: isArtistRef, + artistText: optional(isStringNonEmpty), annotation: optional(isStringNonEmpty), countInDurationTotals: optional(isBoolean), |