// @ts-nocheck
/* eslint-disable */

// Components
// import VCombobox from '../VCombobox'

// Utilities
import {
  mount,
  Wrapper,
} from '@vue/test-utils'
// import { keyCodes } from '../../../util/helpers'

describe.skip('VCombobox.ts', () => {
  type Instance = InstanceType<typeof VCombobox>
  let mountFunction: (options?: object) => Wrapper<Instance>

  beforeEach(() => {
    document.body.setAttribute('data-app', 'true')

    mountFunction = (options = {}) => {
      return mount(VCombobox, {
        // https://github.com/vuejs/vue-test-utils/issues/1130
        sync: false,
        mocks: {
          $vuetify: {
            lang: {
              t: (val: string) => val,
            },
            theme: {
              dark: false,
            },
          },
        },
        ...options,
      })
    }
  })

  function createMultipleCombobox (propsData) {
    const change = jest.fn()
    const wrapper = mountFunction({
      attachToDocument: true,
      propsData: Object.assign({
        multiple: true,
        value: [],
      }, propsData),
    })

    wrapper.vm.$on('input', change)
    return { wrapper, change }
  }

  it('should create new values when tagging', async () => {
    const { wrapper, change } = createMultipleCombobox({})

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    element.value = 'foo'
    input.trigger('input')
    input.trigger('keydown.enter')

    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenCalledWith(['foo'])
  })

  it('should change selectedIndex with keyboard', async () => {
    const { wrapper } = createMultipleCombobox({
      value: ['foo', 'bar'],
    })

    const input = wrapper.find('input')

    input.trigger('focus')
    await wrapper.vm.$nextTick()

    for (const index of [1, 0, -1]) {
      input.trigger('keydown.left')
      await wrapper.vm.$nextTick()
      expect(wrapper.vm.selectedIndex).toBe(index)
    }
  })

  it('should delete a tagged item when selected and backspace/delete is pressed', async () => {
    const { wrapper, change } = createMultipleCombobox({
      value: ['foo', 'bar'],
    })

    const input = wrapper.find('input')

    input.trigger('focus')
    input.trigger('keydown.left')
    expect(wrapper.vm.selectedIndex).toBe(1)

    input.trigger('keydown.delete')
    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledWith(['foo'])
    expect(wrapper.vm.selectedIndex).toBe(0)

    const backspace = new Event('keydown')
    backspace.keyCode = keyCodes.delete

    input.element.dispatchEvent(backspace) // Avoriaz doesn't wrap keydown.backspace
    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledWith([])
    expect(wrapper.vm.selectedIndex).toBe(-1)
  })

  it('should add a tag on enter using the current searchValue', async () => {
    const { wrapper, change } = createMultipleCombobox({
      items: ['bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    await wrapper.vm.$nextTick()

    element.value = 'ba'
    input.trigger('input')
    await wrapper.vm.$nextTick()
    input.trigger('keydown.enter')
    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenCalledWith(['ba'])
  })

  it.skip('should add a tag on left arrow and select the previous tag', async () => {
    const { wrapper, change } = createMultipleCombobox({
      value: ['foo'],
      items: ['foo', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    element.value = 'b'
    input.trigger('input')
    input.trigger('keydown.left')

    expect(change).toHaveBeenCalledWith(['foo', 'b'])
    expect(wrapper.vm.selectedIndex).toBe(0)
  })

  it('should remove a duplicate tag and add it to the end', async () => {
    const { wrapper, change } = createMultipleCombobox({
      value: ['foo', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    await wrapper.vm.$nextTick()

    element.value = 'foo'
    input.trigger('input')
    input.trigger('keydown.enter')
    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenCalledWith(['bar', 'foo'])
  })

  it('should add tag with valid search value on blur', async () => {
    const { wrapper, change } = createMultipleCombobox({})

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    element.value = 'bar'
    input.trigger('input')
    input.trigger('keydown.enter')

    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenCalledWith(['bar'])
  })

  it('should be able to add a tag from user input after deleting a tag with delete', async () => {
    const { wrapper, change } = createMultipleCombobox({
      multiple: true,
      value: ['foo', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    input.trigger('keydown.left')
    expect(wrapper.vm.selectedIndex).toBe(1)
    input.trigger('keydown.delete')
    expect(change).toHaveBeenCalledWith(['foo'])
    expect(wrapper.vm.selectedIndex).toBe(0)

    // Must be reset for input to update
    wrapper.vm.selectedIndex = -1
    await wrapper.vm.$nextTick()

    element.value = 'baz'

    input.trigger('input')
    input.trigger('keydown.enter')

    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenCalledWith(['foo', 'baz'])
    expect(wrapper.vm.selectedIndex).toBe(-1)
  })

  it('should be able to add a tag from user input after clicking a deletable chip', async () => {
    const { wrapper, change } = createMultipleCombobox({
      chips: true,
      clearable: true,
      deletableChips: true,
      multiple: true,
      value: ['foo', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement
    const chip = wrapper.findAll('.v-chip').at(1)
    const close = chip.find('.v-chip__close')

    input.trigger('focus')
    chip.trigger('click')
    close.trigger('click')
    expect(change).toHaveBeenCalledWith(['foo'])
    expect(wrapper.vm.selectedIndex).toBe(-1)

    element.value = 'baz'
    input.trigger('input')
    expect(wrapper.vm.internalSearch).toBe('baz')
    input.trigger('keydown.enter')

    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenCalledWith(['foo', 'baz'])
    expect(wrapper.vm.selectedIndex).toBe(-1)
  })

  // This test is actually almost useless
  it('should not change search when selecting an index', () => {
    const { wrapper } = createMultipleCombobox({
      chips: true,
      multiple: true,
      value: ['foo', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    expect(wrapper.vm.selectedIndex).toBe(-1)

    input.trigger('keydown.left')
    expect(wrapper.vm.selectedIndex).toBe(1)

    expect(wrapper.vm.internalSearch).toBeUndefined()
    input.trigger('keydown.right')
    element.value = 'fizz'
    input.trigger('input')

    expect(wrapper.vm.internalSearch).toBe('fizz')
    expect(wrapper.vm.selectedIndex).toBe(-1)
  })

  // eslint-disable-next-line max-statements
  it('should create new items when a delimiter is entered', async () => {
    const { wrapper, change } = createMultipleCombobox({
      delimiters: [', ', 'baz'],
    })

    await wrapper.vm.$nextTick()

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement
    input.trigger('focus')

    element.value = 'foo,'
    input.trigger('input')

    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledTimes(0)

    element.value += ' '
    input.trigger('input')

    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledTimes(1)
    expect(change).toHaveBeenCalledWith(['foo'])
    expect(element.value).toBe('')

    element.value = 'foo,barba'
    input.trigger('input')

    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledTimes(1)

    element.value += 'z'
    input.trigger('input')

    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledTimes(2)
    expect(change).toHaveBeenCalledWith(['foo', 'foo,bar'])
    expect(element.value).toBe('')
  })

  it('should allow the editing of an existing value', async () => {
    const { wrapper } = createMultipleCombobox({
      chips: true,
      value: ['foo'],
    })

    const change = jest.fn()
    const internal = jest.fn()
    const chip = wrapper.find('.v-chip')
    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    wrapper.vm.$on('change', change)
    wrapper.vm.$watch('internalValue', internal)

    expect(wrapper.vm.editingIndex).toBe(-1)
    expect(wrapper.vm.internalSearch).toBeUndefined()

    chip.trigger('dblclick')

    expect(wrapper.vm.editingIndex).toBe(0)
    expect(wrapper.vm.internalSearch).toBe('foo')

    element.value = 'foobar'
    input.trigger('input')
    input.trigger('keydown.enter')

    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenCalledWith(['foobar'])
    expect(internal).toHaveBeenCalledWith(['foobar'], ['foo'])
  })

  it('should paste as item if source of pasted text is item in another v-combobox/v-autocomplete', async () => {
    const { wrapper, change } = createMultipleCombobox({
      items: ['aaa', 'bbb'],
    })

    const input = wrapper.find('input')
    const getData = jest.fn(mimeType => 'ccc')
    const event = {
      clipboardData: {
        getData,
      },
    }

    input.trigger('focus')
    input.trigger('paste', event)

    expect(getData).toHaveBeenCalledTimes(1)
    expect(getData).toHaveBeenCalledWith('text/vnd.vuetify.autocomplete.item+plain')
    expect(change).toHaveBeenCalledWith(['ccc'])
  })

  it('should paste as text if source of pasted text is not item in another v-combobox/v-autocomplete', async () => {
    const { wrapper, change } = createMultipleCombobox({
      items: ['aaa', 'bbb'],
    })

    const input = wrapper.find('input')
    const getData = jest.fn(mimeType => mimeType === 'text/plain' ? 'ccc' : '')
    const event = {
      clipboardData: {
        getData,
      },
    }

    input.trigger('focus')
    input.trigger('paste', event)

    expect(change).not.toHaveBeenCalled()
    // expect(input.element.value).toBe('ccc')  // can be checked only in browser environment
  })

  it('should not add search to list when selecting items with keyboard', async () => {
    const { wrapper, change } = createMultipleCombobox({
      chips: true,
      multiple: true,
      items: ['aaa', 'bbb'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    element.value = 'a'
    input.trigger('input')
    input.trigger('keydown.down')

    await wrapper.vm.$nextTick()

    input.trigger('keydown.enter')

    await wrapper.vm.$nextTick()

    expect(wrapper.vm.internalSearch).toBeNull()
    expect(change).toHaveBeenCalledWith(['aaa'])
  })

  // https://github.com/vuetifyjs/vuetify/issues/12781
  // eslint-disable-next-line max-statements
  it('should correctly add items after deletion and blur', async () => {
    const { wrapper, change } = createMultipleCombobox({
      multiple: true,
      chips: true,
      value: ['foo', 'bar'],
      items: ['foo', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    // delete 'bar'
    input.trigger('focus')
    input.trigger('keydown.left')
    expect(wrapper.vm.selectedIndex).toBe(1)
    input.trigger('keydown.delete')
    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledWith(['foo'])
    expect(wrapper.vm.selectedIndex).toBe(0)

    // Lose focus
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledWith(['foo'])

    // Add 'bar' again
    input.trigger('focus')
    element.value = 'bar'
    input.trigger('input')
    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()
    input.trigger('keydown.enter')
    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenLastCalledWith(['foo', 'bar'])

    // Set 'bar' as search input
    element.value = 'bar'
    input.trigger('input')
    await wrapper.vm.$nextTick()
    expect(wrapper.vm.internalSearch).toBe('bar')

    // Lose focus
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenLastCalledWith(['foo', 'bar'])
  })

  // https://github.com/vuetifyjs/vuetify/issues/13274
  it('should not add empty values', async () => {
    const { wrapper, change } = createMultipleCombobox({
      chips: true,
      multiple: true,
      items: ['foo'],
      value: ['foo'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    // Add a value and then remove it
    input.trigger('focus')
    element.value = 'a'
    input.trigger('input')
    await wrapper.vm.$nextTick()
    element.value = ''
    input.trigger('input')
    await wrapper.vm.$nextTick()

    // Lose focus
    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()

    expect(change).not.toHaveBeenCalled()
  })

  // https://github.com/vuetifyjs/vuetify/issues/10827
  it('should not add empty chips after clear and re-select', async () => {
    const { wrapper, change } = createMultipleCombobox({
      chips: true,
      multiple: true,
      clearable: true,
      items: ['foo', 'bar'],
      value: ['foo', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    // Dbl click chip at index 1
    const chip = wrapper.findAll('.v-chip').at(1)
    chip.trigger('dblclick')
    expect(wrapper.vm.editingIndex).toBe(1)
    expect(wrapper.vm.internalSearch).toBe('bar')

    // Click clear button
    const clear = wrapper.find('.v-input__icon--clear .v-icon')
    clear.trigger('click')
    await wrapper.vm.$nextTick()
    expect(change).toHaveBeenCalledWith([])
    await wrapper.vm.$nextTick()

    // Add 'foo'
    input.trigger('focus')
    element.value = 'foo'
    input.trigger('input')
    await wrapper.vm.$nextTick()
    input.trigger('keydown.enter')
    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenLastCalledWith(['foo'])
  })

  // https://github.com/vuetifyjs/vuetify/issues/12351
  it('should correctly handle duplicate items', async () => {
    const { wrapper, change } = createMultipleCombobox({
      chips: true,
      multiple: true,
      items: [
        { text: 'foo', value: 'foo' },
        { text: 'bar', value: 'bar' },
      ],
      value: [
        { text: 'foo', value: 'foo' },
      ],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    input.trigger('focus')
    element.value = 'foo'
    input.trigger('input')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.tab')
    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenLastCalledWith([{ text: 'foo', value: 'foo' }])
  })

  // https://github.com/vuetifyjs/vuetify/issues/6364
  it('should not add duplicate chip after edit', async () => {
    const { wrapper, change } = createMultipleCombobox({
      chips: true,
      multiple: true,
      clearable: true,
      items: ['foo', 'bar'],
      value: ['foo', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    // Dbl click chip at index 1
    const chip = wrapper.findAll('.v-chip').at(1)
    chip.trigger('dblclick')
    expect(wrapper.vm.editingIndex).toBe(1)
    expect(wrapper.vm.internalSearch).toBe('bar')

    // Add a duplicate value - 'foo'
    input.trigger('focus')
    element.value = 'foo'
    input.trigger('input')
    await wrapper.vm.$nextTick()
    input.trigger('keydown.enter')
    await wrapper.vm.$nextTick()

    expect(change).toHaveBeenLastCalledWith(['bar', 'foo'])
  })

  // example 1 in https://github.com/vuetifyjs/vuetify/issues/14194
  it('should not point to a result that does not exist as in example 1', async () => {
    const { wrapper } = createMultipleCombobox({
      items: ['a', 'aa', 'aaa', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    const listIndexUpdate = jest.fn()
    wrapper.vm.$on('update:list-index', listIndexUpdate)

    input.trigger('focus')
    await wrapper.vm.$nextTick()
    element.value = 'a'
    input.trigger('input')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()

    element.value = 'aa'
    input.trigger('input')
    await wrapper.vm.$nextTick()
    expect(listIndexUpdate.mock.calls.length === 6).toBe(true)
    expect(listIndexUpdate.mock.calls[0][0]).toBe(-1)
    expect(listIndexUpdate.mock.calls[1][0]).toBe(0)
    expect(listIndexUpdate.mock.calls[2][0]).toBe(1)
    expect(listIndexUpdate.mock.calls[3][0]).toBe(2)
    expect(listIndexUpdate.mock.calls[4][0]).toBe(3)
    expect(listIndexUpdate.mock.calls[5][0]).toBe(-1)
  })

  // example 2 in https://github.com/vuetifyjs/vuetify/issues/14194
  it('should not change selection on search input as in example 2', async () => {
    const { wrapper } = createMultipleCombobox({
      items: ['a', 'aa', 'aaa', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    const listIndexUpdate = jest.fn()
    wrapper.vm.$on('update:list-index', listIndexUpdate)

    input.trigger('focus')
    await wrapper.vm.$nextTick()
    element.value = 'a'
    input.trigger('input')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()

    element.value = 'aa'
    input.trigger('input')
    await wrapper.vm.$nextTick()

    expect(listIndexUpdate.mock.calls.length === 5).toBe(true)
    expect(listIndexUpdate.mock.calls[0][0]).toBe(-1)
    expect(listIndexUpdate.mock.calls[1][0]).toBe(0)
    expect(listIndexUpdate.mock.calls[2][0]).toBe(1)
    expect(listIndexUpdate.mock.calls[3][0]).toBe(2)
    expect(listIndexUpdate.mock.calls[4][0]).toBe(1)
  })

  // example 3 in https://github.com/vuetifyjs/vuetify/issues/14194
  it('should not point to a result that does not exist as in example 3', async () => {
    const { wrapper } = createMultipleCombobox({
      items: ['a', 'aa', 'aaa', 'bar'],
    })

    const input = wrapper.find('input')
    const element = input.element as HTMLInputElement

    const listIndexUpdate = jest.fn()
    wrapper.vm.$on('update:list-index', listIndexUpdate)

    input.trigger('focus')
    await wrapper.vm.$nextTick()
    element.value = 'a'
    input.trigger('input')
    await wrapper.vm.$nextTick()

    input.trigger('keydown.down')
    await wrapper.vm.$nextTick()

    element.value = 'aaaa'
    input.trigger('input')
    await wrapper.vm.$nextTick()

    expect(listIndexUpdate.mock.calls.length === 3).toBe(true)
    expect(listIndexUpdate.mock.calls[0][0]).toBe(-1)
    expect(listIndexUpdate.mock.calls[1][0]).toBe(0)
    expect(listIndexUpdate.mock.calls[2][0]).toBe(-1)
  })
})


onClick:prependInner={ (e: MouseEvent) => emit('click:prepend-inner', e) }

onClick:appendInner={ (e: MouseEvent) => emit('click:append-inner', e) }

active={ isActive.value || isDirty.value }

keep keep keep replace replace keep keep keep keep keep

// Styles
import './VInput.sass'

// Components
import { VIcon } from '@/components/VIcon'
import { VMessages } from '@/components/VMessages'

// Composables
import { makeDensityProps, useDensity } from '@/composables/density'
import { makeValidationProps, useValidation } from '@/composables/validation'

// Utilities
import { computed } from 'vue'
import { genericComponent, getUid, pick, propsFactory, useRender } from '@/util'

// Types
import type { ComputedRef, ExtractPropTypes, PropType, Ref } from 'vue'
import type { MakeSlots } from '@/util'

export interface VInputSlot {
  id: ComputedRef<string>
  isDirty: ComputedRef<boolean>
  isDisabled: ComputedRef<boolean>
  isReadonly: ComputedRef<boolean>
  isPristine: Ref<boolean>
  isValid: ComputedRef<boolean | null>
  isValidating: Ref<boolean>
  reset: () => void
  resetValidation: () => void
  validate: () => void
}

export const makeVInputProps = propsFactory({
  id: String,
  appendIcon: String,
  prependIcon: String,
  hideDetails: [Boolean, String] as PropType<boolean | 'auto'>,
  messages: {
    type: [Array, String] as PropType<string | string[]>,
    default: () => ([]),
  },
  direction: {
    type: String as PropType<'horizontal' | 'vertical'>,
    default: 'horizontal',
    validator: (v: any) => ['horizontal', 'vertical'].includes(v),
  },

  ...makeDensityProps(),
  ...makeValidationProps(),
})

export type VInputSlots = MakeSlots<{
  default: [VInputSlot]
  prepend: [VInputSlot]
  append: [VInputSlot]
  details: [VInputSlot]
}>

export const VInput = genericComponent<new <T>() => {
  $slots: VInputSlots
}>()({
  name: 'VInput',

  props: {
    ...makeVInputProps(),
  },

  emits: {
    'click:prepend': (e: MouseEvent) => true,
    'click:append': (e: MouseEvent) => true,
    'update:modelValue': (val: any) => true,
  },

  setup (props, { slots, emit }) {
    const { densityClasses } = useDensity(props)
    const {
      errorMessages,
      isDirty,
      isDisabled,
      isReadonly,
      isPristine,
      isValid,
      isValidating,
      reset,
      resetValidation,
      validate,
      validationClasses,
    } = useValidation(props)

    const uid = getUid()
    const id = computed(() => props.id || `input-${uid}`)

    const slotProps = computed<VInputSlot>(() => ({
      id,
      isDirty,
      isDisabled,
      isReadonly,
      isPristine,
      isValid,
      isValidating,
      reset,
      resetValidation,
      validate,
    }))

    useRender(() => {
      const hasPrepend = !!(slots.prepend || props.prependIcon)
      const hasAppend = !!(slots.append || props.appendIcon)
      const hasMessages = !!(
        props.messages?.length ||
        errorMessages.value.length
      )
      const hasDetails = !props.hideDetails || (
        props.hideDetails === 'auto' &&
        hasMessages
      )

      return (
        <div class={[
          'v-input',
          `v-input--${props.direction}`,
          densityClasses.value,
          validationClasses.value,
        ]}
        >
          { hasPrepend && (

              { slots?.prepend?.(slotProps.value) }

              { props.prependIcon && (
                <VIcon
                  onClick={ (e: MouseEvent) => emit('click:prepend', e) }
                  icon={ props.prependIcon }
                />
              ) }

          ) }

          { slots.default && (

              { slots.default?.(slotProps.value) }

          ) }

          { hasAppend && (

              { slots?.append?.(slotProps.value) }

              { props.appendIcon && (
                <VIcon
                  onClick={ (e: MouseEvent) => emit('click:append', e) }
                  icon={ props.appendIcon }
                />
              ) }

          ) }

          { hasDetails && (

              <VMessages
                active={ hasMessages }
                messages={ errorMessages.value.length > 0
                  ? errorMessages.value
                  : props.messages
                }
                v-slots={{ message: slots.message }}
              />

              { slots.details?.(slotProps.value) }

          ) }

      )
    })

    return {
      reset,
      resetValidation,
      validate,
    }
  },
})

export type VInput = InstanceType<typeof VInput>

export function filterInputProps (props: ExtractPropTypes<ReturnType<typeof makeVInputProps>>) {
  return pick(props, Object.keys(VInput.props) as any)
}


// Styles
import './VCarousel.sass'

// Components
import { VBtn } from '@/components/VBtn'
import { VDefaultsProvider } from '@/components/VDefaultsProvider'
import { VProgressLinear } from '@/components/VProgressLinear'
import { VWindow } from '@/components/VWindow'

// Composables
import { useLocale } from '@/composables/locale'
import { useProxiedModel } from '@/composables/proxiedModel'

// Utilities
import { convertToUnit, defineComponent, useRender } from '@/util'
import { onMounted, ref, watch } from 'vue'

// Types
import type { PropType } from 'vue'
import type { GroupProvide } from '@/composables/group'

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

  props: {
    color: String,
    cycle: Boolean,
    delimiterIcon: {
      type: String,
      default: '$delimiter',
    },
    height: {
      type: [Number, String],
      default: 500,
    },
    hideDelimiters: Boolean,
    hideDelimiterBackground: Boolean,
    interval: {
      type: [Number, String],
      default: 6000,
      validator: (value: string | number) => value > 0,
    },
    modelValue: null,
    progress: [Boolean, String],
    showArrows: {
      type: [Boolean, String],
      default: true,
      validator: (v: any) => typeof v === 'boolean' || v === 'hover',
    },
    verticalDelimiters: [Boolean, String] as PropType<boolean | 'left' | 'right'>,
  },

  emits: {
    'update:modelValue': (val: any) => true,
  },

  setup (props, { slots }) {
    const model = useProxiedModel(props, 'modelValue')
    const { t } = useLocale()
    const windowRef = ref<typeof VWindow>()

    let slideTimeout = -1
    watch(model, restartTimeout)
    watch(() => props.interval, restartTimeout)
    watch(() => props.cycle, val => {
      if (val) restartTimeout()
      else window.clearTimeout(slideTimeout)
    })

    onMounted(startTimeout)

    function startTimeout () {
      if (!props.cycle || !windowRef.value) return

      slideTimeout = window.setTimeout(windowRef.value.group.next, +props.interval > 0 ? +props.interval : 6000)
    }

    function restartTimeout () {
      window.clearTimeout(slideTimeout)
      window.requestAnimationFrame(startTimeout)
    }

    useRender(() => (
      <VWindow
        ref={ windowRef }
        v-model={ model.value }
        class={[
          'v-carousel',
          {
            'v-carousel--hide-delimiter-background': props.hideDelimiterBackground,
            'v-carousel--vertical-delimiters': props.verticalDelimiters,
          },
        ]}
        style={{ height: convertToUnit(props.height) }}
        continuous
        showArrows={ props.showArrows }
        mandatory="force"
      >
        {{
          default: slots.default,
          additional: ({ group }: { group: GroupProvide }) => (
            <>
              { !props.hideDelimiters && (
                <div
                  class="v-carousel__controls"
                  style={{
                    left: props.verticalDelimiters === 'left' && props.verticalDelimiters ? 0 : 'auto',
                    right: props.verticalDelimiters === 'right' ? 0 : 'auto',
                  }}
                >
                  { group.items.value.length > 0 && (
                    <VDefaultsProvider
                      defaults={{
                        VBtn: {
                          color: props.color,
                          icon: props.delimiterIcon,
                          size: 'x-small',
                          variant: 'text',
                        },
                      }}
                      scoped
                    >
                      { group.items.value.map((item: any) => {
                        const props = {
                          'aria-label': t('$vuetify.carousel.ariaLabel.delimiter'),
                          class: [group.isSelected(item.id) && 'v-btn--selected'],
                          onClick: () => group.select(item.id, true),
                        }

                        return slots.item
                          ? slots.item({ props, item })
                          : (<VBtn { ...item } { ...props } />)
                      }) }
                    </VDefaultsProvider>
                  ) }
                </div>
              )}

              { props.progress && (
                <VProgressLinear
                  class="v-carousel__progress"
                  color={ typeof props.progress === 'string' ? props.progress : undefined }
                  modelValue={ (group.getItemIndex(model.value) + 1) / group.items.value.length * 100 }
                />
              )}
            </>
          ),
          prev: slots.prev,
          next: slots.next,
        }}
      </VWindow>
    ))
  },
})


// Utilities
import { getCurrentInstance } from './getCurrentInstance'

// Types
import type { VNode } from 'vue'

export function useRender (render: () => VNode): void {
  const vm = getCurrentInstance('useRender') as any
  vm.render = render
}


Icon to display when step is marked as completed

Accepts a mixed array of types `function`, `boolean` and `string`. Functions pass an input value as an argument and must return either `true` / `false` or a `string` containing an error message. The input field will enter an error state if a function returns (or any value in the array contains) `false` or is a `string`


'use strict';
const url = require('url');
/**
* Asset link tag
*
* Syntax:

const util = require('hexo-util');
const escapeHTML = util.escapeHTML;

'use strict';

const Promise = require('bluebird');

describe('asset_link', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo(__dirname);
  const assetLinkTag = require('../../../lib/plugins/tag/asset_link')(hexo);
  const Post = hexo.model('Post');
  const PostAsset = hexo.model('PostAsset');
  let post;

  hexo.config.permalink = ':title/';

  function assetLink(args) {
    return assetLinkTag.call(post, args.split(' '));
  }

  before(() => hexo.init().then(() => Post.insert({
    source: 'foo.md',
    slug: 'foo'
  })).then(post_ => {
    post = post_;

    return Promise.all([
      PostAsset.insert({
        _id: 'bar',
        slug: 'bar',
        post: post._id
      }),
      PostAsset.insert({
        _id: 'spaced asset',
        slug: 'spaced asset',
        post: post._id
      })
    ]);
  }));

  it('default', () => {
    assetLink('bar').should.eql('bar');
  });

  it('title', () => {
    assetLink('bar Hello world').should.eql('Hello world');
  });

  it('with space', () => {
    // {% asset_link "spaced asset" "spaced title" %}
    assetLinkTag.call(post, ['spaced asset', 'spaced title'])
      .should.eql('spaced title');
  });

  it('no slug', () => {
    should.not.exist(assetLink(''));
  });

  it('asset not found', () => {
    should.not.exist(assetLink('boo'));
  });
});


'use strict';

const Promise = require('bluebird');
const fs = require('hexo-fs');

function cleanConsole(args) {
  return Promise.all([
    deleteDatabase(this),
    deletePublicDir(this),
    this.execFilter('after_clean', null, {context: this})
  ]);
}

function deleteDatabase(ctx) {
  const dbPath = ctx.database.options.path;

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

    return fs.unlink(dbPath).then(() => {
      ctx.log.info('Deleted database.');
    });
  });
}

function deletePublicDir(ctx) {
  const publicDir = ctx.public_dir;

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

    return fs.rmdir(publicDir).then(() => {
      ctx.log.info('Deleted public folder.');
    });
  });
}

module.exports = cleanConsole;


'use strict';

function hexoMetaGeneratorInject(data) {
  const { config } = this;
  if (!config.meta_generator ||
  data.match(/<meta\s+name=['|"]?generator['|"]?/i)) return;

  const hexoGeneratorTag = `<meta name="generator" content="Hexo ${this.version}">`;

  return data.replace(/<head>(?!<\/head>).+?<\/head>/, (str) => str.replace('</head>', `${hexoGeneratorTag}</head>`));
}

module.exports = hexoMetaGeneratorInject;


'use strict';

function feedTagHelper(path, options = {}) {
  const title = options.title || this.config.title;

  return `<link rel="alternate" href="${this.url_for(path)}" title="${title}">`;
}

module.exports = feedTagHelper;


keep keep keep keep keep replace keep keep keep keep

const title = escapeHTML(args.length ? args.join(' ') : asset.slug);

'use strict';

const url = require('url');

/**
 * Asset link tag
 *
 * Syntax:
 *   {% asset_link slug [title] %}
 */
module.exports = ctx => {
  const PostAsset = ctx.model('PostAsset');

  return function assetLinkTag(args) {
    const slug = args.shift();
    if (!slug) return;

    const asset = PostAsset.findOne({post: this._id, slug});
    if (!asset) return;

    const title = args.length ? args.join(' ') : asset.slug;

    return `<a href="${url.resolve(ctx.config.root, asset.path)}" title="${title}">${title}</a>`;
  };
};


'use strict';

const Promise = require('bluebird');

function readStream(stream) {
  return new Promise((resolve, reject) => {
    let data = '';

    stream.on('data', chunk => {
      data += chunk.toString();
    }).on('end', () => {
      resolve(data);
    }).on('error', reject);
  });
}

exports.read = readStream;


'use strict';

function Locals() {
  this.cache = {};
  this.getters = {};
}

Locals.prototype.get = function(name) {
  if (typeof name !== 'string') throw new TypeError('name must be a string!');

  let cache = this.cache[name];

  if (cache == null) {
    const getter = this.getters[name];
    if (!getter) return;

    cache = this.cache[name] = getter();
  }

  return cache;
};

Locals.prototype.set = function(name, value) {
  if (typeof name !== 'string') throw new TypeError('name must be a string!');
  if (value == null) throw new TypeError('value is required!');

  const getter = typeof value === 'function' ? value : () => value;

  this.getters[name] = getter;
  this.cache[name] = null;

  return this;
};

Locals.prototype.remove = function(name) {
  if (typeof name !== 'string') throw new TypeError('name must be a string!');

  this.getters[name] = null;
  this.cache[name] = null;

  return this;
};

Locals.prototype.invalidate = function() {
  this.cache = {};

  return this;
};

Locals.prototype.toObject = function() {
  const result = {};
  const keys = Object.keys(this.getters);

  for (let i = 0, len = keys.length; i < len; i++) {
    const key = keys[i];
    const item = this.get(key);

    if (item != null) result[key] = item;
  }

  return result;
};

module.exports = Locals;


'use strict';

describe('Migrator', () => {
  const Migrator = require('../../../lib/extend/migrator');

  it('register()', () => {
    const d = new Migrator();

    // name, fn
    d.register('test', () => {});

    d.get('test').should.exist;

    // no name
    try {
      d.register();
    } catch (err) {
      err.should.be
        .instanceOf(TypeError)
        .property('message', 'name is required');
    }

    // no fn
    try {
      d.register('test');
    } catch (err) {
      err.should.be
        .instanceOf(TypeError)
        .property('message', 'fn must be a function');
    }
  });

  it('register() - promisify', () => {
    const d = new Migrator();

    d.register('test', (args, callback) => {
      args.should.eql({foo: 'bar'});
      callback(null, 'foo');
    });

    d.get('test')({
      foo: 'bar'
    }).then(result => {
      result.should.eql('foo');
    });
  });

  it('register() - Promise.method', () => {
    const d = new Migrator();

    d.register('test', args => {
      args.should.eql({foo: 'bar'});
      return 'foo';
    });

    d.get('test')({
      foo: 'bar'
    }).then(result => {
      result.should.eql('foo');
    });
  });

  it('list()', () => {
    const d = new Migrator();

    d.register('test', () => {});

    d.list().should.have.keys(['test']);
  });

  it('get()', () => {
    const d = new Migrator();

    d.register('test', () => {});

    d.get('test').should.exist;
  });
});


'use strict';
/**
* Post link tag
*
* Syntax:
*   {% post_link slug [title] %}

'use strict';

describe('post_link', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo(__dirname);
  const postLink = require('../../../lib/plugins/tag/post_link')(hexo);
  const Post = hexo.model('Post');

  hexo.config.permalink = ':title/';

  before(() => hexo.init().then(() => Post.insert({
    source: 'foo',
    slug: 'foo',
    title: 'Hello world'
  })));

  it('default', () => {
    postLink(['foo']).should.eql('Hello world');
  });

  it('title', () => {
    postLink(['foo', 'test']).should.eql('test');
  });

  it('no slug', () => {
    should.not.exist(postLink([]));
  });

  it('post not found', () => {
    should.not.exist(postLink(['bar']));
  });
});


'use strict';

const moment = require('moment-timezone');
const { isMoment } = moment;
const isDate = require('lodash/isDate');

function getMoment(date, lang, timezone) {
  if (date == null) date = moment();
  if (!isMoment(date)) date = moment(isDate(date) ? date : new Date(date));

  if (lang) date = date.locale(lang);
  if (timezone) date = date.tz(timezone);

  return date;
}

function toISOString(date) {
  if (date == null) {
    return new Date().toISOString();
  }

  if (date instanceof Date || isMoment(date)) {
    return date.toISOString();
  }

  return new Date(date).toISOString();
}

function dateHelper(date, format) {
  const { config } = this;
  const moment = getMoment(date, getLanguage(this), config.timezone);
  return moment.format(format || config.date_format);
}

function timeHelper(date, format) {
  const { config } = this;
  const moment = getMoment(date, getLanguage(this), config.timezone);
  return moment.format(format || config.time_format);
}

function fullDateHelper(date, format) {
  if (format) {
    const moment = getMoment(date, getLanguage(this), this.config.timezone);
    return moment.format(format);
  }

  return `${this.date(date)} ${this.time(date)}`;
}

function relativeDateHelper(date) {
  const { config } = this;
  const moment = getMoment(date, getLanguage(this), config.timezone);
  return moment.fromNow();
}

function timeTagHelper(date, format) {
  const { config } = this;
  return `<time datetime="${toISOString(date)}">${this.date(date, format, getLanguage(this), config.timezone)}</time>`;
}

function getLanguage(ctx) {
  return ctx.page.lang || ctx.page.language || ctx.config.language;
}

exports.date = dateHelper;
exports.date_xml = toISOString;
exports.time = timeHelper;
exports.full_date = fullDateHelper;
exports.relative_date = relativeDateHelper;
exports.time_tag = timeTagHelper;
exports.moment = moment;


'use strict';

const sinon = require('sinon');
const pathFn = require('path');

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

  it('default values', () => Asset.insert({
    _id: 'foo',
    path: 'bar'
  }).then(data => {
    data.modified.should.be.true;
    return Asset.removeById(data._id);
  }));

  it('_id - required', () => {
    const errorCallback = sinon.spy(err => {
      err.should.have.property('message', 'ID is not defined');
    });

    return Asset.insert({}).catch(errorCallback).finally(() => {
      errorCallback.calledOnce.should.be.true;
    });
  });

  it('path - required', () => {
    const errorCallback = sinon.spy(err => {
      err.should.have.property('message', '`path` is required!');
    });

    return Asset.insert({
      _id: 'foo'
    }).catch(errorCallback).finally(() => {
      errorCallback.calledOnce.should.be.true;
    });
  });

  it('source - virtual', () => Asset.insert({
    _id: 'foo',
    path: 'bar'
  }).then(data => {
    data.source.should.eql(pathFn.join(hexo.base_dir, data._id));
    return Asset.removeById(data._id);
  }));
});


'use strict';

const fs = require('hexo-fs');

describe('clean', () => {
  const Hexo = require('../../../lib/hexo');
  let hexo, clean;

  beforeEach(() => {
    hexo = new Hexo(__dirname, {silent: true});
    clean = require('../../../lib/plugins/console/clean').bind(hexo);
  });

  it('delete database', () => {
    const dbPath = hexo.database.options.path;

    return fs.writeFile(dbPath, '').then(() => clean()).then(() => fs.exists(dbPath)).then(exist => {
      exist.should.be.false;
    });
  });

  it('delete public folder', () => {
    const publicDir = hexo.public_dir;

    return fs.mkdirs(publicDir).then(() => clean()).then(() => fs.exists(publicDir)).then(exist => {
      exist.should.be.false;
    });
  });

  it('execute corresponding filter', () => {
    const extraDbPath = hexo.database.options.path + '.tmp';

    hexo.extend.filter.register('after_clean', () => {
      return fs.unlink(extraDbPath);
    });

    return fs.writeFile(extraDbPath, '').then(() => clean()).then(() => fs.exists(extraDbPath)).then(exist => {
      exist.should.be.false;
    });
  });
});


const title = args.length ? args.join(' ') : post.title;

return `<a href="${ctx.config.root}${post.path}" title="${title}">${title}</a>`;

keep keep keep keep keep replace keep keep keep

const title = escapeHTML(args.length ? args.join(' ') : post.title);


'use strict';

const { resolve } = require('path');
const tildify = require('tildify');
const prettyHrtime = require('pretty-hrtime');
const fs = require('hexo-fs');
const chalk = require('chalk');

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

  const baseDir = this.base_dir;
  const src = resolve(baseDir, args._[0]);
  const output = args.o || args.output;
  const start = process.hrtime();
  const { log } = this;

  return this.render.render({
    path: src,
    engine: args.engine
  }).then(result => {
    if (typeof result === 'object') {
      if (args.pretty) {
        result = JSON.stringify(result, null, '  ');
      } else {
        result = JSON.stringify(result);
      }
    }

    if (!output) return console.log(result);

    const dest = resolve(baseDir, output);
    const interval = prettyHrtime(process.hrtime(start));

    log.info('Rendered in %s: %s -> %s', chalk.cyan(interval), chalk.magenta(tildify(src)), chalk.magenta(tildify(dest)));
    return fs.writeFile(dest, result);
  });
}

module.exports = renderConsole;


'use strict';

const url = require('url');

function urlForHelper(path = '/', options) {
  if (path[0] === '#' || path.startsWith('//')) {
    return path;
  }

  const { config } = this;
  const { root } = config;
  const data = url.parse(path);

  options = Object.assign({
    relative: config.relative_link
  }, options);

  // Exit if this is an external path
  if (data.protocol) {
    return path;
  }

  // Resolve relative url
  if (options.relative) {
    return this.relative_url(this.path, path);
  }

  // Prepend root path
  path = root + path;

  return path.replace(/\/{2,}/g, '/');
}

module.exports = urlForHelper;


'use strict';

describe('Deployer', () => {
  const Deployer = require('../../../lib/extend/deployer');

  it('register()', () => {
    const d = new Deployer();

    // name, fn
    d.register('test', () => {});

    d.get('test').should.exist;

    // no name
    try {
      d.register();
    } catch (err) {
      err.should.be
        .instanceOf(TypeError)
        .property('message', 'name is required');
    }

    // no fn
    try {
      d.register('test');
    } catch (err) {
      err.should.be
        .instanceOf(TypeError)
        .property('message', 'fn must be a function');
    }
  });

  it('register() - promisify', () => {
    const d = new Deployer();

    d.register('test', (args, callback) => {
      args.should.eql({foo: 'bar'});
      callback(null, 'foo');
    });

    d.get('test')({
      foo: 'bar'
    }).then(result => {
      result.should.eql('foo');
    });
  });

  it('register() - Promise.method', () => {
    const d = new Deployer();

    d.register('test', args => {
      args.should.eql({foo: 'bar'});
      return 'foo';
    });

    d.get('test')({
      foo: 'bar'
    }).then(result => {
      result.should.eql('foo');
    });
  });

  it('list()', () => {
    const d = new Deployer();

    d.register('test', () => {});

    d.list().should.have.keys(['test']);
  });

  it('get()', () => {
    const d = new Deployer();

    d.register('test', () => {});

    d.get('test').should.exist;
  });
});


assetLink('bar Hello world').should.eql('Hello world');

// {% asset_link "spaced asset" "spaced title" %}

assetLinkTag.call(post, ['spaced asset', 'spaced title'])

assetLink('bar Hello <world>').should.eql('Hello <world>');


'use strict';

const Color = require('hexo-util').Color;

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

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

  const min = options.min_font || 10;
  const max = options.max_font || 20;
  const orderby = options.orderby || 'name';
  const order = options.order || 1;
  const unit = options.unit || 'px';
  let color = options.color;
  const { transform } = options;
  const separator = options.separator || ' ';
  const result = [];
  let startColor, endColor;

  if (color) {
    startColor = new Color(options.start_color);
    endColor = new Color(options.end_color);

    if (!startColor || !endColor) color = false;
  }

  // Sort the tags
  if (orderby === 'random' || orderby === 'rand') {
    tags = tags.random();
  } else {
    tags = tags.sort(orderby, order);
  }

  // Ignore tags with zero posts
  tags = tags.filter(tag => tag.length);

  // Limit the number of tags
  if (options.amount) {
    tags = tags.limit(options.amount);
  }

  const sizes = [];

  tags.sort('length').forEach(tag => {
    const { length } = tag;
    if (sizes.includes(length)) return;

    sizes.push(length);
  });

  const length = sizes.length - 1;

  tags.forEach(tag => {
    const ratio = length ? sizes.indexOf(tag.length) / length : 0;
    const size = min + ((max - min) * ratio);
    let style = `font-size: ${parseFloat(size.toFixed(2))}${unit};`;

    if (color) {
      const midColor = startColor.mix(endColor, ratio);
      style += ` color: ${midColor.toString()}`;
    }

    result.push(
      `<a href="${this.url_for(tag.path)}" style="${style}">${transform ? transform(tag.name) : tag.name}</a>`
    );
  });

  return result.join(separator);
}

module.exports = tagcloudHelper;


'use strict';

const models = require('../models');

module.exports = ctx => {
  const db = ctx.database;

  const keys = Object.keys(models);

  for (let i = 0, len = keys.length; i < len; i++) {
    const key = keys[i];
    db.model(key, models[key](ctx));
  }
};


keep keep keep keep replace keep keep keep

before(() => hexo.init().then(() => Post.insert([{

'use strict';

const Promise = require('bluebird');

describe('asset_link', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo(__dirname);
  const assetLinkTag = require('../../../lib/plugins/tag/asset_link')(hexo);
  const Post = hexo.model('Post');
  const PostAsset = hexo.model('PostAsset');
  let post;

  hexo.config.permalink = ':title/';

  function assetLink(args) {
    return assetLinkTag.call(post, args.split(' '));
  }

  before(() => hexo.init().then(() => Post.insert({
    source: 'foo.md',
    slug: 'foo'
  })).then(post_ => {
    post = post_;

    return Promise.all([
      PostAsset.insert({
        _id: 'bar',
        slug: 'bar',
        post: post._id
      }),
      PostAsset.insert({
        _id: 'spaced asset',
        slug: 'spaced asset',
        post: post._id
      })
    ]);
  }));

  it('default', () => {
    assetLink('bar').should.eql('bar');
  });

  it('title', () => {
    assetLink('bar Hello world').should.eql('Hello world');
  });

  it('with space', () => {
    // {% asset_link "spaced asset" "spaced title" %}
    assetLinkTag.call(post, ['spaced asset', 'spaced title'])
      .should.eql('spaced title');
  });

  it('no slug', () => {
    should.not.exist(assetLink(''));
  });

  it('asset not found', () => {
    should.not.exist(assetLink('boo'));
  });
});


'use strict';

const url = require('url');
const img = require('./img');

/**
 * Asset image tag
 *
 * Syntax:
 *   {% asset_img [class names] slug [width] [height] [title text [alt text]]%}
 */
module.exports = ctx => {
  const PostAsset = ctx.model('PostAsset');

  return function assetImgTag(args) {
    const len = args.length;

    // Find image URL
    for (let i = 0; i < len; i++) {
      const asset = PostAsset.findOne({post: this._id, slug: args[i]});
      if (asset) {
        args[i] = url.resolve('/', asset.path);
        return img(ctx)(args);
      }
    }
  };
};


'use strict';

describe('i18n locals', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo();
  const i18nFilter = require('../../../lib/plugins/filter/template_locals/i18n').bind(hexo);
  const theme = hexo.theme;
  const i18n = theme.i18n;

  // Default language
  i18n.languages = ['en', 'default'];

  // Fixtures
  i18n.set('de', {
    Home: 'Zuhause'
  });

  i18n.set('default', {
    Home: 'Default Home'
  });

  i18n.set('en', {
    Home: 'Home'
  });

  i18n.set('zh-tw', {
    Home: '首頁'
  });

  it('page.lang set', () => {
    const locals = {
      config: hexo.config,
      page: {
        lang: 'zh-tw'
      }
    };

    i18nFilter(locals);

    locals.__('Home').should.eql('首頁');
  });

  it('page.language set', () => {
    const locals = {
      config: hexo.config,
      page: {
        language: 'zh-tw'
      }
    };

    i18nFilter(locals);

    locals.__('Home').should.eql('首頁');
  });

  it('detect by path (lang found)', () => {
    const locals = {
      config: hexo.config,
      page: {},
      path: 'zh-tw/index.html'
    };

    i18nFilter(locals);

    locals.page.lang.should.eql('zh-tw');
    locals.page.canonical_path.should.eql('index.html');
    locals.__('Home').should.eql('首頁');
  });

  it('detect by path (lang not found)', () => {
    const locals = {
      config: hexo.config,
      page: {},
      path: 'news/index.html'
    };

    i18nFilter(locals);

    locals.page.lang.should.eql('en');
    locals.page.canonical_path.should.eql('news/index.html');
    locals.__('Home').should.eql('Home');
  });

  it('use config by default', () => {
    const locals = {
      config: hexo.config,
      page: {},
      path: 'index.html'
    };

    i18nFilter(locals);

    locals.page.lang.should.eql('en');
    locals.page.canonical_path.should.eql('index.html');
    locals.__('Home').should.eql('Home');
  });

  it('use config by default - with multiple languages, first language should be used', () => {
    const oldConfig = i18n.languages;
    i18n.languages = ['zh-tw', 'en', 'default'];

    const locals = {
      config: hexo.config,
      page: {},
      path: 'index.html'
    };

    i18nFilter(locals);

    locals.page.lang.should.eql('zh-tw');
    locals.page.canonical_path.should.eql('index.html');
    locals.__('Home').should.eql('首頁');

    i18n.languages = oldConfig;
  });

  it('use config by default - with no languages, default language should be used', () => {
    const oldConfig = i18n.language;
    i18n.languages = ['default'];

    const locals = {
      config: hexo.config,
      page: {},
      path: 'index.html'
    };

    i18nFilter(locals);

    locals.page.lang.should.eql('default');
    locals.page.canonical_path.should.eql('index.html');
    locals.__('Home').should.eql('Default Home');

    i18n.languages = oldConfig;
  });

  it('use config by default - with unknown language, default language should be used', () => {
    const oldConfig = i18n.languages;
    i18n.languages = ['fr', 'default'];

    const locals = {
      config: hexo.config,
      page: {},
      path: 'index.html'
    };

    i18nFilter(locals);

    locals.page.lang.should.eql('fr');
    locals.page.canonical_path.should.eql('index.html');
    locals.__('Home').should.eql('Default Home');

    i18n.languages = oldConfig;
  });

  it('use config by default - with no set language and no default file take first available', () => {
    const oldConfig = i18n.languages;
    const oldSet = i18n.get('default');
    i18n.remove('default');
    i18n.languages = ['default'];

    const locals = {
      config: hexo.config,
      page: {},
      path: 'index.html'
    };

    i18nFilter(locals);

    locals.page.lang.should.eql('default');
    locals.page.canonical_path.should.eql('index.html');
    locals.__('Home').should.eql('Zuhause');

    i18n.set('default', oldSet);
    i18n.languages = oldConfig;
  });
});


keep keep keep replace keep keep keep keep keep

},
{
source: 'title-with-tag',
slug: 'title-with-tag',
title: 'Hello <new world>!'
}])));

'use strict';

/**
 * Post link tag
 *
 * Syntax:
 *   {% post_link slug [title] %}
 */
module.exports = ctx => {
  const Post = ctx.model('Post');

  return function postLinkTag(args) {
    const slug = args.shift();
    if (!slug) return;

    const post = Post.findOne({slug});
    if (!post) return;

    const title = args.length ? args.join(' ') : post.title;

    return `<a href="${ctx.config.root}${post.path}" title="${title}">${title}</a>`;
  };
};


'use strict';

const fs = require('hexo-fs');
const { basename, extname, join } = require('path');
const stripIndent = require('strip-indent');
const { highlight } = require('hexo-util');

const rCaptionTitleFile = /(.*)?(?:\s+|^)(\/*\S+)/;
const rLang = /\s*lang:(\w+)/i;
const rFrom = /\s*from:(\d+)/i;
const rTo = /\s*to:(\d+)/i;

/**
* Include code tag
*
* Syntax:
*   {% include_code [title] [lang:language] path/to/file %}
*/

module.exports = ctx => function includeCodeTag(args) {
  const config = ctx.config.highlight || {};
  let codeDir = ctx.config.code_dir;
  let arg = args.join(' ');

  // Add trailing slash to codeDir
  if (codeDir[codeDir.length - 1] !== '/') codeDir += '/';

  let lang = '';
  arg = arg.replace(rLang, (match, _lang) => {
    lang = _lang;
    return '';
  });
  let from = 0;
  arg = arg.replace(rFrom, (match, _from) => {
    from = _from - 1;
    return '';
  });
  let to = Number.MAX_VALUE;
  arg = arg.replace(rTo, (match, _to) => {
    to = _to;
    return '';
  });

  const match = arg.match(rCaptionTitleFile);

  // Exit if path is not defined
  if (!match) return;

  const path = match[2];

  // If the title is not defined, use file name instead
  const title = match[1] || basename(path);

  // If the language is not defined, use file extension instead
  lang = lang || extname(path).substring(1);

  const src = join(ctx.source_dir, codeDir, path);

  const caption = `<span>${title}</span><a href="${ctx.config.root}${codeDir}${path}">view raw</a>`;

  const options = {
    lang,
    caption,
    gutter: config.line_number,
    hljs: config.hljs,
    tab: config.tab_replace
  };

  return fs.exists(src).then(exist => {
    if (exist) return fs.readFile(src);
  }).then(code => {
    if (!code) return;

    code = stripIndent(code);
    const lines = code.split('\n');
    code = lines.slice(from, to).join('\n').trim();

    if (!config.enable) {
      return `<pre><code>${code}</code></pre>`;
    }

    return highlight(code, options);
  });
};


'use strict';

const Promise = require('bluebird');

function readStream(stream) {
  return new Promise((resolve, reject) => {
    let data = '';

    stream.on('data', chunk => {
      data += chunk.toString();
    }).on('end', () => {
      resolve(data);
    }).on('error', reject);
  });
}

exports.read = readStream;


'use strict';

const Promise = require('bluebird');
const { sep, join, dirname } = require('path');
const tildify = require('tildify');
const Database = require('warehouse');
const _ = require('lodash');
const chalk = require('chalk');
const { EventEmitter } = require('events');
const fs = require('hexo-fs');
const Module = require('module');
const vm = require('vm');
const pkg = require('../../package.json');
const logger = require('hexo-log');
const extend = require('../extend');
const Render = require('./render');
const registerModels = require('./register_models');
const Post = require('./post');
const Scaffold = require('./scaffold');
const Source = require('./source');
const Router = require('./router');
const Theme = require('../theme');
const Locals = require('./locals');
const defaultConfig = require('./default_config');
const loadDatabase = require('./load_database');
const multiConfigPath = require('./multi_config_path');
const resolve = require('resolve');

const { cloneDeep, debounce } = _;

const libDir = dirname(__dirname);
const dbVersion = 1;

function Hexo(base = process.cwd(), args = {}) {
  Reflect.apply(EventEmitter, this, []);

  this.base_dir = base + sep;
  this.public_dir = join(base, 'public') + sep;
  this.source_dir = join(base, 'source') + sep;
  this.plugin_dir = join(base, 'node_modules') + sep;
  this.script_dir = join(base, 'scripts') + sep;
  this.scaffold_dir = join(base, 'scaffolds') + sep;
  this.theme_dir = join(base, 'themes', defaultConfig.theme) + sep;
  this.theme_script_dir = join(this.theme_dir, 'scripts') + sep;

  this.env = {
    args,
    debug: Boolean(args.debug),
    safe: Boolean(args.safe),
    silent: Boolean(args.silent),
    env: process.env.NODE_ENV || 'development',
    version: pkg.version,
    init: false
  };

  this.extend = {
    console: new extend.Console(),
    deployer: new extend.Deployer(),
    filter: new extend.Filter(),
    generator: new extend.Generator(),
    helper: new extend.Helper(),
    migrator: new extend.Migrator(),
    processor: new extend.Processor(),
    renderer: new extend.Renderer(),
    tag: new extend.Tag()
  };

  this.config = cloneDeep(defaultConfig);

  this.log = logger(this.env);

  this.render = new Render(this);

  this.route = new Router();

  this.post = new Post(this);

  this.scaffold = new Scaffold(this);

  this._dbLoaded = false;

  this._isGenerating = false;

  // If `output` is provided, use that as the
  // root for saving the db. Otherwise default to `base`.
  const dbPath = args.output || base;

  this.log.d(`Writing database to ${dbPath}/db.json`);

  this.database = new Database({
    version: dbVersion,
    path: join(dbPath, 'db.json')
  });

  const mcp = multiConfigPath(this);

  this.config_path = args.config ? mcp(base, args.config, args.output) :
    join(base, '_config.yml');

  registerModels(this);

  this.source = new Source(this);
  this.theme = new Theme(this);
  this.locals = new Locals(this);
  this._bindLocals();
}

require('util').inherits(Hexo, EventEmitter);

Hexo.prototype._bindLocals = function() {
  const db = this.database;
  const { locals } = this;

  locals.set('posts', () => {
    const query = {};

    if (!this.config.future) {
      query.date = {$lte: Date.now()};
    }

    if (!this._showDrafts()) {
      query.published = true;
    }

    return db.model('Post').find(query);
  });

  locals.set('pages', () => {
    const query = {};

    if (!this.config.future) {
      query.date = {$lte: Date.now()};
    }

    return db.model('Page').find(query);
  });

  locals.set('categories', () => db.model('Category'));

  locals.set('tags', () => db.model('Tag'));

  locals.set('data', () => {
    const obj = {};

    db.model('Data').forEach(data => {
      obj[data._id] = data.data;
    });

    return obj;
  });
};

Hexo.prototype.init = function() {
  this.log.debug('Hexo version: %s', chalk.magenta(this.version));
  this.log.debug('Working directory: %s', chalk.magenta(tildify(this.base_dir)));

  // Load internal plugins
  require('../plugins/console')(this);
  require('../plugins/filter')(this);
  require('../plugins/generator')(this);
  require('../plugins/helper')(this);
  require('../plugins/processor')(this);
  require('../plugins/renderer')(this);
  require('../plugins/tag')(this);

  // Load config
  return Promise.each([
    'update_package', // Update package.json
    'load_config', // Load config
    'load_plugins' // Load external plugins & scripts
  ], name => require(`./${name}`)(this)).then(() => this.execFilter('after_init', null, {context: this})).then(() => {
    // Ready to go!
    this.emit('ready');
  });
};

Hexo.prototype.call = function(name, args, callback) {
  if (!callback && typeof args === 'function') {
    callback = args;
    args = {};
  }

  return new Promise((resolve, reject) => {
    const c = this.extend.console.get(name);

    if (c) {
      Reflect.apply(c, this, [args]).then(resolve, reject);
    } else {
      reject(new Error(`Console \`${name}\` has not been registered yet!`));
    }
  }).asCallback(callback);
};

Hexo.prototype.model = function(name, schema) {
  return this.database.model(name, schema);
};

Hexo.prototype.resolvePlugin = function(name) {
  const baseDir = this.base_dir;

  try {
    // Try to resolve the plugin with the resolve.sync.
    return resolve.sync(name, { basedir: baseDir });
  } catch (err) {
    // There was an error (likely the plugin wasn't found), so return a possibly
    // non-existing path that a later part of the resolution process will check.
    return join(baseDir, 'node_modules', name);
  }
};

Hexo.prototype.loadPlugin = function(path, callback) {
  return fs.readFile(path).then(script => {
    // Based on: https://github.com/joyent/node/blob/v0.10.33/src/node.js#L516
    const module = new Module(path);
    module.filename = path;
    module.paths = Module._nodeModulePaths(path);

    function require(path) {
      return module.require(path);
    }

    require.resolve = request => Module._resolveFilename(request, module);

    require.main = process.mainModule;
    require.extensions = Module._extensions;
    require.cache = Module._cache;

    script = `(function(exports, require, module, __filename, __dirname, hexo){${script}});`;

    const fn = vm.runInThisContext(script, path);

    return fn(module.exports, require, module, path, dirname(path), this);
  }).asCallback(callback);
};

Hexo.prototype._showDrafts = function() {
  const { args } = this.env;
  return args.draft || args.drafts || this.config.render_drafts;
};

Hexo.prototype.load = function(callback) {
  return loadDatabase(this).then(() => {
    this.log.info('Start processing');

    return Promise.all([
      this.source.process(),
      this.theme.process()
    ]);
  }).then(() => this._generate({cache: true})).asCallback(callback);
};

Hexo.prototype.watch = function(callback) {
  this._watchBox = debounce(() => this._generate({cache: false}), 100);

  return loadDatabase(this).then(() => {
    this.log.info('Start processing');

    return Promise.all([
      this.source.watch(),
      this.theme.watch()
    ]);
  }).then(() => {
    this.source.on('processAfter', this._watchBox);
    this.theme.on('processAfter', this._watchBox);

    return this._generate({cache: false});
  }).asCallback(callback);
};

Hexo.prototype.unwatch = function() {
  if (this._watchBox != null) {
    this.source.removeListener('processAfter', this._watchBox);
    this.theme.removeListener('processAfter', this._watchBox);

    this._watchBox = null;
  }

  stopWatcher(this.source);
  stopWatcher(this.theme);
};

function stopWatcher(box) {
  if (box.isWatching()) box.unwatch();
}

Hexo.prototype._generateLocals = function() {
  const { config, theme } = this;

  function Locals(path, locals) {
    this.page = typeof locals === 'object' ? locals : {};
    if (this.page.path == null) this.page.path = path;

    this.path = path;
    this.url = `${config.url}/${path}`;
  }

  Locals.prototype.config = config;
  Locals.prototype.theme = Object.assign({}, config, theme.config, config.theme_config);
  Locals.prototype._ = _;
  Locals.prototype.layout = 'layout';
  Locals.prototype.env = this.env;
  Locals.prototype.view_dir = join(this.theme_dir, 'layout') + sep;
  Locals.prototype.site = this.locals.toObject();

  return Locals;
};

Hexo.prototype._runGenerators = function() {
  this.locals.invalidate();
  const siteLocals = this.locals.toObject();
  const generators = this.extend.generator.list();
  const { log } = this;

  // Run generators
  return Promise.map(Object.keys(generators), key => {
    const generator = generators[key];

    return Reflect.apply(generator, this, [siteLocals]).then(data => {
      log.debug('Generator: %s', chalk.magenta(key));
      return data;
    });
  }).reduce((result, data) => {
    return data ? result.concat(data) : result;
  }, []);
};

const routeCache = new WeakMap();

const unique = array => array.filter((item, index, self) => self.indexOf(item) === index);

const castArray = obj => { return Array.isArray(obj) ? obj : [obj]; };

const createLoadThemeRoute = function(generatorResult, locals, ctx) {
  const { log, theme } = ctx;
  const { path, cache: useCache } = locals;

  const layout = unique(castArray(generatorResult.layout));
  const layoutLength = layout.length;

  return () => {
    if (useCache && routeCache.has(generatorResult)) return routeCache.get(generatorResult);

    for (let i = 0; i < layoutLength; i++) {
      const name = layout[i];
      const view = theme.getView(name);

      if (view) {
        log.debug(`Rendering HTML ${name}: ${chalk.magenta(path)}`);
        return view.render(locals).tap(result => {
          routeCache.set(generatorResult, result);
        }).tapCatch(err => {
          log.error({ err }, `Render HTML failed: ${chalk.magenta(path)}`);
        });
      }
    }

    log.warn(`No layout: ${chalk.magenta(path)}`);
  };
};

Hexo.prototype._routerReflesh = function(runningGenerators, useCache) {
  const { route } = this;
  const routeList = route.list();
  const Locals = this._generateLocals();
  Locals.prototype.cache = useCache;

  return runningGenerators.map(generatorResult => {
    if (typeof generatorResult !== 'object' || generatorResult.path == null) return undefined;

    // add Route
    const path = route.format(generatorResult.path);
    const { data, layout } = generatorResult;

    if (!layout) {
      route.set(path, data);
      return path;
    }

    return this.execFilter('template_locals', new Locals(path, data), {context: this})
      .then(locals => { route.set(path, createLoadThemeRoute(generatorResult, locals, this)); })
      .thenReturn(path);
  }).then(newRouteList => {
    // Remove old routes
    const removed = routeList.filter(item => !newRouteList.includes(item));

    for (let i = 0, len = removed.length; i < len; i++) {
      route.remove(removed[i]);
    }
  });
};

Hexo.prototype._generate = function(options = {}) {
  if (this._isGenerating) return;

  const useCache = options.cache;

  this._isGenerating = true;

  this.emit('generateBefore');

  // Run before_generate filters
  return this.execFilter('before_generate', this.locals.get('data'), {context: this})
    .then(() => this._routerReflesh(this._runGenerators(), useCache)).then(() => {
      this.emit('generateAfter');

      // Run after_generate filters
      return this.execFilter('after_generate', null, {context: this});
    }).finally(() => {
      this._isGenerating = false;
    });
};

Hexo.prototype.exit = function(err) {
  if (err) {
    this.log.fatal(
      {err},
      'Something\'s wrong. Maybe you can find the solution here: %s',
      chalk.underline('https://hexo.io/docs/troubleshooting.html')
    );
  }

  return this.execFilter('before_exit', null, {context: this}).then(() => {
    this.emit('exit', err);
  });
};

Hexo.prototype.execFilter = function(type, data, options) {
  return this.extend.filter.exec(type, data, options);
};

Hexo.prototype.execFilterSync = function(type, data, options) {
  return this.extend.filter.execSync(type, data, options);
};

Hexo.lib_dir = Hexo.prototype.lib_dir = libDir + sep;

Hexo.core_dir = Hexo.prototype.core_dir = dirname(libDir) + sep;

Hexo.version = Hexo.prototype.version = pkg.version;

module.exports = Hexo;


keep keep keep keep add keep keep keep keep keep

'use strict';

const url = require('url');

/**
 * Asset link tag
 *
 * Syntax:
 *   {% asset_link slug [title] %}
 */
module.exports = ctx => {
  const PostAsset = ctx.model('PostAsset');

  return function assetLinkTag(args) {
    const slug = args.shift();
    if (!slug) return;

    const asset = PostAsset.findOne({post: this._id, slug});
    if (!asset) return;

    const title = args.length ? args.join(' ') : asset.slug;

    return `<a href="${url.resolve(ctx.config.root, asset.path)}" title="${title}">${title}</a>`;
  };
};


'use strict';

const pathFn = require('path');
const fs = require('hexo-fs');
const Promise = require('bluebird');

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

  before(() => Promise.all([
    fs.mkdirs(themeDir),
    fs.writeFile(hexo.config_path, 'theme: test')
  ]).then(() => hexo.init()));

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

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

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

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

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

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

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

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

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

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

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

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

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

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


'use strict';

const Promise = require('bluebird');
const { Pattern } = require('hexo-util');

function Processor() {
  this.store = [];
}

Processor.prototype.list = function() {
  return this.store;
};

Processor.prototype.register = function(pattern, fn) {
  if (!fn) {
    if (typeof pattern === 'function') {
      fn = pattern;
      pattern = /(.*)/;
    } else {
      throw new TypeError('fn must be a function');
    }
  }

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

  this.store.push({
    pattern: new Pattern(pattern),
    process: fn
  });
};

module.exports = Processor;


'use strict';

const fs = require('hexo-fs');
const moment = require('moment');
const pathFn = require('path');
const Promise = require('bluebird');
const sinon = require('sinon');

describe('new', () => {
  const Hexo = require('../../../lib/hexo');
  const hexo = new Hexo(pathFn.join(__dirname, 'new_test'), {silent: true});
  const n = require('../../../lib/plugins/console/new').bind(hexo);
  const post = hexo.post;
  const now = Date.now();
  let clock;

  before(() => {
    clock = sinon.useFakeTimers(now);

    return fs.mkdirs(hexo.base_dir).then(() => hexo.init()).then(() => hexo.scaffold.set('post', [
      'title: {{ title }}',
      'date: {{ date }}',
      'tags:',
      '---'
    ].join('\n'))).then(() => hexo.scaffold.set('draft', [
      'title: {{ title }}',
      'tags:',
      '---'
    ].join('\n')));
  });

  after(() => {
    clock.restore();
    return fs.rmdir(hexo.base_dir);
  });

  it('title', () => {
    const date = moment(now);
    const path = pathFn.join(hexo.source_dir, '_posts', 'Hello-World.md');
    const body = [
      'title: Hello World',
      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),
      'tags:',
      '---'
    ].join('\n') + '\n';

    return n({
      _: ['Hello World']
    }).then(() => fs.readFile(path)).then(content => {
      content.should.eql(body);
      return fs.unlink(path);
    });
  });

  it('layout', () => {
    const path = pathFn.join(hexo.source_dir, '_drafts', 'Hello-World.md');
    const body = [
      'title: Hello World',
      'tags:',
      '---'
    ].join('\n') + '\n';

    return n({
      _: ['draft', 'Hello World']
    }).then(() => fs.readFile(path)).then(content => {
      content.should.eql(body);
      return fs.unlink(path);
    });
  });

  it('slug', () => {
    const date = moment(now);
    const path = pathFn.join(hexo.source_dir, '_posts', 'foo.md');
    const body = [
      'title: Hello World',
      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),
      'tags:',
      '---'
    ].join('\n') + '\n';

    return n({
      _: ['Hello World'],
      slug: 'foo'
    }).then(() => fs.readFile(path)).then(content => {
      content.should.eql(body);
      return fs.unlink(path);
    });
  });

  it('path', () => {
    const date = moment(now);
    const path = pathFn.join(hexo.source_dir, '_posts', 'bar.md');
    const body = [
      'title: Hello World',
      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),
      'tags:',
      '---'
    ].join('\n') + '\n';

    return n({
      _: ['Hello World'],
      slug: 'foo',
      path: 'bar'
    }).then(() => fs.readFile(path)).then(content => {
      content.should.eql(body);
      return fs.unlink(path);
    });
  });

  it('rename if target existed', () => {
    const path = pathFn.join(hexo.source_dir, '_posts', 'Hello-World-1.md');

    return post.create({
      title: 'Hello World'
    }).then(() => n({
      _: ['Hello World']
    })).then(() => fs.exists(path)).then(exist => {
      exist.should.be.true;

      return Promise.all([
        fs.unlink(path),
        fs.unlink(pathFn.join(hexo.source_dir, '_posts', 'Hello-World.md'))
      ]);
    });
  });

  it('replace existing files', () => {
    const path = pathFn.join(hexo.source_dir, '_posts', 'Hello-World.md');

    return post.create({
      title: 'Hello World'
    }).then(() => n({
      _: ['Hello World'],
      replace: true
    })).then(() => fs.exists(pathFn.join(hexo.source_dir, '_posts', 'Hello-World-1.md'))).then(exist => {
      exist.should.be.false;
      return fs.unlink(path);
    });
  });

  it('extra data', () => {
    const date = moment(now);
    const path = pathFn.join(hexo.source_dir, '_posts', 'Hello-World.md');
    const body = [
      'title: Hello World',
      'foo: bar',
      'date: ' + date.format('YYYY-MM-DD HH:mm:ss'),
      'tags:',
      '---'
    ].join('\n') + '\n';

    return n({
      _: ['Hello World'],
      foo: 'bar'
    }).then(() => fs.readFile(path)).then(content => {
      content.should.eql(body);
      return fs.unlink(path);
    });
  });
});


// specificity has to be higher to override theme !important

color: rgba(var(--v-theme-on-surface), $button-disabled-opacity) !important

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

.v-btn
  align-items: center
  border-radius: $button-border-radius
  display: inline-grid
  grid-template-areas: "prepend content append"
  grid-template-columns: max-content auto max-content
  font-weight: $button-font-weight
  justify-content: center
  letter-spacing: $button-text-letter-spacing
  line-height: $button-line-height
  max-width: $button-max-width
  outline: none
  position: relative
  text-decoration: none
  text-indent: $button-text-letter-spacing
  text-transform: $button-text-transform
  transition-property: $button-transition-property
  transition-duration: 0.28s
  transition-timing-function: settings.$standard-easing
  user-select: none
  vertical-align: $button-vertical-align
  flex-shrink: 0

  @at-root
    @include button-sizes()
    @include button-density('height', $button-density)

  @include tools.border($button-border...)
  @include tools.position($button-positions)
  @include tools.states('.v-btn__overlay')
  @include tools.variant($button-variants...)

  @supports selector(:focus-visible)
    &::after
      @include tools.absolute(true)
      pointer-events: none
      border: 2px solid currentColor
      border-radius: inherit
      opacity: 0
      transition: opacity .2s ease-in-out

    &:focus-visible::after
      opacity: calc(.25 * var(--v-theme-overlay-multiplier))

  &--icon
    border-radius: $button-icon-border-radius
    min-width: 0
    padding: 0

    // ensure that default
    // v-icon size is 24px
    &.v-btn--size-default
      font-size: $button-icon-font-size

    @at-root &
      @include button-density(('width', 'height'), $button-icon-density)

  &--elevated
    &:hover,
    &:focus
      +tools.elevation(map.get($button-elevation, 'hover'))

    &:active
      +tools.elevation(map.get($button-elevation, 'active'))

  &--flat
    box-shadow: none

  &--block
    display: flex
    flex: 1 0 auto
    min-width: 100%

  &--disabled
    pointer-events: none
    opacity: $button-disabled-opacity

    &.v-btn--variant-elevated,
    &.v-btn--variant-flat
      color: rgba(var(--v-theme-on-surface), $button-disabled-opacity)
      background: rgb(var(--v-theme-surface))
      box-shadow: none
      opacity: 1

      .v-btn__overlay
        // __overlay uses currentColor, so we need to divide
        // by the text opacity to get the correct value
        opacity: math.div($button-disabled-overlay, $button-disabled-opacity)

  &--loading
    pointer-events: none

    .v-btn__content,
    .v-btn__prepend,
    .v-btn__append
      opacity: 0

  &--stacked
    grid-template-areas: "prepend" "content" "append"
    grid-template-columns: auto
    grid-template-rows: max-content max-content max-content
    justify-items: center
    align-content: center

    .v-btn__content
      flex-direction: column
      line-height: $button-stacked-line-height

    .v-btn__prepend,
    .v-btn__append,
    .v-btn__content > .v-icon--start,
    .v-btn__content > .v-icon--end
      margin-inline-start: 0
      margin-inline-end: 0

    .v-btn__prepend,
    .v-btn__content > .v-icon--start
      margin-bottom: $button-stacked-icon-margin

    .v-btn__append,
    .v-btn__content > .v-icon--end
      margin-top: $button-stacked-icon-margin

    @at-root
      @include button-sizes($button-stacked-sizes, true)
      @include button-density('height', $button-stacked-density)

  &--rounded
    @include tools.rounded($button-rounded-border-radius)

  .v-icon
    --v-icon-size-multiplier: #{calc(18/21)}

  &--icon
    .v-icon
      --v-icon-size-multiplier: 1

  &--stacked
    .v-icon
      --v-icon-size-multiplier: #{calc(24/21)}

.v-btn__loader
  align-items: center
  display: flex
  height: 100%
  justify-content: center
  left: 0
  position: absolute
  top: 0
  width: 100%

.v-btn__content,
.v-btn__prepend,
.v-btn__append
  align-items: center
  display: flex
  transition: $button-content-transition

.v-btn__prepend
  grid-area: prepend
  margin-inline-start: $button-margin-start
  margin-inline-end: $button-margin-end

.v-btn__append
  grid-area: append
  margin-inline-start: $button-margin-end
  margin-inline-end: $button-margin-start

.v-btn__content
  grid-area: content
  justify-content: center
  white-space: $button-white-space

  > .v-icon--start
    margin-inline-start: $button-margin-start
    margin-inline-end: $button-margin-end

  > .v-icon--end
    margin-inline-start: $button-margin-end
    margin-inline-end: $button-margin-start

  .v-btn--stacked &
    white-space: normal

.v-btn__overlay
  background-color: currentColor
  border-radius: inherit
  opacity: 0
  transition: opacity .2s ease-in-out

.v-btn__overlay,
.v-btn__underlay
  @include tools.absolute()
  pointer-events: none

// VAppBar
.v-btn
  .v-app-bar &
    @include tools.elevation($button-app-bar-elevation)

// VCard
.v-btn
  .v-card-actions &
    padding: $button-card-actions-padding

    @include tools.elevation($button-card-actions-elevation)

  ~ .v-btn
    .v-card-actions &
      margin-inline-start: $button-card-actions-margin

// VBanner
.v-btn
  .v-banner-actions &
    padding: $button-banner-actions-padding

// VPagination
.v-btn
  .v-pagination &
    @include tools.rounded($button-pagination-border-radius)

  &__overlay
    transition: none

    .v-pagination__item--is-active &
      opacity: $button-pagination-active-overlay-opacity

// VSnackbar
.v-btn
  .v-snackbar-actions &
    padding: $button-snackbar-action-padding


---
nav: For Enterprise
meta:
  title: Vuetify for Enterprise
  description: Get the tools you need to continuously catalog and understand the open source software that your application depends on with the Tidelift subscription.
  keywords: vuetify enterprise, tidelift enterprise, tidelift vuetify
related:
  - /introduction/support/
  - /about/security-disclosure/
  - /about/meet-the-team/
---

# Vuetify for Enterprise

The Tidelift Subscription is a managed open source subscription for application dependencies covering millions of open source projects across JavaScript, Python, Java, PHP, Ruby, .NET, and more.

## Available as part of the Tidelift Subscription

Vuetify and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.

If you want the flexibility of open source and the confidence of commercial-grade software, this is for you.

## The Tidelift Subscription manages your dependencies for you

- Get the tools you need to continuously catalog and understand the open source software that your application depends on.
- Your subscription helps pay the open source community maintainers of the exact packages you use to ensure they meet the standards you require
- Address issues proactively, with tools that scan for new security, licensing, and maintenance issues, and alert our participating open source maintainers so they can resolve them on your behalf.
- Measure and improve your open source dependencies health—which improves your app's health—and get a short list of high-impact steps your team can take to improve them even more.
- Get commercial assurances that don't come for free with open source packages, like intellectual property indemnification and support under a service level agreement. You expect these guarantees from proprietary software, and you can get them when using open source as well.

The end result? All of the capabilities you expect from commercial-grade software, for the full breadth of open source you use. That means less time grappling with esoteric open source trivia, and more time building your own applications—and your business.

<backmatter />


<template>
  <v-card>
    <v-layout>
      <v-navigation-drawer
        expand-on-hover
        rail
      >
        <v-list>
          <v-list-item
            prepend-avatar="https://randomuser.me/api/portraits/women/85.jpg"
            title="Sandra Adams"
            subtitle="sandra_a88@gmailcom"
          ></v-list-item>
        </v-list>

        <v-divider></v-divider>

        <v-list density="compact" nav>
          <v-list-item prepend-icon="mdi-folder" title="My Files" value="myfiles"></v-list-item>
          <v-list-item prepend-icon="mdi-account-multiple" title="Shared with me" value="shared"></v-list-item>
          <v-list-item prepend-icon="mdi-star" title="Starred" value="starred"></v-list-item>
        </v-list>
      </v-navigation-drawer>

      <v-main style="height: 250px"></v-main>
    </v-layout>
  </v-card>
</template>

<script>
  export default {
    //
  }
</script>


@use '../../styles/tools'
@use './variables' as *

.v-kbd
  background: rgb(var(--v-kbd-background-color))
  border-radius: $kbd-border-radius
  color: rgb(var(--v-kbd-color))
  font-size: $kbd-font-size
  font-weight: $kbd-font-weight
  padding: $kbd-padding

  @include tools.elevation($kbd-elevation)


@use 'sass:math';
@use 'sass:map';
@use '../../styles/settings';
@use '../../styles/tools';

// Defaults
$button-app-bar-elevation: 0 !default;
$button-background: rgb(var(--v-theme-surface)) !default;
$button-color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity)) !default;
$button-banner-actions-padding: 0 8px !default;
$button-pagination-active-overlay-opacity: var(--v-border-opacity) !default;
$button-pagination-border-radius: settings.$border-radius-root !default;
$button-border-color: settings.$border-color-root !default;
$button-border-radius: settings.$border-radius-root !default;
$button-border-style: settings.$border-style-root !default;
$button-border-thin-width: thin !default;
$button-border-width: 0 !default;
$button-card-actions-elevation: 0 !default;
$button-card-actions-margin: .5rem !default;
$button-card-actions-padding: 0 8px !default;
$button-content-transition: transform, opacity .2s settings.$standard-easing !default;
$button-disabled-opacity: 0.26 !default;
$button-disabled-overlay: 0.12 !default;
$button-elevation: ('default': 2, 'hover': 4, 'active': 8) !default;
$button-font-size: tools.map-deep-get(settings.$typography, 'button', 'size') !default;
$button-font-weight: tools.map-deep-get(settings.$typography, 'button', 'weight') !default;
$button-height: 36px !default;
$button-stacked-height: 72px !default;
$button-stacked-icon-margin: 4px !default;
$button-icon-border-radius: map.get(settings.$rounded, 'circle') !default;
$button-icon-font-size: 1rem !default;
$button-line-height: normal !default;
$button-stacked-line-height: 1.25 !default;
$button-plain-opacity: .62 !default;
$button-padding-ratio: 2.25 !default;
$button-stacked-padding-ratio: 4.5 !default;
$button-margin-start-multiplier: -9 !default;
$button-margin-end-multiplier: 4.5 !default;
$button-margin-start: calc(var(--v-btn-height) / #{$button-margin-start-multiplier}) !default;
$button-margin-end: calc(var(--v-btn-height) / #{$button-margin-end-multiplier}) !default;
$button-max-width: 100% !default;
$button-positions: absolute fixed !default;
$button-text-letter-spacing: tools.map-deep-get(settings.$typography, 'button', 'letter-spacing') !default;
$button-text-transform: tools.map-deep-get(settings.$typography, 'button', 'text-transform') !default;
$button-transition-property: box-shadow, transform, opacity, background !default;
$button-vertical-align: middle !default;
$button-width-ratio: math.div(16, 9) !default;
$button-snackbar-action-padding: 0 8px !default;
$button-stacked-width-ratio: 1 !default;
$button-rounded-border-radius: map.get(settings.$rounded, 'xl') !default;
$button-white-space: nowrap !default;

$button-density: ('default': 0, 'comfortable': -2, 'compact': -3) !default;
$button-stacked-density: ('default': 0, 'comfortable': -4, 'compact': -6) !default;
$button-icon-density: ('default': 3, 'comfortable': 0, 'compact': -2) !default;

$button-border: (
  $button-border-color,
  $button-border-style,
  $button-border-width,
  $button-border-thin-width
) !default;

$button-sizes: () !default;
$button-sizes: map.merge(
    (
      'height': $button-height,
      'font-size': $button-font-size,
      'width-ratio': $button-width-ratio,
      'padding-ratio': $button-padding-ratio
    ),
    $button-sizes
);

$button-stacked-sizes: () !default;
$button-stacked-sizes: map.merge(
    (
      'height': $button-stacked-height,
      'font-size': $button-font-size,
      'width-ratio': $button-stacked-width-ratio,
      'padding-ratio': $button-stacked-padding-ratio
    ),
    $button-stacked-sizes
);

$button-variants: (
  $button-background,
  $button-color,
  map.get($button-elevation, 'default'),
  $button-plain-opacity,
  'v-btn'
);


// Setup
// import type { ComponentOptions } from 'vue'

// Utilities
import toHaveBeenWarnedInit from './util/to-have-been-warned'

// export function functionalContext (context: ComponentOptions<Vue> = {}, children = []) {
//   if (!Array.isArray(children)) children = [children]
//   return {
//     context: {
//       data: {},
//       props: {},
//       ...context,
//     },
//     children,
//   }
// }

export function touch (element: Element) {
  const createTrigger = (eventName: string) => (clientX: number, clientY: number) => {
    const touches = [{ clientX, clientY }]
    const event = new Event(eventName)

    ;(event as any).touches = touches
    ;(event as any).changedTouches = touches
    element.dispatchEvent(event)

    return touch(element)
  }

  return {
    start: createTrigger('touchstart'),
    move: createTrigger('touchmove'),
    end: createTrigger('touchend'),
  }
}

export const wait = (timeout?: number) => {
  return new Promise(resolve => setTimeout(resolve, timeout))
}

export const waitAnimationFrame = (timeout?: number) => {
  return new Promise(resolve => requestAnimationFrame(resolve))
}

export const resizeWindow = (width = window.innerWidth, height = window.innerHeight) => {
  (window as any).innerWidth = width
  ;(window as any).innerHeight = height
  window.dispatchEvent(new Event('resize'))

  return wait(200)
}

export const scrollWindow = (y: number) => {
  (window as any).pageYOffset = y
  window.dispatchEvent(new Event('scroll'))

  return wait(200)
}

export const scrollElement = (element: Element, y: number) => {
  element.scrollTop = y
  element.dispatchEvent(new Event('scroll'))

  return wait(200)
}

// Add a global mockup for IntersectionObserver
class IntersectionObserver {
  callback?: (entries: any, observer: any) => {}

  constructor (callback: any) {
    this.callback = callback
  }

  observe () {
    this.callback?.([], this)
    return null
  }

  unobserve () {
    this.callback = undefined
    return null
  }
}

(global as any).IntersectionObserver = IntersectionObserver

class ResizeObserver {
  callback?: ResizeObserverCallback

  constructor (callback: ResizeObserverCallback) {
    this.callback = callback
  }

  observe () {
    this.callback?.([], this)
  }

  unobserve () {
    this.callback = undefined
  }

  disconnect () {
    this.callback = undefined
  }
}

(global as any).ResizeObserver = ResizeObserver

;(global as any).CSS = { supports: () => false }

toHaveBeenWarnedInit()


export default {
  badge: 'Badge',
  close: 'Fermer',
  dataIterator: {
    noResultsText: 'Aucun enregistrement correspondant trouvé',
    loadingText: `Chargement de l'élément...`,
  },
  dataTable: {
    itemsPerPageText: 'Lignes par page :',
    ariaLabel: {
      sortDescending: 'Tri décroissant.',
      sortAscending: 'Tri croissant.',
      sortNone: 'Non trié.',
      activateNone: 'Activer pour supprimer le tri.',
      activateDescending: 'Activer pour trier par ordre décroissant.',
      activateAscending: 'Activer pour trier par ordre croissant.',
    },
    sortBy: 'Trier par',
  },
  dataFooter: {
    itemsPerPageText: 'Élements par page :',
    itemsPerPageAll: 'Tous',
    nextPage: 'Page suivante',
    prevPage: 'Page précédente',
    firstPage: 'Première page',
    lastPage: 'Dernière page',
    pageText: '{0}-{1} de {2}',
  },
  datePicker: {
    itemsSelected: '{0} sélectionné(s)',
    nextMonthAriaLabel: 'Le mois prochain',
    nextYearAriaLabel: `L'année prochaine`,
    prevMonthAriaLabel: 'Le mois précédent',
    prevYearAriaLabel: 'Année précédente',
  },
  noDataText: 'Aucune donnée disponible',
  carousel: {
    prev: 'Visuel précédent',
    next: 'Visuel suivant',
    ariaLabel: {
      delimiter: 'Diapositive {0} de {1}',
    },
  },
  calendar: {
    moreEvents: '{0} de plus',
  },
  fileInput: {
    counter: '{0} fichier(s)',
    counterSize: '{0} fichier(s) ({1} au total)',
  },
  timePicker: {
    am: 'AM',
    pm: 'PM',
  },
  pagination: {
    ariaLabel: {
      root: 'Navigation de pagination',
      next: 'Page suivante',
      previous: 'Page précédente',
      page: 'Aller à la page {0}',
      currentPage: 'Page actuelle, Page {0}',
      first: 'First page',
      last: 'Last page',
    },
  },
  rating: {
    ariaLabel: {
      item: 'Note de {0} sur {1}',
    },
  },
}


---
meta:
  title: CSS Reset
  description: Vuetify uses ress.min, a complete browser reset based off or normalize.css.
  keywords: ress.min, css reset, vuetify css reset
related:
  - /styles/colors/
  - /styles/text-and-typography/
  - /features/sass-variables/
---

# CSS Reset

Opinionated base styles for Vuetify projects.

<entry />

## Bootstrapping

ress is a modern CSS reset that applies a solid base for stylesheets. It is built on top of [normalize.css](https://github.com/necolas/normalize.css) and adds new features such as specifying `font-family: monospace` for `<code>` elements, removing all `outlines` from elements when hovering, and much much more. Additional information can be found on the [ress GitHub repository](https://github.com/filipelinhares/ress).

<alert type="warning">

  The Vuetify style reset is applied globally and affects default elements such as `button` and `input`. This also includes anything located outside of the [v-app](/components/application) component.

</alert>

These styles are automatically imported within `src/styles/generic/_reset.scss` and bootstrapped as **Generic** styles within `src/styles/generic/_index.scss`:

```scss { resource="styles/generic/_index.scss" }
// Generic styling for bare HTML elements (like H1, A, etc.).
// These come with default styling from the browser so that
// we can redefine them here.
@import './reset.scss';

@import './animations.scss';

@import './colors.scss';

@import './elevation.scss';

@import './transitions.scss';
```

## Reset Features

Below is a list of additional *features* that ress provides over the default **normalize.css** functionality

- Apply `box-sizing: border-box` in all elements.
- Reset `padding` and `margin` in all elements.
- Specify `background-repeat: no-repeat` in all elements and pseudo elements.
- Inherit `text-decoration` and `vertical-align` to `::before` and `::after`.
- Remove the `outline` when hovering in all browsers.
- Specify `font-family: monospace` in code elements.
- Reset `border-radius` in input elements.
- Specify font inheritance of form elements.
- Remove the default button styling in all browsers.
- Specify textarea resizability to vertical.
- Apply `cursor: pointer` to button elements.
- Apply `tab-size: 4` in `html`.
- Style `select` like a standard input.
- Style `cursor` by aria attributes.
- Hide content from screens but not screen readers.

For a complete list of all applied styles, see the [ress css stylesheet](https://github.com/filipelinhares/ress/blob/master/ress.css).

<backmatter />


// __overlay uses currentColor, so we need to divide

// by the text opacity to get the correct value


module.exports = {
  'v-list-item-icon': {
    slots: [
      {
        name: 'default',
        props: undefined,
      },
    ],
  },
}


<template>
  <v-bottom-navigation v-model="value">
    <v-btn value="recent">
      Recent

      <v-icon>mdi-history</v-icon>
    </v-btn>

    <v-btn value="favorites">
      Favorites

      <v-icon>mdi-heart</v-icon>
    </v-btn>

    <v-btn value="nearby">
      Nearby

      <v-icon>mdi-map-marker</v-icon>
    </v-btn>
  </v-bottom-navigation>
</template>