Files
retroSoC/publications/datasheets/style.typ
T
maksyuki fc6440989f docs: add environment reference and enhance publication validation
- Introduced  to define the development environment and validate runtime/platform contracts.
- Enhanced  with  to ensure change markers adhere to publication rules.
- Updated  to integrate boot acceptance checks and ensure dependencies are correctly managed.
- Added tests for environment validation, publication change scope, and CI stages to prevent regression and ensure compliance with publication standards.
- Implemented checks in  to validate the presence of development environment dependencies.
- Refactored existing tests to improve coverage and ensure accurate validation of publication claims and software configurations.
2026-09-23 17:09:55 +08:00

373 lines
17 KiB
Typst

#let ink = rgb("#292C31")
#let gold = rgb("#A38442")
#let pale-gold = rgb("#F4EFE4")
#let gray = rgb("#F1F2F3")
#let rule = rgb("#D5D6D8")
#let muted = rgb("#62666C")
#let link-color = rgb("#245A81")
#let inline-code-color = rgb("#B12146")
#let table-stroke = 0.6pt + black
#let mono = "FiraCode Nerd Font"
#let data = json(sys.inputs.at("data"))
#let doc = data.document
#let ip-chapters = json("chapter-index.json")
// RM0486-inspired spacing, adapted to the retained Inter sizes. Inter's
// cap-height-to-baseline box is 7.639 pt at 10.5 pt and 6.548 pt at 9 pt.
// Thus these leading values yield 12.6 / 10.8 pt baseline pitches, rather
// than treating Typst's `leading` as the whole distance between baselines.
#let rhythm = (
body-leading: 4.96pt, body-spacing: 11.26pt,
small-leading: 4.25pt, small-spacing: 8.25pt,
heading-before: (28pt,24pt,18pt,12pt,10pt),
heading-after: (18pt,10pt,8pt,6pt,5pt),
heading-gap: 8pt, minor-before: 12pt, minor-after: 6pt,
register-before: 12pt, register-after: 4pt,
list-spacing: 7.96pt, list-indent: 0pt, list-body-indent: 8pt,
figure-space: 12pt, figure-caption: 6pt, table-caption: 4pt,
table-inset: (x:6pt,y:5pt), table-notes: 6pt,
continuation-size: 8.5pt, continuation-gap: 2pt,
code-space: 8pt, code-inset: 8pt,
note-space: 12pt, note-inset: 10pt,
metadata-before: 4pt, metadata-after: 6pt,
bits-space: 6pt, bits-inset: (x:3pt,y:5pt),
toc-leading: 9.09pt, toc-indent: 14pt, toc-group-before: 12pt, toc-group-after: 4pt,
header-gap: 4pt,
cover-leading: 4.96pt, cover-spacing: 3.675pt, cover-list-spacing: 7.96pt,
cover-heading-before: (12pt,12pt), cover-heading-after: 6pt,
cover-gap: 3pt, cover-meta-gap: 4pt, cover-rule-gap: 2pt,
cover-gutter: 7mm, cover-list-indent: 0.5em,
diagram-leading: 0.4em, inventory-leading: 0.35em,
)
#let heading-layout(it, before: none, after: none) = {
if it.outlined and it.numbering != none and it.level <= 2 {
context {
let preceding = query(selector(heading).before(it.location(),inclusive:false)).filter(h=>h.outlined and h.numbering!=none)
let chapters = preceding.filter(h=>h.level==1)
let product-brief = if it.level==1 {it.body==[Product Brief]}
else {chapters.len()>0 and chapters.last().body==[Product Brief]}
// Chapter openers and their first section form one heading group.
// Subsequent sections break independently, even when their text is short.
let chapter-opener = it.level==2 and preceding.len()>0 and preceding.last().level==1
if not product-brief and not chapter-opener {pagebreak(weak:true)}
}
}
let index = calc.min(it.level - 1,4)
let sizes = (21pt,16pt,13pt,11pt,10.5pt)
block(above: if before == none {rhythm.heading-before.at(index)} else {before},
below: if after == none {rhythm.heading-after.at(index)} else {after}, sticky:true)[
#set text(size:sizes.at(index),weight:"semibold")
#set par(leading:rhythm.body-leading,spacing:0pt)
#if it.numbering == none { it.body } else {
grid(columns:(auto,1fr),column-gutter:rhythm.heading-gap,
text(fill:gold,counter(heading).display(it.numbering)),it.body)
}
]
}
#let minor-title(body) = block(above:rhythm.minor-before,below:rhythm.minor-after,sticky:true,strong(body))
// Only this editing round emits active ranges; earlier markers remain historical.
#let current-change(id) = id.starts-with(doc.change_prefix) or id in (
"contents","table-directory","figure-directory")
#let change-start(id, title, category:"modified") = context {
if current-change(id) {metadata((
kind:"publication-change-start",id:id,title:title,category:category,page:here().page()))}
}
#let change-end(id) = context {
if current-change(id) {metadata((kind:"publication-change-end",id:id,page:here().page()))}
}
#let code-block(body, breakable:false) = block(above:rhythm.code-space,below:rhythm.code-space,
width:100%,inset:rhythm.code-inset,fill:gray,breakable:breakable)[
#set par(leading:rhythm.small-leading,spacing:rhythm.small-spacing)
#body
]
#let artifact-mark(state) = {
assert(state in ("empty", "left-half", "full"), message:"Unknown artifact mark")
box(width:3mm,height:3mm)[
#place(top + left,circle(radius:1.5mm,fill:white,stroke:none))
#if state=="left-half" {
place(top + left,box(width:1.5mm,height:3mm,clip:true)[
#circle(radius:1.5mm,fill:pale-gold,stroke:none)
])
} else if state=="full" {
place(top + left,circle(radius:1.5mm,fill:pale-gold,stroke:none))
}
#place(top + left,circle(radius:1.5mm,fill:none,
stroke:0.6pt + if state=="empty" {ink} else {gold}))
]
}
#let artifact-evaluation(states) = rect(width:92mm,height:10mm,radius:1.5mm,
fill:white,stroke:0.6pt + ink,inset:(x:2mm,y:1mm))[
#set text(size:9pt,weight:"regular",fill:ink)
#set par(leading:rhythm.small-leading,spacing:0pt)
#align(center + horizon)[
#grid(columns:(1fr,1fr,1fr,1fr),row-gutter:2pt,align:center,
..states.map(state=>state.name),
..states.map(state=>artifact-mark(state.fill)),
)
]
]
#let cover(body, evaluation:none) = {
show heading: it => {
if it.level==1 and it.body==[Product Brief] and evaluation!=none {
block(width:100%,above:rhythm.cover-heading-before.first(),
below:rhythm.cover-heading-after,sticky:true)[
#grid(columns:(1fr,92mm),column-gutter:4mm,row-gutter:2pt,
align(horizon,heading-layout(it,before:0pt,after:0pt)),
artifact-evaluation(evaluation),
[], align(center,text(9pt,weight:"regular",fill:black)[Artifact Evaluation]),
)
]
} else {
heading-layout(it,
before:rhythm.cover-heading-before.at(calc.min(it.level - 1,1)),
after:rhythm.cover-heading-after)
}
}
body
}
#let omit-link-underline(it) = it.body
#let repository-footer-link() = context {
show underline: omit-link-underline
show link: set text(fill:link-color,weight:"regular")
let body=text(font:"Inter",size:9pt,weight:"regular",fill:link-color)[https://github.com/retroSoC/retroSoC]
link("https://github.com/retroSoC/retroSoC",body)
}
#let contents(depth:5) = {
set text(size:9.5pt,fill:ink)
show link: set text(fill:ink)
show underline: omit-link-underline
set par(leading:rhythm.toc-leading,spacing:rhythm.toc-leading)
set outline(indent:rhythm.toc-indent)
show outline.entry: it => context {
let peers = query(heading).filter(h=>h.outlined and h.level==it.level and h.numbering!=none)
let widths = peers.map(h=>measure(text(weight:if it.level==1 {"bold"} else {"regular"},
numbering(h.numbering,..counter(heading).at(h.location())))).width)
let prefix-width = calc.max(0pt,..widths)
let prefix = if it.prefix()==none {none} else if it.level==1 {strong(it.prefix())} else {it.prefix()}
// Scope emphasis by the section ancestor, not by a hard-coded chapter number.
let ancestors = query(selector(heading).before(it.element.location(),inclusive:true)).filter(h=>h.level<=2)
let peripheral = ancestors.len()>0 and ancestors.last().body==[Peripherals]
let inner = if it.level==1 {
[#strong(it.body())#h(0.5em)#box(width:1fr,it.fill)#h(0.5em)#it.page()]
} else if peripheral {
// Keep parentheses, separators and qualifiers such as "partial" regular.
show regex("\\([^()]*\\)"): group => {
show regex("\\b[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*\\b"): strong
group
}
it.inner()
} else {it.inner()}
block(above:rhythm.toc-leading + if it.level==1 {rhythm.toc-group-before} else {0pt},
below:rhythm.toc-leading + if it.level==1 {rhythm.toc-group-after} else {0pt},breakable:false)[
#link(it.element.location(),it.indented(
if prefix==none {none} else {box(width:prefix-width,prefix)},inner,gap:rhythm.heading-gap))
]
}
outline(title:[Contents],depth:depth)
}
#let figure-directory(tables:false) = context {
set text(font:"Inter",size:9.5pt,fill:ink,weight:"regular")
show link: set text(fill:ink)
show underline: omit-link-underline
set par(leading:rhythm.toc-leading,spacing:rhythm.toc-leading)
set outline(indent:0pt)
show outline: set heading(bookmarked:true)
// Include every numbered non-table figure, including native vector diagrams.
// Continuation notices and unnumbered register layouts are not figure entries.
let included(f) = (f.kind==table)==tables and f.numbering!=none and f.caption!=none
let prefix(entry) = [#(entry.prefix()).]
let peers = query(figure).filter(included)
let widths = peers.map(f=>measure(prefix(outline.entry(1,f))).width)
let prefix-width = calc.max(0pt,..widths)
show outline.entry: it => {
if included(it.element) {
block(above:rhythm.toc-leading,below:rhythm.toc-leading,breakable:false)[
#link(it.element.location(),it.indented(
box(width:prefix-width,prefix(it)),it.inner(),gap:rhythm.heading-gap))
]
}
}
outline(title:if tables {[List of tables]} else {[List of figures]},target:figure)
}
#let inline-code(body) = context {
// Link scopes already select link-color; keep that semantic cue and underline.
set text(fill:if text.fill==link-color {link-color} else {inline-code-color})
body
}
#let code(value) = inline-code(text(font: mono, size: 9pt, weight:"regular",
value.replace("_", "_\u{200b}").replace(".", ".\u{200b}")))
#let source(path, title: "Interface and implementation reference", line:none) = {
// Publication metadata can postdate the frozen hardware commit. Route readers
// to the delivered manifest instead of constructing a nonexistent old blob.
if path.starts-with("publications/") {
return link(<publication-provenance>,text(size:9pt,title))
}
let managed = data.managed_sources.find(s=>path.starts-with(s.destination + "/"))
let url = if managed == none {
"https://github.com/retroSoC/retroSoC/blob/" + doc.source_revision + "/" + path
} else {
managed.url.trim(".git", at:end) + "/blob/" + managed.revision + "/" + path.slice(managed.destination.len()+1)
}
if line!=none { url += "#L"+str(line) }
link(url, text(size: 9pt, title))
}
#let source-note(path, title:"Interface and implementation reference", line:none) = block(
above:rhythm.metadata-before,below:rhythm.metadata-after,breakable:false)[
#set par(leading:rhythm.small-leading,spacing:rhythm.small-spacing)
#source(path,title:title,line:line)
]
#let note(body, title: "Integration note", below: rhythm.note-space) = block(
width: 100%, inset: rhythm.note-inset, fill: pale-gold, stroke: (left: 2pt + gold),
above: rhythm.note-space, below: below, breakable: false,
)[#text(weight: "semibold", title) #linebreak() #body]
#let tbd(body) = note(body, title: "TBD - specification not available")
#let placeholder(title, height: 35mm) = block(
width: 100%, height: height, stroke: 0.7pt + rule, fill: gray,
inset: rhythm.note-inset, above: rhythm.note-space, below: rhythm.note-space, breakable: false,
)[#align(center + horizon)[#text(weight: "semibold", title) \
#text(9pt, fill: muted)[TBD - awaiting reviewed engineering data]]]
// Frame each row at its outer edges without adding internal vertical rules.
// Repeated header rows supply the top edge on every continued table segment.
#let table-rules(columns, header-row:0) = (x,y) => (
top: if y==header-row {table-stroke} else {none},
bottom: table-stroke,
left: if x==0 {table-stroke} else {none},
right: if x==columns - 1 {table-stroke} else {none},
)
#let continuation-notice() = layout(size => {
let body = block(width:100%,above:0pt,below:0pt)[
#set text(size:rhythm.continuation-size,weight:"regular",fill:black,
top-edge:"ascender",bottom-edge:"descender")
#set par(leading:4pt,spacing:0pt)
(continued)
]
let extent = measure(body,width:size.width)
block(width:100%,above:rhythm.table-caption,below:0pt,
inset:(bottom:rhythm.continuation-gap))[
#block(width:extent.width,height:extent.height,above:0pt,below:0pt,body)<table-continuation-region>
]
})
#let ds-table(key, caption, headers, rows, widths: auto, notes: none) = {
let anchor = label("start-" + key)
figure(
kind: table, supplement: [Table], caption: caption,
block(breakable: rows.len() > 8, width: 100%)[
#metadata(none)#anchor
#set text(size: 9pt)
#set par(leading: rhythm.small-leading, spacing: rhythm.small-spacing)
#table(
columns: if widths == auto { headers.len() } else { widths },
inset: rhythm.table-inset,
stroke: table-rules(headers.len(),header-row:1),
fill: (x, y) => if y == 1 { gray } else { none },
align: left + horizon,
table.header(repeat: true,
table.cell(colspan: headers.len(), inset: 0pt,
stroke:(top:none,bottom:table-stroke,left:none,right:none),fill: white)[
#context if here().page() > query(anchor).first().location().page() {
continuation-notice()
}
],
..headers.map(h => table.cell(fill: gray, text(weight: "semibold", h))),
),
..rows.map(row => row.map(v => table.cell(breakable: false, v))).flatten(),
)
#if notes != none { block(above: rhythm.table-notes, breakable: false, text(9pt, fill: muted, notes)) }
],
)
}
#let ip(id) = {
let entry = data.catalog.find(e => e.id == id)
let windows = data.regions.filter(r => r.symbol in entry.regions)
let irqs = data.interrupts.filter(i => i.name in entry.irqs)
block(above: rhythm.metadata-before, below: rhythm.metadata-after, breakable: false, sticky: true)[
#entry.summary
#set text(size: 9pt)
#set par(leading: rhythm.small-leading, spacing: rhythm.small-spacing)
#table(columns: (1.3fr, 1fr, 0.7fr), inset: rhythm.table-inset,
stroke: table-rules(3),
table.header(..([Address window],[Base address],[Size]).map(h=>
table.cell(fill:gray,text(weight:"semibold",h)))),
..windows.map(r => (code(r.symbol), code(r.base_hex), r.size_label)).flatten(),
)
#block(above: rhythm.metadata-before)[
*LP IRQ:* #if irqs.len() == 0 { [No dedicated source.] } else {
irqs.map(i => str(i.core_bit) + " (" + i.name.replace("_", " ") + ")").join("; ")
}
]
#for (n, p) in entry.sources.enumerate() {
source(p, title: if n == 0 { "Detailed contract / source" } else { "Additional source" })
if n < entry.sources.len() - 1 { [ · ] }
}
]
}
#let template(body) = {
set document(title: doc.title, author: doc.author,
keywords: ("retroSoC", "Mini", "Gen2", "Gen2+", "datasheet", doc.document_id, "v"+doc.version, doc.status))
set text(font: "Inter", size: 10.5pt, fill: ink, lang: "en", weight: "regular")
show strong: it => text(weight:"bold",it.body)
set par(justify: false, leading: rhythm.body-leading, spacing: rhythm.body-spacing,first-line-indent:0pt)
set list(indent:rhythm.list-indent,body-indent:rhythm.list-body-indent,spacing:rhythm.list-spacing)
set enum(indent:rhythm.list-indent,body-indent:rhythm.list-body-indent,spacing:rhythm.list-spacing)
set page(paper: "a4", margin: (x: 19mm, top: 21mm, bottom: 20mm),
header: context {
let hs = query(heading.where(level: 1)).filter(h => h.location().page() <= here().page())
// Heading locations avoid the extra layout pass needed by page-valued metadata.
// A later chapter/section heading closes the preceding IP's header scope.
let navigation=query(selector(heading.where(level:1)).or(heading.where(level:2),
<multimedia-overview>,
..ip-chapters.map(chapter=>label(chapter.id))))
.filter(h=>h.location().page()<=here().page())
let active=if navigation.len()==0 {none} else {
ip-chapters.find(chapter=>label(chapter.id)==navigation.last().fields().at("label",default:none))
}
set text(size: 9pt, fill: muted)
grid(columns: (1fr, 1fr), doc.title, align(right)[
#if active!=none { upper(active.id) } else if hs.len() > 0 { hs.last().body } else { [Product datasheet] }
])
v(rhythm.header-gap)
line(length: 100%, stroke: 0.6pt + gold)
},
footer: context {
set text(size: 9pt, fill: muted)
set par(leading:0pt,spacing:0pt)
line(length: 100%, stroke: 0.35pt + rule)
v(rhythm.header-gap)
grid(columns: (1fr, auto, 1fr),
repository-footer-link(),
[#counter(page).display() / #counter(page).final().first()],
align(right)[#doc.date],
)
},
)
set heading(numbering: "1.1.1.1.1", outlined: true)
show heading: heading-layout
show raw: set text(font: mono, size: 9pt, weight:"regular")
show raw.where(block:false): inline-code
show link: set text(fill:link-color)
show link: it => underline(stroke:0.4pt + link-color,offset:2pt,it)
show ref: it => {
if it.element!=none and it.element.func()==heading and it.form=="normal" {
set text(fill:link-color)
show underline: omit-link-underline
it
} else {it}
}
set figure(gap: rhythm.figure-caption)
show figure: set block(above:rhythm.figure-space,below:rhythm.figure-space,breakable:false)
set figure.caption(position:top)
show figure.where(kind: table): set figure(gap:rhythm.table-caption)
show figure.where(kind: table): set block(breakable: true)
show figure.caption: set text(size: 9pt, fill: black)
show figure.caption: it => block(above:0pt,below:0pt,sticky:it.position==top,it)
body
}