diff options
Diffstat (limited to 'src')
34 files changed, 1085 insertions, 118 deletions
diff --git a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js new file mode 100644 index 00000000..34a45ffc --- /dev/null +++ b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js @@ -0,0 +1,129 @@ +import {stitchArrays} from '#sugar'; +import {filterMultipleArrays, sortMultipleArrays} from '#wiki-data'; + +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.$('artTagSidebar.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 c04bfb68..4304b95a 100644 --- a/src/content/dependencies/generateArtTagGalleryPage.js +++ b/src/content/dependencies/generateArtTagGalleryPage.js @@ -1,13 +1,16 @@ -import {stitchArrays} from '#sugar'; +import {empty, stitchArrays, unique} from '#sugar'; import {sortAlbumsTracksChronologically} from '#wiki-data'; export default { contentDependencies: [ + 'generateArtTagNavLinks', 'generateCoverGrid', 'generatePageLayout', + 'generateQuickDescription', 'image', 'linkAlbum', - 'linkArtTag', + 'linkArtTagInfo', + 'linkArtTagGallery', 'linkTrack', ], @@ -19,61 +22,91 @@ 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, { - getDate: thing => thing.coverArtDate, + 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.infoPageLink = + relation('linkArtTagInfo', artTag); + + relations.quickDescription = + relation('generateQuickDescription', artTag); + + 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.numArtworks = 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.coverArtists = + query.allThings.map(thing => + thing.coverArtistContribs + .map(({who: artist}) => artist.name)); + + data.onlyFeaturedIndirectly = + query.allThings.map(thing => + !query.directThings.includes(thing)); + return data; }, @@ -81,7 +114,7 @@ export default { return relations.layout .slots({ title: - language.$('tagPage.title', { + language.$('artTagGalleryPage.title', { tag: data.name, }), @@ -91,44 +124,66 @@ export default { mainClasses: ['top-index'], mainContent: [ - html.tag('p', - {class: 'quick-info'}, - language.$('tagPage.infoLine', { - coverArts: language.countCoverArts(data.numArtworks, { - unit: true, - }), - })), + relations.quickDescription + .slot('infoPageLink', relations.infoPageLink), + + html.tag('p', {class: 'quick-info'}, + (data.numArtworks === 0 + ? [ + language.$('artTagGalleryPage.infoLine.notFeatured'), + html.tag('br'), + language.$('artTagGalleryPage.infoLine.callToAction'), + ] + : language.$('artTagGalleryPage.infoLine', { + coverArts: language.countArtworks(data.numArtworks, { + unit: true, + }), + }))), + + relations.ancestorLinks && + html.tag('p', {class: 'quick-info'}, + language.$('artTagGalleryPage.descendsFrom', { + tags: language.formatConjunctionList(relations.ancestorLinks), + })), + + relations.descendantLinks && + html.tag('p', {clasS: 'quick-info'}, + language.$('artTagGalleryPage.desendants', { + tags: language.formatUnitList(relations.descendantLinks), + })), relations.coverGrid .slots({ links: relations.links, names: data.names, + lazy: 12, + + classes: + data.onlyFeaturedIndirectly.map(onlyFeaturedIndirectly => + (onlyFeaturedIndirectly ? 'featured-indirectly' : '')), + images: stitchArrays({ image: relations.images, path: data.paths, }).map(({image, path}) => image.slot('path', path)), + + info: + data.coverArtists.map(names => + (names === null + ? null + : language.$('misc.albumGrid.details.coverArtists', { + artists: language.formatUnitList(names), + }))), }), ], navLinkStyle: 'hierarchical', - navLinks: [ - {auto: 'home'}, - - data.enableListings && - { - path: ['localized.listingIndex'], - title: language.$('listingIndex.title'), - }, - - { - html: - language.$('tagPage.nav.tag', { - tag: relations.artTagMainLink, - }), - }, - ], + navLinks: + relations.navLinks + .slot('currentExtra', 'gallery') + .content, }); }, }; diff --git a/src/content/dependencies/generateArtTagInfoPage.js b/src/content/dependencies/generateArtTagInfoPage.js new file mode 100644 index 00000000..056d9749 --- /dev/null +++ b/src/content/dependencies/generateArtTagInfoPage.js @@ -0,0 +1,224 @@ +import {empty, stitchArrays, unique} from '#sugar'; + +export default { + contentDependencies: [ + 'generateArtTagNavLinks', + 'generateArtTagSidebar', + 'generateContentHeading', + 'generatePageLayout', + 'linkArtTagGallery', + 'linkArtTagInfo', + 'linkExternal', + 'transformContent', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl({wikiInfo}) { + return { + enableListings: wikiInfo.enableListings, + }; + }, + + query(sprawl, artTag) { + const directThings = artTag.directlyTaggedInThings; + const indirectThings = artTag.indirectlyTaggedInThings; + const allThings = unique([...directThings, ...indirectThings]); + + return {directThings, indirectThings, allThings}; + }, + + relations(relation, query, sprawl, artTag) { + const relations = {}; + const sec = relations.sections = {}; + + relations.layout = + relation('generatePageLayout'); + + relations.navLinks = + relation('generateArtTagNavLinks', artTag); + + relations.sidebar = + relation('generateArtTagSidebar', artTag); + + const info = sec.info = {}; + + if (artTag.description) { + info.description = + relation('transformContent', artTag.description); + } + + if (!empty(query.allThings)) { + info.galleryLink = + relation('linkArtTagGallery', artTag); + } + + if (!empty(artTag.extraReadingURLs)) { + info.extraReadingLinks = + artTag.extraReadingURLs + .map(url => relation('linkExternal', url)); + } + + if (!empty(artTag.directAncestorArtTags)) { + const ancestors = sec.ancestors = {}; + + ancestors.heading = + relation('generateContentHeading'); + + ancestors.directAncestorLinks = + artTag.directAncestorArtTags + .map(artTag => relation('linkArtTagInfo', artTag)); + } + + if (!empty(artTag.directDescendantArtTags)) { + const descendants = sec.descendants = {}; + + descendants.heading = + relation('generateContentHeading'); + + descendants.directDescendantInfoLinks = + artTag.directDescendantArtTags + .map(artTag => relation('linkArtTagInfo', artTag)); + + const allDescendantsHaveMoreDescendants = + artTag.directDescendantArtTags + .every(artTag => !empty(artTag.directDescendantArtTags)); + + if (!allDescendantsHaveMoreDescendants) { + descendants.directDescendantGalleryLinks = + artTag.directDescendantArtTags + .map(artTag => relation('linkArtTagGallery', artTag)); + } + } + + return relations; + }, + + data(query, sprawl, artTag) { + const data = {}; + + data.enableListings = sprawl.enableListings; + + data.name = artTag.name; + data.color = artTag.color; + + data.numArtworksIndirectly = query.indirectThings.length; + data.numArtworksDirectly = query.directThings.length; + data.numArtworksTotal = query.allThings.length; + + data.names = + query.allThings.map(thing => thing.name); + + data.paths = + query.allThings.map(thing => + (thing.album + ? ['media.trackCover', thing.album.directory, thing.directory, thing.coverArtFileExtension] + : ['media.albumCover', thing.directory, thing.coverArtFileExtension])); + + data.onlyFeaturedIndirectly = + query.allThings.map(thing => + !query.directThings.includes(thing)); + + return data; + }, + + generate(data, relations, {html, language}) { + const {sections: sec} = relations; + const nameOption = {tag: language.sanitize(data.name)}; + + return relations.layout + .slots({ + title: language.$('artTagInfoPage.title', nameOption), + headingMode: 'sticky', + color: data.color, + + mainContent: [ + html.tag('p', + (data.numArtworksTotal === 0 + ? language.$('artTagInfoPage.featuredIn.notFeatured') + : data.numArtworksDirectly === 0 + ? language.$('artTagInfoPage.featuredIn.indirectlyOnly', { + artworks: language.countArtworks(data.numArtworksIndirectly, {unit: true}), + }) + : data.numArtworksIndirectly === 0 + ? language.$('artTagInfoPage.featuredIn.directlyOnly', { + artworks: language.countArtworks(data.numArtworksDirectly, {unit: true}), + }) + : language.$('artTagInfoPage.featuredIn.directlyAndIndirectly', { + artworksDirectly: language.countArtworks(data.numArtworksDirectly, {unit: true}), + artworksIndirectly: language.countArtworks(data.numArtworksIndirectly, {unit: false}), + artworksTotal: language.countArtworks(data.numArtworksTotal, {unit: false}), + }))), + + sec.info.galleryLink && + html.tag('p', + language.$('artTagInfoPage.viewArtGallery', { + link: + sec.info.galleryLink + .slot('content', language.$('artTagInfoPage.viewArtGallery.link')), + })), + + html.tag('blockquote', + {[html.onlyIfContent]: true}, + sec.info.description + ?.slot('mode', 'multiline')), + + sec.info.extraReadingLinks && + html.tag('p', + language.$('artTagInfoPage.readMoreOn', { + ...nameOption, + links: language.formatDisjunctionList(sec.info.extraReadingLinks), + })), + + sec.ancestors && [ + sec.ancestors.heading + .slot('title', + language.$('artTagInfoPage.descendsFromTags', nameOption)), + + html.tag('ul', + sec.ancestors.directAncestorLinks + .map(link => + html.tag('li', + language.$('artTagInfoPage.descendsFromTags.item', { + tag: link, + })))), + ], + + sec.descendants && [ + sec.descendants.heading + .slot('title', + language.$('artTagInfoPage.descendantTags', nameOption)), + + !sec.descendants.directDescendantGalleryLinks && + html.tag('ul', + sec.descendants.directDescendantInfoLinks + .map(link => + html.tag('li', + language.$('artTagInfoPage.descendantTags.item', { + tag: link, + })))), + + sec.descendants.directDescendantGalleryLinks && + html.tag('ul', + stitchArrays({ + infoLink: sec.descendants.directDescendantInfoLinks, + galleryLink: sec.descendants.directDescendantGalleryLinks, + }).map(({infoLink, galleryLink}) => + html.tag('li', + language.$('artTagInfoPage.descendantTags.item.withGallery', { + tag: infoLink, + + gallery: + galleryLink.slot('content', + language.$('artTagInfoPage.descendantTags.item.gallery')), + })))), + ], + ], + + navLinkStyle: 'hierarchical', + navLinks: relations.navLinks.content, + + ...relations.sidebar, + }); + }, +}; diff --git a/src/content/dependencies/generateArtTagNavLinks.js b/src/content/dependencies/generateArtTagNavLinks.js new file mode 100644 index 00000000..34f95f6e --- /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 00000000..f6787a8c --- /dev/null +++ b/src/content/dependencies/generateArtTagSidebar.js @@ -0,0 +1,88 @@ +import {empty, stitchArrays} from '#sugar'; +import {collectTreeLeaves} from '#wiki-data'; + +export default { + contentDependencies: [ + '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) => ({ + 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}) => ({ + leftSidebarContent: [ + 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/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index aa6efe5e..adfee1da 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -30,7 +30,7 @@ export default { entry: { type: 'albumCover', album: album, - date: album.coverArtDate, + date: album.coverArtDate ?? album.date, contribs: album.coverArtistContribs, }, })), @@ -40,7 +40,7 @@ export default { entry: { type: 'albumWallpaper', album: album, - date: album.coverArtDate, + date: album.coverArtDate ?? album.date, contribs: album.wallpaperArtistContribs, }, })), @@ -50,7 +50,7 @@ export default { entry: { type: 'albumBanner', album: album, - date: album.coverArtDate, + date: album.coverArtDate ?? album.date, contribs: album.bannerArtistContribs, }, })), @@ -60,7 +60,7 @@ export default { entry: { type: 'trackCover', album: track.album, - date: track.coverArtDate, + date: track.coverArtDate ?? track.album.date, track: track, contribs: track.coverArtistContribs, }, @@ -69,7 +69,7 @@ export default { sortEntryThingPairs(entries, things => sortAlbumsTracksChronologically(things, { - getDate: thing => thing.coverArtDate, + getDate: thing => thing.coverArtDate ?? thing.date, })); const chunks = diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index aeba97de..31a4a6bb 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -1,7 +1,7 @@ import {empty} from '#sugar'; export default { - contentDependencies: ['image', 'linkArtTag'], + contentDependencies: ['image', 'linkArtTagGallery'], extraDependencies: ['html', 'language'], relations(relation, artTags) { @@ -11,12 +11,12 @@ export default { relation('image', artTags); if (artTags) { - relations.tagLinks = + relations.artTagLinks = artTags - .filter(tag => !tag.isContentWarning) - .map(tag => relation('linkArtTag', tag)); + .filter(artTag => !artTag.isContentWarning) + .map(artTag => relation('linkArtTagGallery', artTag)); } else { - relations.tagLinks = null; + relations.artTagLinks = null; } return relations; @@ -52,12 +52,12 @@ export default { square: true, }), - !empty(relations.tagLinks) && + !empty(relations.artTagLinks) && html.tag('p', language.$('releaseInfo.artTags.inline', { tags: language.formatUnitList( - relations.tagLinks + relations.artTagLinks .map(tagLink => tagLink.slot('preferShortName', true))), })), ]); diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 5636e4f3..6ac0f941 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.oneOf( + v.isArray, + v.isString))), + }, + lazy: {validate: v => v.oneOf(v.isWholeNumber, v.isBoolean)}, actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, @@ -24,13 +37,26 @@ export default { return ( html.tag('div', {class: 'grid-listing'}, [ stitchArrays({ + classOrClasses: slots.classes, image: slots.images, link: slots.links, name: slots.names, info: slots.info, - }).map(({image, link, name, info}, index) => + }).map(({classOrClasses, image, link, name, info}, index) => link.slots({ - attributes: {class: ['grid-item', 'box']}, + attributes: { + class: [ + 'grid-item', + 'box', + ... + (Array.isArray(classOrClasses) + ? classOrClasses + : typeof classOrClasses === 'string' + ? [classOrClasses] + : []), + ], + }, + content: [ image.slots({ thumb: 'medium', diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index 47239f55..49869c76 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -13,8 +13,10 @@ export default { 'generateGroupNavLinks', 'generateGroupSidebar', 'generatePageLayout', + 'generateQuickDescription', 'image', 'linkAlbum', + 'linkGroup', 'linkListing', ], @@ -70,6 +72,12 @@ export default { .map(album => relation('image', album.artTags)); } + relations.quickDescription = + relation('generateQuickDescription', group); + + relations.quickDescriptionInfoLink = + relation('linkGroup', group); + relations.coverGrid = relation('generateCoverGrid'); @@ -143,6 +151,9 @@ export default { image.slot('path', path)), }), + relations.quickDescription + .slot('infoPageLink', relations.quickDescriptionInfoLink), + html.tag('p', {class: 'quick-info'}, language.$('groupGalleryPage.infoLine', { diff --git a/src/content/dependencies/generateQuickDescription.js b/src/content/dependencies/generateQuickDescription.js new file mode 100644 index 00000000..136769ac --- /dev/null +++ b/src/content/dependencies/generateQuickDescription.js @@ -0,0 +1,41 @@ +export default { + contentDependencies: ['transformContent'], + extraDependencies: ['html', 'language'], + + relations: (relation, thing) => + ({description: + (thing.descriptionShort || thing.description + ? relation('transformContent', + thing.descriptionShort ?? thing.description) + : null)}), + + data: (thing) => + ({hasLongerDescription: + thing.description && + thing.descriptionShort && + thing.descriptionShort !== thing.description}), + + slots: { + infoPageLink: {type: 'html'}, + }, + + generate(data, relations, slots, {html, language}) { + return html.tag('p', + { + [html.joinChildren]: html.tag('br'), + [html.onlyIfContent]: true, + class:' quick-info', + }, + [ + relations.description?.slot('mode', 'inline'), + + data.hasLongerDescription && + slots.infoPageLink && + language.$('misc.quickDescription.moreInfo', { + link: + slots.infoPageLink + .slot('content', language.$('misc.quickDescription.moreInfo.link')), + }), + ]); + }, +}; diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 1083d863..93334948 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -82,7 +82,7 @@ export default { ...artist.albumsAsCoverArtist, ...artist.tracksAsCoverArtist, ], { - getDate: albumOrTrack => albumOrTrack.coverArtDate, + getDate: thing => thing.coverArtDate ?? thing.date, }), }), diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 6c0aeecd..006be156 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -20,8 +20,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 00000000..964258e1 --- /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 00000000..a92b69c1 --- /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 index 7ddb7786..409cb3c0 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/linkExternal.js b/src/content/dependencies/linkExternal.js index 73c656e3..b1859dde 100644 --- a/src/content/dependencies/linkExternal.js +++ b/src/content/dependencies/linkExternal.js @@ -56,6 +56,18 @@ export default { : language.$('misc.external.youtube.fullAlbum') : language.$('misc.external.youtube') + : domain.includes('fandom.com') + ? domain.includes('mspaintadventures.') + ? data.url.match(/\/wiki\/(.+)\/?$/) + ? language.$('misc.external.fandom.mspaintadventures.page', { + page: + language.sanitize( + decodeURIComponent(data.url.match(/\/wiki\/(.+)\/?$/)[1]) + .replace(/_/g, ' ')), + }) + : language.$('misc.external.fandom.mspaintadventures') + : language.$('misc.external.fandom') + : domain.includes('soundcloud') ? language.$('misc.external.soundcloud') diff --git a/src/content/dependencies/listTagsByName.js b/src/content/dependencies/listTagsByName.js index 8571ccd0..9bec9eaa 100644 --- a/src/content/dependencies/listTagsByName.js +++ b/src/content/dependencies/listTagsByName.js @@ -1,8 +1,8 @@ -import {stitchArrays} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; import {sortAlphabetically} from '#wiki-data'; 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/listTagsByUses.js b/src/content/dependencies/listTagsByUses.js index 98a50b89..9eb6f185 100644 --- a/src/content/dependencies/listTagsByUses.js +++ b/src/content/dependencies/listTagsByUses.js @@ -1,23 +1,25 @@ -import {stitchArrays} from '#sugar'; +import {stitchArrays, unique} from '#sugar'; import {filterByCount, sortAlphabetically, sortByCount} from '#wiki-data'; export default { - contentDependencies: ['generateListingPage', 'linkArtTag'], + contentDependencies: ['generateListingPage', 'linkArtTagGallery'], extraDependencies: ['language', 'wikiData'], - sprawl({artTagData}) { - return {artTagData}; - }, + sprawl: ({artTagData}) => + ({artTagData}), query({artTagData}, spec) { const artTags = sortAlphabetically( artTagData - .filter(tag => !tag.isContentWarning)); + .filter(artTag => !artTag.isContentWarning)); const counts = - artTags - .map(tag => tag.taggedInThings.length); + artTags.map(artTag => + unique([ + ...artTag.directlyTaggedInThings, + ...artTag.indirectlyTaggedInThings, + ]).length); filterByCount(artTags, counts); sortByCount(artTags, counts, {greatestFirst: true}); @@ -25,26 +27,20 @@ export default { return {spec, artTags, counts}; }, - relations(relation, query) { - return { - page: relation('generateListingPage', query.spec), + relations: (relation, query) => ({ + page: + relation('generateListingPage', query.spec), - artTagLinks: - query.artTags - .map(tag => relation('linkArtTag', tag)), - }; - }, + artTagLinks: + query.artTags + .map(artTag => relation('linkArtTagGallery', artTag)), + }), - data(query) { - return { - counts: - query.artTags - .map(tag => tag.taggedInThings.length), - }; - }, + data: (query) => + ({counts: query.counts}), - generate(data, relations, {language}) { - return relations.page.slots({ + generate: (data, relations, {language}) => + relations.page.slots({ type: 'rows', rows: stitchArrays({ @@ -54,6 +50,5 @@ export default { tag: link, timesUsed: language.countTimesUsed(count, {unit: true}), })), - }); - }, + }), }; diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 9a5ac456..9c1f9529 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -104,7 +104,11 @@ export const replacerSpec = { }, tag: { find: 'artTag', - link: 'tag', + link: 'artTag', + }, + 'tag-info': { + find: 'artTag', + link: 'artTagInfo', }, track: { find: 'track', @@ -118,13 +122,14 @@ const linkThingRelationMap = { albumGallery: 'linkAlbumGallery', artist: 'linkArtist', artistGallery: 'linkArtistGallery', + artTag: 'linkArtTagDynamically', + artTagInfo: 'linkArtTagInfo', flash: 'linkFlash', groupInfo: 'linkGroup', groupGallery: 'linkGroupGallery', listing: 'linkListing', newsEntry: 'linkNewsEntry', staticPage: 'linkStaticPage', - tag: 'linkArtTag', track: 'linkTrack', }; diff --git a/src/data/composite/control-flow/raiseOutputWithoutDependency.js b/src/data/composite/control-flow/raiseOutputWithoutDependency.js index 03d8036a..3d04f8a9 100644 --- a/src/data/composite/control-flow/raiseOutputWithoutDependency.js +++ b/src/data/composite/control-flow/raiseOutputWithoutDependency.js @@ -17,7 +17,7 @@ export default templateCompositeFrom({ outputs: ({ [input.staticValue('output')]: output, - }) => Object.keys(output), + }) => Object.keys(output ?? {}), steps: () => [ withResultOfAvailabilityCheck({ diff --git a/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js index 3c39f5ba..ffa83a94 100644 --- a/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js +++ b/src/data/composite/control-flow/raiseOutputWithoutUpdateValue.js @@ -16,7 +16,7 @@ export default templateCompositeFrom({ outputs: ({ [input.staticValue('output')]: output, - }) => Object.keys(output), + }) => Object.keys(output ?? {}), steps: () => [ withResultOfAvailabilityCheck({ 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 00000000..bbd38293 --- /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 00000000..c643cf23 --- /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 00000000..d5caa99e --- /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 1266a4e0..a530ba8c 100644 --- a/src/data/things/art-tag.js +++ b/src/data/things/art-tag.js @@ -1,17 +1,27 @@ import {input} from '#composite'; -import {sortAlbumsTracksChronologically} from '#wiki-data'; +import find from '#find'; +import {unique} from '#sugar'; import {isName} from '#validators'; +import {sortAlbumsTracksChronologically} from '#wiki-data'; -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'; + import Thing from './thing.js'; export class ArtTag extends Thing { @@ -24,6 +34,7 @@ export class ArtTag extends Thing { directory: directory(), color: color(), isContentWarning: flag(false), + extraReadingURLs: urls(), nameShort: [ exposeUpdateValueOrContinue({ @@ -37,14 +48,36 @@ export class ArtTag extends Thing { }, ], + description: simpleString(), + + directDescendantArtTags: referenceList({ + class: input.value(ArtTag), + find: input.value(find.artTag), + data: 'artTagData', + }), + // Update only albumData: wikiData(Album), + artTagData: wikiData(ArtTag), trackData: wikiData(Track), // Expose only - taggedInThings: { + descriptionShort: [ + exitWithoutDependency({ + dependency: 'description', + mode: input.value('falsy'), + }), + + { + dependencies: ['description'], + compute: ({description}) => + description.split('<hr class="split">')[0], + }, + ], + + directlyTaggedInThings: { flags: {expose: true}, expose: { @@ -56,5 +89,32 @@ export class ArtTag extends Thing { {getDate: o => o.coverArtDate}), }, }, + + indirectlyTaggedInThings: [ + withAllDescendantArtTags(), + + { + dependencies: ['#allDescendantArtTags'], + compute: ({'#allDescendantArtTags': allDescendantArtTags}) => + unique( + allDescendantArtTags + .flatMap(artTag => artTag.directlyTaggedInThings)), + }, + ], + + allDescendantArtTags: [ + withAllDescendantArtTags(), + exposeDependency({dependency: '#allDescendantArtTags'}), + ], + + directAncestorArtTags: reverseReferenceList({ + data: 'artTagData', + list: input.value('directDescendantArtTags'), + }), + + ancestorArtTagBaobabTree: [ + withAncestorArtTagBaobabTree(), + exposeDependency({dependency: '#ancestorArtTagBaobabTree'}), + ], }); } diff --git a/src/data/things/language.js b/src/data/things/language.js index fe74f7bf..cd719d0c 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -376,6 +376,7 @@ Object.assign(Language.prototype, { countAdditionalFiles: countHelper('additionalFiles', 'files'), countAlbums: countHelper('albums'), countArtworks: countHelper('artworks'), + countArtTags: countHelper('artTags', 'tags'), countFlashes: countHelper('flashes'), countCommentaryEntries: countHelper('commentaryEntries', 'entries'), countContributions: countHelper('contributions'), diff --git a/src/data/yaml.js b/src/data/yaml.js index c799be5f..09465648 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -459,9 +459,13 @@ export const processArtTagDocument = makeProcessDocument(T.ArtTag, { name: 'Tag', nameShort: 'Short Name', directory: 'Directory', + description: 'Description', + extraReadingURLs: 'Extra Reading URLs', color: 'Color', isContentWarning: 'Is CW', + + directDescendantArtTags: 'Direct Descendant Tags', }, }); @@ -1359,7 +1363,7 @@ export function linkWikiDataArrays(wikiData, { assignWikiData(WD.groupCategoryData, 'groupData'); assignWikiData(WD.flashData, 'artistData', 'flashActData', 'trackData'); assignWikiData(WD.flashActData, 'flashData'); - assignWikiData(WD.artTagData, 'albumData', 'trackData'); + assignWikiData(WD.artTagData, 'albumData', 'artTagData', 'trackData'); assignWikiData(WD.homepageLayout?.rows, 'albumData', 'groupData'); } @@ -1477,6 +1481,10 @@ export function filterReferenceErrors(wikiData) { artTags: 'artTag', }], + ['artTagData', processArtTagDocument, { + directDescendantArtTags: 'artTag', + }], + ['trackData', processTrackDocument, { artistContribs: '_contrib', contributorContribs: '_contrib', diff --git a/src/find.js b/src/find.js index c8edce98..141dd599 100644 --- a/src/find.js +++ b/src/find.js @@ -69,7 +69,7 @@ function findHelper({ // errors for null matches (with details about the error), while 'warn' and // 'quiet' both return null, with 'warn' logging details directly to the // console. - return (fullRef, data, {mode = 'warn'}) => { + return (fullRef, data, {mode = 'warn'} = {}) => { if (!fullRef) return null; if (typeof fullRef !== 'string') { throw new TypeError(`Expected a string, got ${typeAppearance(fullRef)}`); @@ -134,10 +134,10 @@ const find = { artTag: findHelper({ referenceTypes: ['tag'], - getMatchableNames: tag => - (tag.isContentWarning - ? [`cw: ${tag.name}`] - : [tag.name]), + getMatchableNames: artTag => + (artTag.isContentWarning + ? [`cw: ${artTag.name}`] + : [artTag.name]), }), flash: findHelper({ diff --git a/src/page/tag.js b/src/page/art-tag.js index 8942aea9..5b61229d 100644 --- a/src/page/tag.js +++ b/src/page/art-tag.js @@ -1,6 +1,6 @@ // Art tag page specification. -export const description = `per-artwork-tag gallery pages`; +export const description = `per-art-tag info & gallery pages`; export function condition({wikiData}) { return wikiData.wikiInfo.enableArtTagUI; @@ -14,7 +14,17 @@ export function pathsForTarget(tag) { return [ { type: 'page', - path: ['tag', tag.directory], + path: ['artTagInfo', tag.directory], + + contentFunction: { + name: 'generateArtTagInfoPage', + args: [tag], + }, + }, + + { + type: 'page', + path: ['artTagGallery', tag.directory], contentFunction: { name: 'generateArtTagGalleryPage', diff --git a/src/page/index.js b/src/page/index.js index 48e22d2e..557384a6 100644 --- a/src/page/index.js +++ b/src/page/index.js @@ -1,11 +1,11 @@ export * as album from './album.js'; export * as artist from './artist.js'; export * as artistAlias from './artist-alias.js'; +export * as artTag from './art-tag.js'; export * as flash from './flash.js'; export * as group from './group.js'; export * as homepage from './homepage.js'; export * as listing from './listing.js'; export * as news from './news.js'; export * as static from './static.js'; -export * as tag from './tag.js'; export * as track from './track.js'; diff --git a/src/static/site4.css b/src/static/site4.css index ab17bf0c..65df2656 100644 --- a/src/static/site4.css +++ b/src/static/site4.css @@ -296,6 +296,11 @@ body::before { margin: 0; } +.sidebar h2:first-child { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + .sidebar h3 { font-size: 1.1em; font-style: oblique; @@ -347,6 +352,36 @@ body::before { padding-left: 10px; } +.sidebar details.has-tree-list[open] summary { + font-weight: 800; +} + +.sidebar dl.tree-list { + margin-top: 0.25em; + line-height: 1.25em; + padding-left: 15px; +} + +.sidebar dl.tree-list dt { + display: list-item; + list-style-type: disc; + padding-left: 0; + margin-left: 20px; +} + +.sidebar dl.tree-list dl { + padding-left: 15px; +} + +.sidebar dl.tree-list dd { + margin-left: 0; +} + +.sidebar dl.tree-list dt.current a { + font-weight: 800; + border-bottom: 1px solid; +} + .sidebar li.current { font-weight: 800; } @@ -612,6 +647,9 @@ html[data-url-key="localized.home"] #content h1 { .quick-info { text-align: center; + padding-left: 12%; + padding-right: 12%; + line-height: 1.25em; } ul.quick-info { diff --git a/src/strings-default.json b/src/strings-default.json index 0ad7a516..a955eef2 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -29,6 +29,13 @@ "count.artworks.withUnit.few": "", "count.artworks.withUnit.many": "", "count.artworks.withUnit.other": "{ARTWORKS} artworks", + "count.artTags": "{TAGS}", + "count.artTags.withUnit.zero": "", + "count.artTags.withUnit.one": "{TAGS} tag", + "count.artTags.withUnit.two": "", + "count.artTags.withUnit.few": "", + "count.artTags.withUnit.many": "", + "count.artTags.withUnit.other": "{TAGS} tags", "count.commentaryEntries": "{ENTRIES}", "count.commentaryEntries.withUnit.zero": "", "count.commentaryEntries.withUnit.one": "{ENTRIES} entry", @@ -179,6 +186,9 @@ "misc.external.bandcamp": "Bandcamp", "misc.external.bandcamp.domain": "Bandcamp ({DOMAIN})", "misc.external.deviantart": "DeviantArt", + "misc.external.fandom": "Fandom", + "misc.external.fandom.mspaintadventures": "MSPA Wiki", + "misc.external.fandom.mspaintadventures.page": "MSPA Wiki ({PAGE})", "misc.external.instagram": "Instagram", "misc.external.mastodon": "Mastodon", "misc.external.mastodon.domain": "Mastodon ({DOMAIN})", @@ -205,6 +215,8 @@ "misc.nav.gallery": "Gallery", "misc.pageTitle": "{TITLE}", "misc.pageTitle.withWikiName": "{TITLE} | {WIKI_NAME}", + "misc.quickDescription.moreInfo": "({LINK})", + "misc.quickDescription.moreInfo.link": "More info...", "misc.skippers.skipTo": "Skip to:", "misc.skippers.content": "Content", "misc.skippers.sidebar": "Sidebar", @@ -315,6 +327,28 @@ "artistPage.nav.artist": "Artist: {ARTIST}", "artistGalleryPage.title": "{ARTIST} - Gallery", "artistGalleryPage.infoLine": "Contributed to {COVER_ARTS}.", + "artTagPage.nav.tag": "Tag: {TAG}", + "artTagInfoPage.title": "{TAG}", + "artTagInfoPage.viewArtGallery": "View this tag's {LINK}!", + "artTagInfoPage.viewArtGallery.link": "art gallery", + "artTagInfoPage.readMoreOn": "Read more about {TAG} on {LINKS}.", + "artTagInfoPage.featuredIn.notFeatured": "This tag hasn't been featured in any artworks yet.", + "artTagInfoPage.featuredIn.directlyOnly": "This tag is featured in {ARTWORKS}.", + "artTagInfoPage.featuredIn.indirectlyOnly": "This tag is featured in {ARTWORKS}, but only indirectly - have a look at its descendant tags!", + "artTagInfoPage.featuredIn.directlyAndIndirectly": "This tag is directly featured in {ARTWORKS_DIRECTLY}, and indirectly in {ARTWORKS_INDIRECTLY} more, for a total of {ARTWORKS_TOTAL}.", + "artTagInfoPage.descendsFromTags": "Tags that {TAG} descends from:", + "artTagInfoPage.descendsFromTags.item": "{TAG}", + "artTagInfoPage.descendantTags": "Tags that descend from {TAG}:", + "artTagInfoPage.descendantTags.item": "{TAG}", + "artTagInfoPage.descendantTags.item.withGallery": "{TAG} ({GALLERY})", + "artTagInfoPage.descendantTags.item.gallery": "Gallery", + "artTagGalleryPage.title": "{TAG}", + "artTagGalleryPage.infoLine": "Featured in {COVER_ARTS}.", + "artTagGalleryPage.infoLine.notFeatured": "This tag hasn't been featured in any artworks yet.", + "artTagGalleryPage.infoLine.callToAction": "Maybe your track will be the first!", + "artTagGalleryPage.descendsFrom": "Descends from {TAGS}.", + "artTagGalleryPage.desendants": "Direct descendants: {TAGS}.", + "artTagSidebar.otherTagsExempt": "(…another {TAGS}…)", "commentaryIndex.title": "Commentary", "commentaryIndex.infoLine": "{WORDS} across {ENTRIES}, in all.", "commentaryIndex.albumList.title": "Choose an album:", @@ -495,9 +529,6 @@ "newsEntryPage.published": "(Published {DATE}.)", "redirectPage.title": "Moved to {TITLE}", "redirectPage.infoLine": "This page has been moved to {TARGET}.", - "tagPage.title": "{TAG}", - "tagPage.infoLine": "Appears in {COVER_ARTS}.", - "tagPage.nav.tag": "Tag: {TAG}", "trackPage.title": "{TRACK}", "trackPage.referenceList.fandom": "Fandom:", "trackPage.referenceList.official": "Official:", diff --git a/src/url-spec.js b/src/url-spec.js index 4d103134..e4c0c65b 100644 --- a/src/url-spec.js +++ b/src/url-spec.js @@ -47,7 +47,10 @@ const urlSpec = { newsEntry: 'news/<>/', staticPage: '<>/', - tag: 'tag/<>/', + + artTagInfo: 'tag/<>/info/', + artTagGallery: 'tag/<>/', + track: 'track/<>/', }, }, diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js index ac652b27..a85dd9a2 100644 --- a/src/util/wiki-data.js +++ b/src/util/wiki-data.js @@ -97,6 +97,38 @@ export function chunkMultipleArrays(...args) { return results; } +// This (or its helper function) should probably be a generator, but generators +// are scary... Note that the root node is never considered a leaf, even if it +// doesn't have any branches. It does NOT pay attention to the *values* of the +// leaf nodes - it's suited to handle this kind of form: +// +// { +// foo: { +// bar: {}, +// baz: {}, +// qux: { +// woz: {}, +// }, +// }, +// } +// +// for which it outputs ['bar', 'baz', 'woz']. +// +export function collectTreeLeaves(tree) { + const recursive = ([key, value]) => + (value instanceof Map + ? (value.size === 0 + ? [key] + : Array.from(value.entries()).flatMap(recursive)) + : (empty(Object.keys(value)) + ? [key] + : Object.entries(value).flatMap(recursive))); + + const root = Symbol(); + const leaves = recursive([root, tree]); + return (leaves[0] === root ? [] : leaves); +} + // Sorting functions - all utils here are mutating, so make sure to initially // slice/filter/somehow generate a new array from input data if retaining the // initial sort matters! (Spoilers: If what you're doing involves any kind of @@ -871,7 +903,7 @@ export function filterItemsForCarousel(items) { return items .filter(item => item.hasCoverArt) - .filter(item => item.artTags.every(tag => !tag.isContentWarning)) + .filter(item => item.artTags.every(artTag => !artTag.isContentWarning)) .slice(0, maxCarouselLayoutItems + 1); } |