From 0a3b163772ab56ec0c0e775deec0ec3bed2b2825 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 11 Jun 2023 11:25:34 -0300 Subject: infra: don't perform redundant checks in expectDependencies --- src/content-function.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) (limited to 'src/content-function.js') diff --git a/src/content-function.js b/src/content-function.js index 73e4629e..2eb12f0e 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -9,6 +9,22 @@ export default function contentFunction({ data, generate, }) { + // Initial checks. These only need to be run once per description of a + // content function, and don't depend on any mutable context (e.g. which + // dependencies have been fulfilled so far). + + if (!generate) { + throw new Error(`Expected generate function`); + } + + if (sprawl && !extraDependencies.includes('wikiData')) { + throw new Error(`Content functions which sprawl must specify wikiData in extraDependencies`); + } + + // Pass all the details to expectDependencies, which will recursively build + // up a set of fulfilled dependencies and make functions like `relations` + // and `generate` callable only with sufficient fulfilled dependencies. + return expectDependencies({ sprawl, relations, @@ -33,18 +49,10 @@ export function expectDependencies({ expectedExtraDependencyKeys, fulfilledDependencies, }) { - if (!generate) { - throw new Error(`Expected generate function`); - } - 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); const invalidatingDependencyKeys = Object.entries(fulfilledDependencies) -- cgit 1.3.0-6-gf8a5 From 61d41ae57ec745b75ff9bc0568a2c6b9873acb89 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 11 Jun 2023 11:26:33 -0300 Subject: infra: move fulfilled clause to end of if-else/if-else chain --- src/content-function.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'src/content-function.js') diff --git a/src/content-function.js b/src/content-function.js index 2eb12f0e..c86d55a8 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -74,7 +74,14 @@ export function expectDependencies({ annotateFunction(wrappedGenerate, {name: generate, trait: 'invalidated'}); wrappedGenerate.fulfilled = false; - } else if (empty(missingContentDependencyKeys) && empty(missingExtraDependencyKeys)) { + } else if (!empty(missingContentDependencyKeys) || !empty(missingExtraDependencyKeys)) { + wrappedGenerate = function() { + throw new Error(`Dependencies still needed: ${missingContentDependencyKeys.concat(missingExtraDependencyKeys).join(', ')}`); + }; + + annotateFunction(wrappedGenerate, {name: generate, trait: 'unfulfilled'}); + wrappedGenerate.fulfilled = false; + } else { wrappedGenerate = function(arg1, arg2) { if (hasDataFunction && !arg1) { throw new Error(`Expected data`); @@ -103,13 +110,6 @@ export function expectDependencies({ wrappedGenerate.fulfill = function() { throw new Error(`All dependencies already fulfilled`); }; - } else { - wrappedGenerate = function() { - throw new Error(`Dependencies still needed: ${missingContentDependencyKeys.concat(missingExtraDependencyKeys).join(', ')}`); - }; - - annotateFunction(wrappedGenerate, {name: generate, trait: 'unfulfilled'}); - wrappedGenerate.fulfilled = false; } wrappedGenerate[contentFunction.identifyingSymbol] = true; -- cgit 1.3.0-6-gf8a5 From 4eba3396a31ffb13151d5603f2ba9473ef3d7a8c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 11 Jun 2023 11:42:06 -0300 Subject: infra: annotate errors with name outside fulfillDependencies --- src/content-function.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) (limited to 'src/content-function.js') diff --git a/src/content-function.js b/src/content-function.js index c86d55a8..ab9977f2 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -108,7 +108,7 @@ export function expectDependencies({ wrappedGenerate.fulfilled = true; wrappedGenerate.fulfill = function() { - throw new Error(`All dependencies already fulfilled`); + throw new Error(`All dependencies already fulfilled (${generate.name})`); }; } @@ -127,6 +127,21 @@ export function expectDependencies({ } wrappedGenerate.fulfill ??= function fulfill(dependencies) { + let newlyFulfilledDependencies; + + try { + newlyFulfilledDependencies = + fulfillDependencies({ + dependencies, + expectedContentDependencyKeys, + expectedExtraDependencyKeys, + fulfilledDependencies, + }); + } catch (error) { + error.message += ` (${generate.name})`; + throw error; + } + return expectDependencies({ sprawl, relations, @@ -136,15 +151,9 @@ export function expectDependencies({ expectedContentDependencyKeys, expectedExtraDependencyKeys, - fulfilledDependencies: fulfillDependencies({ - name: generate.name, - dependencies, - - expectedContentDependencyKeys, - expectedExtraDependencyKeys, - fulfilledDependencies, - }), + fulfilledDependencies: newlyFulfilledDependencies, }); + }; Object.assign(wrappedGenerate, { @@ -156,7 +165,6 @@ export function expectDependencies({ } export function fulfillDependencies({ - name, dependencies, expectedContentDependencyKeys, expectedExtraDependencyKeys, @@ -208,7 +216,7 @@ export function fulfillDependencies({ } if (!empty(errors)) { - throw new AggregateError(errors, `Errors fulfilling dependencies for ${name}`); + throw new AggregateError(errors, `Errors fulfilling dependencies`); } return newFulfilledDependencies; -- cgit 1.3.0-6-gf8a5 From 57aeed75e5ed503f5b79c3df730ae6b898652dc3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 11 Jun 2023 12:37:54 -0300 Subject: infra: treat fulfillment information as sets & reuse where possible --- src/content-function.js | 130 ++++++++++++++++++++++++++++-------------------- 1 file changed, 77 insertions(+), 53 deletions(-) (limited to 'src/content-function.js') diff --git a/src/content-function.js b/src/content-function.js index ab9977f2..18ede8e8 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -1,4 +1,8 @@ -import {annotateFunction, empty} from './util/sugar.js'; +import { + annotateFunction, + empty, + setIntersection, +} from './util/sugar.js'; export default function contentFunction({ contentDependencies = [], @@ -9,15 +13,25 @@ export default function contentFunction({ data, generate, }) { + const expectedContentDependencyKeys = new Set(contentDependencies); + const expectedExtraDependencyKeys = new Set(extraDependencies); + // Initial checks. These only need to be run once per description of a // content function, and don't depend on any mutable context (e.g. which // dependencies have been fulfilled so far). + const overlappingContentExtraDependencyKeys = + setIntersection(expectedContentDependencyKeys, expectedExtraDependencyKeys); + + if (!empty(overlappingContentExtraDependencyKeys)) { + throw new Error(`Overlap in content and extra dependency keys: ${[...overlappingContentExtraDependencyKeys].join(', ')}`); + } + if (!generate) { throw new Error(`Expected generate function`); } - if (sprawl && !extraDependencies.includes('wikiData')) { + if (sprawl && !expectedExtraDependencyKeys.has('wikiData')) { throw new Error(`Content functions which sprawl must specify wikiData in extraDependencies`); } @@ -31,8 +45,11 @@ export default function contentFunction({ data, generate, - expectedContentDependencyKeys: contentDependencies, - expectedExtraDependencyKeys: extraDependencies, + expectedContentDependencyKeys, + expectedExtraDependencyKeys, + missingContentDependencyKeys: new Set(expectedContentDependencyKeys), + missingExtraDependencyKeys: new Set(expectedExtraDependencyKeys), + fulfilledDependencyKeys: new Set(), fulfilledDependencies: {}, }); } @@ -47,36 +64,36 @@ export function expectDependencies({ expectedContentDependencyKeys, expectedExtraDependencyKeys, + missingContentDependencyKeys, + missingExtraDependencyKeys, + fulfilledDependencyKeys, fulfilledDependencies, }) { const hasSprawlFunction = !!sprawl; const hasRelationsFunction = !!relations; const hasDataFunction = !!data; - const fulfilledDependencyKeys = Object.keys(fulfilledDependencies); + const invalidatingDependencyKeys = + Object.entries(fulfilledDependencies) + .filter(([key, value]) => value?.fulfilled === false) + .map(([key]) => key); - const invalidatingDependencyKeys = Object.entries(fulfilledDependencies) - .filter(([key, value]) => value?.fulfilled === false) - .map(([key]) => key); - - const missingContentDependencyKeys = expectedContentDependencyKeys - .filter(key => !fulfilledDependencyKeys.includes(key)); - - const missingExtraDependencyKeys = expectedExtraDependencyKeys - .filter(key => !fulfilledDependencyKeys.includes(key)); + const isInvalidated = !empty(invalidatingDependencyKeys); + const isMissingContentDependencies = !empty(missingContentDependencyKeys); + const isMissingExtraDependencies = !empty(missingExtraDependencyKeys); let wrappedGenerate; - if (!empty(invalidatingDependencyKeys)) { + if (isInvalidated) { wrappedGenerate = function() { throw new Error(`Generate invalidated because unfulfilled dependencies provided: ${invalidatingDependencyKeys.join(', ')}`); }; annotateFunction(wrappedGenerate, {name: generate, trait: 'invalidated'}); wrappedGenerate.fulfilled = false; - } else if (!empty(missingContentDependencyKeys) || !empty(missingExtraDependencyKeys)) { + } else if (isMissingContentDependencies || isMissingExtraDependencies) { wrappedGenerate = function() { - throw new Error(`Dependencies still needed: ${missingContentDependencyKeys.concat(missingExtraDependencyKeys).join(', ')}`); + throw new Error(`Dependencies still needed: ${[...missingContentDependencyKeys, ...missingExtraDependencyKeys].join(', ')}`); }; annotateFunction(wrappedGenerate, {name: generate, trait: 'unfulfilled'}); @@ -127,16 +144,22 @@ export function expectDependencies({ } wrappedGenerate.fulfill ??= function fulfill(dependencies) { - let newlyFulfilledDependencies; + // To avoid unneeded destructuring, `fullfillDependencies` is a mutating + // function. But `fulfill` itself isn't meant to mutate! We create a copy + // of these variables, so their original values are kept for additional + // calls to this same `fulfill`. + const newlyMissingContentDependencyKeys = new Set(missingContentDependencyKeys); + const newlyMissingExtraDependencyKeys = new Set(missingExtraDependencyKeys); + const newlyFulfilledDependencyKeys = new Set(fulfilledDependencyKeys); + const newlyFulfilledDependencies = {...fulfilledDependencies}; try { - newlyFulfilledDependencies = - fulfillDependencies({ - dependencies, - expectedContentDependencyKeys, - expectedExtraDependencyKeys, - fulfilledDependencies, - }); + fulfillDependencies(dependencies, { + missingContentDependencyKeys: newlyMissingContentDependencyKeys, + missingExtraDependencyKeys: newlyMissingExtraDependencyKeys, + fulfilledDependencyKeys: newlyFulfilledDependencyKeys, + fulfilledDependencies: newlyFulfilledDependencies, + }); } catch (error) { error.message += ` (${generate.name})`; throw error; @@ -150,7 +173,9 @@ export function expectDependencies({ expectedContentDependencyKeys, expectedExtraDependencyKeys, - + missingContentDependencyKeys: newlyMissingContentDependencyKeys, + missingExtraDependencyKeys: newlyMissingExtraDependencyKeys, + fulfilledDependencyKeys: newlyFulfilledDependencyKeys, fulfilledDependencies: newlyFulfilledDependencies, }); @@ -164,62 +189,61 @@ export function expectDependencies({ return wrappedGenerate; } -export function fulfillDependencies({ - dependencies, - expectedContentDependencyKeys, - expectedExtraDependencyKeys, +export function fulfillDependencies(dependencies, { + missingContentDependencyKeys, + missingExtraDependencyKeys, + fulfilledDependencyKeys, fulfilledDependencies, }) { - const newFulfilledDependencies = {...fulfilledDependencies}; - const fulfilledDependencyKeys = Object.keys(fulfilledDependencies); + // This is a mutating function. Be aware: it WILL mutate the provided sets + // and objects EVEN IF there are errors. This function doesn't exit early, + // so all provided dependencies which don't have an associated error should + // be treated as fulfilled (this is reflected via fulfilledDependencyKeys). const errors = []; - let bail = false; for (let [key, value] of Object.entries(dependencies)) { - if (fulfilledDependencyKeys.includes(key)) { + if (fulfilledDependencyKeys.has(key)) { errors.push(new Error(`Dependency ${key} is already fulfilled`)); - bail = true; continue; } - const isContentKey = expectedContentDependencyKeys.includes(key); - const isExtraKey = expectedExtraDependencyKeys.includes(key); + const isContentKey = missingContentDependencyKeys.has(key); + const isExtraKey = missingExtraDependencyKeys.has(key); if (!isContentKey && !isExtraKey) { errors.push(new Error(`Dependency ${key} is not expected`)); - bail = true; continue; } if (value === undefined) { errors.push(new Error(`Dependency ${key} was provided undefined`)); - bail = true; continue; } - if (isContentKey && !value?.[contentFunction.identifyingSymbol]) { - errors.push(new Error(`Content dependency ${key} is not a content function (got ${value})`)); - bail = true; - continue; - } + const isContentFunction = !!value?.[contentFunction.identifyingSymbol]; - if (isExtraKey && value?.[contentFunction.identifyingSymbol]) { - errors.push(new Error(`Extra dependency ${key} is a content function`)); - bail = true; - continue; + if (isContentKey) { + if (!isContentFunction) { + errors.push(new Error(`Content dependency ${key} is not a content function (got ${value})`)); + continue; + } + missingContentDependencyKeys.delete(key); + } else if (isExtraKey) { + if (isContentFunction) { + errors.push(new Error(`Extra dependency ${key} is a content function`)); + continue; + } + missingExtraDependencyKeys.delete(key); } - if (!bail) { - newFulfilledDependencies[key] = value; - } + fulfilledDependencyKeys.add(key); + fulfilledDependencies[key] = value; } if (!empty(errors)) { throw new AggregateError(errors, `Errors fulfilling dependencies`); } - - return newFulfilledDependencies; } export function getRelationsTree(dependencies, contentFunctionName, wikiData, ...args) { -- cgit 1.3.0-6-gf8a5 From bd2862e1b194d72fd403626897bf559a1eba9107 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 11 Jun 2023 12:50:47 -0300 Subject: infra: reuse invalidatingDependencyKeys instead of reconstructing --- src/content-function.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) (limited to 'src/content-function.js') diff --git a/src/content-function.js b/src/content-function.js index 18ede8e8..7b975e6a 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -49,6 +49,7 @@ export default function contentFunction({ expectedExtraDependencyKeys, missingContentDependencyKeys: new Set(expectedContentDependencyKeys), missingExtraDependencyKeys: new Set(expectedExtraDependencyKeys), + invalidatingDependencyKeys: new Set(), fulfilledDependencyKeys: new Set(), fulfilledDependencies: {}, }); @@ -66,6 +67,7 @@ export function expectDependencies({ expectedExtraDependencyKeys, missingContentDependencyKeys, missingExtraDependencyKeys, + invalidatingDependencyKeys, fulfilledDependencyKeys, fulfilledDependencies, }) { @@ -73,11 +75,6 @@ export function expectDependencies({ const hasRelationsFunction = !!relations; const hasDataFunction = !!data; - const invalidatingDependencyKeys = - Object.entries(fulfilledDependencies) - .filter(([key, value]) => value?.fulfilled === false) - .map(([key]) => key); - const isInvalidated = !empty(invalidatingDependencyKeys); const isMissingContentDependencies = !empty(missingContentDependencyKeys); const isMissingExtraDependencies = !empty(missingExtraDependencyKeys); @@ -86,7 +83,7 @@ export function expectDependencies({ if (isInvalidated) { wrappedGenerate = function() { - throw new Error(`Generate invalidated because unfulfilled dependencies provided: ${invalidatingDependencyKeys.join(', ')}`); + throw new Error(`Generate invalidated because unfulfilled dependencies provided: ${[...invalidatingDependencyKeys].join(', ')}`); }; annotateFunction(wrappedGenerate, {name: generate, trait: 'invalidated'}); @@ -150,6 +147,7 @@ export function expectDependencies({ // calls to this same `fulfill`. const newlyMissingContentDependencyKeys = new Set(missingContentDependencyKeys); const newlyMissingExtraDependencyKeys = new Set(missingExtraDependencyKeys); + const newlyInvalidatingDependencyKeys = new Set(invalidatingDependencyKeys); const newlyFulfilledDependencyKeys = new Set(fulfilledDependencyKeys); const newlyFulfilledDependencies = {...fulfilledDependencies}; @@ -157,6 +155,7 @@ export function expectDependencies({ fulfillDependencies(dependencies, { missingContentDependencyKeys: newlyMissingContentDependencyKeys, missingExtraDependencyKeys: newlyMissingExtraDependencyKeys, + invalidatingDependencyKeys: newlyInvalidatingDependencyKeys, fulfilledDependencyKeys: newlyFulfilledDependencyKeys, fulfilledDependencies: newlyFulfilledDependencies, }); @@ -175,6 +174,7 @@ export function expectDependencies({ expectedExtraDependencyKeys, missingContentDependencyKeys: newlyMissingContentDependencyKeys, missingExtraDependencyKeys: newlyMissingExtraDependencyKeys, + invalidatingDependencyKeys: newlyInvalidatingDependencyKeys, fulfilledDependencyKeys: newlyFulfilledDependencyKeys, fulfilledDependencies: newlyFulfilledDependencies, }); @@ -192,6 +192,7 @@ export function expectDependencies({ export function fulfillDependencies(dependencies, { missingContentDependencyKeys, missingExtraDependencyKeys, + invalidatingDependencyKeys, fulfilledDependencyKeys, fulfilledDependencies, }) { @@ -228,12 +229,18 @@ export function fulfillDependencies(dependencies, { errors.push(new Error(`Content dependency ${key} is not a content function (got ${value})`)); continue; } + + if (!value.fulfilled) { + invalidatingDependencyKeys.add(key); + } + missingContentDependencyKeys.delete(key); } else if (isExtraKey) { if (isContentFunction) { errors.push(new Error(`Extra dependency ${key} is a content function`)); continue; } + missingExtraDependencyKeys.delete(key); } -- cgit 1.3.0-6-gf8a5 From 8025665eec0d02f67d6333cd10a6826c6703bb28 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 11 Jun 2023 13:26:29 -0300 Subject: infra: quick code style changes --- src/content-function.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'src/content-function.js') diff --git a/src/content-function.js b/src/content-function.js index 7b975e6a..3f9d658a 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -118,12 +118,12 @@ export function expectDependencies({ } }; - annotateFunction(wrappedGenerate, {name: generate, trait: 'fulfilled'}); - wrappedGenerate.fulfilled = true; - wrappedGenerate.fulfill = function() { throw new Error(`All dependencies already fulfilled (${generate.name})`); }; + + annotateFunction(wrappedGenerate, {name: generate, trait: 'fulfilled'}); + wrappedGenerate.fulfilled = true; } wrappedGenerate[contentFunction.identifyingSymbol] = true; @@ -222,7 +222,11 @@ export function fulfillDependencies(dependencies, { continue; } - const isContentFunction = !!value?.[contentFunction.identifyingSymbol]; + const isContentFunction = + !!value?.[contentFunction.identifyingSymbol]; + + const isFulfilledContentFunction = + isContentFunction && value.fulfilled; if (isContentKey) { if (!isContentFunction) { @@ -230,7 +234,7 @@ export function fulfillDependencies(dependencies, { continue; } - if (!value.fulfilled) { + if (!isFulfilledContentFunction) { invalidatingDependencyKeys.add(key); } -- cgit 1.3.0-6-gf8a5 From 1fe5de18a35a51b251c08995e619a5a526425236 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 12 Jun 2023 14:51:45 -0300 Subject: infra: automatically interface html.stationery via slots --- src/content-function.js | 49 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) (limited to 'src/content-function.js') diff --git a/src/content-function.js b/src/content-function.js index 3f9d658a..d4cc3dbc 100644 --- a/src/content-function.js +++ b/src/content-function.js @@ -8,6 +8,7 @@ export default function contentFunction({ contentDependencies = [], extraDependencies = [], + slots, sprawl, relations, data, @@ -35,11 +36,16 @@ export default function contentFunction({ throw new Error(`Content functions which sprawl must specify wikiData in extraDependencies`); } + if (slots && !expectedExtraDependencyKeys.has('html')) { + throw new Error(`Content functions with slots must specify html in extraDependencies`); + } + // Pass all the details to expectDependencies, which will recursively build // up a set of fulfilled dependencies and make functions like `relations` // and `generate` callable only with sufficient fulfilled dependencies. return expectDependencies({ + slots, sprawl, relations, data, @@ -58,6 +64,7 @@ export default function contentFunction({ contentFunction.identifyingSymbol = Symbol(`Is a content function?`); export function expectDependencies({ + slots, sprawl, relations, data, @@ -74,6 +81,7 @@ export function expectDependencies({ const hasSprawlFunction = !!sprawl; const hasRelationsFunction = !!relations; const hasDataFunction = !!data; + const hasSlotsDescription = !!slots; const isInvalidated = !empty(invalidatingDependencyKeys); const isMissingContentDependencies = !empty(missingContentDependencyKeys); @@ -96,7 +104,7 @@ export function expectDependencies({ annotateFunction(wrappedGenerate, {name: generate, trait: 'unfulfilled'}); wrappedGenerate.fulfilled = false; } else { - wrappedGenerate = function(arg1, arg2) { + const callUnderlyingGenerate = ([arg1, arg2], ...extraArgs) => { if (hasDataFunction && !arg1) { throw new Error(`Expected data`); } @@ -110,14 +118,46 @@ export function expectDependencies({ } if (hasDataFunction && hasRelationsFunction) { - return generate(arg1, arg2, fulfilledDependencies); + return generate(arg1, arg2, ...extraArgs, fulfilledDependencies); } else if (hasDataFunction || hasRelationsFunction) { - return generate(arg1, fulfilledDependencies); + return generate(arg1, ...extraArgs, fulfilledDependencies); } else { - return generate(fulfilledDependencies); + return generate(...extraArgs, fulfilledDependencies); } }; + if (hasSlotsDescription) { + const stationery = fulfilledDependencies.html.stationery({ + annotation: generate.name, + + // These extra slots are for the data and relations (positional) args. + // No hacks to store them temporarily or otherwise "invisibly" alter + // the behavior of the template description's `content`, since that + // would be expressly against the purpose of templates! + slots: { + _cfArg1: {validate: v => v.isObject}, + _cfArg2: {validate: v => v.isObject}, + ...slots, + }, + + content(slots) { + const args = [slots._cfArg1, slots._cfArg2]; + return callUnderlyingGenerate(args, slots); + }, + }); + + wrappedGenerate = function(...args) { + return stationery.template().slots({ + _cfArg1: args[0] ?? null, + _cfArg2: args[1] ?? null, + }); + }; + } else { + wrappedGenerate = function(...args) { + return callUnderlyingGenerate(args); + }; + } + wrappedGenerate.fulfill = function() { throw new Error(`All dependencies already fulfilled (${generate.name})`); }; @@ -165,6 +205,7 @@ export function expectDependencies({ } return expectDependencies({ + slots, sprawl, relations, data, -- cgit 1.3.0-6-gf8a5