diff options
-rw-r--r-- | data-tests/index.js | 4 | ||||
-rw-r--r-- | src/content/dependencies/index.js | 8 | ||||
-rw-r--r-- | src/data/things/album.js | 108 | ||||
-rw-r--r-- | src/data/things/art-tag.js | 20 | ||||
-rw-r--r-- | src/data/things/artist.js | 35 | ||||
-rw-r--r-- | src/data/things/cacheable-object.js | 4 | ||||
-rw-r--r-- | src/data/things/composite.js | 22 | ||||
-rw-r--r-- | src/data/things/flash.js | 50 | ||||
-rw-r--r-- | src/data/things/group.js | 37 | ||||
-rw-r--r-- | src/data/things/homepage-layout.js | 29 | ||||
-rw-r--r-- | src/data/things/language.js | 12 | ||||
-rw-r--r-- | src/data/things/news-entry.js | 15 | ||||
-rw-r--r-- | src/data/things/static-page.js | 14 | ||||
-rw-r--r-- | src/data/things/thing.js | 554 | ||||
-rw-r--r-- | src/data/things/track.js | 83 | ||||
-rw-r--r-- | src/data/things/validators.js | 6 | ||||
-rw-r--r-- | src/data/things/wiki-info.js | 34 | ||||
-rw-r--r-- | src/data/yaml.js | 42 | ||||
-rw-r--r-- | src/find.js | 6 | ||||
-rw-r--r-- | src/gen-thumbs.js | 6 | ||||
-rwxr-xr-x | src/upd8.js | 14 | ||||
-rw-r--r-- | src/util/cli.js | 2 | ||||
-rw-r--r-- | src/util/sugar.js | 10 |
23 files changed, 624 insertions, 491 deletions
diff --git a/data-tests/index.js b/data-tests/index.js index b05de9ed..d0770907 100644 --- a/data-tests/index.js +++ b/data-tests/index.js @@ -3,7 +3,7 @@ import {fileURLToPath} from 'node:url'; import chokidar from 'chokidar'; -import {color, logError, logInfo, logWarn, parseOptions} from '#cli'; +import {colors, logError, logInfo, logWarn, parseOptions} from '#cli'; import {isMain} from '#node-utils'; import {getContextAssignments} from '#repl'; import {bindOpts, showAggregate} from '#sugar'; @@ -24,7 +24,7 @@ async function main() { } console.log(`HSMusic automated data tests`); - console.log(`${color.bright(color.yellow(`:star:`))} Now featuring quick-reloading! ${color.bright(color.cyan(`:earth:`))}`); + console.log(`${colors.bright(colors.yellow(`:star:`))} Now featuring quick-reloading! ${colors.bright(colors.cyan(`:earth:`))}`); // Watch adjacent files in data-tests directory const metaPath = fileURLToPath(import.meta.url); diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index 3bc34845..57025a5d 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -6,7 +6,7 @@ import {fileURLToPath} from 'node:url'; import chokidar from 'chokidar'; import {ESLint} from 'eslint'; -import {color, logWarn} from '#cli'; +import {colors, logWarn} from '#cli'; import contentFunction, {ContentFunctionSpecError} from '#content-function'; import {annotateFunction} from '#sugar'; @@ -192,7 +192,7 @@ export function watchContentDependencies({ if (logging && emittedReady) { const timestamp = new Date().toLocaleString('en-US', {timeStyle: 'medium'}); - console.log(color.green(`[${timestamp}] Updated ${functionName}`)); + console.log(colors.green(`[${timestamp}] Updated ${functionName}`)); } contentDependencies[functionName] = fn; @@ -219,9 +219,9 @@ export function watchContentDependencies({ } if (typeof error === 'string') { - console.error(color.yellow(error)); + console.error(colors.yellow(error)); } else if (error instanceof ContentFunctionSpecError) { - console.error(color.yellow(error.message)); + console.error(colors.yellow(error.message)); } else { console.error(error); } diff --git a/src/data/things/album.js b/src/data/things/album.js index da018856..9cf58641 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -2,7 +2,25 @@ import find from '#find'; import {empty} from '#sugar'; import {isDate, isDimensions, isTrackSectionList} from '#validators'; -import Thing from './thing.js'; +import Thing, { + additionalFiles, + commentary, + color, + commentatorArtists, + contribsByRef, + contribsPresent, + directory, + dynamicContribs, + fileExtension, + flag, + name, + resolvedReferenceList, + referenceList, + simpleDate, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Album extends Thing { static [Thing.referenceType] = 'album'; @@ -10,14 +28,14 @@ export class Album extends Thing { static [Thing.getPropertyDescriptors] = ({ArtTag, Artist, Group, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Album'), - color: Thing.common.color(), - directory: Thing.common.directory(), - urls: Thing.common.urls(), + name: name('Unnamed Album'), + color: color(), + directory: directory(), + urls: urls(), - date: Thing.common.simpleDate(), - trackArtDate: Thing.common.simpleDate(), - dateAddedToWiki: Thing.common.simpleDate(), + date: simpleDate(), + trackArtDate: simpleDate(), + dateAddedToWiki: simpleDate(), coverArtDate: { flags: {update: true, expose: true}, @@ -36,14 +54,14 @@ export class Album extends Thing { }, }, - artistContribsByRef: Thing.common.contribsByRef(), - coverArtistContribsByRef: Thing.common.contribsByRef(), - trackCoverArtistContribsByRef: Thing.common.contribsByRef(), - wallpaperArtistContribsByRef: Thing.common.contribsByRef(), - bannerArtistContribsByRef: Thing.common.contribsByRef(), + artistContribsByRef: contribsByRef(), + coverArtistContribsByRef: contribsByRef(), + trackCoverArtistContribsByRef: contribsByRef(), + wallpaperArtistContribsByRef: contribsByRef(), + bannerArtistContribsByRef: contribsByRef(), - groupsByRef: Thing.common.referenceList(Group), - artTagsByRef: Thing.common.referenceList(ArtTag), + groupsByRef: referenceList(Group), + artTagsByRef: referenceList(ArtTag), trackSections: { flags: {update: true, expose: true}, @@ -81,58 +99,58 @@ export class Album extends Thing { }, }, - coverArtFileExtension: Thing.common.fileExtension('jpg'), - trackCoverArtFileExtension: Thing.common.fileExtension('jpg'), + coverArtFileExtension: fileExtension('jpg'), + trackCoverArtFileExtension: fileExtension('jpg'), - wallpaperStyle: Thing.common.simpleString(), - wallpaperFileExtension: Thing.common.fileExtension('jpg'), + wallpaperStyle: simpleString(), + wallpaperFileExtension: fileExtension('jpg'), - bannerStyle: Thing.common.simpleString(), - bannerFileExtension: Thing.common.fileExtension('jpg'), + bannerStyle: simpleString(), + bannerFileExtension: fileExtension('jpg'), bannerDimensions: { flags: {update: true, expose: true}, update: {validate: isDimensions}, }, - hasTrackNumbers: Thing.common.flag(true), - isListedOnHomepage: Thing.common.flag(true), - isListedInGalleries: Thing.common.flag(true), + hasTrackNumbers: flag(true), + isListedOnHomepage: flag(true), + isListedInGalleries: flag(true), - commentary: Thing.common.commentary(), - additionalFiles: Thing.common.additionalFiles(), + commentary: commentary(), + additionalFiles: additionalFiles(), // Update only - artistData: Thing.common.wikiData(Artist), - artTagData: Thing.common.wikiData(ArtTag), - groupData: Thing.common.wikiData(Group), - trackData: Thing.common.wikiData(Track), + artistData: wikiData(Artist), + artTagData: wikiData(ArtTag), + groupData: wikiData(Group), + trackData: wikiData(Track), // Expose only - artistContribs: Thing.common.dynamicContribs('artistContribsByRef'), - coverArtistContribs: Thing.common.dynamicContribs('coverArtistContribsByRef'), - trackCoverArtistContribs: Thing.common.dynamicContribs('trackCoverArtistContribsByRef'), - wallpaperArtistContribs: Thing.common.dynamicContribs('wallpaperArtistContribsByRef'), - bannerArtistContribs: Thing.common.dynamicContribs('bannerArtistContribsByRef'), + artistContribs: dynamicContribs('artistContribsByRef'), + coverArtistContribs: dynamicContribs('coverArtistContribsByRef'), + trackCoverArtistContribs: dynamicContribs('trackCoverArtistContribsByRef'), + wallpaperArtistContribs: dynamicContribs('wallpaperArtistContribsByRef'), + bannerArtistContribs: dynamicContribs('bannerArtistContribsByRef'), - commentatorArtists: Thing.common.commentatorArtists(), + commentatorArtists: commentatorArtists(), - groups: Thing.common.resolvedReferenceList({ + groups: resolvedReferenceList({ list: 'groupsByRef', data: 'groupData', find: find.group, }), - artTags: Thing.common.resolvedReferenceList({ + artTags: resolvedReferenceList({ list: 'artTagsByRef', data: 'artTagData', find: find.artTag, }), - hasCoverArt: Thing.common.contribsPresent('coverArtistContribsByRef'), - hasWallpaperArt: Thing.common.contribsPresent('wallpaperArtistContribsByRef'), - hasBannerArt: Thing.common.contribsPresent('bannerArtistContribsByRef'), + hasCoverArt: contribsPresent('coverArtistContribsByRef'), + hasWallpaperArt: contribsPresent('wallpaperArtistContribsByRef'), + hasBannerArt: contribsPresent('bannerArtistContribsByRef'), tracks: { flags: {expose: true}, @@ -192,9 +210,9 @@ export class Album extends Thing { export class TrackSectionHelper extends Thing { static [Thing.getPropertyDescriptors] = () => ({ - name: Thing.common.name('Unnamed Track Group'), - color: Thing.common.color(), - dateOriginallyReleased: Thing.common.simpleDate(), - isDefaultTrackGroup: Thing.common.flag(false), + name: name('Unnamed Track Group'), + color: color(), + dateOriginallyReleased: simpleDate(), + isDefaultTrackGroup: flag(false), }) } diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 5d7d0cbf..3d65b578 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,6 +1,12 @@ import {sortAlbumsTracksChronologically} from '#wiki-data'; -import Thing from './thing.js'; +import Thing, { + color, + directory, + flag, + name, + wikiData, +} from './thing.js'; export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; @@ -8,10 +14,10 @@ export class ArtTag extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Art Tag'), - directory: Thing.common.directory(), - color: Thing.common.color(), - isContentWarning: Thing.common.flag(false), + name: name('Unnamed Art Tag'), + directory: directory(), + color: color(), + isContentWarning: flag(false), nameShort: { flags: {update: true, expose: true}, @@ -25,8 +31,8 @@ export class ArtTag extends Thing { // Update only - albumData: Thing.common.wikiData(Album), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + trackData: wikiData(Track), // Expose only diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 93a1b51b..2676591a 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -1,7 +1,16 @@ import find from '#find'; import {isName, validateArrayItems} from '#validators'; -import Thing from './thing.js'; +import Thing, { + directory, + fileExtension, + flag, + name, + simpleString, + singleReference, + urls, + wikiData, +} from './thing.js'; export class Artist extends Thing { static [Thing.referenceType] = 'artist'; @@ -9,13 +18,13 @@ export class Artist extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({ // Update & expose - name: Thing.common.name('Unnamed Artist'), - directory: Thing.common.directory(), - urls: Thing.common.urls(), - contextNotes: Thing.common.simpleString(), + name: name('Unnamed Artist'), + directory: directory(), + urls: urls(), + contextNotes: simpleString(), - hasAvatar: Thing.common.flag(false), - avatarFileExtension: Thing.common.fileExtension('jpg'), + hasAvatar: flag(false), + avatarFileExtension: fileExtension('jpg'), aliasNames: { flags: {update: true, expose: true}, @@ -23,15 +32,15 @@ export class Artist extends Thing { expose: {transform: (names) => names ?? []}, }, - isAlias: Thing.common.flag(), - aliasedArtistRef: Thing.common.singleReference(Artist), + isAlias: flag(), + aliasedArtistRef: singleReference(Artist), // Update only - albumData: Thing.common.wikiData(Album), - artistData: Thing.common.wikiData(Artist), - flashData: Thing.common.wikiData(Flash), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + artistData: wikiData(Artist), + flashData: wikiData(Flash), + trackData: wikiData(Track), // Expose only diff --git a/src/data/things/cacheable-object.js b/src/data/things/cacheable-object.js index 62c23d13..92a46d66 100644 --- a/src/data/things/cacheable-object.js +++ b/src/data/things/cacheable-object.js @@ -76,7 +76,7 @@ import {inspect as nodeInspect} from 'node:util'; -import {color, ENABLE_COLOR} from '#cli'; +import {colors, ENABLE_COLOR} from '#cli'; function inspect(value) { return nodeInspect(value, {colors: ENABLE_COLOR}); @@ -183,7 +183,7 @@ export default class CacheableObject { } } catch (error) { error.message = [ - `Property ${color.green(property)}`, + `Property ${colors.green(property)}`, `(${inspect(this[property])} -> ${inspect(newValue)}):`, error.message ].join(' '); diff --git a/src/data/things/composite.js b/src/data/things/composite.js index 5b6de901..fd52aa0f 100644 --- a/src/data/things/composite.js +++ b/src/data/things/composite.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import { empty, @@ -346,8 +346,8 @@ export function compositeFrom(firstArg, secondArg) { if (compositeFrom.debug === true) { const label = (annotation - ? color.dim(`[composite: ${annotation}]`) - : color.dim(`[composite]`)); + ? colors.dim(`[composite: ${annotation}]`) + : colors.dim(`[composite]`)); const result = fn(); if (Array.isArray(result)) { console.log(label, ...result.map(value => @@ -594,9 +594,9 @@ export function compositeFrom(firstArg, secondArg) { const availableDependencies = {...initialDependencies}; if (expectingTransform) { - debug(() => [color.bright(`begin composition - transforming from:`), initialValue]); + debug(() => [colors.bright(`begin composition - transforming from:`), initialValue]); } else { - debug(() => color.bright(`begin composition - not transforming`)); + debug(() => colors.bright(`begin composition - not transforming`)); } for (let i = 0; i < steps.length; i++) { @@ -641,7 +641,7 @@ export function compositeFrom(firstArg, secondArg) { throw new TypeError(`Inferred early-exit is disallowed in nested compositions`); } - debug(() => color.bright(`end composition - exit (inferred)`)); + debug(() => colors.bright(`end composition - exit (inferred)`)); return result; } @@ -652,7 +652,7 @@ export function compositeFrom(firstArg, secondArg) { const {providedValue} = continuationStorage; debug(() => [`step #${i+1} - result: exit (explicit) ->`, providedValue]); - debug(() => color.bright(`end composition - exit (explicit)`)); + debug(() => colors.bright(`end composition - exit (explicit)`)); if (baseComposes) { return continuationIfApplicable.exit(providedValue); @@ -708,17 +708,17 @@ export function compositeFrom(firstArg, secondArg) { case 'raise': debug(() => (isBase - ? color.bright(`end composition - raise (base: explicit)`) - : color.bright(`end composition - raise`))); + ? colors.bright(`end composition - raise (base: explicit)`) + : colors.bright(`end composition - raise`))); return continuationIfApplicable(...continuationArgs); case 'raiseAbove': - debug(() => color.bright(`end composition - raiseAbove`)); + debug(() => colors.bright(`end composition - raiseAbove`)); return continuationIfApplicable.raise(...continuationArgs); case 'continuation': if (isBase) { - debug(() => color.bright(`end composition - raise (inferred)`)); + debug(() => colors.bright(`end composition - raise (inferred)`)); return continuationIfApplicable(...continuationArgs); } else { Object.assign(availableDependencies, continuingWithDependencies); diff --git a/src/data/things/flash.js b/src/data/things/flash.js index ce2e7fac..4e640dac 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -8,7 +8,19 @@ import { oneOf, } from '#validators'; -import Thing from './thing.js'; +import Thing, { + dynamicContribs, + color, + contribsByRef, + fileExtension, + name, + referenceList, + resolvedReferenceList, + simpleDate, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; @@ -16,7 +28,7 @@ export class Flash extends Thing { static [Thing.getPropertyDescriptors] = ({Artist, Track, FlashAct}) => ({ // Update & expose - name: Thing.common.name('Unnamed Flash'), + name: name('Unnamed Flash'), directory: { flags: {update: true, expose: true}, @@ -44,27 +56,27 @@ export class Flash extends Thing { }, }, - date: Thing.common.simpleDate(), + date: simpleDate(), - coverArtFileExtension: Thing.common.fileExtension('jpg'), + coverArtFileExtension: fileExtension('jpg'), - contributorContribsByRef: Thing.common.contribsByRef(), + contributorContribsByRef: contribsByRef(), - featuredTracksByRef: Thing.common.referenceList(Track), + featuredTracksByRef: referenceList(Track), - urls: Thing.common.urls(), + urls: urls(), // Update only - artistData: Thing.common.wikiData(Artist), - trackData: Thing.common.wikiData(Track), - flashActData: Thing.common.wikiData(FlashAct), + artistData: wikiData(Artist), + trackData: wikiData(Track), + flashActData: wikiData(FlashAct), // Expose only - contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'), + contributorContribs: dynamicContribs('contributorContribsByRef'), - featuredTracks: Thing.common.resolvedReferenceList({ + featuredTracks: resolvedReferenceList({ list: 'featuredTracksByRef', data: 'trackData', find: find.track, @@ -111,10 +123,10 @@ export class FlashAct extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed Flash Act'), - color: Thing.common.color(), - anchor: Thing.common.simpleString(), - jump: Thing.common.simpleString(), + name: name('Unnamed Flash Act'), + color: color(), + anchor: simpleString(), + jump: simpleString(), jumpColor: { flags: {update: true, expose: true}, @@ -126,15 +138,15 @@ export class FlashAct extends Thing { } }, - flashesByRef: Thing.common.referenceList(Flash), + flashesByRef: referenceList(Flash), // Update only - flashData: Thing.common.wikiData(Flash), + flashData: wikiData(Flash), // Expose only - flashes: Thing.common.resolvedReferenceList({ + flashes: resolvedReferenceList({ list: 'flashesByRef', data: 'flashData', find: find.flash, diff --git a/src/data/things/group.js b/src/data/things/group.js index 6c712847..873c6d88 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -1,6 +1,15 @@ import find from '#find'; -import Thing from './thing.js'; +import Thing, { + color, + directory, + name, + referenceList, + resolvedReferenceList, + simpleString, + urls, + wikiData, +} from './thing.js'; export class Group extends Thing { static [Thing.referenceType] = 'group'; @@ -8,23 +17,23 @@ export class Group extends Thing { static [Thing.getPropertyDescriptors] = ({Album}) => ({ // Update & expose - name: Thing.common.name('Unnamed Group'), - directory: Thing.common.directory(), + name: name('Unnamed Group'), + directory: directory(), - description: Thing.common.simpleString(), + description: simpleString(), - urls: Thing.common.urls(), + urls: urls(), - featuredAlbumsByRef: Thing.common.referenceList(Album), + featuredAlbumsByRef: referenceList(Album), // Update only - albumData: Thing.common.wikiData(Album), - groupCategoryData: Thing.common.wikiData(GroupCategory), + albumData: wikiData(Album), + groupCategoryData: wikiData(GroupCategory), // Expose only - featuredAlbums: Thing.common.resolvedReferenceList({ + featuredAlbums: resolvedReferenceList({ list: 'featuredAlbumsByRef', data: 'albumData', find: find.album, @@ -77,18 +86,18 @@ export class GroupCategory extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Group Category'), - color: Thing.common.color(), + name: name('Unnamed Group Category'), + color: color(), - groupsByRef: Thing.common.referenceList(Group), + groupsByRef: referenceList(Group), // Update only - groupData: Thing.common.wikiData(Group), + groupData: wikiData(Group), // Expose only - groups: Thing.common.resolvedReferenceList({ + groups: resolvedReferenceList({ list: 'groupsByRef', data: 'groupData', find: find.group, diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js index 59656b41..ab6f4cff 100644 --- a/src/data/things/homepage-layout.js +++ b/src/data/things/homepage-layout.js @@ -9,13 +9,22 @@ import { validateInstanceOf, } from '#validators'; -import Thing from './thing.js'; +import Thing, { + color, + name, + referenceList, + resolvedReference, + resolvedReferenceList, + simpleString, + singleReference, + wikiData, +} from './thing.js'; export class HomepageLayout extends Thing { static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({ // Update & expose - sidebarContent: Thing.common.simpleString(), + sidebarContent: simpleString(), navbarLinks: { flags: {update: true, expose: true}, @@ -36,7 +45,7 @@ export class HomepageLayoutRow extends Thing { static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Homepage Row'), + name: name('Unnamed Homepage Row'), type: { flags: {update: true, expose: true}, @@ -48,15 +57,15 @@ export class HomepageLayoutRow extends Thing { }, }, - color: Thing.common.color(), + color: color(), // Update only // These aren't necessarily used by every HomepageLayoutRow subclass, but // for convenience of providing this data, every row accepts all wiki data // arrays depended upon by any subclass's behavior. - albumData: Thing.common.wikiData(Album), - groupData: Thing.common.wikiData(Group), + albumData: wikiData(Album), + groupData: wikiData(Group), }); } @@ -92,8 +101,8 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { }, }, - sourceGroupByRef: Thing.common.singleReference(Group), - sourceAlbumsByRef: Thing.common.referenceList(Album), + sourceGroupByRef: singleReference(Group), + sourceAlbumsByRef: referenceList(Album), countAlbumsFromGroup: { flags: {update: true, expose: true}, @@ -107,13 +116,13 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow { // Expose only - sourceGroup: Thing.common.resolvedReference({ + sourceGroup: resolvedReference({ ref: 'sourceGroupByRef', data: 'groupData', find: find.group, }), - sourceAlbums: Thing.common.resolvedReferenceList({ + sourceAlbums: resolvedReferenceList({ list: 'sourceAlbumsByRef', data: 'albumData', find: find.album, diff --git a/src/data/things/language.js b/src/data/things/language.js index 0638afa2..c98495dc 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,6 +1,10 @@ import {isLanguageCode} from '#validators'; -import Thing from './thing.js'; +import Thing, { + externalFunction, + flag, + simpleString, +} from './thing.js'; export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ @@ -16,7 +20,7 @@ export class Language extends Thing { // Human-readable name. This should be the language's own native name, not // localized to any other language. - name: Thing.common.simpleString(), + name: simpleString(), // Language code specific to JavaScript's Internationalization (Intl) API. // Usually this will be the same as the language's general code, but it @@ -38,7 +42,7 @@ export class Language extends Thing { // with languages that are currently in development and not ready for // formal release, or which are just kept hidden as "experimental zones" // for wiki development or content testing. - hidden: Thing.common.flag(false), + hidden: flag(false), // Mapping of translation keys to values (strings). Generally, don't // access this object directly - use methods instead. @@ -66,7 +70,7 @@ export class Language extends Thing { // Update only - escapeHTML: Thing.common.externalFunction(), + escapeHTML: externalFunction(), // Expose only diff --git a/src/data/things/news-entry.js b/src/data/things/news-entry.js index 43911410..6984874e 100644 --- a/src/data/things/news-entry.js +++ b/src/data/things/news-entry.js @@ -1,4 +1,9 @@ -import Thing from './thing.js'; +import Thing, { + directory, + name, + simpleDate, + simpleString, +} from './thing.js'; export class NewsEntry extends Thing { static [Thing.referenceType] = 'news-entry'; @@ -6,11 +11,11 @@ export class NewsEntry extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed News Entry'), - directory: Thing.common.directory(), - date: Thing.common.simpleDate(), + name: name('Unnamed News Entry'), + directory: directory(), + date: simpleDate(), - content: Thing.common.simpleString(), + content: simpleString(), // Expose only diff --git a/src/data/things/static-page.js b/src/data/things/static-page.js index ae0ca420..0133e0b6 100644 --- a/src/data/things/static-page.js +++ b/src/data/things/static-page.js @@ -1,6 +1,10 @@ import {isName} from '#validators'; -import Thing from './thing.js'; +import Thing, { + directory, + name, + simpleString, +} from './thing.js'; export class StaticPage extends Thing { static [Thing.referenceType] = 'static'; @@ -8,7 +12,7 @@ export class StaticPage extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose - name: Thing.common.name('Unnamed Static Page'), + name: name('Unnamed Static Page'), nameShort: { flags: {update: true, expose: true}, @@ -20,8 +24,8 @@ export class StaticPage extends Thing { }, }, - directory: Thing.common.directory(), - content: Thing.common.simpleString(), - stylesheet: Thing.common.simpleString(), + directory: directory(), + content: simpleString(), + stylesheet: simpleString(), }); } diff --git a/src/data/things/thing.js b/src/data/things/thing.js index 98dec3c3..19f00b3e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -3,7 +3,7 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import find from '#find'; import {empty, stitchArrays} from '#sugar'; import {filterMultipleArrays, getKebabCase} from '#wiki-data'; @@ -41,297 +41,329 @@ export default class Thing extends CacheableObject { static getPropertyDescriptors = Symbol('Thing.getPropertyDescriptors'); static getSerializeDescriptors = Symbol('Thing.getSerializeDescriptors'); - // Regularly reused property descriptors, for ease of access and generally - // duplicating less code across wiki data types. These are specialized utility - // functions, so check each for how its own arguments behave! - static common = { - name: (defaultName) => ({ - flags: {update: true, expose: true}, - update: {validate: isName, default: defaultName}, - }), + // Default custom inspect function, which may be overridden by Thing + // subclasses. This will be used when displaying aggregate errors and other + // command-line logging - it's the place to provide information useful in + // identifying the Thing being presented. + [inspect.custom]() { + const cname = this.constructor.name; - color: () => ({ - flags: {update: true, expose: true}, - update: {validate: isColor}, - }), + return ( + (this.name ? `${cname} ${colors.green(`"${this.name}"`)}` : `${cname}`) + + (this.directory ? ` (${colors.blue(Thing.getReference(this))})` : '') + ); + } - directory: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDirectory}, - expose: { - dependencies: ['name'], - transform(directory, {name}) { - if (directory === null && name === null) return null; - else if (directory === null) return getKebabCase(name); - else return directory; - }, - }, - }), + static getReference(thing) { + if (!thing.constructor[Thing.referenceType]) { + throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); + } - urls: () => ({ - flags: {update: true, expose: true}, - update: {validate: validateArrayItems(isURL)}, - expose: {transform: (value) => value ?? []}, - }), + if (!thing.directory) { + throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); + } - // A file extension! Or the default, if provided when calling this. - fileExtension: (defaultFileExtension = null) => ({ - flags: {update: true, expose: true}, - update: {validate: isFileExtension}, - expose: {transform: (value) => value ?? defaultFileExtension}, - }), + return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; + } +} + +// Property descriptor templates +// +// Regularly reused property descriptors, for ease of access and generally +// duplicating less code across wiki data types. These are specialized utility +// functions, so check each for how its own arguments behave! + +export function name(defaultName) { + return { + flags: {update: true, expose: true}, + update: {validate: isName, default: defaultName}, + }; +} + +export function color() { + return { + flags: {update: true, expose: true}, + update: {validate: isColor}, + }; +} - // Straightforward flag descriptor for a variety of property purposes. - // Provide a default value, true or false! - flag: (defaultValue = false) => { - if (typeof defaultValue !== 'boolean') { - throw new TypeError(`Always set explicit defaults for flags!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: isBoolean, default: defaultValue}, - }; +export function directory() { + return { + flags: {update: true, expose: true}, + update: {validate: isDirectory}, + expose: { + dependencies: ['name'], + transform(directory, {name}) { + if (directory === null && name === null) return null; + else if (directory === null) return getKebabCase(name); + else return directory; + }, }, + }; +} - // General date type, used as the descriptor for a bunch of properties. - // This isn't dynamic though - it won't inherit from a date stored on - // another object, for example. - simpleDate: () => ({ - flags: {update: true, expose: true}, - update: {validate: isDate}, - }), +export function urls() { + return { + flags: {update: true, expose: true}, + update: {validate: validateArrayItems(isURL)}, + expose: {transform: (value) => value ?? []}, + }; +} - // General string type. This should probably generally be avoided in favor - // of more specific validation, but using it makes it easy to find where we - // might want to improve later, and it's a useful shorthand meanwhile. - simpleString: () => ({ - flags: {update: true, expose: true}, - update: {validate: isString}, - }), +// A file extension! Or the default, if provided when calling this. +export function fileExtension(defaultFileExtension = null) { + return { + flags: {update: true, expose: true}, + update: {validate: isFileExtension}, + expose: {transform: (value) => value ?? defaultFileExtension}, + }; +} - // External function. These should only be used as dependencies for other - // properties, so they're left unexposed. - externalFunction: () => ({ - flags: {update: true}, - update: {validate: (t) => typeof t === 'function'}, - }), +// Straightforward flag descriptor for a variety of property purposes. +// Provide a default value, true or false! +export function flag(defaultValue = false) { + // TODO: ^ Are you actually kidding me + if (typeof defaultValue !== 'boolean') { + throw new TypeError(`Always set explicit defaults for flags!`); + } - // Super simple "contributions by reference" list, used for a variety of - // properties (Artists, Cover Artists, etc). This is the property which is - // externally provided, in the form: - // - // [ - // {who: 'Artist Name', what: 'Viola'}, - // {who: 'artist:john-cena', what: null}, - // ... - // ] - // - // ...processed from YAML, spreadsheet, or any other kind of input. - contribsByRef: () => ({ - flags: {update: true, expose: true}, - update: {validate: isContributionList}, - }), + return { + flags: {update: true, expose: true}, + update: {validate: isBoolean, default: defaultValue}, + }; +} - // Artist commentary! Generally present on tracks and albums. - commentary: () => ({ - flags: {update: true, expose: true}, - update: {validate: isCommentary}, - }), +// General date type, used as the descriptor for a bunch of properties. +// This isn't dynamic though - it won't inherit from a date stored on +// another object, for example. +export function simpleDate() { + return { + flags: {update: true, expose: true}, + update: {validate: isDate}, + }; +} - // This is a somewhat more involved data structure - it's for additional - // or "bonus" files associated with albums or tracks (or anything else). - // It's got this form: - // - // [ - // {title: 'Booklet', files: ['Booklet.pdf']}, - // { - // title: 'Wallpaper', - // description: 'Cool Wallpaper!', - // files: ['1440x900.png', '1920x1080.png'] - // }, - // {title: 'Alternate Covers', description: null, files: [...]}, - // ... - // ] - // - additionalFiles: () => ({ - flags: {update: true, expose: true}, - update: {validate: isAdditionalFileList}, - expose: { - transform: (additionalFiles) => - additionalFiles ?? [], - }, - }), +// General string type. This should probably generally be avoided in favor +// of more specific validation, but using it makes it easy to find where we +// might want to improve later, and it's a useful shorthand meanwhile. +export function simpleString() { + return { + flags: {update: true, expose: true}, + update: {validate: isString}, + }; +} - // A reference list! Keep in mind this is for general references to wiki - // objects of (usually) other Thing subclasses, not specifically leitmotif - // references in tracks (although that property uses referenceList too!). - // - // The underlying function validateReferenceList expects a string like - // 'artist' or 'track', but this utility keeps from having to hard-code the - // string in multiple places by referencing the value saved on the class - // instead. - referenceList: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReferenceList(referenceType)}, - }; - }, +// External function. These should only be used as dependencies for other +// properties, so they're left unexposed. +export function externalFunction() { + return { + flags: {update: true}, + update: {validate: (t) => typeof t === 'function'}, + }; +} - // Corresponding function for a single reference. - singleReference: (thingClass) => { - const {[Thing.referenceType]: referenceType} = thingClass; - if (!referenceType) { - throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); - } - - return { - flags: {update: true, expose: true}, - update: {validate: validateReference(referenceType)}, - }; - }, +// Super simple "contributions by reference" list, used for a variety of +// properties (Artists, Cover Artists, etc). This is the property which is +// externally provided, in the form: +// +// [ +// {who: 'Artist Name', what: 'Viola'}, +// {who: 'artist:john-cena', what: null}, +// ... +// ] +// +// ...processed from YAML, spreadsheet, or any other kind of input. +export function contribsByRef() { + return { + flags: {update: true, expose: true}, + update: {validate: isContributionList}, + }; +} - // Corresponding dynamic property to referenceList, which takes the values - // in the provided property and searches the specified wiki data for - // matching actual Thing-subclass objects. - resolvedReferenceList({list, data, find}) { - return compositeFrom(`Thing.common.resolvedReferenceList`, [ - withResolvedReferenceList({ - list, data, find, - notFoundMode: 'filter', - }), +// Artist commentary! Generally present on tracks and albums. +export function commentary() { + return { + flags: {update: true, expose: true}, + update: {validate: isCommentary}, + }; +} - exposeDependency({dependency: '#resolvedReferenceList'}), - ]); +// This is a somewhat more involved data structure - it's for additional +// or "bonus" files associated with albums or tracks (or anything else). +// It's got this form: +// +// [ +// {title: 'Booklet', files: ['Booklet.pdf']}, +// { +// title: 'Wallpaper', +// description: 'Cool Wallpaper!', +// files: ['1440x900.png', '1920x1080.png'] +// }, +// {title: 'Alternate Covers', description: null, files: [...]}, +// ... +// ] +// +export function additionalFiles() { + return { + flags: {update: true, expose: true}, + update: {validate: isAdditionalFileList}, + expose: { + transform: (additionalFiles) => + additionalFiles ?? [], }, + }; +} - // Corresponding function for a single reference. - resolvedReference({ref, data, find}) { - return compositeFrom(`Thing.common.resolvedReference`, [ - withResolvedReference({ref, data, find}), - exposeDependency({dependency: '#resolvedReference'}), - ]); - }, +// A reference list! Keep in mind this is for general references to wiki +// objects of (usually) other Thing subclasses, not specifically leitmotif +// references in tracks (although that property uses referenceList too!). +// +// The underlying function validateReferenceList expects a string like +// 'artist' or 'track', but this utility keeps from having to hard-code the +// string in multiple places by referencing the value saved on the class +// instead. +export function referenceList(thingClass) { + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } - // Corresponding dynamic property to contribsByRef, which takes the values - // in the provided property and searches the object's artistData for - // matching actual Artist objects. The computed structure has the same form - // as contribsByRef, but with Artist objects instead of string references: - // - // [ - // {who: (an Artist), what: 'Viola'}, - // {who: (an Artist), what: null}, - // ... - // ] - // - // Contributions whose "who" values don't match anything in artistData are - // filtered out. (So if the list is all empty, chances are that either the - // reference list is somehow messed up, or artistData isn't being provided - // properly.) - dynamicContribs(contribsByRefProperty) { - return compositeFrom(`Thing.common.dynamicContribs`, [ - withResolvedContribs({ - from: contribsByRefProperty, - into: '#contribs', - }), + return { + flags: {update: true, expose: true}, + update: {validate: validateReferenceList(referenceType)}, + }; +} - exposeDependency({dependency: '#contribs'}), - ]); - }, +// Corresponding function for a single reference. +export function singleReference(thingClass) { + const {[Thing.referenceType]: referenceType} = thingClass; + if (!referenceType) { + throw new Error(`The passed constructor ${thingClass.name} doesn't define Thing.referenceType!`); + } - // Nice 'n simple shorthand for an exposed-only flag which is true when any - // contributions are present in the specified property. - contribsPresent: (contribsByRefProperty) => ({ - flags: {expose: true}, - expose: { - dependencies: [contribsByRefProperty], - compute({ - [contribsByRefProperty]: contribsByRef, - }) { - return !empty(contribsByRef); - }, - } + return { + flags: {update: true, expose: true}, + update: {validate: validateReference(referenceType)}, + }; +} + +// Corresponding dynamic property to referenceList, which takes the values +// in the provided property and searches the specified wiki data for +// matching actual Thing-subclass objects. +export function resolvedReferenceList({list, data, find}) { + return compositeFrom(`resolvedReferenceList`, [ + withResolvedReferenceList({ + list, data, find, + notFoundMode: 'filter', }), - // Neat little shortcut for "reversing" the reference lists stored on other - // things - for example, tracks specify a "referenced tracks" property, and - // you would use this to compute a corresponding "referenced *by* tracks" - // property. Naturally, the passed ref list property is of the things in the - // wiki data provided, not the requesting Thing itself. - reverseReferenceList({data, list}) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ - withReverseReferenceList({data, list}), - exposeDependency({dependency: '#reverseReferenceList'}), - ]); - }, + exposeDependency({dependency: '#resolvedReferenceList'}), + ]); +} - // General purpose wiki data constructor, for properties like artistData, - // trackData, etc. - wikiData: (thingClass) => ({ - flags: {update: true}, - update: { - validate: validateArrayItems(validateInstanceOf(thingClass)), - }, - }), +// Corresponding function for a single reference. +export function resolvedReference({ref, data, find}) { + return compositeFrom(`resolvedReference`, [ + withResolvedReference({ref, data, find}), + exposeDependency({dependency: '#resolvedReference'}), + ]); +} - // This one's kinda tricky: it parses artist "references" from the - // commentary content, and finds the matching artist for each reference. - // This is mostly useful for credits and listings on artist pages. - commentatorArtists: () => ({ - flags: {expose: true}, - - expose: { - dependencies: ['artistData', 'commentary'], - - compute: ({artistData, commentary}) => - artistData && commentary - ? Array.from( - new Set( - Array.from( - commentary - .replace(/<\/?b>/g, '') - .matchAll(/<i>(?<who>.*?):<\/i>/g) - ).map(({groups: {who}}) => - find.artist(who, artistData, {mode: 'quiet'}) - ) - ) - ) - : [], - }, +// Corresponding dynamic property to contribsByRef, which takes the values +// in the provided property and searches the object's artistData for +// matching actual Artist objects. The computed structure has the same form +// as contribsByRef, but with Artist objects instead of string references: +// +// [ +// {who: (an Artist), what: 'Viola'}, +// {who: (an Artist), what: null}, +// ... +// ] +// +// Contributions whose "who" values don't match anything in artistData are +// filtered out. (So if the list is all empty, chances are that either the +// reference list is somehow messed up, or artistData isn't being provided +// properly.) +export function dynamicContribs(contribsByRefProperty) { + return compositeFrom(`dynamicContribs`, [ + withResolvedContribs({ + from: contribsByRefProperty, + into: '#contribs', }), - }; - - // Default custom inspect function, which may be overridden by Thing - // subclasses. This will be used when displaying aggregate errors and other - // command-line logging - it's the place to provide information useful in - // identifying the Thing being presented. - [inspect.custom]() { - const cname = this.constructor.name; - return ( - (this.name ? `${cname} ${color.green(`"${this.name}"`)}` : `${cname}`) + - (this.directory ? ` (${color.blue(Thing.getReference(this))})` : '') - ); - } + exposeDependency({dependency: '#contribs'}), + ]); +} - static getReference(thing) { - if (!thing.constructor[Thing.referenceType]) { - throw TypeError(`Passed Thing is ${thing.constructor.name}, which provides no [Thing.referenceType]`); +// Nice 'n simple shorthand for an exposed-only flag which is true when any +// contributions are present in the specified property. +export function contribsPresent(contribsByRefProperty) { + return { + flags: {expose: true}, + expose: { + dependencies: [contribsByRefProperty], + compute({ + [contribsByRefProperty]: contribsByRef, + }) { + return !empty(contribsByRef); + }, } + }; +} - if (!thing.directory) { - throw TypeError(`Passed ${thing.constructor.name} is missing its directory`); - } +// Neat little shortcut for "reversing" the reference lists stored on other +// things - for example, tracks specify a "referenced tracks" property, and +// you would use this to compute a corresponding "referenced *by* tracks" +// property. Naturally, the passed ref list property is of the things in the +// wiki data provided, not the requesting Thing itself. +export function reverseReferenceList({data, list}) { + return compositeFrom(`reverseReferenceList`, [ + withReverseReferenceList({data, list}), + exposeDependency({dependency: '#reverseReferenceList'}), + ]); +} - return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; - } +// General purpose wiki data constructor, for properties like artistData, +// trackData, etc. +export function wikiData(thingClass) { + return { + flags: {update: true}, + update: { + validate: validateArrayItems(validateInstanceOf(thingClass)), + }, + }; } +// This one's kinda tricky: it parses artist "references" from the +// commentary content, and finds the matching artist for each reference. +// This is mostly useful for credits and listings on artist pages. +export function commentatorArtists(){ + return { + flags: {expose: true}, + + expose: { + dependencies: ['artistData', 'commentary'], + + compute: ({artistData, commentary}) => + artistData && commentary + ? Array.from( + new Set( + Array.from( + commentary + .replace(/<\/?b>/g, '') + .matchAll(/<i>(?<who>.*?):<\/i>/g) + ).map(({groups: {who}}) => + find.artist(who, artistData, {mode: 'quiet'}) + ) + ) + ) + : [], + }, + }; +} + +// Compositional utilities + // Resolves the contribsByRef contained in the provided dependency, // providing (named by the second argument) the result. "Resolving" // means mapping the "who" reference of each contribution to an artist @@ -479,14 +511,14 @@ export function withResolvedReferenceList({ ]); } -// Check out the info on Thing.common.reverseReferenceList! +// Check out the info on reverseReferenceList! // This is its composable form. export function withReverseReferenceList({ data, list: refListProperty, into = '#reverseReferenceList', }) { - return compositeFrom(`Thing.common.reverseReferenceList`, [ + return compositeFrom(`withReverseReferenceList`, [ exitWithoutDependency({ dependency: data, value: [], diff --git a/src/data/things/track.js b/src/data/things/track.js index 10b966a7..41c92092 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color} from '#cli'; +import {colors} from '#cli'; import find from '#find'; import {empty} from '#sugar'; import {isColor, isDate, isDuration, isFileExtension} from '#validators'; @@ -16,6 +16,23 @@ import { } from '#composite'; import Thing, { + additionalFiles, + commentary, + commentatorArtists, + contribsByRef, + directory, + dynamicContribs, + flag, + name, + referenceList, + resolvedReference, + resolvedReferenceList, + reverseReferenceList, + simpleDate, + singleReference, + simpleString, + urls, + wikiData, withResolvedContribs, withResolvedReference, withReverseReferenceList, @@ -27,24 +44,24 @@ export class Track extends Thing { static [Thing.getPropertyDescriptors] = ({Album, ArtTag, Artist, Flash}) => ({ // Update & expose - name: Thing.common.name('Unnamed Track'), - directory: Thing.common.directory(), + name: name('Unnamed Track'), + directory: directory(), duration: { flags: {update: true, expose: true}, update: {validate: isDuration}, }, - urls: Thing.common.urls(), - dateFirstReleased: Thing.common.simpleDate(), + urls: urls(), + dateFirstReleased: simpleDate(), - artistContribsByRef: Thing.common.contribsByRef(), - contributorContribsByRef: Thing.common.contribsByRef(), - coverArtistContribsByRef: Thing.common.contribsByRef(), + artistContribsByRef: contribsByRef(), + contributorContribsByRef: contribsByRef(), + coverArtistContribsByRef: contribsByRef(), - referencedTracksByRef: Thing.common.referenceList(Track), - sampledTracksByRef: Thing.common.referenceList(Track), - artTagsByRef: Thing.common.referenceList(ArtTag), + referencedTracksByRef: referenceList(Track), + sampledTracksByRef: referenceList(Track), + artTagsByRef: referenceList(ArtTag), color: compositeFrom(`Track.color`, [ exposeUpdateValueOrContinue(), @@ -74,7 +91,7 @@ export class Track extends Thing { // 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: Thing.common.flag(), + 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 @@ -117,27 +134,27 @@ export class Track extends Thing { }), ]), - originalReleaseTrackByRef: Thing.common.singleReference(Track), + originalReleaseTrackByRef: singleReference(Track), - dataSourceAlbumByRef: Thing.common.singleReference(Album), + dataSourceAlbumByRef: singleReference(Album), - commentary: Thing.common.commentary(), - lyrics: Thing.common.simpleString(), - additionalFiles: Thing.common.additionalFiles(), - sheetMusicFiles: Thing.common.additionalFiles(), - midiProjectFiles: Thing.common.additionalFiles(), + commentary: commentary(), + lyrics: simpleString(), + additionalFiles: additionalFiles(), + sheetMusicFiles: additionalFiles(), + midiProjectFiles: additionalFiles(), // Update only - albumData: Thing.common.wikiData(Album), - artistData: Thing.common.wikiData(Artist), - artTagData: Thing.common.wikiData(ArtTag), - flashData: Thing.common.wikiData(Flash), - trackData: Thing.common.wikiData(Track), + albumData: wikiData(Album), + artistData: wikiData(Artist), + artTagData: wikiData(ArtTag), + flashData: wikiData(Flash), + trackData: wikiData(Track), // Expose only - commentatorArtists: Thing.common.commentatorArtists(), + commentatorArtists: commentatorArtists(), album: compositeFrom(`Track.album`, [ withAlbum(), @@ -151,7 +168,7 @@ export class Track extends Thing { // not generally relevant information). It's also not guaranteed that // dataSourceAlbum is available (depending on the Track creator to optionally // provide dataSourceAlbumByRef). - dataSourceAlbum: Thing.common.resolvedReference({ + dataSourceAlbum: resolvedReference({ ref: 'dataSourceAlbumByRef', data: 'albumData', find: find.album, @@ -226,7 +243,7 @@ export class Track extends Thing { contributorContribs: compositeFrom(`Track.contributorContribs`, [ inheritFromOriginalRelease({property: 'contributorContribs'}), - Thing.common.dynamicContribs('contributorContribsByRef'), + dynamicContribs('contributorContribsByRef'), ]), // Cover artists aren't inherited from the original release, since it @@ -260,7 +277,7 @@ export class Track extends Thing { referencedTracks: compositeFrom(`Track.referencedTracks`, [ inheritFromOriginalRelease({property: 'referencedTracks'}), - Thing.common.resolvedReferenceList({ + resolvedReferenceList({ list: 'referencedTracksByRef', data: 'trackData', find: find.track, @@ -269,14 +286,14 @@ export class Track extends Thing { sampledTracks: compositeFrom(`Track.sampledTracks`, [ inheritFromOriginalRelease({property: 'sampledTracks'}), - Thing.common.resolvedReferenceList({ + resolvedReferenceList({ list: 'sampledTracksByRef', data: 'trackData', find: find.track, }), ]), - artTags: Thing.common.resolvedReferenceList({ + artTags: resolvedReferenceList({ list: 'artTagsByRef', data: 'artTagData', find: find.artTag, @@ -299,7 +316,7 @@ export class Track extends Thing { property: 'sampledTracks', }), - featuredInFlashes: Thing.common.reverseReferenceList({ + featuredInFlashes: reverseReferenceList({ data: 'flashData', list: 'featuredTracks', }), @@ -311,7 +328,7 @@ export class Track extends Thing { parts.push(Thing.prototype[inspect.custom].apply(this)); if (this.originalReleaseTrackByRef) { - parts.unshift(`${color.yellow('[rerelease]')} `); + parts.unshift(`${colors.yellow('[rerelease]')} `); } let album; @@ -322,7 +339,7 @@ export class Track extends Thing { (albumIndex === -1 ? '#?' : `#${albumIndex + 1}`); - parts.push(` (${color.yellow(trackNum)} in ${color.green(albumName)})`); + parts.push(` (${colors.yellow(trackNum)} in ${colors.green(albumName)})`); } return parts.join(''); diff --git a/src/data/things/validators.js b/src/data/things/validators.js index 5748eacf..4c8f683b 100644 --- a/src/data/things/validators.js +++ b/src/data/things/validators.js @@ -1,6 +1,6 @@ import {inspect as nodeInspect} from 'node:util'; -import {color, ENABLE_COLOR} from '#cli'; +import {colors, ENABLE_COLOR} from '#cli'; import {withAggregate} from '#sugar'; function inspect(value) { @@ -174,7 +174,7 @@ function validateArrayItemsHelper(itemValidator) { throw new Error(`Expected validator to return true`); } } catch (error) { - error.message = `(index: ${color.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; + error.message = `(index: ${colors.yellow(`#${index}`)}, item: ${inspect(item)}) ${error.message}`; throw error; } }; @@ -264,7 +264,7 @@ export function validateProperties(spec) { try { specValidator(value); } catch (error) { - error.message = `(key: ${color.green(specKey)}, value: ${inspect(value)}) ${error.message}`; + error.message = `(key: ${colors.green(specKey)}, value: ${inspect(value)}) ${error.message}`; throw error; } }); diff --git a/src/data/things/wiki-info.js b/src/data/things/wiki-info.js index 0ccef5ed..416b6c4e 100644 --- a/src/data/things/wiki-info.js +++ b/src/data/things/wiki-info.js @@ -1,13 +1,21 @@ import find from '#find'; import {isLanguageCode, isName, isURL} from '#validators'; -import Thing from './thing.js'; +import Thing, { + color, + flag, + name, + referenceList, + resolvedReferenceList, + simpleString, + wikiData, +} from './thing.js'; export class WikiInfo extends Thing { static [Thing.getPropertyDescriptors] = ({Group}) => ({ // Update & expose - name: Thing.common.name('Unnamed Wiki'), + name: name('Unnamed Wiki'), // Displayed in nav bar. nameShort: { @@ -20,12 +28,12 @@ export class WikiInfo extends Thing { }, }, - color: Thing.common.color(), + color: color(), // One-line description used for <meta rel="description"> tag. - description: Thing.common.simpleString(), + description: simpleString(), - footerContent: Thing.common.simpleString(), + footerContent: simpleString(), defaultLanguage: { flags: {update: true, expose: true}, @@ -37,22 +45,22 @@ export class WikiInfo extends Thing { update: {validate: isURL}, }, - divideTrackListsByGroupsByRef: Thing.common.referenceList(Group), + divideTrackListsByGroupsByRef: referenceList(Group), // Feature toggles - enableFlashesAndGames: Thing.common.flag(false), - enableListings: Thing.common.flag(false), - enableNews: Thing.common.flag(false), - enableArtTagUI: Thing.common.flag(false), - enableGroupUI: Thing.common.flag(false), + enableFlashesAndGames: flag(false), + enableListings: flag(false), + enableNews: flag(false), + enableArtTagUI: flag(false), + enableGroupUI: flag(false), // Update only - groupData: Thing.common.wikiData(Group), + groupData: wikiData(Group), // Expose only - divideTrackListsByGroups: Thing.common.resolvedReferenceList({ + divideTrackListsByGroups: resolvedReferenceList({ list: 'divideTrackListsByGroupsByRef', data: 'groupData', find: find.group, diff --git a/src/data/yaml.js b/src/data/yaml.js index 2ad2d41d..c0aad943 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -7,7 +7,7 @@ import {inspect as nodeInspect} from 'node:util'; import yaml from 'js-yaml'; -import {color, ENABLE_COLOR, logInfo, logWarn} from '#cli'; +import {colors, ENABLE_COLOR, logInfo, logWarn} from '#cli'; import find, {bindFind} from '#find'; import {traverse} from '#node-utils'; import T from '#things'; @@ -137,7 +137,7 @@ function makeProcessDocument( const name = document[nameField]; error.message = name ? `(name: ${inspect(name)}) ${error.message}` - : `(${color.dim(`no name found`)}) ${error.message}`; + : `(${colors.dim(`no name found`)}) ${error.message}`; throw error; } }; @@ -195,7 +195,7 @@ function makeProcessDocument( const thing = Reflect.construct(thingClass, []); - withAggregate({message: `Errors applying ${color.green(thingClass.name)} properties`}, ({call}) => { + withAggregate({message: `Errors applying ${colors.green(thingClass.name)} properties`}, ({call}) => { for (const [property, value] of Object.entries(sourceProperties)) { call(() => (thing[property] = value)); } @@ -228,7 +228,7 @@ makeProcessDocument.FieldCombinationsError = class FieldCombinationsError extend makeProcessDocument.FieldCombinationError = class FieldCombinationError extends Error { constructor(fields, message) { const fieldNames = Object.keys(fields); - const combinePart = `Don't combine ${fieldNames.map(field => color.red(field)).join(', ')}`; + const combinePart = `Don't combine ${fieldNames.map(field => colors.red(field)).join(', ')}`; const messagePart = (typeof message === 'function' @@ -1009,7 +1009,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { } catch (error) { error.message += (error.message.includes('\n') ? '\n' : ' ') + - `(file: ${color.bright(color.blue(path.relative(dataPath, x.file)))})`; + `(file: ${colors.bright(colors.blue(path.relative(dataPath, x.file)))})`; throw error; } }; @@ -1032,7 +1032,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { // just without the callbacks. Thank you. const filterBlankDocuments = documents => { const aggregate = openAggregate({ - message: `Found blank documents - check for extra '${color.cyan(`---`)}'`, + message: `Found blank documents - check for extra '${colors.cyan(`---`)}'`, }); const filteredDocuments = @@ -1076,10 +1076,10 @@ export async function loadAndProcessDataDocuments({dataPath}) { if (count === 1) { const range = `#${start + 1}`; - parts.push(`${count} document (${color.yellow(range)}), `); + parts.push(`${count} document (${colors.yellow(range)}), `); } else { const range = `#${start + 1}-${end + 1}`; - parts.push(`${count} documents (${color.yellow(range)}), `); + parts.push(`${count} documents (${colors.yellow(range)}), `); } if (previous === null) { @@ -1089,7 +1089,7 @@ export async function loadAndProcessDataDocuments({dataPath}) { } else { const previousDescription = Object.entries(previous).at(0).join(': '); const nextDescription = Object.entries(next).at(0).join(': '); - parts.push(`between "${color.cyan(previousDescription)}" and "${color.cyan(nextDescription)}"`); + parts.push(`between "${colors.cyan(previousDescription)}" and "${colors.cyan(nextDescription)}"`); } aggregate.push(new Error(parts.join(''))); @@ -1395,7 +1395,7 @@ export function filterDuplicateDirectories(wikiData) { const aggregate = openAggregate({message: `Duplicate directories found`}); for (const thingDataProp of deduplicateSpec) { const thingData = wikiData[thingDataProp]; - aggregate.nest({message: `Duplicate directories found in ${color.green('wikiData.' + thingDataProp)}`}, ({call}) => { + aggregate.nest({message: `Duplicate directories found in ${colors.green('wikiData.' + thingDataProp)}`}, ({call}) => { const directoryPlaces = Object.create(null); const duplicateDirectories = []; @@ -1421,7 +1421,7 @@ export function filterDuplicateDirectories(wikiData) { const places = directoryPlaces[directory]; call(() => { throw new Error( - `Duplicate directory ${color.green(directory)}:\n` + + `Duplicate directory ${colors.green(directory)}:\n` + places.map((thing) => ` - ` + inspect(thing)).join('\n') ); }); @@ -1516,7 +1516,7 @@ export function filterReferenceErrors(wikiData) { for (const [thingDataProp, providedProcessDocumentFn, propSpec] of referenceSpec) { const thingData = getNestedProp(wikiData, thingDataProp); - aggregate.nest({message: `Reference errors in ${color.green('wikiData.' + thingDataProp)}`}, ({nest}) => { + aggregate.nest({message: `Reference errors in ${colors.green('wikiData.' + thingDataProp)}`}, ({nest}) => { const things = Array.isArray(thingData) ? thingData : [thingData]; for (const thing of things) { @@ -1535,7 +1535,7 @@ export function filterReferenceErrors(wikiData) { const value = thing[property]; if (value === undefined) { - push(new TypeError(`Property ${color.red(property)} isn't valid for ${color.green(thing.constructor.name)}`)); + push(new TypeError(`Property ${colors.red(property)} isn't valid for ${colors.green(thing.constructor.name)}`)); continue; } @@ -1553,7 +1553,7 @@ export function filterReferenceErrors(wikiData) { // No need to check if the original exists here. Aliases are automatically // created from a field on the original, so the original certainly exists. const original = find.artist(alias.aliasedArtistRef, wikiData.artistData, {mode: 'quiet'}); - throw new Error(`Reference ${color.red(contribRef.who)} is to an alias, should be ${color.green(original.name)}`); + throw new Error(`Reference ${colors.red(contribRef.who)} is to an alias, should be ${colors.green(original.name)}`); } return boundFind.artist(contribRef.who); @@ -1578,12 +1578,12 @@ export function filterReferenceErrors(wikiData) { const shouldBeMessage = (originalByName - ? color.green(original.name) + ? colors.green(original.name) : original - ? color.green('track:' + original.directory) - : color.green(track.originalReleaseTrackByRef)); + ? colors.green('track:' + original.directory) + : colors.green(track.originalReleaseTrackByRef)); - throw new Error(`Reference ${color.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`); + throw new Error(`Reference ${colors.red(trackRef)} is to a rerelease, should be ${shouldBeMessage}`); } return track; @@ -1614,13 +1614,13 @@ export function filterReferenceErrors(wikiData) { const fieldPropertyMessage = (processDocumentFn?.propertyFieldMapping?.[property] - ? ` in field ${color.green(processDocumentFn.propertyFieldMapping[property])}` - : ` in property ${color.green(property)}`); + ? ` in field ${colors.green(processDocumentFn.propertyFieldMapping[property])}` + : ` in property ${colors.green(property)}`); const findFnMessage = (findFnKey.startsWith('_') ? `` - : ` (${color.green('find.' + findFnKey)})`); + : ` (${colors.green('find.' + findFnKey)})`); const errorMessage = (Array.isArray(value) diff --git a/src/find.js b/src/find.js index b8230800..5ad8dae7 100644 --- a/src/find.js +++ b/src/find.js @@ -1,6 +1,6 @@ import {inspect} from 'node:util'; -import {color, logWarn} from '#cli'; +import {colors, logWarn} from '#cli'; function warnOrThrow(mode, message) { if (mode === 'error') { @@ -66,7 +66,7 @@ function findHelper(keys, findFns = {}) { const found = key ? byDirectory(ref, data, mode) : byName(ref, data, mode); if (!found) { - warnOrThrow(mode, `Didn't match anything for ${color.bright(fullRef)}`); + warnOrThrow(mode, `Didn't match anything for ${colors.bright(fullRef)}`); } cacheForThisData[fullRef] = found; @@ -102,7 +102,7 @@ function matchName(ref, data, mode) { if (ref !== thing.name) { warnOrThrow( mode, - `Bad capitalization: ${color.red(ref)} -> ${color.green(thing.name)}` + `Bad capitalization: ${colors.red(ref)} -> ${colors.green(thing.name)}` ); } diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 741cdff3..fafd17f6 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -94,7 +94,7 @@ import * as path from 'node:path'; import dimensionsOf from 'image-size'; import { - color, + colors, fileIssue, logError, logInfo, @@ -662,14 +662,14 @@ export async function verifyImagePaths(mediaPath, {urls, wikiData}) { if (!empty(missing)) { logWarn`** Some image files are missing! (${missing.length + ' files'}) **`; for (const file of missing) { - console.warn(color.yellow(` - `) + file); + console.warn(colors.yellow(` - `) + file); } } if (!empty(misplaced)) { logWarn`** Some image files are misplaced! (${misplaced.length + ' files'}) **`; for (const file of misplaced) { - console.warn(color.yellow(` - `) + file); + console.warn(colors.yellow(` - `) + file); } } } diff --git a/src/upd8.js b/src/upd8.js index 2ec231c9..f6091ca2 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -47,7 +47,7 @@ import {generateURLs, urlSpec} from '#urls'; import {sortByName} from '#wiki-data'; import { - color, + colors, decorateTime, logWarn, logInfo, @@ -279,7 +279,7 @@ async function main() { const indentWrap = (spaces, str) => wrap(str, {width: 60 - spaces, indent: ' '.repeat(spaces)}); const showOptions = (msg, options) => { - console.log(color.bright(msg)); + console.log(colors.bright(msg)); const entries = Object.entries(options); const sortedOptions = sortByName(entries @@ -310,13 +310,13 @@ async function main() { console.log(''); } - console.log(color.bright(` --` + name) + + console.log(colors.bright(` --` + name) + (aliases.length - ? ` (or: ${aliases.map(alias => color.bright(`--` + alias)).join(', ')})` + ? ` (or: ${aliases.map(alias => colors.bright(`--` + alias)).join(', ')})` : '') + (descriptor.help ? '' - : color.dim(' (no help provided)'))); + : colors.dim(' (no help provided)'))); if (wrappedHelp) { console.log(wrappedHelp); @@ -336,7 +336,7 @@ async function main() { }; console.log( - color.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) + + colors.bright(`hsmusic (aka. Homestuck Music Wiki)\n`) + `static wiki software cataloguing collaborative creation\n`); console.log(indentWrap(0, @@ -496,7 +496,7 @@ async function main() { { const logThings = (thingDataProp, label) => - logInfo` - ${wikiData[thingDataProp]?.length ?? color.red('(Missing!)')} ${color.normal(color.dim(label))}`; + logInfo` - ${wikiData[thingDataProp]?.length ?? colors.red('(Missing!)')} ${colors.normal(colors.dim(label))}`; try { logInfo`Loaded data and processed objects:`; logThings('albumData', 'albums'); diff --git a/src/util/cli.js b/src/util/cli.js index e8c8c79f..9f2b35ab 100644 --- a/src/util/cli.js +++ b/src/util/cli.js @@ -17,7 +17,7 @@ export const ENABLE_COLOR = const C = (n) => ENABLE_COLOR ? (text) => `\x1b[${n}m${text}\x1b[0m` : (text) => text; -export const color = { +export const colors = { bright: C('1'), dim: C('2'), normal: C('22'), diff --git a/src/util/sugar.js b/src/util/sugar.js index 1ba3f3ae..ebb7d61e 100644 --- a/src/util/sugar.js +++ b/src/util/sugar.js @@ -6,7 +6,7 @@ // It will likely only do exactly what I want it to, and only in the cases I // decided were relevant enough to 8other handling. -import {color} from './cli.js'; +import {colors} from './cli.js'; // Apparently JavaScript doesn't come with a function to split an array into // chunks! Weird. Anyway, this is an awesome place to use a generator, even @@ -566,10 +566,10 @@ export function showAggregate(topError, { .trim() .replace(/file:\/\/.*\.js/, (match) => pathToFileURL(match)) : '(no stack trace)'; - header += ` ${color.dim(tracePart)}`; + header += ` ${colors.dim(tracePart)}`; } - const bar = level % 2 === 0 ? '\u2502' : color.dim('\u254e'); - const head = level % 2 === 0 ? '\u257f' : color.dim('\u257f'); + const bar = level % 2 === 0 ? '\u2502' : colors.dim('\u254e'); + const head = level % 2 === 0 ? '\u257f' : colors.dim('\u257f'); if (error instanceof AggregateError) { return ( @@ -605,7 +605,7 @@ export function decorateErrorWithIndex(fn) { try { return fn(x, index, array); } catch (error) { - error.message = `(${color.yellow(`#${index + 1}`)}) ${error.message}`; + error.message = `(${colors.yellow(`#${index + 1}`)}) ${error.message}`; throw error; } }; |