diff options
Diffstat (limited to 'src/data/things')
-rw-r--r-- | src/data/things/additional-file.js | 11 | ||||
-rw-r--r-- | src/data/things/additional-name.js | 10 | ||||
-rw-r--r-- | src/data/things/album.js | 522 | ||||
-rw-r--r-- | src/data/things/art-tag.js | 37 | ||||
-rw-r--r-- | src/data/things/artist.js | 117 | ||||
-rw-r--r-- | src/data/things/artwork.js | 96 | ||||
-rw-r--r-- | src/data/things/content.js | 48 | ||||
-rw-r--r-- | src/data/things/contribution.js | 120 | ||||
-rw-r--r-- | src/data/things/flash.js | 20 | ||||
-rw-r--r-- | src/data/things/group.js | 67 | ||||
-rw-r--r-- | src/data/things/homepage-layout.js | 42 | ||||
-rw-r--r-- | src/data/things/language.js | 58 | ||||
-rw-r--r-- | src/data/things/news-entry.js | 8 | ||||
-rw-r--r-- | src/data/things/sorting-rule.js | 26 | ||||
-rw-r--r-- | src/data/things/static-page.js | 10 | ||||
-rw-r--r-- | src/data/things/track.js | 515 | ||||
-rw-r--r-- | src/data/things/wiki-info.js | 10 |
17 files changed, 1223 insertions, 494 deletions
diff --git a/src/data/things/additional-file.js b/src/data/things/additional-file.js index 398d0af5..b15f62e0 100644 --- a/src/data/things/additional-file.js +++ b/src/data/things/additional-file.js @@ -2,10 +2,9 @@ import {input} from '#composite'; import Thing from '#thing'; import {isString, validateArrayItems} from '#validators'; -import {contentString, simpleString, thing} from '#composite/wiki-properties'; - import {exposeConstant, exposeUpdateValueOrContinue} from '#composite/control-flow'; +import {contentString, simpleString, thing} from '#composite/wiki-properties'; export class AdditionalFile extends Thing { static [Thing.getPropertyDescriptors] = () => ({ @@ -26,6 +25,14 @@ export class AdditionalFile extends Thing { value: input.value([]), }), ], + + // Expose only + + isAdditionalFile: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { diff --git a/src/data/things/additional-name.js b/src/data/things/additional-name.js index 4c23f291..99f3ee46 100644 --- a/src/data/things/additional-name.js +++ b/src/data/things/additional-name.js @@ -1,5 +1,7 @@ +import {input} from '#composite'; import Thing from '#thing'; +import {exposeConstant} from '#composite/control-flow'; import {contentString, thing} from '#composite/wiki-properties'; export class AdditionalName extends Thing { @@ -10,6 +12,14 @@ export class AdditionalName extends Thing { name: contentString(), annotation: contentString(), + + // Expose only + + isAdditionalName: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { diff --git a/src/data/things/album.js b/src/data/things/album.js index af42c6fa..427c5d7f 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -10,7 +10,8 @@ import {traverse} from '#node-utils'; import {sortAlbumsTracksChronologically, sortChronologically} from '#sort'; import {empty} from '#sugar'; import Thing from '#thing'; -import {isColor, isDate, isDirectory, isNumber} from '#validators'; +import {is, isColor, isContributionList, isDate, isDirectory, isNumber} + from '#validators'; import { parseAdditionalFiles, @@ -25,12 +26,22 @@ import { parseWallpaperParts, } from '#yaml'; -import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} - from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; -import {exitWithoutContribs, withDirectory, withCoverArtDate} - from '#composite/wiki-data'; +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + +import { + exitWithoutArtwork, + withDirectory, + withHasArtwork, + withResolvedContribs, +} from '#composite/wiki-data'; import { color, @@ -58,7 +69,7 @@ import { wikiData, } from '#composite/wiki-properties'; -import {withHasCoverArt, withTracks} from '#composite/things/album'; +import {withCoverArtDate, withTracks} from '#composite/things/album'; import {withAlbum, withContinueCountingFrom, withStartCountingFrom} from '#composite/things/track-section'; @@ -76,7 +87,13 @@ export class Album extends Thing { TrackSection, WikiInfo, }) => ({ - // Update & expose + // > Update & expose - Internal relationships + + trackSections: thingList({ + class: input.value(TrackSection), + }), + + // > Update & expose - Identifying metadata name: name('Unnamed Album'), directory: directory(), @@ -97,20 +114,105 @@ export class Album extends Thing { alwaysReferenceTracksByDirectory: flag(false), suffixTrackDirectories: flag(false), - color: color(), - urls: urls(), + style: [ + exposeUpdateValueOrContinue({ + validate: input.value(is(...[ + 'album', + 'single', + ])), + }), - additionalNames: thingList({ - class: input.value(AdditionalName), - }), + exposeConstant({ + value: input.value('album'), + }), + ], bandcampAlbumIdentifier: simpleString(), bandcampArtworkIdentifier: simpleString(), + additionalNames: thingList({ + class: input.value(AdditionalName), + }), + date: simpleDate(), - trackArtDate: simpleDate(), dateAddedToWiki: simpleDate(), + // > Update & expose - Credits and contributors + + artistContribs: contributionList({ + date: 'date', + artistProperty: input.value('albumArtistContributions'), + }), + + trackArtistContribs: [ + withResolvedContribs({ + from: input.updateValue({validate: isContributionList}), + thingProperty: input.thisProperty(), + artistProperty: input.value('albumTrackArtistContributions'), + date: 'date', + }).outputs({ + '#resolvedContribs': '#trackArtistContribs', + }), + + exposeDependencyOrContinue({ + dependency: '#trackArtistContribs', + mode: input.value('empty'), + }), + + withResolvedContribs({ + from: 'artistContribs', + thingProperty: input.thisProperty(), + artistProperty: input.value('albumTrackArtistContributions'), + date: 'date', + }).outputs({ + '#resolvedContribs': '#trackArtistContribs', + }), + + exposeDependency({dependency: '#trackArtistContribs'}), + ], + + // > Update & expose - General configuration + + countTracksInArtistTotals: flag(true), + + hasTrackNumbers: flag(true), + isListedOnHomepage: flag(true), + isListedInGalleries: flag(true), + + hideDuration: flag(false), + + // > Update & expose - General metadata + + color: color(), + + urls: urls(), + + // > Update & expose - Artworks + + coverArtworks: [ + // This works, lol, because this array describes `expose.transform` for + // the coverArtworks property, and compositions generally access the + // update value, not what's exposed by property access out in the open. + // There's no recursion going on here. + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + value: input.value([]), + }), + + constitutibleArtworkList.fromYAMLFieldSpec + .call(this, 'Cover Artwork'), + ], + + coverArtistContribs: [ + withCoverArtDate(), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumCoverArtistContributions'), + }), + ], + coverArtDate: [ withCoverArtDate({ from: input.updateValue({ @@ -122,52 +224,61 @@ export class Album extends Thing { ], coverArtFileExtension: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), + fileExtension('jpg'), ], - trackCoverArtFileExtension: fileExtension('jpg'), + coverArtDimensions: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), - wallpaperFileExtension: [ - exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), - fileExtension('jpg'), + dimensions(), ], - bannerFileExtension: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - fileExtension('jpg'), - ], + artTags: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + value: input.value([]), + }), - wallpaperStyle: [ - exitWithoutContribs({contribs: 'wallpaperArtistContribs'}), - simpleString(), + referenceList({ + class: input.value(ArtTag), + find: soupyFind.input('artTag'), + }), ], - wallpaperParts: [ - exitWithoutContribs({ - contribs: 'wallpaperArtistContribs', + referencedArtworks: [ + exitWithoutArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', value: input.value([]), }), - wallpaperParts(), + referencedArtworkList(), ], - bannerStyle: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - simpleString(), - ], + trackCoverArtistContribs: contributionList({ + // May be null, indicating cover art was added for tracks on the date + // each track specifies, or else the track's own release date. + date: 'trackArtDate', - coverArtDimensions: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), - dimensions(), - ], + // This is the "correct" value, but it gets overwritten - with the same + // value - regardless. + artistProperty: input.value('trackCoverArtistContributions'), + }), - trackDimensions: dimensions(), + trackArtDate: simpleDate(), - bannerDimensions: [ - exitWithoutContribs({contribs: 'bannerArtistContribs'}), - dimensions(), - ], + trackCoverArtFileExtension: fileExtension('jpg'), + + trackDimensions: dimensions(), wallpaperArtwork: [ exitWithoutDependency({ @@ -180,119 +291,115 @@ export class Album extends Thing { .call(this, 'Wallpaper Artwork'), ], - bannerArtwork: [ - exitWithoutDependency({ - dependency: 'bannerArtistContribs', - mode: input.value('empty'), - value: input.value(null), - }), + wallpaperArtistContribs: [ + withCoverArtDate(), - constitutibleArtwork.fromYAMLFieldSpec - .call(this, 'Banner Artwork'), + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumWallpaperArtistContributions'), + }), ], - coverArtworks: [ - withHasCoverArt(), - - exitWithoutDependency({ - dependency: '#hasCoverArt', - mode: input.value('falsy'), - value: input.value([]), + wallpaperFileExtension: [ + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', }), - constitutibleArtworkList.fromYAMLFieldSpec - .call(this, 'Cover Artwork'), + fileExtension('jpg'), ], - hasTrackNumbers: flag(true), - isListedOnHomepage: flag(true), - isListedInGalleries: flag(true), + wallpaperStyle: [ + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', + }), - commentary: thingList({ - class: input.value(CommentaryEntry), - }), + simpleString(), + ], - creditingSources: thingList({ - class: input.value(CreditingSourcesEntry), - }), + wallpaperParts: [ + // kinda nonsensical or at least unlikely lol, but y'know + exitWithoutArtwork({ + contribs: 'wallpaperArtistContribs', + artwork: 'wallpaperArtwork', + value: input.value([]), + }), - additionalFiles: thingList({ - class: input.value(AdditionalFile), - }), + wallpaperParts(), + ], - trackSections: thingList({ - class: input.value(TrackSection), - }), + bannerArtwork: [ + exitWithoutDependency({ + dependency: 'bannerArtistContribs', + mode: input.value('empty'), + value: input.value(null), + }), - artistContribs: contributionList({ - date: 'date', - artistProperty: input.value('albumArtistContributions'), - }), + constitutibleArtwork.fromYAMLFieldSpec + .call(this, 'Banner Artwork'), + ], - coverArtistContribs: [ + bannerArtistContribs: [ withCoverArtDate(), contributionList({ date: '#coverArtDate', - artistProperty: input.value('albumCoverArtistContributions'), + artistProperty: input.value('albumBannerArtistContributions'), }), ], - trackCoverArtistContribs: contributionList({ - // May be null, indicating cover art was added for tracks on the date - // each track specifies, or else the track's own release date. - date: 'trackArtDate', - - // This is the "correct" value, but it gets overwritten - with the same - // value - regardless. - artistProperty: input.value('trackCoverArtistContributions'), - }), + bannerFileExtension: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', + }), - wallpaperArtistContribs: [ - withCoverArtDate(), + fileExtension('jpg'), + ], - contributionList({ - date: '#coverArtDate', - artistProperty: input.value('albumWallpaperArtistContributions'), + bannerDimensions: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', }), - ], - bannerArtistContribs: [ - withCoverArtDate(), + dimensions(), + ], - contributionList({ - date: '#coverArtDate', - artistProperty: input.value('albumBannerArtistContributions'), + bannerStyle: [ + exitWithoutArtwork({ + contribs: 'bannerArtistContribs', + artwork: 'bannerArtwork', }), + + simpleString(), ], + // > Update & expose - Groups + groups: referenceList({ class: input.value(Group), find: soupyFind.input('group'), }), - artTags: [ - exitWithoutContribs({ - contribs: 'coverArtistContribs', - value: input.value([]), - }), + // > Update & expose - Content entries - referenceList({ - class: input.value(ArtTag), - find: soupyFind.input('artTag'), - }), - ], + commentary: thingList({ + class: input.value(CommentaryEntry), + }), - referencedArtworks: [ - exitWithoutContribs({ - contribs: 'coverArtistContribs', - value: input.value([]), - }), + creditingSources: thingList({ + class: input.value(CreditingSourcesEntry), + }), - referencedArtworkList(), - ], + // > Update & expose - Additional files - // Update only + additionalFiles: thingList({ + class: input.value(AdditionalFile), + }), + + // > Update only find: soupyFind(), reverse: soupyReverse(), @@ -307,13 +414,23 @@ export class Album extends Thing { class: input.value(WikiInfo), }), - // Expose only + // > Expose only + + isAlbum: [ + exposeConstant({ + value: input.value(true), + }), + ], commentatorArtists: commentatorArtists(), hasCoverArt: [ - withHasCoverArt(), - exposeDependency({dependency: '#hasCoverArt'}), + withHasArtwork({ + contribs: 'coverArtistContribs', + artworks: 'coverArtworks', + }), + + exposeDependency({dependency: '#hasArtwork'}), ], hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}), @@ -457,6 +574,9 @@ export class Album extends Thing { albumArtistContributionsBy: soupyReverse.contributionsBy('albumData', 'artistContribs'), + albumTrackArtistContributionsBy: + soupyReverse.contributionsBy('albumData', 'trackArtistContribs'), + albumCoverArtistContributionsBy: soupyReverse.artworkContributionsBy('albumData', 'coverArtworks'), @@ -476,21 +596,15 @@ export class Album extends Thing { static [Thing.yamlDocumentSpec] = { fields: { - 'Album': {property: 'name'}, + // Identifying metadata + 'Album': {property: 'name'}, 'Directory': {property: 'directory'}, 'Directory Suffix': {property: 'directorySuffix'}, 'Suffix Track Directories': {property: 'suffixTrackDirectories'}, - 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, - 'Always Reference Tracks By Directory': { - property: 'alwaysReferenceTracksByDirectory', - }, - - 'Additional Names': { - property: 'additionalNames', - transform: parseAdditionalNames, - }, + 'Always Reference Tracks By Directory': {property: 'alwaysReferenceTracksByDirectory'}, + 'Style': {property: 'style'}, 'Bandcamp Album ID': { property: 'bandcampAlbumIdentifier', @@ -502,18 +616,53 @@ export class Album extends Thing { transform: String, }, + 'Additional Names': { + property: 'additionalNames', + transform: parseAdditionalNames, + }, + 'Date': { property: 'date', transform: parseDate, }, - 'Color': {property: 'color'}, - 'URLs': {property: 'urls'}, + 'Date Added': { + property: 'dateAddedToWiki', + transform: parseDate, + }, + + // Credits and contributors + + 'Artists': { + property: 'artistContribs', + transform: parseContributors, + }, + + 'Track Artists': { + property: 'trackArtistContribs', + transform: parseContributors, + }, + + // General configuration + + 'Count Tracks In Artist Totals': {property: 'countTracksInArtistTotals'}, 'Has Track Numbers': {property: 'hasTrackNumbers'}, 'Listed on Homepage': {property: 'isListedOnHomepage'}, 'Listed in Galleries': {property: 'isListedInGalleries'}, + 'Hide Duration': {property: 'hideDuration'}, + + // General metadata + + 'Color': {property: 'color'}, + + 'URLs': {property: 'urls'}, + + // Artworks + // (Note - this YAML section is deliberately ordered differently + // than the corresponding property descriptors.) + 'Cover Artwork': { property: 'coverArtworks', transform: @@ -557,27 +706,29 @@ export class Album extends Thing { }), }, + 'Cover Artists': { + property: 'coverArtistContribs', + transform: parseContributors, + }, + 'Cover Art Date': { property: 'coverArtDate', transform: parseDate, }, - 'Default Track Cover Art Date': { - property: 'trackArtDate', - transform: parseDate, + 'Cover Art Dimensions': { + property: 'coverArtDimensions', + transform: parseDimensions, }, - 'Date Added': { - property: 'dateAddedToWiki', - transform: parseDate, + 'Default Track Cover Artists': { + property: 'trackCoverArtistContribs', + transform: parseContributors, }, - 'Cover Art File Extension': {property: 'coverArtFileExtension'}, - 'Track Art File Extension': {property: 'trackCoverArtFileExtension'}, - - 'Cover Art Dimensions': { - property: 'coverArtDimensions', - transform: parseDimensions, + 'Default Track Cover Art Date': { + property: 'trackArtDate', + transform: parseDate, }, 'Default Track Dimensions': { @@ -590,8 +741,6 @@ export class Album extends Thing { transform: parseContributors, }, - 'Wallpaper File Extension': {property: 'wallpaperFileExtension'}, - 'Wallpaper Style': {property: 'wallpaperStyle'}, 'Wallpaper Parts': { @@ -604,14 +753,31 @@ export class Album extends Thing { transform: parseContributors, }, - 'Banner Style': {property: 'bannerStyle'}, - 'Banner File Extension': {property: 'bannerFileExtension'}, - 'Banner Dimensions': { property: 'bannerDimensions', transform: parseDimensions, }, + 'Banner Style': {property: 'bannerStyle'}, + + 'Cover Art File Extension': {property: 'coverArtFileExtension'}, + 'Track Art File Extension': {property: 'trackCoverArtFileExtension'}, + 'Wallpaper File Extension': {property: 'wallpaperFileExtension'}, + 'Banner File Extension': {property: 'bannerFileExtension'}, + + 'Art Tags': {property: 'artTags'}, + + 'Referenced Artworks': { + property: 'referencedArtworks', + transform: parseAnnotatedReferences, + }, + + // Groups + + 'Groups': {property: 'groups'}, + + // Content entries + 'Commentary': { property: 'commentary', transform: parseCommentary, @@ -622,40 +788,40 @@ export class Album extends Thing { transform: parseCreditingSources, }, + // Additional files + 'Additional Files': { property: 'additionalFiles', transform: parseAdditionalFiles, }, - 'Referenced Artworks': { - property: 'referencedArtworks', - transform: parseAnnotatedReferences, - }, + // Shenanigans 'Franchises': {ignore: true}, + 'Review Points': {ignore: true}, + }, - 'Artists': { - property: 'artistContribs', - transform: parseContributors, - }, - - 'Cover Artists': { - property: 'coverArtistContribs', - transform: parseContributors, - }, + invalidFieldCombinations: [ + {message: `Move commentary on singles to the track`, fields: [ + ['Style', 'single'], + 'Commentary', + ]}, - 'Default Track Cover Artists': { - property: 'trackCoverArtistContribs', - transform: parseContributors, - }, + {message: `Move crediting sources on singles to the track`, fields: [ + ['Style', 'single'], + 'Crediting Sources', + ]}, - 'Groups': {property: 'groups'}, - 'Art Tags': {property: 'artTags'}, + {message: `Move referencing sources on singles to the track`, fields: [ + ['Style', 'single'], + 'Referencing Sources', + ]}, - 'Review Points': {ignore: true}, - }, + {message: `Move additional names on singles to the track`, fields: [ + ['Style', 'single'], + 'Additional Names', + ]}, - invalidFieldCombinations: [ {message: `Specify one wallpaper style or multiple wallpaper parts, not both`, fields: [ 'Wallpaper Parts', 'Wallpaper Style', @@ -835,6 +1001,12 @@ export class Album extends Thing { artwork.fileExtension, ]; } + + // As of writing, albums don't even have a `duration` property... + // so this function will never be called... but the message stands... + countOwnContributionInDurationTotals(_contrib) { + return false; + } } export class TrackSection extends Thing { @@ -892,6 +1064,12 @@ export class TrackSection extends Thing { // Expose only + isTrackSection: [ + exposeConstant({ + value: input.value(true), + }), + ], + directory: [ withAlbum(), diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 518f616b..fff724cb 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,14 +1,23 @@ +export const DATA_ART_TAGS_DIRECTORY = 'art-tags'; export const ART_TAG_DATA_FILE = 'tags.yaml'; +import {readFile} from 'node:fs/promises'; +import * as path from 'node:path'; + import {input} from '#composite'; +import {traverse} from '#node-utils'; import {sortAlphabetically} from '#sort'; import Thing from '#thing'; import {unique} from '#sugar'; import {isName} from '#validators'; import {parseAdditionalNames, parseAnnotatedReferences} from '#yaml'; -import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} - from '#composite/control-flow'; +import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; import { annotatedReferenceList, @@ -79,6 +88,12 @@ export class ArtTag extends Thing { // Expose only + isArtTag: [ + exposeConstant({ + value: input.value(true), + }), + ], + descriptionShort: [ exitWithoutDependency({ dependency: 'description', @@ -174,13 +189,25 @@ export class ArtTag extends Thing { }; static [Thing.getYamlLoadingSpec] = ({ - documentModes: {allInOne}, + documentModes: {allTogether}, thingConstructors: {ArtTag}, }) => ({ title: `Process art tags file`, - file: ART_TAG_DATA_FILE, - documentMode: allInOne, + 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, save: (results) => ({artTagData: results}), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 5b67051c..2905d893 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -5,14 +5,21 @@ import {inspect} from 'node:util'; import CacheableObject from '#cacheable-object'; import {colors} from '#cli'; import {input} from '#composite'; -import {sortAlphabetically} from '#sort'; import {stitchArrays} from '#sugar'; import Thing from '#thing'; import {isName, validateArrayItems} from '#validators'; import {getKebabCase} from '#wiki-data'; import {parseArtwork} from '#yaml'; -import {exitWithoutDependency} from '#composite/control-flow'; +import { + sortAlbumsTracksChronologically, + sortArtworksChronologically, + sortAlphabetically, + sortContributionsChronologically, +} from '#sort'; + +import {exitWithoutDependency, exposeConstant} from '#composite/control-flow'; +import {withReverseReferenceList} from '#composite/wiki-data'; import { constitutibleArtwork, @@ -76,6 +83,12 @@ export class Artist extends Thing { // Expose only + isArtist: [ + exposeConstant({ + value: input.value(true), + }), + ], + trackArtistContributions: reverseReferenceList({ reverse: soupyReverse.input('trackArtistContributionsBy'), }), @@ -96,6 +109,10 @@ export class Artist extends Thing { reverse: soupyReverse.input('albumArtistContributionsBy'), }), + albumTrackArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('albumTrackArtistContributionsBy'), + }), + albumCoverArtistContributions: reverseReferenceList({ reverse: soupyReverse.input('albumCoverArtistContributionsBy'), }), @@ -124,6 +141,102 @@ export class Artist extends Thing { reverse: soupyReverse.input('groupsCloselyLinkedTo'), }), + musicContributions: [ + withReverseReferenceList({ + reverse: soupyReverse.input('trackArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#trackArtistContribs', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('trackContributorContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#trackContributorContribs', + }), + + { + dependencies: [ + '#trackArtistContribs', + '#trackContributorContribs', + ], + + compute: (continuation, { + ['#trackArtistContribs']: trackArtistContribs, + ['#trackContributorContribs']: trackContributorContribs, + }) => continuation({ + ['#contributions']: [ + ...trackArtistContribs, + ...trackContributorContribs, + ], + }), + }, + + { + dependencies: ['#contributions'], + compute: ({'#contributions': contributions}) => + sortContributionsChronologically( + contributions, + sortAlbumsTracksChronologically), + }, + ], + + artworkContributions: [ + withReverseReferenceList({ + reverse: soupyReverse.input('trackCoverArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#trackCoverArtistContribs', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('albumCoverArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#albumCoverArtistContribs', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('albumWallpaperArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#albumWallpaperArtistContribs', + }), + + withReverseReferenceList({ + reverse: soupyReverse.input('albumBannerArtistContributionsBy'), + }).outputs({ + '#reverseReferenceList': '#albumBannerArtistContribs', + }), + + { + dependencies: [ + '#trackCoverArtistContribs', + '#albumCoverArtistContribs', + '#albumWallpaperArtistContribs', + '#albumBannerArtistContribs', + ], + + compute: (continuation, { + ['#trackCoverArtistContribs']: trackCoverArtistContribs, + ['#albumCoverArtistContribs']: albumCoverArtistContribs, + ['#albumWallpaperArtistContribs']: albumWallpaperArtistContribs, + ['#albumBannerArtistContribs']: albumBannerArtistContribs, + }) => continuation({ + ['#contributions']: [ + ...trackCoverArtistContribs, + ...albumCoverArtistContribs, + ...albumWallpaperArtistContribs, + ...albumBannerArtistContribs, + ], + }), + }, + + { + dependencies: ['#contributions'], + compute: ({'#contributions': contributions}) => + sortContributionsChronologically( + contributions, + sortArtworksChronologically), + }, + ], + totalDuration: artistTotalDuration(), }); diff --git a/src/data/things/artwork.js b/src/data/things/artwork.js index 3cf380a0..c54bcced 100644 --- a/src/data/things/artwork.js +++ b/src/data/things/artwork.js @@ -25,7 +25,7 @@ import { parseDimensions, } from '#yaml'; -import {withPropertyFromObject} from '#composite/data'; +import {withPropertyFromList, withPropertyFromObject} from '#composite/data'; import { exitWithoutDependency, @@ -55,8 +55,10 @@ import { } from '#composite/wiki-properties'; import { + withArtTags, withAttachedArtwork, withContainingArtworkList, + withContentWarningArtTags, withContribsFromAttachedArtwork, withPropertyFromAttachedArtwork, withDate, @@ -170,6 +172,7 @@ export class Artwork extends Thing { withResolvedContribs({ from: input.updateValue({validate: isContributionList}), date: '#date', + thingProperty: input.thisProperty(), artistProperty: 'artistContribsArtistProperty', }), @@ -208,47 +211,16 @@ export class Artwork extends Thing { artTagsFromThingProperty: simpleString(), artTags: [ - withResolvedReferenceList({ - list: input.updateValue({ + withArtTags({ + from: input.updateValue({ validate: validateReferenceList(ArtTag[Thing.referenceType]), }), - - find: soupyFind.input('artTag'), - }), - - exposeDependencyOrContinue({ - dependency: '#resolvedReferenceList', - mode: input.value('empty'), - }), - - withPropertyFromAttachedArtwork({ - property: input.value('artTags'), - }), - - exposeDependencyOrContinue({ - dependency: '#attachedArtwork.artTags', - }), - - exitWithoutDependency({ - dependency: 'artTagsFromThingProperty', - value: input.value([]), }), - withPropertyFromObject({ - object: 'thing', - property: 'artTagsFromThingProperty', - }).outputs({ - ['#value']: '#artTags', - }), - - exposeDependencyOrContinue({ + exposeDependency({ dependency: '#artTags', }), - - exposeConstant({ - value: input.value([]), - }), ], referencedArtworksFromThingProperty: simpleString(), @@ -322,6 +294,12 @@ export class Artwork extends Thing { // Expose only + isArtwork: [ + exposeConstant({ + value: input.value(true), + }), + ], + referencedByArtworks: reverseReferenceList({ reverse: soupyReverse.input('artworksWhichReference'), }), @@ -370,6 +348,42 @@ export class Artwork extends Thing { attachingArtworks: reverseReferenceList({ reverse: soupyReverse.input('artworksWhichAttach'), }), + + groups: [ + withPropertyFromObject({ + object: 'thing', + property: input.value('groups'), + }), + + exposeDependencyOrContinue({ + dependency: '#thing.groups', + }), + + exposeConstant({ + value: input.value([]), + }), + ], + + contentWarningArtTags: [ + withContentWarningArtTags(), + + exposeDependency({ + dependency: '#contentWarningArtTags', + }), + ], + + contentWarnings: [ + withContentWarningArtTags(), + + withPropertyFromList({ + list: '#contentWarningArtTags', + property: input.value('name'), + }), + + exposeDependency({ + dependency: '#contentWarningArtTags.name', + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -456,6 +470,18 @@ export class Artwork extends Thing { return this.thing.getOwnArtworkPath(this); } + countOwnContributionInContributionTotals(contrib) { + if (this.attachAbove) { + return false; + } + + if (contrib.annotation?.startsWith('edits for wiki')) { + return false; + } + + return true; + } + [inspect.custom](depth, options, inspect) { const parts = []; diff --git a/src/data/things/content.js b/src/data/things/content.js index e380780c..d2cf32dc 100644 --- a/src/data/things/content.js +++ b/src/data/things/content.js @@ -50,6 +50,10 @@ export class ContentEntry extends Thing { }, accessKind: [ + exitWithoutDependency({ + dependency: 'accessDate', + }), + exposeUpdateValueOrContinue({ validate: input.value( is(...[ @@ -73,7 +77,7 @@ export class ContentEntry extends Thing { }, exposeConstant({ - value: input.value(null), + value: input.value('accessed'), }), ], @@ -105,6 +109,12 @@ export class ContentEntry extends Thing { // Expose only + isContentEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + annotationParts: [ withAnnotationParts({ mode: input.value('strings'), @@ -147,6 +157,12 @@ export class CommentaryEntry extends ContentEntry { static [Thing.getPropertyDescriptors] = () => ({ // Expose only + isCommentaryEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + isWikiEditorCommentary: hasAnnotationPart({ part: input.value('wiki editor'), }), @@ -161,6 +177,12 @@ export class LyricsEntry extends ContentEntry { // Expose only + isLyricsEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + isWikiLyrics: hasAnnotationPart({ part: input.value('wiki lyrics'), }), @@ -196,6 +218,26 @@ export class LyricsEntry extends ContentEntry { }); } -export class CreditingSourcesEntry extends ContentEntry {} +export class CreditingSourcesEntry extends ContentEntry { + static [Thing.getPropertyDescriptors] = () => ({ + // Expose only + + isCreditingSourcesEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + }); +} -export class ReferencingSourcesEntry extends ContentEntry {} +export class ReferencingSourcesEntry extends ContentEntry { + static [Thing.getPropertyDescriptors] = () => ({ + // Expose only + + isReferencingSourceEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + }); +} diff --git a/src/data/things/contribution.js b/src/data/things/contribution.js index b3655eb8..006aeec0 100644 --- a/src/data/things/contribution.js +++ b/src/data/things/contribution.js @@ -5,12 +5,20 @@ import {colors} from '#cli'; import {input} from '#composite'; import {empty} from '#sugar'; import Thing from '#thing'; -import {isStringNonEmpty, isThing, validateReference} from '#validators'; +import {isBoolean, isStringNonEmpty, isThing, validateReference} + from '#validators'; -import {exitWithoutDependency, exposeDependency} from '#composite/control-flow'; import {flag, simpleDate, soupyFind} from '#composite/wiki-properties'; import { + exitWithoutDependency, + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + +import { withFilteredList, withNearbyItemFromList, withPropertyFromList, @@ -19,8 +27,6 @@ import { import { inheritFromContributionPresets, - thingPropertyMatches, - thingReferenceTypeMatches, withContainingReverseContributionList, withContributionArtist, withContributionContext, @@ -70,7 +76,26 @@ export class Contribution extends Thing { property: input.thisProperty(), }), - flag(true), + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + { + dependencies: ['thing', input.myself()], + compute: (continuation, { + ['thing']: thing, + [input.myself()]: contribution, + }) => + (thing.countOwnContributionInContributionTotals?.(contribution) + ? true + : thing.countOwnContributionInContributionTotals + ? false + : continuation()), + }, + + exposeConstant({ + value: input.value(true), + }), ], countInDurationTotals: [ @@ -78,7 +103,37 @@ export class Contribution extends Thing { property: input.thisProperty(), }), - flag(true), + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withPropertyFromObject({ + object: 'thing', + property: input.value('duration'), + }), + + exitWithoutDependency({ + dependency: '#thing.duration', + mode: input.value('falsy'), + value: input.value(false), + }), + + { + dependencies: ['thing', input.myself()], + compute: (continuation, { + ['thing']: thing, + [input.myself()]: contribution, + }) => + (thing.countOwnContributionInDurationTotals?.(contribution) + ? true + : thing.countOwnContributionInDurationTotals + ? false + : continuation()), + }, + + exposeConstant({ + value: input.value(true), + }), ], // Update only @@ -87,6 +142,12 @@ export class Contribution extends Thing { // Expose only + isContribution: [ + exposeConstant({ + value: input.value(true), + }), + ], + context: [ withContributionContext(), @@ -167,38 +228,6 @@ export class Contribution extends Thing { }), ], - isArtistContribution: thingPropertyMatches({ - value: input.value('artistContribs'), - }), - - isContributorContribution: thingPropertyMatches({ - value: input.value('contributorContribs'), - }), - - isCoverArtistContribution: thingPropertyMatches({ - value: input.value('coverArtistContribs'), - }), - - isBannerArtistContribution: thingPropertyMatches({ - value: input.value('bannerArtistContribs'), - }), - - isWallpaperArtistContribution: thingPropertyMatches({ - value: input.value('wallpaperArtistContribs'), - }), - - isForTrack: thingReferenceTypeMatches({ - value: input.value('track'), - }), - - isForAlbum: thingReferenceTypeMatches({ - value: input.value('album'), - }), - - isForFlash: thingReferenceTypeMatches({ - value: input.value('flash'), - }), - previousBySameArtist: [ withContainingReverseContributionList().outputs({ '#containingReverseContributionList': '#list', @@ -238,6 +267,21 @@ export class Contribution extends Thing { dependency: '#nearbyItem', }), ], + + groups: [ + withPropertyFromObject({ + object: 'thing', + property: input.value('groups'), + }), + + exposeDependencyOrContinue({ + dependency: '#thing.groups', + }), + + exposeConstant({ + value: input.value([]), + }), + ], }); [inspect.custom](depth, options, inspect) { diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 160221f0..73b22746 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -149,6 +149,12 @@ export class Flash extends Thing { // Expose only + isFlash: [ + exposeConstant({ + value: input.value(true), + }), + ], + commentatorArtists: commentatorArtists(), act: [ @@ -317,6 +323,12 @@ export class FlashAct extends Thing { // Expose only + isFlashAct: [ + exposeConstant({ + value: input.value(true), + }), + ], + side: [ withFlashSide(), exposeDependency({dependency: '#flashSide'}), @@ -372,6 +384,14 @@ export class FlashSide extends Thing { // Update only find: soupyFind(), + + // Expose only + + isFlashSide: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { diff --git a/src/data/things/group.js b/src/data/things/group.js index 0262a3a5..0935dc93 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -5,17 +5,28 @@ import {inspect} from 'node:util'; import {colors} from '#cli'; import {input} from '#composite'; import Thing from '#thing'; -import {is} from '#validators'; +import {is, isBoolean} from '#validators'; import {parseAnnotatedReferences, parseSerieses} from '#yaml'; +import {withPropertyFromObject} from '#composite/data'; +import {withUniqueReferencingThing} from '#composite/wiki-data'; + +import { + exposeConstant, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + import { annotatedReferenceList, color, contentString, directory, + flag, name, referenceList, soupyFind, + soupyReverse, thing, thingList, urls, @@ -30,6 +41,33 @@ export class Group extends Thing { name: name('Unnamed Group'), directory: directory(), + excludeFromGalleryTabs: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withUniqueReferencingThing({ + reverse: soupyReverse.input('groupCategoriesWhichInclude'), + }).outputs({ + '#uniqueReferencingThing': '#category', + }), + + withPropertyFromObject({ + object: '#category', + property: input.value('excludeGroupsFromGalleryTabs'), + }), + + exposeDependencyOrContinue({ + dependency: '#category.excludeGroupsFromGalleryTabs', + }), + + exposeConstant({ + value: input.value(false), + }), + ], + + divideAlbumsByStyle: flag(false), + description: contentString(), urls: urls(), @@ -54,10 +92,16 @@ export class Group extends Thing { // Update only find: soupyFind(), - reverse: soupyFind(), + reverse: soupyReverse(), // Expose only + isGroup: [ + exposeConstant({ + value: input.value(true), + }), + ], + descriptionShort: { flags: {expose: true}, @@ -133,6 +177,10 @@ export class Group extends Thing { fields: { 'Group': {property: 'name'}, 'Directory': {property: 'directory'}, + + 'Exclude From Gallery Tabs': {property: 'excludeFromGalleryTabs'}, + 'Divide Albums By Style': {property: 'divideAlbumsByStyle'}, + 'Description': {property: 'description'}, 'URLs': {property: 'urls'}, @@ -217,6 +265,8 @@ export class GroupCategory extends Thing { name: name('Unnamed Group Category'), directory: directory(), + excludeGroupsFromGalleryTabs: flag(false), + color: color(), groups: referenceList({ @@ -227,6 +277,14 @@ export class GroupCategory extends Thing { // Update only find: soupyFind(), + + // Expose only + + isGroupCategory: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.reverseSpecs] = { @@ -241,7 +299,12 @@ export class GroupCategory extends Thing { static [Thing.yamlDocumentSpec] = { fields: { 'Category': {property: 'name'}, + 'Color': {property: 'color'}, + + 'Exclude Groups From Gallery Tabs': { + property: 'excludeGroupsFromGalleryTabs', + }, }, }; } diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 3a11c287..2456ca95 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -17,7 +17,7 @@ import { validateReference, } from '#validators'; -import {exposeDependency} from '#composite/control-flow'; +import {exposeConstant, exposeDependency} from '#composite/control-flow'; import {withResolvedReference} from '#composite/wiki-data'; import { @@ -47,6 +47,14 @@ export class HomepageLayout extends Thing { sections: thingList({ class: input.value(HomepageLayoutSection), }), + + // Expose only + + isHomepageLayout: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -156,6 +164,14 @@ export class HomepageLayoutSection extends Thing { rows: thingList({ class: input.value(HomepageLayoutRow), }), + + // Expose only + + isHomepageLayoutSection: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -182,6 +198,12 @@ export class HomepageLayoutRow extends Thing { // Expose only + isHomepageLayoutRow: [ + exposeConstant({ + value: input.value(true), + }), + ], + type: { flags: {expose: true}, @@ -233,6 +255,12 @@ export class HomepageLayoutActionsRow extends HomepageLayoutRow { // Expose only + isHomepageLayoutActionsRow: [ + exposeConstant({ + value: input.value(true), + }), + ], + type: { flags: {expose: true}, expose: {compute: () => 'actions'}, @@ -261,6 +289,12 @@ export class HomepageLayoutAlbumCarouselRow extends HomepageLayoutRow { // Expose only + isHomepageLayoutAlbumCarouselRow: [ + exposeConstant({ + value: input.value(true), + }), + ], + type: { flags: {expose: true}, expose: {compute: () => 'album carousel'}, @@ -321,6 +355,12 @@ export class HomepageLayoutAlbumGridRow extends HomepageLayoutRow { // Expose only + isHomepageLayoutAlbumGridRow: [ + exposeConstant({ + value: input.value(true), + }), + ], + type: { flags: {expose: true}, expose: {compute: () => 'album grid'}, diff --git a/src/data/things/language.js b/src/data/things/language.js index b0124c10..88e8d996 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,8 +1,9 @@ -import { Temporal, toTemporalInstant } from '@js-temporal/polyfill'; +import {Temporal, toTemporalInstant} from '@js-temporal/polyfill'; import {withAggregate} from '#aggregate'; import CacheableObject from '#cacheable-object'; import {logWarn} from '#cli'; +import {input} from '#composite'; import * as html from '#html'; import {empty} from '#sugar'; import {isLanguageCode} from '#validators'; @@ -16,6 +17,7 @@ import { isExternalLinkStyle, } from '#external-links'; +import {exposeConstant} from '#composite/control-flow'; import {externalFunction, flag, name} from '#composite/wiki-properties'; export const languageOptionRegex = /{(?<name>[A-Z0-9_]+)}/g; @@ -127,6 +129,12 @@ export class Language extends Thing { // Expose only + isLanguage: [ + exposeConstant({ + value: input.value(true), + }), + ], + onlyIfOptions: { flags: {expose: true}, expose: { @@ -204,6 +212,10 @@ export class Language extends Thing { } formatString(...args) { + if (typeof args.at(-1) === 'function') { + throw new Error(`Passed function - did you mean language.encapsulate() instead?`); + } + const hasOptions = typeof args.at(-1) === 'object' && args.at(-1) !== null; @@ -310,7 +322,7 @@ export class Language extends Thing { return undefined; } - return optionValue; + return this.sanitize(optionValue); }, }); @@ -375,26 +387,16 @@ export class Language extends Thing { partInProgress += template.slice(lastIndex, match.index); - // Sanitize string arguments in particular. These are taken to come from - // (raw) data and may include special characters that aren't meant to be - // rendered as HTML markup. - const sanitizedInsertion = - this.#sanitizeValueForInsertion(insertion); - - if (typeof sanitizedInsertion === 'string') { - // Join consecutive strings together. - partInProgress += sanitizedInsertion; - } else if ( - sanitizedInsertion instanceof html.Tag && - sanitizedInsertion.contentOnly - ) { - // Collapse string-only tag contents onto the current string part. - partInProgress += sanitizedInsertion.toString(); - } else { - // Push the string part in progress, then the insertion as-is. - outputParts.push(partInProgress); - outputParts.push(sanitizedInsertion); - partInProgress = ''; + for (const insertionItem of html.smush(insertion).content) { + if (typeof insertionItem === 'string') { + // Join consecutive strings together. + partInProgress += insertionItem; + } else { + // Push the string part in progress, then the insertion as-is. + outputParts.push(partInProgress); + outputParts.push(insertionItem); + partInProgress = ''; + } } lastIndex = match.index + match[0].length; @@ -867,14 +869,14 @@ export class Language extends Thing { typicallyLowerCase(string) { // Utter nonsense implementation, so this only works on strings, - // not actual HTML content, and will loudly disrespect *intentful* + // not actual HTML content, and may rudely disrespect *intentful* // capitalization of whatever goes into it. - if (typeof string === 'string') { - return string[0].toLowerCase() + string.slice(1).toLowerCase(); - } else { - return string; - } + if (typeof string !== 'string') return string; + if (string.length <= 1) return string; + if (/^\S+?[A-Z]/.test(string)) return string; + + return string[0].toLowerCase() + string.slice(1); } // Utility function to quickly provide a useful string key diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index 43d1638e..28289f53 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,9 +1,11 @@ export const NEWS_DATA_FILE = 'news.yaml'; +import {input} from '#composite'; import {sortChronologically} from '#sort'; import Thing from '#thing'; import {parseDate} from '#yaml'; +import {exposeConstant} from '#composite/control-flow'; import {contentString, directory, name, simpleDate} from '#composite/wiki-properties'; @@ -22,6 +24,12 @@ export class NewsEntry extends Thing { // Expose only + isNewsEntry: [ + exposeConstant({ + value: input.value(true), + }), + ], + contentShort: { flags: {expose: true}, diff --git a/src/data/things/sorting-rule.js b/src/data/things/sorting-rule.js index ccc4ad89..808a0085 100644 --- a/src/data/things/sorting-rule.js +++ b/src/data/things/sorting-rule.js @@ -3,6 +3,7 @@ export const SORTING_RULE_DATA_FILE = 'sorting-rules.yaml'; import {readFile, writeFile} from 'node:fs/promises'; import * as path from 'node:path'; +import {input} from '#composite'; import {chunkByProperties, compareArrays, unique} from '#sugar'; import Thing from '#thing'; import {isObject, isStringNonEmpty, anyOf, strictArrayOf} from '#validators'; @@ -21,6 +22,7 @@ import { reorderDocumentsInYAMLSourceText, } from '#yaml'; +import {exposeConstant} from '#composite/control-flow'; import {flag} from '#composite/wiki-properties'; function isSelectFollowingEntry(value) { @@ -46,6 +48,14 @@ export class SortingRule extends Thing { flags: {update: true, expose: true}, update: {validate: isStringNonEmpty}, }, + + // Expose only + + isSortingRule: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { @@ -118,6 +128,14 @@ export class ThingSortingRule extends SortingRule { validate: strictArrayOf(isStringNonEmpty), }, }, + + // Expose only + + isThingSortingRule: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(SortingRule, { @@ -217,6 +235,14 @@ export class DocumentSortingRule extends ThingSortingRule { flags: {update: true, expose: true}, update: {validate: isStringNonEmpty}, }, + + // Expose only + + isDocumentSortingRule: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(ThingSortingRule, { diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index 52a09c31..28167df2 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -2,11 +2,13 @@ export const DATA_STATIC_PAGE_DIRECTORY = 'static-page'; import * as path from 'node:path'; +import {input} from '#composite'; import {traverse} from '#node-utils'; import {sortAlphabetically} from '#sort'; import Thing from '#thing'; import {isName} from '#validators'; +import {exposeConstant} from '#composite/control-flow'; import {contentString, directory, flag, name, simpleString} from '#composite/wiki-properties'; @@ -36,6 +38,14 @@ export class StaticPage extends Thing { content: contentString(), absoluteLinks: flag(), + + // Expose only + + isStaticPage: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.findSpecs] = { diff --git a/src/data/things/track.js b/src/data/things/track.js index 8419f8ba..18faebc3 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -25,6 +25,7 @@ import { import {withPropertyFromObject} from '#composite/data'; import { + exitWithoutDependency, exposeConstant, exposeDependency, exposeDependencyOrContinue, @@ -95,7 +96,13 @@ export class Track extends Thing { ReferencingSourcesEntry, WikiInfo, }) => ({ - // Update & expose + // > Update & expose - Internal relationships + + album: thing({ + class: input.value(Album), + }), + + // > Update & expose - Identifying metadata name: name('Unnamed Track'), @@ -129,20 +136,93 @@ export class Track extends Thing { }) ], - album: thing({ - class: input.value(Album), + alwaysReferenceByDirectory: [ + withAlwaysReferenceByDirectory(), + exposeDependency({dependency: '#alwaysReferenceByDirectory'}), + ], + + mainReleaseTrack: singleReference({ + class: input.value(Track), + find: soupyFind.input('track'), }), + bandcampTrackIdentifier: simpleString(), + bandcampArtworkIdentifier: simpleString(), + additionalNames: thingList({ class: input.value(AdditionalName), }), - bandcampTrackIdentifier: simpleString(), - bandcampArtworkIdentifier: simpleString(), + dateFirstReleased: simpleDate(), + + // > Update & expose - Credits and contributors + + artistContribs: [ + inheritContributionListFromMainRelease(), + + withDate(), + + withResolvedContribs({ + from: input.updateValue({validate: isContributionList}), + thingProperty: input.thisProperty(), + artistProperty: input.value('trackArtistContributions'), + date: '#date', + }).outputs({ + '#resolvedContribs': '#artistContribs', + }), + + exposeDependencyOrContinue({ + dependency: '#artistContribs', + mode: input.value('empty'), + }), + + withPropertyFromAlbum({ + property: input.value('trackArtistContribs'), + }), + + withRecontextualizedContributionList({ + list: '#album.trackArtistContribs', + artistProperty: input.value('trackArtistContributions'), + }), + + withRedatedContributionList({ + list: '#album.trackArtistContribs', + date: '#date', + }), + + exposeDependency({dependency: '#album.trackArtistContribs'}), + ], + + contributorContribs: [ + inheritContributionListFromMainRelease(), + + withDate(), + + contributionList({ + date: '#date', + artistProperty: input.value('trackContributorContributions'), + }), + ], + + // > Update & expose - General configuration + + countInArtistTotals: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withPropertyFromAlbum({ + property: input.value('countTracksInArtistTotals'), + }), + + exposeDependency({dependency: '#album.countTracksInArtistTotals'}), + ], + + disableUniqueCoverArt: flag(), + + // > Update & expose - General metadata duration: duration(), - urls: urls(), - dateFirstReleased: simpleDate(), color: [ exposeUpdateValueOrContinue({ @@ -165,37 +245,27 @@ export class Track extends Thing { exposeDependency({dependency: '#album.color'}), ], - alwaysReferenceByDirectory: [ - withAlwaysReferenceByDirectory(), - exposeDependency({dependency: '#alwaysReferenceByDirectory'}), - ], - - // Disables presenting the track as though it has its own unique artwork. - // This flag should only be used in select circumstances, i.e. to override - // an album's trackCoverArtists. This flag supercedes that property, as well - // as the track's own coverArtists. - disableUniqueCoverArt: flag(), - - // File extension for track's corresponding media file. This represents the - // track's unique cover artwork, if any, and does not inherit the extension - // of the album's main artwork. It does inherit trackCoverArtFileExtension, - // if present on the album. - coverArtFileExtension: [ - exitWithoutUniqueCoverArt(), + urls: urls(), - exposeUpdateValueOrContinue({ - validate: input.value(isFileExtension), - }), + // > Update & expose - Artworks - withPropertyFromAlbum({ - property: input.value('trackCoverArtFileExtension'), + trackArtworks: [ + exitWithoutUniqueCoverArt({ + value: input.value([]), }), - exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), + constitutibleArtworkList.fromYAMLFieldSpec + .call(this, 'Track Artwork'), + ], - exposeConstant({ - value: input.value('jpg'), + coverArtistContribs: [ + withCoverArtistContribs({ + from: input.updateValue({ + validate: isContributionList, + }), }), + + exposeDependency({dependency: '#coverArtistContribs'}), ], coverArtDate: [ @@ -208,117 +278,59 @@ export class Track extends Thing { exposeDependency({dependency: '#trackArtDate'}), ], - coverArtDimensions: [ + coverArtFileExtension: [ exitWithoutUniqueCoverArt(), - exposeUpdateValueOrContinue(), + exposeUpdateValueOrContinue({ + validate: input.value(isFileExtension), + }), withPropertyFromAlbum({ - property: input.value('trackDimensions'), + property: input.value('trackCoverArtFileExtension'), }), - exposeDependencyOrContinue({dependency: '#album.trackDimensions'}), - - dimensions(), - ], - - commentary: thingList({ - class: input.value(CommentaryEntry), - }), - - creditingSources: thingList({ - class: input.value(CreditingSourcesEntry), - }), - - referencingSources: thingList({ - class: input.value(ReferencingSourcesEntry), - }), - - lyrics: [ - // TODO: Inherited lyrics are literally the same objects, so of course - // their .thing properties aren't going to point back to this one, and - // certainly couldn't be recontextualized... - inheritFromMainRelease(), + exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - thingList({ - class: input.value(LyricsEntry), + exposeConstant({ + value: input.value('jpg'), }), ], - additionalFiles: thingList({ - class: input.value(AdditionalFile), - }), - - sheetMusicFiles: thingList({ - class: input.value(AdditionalFile), - }), - - midiProjectFiles: thingList({ - class: input.value(AdditionalFile), - }), - - mainReleaseTrack: singleReference({ - class: input.value(Track), - find: soupyFind.input('track'), - }), - - artistContribs: [ - inheritContributionListFromMainRelease(), - - withDate(), - - withResolvedContribs({ - from: input.updateValue({validate: isContributionList}), - thingProperty: input.thisProperty(), - artistProperty: input.value('trackArtistContributions'), - date: '#date', - }).outputs({ - '#resolvedContribs': '#artistContribs', - }), + coverArtDimensions: [ + exitWithoutUniqueCoverArt(), - exposeDependencyOrContinue({ - dependency: '#artistContribs', - mode: input.value('empty'), - }), + exposeUpdateValueOrContinue(), withPropertyFromAlbum({ - property: input.value('artistContribs'), - }), - - withRecontextualizedContributionList({ - list: '#album.artistContribs', - artistProperty: input.value('trackArtistContributions'), + property: input.value('trackDimensions'), }), - withRedatedContributionList({ - list: '#album.artistContribs', - date: '#date', - }), + exposeDependencyOrContinue({dependency: '#album.trackDimensions'}), - exposeDependency({dependency: '#album.artistContribs'}), + dimensions(), ], - contributorContribs: [ - inheritContributionListFromMainRelease(), - - withDate(), + artTags: [ + exitWithoutUniqueCoverArt({ + value: input.value([]), + }), - contributionList({ - date: '#date', - artistProperty: input.value('trackContributorContributions'), + referenceList({ + class: input.value(ArtTag), + find: soupyFind.input('artTag'), }), ], - coverArtistContribs: [ - withCoverArtistContribs({ - from: input.updateValue({ - validate: isContributionList, - }), + referencedArtworks: [ + exitWithoutUniqueCoverArt({ + value: input.value([]), }), - exposeDependency({dependency: '#coverArtistContribs'}), + referencedArtworkList(), ], + // > Update & expose - Referenced tracks + referencedTracks: [ inheritFromMainRelease({ notFoundValue: input.value([]), @@ -341,35 +353,46 @@ export class Track extends Thing { }), ], - trackArtworks: [ - exitWithoutUniqueCoverArt({ - value: input.value([]), - }), + // > Update & expose - Additional files - constitutibleArtworkList.fromYAMLFieldSpec - .call(this, 'Track Artwork'), - ], + additionalFiles: thingList({ + class: input.value(AdditionalFile), + }), - artTags: [ - exitWithoutUniqueCoverArt({ - value: input.value([]), - }), + sheetMusicFiles: thingList({ + class: input.value(AdditionalFile), + }), - referenceList({ - class: input.value(ArtTag), - find: soupyFind.input('artTag'), + midiProjectFiles: thingList({ + class: input.value(AdditionalFile), + }), + + // > Update & expose - Content entries + + lyrics: [ + // TODO: Inherited lyrics are literally the same objects, so of course + // their .thing properties aren't going to point back to this one, and + // certainly couldn't be recontextualized... + inheritFromMainRelease(), + + thingList({ + class: input.value(LyricsEntry), }), ], - referencedArtworks: [ - exitWithoutUniqueCoverArt({ - value: input.value([]), - }), + commentary: thingList({ + class: input.value(CommentaryEntry), + }), - referencedArtworkList(), - ], + creditingSources: thingList({ + class: input.value(CreditingSourcesEntry), + }), + + referencingSources: thingList({ + class: input.value(ReferencingSourcesEntry), + }), - // Update only + // > Update only find: soupyFind(), reverse: soupyReverse(), @@ -389,7 +412,13 @@ export class Track extends Thing { class: input.value(WikiInfo), }), - // Expose only + // > Expose only + + isTrack: [ + exposeConstant({ + value: input.value(true), + }), + ], commentatorArtists: commentatorArtists(), @@ -441,6 +470,34 @@ export class Track extends Thing { exposeDependency({dependency: '#otherReleases'}), ], + commentaryFromMainRelease: [ + withMainRelease(), + + exitWithoutDependency({ + dependency: '#mainRelease', + value: input.value([]), + }), + + withPropertyFromObject({ + object: '#mainRelease', + property: input.value('commentary'), + }), + + exposeDependency({ + dependency: '#mainRelease.commentary', + }), + ], + + groups: [ + withPropertyFromAlbum({ + property: input.value('groups'), + }), + + exposeDependency({ + dependency: '#album.groups', + }), + ], + referencedByTracks: reverseReferenceList({ reverse: soupyReverse.input('tracksWhichReference'), }), @@ -456,14 +513,13 @@ export class Track extends Thing { static [Thing.yamlDocumentSpec] = { fields: { + // Identifying metadata + 'Track': {property: 'name'}, 'Directory': {property: 'directory'}, 'Suffix Directory': {property: 'suffixDirectoryFromAlbum'}, - - 'Additional Names': { - property: 'additionalNames', - transform: parseAdditionalNames, - }, + 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, + 'Main Release': {property: 'mainReleaseTrack'}, 'Bandcamp Track ID': { property: 'bandcampTrackIdentifier', @@ -475,31 +531,32 @@ export class Track extends Thing { transform: String, }, - 'Duration': { - property: 'duration', - transform: parseDuration, + 'Additional Names': { + property: 'additionalNames', + transform: parseAdditionalNames, }, - 'Color': {property: 'color'}, - 'URLs': {property: 'urls'}, - 'Date First Released': { property: 'dateFirstReleased', transform: parseDate, }, - 'Cover Art Date': { - property: 'coverArtDate', - transform: parseDate, - }, + // Credits and contributors - 'Cover Art File Extension': {property: 'coverArtFileExtension'}, + 'Artists': { + property: 'artistContribs', + transform: parseContributors, + }, - 'Cover Art Dimensions': { - property: 'coverArtDimensions', - transform: parseDimensions, + 'Contributors': { + property: 'contributorContribs', + transform: parseContributors, }, + // General configuration + + 'Count In Artist Totals': {property: 'countInArtistTotals'}, + 'Has Cover Art': { property: 'disableUniqueCoverArt', transform: value => @@ -508,28 +565,65 @@ export class Track extends Thing { : value), }, - 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, + // General metadata - 'Lyrics': { - property: 'lyrics', - transform: parseLyrics, + 'Duration': { + property: 'duration', + transform: parseDuration, }, - 'Commentary': { - property: 'commentary', - transform: parseCommentary, + 'Color': {property: 'color'}, + + 'URLs': {property: 'urls'}, + + // Artworks + + 'Track Artwork': { + property: 'trackArtworks', + transform: + parseArtwork({ + thingProperty: 'trackArtworks', + dimensionsFromThingProperty: 'coverArtDimensions', + fileExtensionFromThingProperty: 'coverArtFileExtension', + dateFromThingProperty: 'coverArtDate', + artTagsFromThingProperty: 'artTags', + referencedArtworksFromThingProperty: 'referencedArtworks', + artistContribsFromThingProperty: 'coverArtistContribs', + artistContribsArtistProperty: 'trackCoverArtistContributions', + }), }, - 'Crediting Sources': { - property: 'creditingSources', - transform: parseCreditingSources, + 'Cover Artists': { + property: 'coverArtistContribs', + transform: parseContributors, }, - 'Referencing Sources': { - property: 'referencingSources', - transform: parseReferencingSources, + 'Cover Art Date': { + property: 'coverArtDate', + transform: parseDate, + }, + + 'Cover Art File Extension': {property: 'coverArtFileExtension'}, + + 'Cover Art Dimensions': { + property: 'coverArtDimensions', + transform: parseDimensions, + }, + + 'Art Tags': {property: 'artTags'}, + + 'Referenced Artworks': { + property: 'referencedArtworks', + transform: parseAnnotatedReferences, }, + // Referenced tracks + + 'Referenced Tracks': {property: 'referencedTracks'}, + 'Sampled Tracks': {property: 'sampledTracks'}, + + // Additional files + 'Additional Files': { property: 'additionalFiles', transform: parseAdditionalFiles, @@ -545,54 +639,41 @@ export class Track extends Thing { transform: parseAdditionalFiles, }, - 'Main Release': {property: 'mainReleaseTrack'}, - 'Referenced Tracks': {property: 'referencedTracks'}, - 'Sampled Tracks': {property: 'sampledTracks'}, - - 'Referenced Artworks': { - property: 'referencedArtworks', - transform: parseAnnotatedReferences, - }, + // Content entries - 'Franchises': {ignore: true}, - 'Inherit Franchises': {ignore: true}, - - 'Artists': { - property: 'artistContribs', - transform: parseContributors, + 'Lyrics': { + property: 'lyrics', + transform: parseLyrics, }, - 'Contributors': { - property: 'contributorContribs', - transform: parseContributors, + 'Commentary': { + property: 'commentary', + transform: parseCommentary, }, - 'Cover Artists': { - property: 'coverArtistContribs', - transform: parseContributors, + 'Crediting Sources': { + property: 'creditingSources', + transform: parseCreditingSources, }, - 'Track Artwork': { - property: 'trackArtworks', - transform: - parseArtwork({ - thingProperty: 'trackArtworks', - dimensionsFromThingProperty: 'coverArtDimensions', - fileExtensionFromThingProperty: 'coverArtFileExtension', - dateFromThingProperty: 'coverArtDate', - artTagsFromThingProperty: 'artTags', - referencedArtworksFromThingProperty: 'referencedArtworks', - artistContribsFromThingProperty: 'coverArtistContribs', - artistContribsArtistProperty: 'trackCoverArtistContributions', - }), + 'Referencing Sources': { + property: 'referencingSources', + transform: parseReferencingSources, }, - 'Art Tags': {property: 'artTags'}, + // Shenanigans + 'Franchises': {ignore: true}, + 'Inherit Franchises': {ignore: true}, 'Review Points': {ignore: true}, }, invalidFieldCombinations: [ + {message: `Secondary releases never count in artist totals`, fields: [ + 'Main Release', + 'Count In Artist Totals', + ]}, + {message: `Secondary releases inherit references from the main one`, fields: [ 'Main Release', 'Referenced Tracks', @@ -779,6 +860,30 @@ export class Track extends Thing { ]; } + countOwnContributionInContributionTotals(_contrib) { + if (!this.countInArtistTotals) { + return false; + } + + if (this.isSecondaryRelease) { + return false; + } + + return true; + } + + countOwnContributionInDurationTotals(_contrib) { + if (!this.countInArtistTotals) { + return false; + } + + if (this.isSecondaryRelease) { + return false; + } + + return true; + } + [inspect.custom](depth) { const parts = []; diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index f97f9027..b6057735 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -13,7 +13,7 @@ import { isURL, } from '#validators'; -import {exitWithoutDependency} from '#composite/control-flow'; +import {exitWithoutDependency, exposeConstant} from '#composite/control-flow'; import { contentString, @@ -119,6 +119,14 @@ export class WikiInfo extends Thing { default: false, }, }, + + // Expose only + + isWikiInfo: [ + exposeConstant({ + value: input.value(true), + }), + ], }); static [Thing.yamlDocumentSpec] = { |