diff options
| author | (quasar) nebula <qznebula@protonmail.com> | 2026-04-02 23:27:55 -0300 |
|---|---|---|
| committer | (quasar) nebula <qznebula@protonmail.com> | 2026-04-02 23:27:55 -0300 |
| commit | 818e89c7ee0a426ac5f66a4079c70e047627a7f2 (patch) | |
| tree | 489d6651767e7b7214324e8dfbc47fed1d2deda6 /src | |
| parent | b250e91a74b244cc5decd79f5604cfb8a811421a (diff) | |
content: generateDividedTrackList: context groups
Diffstat (limited to 'src')
| -rw-r--r-- | src/common-util/sugar.js | 32 | ||||
| -rw-r--r-- | src/common-util/wiki-data.js | 51 | ||||
| -rw-r--r-- | src/content/dependencies/generateDividedTrackList.js | 145 | ||||
| -rw-r--r-- | src/static/js/search-worker.js | 24 |
4 files changed, 178 insertions, 74 deletions
diff --git a/src/common-util/sugar.js b/src/common-util/sugar.js index c988156c..26f33c20 100644 --- a/src/common-util/sugar.js +++ b/src/common-util/sugar.js @@ -229,6 +229,38 @@ export const mapInPlace = (array, fn) => export const unique = (arr) => Array.from(new Set(arr)); +export function* permutations(array) { + switch (array.length) { + case 0: return; + case 1: yield array; return; + default: { + const behind = []; + const ahead = array.slice(); + while (ahead.length) { + const here = ahead.shift(); + + yield* + permutations([...behind, ...ahead]) + .map(slice => [here, ...slice]); + + behind.push(here); + } + } + } +} + +export function* runs(array) { + switch (array.length) { + case 0: return; + case 1: yield array; return; + default: { + yield* runs(array.slice(1)).map(run => [array[0], ...run]); + yield [array[0]]; + yield* runs(array.slice(1)); + } + } +} + export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) => arr1.length === arr2.length && (checkOrder diff --git a/src/common-util/wiki-data.js b/src/common-util/wiki-data.js index 74222e9e..21e15725 100644 --- a/src/common-util/wiki-data.js +++ b/src/common-util/wiki-data.js @@ -506,8 +506,23 @@ export class TupleMap { } export class TupleMapForBabies { - #here = new WeakMap(); - #next = new WeakMap(); + #here; + #next; + #mode; + + constructor(mode = 'weak') { + if (mode === 'weak') { + this.#mode = 'weak'; + this.#here = new WeakMap(); + this.#next = new WeakMap(); + } else if (mode === 'strong') { + this.#mode = 'strong'; + this.#here = new Map(); + this.#next = new Map(); + } else { + throw new Error(`Expected mode to be weak or strong`); + } + } set(...args) { const first = args.at(0); @@ -519,7 +534,7 @@ export class TupleMapForBabies { } else if (this.#next.has(first)) { this.#next.get(first).set(...rest, last); } else { - const tupleMap = new TupleMapForBabies(); + const tupleMap = new TupleMapForBabies(this.#mode); this.#next.set(first, tupleMap); tupleMap.set(...rest, last); } @@ -550,6 +565,36 @@ export class TupleMapForBabies { return false; } } + + *keys() { + if (this.#mode === 'weak') { + throw new Error(`Can't get keys of a weak tuple map`); + } + + for (const key of this.#here.keys()) { + yield [key]; + + if (this.#next.has(key)) { + for (const next of this.#next.get(key).keys()) { + yield [key, ...next]; + } + } + } + } + + *values() { + if (this.#mode === 'weak') { + throw new Error(`Can't get values of a weak tuple map`); + } + + for (const key of this.#here.keys()) { + yield this.#here.get(key); + + if (this.#next.has(key)) { + yield* this.#next.get(key).values(); + } + } + } } const combinedWikiDataTupleMap = new TupleMapForBabies(); diff --git a/src/content/dependencies/generateDividedTrackList.js b/src/content/dependencies/generateDividedTrackList.js index ea349ec8..584eb920 100644 --- a/src/content/dependencies/generateDividedTrackList.js +++ b/src/content/dependencies/generateDividedTrackList.js @@ -1,4 +1,5 @@ -import {empty, filterMultipleArrays, stitchArrays} from '#sugar'; +import {empty, filterMultipleArrays, runs, stitchArrays, unique} from '#sugar'; +import {TupleMapForBabies} from '#wiki-data'; export default { sprawl: ({wikiInfo}) => ({ @@ -6,40 +7,81 @@ export default { wikiInfo.divideTrackListsByGroups, }), - query(sprawl, tracks, _contextTrack) { - const dividingGroups = sprawl.divideTrackListsByGroups; + query(sprawl, tracks, contextTrack) { + const wikiDividingGroups = sprawl.divideTrackListsByGroups; - const groupings = new Map(); + const contextDividingGroups = + contextTrack.groups + .filter(group => !wikiDividingGroups.includes(group)); + + const contextGroupRuns = + Array.from(runs(contextDividingGroups)); + + const groupings = new TupleMapForBabies('strong'); const ungroupedTracks = []; - // Entry order matters! Add blank lists for each group - // in the order that those groups are provided. - for (const group of dividingGroups) { - groupings.set(group, []); - } + const order = [ + ...contextGroupRuns, + ...wikiDividingGroups.map(group => [group]), + ]; for (const track of tracks) { - const firstMatchingGroup = - dividingGroups.find(group => group.albums.includes(track.album)); + let run; + getRun: { + run = + contextDividingGroups + .filter(group => track.groups.includes(group)); + + if (!empty(run)) { + break getRun; + } + + const wikiGroup = + wikiDividingGroups.find(group => track.groups.includes(group)); + + if (wikiGroup) { + run = [wikiGroup]; + break getRun; + } - if (firstMatchingGroup) { - groupings.get(firstMatchingGroup).push(track); - } else { ungroupedTracks.push(track); + continue; + } + + if (groupings.has(...run)) { + groupings.get(...run).push(track); + } else { + groupings.set(...run, [track]); } } - const groups = Array.from(groupings.keys()); - const groupedTracks = Array.from(groupings.values()); + let groupingGroups = order.slice(); + const groupedTracks = order.map(run => groupings.get(...run) ?? []); - // Drop the empty lists, so just the groups which - // at least a single track matched are left. filterMultipleArrays( - groups, + groupingGroups, groupedTracks, - (_group, tracks) => !empty(tracks)); - - return {groups, groupedTracks, ungroupedTracks}; + (_groups, tracks) => !empty(tracks)); + + const presentContextDividingGroups = + unique(groupingGroups.flat()) + .filter(group => contextDividingGroups.includes(group)); + + const uninformativeGroups = + presentContextDividingGroups.filter((group, index) => + presentContextDividingGroups + .slice(0, index) + .some(earlier => + groupingGroups.every(run => + run.includes(earlier) && run.includes(group) || + !run.includes(earlier) && !run.includes(group)))); + + groupingGroups = + groupingGroups.map(groups => + groups.filter(group => + !uninformativeGroups.includes(group))); + + return {groupingGroups, groupedTracks, ungroupedTracks}; }, relations: (relation, query, sprawl, tracks, contextTrack) => ({ @@ -52,8 +94,9 @@ export default { relation('generateContentHeading'), groupLinks: - query.groups - .map(group => relation('linkGroup', group)), + query.groupingGroups + .map(groups => groups + .map(group => relation('linkGroup', group))), groupedTrackLists: query.groupedTracks @@ -67,8 +110,9 @@ export default { data: (query, _sprawl, _tracks) => ({ groupNames: - query.groups - .map(group => group.name), + query.groupingGroups + .map(groups => groups + .map(group => group.name)), }), slots: { @@ -85,33 +129,36 @@ export default { language.encapsulate('trackList', listCapsule => [ stitchArrays({ - groupName: data.groupNames, - groupLink: relations.groupLinks, + groupNames: data.groupNames, + groupLinks: relations.groupLinks, trackList: relations.groupedTrackLists, }).map(({ - groupName, - groupLink, + groupNames, + groupLinks, trackList, }) => [ - language.encapsulate(listCapsule, 'fromGroup', capsule => - (slots.headingString - ? relations.contentHeading.clone().slots({ - tag: 'dt', - - title: - language.$(capsule, { - group: groupLink.slot('color', false), - }), - - stickyTitle: - language.$(slots.headingString, 'sticky', 'fromGroup', { - group: groupName, - }), - }) - : html.tag('dt', - language.$(capsule, { - group: groupLink.slot('color', false), - })))), + language.encapsulate(listCapsule, 'fromGroup', capsule => { + const title = + language.$(capsule, { + group: + language.formatConjunctionList( + groupLinks.map(link => link.slot('color', false))), + }); + + if (slots.headingString) { + return relations.contentHeading.clone().slots({ + tag: 'dt', + title, + stickyTitle: + language.$(slots.headingString, 'sticky', 'fromGroup', { + group: + language.formatConjunctionList(groupNames), + }), + }); + } else { + return html.tag('dt', title); + } + }), html.tag('dd', trackList), ]), diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js index b79df3d4..9ccaa95d 100644 --- a/src/static/js/search-worker.js +++ b/src/static/js/search-worker.js @@ -6,6 +6,7 @@ import {default as searchSpec, makeSearchIndex} import { empty, groupArray, + permutations, promiseWithResolvers, stitchArrays, unique, @@ -542,7 +543,7 @@ function queryIndex({termsKey, indexKey}, query, options) { const queriesBy = keys => (groupedParticles.get(keys.length) ?? []) - .flatMap(permutations) + .flatMap(particles => Array.from(permutations(particles))) .map(values => values.map(({terms}) => terms.join(' '))) .map(values => stitchArrays({ @@ -692,27 +693,6 @@ function particulate(terms) { return results; } -// This function doesn't even come close to "performant", -// but it only operates on small data here. -function permutations(array) { - switch (array.length) { - case 0: - return []; - - case 1: - return [array]; - - default: - return array.flatMap((item, index) => { - const behind = array.slice(0, index); - const ahead = array.slice(index + 1); - return ( - permutations([...behind, ...ahead]) - .map(rest => [item, ...rest])); - }); - } -} - function queryBoilerplate(index) { const idToDoc = {}; |