diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e47315..95f6fc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,4 +9,25 @@ name: CI jobs: test: - uses: MorePET/github-actions/.github/workflows/ci-python-reusable.yml@main + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install Typst + uses: typst-community/setup-typst@v3 + with: + typst-version: 'latest' + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run tests + run: make test diff --git a/CHANGELOG.md b/CHANGELOG.md index 882b463..f6399d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this template will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.3.0] ### Added @@ -64,6 +64,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Consolidated diagram outputs to project-specific build directories - Simplified gitignore patterns for build artifacts - Updated all include paths to reference build directories + - Unified example and main build processes to use same pipeline (colors → diagrams → pdf → html) + - Example project outputs now in dedicated `example/build/` folder - **Documentation Migration** - Migrated all documentation from Markdown to Typst format @@ -76,6 +78,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - TOC sidebar uses Bootstrap Offcanvas component for better mobile support - Improved regex patterns for SVG injection to prevent content deletion +- **Typst Library Enhancements** + - Isotope and beta particle notation now uses Unicode superscripts/subscripts in HTML export + - Better accessibility with native Unicode text instead of SVG fragments + - Smaller HTML file sizes with improved text selection and copy/paste support + - PDF export unchanged (still uses physica package for proper typesetting) + ### Fixed - **Table Column Widths** diff --git a/Makefile b/Makefile index 253de3a..1dc1344 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ THEME_TOGGLE ?= yes TECH_DOC_SRC = docs/main.typ TECH_DOC_OUT = build/technical-documentation EXAMPLE_SRC = example/docs/main.typ -EXAMPLE_OUT = example/build/example-documentation +EXAMPLE_OUT = example/build/technical-documentation # Default target - builds the technical-documentation project all: technical-documentation @@ -48,17 +48,15 @@ technical-documentation: @$(MAKE) build-summary OUT=$(TECH_DOC_OUT) @echo "🌓 Toggle dark mode with the button in top-right" -# Build example project (depends on python-project artifacts) +# Build example project (uses same build process as technical-documentation) example: @echo "==================================================" @echo "🎨 Building Example Project" @echo "==================================================" - @$(MAKE) python-api - @$(MAKE) python-tests - @$(MAKE) python-diagrams @$(MAKE) build-project SRC=$(EXAMPLE_SRC) OUT=$(EXAMPLE_OUT) PROJECT=example @$(MAKE) server-start @$(MAKE) build-summary OUT=$(EXAMPLE_OUT) + @echo "🌓 Toggle dark mode with the button in top-right" # Generate color files (CSS and Typst) from colors.json colors: @@ -132,10 +130,10 @@ check: # Internal: Check that build outputs exist check-outputs: - @test -f technical-documentation/build/technical-documentation.pdf && echo "✓ Technical doc PDF exists" || echo "❌ Technical doc PDF missing" - @test -f technical-documentation/build/technical-documentation.html && echo "✓ Technical doc HTML exists" || echo "❌ Technical doc HTML missing" - @test -f example/build/example-documentation.pdf && echo "✓ Example PDF exists" || echo "❌ Example PDF missing" - @test -f example/build/example-documentation.html && echo "✓ Example HTML exists" || echo "❌ Example HTML missing" + @test -f build/technical-documentation.pdf && echo "✓ Technical doc PDF exists" || echo "❌ Technical doc PDF missing" + @test -f build/technical-documentation.html && echo "✓ Technical doc HTML exists" || echo "❌ Technical doc HTML missing" + @test -f example/build/technical-documentation.pdf && echo "✓ Example PDF exists" || echo "❌ Example PDF missing" + @test -f example/build/technical-documentation.html && echo "✓ Example HTML exists" || echo "❌ Example HTML missing" @test -f lib/generated/colors.css && echo "✓ Colors generated" || echo "❌ Colors missing" # Quick test - compile everything and check outputs exist @@ -146,7 +144,7 @@ test: technical-documentation example # Internal: Remove PDF and HTML outputs clean-outputs: - @rm -rf technical-documentation/build/ + @rm -rf build/ @rm -rf example/build/ @rm -rf example/python-project/build/ @rm -f *_temp*.html diff --git a/docs/main.typ b/docs/main.typ index 3761249..063359a 100644 --- a/docs/main.typ +++ b/docs/main.typ @@ -51,7 +51,7 @@ #strong[Intended Purpose] \ The DUPLET / EXOPET system is intended to perform simultaneous dual-tracer Positron Emission Tomography (PET) imaging using -radiopharmaceuticals that emit β⁺ particles, enabling non-invasive in +radiopharmaceuticals that emit #betaplus particles, enabling non-invasive in vivo detection, spatial localization, and functional characterization of cancer lesions with high spatial and temporal resolution. It aims to improve diagnosis and therapy planning by resolving intralesional and @@ -89,8 +89,8 @@ analyzing #strong[list-mode PET data];, enabling: - Capture of full photon energy and timestamp data for each detection event. - Identification of #strong[triplet events];: two 511 keV annihilation - photons and an additional prompt nuclear gamma (e.g., from 68Ga or - 44Sc decay). + photons and an additional prompt nuclear gamma (e.g., from #Ga68 or + #Sc44 decay). - Separation of signals from multiple tracers in one scan by applying #strong[energy-time discrimination];, positron lifetime analysis, and ML-supported pattern recognition. @@ -117,7 +117,7 @@ analyzing #strong[list-mode PET data];, enabling: algorithms using MVA/BDT, supervised learning, and domain-adapted image reconstruction (CASTOR, STIR, Geant4/GATE).], [Radiotracers and Isotopes], [Dual-tracer combinations, typically - \[18F\]-FDG and \[68Ga\]-PSMA or \[44Sc\]-FAPI.], + #FDG and #Ga68PSMA or #Sc44FAPI().], [Image Analysis Workstation], [GPU-supported server platform integrated with BioMedIT/Leonhard Med.], table.hline(), @@ -125,7 +125,7 @@ analyzing #strong[list-mode PET data];, enabling: ==== Accessories - Phantom sets for validation and calibration -- Cyclotron-based isotope production (e.g., \[44Sc\] via Ca(p,n)Sc) +- Cyclotron-based isotope production (e.g., #Sc44 via Ca(p,n)Sc) - Dose calibrators, gamma spectrometers, and QA tools - Clinical interface (graphical front-end for acquisition and processing) @@ -314,7 +314,7 @@ analyzing #strong[list-mode PET data];, enabling: oncology to bridge human and veterinary diagnostics.], [Radiopharmacists / Radiochemists], [Experts responsible for the production, radiolabeling, quality control, and dosing of PET - tracers such as \[¹⁸F\]FDG and \[⁴⁴Sc\]-FAPI-04 under GMP or + tracers such as #FDG and #Sc44FAPI04 under GMP or equivalent standards.], [Software Developers / Data Scientists], [Personnel developing and maintaining the reconstruction, dual-tracer separation, and ML @@ -449,53 +449,50 @@ analyzing #strong[list-mode PET data];, enabling: table.hline(), ) -===== Systemkomponentenübersicht – DUPLET / EXOPET - +===== System Components Overview – DUPLET / EXOPET + #table( columns: (5cm, auto), align: (left,auto,), table.hline(), - [Systemkomponente], [Definition und Zweck], + [System Component], [Definition and Purpose], table.hline(), - [PET-Detektorhardware (EXOPET-Ring)], [Modularer Ring aus - LYSO-Kristallen und SiPMs zur Detektion von β⁺-Annihilations- und - nuklearen Gammastrahlen (z. B. von ⁶⁸Ga oder ⁴⁴Sc). Ermöglicht - simultane Multi-Tracer-Bildgebung mit hoher räumlicher und - zeitlicher Auflösung.], - [Akquisitionselektronik & DAQ-System], [FPGA-basierte Zeitstempelung - und Digitalisierung aller Detektionsereignisse im List-Mode (inkl. - Energie & Zeit). Hohe Datenrate und Echtzeit-Speicherung zur - ML-gestützten Ereignisanalyse.], - [Bildverarbeitungssoftware & ML-Pipeline], [Software-Framework zur - Tracer-Diskriminierung (z. B. ⁶⁸Ga vs.~¹⁸F), basierend auf - Zeit-Energie-Korrelationen und ML-Algorithmen. Unterstützt - quantitative Bildrekonstruktion (z. B. SUV-Berechnung) und - Positronen-Lifetime-Analyse.], - [Benutzeroberfläche & Workflow-Software], [Touchscreen-gesteuerte - Bedienoberfläche zur Steuerung von Injektionen, Start/Stop des - Scans, Logging, Patientensicherheit und Positionierung. - Workflowführung zur Sicherstellung von Dual-Tracer-Kompatibilität.], - [Radiopharmazeutische Schnittstelle], [Eingabemodule für - Tracerinformationen (z. B. Aktivität, Injektionszeitpunkt, - Halbwertszeit), automatisierte Zerfallskorrektur und Verknüpfung mit - Dosimetriealgorithmen. GMP-konform.], - [Gehäuse & Mechanik], [Clamp-On-Ringstruktur zur Nachrüstung - bestehender PET/CT-Geräte. Wartungsfreundliches, modulares Design - mit Schnellzugriff auf Komponenten für Servicezwecke.], - [Datenexport & PACS-Integration], [HL7- und DICOM-kompatibler Export - an klinische IT-Systeme (PACS/RIS). Absicherung der Datenübertragung - durch AES-Verschlüsselung, Audit-Trail und BioMedIT-Anbindung.], - [Kalibrierung & QA-Zubehör], [NEMA-NU2-konforme Phantome (z. B. IEC - Body Phantom), automatische Kalibrierungsroutinen, - Tageskontrollfunktionen für Energie, Zeitauflösung und - Sensitivität.], - [Tierbildgebungsschnittstelle], [Spezielle Aufnahmeeinsätze und - Immobilisierungshilfen zur PET/CT-Diagnostik bei Hunden (z. B. für - komparative Onkologie). Anpassbar für Kleintiere.], - [IT-Sicherheit & Datenmanagement], [Sichere Speicherung und - Verarbeitung von Rohdaten und Patientendaten. DSGVO/HFG-konforme - Pseudonymisierung, rollenbasiertes Benutzer-Login, Datenübertragung - über BioMedIT/Leonhard Med.], + [PET Detector Hardware (EXOPET Ring)], [Modular ring of LYSO + crystals and SiPMs for detection of #betaplus annihilation and nuclear + gamma rays (e.g., from #Ga68 or #Sc44). Enables simultaneous + multi-tracer imaging with high spatial and temporal resolution.], + [Acquisition Electronics & DAQ System], [FPGA-based timestamping and + digitization of all detection events in list mode (incl. energy & + time). High data rate and real-time storage for ML-supported event + analysis.], + [Image Processing Software & ML Pipeline], [Software framework for + tracer discrimination (e.g., #Ga68 vs.~#F18), based on time-energy + correlations and ML algorithms. Supports quantitative image + reconstruction (e.g., SUV calculation) and positron lifetime + analysis.], + [User Interface & Workflow Software], [Touchscreen-controlled + interface for managing injections, scan start/stop, logging, patient + safety, and positioning. Workflow guidance to ensure dual-tracer + compatibility.], + [Radiopharmaceutical Interface], [Input modules for tracer + information (e.g., activity, injection time, half-life), automated + decay correction, and integration with dosimetry algorithms. + GMP-compliant.], + [Housing & Mechanics], [Clamp-on ring structure for retrofitting + existing PET/CT devices. Maintenance-friendly, modular design with + quick access to components for service purposes.], + [Data Export & PACS Integration], [HL7 and DICOM-compatible export + to clinical IT systems (PACS/RIS). Data transmission secured through + AES encryption, audit trail, and BioMedIT integration.], + [Calibration & QA Accessories], [NEMA NU-2 compliant phantoms (e.g., + IEC Body Phantom), automatic calibration routines, daily quality + control functions for energy, time resolution, and sensitivity.], + [Animal Imaging Interface], [Special imaging inserts and + immobilization aids for PET/CT diagnostics in dogs (e.g., for + comparative oncology). Adaptable for small animals.], + [IT Security & Data Management], [Secure storage and processing of + raw data and patient data. GDPR/HFG-compliant pseudonymization, + role-based user login, data transmission via BioMedIT/Leonhard Med.], table.hline(), ) === Verification and Validation (V&V) diff --git a/lib/NOTATIONS-README.md b/lib/NOTATIONS-README.md new file mode 100644 index 0000000..9909545 --- /dev/null +++ b/lib/NOTATIONS-README.md @@ -0,0 +1,258 @@ +# Notations Package Documentation + +## Overview + +The `notations.typ` package provides a comprehensive system for writing isotope and radiopharmaceutical notation in Typst documents, using the `@preview/physica:0.9.4` package. + +## Usage + +```typst +#import "lib/notations.typ": * +// or through the technical documentation package: +#import "lib/technical-documentation-package.typ": * +``` + +## Isotope Notation + +### Individual Isotopes + +Pre-defined isotope constants with proper superscript mass numbers: + +```typst +#F18 // ¹⁸F +#F19 // ¹⁹F +#Ga68 // ⁶⁸Ga +#Sc44 // ⁴⁴Sc +#Sc47 // ⁴⁷Sc +#C11 // ¹¹C +#C13 // ¹³C +#C14 // ¹⁴C +#I123 // ¹²³I +#I131 // ¹³¹I +#Tc99m // ⁹⁹ᵐTc +#O15 // ¹⁵O +#N13 // ¹³N +``` + +### Custom Isotopes + +Use the `nuclide()` function for custom isotopes: + +```typst +#nuclide("U", A: 235) // ²³⁵U (mass number only) +#nuclide("U", A: 235, Z: 92) // ²³⁵₉₂U (mass and atomic number) +``` + +## Radiopharmaceutical Tracers (DRY & SOLID Design) + +### Core Building Blocks + +The system follows SOLID principles with three composable functions: + +1. **`isotope(element, a: mass)`** - creates isotope notation +2. **`ligand(name, id: none)`** - creates ligand with optional ID +3. **`tracer(isotope, ligand)`** - combines isotope + ligand + +### Creating Ligands + +```typst +#ligand("FDG") // FDG +#ligand("PSMA") // PSMA +#ligand("FAPI") // FAPI +#ligand("FAPI", id: "04") // FAPI-04 +#ligand("PSMA", id: "617") // PSMA-617 +``` + +### Creating Tracers by Composition + +Combine any isotope with any ligand: + +```typst +// Using named parameters (recommended for clarity) +#tracer(isotope: F18, ligand: ligand("FDG")) + +// Using positional parameters +#tracer(F18, ligand("FDG")) // ¹⁸F-FDG +#tracer(Ga68, ligand("PSMA")) // ⁶⁸Ga-PSMA +#tracer(Sc44, ligand("FAPI", id: "04")) // ⁴⁴Sc-FAPI-04 +#tracer(I131, ligand("MIBG")) // ¹³¹I-MIBG +#tracer(C11, ligand("Methionine")) // ¹¹C-Methionine +``` + +### Pre-defined Common Tracers + +For convenience, common tracers are pre-defined: + +```typst +#FDG // ¹⁸F-FDG +#Ga68PSMA // ⁶⁸Ga-PSMA +#Sc44PSMA // ⁴⁴Sc-PSMA +#F18PSMA // ¹⁸F-PSMA +#Ga68FAPI04 // ⁶⁸Ga-FAPI-04 +#Sc44FAPI04 // ⁴⁴Sc-FAPI-04 +#Ga68FAPI46 // ⁶⁸Ga-FAPI-46 +#Sc44FAPI46 // ⁴⁴Sc-FAPI-46 +#Ga68DOTATATE // ⁶⁸Ga-DOTATATE +#Ga68DOTATOC // ⁶⁸Ga-DOTATOC +#C11PIB // ¹¹C-PIB +#F18FET // ¹⁸F-FET +#I131MIBG // ¹³¹I-MIBG +``` + +### Dynamic FAPI Functions + +FAPI tracers support flexible ID specification: + +```typst +#Ga68FAPI() // ⁶⁸Ga-FAPI (no ID) +#Ga68FAPI(id: "04") // ⁶⁸Ga-FAPI-04 +#Ga68FAPI(id: "46") // ⁶⁸Ga-FAPI-46 +#Sc44FAPI(id: "02") // ⁴⁴Sc-FAPI-02 +``` + +### Pre-defined Ligand Constants + +Common ligands are available as constants: + +```typst +FDG-ligand // FDG +PSMA-ligand // PSMA +FAPI-ligand // FAPI +FAPI04-ligand // FAPI-04 +FAPI46-ligand // FAPI-46 +DOTATATE-ligand // DOTATATE +DOTATOC-ligand // DOTATOC +PIB-ligand // PIB +FET-ligand // FET +MIBG-ligand // MIBG +``` + +Use them to create custom combinations: + +```typst +#tracer(F18, PSMA-ligand) // ¹⁸F-PSMA +#tracer(Sc47, FAPI04-ligand) // ⁴⁷Sc-FAPI-04 +``` + +## Particles and Symbols + +```typst +#betaplus // β⁺ +#betaminus // β⁻ +``` + +## Examples in Context + +```typst +#import "lib/notations.typ": * + += PET Imaging Study + +Radiopharmaceuticals that emit #betaplus particles enable PET imaging. + +Dual-tracer combinations typically include #FDG and #Ga68PSMA or #Sc44FAPI(). + +The radiopharmacist prepared #FDG and #Sc44FAPI04 under GMP conditions. + +Comparison study: #F18PSMA vs #Ga68PSMA in prostate cancer imaging. + +Detection of triplet events from #Ga68 or #Sc44 decay enables multi-tracer separation. +``` + +## Creating Custom Tracers + +The composable design allows maximum flexibility: + +```typst +// Create a custom ligand +#let MyLigand = ligand("CustomCompound", id: "v2") + +// Use it with any isotope +#tracer(F18, MyLigand) // ¹⁸F-CustomCompound-v2 +#tracer(Ga68, MyLigand) // ⁶⁸Ga-CustomCompound-v2 + +// On-the-fly custom tracers +#tracer(N13, ligand("Ammonia")) // ¹³N-Ammonia +#tracer(O15, ligand("Water")) // ¹⁵O-Water +#tracer(C11, ligand("Acetate")) // ¹¹C-Acetate +#tracer(F18, ligand("Florbetapir")) // ¹⁸F-Florbetapir +#tracer(Ga68, ligand("PSMA", id: "617")) // ⁶⁸Ga-PSMA-617 +``` + +## Technical Notes + +### HTML Export with Unicode Superscripts + +The notation system uses different rendering approaches for PDF and HTML: + +**How it works:** +- **PDF export:** Uses the `physica` package's `isotope()` function for proper mathematical typography +- **HTML export:** Uses Unicode superscript characters (e.g., ⁴⁴, ⁶⁸, ¹⁸) for clean text output +- Automatic mode detection via `sys.inputs.at("html-export", default: "false")` +- Inline isotopes stay inline within paragraphs in both formats + +**Implementation:** + +```typst +#let nuclide(element, A: none, Z: none) = context { + let is-html = sys.inputs.at("html-export", default: "false") == "true" + + if is-html { + // HTML: use Unicode superscripts/subscripts + [#to-superscript(str(A))#element] + } else { + // PDF: use physica package for proper typography + isotope(element, a: str(A)) + } +} +``` + +**Benefits:** +- Clean ASCII/Unicode text in HTML (no SVG fragments needed) +- Perfect math typography in PDF using physica package +- Better accessibility - screen readers can read the Unicode text directly +- Smaller HTML file sizes (no embedded SVGs for isotopes) +- Better text selection and copy/paste in HTML + +### Implementation Details + +- Unicode superscript mapping: 0→⁰, 1→¹, 2→², 3→³, 4→⁴, 5→⁵, 6→⁶, 7→⁷, 8→⁸, 9→⁹, m→ᵐ, n→ⁿ, +→⁺, -→⁻ +- Unicode subscript mapping: 0→₀, 1→₁, 2→₂, 3→₃, 4→₄, 5→₅, 6→₆, 7→₇, 8→₈, 9→₉ +- Beta particles (β⁺ and β⁻) also use Unicode in HTML export for consistency +- Hyphens in tracer names are escaped to prevent interpretation as subtraction +- Supports full range of mass numbers (0-9) and special characters (m for metastable states) +- The build script (`build-html-bootstrap.py`) automatically sets the `html-export=true` flag + +## Design Principles (DRY & SOLID) + +### Single Responsibility Principle + +Each function has one clear purpose: + +- `isotope()` - creates isotope notation only +- `ligand()` - creates ligand notation only +- `tracer()` - combines isotope + ligand only + +### Don't Repeat Yourself (DRY) + +- No element-specific tracer functions (removed `F18tracer()`, `Ga68tracer()`, etc.) +- Single `tracer()` function handles all isotope-ligand combinations +- Ligand logic centralized in one `ligand()` function + +### Open/Closed Principle + +- System is open for extension (add new isotopes/ligands) +- Closed for modification (core functions don't need changes) +- Example: Add new isotopes by defining constants, not modifying functions + +### Composition over Inheritance + +- Build complex tracers by composing simple building blocks +- `tracer(isotope, ligand)` composes two independent concepts +- Users can create any combination without code changes + +### Dependency Inversion + +- High-level pre-defined tracers (`#FDG`, `#Ga68PSMA`) depend on abstractions +- Core functions (`tracer`, `ligand`, `isotope`) are stable abstractions +- Changes to specific tracers don't affect the core system diff --git a/lib/notations.typ b/lib/notations.typ new file mode 100644 index 0000000..0362447 --- /dev/null +++ b/lib/notations.typ @@ -0,0 +1,198 @@ +// Nuclear and radiopharmaceutical notation package +// Uses physica package for PDF, Unicode superscripts for HTML export +// +// HTML export automatically uses clean Unicode text (e.g., ⁴⁴Sc) +// PDF export uses physica package for proper mathematical typesetting + +#import "@preview/physica:0.9.4": isotope + +// ======================================== +// Unicode character mappings for HTML export +// ======================================== + +// Map regular digits and letters to Unicode superscript characters +#let to-superscript(text) = { + let char-map = ( + "0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴", + "5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹", + "m": "ᵐ", "n": "ⁿ", + "+": "⁺", "-": "⁻", + ) + + let result = "" + for char in text { + result += char-map.at(char, default: char) + } + result +} + +// Map regular digits to Unicode subscript characters +#let to-subscript(text) = { + let char-map = ( + "0": "₀", "1": "₁", "2": "₂", "3": "₃", "4": "₄", + "5": "₅", "6": "₆", "7": "₇", "8": "₈", "9": "₉", + ) + + let result = "" + for char in text { + result += char-map.at(char, default: char) + } + result +} + +// ======================================== +// Core isotope notation function +// ======================================== +// A = mass number (superscript) +// Z = atomic number (subscript, optional) +// +// PDF: Uses physica package for proper math typography +// HTML: Uses Unicode superscripts for clean text output +#let nuclide(element, A: none, Z: none) = context { + let is-html = sys.inputs.at("html-export", default: "false") == "true" + + if is-html { + // HTML export: use Unicode superscripts/subscripts + if Z != none { + [#to-superscript(str(A))#to-subscript(str(Z))#element] + } else if A != none { + [#to-superscript(str(A))#element] + } else { + element + } + } else { + // PDF export: use physica package for proper typography + if Z != none { + isotope(element, a: str(A), z: str(Z)) + } else if A != none { + isotope(element, a: str(A)) + } else { + element + } + } +} + +// ======================================== +// Convenience wrappers for common isotopes +// ======================================== + +// Fluorine isotopes +#let F18 = nuclide("F", A: "18") +#let F19 = nuclide("F", A: "19") + +// Gallium isotopes +#let Ga68 = nuclide("Ga", A: "68") + +// Scandium isotopes +#let Sc44 = nuclide("Sc", A: "44") +#let Sc47 = nuclide("Sc", A: "47") + +// Carbon isotopes (for research/labeling) +#let C11 = nuclide("C", A: "11") +#let C13 = nuclide("C", A: "13") +#let C14 = nuclide("C", A: "14") + +// Iodine isotopes +#let I123 = nuclide("I", A: "123") +#let I131 = nuclide("I", A: "131") + +// Technetium +#let Tc99m = nuclide("Tc", A: "99m") + +// Oxygen +#let O15 = nuclide("O", A: "15") + +// Nitrogen +#let N13 = nuclide("N", A: "13") + +// ======================================== +// Radiopharmaceutical Tracer System +// ======================================== + +// Ligand builder function +// Creates a ligand with optional ID +#let ligand(name, id: none) = { + if id != none and id != "" { + [#name\-#id] + } else { + [#name] + } +} + +// Core tracer builder function +// Combines isotope and ligand into a radiopharmaceutical notation +// Call with: tracer(isotope: F18, ligand: FDG-ligand) or tracer(F18, FDG-ligand) +#let tracer(isotope, ligand) = { + [#isotope\-#ligand] +} + +// ======================================== +// Common ligands +// ======================================== + +#let FDG-ligand = ligand("FDG") +#let PSMA-ligand = ligand("PSMA") +#let FAPI-ligand = ligand("FAPI") +#let FAPI04-ligand = ligand("FAPI", id: "04") +#let FAPI46-ligand = ligand("FAPI", id: "46") +#let DOTATATE-ligand = ligand("DOTATATE") +#let DOTATOC-ligand = ligand("DOTATOC") +#let PIB-ligand = ligand("PIB") +#let FET-ligand = ligand("FET") +#let MIBG-ligand = ligand("MIBG") + +// ======================================== +// Common radiopharmaceutical compounds +// ======================================== + +// FDG variants +#let FDG = tracer(F18, FDG-ligand) + +// PSMA variants +#let Ga68PSMA = tracer(Ga68, PSMA-ligand) +#let Sc44PSMA = tracer(Sc44, PSMA-ligand) +#let F18PSMA = tracer(F18, PSMA-ligand) + +// FAPI variants - as functions for flexible ID +#let Ga68FAPI(id: none) = tracer(Ga68, ligand("FAPI", id: id)) +#let Sc44FAPI(id: none) = tracer(Sc44, ligand("FAPI", id: id)) + +// Common specific FAPI compounds +#let Ga68FAPI04 = tracer(Ga68, FAPI04-ligand) +#let Sc44FAPI04 = tracer(Sc44, FAPI04-ligand) +#let Ga68FAPI46 = tracer(Ga68, FAPI46-ligand) +#let Sc44FAPI46 = tracer(Sc44, FAPI46-ligand) + +// DOTATATE/DOTATOC variants +#let Ga68DOTATATE = tracer(Ga68, DOTATATE-ligand) +#let Ga68DOTATOC = tracer(Ga68, DOTATOC-ligand) + +// Other common tracers +#let C11PIB = tracer(C11, PIB-ligand) +#let F18FET = tracer(F18, FET-ligand) +#let I131MIBG = tracer(I131, MIBG-ligand) + +// ======================================== +// Greek symbols and particles +// ======================================== + +// Beta particles with Unicode support for HTML +// PDF: Uses math mode for proper typography +// HTML: Uses Unicode β⁺ and β⁻ for clean text output +#let betaplus = context { + let is-html = sys.inputs.at("html-export", default: "false") == "true" + if is-html { + [β⁺] + } else { + $beta^+$ + } +} + +#let betaminus = context { + let is-html = sys.inputs.at("html-export", default: "false") == "true" + if is-html { + [β⁻] + } else { + $beta^-$ + } +} diff --git a/lib/technical-documentation-package.typ b/lib/technical-documentation-package.typ index 9103f5b..fcf165b 100644 --- a/lib/technical-documentation-package.typ +++ b/lib/technical-documentation-package.typ @@ -7,6 +7,9 @@ // Import Cheq for markdown-like checklists #import "@preview/cheq:0.3.0": checklist +// Import and re-export notations for isotopes and radiopharmaceuticals +#import "notations.typ": * + // Main template function that applies all formatting #let tech-doc( body, @@ -47,8 +50,17 @@ // ) // Set text properties + // PDF: Libertinus Serif for beautiful typography + // HTML: System fonts to match Bootstrap 5.3.2 + let is-html = sys.inputs.at("html-export", default: "false") == "true" + let body-font = if is-html { + ("system-ui", "Arial", "sans-serif") + } else { + "Libertinus Serif" + } + set text( - font: "Libertinus Serif", + font: body-font, size: 10pt, lang: "en", ) @@ -66,6 +78,28 @@ block(above: 1.4em, below: 1em, it) } + // Configure math equations + // - PDF: Use proper math font for best typography + // - HTML: Use system fonts to match Bootstrap's default font stack + // - Box inline equations to keep them inline in paragraphs + // Note: Isotopes now use Unicode superscripts directly (not math equations) + set math.equation(numbering: none) + show math.equation: it => context { + let is-html = sys.inputs.at("html-export", default: "false") == "true" + + if is-html { + // HTML export: match Bootstrap's system font stack (sans-serif) + // Bootstrap 5.3.2 uses: system-ui, -apple-system, "Segoe UI", Roboto, etc. + set text(font: ("system-ui", "Arial", "sans-serif")) + // Wrap inline equations in box so they don't interrupt paragraphs + show: if it.block { it => it } else { box } + html.frame(it) + } else { + // PDF export: use default math font for proper math typography + it + } + } + // Note: HTML styling must be added after export using Bootstrap build script: // python3 scripts/build-html-bootstrap.py your-file.typ your-file.html // diff --git a/scripts/build-html-bootstrap.py b/scripts/build-html-bootstrap.py index 178220f..aca8bea 100755 --- a/scripts/build-html-bootstrap.py +++ b/scripts/build-html-bootstrap.py @@ -34,6 +34,8 @@ def compile_typst_to_html(typ_file: Path, html_file: Path) -> bool: "html", "--input", "use-svg=true", + "--input", + "html-export=true", str(typ_file), str(html_file), ],