#!/usr/bin/env nu # Project the typed expediente CASEs (.ncl) → the standalone informe, one per locale: # site/public/images/expedientes//expedientes.html # # The informe was hand-kept HTML with its data inlined as a JS array, while the ARTICLES # were already projected from the .ncl. Same cases, two hands — so they drifted: deriva's # ledger carried 6 rows in the article and 8 here, and `espejo` never made it in at all, # which left its own article linking to an anchor that did not exist. One source now; the # viewer is a projection like everything else, and `just informe-check` says so. # # The template (scripts/build/informe-template.html) owns the CSS, the chrome and the # render() function. This script owns only the data: it composes each CASE with the # locale × skin vocabulary into the shape render() already reads. # # Usage: # nu scripts/build/gen-informe.nu # every locale # nu scripts/build/gen-informe.nu --check # assert reproducible, write nothing use ./expediente-vocab.nu [VOCAB] def esc []: string -> string { $in | str replace --all '&' '&' | str replace --all '<' '<' | str replace --all '>' '>' } def ledger [rows: list]: nothing -> list { $rows | each {|r| [$r.label, $r.value, (if $r.emphasis { 1 } else { 0 })] } } # The renderer wraps nothing in quotes; the old hand-kept data carried the curly quotes # inside each alibi. Add them here so the .ncl stores the words, not the typography. def quoted [xs: list]: nothing -> list { $xs | each {|a| $"“($a)”" } } def skin-data [c: record, sk: record, lang: string, skin: string]: nothing -> record { let v = ($VOCAB | get $lang) let iv = ($v.skins | get $skin | get informe) { kicker: ($sk.kicker? | default $iv.kicker) h1: [$sk.h1_lead, $sk.h1_red] logline: $sk.logline meta: [$iv.meta_case, $iv.meta_class, $sk.victim, $"($v.md.status_label): ($iv.status_word)"] # The exhibit label is half vocabulary ("Exhibit A — ") and half case ("El cuerpo"). ex: ($sk.exhibits | enumerate | each {|e| $"($iv.exhibit_prefix | get $e.index)($e.item)" }) h2: $sk.headings lead2: $sk.lead_cost lead3: $sk.lead_suspects weaponCap: $sk.weapon_caption weaponAfter: $sk.weapon_after fixCap: $sk.fix_caption fixAfter: $sk.fix_after verdictLead: $sk.verdict_lead no: $iv.suspect_noun stamp: [$iv.stamp_lead, $sk.stamp] alibis: (quoted $sk.alibis) tags: $sk.suspect_tags footL: $"($iv.foot_left) ($c.case_no)" footR: $sk.foot_right } } def case-data [c: record, lang: string]: nothing -> record { let v = ($VOCAB | get $lang) let n = ($c.suspects | length) for skin in ($c.skins | columns) { let sk = ($c.skins | get $skin) if ($sk.alibis | length) != $n or ($sk.suspect_tags | length) != $n { error make { msg: $"($c.id)/($skin): ($n) suspects but ($sk.alibis | length) alibis and ($sk.suspect_tags | length) tags — they are index-parallel" } } } { id: $c.id caseNo: $c.case_no classification: $c.classification tab: $c.tab printHref: $c.print_href costCaption: $c.cost_caption # The glossary is set in richer type here than in the article; same definition. glossary: ($c.glossary | each {|t| { t: $t.term, d: (if ($t.def_html | is-empty) { $t.def } else { $t.def_html }) } }) glossaryMore: $"($v.md.protocol)ontoref.dev" cost: (ledger $c.cost) yielded: (ledger $c.yielded) yieldedTitle: (if ($c.yielded_title | is-empty) { $v.md.yielded_title } else { $c.yielded_title }) suspectsTech: ($c.suspects | each {|s| $s.name }) # A snippet with no highlighted variant is shown escaped rather than not shown. weaponCode: (if ($c.weapon.code_html | is-empty) { $c.weapon.code | str trim | esc } else { $c.weapon.code_html | str trim }) fixCode: (if ($c.fix.code_html | is-empty) { $c.fix.code | str trim | esc } else { $c.fix.code_html | str trim }) contain: ($c.containment | each {|r| [$r.facet, (if ($r.mechanism_html | is-empty) { $r.mechanism } else { $r.mechanism_html })] }) verdictClose: $c.verdict_close skin: ($c.skin | into string | str replace "'" "") skins: ($c.skins | columns | reduce --fold {} {|skin, acc| $acc | insert $skin (skin-data $c ($c.skins | get $skin) $lang $skin) }) } } # The reading order is the one the articles already declare in their frontmatter. A second # ordering rule would be a second source of truth. def sort-order [ncl: path]: nothing -> int { let md = ($ncl | str replace ".ncl" ".md") if not ($md | path exists) { return 999 } open --raw $md | lines | where {|l| $l | str starts-with "sort_order:" } | get -o 0 | default "sort_order: 999" | str replace --regex '^sort_order:\s*' '' | str trim | into int } def main [ --root: string = ".", --check, ] { let base = $"($root)/site/content/expedientes" let tpl = (open --raw $"($root)/scripts/build/informe-template.html") let langs = (ls $base | where type == dir | get name | each {|d| $d | path basename } | where {|d| not ($d | str starts-with "_") } | sort) if ($langs | is-empty) { error make { msg: $"no locale dirs under ($base)" } } mut drift = [] for lang in $langs { let ncls = (glob $"($base)/($lang)/*/*.ncl") if ($ncls | is-empty) { continue } let cases = ( $ncls | each {|f| { order: (sort-order $f), c: (^nickel export $f | from json) } } | sort-by order | each {|r| case-data $r.c $lang } ) let v = ($VOCAB | get $lang) let data = $" var UI = ($v.ui | to json -r);\n\n var CASES = ($cases | to json -r);" let html = ( $tpl | str replace --all "{{LANG}}" $lang | str replace --all "{{TITLE}}" $v.ui.page_title | str replace "{{DATA}}" $data ) let out = $"($root)/site/public/images/expedientes/($lang)/expedientes.html" if $check { let cur = (if ($out | path exists) { open --raw $out } else { "" }) if $cur != $html { $drift = ($drift | append $"($lang)/expedientes.html") } } else { mkdir ($out | path dirname) $html | save --force $out print $"informe → ($lang)/expedientes.html [($cases | length) casos]" } } if $check { if ($drift | is-empty) { print $"informe-check: ($langs | length) locale\(s\) reproduce from their .ncl — no hand-edited viewer" } else { print $"informe-check: DRIFT — ($drift | str join ', ') differ from what the .ncl produce." print " the viewer is a projection; edit the .ncl, never the HTML." exit 1 } } }