From 6d8fe82b5386af536ca96eb1d89150e201c603e9 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 25 May 2023 13:23:04 -0300 Subject: content: sprawl & transformContent Sprawling basically means tying a component to objects which aren't directly passed to it. This is necessary for functions like transformContent, which was *mostly* implemented here (the multiline/lyrics modes are stubs, and a number of links haven't been implemented yet). --- src/content-function.js | 78 +++++--- src/content/dependencies/generatePageLayout.js | 34 +++- src/content/dependencies/linkArtistGallery.js | 8 + src/content/dependencies/linkFlash.js | 8 + src/content/dependencies/linkGroupGallery.js | 8 + src/content/dependencies/linkListing.js | 8 + src/content/dependencies/linkNewsEntry.js | 8 + src/content/dependencies/linkStaticPage.js | 8 + src/content/dependencies/transformContent.js | 259 +++++++++++++++++++++++++ src/util/replacer.js | 11 +- src/util/transform-content.js | 3 +- src/write/bind-utilities.js | 1 + src/write/build-modes/live-dev-server.js | 21 +- 13 files changed, 405 insertions(+), 50 deletions(-) create mode 100644 src/content/dependencies/linkArtistGallery.js create mode 100644 src/content/dependencies/linkFlash.js create mode 100644 src/content/dependencies/linkGroupGallery.js create mode 100644 src/content/dependencies/linkListing.js create mode 100644 src/content/dependencies/linkNewsEntry.js create mode 100644 src/content/dependencies/linkStaticPage.js create mode 100644 src/content/dependencies/transformContent.js diff --git a/src/content-function.js b/src/content-function.js index bdf9cd2..921b5bc 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -4,14 +4,16 @@ export default function contentFunction({ contentDependencies = [], extraDependencies = [], + sprawl, + relations, data, generate, - relations, }) { return expectDependencies({ + sprawl, + relations, data, generate, - relations, expectedContentDependencyKeys: contentDependencies, expectedExtraDependencyKeys: extraDependencies, @@ -22,9 +24,10 @@ export default function contentFunction({ contentFunction.identifyingSymbol = Symbol(`Is a content function?`); export function expectDependencies({ + sprawl, + relations, data, generate, - relations, expectedContentDependencyKeys, expectedExtraDependencyKeys, @@ -34,8 +37,13 @@ export function expectDependencies({ throw new Error(`Expected generate function`); } - const hasDataFunction = !!data; + const hasSprawlFunction = !!sprawl; const hasRelationsFunction = !!relations; + const hasDataFunction = !!data; + + if (hasSprawlFunction && !expectedExtraDependencyKeys.includes('wikiData')) { + throw new Error(`Content functions which sprawl must specify wikiData in extraDependencies`); + } const fulfilledDependencyKeys = Object.keys(fulfilledDependencies); @@ -98,27 +106,24 @@ export function expectDependencies({ wrappedGenerate[contentFunction.identifyingSymbol] = true; - if (hasDataFunction) { - if (empty(missingContentDependencyKeys)) { - wrappedGenerate.data = data; - } else { - wrappedGenerate.data = function() { - throw new Error(`Dependencies still needed: ${missingContentDependencyKeys.join(', ')}`); - }; - - annotateFunction(wrappedGenerate.data, {name: data, trait: 'unfulfilled'}); - } + if (hasSprawlFunction) { + wrappedGenerate.sprawl = sprawl; } if (hasRelationsFunction) { wrappedGenerate.relations = relations; } + if (hasDataFunction) { + wrappedGenerate.data = data; + } + wrappedGenerate.fulfill ??= function fulfill(dependencies) { return expectDependencies({ + sprawl, + relations, data, generate, - relations, expectedContentDependencyKeys, expectedExtraDependencyKeys, @@ -201,19 +206,25 @@ export function fulfillDependencies({ return newFulfilledDependencies; } -export function getRelationsTree(dependencies, contentFunctionName, ...args) { +export function getRelationsTree(dependencies, contentFunctionName, wikiData, ...args) { const relationIdentifier = Symbol('Relation'); function recursive(contentFunctionName, ...args) { const contentFunction = dependencies[contentFunctionName]; - if (!contentFunctionName) { + if (!contentFunction) { throw new Error(`Couldn't find dependency ${contentFunctionName}`); } - if (!contentFunction?.relations) { + if (!contentFunction.relations) { return null; } + // TODO: Evaluating a sprawl might belong somewhere better than here, lol... + const sprawl = + (contentFunction.sprawl + ? contentFunction.sprawl(wikiData, ...args) + : null) + const relationSlots = {}; const relationSymbolMessage = (() => { @@ -227,7 +238,10 @@ export function getRelationsTree(dependencies, contentFunctionName, ...args) { return {[relationIdentifier]: relationSymbol}; }; - const relationsLayout = contentFunction.relations(relationFunction, ...args); + const relationsLayout = + (sprawl + ? contentFunction.relations(relationFunction, sprawl, ...args) + : contentFunction.relations(relationFunction, ...args)) const relationsTree = Object.fromEntries( Object.getOwnPropertySymbols(relationSlots) @@ -354,7 +368,7 @@ export function quickEvaluate({ })); } - const treeInfo = getRelationsTree(allContentDependencies, name, ...args); + const treeInfo = getRelationsTree(allContentDependencies, name, allExtraDependencies.wikiData ?? {}, ...args); const flatTreeInfo = flattenRelationsTree(treeInfo); const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo; @@ -421,15 +435,25 @@ export function quickEvaluate({ const slotResults = {}; - function runContentFunction({name, args, relations}) { + function runContentFunction({name, args, relations: flatRelations}) { const contentFunction = fulfilledContentDependencies[name]; - const filledRelations = - fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, relations); - const generateArgs = [ - contentFunction.data?.(...args), - filledRelations, - ].filter(Boolean); + if (!contentFunction) { + throw new Error(`Content function ${name} unfulfilled or not listed`); + } + + const sprawl = + contentFunction.sprawl?.(allExtraDependencies.wikiData, ...args); + + const relations = + fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, flatRelations); + + const data = + (sprawl + ? contentFunction.data?.(sprawl, ...args) + : contentFunction.data?.(...args)); + + const generateArgs = [data, relations].filter(Boolean); return contentFunction(...generateArgs); } diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index e9de61d..be61a6c 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -4,6 +4,7 @@ export default { contentDependencies: [ 'generateFooterLocalizationLinks', 'generateStickyHeadingContainer', + 'transformContent', ], extraDependencies: [ @@ -12,10 +13,23 @@ export default { 'language', 'to', 'transformMultiline', - 'wikiInfo', + 'wikiData', ], - relations(relation) { + sprawl({wikiInfo}) { + return { + footerContent: wikiInfo.footerContent, + wikiName: wikiInfo.nameShort, + }; + }, + + data({wikiName}) { + return { + wikiName, + }; + }, + + relations(relation, sprawl) { const relations = {}; relations.footerLocalizationLinks = @@ -24,16 +38,17 @@ export default { relations.stickyHeadingContainer = relation('generateStickyHeadingContainer'); + relations.defaultFooterContent = + relation('transformContent', sprawl.footerContent); + return relations; }, - generate(relations, { + generate(data, relations, { cachebust, html, language, to, - transformMultiline, - wikiInfo, }) { const sidebarSlots = side => ({ // Content is a flat HTML array. It'll generate one sidebar section @@ -186,8 +201,9 @@ export default { let footerContent = slots.footerContent; - if (html.isBlank(footerContent) && wikiInfo.footerContent) { - footerContent = transformMultiline(wikiInfo.footerContent); + if (html.isBlank(footerContent)) { + footerContent = relations.defaultFooterContent + .slot('mode', 'multiline'); } const mainHTML = @@ -251,7 +267,7 @@ export default { switch (cur.auto) { case 'home': - title = wikiInfo.nameShort; + title = data.wikiName; href = to('localized.home'); break; case 'current': @@ -400,7 +416,7 @@ export default { showWikiNameInTitle ? language.formatString('misc.pageTitle.withWikiName', { title, - wikiName: wikiInfo.nameShort, + wikiName: data.wikiName, }) : language.formatString('misc.pageTitle', {title})), */ diff --git a/src/content/dependencies/linkArtistGallery.js b/src/content/dependencies/linkArtistGallery.js new file mode 100644 index 0000000..66dc172 --- /dev/null +++ b/src/content/dependencies/linkArtistGallery.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, artist) => + ({link: relation('linkThing', 'localized.artistGallery', artist)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkFlash.js b/src/content/dependencies/linkFlash.js new file mode 100644 index 0000000..93dd5a2 --- /dev/null +++ b/src/content/dependencies/linkFlash.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, flash) => + ({link: relation('linkThing', 'localized.flash', flash)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkGroupGallery.js b/src/content/dependencies/linkGroupGallery.js new file mode 100644 index 0000000..86c4a0f --- /dev/null +++ b/src/content/dependencies/linkGroupGallery.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, group) => + ({link: relation('linkThing', 'localized.groupGallery', group)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkListing.js b/src/content/dependencies/linkListing.js new file mode 100644 index 0000000..f27d93a --- /dev/null +++ b/src/content/dependencies/linkListing.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, listing) => + ({link: relation('linkThing', 'localized.listing', listing)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkNewsEntry.js b/src/content/dependencies/linkNewsEntry.js new file mode 100644 index 0000000..1fb32dd --- /dev/null +++ b/src/content/dependencies/linkNewsEntry.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, newsEntry) => + ({link: relation('linkThing', 'localized.newsEntry', newsEntry)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/linkStaticPage.js b/src/content/dependencies/linkStaticPage.js new file mode 100644 index 0000000..032af6c --- /dev/null +++ b/src/content/dependencies/linkStaticPage.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, staticPage) => + ({link: relation('linkThing', 'localized.staticPage', staticPage)}), + + generate: (relations) => relations.link, +}; diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js new file mode 100644 index 0000000..262c298 --- /dev/null +++ b/src/content/dependencies/transformContent.js @@ -0,0 +1,259 @@ +import {bindFind} from '../../util/find.js'; +import {parseInput} from '../../util/replacer.js'; +import {replacerSpec} from '../../util/transform-content.js'; + +const linkThingRelationMap = { + album: 'linkAlbum', + albumCommentary: 'linkAlbumCommentary', + albumGallery: 'linkAlbumGallery', + artist: 'linkArtist', + artistGallery: 'linkArtistGallery', + flash: 'linkFlash', + group: 'linkGroup', + groupGallery: 'linkGroupGallery', + listing: 'linkListing', + newsEntry: 'linkNewsEntry', + staticPage: 'linkStaticPage', + tag: 'linkArtTag', + track: 'linkTrack', +}; + +const linkValueRelationMap = { + // media: 'linkPathFromMedia', + // root: 'linkPathFromRoot', + // site: 'linkPathFromSite', +}; + +const linkIndexRelationMap = { + // commentaryIndex: 'linkCommentaryIndex', + // flashIndex: 'linkFlashIndex', + // home: 'linkHome', + // listingIndex: 'linkListingIndex', + // newsIndex: 'linkNewsIndex', +}; + +function getPlaceholder(node, content) { + return {type: 'text', data: content.slice(node.i, node.iEnd)}; +} + +export default { + contentDependencies: [ + ...Object.values(linkThingRelationMap), + ...Object.values(linkValueRelationMap), + ...Object.values(linkIndexRelationMap), + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl(wikiData, content) { + const find = bindFind(wikiData); + + const parsedNodes = parseInput(content); + + return { + nodes: parsedNodes + .map(node => { + if (node.type !== 'tag') { + return node; + } + + const placeholder = getPlaceholder(node, content); + + const replacerKeyImplied = !node.data.replacerKey; + const replacerKey = replacerKeyImplied ? 'track' : node.data.replacerKey.data; + + // TODO: We don't support recursive nodes like before, at the moment. Sorry! + // const replacerValue = transformNodes(node.data.replacerValue, opts); + const replacerValue = node.data.replacerValue[0].data; + + const spec = replacerSpec[replacerKey]; + + if (!spec) { + return placeholder; + } + + if (spec.link) { + let data = {key: spec.link}; + + determineData: { + // No value at all: this is an index link. + if (!replacerValue) { + break determineData; + } + + // Nothing to find: the link operates on a path or string, not a data object. + if (!spec.find) { + data.value = replacerValue; + break determineData; + } + + const thing = + find[spec.find]( + (replacerKeyImplied + ? replacerValue + : replacerKey + `:` + replacerValue), + wikiData); + + // Nothing was found: this is unexpected, so return placeholder. + if (!thing) { + return placeholder; + } + + // Something was found: the link operates on that thing. + data.thing = thing; + } + + const {transformName} = spec; + + // TODO: Again, no recursive nodes. Sorry! + // const enteredLabel = node.data.label && transformNode(node.data.label, opts); + const enteredLabel = node.data.label?.data; + const enteredHash = node.data.hash?.data; + + data.label = + enteredLabel ?? + (transformName && data.thing.name + ? transformName(data.thing.name) + : null); + + data.hash = enteredHash ?? null; + + return {i: node.i, iEnd: node.iEnd, type: 'link', data}; + } + + // This will be another {type: 'tag'} node which gets processed in + // generate. + return node; + }), + }; + }, + + data(sprawl, content) { + return { + content, + + nodes: + sprawl.nodes + .map(node => { + // Replace link nodes with a stub. It'll be replaced (by position) + // with an item from relations. + if (node.type === 'link') { + return {type: 'link'}; + } + + // Other nodes will get processed in generate. + return node; + }), + }; + }, + + relations(relation, sprawl, content) { + const {nodes} = sprawl; + + const relationOrPlaceholder = + (node, name, arg) => + (name + ? { + link: relation(name, arg), + label: node.data.label, + hash: node.data.hash, + } + : getPlaceholder(node, content)); + + return { + links: + nodes + .filter(({type}) => type === 'link') + .map(node => { + const {key, thing, value} = node.data; + + if (thing) { + return relationOrPlaceholder(node, linkThingRelationMap[key], thing); + } else if (value) { + return relationOrPlaceholder(node, linkValueRelationMap[key], value); + } else { + return relationOrPlaceholder(node, linkIndexRelationMap[key]); + } + }), + }; + }, + + generate(data, relations, {html, language}) { + let linkIndex = 0; + + // This array contains only straight text and link nodes, which are directly + // representable in html (so no further processing is needed on the level of + // individual nodes). + const contentFromNodes = + data.nodes.map(node => { + if (node.type === 'text') { + return {type: 'text', data: node.data}; + } + + if (node.type === 'link') { + const {link, label, hash} = relations.links[linkIndex++]; + return { + type: 'link', + data: link.slots({content: label, hash}), + }; + } + + if (node.type === 'tag') { + const {replacerKey, replacerValue} = node.data; + + const spec = replacerSpec[replacerKey]; + + if (!spec) { + return getPlaceholder(node, data.content); + } + + const {value: valueFn, html: htmlFn} = spec; + + const value = + (valueFn + ? valueFn(replacerValue) + : replacerValue); + + const contents = + (htmlFn + ? htmlFn(value, {html, language}) + : value); + + return {type: 'text', data: contents}; + } + + return getPlaceholder(node, data.content); + }); + + return html.template({ + annotation: `transformContent`, + + slots: { + mode: { + validate: v => v.is('inline', 'multiline', 'lyrics'), + default: 'multiline', + }, + }, + + content(slots) { + // In inline mode, no further processing is needed! + + if (slots.mode === 'inline') { + return html.tags(contentFromNodes.map(node => node.data)); + } + + // In multiline mode... + + if (slots.mode === 'multiline') { + return html.tags(contentFromNodes.map(node => node.data)); + } + + // In lyrics mode... + + if (slots.mode === 'lyrics') { + return html.tags(contentFromNodes.map(node => node.data)); + } + }, + }); + }, +} diff --git a/src/util/replacer.js b/src/util/replacer.js index ea957ed..50a9000 100644 --- a/src/util/replacer.js +++ b/src/util/replacer.js @@ -221,11 +221,10 @@ function parseNodes(input, i, stopAt, textOnly) { let hash; if (stop_literal === tagHash) { - N = parseNodes(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]); + N = parseOneTextNode(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]); if (!stopped) throw endOfInput(i, `reading hash`); - - if (!N) throw makeError(i, `Expected content (hash).`); + if (!N) throw makeError(i, `Expected text (hash).`); hash = N; i = stop_iParse; @@ -294,6 +293,10 @@ function parseNodes(input, i, stopAt, textOnly) { } export function parseInput(input) { + if (typeof input !== 'string') { + throw new TypeError(`Expected input to be string, got ${input}`); + } + try { return parseNodes(input, 0); } catch (errorNode) { @@ -378,7 +381,7 @@ function evaluateTag(node, opts) { (transformName && transformName(value.name, node, input)) || null; - const hash = node.data.hash && transformNodes(node.data.hash, opts); + const hash = node.data.hash && transformNode(node.data.hash, opts); const args = node.data.args && diff --git a/src/util/transform-content.js b/src/util/transform-content.js index d1d0f51..454cb37 100644 --- a/src/util/transform-content.js +++ b/src/util/transform-content.js @@ -3,7 +3,6 @@ // interfaces for converting various content found in wiki data to HTML for // display on the site. -import * as html from './html.js'; export {transformInline} from './replacer.js'; export const replacerSpec = { @@ -34,7 +33,7 @@ export const replacerSpec = { date: { find: null, value: (ref) => new Date(ref), - html: (date, {language}) => + html: (date, {html, language}) => html.tag('time', {datetime: date.toString()}, language.formatDate(date)), diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index be9ad66..a31e02f 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -49,6 +49,7 @@ export function bindUtilities({ thumb, to, urls, + wikiData, wikiInfo: wikiData.wikiInfo, }); diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index c15fc46..d4b7472 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -364,7 +364,7 @@ export async function go({ // NOTE: ALL THIS STUFF IS PASTED, REVIEW AND INTEGRATE SOON(TM) - const treeInfo = getRelationsTree(allContentDependencies, name, ...args); + const treeInfo = getRelationsTree(allContentDependencies, name, wikiData, ...args); const flatTreeInfo = flattenRelationsTree(treeInfo); const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo; @@ -431,20 +431,25 @@ export async function go({ const slotResults = {}; - function runContentFunction({name, args, relations}) { + function runContentFunction({name, args, relations: flatRelations}) { const contentFunction = fulfilledContentDependencies[name]; if (!contentFunction) { throw new Error(`Content function ${name} unfulfilled or not listed`); } - const filledRelations = - fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, relations); + const sprawl = + contentFunction.sprawl?.(allExtraDependencies.wikiData, ...args); - const generateArgs = [ - contentFunction.data?.(...args), - filledRelations, - ].filter(Boolean); + const relations = + fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, flatRelations); + + const data = + (sprawl + ? contentFunction.data?.(sprawl, ...args) + : contentFunction.data?.(...args)); + + const generateArgs = [data, relations].filter(Boolean); return contentFunction(...generateArgs); } -- cgit 1.3.0-6-gf8a5