diff --git a/package.json b/package.json index c4b74c260..f0f2075c9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "#composite/wiki-data": "./src/data/composite/wiki-data/index.js", "#composite/wiki-properties": "./src/data/composite/wiki-properties/index.js", "#composite/things/album": "./src/data/composite/things/album/index.js", + "#composite/things/art-tag": "./src/data/composite/things/art-tag/index.js", "#composite/things/artist": "./src/data/composite/things/artist/index.js", "#composite/things/contribution": "./src/data/composite/things/contribution/index.js", "#composite/things/flash": "./src/data/composite/things/flash/index.js", diff --git a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js new file mode 100644 index 000000000..87b380590 --- /dev/null +++ b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js @@ -0,0 +1,128 @@ +import {filterMultipleArrays, sortMultipleArrays, stitchArrays} from '#sugar'; + +export default { + contentDependencies: ['linkArtTagDynamically'], + extraDependencies: ['html', 'language'], + + // Recursion ain't too pretty! + + query(ancestorArtTag, targetArtTag) { + const recursive = artTag => { + const artTags = + artTag.directDescendantArtTags.slice(); + + const displayBriefly = + !artTags.includes(targetArtTag) && + artTags.length > 3; + + const artTagsIncludeTargetArtTag = + artTags.map(artTag => artTag.allDescendantArtTags.includes(targetArtTag)); + + const numExemptArtTags = + (displayBriefly + ? artTagsIncludeTargetArtTag + .filter(includesTargetArtTag => !includesTargetArtTag) + .length + : null); + + const sublists = + stitchArrays({ + artTag: artTags, + includesTargetArtTag: artTagsIncludeTargetArtTag, + }).map(({artTag, includesTargetArtTag}) => + (includesTargetArtTag + ? recursive(artTag) + : null)); + + if (displayBriefly) { + filterMultipleArrays(artTags, sublists, + (artTag, sublist) => + artTag === targetArtTag || + sublist !== null); + } else { + sortMultipleArrays(artTags, sublists, + (artTagA, artTagB, sublistA, sublistB) => + (sublistA && sublistB + ? 0 + : !sublistA && !sublistB + ? 0 + : sublistA + ? 1 + : -1)); + } + + return { + displayBriefly, + numExemptArtTags, + artTags, + sublists, + }; + }; + + return {root: recursive(ancestorArtTag)}; + }, + + relations(relation, query, _ancestorArtTag, _targetArtTag) { + const recursive = ({artTags, sublists}) => ({ + artTagLinks: + artTags + .map(artTag => relation('linkArtTagDynamically', artTag)), + + sublists: + sublists + .map(sublist => (sublist ? recursive(sublist) : null)), + }); + + return {root: recursive(query.root)}; + }, + + data(query, _ancestorArtTag, targetArtTag) { + const recursive = ({displayBriefly, numExemptArtTags, artTags, sublists}) => ({ + displayBriefly, + numExemptArtTags, + + artTagsAreTargetTag: + artTags + .map(artTag => artTag === targetArtTag), + + sublists: + sublists + .map(sublist => (sublist ? recursive(sublist) : null)), + }); + + return {root: recursive(query.root)}; + }, + + generate(data, relations, {html, language}) { + const recursive = (dataNode, relationsNode) => + html.tag('dl', {class: dataNode === data.root && 'tree-list'}, [ + dataNode.displayBriefly && + html.tag('dt', + language.$('artTagPage.sidebar.otherTagsExempt', { + tags: + language.countArtTags(dataNode.numExemptArtTags, {unit: true}), + })), + + stitchArrays({ + isTargetTag: dataNode.artTagsAreTargetTag, + dataSublist: dataNode.sublists, + + artTagLink: relationsNode.artTagLinks, + relationsSublist: relationsNode.sublists, + }).map(({ + isTargetTag, dataSublist, + artTagLink, relationsSublist, + }) => [ + html.tag('dt', + {class: (dataSublist || isTargetTag) && 'current'}, + artTagLink), + + dataSublist && + html.tag('dd', + recursive(dataSublist, relationsSublist)), + ]), + ]); + + return recursive(data.root, relations.root); + }, +}; diff --git a/src/content/dependencies/generateArtTagGalleryPage.js b/src/content/dependencies/generateArtTagGalleryPage.js index c51faeba8..4d7f4feab 100644 --- a/src/content/dependencies/generateArtTagGalleryPage.js +++ b/src/content/dependencies/generateArtTagGalleryPage.js @@ -1,13 +1,18 @@ import {sortAlbumsTracksChronologically} from '#sort'; -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; export default { contentDependencies: [ + 'generateArtTagGalleryPageFeaturedLine', + 'generateArtTagGalleryPageShowingLine', + 'generateArtTagNavLinks', 'generateCoverGrid', 'generatePageLayout', + 'generateQuickDescription', 'image', 'linkAlbum', - 'linkArtTag', + 'linkArtTagGallery', + 'linkExternal', 'linkTrack', ], @@ -19,74 +24,114 @@ export default { }; }, - query(sprawl, tag) { - const things = tag.taggedInThings.slice(); + query(sprawl, artTag) { + const directThings = artTag.directlyTaggedInThings; + const indirectThings = artTag.indirectlyTaggedInThings; + const allThings = unique([...directThings, ...indirectThings]); - sortAlbumsTracksChronologically(things, { + sortAlbumsTracksChronologically(allThings, { getDate: thing => thing.coverArtDate ?? thing.date, latestFirst: true, }); - return {things}; + return {directThings, indirectThings, allThings}; }, - relations(relation, query, sprawl, tag) { + relations(relation, query, sprawl, artTag) { const relations = {}; relations.layout = relation('generatePageLayout'); - relations.artTagMainLink = - relation('linkArtTag', tag); + relations.navLinks = + relation('generateArtTagNavLinks', artTag); + + relations.quickDescription = + relation('generateQuickDescription', artTag); + + relations.featuredLine = + relation('generateArtTagGalleryPageFeaturedLine'); + + relations.showingLine = + relation('generateArtTagGalleryPageShowingLine'); + + if (!empty(artTag.extraReadingURLs)) { + relations.extraReadingLinks = + artTag.extraReadingURLs + .map(url => relation('linkExternal', url)); + } + + if (!empty(artTag.directAncestorArtTags)) { + relations.ancestorLinks = + artTag.directAncestorArtTags + .map(artTag => relation('linkArtTagGallery', artTag)); + } + + if (!empty(artTag.directDescendantArtTags)) { + relations.descendantLinks = + artTag.directDescendantArtTags + .map(artTag => relation('linkArtTagGallery', artTag)); + } relations.coverGrid = relation('generateCoverGrid'); relations.links = - query.things.map(thing => - (thing.album - ? relation('linkTrack', thing) - : relation('linkAlbum', thing))); + query.allThings + .map(thing => + (thing.album + ? relation('linkTrack', thing) + : relation('linkAlbum', thing))); relations.images = - query.things.map(thing => - relation('image', thing.artTags)); + query.allThings + .map(thing => relation('image', thing.artTags)); return relations; }, - data(query, sprawl, tag) { + data(query, sprawl, artTag) { const data = {}; data.enableListings = sprawl.enableListings; - data.name = tag.name; - data.color = tag.color; + data.name = artTag.name; + data.color = artTag.color; - data.numArtworks = query.things.length; + data.numArtworksIndirectly = query.indirectThings.length; + data.numArtworksDirectly = query.directThings.length; + data.numArtworksTotal = query.allThings.length; data.names = - query.things.map(thing => thing.name); + query.allThings.map(thing => thing.name); data.paths = - query.things.map(thing => + query.allThings.map(thing => (thing.album ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] : ['media.albumCover', thing.directory, thing.coverArtFileExtension])); data.dimensions = - query.things.map(thing => thing.coverArtDimensions); + query.allThings.map(thing => thing.coverArtDimensions); data.coverArtists = - query.things.map(thing => + query.allThings.map(thing => thing.coverArtistContribs .map(({artist}) => artist.name)); + data.onlyFeaturedIndirectly = + query.allThings.map(thing => + !query.directThings.includes(thing)); + + data.hasMixedDirectIndirect = + data.onlyFeaturedIndirectly.includes(true) && + data.onlyFeaturedIndirectly.includes(false); + return data; }, generate: (data, relations, {html, language}) => - language.encapsulate('tagPage', pageCapsule => + language.encapsulate('artTagGalleryPage', pageCapsule => relations.layout.slots({ title: language.$(pageCapsule, 'title', { @@ -99,17 +144,71 @@ export default { mainClasses: ['top-index'], mainContent: [ - html.tag('p', {class: 'quick-info'}, - language.$(pageCapsule, 'infoLine', { - coverArts: language.countCoverArts(data.numArtworks, { - unit: true, + relations.quickDescription.slots({ + extraReadingLinks: relations.extraReadingLinks ?? null, + }), + + data.numArtworksTotal === 0 && + html.tag('p', {class: 'quick-info'}, + language.encapsulate(pageCapsule, 'featuredLine.notFeatured', capsule => [ + language.$(capsule), + html.tag('br'), + language.$(capsule, 'callToAction'), + ])), + + relations.featuredLine.clone() + .slots({ + showing: 'all', + count: data.numArtworksTotal, + }), + + data.hasMixedDirectIndirect && [ + relations.featuredLine.clone() + .slots({ + showing: 'direct', + count: data.numArtworksDirectly, + }), + + relations.featuredLine.clone() + .slots({ + showing: 'indirect', + count: data.numArtworksIndirectly, }), - })), + ], + + relations.ancestorLinks && + html.tag('p', {class: 'quick-info'}, + language.$(pageCapsule, 'descendsFrom', { + tags: language.formatConjunctionList(relations.ancestorLinks), + })), + + relations.descendantLinks && + html.tag('p', {class: 'quick-info'}, + language.$(pageCapsule, 'descendants', { + tags: language.formatUnitList(relations.descendantLinks), + })), + + data.hasMixedDirectIndirect && [ + relations.showingLine.clone() + .slot('showing', 'all'), + + relations.showingLine.clone() + .slot('showing', 'direct'), + + relations.showingLine.clone() + .slot('showing', 'indirect'), + ], relations.coverGrid .slots({ links: relations.links, names: data.names, + lazy: 12, + + classes: + data.onlyFeaturedIndirectly.map(onlyFeaturedIndirectly => + (onlyFeaturedIndirectly ? 'featured-indirectly' : '')), + images: stitchArrays({ image: relations.images, @@ -132,21 +231,9 @@ export default { ], navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - - data.enableListings && - { - path: ['localized.listingIndex'], - title: language.$('listingIndex.title'), - }, - - { - html: - language.$(pageCapsule, 'nav.tag', { - tag: relations.artTagMainLink, - }), - }, - ], + navLinks: + relations.navLinks + .slot('currentExtra', 'gallery') + .content, })), }; diff --git a/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js new file mode 100644 index 000000000..b4620fa41 --- /dev/null +++ b/src/content/dependencies/generateArtTagGalleryPageFeaturedLine.js @@ -0,0 +1,23 @@ +export default { + extraDependencies: ['html', 'language'], + + slots: { + showing: { + validate: v => v.is('all', 'direct', 'indirect'), + }, + + count: {type: 'number'}, + }, + + generate: (slots, {html, language}) => + language.encapsulate('artTagGalleryPage', pageCapsule => + html.tag('p', {class: 'quick-info'}, + {id: `featured-${slots.showing}-line`}, + + language.$(pageCapsule, 'featuredLine', slots.showing, { + coverArts: + language.countArtworks(slots.count, { + unit: true, + }), + }))), +}; diff --git a/src/content/dependencies/generateArtTagGalleryPageShowingLine.js b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js new file mode 100644 index 000000000..6df4d0e53 --- /dev/null +++ b/src/content/dependencies/generateArtTagGalleryPageShowingLine.js @@ -0,0 +1,22 @@ +export default { + extraDependencies: ['html', 'language'], + + slots: { + showing: { + validate: v => v.is('all', 'direct', 'indirect'), + }, + + count: {type: 'number'}, + }, + + generate: (slots, {html, language}) => + language.encapsulate('artTagGalleryPage', pageCapsule => + html.tag('p', {class: 'quick-info'}, + {id: `showing-${slots.showing}-line`}, + + language.$(pageCapsule, 'showingLine', { + showing: + html.tag('a', {href: '#'}, + language.$(pageCapsule, 'showingLine', slots.showing)), + }))), +}; diff --git a/src/content/dependencies/generateArtTagInfoPage.js b/src/content/dependencies/generateArtTagInfoPage.js new file mode 100644 index 000000000..724639873 --- /dev/null +++ b/src/content/dependencies/generateArtTagInfoPage.js @@ -0,0 +1,243 @@ +import {empty, stitchArrays, unique} from '#sugar'; + +export default { + contentDependencies: [ + 'generateArtTagNavLinks', + 'generateArtTagSidebar', + 'generateContentHeading', + 'generatePageLayout', + 'linkArtTagGallery', + 'linkArtTagInfo', + 'linkExternal', + 'transformContent', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({wikiInfo}) => ({ + enableListings: wikiInfo.enableListings, + }), + + query(sprawl, artTag) { + const query = {}; + + query.directThings = + artTag.directlyTaggedInThings; + + query.indirectThings = + artTag.indirectlyTaggedInThings; + + query.allThings = + unique([...query.directThings, ...query.indirectThings]); + + query.allDescendantsHaveMoreDescendants = + artTag.directDescendantArtTags + .every(descendant => !empty(descendant.directDescendantArtTags)); + + return query; + }, + + relations: (relation, query, sprawl, artTag) => ({ + layout: + relation('generatePageLayout'), + + navLinks: + relation('generateArtTagNavLinks', artTag), + + sidebar: + relation('generateArtTagSidebar', artTag), + + contentHeading: + relation('generateContentHeading'), + + description: + relation('transformContent', artTag.description), + + galleryLink: + (empty(query.allThings) + ? null + : relation('linkArtTagGallery', artTag)), + + extraReadingLinks: + artTag.extraReadingURLs + .map(url => relation('linkExternal', url)), + + directAncestorLinks: + artTag.directAncestorArtTags + .map(artTag => relation('linkArtTagInfo', artTag)), + + directDescendantInfoLinks: + artTag.directDescendantArtTags + .map(artTag => relation('linkArtTagInfo', artTag)), + + directDescendantGalleryLinks: + artTag.directDescendantArtTags.map(artTag => + (query.allDescendantsHaveMoreDescendants + ? null + : relation('linkArtTagInfo', artTag))), + }), + + data: (query, sprawl, artTag) => ({ + enableListings: + sprawl.enableListings, + + name: + artTag.name, + + color: + artTag.color, + + numArtworksIndirectly: + query.indirectThings.length, + + numArtworksDirectly: + query.directThings.length, + + numArtworksTotal: + query.allThings.length, + + names: + query.allThings.map(thing => thing.name), + + paths: + query.allThings.map(thing => + (thing.album + ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] + : ['media.albumCover', thing.directory, thing.coverArtFileExtension])), + + onlyFeaturedIndirectly: + query.allThings.map(thing => + !query.directThings.includes(thing)), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('artTagInfoPage', pageCapsule => + relations.layout.slots({ + title: + language.$(pageCapsule, 'title', { + tag: language.sanitize(data.name), + }), + + headingMode: 'static', + color: data.color, + + mainContent: [ + html.tag('p', + language.encapsulate(pageCapsule, 'featuredIn', capsule => + (data.numArtworksTotal === 0 + ? language.$(capsule, 'notFeatured') + + : data.numArtworksDirectly === 0 + ? language.$(capsule, 'indirectlyOnly', { + artworks: + language.countArtworks(data.numArtworksIndirectly, {unit: true}), + }) + + : data.numArtworksIndirectly === 0 + ? language.$(capsule, 'directlyOnly', { + artworks: + language.countArtworks(data.numArtworksDirectly, {unit: true}), + }) + + : language.$(capsule, 'directlyAndIndirectly', { + artworksDirectly: + language.countArtworks(data.numArtworksDirectly, {unit: true}), + + artworksIndirectly: + language.countArtworks(data.numArtworksIndirectly, {unit: false}), + + artworksTotal: + language.countArtworks(data.numArtworksTotal, {unit: false}), + })))), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$(pageCapsule, 'viewArtGallery', { + [language.onlyIfOptions]: ['link'], + + link: + relations.galleryLink + ?.slot('content', language.$(pageCapsule, 'viewArtGallery.link')), + })), + + html.tag('blockquote', + {[html.onlyIfContent]: true}, + + relations.description + .slot('mode', 'multiline')), + + html.tag('p', + {[html.onlyIfContent]: true}, + + language.$(pageCapsule, 'readMoreOn', { + [language.onlyIfOptions]: ['links'], + + tag: language.sanitize(data.name), + links: language.formatDisjunctionList(relations.extraReadingLinks), + })), + + language.encapsulate(pageCapsule, 'descendsFromTags', listCapsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + title: + language.$(listCapsule, { + tag: language.sanitize(data.name), + }), + }), + + html.tag('ul', + {[html.onlyIfContent]: true}, + + relations.directAncestorLinks + .map(link => + html.tag('li', + language.$(listCapsule, 'item', { + tag: link, + })))), + ])), + + language.encapsulate(pageCapsule, 'descendantTags', listCapsule => + html.tags([ + relations.contentHeading.clone() + .slots({ + title: + language.$(listCapsule, { + tag: language.sanitize(data.name), + }), + }), + + html.tag('ul', + {[html.onlyIfContent]: true}, + + stitchArrays({ + infoLink: relations.directDescendantInfoLinks, + galleryLink: relations.directDescendantGalleryLinks, + }).map(({infoLink, galleryLink}) => + html.tag('li', + language.encapsulate(listCapsule, 'item', itemCapsule => + language.encapsulate(itemCapsule, workingCapsule => { + const workingOptions = {}; + + workingOptions.tag = infoLink; + + if (!html.isBlank(galleryLink)) { + workingCapsule += '.withGallery'; + workingOptions.gallery = + galleryLink.slot('content', + language.$(itemCapsule, 'withGallery.gallery')); + } + + return language.$(workingCapsule, workingOptions); + }))))), + ])), + ], + + navLinkStyle: 'hierarchical', + navLinks: relations.navLinks.content, + + leftSidebar: + relations.sidebar, + })), +}; diff --git a/src/content/dependencies/generateArtTagNavLinks.js b/src/content/dependencies/generateArtTagNavLinks.js new file mode 100644 index 000000000..34f95f6ea --- /dev/null +++ b/src/content/dependencies/generateArtTagNavLinks.js @@ -0,0 +1,80 @@ +import {empty} from '#sugar'; + +export default { + contentDependencies: [ + 'linkArtTagInfo', + 'linkArtTagGallery', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({wikiInfo}) => + ({enableListings: wikiInfo.enableListings}), + + relations: (relation, sprawl, tag) => ({ + mainLink: + relation('linkArtTagInfo', tag), + + infoLink: + relation('linkArtTagInfo', tag), + + galleryLink: + relation('linkArtTagGallery', tag), + }), + + data: (sprawl) => + ({enableListings: sprawl.enableListings}), + + slots: { + currentExtra: { + validate: v => v.is('gallery'), + }, + }, + + generate(data, relations, slots, {language}) { + if (!data.enableListings) { + return [ + {auto: 'home'}, + {auto: 'current'}, + ]; + } + + const infoLink = + relations.infoLink.slots({ + attributes: {class: slots.currentExtra === null && 'current'}, + content: language.$('misc.nav.info'), + }); + + const extraLinks = [ + relations.galleryLink?.slots({ + attributes: {class: slots.currentExtra === 'gallery' && 'current'}, + content: language.$('misc.nav.gallery'), + }), + ]; + + const extrasPart = + (empty(extraLinks) + ? '' + : language.formatUnitList([infoLink, ...extraLinks])); + + const accent = `(${extrasPart})`; + + return [ + {auto: 'home'}, + + data.enableListings && + { + path: ['localized.listingIndex'], + title: language.$('listingIndex.title'), + }, + + { + accent, + html: + language.$('artTagPage.nav.tag', { + tag: relations.mainLink, + }), + }, + ].filter(Boolean); + }, +}; diff --git a/src/content/dependencies/generateArtTagSidebar.js b/src/content/dependencies/generateArtTagSidebar.js new file mode 100644 index 000000000..abee1b6a2 --- /dev/null +++ b/src/content/dependencies/generateArtTagSidebar.js @@ -0,0 +1,108 @@ +import {collectTreeLeaves, empty, stitchArrays} from '#sugar'; + +export default { + contentDependencies: [ + 'generatePageSidebar', + 'generatePageSidebarBox', + 'generateArtTagAncestorDescendantMapList', + 'linkArtTagDynamically', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({artTagData}) => + ({artTagData}), + + query(sprawl, artTag) { + const baobab = artTag.ancestorArtTagBaobabTree; + const uniqueLeaves = new Set(collectTreeLeaves(baobab)); + + // Just match the order in tag data. + const furthestAncestorArtTags = + sprawl.artTagData + .filter(artTag => uniqueLeaves.has(artTag)); + + return {furthestAncestorArtTags}; + }, + + relations: (relation, query, sprawl, artTag) => ({ + sidebar: + relation('generatePageSidebar'), + + sidebarBox: + relation('generatePageSidebarBox'), + + artTagLink: + relation('linkArtTagDynamically', artTag), + + directDescendantArtTagLinks: + artTag.directDescendantArtTags + .map(descendantArtTag => + relation('linkArtTagDynamically', descendantArtTag)), + + furthestAncestorArtTagMapLists: + query.furthestAncestorArtTags + .map(ancestorArtTag => + relation('generateArtTagAncestorDescendantMapList', + ancestorArtTag, + artTag)), + }), + + data: (query, sprawl, artTag) => ({ + name: artTag.name, + + furthestAncestorArtTagNames: + query.furthestAncestorArtTags + .map(ancestorArtTag => ancestorArtTag.name), + }), + + generate(data, relations, {html, language}) { + if ( + empty(relations.directDescendantArtTagLinks) && + empty(relations.furthestAncestorArtTagMapLists) + ) { + return relations.sidebar; + } + + return relations.sidebar.slots({ + boxes: [ + relations.sidebarBox.slots({ + content: [ + html.tag('h1', + relations.artTagLink), + + !empty(relations.directDescendantArtTagLinks) && + html.tag('details', {class: 'current', open: true}, [ + html.tag('summary', + html.tag('span', {class: 'group-name'}, + language.sanitize(data.name))), + + html.tag('ul', + relations.directDescendantArtTagLinks + .map(link => html.tag('li', link))), + ]), + + stitchArrays({ + name: data.furthestAncestorArtTagNames, + list: relations.furthestAncestorArtTagMapLists, + }).map(({name, list}) => + html.tag('details', + { + class: 'has-tree-list', + open: + empty(relations.directDescendantArtTagLinks) && + relations.furthestAncestorArtTagMapLists.length === 1, + }, + [ + html.tag('summary', + html.tag('span', {class: 'group-name'}, + language.sanitize(name))), + + list, + ])), + ], + }), + ], + }); + }, +}; diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index 3d5a614fc..831c873d5 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -1,7 +1,7 @@ import {stitchArrays} from '#sugar'; export default { - contentDependencies: ['image', 'linkArtTag'], + contentDependencies: ['image', 'linkArtTagGallery'], extraDependencies: ['html'], query: (artTags) => ({ @@ -15,10 +15,10 @@ export default { image: relation('image', artTags), - tagLinks: + artTagLinks: query.linkableArtTags .filter(tag => !tag.isContentWarning) - .map(tag => relation('linkArtTag', tag)), + .map(tag => relation('linkArtTagGallery', tag)), }), data: (query) => { @@ -93,11 +93,11 @@ export default { {[html.onlyIfContent]: true}, stitchArrays({ - tagLink: relations.tagLinks, + artTagLink: relations.artTagLinks, preferShortName: data.preferShortName, - }).map(({tagLink, preferShortName}) => + }).map(({artTagLink, preferShortName}) => html.tag('li', - tagLink.slot('preferShortName', preferShortName)))), + artTagLink.slot('preferShortName', preferShortName)))), ]); case 'thumbnail': diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 0433aaf1f..8a0224d65 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -16,6 +16,19 @@ export default { names: {validate: v => v.strictArrayOf(v.isHTML)}, info: {validate: v => v.strictArrayOf(v.isHTML)}, + // Differentiating from sparseArrayOf here - this list of classes should + // have the same length as the items above, i.e. nulls aren't going to be + // filtered out of it, but it is okay to *include* null (standing in for + // no classes for this grid item). + classes: { + validate: v => + v.strictArrayOf( + v.optional( + v.anyOf( + v.isArray, + v.isString))), + }, + lazy: {validate: v => v.anyOf(v.isWholeNumber, v.isBoolean)}, actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, @@ -24,14 +37,23 @@ export default { return ( html.tag('div', {class: 'grid-listing'}, [ stitchArrays({ + classes: slots.classes, image: slots.images, link: slots.links, name: slots.names, info: slots.info, - }).map(({image, link, name, info}, index) => + }).map(({classes, image, link, name, info}, index) => link.slots({ - attributes: {class: ['grid-item', 'box']}, + attributes: [ + {class: ['grid-item', 'box']}, + + (classes + ? {class: classes} + : null), + ], + colorContext: 'image-box', + content: [ image.slots({ thumb: 'medium', diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index b1f02819a..d2ca50e62 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -27,8 +27,8 @@ export default { if (artTags) { data.contentWarnings = artTags - .filter(tag => tag.isContentWarning) - .map(tag => tag.name); + .filter(artTag => artTag.isContentWarning) + .map(artTag => artTag.name); } else { data.contentWarnings = null; } diff --git a/src/content/dependencies/linkArtTagDynamically.js b/src/content/dependencies/linkArtTagDynamically.js new file mode 100644 index 000000000..964258e13 --- /dev/null +++ b/src/content/dependencies/linkArtTagDynamically.js @@ -0,0 +1,14 @@ +export default { + contentDependencies: ['linkArtTagGallery', 'linkArtTagInfo'], + extraDependencies: ['pagePath'], + + relations: (relation, artTag) => ({ + galleryLink: relation('linkArtTagGallery', artTag), + infoLink: relation('linkArtTagInfo', artTag), + }), + + generate: (relations, {pagePath}) => + (pagePath[0] === 'artTagInfo' + ? relations.infoLink + : relations.galleryLink), +}; diff --git a/src/content/dependencies/linkArtTagGallery.js b/src/content/dependencies/linkArtTagGallery.js new file mode 100644 index 000000000..a92b69c1c --- /dev/null +++ b/src/content/dependencies/linkArtTagGallery.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, artTag) => + ({link: relation('linkThing', 'localized.artTagGallery', artTag)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkArtTag.js b/src/content/dependencies/linkArtTagInfo.js similarity index 66% rename from src/content/dependencies/linkArtTag.js rename to src/content/dependencies/linkArtTagInfo.js index 7ddb7786f..409cb3c05 100644 --- a/src/content/dependencies/linkArtTag.js +++ b/src/content/dependencies/linkArtTagInfo.js @@ -2,7 +2,7 @@ export default { contentDependencies: ['linkThing'], relations: (relation, artTag) => - ({link: relation('linkThing', 'localized.tag', artTag)}), + ({link: relation('linkThing', 'localized.artTagInfo', artTag)}), generate: (relations) => relations.link, }; diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js index b3a547471..5386dcdc2 100644 --- a/src/content/dependencies/listArtTagNetwork.js +++ b/src/content/dependencies/listArtTagNetwork.js @@ -1 +1,366 @@ -export default {generate() {}}; +import {sortAlphabetically} from '#sort'; +import {empty, stitchArrays, unique} from '#sugar'; + +export default { + contentDependencies: ['generateListingPage', 'linkArtTagInfo'], + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl({artTagData}) { + return {artTagData}; + }, + + query(sprawl, spec) { + const artTags = + sprawl.artTagData.filter(artTag => !artTag.isContentWarning); + + const rootArtTags = + artTags + .filter(artTag => !empty(artTag.directDescendantArtTags)) + .filter(artTag => + empty(artTag.directAncestorArtTags) || + artTag.directAncestorArtTags.length >= 2); + + sortAlphabetically(rootArtTags); + + rootArtTags.sort( + ({directAncestorArtTags: ancestorsA}, + {directAncestorArtTags: ancestorsB}) => + ancestorsA.length - ancestorsB.length); + + const getStats = (artTag) => ({ + directUses: + artTag.directlyTaggedInThings.length, + + // Not currently displayed + directAndIndirectUses: + unique([ + ...artTag.indirectlyTaggedInThings, + ...artTag.directlyTaggedInThings, + ]).length, + + totalUses: + [ + ...artTag.directlyTaggedInThings, + ... + artTag.allDescendantArtTags + .flatMap(artTag => artTag.directlyTaggedInThings), + ].length, + + descendants: + artTag.allDescendantArtTags.length, + + leaves: + (empty(artTag.directDescendantArtTags) + ? null + : artTag.allDescendantArtTags + .filter(artTag => empty(artTag.directDescendantArtTags)) + .length), + }); + + const recursive = (artTag, depth) => { + const descendantNodes = + (empty(artTag.directDescendantArtTags) + ? null + : depth > 0 && artTag.directAncestorArtTags.length >= 2 + ? null + : artTag.directDescendantArtTags + .map(artTag => recursive(artTag, depth + 1))); + + descendantNodes?.sort( + ({descendantNodes: descendantNodesA}, + {descendantNodes: descendantNodesB}) => + (descendantNodesA ? 1 : 0) + - (descendantNodesB ? 1 : 0)); + + const recursiveGetRootAncestor = ancestorArtTag => + (ancestorArtTag.directAncestorArtTags.length === 1 + ? recursiveGetRootAncestor(ancestorArtTag.directAncestorArtTags[0]) + : ancestorArtTag); + + const ancestorRootArtTags = + (depth === 0 && !empty(artTag.directAncestorArtTags) + ? unique(artTag.directAncestorArtTags.map(recursiveGetRootAncestor)) + : null); + + const stats = getStats(artTag); + + return { + artTag, + stats, + descendantNodes, + ancestorRootArtTags, + }; + }; + + const uppermostRootTags = + artTags + .filter(artTag => !empty(artTag.directDescendantArtTags)) + .filter(artTag => empty(artTag.directAncestorArtTags)); + + const orphanArtTags = + artTags + .filter(artTag => empty(artTag.directDescendantArtTags)) + .filter(artTag => empty(artTag.directAncestorArtTags)); + + return { + spec, + + rootNodes: + rootArtTags + .map(artTag => recursive(artTag, 0)), + + uppermostRootTags, + orphanArtTags, + }; + }, + + relations(relation, query) { + const recursive = queryNode => ({ + artTagLink: + relation('linkArtTagInfo', queryNode.artTag), + + ancestorTagLinks: + queryNode.ancestorRootArtTags + ?.map(artTag => relation('linkArtTagInfo', artTag)) + ?? null, + + descendantNodes: + queryNode.descendantNodes + ?.map(recursive) + ?? null, + }); + + return { + page: + relation('generateListingPage', query.spec), + + rootNodes: + query.rootNodes.map(recursive), + + uppermostRootTagLinks: + query.uppermostRootTags + .map(artTag => relation('linkArtTagInfo', artTag)), + + orphanArtTagLinks: + query.orphanArtTags + .map(artTag => relation('linkArtTagInfo', artTag)), + }; + }, + + data(query) { + const rootArtTags = query.rootNodes.map(({artTag}) => artTag); + + const recursive = queryNode => ({ + directory: + queryNode.artTag.directory, + + directUses: + queryNode.stats.directUses, + + totalUses: + queryNode.stats.totalUses, + + descendants: + queryNode.stats.descendants, + + leaves: + queryNode.stats.leaves, + + representsRoot: + rootArtTags.includes(queryNode.artTag), + + ancestorTagDirectories: + queryNode.ancestorRootArtTags + ?.map(artTag => artTag.directory) + ?? null, + + descendantNodes: + queryNode.descendantNodes + ?.map(recursive) + ?? null, + }); + + return { + rootNodes: + query.rootNodes.map(recursive), + + uppermostRootTagDirectories: + query.uppermostRootTags + .map(artTag => artTag.directory), + }; + }, + + generate(data, relations, {html, language}) { + const prefix = `listingPage.listArtTags.network`; + + const wrapTagWithJumpTo = (dataNode, relationsNode, depth) => + (depth === 0 + ? relationsNode.artTagLink + : dataNode.representsRoot + ? language.$(prefix, 'tag.jumpToRoot', { + tag: + relationsNode.artTagLink.slots({ + anchor: true, + hash: dataNode.directory, + }), + }) + : relationsNode.artTagLink); + + const wrapTagWithStats = (dataNode, relationsNode, depth) => [ + html.tag('span', {class: 'network-tag'}, + language.$(prefix, 'tag', { + tag: + wrapTagWithJumpTo(dataNode, relationsNode, depth), + })), + + html.tag('span', {class: 'network-tag'}, + {class: 'with-stat'}, + {style: 'display: none'}, + + language.$(prefix, 'tag.withStat', { + tag: + wrapTagWithJumpTo(dataNode, relationsNode, depth), + + stat: + html.tag('span', {class: 'network-tag-stat'}, + language.$(prefix, 'tag.withStat.stat', { + stat: [ + html.tag('span', {class: 'network-tag-direct-uses-stat'}, + dataNode.directUses.toString()), + + html.tag('span', {class: 'network-tag-total-uses-stat'}, + dataNode.totalUses.toString()), + + html.tag('span', {class: 'network-tag-descendants-stat'}, + dataNode.descendants.toString()), + + html.tag('span', {class: 'network-tag-leaves-stat'}, + (dataNode.leaves === null + ? language.$(prefix, 'tag.withStat.notApplicable') + : dataNode.leaves.toString())), + ], + })), + })) + ]; + + const recursive = (dataNode, relationsNode, depth) => [ + html.tag('dt', + { + id: depth === 0 && dataNode.directory, + class: depth % 2 === 0 ? 'even' : 'odd', + }, + + (depth === 0 + ? (relationsNode.ancestorTagLinks + ? language.$(prefix, 'root.withAncestors', { + tag: + wrapTagWithStats(dataNode, relationsNode, depth), + + ancestors: + language.formatUnitList( + stitchArrays({ + link: relationsNode.ancestorTagLinks, + directory: dataNode.ancestorTagDirectories, + }).map(({link, directory}) => + link.slots({ + anchor: true, + hash: directory, + }))), + }) + : language.$(prefix, 'root.jumpToTop', { + tag: + wrapTagWithStats(dataNode, relationsNode, depth), + + link: + html.tag('a', {href: '#top'}, + language.$(prefix, 'root.jumpToTop.link')), + })) + : wrapTagWithStats(dataNode, relationsNode, depth))), + + dataNode.descendantNodes && + relationsNode.descendantNodes && + html.tag('dd', + {class: depth % 2 === 0 ? 'even' : 'odd'}, + html.tag('dl', + stitchArrays({ + dataNode: dataNode.descendantNodes, + relationsNode: relationsNode.descendantNodes, + }).map(({dataNode, relationsNode}) => + recursive(dataNode, relationsNode, depth + 1)))), + ]; + + return relations.page.slots({ + type: 'custom', + + content: [ + html.tag('p', {id: 'network-stat-line'}, + language.$(prefix, 'statLine', { + stat: [ + html.tag('a', {id: 'network-stat-none'}, + {href: '#'}, + language.$(prefix, 'statLine.none')), + + html.tag('a', {id: 'network-stat-total-uses'}, + {href: '#'}, + {style: 'display: none'}, + language.$(prefix, 'statLine.totalUses')), + + html.tag('a', {id: 'network-stat-direct-uses'}, + {href: '#'}, + {style: 'display: none'}, + language.$(prefix, 'statLine.directUses')), + + html.tag('a', {id: 'network-stat-descendants'}, + {href: '#'}, + {style: 'display: none'}, + language.$(prefix, 'statLine.descendants')), + + html.tag('a', {id: 'network-stat-leaves'}, + {href: '#'}, + {style: 'display: none'}, + language.$(prefix, 'statLine.leaves')), + ], + })), + + html.tag('dl', {id: 'network-top-dl'}, [ + html.tag('dt', {id: 'top'}, + language.$(prefix, 'jumpToRoot.title')), + + html.tag('dd', + html.tag('ul', + stitchArrays({ + link: relations.uppermostRootTagLinks, + directory: data.uppermostRootTagDirectories, + }).map(({link, directory}) => + html.tag('li', + language.$(prefix, 'jumpToRoot.item', { + tag: + link.slots({ + anchor: true, + hash: directory, + }), + }))))), + + stitchArrays({ + dataNode: data.rootNodes, + relationsNode: relations.rootNodes, + }).map(({dataNode, relationsNode}) => + recursive(dataNode, relationsNode, 0)), + + !empty(relations.orphanArtTagLinks) && [ + html.tag('dt', + language.$(prefix, 'orphanArtTags.title')), + + html.tag('dd', + html.tag('ul', + relations.orphanArtTagLinks.map(orphanArtTagLink => + html.tag('li', + language.$(prefix, 'orphanArtTags.item', { + tag: orphanArtTagLink, + }))))), + ], + ]), + ], + }); + }, +}; diff --git a/src/content/dependencies/listTagsByName.js b/src/content/dependencies/listArtTagsByName.js similarity index 68% rename from src/content/dependencies/listTagsByName.js rename to src/content/dependencies/listArtTagsByName.js index d7022a55d..31856478b 100644 --- a/src/content/dependencies/listTagsByName.js +++ b/src/content/dependencies/listArtTagsByName.js @@ -1,8 +1,8 @@ import {sortAlphabetically} from '#sort'; -import {stitchArrays} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; export default { - contentDependencies: ['generateListingPage', 'linkArtTag'], + contentDependencies: ['generateListingPage', 'linkArtTagGallery'], extraDependencies: ['language', 'wikiData'], sprawl({artTagData}) { @@ -16,7 +16,7 @@ export default { artTags: sortAlphabetically( artTagData - .filter(tag => !tag.isContentWarning)), + .filter(artTag => !artTag.isContentWarning)), }; }, @@ -26,15 +26,18 @@ export default { artTagLinks: query.artTags - .map(tag => relation('linkArtTag', tag)), + .map(artTag => relation('linkArtTagGallery', artTag)), }; }, data(query) { return { counts: - query.artTags - .map(tag => tag.taggedInThings.length), + query.artTags.map(artTag => + unique([ + ...artTag.indirectlyTaggedInThings, + ...artTag.directlyTaggedInThings, + ]).length), }; }, diff --git a/src/content/dependencies/listArtTagsByUses.js b/src/content/dependencies/listArtTagsByUses.js new file mode 100644 index 000000000..fcd324f77 --- /dev/null +++ b/src/content/dependencies/listArtTagsByUses.js @@ -0,0 +1,54 @@ +import {sortAlphabetically, sortByCount} from '#sort'; +import {filterByCount, stitchArrays, unique} from '#sugar'; + +export default { + contentDependencies: ['generateListingPage', 'linkArtTagGallery'], + extraDependencies: ['language', 'wikiData'], + + sprawl: ({artTagData}) => + ({artTagData}), + + query({artTagData}, spec) { + const artTags = + sortAlphabetically( + artTagData + .filter(artTag => !artTag.isContentWarning)); + + const counts = + artTags.map(artTag => + unique([ + ...artTag.directlyTaggedInThings, + ...artTag.indirectlyTaggedInThings, + ]).length); + + filterByCount(artTags, counts); + sortByCount(artTags, counts, {greatestFirst: true}); + + return {spec, artTags, counts}; + }, + + relations: (relation, query) => ({ + page: + relation('generateListingPage', query.spec), + + artTagLinks: + query.artTags + .map(artTag => relation('linkArtTagGallery', artTag)), + }), + + data: (query) => + ({counts: query.counts}), + + generate: (data, relations, {language}) => + relations.page.slots({ + type: 'rows', + rows: + stitchArrays({ + link: relations.artTagLinks, + count: data.counts, + }).map(({link, count}) => ({ + tag: link, + timesUsed: language.countTimesUsed(count, {unit: true}), + })), + }), +}; diff --git a/src/content/dependencies/listTagsByUses.js b/src/content/dependencies/listTagsByUses.js deleted file mode 100644 index 00c700a57..000000000 --- a/src/content/dependencies/listTagsByUses.js +++ /dev/null @@ -1,59 +0,0 @@ -import {sortAlphabetically, sortByCount} from '#sort'; -import {filterByCount, stitchArrays} from '#sugar'; - -export default { - contentDependencies: ['generateListingPage', 'linkArtTag'], - extraDependencies: ['language', 'wikiData'], - - sprawl({artTagData}) { - return {artTagData}; - }, - - query({artTagData}, spec) { - const artTags = - sortAlphabetically( - artTagData - .filter(tag => !tag.isContentWarning)); - - const counts = - artTags - .map(tag => tag.taggedInThings.length); - - filterByCount(artTags, counts); - sortByCount(artTags, counts, {greatestFirst: true}); - - return {spec, artTags, counts}; - }, - - relations(relation, query) { - return { - page: relation('generateListingPage', query.spec), - - artTagLinks: - query.artTags - .map(tag => relation('linkArtTag', tag)), - }; - }, - - data(query) { - return { - counts: - query.artTags - .map(tag => tag.taggedInThings.length), - }; - }, - - generate(data, relations, {language}) { - return relations.page.slots({ - type: 'rows', - rows: - stitchArrays({ - link: relations.artTagLinks, - count: data.counts, - }).map(({link, count}) => ({ - tag: link, - timesUsed: language.countTimesUsed(count, {unit: true}), - })), - }); - }, -}; diff --git a/src/data/checks.js b/src/data/checks.js index f5aa551f2..e47f985b6 100644 --- a/src/data/checks.js +++ b/src/data/checks.js @@ -186,6 +186,10 @@ export function filterReferenceErrors(wikiData, { commentary: '_commentary', }], + ['artTagData', { + directDescendantArtTags: 'artTag', + }], + ['flashData', { commentary: '_commentary', }], diff --git a/src/data/composite/things/art-tag/index.js b/src/data/composite/things/art-tag/index.js new file mode 100644 index 000000000..bbd38293f --- /dev/null +++ b/src/data/composite/things/art-tag/index.js @@ -0,0 +1,2 @@ +export {default as withAllDescendantArtTags} from './withAllDescendantArtTags.js'; +export {default as withAncestorArtTagBaobabTree} from './withAncestorArtTagBaobabTree.js'; diff --git a/src/data/composite/things/art-tag/withAllDescendantArtTags.js b/src/data/composite/things/art-tag/withAllDescendantArtTags.js new file mode 100644 index 000000000..c643cf23f --- /dev/null +++ b/src/data/composite/things/art-tag/withAllDescendantArtTags.js @@ -0,0 +1,45 @@ +// Gets all the art tags which descend from this one - that means its own direct +// descendants, but also all the direct and indirect desceands of each of those! +// The results aren't specially sorted, but they won't contain any duplicates +// (for example if two descendant tags both route deeper to end up including +// some of the same tags). + +import {input, templateCompositeFrom} from '#composite'; +import find from '#find'; +import {unique} from '#sugar'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withResolvedReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withAllDescendantArtTags`, + + outputs: ['#allDescendantArtTags'], + + steps: () => [ + raiseOutputWithoutDependency({ + dependency: 'directDescendantArtTags', + mode: input.value('empty'), + output: input.value({'#allDescendantArtTags': []}) + }), + + withResolvedReferenceList({ + list: 'directDescendantArtTags', + data: 'artTagData', + find: input.value(find.artTag), + }), + + { + dependencies: ['#resolvedReferenceList'], + compute: (continuation, { + ['#resolvedReferenceList']: directDescendantArtTags, + }) => continuation({ + ['#allDescendantArtTags']: + unique([ + ...directDescendantArtTags, + ...directDescendantArtTags.flatMap(artTag => artTag.allDescendantArtTags), + ]), + }), + }, + ], +}) diff --git a/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js b/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js new file mode 100644 index 000000000..d5caa99e5 --- /dev/null +++ b/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js @@ -0,0 +1,46 @@ +// Gets all the art tags which are ancestors of this one as a "baobab tree" - +// what you'd typically think of as roots are all up in the air! Since this +// really is backwards from the way that the art tag tree is written in data, +// chances are pretty good that there will be many of the exact same "leaf" +// nodes - art tags which don't themselves have any ancestors. In the actual +// data structure, each node is a Map, with keys for each ancestor and values +// for each ancestor's own baobab (thus a branching structure, just like normal +// trees in this regard). + +import {input, templateCompositeFrom} from '#composite'; + +import {raiseOutputWithoutDependency} from '#composite/control-flow'; +import {withReverseReferenceList} from '#composite/wiki-data'; + +export default templateCompositeFrom({ + annotation: `withAncestorArtTagBaobabTree`, + + outputs: ['#ancestorArtTagBaobabTree'], + + steps: () => [ + withReverseReferenceList({ + data: 'artTagData', + list: input.value('directDescendantArtTags'), + }).outputs({ + ['#reverseReferenceList']: '#directAncestorArtTags', + }), + + raiseOutputWithoutDependency({ + dependency: '#directAncestorArtTags', + mode: input.value('empty'), + output: input.value({'#ancestorArtTagBaobabTree': {}}) + }), + + { + dependencies: ['#directAncestorArtTags'], + compute: (continuation, { + ['#directAncestorArtTags']: directAncestorArtTags, + }) => continuation({ + ['#ancestorArtTagBaobabTree']: + new Map( + directAncestorArtTags + .map(artTag => [artTag, artTag.ancestorArtTagBaobabTree])), + }), + }, + ], +}); diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js index 3149b3108..a9c8e913e 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,20 +1,30 @@ export const ART_TAG_DATA_FILE = 'tags.yaml'; import {input} from '#composite'; +import find from '#find'; import {sortAlphabetically, sortAlbumsTracksChronologically} from '#sort'; import Thing from '#thing'; +import {unique} from '#sugar'; import {isName} from '#validators'; -import {exposeUpdateValueOrContinue} from '#composite/control-flow'; +import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} + from '#composite/control-flow'; import { color, directory, flag, + referenceList, + reverseReferenceList, + simpleString, name, + urls, wikiData, } from '#composite/wiki-properties'; +import {withAllDescendantArtTags, withAncestorArtTagBaobabTree} + from '#composite/things/art-tag'; + export class ArtTag extends Thing { static [Thing.referenceType] = 'tag'; static [Thing.friendlyName] = `Art Tag`; @@ -26,6 +36,7 @@ export class ArtTag extends Thing { directory: directory(), color: color(), isContentWarning: flag(false), + extraReadingURLs: urls(), nameShort: [ exposeUpdateValueOrContinue({ @@ -39,19 +50,44 @@ export class ArtTag extends Thing { }, ], + description: simpleString(), + + directDescendantArtTags: referenceList({ + class: input.value(ArtTag), + find: input.value(find.artTag), + data: 'artTagData', + }), + // Update only albumData: wikiData({ class: input.value(Album), }), + artTagData: wikiData({ + class: input.value(ArtTag), + }), + trackData: wikiData({ class: input.value(Track), }), // Expose only - taggedInThings: { + descriptionShort: [ + exitWithoutDependency({ + dependency: 'description', + mode: input.value('falsy'), + }), + + { + dependencies: ['description'], + compute: ({description}) => + description.split('