@use 'sass:math'
@use '../../styles/settings'
@use '../../styles/tools'
@use './variables' as *

// Theme
.v-expansion-panel
  background-color: $expansion-panel-background-color
  color: $expansion-panel-color

  &:not(:first-child)::after
    border-color: $expansion-panel-border-color

  &--disabled
    .v-expansion-panel-title
      color: $expansion-panel-disabled-color

      .v-expansion-panel-title__overlay
        // This is multiplied by the text opacity,
        // so we need to divide it to get the desired value
        // TODO: Should disabled be part of states mixin?
        opacity: math.div($expansion-panel-disabled-overlay, $expansion-panel-disabled-opacity)

// Block
.v-expansion-panels
  display: flex
  flex-wrap: wrap
  justify-content: center
  list-style-type: none
  padding: 0
  width: 100%
  position: relative
  z-index: 1

  &:not(&--variant-accordion)
    > :not(:first-child):not(:last-child):not(.v-expansion-panel--active):not(.v-expansion-panel--before-active)
      border-bottom-left-radius: 0 !important
      border-bottom-right-radius: 0 !important

    > :not(:first-child):not(:last-child):not(.v-expansion-panel--active):not(.v-expansion-panel--after-active)
      border-top-left-radius: 0 !important
      border-top-right-radius: 0 !important

    > :first-child:not(.v-expansion-panel--active):not(.v-expansion-panel--before-active)
      border-bottom-left-radius: 0 !important
      border-bottom-right-radius: 0 !important

    > :last-child:not(.v-expansion-panel--active):not(.v-expansion-panel--after-active)
      border-top-left-radius: 0 !important
      border-top-right-radius: 0 !important

  &--variant-accordion
    > :first-child
      border-bottom-left-radius: 0 !important
      border-bottom-right-radius: 0 !important

    > :last-child
      border-top-left-radius: 0 !important
      border-top-right-radius: 0 !important

      .v-expansion-panel-title--active
        border-bottom-left-radius: initial
        border-bottom-right-radius: initial

    > :not(:first-child):not(:last-child)
      border-radius: 0 !important

    .v-expansion-panel-title__overlay
      transition: 0.3s border-radius settings.$standard-easing

// Element
.v-expansion-panel
  flex: 1 0 100%
  max-width: 100%
  position: relative
  transition: .3s all settings.$standard-easing
  transition-property: margin-top, border-radius, border, max-width
  border-radius: $expansion-panel-border-radius

  &:not(:first-child)::after
    border-top-style: solid
    border-top-width: thin
    content: ''
    left: 0
    position: absolute
    right: 0
    top: 0
    transition: 0.3s opacity settings.$standard-easing

  &--disabled
    .v-expansion-panel-title
      pointer-events: none

  &--active
    &:not(:first-child),
    + .v-expansion-panel
      margin-top: $expansion-panel-active-margin

      &::after
        opacity: 0

    > .v-expansion-panel-title
      border-bottom-left-radius: 0
      border-bottom-right-radius: 0
      min-height: $expansion-panel-active-title-min-height

.v-expansion-panel__shadow
  @include tools.absolute()
  @include tools.elevation(2)
  border-radius: inherit
  z-index: -1

.v-expansion-panel-title
  align-items: center
  text-align: start
  border-radius: inherit
  display: flex
  font-size: $expansion-panel-title-font-size
  line-height: 1
  min-height: $expansion-panel-title-min-height
  outline: none
  padding: $expansion-panel-title-padding
  position: relative
  transition: .3s min-height settings.$standard-easing
  width: 100%
  justify-content: space-between
  @include tools.states('.v-expansion-panel-title__overlay', true)

  &--active::before
    opacity: 0.12

.v-expansion-panel-title__overlay
  @include tools.absolute()
  background-color: currentColor
  border-radius: inherit
  opacity: 0

.v-expansion-panel-title__icon
  display: inline-flex
  margin-bottom: -4px
  margin-top: -4px
  user-select: none
  margin-inline-start: auto

.v-expansion-panel-text
  display: flex

  &__wrapper
    padding: $expansion-panel-text-padding
    flex: 1 1 auto
    max-width: 100%

// Variants
.v-expansion-panels--variant-accordion
  > .v-expansion-panel
    margin-top: 0

    &::after
      opacity: 1

.v-expansion-panels--variant-popout
  > .v-expansion-panel
    max-width: $expansion-panel-popout-max-width

    &--active
      max-width: $expansion-panel-popout-active-max-width

.v-expansion-panels--variant-inset
  > .v-expansion-panel
    max-width: $expansion-panel-inset-max-width

    &--active
      max-width: $expansion-panel-inset-active-max-width


export { VSystemBar } from './VSystemBar'


// Defaults
$pagination-item-margin: .3rem !default;


function blockScrollStrategy (data: ScrollStrategyData, props: StrategyProps) {

const offsetParent = data.root.value?.offsetParent

// Utilities
import { convertToUnit, getScrollParents, hasScrollbar, IN_BROWSER, propsFactory } from '@/util'
import { effectScope, nextTick, onScopeDispose, watchEffect } from 'vue'
import { requestNewFrame } from './requestNewFrame'

// Types
import type { EffectScope, PropType, Ref } from 'vue'

export interface ScrollStrategyData {
  root: Ref<HTMLElement | undefined>
  contentEl: Ref<HTMLElement | undefined>
  activatorEl: Ref<HTMLElement | undefined>
  isActive: Ref<boolean>
  updateLocation: Ref<((e: Event) => void) | undefined>
}

const scrollStrategies = {
  none: null,
  close: closeScrollStrategy,
  block: blockScrollStrategy,
  reposition: repositionScrollStrategy,
}

interface StrategyProps {
  scrollStrategy: keyof typeof scrollStrategies | ((data: ScrollStrategyData) => void)
}

export const makeScrollStrategyProps = propsFactory({
  scrollStrategy: {
    type: [String, Function] as PropType<StrategyProps['scrollStrategy']>,
    default: 'block',
    validator: (val: any) => typeof val === 'function' || val in scrollStrategies,
  },
})

export function useScrollStrategies (
  props: StrategyProps,
  data: ScrollStrategyData
) {
  if (!IN_BROWSER) return

  let scope: EffectScope | undefined
  watchEffect(async () => {
    scope?.stop()

    if (!(data.isActive.value && props.scrollStrategy)) return

    scope = effectScope()
    await nextTick()
    scope.run(() => {
      if (typeof props.scrollStrategy === 'function') {
        props.scrollStrategy(data)
      } else {
        scrollStrategies[props.scrollStrategy]?.(data)
      }
    })
  })
}

function closeScrollStrategy (data: ScrollStrategyData) {
  function onScroll (e: Event) {
    data.isActive.value = false
  }

  bindScroll(data.activatorEl.value ?? data.contentEl.value, onScroll)
}

function blockScrollStrategy (data: ScrollStrategyData) {
  const scrollElements = [...new Set([
    ...getScrollParents(data.activatorEl.value),
    ...getScrollParents(data.contentEl.value),
  ])].filter(el => !el.classList.contains('v-overlay-scroll-blocked'))
  const scrollbarWidth = window.innerWidth - document.documentElement.offsetWidth

  const scrollableParent = (el => hasScrollbar(el) && el)(data.root.value?.offsetParent || document.documentElement)
  if (scrollableParent) {
    data.root.value!.classList.add('v-overlay--scroll-blocked')
  }

  scrollElements.forEach((el, i) => {
    el.style.setProperty('--v-body-scroll-x', convertToUnit(-el.scrollLeft))
    el.style.setProperty('--v-body-scroll-y', convertToUnit(-el.scrollTop))
    el.style.setProperty('--v-scrollbar-offset', convertToUnit(scrollbarWidth))
    el.classList.add('v-overlay-scroll-blocked')
  })

  onScopeDispose(() => {
    scrollElements.forEach((el, i) => {
      const x = parseFloat(el.style.getPropertyValue('--v-body-scroll-x'))
      const y = parseFloat(el.style.getPropertyValue('--v-body-scroll-y'))

      el.style.removeProperty('--v-body-scroll-x')
      el.style.removeProperty('--v-body-scroll-y')
      el.style.removeProperty('--v-scrollbar-offset')
      el.classList.remove('v-overlay-scroll-blocked')

      el.scrollLeft = -x
      el.scrollTop = -y
    })
    if (scrollableParent) {
      data.root.value!.classList.remove('v-overlay--scroll-blocked')
    }
  })
}

function repositionScrollStrategy (data: ScrollStrategyData) {
  let slow = false
  let raf = -1

  function update (e: Event) {
    requestNewFrame(() => {
      const start = performance.now()
      data.updateLocation.value?.(e)
      const time = performance.now() - start
      slow = time / (1000 / 60) > 2
    })
  }

  bindScroll(data.activatorEl.value ?? data.contentEl.value, e => {
    if (slow) {
      // If the position calculation is slow,
      // defer updates until scrolling is finished.
      // Browsers usually fire one scroll event per frame so
      // we just wait until we've got two frames without an event
      cancelAnimationFrame(raf)
      raf = requestAnimationFrame(() => {
        raf = requestAnimationFrame(() => {
          update(e)
        })
      })
    } else {
      update(e)
    }
  })
}

/** @private */
function bindScroll (el: HTMLElement | undefined, onScroll: (e: Event) => void) {
  const scrollElements = [document, ...getScrollParents(el)]
  scrollElements.forEach(el => {
    el.addEventListener('scroll', onScroll, { passive: true })
  })

  onScopeDispose(() => {
    scrollElements.forEach(el => {
      el.removeEventListener('scroll', onScroll)
    })
  })
}


<template>
  <v-container>
    <v-select
      v-model="selectionType"
      :items="['leaf', 'independent']"
      label="Selection type"
    ></v-select>
    <v-row>
      <v-col>
        <v-treeview
          v-model="selection"
          :items="items"
          :selection-type="selectionType"
          selectable
          return-object
          open-all
        ></v-treeview>
      </v-col>
      <v-divider vertical></v-divider>
      <v-col
        class="pa-6"
        cols="6"
      >
        <template v-if="!selection.length">
          No nodes selected.
        </template>
        <template v-else>
          <div
            v-for="node in selection"
            :key="node.id"
          >
            {{ node.name }}
          </div>
        </template>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
  export default {
    data: () => ({
      selectionType: 'leaf',
      selection: [],
      items: [
        {
          id: 1,
          name: 'Root',
          children: [
            { id: 2, name: 'Child #1' },
            { id: 3, name: 'Child #2' },
            {
              id: 4,
              name: 'Child #3',
              children: [
                { id: 5, name: 'Grandchild #1' },
                { id: 6, name: 'Grandchild #2' },
              ],
            },
          ],
        },
      ],
    }),
  }
</script>


// Composables
import { useHydration } from '@/composables/hydration'

// Utilities
import { defineComponent } from '@/util'
import { ref } from 'vue'

export const VNoSsr = defineComponent({
  name: 'VNoSsr',

  setup (_, { slots }) {
    const show = ref(false)

    useHydration(() => (show.value = true))

    return () => show.value && slots.default?.()
  },
})


module.exports = {
  'v-app-bar': {
    slots: [
      {
        name: 'default',
        props: undefined,
      },
      {
        name: 'extension',
        props: undefined,
      },
      {
        name: 'img',
        props: {
          props: '{ height: string, src: string | srcObject }',
        },
      },
    ],
  },
}


const scrollbarWidth = window.innerWidth - document.documentElement.offsetWidth

...getScrollParents(data.activatorEl.value, props.contained ? offsetParent : undefined),

...getScrollParents(data.contentEl.value, props.contained ? offsetParent : undefined),

export function getScrollParent (el?: HTMLElement) {
  while (el) {
    if (hasScrollbar(el)) return el
    el = el.parentElement!
  }

  return document.scrollingElement as HTMLElement
}

export function getScrollParents (el?: HTMLElement) {
  const elements: HTMLElement[] = []
  while (el) {
    if (hasScrollbar(el)) elements.push(el)
    el = el.parentElement!
  }

  return elements
}

export function hasScrollbar (el?: Element | null) {
  if (!el || el.nodeType !== Node.ELEMENT_NODE) return false

  const style = window.getComputedStyle(el)
  return style.overflowY === 'scroll' || (style.overflowY === 'auto' && el.scrollHeight > el.clientHeight)
}


<template>

      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

</template>


<template>
  <v-switch
    v-model="model"
    hide-details
    inset
    :label="`Switch: ${model.toString()}`"
  ></v-switch>
</template>

<script>
  export default {
    data () {
      return {
        model: true,
      }
    },
  }
</script>


Options that are passed to the [Intersection observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) constructor.

Sets the component transition. Can be one of the [built in transitions](/styles/transitions) or one your own.


const scrollableParent = (el => hasScrollbar(el) && el)(data.root.value?.offsetParent || document.documentElement)

keep keep keep replace keep keep keep keep keep

const scrollableParent = (el => hasScrollbar(el) && el)(offsetParent || document.documentElement)


<template>
  <v-timeline direction="horizontal">
    <v-timeline-item>
      <template v-slot:opposite>
        Opposite content
      </template>

Content title

          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    </v-timeline-item>

    <v-timeline-item>
      <template v-slot:opposite>
        Opposite content
      </template>

Content title

          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    </v-timeline-item>

    <v-timeline-item>
      <template v-slot:opposite>
        Opposite content
      </template>

Content title

          Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

    </v-timeline-item>
  </v-timeline>
</template>


Emitted when the input is changed by user interaction


return document.scrollingElement as HTMLElement

export function getScrollParents (el?: HTMLElement) {

export function getScrollParents (el?: Element | null, stopAt?: Element | null) {


// Directives
import Touch from '../'

// Types
import { nextTick } from 'vue'
import type { TouchValue } from '../'

// Utilities
import { mount } from '@vue/test-utils'
import { describe, expect, it } from '@jest/globals'
import { touch } from '@/../test'

describe('v-touch', () => {
  const mountFunction = (value: TouchValue): Element => {
    const wrapper = mount({
      directives: { Touch },
      props: {
        value: Object,
      },
      template: '
',
    }, { props: { value } })

    return wrapper.element
  }

  it('should call directive handlers', () => {
    const down = jest.fn()
    touch(mountFunction({ down })).start(0, 0).end(0, 20)
    expect(down).toHaveBeenCalled()

    const up = jest.fn()
    touch(mountFunction({ up })).start(0, 0).end(0, -20)
    expect(up).toHaveBeenCalled()

    const left = jest.fn()
    touch(mountFunction({ left })).start(0, 0).end(-20, 0)
    expect(left).toHaveBeenCalled()

    const right = jest.fn()
    touch(mountFunction({ right })).start(0, 0).end(20, 0)
    expect(right).toHaveBeenCalled()

    const start = jest.fn()
    touch(mountFunction({ start })).start(0, 0)
    expect(start).toHaveBeenCalled()

    const move = jest.fn()
    touch(mountFunction({ move })).move(0, 0)
    expect(move).toHaveBeenCalled()

    const end = jest.fn()
    touch(mountFunction({ end })).end(0, 0)
    expect(end).toHaveBeenCalled()
  })

  it('should call directive handlers if not straight down/up/right/left', async () => {
    const nope = jest.fn()
    const down = jest.fn()
    touch(mountFunction({ down, right: nope })).start(0, 0).end(5, 20)
    expect(nope).not.toHaveBeenCalled()
    expect(down).toHaveBeenCalled()
  })

  it('should not call directive handlers if distance is too small', async () => {
    const down = jest.fn()
    touch(mountFunction({ down })).start(0, 0).end(0, 10)
    expect(down).not.toHaveBeenCalled()

    const up = jest.fn()
    touch(mountFunction({ up })).start(0, 0).end(0, -10)
    expect(up).not.toHaveBeenCalled()

    const left = jest.fn()
    touch(mountFunction({ left })).start(0, 0).end(-10, 0)
    expect(left).not.toHaveBeenCalled()

    const right = jest.fn()
    touch(mountFunction({ right })).start(0, 0).end(10, 0)
    expect(right).not.toHaveBeenCalled()
  })

  it('should unmount', async () => {
    const start = jest.fn()
    const wrapper = mount({
      directives: { Touch },
      props: {
        value: Object,
        bound: Boolean,
      },
      template: '

',
    }, {
      props: {
        value: { start },
        bound: true,
      },
    })
    const el = wrapper.element

    await nextTick()
    await wrapper.setProps({ bound: false })

    touch(el).start(0, 0)
    expect(start.mock.calls).toHaveLength(0)
  })
})


// Icons
import { mdi } from '@/iconsets/mdi'

// Types
import type { Blueprint } from '@/framework'

export const md1: Blueprint = {
  defaults: {
    global: {
      rounded: 'sm',
    },
    VAutocomplete: {
      variant: 'underlined',
    },
    VBanner: {
      color: 'primary',
    },
    VBtn: {
      color: 'primary',
      rounded: 0,
    },
    VCheckbox: {
      color: 'secondary',
    },
    VCombobox: {
      variant: 'underlined',
    },
    VSelect: {
      variant: 'underlined',
    },
    VSlider: {
      color: 'primary',
    },
    VTabs: {
      color: 'primary',
    },
    VTextarea: {
      variant: 'underlined',
    },
    VTextField: {
      variant: 'underlined',
    },
  },
  icons: {
    defaultSet: 'mdi',
    sets: {
      mdi,
    },
  },
  theme: {
    themes: {
      light: {
        colors: {
          primary: '#3F51B5',
          'primary-darken-1': '#303F9F',
          'primary-lighten-1': '#C5CAE9',
          secondary: '#FF4081',
          'secondary-darken-1': '#F50057',
          'secondary-lighten-1': '#FF80AB',
          accent: '#009688',
        },
      },
    },
  },
}


<template>
  <v-navigation-drawer
    id="app-toc"
    color="background"
    floating
    sticky
    location="right"
    width="256"
  >
    <template
      v-if="toc?.length"
      #prepend
    >
      <app-headline
        class="mt-4 mb-2 ml-4"
        path="contents"
      />
    </template>

    <ul class="ml-5">
      <router-link
        v-for="{ to, level, text } in toc"
        v-slot="{ href }"
        :key="text"
        :to="to"
        custom
      >
        <li
          :class="[
            'pl-3 text-body-2 py-1 font-weight-regular',
            {
              'text-primary router-link-active': route.hash === to,
              'text-grey': route.hash !== to,
              'pl-6': level === 3,
              'pl-9': level === 4,
              'pl-12': level === 5,
            }
          ]"
        >
          <a
            :href="href"
            class="v-toc-link d-block transition-swing text-decoration-none"
            @click.prevent.stop="onClick(to)"
            v-text="text"
          />
        </li>
      </router-link>
    </ul>

    <template #append>
      <v-container>
        <app-headline
          v-if="sponsors.length"
          class="mb-1 mt-n1"
          path="sponsors"
          size="subtitle-1"
        />

        <v-row dense>
          <v-col
            v-for="sponsor of sponsors"
            :key="sponsor.slug"
            :cols="sponsor.metadata.tier === -1 ? 12 : 6"
            class="d-inline-flex"
          >
            <sponsor-card
              :max-height="sponsor.metadata.tier === -1 ? 52 : 40"
              :sponsor="sponsor"
              :color="dark ? undefined : 'grey-lighten-5'"
            />
          </v-col>

          <v-col cols="12">
            <sponsor-link block size="large" />
          </v-col>

          <v-col cols="12">
            <carbon />
          </v-col>
        </v-row>
      </v-container>
    </template>
  </v-navigation-drawer>
</template>

<script lang="ts">
  // Components
  import SponsorCard from '@/components/sponsor/Card.vue'
  import SponsorLink from '@/components/sponsor/Link.vue'

  // Composables
  import { RouteLocation, Router, useRoute, useRouter } from 'vue-router'
  import { useSponsorsStore } from '../../store/sponsors'
  import { useTheme } from 'vuetify'

  // Utilities
  import { computed, defineComponent, onBeforeMount, ref } from 'vue'

  type TocItem = {
    to: string;
    text: string;
    level: number;
  }

  function useUpdateHashOnScroll (route: RouteLocation, router: Router) {
    const scrolling = ref(false)
    let offsets: number[] = []
    let timeout: any = 0

    function calculateOffsets () {
      const offsets = []
      const toc = route.meta.toc as any[]

      for (const item of toc.slice().reverse()) {
        const section = document.getElementById(item.to.slice(1))

        if (!section) continue

        offsets.push(section.offsetTop - 48)
      }

      return offsets
    }

    async function findActiveHash () {
      const toc = route.meta.toc as any[]
      // if (this.$vuetify.breakpoint.mobile) return

      const currentOffset = (
        window.pageYOffset ||
        document.documentElement.offsetTop ||
        0
      )

      // If we are at the top of the page
      // reset the offset
      if (currentOffset === 0) {
        if (route.hash) {
          router.replace({ path: route.path })
        }

        return
      }

      if (
        offsets.length !== toc.length
      ) {
        offsets = calculateOffsets()
      }

      const index = offsets.findIndex(offset => {
        return offset < currentOffset
      })

      let tindex = index > -1
        ? offsets.length - 1 - index
        : 0

      if (currentOffset + window.innerHeight === document.documentElement.offsetHeight) {
        tindex = toc.length - 1
      }

      const hash = toc[tindex].to

      if (hash === route.hash) return

      scrolling.value = true

      await router.replace({
        path: route.path,
        hash,
      })

      scrolling.value = false
    }

    function onScroll () {
      const toc = route.meta.toc as any[]

      clearTimeout(timeout)

      if (
        scrolling.value ||
        !toc.length
      ) return

      timeout = setTimeout(findActiveHash, 17)
    }

    return { onScroll, scrolling }
  }

  export default defineComponent({
    name: 'AppToc',

    components: {
      SponsorCard,
      SponsorLink,
    },

    setup () {
      const route = useRoute()
      const router = useRouter()
      const theme = useTheme()

      const { onScroll, scrolling } = useUpdateHashOnScroll(route, router)

      async function onClick (hash: string) {
        if (route.hash === hash) return

        scrolling.value = true

        router.replace({ path: route.path, hash })

        // await this.$vuetify.goTo(hash)
        // await wait(200)

        scrolling.value = false
      }

      const sponsorStore = useSponsorsStore()

      onBeforeMount(async () => sponsorStore.load())

      return {
        toc: computed(() => route.meta.toc as TocItem[]),
        onClick,
        onScroll,
        sponsors: computed(() => (
          sponsorStore.sponsors
            .filter(sponsor => sponsor.metadata.tier <= 2)
            .sort((a, b) => {
              const aTier = a.metadata.tier
              const bTier = b.metadata.tier

              return aTier === bTier ? 0 : aTier > bTier ? 1 : -1
            })
        )),
        dark: computed(() => theme.current.value.dark),
        route,
      }
    },
  })
</script>

<style lang="sass">
  #app-toc
    ul
      list-style-type: none

    li
      border-left: 2px solid #E5E5E5

      &.router-link-active
        border-left-color: currentColor

    .v-toc-link
      color: inherit

    &.theme--dark
      li:not(.router-link-active)
        border-left-color: rgba(255, 255, 255, 0.5)

    .v-navigation-drawer__content
      height: auto
      margin-right: 12px
</style>


const elements: HTMLElement[] = []
while (el) {

if (stopAt && el && !stopAt.contains(el)) return elements


<template>
  <div class="text-center">
    <v-chip
      class="ma-2"
      color="success"
      variant="outlined"
    >
      <v-icon start icon="mdi-server-plus"></v-icon>
      Server Status
    </v-chip>

    <v-chip
      class="ma-2"
      color="primary"
      variant="outlined"
    >
      User Account
      <v-icon end icon="mdi-account-outline"></v-icon>
    </v-chip>
  </div>
</template>


<template>
  <v-table theme="dark">

          Name

          Calories

        {{ item.name }}
        {{ item.calories }}

  </v-table>
</template>


<template>
  <v-row align="center">
    <v-checkbox
      v-model="landscape"
      label="Landscape"
    ></v-checkbox>
    <v-date-picker
      v-model="picker"
      :landscape="landscape"
      type="month"
    ></v-date-picker>
  </v-row>
</template>

<script>
  export default {
    data () {
      return {
        picker: new Date().toISOString().substr(0, 7),
        landscape: false,
      }
    },
  }
</script>


if (hasScrollbar(el)) elements.push(el as HTMLElement)


<template>
  <v-row justify="center">
    <v-col
      cols="12"
      sm="8"
      md="6"
    >
      <v-card>
        <v-toolbar
          color="orange lighten-1"
          dark
        >
          <v-app-bar-nav-icon></v-app-bar-nav-icon>

          <v-toolbar-title>Message Board</v-toolbar-title>

          <v-spacer></v-spacer>

          <v-btn icon>
            <v-icon>mdi-magnify</v-icon>
          </v-btn>
        </v-toolbar>

        <v-list lines="two">
          <template v-for="(item, index) in items">
            <v-subheader
              v-if="item.header"
              :key="item.header"
              inset
            >
              {{
                item.header
              }}
            </v-subheader>

            <v-divider
              v-else-if="item.divider"
              :key="index"
              inset
            ></v-divider>

            <v-list-item
              v-else
              :key="item.title"
              :prepend-avatar="item.avatar"
              ripple
            >
              <template v-slot:title>

              </template>
              <template v-slot:subtitle>

              </template>
            </v-list-item>
          </template>
        </v-list>
      </v-card>
    </v-col>
  </v-row>
</template>


const vuetifyPackage = require('./package.json')

module.exports = {
  assumptions: {
    noDocumentAll: true
  },
  ignore: [/\.d\.ts$/],
  presets: [
    ['@babel/preset-env', {
      modules: false,
    }],
    '@babel/preset-typescript',
  ],
  plugins: [
    ['@vue/babel-plugin-jsx', { optimize: false, enableObjectSlots: false }],
    ['transform-define', {
      __VUETIFY_VERSION__: vuetifyPackage.version,
      __REQUIRED_VUE__: vuetifyPackage.peerDependencies.vue,
    }],
    ['module-resolver', {
      root: ['.'],
      alias: {
        '@': './src',
      },
    }],
  ],
  env: {
    test: {
      presets: [
        ['@babel/preset-env', {
          targets: { node: true },
          modules: 'commonjs',
        }],
      ],
    },
    lib: {
      ignore: ['**/__tests__'],
      plugins: [
        ['babel-plugin-add-import-extension', { extension: 'mjs' }],
        ['./build/babel-plugin-replace-import-extension', { extMapping: {
          '.sass': '.css',
          '.scss': '.css',
        }}],
      ],
    },
  },
}


/* eslint-disable max-statements */
/* eslint-disable no-labels */

// Utilities
import { getPropertyFromItem, propsFactory, wrapInArray } from '@/util'
import { computed, unref } from 'vue'

// Types
import type { PropType, Ref } from 'vue'
import type { MaybeRef } from '@/util'
import type { InternalItem } from './items'

/**
 * - match without highlight
 * - single match (index), length already known
 * - single match (start, end)
 * - multiple matches (start, end), probably shouldn't overlap
 */
export type FilterMatch = boolean | number | [number, number] | [number, number][]
export type FilterFunction = (value: string, query: string, item?: any) => FilterMatch
export type FilterKeyFunctions = Record<string, FilterFunction>
export type FilterKeys = string | string[]
export type FilterMode = 'some' | 'every' | 'union' | 'intersection'

export interface FilterProps {
  customFilter?: FilterFunction
  customKeyFilter?: FilterKeyFunctions
  filterKeys?: FilterKeys
  filterMode?: FilterMode
  noFilter?: boolean
}

// Composables
export const defaultFilter: FilterFunction = (value, query, item) => {
  if (value == null || query == null) return -1

  return value.toString().toLocaleLowerCase().indexOf(query.toString().toLocaleLowerCase())
}

export const makeFilterProps = propsFactory({
  customFilter: Function as PropType<FilterFunction>,
  customKeyFilter: Object as PropType<FilterKeyFunctions>,
  filterKeys: [Array, String] as PropType<FilterKeys>,
  filterMode: {
    type: String as PropType<FilterMode>,
    default: 'intersection',
  },
  noFilter: Boolean,
}, 'filter')

export function filterItems (
  items: any[],
  query: string,
  options?: {
    customKeyFilter?: FilterKeyFunctions
    default?: FilterFunction
    filterKeys?: FilterKeys
    filterMode?: FilterMode
    noFilter?: boolean
  },
) {
  const array: { index: number, matches: Record<string, FilterMatch> }[] = []
  // always ensure we fall back to a functioning filter
  const filter = options?.default ?? defaultFilter
  const keys = options?.filterKeys ? wrapInArray(options.filterKeys) : false
  const customFiltersLength = Object.keys(options?.customKeyFilter ?? {}).length

  if (!items?.length) return array

  loop:
  for (let i = 0; i < items.length; i++) {
    const item = items[i]
    const customMatches: Record<string, FilterMatch> = {}
    const defaultMatches: Record<string, FilterMatch> = {}
    let match: FilterMatch = -1

    if (query && !options?.noFilter) {
      if (typeof item === 'object') {
        const filterKeys = keys || Object.keys(item)

        for (const key of filterKeys) {
          const value = getPropertyFromItem(item as any, key, item)
          const keyFilter = options?.customKeyFilter?.[key]

          match = keyFilter
            ? keyFilter(value, query, item)
            : filter(value, query, item)

          if (match !== -1 && match !== false) {
            if (keyFilter) customMatches[key] = match
            else defaultMatches[key] = match
          } else if (options?.filterMode === 'every') {
            continue loop
          }
        }
      } else {
        match = filter(item, query, item)
        if (match !== -1 && match !== false) {
          defaultMatches.title = match
        }
      }

      const defaultMatchesLength = Object.keys(defaultMatches).length
      const customMatchesLength = Object.keys(customMatches).length

      if (!defaultMatchesLength && !customMatchesLength) continue

      if (
        options?.filterMode === 'union' &&
        customMatchesLength !== customFiltersLength &&
        !defaultMatchesLength
      ) continue

      if (
        options?.filterMode === 'intersection' &&
        (
          customMatchesLength !== customFiltersLength ||
          !defaultMatchesLength
        )
      ) continue
    }

    array.push({ index: i, matches: { ...defaultMatches, ...customMatches } })
  }

  return array
}

export function useFilter (
  props: FilterProps,
  items: MaybeRef<InternalItem[]>,
  query?: Ref<string | undefined>,
) {
  const strQuery = computed(() => (
    typeof query?.value !== 'string' &&
    typeof query?.value !== 'number'
  ) ? '' : String(query.value))

  const filteredItems = computed(() => {
    const transformedItems = unref(items)
    const matches = filterItems(
      transformedItems,
      strQuery.value,
      {
        customKeyFilter: props.customKeyFilter,
        default: props.customFilter,
        filterKeys: props.filterKeys,
        filterMode: props.filterMode,
        noFilter: props.noFilter,
      },
    )

    return matches.map(({ index, matches }) => ({
      item: transformedItems[index],
      matches,
    }))
  })

  return { filteredItems }
}


node-version: ${{ matrix.node-version }}

restore-keys: ${{ runner.OS }}-npm-cache

keep keep keep keep keep replace replace keep keep keep

restore-keys: ${{ runner.os }}-npm-cache

os:
  - linux

language: node_js

addons:
  apt:
    packages:
    - time

cache:
  npm: true

node_js:
  - "10"
  - "12"
  - "14"

script:
  - if [[ $TRAVIS_OS_NAME == "linux" ]]; then
      npm run eslint;
    fi
  - npm run test-cov
  - if [[ $TRAVIS_OS_NAME == "linux" ]]; then
      ./test/benchmark.sh;
    fi

after_script:
  - npm install coveralls
  - nyc report --reporter=text-lcov | coveralls

deploy:
  provider: surge
  project: ./0x/
  skip_cleanup: true
  domain: "${TRAVIS_COMMIT}-${TRAVIS_NODE_VERSION}-hexo.surge.sh"
  on:
    all_branches: true


'use strict';

const { join, sep } = require('path');
const { appendFile, mkdir, mkdirs, rename, rmdir, stat, unlink, writeFile } = require('hexo-fs');
const Promise = require('bluebird');
const { hash, Pattern } = require('hexo-util');
const { spy, match, assert: sinonAssert } = require('sinon');

describe('Box', () => {
  const Hexo = require('../../../lib/hexo');
  const baseDir = join(__dirname, 'box_tmp');
  const Box = require('../../../lib/box');

  const newBox = (path, config) => {
    const hexo = new Hexo(baseDir, { silent: true });
    hexo.config = Object.assign(hexo.config, config);
    const base = path ? join(baseDir, path) : baseDir;
    return new Box(hexo, base);
  };

  before(() => mkdir(baseDir));

  after(() => rmdir(baseDir));

  it('constructor - add trailing "/" to the base path', () => {
    const box = newBox('foo');
    box.base.should.eql(join(baseDir, 'foo') + sep);
  });

  it('addProcessor() - no pattern', () => {
    const box = newBox();

    box.addProcessor(() => 'test');

    const p = box.processors[0];

    p.pattern.match('').should.eql({});
    p.process().should.eql('test');
  });

  it('addProcessor() - with regex', () => {
    const box = newBox();

    box.addProcessor(/^foo/, () => 'test');

    const p = box.processors[0];

    p.pattern.match('foobar').should.be.ok;
    p.pattern.should.be.an.instanceof(Pattern);
    p.process().should.eql('test');
  });

  it('addProcessor() - with pattern', () => {
    const box = newBox();

    box.addProcessor(new Pattern(/^foo/), () => 'test');

    const p = box.processors[0];

    p.pattern.match('foobar').should.be.ok;
    p.pattern.should.be.an.instanceof(Pattern);
    p.process().should.eql('test');
  });

  it('addProcessor() - no fn', () => {
    const box = newBox();

    should.throw(() => box.addProcessor('test'), 'fn must be a function');
  });

  it('process()', async () => {
    const box = newBox('test');
    const data = {};

    box.addProcessor(file => {
      data[file.path] = file;
    });

    await Promise.all([
      writeFile(join(box.base, 'a.txt'), 'a'),
      writeFile(join(box.base, 'b', 'c.js'), 'c')
    ]);

    await box.process();

    for (const [key, item] of Object.entries(data)) {
      item.path.should.eql(key);
      item.source.should.eql(join(box.base, key));
      item.type.should.eql('create');
      item.params.should.eql({});
    }

    await rmdir(box.base);
  });

  it('process() - do nothing if target does not exist', async () => {
    const box = newBox('test');

    return box.process();
  });

  it('process() - create', async () => {
    const box = newBox('test');
    const name = 'a.txt';
    const path = join(box.base, name);

    const processor = spy();
    box.addProcessor(processor);

    await writeFile(path, 'a');
    await box.process();

    sinonAssert.calledWithMatch(processor, { type: 'create', path: name });

    await rmdir(box.base);
  });

  it('process() - update (mtime changed and hash changed)', async () => {
    const box = newBox('test');
    const name = 'a.txt';
    const path = join(box.base, name);
    const cacheId = 'test/' + name;

    const processor = spy();
    box.addProcessor(processor);

    await Promise.all([
      writeFile(path, 'a'),
      box.Cache.insert({
        _id: cacheId,
        modified: 0,
        hash: hash('b').toString('hex')
      })
    ]);
    await box.process();

    sinonAssert.calledWithMatch(processor, { type: 'update', path: name });

    await rmdir(box.base);
  });

  it('process() - skip (mtime changed but hash matched)', async () => {
    const box = newBox('test');
    const name = 'a.txt';
    const path = join(box.base, name);
    const cacheId = 'test/' + name;

    const processor = spy();
    box.addProcessor(processor);

    await writeFile(path, 'a');
    await stat(path);
    await box.Cache.insert({
      _id: cacheId,
      modified: 0,
      hash: hash('a').toString('hex')
    });
    await box.process();

    sinonAssert.calledWithMatch(processor, { type: 'skip', path: name });

    await rmdir(box.base);
  });

  it('process() - skip (hash changed but mtime matched)', async () => {
    const box = newBox('test');
    const name = 'a.txt';
    const path = join(box.base, name);
    const cacheId = 'test/' + name;

    const processor = spy();
    box.addProcessor(processor);

    await writeFile(path, 'a');
    const stats = await stat(path);
    await box.Cache.insert({
      _id: cacheId,
      modified: stats.mtime,
      hash: hash('b').toString('hex')
    });
    await box.process();

    sinonAssert.calledWithMatch(processor, { type: 'skip', path: name });

    await rmdir(box.base);
  });

  it('process() - skip (mtime matched and hash matched)', async () => {
    const box = newBox('test');
    const name = 'a.txt';
    const path = join(box.base, name);
    const cacheId = 'test/' + name;

    const processor = spy();
    box.addProcessor(processor);

    await writeFile(path, 'a');
    const stats = await stat(path);
    await box.Cache.insert({
      _id: cacheId,
      modified: stats.mtime,
      hash: hash('a').toString('hex')
    });
    await box.process();

    sinonAssert.calledWithMatch(processor, { type: 'skip', path: name });

    await rmdir(box.base);
  });

  it('process() - delete', async () => {
    const box = newBox('test');
    const cacheId = 'test/a.txt';

    const processor = spy();
    box.addProcessor(processor);

    await Promise.all([
      mkdirs(box.base),
      box.Cache.insert({
        _id: cacheId
      })
    ]);
    await box.process();

    sinonAssert.calledWith(processor, match.has('type', 'delete'));
    processor.calledOnce.should.be.true;

    await rmdir(box.base);
  });

  it('process() - params', async () => {
    const box = newBox('test');
    const path = join(box.base, 'posts', '123456');

    const processor = spy();

    box.addProcessor('posts/:id', processor);

    await writeFile(path, 'a');
    await box.process();

    sinonAssert.calledWith(processor, match.has('params', match.has('id', '123456')));
    processor.calledOnce.should.be.true;

    await rmdir(box.base);
  });

  it('process() - handle null ignore', async () => {
    const box = newBox('test', { ignore: null });
    const data = {};

    box.addProcessor(file => {
      data[file.path] = file;
    });

    await writeFile(join(box.base, 'foo.txt'), 'foo');
    await box.process();

    data.should.have.all.keys(['foo.txt']);

    await rmdir(box.base);
  });

  it('process() - skip files if they match a glob epression in ignore', async () => {
    const box = newBox('test', { ignore: '**/ignore_me' });
    const data = {};

    box.addProcessor(file => {
      data[file.path] = file;
    });

    await Promise.all([
      writeFile(join(box.base, 'foo.txt'), 'foo'),
      writeFile(join(box.base, 'ignore_me', 'bar.txt'), 'ignore_me')
    ]);
    await box.process();

    data.should.have.all.keys(['foo.txt']);

    await rmdir(box.base);
  });

  it('process() - skip files if they match any of the glob expressions in ignore', async () => {
    const box = newBox('test', { ignore: ['**/ignore_me', '**/ignore_me_too.txt'] });
    const data = {};

    box.addProcessor(file => {
      data[file.path] = file;
    });

    await Promise.all([
      writeFile(join(box.base, 'foo.txt'), 'foo'),
      writeFile(join(box.base, 'ignore_me', 'bar.txt'), 'ignore_me'),
      writeFile(join(box.base, 'ignore_me_too.txt'), 'ignore_me_too')
    ]);
    await box.process();

    data.should.have.all.keys(['foo.txt']);

    await rmdir(box.base);
  });

  it('watch() - create', async () => {
    const box = newBox('test');
    const path = 'a.txt';
    const src = join(box.base, path);
    const processor = spy();

    box.addProcessor(processor);

    await writeFile(src, 'a');
    await box.watch();
    box.isWatching().should.be.true;
    await Promise.delay(500);

    sinonAssert.calledWithMatch(processor.firstCall, {
      source: src,
      path: path,
      type: 'create',
      params: {}
    });

    box.unwatch();
    await rmdir(box.base);
  });

  it('watch() - update', async () => {
    const box = newBox('test');
    const path = 'a.txt';
    const src = join(box.base, path);
    const cacheId = 'test/' + path;
    const { Cache } = box;
    const processor = spy();

    box.addProcessor(processor);

    await Promise.all([
      writeFile(src, 'a'),
      Cache.insert({_id: cacheId})
    ]);
    await box.watch();
    await appendFile(src, 'b');
    await Promise.delay(500);

    sinonAssert.calledWithMatch(processor.lastCall, {
      source: src,
      path: path,
      type: 'update',
      params: {}
    });

    box.unwatch();
    await rmdir(box.base);
  });

  it('watch() - delete', async () => {
    const box = newBox('test');
    const path = 'a.txt';
    const src = join(box.base, path);
    const cacheId = 'test/' + path;
    const { Cache } = box;
    const processor = spy();

    box.addProcessor(processor);

    await Promise.all([
      writeFile(src, 'a'),
      Cache.insert({_id: cacheId})
    ]);
    await box.watch();
    await unlink(src);
    await Promise.delay(500);

    sinonAssert.calledWithMatch(processor.lastCall, {
      source: src,
      path: path,
      type: 'delete',
      params: {}
    });

    box.unwatch();
    await rmdir(box.base);
  });

  it('watch() - rename file', async () => {
    const box = newBox('test');
    const path = 'a.txt';
    const src = join(box.base, path);
    const newPath = 'b.txt';
    const newSrc = join(box.base, newPath);
    const cacheId = 'test/' + path;
    const { Cache } = box;
    const processor = spy();

    box.addProcessor(processor);

    await Promise.all([
      writeFile(src, 'a'),
      Cache.insert({_id: cacheId})
    ]);
    await box.watch();
    await rename(src, newSrc);
    await Promise.delay(500);

    for (const [file] of processor.args.slice(-2)) {
      switch (file.type) {
        case 'create':
          file.source.should.eql(newSrc);
          file.path.should.eql(newPath);
          break;

        case 'delete':
          file.source.should.eql(src);
          file.path.should.eql(path);
          break;
      }
    }

    box.unwatch();
    await rmdir(box.base);
  });

  it('watch() - rename folder', async () => {
    const box = newBox('test');
    const path = 'a/b.txt';
    const src = join(box.base, path);
    const newPath = 'b/b.txt';
    const newSrc = join(box.base, newPath);
    const cacheId = 'test/' + path;
    const { Cache } = box;
    const processor = spy();

    box.addProcessor(processor);

    await Promise.all([
      writeFile(src, 'a'),
      Cache.insert({_id: cacheId})
    ]);
    await box.watch();
    await rename(join(box.base, 'a'), join(box.base, 'b'));
    await Promise.delay(500);

    for (const [file] of processor.args.slice(-2)) {
      switch (file.type) {
        case 'create':
          file.source.should.eql(newSrc);
          file.path.should.eql(newPath);
          break;

        case 'delete':
          file.source.should.eql(src);
          file.path.should.eql(path);
          break;
      }
    }

    box.unwatch();
    await rmdir(box.base);
  });

  it('watch() - update with simple "ignore" option', async () => {
    const box = newBox('test', {ignore: '**/ignore_me/**'});
    const path1 = 'a.txt';
    const path2 = 'b.txt';
    const src1 = join(box.base, path1);
    const src2 = join(box.base, 'ignore_me', path2);
    const cacheId1 = 'test/' + path1;
    const cacheId2 = 'test/ignore_me/' + path2;
    const { Cache } = box;
    const processor = spy();

    box.addProcessor(processor);

    await Promise.all([
      writeFile(src1, 'a'),
      Cache.insert({_id: cacheId1})
    ]);
    await Promise.all([
      writeFile(src2, 'b'),
      Cache.insert({_id: cacheId2})
    ]);
    await box.watch();
    await appendFile(src1, 'aaa');
    await Promise.delay(500);

    const file = processor.lastCall.args[0];

    file.should.deep.include({
      source: src1,
      path: path1,
      type: 'update',
      params: {}
    });

    await appendFile(src2, 'bbb');
    await Promise.delay(500);

    const file2 = processor.lastCall.args[0];
    file2.should.eql(file); // not changed

    box.unwatch();
    await rmdir(box.base);
  });

  it('watch() - update with complex "ignore" option', async () => {
    const box = newBox('test', {ignore: ['**/ignore_me/**', '**/ignore_me_too.txt']});
    const path1 = 'a.txt';
    const path2 = 'b.txt';
    const path3 = 'ignore_me_too.txt';
    const src1 = join(box.base, path1);
    const src2 = join(box.base, 'ignore_me', path2);
    const src3 = join(box.base, path3);
    const cacheId1 = 'test/' + path1;
    const cacheId2 = 'test/ignore_me/' + path2;
    const cacheId3 = 'test/' + path3;
    const { Cache } = box;
    const processor = spy();

    box.addProcessor(processor);

    await Promise.all([
      writeFile(src1, 'a'),
      Cache.insert({_id: cacheId1})
    ]);
    await Promise.all([
      writeFile(src2, 'b'),
      Cache.insert({_id: cacheId2})
    ]);
    await Promise.all([
      writeFile(src3, 'c'),
      Cache.insert({_id: cacheId3})
    ]);
    await box.watch();
    await appendFile(src1, 'aaa');
    await Promise.delay(500);

    const file = processor.lastCall.args[0];

    file.should.deep.include({
      source: src1,
      path: path1,
      type: 'update',
      params: {}
    });

    await appendFile(src2, 'bbb');
    await Promise.delay(500);

    processor.lastCall.args[0].should.eql(file); // not changed

    await appendFile(src3, 'ccc');
    await Promise.delay(500);

    processor.lastCall.args[0].should.eql(file); // not changed

    box.unwatch();
    await rmdir(box.base);
  });

  it('watch() - watcher has started', async () => {
    const box = newBox();

    await box.watch();

    await box.watch().then(() => {
      should.fail('Return value must be rejected');
    }, err => {
      err.should.property('message', 'Watcher has already started.');
    });

    box.unwatch();
  });

  it('watch() - run process() before start watching', async () => {
    const box = newBox('test');
    const data = [];

    box.addProcessor(file => {
      data.push(file.path);
    });

    await Promise.all([
      writeFile(join(box.base, 'a.txt'), 'a'),
      writeFile(join(box.base, 'b', 'c.js'), 'c')
    ]);
    await box.watch();
    data.should.have.members(['a.txt', 'b/c.js']);

    box.unwatch();
    await rmdir(box.base);
  });

  it('unwatch()', async () => {
    const box = newBox('test');
    const processor = spy();

    await box.watch();
    box.addProcessor(processor);
    box.unwatch();

    await writeFile(join(box.base, 'a.txt'), 'a');
    processor.called.should.be.false;

    box.unwatch();
    await rmdir(box.base);
  });

  it('isWatching()', async () => {
    const box = newBox();

    box.isWatching().should.be.false;

    await box.watch();
    box.isWatching().should.be.true;

    box.unwatch();
    box.isWatching().should.be.false;

    box.unwatch();
  });

  it('processBefore & processAfter events', async () => {
    const box = newBox('test');

    const beforeSpy = spy();
    const afterSpy = spy();

    box.on('processBefore', beforeSpy);
    box.on('processAfter', afterSpy);

    await writeFile(join(box.base, 'a.txt'), 'a');
    await box.process();

    sinonAssert.calledWithMatch(beforeSpy, { type: 'create', path: 'a.txt' });
    sinonAssert.calledWithMatch(afterSpy, { type: 'create', path: 'a.txt' });
    beforeSpy.calledOnce.should.be.true;
    afterSpy.calledOnce.should.be.true;

    await rmdir(box.base);
  });
});


'use strict';

const { join } = require('path');
const { mkdirs, rmdir, writeFile } = require('hexo-fs');

describe('Theme', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo(join(__dirname, 'theme_test'), {silent: true});
  const themeDir = join(hexo.base_dir, 'themes', 'test');

  before(async () => {
    await Promise.all([
      mkdirs(themeDir),
      writeFile(hexo.config_path, 'theme: test')
    ]);
    hexo.init();
  });

  after(() => rmdir(hexo.base_dir));

  it('getView()', () => {
    hexo.theme.setView('test.njk', '');

    // With extension name
    hexo.theme.getView('test.njk').should.have.property('path', 'test.njk');

    // Without extension name
    hexo.theme.getView('test').should.have.property('path', 'test.njk');

    // not exist
    should.not.exist(hexo.theme.getView('abc.njk'));

    hexo.theme.removeView('test.njk');
  });

  it('getView() - escape backslashes', () => {
    hexo.theme.setView('foo/bar.njk', '');

    hexo.theme.getView('foo\\bar.njk').should.have.property('path', 'foo/bar.njk');

    hexo.theme.removeView('foo/bar.njk');
  });

  it('setView()', () => {
    hexo.theme.setView('test.njk', '');

    const view = hexo.theme.getView('test.njk');
    view.path.should.eql('test.njk');

    hexo.theme.removeView('test.njk');
  });

  it('removeView()', () => {
    hexo.theme.setView('test.njk', '');
    hexo.theme.removeView('test.njk');

    should.not.exist(hexo.theme.getView('test.njk'));
  });
});


'use strict';

function markdownHelper(text, options) {
  return this.render(text, 'markdown', options);
}

module.exports = markdownHelper;


- name: Install Dependencies
run: npm install
- name: Test
run: npm test
env:
CI: true


'use strict';

const { extname } = require('path');
const Promise = require('bluebird');
const { readFile, readFileSync } = require('hexo-fs');

const getExtname = str => {
  if (typeof str !== 'string') return '';

  const ext = extname(str);
  return ext[0] === '.' ? ext.slice(1) : ext;
};

const toString = (result, options) => {
  if (!Object.prototype.hasOwnProperty.call(options, 'toString') || typeof result === 'string') return result;

  if (typeof options.toString === 'function') {
    return options.toString(result);
  } else if (typeof result === 'object') {
    return JSON.stringify(result);
  } else if (result.toString) {
    return result.toString();
  }

  return result;
};

class Render {
  constructor(ctx) {
    this.context = ctx;
    this.renderer = ctx.extend.renderer;
  }

  isRenderable(path) {
    return this.renderer.isRenderable(path);
  }

  isRenderableSync(path) {
    return this.renderer.isRenderableSync(path);
  }

  getOutput(path) {
    return this.renderer.getOutput(path);
  }

  getRenderer(ext, sync) {
    return this.renderer.get(ext, sync);
  }

  getRendererSync(ext) {
    return this.getRenderer(ext, true);
  }

  render(data, options, callback) {
    if (!callback && typeof options === 'function') {
      callback = options;
      options = {};
    }

    const ctx = this.context;
    let ext = '';

    return new Promise((resolve, reject) => {
      if (!data) return reject(new TypeError('No input file or string!'));
      if (data.text != null) return resolve(data.text);
      if (!data.path) return reject(new TypeError('No input file or string!'));

      readFile(data.path).then(resolve, reject);
    }).then(text => {
      data.text = text;
      ext = data.engine || getExtname(data.path);
      if (!ext || !this.isRenderable(ext)) return text;

      const renderer = this.getRenderer(ext);
      return Reflect.apply(renderer, ctx, [data, options]);
    }).then(result => {
      result = toString(result, data);
      if (data.onRenderEnd) {
        return data.onRenderEnd(result);
      }

      return result;
    }).then(result => {
      const output = this.getOutput(ext) || ext;
      return ctx.execFilter(`after_render:${output}`, result, {
        context: ctx,
        args: [data]
      });
    }).asCallback(callback);
  }

  renderSync(data, options) {
    if (!data) throw new TypeError('No input file or string!');

    options = options || {};

    const ctx = this.context;

    if (data.text == null) {
      if (!data.path) throw new TypeError('No input file or string!');
      data.text = readFileSync(data.path);
    }

    if (data.text == null) throw new TypeError('No input file or string!');

    const ext = data.engine || getExtname(data.path);
    let result;

    if (ext && this.isRenderableSync(ext)) {
      const renderer = this.getRendererSync(ext);
      result = Reflect.apply(renderer, ctx, [data, options]);
    } else {
      result = data.text;
    }

    const output = this.getOutput(ext) || ext;
    result = toString(result, data);

    if (data.onRenderEnd) {
      result = data.onRenderEnd(result);
    }

    return ctx.execFilterSync(`after_render:${output}`, result, {
      context: ctx,
      args: [data]
    });
  }
}

module.exports = Render;


'use strict';

const Promise = require('bluebird');

class Migrator {
  constructor() {
    this.store = {};
  }

  list() {
    return this.store;
  }

  get(name) {
    return this.store[name];
  }

  register(name, fn) {
    if (!name) throw new TypeError('name is required');
    if (typeof fn !== 'function') throw new TypeError('fn must be a function');

    if (fn.length > 1) {
      fn = Promise.promisify(fn);
    } else {
      fn = Promise.method(fn);
    }

    this.store[name] = fn;
  }
}

module.exports = Migrator;


github-token: ${{ secrets.github_token }}

flag-name: run-${{ runner.os }}-${{ matrix.node-version }}

name: Tester

on: [push, pull_request]

jobs:
  tester:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: ['10.x', '12.x', '14.x']
      fail-fast: false
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: Cache NPM dependencies
        uses: actions/cache@v1
        with:
          path: node_modules
          key: ${{ runner.OS }}-npm-cache
          restore-keys: ${{ runner.OS }}-npm-cache
      - name: Install Dependencies
        run: npm install
      - name: Test
        run: npm test
        env:
          CI: true


'use strict';

describe('is', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo(__dirname);
  const is = require('../../../lib/plugins/helper/is');

  it('is_current', async () => {
    await is.current.call({path: 'index.html', config: hexo.config}).should.be.true;
    await is.current.call({path: 'tags/index.html', config: hexo.config}).should.be.false;
    await is.current.call({path: 'index.html', config: hexo.config}, '/').should.be.true;
    await is.current.call({path: 'index.html', config: hexo.config}, 'index.html').should.be.true;
    await is.current.call({path: 'tags/index.html', config: hexo.config}, '/').should.be.false;
    await is.current.call({path: 'tags/index.html', config: hexo.config}, '/index.html').should.be.false;
    await is.current.call({path: 'index.html', config: hexo.config}, '/', true).should.be.true;
    await is.current.call({path: 'index.html', config: hexo.config}, '/index.html', true).should.be.true;
    await is.current.call({path: 'foo/bar', config: hexo.config}, 'foo', true).should.be.false;
    await is.current.call({path: 'foo/bar', config: hexo.config}, 'foo').should.be.true;
    await is.current.call({path: 'foo/bar', config: hexo.config}, 'foo/bar').should.be.true;
    await is.current.call({path: 'foo/bar', config: hexo.config}, 'foo/baz').should.be.false;
  });

  it('is_home', async () => {
    await is.home.call({page: {__index: true}}).should.be.true;
    await is.home.call({page: {}}).should.be.false;
  });

  it('is_post', async () => {
    await is.post.call({page: {__post: true}}).should.be.true;
    await is.post.call({page: {}}).should.be.false;
  });

  it('is_page', async () => {
    await is.page.call({page: {__page: true}}).should.be.true;
    await is.page.call({page: {}}).should.be.false;
  });

  it('is_archive', async () => {
    await is.archive.call({page: {}}).should.be.false;
    await is.archive.call({page: {archive: true}}).should.be.true;
    await is.archive.call({page: {archive: false}}).should.be.false;
  });

  it('is_year', async () => {
    await is.year.call({page: {}}).should.be.false;
    await is.year.call({page: {archive: true}}).should.be.false;
    await is.year.call({page: {archive: true, year: 2014}}).should.be.true;
    await is.year.call({page: {archive: true, year: 2014}}, 2014).should.be.true;
    await is.year.call({page: {archive: true, year: 2014}}, 2015).should.be.false;
    await is.year.call({page: {archive: true, year: 2014, month: 10}}).should.be.true;
  });

  it('is_month', async () => {
    await is.month.call({page: {}}).should.be.false;
    await is.month.call({page: {archive: true}}).should.be.false;
    await is.month.call({page: {archive: true, year: 2014}}).should.be.false;
    await is.month.call({page: {archive: true, year: 2014, month: 10}}).should.be.true;
    await is.month.call({page: {archive: true, year: 2014, month: 10}}, 2014, 10).should.be.true;
    await is.month.call({page: {archive: true, year: 2014, month: 10}}, 2015, 10).should.be.false;
    await is.month.call({page: {archive: true, year: 2014, month: 10}}, 2014, 12).should.be.false;
    await is.month.call({page: {archive: true, year: 2014, month: 10}}, 10).should.be.true;
    await is.month.call({page: {archive: true, year: 2014, month: 10}}, 12).should.be.false;
  });

  it('is_category', async () => {
    await is.category.call({page: {category: 'foo'}}).should.be.true;
    await is.category.call({page: {category: 'foo'}}, 'foo').should.be.true;
    await is.category.call({page: {category: 'foo'}}, 'bar').should.be.false;
    await is.category.call({page: {}}).should.be.false;
  });

  it('is_tag', async () => {
    await is.tag.call({page: {tag: 'foo'}}).should.be.true;
    await is.tag.call({page: {tag: 'foo'}}, 'foo').should.be.true;
    await is.tag.call({page: {tag: 'foo'}}, 'bar').should.be.false;
    await is.tag.call({page: {}}).should.be.false;
  });
});


'use strict';

const { deepMerge, full_url_for } = require('hexo-util');

describe('Tag', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo();
  const Tag = hexo.model('Tag');
  const Post = hexo.model('Post');
  const PostTag = hexo.model('PostTag');
  const defaults = require('../../../lib/hexo/default_config');

  before(() => hexo.init());

  beforeEach(() => { hexo.config = deepMerge({}, defaults); });

  it('name - required', async () => {
    try {
      await Tag.insert({});
    } catch (err) {
      err.message.should.be.eql('`name` is required!');
    }
  });

  it('slug - virtual', async () => {
    const data = await Tag.insert({
      name: 'foo'
    });
    data.slug.should.eql('foo');

    Tag.removeById(data._id);
  });

  it('slug - tag_map', async () => {
    hexo.config.tag_map = {
      test: 'wat'
    };

    const data = await Tag.insert({
      name: 'test'
    });
    data.slug.should.eql('wat');
    Tag.removeById(data._id);
  });

  it('slug - filename_case: 0', async () => {
    const data = await Tag.insert({
      name: 'WahAHa'
    });
    data.slug.should.eql('WahAHa');

    Tag.removeById(data._id);
  });

  it('slug - filename_case: 1', async () => {
    hexo.config.filename_case = 1;

    const data = await Tag.insert({
      name: 'WahAHa'
    });
    data.slug.should.eql('wahaha');

    Tag.removeById(data._id);
  });

  it('slug - filename_case: 2', async () => {
    hexo.config.filename_case = 2;

    const data = await Tag.insert({
      name: 'WahAHa'
    });

    data.slug.should.eql('WAHAHA');

    Tag.removeById(data._id);
  });

  it('path - virtual', async () => {
    const data = await Tag.insert({
      name: 'foo'
    });

    data.path.should.eql(hexo.config.tag_dir + '/' + data.slug + '/');

    Tag.removeById(data._id);
  });

  it('permalink - virtual', async () => {
    const data = await Tag.insert({
      name: 'foo'
    });

    data.permalink.should.eql(hexo.config.url + '/' + data.path);

    Tag.removeById(data._id);
  });

  it('permalink - trailing_index', async () => {
    hexo.config.pretty_urls.trailing_index = false;
    const data = await Tag.insert({
      name: 'foo'
    });

    data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/index\.html$/, ''));

    Tag.removeById(data._id);
  });

  it('permalink - trailing_html', async () => {
    hexo.config.pretty_urls.trailing_html = false;
    const data = await Tag.insert({
      name: 'foo'
    });

    data.permalink.should.eql(hexo.config.url + '/' + data.path.replace(/\.html$/, ''));

    Tag.removeById(data._id);
  });

  it('permalink - should be encoded', async () => {
    hexo.config.url = 'http://fôo.com';
    const data = await Tag.insert({
      name: '字'
    });

    data.permalink.should.eql(full_url_for.call(hexo, data.path));

    Tag.removeById(data._id);
  });

  it('posts - virtual', async () => {
    const posts = await Post.insert([
      {source: 'foo.md', slug: 'foo'},
      {source: 'bar.md', slug: 'bar'},
      {source: 'baz.md', slug: 'baz'}
    ]);
    await Promise.all(posts.map(post => post.setTags(['foo'])));

    const tag = Tag.findOne({name: 'foo'});

    function mapper(post) {
      return post._id;
    }

    hexo.locals.invalidate();
    tag.posts.map(mapper).should.eql(posts.map(mapper));
    tag.should.have.lengthOf(posts.length);

    await tag.remove();
    await Promise.all(posts.map(post => post.remove()));
  });

  it('posts - draft', async () => {
    const posts = await Post.insert([
      {source: 'foo.md', slug: 'foo', published: true},
      {source: 'bar.md', slug: 'bar', published: false},
      {source: 'baz.md', slug: 'baz', published: true}
    ]);
    await Promise.all(posts.map(post => post.setTags(['foo'])));

    let tag = Tag.findOne({name: 'foo'});

    function mapper(post) {
      return post._id;
    }

    // draft off
    hexo.locals.invalidate();
    tag.posts.eq(0)._id.should.eql(posts[0]._id);
    tag.posts.eq(1)._id.should.eql(posts[2]._id);
    tag.should.have.lengthOf(2);

    // draft on
    hexo.config.render_drafts = true;
    tag = Tag.findOne({name: 'foo'});
    hexo.locals.invalidate();
    tag.posts.map(mapper).should.eql(posts.map(mapper));
    tag.should.have.lengthOf(posts.length);
    hexo.config.render_drafts = false;

    await tag.remove();
    await Promise.all(posts.map(post => post.remove()));
  });

  it('posts - future', async () => {
    const now = Date.now();

    const posts = await Post.insert([
      {source: 'foo.md', slug: 'foo', date: now - 3600},
      {source: 'bar.md', slug: 'bar', date: now + 3600},
      {source: 'baz.md', slug: 'baz', date: now}
    ]);
    await Promise.all(posts.map(post => post.setTags(['foo'])));

    let tag = Tag.findOne({name: 'foo'});

    function mapper(post) {
      return post._id;
    }

    // future on
    hexo.config.future = true;
    hexo.locals.invalidate();
    tag.posts.map(mapper).should.eql(posts.map(mapper));
    tag.should.have.lengthOf(posts.length);

    // future off
    hexo.config.future = false;
    hexo.locals.invalidate();
    tag = Tag.findOne({name: 'foo'});
    tag.posts.eq(0)._id.should.eql(posts[0]._id);
    tag.posts.eq(1)._id.should.eql(posts[2]._id);
    tag.should.have.lengthOf(2);

    await tag.remove();
    await Promise.all(posts.map(post => post.remove()));
  });

  it('check whether a tag exists', async () => {
    let data = await Tag.insert({
      name: 'foo'
    });

    try {
      data = await Tag.insert({
        name: 'foo'
      });
    } catch (err) {
      err.message.should.eql('Tag `foo` has already existed!');
    }

    Tag.removeById(data._id);
  });

  it('remove PostTag references when a tag is removed', async () => {
    const posts = await Post.insert([
      {source: 'foo.md', slug: 'foo'},
      {source: 'bar.md', slug: 'bar'},
      {source: 'baz.md', slug: 'baz'}
    ]);
    await Promise.all(posts.map(post => post.setTags(['foo'])));

    const tag = Tag.findOne({name: 'foo'});
    await Tag.removeById(tag._id);

    PostTag.find({tag_id: tag._id}).should.have.lengthOf(0);

    await Promise.all(posts.map(post => Post.removeById(post._id)));
  });
});


---
name: Question/Help
about: I have a question, help for hexo (e.g. Customize)
title: ''
labels: ''
assignees: ''

---

<!-- NOTE:

If you find that markdown files are not rendered as expected, please go to https://marked.js.org/demo/ to see if it can be reproduced there. If it can be reproduced, please file a bug to https://github.com/markedjs/marked.

If you want help on your bug, please also send us the git repository (GitHub, GitLab, Bitbucket etc...) where your hexo code is stored. It would greatly help. If you prefer not to have your hexo code out in public, please upload to a private GitHub repository and grant read-only access to hexojs/core

-->

## Check List

Please check followings before submitting a new issue.

- [ ] I have already read [Docs page](https://hexo.io/docs/) & [Troubleshooting page](https://hexo.io/docs/troubleshooting)
- [ ] I have already searched existing issues and they are not help to me
- [ ] I examined error or warning messages and it's difficult to solve
- [ ] Using [the latest](https://github.com/hexojs/hexo/releases) version of Hexo (run `hexo version` to check)
- [ ] Node.js is higher than 8.6.0

## Question

<!-- Question description -->

## Environment & Settings

**Node.js & npm version**

```
```

**Your site `_config.yml`** (Optional)

```
```

**Your theme `_config.yml`** (Optional)

```
```

**Hexo and Plugin version(`npm ls --depth 0`)**

```
```

**Your package.json `package.json`**

```
```

## Others

<!-- If you have other information. Please write here. -->


'use strict';

const { join } = require('path');
const { mkdirs, unlink, writeFile, rmdir } = require('hexo-fs');

describe('Load alternate theme config', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo(join(__dirname, 'config_test'), {silent: true});
  const loadThemeConfig = require('../../../lib/hexo/load_theme_config');

  hexo.env.init = true;

  before(() => mkdirs(hexo.base_dir).then(() => hexo.init()));

  after(() => rmdir(hexo.base_dir));

  beforeEach(() => {
    hexo.config.theme_config = { foo: { bar: 'ahhhhhh' } };
    hexo.config.theme = 'test_theme';
  });

  it('hexo.config.theme does not exist', async () => {
    hexo.config.theme = undefined;
    await loadThemeConfig(hexo);
    hexo.config.theme_config.foo.bar.should.eql('ahhhhhh');
    hexo.config.theme_config = {};
  });

  it('_config.[theme].yml does not exist', () => loadThemeConfig(hexo).then(() => {
    hexo.config.theme_config = {};
  }));

  it('_config.[theme].yml exists', () => {
    const configPath = join(hexo.base_dir, '_config.test_theme.yml');

    return writeFile(configPath, 'bar: 1').then(() => loadThemeConfig(hexo)).then(() => {
      hexo.config.theme_config.bar.should.eql(1);
    }).finally(() => unlink(configPath));
  });

  it('_config.[theme].json exists', () => {
    const configPath = join(hexo.base_dir, '_config.test_theme.json');

    return writeFile(configPath, '{"baz": 3}').then(() => loadThemeConfig(hexo)).then(() => {
      hexo.config.theme_config.baz.should.eql(3);
    }).finally(() => unlink(configPath));
  });

  it('_config.[theme].txt exists', () => {
    const configPath = join(hexo.base_dir, '_config.test_theme.txt');

    return writeFile(configPath, 'qux: 1').then(() => loadThemeConfig(hexo)).then(() => {
      should.not.exist(hexo.config.theme_config.qux);
    }).finally(() => unlink(configPath));
  });

  it('merge config', () => {
    const configPath = join(hexo.base_dir, '_config.test_theme.yml');

    const content = [
      'foo:',
      '  bar: yoooo',
      '  baz: true'
    ].join('\n');

    return writeFile(configPath, content).then(() => loadThemeConfig(hexo)).then(() => {
      hexo.config.theme_config.foo.baz.should.eql(true);
      hexo.config.theme_config.foo.bar.should.eql('ahhhhhh');
      hexo.config.theme_config.foo.bar.should.not.eql('yoooo');
    }).finally(() => unlink(configPath));
  });

  it('hexo.config.theme_config does not exist', async () => {
    const configPath = join(hexo.base_dir, '_config.test_theme.yml');

    hexo.config.theme_config = undefined;

    const content = [
      'foo:',
      '  bar: yoooo',
      '  baz: true'
    ].join('\n');

    await writeFile(configPath, content);
    await loadThemeConfig(hexo);

    hexo.config.theme_config.foo.baz.should.eql(true);
    hexo.config.theme_config.foo.bar.should.eql('yoooo');
  });
});


'use strict';

const { underline } = require('chalk');

function migrateConsole(args) {
  // Display help message if user didn't input any arguments
  if (!args._.length) {
    return this.call('help', {_: ['migrate']});
  }

  const type = args._.shift();
  const migrators = this.extend.migrator.list();

  if (!migrators[type]) {
    let help = '';

    help += `${type.magenta} migrator plugin is not installed.\n\n`;
    help += 'Installed migrator plugins:\n';
    help += `  ${Object.keys(migrators).join(', ')}\n\n`;
    help += `For more help, you can check the online docs: ${underline('https://hexo.io/')}`;

    console.log(help);
    return;
  }

  return Reflect.apply(migrators[type], this, [args]);
}

module.exports = migrateConsole;


'use strict';

const titlecase = require('titlecase');

function titlecaseFilter(data) {
  if (!this.config.titlecase || !data.title) return;

  data.title = titlecase(data.title);
}

module.exports = titlecaseFilter;


- if [[ $TRAVIS_OS_NAME == "linux" ]]; then

- nyc report --reporter=text-lcov | coveralls

keep keep keep replace replace replace replace replace replace replace replace replace replace replace keep keep keep


'use strict';

function listCategoriesHelper(categories, options) {
  if (!options && (!categories || !Object.prototype.hasOwnProperty.call(categories, 'length'))) {
    options = categories;
    categories = this.site.categories;
  }

  if (!categories || !categories.length) return '';
  options = options || {};

  const { style = 'list', transform, separator = ', ', suffix = '' } = options;
  const showCount = Object.prototype.hasOwnProperty.call(options, 'show_count') ? options.show_count : true;
  const className = options.class || 'category';
  const depth = options.depth ? parseInt(options.depth, 10) : 0;
  const orderby = options.orderby || 'name';
  const order = options.order || 1;
  const showCurrent = options.show_current || false;
  const childrenIndicator = Object.prototype.hasOwnProperty.call(options, 'children_indicator') ? options.children_indicator : false;

  const prepareQuery = parent => {
    const query = {};

    if (parent) {
      query.parent = parent;
    } else {
      query.parent = {$exists: false};
    }

    return categories.find(query).sort(orderby, order).filter(cat => cat.length);
  };

  const hierarchicalList = (level, parent) => {
    let result = '';

    prepareQuery(parent).forEach((cat, i) => {
      let child;
      if (!depth || level + 1 < depth) {
        child = hierarchicalList(level + 1, cat._id);
      }

      let isCurrent = false;
      if (showCurrent && this.page) {
        for (let j = 0; j < cat.length; j++) {
          const post = cat.posts.data[j];
          if (post && post._id === this.page._id) {
            isCurrent = true;
            break;
          }
        }

        // special case: category page
        isCurrent = isCurrent || (this.page.base && this.page.base.startsWith(cat.path));
      }

      const additionalClassName = child && childrenIndicator ? ` ${childrenIndicator}` : '';

      result += `<li class="${className}-list-item${additionalClassName}">`;

      result += `<a class="${className}-list-link${isCurrent ? ' current' : ''}" href="${this.url_for(cat.path)}${suffix}">`;
      result += transform ? transform(cat.name) : cat.name;
      result += '';

      if (showCount) {
        result += `<span class="${className}-list-count">${cat.length}</span>`;
      }

      if (child) {
        result += `<ul class="${className}-list-child">${child}</ul>`;
      }

      result += '
';
    });

    return result;
  };

  const flatList = (level, parent) => {
    let result = '';

    prepareQuery(parent).forEach((cat, i) => {
      if (i || level) result += separator;

      result += `<a class="${className}-link" href="${this.url_for(cat.path)}${suffix}">`;
      result += transform ? transform(cat.name) : cat.name;

      if (showCount) {
        result += `<span class="${className}-count">${cat.length}</span>`;
      }

      result += '';

      if (!depth || level + 1 < depth) {
        result += flatList(level + 1, cat._id);
      }
    });

    return result;
  };

  if (style === 'list') {
    return `<ul class="${className}-list">${hierarchicalList(0)}</ul>`;
  }

  return flatList(0);
}

module.exports = listCategoriesHelper;


'use strict';

const qs = require('querystring');

describe('mail_to', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo(__dirname);

  const ctx = {
    config: hexo.config
  };

  ctx.url_for = require('../../../lib/plugins/helper/url_for').bind(ctx);

  const mailto = require('../../../lib/plugins/helper/mail_to').bind(ctx);

  it('path', () => {
    mailto('abc@example.com').should.eql('abc@example.com');
  });

  it('text', () => {
    mailto('abc@example.com', 'Email').should.eql('Email');
  });

  it('subject', () => {
    mailto('abc@example.com', 'Email', {subject: 'Hello'})
      .should.eql('Email');
  });

  it('cc (string)', () => {
    const data = {cc: 'abc@abc.com'};
    const querystring = qs.stringify(data);

    mailto('abc@example.com', 'Email', {cc: 'abc@abc.com'})
      .should.eql('Email');
  });

  it('cc (array)', () => {
    const data = {cc: 'abc@abc.com,bcd@bcd.com'};
    const querystring = qs.stringify(data);

    mailto('abc@example.com', 'Email', {cc: ['abc@abc.com', 'bcd@bcd.com']})
      .should.eql('Email');
  });

  it('bcc (string)', () => {
    const data = {bcc: 'abc@abc.com'};
    const querystring = qs.stringify(data);

    mailto('abc@example.com', 'Email', {bcc: 'abc@abc.com'})
      .should.eql('Email');
  });

  it('bcc (array)', () => {
    const data = {bcc: 'abc@abc.com,bcd@bcd.com'};
    const querystring = qs.stringify(data);

    mailto('abc@example.com', 'Email', {bcc: ['abc@abc.com', 'bcd@bcd.com']})
      .should.eql('Email');
  });

  it('body', () => {
    mailto('abc@example.com', 'Email', {body: 'Hello'})
      .should.eql('Email');
  });

  it('class (string)', () => {
    mailto('abc@example.com', 'Email', {class: 'foo'})
      .should.eql('Email');
  });

  it('class (array)', () => {
    mailto('abc@example.com', 'Email', {class: ['foo', 'bar']})
      .should.eql('Email');
  });

  it('id', () => {
    mailto('abc@example.com', 'Email', {id: 'foo'})
      .should.eql('Email');
  });
});


'use strict';

const assert = require('assert');
const moment = require('moment');
const Promise = require('bluebird');
const { join, extname } = require('path');
const { magenta } = require('chalk');
const { load } = require('js-yaml');
const { slugize, escapeRegExp } = require('hexo-util');
const { copyDir, exists, listDir, mkdirs, readFile, rmdir, unlink, writeFile } = require('hexo-fs');
const yfm = require('hexo-front-matter');

const preservedKeys = ['title', 'slug', 'path', 'layout', 'date', 'content'];

const _escapeContent = (cache, str) => {
  const placeholder = '\uFFFC';
  return `<!--${placeholder}${cache.push(str) - 1}-->`;
};

class PostRenderCache {
  constructor() {
    this.cache = [];
  }

  loadContent(str) {
    if (str.includes('hexoPostRenderEscape')) {
      str = str.replace(/<!--hexoPostRenderEscape:/g, '').replace(/:hexoPostRenderEscape-->/g, '');
    }

    const rPlaceholder = /(?:<|<)!--\uFFFC(\d+)--(?:>|>)/g;
    const restored = str.replace(rPlaceholder, (_, index) => {
      assert(this.cache[index]);
      const value = this.cache[index];
      this.cache[index] = null;
      return value;
    });
    if (restored === str) return restored;
    return this.loadContent(restored); // self-recursive for nexted escaping
  }

  escapeAllSwigTags(str) {
    const rSwigVar = /\{\{[\s\S]*?\}\}/g;
    const rSwigComment = /\{#[\s\S]*?#\}/g;
    const rSwigBlock = /\{%[\s\S]*?%\}/g;
    const rSwigFullBlock = /\{% *(.+?)(?: *| +.*?)%\}[\s\S]+?\{% *end\1 *%\}/g;

    const escape = _str => _escapeContent(this.cache, _str);
    return str.replace(rSwigFullBlock, escape)
      .replace(rSwigBlock, escape)
      .replace(rSwigComment, '')
      .replace(rSwigVar, escape);
  }
}

const prepareFrontMatter = data => {
  for (const [key, item] of Object.entries(data)) {
    if (moment.isMoment(item)) {
      data[key] = item.utc().format('YYYY-MM-DD HH:mm:ss');
    } else if (moment.isDate(item)) {
      data[key] = moment.utc(item).format('YYYY-MM-DD HH:mm:ss');
    } else if (typeof item === 'string') {
      data[key] = `"${item}"`;
    }
  }

  return data;
};

const removeExtname = str => {
  return str.substring(0, str.length - extname(str).length);
};

const createAssetFolder = (path, assetFolder) => {
  if (!assetFolder) return Promise.resolve();

  const target = removeExtname(path);

  return exists(target).then(exist => {
    if (!exist) return mkdirs(target);
  });
};

class Post {
  constructor(context) {
    this.context = context;
  }

  create(data, replace, callback) {
    if (!callback && typeof replace === 'function') {
      callback = replace;
      replace = false;
    }

    const ctx = this.context;
    const { config } = ctx;

    data.slug = slugize((data.slug || data.title).toString(), {transform: config.filename_case});
    data.layout = (data.layout || config.default_layout).toLowerCase();
    data.date = data.date ? moment(data.date) : moment();

    return Promise.all([
      // Get the post path
      ctx.execFilter('new_post_path', data, {
        args: [replace],
        context: ctx
      }),
      this._renderScaffold(data)
    ]).spread((path, content) => {
      const result = { path, content };

      return Promise.all([
        // Write content to file
        writeFile(path, content),
        // Create asset folder
        createAssetFolder(path, config.post_asset_folder)
      ]).then(() => {
        ctx.emit('new', result);
      }).thenReturn(result);
    }).asCallback(callback);
  }

  _getScaffold(layout) {
    const ctx = this.context;

    return ctx.scaffold.get(layout).then(result => {
      if (result != null) return result;
      return ctx.scaffold.get('normal');
    });
  }

  _renderScaffold(data) {
    const { tag } = this.context.extend;
    let yfmSplit;

    return this._getScaffold(data.layout).then(scaffold => {
      const frontMatter = prepareFrontMatter({...data});
      yfmSplit = yfm.split(scaffold);

      return tag.render(yfmSplit.data, frontMatter);
    }).then(frontMatter => {
      const { separator } = yfmSplit;
      const jsonMode = separator[0] === ';';

      // Parse front-matter
      const obj = jsonMode ? JSON.parse(`{${frontMatter}}`) : load(frontMatter);

      // Add data which are not in the front-matter
      for (const key of Object.keys(data)) {
        if (!preservedKeys.includes(key) && obj[key] == null) {
          obj[key] = data[key];
        }
      }

      let content = '';
      // Prepend the separator
      if (yfmSplit.prefixSeparator) content += `${separator}\n`;

      content += yfm.stringify(obj, {
        mode: jsonMode ? 'json' : ''
      });

      // Concat content
      content += yfmSplit.content;

      if (data.content) {
        content += `\n${data.content}`;
      }

      return content;
    });
  }

  publish(data, replace, callback) {
    if (!callback && typeof replace === 'function') {
      callback = replace;
      replace = false;
    }

    if (data.layout === 'draft') data.layout = 'post';

    const ctx = this.context;
    const { config } = ctx;
    const draftDir = join(ctx.source_dir, '_drafts');
    const slug = slugize(data.slug.toString(), {transform: config.filename_case});
    data.slug = slug;
    const regex = new RegExp(`^${escapeRegExp(slug)}(?:[^\\/\\\\]+)`);
    let src = '';
    const result = {};

    data.layout = (data.layout || config.default_layout).toLowerCase();

    // Find the draft
    return listDir(draftDir).then(list => {
      return list.find(item => regex.test(item));
    }).then(item => {
      if (!item) throw new Error(`Draft "${slug}" does not exist.`);

      // Read the content
      src = join(draftDir, item);
      return readFile(src);
    }).then(content => {
      // Create post
      Object.assign(data, yfm(content));
      data.content = data._content;
      delete data._content;

      return this.create(data, replace).then(post => {
        result.path = post.path;
        result.content = post.content;
      });
    }).then(() => // Remove the original draft file
      unlink(src)).then(() => {
      if (!config.post_asset_folder) return;

      // Copy assets
      const assetSrc = removeExtname(src);
      const assetDest = removeExtname(result.path);

      return exists(assetSrc).then(exist => {
        if (!exist) return;

        return copyDir(assetSrc, assetDest).then(() => rmdir(assetSrc));
      });
    }).thenReturn(result).asCallback(callback);
  }

  render(source, data = {}, callback) {
    const ctx = this.context;
    const { config } = ctx;
    const { tag } = ctx.extend;
    const ext = data.engine || (source ? extname(source) : '');

    let promise;

    if (data.content != null) {
      promise = Promise.resolve(data.content);
    } else if (source) {
      // Read content from files
      promise = readFile(source);
    } else {
      return Promise.reject(new Error('No input file or string!')).asCallback(callback);
    }

    // disable Nunjucks when the renderer spcify that.
    const disableNunjucks = ext && ctx.render.renderer.get(ext) && !!ctx.render.renderer.get(ext).disableNunjucks;

    const cacheObj = new PostRenderCache();

    return promise.then(content => {
      data.content = content;

      // Run "before_post_render" filters
      return ctx.execFilter('before_post_render', data, {context: ctx});
    }).then(() => {
      // Escape all Nunjucks/Swig tags
      if (!disableNunjucks) {
        data.content = cacheObj.escapeAllSwigTags(data.content);
      }

      const options = data.markdown || {};
      if (!config.highlight.enable) options.highlight = null;

      ctx.log.debug('Rendering post: %s', magenta(source));
      // Render with markdown or other renderer
      return ctx.render.render({
        text: data.content,
        path: source,
        engine: data.engine,
        toString: true,
        onRenderEnd(content) {
          // Replace cache data with real contents
          data.content = cacheObj.loadContent(content);

          // Return content after replace the placeholders
          if (disableNunjucks) return data.content;

          // Render with Nunjucks
          return tag.render(data.content, data);
        }
      }, options);
    }).then(content => {
      data.content = content;

      // Run "after_post_render" filters
      return ctx.execFilter('after_post_render', data, {context: ctx});
    }).asCallback(callback);
  }
}

module.exports = Post;


"hexo": "./bin/hexo"
},
"scripts": {
"eslint": "eslint .",
"test": "mocha test/index.js",
"test-cov": "nyc npm run test",
"lint-staged": "lint-staged"
},
"directories": {
"lib": "./lib",

keep keep keep keep keep replace keep keep keep keep

"test-cov": "nyc --reporter=lcovonly npm run test",

hexo

A fast, simple & powerful blog framework, powered by Node.js.

Tommy Chen <tommy351@gmail.com> (https://zespia.tw)

Abner Chou <hi@abnerchou.me> (https://abnerchou.me)


'use strict';

function saveDatabaseFilter() {
  if (!this.env.init || !this._dbLoaded) return;

  return this.database.save().then(() => {
    this.log.debug('Database saved');
  });
}

module.exports = saveDatabaseFilter;


'use strict';

const tildify = require('tildify');
const { magenta } = require('chalk');

function publishConsole(args) {
  // Display help message if user didn't input any arguments
  if (!args._.length) {
    return this.call('help', {_: ['publish']});
  }

  return this.post.publish({
    slug: args._.pop(),
    layout: args._.length ? args._[0] : this.config.default_layout
  }, args.r || args.replace).then(post => {
    this.log.info('Published: %s', magenta(tildify(post.path)));
  });
}

module.exports = publishConsole;


keep keep keep keep replace keep keep keep keep keep

// Positioned absolutely within #node-view. This is consistent with how nodes are positioned.

import { INodeUi } from '@/Interface';

import mixins from 'vue-typed-mixins';

import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';

export const mouseSelect = mixins(
	deviceSupportHelpers,
	nodeIndex,
).extend({
	data () {
		return {
			selectActive: false,
			selectBox: document.createElement('span'),
		};
	},
	mounted () {
		this.createSelectBox();
	},
	methods: {
		createSelectBox () {
			this.selectBox.id = 'select-box';
			this.selectBox.style.margin = '0px auto';
			this.selectBox.style.border = '2px dotted #FF0000';
			this.selectBox.style.position = 'fixed';
			this.selectBox.style.zIndex = '100';
			this.selectBox.style.visibility = 'hidden';

			this.selectBox.addEventListener('mouseup', this.mouseUpMouseSelect);

			// document.body.appendChild(this.selectBox);
			this.$el.appendChild(this.selectBox);
		},
		isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {
			if (this.isTouchDevice === true) {
				return true;
			}
			if (this.isMacOs) {
				return e.metaKey;
			}
			return e.ctrlKey;
		},
		showSelectBox (event: MouseEvent) {
			// @ts-ignore
			this.selectBox.x = event.pageX;
			// @ts-ignore
			this.selectBox.y = event.pageY;

			this.selectBox.style.left = event.pageX + 'px';
			this.selectBox.style.top = event.pageY + 'px';
			this.selectBox.style.visibility = 'visible';

			this.selectActive = true;
		},
		updateSelectBox (event: MouseEvent) {
			const selectionBox = this.getSelectionBox(event);
			this.selectBox.style.left = selectionBox.x + 'px';
			this.selectBox.style.top = selectionBox.y + 'px';

			this.selectBox.style.width = selectionBox.width + 'px';
			this.selectBox.style.height = selectionBox.height + 'px';
		},
		hideSelectBox () {
			this.selectBox.style.visibility = 'hidden';
			// @ts-ignore
			this.selectBox.x = 0;
			// @ts-ignore
			this.selectBox.y = 0;
			this.selectBox.style.left = '0px';
			this.selectBox.style.top = '0px';
			this.selectBox.style.width = '0px';
			this.selectBox.style.height = '0px';

			this.selectActive = false;
		},
		getSelectionBox (event: MouseEvent) {
			return {
				// @ts-ignore
				x: Math.min(event.pageX, this.selectBox.x),
				// @ts-ignore
				y: Math.min(event.pageY, this.selectBox.y),
				// @ts-ignore
				width: Math.abs(event.pageX - this.selectBox.x),
				// @ts-ignore
				height: Math.abs(event.pageY - this.selectBox.y),
			};
		},
		getNodesInSelection (event: MouseEvent): INodeUi[] {
			const returnNodes: INodeUi[] = [];
			const selectionBox = this.getSelectionBox(event);
			const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;

			// Consider the offset of the workflow when it got moved
			selectionBox.x -= offsetPosition[0];
			selectionBox.y -= offsetPosition[1];

			// Go through all nodes and check if they are selected
			this.$store.getters.allNodes.forEach((node: INodeUi) => {
				// TODO: Currently always uses the top left corner for checking. Should probably use the center instead
				if (node.position[0] < selectionBox.x || node.position[0] > (selectionBox.x + selectionBox.width)) {
					return;
				}
				if (node.position[1] < selectionBox.y || node.position[1] > (selectionBox.y + selectionBox.height)) {
					return;
				}
				returnNodes.push(node);
			});

			return returnNodes;
		},
		mouseDownMouseSelect (e: MouseEvent) {
			if (this.isCtrlKeyPressed(e) === true) {
				// We only care about it when the ctrl key is not pressed at the same time.
				// So we exit when it is pressed.
				return;
			}

			if (this.$store.getters.isActionActive('dragActive')) {
				// If a node does currently get dragged we do not activate the selection
				return;
			}
			this.showSelectBox(e);

			// @ts-ignore // Leave like this. Do not add a anonymous function because then remove would not work anymore
			this.$el.addEventListener('mousemove', this.mouseMoveSelect);
		},
		mouseUpMouseSelect (e: MouseEvent) {
			if (this.selectActive === false) {
				if (this.isTouchDevice === true) {
					// @ts-ignore
					if (e.target && e.target.id.includes('node-view')) {
						// Deselect all nodes
						this.deselectAllNodes();
					}
				}
				// If it is not active return direcly.
				// Else normal node dragging will not work.
				return;
			}

			// @ts-ignore
			this.$el.removeEventListener('mousemove', this.mouseMoveSelect);

			// Deselect all nodes
			this.deselectAllNodes();

			// Select the nodes which are in the selection box
			const selectedNodes = this.getNodesInSelection(e);
			selectedNodes.forEach((node) => {
				this.nodeSelected(node);
			});

			this.hideSelectBox();
		},
		mouseMoveSelect (e: MouseEvent) {
			if (e.buttons === 0) {
				// Mouse button is not pressed anymore so stop selection mode
				// Happens normally when mouse leave the view pressed and then
				// comes back unpressed.
				this.mouseUpMouseSelect(e);
				return;
			}

			this.updateSelectBox(e);
		},

		nodeDeselected (node: INodeUi) {
			this.$store.commit('removeNodeFromSelection', node);
			const nodeElement = `node-${this.getNodeIndex(node.name)}`;
			// @ts-ignore
			this.instance.removeFromDragSelection(nodeElement);
		},
		nodeSelected (node: INodeUi) {
			this.$store.commit('addSelectedNode', node);
			const nodeElement = `node-${this.getNodeIndex(node.name)}`;
			// @ts-ignore
			this.instance.addToDragSelection(nodeElement);
		},
		deselectAllNodes () {
			// @ts-ignore
			this.instance.clearDragSelection();
			this.$store.commit('resetSelectedNodes');
			this.$store.commit('setLastSelectedNode', null);
			this.$store.commit('setLastSelectedNodeOutputIndex', null);
			this.$store.commit('setActiveNode', null);
		},
	},
});


import {
	ICredentialType,
	NodePropertyTypes,
} from 'n8n-workflow';

export class MicrosoftOAuth2Api implements ICredentialType {
	name = 'microsoftOAuth2Api';
	extends = [
		'oAuth2Api',
	];
	displayName = 'Microsoft OAuth2 API';
	documentationUrl = 'microsoft';
	properties = [
		//info about the tenantID
		//https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
		{
			displayName: 'Authorization URL',
			name: 'authUrl',
			type: 'string' as NodePropertyTypes,
			default: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
		},
		{
			displayName: 'Access Token URL',
			name: 'accessTokenUrl',
			type: 'string' as NodePropertyTypes,
			default: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
		},
		{
			displayName: 'Auth URI Query Parameters',
			name: 'authQueryParameters',
			type: 'hidden' as NodePropertyTypes,
			default: 'response_mode=query',
		},
		{
			displayName: 'Authentication',
			name: 'authentication',
			type: 'hidden' as NodePropertyTypes,
			default: 'body',
		},
	];
}


import {
	ICredentialType,
	NodePropertyTypes,
} from 'n8n-workflow';

const scopes = [
	'offline_access',
	'accounting.transactions',
	'accounting.settings',
	'accounting.contacts',
];

export class XeroOAuth2Api implements ICredentialType {
	name = 'xeroOAuth2Api';
	extends = [
		'oAuth2Api',
	];
	displayName = 'Xero OAuth2 API';
	documentationUrl = 'xero';
	properties = [
		{
			displayName: 'Authorization URL',
			name: 'authUrl',
			type: 'hidden' as NodePropertyTypes,
			default: 'https://login.xero.com/identity/connect/authorize',
		},
		{
			displayName: 'Access Token URL',
			name: 'accessTokenUrl',
			type: 'hidden' as NodePropertyTypes,
			default: 'https://identity.xero.com/connect/token',
		},
		{
			displayName: 'Scope',
			name: 'scope',
			type: 'hidden' as NodePropertyTypes,
			default: scopes.join(' '),
		},
		{
			displayName: 'Auth URI Query Parameters',
			name: 'authQueryParameters',
			type: 'hidden' as NodePropertyTypes,
			default: '',
		},
		{
			displayName: 'Authentication',
			name: 'authentication',
			type: 'hidden' as NodePropertyTypes,
			default: 'header',
		},
	];
}


export interface IForm {
	form: object;
}


isCtrlKeyPressed (e: MouseEvent | KeyboardEvent): boolean {

keep keep keep keep replace replace keep keep keep keep keep

const nodeViewEl = this.$el.querySelector('#node-view') as HTMLDivElement;


import {
	IDataObject
 } from 'n8n-workflow';

export interface ITask {
	name?: string;
	content?: string;
	assignees?: string[] | IDataObject;
	tags?: string[];
	status?: string;
	priority?: number;
	due_date?: number;
	due_date_time?: boolean;
	time_estimate?: number;
	start_date?: number;
	start_date_time?: boolean;
	markdown_content?: string;
	notify_all?: boolean;
	parent?: string;
	custom_fields?: IDataObject[];
}