update Markdown skills
This commit is contained in:
@@ -1,8 +1,20 @@
|
||||
import {
|
||||
SUPPORTED_DATA_SOURCES, SUPPORTED_PERIODS, SUPPORTED_SECTION_TYPES,
|
||||
SUPPORTED_SKILL_PAGES, canonicalPage, dataSourceFor, isSourceWritable, surfaceFor,
|
||||
SUPPORTED_SKILL_PAGES, canonicalPage, dataSourceFor, isSourceWritable, placementFor,
|
||||
surfaceFor,
|
||||
} from './surfaces';
|
||||
|
||||
/**
|
||||
* A name, as an id.
|
||||
*
|
||||
* Exported because two places need the same rule and were carrying their own:
|
||||
* a section falling back to its title, and a definition falling back to its
|
||||
* name when it declares no `id:`. Two slug functions that agree today is a
|
||||
* definition whose id changes the day they stop agreeing.
|
||||
*/
|
||||
export const slugify = (value) => String(value || '')
|
||||
.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
|
||||
/**
|
||||
* The `ui:` block of a skill definition, checked and normalized.
|
||||
*
|
||||
@@ -53,9 +65,7 @@ export function normalizeSection(raw, {
|
||||
/* An id is how a section is keyed and de-duplicated, not something an author
|
||||
should have to invent for a definition that declares exactly one. Falls
|
||||
back to the title, then to the skill's own id. */
|
||||
const slug = (value) => String(value || '')
|
||||
.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
const id = slug(raw.id) || slug(raw.title) || slug(fallbackId);
|
||||
const id = slugify(raw.id) || slugify(raw.title) || slugify(fallbackId);
|
||||
if (!id) {
|
||||
errors.push(`${where}: a section needs an \`id\`.`);
|
||||
return null;
|
||||
@@ -70,7 +80,20 @@ export function normalizeSection(raw, {
|
||||
}
|
||||
seen.add(id);
|
||||
|
||||
const type = String(raw.type || '').trim();
|
||||
/**
|
||||
* The shape this section is drawn with.
|
||||
*
|
||||
* Inferred from the source when it is not written, the same way a placement
|
||||
* left out is filled in from the surface. A source declares the shapes it can
|
||||
* fill, in the order they suit it, so the first is the reading its author
|
||||
* would have chosen — and a definition naming a page, a placement and a
|
||||
* source has already said everything that needs saying. Refusing it for the
|
||||
* one field it can derive would be the format asking for ceremony.
|
||||
*/
|
||||
const declaredSource = String(raw.data?.source ?? raw.source ?? '').trim();
|
||||
const type = String(raw.type || '').trim()
|
||||
|| dataSourceFor(declaredSource)?.shapes?.find((shape) => types.includes(shape))
|
||||
|| '';
|
||||
if (!type) {
|
||||
errors.push(`${at || `${where}.${id}`}: a section needs a \`type\`.`);
|
||||
return null;
|
||||
@@ -89,10 +112,16 @@ export function normalizeSection(raw, {
|
||||
if (wantPlacement) {
|
||||
const surface = surfaceFor(page);
|
||||
const declaredPlacement = String(raw.position || raw.placement || '').trim();
|
||||
placement = declaredPlacement || surface.placements[0];
|
||||
if (!surface.placements.includes(placement)) {
|
||||
/* `placementFor` reads the surface's own list and the alias table, so
|
||||
`grid-card` and `after-position-card` are one placement written two ways
|
||||
and neither resolves to something the surface does not offer. */
|
||||
placement = declaredPlacement
|
||||
? placementFor(page, declaredPlacement)
|
||||
: surface.placements[0];
|
||||
if (!placement) {
|
||||
errors.push(
|
||||
`Unsupported placement: ${placement} on ${page}. Supported placements: ${surface.placements.join(', ')}.`
|
||||
`Unsupported placement: ${declaredPlacement} on ${page}. `
|
||||
+ `Supported placements: ${surface.placements.join(', ')}.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -102,7 +131,7 @@ export function normalizeSection(raw, {
|
||||
groups options when a section grows; the flat form is what a one-section
|
||||
definition actually reads like, and refusing it would be the format being
|
||||
precious about punctuation. */
|
||||
const source = String(raw.data?.source ?? raw.source ?? '').trim();
|
||||
const source = declaredSource;
|
||||
if (!source) {
|
||||
errors.push(`${at || `${where}.${id}`}: a section needs \`data.source\`.`);
|
||||
return null;
|
||||
@@ -190,6 +219,22 @@ export function normalizeSection(raw, {
|
||||
* and the difference is structural — an object carrying `type` or `source` is a
|
||||
* section, an object whose keys are page names is a page map. Nothing is
|
||||
* decided by a skill id, and neither shape is privileged.
|
||||
*
|
||||
* There is a third, and it is the one people actually write:
|
||||
*
|
||||
* ui:
|
||||
* - page: Positions
|
||||
* placement: grid-card
|
||||
* source: position.activity
|
||||
* - page: Analytics
|
||||
* placement: panel
|
||||
* source: hires.performance
|
||||
*
|
||||
* A list of sections, each naming its own page. It reads the way the thing
|
||||
* reads — "this skill puts this here, and that there" — and it was the one
|
||||
* shape the parser refused, with a single error that took the whole block down
|
||||
* and left the definition declaring no pages at all. Several entries may name
|
||||
* the same page; they become several sections on it, in the order written.
|
||||
*/
|
||||
const SECTION_KEYS = new Set([
|
||||
'id', 'type', 'title', 'description', 'placement', 'position', 'data', 'source', 'periods',
|
||||
@@ -223,12 +268,68 @@ export function normalizeSkillUi(rawUi, { declaredPages = [], skillId = '' } = {
|
||||
|
||||
if (rawUi == null) return { ui, errors };
|
||||
|
||||
if (typeof rawUi !== 'object' || Array.isArray(rawUi)) {
|
||||
return { ui, errors: ['`ui` must be a section, or a mapping of page names to sections.'] };
|
||||
if (typeof rawUi !== 'object') {
|
||||
return {
|
||||
ui,
|
||||
errors: ['`ui` must be a section, a list of sections, or a mapping of page names to sections.'],
|
||||
};
|
||||
}
|
||||
|
||||
const pages = declaredPages.map(canonicalPage).filter(Boolean);
|
||||
|
||||
/**
|
||||
* A list, where every entry names the page it belongs to.
|
||||
*
|
||||
* Grouped rather than keyed, so two entries naming one page are two sections
|
||||
* on it rather than the second quietly replacing the first — which is what a
|
||||
* page map would have done. `seen` is shared across the whole list because
|
||||
* section ids are unique per definition, not per page.
|
||||
*/
|
||||
if (Array.isArray(rawUi)) {
|
||||
const seen = new Set();
|
||||
|
||||
rawUi.forEach((entry, index) => {
|
||||
const where = `ui[${index}]`;
|
||||
|
||||
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
||||
errors.push(`${where}: each entry must be a mapping of options.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rawPage = entry.page ?? entry.pages ?? entry.surface;
|
||||
if (rawPage == null || Array.isArray(rawPage)) {
|
||||
errors.push(`${where}: an entry needs a \`page\`.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const page = canonicalPage(rawPage);
|
||||
if (!page) {
|
||||
errors.push(
|
||||
`Unsupported page: ${rawPage}. Supported pages: ${SUPPORTED_SKILL_PAGES.join(', ')}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Declared `pages:` still governs reach when it is written: a definition
|
||||
may not render onto a surface it did not say it applied to. When it is
|
||||
not written, the list is the declaration — see `parseSkill`. */
|
||||
if (pages.length && !pages.includes(page)) {
|
||||
errors.push(`\`${where}\` names \`${rawPage}\`, which is not listed under \`pages\`.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const section = normalizeSection(entry, {
|
||||
page, errors, seen, fallbackId: `${skillId}-${index + 1}`, where,
|
||||
});
|
||||
if (!section) return;
|
||||
|
||||
if (!ui[page]) ui[page] = { sections: [] };
|
||||
ui[page].sections.push(section);
|
||||
});
|
||||
|
||||
return { ui, errors };
|
||||
}
|
||||
|
||||
/* Shorthand: one section, applied to every page the skill declares. The keys
|
||||
inside it are section options and are never read as page names — which is
|
||||
exactly what this branch exists to prevent. */
|
||||
|
||||
Reference in New Issue
Block a user