Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 537x 69x 468x 55x 413x 70x 343x 1x 342x 1x 341x 341x 341x 338x 3x 381x 312x 311x 310x 255x 185x 285x 82x 82x 381x 381x 381x 82x | // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* @module Aggregator/Html/Toc
* @description Article-level Table-of-Contents builder. Renders a
* labelled `<nav class="article-toc">` sidebar with one entry per
* emitted H2 section; entries are prefixed with a contextual emoji icon
* that mirrors the Reader Intelligence Guide so the two navigation
* surfaces share a single visual vocabulary.
*/
import {
TOC_ARIA_LABELS,
TRADECRAFT_HEADING_LABELS,
ANALYSIS_INDEX_HEADING_LABELS,
KEY_TAKEAWAYS_HEADING_LABELS,
SUPPLEMENTARY_HEADING_LABELS,
SECTION_TITLE_LABELS,
getLocalizedString,
} from '../../constants/languages.js';
import { escapeHTML } from '../../utils/file-utils.js';
import type { LanguageCode } from '../../types/index.js';
import { READER_GUIDE_SECTION_ID } from '../reader-guide-constants.js';
import {
READER_GUIDE_TITLE_LABELS,
getReaderGuideSectionIcon,
} from '../reader-intelligence-guide.js';
import {
TRADECRAFT_SECTION_ID,
MANIFEST_SECTION_ID,
SUPPLEMENTARY_SECTION_ID,
} from '../artifact-order.js';
import { KEY_TAKEAWAYS_SECTION_ID } from '../key-takeaways.js';
/** One entry in the article-level TOC sidebar (mirrors `TocSection`). */
export interface ArticleTocEntry {
/** Fragment identifier — must match the `id="…"` on the rendered H2. */
readonly id: string;
/** Display title shown in the sidebar nav. */
readonly title: string;
}
/**
* Resolve a localized title for a TOC entry based on its section ID.
* Falls back to the original English title if no translation is available.
*
* @param sectionId - The fragment identifier of the section
* @param fallbackTitle - The English title to fall back to
* @param lang - Target language code
* @returns Localized title string
*/
export function getLocalizedTocTitle(
sectionId: string,
fallbackTitle: string,
lang: LanguageCode
): string {
if (sectionId === READER_GUIDE_SECTION_ID) {
return getLocalizedString(READER_GUIDE_TITLE_LABELS, lang);
}
if (sectionId === TRADECRAFT_SECTION_ID) {
return getLocalizedString(TRADECRAFT_HEADING_LABELS, lang);
}
if (sectionId === MANIFEST_SECTION_ID) {
return getLocalizedString(ANALYSIS_INDEX_HEADING_LABELS, lang);
}
if (sectionId === KEY_TAKEAWAYS_SECTION_ID) {
return getLocalizedString(KEY_TAKEAWAYS_HEADING_LABELS, lang);
}
if (sectionId === SUPPLEMENTARY_SECTION_ID) {
return getLocalizedString(SUPPLEMENTARY_HEADING_LABELS, lang);
}
const sectionKey = sectionId.replace(/^section-/, '');
const sectionLabels = SECTION_TITLE_LABELS[sectionKey];
if (sectionLabels) {
return getLocalizedString(sectionLabels, lang);
}
return fallbackTitle;
}
/**
* Resolve the visual icon glyph used as the Table-of-Contents bullet for
* a given section. Reuses {@link getReaderGuideSectionIcon} for the
* canonical artifact sections (so the TOC and the Reader Intelligence
* Guide share the same visual vocabulary), and adds dedicated icons for
* the aggregator-owned appendix anchors that the guide does not list.
*
* @param sectionId - Anchor id of the section (e.g. `section-risk`,
* `aggregator-tradecraft-references`)
* @returns Single emoji glyph used as the `guide-icon` for that entry
*/
export function getTocSectionIcon(sectionId: string): string {
if (sectionId === READER_GUIDE_SECTION_ID) return '🧭';
if (sectionId === KEY_TAKEAWAYS_SECTION_ID) return '🔑';
if (sectionId === SUPPLEMENTARY_SECTION_ID) return '🗂️';
if (sectionId === TRADECRAFT_SECTION_ID) return '🛠️';
if (sectionId === MANIFEST_SECTION_ID) return '📚';
return getReaderGuideSectionIcon(sectionId);
}
/**
* Build the article-level Table of Contents nav. Renders a labelled
* `<nav class="article-toc">` with one `<a>` per H2 section, keyed by the
* stable fragment ids produced by the aggregator. The containing `<aside>`
* is styled as a sticky, full-height sidebar on wide viewports and
* collapses into a `<details>` disclosure on narrow viewports via
* `styles.css`. Each entry is prefixed with a contextual emoji icon so
* readers can scan the navigation visually as well as textually.
*
* Returns an empty string when `entries` is empty so low-signal
* `ANALYSIS_ONLY` articles (few sections, no value in a TOC) stay compact.
*
* @param entries - Ordered list of emitted H2 sections
* @param lang - Language code used to localise the nav label
* @returns HTML fragment for the sidebar, or `""` when no TOC is needed
*/
export function buildArticleToc(entries: readonly ArticleTocEntry[], lang: LanguageCode): string {
if (entries.length === 0) return '';
const label = escapeHTML(getLocalizedString(TOC_ARIA_LABELS, lang));
const items = entries
.map((e) => {
const displayTitle = getLocalizedTocTitle(e.id, e.title, lang);
const icon = getTocSectionIcon(e.id);
return ` <li><a href="#${escapeHTML(e.id)}"><span class="article-toc-icon" aria-hidden="true">${icon}</span> <span class="article-toc-text">${escapeHTML(displayTitle)}</span></a></li>`;
})
.join('\n');
return [
` <aside class="article-toc-container" aria-labelledby="article-toc-heading">`,
` <details class="article-toc-details" open>`,
` <summary class="article-toc-summary" id="article-toc-heading"><span class="guide-icon" aria-hidden="true">📑</span> ${label}</summary>`,
` <nav class="article-toc" aria-labelledby="article-toc-heading">`,
` <ol class="article-toc-list">`,
items,
` </ol>`,
` </nav>`,
` </details>`,
` </aside>`,
'',
].join('\n');
}
|