feat(explorer): show embedded system definitions on the landing panel and as a topic

This commit is contained in:
Meysam Parvizi
2026-10-11 20:41:33 +02:00
parent 5b281a91e6
commit 2de24aea71
6 changed files with 108 additions and 7 deletions
+28 -4
View File
@@ -54,9 +54,10 @@ function store(key, value) {
// ---------- colors ----------
function assignHues(data) {
const [first, ...rest] = data.groups;
const start = rest.length ? first.topics : [];
const learning = rest.length ? rest.flatMap((g) => g.topics) : data.root.children;
// Every section before the last one holds getting-started topics; the last holds the learning resources.
const learningGroup = data.groups.length > 1 ? data.groups[data.groups.length - 1] : null;
const start = learningGroup ? data.groups.slice(0, -1).flatMap((g) => g.topics) : [];
const learning = learningGroup ? learningGroup.topics : data.root.children;
start.forEach((t, i) => { t.hue = 168 + (start.length > 1 ? (i * 40) / (start.length - 1) : 0); });
learning.forEach((t, i) => { t.hue = (214 + (i * 300) / Math.max(1, learning.length)) % 360; });
data.root.children.forEach((top, index) => {
@@ -134,6 +135,15 @@ function description(blocks) {
const p = h('p');
p.innerHTML = renderInline(block.text);
wrap.append(p);
} else if (block.type === 'quote') {
const quote = h('blockquote');
for (const text of block.paragraphs) {
const p = h('p');
p.innerHTML = renderInline(text);
quote.append(p);
}
wrap.append(h('figure', { class: 'quote' }, quote, h('figcaption', {},
h('a', { href: block.url, target: '_blank', rel: 'noopener noreferrer' }, block.source))));
} else {
const ul = h('ul');
for (const item of block.items) {
@@ -300,16 +310,30 @@ function areaSummary() {
tile(['HARDWARE'], count((b) => b.areas.length === 1 && b.areas[0] === 'HARDWARE'), 'Hardware only'));
}
// The summary and the first quotation stay visible; the other quotations fold away to keep the landing panel short.
function foldedDefinitions(blocks) {
const firstQuote = blocks.findIndex((b) => b.type === 'quote');
const rest = blocks.slice(firstQuote + 1);
return [
description(blocks.slice(0, firstQuote + 1)),
rest.length ? h('details', { class: 'more' }, h('summary', {}, `${rest.length} more definitions`), description(rest)) : null,
];
}
function overview(root) {
const { stats, groups } = state.data;
// A section whose topic quotes sources (README definitions) is shown in full instead of as a chip.
const quoted = (g) => g.topics.find((t) => t.description.some((b) => b.type === 'quote'));
const definitions = groups.find((g) => quoted(g));
return [
section(null, description(root.description)),
definitions ? section(definitions.title, ...foldedDefinitions(quoted(definitions).description)) : null,
section(null, h('div', { class: 'stats' },
h('div', { class: 'stat' }, h('b', {}, stats.topics), h('span', {}, 'topics')),
h('div', { class: 'stat' }, h('b', {}, stats.resources), h('span', {}, 'resources')),
h('div', { class: 'stat' }, h('b', {}, state.plan ? state.plan.connections.filter((c) => c.reason).length : stats.links), h('span', {}, 'connections')))),
state.plan ? section('Diagram topics by area', areaSummary()) : null,
...groups.map((g) => section(g.title, topicChips(g.topics))),
...groups.filter((g) => !quoted(g)).map((g) => section(g.title, topicChips(g.topics))),
section('How to explore', h('ul', { class: 'howto' },
h('li', {}, 'Map follows the roadmap diagram: topics sit in Software, Hardware or both, and are colored by learning priority, a general guide that varies with your specialty and role.'),
h('li', {}, 'Click Required, Recommended or Optional in the legend above the map to highlight topics of that priority.'),
+1 -1
View File
@@ -488,7 +488,7 @@
"Microcontrollers": ["rtos", "mcu"],
"Interfaces & Protocols": ["basic", "wireless", "highSpeed", "industrial", "cellular", "network", "automotive", "display"]
},
"offMap": ["Don't Know Where to Start!"],
"offMap": ["What is an Embedded System?", "Don't Know Where to Start!"],
"readme": {
"ADC / DAC": ["ADC", "DAC"],
"Buildroot / Yocto": ["Buildroot", "Yocto"],
+36 -1
View File
@@ -19,6 +19,8 @@ const URL_PART = String.raw`https?:\/\/[^\s()]+(?:\([^\s()]*\)[^\s()]*)*`;
const HEADING = /^(#{1,6})\s+(.+)$/;
const RESOURCE = new RegExp(String.raw`^\s*[-*]\s+\[(.+)\]\((${URL_PART})\)(.*)$`, 'u');
const HEADING_LINK = new RegExp(String.raw`^\[(.+)\]\((${URL_PART})\)$`, 'u');
// A "▶️ [Source](url)" line introduces the blockquote that follows it, a sourced quotation such as a definition.
const QUOTE_SOURCE = new RegExp(String.raw`^\s*\u25B6\uFE0F?\s*\[(.+)\]\((${URL_PART})\)\s*$`, 'u');
const BULLET = /^\s*[-*]\s+(.+)$/;
const SKIP = /^(\s*>|<!--|!\[|\[!\[|_{3,}\s*$|\|)/;
@@ -93,6 +95,7 @@ export function parseRoadmap(markdown) {
const stack = [];
let target = root;
let topic = null;
let quote = null;
let paragraph = [];
const flush = () => {
@@ -104,6 +107,7 @@ export function parseRoadmap(markdown) {
const heading = line.match(HEADING);
if (heading) {
flush();
quote = null;
const level = heading[1].length;
if (level === 1) continue;
const info = headingInfo(heading[2].trim());
@@ -128,6 +132,37 @@ export function parseRoadmap(markdown) {
continue;
}
const source = line.match(QUOTE_SOURCE);
if (source) {
flush();
quote = null;
if (topic) {
const label = source[1].replace(/:\s*$/, '');
quote = { type: 'quote', source: label, url: source[2], paragraphs: [], gap: false };
topic.description.push(quote);
topic.resources.push(makeResource(label, source[2]));
}
continue;
}
if (quote) {
if (!line.trim()) {
// Blank lines may separate the source from its quote; after the quote has started they end it.
if (quote.paragraphs.length) quote = null;
continue;
}
if (/^\s*>/.test(line) || quote.paragraphs.length) {
const text = line.replace(/^\s*>\s?/, '').trim();
if (!text) quote.gap = true;
else if (!quote.paragraphs.length || quote.gap) {
quote.paragraphs.push(text);
quote.gap = false;
} else quote.paragraphs[quote.paragraphs.length - 1] += ` ${text}`;
continue;
}
quote = null;
}
const resource = line.match(RESOURCE);
if (resource) {
flush();
@@ -214,7 +249,7 @@ export function parseRoadmap(markdown) {
for (const from of topics) {
const texts = [
...from.resources.map((r) => `${r.title} ${r.note}`),
...from.description.flatMap((b) => (b.type === 'p' ? [b.text] : b.items)),
...from.description.flatMap((b) => (b.type === 'p' ? [b.text] : b.type === 'quote' ? b.paragraphs : b.items)),
];
for (const { topic: to, patterns } of matchers) {
if (to === from || isLineage(from, to)) continue;
+12 -1
View File
@@ -16,7 +16,7 @@ test('builds the topic tree from the README headings', () => {
assert.equal(find('1-Wire').parent.title, 'Basic Protocols');
assert.equal(find('High-Performance Computing (HPC)').parent.title, 'Multithreading & Parallel Processing');
assert.equal(find('History'), undefined);
assert.deepEqual(data.groups.map((g) => g.title), ["Don't Know Where to Start!", 'Learning Resources']);
assert.deepEqual(data.groups.map((g) => g.title), ['What is an Embedded System?', "Don't Know Where to Start!", 'Learning Resources']);
});
test('gives every topic a unique slug', () => {
@@ -38,6 +38,17 @@ test('keeps descriptions and the README introduction', () => {
assert.match(find('Microcontrollers').description[0].text, /^Microcontrollers are integrated circuits/);
});
test('parses sourced quotations as quote blocks and resources', () => {
const defs = find('Embedded System Definitions');
const quotes = defs.description.filter((b) => b.type === 'quote');
assert.equal(quotes.length, 5);
assert.equal(quotes[0].source, 'ISO/IEC/IEEE 24765 Standard');
assert.equal(quotes[0].paragraphs.length, 2);
assert.match(quotes[0].paragraphs[0], /rapid transit system\.$/, 'a lazy continuation line joins its paragraph');
assert.deepEqual(defs.resources.map((r) => r.title).slice(0, 2), ['ISO/IEC/IEEE 24765 Standard', '"Making Embedded Systems" Book']);
assert.equal(data.root.description.some((b) => b.type === 'quote'), false, 'README callouts are not quotations');
});
test('links topics that share a resource or mention each other', () => {
assert.ok(linked(find('Docker'), find('CI/CD Pipelines')).shared.length > 0);
assert.ok(linked(find('I2C'), find('Sensors')).mentions.length > 0);
+27
View File
@@ -702,6 +702,33 @@ button {
padding-left: 20px;
}
.desc .quote {
margin: 0 0 12px;
padding-left: 12px;
border-left: 3px solid color-mix(in srgb, var(--accent) 55%, transparent);
}
.desc .quote blockquote {
margin: 0;
}
.desc .quote figcaption {
margin-top: 2px;
font-size: 12px;
}
.desc .quote figcaption a {
color: var(--muted);
}
.panel-section .more summary {
margin-bottom: 10px;
color: var(--accent);
font-family: var(--font);
font-size: 13px;
cursor: pointer;
}
.desc code {
padding: 1px 5px;
border-radius: 5px;