Greasy Fork is available in English.
Swagger 站点工具脚本 💪 | 保存浏览历史 🕘 | 显示收藏夹 ⭐️ | 点击 path 快速定位 🎯 | 快速复制 API path 🔗
// ==UserScript==// @name swagger-toolkit// @namespace https://github.com/SublimeCT/greasy_monkey_scripts// @version 1.2.0// @description Swagger 站点工具脚本 💪 | 保存浏览历史 🕘 | 显示收藏夹 ⭐️ | 点击 path 快速定位 🎯 | 快速复制 API path 🔗// @description:en Swagger Toolkit Script 💪 | save history in sidebar 🕘 | has favorites list in sidebar ⭐️ | click path(in sidebar) to jump 🎯 | copy(hover API) API path 🔗// @note v1.0.1 增加当前页是不是 swagger 构建的文档判断; 自动展开所有 tag, 以定位到对应的 API;// @note v1.1.0 增加复制 API path 功能// @note v1.1.1 fix: 修复增加历史记录时将 toolkit-btn-group 内容一起加进去的问题// @note v1.2.0 feat: 增加多语言(英语)支持// @author Sven// @icon https://static1.smartbear.co/swagger/media/assets/swagger_fav.png// @match *://*/docs/index.html// @match *://*/docs/api/index.html// @match https://petstore.swagger.io// @grant none// ==/UserScript==; (() => {// @require file:///Users/test/projects/greasy_monkey_scripts/swagger_toolkit.jsconst TIMES = 30let current = 0let isLoaded = falseconst interval = setInterval(() => {if (++current >= TIMES) {clearInterval(interval)return}const item = document.querySelector('.opblock-tag')const swaggerAPI = window.SwaggerUIBundleif (!item || !swaggerAPI) returnif (!isLoaded) {// 首先展开所有 tag, 否则无法定位const notOpenTags = document.querySelectorAll('.opblock-tag[data-is-open=false]') || []for (const tag of Array.from(notOpenTags)) {tag.click()}// 增加监听事件const wrapper = document.querySelector('.swagger-ui')wrapper.addEventListener('click', evt => {// 点击接口标题时在当前 URL 中加入锚点const linkTitleDom = evt.target.closest('.opblock-summary')if (linkTitleDom) {const linkDom = linkTitleDom.parentNodeconst isOpen = !linkDom.classList.contains('is-open')const hash = isOpen ? linkDom.id : ''if (hash) location.hash = hashreturn}// 点击接口中的 Model 时同步展开下方数据结构const modelLinkDom = evt.target.closest('ul.tab')if (modelLinkDom && evt.target.innerText.trim() === 'Model') {setTimeout(() => {const icons = modelLinkDom.nextElementSibling.querySelectorAll('.model-toggle.collapsed')if (icons.length) icons[icons.length - 1].click()}, 300)return}})if (location.hash) {observeHash()window.addEventListener('hashchange', observeHash)}isLoaded = truereturn}}, 300);const observeHash = evt => {const linkedDom = document.getElementById(location.hash.length > 0 ? location.hash.substr(1) : '')if (linkedDom) {const isOpen = linkedDom.classList.contains('is-open')linkedDom.scrollIntoView()if (!isOpen) linkedDom.querySelector('.opblock-summary').click()console.log('scroll into view: ', linkedDom, linkedDom.querySelector('.opblock-summary'))}}class Sheets {static sheets = `body {--row-width: 13vw;--row-min-width: 245px;--row-title-font-size: 14px;--body-wrapper-width: 80vw;--body-wrapper-margin-right: 3vw;--body-wrapper-min-width: 800px;--body-btn-group-width: 20px;}/* 应用于 Copy input */.toolkit-hidden { width: 1; height: 1; }/* 接口信息部分样式 */#swagger-ui .opblock .toolkit-path-btn-group { margin-left: 10px; display: none; }#swagger-ui .opblock:hover .toolkit-path-btn-group { display: block; }#swagger-ui .opblock .toolkit-path-btn-group a { text-decoration: none; }/* 页面内容主体布局 */#swagger-ui div.topbar { display: flex; justify-content: flex-end; }#swagger-ui div.topbar .wrapper { margin: 0; width: var(--body-wrapper-width); min-width: var(--body-wrapper-min-width); margin-right: var(--body-wrapper-margin-right) }#swagger-ui div.swagger-ui { display: flex; justify-content: flex-end; }#swagger-ui div.swagger-ui .wrapper { margin: 0; width: var(--body-wrapper-width); min-width: var(--body-wrapper-min-width); margin-right: var(--body-wrapper-margin-right) }/* sidebar part */#swagger-toolkit-sidebar {width: var(--row-width);min-width: var(--row-min-width);display: flex;position: fixed;top: 0;left: 0;height: 100vh;flex-direction: column;justify-content: space-between;background-color: #FAFAFA;border-right: 1px solid #c4d6d6;}#swagger-toolkit-sidebar .list { width: 100%; }#swagger-toolkit-sidebar .list > header { font-size: 18px; background-color: #999; }#swagger-toolkit-sidebar .list > header > .title { color: #FFF; text-align: center; font-weight: 200; }#swagger-toolkit-sidebar .row { display: flex; padding-bottom: 5px; width: 100%; cursor: pointer; text-decoration: none; }#swagger-toolkit-sidebar .row.method-DELETE { background-color: rgba(249,62,62,.1); }#swagger-toolkit-sidebar .row.method-DELETE:hover { background-color: rgba(249,62,62,.5); }#swagger-toolkit-sidebar .row.method-GET { background-color: rgba(97,175,254,.1); }#swagger-toolkit-sidebar .row.method-GET:hover { background-color: rgba(97,175,254,.5); }#swagger-toolkit-sidebar .row.method-POST { background-color: rgba(73,204,144,.1); }#swagger-toolkit-sidebar .row.method-POST:hover { background-color: rgba(73,204,144,.5); }#swagger-toolkit-sidebar .row.method-PUT { background-color: rgba(252,161,48,.1); }#swagger-toolkit-sidebar .row.method-PUT:hover { background-color: rgba(252,161,48,.5); }#swagger-toolkit-sidebar .row.method-PATCH { background-color: rgba(80,227,194,.1); }#swagger-toolkit-sidebar .row.method-PATCH:hover { background-color: rgba(80,227,194,.5); }#swagger-toolkit-sidebar .row .description { color: #333; font-size: 14px; width: calc(var(--row-width) - var(--body-btn-group-width)); min-width: calc(var(--row-min-width) - var(--body-btn-group-width)); }#swagger-toolkit-sidebar .row .method { display: flex; line-height: 45px; min-width: 64px; }#swagger-toolkit-sidebar .row .path > a { color: #409EFF; }#swagger-toolkit-sidebar .row .btn-group { font-size: 12px; }#swagger-toolkit-sidebar .row .btn-group > a { text-decoration: none; display: block; }#swagger-toolkit-sidebar .row .btn-group > a:hover { font-size: 14px; }/* helper */.tool-text-size-fixed { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }`static inject() {const sheet = document.createTextNode(Sheets.sheets)const el = document.createElement('style')el.id = 'swagger-toolkit-sheets'el.appendChild(sheet)document.getElementsByTagName('head')[0].appendChild(el)}}class LinkStore {key = ''path = ''method = ''description = '' // 接口名id = ''createdat = 0static MAX_LENGTH = 10static save(row, key) {const store = new LinkStore()store.id = row.idstore.key = keystore.method = row.querySelector('.opblock-summary-method').innerTextstore.path = row.querySelector('.opblock-summary-path > a').innerTextstore.description = row.querySelector('.opblock-summary-description').innerTextLinkStore.add(key, store)}static add(key, store, filterRepeat) {let data = LinkStore.getStore(key)if (filterRepeat) {for (const row of data) {if (row.id === store.id && store.path === store.path) return false}}data.unshift(store)if (data.length > LinkStore.MAX_LENGTH) data = data.slice(0, LinkStore.MAX_LENGTH)localStorage.setItem(key, JSON.stringify(data))}static remove(key, index) {let data = LinkStore.getStore(key)data.splice(index, 1)localStorage.setItem(key, JSON.stringify(data))}static getStore(key) {let store = []try {const _store = localStorage.getItem(key)if (_store) store = JSON.parse(_store)} catch (err) {console.error(err)}return store}}class Pane {dom = nulllocalKey = nulltitle = nullplaceholder = '暂无数据'placeholder_en = 'no data'btnSave = '收藏'btnSave_en = 'add to favorites'btnRemove = '删除'btnRemove_en = 'remove'enableMarkBtn = false/*** 生成或更新当前 Pane* @description 将生成 `.list>(header>.title)+(a.row>(.method+.contents>(.description+a.path)))`*/generateDom(isUpdate) {if (isUpdate) this.dom.innerHTML = ''const list = isUpdate ? this.dom : document.createElement('div')list.classList.add('list')list.classList.add(this.localKey)list.setAttribute('data-key', this.localKey)// 添加 headerconst header = document.createElement('header')const title = document.createElement('div')title.classList.add('title')title.innerText = this.getLabelByLanguage('title')list.appendChild(header)header.appendChild(title)// 添加数据const data = LinkStore.getStore(this.localKey)for (const dataRow of data) {const row = document.createElement('a')row.href = '#' + dataRow.idrow.setAttribute('data-row', JSON.stringify(dataRow))const method = document.createElement('div')method.innerText = dataRow.methodconst contents = document.createElement('div')const description = document.createElement('div')description.innerText = dataRow.descriptionconst path = document.createElement('div')const pathLink = document.createElement('a')pathLink.innerText = dataRow.pathpathLink.href = '#' + dataRow.idconst btnGroup = document.createElement('div')const markBtn = document.createElement('a')if (this.enableMarkBtn) {markBtn.href = 'javascript:;'markBtn.setAttribute('title', this.getLabelByLanguage('btnSave'))markBtn.innerText = '⭐️'}const deleteBtn = document.createElement('a')deleteBtn.href = 'javascript:;'deleteBtn.setAttribute('title', this.getLabelByLanguage('btnRemove'))deleteBtn.innerText = '✖️'row.classList.add('row')row.classList.add('method-' + dataRow.method)method.classList.add('method')contents.classList.add('contents')description.classList.add('description')description.classList.add('tool-text-size-fixed')path.classList.add('path')btnGroup.classList.add('btn-group')if (this.enableMarkBtn) markBtn.classList.add('btn-mark')deleteBtn.classList.add('btn-delete')path.appendChild(pathLink)contents.appendChild(description)contents.appendChild(path)// row.appendChild(method)row.appendChild(contents)row.appendChild(btnGroup)btnGroup.appendChild(deleteBtn)if (this.enableMarkBtn) btnGroup.appendChild(markBtn)list.appendChild(row)}if (data.length === 0) list.appendChild(this.getPlaceholderDom())this.dom = listif (typeof this.afterGenerageDom === 'function') this.afterGenerageDom()return list}getPlaceholderDom() {const dom = document.createElement('section')dom.innerText = this.getLabelByLanguage('placeholder')return dom}getLabelByLanguage(field, language) {let lang = languageif (!lang) {const _lang = navigator.languagelang = _lang.indexOf('zh') === 0 ? '' : 'en'}return this[`${field}${lang ? ('_' + lang) : '' }`]}}class HistoryPane extends Pane {localKey = 'swagger-toolkit-history'title = '浏览历史'title_en = 'History'placeholder = '暂无浏览历史数据'placeholder_en = 'No history at present'enableMarkBtn = true}class MarkPane extends Pane {localKey = 'swagger-toolkit-mark'title = '收藏夹'title_en = 'Favorites'placeholder = '暂无收藏数据, 点击 ⭐️ 按钮添加'placeholder_en = 'No favorite data, click ⭐️ button to add'afterGenerageDom() {this.dom}}class SideBar {static dom = nullstatic panes = []static pathBtnGroupClassName = 'toolkit-path-btn-group'static copyInput = document.createElement('input')initCopyDOM() {SideBar.copyInput.classList.add('toolkit-hidden')document.body.appendChild(SideBar.copyInput)return this}addListeners() {window.addEventListener('hashchange', () => {const _path = location.hash.length > 0 ? location.hash.substr(1) : ''if (!_path) returnconst row = document.getElementById(_path) || (document.querySelector(`a[href="#${_path}"]`) && document.querySelector(`a[href="#${_path}"]`).closest('.opblock'))if (row) LinkStore.save(row, 'swagger-toolkit-history')this._updatePane('swagger-toolkit-history')})document.querySelector('#swagger-ui').addEventListener('mouseover', evt => {this._showPathBtnGroup(evt) // 显示在 path 栏中的按钮组})return this}_showPathBtnGroup(evt) {const opblock = evt.target.closest('.opblock')if (!opblock) returnthis._appendPathBtnGroupDOM(opblock)}_appendPathBtnGroupDOM(opblock) {if (opblock.querySelector('.' + SideBar.pathBtnGroupClassName)) returnconst group = document.createElement('div')const copyBtn = document.createElement('a')group.classList.add(SideBar.pathBtnGroupClassName)copyBtn.setAttribute('href', 'javascript:;')copyBtn.classList.add('btn-copy')copyBtn.innerText = '🔗'copyBtn.setAttribute('title', 'copy')group.appendChild(copyBtn)copyBtn.addEventListener('click', evt => {this._copyPath(evt)})const pathDOM = opblock.querySelector('.opblock-summary-path')if (pathDOM) pathDOM.appendChild(group)}_copyPath(evt) {evt.stopPropagation()const pathDOM = evt.target.closest('.opblock-summary-path')if (!pathDOM) returnconst pathLink = pathDOM.querySelector('a')if (!pathLink) returnconst path = pathLink.innerTextSideBar.copyInput.value = pathSideBar.copyInput.select()document.execCommand('Copy')console.log('copy successfuly')}generateDom() {const sidebar = document.createElement('sidebar')sidebar.id = 'swagger-toolkit-sidebar'SideBar.dom = sidebarreturn this}inject() {document.body.appendChild(SideBar.dom)return this}appendPanes() {for (const pane of SideBar.panes) {SideBar.dom.appendChild(pane.generateDom())}return this}_updatePane(key) {for (const pane of SideBar.panes) {if (pane.localKey !== key) continuepane.generateDom(true)}}appendPanesListeners() {SideBar.dom.addEventListener('click', evt => {if (evt.target.classList.contains('btn-delete')) {evt.preventDefault()evt.stopPropagation()const index = this._getRowIndex({ btnItem: evt.target })const key = evt.target.parentNode.parentNode.parentNode.getAttribute('data-key')LinkStore.remove(key, index)this._updatePane(key)} else if (evt.target.classList.contains('btn-mark')) {evt.preventDefault()evt.stopPropagation()const row = evt.target.parentNode.parentNode.getAttribute('data-row')LinkStore.add('swagger-toolkit-mark', JSON.parse(row), true)this._updatePane('swagger-toolkit-mark')}})}_getRowIndex({ btnItem }) {const listDom = Array.from(btnItem.parentNode.parentNode.parentNode.children)for (let index = listDom.length; index--;) {if (listDom[index] === btnItem.parentNode.parentNode) return index - 1}return -1}}Sheets.inject()SideBar.panes.push(new HistoryPane())SideBar.panes.push(new MarkPane())window.$$_SideBar = new SideBar()window.$$_SideBar.initCopyDOM().addListeners().generateDom().appendPanes().inject().appendPanesListeners()})();