|
54 | 54 | CATALOGUE = (data.processors || []).slice().sort((a, b) => (a.title || a.type).localeCompare(b.title || b.type)); |
55 | 55 | CATALOGUE.forEach(p => { TITLE[p.type] = p.title || p.type; }); |
56 | 56 |
|
57 | | - populateCategories(); |
| 57 | + populateTags(); |
58 | 58 | document.getElementById("pc-search").addEventListener("input", renderCatalogue); |
59 | | - document.getElementById("pc-category").addEventListener("change", renderCatalogue); |
| 59 | + document.getElementById("pc-tag").addEventListener("change", renderCatalogue); |
60 | 60 |
|
61 | 61 | // Remember the empty-state markup (before anything overwrites it) so back/forward can |
62 | 62 | // restore it when the visitor navigates back past every processor they opened. |
|
81 | 81 | renderCatalogue(); |
82 | 82 | } |
83 | 83 |
|
84 | | - // Fill the category dropdown with the categories actually present in the catalogue. |
85 | | - function populateCategories() { |
86 | | - const categories = [...new Set(CATALOGUE.map(p => p.category || "(uncategorised)"))].sort(); |
87 | | - const select = document.getElementById("pc-category"); |
88 | | - categories.forEach(category => { |
| 84 | + // Fill the tag dropdown with every tag present in the catalogue. Tags are the new way |
| 85 | + // to filter (a superset of categories -- a processor's first tag is its category), so |
| 86 | + // this lists them all, alphabetically. |
| 87 | + function populateTags() { |
| 88 | + const tags = [...new Set(CATALOGUE.flatMap(p => p.tags || []))].sort(); |
| 89 | + const select = document.getElementById("pc-tag"); |
| 90 | + tags.forEach(tag => { |
89 | 91 | const option = document.createElement("option"); |
90 | | - option.value = category; |
91 | | - option.textContent = category; |
| 92 | + option.value = tag; |
| 93 | + option.textContent = tag; |
92 | 94 | select.appendChild(option); |
93 | 95 | }); |
94 | 96 | } |
|
100 | 102 | // "N of M" count and re-attaches the click handler that opens a card's detail. |
101 | 103 | function renderCatalogue() { |
102 | 104 | const query = document.getElementById("pc-search").value.trim().toLowerCase(); |
103 | | - const category = document.getElementById("pc-category").value; |
| 105 | + const tag = document.getElementById("pc-tag").value; |
104 | 106 |
|
105 | 107 | let items = CATALOGUE; |
106 | | - if (category) items = items.filter(p => (p.category || "(uncategorised)") === category); |
| 108 | + if (tag) items = items.filter(p => (p.tags || []).includes(tag)); |
107 | 109 | if (query) items = items.filter(p => |
108 | | - `${p.type} ${p.title || ""} ${p.category || ""} ${p.description || ""}`.toLowerCase().includes(query)); |
| 110 | + `${p.type} ${p.title || ""} ${(p.tags || []).join(" ")} ${p.description || ""}`.toLowerCase().includes(query)); |
109 | 111 |
|
110 | 112 | const groups = {}; |
111 | 113 | items.forEach(p => { const c = p.category || "(uncategorised)"; (groups[c] = groups[c] || []).push(p); }); |
|
128 | 130 | p.is_filter ? '<span class="inline-label property-badge pc-filter">filter</span>' : "", |
129 | 131 | p.has_override ? '<span class="inline-label property-badge pc-approx">override</span>' : "" |
130 | 132 | ].join(""); |
| 133 | + // the first tag is the category (already the group heading), so show only the rest |
| 134 | + const tags = (p.tags || []).slice(1).map(t => `<span class="pc-tag">${esc(t)}</span>`).join(""); |
131 | 135 | return `<button class="pc-card" data-type="${esc(p.type)}"> |
132 | 136 | <div class="pc-card-head"><h4>${esc(p.title || p.type)}</h4><span class="pc-badges">${badges}</span></div> |
133 | 137 | <p class="pc-desc">${esc(p.description || "")}</p> |
| 138 | + ${tags ? `<div class="pc-tags">${tags}</div>` : ""} |
134 | 139 | </button>`; |
135 | 140 | } |
136 | 141 |
|
|
296 | 301 | return next || '<p class="pc-muted">Nothing runs on this output.</p>'; |
297 | 302 | } |
298 | 303 |
|
299 | | - // Assemble the whole detail pane for one processor: heading and badges, a short |
300 | | - // metadata list (type / category / what it produces / description), then the three |
301 | | - // blocks. "Produces" is left out when the output shape didn't pin down a format. |
| 304 | + // Render a references-style string: turn any [text](url) markdown links into anchors, |
| 305 | + // escaping everything else. Plain citations (no link) pass through unchanged. |
| 306 | + function mdLinks(s) { |
| 307 | + let out = "", last = 0, re = /\[([^\]]+)\]\(([^)]+)\)/g, m; |
| 308 | + while ((m = re.exec(s))) { |
| 309 | + out += esc(s.slice(last, m.index)); |
| 310 | + out += `<a href="${esc(m[2])}" target="_blank" rel="noopener">${esc(m[1])}</a>`; |
| 311 | + last = m.index + m[0].length; |
| 312 | + } |
| 313 | + return out + esc(s.slice(last)); |
| 314 | + } |
| 315 | + |
| 316 | + // Info / warning notice boxes, styled like the run-processor card: warnings (pitfalls) |
| 317 | + // in yellow, info (helpful extras) in blue. One box per item, matching the design mockup. |
| 318 | + function noticeBoxes(items, kind) { |
| 319 | + if (!items || !items.length) return ""; |
| 320 | + const icon = kind === "warning" ? "triangle-exclamation" : "circle-info"; |
| 321 | + return items.map(t => |
| 322 | + `<p class="pc-notice pc-notice-${kind}"><i class="fa fa-${icon}" aria-hidden="true"></i><span>${esc(t)}</span></p>` |
| 323 | + ).join(""); |
| 324 | + } |
| 325 | + |
| 326 | + // Assemble the whole detail pane for one processor: a card (icon + title + badges/tags, |
| 327 | + // description, warning then info boxes, and a footer of what it produces + references), |
| 328 | + // then a compact type/category line and the three collapsible blocks. |
302 | 329 | function detailHtml(info) { |
303 | 330 | const shape = info.output_shape || {}; |
304 | 331 |
|
|
314 | 341 | : '<p class="pc-muted">No declared compatibility (defaults to top-level datasets).</p>'; |
315 | 342 | const produces = [shape.extension, shape.media_type].filter(v => v && v !== "unknown").join(" · "); |
316 | 343 |
|
| 344 | + // the first tag is the category (shown below), so chip only the rest next to the title |
| 345 | + const tags = (info.tags || []).slice(1).map(t => `<span class="pc-tag">${esc(t)}</span>`).join(""); |
| 346 | + const icon = info.icon ? `<i class="fa fa-fw fa-${esc(info.icon)}" aria-hidden="true"></i> ` : ""; |
| 347 | + const references = (info.references || []).length |
| 348 | + ? `<div class="pc-refs">${info.references.map(r => `<span>${mdLinks(r)}</span>`).join("")}</div>` : ""; |
| 349 | + const foot = (produces || references) |
| 350 | + ? `<div class="pc-detail-foot">${produces ? `<span class="pc-produces"><i class="fa fa-table" aria-hidden="true"></i> ${esc(produces)}</span>` : "<span></span>"}${references}</div>` |
| 351 | + : ""; |
| 352 | + |
317 | 353 | return ` |
318 | | - <h2><span>${esc(info.title || info.type)}</span></h2> |
319 | | - <p class="pc-badges">${badges}</p> |
320 | | - <dl class="metadata-wrapper"> |
| 354 | + <div class="pc-detail-card"> |
| 355 | + <header class="pc-detail-head"> |
| 356 | + <h2>${icon}<span>${esc(info.title || info.type)}</span></h2> |
| 357 | + <span class="pc-badges">${badges}${tags}</span> |
| 358 | + </header> |
| 359 | + ${info.description ? `<p class="pc-detail-desc">${esc(info.description)}</p>` : ""} |
| 360 | + ${noticeBoxes(info.warnings, "warning")} |
| 361 | + ${noticeBoxes(info.info, "info")} |
| 362 | + ${foot} |
| 363 | + </div> |
| 364 | + <dl class="metadata-wrapper pc-meta"> |
321 | 365 | <div class="fullwidth"><dt>Type</dt><dd><code>${esc(info.type)}</code></dd></div> |
322 | 366 | ${info.category ? `<div class="fullwidth"><dt>Category</dt><dd>${esc(info.category)}</dd></div>` : ""} |
323 | | - ${produces ? `<div class="fullwidth"><dt>Produces</dt><dd>${esc(produces)}</dd></div>` : ""} |
324 | | - ${info.description ? `<div class="fullwidth"><dt>About</dt><dd>${esc(info.description)}</dd></div>` : ""} |
325 | 367 | </dl> |
326 | 368 | ${section("How to run", true, howToRunHtml(info))} |
327 | 369 | ${section("What can run on this", false, followupsHtml(info))} |
|
0 commit comments