mirror of
https://github.com/m3y54m/Embedded-Engineering-Roadmap.git
synced 2026-10-10 08:18:16 +03:00
151 lines
6.1 KiB
JavaScript
151 lines
6.1 KiB
JavaScript
// Links the topics drawn on the map (map.json) to README topics, so every topic knows
|
|
// which areas (Software, Hardware, Soft skills) it sits in and how important the map marks it.
|
|
|
|
export const AREA_ORDER = ['SOFTWARE', 'HARDWARE', 'SOFT SKILLS'];
|
|
export const IMPORTANCE_LEVELS = ['required', 'recommended', 'possible'];
|
|
const RANK = { required: 3, recommended: 2, possible: 1 };
|
|
|
|
const key = (text) => text.normalize('NFKC').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
|
|
function topicIndex(topics) {
|
|
const index = new Map();
|
|
const add = (k, topic) => {
|
|
if (k && !index.has(k)) index.set(k, topic);
|
|
};
|
|
const byDepth = [...topics].sort((a, b) => a.depth - b.depth);
|
|
for (const t of byDepth) add(key(t.title), t);
|
|
for (const t of byDepth) {
|
|
const base = t.title.replace(/\s*\([^)]*\)\s*/g, ' ').trim();
|
|
add(key(base), t);
|
|
const inner = t.title.match(/\(([^)]+)\)/);
|
|
if (inner && /^[A-Z0-9-]{2,6}$/.test(inner[1])) add(key(inner[1]), t);
|
|
add(key(base.split(' / ')[0]), t);
|
|
}
|
|
return index;
|
|
}
|
|
|
|
export function linkDiagram(map, data) {
|
|
const index = topicIndex(data.topics);
|
|
// map.json "readme": map labels whose README heading is worded differently (or has no section of its own).
|
|
const aliases = map.readme || {};
|
|
const resolve = (text) => {
|
|
if (Object.hasOwn(aliases, text)) return [aliases[text]].flat().map((title) => index.get(key(title))).filter(Boolean);
|
|
const topic = index.get(key(text));
|
|
return topic ? [topic] : [];
|
|
};
|
|
|
|
// A cluster's areas are those of the band it sits in; a group's are the areas all its clusters share.
|
|
const clusterAreas = new Map();
|
|
const clusters = {};
|
|
const boxes = [];
|
|
for (const band of map.bands) {
|
|
for (const [cluster, spec] of Object.entries(band.clusters)) {
|
|
clusterAreas.set(cluster, band.areas);
|
|
clusters[cluster] = { header: spec.header, caption: spec.caption, per: spec.per };
|
|
if (spec.header) boxes.push({ text: spec.header, importance: null, areas: band.areas, cluster, header: true });
|
|
for (const [text, importance] of spec.topics) boxes.push({ text, importance, areas: band.areas, cluster, header: false });
|
|
}
|
|
}
|
|
boxes.forEach((b, i) => Object.assign(b, { id: `box-${i}`, topics: resolve(b.text) }));
|
|
const regions = Object.entries(map.groups).map(([name, clusters]) => {
|
|
const areas = AREA_ORDER.filter((a) => clusters.every((c) => (clusterAreas.get(c) || []).includes(a)));
|
|
return { name, clusters, areas, topics: resolve(name) };
|
|
});
|
|
|
|
const boxOf = new Map();
|
|
for (const t of data.topics) {
|
|
t.areas = [];
|
|
t.importance = null;
|
|
t.boxes = [];
|
|
}
|
|
for (const box of boxes) {
|
|
for (const t of box.topics) {
|
|
t.boxes.push(box);
|
|
if (!boxOf.has(t)) boxOf.set(t, box);
|
|
for (const area of box.areas) if (!t.areas.includes(area)) t.areas.push(area);
|
|
if (box.importance && (!t.importance || RANK[box.importance] > RANK[t.importance])) t.importance = box.importance;
|
|
}
|
|
}
|
|
for (const region of regions) {
|
|
for (const t of region.topics) {
|
|
for (const area of region.areas) if (!t.areas.includes(area)) t.areas.push(area);
|
|
}
|
|
}
|
|
// Subtopics that are not drawn in the diagram inherit the areas of their closest drawn ancestor;
|
|
// a topic that is not drawn itself but has drawn subtopics (Soft Skills) takes theirs.
|
|
const fromChildren = new Map();
|
|
for (const t of data.topics) {
|
|
if (!t.areas.length) continue;
|
|
for (let p = t.parent; p; p = p.parent) {
|
|
if (!fromChildren.has(p)) fromChildren.set(p, new Set());
|
|
for (const area of t.areas) fromChildren.get(p).add(area);
|
|
}
|
|
}
|
|
for (const t of data.topics) {
|
|
if (!t.areas.length && fromChildren.has(t)) t.areas = [...fromChildren.get(t)];
|
|
if (!t.areas.length) for (let p = t.parent; p && !t.areas.length; p = p.parent) t.areas = [...(p.areas || [])];
|
|
t.areas.sort((a, b) => AREA_ORDER.indexOf(a) - AREA_ORDER.indexOf(b));
|
|
}
|
|
|
|
// The box that stands for a topic on the map: its own, or the closest ancestor's.
|
|
const boxFor = (topic) => {
|
|
for (let t = topic; t; t = t.parent) if (boxOf.has(t)) return boxOf.get(t);
|
|
return null;
|
|
};
|
|
|
|
// A connection joins two boxes. `reason` is set for the hand-picked ones in map.json; the rest
|
|
// are evidence only (a shared resource, or one topic's text naming another).
|
|
const pairs = new Map();
|
|
const pairFor = (a, b) => {
|
|
const [p, q] = a.id < b.id ? [a, b] : [b, a];
|
|
const id = `${p.id}|${q.id}`;
|
|
if (!pairs.has(id)) pairs.set(id, { a: p, b: q, reason: null, weight: 0, shared: 0, links: [] });
|
|
return pairs.get(id);
|
|
};
|
|
|
|
const topicBoxes = new Map(boxes.filter((b) => !b.header).map((b) => [b.text, b]));
|
|
const invalidLinks = [];
|
|
for (const box of boxes) box.related = [];
|
|
for (const [from, to, reason] of map.links || []) {
|
|
const a = topicBoxes.get(from);
|
|
const b = topicBoxes.get(to);
|
|
if (!a || !b || a === b || typeof reason !== 'string' || !reason.trim()) {
|
|
invalidLinks.push(`${from} - ${to}`);
|
|
continue;
|
|
}
|
|
const pair = pairFor(a, b);
|
|
if (pair.reason) {
|
|
invalidLinks.push(`${from} - ${to} (listed twice)`);
|
|
continue;
|
|
}
|
|
pair.reason = reason.trim();
|
|
a.related.push({ box: b, reason: pair.reason });
|
|
b.related.push({ box: a, reason: pair.reason });
|
|
}
|
|
|
|
for (const link of data.links) {
|
|
const a = boxFor(link.source);
|
|
const b = boxFor(link.target);
|
|
if (!a || !b || a === b) continue;
|
|
const pair = pairFor(a, b);
|
|
pair.weight += link.weight;
|
|
pair.shared += link.shared.length;
|
|
pair.links.push(link);
|
|
}
|
|
|
|
return {
|
|
regions,
|
|
clusters,
|
|
boxes,
|
|
boxFor,
|
|
connections: [...pairs.values()],
|
|
unmatched: boxes.filter((b) => !b.header && !b.topics.length).map((b) => b.text),
|
|
invalidLinks,
|
|
invalidReadme: Object.entries(aliases).flatMap(([label, titles]) => {
|
|
const onMap = boxes.some((b) => b.text === label) || label in map.groups;
|
|
const missing = [titles].flat().filter((title) => !index.has(key(title)));
|
|
return onMap && !missing.length ? [] : [`${label} -> ${[titles].flat().join(' + ')}`];
|
|
}), invalid: boxes.filter((b) => !b.header && !IMPORTANCE_LEVELS.includes(b.importance)).map((b) => `${b.text}: ${b.importance}`),
|
|
};
|
|
}
|