feat: write template
This commit is contained in:
commit
a0a6d2cf84
98 files changed
+6609
No files matched your search
@@ -0,0 +1,49 @@
|
||||
# oasviz - OpenAPI Specification Visualizer
|
||||
|
||||
`oasviz` is a Typst library designed for visualizing OpenAPI Specification (OAS) documents. It provides a suite of components to render API endpoints, data schemas, and server configurations in a structured and aesthetically pleasing format.
|
||||
|
||||
This library was significantly refactored and optimized with the assistance of the **Gemini CLI Development Agent**.
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
- **Style Inheritance**: The library is built to be "style-neutral." It does not impose its own font sizes, internal padding, or block margins. Instead, it is designed to inherit these settings from the global `set` rules of the host document (e.g., `lib.typ`).
|
||||
- **Swiss Minimalism**: Follows a Swiss-inspired minimalist aesthetic, utilizing square shapes (`radius: 0pt`), clean borders, and a high-contrast layout.
|
||||
- **Project Integration**: Deeply integrated with the project's core utilities:
|
||||
- `../pallete.typ`: Uses the centralized palette for UI elements and semantic HTTP method colors.
|
||||
- `../util/tabler.typ`: Utilizes specialized table layouts for operation documentation.
|
||||
|
||||
## Core Features
|
||||
|
||||
### 1. Localization (i18n)
|
||||
|
||||
The library uses a dedicated `translate` function supporting English and German. It employs a flat dot-notation key structure for clarity and maintainability. Recent updates have expanded the key set to ensure full localization of all UI elements, including captions and schema details.
|
||||
|
||||
**Key Improvements:**
|
||||
|
||||
- **Expanded Abbreviations**: "Ex" is now "Example" (EN) / "Beispiel" (DE), and "Ref" is "Uses schema" (EN) / "Verwendet Schema" (DE).
|
||||
- **Full Schema Localization**: Terms like "Type", "Array of", and "any" are now fully translated.
|
||||
- **Consistent Naming**: All keys follow a strict dot-notation pattern (e.g., `endpoints.uses-schema`, `common.example`).
|
||||
|
||||
**Example Keys:**
|
||||
|
||||
- `translate("endpoints.title")`
|
||||
- `translate("servers.table.addr")`
|
||||
- `translate("common.example")`
|
||||
|
||||
### 2. Semantic Method Badges
|
||||
|
||||
HTTP methods are visualized using flat, color-coded badges that prioritize readability while maintaining the minimalist design language.
|
||||
|
||||
### 3. Schema Rendering
|
||||
|
||||
Automatically renders complex JSON Schema definitions into clean, readable tables, complete with property types, descriptions, and examples.
|
||||
|
||||
## Usage
|
||||
|
||||
The primary entry point is the `render` function, which accepts a Typst dictionary representing an OpenAPI document:
|
||||
|
||||
```typst
|
||||
#import "oasviz/oasviz.typ": render
|
||||
#let data = yaml("openapi.yaml")
|
||||
#render(data)
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
// Localization Helper
|
||||
#let translate(key) = {
|
||||
context {
|
||||
let lang = text.lang
|
||||
let en = (
|
||||
"servers.title": "Servers",
|
||||
"servers.desc": "This section lists the available servers used to access the API.",
|
||||
"servers.caption": "Server List",
|
||||
"servers.table.addr": "Address",
|
||||
"servers.table.desc": "Description",
|
||||
"schemas.title": "Schemas",
|
||||
"schemas.desc": "This section defines the data structures sent and received by the API.",
|
||||
"schemas.caption": "Schema",
|
||||
"schemas.property": "Property",
|
||||
"schemas.type": "Type",
|
||||
"schemas.desc-ex": "Description/Example",
|
||||
"endpoints.title": "Endpoints",
|
||||
"endpoints.desc": "This section describes the available commands (endpoints) that can be sent to the API.",
|
||||
"endpoints.endpoint": "Endpoint",
|
||||
"endpoints.method": "HTTP Method",
|
||||
"endpoints.summary": "Summary",
|
||||
"endpoints.description": "Description",
|
||||
"endpoints.parameters": "Parameters:",
|
||||
"endpoints.request-body": "Request Body:",
|
||||
"endpoints.responses": "Responses:",
|
||||
"endpoints.uses-schema": "Uses schema ",
|
||||
"endpoints.array-of": "Array of ",
|
||||
"endpoints.type-prefix": "Type",
|
||||
"endpoints.caption": "Endpoint",
|
||||
"common.example": "Example",
|
||||
"common.any": "any",
|
||||
)
|
||||
let de = (
|
||||
"servers.title": "Server",
|
||||
"servers.desc": "In diesem Abschnitt werden die verfügbaren Server aufgelistet, über die die API erreicht werden kann.",
|
||||
"servers.caption": "Serverliste",
|
||||
"servers.table.addr": "Adresse",
|
||||
"servers.table.desc": "Beschreibung",
|
||||
"schemas.title": "Datenschemata",
|
||||
"schemas.desc": "Hier sind die Datenstrukturen definiert, die von der API gesendet und empfangen werden.",
|
||||
"schemas.caption": "Schema",
|
||||
"schemas.property": "Eigenschaft",
|
||||
"schemas.type": "Typ",
|
||||
"schemas.desc-ex": "Beschreibung/Beispiel",
|
||||
"endpoints.title": "Endpunkte",
|
||||
"endpoints.desc": "In diesem Abschnitt werden die verfügbaren Befehle (Endpunkte) beschrieben, die an die API gesendet werden können.",
|
||||
"endpoints.endpoint": "Endpunkt",
|
||||
"endpoints.method": "HTTP-Methode",
|
||||
"endpoints.summary": "Zusammenfassung",
|
||||
"endpoints.description": "Beschreibung",
|
||||
"endpoints.parameters": [Parameter: \ (Name, Ort, Typ, \ Beschreibung)],
|
||||
"endpoints.request-body": "Anfrage-Daten:",
|
||||
"endpoints.responses": [Antworten: \ (Statuscode, Daten) ],
|
||||
"endpoints.uses-schema": "Verwendet Schema ",
|
||||
"endpoints.array-of": "Liste von ",
|
||||
"endpoints.type-prefix": "Typ",
|
||||
"endpoints.caption": "Endpunkt",
|
||||
"common.example": "Beispiel",
|
||||
"common.any": "beliebig",
|
||||
)
|
||||
|
||||
let dict = if lang == "de" { de } else { en }
|
||||
let val = dict.at(key, default: key)
|
||||
|
||||
if val != none {
|
||||
val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// oasviz - OpenAPI Specification Visualizer for Typst
|
||||
//
|
||||
// This package provides tools to render OpenAPI Specification (OAS) documents
|
||||
// directly within Typst.
|
||||
//
|
||||
#import "./oasviz.typ": render
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user