(cherry picked from commit367fd0d240) (cherry picked from commit068e1e0f1c)
62 lines
3.7 KiB
JavaScript
62 lines
3.7 KiB
JavaScript
// The scope rule of design 1.2. Scope comes from the path alone (D2): no file content can change it.
|
|
// Paths are repo-relative and compared exactly, case included (R7 handles case-only twins).
|
|
const inside = (folder, path) => path.startsWith(folder.endsWith('/') ? folder : `${folder}/`);
|
|
|
|
// { scope: 'supporting' | 'playground' | 'canon', universe, book, type, folder } or { scope: 'unmapped' }.
|
|
// Prefixes come first (ruling after r11, C2-01c), supporting before playground (C2-01d, design 1.2):
|
|
// a `.md` file under any entry's supporting prefix is supporting, and otherwise one under a playground
|
|
// prefix is playground. Either way `universe` is that entry and `book` is the book folder that holds
|
|
// the prefix, or null at universe level and in a draft. Otherwise a `.md` file in a book folder is
|
|
// canon. `type` is the mapped type of the first folder under the prefix or under the book (canon),
|
|
// 'manuscript' for a manuscript folder, else null. `folder` is that folder name. checkMapping keeps
|
|
// prefixes of one list from overlapping, and keeps a supporting prefix from holding a playground
|
|
// prefix, so the first match is the only one that counts.
|
|
export function classify(mapping, path) {
|
|
if (typeof path !== 'string' || !path.endsWith('.md')) return { scope: 'unmapped' };
|
|
const entries = Object.entries(mapping.universes);
|
|
for (const [universe, entry] of entries) {
|
|
const prefix = entry.supporting.find(p => inside(p, path));
|
|
if (prefix) return place(mapping, path, prefix, { scope: 'supporting', universe, book: entry.books.find(b => inside(b, prefix)) ?? null });
|
|
}
|
|
for (const [universe, entry] of entries) {
|
|
const prefix = entry.playground.find(p => inside(p, path));
|
|
if (prefix) return place(mapping, path, prefix, { scope: 'playground', universe, book: entry.books.find(b => inside(b, prefix)) ?? null });
|
|
}
|
|
for (const [universe, entry] of entries) {
|
|
const book = entry.books.find(b => inside(b, path));
|
|
if (book) return place(mapping, path, `${book}/`, { scope: 'canon', universe, book });
|
|
}
|
|
return { scope: 'unmapped' };
|
|
}
|
|
|
|
function place(mapping, path, under, where) {
|
|
const rest = path.slice(under.length).split('/');
|
|
const folder = rest.length > 1 ? rest[0] : null;
|
|
const type = folder === null ? null : Object.hasOwn(mapping.types, folder) ? mapping.types[folder]
|
|
: mapping.manuscript.includes(folder) ? 'manuscript' : null;
|
|
return { ...where, type, folder };
|
|
}
|
|
|
|
// C2-15 (design 1.4): whether an `exclude` constraint of universe `hybrid` covers `path`. The type form
|
|
// `source/Type` (first segment one of the hybrid's sources, the rest one key of `types`) covers that type
|
|
// folder in each of the source's books. Otherwise the exclude is a path and covers itself and what lies
|
|
// inside it. checkMapping keeps the two readings from both fitting. False for a universe that isn't a
|
|
// hybrid. Reads only the path (D2).
|
|
export function excluded(mapping, hybrid, path) {
|
|
const entry = mapping.universes[hybrid];
|
|
if (entry?.kind !== 'hybrid' || typeof path !== 'string') return false;
|
|
return entry.constraints.some(c => {
|
|
if (c.exclude === undefined) return false;
|
|
const type = typeForm(mapping, entry, c.exclude);
|
|
if (type) return mapping.universes[type.source].books.some(book => path.startsWith(`${book}/${type.folder}/`));
|
|
return path === c.exclude || path.startsWith(`${c.exclude}/`);
|
|
});
|
|
}
|
|
|
|
// { source, folder } when `exclude` reads as the type form for this hybrid entry, else null.
|
|
export function typeForm(mapping, entry, exclude) {
|
|
const [source, ...rest] = exclude.split('/');
|
|
const folder = rest.join('/');
|
|
return entry.sources.includes(source) && rest.length === 1 && Object.hasOwn(mapping.types, folder) ? { source, folder } : null;
|
|
}
|