feat: write template

This commit is contained in:
u80864958 committed 2026-09-18 16:36:52 +02:00
commit a0a6d2cf84
98 files changed
+6609

No files matched your search

+279
View File
@@ -0,0 +1,279 @@
// oasviz.typ
//
// oasviz is a Typst library designed for visualizing OpenAPI Specification (OAS) documents.
// It provides a suite of functions to render API endpoints, schemas, servers, and other
// components in a structured and aesthetically pleasing format, ideal for technical
// documentation and API reports.
//
#import "i18n.typ": translate
#import "../util/tabler.typ"
#import "../pallete.typ": palette
// Helper to render a badge for HTTP methods
#let method-badge(method) = {
let lightness = "600"
let color = if method == "get" { palette.blue.at(lightness) } else if method == "post" {
palette.green.at(lightness)
} else if method == "put" {
palette.orange.at(lightness)
} else if method == "delete" { palette.red.at(lightness) } else if method == "patch" {
palette.purple.at(lightness)
} else { palette.gray.at(lightness) }
box(
fill: color,
stroke: none,
radius: 2pt,
inset: (x: 0.4em, y: 0.4em),
text(weight: "bold", fill: white, size: 0.9em, upper(method)),
)
}
// Render Servers
#let render-servers(servers) = {
if servers != none and servers.len() > 0 [
= #translate("servers.title")
#translate("servers.desc")
#figure(
table(
columns: (auto, 1fr),
stroke: none,
align: left,
translate("servers.table.addr"),
translate("servers.table.desc"),
table.hline(stroke: 1pt + palette.ui.border),
..(
for server in servers {
(
raw(server.url),
if "description" in server { server.description } else { "-" },
)
}
),
),
caption: translate("servers.caption"),
)
]
}
#let _get-type-display(val) = {
if val == none { return translate("common.any") }
if "$ref" in val {
return raw(val.at("$ref").split("/").last())
}
if "type" in val {
if val.type == "array" and "items" in val {
return [#translate("endpoints.array-of") #_get-type-display(val.items)]
}
return val.type
}
return translate("common.any")
}
#let _resolve-all-properties(schema, all-schemas, seen: ()) = {
let props = (:)
if "properties" in schema {
props = props + schema.properties
}
if "allOf" in schema {
for element in schema.allOf {
if "$ref" in element {
let ref-name = element.at("$ref").split("/").last()
if ref-name not in seen {
let ref-schema = all-schemas.at(ref-name, default: (:))
props = props + _resolve-all-properties(ref-schema, all-schemas, seen: seen + (ref-name,))
}
} else if "properties" in element {
props = props + element.properties
} else if "allOf" in element {
props = props + _resolve-all-properties(element, all-schemas, seen: seen)
}
}
}
return props
}
// Render a single Schema/Type
#let render-schema(name, schema, all-schemas: (:)) = [
=== #name #if "type" in schema {
[ (#schema.type)]
}
#let props = _resolve-all-properties(schema, all-schemas)
#if props.len() > 0 {
figure(
table(
columns: (auto, auto, 1fr),
align: (x, y) => left,
table.header(
[*#translate("schemas.property")*], [*#translate("schemas.type")*], [*#translate("schemas.desc-ex")*]
),
..props
.pairs()
.map(((key, val)) => {
(
raw(key),
_get-type-display(val),
{
if "description" in val { val.description + " " }
if "example" in val {
text(fill: palette.ui.text-light, [#translate("common.example"): #val.example])
}
},
)
})
.flatten(),
),
caption: [#translate("schemas.caption") #name],
)
} else {
sym.dash
}
]
// Render Components/Schemas
#let render-components(components) = {
if components != none and "schemas" in components {
[
= #translate("schemas.title")
#context if translate("schemas.desc") != none {
par(translate("schemas.desc"))
}
]
let schemas = components.schemas
for (name, schema) in schemas.pairs() {
render-schema(name, schema, all-schemas: schemas)
}
}
}
#let _resolve-ref(val, pool) = {
if type(val) == dictionary and "$ref" in val {
let ref-name = val.at("$ref").split("/").last()
return pool.at(ref-name, default: val)
}
return val
}
// Render a single Operation (Endpoint)
#let render-operation(method, path, op, components: (:)) = [
#figure(
tabler.sideways(
columns: (auto, auto, auto, auto, 1fr),
translate("endpoints.endpoint"),
table.cell(colspan: 4, strong(path)),
translate("endpoints.method"),
table.cell(colspan: 4, method-badge(method)),
..if "summary" in op {
(translate("endpoints.summary"), table.cell(colspan: 4, op.summary))
},
..if "description" in op {
(translate("endpoints.description"), table.cell(colspan: 4, op.description))
},
..if "parameters" in op and op.parameters.len() > 0 {
(
table.cell(rowspan: op.parameters.len(), translate("endpoints.parameters")),
..for p-raw in op.parameters {
let p = _resolve-ref(p-raw, components.at("parameters", default: (:)))
(
p.name,
p.at("in", default: "query"),
if "schema" in p { _get-type-display(p.schema) } else { "-" },
p.at("description", default: ""),
)
},
)
},
..if "requestBody" in op {
let rb = _resolve-ref(op.requestBody, components.at("requestBodies", default: (:)))
(
translate("endpoints.request-body"),
..for (media-type, details) in rb.at("content", default: (:)).pairs() {
(
table.cell(
colspan: 2,
raw(media-type),
),
table.cell(colspan: 2, if "schema" in details {
[#translate("endpoints.type-prefix"): #_get-type-display(details.schema)]
} else {
"-"
}),
)
},
)
},
..if "responses" in op {
(
table.cell(rowspan: op.responses.len(), translate("endpoints.responses")),
..op
.responses
.pairs()
.map(((code, resp-raw)) => {
let resp = _resolve-ref(resp-raw, components.at("responses", default: (:)))
let schema-display = []
if "content" in resp {
let content-map = resp.content
let media-type = if "application/json" in content-map { "application/json" } else {
content-map.keys().at(0, default: none)
}
if media-type != none {
let media-obj = content-map.at(media-type)
if "schema" in media-obj {
schema-display = [#translate("endpoints.type-prefix"): #_get-type-display(media-obj.schema)]
}
}
}
(
strong(code),
table.cell(colspan: 3, [
#resp.at("description", default: "")
#if schema-display != [] [ \ #text(fill: palette.ui.text-light, schema-display) ]
]),
)
})
.flatten(),
)
},
),
caption: [#translate("endpoints.caption") #upper(method) #path],
)
]
// Main Render Function
#let render(data) = {
show figure.where(kind: table): set block(breakable: false)
let components = data.at("components", default: (:))
// Servers
if "servers" in data {
render-servers(data.servers)
}
// Paths
if "paths" in data {
[
= #translate("endpoints.title")
#context if translate("endpoints.desc") != none {
par(translate("endpoints.desc"))
}
]
for (path, item) in data.paths.pairs() {
let methods = ("get", "post", "put", "delete", "patch", "head", "options", "trace")
for method in methods {
if method in item {
render-operation(method, path, item.at(method), components: components)
}
}
}
}
// Components
if "components" in data {
render-components(data.components)
}
}