diff options
Diffstat (limited to 'src/data/things')
-rw-r--r-- | src/data/things/album.js | 78 | ||||
-rw-r--r-- | src/data/things/artist.js | 125 | ||||
-rw-r--r-- | src/data/things/contribution.js | 265 | ||||
-rw-r--r-- | src/data/things/flash.js | 27 | ||||
-rw-r--r-- | src/data/things/index.js | 2 | ||||
-rw-r--r-- | src/data/things/language.js | 237 | ||||
-rw-r--r-- | src/data/things/track.js | 123 | ||||
-rw-r--r-- | src/data/things/wiki-info.js | 42 |
8 files changed, 704 insertions, 195 deletions
diff --git a/src/data/things/album.js b/src/data/things/album.js index e9f55b2c..a0021946 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -18,8 +18,13 @@ import {parseAdditionalFiles, parseContributors, parseDate, parseDimensions} import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; -import {exitWithoutContribs, withDirectory, withResolvedReference} - from '#composite/wiki-data'; + +import { + exitWithoutContribs, + withDirectory, + withResolvedReference, + withCoverArtDate, +} from '#composite/wiki-data'; import { additionalFiles, @@ -37,6 +42,7 @@ import { simpleDate, simpleString, singleReference, + thing, urls, wikiData, } from '#composite/wiki-properties'; @@ -53,6 +59,7 @@ export class Album extends Thing { Group, Track, TrackSection, + WikiInfo, }) => ({ // Update & expose @@ -71,13 +78,16 @@ export class Album extends Thing { dateAddedToWiki: simpleDate(), coverArtDate: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), + // TODO: Why does this fall back, but Track.coverArtDate doesn't? + withCoverArtDate({ + from: input.updateValue({ + validate: isDate, + }), - exposeUpdateValueOrContinue({ - validate: input.value(isDate), + fallback: input.value(true), }), - exposeDependency({dependency: 'date'}), + exposeDependency({dependency: '#coverArtDate'}), ], coverArtFileExtension: [ @@ -130,11 +140,53 @@ export class Album extends Thing { find: input.value(find.unqualifiedTrackSection), }), - artistContribs: contributionList(), - coverArtistContribs: contributionList(), - trackCoverArtistContribs: contributionList(), - wallpaperArtistContribs: contributionList(), - bannerArtistContribs: contributionList(), + artistContribs: contributionList({ + date: 'date', + artistProperty: input.value('albumArtistContributions'), + }), + + coverArtistContribs: [ + withCoverArtDate({ + fallback: input.value(true), + }), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumCoverArtistContributions'), + }), + ], + + 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'), + }), + + wallpaperArtistContribs: [ + withCoverArtDate({ + fallback: input.value(true), + }), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumWallpaperArtistContributions'), + }), + ], + + bannerArtistContribs: [ + withCoverArtDate({ + fallback: input.value(true), + }), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumBannerArtistContributions'), + }), + ], groups: referenceList({ class: input.value(Group), @@ -173,6 +225,10 @@ export class Album extends Thing { class: input.value(TrackSection), }), + wikiInfo: thing({ + class: input.value(WikiInfo), + }), + // Expose only commentatorArtists: commentatorArtists(), diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 841d652f..6d5e33c0 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -12,6 +12,7 @@ import Thing from '#thing'; import {isName, validateArrayItems} from '#validators'; import {getKebabCase} from '#wiki-data'; +import {exposeDependency} from '#composite/control-flow'; import {withReverseContributionList} from '#composite/wiki-data'; import { @@ -27,6 +28,8 @@ import { wikiData, } from '#composite/wiki-properties'; +import {artistTotalDuration} from '#composite/things/artist'; + export class Artist extends Thing { static [Thing.referenceType] = 'artist'; static [Thing.wikiDataArray] = 'artistData'; @@ -77,146 +80,52 @@ export class Artist extends Thing { // Expose only - tracksAsArtist: reverseContributionList({ + trackArtistContributions: reverseContributionList({ data: 'trackData', list: input.value('artistContribs'), }), - tracksAsContributor: reverseContributionList({ + trackContributorContributions: reverseContributionList({ data: 'trackData', list: input.value('contributorContribs'), }), - tracksAsCoverArtist: reverseContributionList({ + trackCoverArtistContributions: reverseContributionList({ data: 'trackData', list: input.value('coverArtistContribs'), }), - tracksAsAny: [ - withReverseContributionList({ - data: 'trackData', - list: input.value('artistContribs'), - }).outputs({ - '#reverseContributionList': '#tracksAsArtist', - }), - - withReverseContributionList({ - data: 'trackData', - list: input.value('contributorContribs'), - }).outputs({ - '#reverseContributionList': '#tracksAsContributor', - }), - - withReverseContributionList({ - data: 'trackData', - list: input.value('coverArtistContribs'), - }).outputs({ - '#reverseContributionList': '#tracksAsCoverArtist', - }), - - { - dependencies: [ - '#tracksAsArtist', - '#tracksAsContributor', - '#tracksAsCoverArtist', - ], - - compute: ({ - ['#tracksAsArtist']: tracksAsArtist, - ['#tracksAsContributor']: tracksAsContributor, - ['#tracksAsCoverArtist']: tracksAsCoverArtist, - }) => - unique([ - ...tracksAsArtist, - ...tracksAsContributor, - ...tracksAsCoverArtist, - ]), - }, - ], - tracksAsCommentator: reverseReferenceList({ data: 'trackData', list: input.value('commentatorArtists'), }), - albumsAsAlbumArtist: reverseContributionList({ + albumArtistContributions: reverseContributionList({ data: 'albumData', list: input.value('artistContribs'), }), - albumsAsCoverArtist: reverseContributionList({ + albumCoverArtistContributions: reverseContributionList({ data: 'albumData', list: input.value('coverArtistContribs'), }), - albumsAsWallpaperArtist: reverseContributionList({ + albumWallpaperArtistContributions: reverseContributionList({ data: 'albumData', list: input.value('wallpaperArtistContribs'), }), - albumsAsBannerArtist: reverseContributionList({ + albumBannerArtistContributions: reverseContributionList({ data: 'albumData', list: input.value('bannerArtistContribs'), }), - albumsAsAny: [ - withReverseContributionList({ - data: 'albumData', - list: input.value('artistContribs'), - }).outputs({ - '#reverseContributionList': '#albumsAsArtist', - }), - - withReverseContributionList({ - data: 'albumData', - list: input.value('coverArtistContribs'), - }).outputs({ - '#reverseContributionList': '#albumsAsCoverArtist', - }), - - withReverseContributionList({ - data: 'albumData', - list: input.value('wallpaperArtistContribs'), - }).outputs({ - '#reverseContributionList': '#albumsAsWallpaperArtist', - }), - - withReverseContributionList({ - data: 'albumData', - list: input.value('bannerArtistContribs'), - }).outputs({ - '#reverseContributionList': '#albumsAsBannerArtist', - }), - - { - dependencies: [ - '#albumsAsArtist', - '#albumsAsCoverArtist', - '#albumsAsWallpaperArtist', - '#albumsAsBannerArtist', - ], - - compute: ({ - ['#albumsAsArtist']: albumsAsArtist, - ['#albumsAsCoverArtist']: albumsAsCoverArtist, - ['#albumsAsWallpaperArtist']: albumsAsWallpaperArtist, - ['#albumsAsBannerArtist']: albumsAsBannerArtist, - }) => - unique([ - ...albumsAsArtist, - ...albumsAsCoverArtist, - ...albumsAsWallpaperArtist, - ...albumsAsBannerArtist, - ]), - }, - ], - albumsAsCommentator: reverseReferenceList({ data: 'albumData', list: input.value('commentatorArtists'), }), - flashesAsContributor: reverseContributionList({ + flashContributorContributions: reverseContributionList({ data: 'flashData', list: input.value('contributorContribs'), }), @@ -225,6 +134,8 @@ export class Artist extends Thing { data: 'flashData', list: input.value('commentatorArtists'), }), + + totalDuration: artistTotalDuration(), }); static [Thing.getSerializeDescriptors] = ({ @@ -240,18 +151,8 @@ export class Artist extends Thing { aliasNames: S.id, - tracksAsArtist: S.toRefs, - tracksAsContributor: S.toRefs, - tracksAsCoverArtist: S.toRefs, tracksAsCommentator: S.toRefs, - - albumsAsAlbumArtist: S.toRefs, - albumsAsCoverArtist: S.toRefs, - albumsAsWallpaperArtist: S.toRefs, - albumsAsBannerArtist: S.toRefs, albumsAsCommentator: S.toRefs, - - flashesAsContributor: S.toRefs, }); static [Thing.findSpecs] = { diff --git a/src/data/things/contribution.js b/src/data/things/contribution.js new file mode 100644 index 00000000..79acf1e1 --- /dev/null +++ b/src/data/things/contribution.js @@ -0,0 +1,265 @@ +import {inspect} from 'node:util'; + +import CacheableObject from '#cacheable-object'; +import {colors} from '#cli'; +import {input} from '#composite'; +import {empty} from '#sugar'; +import Thing from '#thing'; +import {isStringNonEmpty, isThing, validateReference} from '#validators'; + +import {exitWithoutDependency, exposeDependency} from '#composite/control-flow'; +import {withNearbyItemFromList, withPropertyFromObject} from '#composite/data'; +import {withResolvedReference} from '#composite/wiki-data'; +import {flag, simpleDate} from '#composite/wiki-properties'; + +import { + inheritFromContributionPresets, + thingPropertyMatches, + thingReferenceTypeMatches, + withContainingReverseContributionList, + withContributionArtist, + withContributionContext, + withMatchingContributionPresets, +} from '#composite/things/contribution'; + +export class Contribution extends Thing { + static [Thing.getPropertyDescriptors] = () => ({ + // Update & expose + + thing: { + flags: {update: true, expose: true}, + update: {validate: isThing}, + }, + + thingProperty: { + flags: {update: true, expose: true}, + update: {validate: isStringNonEmpty}, + }, + + artistProperty: { + flags: {update: true, expose: true}, + update: {validate: isStringNonEmpty}, + }, + + date: simpleDate(), + + artist: [ + withContributionArtist({ + ref: input.updateValue({ + validate: validateReference('artist'), + }), + }), + + exposeDependency({ + dependency: '#artist', + }), + ], + + annotation: { + flags: {update: true, expose: true}, + update: {validate: isStringNonEmpty}, + }, + + countInContributionTotals: [ + inheritFromContributionPresets({ + property: input.thisProperty(), + }), + + flag(true), + ], + + countInDurationTotals: [ + inheritFromContributionPresets({ + property: input.thisProperty(), + }), + + flag(true), + ], + + // Expose only + + context: [ + withContributionContext(), + + { + dependencies: [ + '#contributionTarget', + '#contributionProperty', + ], + + compute: ({ + ['#contributionTarget']: target, + ['#contributionProperty']: property, + }) => ({ + target, + property, + }), + }, + ], + + matchingPresets: [ + withMatchingContributionPresets(), + + exposeDependency({ + dependency: '#matchingContributionPresets', + }), + ], + + // All the contributions from the list which includes this contribution. + // Note that this list contains not only other contributions by the same + // artist, but also this very contribution. It doesn't mix contributions + // exposed on different properties. + associatedContributions: [ + exitWithoutDependency({ + dependency: 'thing', + value: input.value([]), + }), + + exitWithoutDependency({ + dependency: 'thingProperty', + value: input.value([]), + }), + + withPropertyFromObject({ + object: 'thing', + property: 'thingProperty', + }), + + exposeDependency({ + dependency: '#value', + }), + ], + + 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', + }), + + exitWithoutDependency({ + dependency: '#list', + }), + + withNearbyItemFromList({ + list: '#list', + item: input.myself(), + offset: input.value(-1), + }), + + exposeDependency({ + dependency: '#nearbyItem', + }), + ], + + nextBySameArtist: [ + withContainingReverseContributionList().outputs({ + '#containingReverseContributionList': '#list', + }), + + exitWithoutDependency({ + dependency: '#list', + }), + + withNearbyItemFromList({ + list: '#list', + item: input.myself(), + offset: input.value(+1), + }), + + exposeDependency({ + dependency: '#nearbyItem', + }), + ], + }); + + [inspect.custom](depth, options, inspect) { + const parts = []; + const accentParts = []; + + parts.push(Thing.prototype[inspect.custom].apply(this)); + + if (this.annotation) { + accentParts.push(colors.green(`"${this.annotation}"`)); + } + + if (this.date) { + accentParts.push(colors.yellow(this.date.toLocaleDateString())); + } + + let artistRef; + if (depth >= 0) { + let artist; + try { + artist = this.artist; + } catch (_error) { + // Computing artist might crash for any reason - don't distract from + // other errors as a result of inspecting this contribution. + } + + if (artist) { + artistRef = + colors.blue(Thing.getReference(artist)); + } + } else { + artistRef = + colors.green(CacheableObject.getUpdateValue(this, 'artist')); + } + + if (artistRef) { + accentParts.push(`by ${artistRef}`); + } + + if (this.thing) { + if (depth >= 0) { + const newOptions = { + ...options, + depth: + (options.depth === null + ? null + : options.depth - 1), + }; + + accentParts.push(`to ${inspect(this.thing, newOptions)}`); + } else { + accentParts.push(`to ${colors.blue(Thing.getReference(this.thing))}`); + } + } + + if (!empty(accentParts)) { + parts.push(` (${accentParts.join(', ')})`); + } + + return parts.join(''); + } +} diff --git a/src/data/things/flash.js b/src/data/things/flash.js index ceed79f7..89e59fe7 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -7,7 +7,7 @@ import {sortFlashesChronologically} from '#sort'; import Thing from '#thing'; import {anyOf, isColor, isContentString, isDirectory, isNumber, isString} from '#validators'; -import {parseDate, parseContributors} from '#yaml'; +import {parseContributors, parseDate, parseDimensions} from '#yaml'; import {withPropertyFromObject} from '#composite/data'; @@ -24,11 +24,13 @@ import { commentatorArtists, contentString, contributionList, + dimensions, directory, fileExtension, name, referenceList, simpleDate, + thing, urls, wikiData, } from '#composite/wiki-properties'; @@ -39,7 +41,12 @@ import {withFlashSide} from '#composite/things/flash-act'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; - static [Thing.getPropertyDescriptors] = ({Artist, Track, FlashAct}) => ({ + static [Thing.getPropertyDescriptors] = ({ + Artist, + Track, + FlashAct, + WikiInfo, + }) => ({ // Update & expose name: name('Unnamed Flash'), @@ -89,7 +96,12 @@ export class Flash extends Thing { coverArtFileExtension: fileExtension('jpg'), - contributorContribs: contributionList(), + coverArtDimensions: dimensions(), + + contributorContribs: contributionList({ + date: 'date', + artistProperty: input.value('flashContributorContributions'), + }), featuredTracks: referenceList({ class: input.value(Track), @@ -115,6 +127,10 @@ export class Flash extends Thing { class: input.value(FlashAct), }), + wikiInfo: thing({ + class: input.value(WikiInfo), + }), + // Expose only commentatorArtists: commentatorArtists(), @@ -171,6 +187,11 @@ export class Flash extends Thing { 'Cover Art File Extension': {property: 'coverArtFileExtension'}, + 'Cover Art Dimensions': { + property: 'coverArtDimensions', + transform: parseDimensions, + }, + 'Featured Tracks': {property: 'featuredTracks'}, 'Contributors': { diff --git a/src/data/things/index.js b/src/data/things/index.js index 4f87f492..f18e283a 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -11,6 +11,7 @@ import Thing from '#thing'; import * as albumClasses from './album.js'; import * as artTagClasses from './art-tag.js'; import * as artistClasses from './artist.js'; +import * as contributionClasses from './contribution.js'; import * as flashClasses from './flash.js'; import * as groupClasses from './group.js'; import * as homepageLayoutClasses from './homepage-layout.js'; @@ -24,6 +25,7 @@ const allClassLists = { 'album.js': albumClasses, 'art-tag.js': artTagClasses, 'artist.js': artistClasses, + 'contribution.js': contributionClasses, 'flash.js': flashClasses, 'group.js': groupClasses, 'homepage-layout.js': homepageLayoutClasses, diff --git a/src/data/things/language.js b/src/data/things/language.js index dbe1ff3d..88f16ecb 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -127,6 +127,13 @@ export class Language extends Thing { // Expose only + onlyIfOptions: { + flags: {expose: true}, + expose: { + compute: () => Symbol.for(`language.onlyIfOptions`), + }, + }, + intl_date: this.#intlHelper(Intl.DateTimeFormat, {full: true}), intl_number: this.#intlHelper(Intl.NumberFormat), intl_listConjunction: this.#intlHelper(Intl.ListFormat, {type: 'conjunction'}), @@ -201,9 +208,7 @@ export class Language extends Thing { args.at(-1) !== null; const key = - (hasOptions ? args.slice(0, -1) : args) - .filter(Boolean) - .join('.'); + this.#joinKeyParts(hasOptions ? args.slice(0, -1) : args); const options = (hasOptions @@ -218,18 +223,42 @@ export class Language extends Thing { throw new Error(`Invalid key ${key} accessed`); } + const constantCasify = name => + name + .replace(/[A-Z]/g, '_$&') + .toUpperCase(); + // These will be filled up as we iterate over the template, slotting in // each option (if it's present). const missingOptionNames = new Set(); + // These will also be filled. It's a bit different of an error, indicating + // a provided option was *expected,* but its value was null, undefined, or + // blank HTML content. + const valuelessOptionNames = new Set(); + + // These *might* be missing, and if they are, that's OK!! Instead of adding + // to the valueless set above, we'll just mark to return a blank for the + // whole string. + const expectedValuelessOptionNames = + new Set( + (options[this.onlyIfOptions] ?? []) + .map(constantCasify)); + + let seenExpectedValuelessOption = false; + + const isValueless = + value => + value === null || + value === undefined || + html.isBlank(value); + // And this will have entries deleted as they're encountered in the // template. Leftover entries are misplaced. const optionsMap = new Map( Object.entries(options).map(([name, value]) => [ - name - .replace(/[A-Z]/g, '_$&') - .toUpperCase(), + constantCasify(name), value, ])); @@ -239,32 +268,48 @@ export class Language extends Thing { match: languageOptionRegex, insert: ({name: optionName}, canceledForming) => { - if (optionsMap.has(optionName)) { - let optionValue; - - // We'll only need the option's value if we're going to use it as - // part of the formed output (see below). - if (!canceledForming) { - optionValue = optionsMap.get(optionName); - } - - // But we always have to delete expected options off the provided - // option map, since the leftovers are what will be used to tell - // which are misplaced. - optionsMap.delete(optionName); + if (!optionsMap.has(optionName)) { + missingOptionNames.add(optionName); - if (canceledForming) { - return undefined; - } else { - return optionValue; - } - } else { // We don't need to continue forming the output if we've hit a // missing option name, since the end result of this formatString // call will be a thrown error, and formed output won't be needed. - missingOptionNames.add(optionName); + // Return undefined to mark canceledForming for the following + // iterations (and exit early out of this iteration). + return undefined; + } + + // Even if we're not actually forming the output anymore, we'll still + // have to access this option's value to check if it is invalid. + const optionValue = optionsMap.get(optionName); + + // We always have to delete expected options off the provided option + // map, since the leftovers are what will be used to tell which are + // misplaced - information you want even (or doubly so) if we've + // already stopped forming the output thanks to missing options. + optionsMap.delete(optionName); + + // Just like if an option is missing, a valueless option cancels + // forming the rest of the output. + if (isValueless(optionValue)) { + // It's also an error, *except* if this option is one of the ones + // that we're indicated to *expect* might be valueless! In that case, + // we still need to stop forming the string (and mark a separate flag + // so that we return a blank), but it's not an error. + if (expectedValuelessOptionNames.has(optionName)) { + seenExpectedValuelessOption = true; + } else { + valuelessOptionNames.add(optionName); + } + return undefined; } + + if (canceledForming) { + return undefined; + } + + return optionValue; }, }); @@ -272,17 +317,30 @@ export class Language extends Thing { Array.from(optionsMap.keys()); withAggregate({message: `Errors in options for string "${key}"`}, ({push}) => { + const names = set => Array.from(set).join(', '); + if (!empty(missingOptionNames)) { - const names = Array.from(missingOptionNames).join(`, `); - push(new Error(`Missing options: ${names}`)); + push(new Error( + `Missing options: ${names(missingOptionNames)}`)); + } + + if (!empty(valuelessOptionNames)) { + push(new Error( + `Valueless options: ${names(valuelessOptionNames)}`)); } if (!empty(misplacedOptionNames)) { - const names = Array.from(misplacedOptionNames).join(`, `); - push(new Error(`Unexpected options: ${names}`)); + push(new Error( + `Unexpected options: ${names(misplacedOptionNames)}`)); } }); + // If an option was valueless as marked to expect, then that indicates + // the whole string should be treated as blank content. + if (seenExpectedValuelessOption) { + return html.blank(); + } + return output; } @@ -416,11 +474,32 @@ export class Language extends Thing { } formatDate(date) { + // Null or undefined date is blank content. + if (date === null || date === undefined) { + return html.blank(); + } + this.assertIntlAvailable('intl_date'); return this.intl_date.format(date); } formatDateRange(startDate, endDate) { + // formatDateRange expects both values to be present, but if both are null + // or both are undefined, that's just blank content. + const hasStart = startDate !== null && startDate !== undefined; + const hasEnd = endDate !== null && endDate !== undefined; + if (!hasStart || !hasEnd) { + if (startDate === endDate) { + return html.blank(); + } else if (hasStart) { + throw new Error(`Expected both start and end of date range, got only start`); + } else if (hasEnd) { + throw new Error(`Expected both start and end of date range, got only end`); + } else { + throw new Error(`Got mismatched ${startDate}/${endDate} for start and end`); + } + } + this.assertIntlAvailable('intl_date'); return this.intl_date.formatRange(startDate, endDate); } @@ -431,6 +510,17 @@ export class Language extends Thing { days: numDays = 0, approximate = false, }) { + // Give up if any of years, months, or days is null or undefined. + // These default to zero, so something's gone pretty badly wrong to + // pass in all or partial missing values. + if ( + numYears === undefined || numYears === null || + numMonths === undefined || numMonths === null || + numDays === undefined || numDays === null + ) { + throw new Error(`Expected values or default zero for years, months, and days`); + } + let basis; const years = this.countYears(numYears, {unit: true}); @@ -468,6 +558,14 @@ export class Language extends Thing { approximate = true, absolute = true, } = {}) { + // Give up if current and/or reference date is null or undefined. + if ( + currentDate === undefined || currentDate === null || + referenceDate === undefined || referenceDate === null + ) { + throw new Error(`Expected values for currentDate and referenceDate`); + } + const currentInstant = toTemporalInstant.apply(currentDate); const referenceInstant = toTemporalInstant.apply(referenceDate); @@ -528,6 +626,12 @@ export class Language extends Thing { } formatDuration(secTotal, {approximate = false, unit = false} = {}) { + // Null or undefined duration is blank content. + if (secTotal === null || secTotal === undefined) { + return html.blank(); + } + + // Zero duration is a "missing" string. if (secTotal === 0) { return this.formatString('count.duration.missing'); } @@ -565,6 +669,11 @@ export class Language extends Thing { throw new TypeError(`externalLinkSpec unavailable`); } + // Null or undefined url is blank content. + if (url === null || url === undefined) { + return html.blank(); + } + isExternalLinkContext(context); if (style === 'all') { @@ -589,16 +698,31 @@ export class Language extends Thing { } formatIndex(value) { + // Null or undefined value is blank content. + if (value === null || value === undefined) { + return html.blank(); + } + this.assertIntlAvailable('intl_pluralOrdinal'); return this.formatString('count.index.' + this.intl_pluralOrdinal.select(value), {index: value}); } formatNumber(value) { + // Null or undefined value is blank content. + if (value === null || value === undefined) { + return html.blank(); + } + this.assertIntlAvailable('intl_number'); return this.intl_number.format(value); } formatWordCount(value) { + // Null or undefined value is blank content. + if (value === null || value === undefined) { + return html.blank(); + } + const num = this.formatNumber( value > 1000 ? Math.floor(value / 100) / 10 : value ); @@ -612,6 +736,11 @@ export class Language extends Thing { } #formatListHelper(array, processFn) { + // Empty lists, null, and undefined are blank content. + if (empty(array) || array === null || array === undefined) { + return html.blank(); + } + // Operate on "insertion markers" instead of the actual contents of the // array, because the process function (likely an Intl operation) is taken // to only operate on strings. We'll insert the contents of the array back @@ -673,10 +802,22 @@ export class Language extends Thing { // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB formatFileSize(bytes) { - if (!bytes) return ''; + // Null or undefined bytes is blank content. + if (bytes === null || bytes === undefined) { + return html.blank(); + } + + // Zero bytes is blank content. + if (bytes === 0) { + return html.blank(); + } bytes = parseInt(bytes); - if (isNaN(bytes)) return ''; + + // Non-number bytes is blank content! Wow. + if (isNaN(bytes)) { + return html.blank(); + } const round = (exp) => Math.round(bytes / 10 ** (exp - 1)) / 10; @@ -700,10 +841,42 @@ export class Language extends Thing { return this.formatString('count.fileSize.bytes', {bytes}); } } + + // Utility function to quickly provide a useful string key + // (generally a prefix) to stuff nested beneath it. + encapsulate(...args) { + const fn = + (typeof args.at(-1) === 'function' + ? args.at(-1) + : null); + + const parts = + (fn + ? args.slice(0, -1) + : args); + + const capsule = + this.#joinKeyParts(parts); + + if (fn) { + return fn(capsule); + } else { + return capsule; + } + } + + #joinKeyParts(parts) { + return parts.filter(Boolean).join('.'); + } } const countHelper = (stringKey, optionName = stringKey) => function(value, {unit = false} = {}) { + // Null or undefined value is blank content. + if (value === null || value === undefined) { + return html.blank(); + } + return this.formatString( unit ? `count.${stringKey}.withUnit.` + this.getUnitForm(value) diff --git a/src/data/things/track.js b/src/data/things/track.js index 725b1bb7..4aaf364c 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -18,7 +18,6 @@ import { } from '#yaml'; import {withPropertyFromObject} from '#composite/data'; -import {withResolvedContribs} from '#composite/wiki-data'; import { exitWithoutDependency, @@ -26,9 +25,16 @@ import { exposeDependency, exposeDependencyOrContinue, exposeUpdateValueOrContinue, + exposeWhetherDependencyAvailable, } from '#composite/control-flow'; import { + withRecontextualizedContributionList, + withRedatedContributionList, + withResolvedContribs, +} from '#composite/wiki-data'; + +import { additionalFiles, additionalNameList, commentary, @@ -43,8 +49,9 @@ import { referenceList, reverseReferenceList, simpleDate, - singleReference, simpleString, + singleReference, + thing, urls, wikiData, } from '#composite/wiki-properties'; @@ -52,21 +59,31 @@ import { import { exitWithoutUniqueCoverArt, inferredAdditionalNameList, + inheritContributionListFromOriginalRelease, inheritFromOriginalRelease, sharedAdditionalNameList, trackReverseReferenceList, withAlbum, withAlwaysReferenceByDirectory, withContainingTrackSection, + withDate, withHasUniqueCoverArt, + withOriginalRelease, withOtherReleases, withPropertyFromAlbum, + withTrackArtDate, } from '#composite/things/track'; export class Track extends Thing { static [Thing.referenceType] = 'track'; - static [Thing.getPropertyDescriptors] = ({Album, ArtTag, Artist, Flash}) => ({ + static [Thing.getPropertyDescriptors] = ({ + Album, + ArtTag, + Artist, + Flash, + WikiInfo, + }) => ({ // Update & expose name: name('Unnamed Track'), @@ -137,27 +154,14 @@ export class Track extends Thing { }), ], - // Date of cover art release. Like coverArtFileExtension, this represents - // only the track's own unique cover artwork, if any. This exposes only as - // the track's own coverArtDate or its album's trackArtDate, so if neither - // is specified, this value is null. coverArtDate: [ - withHasUniqueCoverArt(), - - exitWithoutDependency({ - dependency: '#hasUniqueCoverArt', - mode: input.value('falsy'), - }), - - exposeUpdateValueOrContinue({ - validate: input.value(isDate), - }), - - withPropertyFromAlbum({ - property: input.value('trackArtDate'), + withTrackArtDate({ + from: input.updateValue({ + validate: isDate, + }), }), - exposeDependency({dependency: '#album.trackArtDate'}), + exposeDependency({dependency: '#trackArtDate'}), ], coverArtDimensions: [ @@ -192,12 +196,15 @@ export class Track extends Thing { }), artistContribs: [ - inheritFromOriginalRelease({ - notFoundValue: input.value([]), - }), + inheritContributionListFromOriginalRelease(), + + withDate(), withResolvedContribs({ from: input.updateValue({validate: isContributionList}), + thingProperty: input.thisProperty(), + artistProperty: input.value('trackArtistContributions'), + date: '#date', }).outputs({ '#resolvedContribs': '#artistContribs', }), @@ -211,15 +218,28 @@ export class Track extends Thing { property: input.value('artistContribs'), }), + withRecontextualizedContributionList({ + list: '#album.artistContribs', + artistProperty: input.value('trackArtistContributions'), + }), + + withRedatedContributionList({ + list: '#album.artistContribs', + date: '#date', + }), + exposeDependency({dependency: '#album.artistContribs'}), ], contributorContribs: [ - inheritFromOriginalRelease({ - notFoundValue: input.value([]), - }), + inheritContributionListFromOriginalRelease(), - contributionList(), + withDate(), + + contributionList({ + date: '#date', + artistProperty: input.value('trackContributorContributions'), + }), ], // Cover artists aren't inherited from the original release, since it @@ -230,8 +250,15 @@ export class Track extends Thing { value: input.value([]), }), + withTrackArtDate({ + fallback: input.value(true), + }), + withResolvedContribs({ from: input.updateValue({validate: isContributionList}), + thingProperty: input.thisProperty(), + artistProperty: input.value('trackCoverArtistContributions'), + date: '#trackArtDate', }).outputs({ '#resolvedContribs': '#coverArtistContribs', }), @@ -245,6 +272,16 @@ export class Track extends Thing { property: input.value('trackCoverArtistContribs'), }), + withRecontextualizedContributionList({ + list: '#album.trackCoverArtistContribs', + artistProperty: input.value('trackCoverArtistContributions'), + }), + + withRedatedContributionList({ + list: '#album.trackCoverArtistContribs', + date: '#trackArtDate', + }), + exposeDependency({dependency: '#album.trackCoverArtistContribs'}), ], @@ -306,6 +343,10 @@ export class Track extends Thing { class: input.value(Track), }), + wikiInfo: thing({ + class: input.value(WikiInfo), + }), + // Expose only commentatorArtists: commentatorArtists(), @@ -316,13 +357,8 @@ export class Track extends Thing { ], date: [ - exposeDependencyOrContinue({dependency: 'dateFirstReleased'}), - - withPropertyFromAlbum({ - property: input.value('date'), - }), - - exposeDependency({dependency: '#album.date'}), + withDate(), + exposeDependency({dependency: '#date'}), ], hasUniqueCoverArt: [ @@ -330,6 +366,23 @@ export class Track extends Thing { exposeDependency({dependency: '#hasUniqueCoverArt'}), ], + isOriginalRelease: [ + withOriginalRelease(), + + exposeWhetherDependencyAvailable({ + dependency: '#originalRelease', + negate: input.value(true), + }), + ], + + isRerelease: [ + withOriginalRelease(), + + exposeWhetherDependencyAvailable({ + dependency: '#originalRelease', + }), + ], + otherReleases: [ withOtherReleases(), exposeDependency({dependency: '#otherReleases'}), diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 316bd3bb..b7b7b01c 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -3,8 +3,18 @@ export const WIKI_INFO_FILE = 'wiki-info.yaml'; import {input} from '#composite'; import find from '#find'; import Thing from '#thing'; -import {isColor, isLanguageCode, isName, isURL} from '#validators'; - +import {parseContributionPresets} from '#yaml'; + +import { + isBoolean, + isColor, + isContributionPresetList, + isLanguageCode, + isName, + isURL, +} from '#validators'; + +import {exitWithoutDependency} from '#composite/control-flow'; import {contentString, flag, name, referenceList, wikiData} from '#composite/wiki-properties'; @@ -57,6 +67,11 @@ export class WikiInfo extends Thing { data: 'groupData', }), + contributionPresets: { + flags: {update: true, expose: true}, + update: {validate: isContributionPresetList}, + }, + // Feature toggles enableFlashesAndGames: flag(false), enableListings: flag(false), @@ -64,8 +79,26 @@ export class WikiInfo extends Thing { enableArtTagUI: flag(false), enableGroupUI: flag(false), + enableSearch: [ + exitWithoutDependency({ + dependency: 'searchDataAvailable', + mode: input.value('falsy'), + value: input.value(false), + }), + + flag(true), + ], + // Update only + searchDataAvailable: { + flags: {update: true}, + update: { + validate: isBoolean, + default: false, + }, + }, + groupData: wikiData({ class: input.value(Group), }), @@ -86,6 +119,11 @@ export class WikiInfo extends Thing { 'Enable News': {property: 'enableNews'}, 'Enable Art Tag UI': {property: 'enableArtTagUI'}, 'Enable Group UI': {property: 'enableGroupUI'}, + + 'Contribution Presets': { + property: 'contributionPresets', + transform: parseContributionPresets, + }, }, }; |