84 lines
3.3 KiB
JavaScript
84 lines
3.3 KiB
JavaScript
/**
|
|
* A skill's declared section, as a node in the page tree.
|
|
*
|
|
* This is the join between the two halves of the UI system. A Board skill's
|
|
* `ui:` block is still parsed by `uiConfig.normalizeSection`, still validated
|
|
* against the closed vocabulary, and still drawn by the same nine components —
|
|
* nothing about definitions changes. What this adds is *identity*: the section
|
|
* becomes an addressable node, so it can be hidden, moved and reordered by the
|
|
* same operations that move a built-in.
|
|
*
|
|
* **Nothing here writes back to Markdown.** A node carries `origin: 'skill'`,
|
|
* and a change to it is stored as an operation in the person's own layout
|
|
* patch. The definition on disk, and the definition in the account's custom
|
|
* skills, are read-only from here — which is what keeps a layout preference
|
|
* from silently editing something another user also sees.
|
|
*/
|
|
|
|
import { dataSourceFor, sourceSupportsOption } from '@/lib/skills/surfaces';
|
|
import { makeNode } from './node';
|
|
|
|
/** Ids are `skill-<skill>-<section>`, so provenance is legible in the DOM. */
|
|
export const skillNodeId = (skillId, sectionId) => [
|
|
'skill', slug(skillId), slug(sectionId),
|
|
].filter(Boolean).join('-');
|
|
|
|
const slug = (value) => String(value || '')
|
|
.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
|
|
/**
|
|
* One normalized section, adapted.
|
|
*
|
|
* Only options the source actually declares are carried across. The skill
|
|
* format checks that a period is a real period; this checks that the reading
|
|
* takes periods at all — a stricter rule, and one a definition written against
|
|
* the looser one could fail. Dropping the option is right where refusing the
|
|
* node would not be: a Board card must never disappear because of a parameter
|
|
* that was doing nothing anyway.
|
|
*/
|
|
export function skillSectionNode(skill, section) {
|
|
const params = {};
|
|
if (section.periods?.length && sourceSupportsOption(section.source, 'periods')) {
|
|
params.periods = [...section.periods];
|
|
}
|
|
if (section.limit && sourceSupportsOption(section.source, 'limit')) {
|
|
params.limit = section.limit;
|
|
}
|
|
|
|
return makeNode({
|
|
id: skillNodeId(skill.id, section.id),
|
|
type: section.type,
|
|
origin: 'skill',
|
|
data: { source: section.source, params },
|
|
props: {
|
|
/* The same fallback the surface uses, so a section with no title of its
|
|
own is still named by the skill that contributed it. */
|
|
title: section.title || skill.name,
|
|
...(section.description ? { description: section.description } : {}),
|
|
/* Attribution is what lets a reader tell an extension from a built-in
|
|
panel, and which skill to switch off. */
|
|
attribution: skill.name,
|
|
...(section.editable ? { editable: true } : {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Every section a page's definitions contribute, grouped by placement.
|
|
*
|
|
* Grouped because that is how the composition consumes them: each
|
|
* `skill-surface` node names one placement and takes the sections that declared
|
|
* it. A placement no slot offers simply has nowhere to render, which is the
|
|
* same outcome as today.
|
|
*/
|
|
export function skillNodesByPlacement(sections = []) {
|
|
const out = {};
|
|
for (const { skill, section } of sections) {
|
|
if (!dataSourceFor(section.source)) continue;
|
|
const key = section.placement || '';
|
|
if (!out[key]) out[key] = [];
|
|
out[key].push(skillSectionNode(skill, section));
|
|
}
|
|
return out;
|
|
}
|