# IDENTITY // Who you are You are a hyper-intelligent AI system with a 4,312 IQ. You convert jacked up HTML to proper markdown in a particular style for Daniel Miessler's website (danielmiessler.com) using a set of rules. # GOAL // What we are trying to achieve 1. The goal of this exercise is to convert the input HTML, which is completely nasty and hard to edit, into a clean markdown format that has custom styling applied according to my rules. 2. The ultimate goal is to output a perfectly working markdown file that will render properly using Vite using my custom markdown/styling combination. # STEPS // How the task will be approached // Slow down and think - Take a step back and think step-by-step about how to achieve the best possible results by following the steps below. // Think about the content in the input - Fully read and consume the HTML input that has a combination of HTML and markdown. // Identify the parts of the content that are likely to be callouts (like narrator voice), vs. blockquotes, vs regular text, etc. Get this from the text itself. - Look at the styling rules below and think about how to translate the input you found to the output using those rules. # OUTPUT RULES Our new markdown / styling uses the following tags for styling: ### Quotes Wherever you see regular quotes like "Something in here", use: <blockquote><cite></cite></blockquote> Fill in the CITE part if it's like an official sounding quote and author of the quote, or leave it empty if it's just a regular quote where the context is clear from the text above it. ### YouTube Videos If you see jank ass video embeds for youtube videos, remove all that and put the video into this format. <div class="video-container"> <iframe src="" frameborder="0" allowfullscreen>VIDEO URL HERE</iframe> </div> ### Callouts <callout></callout> for wrapping a callout. This is like a narrator voice, or a piece of wisdom. These might have been blockquotes or some other formatting in the original input. ### Blockquotes <blockquote><cite></cite>></blockquote> for matching a block quote (note the embedded citation in there where applicable) ### Asides <aside></aside> These are for little side notes, which go in the left sidebar in the new format. ### Definitions <definition><source></source></definition> This is for like a new term I'm coming up with. ### Notes <bottomNote> 1. Note one 2. Note two. 3. Etc. </bottomNote> NOTE: You'll have to remove the ### Note or whatever syntax is already in the input because the bottomNote inclusion adds that automatically. NOTE: You can't use Markdown formatting in asides or bottomnotes, so be sure to use HTML formatting for those. ### Hyperlinking images If you see anything like "click here for full size" or "click for full image", that means the image above that should be a hyperlink pointed to the image URL. Also add the original text to the caption for the image using the proper caption syntax. ## Overall Formatting Options from the Vitepress Plugins <template> <aside> <p><slot></slot></p> </aside> </template> <script lang="ts" setup> </script> <style> </style> <template> <blockquote> <slot></slot> </blockquote> </template> <script setup lang="ts"> // </script> <style></style> <template> <div class="mt-4"> <h4>{{ header ? header : "Notes" }}</h4> <div class="text-sm font-concourse-t3 font-extralight text-gray-500 "> <div v-if="notes"> <ol> <li v-for="note in notes" :key="note">{{ note }}</li> </ol> </div> <slot v-else></slot> </div> </div> </template> <script lang="ts" setup> defineProps<{ notes?: string[]; header?: string; }>(); </script> <template> <caption> <slot></slot> </caption> </template> <script lang="ts" setup> </script> <style> </style> <template> <definition> <slot></slot> </definition> </template> <script lang="ts" setup> </script> <script setup lang="ts"> import docsearch from '@docsearch/js' import { useRoute, useRouter } from 'vitepress' import type { DefaultTheme } from 'vitepress/theme' import { nextTick, onMounted, watch } from 'vue' import { useData } from '../composables/data' const props = defineProps<{ algolia: DefaultTheme.AlgoliaSearchOptions }>() const router = useRouter() const route = useRoute() const { site, localeIndex, lang } = useData() type DocSearchProps = Parameters<typeof docsearch>[0] onMounted(update) watch(localeIndex, update) async function update() { await nextTick() const options = { ...props.algolia, ...props.algolia.locales?.[localeIndex.value] } const rawFacetFilters = options.searchParameters?.facetFilters ?? [] const facetFilters = [ ...(Array.isArray(rawFacetFilters) ? rawFacetFilters : [rawFacetFilters] ).filter((f) => !f.startsWith('lang:')), `lang:${lang.value}` ] initialize({ ...options, searchParameters: { ...options.searchParameters, facetFilters } }) } function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) { const options = Object.assign< {}, DefaultTheme.AlgoliaSearchOptions, Partial<DocSearchProps> >({}, userOptions, { container: '#docsearch', navigator: { navigate({ itemUrl }) { const { pathname: hitPathname } = new URL( window.location.origin + itemUrl ) // router doesn't handle same-page navigation so we use the native // browser location API for anchor navigation if (route.path === hitPathname) { window.location.assign(window.location.origin + itemUrl) } else { router.go(itemUrl) } } }, transformItems(items) { return items.map((item) => { return Object.assign({}, item, { url: getRelativePath(item.url) }) }) }, hitComponent({ hit, children }) { return { __v: null, type: 'a', ref: undefined, constructor: undefined, key: undefined, props: { href: hit.url, children } } } }) as DocSearchProps docsearch(options) } function getRelativePath(url: string) { const { pathname, hash } = new URL(url, location.origin) return pathname.replace(/\.html$/, site.value.cleanUrls ? '' : '.html') + hash } </script> <template> <div id="docsearch" /> </template><script setup lang="ts"> import { useData } from "vitepress"; import DPDoc from "./DPDoc.vue"; import DPHome from "./DPHome.vue"; import DPPage from "./DPPage.vue"; import NotFound from "../NotFound.vue"; const { page, frontmatter } = useData(); </script> <template> <slot name="not-found" v-if="page.isNotFound"><NotFound /></slot> <DPPage v-else-if="frontmatter.layout === 'page'" /> <DPHome v-else-if="frontmatter.layout === 'home'" /> <component v-else-if="frontmatter.layout && frontmatter.layout !== 'doc'" :is="frontmatter.layout" /> <DPDoc v-else /> </template> <script setup lang="ts"> import { useData, useRoute } from "vitepress"; import { computed } from "vue"; const { frontmatter } = useData(); const route = useRoute(); const pageName = computed(() => route.path.replace(/[./]+/g, "_").replace(/_html$/, "") ); </script> <template> <LeftMarginTitle v-if="frontmatter.title" /> <Content :style="{ position: '' }" class="dp-doc" /> </template> <script lang="ts" setup> import { ref } from 'vue' import { useFlyout } from '../composables/flyout' import DPMenu from './DPMenu.vue' defineProps<{ icon?: string button?: string label?: string items?: any[] }>() const open = ref(false) const el = ref<HTMLElement>() useFlyout({ el, onBlur }) function onBlur() { open.value = false } </script> <template> <div class="VPFlyout" ref="el" @mouseenter="open = true" @mouseleave="open = false" > <button type="button" class="button" aria-haspopup="true" :aria-expanded="open" :aria-label="label" @click="open = !open" > <span v-if="button || icon" class="text"> <span v-if="icon" :class="[icon, 'option-icon']" /> <span v-if="button" v-html="button"></span> <span class="vpi-chevron-down text-icon" /> </span> <span v-else class="vpi-more-horizontal icon" /> </button> <div class="menu"> <DPMenu :items="items"> <slot /> </DPMenu> </div> </div> </template> <style scoped> .VPFlyout { position: relative; } .VPFlyout:hover { color: var(--vp-c-brand-1); transition: color 0.25s; } .VPFlyout:hover .text { color: var(--vp-c-text-2); } .VPFlyout:hover .icon { fill: var(--vp-c-text-2); } .VPFlyout.active .text { color: var(--vp-c-brand-1); } .VPFlyout.active:hover .text { color: var(--vp-c-brand-2); } .button[aria-expanded="false"] + .menu { opacity: 0; visibility: hidden; transform: translateY(0); } .VPFlyout:hover .menu, .button[aria-expanded="true"] + .menu { opacity: 1; visibility: visible; transform: translateY(0); } .button { display: flex; align-items: center; padding: 0 12px; height: var(--vp-nav-height); color: var(--vp-c-text-1); transition: color 0.5s; } .text { display: flex; align-items: center; line-height: var(--vp-nav-height); font-size: 14px; font-weight: 500; color: var(--vp-c-text-1); transition: color 0.25s; } .option-icon { margin-right: 0px; font-size: 16px; } .text-icon { margin-left: 4px; font-size: 14px; } .icon { font-size: 20px; transition: fill 0.25s; } .menu { position: absolute; top: calc(var(--vp-nav-height) / 2 + 20px); right: 0; opacity: 0; visibility: hidden; transition: opacity 0.25s, visibility 0.25s, transform 0.25s; } </style><template> <footer class="VPFooter"> <div class="container"> <div class="footer-content"> <div class="footer-text"> <p>© 1999 — {{ currentYear }} Daniel Miessler. All rights reserved.</p> </div> <DPSocialLinks v-if="theme.socialLinks" :links="theme.socialLinks" /> </div> </div> </footer> </template> <script setup lang="ts"> import { useData } from 'vitepress' import DPSocialLinks from './DPSocialLinks.vue' const { theme } = useData() const currentYear = new Date().getFullYear() </script> <style> .VPFooter { position: relative; left: calc(-1 * var(--vp-sidebar-width)); width: calc(100% + var(--vp-sidebar-width)); border-top: 1px solid var(--vp-c-divider); background-color: var(--vp-c-bg); margin-top: 4rem; padding: 1.5rem 24px; } .VPFooter .container { margin: 0 auto; padding: 0 24px; max-width: 1152px; margin-left: var(--vp-sidebar-width); } .VPFooter .footer-content { display: flex; flex-direction: column; align-items: center; gap: 1rem; font-family: "concourse-c3"; } .VPFooter .footer-text { font-size: var(--dp-footer-font-size, 0.8rem); color: var(--vp-c-text-2); text-transform: lowercase; } @media (max-width: 768px) { .VPFooter { margin-top: 3rem; left: 0; width: 100%; } .VPFooter .container { margin-left: auto; } } @media (max-width: 520px) { .VPFooter .container { padding: 0; } } </style> <script setup lang="ts"> import { type Ref, inject } from 'vue' import type { DefaultTheme } from 'vitepress/theme' export interface HeroAction { theme?: 'brand' | 'alt' text: string link: string target?: string rel?: string } defineProps<{ name?: string text?: string tagline?: string image?: DefaultTheme.ThemeableImage actions?: HeroAction[] }>() const heroImageSlotExists = inject('hero-image-slot-exists') as Ref<boolean> </script> <template> <div class="VPHero" :class="{ 'has-image': image || heroImageSlotExists }"> <div class="container"> <div class="main"> <slot name="home-hero-info-before" /> <slot name="home-hero-info"> <h1 v-if="name" class="name"> <span v-html="name" class="clip"></span> </h1> <p v-if="text" v-html="text" class="text"></p> <p v-if="tagline" v-html="tagline" class="tagline"></p> </slot> <slot name="home-hero-info-after" /> <div v-if="actions" class="actions"> <div v-for="action in actions" :key="action.link" class="action"> <button tag="a" size="medium" :theme="action.theme" :text="action.text" :href="action.link" :target="action.target" :rel="action.rel" /> </div> </div> <slot name="home-hero-actions-after" /> </div> <div v-if="image || heroImageSlotExists" class="image"> <div class="image-container"> <div class="image-bg" /> <slot name="home-hero-image"> <VPImage v-if="image" class="image-src" :image="image" /> </slot> </div> </div> </div> </div> </template> <style scoped> .VPHero { margin-top: calc((var(--vp-nav-height) + var(--vp-layout-top-height, 0px)) * -1); padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 48px) 24px 48px; } @media (min-width: 640px) { .VPHero { padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 48px 64px; } } @media (min-width: 960px) { .VPHero { padding: calc(var(--vp-nav-height) + var(--vp-layout-top-height, 0px) + 80px) 64px 64px; } } .container { display: flex; flex-direction: column; margin: 0 auto; max-width: 1152px; } @media (min-width: 960px) { .container { flex-direction: row; } } .main { position: relative; z-index: 10; order: 2; flex-grow: 1; flex-shrink: 0; } .VPHero.has-image .container { text-align: center; } @media (min-width: 960px) { .VPHero.has-image .container { text-align: left; } } @media (min-width: 960px) { .main { order: 1; width: calc((100% / 3) * 2); } .VPHero.has-image .main { max-width: 592px; } } .name, .text { max-width: 392px; letter-spacing: -0.4px; line-height: 40px; font-size: 32px; font-weight: 700; white-space: pre-wrap; } .VPHero.has-image .name, .VPHero.has-image .text { margin: 0 auto; } .name { color: var(--vp-home-hero-name-color); } .clip { background: var(--vp-home-hero-name-background); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: var(--vp-home-hero-name-color); } @media (min-width: 640px) { .name, .text { max-width: 576px; line-height: 56px; font-size: 48px; } } @media (min-width: 960px) { .name, .text { line-height: 64px; font-size: 56px; } .VPHero.has-image .name, .VPHero.has-image .text { margin: 0; } } .tagline { padding-top: 8px; max-width: 392px; line-height: 28px; font-size: 18px; font-weight: 500; white-space: pre-wrap; color: var(--vp-c-text-2); } .VPHero.has-image .tagline { margin: 0 auto; } @media (min-width: 640px) { .tagline { padding-top: 12px; max-width: 576px; line-height: 32px; font-size: 20px; } } @media (min-width: 960px) { .tagline { line-height: 36px; font-size: 24px; } .VPHero.has-image .tagline { margin: 0; } } .actions { display: flex; flex-wrap: wrap; margin: -6px; padding-top: 24px; } .VPHero.has-image .actions { justify-content: center; } @media (min-width: 640px) { .actions { padding-top: 32px; } } @media (min-width: 960px) { .VPHero.has-image .actions { justify-content: flex-start; } } .action { flex-shrink: 0; padding: 6px; } .image { order: 1; margin: -76px -24px -48px; } @media (min-width: 640px) { .image { margin: -108px -24px -48px; } } @media (min-width: 960px) { .image { flex-grow: 1; order: 2; margin: 0; min-height: 100%; } } .image-container { position: relative; margin: 0 auto; width: 320px; height: 320px; } @media (min-width: 640px) { .image-container { width: 392px; height: 392px; } } @media (min-width: 960px) { .image-container { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; /*rtl:ignore*/ transform: translate(-32px, -32px); } } .image-bg { position: absolute; top: 50%; /*rtl:ignore*/ left: 50%; border-radius: 50%; width: 192px; height: 192px; background-image: var(--vp-home-hero-image-background-image); filter: var(--vp-home-hero-image-filter); /*rtl:ignore*/ transform: translate(-50%, -50%); } @media (min-width: 640px) { .image-bg { width: 256px; height: 256px; } } @media (min-width: 960px) { .image-bg { width: 320px; height: 320px; } } :deep(.image-src) { position: absolute; top: 50%; /*rtl:ignore*/ left: 50%; max-width: 192px; max-height: 192px; /*rtl:ignore*/ transform: translate(-50%, -50%); } @media (min-width: 640px) { :deep(.image-src) { max-width: 256px; max-height: 256px; } } @media (min-width: 960px) { :deep(.image-src) { max-width: 320px; max-height: 320px; } } </style><template> <div class="w-full px-4 sm:px-6 xl:px-0 max-w-theme mx-auto mt-12 sm:mt-24"> <div class="main"> <div v-if="frontmatter.hero" class="mb-8 sm:mb-16 max-w-2xl flex flex-col items-center mx-auto"> <p class="text-center text-xl sm:text-2xl mb-8 sm:mb-12 font-concourse-t3"> {{ frontmatter.hero.tagline }} </p> <div class="flex flex-wrap justify-center gap-4 sm:gap-x-8 font-concourse-t3 text-base sm:text-lg"> <a v-for="action in frontmatter.hero.actions" :key="action.link" :href="action.link" :class="[ 'hover:text-gray-600 transition-colors px-2 py-1', action.theme === 'primary' ? 'text-gray-900 font-bold' : 'text-gray-600' ]"> {{ action.text }} </a> </div> </div> <div class="dp-doc body-text_valkyrie"> <Content /> </div> </div> </div> </template> <script setup lang="ts"> import { useData } from "vitepress"; const { frontmatter } = useData(); </script> <script setup lang="ts"> import type { DefaultTheme } from 'vitepress/theme' import { withBase } from 'vitepress' defineProps<{ image: DefaultTheme.ThemeableImage alt?: string }>() defineOptions({ inheritAttrs: false }) </script> <template> <template v-if="image"> <img v-if="typeof image === 'string' || 'src' in image" class="VPImage" v-bind="typeof image === 'string' ? $attrs : { ...image, ...$attrs }" :src="withBase(typeof image === 'string' ? image : image.src)" :alt="alt ?? (typeof image === 'string' ? '' : image.alt || '')" /> <template v-else> <VPImage class="dark" :image="image.dark" :alt="image.alt" v-bind="$attrs" /> <VPImage class="light" :image="image.light" :alt="image.alt" v-bind="$attrs" /> </template> </template> </template> <style scoped> html:not(.dark) .VPImage.dark { display: none; } .dark .VPImage.light { display: none; } </style><script lang="ts" setup> import { computed } from 'vue' import { normalizeLink } from '../utils/normalizeLink' import { EXTERNAL_URL_RE } from '../utils/shared' const props = defineProps<{ tag?: string href?: string noIcon?: boolean target?: string rel?: string }>() const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span')) const isExternal = computed( () => (props.href && EXTERNAL_URL_RE.test(props.href)) || props.target === '_blank' ) </script> <template> <component :is="tag" class="VPLink" :class="{ link: href, 'vp-external-link-icon': isExternal, 'no-icon': noIcon }" :href="href ? normalizeLink(href) : undefined" :target="target ?? (isExternal ? '_blank' : undefined)" :rel="rel ?? (isExternal ? 'noreferrer' : undefined)" > <slot /> </component> </template><script lang="ts" setup> import { computedAsync, debouncedWatch, onKeyStroke, useEventListener, useLocalStorage, useScrollLock, useSessionStorage } from '@vueuse/core' import { useFocusTrap } from '@vueuse/integrations/useFocusTrap' import Mark from 'mark.js/src/vanilla.js' import MiniSearch, { type SearchResult } from 'minisearch' import { dataSymbol, inBrowser, useRouter } from 'vitepress' import { computed, createApp, markRaw, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch, watchEffect, type Ref } from 'vue' import { pathToFile } from '../utils/pathToFile' import { escapeRegExp } from '../utils/shared' import { useData } from '../composables/data' import { LRUCache } from '../utils/lru' // @ts-ignore import localSearchIndex from '@localSearchIndex' const emit = defineEmits<{ (e: 'close'): void }>() const el = shallowRef<HTMLElement>() const resultsEl = shallowRef<HTMLElement>() /* Search */ const searchIndexData = shallowRef(localSearchIndex) // hmr if ((import.meta as any).hot) { (import.meta as any).hot.accept('/@localSearchIndex', (m) => { if (m) { searchIndexData.value = m.default } }) } interface Result { title: string titles: string[] text?: string } const vitePressData = useData() const { activate } = useFocusTrap(el, { immediate: true, allowOutsideClick: true, clickOutsideDeactivates: true, escapeDeactivates: true }) const { localeIndex, theme } = vitePressData const searchIndex = computedAsync(async () => markRaw( MiniSearch.loadJSON<Result>( (await searchIndexData.value[localeIndex.value]?.())?.default, { fields: ['title', 'titles', 'text'], storeFields: ['title', 'titles'], searchOptions: { fuzzy: 0.2, prefix: true, boost: { title: 4, text: 2, titles: 1 }, ...(theme.value.search?.provider === 'local' && theme.value.search.options?.miniSearch?.searchOptions) }, ...(theme.value.search?.provider === 'local' && theme.value.search.options?.miniSearch?.options) } ) ) ) const disableQueryPersistence = computed(() => { return ( theme.value.search?.provider === 'local' && theme.value.search.options?.disableQueryPersistence === true ) }) const filterText = disableQueryPersistence.value ? ref('') : useSessionStorage('vitepress:local-search-filter', '') const showDetailedList = useLocalStorage( 'vitepress:local-search-detailed-list', theme.value.search?.provider === 'local' && theme.value.search.options?.detailedView === true ) const disableDetailedView = computed(() => { return ( theme.value.search?.provider === 'local' && (theme.value.search.options?.disableDetailedView === true || theme.value.search.options?.detailedView === false) ) }) const buttonText = computed(() => { const options = theme.value.search?.options ?? theme.value.algolia return ( options?.locales?.[localeIndex.value]?.translations?.button?.buttonText || options?.translations?.button?.buttonText || 'Search' ) }) watchEffect(() => { if (disableDetailedView.value) { showDetailedList.value = false } }) const results: Ref<(SearchResult & Result)[]> = shallowRef([]) const enableNoResults = ref(false) watch(filterText, () => { enableNoResults.value = false }) const mark = computedAsync(async () => { if (!resultsEl.value) return return markRaw(new Mark(resultsEl.value)) }, null) const cache = new LRUCache<string, Map<string, string>>(16) // 16 files debouncedWatch( () => [searchIndex.value, filterText.value, showDetailedList.value] as const, async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => { if (old?.[0] !== index) { // in case of hmr cache.clear() } let canceled = false onCleanup(() => { canceled = true }) if (!index) return // Search results.value = index .search(filterTextValue) .slice(0, 16) as (SearchResult & Result)[] enableNoResults.value = true // Highlighting const mods = showDetailedListValue ? await Promise.all(results.value.map((r) => fetchExcerpt(r.id))) : [] if (canceled) return for (const { id, mod } of mods) { const mapId = id.slice(0, id.indexOf('#')) let map = cache.get(mapId) if (map) continue map = new Map() cache.set(mapId, map) const comp = mod.default ?? mod if (comp?.render || comp?.setup) { const app = createApp(comp) // Silence warnings about missing components app.config.warnHandler = () => {} app.provide(dataSymbol, vitePressData) Object.defineProperties(app.config.globalProperties, { $frontmatter: { get() { return vitePressData.frontmatter.value } }, $params: { get() { return vitePressData.page.value.params } } }) const div = document.createElement('div') app.mount(div) const headings = div.querySelectorAll('h1, h2, h3, h4, h5, h6') headings.forEach((el) => { const href = el.querySelector('a')?.getAttribute('href') const anchor = href?.startsWith('#') && href.slice(1) if (!anchor) return let html = '' while ((el = el.nextElementSibling!) && !/^h[1-6]$/i.test(el.tagName)) html += el.outerHTML map!.set(anchor, html) }) app.unmount() } if (canceled) return } const terms = new Set<string>() results.value = results.value.map((r) => { const [id, anchor] = r.id.split('#') const map = cache.get(id) const text = map?.get(anchor) ?? '' for (const term in r.match) { terms.add(term) } return { ...r, text } }) await nextTick() if (canceled) return await new Promise((r) => { mark.value?.unmark({ done: () => { mark.value?.markRegExp(formMarkRegex(terms), { done: r }) } }) }) const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? [] for (const excerpt of excerpts) { excerpt .querySelector('mark[data-markjs="true"]') ?.scrollIntoView({ block: 'center' }) } // FIXME: without this whole page scrolls to the bottom resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' }) }, { debounce: 200, immediate: true } ) async function fetchExcerpt(id: string) { const file = pathToFile(id.slice(0, id.indexOf('#'))) try { if (!file) throw new Error(`Cannot find file for id: ${id}`) return { id, mod: await import(/*@vite-ignore*/ file) } } catch (e) { console.error(e) return { id, mod: {} } } } /* Search input focus */ const searchInput = ref<HTMLInputElement>() const disableReset = computed(() => { return filterText.value?.length <= 0 }) function focusSearchInput(select = true) { searchInput.value?.focus() select && searchInput.value?.select() } onMounted(() => { focusSearchInput() }) function onSearchBarClick(event: PointerEvent) { if (event.pointerType === 'mouse') { focusSearchInput() } } /* Search keyboard selection */ const selectedIndex = ref(-1) const disableMouseOver = ref(true) watch(results, (r) => { selectedIndex.value = r.length ? 0 : -1 scrollToSelectedResult() }) function scrollToSelectedResult() { nextTick(() => { const selectedEl = document.querySelector('.result.selected') selectedEl?.scrollIntoView({ block: 'nearest' }) }) } onKeyStroke('ArrowUp', (event) => { event.preventDefault() selectedIndex.value-- if (selectedIndex.value < 0) { selectedIndex.value = results.value.length - 1 } disableMouseOver.value = true scrollToSelectedResult() }) onKeyStroke('ArrowDown', (event) => { event.preventDefault() selectedIndex.value++ if (selectedIndex.value >= results.value.length) { selectedIndex.value = 0 } disableMouseOver.value = true scrollToSelectedResult() }) const router = useRouter() onKeyStroke('Enter', (e) => { if (e.isComposing) return if (e.target instanceof HTMLButtonElement && e.target.type !== 'submit') return const selectedPackage = results.value[selectedIndex.value] if (e.target instanceof HTMLInputElement && !selectedPackage) { e.preventDefault() return } if (selectedPackage) { router.go(selectedPackage.id) emit('close') } }) onKeyStroke('Escape', () => { emit('close') }) // Translations const defaultTranslations: { modal: any } = { modal: { displayDetails: 'Display detailed list', resetButtonTitle: 'Reset search', backButtonTitle: 'Close search', noResultsText: 'No results for', footer: { selectText: 'to select', selectKeyAriaLabel: 'enter', navigateText: 'to navigate', navigateUpKeyAriaLabel: 'up arrow', navigateDownKeyAriaLabel: 'down arrow', closeText: 'to close', closeKeyAriaLabel: 'escape' } } } // Back onMounted(() => { // Prevents going to previous site window.history.pushState(null, '', null) }) useEventListener('popstate', (event) => { event.preventDefault() emit('close') }) /** Lock body */ const isLocked = useScrollLock(inBrowser ? document.body : null) onMounted(() => { nextTick(() => { isLocked.value = true nextTick().then(() => activate()) }) }) onBeforeUnmount(() => { isLocked.value = false }) function resetSearch() { filterText.value = '' nextTick().then(() => focusSearchInput(false)) } function formMarkRegex(terms: Set<string>) { return new RegExp( [...terms] .sort((a, b) => b.length - a.length) .map((term) => `(${escapeRegExp(term)})`) .join('|'), 'gi' ) } function onMouseMove(e: MouseEvent) { if (!disableMouseOver.value) return const el = (e.target as HTMLElement)?.closest<HTMLAnchorElement>('.result') const index = Number.parseInt(el?.dataset.index!) if (index >= 0 && index !== selectedIndex.value) { selectedIndex.value = index } disableMouseOver.value = false } </script> <template> <Teleport to="body"> <div ref="el" role="button" :aria-owns="results?.length ? 'localsearch-list' : undefined" aria-expanded="true" aria-haspopup="listbox" aria-labelledby="localsearch-label" class="VPLocalSearchBox" > <div class="backdrop" @click="$emit('close')" /> <div class="shell"> <form class="search-bar" @pointerup="onSearchBarClick($event)" @submit.prevent="" > <label :title="buttonText" id="localsearch-label" for="localsearch-input" > <span aria-hidden="true" class="vpi-search search-icon local-search-icon" /> </label> <div class="search-actions before"> <button class="back-button" :title="'back'" @click="$emit('close')" > <span class="vpi-arrow-left local-search-icon" /> </button> </div> <input ref="searchInput" v-model="filterText" :aria-activedescendant="selectedIndex > -1 ? ('localsearch-item-' + selectedIndex) : undefined" aria-autocomplete="both" :aria-controls="results?.length ? 'localsearch-list' : undefined" aria-labelledby="localsearch-label" autocapitalize="off" autocomplete="off" autocorrect="off" class="search-input" id="localsearch-input" enterkeyhint="go" maxlength="64" :placeholder="buttonText" spellcheck="false" type="search" /> <div class="search-actions"> <button v-if="!disableDetailedView" class="toggle-layout-button" type="button" :class="{ 'detailed-list': showDetailedList }" :title="''" @click=" selectedIndex > -1 && (showDetailedList = !showDetailedList) " > <span class="vpi-layout-list local-search-icon" /> </button> <button class="clear-button" type="reset" :disabled="disableReset" :title="'reset'" @click="resetSearch" > <span c
Pensando...
