mirror of
https://github.com/m3y54m/Embedded-Engineering-Roadmap.git
synced 2026-10-12 17:28:30 +03:00
feat(explorer): show embedded system definitions on the landing panel and as a topic
This commit is contained in:
+28
-4
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user