Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ docs/
src/**/*.test.ts
eslint.config.mjs
.prettierrc
.gitignore
.gitignore
.claude/
CLAUDE.md
297 changes: 293 additions & 4 deletions docs/examples/redoc-dark.html

Large diffs are not rendered by default.

297 changes: 293 additions & 4 deletions docs/examples/redoc-light.html

Large diffs are not rendered by default.

291 changes: 289 additions & 2 deletions docs/examples/stoplight-dark.html

Large diffs are not rendered by default.

291 changes: 289 additions & 2 deletions docs/examples/stoplight-light.html

Large diffs are not rendered by default.

309 changes: 305 additions & 4 deletions docs/examples/swagger-dark.html

Large diffs are not rendered by default.

309 changes: 305 additions & 4 deletions docs/examples/swagger-light.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default [
...globals.jest,
SwaggerUIBundle: 'readonly',
Redoc: 'readonly',
autoComplete: 'readonly',
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openapi-generate-html",
"version": "0.4.4",
"version": "0.5.0",
"description": "Generate standalone HTML from OpenAPI Specification",
"type": "module",
"bin": {
Expand Down
118 changes: 118 additions & 0 deletions resources/common/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#search-button {
position: fixed;
top: 28px;
right: 28px;
z-index: 10;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 4px;
border: 1px solid #d9d9d9;
padding: 4px 12px 4px 8px;
cursor: pointer;

.icon {
font-size: 18px;
margin-right: 4px;
}
}

#search-dialog {
position: fixed;
top: 10px;
margin: 0 auto;
height: 100%;
background: transparent;
border: none;

.auto-complete-list {
max-height: 265px;
}
}

#auto-complete {
background-color: #fff;
}

.auto-complete-list {
.auto-complete-list-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
border-radius: 0;
}
}

.method-tag {
display: inline-block;
padding: 2px 0;
border-radius: 4px;
font-size: 11px;
color: #fff;
min-width: 54px;
text-align: center;
flex-shrink: 0;
}

.endpoint-info {
display: flex;
flex-direction: column;
flex: 1;
}

.endpoint-path {
font-size: 13px;
}

.endpoint-summary {
font-size: 11px;
color: #666;
}

[data-theme=dark] {
#search-dialog::backdrop {
background-color: rgba(255, 255, 255, 0.1);
}

#search-button {
background-color: #1f1f1f;
border: 1px solid #444;
color: #fff;
}

#auto-complete {
background-color: #1f1f1f;
border: 1px solid #444;
color: #fff;
}

.auto-complete-list {
background-color: #1f1f1f;
border: 1px solid #444;

.auto-complete-list-item {
background-color: #1f1f1f;
border-bottom: 1px solid #444;
color: #fff;

&:hover {
background-color: #333;
}
}

.auto-complete-list-item[aria-selected="true"] {
background-color: #333;
}
}

.endpoint-path {
color: #ccc;
}

.endpoint-summary {
color: #888;
}
}
93 changes: 93 additions & 0 deletions resources/common/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// eslint-disable-next-line no-unused-vars
class SearchDialog {
dialog
autocomplete

constructor(dialogId, inputId, openapi) {
this.dialog = document.getElementById(dialogId)
if (!this.dialog) {
throw new Error(`Dialog with id "${dialogId}" not found`)
}

this.autocomplete = new autoComplete({
selector: `#${inputId}`,
placeHolder: 'Search endpoints',
data: {
src: this.filterEndpoints(openapi),
keys: ['method', 'path', 'summary'],
},
searchEngine: 'loose',
threshold: 0,
resultsList: {
maxResults: undefined,
class: 'auto-complete-list',
element: (list, data) => {
if (data.results.length > 0) {
list.innerHTML = ''
data.results.forEach((item) => {
const li = document.createElement('li')
li.className = 'auto-complete-list-item'
li.innerHTML = `
<div class="method-tag method-${item.value.method.toLowerCase()}">${item.value.method}</div>
<div class="endpoint-info">
<div class="endpoint-path">${item.value.path}</div>
<div class="endpoint-summary">${item.value.summary}</div>
</div>
`
list.appendChild(li)
})
}
},
},
})

this.dialog.addEventListener('click', (event) => {
if (event.target === this.dialog) {
this.hide()
}
})
}

on(event, callback) {
if (event === 'select') {
this.autocomplete.input.addEventListener('selection', (event) => {
callback(event.detail.selection.value)
})
} else {
throw new Error(`Unsupported event: ${event}`)
}
}

show() {
this.autocomplete.input.value = ''
this.autocomplete.start()
this.dialog.showModal()
}

hide() {
this.dialog.close()
}

filterEndpoints(openapi) {
const endpoints = []
const paths = openapi.paths
for (const path in paths) {
const methods = Object.keys(paths[path])
for (const method of methods) {
if (method === 'parameters') {
continue
}
endpoints.push({
method: method.toUpperCase(),
path,
operationId: paths[path][method].operationId,
tag: paths[path][method].tags
? paths[path][method].tags[0]
: 'default',
summary: paths[path][method].summary || '',
})
}
}
return endpoints
}
}
32 changes: 32 additions & 0 deletions resources/redoc/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,36 @@ body {
border: none !important;
background-color: #3f3f46 !important;
}
}

.method-get {
background-color: rgb(47, 129, 50);
}

.method-post {
background-color: rgb(24, 111, 175);
}

.method-put {
background-color: rgb(149, 80, 124);
}

.method-patch {
background-color: rgb(149, 80, 124);
}

.method-delete {
background-color: rgb(204, 51, 51);
}

.method-head {
background-color: rgb(162, 61, 173);
}

.method-options {
background-color: rgb(148, 112, 20);
}

.method-trace {
background-color: #000;
}
38 changes: 37 additions & 1 deletion resources/redoc/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,46 @@ const redark = {
},
}

let option = {}
let option = {
disableSearch: true,
}
if (document.documentElement.getAttribute('data-theme') === 'dark') {
option = { theme: redark }
}

// ref. https://redocly.com/docs/redoc/deployment/html#the-redoc-object
Redoc.init(window.apiDocs, option, document.getElementById('redoc-container'))

// ref. https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform#examples
const modifierKeyPrefix =
navigator.platform.startsWith('Mac') || navigator.platform === 'iPhone'
? '⌘' // command key
: '^' // control key

const searchButton = document.querySelector('#search-button')
searchButton.addEventListener('click', () => searchDialog.show())
searchButton.querySelector('.text').textContent =
`Search ${modifierKeyPrefix}+K`

// eslint-disable-next-line no-undef
const searchDialog = new SearchDialog(
'search-dialog',
'auto-complete',
window.apiDocs,
)
searchDialog.on('select', (selection) => {
const tag = selection.tag
const operationId = selection.operationId
const url = new URL(location.href)
url.hash = `#tag/${tag}/operation/${operationId}`
location.href = url.toString()
searchDialog.hide()
})

document.addEventListener('keydown', function (e) {
const isCmdOrCtrl = modifierKeyPrefix === '⌘' ? e.metaKey : e.ctrlKey
if (isCmdOrCtrl && e.key.toLowerCase() === 'k') {
searchDialog.show()
e.preventDefault()
}
})
15 changes: 13 additions & 2 deletions resources/redoc/template.ejs
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
<!DOCTYPE html>
<html lang="ja" data-theme="<%= theme %>">
<html data-theme="<%= theme %>">
<head>
<title><%= title %></title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="<%= description %>">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=search" />
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.9/dist/css/autoComplete.02.min.css">
<script src="https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.9/dist/autoComplete.min.js"></script>
<style><%- cssCommonContent %></style>
<style><%- cssContent %></style>
<script>window.apiDocs = <%- apiDocs %></script>
</head>
<body>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script>
<button id="search-button">
<span class="icon material-symbols-outlined">search</span>
<span class="text"></span>
</button>
<dialog id="search-dialog">
<input id="auto-complete" autocomplete="off">
</dialog>
<div id="redoc-container"></div>
<script><%- jsCommonContent %></script>
<script><%- jsContent %></script>
</body>
</html>
Loading
Loading