Report#22
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a NAIF-branded Typst report template and exposes it as a Quarto extension so reports can be authored in Typst directly or rendered from Quarto to PDF.
Changes:
- Introduces a reusable Typst report template package (
documents/templates/typst) with an example document and compiled PDF. - Adds a Quarto extension (
_extensions/naif-report) intended to provide anaif-report-typstoutput format using Typst template partials. - Adds a Quarto example report (
documents/templates/quarto/naif-report-example.qmd) demonstrating the intended authoring workflow.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| documents/templates/typst/typst.toml | Defines Typst package metadata and the template entrypoint. |
| documents/templates/typst/lib.typ | Implements the NAIF report layout, cover/title blocks, and helper components. |
| documents/templates/typst/example.typ | Demonstrates how to use the Typst template directly. |
| documents/templates/typst/example.pdf | Rendered sample output for the Typst example. |
| documents/templates/typst/README.md | Documents usage and compilation instructions for the Typst template. |
| documents/templates/quarto/naif-report-example.qmd | Demonstrates rendering a Quarto report using the NAIF Typst format. |
| _extensions/naif-report/typst-template.typ | Typst implementation used by the Quarto extension (currently duplicates lib.typ). |
| _extensions/naif-report/typst-show.typ | Quarto template glue that calls naif-report(...) with metadata mapped from YAML. |
| _extensions/naif-report/_extension.yml | Registers the Quarto format contribution and template partials. |
| _extensions/naif-report/README.md | Documents the intended custom Quarto format and supported metadata fields. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ( | ||
| name: [$it.name.literal$], | ||
| affiliation: [$for(it.affiliations)$$it.name$$sep$, $endfor$], | ||
| email: [$it.email$], |
There was a problem hiding this comment.
email is passed as Typst content (email: [$it.email$]), but the template later uses it in string concatenation ("mailto:" + email). In Typst, content is not a string, so this will likely error at render time. Pass email as a string value instead (e.g. email: "$it.email$") or adjust the template to convert content to a string before concatenation.
| email: [$it.email$], | |
| email: "$it.email$", |
| contributes: | ||
| formats: | ||
| typst: | ||
| doc-category: "NAIF report" | ||
| website: "https://eth-library.github.io/naif/" | ||
| toc: true | ||
| number-sections: true | ||
| template-partials: | ||
| - typst-template.typ | ||
| - typst-show.typ |
There was a problem hiding this comment.
The extension config contributes settings under formats.typst, but the documentation and example report use the custom format name naif-report-typst. As written, format: naif-report-typst: will not be registered by this extension (unless Quarto implicitly aliases it), so the example render will fail. Align these by either defining the format under formats.naif-report-typst (and inheriting from Typst) or updating the docs/examples to use format: typst:.
| - metadata quality | ||
| - repositories | ||
| format: | ||
| naif-report-typst: |
There was a problem hiding this comment.
format is set to naif-report-typst, but the extension currently contributes to the typst format name (see _extensions/naif-report/_extension.yml). Unless the extension actually registers naif-report-typst, this example file won’t render as-is. Update either the extension’s contributed format name or this YAML header to match.
| naif-report-typst: | |
| typst: |
| if not compact-mode { | ||
| counter(page).update(1) | ||
| _cover-page(doc-category, title, subtitle, authors, date, version, logo) | ||
| } |
There was a problem hiding this comment.
counter(page).update(1) is applied before generating the cover page, which means that when show-outline is false, the first numbered content page will be 2 (cover consumes page 1, but has no footer). Consider moving the page-counter reset to just before the main content starts (after cover/outline handling) so the first content page is consistently numbered 1 regardless of whether a TOC is shown.
| if not compact-mode { | ||
| counter(page).update(1) | ||
| _cover-page(doc-category, title, subtitle, authors, date, version, logo) | ||
| } |
There was a problem hiding this comment.
counter(page).update(1) is applied before generating the cover page, which means that when show-outline is false, the first numbered content page will be 2 (cover consumes page 1, but has no footer). Consider moving the page-counter reset to just before the main content starts (after cover/outline handling) so the first content page is consistently numbered 1 regardless of whether a TOC is shown.
| // Brand source: ../../../_brand.yml and ../../../base_colors.scss. | ||
|
|
||
| #let naif-primary = rgb("#A1AB71") | ||
| #let naif-secondary = rgb("#E8E1D0") | ||
| #let naif-dark = rgb("#181818") | ||
| #let naif-accent = rgb("#1F3A5F") | ||
| #let naif-accent-dark = rgb("#162A45") | ||
| #let naif-white = rgb("#FFFFFF") | ||
|
|
||
| #let naif-font-stack = ("Barlow", "Inter", "Arial", "Helvetica") | ||
| #let naif-barlow-font-stack = ("Barlow", "Inter", "Arial", "Helvetica") | ||
| #let naif-mono-font-stack = ("Menlo", "DejaVu Sans Mono") | ||
|
|
||
| #let _is-empty(value) = value == none or value == "" or value == [] | ||
|
|
||
| #let _meta-line(date, version) = { | ||
| if date != none and version != none { | ||
| [#date | #version] | ||
| } else if date != none { | ||
| date | ||
| } else if version != none { | ||
| version | ||
| } else { | ||
| none | ||
| } | ||
| } | ||
|
|
||
| #let _author-card(author) = { | ||
| let name = author.at("name", default: []) | ||
| let affiliation = author.at("affiliation", default: none) | ||
| let email = author.at("email", default: none) | ||
|
|
||
| [ | ||
| #text(weight: 700)[#name] | ||
| #if not _is-empty(affiliation) [ | ||
| #v(2pt) | ||
| #text(size: 9pt)[#affiliation] | ||
| ] | ||
| #if not _is-empty(email) [ | ||
| #v(2pt) | ||
| #link("mailto:" + email)[#email] | ||
| ] | ||
| ] | ||
| } | ||
|
|
||
| #let _authors-block(authors) = { | ||
| if authors.len() > 0 { | ||
| let ncols = calc.min(authors.len(), 3) | ||
| grid( | ||
| columns: (1fr,) * ncols, | ||
| gutter: 10pt, | ||
| row-gutter: 12pt, | ||
| ..authors.map(author => _author-card(author)), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| #let _title-block(title, subtitle, authors, date, version, abstract, keywords) = { | ||
| let meta = _meta-line(date, version) | ||
|
|
||
| rect(width: 100%, fill: naif-secondary, radius: 8pt, inset: 20pt)[ | ||
| #align(center)[ | ||
| #text(size: 8pt, weight: 700, tracking: 0.08em, fill: naif-accent)[NAIF REPORT] | ||
| #v(9pt) | ||
| #text(size: 28pt, weight: 800, fill: naif-dark)[#title] | ||
| #v(5pt) | ||
| #line(length: 35%, stroke: 2pt + naif-primary) | ||
| #if subtitle != none [ | ||
| #v(9pt) | ||
| #text(size: 14pt, weight: 500, fill: naif-accent-dark)[#subtitle] | ||
| ] | ||
| #if meta != none [ | ||
| #v(10pt) | ||
| #text(size: 9pt, weight: 500)[#meta] | ||
| ] | ||
| ] | ||
| ] | ||
|
|
||
| if authors.len() > 0 { | ||
| v(14pt) | ||
| _authors-block(authors) | ||
| } | ||
|
|
||
| if abstract != none { | ||
| v(16pt) | ||
| rect( | ||
| width: 100%, | ||
| fill: naif-white, | ||
| stroke: 0.7pt + naif-secondary, | ||
| radius: 6pt, | ||
| inset: 12pt, | ||
| )[ | ||
| #text(weight: 700, fill: naif-accent)[Abstract] | ||
| #v(4pt) | ||
| #abstract | ||
| #if keywords.len() > 0 [ | ||
| #v(8pt) | ||
| #text(weight: 700)[Keywords.] #keywords.join(", ") | ||
| ] | ||
| ] | ||
| } | ||
|
|
||
| v(20pt) | ||
| } | ||
|
|
There was a problem hiding this comment.
This file appears to duplicate documents/templates/typst/lib.typ (same constants + naif-report implementation). Maintaining two independent copies of the same Typst template increases the risk they drift over time. If possible, make one the single source of truth (e.g., have the extension reference/import the shared file, or generate/copy it as part of a build step).
| // Brand source: ../../../_brand.yml and ../../../base_colors.scss. | |
| #let naif-primary = rgb("#A1AB71") | |
| #let naif-secondary = rgb("#E8E1D0") | |
| #let naif-dark = rgb("#181818") | |
| #let naif-accent = rgb("#1F3A5F") | |
| #let naif-accent-dark = rgb("#162A45") | |
| #let naif-white = rgb("#FFFFFF") | |
| #let naif-font-stack = ("Barlow", "Inter", "Arial", "Helvetica") | |
| #let naif-barlow-font-stack = ("Barlow", "Inter", "Arial", "Helvetica") | |
| #let naif-mono-font-stack = ("Menlo", "DejaVu Sans Mono") | |
| #let _is-empty(value) = value == none or value == "" or value == [] | |
| #let _meta-line(date, version) = { | |
| if date != none and version != none { | |
| [#date | #version] | |
| } else if date != none { | |
| date | |
| } else if version != none { | |
| version | |
| } else { | |
| none | |
| } | |
| } | |
| #let _author-card(author) = { | |
| let name = author.at("name", default: []) | |
| let affiliation = author.at("affiliation", default: none) | |
| let email = author.at("email", default: none) | |
| [ | |
| #text(weight: 700)[#name] | |
| #if not _is-empty(affiliation) [ | |
| #v(2pt) | |
| #text(size: 9pt)[#affiliation] | |
| ] | |
| #if not _is-empty(email) [ | |
| #v(2pt) | |
| #link("mailto:" + email)[#email] | |
| ] | |
| ] | |
| } | |
| #let _authors-block(authors) = { | |
| if authors.len() > 0 { | |
| let ncols = calc.min(authors.len(), 3) | |
| grid( | |
| columns: (1fr,) * ncols, | |
| gutter: 10pt, | |
| row-gutter: 12pt, | |
| ..authors.map(author => _author-card(author)), | |
| ) | |
| } | |
| } | |
| #let _title-block(title, subtitle, authors, date, version, abstract, keywords) = { | |
| let meta = _meta-line(date, version) | |
| rect(width: 100%, fill: naif-secondary, radius: 8pt, inset: 20pt)[ | |
| #align(center)[ | |
| #text(size: 8pt, weight: 700, tracking: 0.08em, fill: naif-accent)[NAIF REPORT] | |
| #v(9pt) | |
| #text(size: 28pt, weight: 800, fill: naif-dark)[#title] | |
| #v(5pt) | |
| #line(length: 35%, stroke: 2pt + naif-primary) | |
| #if subtitle != none [ | |
| #v(9pt) | |
| #text(size: 14pt, weight: 500, fill: naif-accent-dark)[#subtitle] | |
| ] | |
| #if meta != none [ | |
| #v(10pt) | |
| #text(size: 9pt, weight: 500)[#meta] | |
| ] | |
| ] | |
| ] | |
| if authors.len() > 0 { | |
| v(14pt) | |
| _authors-block(authors) | |
| } | |
| if abstract != none { | |
| v(16pt) | |
| rect( | |
| width: 100%, | |
| fill: naif-white, | |
| stroke: 0.7pt + naif-secondary, | |
| radius: 6pt, | |
| inset: 12pt, | |
| )[ | |
| #text(weight: 700, fill: naif-accent)[Abstract] | |
| #v(4pt) | |
| #abstract | |
| #if keywords.len() > 0 [ | |
| #v(8pt) | |
| #text(weight: 700)[Keywords.] #keywords.join(", ") | |
| ] | |
| ] | |
| } | |
| v(20pt) | |
| } | |
| // Import the shared template library so this extension does not duplicate it. | |
| #import "../../documents/templates/typst/lib.typ": * |
| keywords: ($for(keywords)$"$it$"$sep$, $endfor$), | ||
| $endif$ | ||
| $if(lang)$ | ||
| language: "$lang$", |
There was a problem hiding this comment.
The template passes Quarto’s project lang through to Typst (language: "$lang$"). In this repo _quarto.yml sets lang: en-UK, which is a non-standard BCP47 tag (typically en-GB). If Typst validates language tags, this can break rendering. Consider either omitting language unless explicitly set for the report, or normalising common values (e.g. mapping en-UK → en-GB).
| language: "$lang$", | |
| language: if "$lang$" == "en-UK" { "en-GB" } else { "$lang$" }, |
Pull request overview
Adds a NAIF-branded Typst report template and exposes it as a Quarto extension so reports can be authored in Typst directly or rendered from Quarto to PDF.
Changes:
documents/templates/typst) with an example document and compiled PDF._extensions/naif-report) intended to provide anaif-report-typstoutput format using Typst template partials.documents/templates/quarto/naif-report-example.qmd) demonstrating the intended authoring workflow.