Remove old docs
12
docs/.gitignore
vendored
@ -1,12 +0,0 @@
|
|||||||
pids
|
|
||||||
logs
|
|
||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
coverage/
|
|
||||||
run
|
|
||||||
dist
|
|
||||||
.DS_Store
|
|
||||||
.nyc_output
|
|
||||||
.basement
|
|
||||||
config.local.js
|
|
||||||
basement_dist
|
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hetty-docs",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "An HTTP toolkit for security research.",
|
|
||||||
"main": "index.js",
|
|
||||||
"authors": {
|
|
||||||
"name": "David Stotijn",
|
|
||||||
"email": "dstotijn@gmail.com"
|
|
||||||
},
|
|
||||||
"repository": "github.com/dstotijn/hetty/docs",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vuepress dev src",
|
|
||||||
"build": "vuepress build src"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"markdown-it-imsize": "^2.0.1",
|
|
||||||
"vuepress": "^1.5.3"
|
|
||||||
},
|
|
||||||
"dependencies": {}
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
const { description } = require("../../package");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
port: 3000,
|
|
||||||
title: "Hetty",
|
|
||||||
description: description,
|
|
||||||
head: [
|
|
||||||
["meta", { name: "theme-color", content: "#30e3b7" }],
|
|
||||||
["meta", { name: "apple-mobile-web-app-capable", content: "yes" }],
|
|
||||||
[
|
|
||||||
"meta",
|
|
||||||
{ name: "apple-mobile-web-app-status-bar-style", content: "black" },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"meta",
|
|
||||||
{
|
|
||||||
property: "og:title",
|
|
||||||
content: "Hetty",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"meta",
|
|
||||||
{
|
|
||||||
property: "og:description",
|
|
||||||
content: "An HTTP toolkit for security research.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"meta",
|
|
||||||
{
|
|
||||||
property: "og:image",
|
|
||||||
content: "https://hetty.xyz/assets/hetty_v0.2.0_header.png",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
themeConfig: {
|
|
||||||
repo: "dstotijn/hetty",
|
|
||||||
editLinks: true,
|
|
||||||
docsDir: "docs/src",
|
|
||||||
editLinkText: "",
|
|
||||||
lastUpdated: true,
|
|
||||||
logo: "/assets/logo.png",
|
|
||||||
nav: [
|
|
||||||
{
|
|
||||||
text: "Guide",
|
|
||||||
link: "/guide/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Appendix",
|
|
||||||
link: "/appendix/",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
sidebar: {
|
|
||||||
"/guide/": [
|
|
||||||
{
|
|
||||||
title: "Guide",
|
|
||||||
collapsable: false,
|
|
||||||
children: ["", "getting-started", "modules"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"/appendix/": [
|
|
||||||
{
|
|
||||||
title: "Appendix",
|
|
||||||
collapsable: false,
|
|
||||||
children: [""],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: ["@vuepress/plugin-back-to-top", "@vuepress/plugin-medium-zoom"],
|
|
||||||
markdown: {
|
|
||||||
toc: { includeLevel: [2] },
|
|
||||||
extendMarkdown: (md) => {
|
|
||||||
md.use(require("markdown-it-imsize"));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* Client app enhancement file.
|
|
||||||
*
|
|
||||||
* https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default ({
|
|
||||||
Vue, // the version of Vue being used in the VuePress app
|
|
||||||
options, // the options for the root Vue instance
|
|
||||||
router, // the router instance for the app
|
|
||||||
siteData // site metadata
|
|
||||||
}) => {
|
|
||||||
// ...apply enhancements for the site.
|
|
||||||
}
|
|
Before Width: | Height: | Size: 144 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 12 KiB |
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Custom Styles here.
|
|
||||||
*
|
|
||||||
* ref:https://v1.vuepress.vuejs.org/config/#index-styl
|
|
||||||
*/
|
|
||||||
|
|
||||||
.home .hero img
|
|
||||||
width 450px
|
|
||||||
max-width 100%!important
|
|
@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Custom palette here.
|
|
||||||
*
|
|
||||||
* ref:https://v1.vuepress.vuejs.org/zh/config/#palette-styl
|
|
||||||
*/
|
|
||||||
|
|
||||||
$accentColor = #2CC09B
|
|
||||||
$textColor = #2c3e50
|
|
||||||
$borderColor = #eaecef
|
|
||||||
$codeBgColor = #282c34
|
|
||||||
$badgeTipColor = #2CC09B
|
|
@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2018-present, Yuxi (Evan) You
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
@ -1,171 +0,0 @@
|
|||||||
<template>
|
|
||||||
<form
|
|
||||||
id="search-form"
|
|
||||||
class="algolia-search-wrapper search-box"
|
|
||||||
role="search"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="algolia-search-input"
|
|
||||||
class="search-query"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
>
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'AlgoliaSearchBox',
|
|
||||||
|
|
||||||
props: ['options'],
|
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
placeholder: undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
$lang (newValue) {
|
|
||||||
this.update(this.options, newValue)
|
|
||||||
},
|
|
||||||
|
|
||||||
options (newValue) {
|
|
||||||
this.update(newValue, this.$lang)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted () {
|
|
||||||
this.initialize(this.options, this.$lang)
|
|
||||||
this.placeholder = this.$site.themeConfig.searchPlaceholder || ''
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
initialize (userOptions, lang) {
|
|
||||||
Promise.all([
|
|
||||||
import(/* webpackChunkName: "docsearch" */ 'docsearch.js/dist/cdn/docsearch.min.js'),
|
|
||||||
import(/* webpackChunkName: "docsearch" */ 'docsearch.js/dist/cdn/docsearch.min.css')
|
|
||||||
]).then(([docsearch]) => {
|
|
||||||
docsearch = docsearch.default
|
|
||||||
const { algoliaOptions = {}} = userOptions
|
|
||||||
docsearch(Object.assign(
|
|
||||||
{},
|
|
||||||
userOptions,
|
|
||||||
{
|
|
||||||
inputSelector: '#algolia-search-input',
|
|
||||||
// #697 Make docsearch work well at i18n mode.
|
|
||||||
algoliaOptions: Object.assign({
|
|
||||||
'facetFilters': [`lang:${lang}`].concat(algoliaOptions.facetFilters || [])
|
|
||||||
}, algoliaOptions),
|
|
||||||
handleSelected: (input, event, suggestion) => {
|
|
||||||
const { pathname, hash } = new URL(suggestion.url)
|
|
||||||
const routepath = pathname.replace(this.$site.base, '/')
|
|
||||||
const _hash = decodeURIComponent(hash)
|
|
||||||
this.$router.push(`${routepath}${_hash}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
update (options, lang) {
|
|
||||||
this.$el.innerHTML = '<input id="algolia-search-input" class="search-query">'
|
|
||||||
this.initialize(options, lang)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.algolia-search-wrapper
|
|
||||||
& > span
|
|
||||||
vertical-align middle
|
|
||||||
.algolia-autocomplete
|
|
||||||
line-height normal
|
|
||||||
.ds-dropdown-menu
|
|
||||||
background-color #fff
|
|
||||||
border 1px solid #999
|
|
||||||
border-radius 4px
|
|
||||||
font-size 16px
|
|
||||||
margin 6px 0 0
|
|
||||||
padding 4px
|
|
||||||
text-align left
|
|
||||||
&:before
|
|
||||||
border-color #999
|
|
||||||
[class*=ds-dataset-]
|
|
||||||
border none
|
|
||||||
padding 0
|
|
||||||
.ds-suggestions
|
|
||||||
margin-top 0
|
|
||||||
.ds-suggestion
|
|
||||||
border-bottom 1px solid $borderColor
|
|
||||||
.algolia-docsearch-suggestion--highlight
|
|
||||||
color #2c815b
|
|
||||||
.algolia-docsearch-suggestion
|
|
||||||
border-color $borderColor
|
|
||||||
padding 0
|
|
||||||
.algolia-docsearch-suggestion--category-header
|
|
||||||
padding 5px 10px
|
|
||||||
margin-top 0
|
|
||||||
background $accentColor
|
|
||||||
color #fff
|
|
||||||
font-weight 600
|
|
||||||
.algolia-docsearch-suggestion--highlight
|
|
||||||
background rgba(255, 255, 255, 0.6)
|
|
||||||
.algolia-docsearch-suggestion--wrapper
|
|
||||||
padding 0
|
|
||||||
.algolia-docsearch-suggestion--title
|
|
||||||
font-weight 600
|
|
||||||
margin-bottom 0
|
|
||||||
color $textColor
|
|
||||||
.algolia-docsearch-suggestion--subcategory-column
|
|
||||||
vertical-align top
|
|
||||||
padding 5px 7px 5px 5px
|
|
||||||
border-color $borderColor
|
|
||||||
background #f1f3f5
|
|
||||||
&:after
|
|
||||||
display none
|
|
||||||
.algolia-docsearch-suggestion--subcategory-column-text
|
|
||||||
color #555
|
|
||||||
.algolia-docsearch-footer
|
|
||||||
border-color $borderColor
|
|
||||||
.ds-cursor .algolia-docsearch-suggestion--content
|
|
||||||
background-color #e7edf3 !important
|
|
||||||
color $textColor
|
|
||||||
|
|
||||||
@media (min-width: $MQMobile)
|
|
||||||
.algolia-search-wrapper
|
|
||||||
.algolia-autocomplete
|
|
||||||
.algolia-docsearch-suggestion
|
|
||||||
.algolia-docsearch-suggestion--subcategory-column
|
|
||||||
float none
|
|
||||||
width 150px
|
|
||||||
min-width 150px
|
|
||||||
display table-cell
|
|
||||||
.algolia-docsearch-suggestion--content
|
|
||||||
float none
|
|
||||||
display table-cell
|
|
||||||
width 100%
|
|
||||||
vertical-align top
|
|
||||||
.ds-dropdown-menu
|
|
||||||
min-width 515px !important
|
|
||||||
|
|
||||||
@media (max-width: $MQMobile)
|
|
||||||
.algolia-search-wrapper
|
|
||||||
.ds-dropdown-menu
|
|
||||||
min-width calc(100vw - 4rem) !important
|
|
||||||
max-width calc(100vw - 4rem) !important
|
|
||||||
.algolia-docsearch-suggestion--wrapper
|
|
||||||
padding 5px 7px 5px 5px !important
|
|
||||||
.algolia-docsearch-suggestion--subcategory-column
|
|
||||||
padding 0 !important
|
|
||||||
background white !important
|
|
||||||
.algolia-docsearch-suggestion--subcategory-column-text:after
|
|
||||||
content " > "
|
|
||||||
font-size 10px
|
|
||||||
line-height 14.4px
|
|
||||||
display inline-block
|
|
||||||
width 5px
|
|
||||||
margin -3px 3px 0
|
|
||||||
vertical-align middle
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,252 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="dropdown-wrapper"
|
|
||||||
:class="{ open }"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="dropdown-title"
|
|
||||||
type="button"
|
|
||||||
:aria-label="dropdownAriaLabel"
|
|
||||||
@click="handleDropdown"
|
|
||||||
>
|
|
||||||
<span class="title">{{ item.text }}</span>
|
|
||||||
<span
|
|
||||||
class="arrow down"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="mobile-dropdown-title"
|
|
||||||
type="button"
|
|
||||||
:aria-label="dropdownAriaLabel"
|
|
||||||
@click="setOpen(!open)"
|
|
||||||
>
|
|
||||||
<span class="title">{{ item.text }}</span>
|
|
||||||
<span
|
|
||||||
class="arrow"
|
|
||||||
:class="open ? 'down' : 'right'"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<DropdownTransition>
|
|
||||||
<ul
|
|
||||||
v-show="open"
|
|
||||||
class="nav-dropdown"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(subItem, index) in item.items"
|
|
||||||
:key="subItem.link || index"
|
|
||||||
class="dropdown-item"
|
|
||||||
>
|
|
||||||
<h4 v-if="subItem.type === 'links'">
|
|
||||||
{{ subItem.text }}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
v-if="subItem.type === 'links'"
|
|
||||||
class="dropdown-subitem-wrapper"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="childSubItem in subItem.items"
|
|
||||||
:key="childSubItem.link"
|
|
||||||
class="dropdown-subitem"
|
|
||||||
>
|
|
||||||
<NavLink
|
|
||||||
:item="childSubItem"
|
|
||||||
@focusout="
|
|
||||||
isLastItemOfArray(childSubItem, subItem.items) &&
|
|
||||||
isLastItemOfArray(subItem, item.items) &&
|
|
||||||
setOpen(false)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<NavLink
|
|
||||||
v-else
|
|
||||||
:item="subItem"
|
|
||||||
@focusout="isLastItemOfArray(subItem, item.items) && setOpen(false)"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</DropdownTransition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import NavLink from '@theme/components/NavLink.vue'
|
|
||||||
import DropdownTransition from '@theme/components/DropdownTransition.vue'
|
|
||||||
import last from 'lodash/last'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'DropdownLink',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
NavLink,
|
|
||||||
DropdownTransition
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
|
||||||
item: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
open: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
dropdownAriaLabel () {
|
|
||||||
return this.item.ariaLabel || this.item.text
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
$route () {
|
|
||||||
this.open = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
setOpen (value) {
|
|
||||||
this.open = value
|
|
||||||
},
|
|
||||||
|
|
||||||
isLastItemOfArray (item, array) {
|
|
||||||
return last(array) === item
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the dropdown when user tab and click from keyboard.
|
|
||||||
*
|
|
||||||
* Use event.detail to detect tab and click from keyboard. Ref: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
|
|
||||||
* The Tab + Click is UIEvent > KeyboardEvent, so the detail is 0.
|
|
||||||
*/
|
|
||||||
handleDropdown () {
|
|
||||||
const isTriggerByTab = event.detail === 0
|
|
||||||
if (isTriggerByTab) this.setOpen(!this.open)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.dropdown-wrapper
|
|
||||||
cursor pointer
|
|
||||||
.dropdown-title
|
|
||||||
display block
|
|
||||||
font-size 0.9rem
|
|
||||||
font-family inherit
|
|
||||||
cursor inherit
|
|
||||||
padding inherit
|
|
||||||
line-height 1.4rem
|
|
||||||
background transparent
|
|
||||||
border none
|
|
||||||
font-weight 500
|
|
||||||
color $textColor
|
|
||||||
&:hover
|
|
||||||
border-color transparent
|
|
||||||
.arrow
|
|
||||||
vertical-align middle
|
|
||||||
margin-top -1px
|
|
||||||
margin-left 0.4rem
|
|
||||||
.mobile-dropdown-title
|
|
||||||
@extends .dropdown-title
|
|
||||||
display none
|
|
||||||
font-weight 600
|
|
||||||
font-size inherit
|
|
||||||
&:hover
|
|
||||||
color $accentColor
|
|
||||||
.nav-dropdown
|
|
||||||
.dropdown-item
|
|
||||||
color inherit
|
|
||||||
line-height 1.7rem
|
|
||||||
h4
|
|
||||||
margin 0.45rem 0 0
|
|
||||||
border-top 1px solid #eee
|
|
||||||
padding 1rem 1.5rem 0.45rem 1.25rem
|
|
||||||
.dropdown-subitem-wrapper
|
|
||||||
padding 0
|
|
||||||
list-style none
|
|
||||||
.dropdown-subitem
|
|
||||||
font-size 0.9em
|
|
||||||
a
|
|
||||||
display block
|
|
||||||
line-height 1.7rem
|
|
||||||
position relative
|
|
||||||
border-bottom none
|
|
||||||
font-weight 400
|
|
||||||
margin-bottom 0
|
|
||||||
padding 0 1.5rem 0 1.25rem
|
|
||||||
&:hover
|
|
||||||
color $accentColor
|
|
||||||
&.router-link-active
|
|
||||||
color $accentColor
|
|
||||||
&::after
|
|
||||||
content ""
|
|
||||||
width 0
|
|
||||||
height 0
|
|
||||||
border-left 5px solid $accentColor
|
|
||||||
border-top 3px solid transparent
|
|
||||||
border-bottom 3px solid transparent
|
|
||||||
position absolute
|
|
||||||
top calc(50% - 2px)
|
|
||||||
left 9px
|
|
||||||
&:first-child h4
|
|
||||||
margin-top 0
|
|
||||||
padding-top 0
|
|
||||||
border-top 0
|
|
||||||
|
|
||||||
@media (max-width: $MQMobile)
|
|
||||||
.dropdown-wrapper
|
|
||||||
&.open .dropdown-title
|
|
||||||
margin-bottom 0.5rem
|
|
||||||
.dropdown-title
|
|
||||||
display: none
|
|
||||||
.mobile-dropdown-title
|
|
||||||
display: block
|
|
||||||
.nav-dropdown
|
|
||||||
transition height .1s ease-out
|
|
||||||
overflow hidden
|
|
||||||
.dropdown-item
|
|
||||||
h4
|
|
||||||
border-top 0
|
|
||||||
margin-top 0
|
|
||||||
padding-top 0
|
|
||||||
h4, & > a
|
|
||||||
font-size 15px
|
|
||||||
line-height 2rem
|
|
||||||
.dropdown-subitem
|
|
||||||
font-size 14px
|
|
||||||
padding-left 1rem
|
|
||||||
|
|
||||||
@media (min-width: $MQMobile)
|
|
||||||
.dropdown-wrapper
|
|
||||||
height 1.8rem
|
|
||||||
&:hover .nav-dropdown,
|
|
||||||
&.open .nav-dropdown
|
|
||||||
// override the inline style.
|
|
||||||
display block !important
|
|
||||||
&.open:blur
|
|
||||||
display none
|
|
||||||
.nav-dropdown
|
|
||||||
display none
|
|
||||||
// Avoid height shaked by clicking
|
|
||||||
height auto !important
|
|
||||||
box-sizing border-box;
|
|
||||||
max-height calc(100vh - 2.7rem)
|
|
||||||
overflow-y auto
|
|
||||||
position absolute
|
|
||||||
top 100%
|
|
||||||
right 0
|
|
||||||
background-color #fff
|
|
||||||
padding 0.6rem 0
|
|
||||||
border 1px solid #ddd
|
|
||||||
border-bottom-color #ccc
|
|
||||||
text-align left
|
|
||||||
border-radius 0.25rem
|
|
||||||
white-space nowrap
|
|
||||||
margin 0
|
|
||||||
</style>
|
|
@ -1,33 +0,0 @@
|
|||||||
<template>
|
|
||||||
<transition
|
|
||||||
name="dropdown"
|
|
||||||
@enter="setHeight"
|
|
||||||
@after-enter="unsetHeight"
|
|
||||||
@before-leave="setHeight"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'DropdownTransition',
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
setHeight (items) {
|
|
||||||
// explicitly set height so that it can be transitioned
|
|
||||||
items.style.height = items.scrollHeight + 'px'
|
|
||||||
},
|
|
||||||
|
|
||||||
unsetHeight (items) {
|
|
||||||
items.style.height = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.dropdown-enter, .dropdown-leave-to
|
|
||||||
height 0 !important
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,197 +0,0 @@
|
|||||||
<template>
|
|
||||||
<main
|
|
||||||
class="home"
|
|
||||||
:aria-labelledby="data.heroText !== null ? 'main-title' : null"
|
|
||||||
>
|
|
||||||
<header class="hero">
|
|
||||||
<h1 v-if="data.heroImage" id="main-title">
|
|
||||||
<img :src="$withBase(data.heroImage)" :alt="data.heroAlt || 'hero'" />
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p v-if="data.tagline !== null" class="description">
|
|
||||||
{{ data.tagline || $description || "Welcome to your VuePress site" }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-if="data.actionText && data.actionLink" class="action">
|
|
||||||
<NavLink class="action-button" :item="actionLink" />
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div v-if="data.features && data.features.length" class="features">
|
|
||||||
<div
|
|
||||||
v-for="(feature, index) in data.features"
|
|
||||||
:key="index"
|
|
||||||
class="feature"
|
|
||||||
>
|
|
||||||
<h2>{{ feature.title }}</h2>
|
|
||||||
<p>{{ feature.details }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Content class="theme-default-content custom" />
|
|
||||||
|
|
||||||
<div v-if="data.footer" class="footer">
|
|
||||||
{{ data.footer }}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import NavLink from "@theme/components/NavLink.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "Home",
|
|
||||||
|
|
||||||
components: { NavLink },
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
data() {
|
|
||||||
return this.$page.frontmatter;
|
|
||||||
},
|
|
||||||
|
|
||||||
actionLink() {
|
|
||||||
return {
|
|
||||||
link: this.data.actionLink,
|
|
||||||
text: this.data.actionText,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.home {
|
|
||||||
padding: $navbarHeight 2rem 0;
|
|
||||||
max-width: $homePageWidth;
|
|
||||||
margin: 0px auto;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 280px;
|
|
||||||
display: block;
|
|
||||||
margin: 3rem auto 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, .description, .action {
|
|
||||||
margin: 1.8rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
max-width: 35rem;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
color: lighten($textColor, 40%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #fff;
|
|
||||||
background-color: $accentColor;
|
|
||||||
padding: 0.8rem 1.6rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background-color 0.1s ease;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-bottom: 1px solid darken($accentColor, 10%);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: lighten($accentColor, 10%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.features {
|
|
||||||
border-top: 1px solid $borderColor;
|
|
||||||
padding: 1.2rem 0;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: flex-start;
|
|
||||||
align-content: stretch;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 30%;
|
|
||||||
max-width: 30%;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
font-weight: 500;
|
|
||||||
border-bottom: none;
|
|
||||||
padding-bottom: 0;
|
|
||||||
color: lighten($textColor, 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: lighten($textColor, 25%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
padding: 2.5rem;
|
|
||||||
border-top: 1px solid $borderColor;
|
|
||||||
text-align: center;
|
|
||||||
color: lighten($textColor, 25%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $MQMobile) {
|
|
||||||
.home {
|
|
||||||
.features {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature {
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 0 2.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $MQMobileNarrow) {
|
|
||||||
.home {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
padding-right: 1.5rem;
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
img {
|
|
||||||
max-height: 210px;
|
|
||||||
margin: 2rem auto 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, .description, .action {
|
|
||||||
margin: 1.2rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 0.6rem 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature {
|
|
||||||
h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,90 +0,0 @@
|
|||||||
<template>
|
|
||||||
<RouterLink
|
|
||||||
v-if="isInternal"
|
|
||||||
class="nav-link"
|
|
||||||
:to="link"
|
|
||||||
:exact="exact"
|
|
||||||
@focusout.native="focusoutAction"
|
|
||||||
>
|
|
||||||
{{ item.text }}
|
|
||||||
</RouterLink>
|
|
||||||
<a
|
|
||||||
v-else
|
|
||||||
:href="link"
|
|
||||||
class="nav-link external"
|
|
||||||
:target="target"
|
|
||||||
:rel="rel"
|
|
||||||
@focusout="focusoutAction"
|
|
||||||
>
|
|
||||||
{{ item.text }}
|
|
||||||
<OutboundLink v-if="isBlankTarget" />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { isExternal, isMailto, isTel, ensureExt } from '../util'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'NavLink',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
item: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
link () {
|
|
||||||
return ensureExt(this.item.link)
|
|
||||||
},
|
|
||||||
|
|
||||||
exact () {
|
|
||||||
if (this.$site.locales) {
|
|
||||||
return Object.keys(this.$site.locales).some(rootLink => rootLink === this.link)
|
|
||||||
}
|
|
||||||
return this.link === '/'
|
|
||||||
},
|
|
||||||
|
|
||||||
isNonHttpURI () {
|
|
||||||
return isMailto(this.link) || isTel(this.link)
|
|
||||||
},
|
|
||||||
|
|
||||||
isBlankTarget () {
|
|
||||||
return this.target === '_blank'
|
|
||||||
},
|
|
||||||
|
|
||||||
isInternal () {
|
|
||||||
return !isExternal(this.link) && !this.isBlankTarget
|
|
||||||
},
|
|
||||||
|
|
||||||
target () {
|
|
||||||
if (this.isNonHttpURI) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (this.item.target) {
|
|
||||||
return this.item.target
|
|
||||||
}
|
|
||||||
return isExternal(this.link) ? '_blank' : ''
|
|
||||||
},
|
|
||||||
|
|
||||||
rel () {
|
|
||||||
if (this.isNonHttpURI) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (this.item.rel === false) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (this.item.rel) {
|
|
||||||
return this.item.rel
|
|
||||||
}
|
|
||||||
return this.isBlankTarget ? 'noopener noreferrer' : null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
focusoutAction () {
|
|
||||||
this.$emit('focusout')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,156 +0,0 @@
|
|||||||
<template>
|
|
||||||
<nav
|
|
||||||
v-if="userLinks.length || repoLink"
|
|
||||||
class="nav-links"
|
|
||||||
>
|
|
||||||
<!-- user links -->
|
|
||||||
<div
|
|
||||||
v-for="item in userLinks"
|
|
||||||
:key="item.link"
|
|
||||||
class="nav-item"
|
|
||||||
>
|
|
||||||
<DropdownLink
|
|
||||||
v-if="item.type === 'links'"
|
|
||||||
:item="item"
|
|
||||||
/>
|
|
||||||
<NavLink
|
|
||||||
v-else
|
|
||||||
:item="item"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- repo link -->
|
|
||||||
<a
|
|
||||||
v-if="repoLink"
|
|
||||||
:href="repoLink"
|
|
||||||
class="repo-link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{{ repoLabel }}
|
|
||||||
<OutboundLink />
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import DropdownLink from '@theme/components/DropdownLink.vue'
|
|
||||||
import { resolveNavLinkItem } from '../util'
|
|
||||||
import NavLink from '@theme/components/NavLink.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'NavLinks',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
NavLink,
|
|
||||||
DropdownLink
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
userNav () {
|
|
||||||
return this.$themeLocaleConfig.nav || this.$site.themeConfig.nav || []
|
|
||||||
},
|
|
||||||
|
|
||||||
nav () {
|
|
||||||
const { locales } = this.$site
|
|
||||||
if (locales && Object.keys(locales).length > 1) {
|
|
||||||
const currentLink = this.$page.path
|
|
||||||
const routes = this.$router.options.routes
|
|
||||||
const themeLocales = this.$site.themeConfig.locales || {}
|
|
||||||
const languageDropdown = {
|
|
||||||
text: this.$themeLocaleConfig.selectText || 'Languages',
|
|
||||||
ariaLabel: this.$themeLocaleConfig.ariaLabel || 'Select language',
|
|
||||||
items: Object.keys(locales).map(path => {
|
|
||||||
const locale = locales[path]
|
|
||||||
const text = themeLocales[path] && themeLocales[path].label || locale.lang
|
|
||||||
let link
|
|
||||||
// Stay on the current page
|
|
||||||
if (locale.lang === this.$lang) {
|
|
||||||
link = currentLink
|
|
||||||
} else {
|
|
||||||
// Try to stay on the same page
|
|
||||||
link = currentLink.replace(this.$localeConfig.path, path)
|
|
||||||
// fallback to homepage
|
|
||||||
if (!routes.some(route => route.path === link)) {
|
|
||||||
link = path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { text, link }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return [...this.userNav, languageDropdown]
|
|
||||||
}
|
|
||||||
return this.userNav
|
|
||||||
},
|
|
||||||
|
|
||||||
userLinks () {
|
|
||||||
return (this.nav || []).map(link => {
|
|
||||||
return Object.assign(resolveNavLinkItem(link), {
|
|
||||||
items: (link.items || []).map(resolveNavLinkItem)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
repoLink () {
|
|
||||||
const { repo } = this.$site.themeConfig
|
|
||||||
if (repo) {
|
|
||||||
return /^https?:/.test(repo)
|
|
||||||
? repo
|
|
||||||
: `https://github.com/${repo}`
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
repoLabel () {
|
|
||||||
if (!this.repoLink) return
|
|
||||||
if (this.$site.themeConfig.repoLabel) {
|
|
||||||
return this.$site.themeConfig.repoLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
const repoHost = this.repoLink.match(/^https?:\/\/[^/]+/)[0]
|
|
||||||
const platforms = ['GitHub', 'GitLab', 'Bitbucket']
|
|
||||||
for (let i = 0; i < platforms.length; i++) {
|
|
||||||
const platform = platforms[i]
|
|
||||||
if (new RegExp(platform, 'i').test(repoHost)) {
|
|
||||||
return platform
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Source'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.nav-links
|
|
||||||
display inline-block
|
|
||||||
a
|
|
||||||
line-height 1.4rem
|
|
||||||
color inherit
|
|
||||||
&:hover, &.router-link-active
|
|
||||||
color $accentColor
|
|
||||||
.nav-item
|
|
||||||
position relative
|
|
||||||
display inline-block
|
|
||||||
margin-left 1.5rem
|
|
||||||
line-height 2rem
|
|
||||||
&:first-child
|
|
||||||
margin-left 0
|
|
||||||
.repo-link
|
|
||||||
margin-left 1.5rem
|
|
||||||
|
|
||||||
@media (max-width: $MQMobile)
|
|
||||||
.nav-links
|
|
||||||
.nav-item, .repo-link
|
|
||||||
margin-left 0
|
|
||||||
|
|
||||||
@media (min-width: $MQMobile)
|
|
||||||
.nav-links a
|
|
||||||
&:hover, &.router-link-active
|
|
||||||
color $textColor
|
|
||||||
.nav-item > a:not(.external)
|
|
||||||
&:hover, &.router-link-active
|
|
||||||
margin-bottom -2px
|
|
||||||
border-bottom 2px solid lighten($accentColor, 8%)
|
|
||||||
</style>
|
|
@ -1,162 +0,0 @@
|
|||||||
<template>
|
|
||||||
<header class="navbar">
|
|
||||||
<SidebarButton @toggle-sidebar="$emit('toggle-sidebar')" />
|
|
||||||
|
|
||||||
<RouterLink :to="$localePath" class="home-link">
|
|
||||||
<img
|
|
||||||
v-if="$site.themeConfig.logo"
|
|
||||||
class="logo"
|
|
||||||
:src="$withBase($site.themeConfig.logo)"
|
|
||||||
:alt="$siteTitle"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="links"
|
|
||||||
:style="
|
|
||||||
linksWrapMaxWidth
|
|
||||||
? {
|
|
||||||
'max-width': linksWrapMaxWidth + 'px',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<AlgoliaSearchBox v-if="isAlgoliaSearch" :options="algolia" />
|
|
||||||
<SearchBox
|
|
||||||
v-else-if="
|
|
||||||
$site.themeConfig.search !== false &&
|
|
||||||
$page.frontmatter.search !== false
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<NavLinks class="can-hide" />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import AlgoliaSearchBox from "@AlgoliaSearchBox";
|
|
||||||
import SearchBox from "@SearchBox";
|
|
||||||
import SidebarButton from "@theme/components/SidebarButton.vue";
|
|
||||||
import NavLinks from "@theme/components/NavLinks.vue";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "Navbar",
|
|
||||||
|
|
||||||
components: {
|
|
||||||
SidebarButton,
|
|
||||||
NavLinks,
|
|
||||||
SearchBox,
|
|
||||||
AlgoliaSearchBox,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
linksWrapMaxWidth: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
algolia() {
|
|
||||||
return (
|
|
||||||
this.$themeLocaleConfig.algolia || this.$site.themeConfig.algolia || {}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
isAlgoliaSearch() {
|
|
||||||
return this.algolia && this.algolia.apiKey && this.algolia.indexName;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
const MOBILE_DESKTOP_BREAKPOINT = 719; // refer to config.styl
|
|
||||||
const NAVBAR_VERTICAL_PADDING =
|
|
||||||
parseInt(css(this.$el, "paddingLeft")) +
|
|
||||||
parseInt(css(this.$el, "paddingRight"));
|
|
||||||
const handleLinksWrapWidth = () => {
|
|
||||||
if (document.documentElement.clientWidth < MOBILE_DESKTOP_BREAKPOINT) {
|
|
||||||
this.linksWrapMaxWidth = null;
|
|
||||||
} else {
|
|
||||||
this.linksWrapMaxWidth =
|
|
||||||
this.$el.offsetWidth -
|
|
||||||
NAVBAR_VERTICAL_PADDING -
|
|
||||||
((this.$refs.siteName && this.$refs.siteName.offsetWidth) || 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
handleLinksWrapWidth();
|
|
||||||
window.addEventListener("resize", handleLinksWrapWidth, false);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function css(el, property) {
|
|
||||||
// NOTE: Known bug, will return 'auto' if style value is 'auto'
|
|
||||||
const win = el.ownerDocument.defaultView;
|
|
||||||
// null means not to return pseudo styles
|
|
||||||
return win.getComputedStyle(el, null)[property];
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
$navbar-vertical-padding = 0.7rem;
|
|
||||||
$navbar-horizontal-padding = 1.5rem;
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
padding: $navbar-vertical-padding $navbar-horizontal-padding;
|
|
||||||
line-height: $navbarHeight - 1.4rem;
|
|
||||||
|
|
||||||
a, span, img {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: $navbarHeight - 1.4rem;
|
|
||||||
min-width: $navbarHeight - 1.4rem;
|
|
||||||
margin-right: 0.8rem;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-name {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: $textColor;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: white;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
position: absolute;
|
|
||||||
right: $navbar-horizontal-padding;
|
|
||||||
top: $navbar-vertical-padding;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $MQMobile) {
|
|
||||||
.navbar {
|
|
||||||
padding-left: 4rem;
|
|
||||||
|
|
||||||
.can-hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-name {
|
|
||||||
width: calc(100vw - 9.4rem);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,31 +0,0 @@
|
|||||||
<template>
|
|
||||||
<main class="page">
|
|
||||||
<slot name="top" />
|
|
||||||
|
|
||||||
<Content class="theme-default-content" />
|
|
||||||
<PageEdit />
|
|
||||||
|
|
||||||
<PageNav v-bind="{ sidebarItems }" />
|
|
||||||
|
|
||||||
<slot name="bottom" />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import PageEdit from '@theme/components/PageEdit.vue'
|
|
||||||
import PageNav from '@theme/components/PageNav.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { PageEdit, PageNav },
|
|
||||||
props: ['sidebarItems']
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
@require '../styles/wrapper.styl'
|
|
||||||
|
|
||||||
.page
|
|
||||||
padding-bottom 2rem
|
|
||||||
display block
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,155 +0,0 @@
|
|||||||
<template>
|
|
||||||
<footer class="page-edit">
|
|
||||||
<div
|
|
||||||
v-if="editLink"
|
|
||||||
class="edit-link"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="editLink"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>{{ editLinkText }}</a>
|
|
||||||
<OutboundLink />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="lastUpdated"
|
|
||||||
class="last-updated"
|
|
||||||
>
|
|
||||||
<span class="prefix">{{ lastUpdatedText }}:</span>
|
|
||||||
<span class="time">{{ lastUpdated }}</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import isNil from 'lodash/isNil'
|
|
||||||
import { endingSlashRE, outboundRE } from '../util'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PageEdit',
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
lastUpdated () {
|
|
||||||
return this.$page.lastUpdated
|
|
||||||
},
|
|
||||||
|
|
||||||
lastUpdatedText () {
|
|
||||||
if (typeof this.$themeLocaleConfig.lastUpdated === 'string') {
|
|
||||||
return this.$themeLocaleConfig.lastUpdated
|
|
||||||
}
|
|
||||||
if (typeof this.$site.themeConfig.lastUpdated === 'string') {
|
|
||||||
return this.$site.themeConfig.lastUpdated
|
|
||||||
}
|
|
||||||
return 'Last Updated'
|
|
||||||
},
|
|
||||||
|
|
||||||
editLink () {
|
|
||||||
const showEditLink = isNil(this.$page.frontmatter.editLink)
|
|
||||||
? this.$site.themeConfig.editLinks
|
|
||||||
: this.$page.frontmatter.editLink
|
|
||||||
|
|
||||||
const {
|
|
||||||
repo,
|
|
||||||
docsDir = '',
|
|
||||||
docsBranch = 'master',
|
|
||||||
docsRepo = repo
|
|
||||||
} = this.$site.themeConfig
|
|
||||||
|
|
||||||
if (showEditLink && docsRepo && this.$page.relativePath) {
|
|
||||||
return this.createEditLink(
|
|
||||||
repo,
|
|
||||||
docsRepo,
|
|
||||||
docsDir,
|
|
||||||
docsBranch,
|
|
||||||
this.$page.relativePath
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
|
|
||||||
editLinkText () {
|
|
||||||
return (
|
|
||||||
this.$themeLocaleConfig.editLinkText
|
|
||||||
|| this.$site.themeConfig.editLinkText
|
|
||||||
|| `Edit this page`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
createEditLink (repo, docsRepo, docsDir, docsBranch, path) {
|
|
||||||
const bitbucket = /bitbucket.org/
|
|
||||||
if (bitbucket.test(docsRepo)) {
|
|
||||||
const base = docsRepo
|
|
||||||
return (
|
|
||||||
base.replace(endingSlashRE, '')
|
|
||||||
+ `/src`
|
|
||||||
+ `/${docsBranch}/`
|
|
||||||
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
|
||||||
+ path
|
|
||||||
+ `?mode=edit&spa=0&at=${docsBranch}&fileviewer=file-view-default`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const gitlab = /gitlab.com/
|
|
||||||
if (gitlab.test(docsRepo)) {
|
|
||||||
const base = docsRepo
|
|
||||||
return (
|
|
||||||
base.replace(endingSlashRE, '')
|
|
||||||
+ `/-/edit`
|
|
||||||
+ `/${docsBranch}/`
|
|
||||||
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
|
||||||
+ path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const base = outboundRE.test(docsRepo)
|
|
||||||
? docsRepo
|
|
||||||
: `https://github.com/${docsRepo}`
|
|
||||||
return (
|
|
||||||
base.replace(endingSlashRE, '')
|
|
||||||
+ '/edit'
|
|
||||||
+ `/${docsBranch}/`
|
|
||||||
+ (docsDir ? docsDir.replace(endingSlashRE, '') + '/' : '')
|
|
||||||
+ path
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
@require '../styles/wrapper.styl'
|
|
||||||
|
|
||||||
.page-edit
|
|
||||||
@extend $wrapper
|
|
||||||
padding-top 1rem
|
|
||||||
padding-bottom 1rem
|
|
||||||
overflow auto
|
|
||||||
|
|
||||||
.edit-link
|
|
||||||
display inline-block
|
|
||||||
a
|
|
||||||
color lighten($textColor, 25%)
|
|
||||||
margin-right 0.25rem
|
|
||||||
.last-updated
|
|
||||||
float right
|
|
||||||
font-size 0.9em
|
|
||||||
.prefix
|
|
||||||
font-weight 500
|
|
||||||
color lighten($textColor, 25%)
|
|
||||||
.time
|
|
||||||
font-weight 400
|
|
||||||
color #767676
|
|
||||||
|
|
||||||
@media (max-width: $MQMobile)
|
|
||||||
.page-edit
|
|
||||||
.edit-link
|
|
||||||
margin-bottom 0.5rem
|
|
||||||
.last-updated
|
|
||||||
font-size 0.8em
|
|
||||||
float none
|
|
||||||
text-align left
|
|
||||||
|
|
||||||
</style>
|
|
@ -1,163 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="prev || next"
|
|
||||||
class="page-nav"
|
|
||||||
>
|
|
||||||
<p class="inner">
|
|
||||||
<span
|
|
||||||
v-if="prev"
|
|
||||||
class="prev"
|
|
||||||
>
|
|
||||||
←
|
|
||||||
<a
|
|
||||||
v-if="prev.type === 'external'"
|
|
||||||
class="prev"
|
|
||||||
:href="prev.path"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{{ prev.title || prev.path }}
|
|
||||||
|
|
||||||
<OutboundLink />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<RouterLink
|
|
||||||
v-else
|
|
||||||
class="prev"
|
|
||||||
:to="prev.path"
|
|
||||||
>
|
|
||||||
{{ prev.title || prev.path }}
|
|
||||||
</RouterLink>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="next"
|
|
||||||
class="next"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
v-if="next.type === 'external'"
|
|
||||||
:href="next.path"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{{ next.title || next.path }}
|
|
||||||
|
|
||||||
<OutboundLink />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<RouterLink
|
|
||||||
v-else
|
|
||||||
:to="next.path"
|
|
||||||
>
|
|
||||||
{{ next.title || next.path }}
|
|
||||||
</RouterLink>
|
|
||||||
→
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { resolvePage } from '../util'
|
|
||||||
import isString from 'lodash/isString'
|
|
||||||
import isNil from 'lodash/isNil'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PageNav',
|
|
||||||
|
|
||||||
props: ['sidebarItems'],
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
prev () {
|
|
||||||
return resolvePageLink(LINK_TYPES.PREV, this)
|
|
||||||
},
|
|
||||||
|
|
||||||
next () {
|
|
||||||
return resolvePageLink(LINK_TYPES.NEXT, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePrev (page, items) {
|
|
||||||
return find(page, items, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveNext (page, items) {
|
|
||||||
return find(page, items, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const LINK_TYPES = {
|
|
||||||
NEXT: {
|
|
||||||
resolveLink: resolveNext,
|
|
||||||
getThemeLinkConfig: ({ nextLinks }) => nextLinks,
|
|
||||||
getPageLinkConfig: ({ frontmatter }) => frontmatter.next
|
|
||||||
},
|
|
||||||
PREV: {
|
|
||||||
resolveLink: resolvePrev,
|
|
||||||
getThemeLinkConfig: ({ prevLinks }) => prevLinks,
|
|
||||||
getPageLinkConfig: ({ frontmatter }) => frontmatter.prev
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePageLink (
|
|
||||||
linkType,
|
|
||||||
{ $themeConfig, $page, $route, $site, sidebarItems }
|
|
||||||
) {
|
|
||||||
const { resolveLink, getThemeLinkConfig, getPageLinkConfig } = linkType
|
|
||||||
|
|
||||||
// Get link config from theme
|
|
||||||
const themeLinkConfig = getThemeLinkConfig($themeConfig)
|
|
||||||
|
|
||||||
// Get link config from current page
|
|
||||||
const pageLinkConfig = getPageLinkConfig($page)
|
|
||||||
|
|
||||||
// Page link config will overwrite global theme link config if defined
|
|
||||||
const link = isNil(pageLinkConfig) ? themeLinkConfig : pageLinkConfig
|
|
||||||
|
|
||||||
if (link === false) {
|
|
||||||
return
|
|
||||||
} else if (isString(link)) {
|
|
||||||
return resolvePage($site.pages, link, $route.path)
|
|
||||||
} else {
|
|
||||||
return resolveLink($page, sidebarItems)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function find (page, items, offset) {
|
|
||||||
const res = []
|
|
||||||
flatten(items, res)
|
|
||||||
for (let i = 0; i < res.length; i++) {
|
|
||||||
const cur = res[i]
|
|
||||||
if (cur.type === 'page' && cur.path === decodeURIComponent(page.path)) {
|
|
||||||
return res[i + offset]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function flatten (items, res) {
|
|
||||||
for (let i = 0, l = items.length; i < l; i++) {
|
|
||||||
if (items[i].type === 'group') {
|
|
||||||
flatten(items[i].children || [], res)
|
|
||||||
} else {
|
|
||||||
res.push(items[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
@require '../styles/wrapper.styl'
|
|
||||||
|
|
||||||
.page-nav
|
|
||||||
@extend $wrapper
|
|
||||||
padding-top 1rem
|
|
||||||
padding-bottom 0
|
|
||||||
.inner
|
|
||||||
min-height 2rem
|
|
||||||
margin-top 0
|
|
||||||
border-top 1px solid $borderColor
|
|
||||||
padding-top 1rem
|
|
||||||
overflow auto // clear float
|
|
||||||
.next
|
|
||||||
float right
|
|
||||||
</style>
|
|
@ -1,64 +0,0 @@
|
|||||||
<template>
|
|
||||||
<aside class="sidebar">
|
|
||||||
<NavLinks />
|
|
||||||
|
|
||||||
<slot name="top" />
|
|
||||||
|
|
||||||
<SidebarLinks
|
|
||||||
:depth="0"
|
|
||||||
:items="items"
|
|
||||||
/>
|
|
||||||
<slot name="bottom" />
|
|
||||||
</aside>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import SidebarLinks from '@theme/components/SidebarLinks.vue'
|
|
||||||
import NavLinks from '@theme/components/NavLinks.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Sidebar',
|
|
||||||
|
|
||||||
components: { SidebarLinks, NavLinks },
|
|
||||||
|
|
||||||
props: ['items']
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.sidebar
|
|
||||||
ul
|
|
||||||
padding 0
|
|
||||||
margin 0
|
|
||||||
list-style-type none
|
|
||||||
a
|
|
||||||
display inline-block
|
|
||||||
.nav-links
|
|
||||||
display none
|
|
||||||
border-bottom 1px solid $borderColor
|
|
||||||
padding 0.5rem 0 0.75rem 0
|
|
||||||
a
|
|
||||||
font-weight 600
|
|
||||||
.nav-item, .repo-link
|
|
||||||
display block
|
|
||||||
line-height 1.25rem
|
|
||||||
font-size 1.1em
|
|
||||||
padding 0.5rem 0 0.5rem 1.5rem
|
|
||||||
& > .sidebar-links
|
|
||||||
padding 1.5rem 0
|
|
||||||
& > li > a.sidebar-link
|
|
||||||
font-size 1.1em
|
|
||||||
line-height 1.7
|
|
||||||
font-weight bold
|
|
||||||
& > li:not(:first-child)
|
|
||||||
margin-top .75rem
|
|
||||||
|
|
||||||
@media (max-width: $MQMobile)
|
|
||||||
.sidebar
|
|
||||||
.nav-links
|
|
||||||
display block
|
|
||||||
.dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active::after
|
|
||||||
top calc(1rem - 2px)
|
|
||||||
& > .sidebar-links
|
|
||||||
padding 1rem 0
|
|
||||||
</style>
|
|
@ -1,40 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="sidebar-button"
|
|
||||||
@click="$emit('toggle-sidebar')"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class="icon"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
aria-hidden="true"
|
|
||||||
role="img"
|
|
||||||
viewBox="0 0 448 512"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M436 124H12c-6.627 0-12-5.373-12-12V80c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12zm0 160H12c-6.627 0-12-5.373-12-12v-32c0-6.627 5.373-12 12-12h424c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12z"
|
|
||||||
class=""
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.sidebar-button
|
|
||||||
cursor pointer
|
|
||||||
display none
|
|
||||||
width 1.25rem
|
|
||||||
height 1.25rem
|
|
||||||
position absolute
|
|
||||||
padding 0.6rem
|
|
||||||
top 0.6rem
|
|
||||||
left 1rem
|
|
||||||
.icon
|
|
||||||
display block
|
|
||||||
width 1.25rem
|
|
||||||
height 1.25rem
|
|
||||||
|
|
||||||
@media (max-width: $MQMobile)
|
|
||||||
.sidebar-button
|
|
||||||
display block
|
|
||||||
</style>
|
|
@ -1,141 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section
|
|
||||||
class="sidebar-group"
|
|
||||||
:class="[
|
|
||||||
{
|
|
||||||
collapsable,
|
|
||||||
'is-sub-group': depth !== 0
|
|
||||||
},
|
|
||||||
`depth-${depth}`
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<RouterLink
|
|
||||||
v-if="item.path"
|
|
||||||
class="sidebar-heading clickable"
|
|
||||||
:class="{
|
|
||||||
open,
|
|
||||||
'active': isActive($route, item.path)
|
|
||||||
}"
|
|
||||||
:to="item.path"
|
|
||||||
@click.native="$emit('toggle')"
|
|
||||||
>
|
|
||||||
<span>{{ item.title }}</span>
|
|
||||||
<span
|
|
||||||
v-if="collapsable"
|
|
||||||
class="arrow"
|
|
||||||
:class="open ? 'down' : 'right'"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<p
|
|
||||||
v-else
|
|
||||||
class="sidebar-heading"
|
|
||||||
:class="{ open }"
|
|
||||||
@click="$emit('toggle')"
|
|
||||||
>
|
|
||||||
<span>{{ item.title }}</span>
|
|
||||||
<span
|
|
||||||
v-if="collapsable"
|
|
||||||
class="arrow"
|
|
||||||
:class="open ? 'down' : 'right'"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<DropdownTransition>
|
|
||||||
<SidebarLinks
|
|
||||||
v-if="open || !collapsable"
|
|
||||||
class="sidebar-group-items"
|
|
||||||
:items="item.children"
|
|
||||||
:sidebar-depth="item.sidebarDepth"
|
|
||||||
:initial-open-group-index="item.initialOpenGroupIndex"
|
|
||||||
:depth="depth + 1"
|
|
||||||
/>
|
|
||||||
</DropdownTransition>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { isActive } from '../util'
|
|
||||||
import DropdownTransition from '@theme/components/DropdownTransition.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'SidebarGroup',
|
|
||||||
|
|
||||||
components: {
|
|
||||||
DropdownTransition
|
|
||||||
},
|
|
||||||
|
|
||||||
props: [
|
|
||||||
'item',
|
|
||||||
'open',
|
|
||||||
'collapsable',
|
|
||||||
'depth'
|
|
||||||
],
|
|
||||||
|
|
||||||
// ref: https://vuejs.org/v2/guide/components-edge-cases.html#Circular-References-Between-Components
|
|
||||||
beforeCreate () {
|
|
||||||
this.$options.components.SidebarLinks = require('@theme/components/SidebarLinks.vue').default
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: { isActive }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.sidebar-group
|
|
||||||
.sidebar-group
|
|
||||||
padding-left 0.5em
|
|
||||||
&:not(.collapsable)
|
|
||||||
.sidebar-heading:not(.clickable)
|
|
||||||
cursor auto
|
|
||||||
color inherit
|
|
||||||
// refine styles of nested sidebar groups
|
|
||||||
&.is-sub-group
|
|
||||||
padding-left 0
|
|
||||||
& > .sidebar-heading
|
|
||||||
font-size 0.95em
|
|
||||||
line-height 1.4
|
|
||||||
font-weight normal
|
|
||||||
padding-left 2rem
|
|
||||||
&:not(.clickable)
|
|
||||||
opacity 0.5
|
|
||||||
& > .sidebar-group-items
|
|
||||||
padding-left 1rem
|
|
||||||
& > li > .sidebar-link
|
|
||||||
font-size: 0.95em;
|
|
||||||
border-left none
|
|
||||||
&.depth-2
|
|
||||||
& > .sidebar-heading
|
|
||||||
border-left none
|
|
||||||
|
|
||||||
.sidebar-heading
|
|
||||||
color $textColor
|
|
||||||
transition color .15s ease
|
|
||||||
cursor pointer
|
|
||||||
font-size 1.1em
|
|
||||||
font-weight bold
|
|
||||||
// text-transform uppercase
|
|
||||||
padding 0.35rem 1.5rem 0.35rem 1.25rem
|
|
||||||
width 100%
|
|
||||||
box-sizing border-box
|
|
||||||
margin 0
|
|
||||||
border-left 0.25rem solid transparent
|
|
||||||
&.open, &:hover
|
|
||||||
color inherit
|
|
||||||
.arrow
|
|
||||||
position relative
|
|
||||||
top -0.12em
|
|
||||||
left 0.5em
|
|
||||||
&.clickable
|
|
||||||
&.active
|
|
||||||
font-weight 600
|
|
||||||
color $accentColor
|
|
||||||
border-left-color $accentColor
|
|
||||||
&:hover
|
|
||||||
color $accentColor
|
|
||||||
|
|
||||||
.sidebar-group-items
|
|
||||||
transition height .1s ease-out
|
|
||||||
font-size 0.95em
|
|
||||||
overflow hidden
|
|
||||||
</style>
|
|
@ -1,133 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { isActive, hashRE, groupHeaders } from '../util'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
functional: true,
|
|
||||||
|
|
||||||
props: ['item', 'sidebarDepth'],
|
|
||||||
|
|
||||||
render (h,
|
|
||||||
{
|
|
||||||
parent: {
|
|
||||||
$page,
|
|
||||||
$site,
|
|
||||||
$route,
|
|
||||||
$themeConfig,
|
|
||||||
$themeLocaleConfig
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
item,
|
|
||||||
sidebarDepth
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
// use custom active class matching logic
|
|
||||||
// due to edge case of paths ending with / + hash
|
|
||||||
const selfActive = isActive($route, item.path)
|
|
||||||
// for sidebar: auto pages, a hash link should be active if one of its child
|
|
||||||
// matches
|
|
||||||
const active = item.type === 'auto'
|
|
||||||
? selfActive || item.children.some(c => isActive($route, item.basePath + '#' + c.slug))
|
|
||||||
: selfActive
|
|
||||||
const link = item.type === 'external'
|
|
||||||
? renderExternal(h, item.path, item.title || item.path)
|
|
||||||
: renderLink(h, item.path, item.title || item.path, active)
|
|
||||||
|
|
||||||
const maxDepth = [
|
|
||||||
$page.frontmatter.sidebarDepth,
|
|
||||||
sidebarDepth,
|
|
||||||
$themeLocaleConfig.sidebarDepth,
|
|
||||||
$themeConfig.sidebarDepth,
|
|
||||||
1
|
|
||||||
].find(depth => depth !== undefined)
|
|
||||||
|
|
||||||
const displayAllHeaders = $themeLocaleConfig.displayAllHeaders
|
|
||||||
|| $themeConfig.displayAllHeaders
|
|
||||||
|
|
||||||
if (item.type === 'auto') {
|
|
||||||
return [link, renderChildren(h, item.children, item.basePath, $route, maxDepth)]
|
|
||||||
} else if ((active || displayAllHeaders) && item.headers && !hashRE.test(item.path)) {
|
|
||||||
const children = groupHeaders(item.headers)
|
|
||||||
return [link, renderChildren(h, children, item.path, $route, maxDepth)]
|
|
||||||
} else {
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLink (h, to, text, active, level) {
|
|
||||||
const component = {
|
|
||||||
props: {
|
|
||||||
to,
|
|
||||||
activeClass: '',
|
|
||||||
exactActiveClass: ''
|
|
||||||
},
|
|
||||||
class: {
|
|
||||||
active,
|
|
||||||
'sidebar-link': true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level > 2) {
|
|
||||||
component.style = {
|
|
||||||
'padding-left': level + 'rem'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h('RouterLink', component, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderChildren (h, children, path, route, maxDepth, depth = 1) {
|
|
||||||
if (!children || depth > maxDepth) return null
|
|
||||||
return h('ul', { class: 'sidebar-sub-headers' }, children.map(c => {
|
|
||||||
const active = isActive(route, path + '#' + c.slug)
|
|
||||||
return h('li', { class: 'sidebar-sub-header' }, [
|
|
||||||
renderLink(h, path + '#' + c.slug, c.title, active, c.level - 1),
|
|
||||||
renderChildren(h, c.children, path, route, maxDepth, depth + 1)
|
|
||||||
])
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderExternal (h, to, text) {
|
|
||||||
return h('a', {
|
|
||||||
attrs: {
|
|
||||||
href: to,
|
|
||||||
target: '_blank',
|
|
||||||
rel: 'noopener noreferrer'
|
|
||||||
},
|
|
||||||
class: {
|
|
||||||
'sidebar-link': true
|
|
||||||
}
|
|
||||||
}, [text, h('OutboundLink')])
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.sidebar .sidebar-sub-headers
|
|
||||||
padding-left 1rem
|
|
||||||
font-size 0.95em
|
|
||||||
|
|
||||||
a.sidebar-link
|
|
||||||
font-size 1em
|
|
||||||
font-weight 400
|
|
||||||
display inline-block
|
|
||||||
color $textColor
|
|
||||||
border-left 0.25rem solid transparent
|
|
||||||
padding 0.35rem 1rem 0.35rem 1.25rem
|
|
||||||
line-height 1.4
|
|
||||||
width: 100%
|
|
||||||
box-sizing: border-box
|
|
||||||
&:hover
|
|
||||||
color $accentColor
|
|
||||||
&.active
|
|
||||||
font-weight 600
|
|
||||||
color $accentColor
|
|
||||||
border-left-color $accentColor
|
|
||||||
.sidebar-group &
|
|
||||||
padding-left 2rem
|
|
||||||
.sidebar-sub-headers &
|
|
||||||
padding-top 0.25rem
|
|
||||||
padding-bottom 0.25rem
|
|
||||||
border-left none
|
|
||||||
&.active
|
|
||||||
font-weight 500
|
|
||||||
</style>
|
|
@ -1,103 +0,0 @@
|
|||||||
<template>
|
|
||||||
<ul
|
|
||||||
v-if="items.length"
|
|
||||||
class="sidebar-links"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(item, i) in items"
|
|
||||||
:key="i"
|
|
||||||
>
|
|
||||||
<SidebarGroup
|
|
||||||
v-if="item.type === 'group'"
|
|
||||||
:item="item"
|
|
||||||
:open="i === openGroupIndex"
|
|
||||||
:collapsable="item.collapsable || item.collapsible"
|
|
||||||
:depth="depth"
|
|
||||||
@toggle="toggleGroup(i)"
|
|
||||||
/>
|
|
||||||
<SidebarLink
|
|
||||||
v-else
|
|
||||||
:sidebar-depth="sidebarDepth"
|
|
||||||
:item="item"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import SidebarGroup from '@theme/components/SidebarGroup.vue'
|
|
||||||
import SidebarLink from '@theme/components/SidebarLink.vue'
|
|
||||||
import { isActive } from '../util'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'SidebarLinks',
|
|
||||||
|
|
||||||
components: { SidebarGroup, SidebarLink },
|
|
||||||
|
|
||||||
props: [
|
|
||||||
'items',
|
|
||||||
'depth', // depth of current sidebar links
|
|
||||||
'sidebarDepth', // depth of headers to be extracted
|
|
||||||
'initialOpenGroupIndex'
|
|
||||||
],
|
|
||||||
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
openGroupIndex: this.initialOpenGroupIndex || 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
'$route' () {
|
|
||||||
this.refreshIndex()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created () {
|
|
||||||
this.refreshIndex()
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
refreshIndex () {
|
|
||||||
const index = resolveOpenGroupIndex(
|
|
||||||
this.$route,
|
|
||||||
this.items
|
|
||||||
)
|
|
||||||
if (index > -1) {
|
|
||||||
this.openGroupIndex = index
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleGroup (index) {
|
|
||||||
this.openGroupIndex = index === this.openGroupIndex ? -1 : index
|
|
||||||
},
|
|
||||||
|
|
||||||
isActive (page) {
|
|
||||||
return isActive(this.$route, page.regularPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveOpenGroupIndex (route, items) {
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i]
|
|
||||||
if (descendantIsActive(route, item)) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
function descendantIsActive (route, item) {
|
|
||||||
if (item.type === 'group') {
|
|
||||||
return item.children.some(child => {
|
|
||||||
if (child.type === 'group') {
|
|
||||||
return descendantIsActive(route, child)
|
|
||||||
} else {
|
|
||||||
return child.type === 'page' && isActive(route, child.path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,44 +0,0 @@
|
|||||||
<script>
|
|
||||||
export default {
|
|
||||||
functional: true,
|
|
||||||
props: {
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'tip'
|
|
||||||
},
|
|
||||||
text: String,
|
|
||||||
vertical: {
|
|
||||||
type: String,
|
|
||||||
default: 'top'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
render (h, { props, slots }) {
|
|
||||||
return h('span', {
|
|
||||||
class: ['badge', props.type],
|
|
||||||
style: {
|
|
||||||
verticalAlign: props.vertical
|
|
||||||
}
|
|
||||||
}, props.text || slots().default)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
.badge
|
|
||||||
display inline-block
|
|
||||||
font-size 14px
|
|
||||||
height 18px
|
|
||||||
line-height 18px
|
|
||||||
border-radius 3px
|
|
||||||
padding 0 6px
|
|
||||||
color white
|
|
||||||
background-color #42b983
|
|
||||||
&.tip, &.green
|
|
||||||
background-color $badgeTipColor
|
|
||||||
&.error
|
|
||||||
background-color $badgeErrorColor
|
|
||||||
&.warning, &.warn, &.yellow
|
|
||||||
background-color $badgeWarningColor
|
|
||||||
& + &
|
|
||||||
margin-left 5px
|
|
||||||
</style>
|
|
@ -1,36 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="theme-code-block"
|
|
||||||
:class="{ 'theme-code-block__active': active }"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'CodeBlock',
|
|
||||||
props: {
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
active: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.theme-code-block {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.theme-code-block__active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.theme-code-block > pre {
|
|
||||||
background-color: orange;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,105 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="theme-code-group">
|
|
||||||
<div class="theme-code-group__nav">
|
|
||||||
<ul class="theme-code-group__ul">
|
|
||||||
<li
|
|
||||||
v-for="(tab, i) in codeTabs"
|
|
||||||
:key="tab.title"
|
|
||||||
class="theme-code-group__li"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="theme-code-group__nav-tab"
|
|
||||||
:class="{
|
|
||||||
'theme-code-group__nav-tab-active': i === activeCodeTabIndex,
|
|
||||||
}"
|
|
||||||
@click="changeCodeTab(i)"
|
|
||||||
>
|
|
||||||
{{ tab.title }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<slot />
|
|
||||||
<pre
|
|
||||||
v-if="codeTabs.length < 1"
|
|
||||||
class="pre-blank"
|
|
||||||
>// Make sure to add code blocks to your code group</pre>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'CodeGroup',
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
codeTabs: [],
|
|
||||||
activeCodeTabIndex: -1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
activeCodeTabIndex (index) {
|
|
||||||
this.codeTabs.forEach(tab => {
|
|
||||||
tab.elm.classList.remove('theme-code-block__active')
|
|
||||||
})
|
|
||||||
this.codeTabs[index].elm.classList.add('theme-code-block__active')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.codeTabs = (this.$slots.default || []).filter(slot => Boolean(slot.componentOptions)).map((slot, index) => {
|
|
||||||
if (slot.componentOptions.propsData.active === '') {
|
|
||||||
this.activeCodeTabIndex = index
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: slot.componentOptions.propsData.title,
|
|
||||||
elm: slot.elm
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.activeCodeTabIndex === -1 && this.codeTabs.length > 0) {
|
|
||||||
this.activeCodeTabIndex = 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
changeCodeTab (index) {
|
|
||||||
this.activeCodeTabIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
|
||||||
.theme-code-group {}
|
|
||||||
.theme-code-group__nav {
|
|
||||||
margin-bottom: -35px;
|
|
||||||
background-color: $codeBgColor;
|
|
||||||
padding-bottom: 22px;
|
|
||||||
border-top-left-radius: 6px;
|
|
||||||
border-top-right-radius: 6px;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
.theme-code-group__ul {
|
|
||||||
margin: auto 0;
|
|
||||||
padding-left: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
.theme-code-group__li {}
|
|
||||||
.theme-code-group__nav-tab {
|
|
||||||
border: 0;
|
|
||||||
padding: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: transparent;
|
|
||||||
font-size: 0.85em;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.theme-code-group__nav-tab-active {
|
|
||||||
border-bottom: #42b983 1px solid;
|
|
||||||
}
|
|
||||||
.pre-blank {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,59 +0,0 @@
|
|||||||
const path = require('path')
|
|
||||||
|
|
||||||
// Theme API.
|
|
||||||
module.exports = (options, ctx) => {
|
|
||||||
const { themeConfig, siteConfig } = ctx
|
|
||||||
|
|
||||||
// resolve algolia
|
|
||||||
const isAlgoliaSearch = (
|
|
||||||
themeConfig.algolia
|
|
||||||
|| Object
|
|
||||||
.keys(siteConfig.locales && themeConfig.locales || {})
|
|
||||||
.some(base => themeConfig.locales[base].algolia)
|
|
||||||
)
|
|
||||||
|
|
||||||
const enableSmoothScroll = themeConfig.smoothScroll === true
|
|
||||||
|
|
||||||
return {
|
|
||||||
alias () {
|
|
||||||
return {
|
|
||||||
'@AlgoliaSearchBox': isAlgoliaSearch
|
|
||||||
? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue')
|
|
||||||
: path.resolve(__dirname, 'noopModule.js')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
['@vuepress/active-header-links', options.activeHeaderLinks],
|
|
||||||
'@vuepress/search',
|
|
||||||
'@vuepress/plugin-nprogress',
|
|
||||||
['container', {
|
|
||||||
type: 'tip',
|
|
||||||
defaultTitle: {
|
|
||||||
'/': 'TIP',
|
|
||||||
'/zh/': '提示'
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
['container', {
|
|
||||||
type: 'warning',
|
|
||||||
defaultTitle: {
|
|
||||||
'/': 'WARNING',
|
|
||||||
'/zh/': '注意'
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
['container', {
|
|
||||||
type: 'danger',
|
|
||||||
defaultTitle: {
|
|
||||||
'/': 'WARNING',
|
|
||||||
'/zh/': '警告'
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
['container', {
|
|
||||||
type: 'details',
|
|
||||||
before: info => `<details class="custom-block details">${info ? `<summary>${info}</summary>` : ''}\n`,
|
|
||||||
after: () => '</details>\n'
|
|
||||||
}],
|
|
||||||
['smooth-scroll', enableSmoothScroll]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="theme-container">
|
|
||||||
<div class="theme-default-content">
|
|
||||||
<h1>404</h1>
|
|
||||||
|
|
||||||
<blockquote>{{ getMsg() }}</blockquote>
|
|
||||||
|
|
||||||
<RouterLink to="/">
|
|
||||||
Take me home.
|
|
||||||
</RouterLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const msgs = [
|
|
||||||
`There's nothing here.`,
|
|
||||||
`How did we get here?`,
|
|
||||||
`That's a Four-Oh-Four.`,
|
|
||||||
`Looks like we've got some broken links.`
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
getMsg () {
|
|
||||||
return msgs[Math.floor(Math.random() * msgs.length)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,137 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="theme-container"
|
|
||||||
:class="pageClasses"
|
|
||||||
@touchstart="onTouchStart"
|
|
||||||
@touchend="onTouchEnd"
|
|
||||||
>
|
|
||||||
<Navbar v-if="shouldShowNavbar" @toggle-sidebar="toggleSidebar" />
|
|
||||||
|
|
||||||
<div class="sidebar-mask" @click="toggleSidebar(false)" />
|
|
||||||
|
|
||||||
<Sidebar :items="sidebarItems" @toggle-sidebar="toggleSidebar">
|
|
||||||
<template #top>
|
|
||||||
<slot name="sidebar-top" />
|
|
||||||
</template>
|
|
||||||
<template #bottom>
|
|
||||||
<slot name="sidebar-bottom" />
|
|
||||||
</template>
|
|
||||||
</Sidebar>
|
|
||||||
|
|
||||||
<Home v-if="$page.frontmatter.home" />
|
|
||||||
|
|
||||||
<Page v-else :sidebar-items="sidebarItems">
|
|
||||||
<template #top>
|
|
||||||
<slot name="page-top" />
|
|
||||||
</template>
|
|
||||||
<template #bottom>
|
|
||||||
<slot name="page-bottom" />
|
|
||||||
</template>
|
|
||||||
</Page>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Home from "@theme/components/Home.vue";
|
|
||||||
import Navbar from "@theme/components/Navbar.vue";
|
|
||||||
import Page from "@theme/components/Page.vue";
|
|
||||||
import Sidebar from "@theme/components/Sidebar.vue";
|
|
||||||
import { resolveSidebarItems } from "../util";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "Layout",
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Home,
|
|
||||||
Page,
|
|
||||||
Sidebar,
|
|
||||||
Navbar,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isSidebarOpen: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
shouldShowNavbar() {
|
|
||||||
const { themeConfig } = this.$site;
|
|
||||||
const { frontmatter } = this.$page;
|
|
||||||
if (frontmatter.navbar === false || themeConfig.navbar === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
this.$title ||
|
|
||||||
themeConfig.logo ||
|
|
||||||
themeConfig.repo ||
|
|
||||||
themeConfig.nav ||
|
|
||||||
this.$themeLocaleConfig.nav
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldShowSidebar() {
|
|
||||||
const { frontmatter } = this.$page;
|
|
||||||
return (
|
|
||||||
!frontmatter.home &&
|
|
||||||
frontmatter.sidebar !== false &&
|
|
||||||
this.sidebarItems.length
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
sidebarItems() {
|
|
||||||
return resolveSidebarItems(
|
|
||||||
this.$page,
|
|
||||||
this.$page.regularPath,
|
|
||||||
this.$site,
|
|
||||||
this.$localePath
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
pageClasses() {
|
|
||||||
const userPageClass = this.$page.frontmatter.pageClass;
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"no-navbar": !this.shouldShowNavbar,
|
|
||||||
"sidebar-open": this.isSidebarOpen,
|
|
||||||
"no-sidebar": !this.shouldShowSidebar,
|
|
||||||
},
|
|
||||||
userPageClass,
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.$router.afterEach(() => {
|
|
||||||
this.isSidebarOpen = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
toggleSidebar(to) {
|
|
||||||
this.isSidebarOpen = typeof to === "boolean" ? to : !this.isSidebarOpen;
|
|
||||||
this.$emit("toggle-sidebar", this.isSidebarOpen);
|
|
||||||
},
|
|
||||||
|
|
||||||
// side swipe
|
|
||||||
onTouchStart(e) {
|
|
||||||
this.touchStart = {
|
|
||||||
x: e.changedTouches[0].clientX,
|
|
||||||
y: e.changedTouches[0].clientY,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onTouchEnd(e) {
|
|
||||||
const dx = e.changedTouches[0].clientX - this.touchStart.x;
|
|
||||||
const dy = e.changedTouches[0].clientY - this.touchStart.y;
|
|
||||||
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) {
|
|
||||||
if (dx > 0 && this.touchStart.x <= 80) {
|
|
||||||
this.toggleSidebar(true);
|
|
||||||
} else {
|
|
||||||
this.toggleSidebar(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -1 +0,0 @@
|
|||||||
export default {}
|
|
@ -1,22 +0,0 @@
|
|||||||
@require './config'
|
|
||||||
|
|
||||||
.arrow
|
|
||||||
display inline-block
|
|
||||||
width 0
|
|
||||||
height 0
|
|
||||||
&.up
|
|
||||||
border-left 4px solid transparent
|
|
||||||
border-right 4px solid transparent
|
|
||||||
border-bottom 6px solid $arrowBgColor
|
|
||||||
&.down
|
|
||||||
border-left 4px solid transparent
|
|
||||||
border-right 4px solid transparent
|
|
||||||
border-top 6px solid $arrowBgColor
|
|
||||||
&.right
|
|
||||||
border-top 4px solid transparent
|
|
||||||
border-bottom 4px solid transparent
|
|
||||||
border-left 6px solid $arrowBgColor
|
|
||||||
&.left
|
|
||||||
border-top 4px solid transparent
|
|
||||||
border-bottom 4px solid transparent
|
|
||||||
border-right 6px solid $arrowBgColor
|
|
@ -1,137 +0,0 @@
|
|||||||
{$contentClass}
|
|
||||||
code
|
|
||||||
color lighten($textColor, 20%)
|
|
||||||
padding 0.25rem 0.5rem
|
|
||||||
margin 0
|
|
||||||
font-size 0.85em
|
|
||||||
background-color rgba(27,31,35,0.05)
|
|
||||||
border-radius 3px
|
|
||||||
.token
|
|
||||||
&.deleted
|
|
||||||
color #EC5975
|
|
||||||
&.inserted
|
|
||||||
color $accentColor
|
|
||||||
|
|
||||||
{$contentClass}
|
|
||||||
pre, pre[class*="language-"]
|
|
||||||
line-height 1.4
|
|
||||||
padding 1.25rem 1.5rem
|
|
||||||
margin 0.85rem 0
|
|
||||||
background-color $codeBgColor
|
|
||||||
border-radius 6px
|
|
||||||
overflow auto
|
|
||||||
code
|
|
||||||
color #fff
|
|
||||||
padding 0
|
|
||||||
background-color transparent
|
|
||||||
border-radius 0
|
|
||||||
|
|
||||||
div[class*="language-"]
|
|
||||||
position relative
|
|
||||||
background-color $codeBgColor
|
|
||||||
border-radius 6px
|
|
||||||
.highlight-lines
|
|
||||||
user-select none
|
|
||||||
padding-top 1.3rem
|
|
||||||
position absolute
|
|
||||||
top 0
|
|
||||||
left 0
|
|
||||||
width 100%
|
|
||||||
line-height 1.4
|
|
||||||
.highlighted
|
|
||||||
background-color rgba(0, 0, 0, 66%)
|
|
||||||
pre, pre[class*="language-"]
|
|
||||||
background transparent
|
|
||||||
position relative
|
|
||||||
z-index 1
|
|
||||||
&::before
|
|
||||||
position absolute
|
|
||||||
z-index 3
|
|
||||||
top 0.8em
|
|
||||||
right 1em
|
|
||||||
font-size 0.75rem
|
|
||||||
color rgba(255, 255, 255, 0.4)
|
|
||||||
&:not(.line-numbers-mode)
|
|
||||||
.line-numbers-wrapper
|
|
||||||
display none
|
|
||||||
&.line-numbers-mode
|
|
||||||
.highlight-lines .highlighted
|
|
||||||
position relative
|
|
||||||
&:before
|
|
||||||
content ' '
|
|
||||||
position absolute
|
|
||||||
z-index 3
|
|
||||||
left 0
|
|
||||||
top 0
|
|
||||||
display block
|
|
||||||
width $lineNumbersWrapperWidth
|
|
||||||
height 100%
|
|
||||||
background-color rgba(0, 0, 0, 66%)
|
|
||||||
pre
|
|
||||||
padding-left $lineNumbersWrapperWidth + 1 rem
|
|
||||||
vertical-align middle
|
|
||||||
.line-numbers-wrapper
|
|
||||||
position absolute
|
|
||||||
top 0
|
|
||||||
width $lineNumbersWrapperWidth
|
|
||||||
text-align center
|
|
||||||
color rgba(255, 255, 255, 0.3)
|
|
||||||
padding 1.25rem 0
|
|
||||||
line-height 1.4
|
|
||||||
br
|
|
||||||
user-select none
|
|
||||||
.line-number
|
|
||||||
position relative
|
|
||||||
z-index 4
|
|
||||||
user-select none
|
|
||||||
font-size 0.85em
|
|
||||||
&::after
|
|
||||||
content ''
|
|
||||||
position absolute
|
|
||||||
z-index 2
|
|
||||||
top 0
|
|
||||||
left 0
|
|
||||||
width $lineNumbersWrapperWidth
|
|
||||||
height 100%
|
|
||||||
border-radius 6px 0 0 6px
|
|
||||||
border-right 1px solid rgba(0, 0, 0, 66%)
|
|
||||||
background-color $codeBgColor
|
|
||||||
|
|
||||||
|
|
||||||
for lang in $codeLang
|
|
||||||
div{'[class~="language-' + lang + '"]'}
|
|
||||||
&:before
|
|
||||||
content ('' + lang)
|
|
||||||
|
|
||||||
div[class~="language-javascript"]
|
|
||||||
&:before
|
|
||||||
content "js"
|
|
||||||
|
|
||||||
div[class~="language-typescript"]
|
|
||||||
&:before
|
|
||||||
content "ts"
|
|
||||||
|
|
||||||
div[class~="language-markup"]
|
|
||||||
&:before
|
|
||||||
content "html"
|
|
||||||
|
|
||||||
div[class~="language-markdown"]
|
|
||||||
&:before
|
|
||||||
content "md"
|
|
||||||
|
|
||||||
div[class~="language-json"]:before
|
|
||||||
content "json"
|
|
||||||
|
|
||||||
div[class~="language-ruby"]:before
|
|
||||||
content "rb"
|
|
||||||
|
|
||||||
div[class~="language-python"]:before
|
|
||||||
content "py"
|
|
||||||
|
|
||||||
div[class~="language-bash"]:before
|
|
||||||
content "sh"
|
|
||||||
|
|
||||||
div[class~="language-php"]:before
|
|
||||||
content "php"
|
|
||||||
|
|
||||||
@import '~prismjs/themes/prism-tomorrow.css'
|
|
@ -1 +0,0 @@
|
|||||||
$contentClass = '.theme-default-content'
|
|
@ -1,44 +0,0 @@
|
|||||||
.custom-block
|
|
||||||
.custom-block-title
|
|
||||||
font-weight 600
|
|
||||||
margin-bottom -0.4rem
|
|
||||||
&.tip, &.warning, &.danger
|
|
||||||
padding .1rem 1.5rem
|
|
||||||
border-left-width .5rem
|
|
||||||
border-left-style solid
|
|
||||||
margin 1rem 0
|
|
||||||
&.tip
|
|
||||||
background-color #f3f5f7
|
|
||||||
border-color #42b983
|
|
||||||
&.warning
|
|
||||||
background-color rgba(255,229,100,.3)
|
|
||||||
border-color darken(#ffe564, 35%)
|
|
||||||
color darken(#ffe564, 70%)
|
|
||||||
.custom-block-title
|
|
||||||
color darken(#ffe564, 50%)
|
|
||||||
a
|
|
||||||
color $textColor
|
|
||||||
&.danger
|
|
||||||
background-color #ffe6e6
|
|
||||||
border-color darken(red, 20%)
|
|
||||||
color darken(red, 70%)
|
|
||||||
.custom-block-title
|
|
||||||
color darken(red, 40%)
|
|
||||||
a
|
|
||||||
color $textColor
|
|
||||||
&.details
|
|
||||||
display block
|
|
||||||
position relative
|
|
||||||
border-radius 2px
|
|
||||||
margin 1.6em 0
|
|
||||||
padding 1.6em
|
|
||||||
background-color #eee
|
|
||||||
h4
|
|
||||||
margin-top 0
|
|
||||||
figure, p
|
|
||||||
&:last-child
|
|
||||||
margin-bottom 0
|
|
||||||
padding-bottom 0
|
|
||||||
summary
|
|
||||||
outline none
|
|
||||||
cursor pointer
|
|
@ -1,200 +0,0 @@
|
|||||||
@require './config'
|
|
||||||
@require './code'
|
|
||||||
@require './custom-blocks'
|
|
||||||
@require './arrow'
|
|
||||||
@require './wrapper'
|
|
||||||
@require './toc'
|
|
||||||
|
|
||||||
html, body
|
|
||||||
padding 0
|
|
||||||
margin 0
|
|
||||||
background-color #fff
|
|
||||||
|
|
||||||
body
|
|
||||||
font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif
|
|
||||||
-webkit-font-smoothing antialiased
|
|
||||||
-moz-osx-font-smoothing grayscale
|
|
||||||
font-size 16px
|
|
||||||
color $textColor
|
|
||||||
|
|
||||||
.page
|
|
||||||
padding-left $sidebarWidth
|
|
||||||
|
|
||||||
.navbar
|
|
||||||
position fixed
|
|
||||||
z-index 20
|
|
||||||
top 0
|
|
||||||
left 0
|
|
||||||
right 0
|
|
||||||
height $navbarHeight
|
|
||||||
background-color #fff
|
|
||||||
box-sizing border-box
|
|
||||||
border-bottom 1px solid $borderColor
|
|
||||||
|
|
||||||
.sidebar-mask
|
|
||||||
position fixed
|
|
||||||
z-index 9
|
|
||||||
top 0
|
|
||||||
left 0
|
|
||||||
width 100vw
|
|
||||||
height 100vh
|
|
||||||
display none
|
|
||||||
|
|
||||||
.sidebar
|
|
||||||
font-size 16px
|
|
||||||
background-color #fff
|
|
||||||
width $sidebarWidth
|
|
||||||
position fixed
|
|
||||||
z-index 10
|
|
||||||
margin 0
|
|
||||||
top $navbarHeight
|
|
||||||
left 0
|
|
||||||
bottom 0
|
|
||||||
box-sizing border-box
|
|
||||||
border-right 1px solid $borderColor
|
|
||||||
overflow-y auto
|
|
||||||
|
|
||||||
{$contentClass}:not(.custom)
|
|
||||||
@extend $wrapper
|
|
||||||
> *:first-child
|
|
||||||
margin-top $navbarHeight
|
|
||||||
|
|
||||||
a:hover
|
|
||||||
text-decoration underline
|
|
||||||
|
|
||||||
p.demo
|
|
||||||
padding 1rem 1.5rem
|
|
||||||
border 1px solid #ddd
|
|
||||||
border-radius 4px
|
|
||||||
|
|
||||||
img
|
|
||||||
max-width 100%
|
|
||||||
|
|
||||||
{$contentClass}.custom
|
|
||||||
padding 0
|
|
||||||
margin 0
|
|
||||||
|
|
||||||
img
|
|
||||||
max-width 100%
|
|
||||||
|
|
||||||
a
|
|
||||||
font-weight 500
|
|
||||||
color $accentColor
|
|
||||||
text-decoration none
|
|
||||||
|
|
||||||
p a code
|
|
||||||
font-weight 400
|
|
||||||
color $accentColor
|
|
||||||
|
|
||||||
kbd
|
|
||||||
background #eee
|
|
||||||
border solid 0.15rem #ddd
|
|
||||||
border-bottom solid 0.25rem #ddd
|
|
||||||
border-radius 0.15rem
|
|
||||||
padding 0 0.15em
|
|
||||||
|
|
||||||
blockquote
|
|
||||||
font-size 1rem
|
|
||||||
color #999;
|
|
||||||
border-left .2rem solid #dfe2e5
|
|
||||||
margin 1rem 0
|
|
||||||
padding .25rem 0 .25rem 1rem
|
|
||||||
|
|
||||||
& > p
|
|
||||||
margin 0
|
|
||||||
|
|
||||||
ul, ol
|
|
||||||
padding-left 1.2em
|
|
||||||
|
|
||||||
strong
|
|
||||||
font-weight 600
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6
|
|
||||||
font-weight 600
|
|
||||||
line-height 1.25
|
|
||||||
|
|
||||||
{$contentClass}:not(.custom) > &
|
|
||||||
margin-top (0.5rem - $navbarHeight)
|
|
||||||
padding-top ($navbarHeight + 1rem)
|
|
||||||
margin-bottom 0
|
|
||||||
|
|
||||||
&:first-child
|
|
||||||
margin-top -1.5rem
|
|
||||||
margin-bottom 1rem
|
|
||||||
|
|
||||||
+ p, + pre, + .custom-block
|
|
||||||
margin-top 2rem
|
|
||||||
|
|
||||||
&:hover .header-anchor
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
h1
|
|
||||||
font-size 2.2rem
|
|
||||||
|
|
||||||
h2
|
|
||||||
font-size 1.65rem
|
|
||||||
padding-bottom .3rem
|
|
||||||
border-bottom 1px solid $borderColor
|
|
||||||
|
|
||||||
h3
|
|
||||||
font-size 1.35rem
|
|
||||||
|
|
||||||
a.header-anchor
|
|
||||||
font-size 0.85em
|
|
||||||
float left
|
|
||||||
margin-left -0.87em
|
|
||||||
padding-right 0.23em
|
|
||||||
margin-top 0.125em
|
|
||||||
opacity 0
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
text-decoration none
|
|
||||||
|
|
||||||
code, kbd, .line-number
|
|
||||||
font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace
|
|
||||||
|
|
||||||
p, ul, ol
|
|
||||||
line-height 1.7
|
|
||||||
|
|
||||||
hr
|
|
||||||
border 0
|
|
||||||
border-top 1px solid $borderColor
|
|
||||||
|
|
||||||
table
|
|
||||||
border-collapse collapse
|
|
||||||
margin 1rem 0
|
|
||||||
display: block
|
|
||||||
overflow-x: auto
|
|
||||||
|
|
||||||
tr
|
|
||||||
border-top 1px solid #dfe2e5
|
|
||||||
|
|
||||||
&:nth-child(2n)
|
|
||||||
background-color #f6f8fa
|
|
||||||
|
|
||||||
th, td
|
|
||||||
border 1px solid #dfe2e5
|
|
||||||
padding .6em 1em
|
|
||||||
|
|
||||||
.theme-container
|
|
||||||
&.sidebar-open
|
|
||||||
.sidebar-mask
|
|
||||||
display: block
|
|
||||||
|
|
||||||
&.no-navbar
|
|
||||||
{$contentClass}:not(.custom) > h1, h2, h3, h4, h5, h6
|
|
||||||
margin-top 1.5rem
|
|
||||||
padding-top 0
|
|
||||||
|
|
||||||
.sidebar
|
|
||||||
top 0
|
|
||||||
|
|
||||||
@media (min-width: ($MQMobile + 1px))
|
|
||||||
.theme-container.no-sidebar
|
|
||||||
.sidebar
|
|
||||||
display none
|
|
||||||
|
|
||||||
.page
|
|
||||||
padding-left 0
|
|
||||||
|
|
||||||
@require 'mobile.styl'
|
|
@ -1,37 +0,0 @@
|
|||||||
@require './config'
|
|
||||||
|
|
||||||
$mobileSidebarWidth = $sidebarWidth * 0.82
|
|
||||||
|
|
||||||
// narrow desktop / iPad
|
|
||||||
@media (max-width: $MQNarrow)
|
|
||||||
.sidebar
|
|
||||||
font-size 15px
|
|
||||||
width $mobileSidebarWidth
|
|
||||||
.page
|
|
||||||
padding-left $mobileSidebarWidth
|
|
||||||
|
|
||||||
// wide mobile
|
|
||||||
@media (max-width: $MQMobile)
|
|
||||||
.sidebar
|
|
||||||
top 0
|
|
||||||
padding-top $navbarHeight
|
|
||||||
transform translateX(-100%)
|
|
||||||
transition transform .2s ease
|
|
||||||
.page
|
|
||||||
padding-left 0
|
|
||||||
.theme-container
|
|
||||||
&.sidebar-open
|
|
||||||
.sidebar
|
|
||||||
transform translateX(0)
|
|
||||||
&.no-navbar
|
|
||||||
.sidebar
|
|
||||||
padding-top: 0
|
|
||||||
|
|
||||||
// narrow mobile
|
|
||||||
@media (max-width: $MQMobileNarrow)
|
|
||||||
h1
|
|
||||||
font-size 1.9rem
|
|
||||||
{$contentClass}
|
|
||||||
div[class*="language-"]
|
|
||||||
margin 0.85rem -1.5rem
|
|
||||||
border-radius 0
|
|
@ -1,3 +0,0 @@
|
|||||||
.table-of-contents
|
|
||||||
.badge
|
|
||||||
vertical-align middle
|
|
@ -1,9 +0,0 @@
|
|||||||
$wrapper
|
|
||||||
max-width $contentWidth
|
|
||||||
margin 0 auto
|
|
||||||
padding 2rem 2.5rem
|
|
||||||
@media (max-width: $MQNarrow)
|
|
||||||
padding 2rem
|
|
||||||
@media (max-width: $MQMobileNarrow)
|
|
||||||
padding 1.5rem
|
|
||||||
|
|
@ -1,244 +0,0 @@
|
|||||||
export const hashRE = /#.*$/
|
|
||||||
export const extRE = /\.(md|html)$/
|
|
||||||
export const endingSlashRE = /\/$/
|
|
||||||
export const outboundRE = /^[a-z]+:/i
|
|
||||||
|
|
||||||
export function normalize (path) {
|
|
||||||
return decodeURI(path)
|
|
||||||
.replace(hashRE, '')
|
|
||||||
.replace(extRE, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHash (path) {
|
|
||||||
const match = path.match(hashRE)
|
|
||||||
if (match) {
|
|
||||||
return match[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isExternal (path) {
|
|
||||||
return outboundRE.test(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isMailto (path) {
|
|
||||||
return /^mailto:/.test(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isTel (path) {
|
|
||||||
return /^tel:/.test(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ensureExt (path) {
|
|
||||||
if (isExternal(path)) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
const hashMatch = path.match(hashRE)
|
|
||||||
const hash = hashMatch ? hashMatch[0] : ''
|
|
||||||
const normalized = normalize(path)
|
|
||||||
|
|
||||||
if (endingSlashRE.test(normalized)) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return normalized + '.html' + hash
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isActive (route, path) {
|
|
||||||
const routeHash = decodeURIComponent(route.hash)
|
|
||||||
const linkHash = getHash(path)
|
|
||||||
if (linkHash && routeHash !== linkHash) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const routePath = normalize(route.path)
|
|
||||||
const pagePath = normalize(path)
|
|
||||||
return routePath === pagePath
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolvePage (pages, rawPath, base) {
|
|
||||||
if (isExternal(rawPath)) {
|
|
||||||
return {
|
|
||||||
type: 'external',
|
|
||||||
path: rawPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (base) {
|
|
||||||
rawPath = resolvePath(rawPath, base)
|
|
||||||
}
|
|
||||||
const path = normalize(rawPath)
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
|
||||||
if (normalize(pages[i].regularPath) === path) {
|
|
||||||
return Object.assign({}, pages[i], {
|
|
||||||
type: 'page',
|
|
||||||
path: ensureExt(pages[i].path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`)
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolvePath (relative, base, append) {
|
|
||||||
const firstChar = relative.charAt(0)
|
|
||||||
if (firstChar === '/') {
|
|
||||||
return relative
|
|
||||||
}
|
|
||||||
|
|
||||||
if (firstChar === '?' || firstChar === '#') {
|
|
||||||
return base + relative
|
|
||||||
}
|
|
||||||
|
|
||||||
const stack = base.split('/')
|
|
||||||
|
|
||||||
// remove trailing segment if:
|
|
||||||
// - not appending
|
|
||||||
// - appending to trailing slash (last segment is empty)
|
|
||||||
if (!append || !stack[stack.length - 1]) {
|
|
||||||
stack.pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve relative path
|
|
||||||
const segments = relative.replace(/^\//, '').split('/')
|
|
||||||
for (let i = 0; i < segments.length; i++) {
|
|
||||||
const segment = segments[i]
|
|
||||||
if (segment === '..') {
|
|
||||||
stack.pop()
|
|
||||||
} else if (segment !== '.') {
|
|
||||||
stack.push(segment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure leading slash
|
|
||||||
if (stack[0] !== '') {
|
|
||||||
stack.unshift('')
|
|
||||||
}
|
|
||||||
|
|
||||||
return stack.join('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { Page } page
|
|
||||||
* @param { string } regularPath
|
|
||||||
* @param { SiteData } site
|
|
||||||
* @param { string } localePath
|
|
||||||
* @returns { SidebarGroup }
|
|
||||||
*/
|
|
||||||
export function resolveSidebarItems (page, regularPath, site, localePath) {
|
|
||||||
const { pages, themeConfig } = site
|
|
||||||
|
|
||||||
const localeConfig = localePath && themeConfig.locales
|
|
||||||
? themeConfig.locales[localePath] || themeConfig
|
|
||||||
: themeConfig
|
|
||||||
|
|
||||||
const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar
|
|
||||||
if (pageSidebarConfig === 'auto') {
|
|
||||||
return resolveHeaders(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar
|
|
||||||
if (!sidebarConfig) {
|
|
||||||
return []
|
|
||||||
} else {
|
|
||||||
const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig)
|
|
||||||
if (config === 'auto') {
|
|
||||||
return resolveHeaders(page)
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
? config.map(item => resolveItem(item, pages, base))
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { Page } page
|
|
||||||
* @returns { SidebarGroup }
|
|
||||||
*/
|
|
||||||
function resolveHeaders (page) {
|
|
||||||
const headers = groupHeaders(page.headers || [])
|
|
||||||
return [{
|
|
||||||
type: 'group',
|
|
||||||
collapsable: false,
|
|
||||||
title: page.title,
|
|
||||||
path: null,
|
|
||||||
children: headers.map(h => ({
|
|
||||||
type: 'auto',
|
|
||||||
title: h.title,
|
|
||||||
basePath: page.path,
|
|
||||||
path: page.path + '#' + h.slug,
|
|
||||||
children: h.children || []
|
|
||||||
}))
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function groupHeaders (headers) {
|
|
||||||
// group h3s under h2
|
|
||||||
headers = headers.map(h => Object.assign({}, h))
|
|
||||||
let lastH2
|
|
||||||
headers.forEach(h => {
|
|
||||||
if (h.level === 2) {
|
|
||||||
lastH2 = h
|
|
||||||
} else if (lastH2) {
|
|
||||||
(lastH2.children || (lastH2.children = [])).push(h)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return headers.filter(h => h.level === 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveNavLinkItem (linkItem) {
|
|
||||||
return Object.assign(linkItem, {
|
|
||||||
type: linkItem.items && linkItem.items.length ? 'links' : 'link'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param { Route } route
|
|
||||||
* @param { Array<string|string[]> | Array<SidebarGroup> | [link: string]: SidebarConfig } config
|
|
||||||
* @returns { base: string, config: SidebarConfig }
|
|
||||||
*/
|
|
||||||
export function resolveMatchingConfig (regularPath, config) {
|
|
||||||
if (Array.isArray(config)) {
|
|
||||||
return {
|
|
||||||
base: '/',
|
|
||||||
config: config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const base in config) {
|
|
||||||
if (ensureEndingSlash(regularPath).indexOf(encodeURI(base)) === 0) {
|
|
||||||
return {
|
|
||||||
base,
|
|
||||||
config: config[base]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureEndingSlash (path) {
|
|
||||||
return /(\.html|\/)$/.test(path)
|
|
||||||
? path
|
|
||||||
: path + '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveItem (item, pages, base, groupDepth = 1) {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
return resolvePage(pages, item, base)
|
|
||||||
} else if (Array.isArray(item)) {
|
|
||||||
return Object.assign(resolvePage(pages, item[0], base), {
|
|
||||||
title: item[1]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const children = item.children || []
|
|
||||||
if (children.length === 0 && item.path) {
|
|
||||||
return Object.assign(resolvePage(pages, item.path, base), {
|
|
||||||
title: item.title
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'group',
|
|
||||||
path: item.path,
|
|
||||||
title: item.title,
|
|
||||||
sidebarDepth: item.sidebarDepth,
|
|
||||||
initialOpenGroupIndex: item.initialOpenGroupIndex,
|
|
||||||
children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)),
|
|
||||||
collapsable: item.collapsable !== false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
sidebarDepth: 1
|
|
||||||
sidebar: auto
|
|
||||||
---
|
|
||||||
|
|
||||||
# Appendix
|
|
||||||
|
|
||||||
## GraphQL API
|
|
||||||
|
|
||||||
Hetty exposes a GraphQL API over HTTP for managing all its features. This API is
|
|
||||||
used by the web admin interface; a Next.js app using Apollo Client.
|
|
||||||
|
|
||||||
### Playground
|
|
||||||
|
|
||||||
You can also introspect and manually experiment with the API via the included GraphQL Playground. To access it, start Hetty and visit: [http://localhost:8080/api/playground](http://localhost:8080/api/playground).
|
|
||||||
|
|
||||||
### Schema
|
|
||||||
|
|
||||||
<<< @/../pkg/api/schema.graphql
|
|
||||||
|
|
||||||
Source: [pkg/api/schema.graphql](https://github.com/dstotijn/hetty/blob/master/pkg/api/schema.graphql)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 David Stotijn
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 27 KiB |
@ -1,84 +0,0 @@
|
|||||||
# Getting Started
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Hetty compiles to a static binary, with an embedded BadgerDB database and web
|
|
||||||
admin interface.
|
|
||||||
|
|
||||||
### Install pre-built release (recommended)
|
|
||||||
|
|
||||||
👉 Downloads for Linux, macOS and Windows are available on the [releases page](https://github.com/dstotijn/hetty/releases).
|
|
||||||
|
|
||||||
### Build from source
|
|
||||||
|
|
||||||
#### Prerequisites
|
|
||||||
|
|
||||||
- [Go 1.16](https://golang.org/)
|
|
||||||
- [Yarn](https://yarnpkg.com/)
|
|
||||||
|
|
||||||
When building from source, the static resources for the admin interface
|
|
||||||
(Next.js) need to be generated via [Yarn](https://yarnpkg.com/). The generated
|
|
||||||
files will be embedded (using the [embed](https://golang.org/pkg/embed/)
|
|
||||||
package) when you use the `build` Makefile target.
|
|
||||||
|
|
||||||
Clone the repository and use the `build` make target to create a binary:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ git clone git@github.com:dstotijn/hetty.git
|
|
||||||
$ cd hetty
|
|
||||||
$ make build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
A Docker image is available on Docker Hub: [`dstotijn/hetty`](https://hub.docker.com/r/dstotijn/hetty).
|
|
||||||
For persistent storage of CA certificate and project databases, mount a volume:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ mkdir -p $HOME/.hetty
|
|
||||||
$ docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 dstotijn/hetty
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
When Hetty is started, by default it listens on `:8080` and is accessible via
|
|
||||||
[http://localhost:8080](http://localhost:8080). Depending on incoming HTTP
|
|
||||||
requests, it either acts as a MITM proxy, or it serves the API and web interface.
|
|
||||||
|
|
||||||
By default, the projects database files and CA certificates are stored in a `.hetty`
|
|
||||||
directory under the user's home directory (`$HOME` on Linux/macOS, `%USERPROFILE%`
|
|
||||||
on Windows).
|
|
||||||
|
|
||||||
To start, ensure `hetty` (downloaded from a release, or manually built) is in your
|
|
||||||
`$PATH` and run:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ hetty
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see:
|
|
||||||
|
|
||||||
```
|
|
||||||
2022/01/26 10:34:24 [INFO] Hetty (v0.3.2) is running on :8080 ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, visit [http://localhost:8080](http://localhost:8080) to get started.
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
An overview of available configuration flags:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ hetty -h
|
|
||||||
Usage of ./hetty:
|
|
||||||
-addr string
|
|
||||||
TCP address to listen on, in the form "host:port" (default ":8080")
|
|
||||||
-adminPath string
|
|
||||||
File path to admin build
|
|
||||||
-cert string
|
|
||||||
CA certificate filepath. Creates a new CA certificate if file doesn't exist (default "~/.hetty/hetty_cert.pem")
|
|
||||||
-key string
|
|
||||||
CA private key filepath. Creates a new CA private key if file doesn't exist (default "~/.hetty/hetty_key.pem")
|
|
||||||
-db string
|
|
||||||
Database directory path (default "~/.hetty/db")
|
|
||||||
```
|
|
Before Width: | Height: | Size: 144 KiB |
@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
sidebarDepth: 0
|
|
||||||
---
|
|
||||||
|
|
||||||
# Introduction
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
**Hetty** is an HTTP toolkit for security research. It aims to become an open
|
|
||||||
source alternative to commercial software like Burp Suite Pro, with powerful
|
|
||||||
features tailored to the needs of the infosec and bug bounty community.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Machine-in-the-middle (MITM) HTTP/1.1 proxy with logs
|
|
||||||
- Project based database storage (BadgerDB)
|
|
||||||
- Scope support
|
|
||||||
- Headless management API using GraphQL
|
|
||||||
- Embedded web admin interface (Next.js)
|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
Hetty is in early development. Additional features are planned
|
|
||||||
for a `v1.0` release. Please see the <a href="https://github.com/dstotijn/hetty/projects/1">backlog</a>
|
|
||||||
for details.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
|
|
||||||
[](https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty)
|
|
Before Width: | Height: | Size: 49 KiB |
@ -1,234 +0,0 @@
|
|||||||
---
|
|
||||||
sidebarDepth: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
# Modules
|
|
||||||
|
|
||||||
Hetty consists of various _modules_ that together form an HTTP toolkit. They
|
|
||||||
typically are managed via the web admin interface. Some modules expose settings
|
|
||||||
and behavior that is leveraged by other modules.
|
|
||||||
|
|
||||||
The available modules:
|
|
||||||
|
|
||||||
[[toc]]
|
|
||||||
|
|
||||||
## Projects
|
|
||||||
|
|
||||||
Projects are stored in a single BadgerDB database on disk.
|
|
||||||
They allow you organize your work, for example to split your work between research
|
|
||||||
targets.
|
|
||||||
|
|
||||||
You can create multiple projects, but only one can be open at a time. Most other
|
|
||||||
modules are useful only if you have a project opened, so creating a project is
|
|
||||||
typically the first thing you do when you start using Hetty.
|
|
||||||
|
|
||||||
### Creating a new project
|
|
||||||
|
|
||||||
When you open the Hetty admin interface after starting the program, you’ll be prompted
|
|
||||||
on the homepage to “Manage projects”, which leads to the “Projects” page where
|
|
||||||
you can open an existing project or create a new one:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
Projects are stored in a single BadgerDB database, stored in `$HOME/.hetty/db` on Linux
|
|
||||||
and macOS, and `%USERPROFILE%/.hetty/db` on Windows. You can override this path with
|
|
||||||
the `-db` flag. See: [Usage](/guide/getting-started.md#usage).
|
|
||||||
:::
|
|
||||||
|
|
||||||
### Managing projects
|
|
||||||
|
|
||||||
You can open and delete existing projects on the “Projects” page, available via
|
|
||||||
the folder icon in the menu bar.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
An opened (_active_) project is listed in green. You can close it using the “X”
|
|
||||||
button. To delete a project, use the trash bin icon.
|
|
||||||
|
|
||||||
::: danger
|
|
||||||
Deleting a project is irreversible.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Proxy
|
|
||||||
|
|
||||||
Hetty features a HTTP/1.1 proxy server with machine-in-the-middle (MITM) behavior.
|
|
||||||
For now, its only configuration is done via command line flags.
|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
Support for HTTP/2 and WebSockets are currently not supported, but this will
|
|
||||||
likely be addressed in the (near) future.
|
|
||||||
:::
|
|
||||||
|
|
||||||
### Network address
|
|
||||||
|
|
||||||
To configure the network address that the proxy listens on, use the `-addr` flag
|
|
||||||
when starting Hetty. The address needs to be in the format `[host]:port`. E.g.
|
|
||||||
`localhost:3000` or `:3000`. If the host in the address is empty or a literal
|
|
||||||
unspecified IP address, Hetty listens on all available unicast and anycast IP
|
|
||||||
addresses of the local system.
|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
When not specified with `-addr`, Hetty by default listens on `:8080`.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Example of starting Hetty, binding to port `3000` on all IPs of the local system:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ hetty -addr :3000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using the proxy
|
|
||||||
|
|
||||||
To use Hetty as an HTTP proxy server, you’ll need to configure your HTTP client (e.g.
|
|
||||||
your browser or mobile OS). Refer to your client documentation or use a search
|
|
||||||
engine to find instructions for setting a HTTP proxy.
|
|
||||||
|
|
||||||
### Certificate Authority (CA)
|
|
||||||
|
|
||||||
In order for Hetty to proxy requests going to HTTPS endpoints, a root CA certificate for
|
|
||||||
Hetty will need to be set up. Furthermore, the CA certificate needs to be
|
|
||||||
installed to the host for them to be trusted by your browser. The following steps
|
|
||||||
will cover how you can generate a certificate, provide it to Hetty, and how
|
|
||||||
you can install it in your local CA store.
|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
Certificate management features (e.g. automated installing of a root CA to your local
|
|
||||||
OS or browser trust store) are planned for a future release. In the meantime, please
|
|
||||||
use the instructions below.
|
|
||||||
:::
|
|
||||||
|
|
||||||
#### Generating a CA certificate
|
|
||||||
|
|
||||||
You can generate a CA keypair two different ways. The first is bundled directly
|
|
||||||
with Hetty, and simplifies the process immensely. The alternative is using OpenSSL
|
|
||||||
to generate them, which provides more control over expiration time and cryptography
|
|
||||||
used, but requires you install the OpenSSL tooling. The first is suggested for any
|
|
||||||
beginners trying to get started.
|
|
||||||
|
|
||||||
#### Generating CA certificates with hetty
|
|
||||||
|
|
||||||
Hetty will generate the default key and certificate on its own if none are supplied
|
|
||||||
or found in `~/.hetty/` when first running the CLI. To generate a default key and
|
|
||||||
certificate with hetty, simply run the command with no arguments.
|
|
||||||
|
|
||||||
You should now have a key and certificate located at `~/.hetty/hetty_key.pem` and
|
|
||||||
`~/.hetty/hetty_cert.pem` respectively.
|
|
||||||
|
|
||||||
#### Generating CA certificates with OpenSSL
|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
This following instructions are for Linux but should provide guidance for Windows
|
|
||||||
and macOS as well.
|
|
||||||
:::
|
|
||||||
|
|
||||||
You can start off by generating a new key and CA certificate which will both expire
|
|
||||||
after a month.
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
$ mkdir ~/.hetty
|
|
||||||
$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 31 -keyout ~/.hetty/hetty_key.pem -out ~/.hetty/hetty_cert.pem
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The default location which `hetty` will check for the key and CA certificate is under
|
|
||||||
`~/.hetty/`, at `hetty_key.pem` and `hetty_cert.pem` respectively. You can move them
|
|
||||||
here and `hetty` will detect them automatically. Otherwise, you can specify the
|
|
||||||
location of these as arguments to `hetty`.
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
\$ hetty -key /some/directory/key.pem -cert /some/directory/cert.pem
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Trusting the CA certificate
|
|
||||||
|
|
||||||
In order for your browser to allow traffic to the local Hetty proxy, you may need
|
|
||||||
to install these certificates to your local CA store.
|
|
||||||
|
|
||||||
On Ubuntu, you can update your local CA store with the certificate by running the
|
|
||||||
following commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
$ sudo cp ~/.hetty/hetty_cert.pem /usr/local/share/ca-certificates/hetty.crt
|
|
||||||
$ sudo update-ca-certificates
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
On Windows, you would add your certificate by using the Certificate Manager,
|
|
||||||
which you can run via:
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
certmgr.msc
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
On macOS, you can add your certificate by using the Keychain Access program. This
|
|
||||||
can be found under `Application/Utilities/Keychain Access.app`. After opening this,
|
|
||||||
drag the certificate into the app. Next, open the certificate in the app, enter the
|
|
||||||
_Trust_ section, and under _When using this certificate_ select _Always Trust_.
|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
Various Linux distributions may require other steps or commands for updating
|
|
||||||
their certificate authority. See the documentation relevant to your distribution for
|
|
||||||
more information on how to update the system to trust your self-signed certificate.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
The scope module lets you define _rules_ that other modules can use to control
|
|
||||||
their behavior. For example, the [proxy logs module](#proxy-logs) can be configured to only
|
|
||||||
show logs for in-scope requests; meaning only requests are shown that match one
|
|
||||||
or more scope rules.
|
|
||||||
|
|
||||||
### Managing scope rules
|
|
||||||
|
|
||||||
You can manage scope rules via the “Scope” page, available via the crosshair icon
|
|
||||||
in the menu bar.
|
|
||||||
|
|
||||||
A rule consists of a _type_ and a regular expression ([RE2 syntax](https://github.com/google/re2/wiki/Syntax)).
|
|
||||||
The only supported type at the moment is “URL”.
|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
Just like all module configuration, scope rules are defined and stored per-project.
|
|
||||||
:::
|
|
||||||
|
|
||||||
#### Adding a rule
|
|
||||||
|
|
||||||
On the ”Scope” page, enter a regular expression and click “Add rule”:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
_Example: Rule that matches URLs with `example.com` (or any subdomain) on any path._
|
|
||||||
|
|
||||||
#### Deleting rules
|
|
||||||
|
|
||||||
Use the trash icon next to an existing scope rule to delete it.
|
|
||||||
|
|
||||||
## Proxy logs
|
|
||||||
|
|
||||||
You can few logs captured by the Proxy module on the Proxy logs page, available
|
|
||||||
via the proxy icon in the menu bar.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Showing a log entry
|
|
||||||
|
|
||||||
Click a row in the overview table to view log details in the bottom request and
|
|
||||||
response panes. When a request and/or response has a body, it's shown below the
|
|
||||||
HTTP headers. Header keys and values can be copied to clipboard by clicking them.
|
|
||||||
|
|
||||||
### Filtering logs
|
|
||||||
|
|
||||||
To only show log entries that match any of the [scope rules](#scope), click the
|
|
||||||
filter icon in the search bar and select “Only show in-scope requests”:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
::: tip INFO
|
|
||||||
At the moment of writing (`v0.2.0`), text based search is not implemented yet.
|
|
||||||
:::
|
|
Before Width: | Height: | Size: 410 KiB |
@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
home: true
|
|
||||||
heroImage: https://hetty.xyz/assets/logo.png
|
|
||||||
actionText: Read the docs →
|
|
||||||
actionLink: /guide/
|
|
||||||
---
|
|