/** * EntityAttributes — renders an entity's descriptive content and structured * attributes straight from its wiki frontmatter. The generated entity files * carry their real content in YAML fields (narrative_summary_*, maneuver_notes, * shape, color, roles, countries, …) while the markdown body holds only empty * "## Description" headings — so the page must surface the frontmatter. */ type FM = Record; const ATTR_LABELS: Record = { event_class: "Tipo de evento", date_start: "Início", date_end: "Fim", date_confidence: "Confiança da data", primary_location_names: "Locais", primary_location_geo_classes: "Classe do local", geo_class: "Classe geográfica", countries: "Países", regions_or_states: "Regiões / estados", org_class: "Tipo de organização", person_class: "Tipo de pessoa", affiliations: "Afiliações", roles: "Funções / papéis", shape: "Forma", color: "Cor", medium: "Meio", size_estimate_m: "Tamanho estimado (m)", altitude_ft: "Altitude (ft)", speed_kts: "Velocidade (kt)", }; // Order in which attributes are shown (only those present render). const ATTR_ORDER = [ "event_class", "person_class", "org_class", "shape", "color", "medium", "size_estimate_m", "altitude_ft", "speed_kts", "date_start", "date_end", "date_confidence", "geo_class", "countries", "regions_or_states", "primary_location_names", "primary_location_geo_classes", "affiliations", "roles", ]; function clean(v: unknown): string | null { const s = typeof v === "string" ? v.trim() : ""; return s && s.toLowerCase() !== "null" ? s : null; } // Placeholder values that carry no real attribute information — hidden from the // ATRIBUTOS grid (but never from the free-text description). const EMPTY_TOKENS = new Set([ "null", "none", "n/a", "na", "unknown", "unidentified", "undetermined", "unspecified", "not specified", "not stated", "not applicable", ]); function isEmptyToken(s: string): boolean { return EMPTY_TOKENS.has(s.trim().toLowerCase()); } function fmtValue(v: unknown): string | null { if (v == null) return null; if (Array.isArray(v)) { const items = v .map((x) => (typeof x === "string" ? x.trim() : String(x))) .filter((x) => x && !x.startsWith("[[") && !isEmptyToken(x)); return items.length ? items.join(", ") : null; } if (typeof v === "number") return String(v); const s = clean(v); return s && !isEmptyToken(s) ? s : null; } export function EntityAttributes({ fm }: { fm: FM }) { const ptText = clean(fm.narrative_summary_pt_br) ?? clean(fm.description_pt_br); const enText = clean(fm.narrative_summary_en) ?? clean(fm.description_en); const notes = clean(fm.maneuver_notes); // source-language only (uap_object) const attrs = ATTR_ORDER.map((k) => [k, fmtValue(fm[k])] as const).filter( ([, v]) => v !== null, ); const hasDescription = Boolean(ptText || enText || notes); if (!hasDescription && attrs.length === 0) return null; return (
{hasDescription && ( <> {ptText && (

Descrição (PT-BR)

{ptText}

)} {enText && (

Description (EN)

{enText}

)} {notes && !ptText && !enText && (

Descrição · Description

{notes}

)} {notes && (ptText || enText) && (

Notas de manobra / aparência

{notes}

)} )} {attrs.length > 0 && (

Atributos

{attrs.map(([k, v]) => (
{ATTR_LABELS[k] ?? k}
{v}
))}
)}
); }