@@ -1,13 +1,5 @@
|
||||
import { isAfter, isBefore } from 'date-fns'
|
||||
|
||||
export function isBetweenChristmasAndHalfOfJanuary(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const christmas = new Date(year, 11, 20) // December 20
|
||||
const halfOfJanuary = new Date(year, 0, 10) // January 10 of the next year
|
||||
|
||||
return isAfter(date, christmas) || isBefore(date, halfOfJanuary)
|
||||
}
|
||||
|
||||
export function isBetweenHalloweenAndHalfOfNovember(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const halloween = new Date(year, 9, 25) // October 25
|
||||
@@ -15,3 +7,11 @@ export function isBetweenHalloweenAndHalfOfNovember(date: Date) {
|
||||
|
||||
return isAfter(date, halloween) && isBefore(date, halfOfNovember)
|
||||
}
|
||||
|
||||
export function isBetweenChristmasAndHalfOfJanuary(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const christmas = new Date(year, 11, 20) // December 20
|
||||
const halfOfJanuary = new Date(year, 0, 10) // January 10 of the next year
|
||||
|
||||
return isAfter(date, christmas) || isBefore(date, halfOfJanuary)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export function useEditLink() {
|
||||
const { page, theme } = useData()
|
||||
const { theme, page } = useData()
|
||||
const { t } = useI18n()
|
||||
|
||||
return computed(() => {
|
||||
const { pattern = '', text = t('docs.theme.doc.community.edit.title') } = theme.value.editLink || {}
|
||||
const { text = t('docs.theme.doc.community.edit.title'), pattern = '' } = theme.value.editLink || {}
|
||||
let url: string
|
||||
if (typeof pattern === 'function') {
|
||||
url = pattern(page.value)
|
||||
@@ -16,6 +16,6 @@ export function useEditLink() {
|
||||
url = pattern.replace(/:path/g, page.value.filePath)
|
||||
}
|
||||
|
||||
return { text, url }
|
||||
return { url, text }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,10 +7,6 @@ import { getScrollOffset } from 'vitepress'
|
||||
import { onMounted, onUpdated } from 'vue'
|
||||
|
||||
export interface Header {
|
||||
/**
|
||||
* The children of the header
|
||||
*/
|
||||
children: Header[]
|
||||
/**
|
||||
* The level of the header
|
||||
*
|
||||
@@ -18,11 +14,9 @@ export interface Header {
|
||||
*/
|
||||
level: number
|
||||
/**
|
||||
* Link of the header
|
||||
*
|
||||
* Typically using `#${slug}` as the anchor hash
|
||||
* The title of the header
|
||||
*/
|
||||
link: string
|
||||
title: string
|
||||
/**
|
||||
* The slug of the header
|
||||
*
|
||||
@@ -30,17 +24,33 @@ export interface Header {
|
||||
*/
|
||||
slug: string
|
||||
/**
|
||||
* The title of the header
|
||||
* Link of the header
|
||||
*
|
||||
* Typically using `#${slug}` as the anchor hash
|
||||
*/
|
||||
title: string
|
||||
link: string
|
||||
/**
|
||||
* The children of the header
|
||||
*/
|
||||
children: Header[]
|
||||
}
|
||||
|
||||
// cached list of anchor elements from resolveHeaders
|
||||
const resolvedHeaders: { element: HTMLHeadElement, link: string }[] = []
|
||||
|
||||
export type MenuItem = Omit<Header, 'children' | 'slug'> & {
|
||||
children?: MenuItem[]
|
||||
export type MenuItem = Omit<Header, 'slug' | 'children'> & {
|
||||
element: HTMLHeadElement
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
export function resolveTitle(theme: DefaultTheme.Config) {
|
||||
return (
|
||||
(typeof theme.outline === 'object'
|
||||
&& !Array.isArray(theme.outline)
|
||||
&& theme.outline.label)
|
||||
|| theme.outlineTitle
|
||||
|| 'On this page'
|
||||
)
|
||||
}
|
||||
|
||||
export function getHeaders(range: DefaultTheme.Config['outline']) {
|
||||
@@ -50,15 +60,35 @@ export function getHeaders(range: DefaultTheme.Config['outline']) {
|
||||
const level = Number(el.tagName[1])
|
||||
return {
|
||||
element: el as HTMLHeadElement,
|
||||
level,
|
||||
link: `#${el.id}`,
|
||||
title: serializeHeader(el),
|
||||
link: `#${el.id}`,
|
||||
level,
|
||||
}
|
||||
})
|
||||
|
||||
return resolveHeaders(headers, range)
|
||||
}
|
||||
|
||||
function serializeHeader(h: Element): string {
|
||||
let ret = ''
|
||||
for (const node of Array.from(h.childNodes)) {
|
||||
if (node.nodeType === 1) {
|
||||
if (
|
||||
(node as Element).classList.contains('VPBadge')
|
||||
|| (node as Element).classList.contains('header-anchor')
|
||||
|| (node as Element).classList.contains('ignore-header')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
ret += node.textContent
|
||||
}
|
||||
else if (node.nodeType === 3) {
|
||||
ret += node.textContent
|
||||
}
|
||||
}
|
||||
return ret.trim()
|
||||
}
|
||||
|
||||
export function resolveHeaders(
|
||||
headers: MenuItem[],
|
||||
range?: DefaultTheme.Config['outline'],
|
||||
@@ -110,16 +140,6 @@ export function resolveHeaders(
|
||||
return ret
|
||||
}
|
||||
|
||||
export function resolveTitle(theme: DefaultTheme.Config) {
|
||||
return (
|
||||
(typeof theme.outline === 'object'
|
||||
&& !Array.isArray(theme.outline)
|
||||
&& theme.outline.label)
|
||||
|| theme.outlineTitle
|
||||
|| 'On this page'
|
||||
)
|
||||
}
|
||||
|
||||
export function useActiveAnchor(
|
||||
container: Ref<HTMLElement>,
|
||||
marker: Ref<HTMLElement>,
|
||||
@@ -173,7 +193,7 @@ export function useActiveAnchor(
|
||||
}
|
||||
|
||||
// find the last header above the top of viewport
|
||||
let activeLink: null | string = null
|
||||
let activeLink: string | null = null
|
||||
for (const { link, top } of headers) {
|
||||
if (top > scrollY + getScrollOffset() + 4) {
|
||||
break
|
||||
@@ -184,7 +204,7 @@ export function useActiveAnchor(
|
||||
activateLink(activeLink)
|
||||
}
|
||||
|
||||
function activateLink(hash: null | string) {
|
||||
function activateLink(hash: string | null) {
|
||||
if (prevActiveLink) {
|
||||
prevActiveLink.classList.remove('active')
|
||||
}
|
||||
@@ -228,26 +248,6 @@ function getAbsoluteTop(element: HTMLElement): number {
|
||||
return offsetTop
|
||||
}
|
||||
|
||||
function serializeHeader(h: Element): string {
|
||||
let ret = ''
|
||||
for (const node of Array.from(h.childNodes)) {
|
||||
if (node.nodeType === 1) {
|
||||
if (
|
||||
(node as Element).classList.contains('VPBadge')
|
||||
|| (node as Element).classList.contains('header-anchor')
|
||||
|| (node as Element).classList.contains('ignore-header')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
ret += node.textContent
|
||||
}
|
||||
else if (node.nodeType === 3) {
|
||||
ret += node.textContent
|
||||
}
|
||||
}
|
||||
return ret.trim()
|
||||
}
|
||||
|
||||
function throttleAndDebounce(fn: () => void, delay: number): () => void {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
let called = false
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getFlatSideBarLinks, getSidebar, isActive } from './sidebar'
|
||||
* - Respects frontmatter overrides and hides when disabled.
|
||||
*/
|
||||
export function usePrevNext() {
|
||||
const { frontmatter, lang, page, theme } = useData()
|
||||
const { page, theme, frontmatter, lang } = useData()
|
||||
|
||||
return computed(() => {
|
||||
// Blog-specific navigation: ensure next/prev stay within same language blog directory
|
||||
@@ -53,7 +53,6 @@ export function usePrevNext() {
|
||||
? undefined
|
||||
: prevPost
|
||||
? {
|
||||
link: withBase(prevPost.url),
|
||||
text:
|
||||
(typeof frontmatter.value.prev === 'string'
|
||||
? frontmatter.value.prev
|
||||
@@ -61,6 +60,7 @@ export function usePrevNext() {
|
||||
? frontmatter.value.prev.text
|
||||
: undefined)
|
||||
?? prevPost.title,
|
||||
link: withBase(prevPost.url),
|
||||
}
|
||||
: undefined
|
||||
|
||||
@@ -68,7 +68,6 @@ export function usePrevNext() {
|
||||
? undefined
|
||||
: nextPost
|
||||
? {
|
||||
link: withBase(nextPost.url),
|
||||
text:
|
||||
(typeof frontmatter.value.next === 'string'
|
||||
? frontmatter.value.next
|
||||
@@ -76,15 +75,16 @@ export function usePrevNext() {
|
||||
? frontmatter.value.next.text
|
||||
: undefined)
|
||||
?? nextPost.title,
|
||||
link: withBase(nextPost.url),
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
next: blogNext,
|
||||
prev: blogPrev,
|
||||
next: blogNext,
|
||||
} as {
|
||||
next?: { link?: string, text?: string }
|
||||
prev?: { link?: string, text?: string }
|
||||
prev?: { text?: string, link?: string }
|
||||
next?: { text?: string, link?: string }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,9 +110,9 @@ export function usePrevNext() {
|
||||
const isSectionRoot = currentFullUrl.replace(/[?#].*$/, '') === sectionBase
|
||||
if (isSectionRoot) {
|
||||
return {
|
||||
next: undefined,
|
||||
prev: undefined,
|
||||
} as { next?: { link?: string, text?: string }, prev?: { link?: string, text?: string } }
|
||||
next: undefined,
|
||||
} as { prev?: { text?: string, link?: string }, next?: { text?: string, link?: string } }
|
||||
}
|
||||
// Keep navigation within the same docs section and exclude the section root itself
|
||||
// to avoid showing a "next" link that points back to the section index.
|
||||
@@ -136,29 +136,9 @@ export function usePrevNext() {
|
||||
})
|
||||
|
||||
return {
|
||||
next: hideNext || index < 0 || index >= candidates.length - 1
|
||||
? undefined
|
||||
: {
|
||||
link:
|
||||
(typeof frontmatter.value.next === 'object'
|
||||
? frontmatter.value.next.link
|
||||
: undefined) ?? candidates[index + 1]?.link,
|
||||
text:
|
||||
(typeof frontmatter.value.next === 'string'
|
||||
? frontmatter.value.next
|
||||
: typeof frontmatter.value.next === 'object'
|
||||
? frontmatter.value.next.text
|
||||
: undefined)
|
||||
?? candidates[index + 1]?.docFooterText
|
||||
?? candidates[index + 1]?.text,
|
||||
},
|
||||
prev: hidePrev || index <= 0
|
||||
? undefined
|
||||
: {
|
||||
link:
|
||||
(typeof frontmatter.value.prev === 'object'
|
||||
? frontmatter.value.prev.link
|
||||
: undefined) ?? candidates[index - 1]?.link,
|
||||
text:
|
||||
(typeof frontmatter.value.prev === 'string'
|
||||
? frontmatter.value.prev
|
||||
@@ -167,10 +147,30 @@ export function usePrevNext() {
|
||||
: undefined)
|
||||
?? candidates[index - 1]?.docFooterText
|
||||
?? candidates[index - 1]?.text,
|
||||
link:
|
||||
(typeof frontmatter.value.prev === 'object'
|
||||
? frontmatter.value.prev.link
|
||||
: undefined) ?? candidates[index - 1]?.link,
|
||||
},
|
||||
next: hideNext || index < 0 || index >= candidates.length - 1
|
||||
? undefined
|
||||
: {
|
||||
text:
|
||||
(typeof frontmatter.value.next === 'string'
|
||||
? frontmatter.value.next
|
||||
: typeof frontmatter.value.next === 'object'
|
||||
? frontmatter.value.next.text
|
||||
: undefined)
|
||||
?? candidates[index + 1]?.docFooterText
|
||||
?? candidates[index + 1]?.text,
|
||||
link:
|
||||
(typeof frontmatter.value.next === 'object'
|
||||
? frontmatter.value.next.link
|
||||
: undefined) ?? candidates[index + 1]?.link,
|
||||
},
|
||||
} as {
|
||||
next?: { link?: string, text?: string }
|
||||
prev?: { link?: string, text?: string }
|
||||
prev?: { text?: string, link?: string }
|
||||
next?: { text?: string, link?: string }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
export interface SidebarControl {
|
||||
collapsed: Ref<boolean>
|
||||
collapsible: ComputedRef<boolean>
|
||||
isLink: ComputedRef<boolean>
|
||||
isActiveLink: Ref<boolean>
|
||||
hasActiveLink: ComputedRef<boolean>
|
||||
hasChildren: ComputedRef<boolean>
|
||||
isActiveLink: Ref<boolean>
|
||||
isLink: ComputedRef<boolean>
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function useCloseSidebarOnEscape(
|
||||
export function useSidebarControl(
|
||||
item: ComputedRef<DefaultTheme.SidebarItem>,
|
||||
): SidebarControl {
|
||||
const { hash, page } = useData()
|
||||
const { page, hash } = useData()
|
||||
|
||||
const collapsed = ref(false)
|
||||
|
||||
@@ -106,10 +106,10 @@ export function useSidebarControl(
|
||||
return {
|
||||
collapsed,
|
||||
collapsible,
|
||||
isLink,
|
||||
isActiveLink,
|
||||
hasActiveLink,
|
||||
hasChildren,
|
||||
isActiveLink,
|
||||
isLink,
|
||||
toggle,
|
||||
}
|
||||
}
|
||||
@@ -121,35 +121,70 @@ const HASH_RE = /#.*$/
|
||||
const HASH_OR_QUERY_RE = /[?#].*$/
|
||||
const INDEX_OR_EXT_RE = /(?:(^|\/)index)?\.(?:md|html)$/
|
||||
|
||||
// From https://github.com/vuejs/vitepress/blob/fa81e89643523170047ca2c9a690f4d7adf4ffdc/src/client/theme-default/support/sidebar.ts
|
||||
export interface SidebarLink {
|
||||
docFooterText?: string
|
||||
link: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export function getFlatSideBarLinks(sidebar: SidebarItem[]): SidebarLink[] {
|
||||
const links: SidebarLink[] = []
|
||||
|
||||
function recursivelyExtractLinks(items: SidebarItem[]) {
|
||||
for (const item of items) {
|
||||
if (item.text && item.link) {
|
||||
links.push({
|
||||
docFooterText: item.docFooterText,
|
||||
link: item.link,
|
||||
text: item.text,
|
||||
})
|
||||
}
|
||||
|
||||
if (item.items) {
|
||||
recursivelyExtractLinks(item.items)
|
||||
}
|
||||
}
|
||||
export function isActive(
|
||||
currentPath: string,
|
||||
matchPath?: string,
|
||||
asRegex: boolean = false,
|
||||
): boolean {
|
||||
if (matchPath === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
recursivelyExtractLinks(sidebar)
|
||||
if (currentPath.startsWith('/')) {
|
||||
currentPath = normalize(`${currentPath}`)
|
||||
}
|
||||
else {
|
||||
currentPath = normalize(`/${currentPath}`)
|
||||
}
|
||||
|
||||
return links
|
||||
if (asRegex) {
|
||||
return new RegExp(matchPath).test(currentPath)
|
||||
}
|
||||
|
||||
if (normalize(matchPath) !== currentPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hashMatch = matchPath.match(HASH_RE)
|
||||
|
||||
if (hashMatch) {
|
||||
return (inBrowser ? location.hash : '') === hashMatch[0]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function normalize(path: string): string {
|
||||
return decodeURI(path)
|
||||
.replace(HASH_OR_QUERY_RE, '')
|
||||
.replace(INDEX_OR_EXT_RE, '$1')
|
||||
}
|
||||
|
||||
// From https://github.com/vuejs/vitepress/blob/97f9469b6d4eb7ba9de9a1111986581d1f704ec3/src/client/theme-default/support/sidebar.ts
|
||||
function containsActiveLink(
|
||||
path: string,
|
||||
items: any | any[],
|
||||
): boolean {
|
||||
if (Array.isArray(items)) {
|
||||
return items.some(item => containsActiveLink(path, item))
|
||||
}
|
||||
|
||||
return isActive(path, items.link)
|
||||
? true
|
||||
: items.items
|
||||
? containsActiveLink(path, items.items)
|
||||
: false
|
||||
}
|
||||
|
||||
// From https://github.com/vuejs/vitepress/blob/fa81e89643523170047ca2c9a690f4d7adf4ffdc/src/client/theme-default/support/sidebar.ts
|
||||
export interface SidebarLink {
|
||||
text: string
|
||||
link: string
|
||||
docFooterText?: string
|
||||
}
|
||||
|
||||
function ensureStartingSlash(path: string): string {
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,6 +245,30 @@ export function getSidebarGroups(sidebar: SidebarItem[]): SidebarItem[] {
|
||||
return groups
|
||||
}
|
||||
|
||||
export function getFlatSideBarLinks(sidebar: SidebarItem[]): SidebarLink[] {
|
||||
const links: SidebarLink[] = []
|
||||
|
||||
function recursivelyExtractLinks(items: SidebarItem[]) {
|
||||
for (const item of items) {
|
||||
if (item.text && item.link) {
|
||||
links.push({
|
||||
text: item.text,
|
||||
link: item.link,
|
||||
docFooterText: item.docFooterText,
|
||||
})
|
||||
}
|
||||
|
||||
if (item.items) {
|
||||
recursivelyExtractLinks(item.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recursivelyExtractLinks(sidebar)
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given sidebar item contains any active link.
|
||||
*/
|
||||
@@ -228,39 +287,6 @@ export function hasActiveLink(
|
||||
: false
|
||||
}
|
||||
|
||||
export function isActive(
|
||||
currentPath: string,
|
||||
matchPath?: string,
|
||||
asRegex: boolean = false,
|
||||
): boolean {
|
||||
if (matchPath === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (currentPath.startsWith('/')) {
|
||||
currentPath = normalize(`${currentPath}`)
|
||||
}
|
||||
else {
|
||||
currentPath = normalize(`/${currentPath}`)
|
||||
}
|
||||
|
||||
if (asRegex) {
|
||||
return new RegExp(matchPath).test(currentPath)
|
||||
}
|
||||
|
||||
if (normalize(matchPath) !== currentPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hashMatch = matchPath.match(HASH_RE)
|
||||
|
||||
if (hashMatch) {
|
||||
return (inBrowser ? location.hash : '') === hashMatch[0]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function addBase(items: SidebarItem[], _base?: string): SidebarItem[] {
|
||||
return Array.from(items, (_item) => {
|
||||
const item = { ..._item }
|
||||
@@ -272,29 +298,3 @@ function addBase(items: SidebarItem[], _base?: string): SidebarItem[] {
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
// From https://github.com/vuejs/vitepress/blob/97f9469b6d4eb7ba9de9a1111986581d1f704ec3/src/client/theme-default/support/sidebar.ts
|
||||
function containsActiveLink(
|
||||
path: string,
|
||||
items: any | any[],
|
||||
): boolean {
|
||||
if (Array.isArray(items)) {
|
||||
return items.some(item => containsActiveLink(path, item))
|
||||
}
|
||||
|
||||
return isActive(path, items.link)
|
||||
? true
|
||||
: items.items
|
||||
? containsActiveLink(path, items.items)
|
||||
: false
|
||||
}
|
||||
|
||||
function ensureStartingSlash(path: string): string {
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
|
||||
function normalize(path: string): string {
|
||||
return decodeURI(path)
|
||||
.replace(HASH_OR_QUERY_RE, '')
|
||||
.replace(INDEX_OR_EXT_RE, '$1')
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function themeColorFromPropertyOf(colorFromClass: string, property: strin
|
||||
* Reading VitePress' `isDark` also avoids stray `useDark()` instances that
|
||||
* force the theme back to the system preference.
|
||||
*/
|
||||
export function themeColorFromValue(value: string | { dark: string, light: string }): () => Promise<string> {
|
||||
export function themeColorFromValue(value: string | { light: string, dark: string }): () => Promise<string> {
|
||||
const { isDark } = useData()
|
||||
return async () => {
|
||||
if (typeof value === 'string') {
|
||||
@@ -36,7 +36,7 @@ export function themeColorFromValue(value: string | { dark: string, light: strin
|
||||
}
|
||||
}
|
||||
|
||||
export function useThemeColor(colorFrom: () => Promise<string> | string) {
|
||||
export function useThemeColor(colorFrom: () => string | Promise<string>) {
|
||||
async function updateThemeColor() {
|
||||
if (!('document' in globalThis) || globalThis.document == null)
|
||||
return
|
||||
|
||||
+787
-787
File diff suppressed because it is too large
Load Diff
@@ -3,16 +3,16 @@ import type { DefaultTheme } from 'vitepress'
|
||||
import contributorNames from './contributor-names.json'
|
||||
|
||||
export interface Contributor {
|
||||
avatar: string
|
||||
name: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
export interface CoreTeam extends DefaultTheme.TeamMember {
|
||||
discord?: string
|
||||
// required to download avatars from GitHub
|
||||
github: string
|
||||
mastodon?: string
|
||||
twitter?: string
|
||||
mastodon?: string
|
||||
discord?: string
|
||||
youtube?: string
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ function getAvatarUrl(name: string) {
|
||||
|
||||
export const contributors = (contributorNames as string[]).reduce((acc, name) => {
|
||||
contributorsAvatars[name] = getAvatarUrl(name)
|
||||
acc.push({ avatar: contributorsAvatars[name], name })
|
||||
acc.push({ name, avatar: contributorsAvatars[name] })
|
||||
return acc
|
||||
}, [] as Contributor[])
|
||||
function createLinks(tm: CoreTeam): CoreTeam {
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import { defineLoader } from 'vitepress'
|
||||
|
||||
export interface NightlyBuild {
|
||||
conclusion: string
|
||||
created_at: string
|
||||
head_commit_message: string
|
||||
head_sha: string
|
||||
html_url: string
|
||||
id: number
|
||||
export interface Release {
|
||||
name: string
|
||||
status: string
|
||||
updated_at: string
|
||||
workflow_name: string
|
||||
tag_name: string
|
||||
html_url: string
|
||||
published_at: string
|
||||
prerelease: boolean
|
||||
draft: boolean
|
||||
body: string
|
||||
}
|
||||
|
||||
export interface Release {
|
||||
body: string
|
||||
draft: boolean
|
||||
html_url: string
|
||||
export interface NightlyBuild {
|
||||
id: number
|
||||
name: string
|
||||
prerelease: boolean
|
||||
published_at: string
|
||||
tag_name: string
|
||||
html_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
status: string
|
||||
conclusion: string
|
||||
workflow_name: string
|
||||
head_sha: string
|
||||
head_commit_message: string
|
||||
}
|
||||
|
||||
export interface ReleasesData {
|
||||
stable: Release[]
|
||||
prerelease: Release[]
|
||||
nightly: NightlyBuild[]
|
||||
nightlyUrl: string
|
||||
prerelease: Release[]
|
||||
stable: Release[]
|
||||
}
|
||||
|
||||
declare const data: ReleasesData
|
||||
@@ -91,17 +91,17 @@ export default defineLoader({
|
||||
if (actionsResponse.ok) {
|
||||
const actionsData = await actionsResponse.json()
|
||||
nightlyBuilds = actionsData.workflow_runs?.map((run: {
|
||||
conclusion: string
|
||||
id: number
|
||||
name: string
|
||||
head_sha: string
|
||||
html_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
status: string
|
||||
conclusion: string
|
||||
head_commit?: {
|
||||
message: string
|
||||
}
|
||||
head_sha: string
|
||||
html_url: string
|
||||
id: number
|
||||
name: string
|
||||
status: string
|
||||
updated_at: string
|
||||
}) => {
|
||||
const shortSha = run.head_sha.substring(0, 7)
|
||||
// Get first line of commit message
|
||||
@@ -109,16 +109,16 @@ export default defineLoader({
|
||||
const firstLine = commitMessage.split('\n')[0]
|
||||
|
||||
return {
|
||||
conclusion: run.conclusion,
|
||||
created_at: run.created_at,
|
||||
head_commit_message: commitMessage,
|
||||
head_sha: shortSha,
|
||||
html_url: run.html_url,
|
||||
id: run.id,
|
||||
name: firstLine,
|
||||
status: run.status,
|
||||
html_url: run.html_url,
|
||||
created_at: run.created_at,
|
||||
updated_at: run.updated_at,
|
||||
status: run.status,
|
||||
conclusion: run.conclusion,
|
||||
workflow_name: run.name,
|
||||
head_sha: shortSha,
|
||||
head_commit_message: commitMessage,
|
||||
}
|
||||
}) || []
|
||||
}
|
||||
@@ -128,20 +128,20 @@ export default defineLoader({
|
||||
}
|
||||
|
||||
return {
|
||||
stable,
|
||||
prerelease,
|
||||
nightly: nightlyBuilds,
|
||||
nightlyUrl,
|
||||
prerelease,
|
||||
stable,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch releases:', error)
|
||||
// Return empty data if fetch fails
|
||||
return {
|
||||
stable: [],
|
||||
prerelease: [],
|
||||
nightly: [],
|
||||
nightlyUrl,
|
||||
prerelease: [],
|
||||
stable: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,12 +7,12 @@ import { formatDate } from '../utils/utils'
|
||||
const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG
|
||||
|
||||
interface Document {
|
||||
date: ReturnType<typeof formatDate>
|
||||
frontmatter?: Record<string, any>
|
||||
lang: string
|
||||
title: string
|
||||
url: string
|
||||
urlWithoutLang: string
|
||||
lang: string
|
||||
date: ReturnType<typeof formatDate>
|
||||
frontmatter?: Record<string, any>
|
||||
}
|
||||
|
||||
declare const data: Document[]
|
||||
@@ -21,7 +21,7 @@ export { data }
|
||||
export default createContentLoader('**/*.md', {
|
||||
transform(raw): Document[] {
|
||||
return raw
|
||||
.map(({ frontmatter, url }) => {
|
||||
.map(({ url, frontmatter }) => {
|
||||
const foundLanguage = Object.values(config.userConfig.locales!).find((locale) => {
|
||||
let normalizedLanguagePrefix = locale.lang || 'en'
|
||||
if (!normalizedLanguagePrefix.startsWith('/')) {
|
||||
@@ -32,12 +32,12 @@ export default createContentLoader('**/*.md', {
|
||||
})
|
||||
|
||||
return {
|
||||
date: formatDate(frontmatter.date),
|
||||
frontmatter,
|
||||
lang: foundLanguage?.lang || 'en',
|
||||
title: frontmatter.title,
|
||||
url,
|
||||
urlWithoutLang: url.replace(`/${foundLanguage?.lang || 'en'}`, ''),
|
||||
date: formatDate(frontmatter.date),
|
||||
lang: foundLanguage?.lang || 'en',
|
||||
frontmatter,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.date.time - a.date.time)
|
||||
|
||||
@@ -3,26 +3,26 @@ import { webcrypto } from 'node:crypto'
|
||||
import { createContentLoader } from 'vitepress'
|
||||
|
||||
export interface Author {
|
||||
avatar?: string
|
||||
avatarFallback: string
|
||||
role: string
|
||||
kind: 'person' | 'team'
|
||||
|
||||
displayName: string
|
||||
|
||||
githubEmail?: string
|
||||
githubUsername?: string
|
||||
githubEmail?: string
|
||||
|
||||
kind: 'person' | 'team'
|
||||
role: string
|
||||
avatar?: string
|
||||
avatarFallback: string
|
||||
}
|
||||
|
||||
interface MarkdownAuthor {
|
||||
avatar?: string
|
||||
githubEmail?: string
|
||||
githubUsername?: string
|
||||
kind?: 'person' | 'team'
|
||||
|
||||
name?: string
|
||||
role?: string
|
||||
kind?: 'person' | 'team'
|
||||
avatar?: string
|
||||
|
||||
githubUsername?: string
|
||||
githubEmail?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ async function digestStringAsSHA256(message: string) {
|
||||
return hashHex
|
||||
}
|
||||
|
||||
async function newAvatarForAuthor(mappedAuthor?: null | { displayName?: string, githubUsername?: string, overrideAvatar?: string }, email?: null | string): Promise<string> {
|
||||
async function newAvatarForAuthor(mappedAuthor?: { overrideAvatar?: string, githubUsername?: string, displayName?: string } | null, email?: string | null): Promise<string> {
|
||||
if (mappedAuthor) {
|
||||
if (mappedAuthor.overrideAvatar)
|
||||
return mappedAuthor.overrideAvatar
|
||||
@@ -54,10 +54,10 @@ async function newAvatarForAuthor(mappedAuthor?: null | { displayName?: string,
|
||||
}
|
||||
|
||||
export default createContentLoader('**/*.md', {
|
||||
async transform(raw): Promise<Array<{ authors: Author[], url: string }>> {
|
||||
async transform(raw): Promise<Array<{ url: string, authors: Author[] }>> {
|
||||
return (await Promise.all(
|
||||
raw
|
||||
.map(async ({ frontmatter, url }) => {
|
||||
.map(async ({ url, frontmatter }) => {
|
||||
const authors: MarkdownAuthor[] = frontmatter.authors
|
||||
if (!authors || !Array.isArray(authors)) {
|
||||
return
|
||||
@@ -67,22 +67,22 @@ export default createContentLoader('**/*.md', {
|
||||
const displayName = author.name || author.githubUsername || author.githubEmail || 'Unknown Author'
|
||||
|
||||
return {
|
||||
avatar: author.avatar || await newAvatarForAuthor({ displayName, githubUsername: author.githubUsername }, author.githubEmail),
|
||||
avatarFallback: `https://gravatar.com/avatar/${await digestStringAsSHA256(displayName)}?d=retro`,
|
||||
role: author.role || 'Contributor',
|
||||
kind: author.kind || 'person',
|
||||
|
||||
displayName,
|
||||
|
||||
githubEmail: author.githubEmail,
|
||||
githubUsername: author.githubUsername,
|
||||
githubEmail: author.githubEmail,
|
||||
|
||||
kind: author.kind || 'person',
|
||||
role: author.role || 'Contributor',
|
||||
avatar: author.avatar || await newAvatarForAuthor({ githubUsername: author.githubUsername, displayName }, author.githubEmail),
|
||||
avatarFallback: `https://gravatar.com/avatar/${await digestStringAsSHA256(displayName)}?d=retro`,
|
||||
}
|
||||
}))
|
||||
|
||||
return {
|
||||
authors: authorsTransformed,
|
||||
url,
|
||||
authors: authorsTransformed,
|
||||
}
|
||||
}),
|
||||
)).filter(item => item != null)
|
||||
|
||||
@@ -14,13 +14,13 @@ const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG
|
||||
const base = config.userConfig.base || env.BASE_URL || '/'
|
||||
|
||||
interface Post {
|
||||
date: ReturnType<typeof formatDate>
|
||||
excerpt: string | undefined
|
||||
frontmatter?: Record<string, any>
|
||||
lang: string
|
||||
title: string
|
||||
url: string
|
||||
urlWithoutLang: string
|
||||
lang: string
|
||||
date: ReturnType<typeof formatDate>
|
||||
excerpt: string | undefined
|
||||
frontmatter?: Record<string, any>
|
||||
}
|
||||
|
||||
declare const data: Post[]
|
||||
@@ -77,12 +77,12 @@ function withDirname(url?: string, cwd?: string) {
|
||||
}
|
||||
|
||||
export default createContentLoader('**/blog/**/*.md', {
|
||||
excerpt: true,
|
||||
includeSrc: true,
|
||||
render: true,
|
||||
excerpt: true,
|
||||
async transform(raw): Promise<Post[]> {
|
||||
return (await Promise.all(raw
|
||||
.map(async ({ excerpt, frontmatter, url }) => {
|
||||
.map(async ({ url, frontmatter, excerpt }) => {
|
||||
const foundLanguage = Object.values(config.userConfig.locales!).find((locale) => {
|
||||
let normalizedLanguagePrefix = locale.lang || 'en'
|
||||
if (!normalizedLanguagePrefix.startsWith('/')) {
|
||||
@@ -117,19 +117,19 @@ export default createContentLoader('**/blog/**/*.md', {
|
||||
const previewCoverDark = withBase(await fileToUrl(withDirname(fromAtAssets(frontmatter['preview-cover']?.dark), cwdFromUrl(url))), base)
|
||||
|
||||
const res = {
|
||||
date: formatDate(frontmatter.date),
|
||||
excerpt,
|
||||
frontmatter: {
|
||||
...frontmatter,
|
||||
'preview-cover': {
|
||||
dark: previewCoverDark,
|
||||
light: previewCoverLight,
|
||||
},
|
||||
},
|
||||
lang: foundLanguage?.lang || 'en',
|
||||
title: frontmatter.title,
|
||||
url,
|
||||
urlWithoutLang: url.replace(`/${foundLanguage?.lang || 'en'}`, ''),
|
||||
excerpt,
|
||||
date: formatDate(frontmatter.date),
|
||||
lang: foundLanguage?.lang || 'en',
|
||||
frontmatter: {
|
||||
...frontmatter,
|
||||
'preview-cover': {
|
||||
light: previewCoverLight,
|
||||
dark: previewCoverDark,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return res
|
||||
|
||||
@@ -7,25 +7,25 @@ import { formatDate } from '../utils/utils'
|
||||
const config: SiteConfig = (globalThis as any).VITEPRESS_CONFIG
|
||||
|
||||
interface ChronicleEntry {
|
||||
date: ReturnType<typeof formatDate>
|
||||
excerpt: string | undefined
|
||||
frontmatter?: Record<string, any>
|
||||
lang: string
|
||||
title: string
|
||||
url: string
|
||||
urlWithoutLang: string
|
||||
lang: string
|
||||
date: ReturnType<typeof formatDate>
|
||||
excerpt: string | undefined
|
||||
frontmatter?: Record<string, any>
|
||||
}
|
||||
|
||||
declare const data: ChronicleEntry[]
|
||||
export { data }
|
||||
|
||||
export default createContentLoader('**/chronicles/**/*.md', {
|
||||
excerpt: true,
|
||||
includeSrc: true,
|
||||
render: true,
|
||||
excerpt: true,
|
||||
transform(raw): ChronicleEntry[] {
|
||||
return raw
|
||||
.map(({ excerpt, frontmatter, url }) => {
|
||||
.map(({ url, frontmatter, excerpt }) => {
|
||||
const foundLanguage = Object.values(config.userConfig.locales!).find((locale) => {
|
||||
let normalizedLanguagePrefix = locale.lang || 'en'
|
||||
if (!normalizedLanguagePrefix.startsWith('/')) {
|
||||
@@ -36,13 +36,13 @@ export default createContentLoader('**/chronicles/**/*.md', {
|
||||
})
|
||||
|
||||
return {
|
||||
date: formatDate(frontmatter.date),
|
||||
excerpt,
|
||||
frontmatter,
|
||||
lang: foundLanguage?.lang || 'en',
|
||||
title: frontmatter.title,
|
||||
url,
|
||||
urlWithoutLang: url.replace(`/${foundLanguage?.lang || 'en'}`, ''),
|
||||
excerpt,
|
||||
date: formatDate(frontmatter.date),
|
||||
lang: foundLanguage?.lang || 'en',
|
||||
frontmatter,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.date.time - a.date.time)
|
||||
|
||||
@@ -9,102 +9,6 @@ import matter from 'gray-matter'
|
||||
|
||||
import { glob } from 'tinyglobby'
|
||||
|
||||
interface VitePressConfig extends ResolvedConfig {
|
||||
vitepress: SiteConfig
|
||||
}
|
||||
|
||||
export function frontmatterAssets(): Plugin {
|
||||
let resolvedConfig: undefined | VitePressConfig
|
||||
const mAssetAbsoluteUrlMetadata = new Map<string, { builtUrl?: string, hash?: string, url: string }>()
|
||||
const mapAssetBuiltUrlAssetAbsoluteUrl = new Map<string, string>()
|
||||
|
||||
async function fileToUrl(file: string) {
|
||||
if (!file) {
|
||||
return {
|
||||
url: file,
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parse(file)
|
||||
const hash = createHash('sha256')
|
||||
.update(await readFile(file))
|
||||
.digest('hex')
|
||||
.slice(0, 8)
|
||||
|
||||
return {
|
||||
hash,
|
||||
url: `/assets/${parsed.name}.${hash}${parsed.ext}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async configResolved(config) {
|
||||
resolvedConfig = config as VitePressConfig
|
||||
|
||||
const markdownFiles = await glob('**/*.md', { absolute: true, cwd: resolvedConfig?.vitepress.srcDir || '', ignore: ['**/node_modules/**'] })
|
||||
for (const file of markdownFiles) {
|
||||
const res = (await readFile(file))
|
||||
const { data } = matter(res.toString('utf-8'))
|
||||
if (Object.keys(data).length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
recursivelyFindAtAssets(data, (matched) => {
|
||||
const assetPath = fromAtAssets(matched)
|
||||
let absoluteAssetPath: string
|
||||
if (assetPath.startsWith('/')) {
|
||||
absoluteAssetPath = join(resolvedConfig?.vitepress.srcDir || '', assetPath)
|
||||
}
|
||||
else {
|
||||
absoluteAssetPath = join(dirname(file), assetPath)
|
||||
}
|
||||
|
||||
mAssetAbsoluteUrlMetadata.set(absoluteAssetPath, { builtUrl: file, hash: '', url: file })
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
for (const [key, value] of mAssetAbsoluteUrlMetadata) {
|
||||
const { hash, url } = await fileToUrl(key)
|
||||
mapAssetBuiltUrlAssetAbsoluteUrl.set(url, key)
|
||||
mAssetAbsoluteUrlMetadata.set(key, { builtUrl: url, hash, url: value.url })
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
const requesting = withoutBase(req.url, resolvedConfig?.base)
|
||||
if (!requesting || !mapAssetBuiltUrlAssetAbsoluteUrl.has(requesting)) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const filePath = mapAssetBuiltUrlAssetAbsoluteUrl.get(requesting)
|
||||
if (!filePath) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const ext = parse(filePath).ext.slice(1)
|
||||
const fileContent = await readFile(filePath)
|
||||
|
||||
res.writeHead(200, {
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
'Content-Length': fileContent.length,
|
||||
'Content-Type': ext === 'svg' ? 'image/svg+xml' : `image/${ext}`,
|
||||
})
|
||||
res.end(fileContent)
|
||||
res.end()
|
||||
})
|
||||
},
|
||||
enforce: 'pre',
|
||||
name: '@proj-airi/docs:vite-plugin-frontmatter-assets',
|
||||
async writeBundle() {
|
||||
for (const [builtUrl, absoluteUrl] of mapAssetBuiltUrlAssetAbsoluteUrl.entries()) {
|
||||
const content = await this.fs.readFile(absoluteUrl)
|
||||
await this.fs.writeFile(join(resolvedConfig!.vitepress.outDir!, builtUrl), content)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function fromAtAssets(url: string): string {
|
||||
const reg = /^@assets\((?:'(\S+)'|"(\S+)"|(\S+))\)$/
|
||||
if (reg.test(url)) {
|
||||
@@ -124,6 +28,10 @@ function fromAtAssets(url: string): string {
|
||||
return url
|
||||
}
|
||||
|
||||
interface VitePressConfig extends ResolvedConfig {
|
||||
vitepress: SiteConfig
|
||||
}
|
||||
|
||||
function recursivelyFindAtAssets(propertyMaybeObjectOrScalar: unknown, fn: (value: string) => string | undefined) {
|
||||
if (typeof propertyMaybeObjectOrScalar === 'string') {
|
||||
// eslint-disable-next-line regexp/no-unused-capturing-group
|
||||
@@ -181,3 +89,95 @@ function withoutBase(url?: string, base?: string): string | undefined {
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export function frontmatterAssets(): Plugin {
|
||||
let resolvedConfig: VitePressConfig | undefined
|
||||
const mAssetAbsoluteUrlMetadata = new Map<string, { url: string, builtUrl?: string, hash?: string }>()
|
||||
const mapAssetBuiltUrlAssetAbsoluteUrl = new Map<string, string>()
|
||||
|
||||
async function fileToUrl(file: string) {
|
||||
if (!file) {
|
||||
return {
|
||||
url: file,
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parse(file)
|
||||
const hash = createHash('sha256')
|
||||
.update(await readFile(file))
|
||||
.digest('hex')
|
||||
.slice(0, 8)
|
||||
|
||||
return {
|
||||
hash,
|
||||
url: `/assets/${parsed.name}.${hash}${parsed.ext}`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: '@proj-airi/docs:vite-plugin-frontmatter-assets',
|
||||
enforce: 'pre',
|
||||
async configResolved(config) {
|
||||
resolvedConfig = config as VitePressConfig
|
||||
|
||||
const markdownFiles = await glob('**/*.md', { ignore: ['**/node_modules/**'], cwd: resolvedConfig?.vitepress.srcDir || '', absolute: true })
|
||||
for (const file of markdownFiles) {
|
||||
const res = (await readFile(file))
|
||||
const { data } = matter(res.toString('utf-8'))
|
||||
if (Object.keys(data).length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
recursivelyFindAtAssets(data, (matched) => {
|
||||
const assetPath = fromAtAssets(matched)
|
||||
let absoluteAssetPath: string
|
||||
if (assetPath.startsWith('/')) {
|
||||
absoluteAssetPath = join(resolvedConfig?.vitepress.srcDir || '', assetPath)
|
||||
}
|
||||
else {
|
||||
absoluteAssetPath = join(dirname(file), assetPath)
|
||||
}
|
||||
|
||||
mAssetAbsoluteUrlMetadata.set(absoluteAssetPath, { url: file, builtUrl: file, hash: '' })
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
for (const [key, value] of mAssetAbsoluteUrlMetadata) {
|
||||
const { url, hash } = await fileToUrl(key)
|
||||
mapAssetBuiltUrlAssetAbsoluteUrl.set(url, key)
|
||||
mAssetAbsoluteUrlMetadata.set(key, { url: value.url, builtUrl: url, hash })
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
const requesting = withoutBase(req.url, resolvedConfig?.base)
|
||||
if (!requesting || !mapAssetBuiltUrlAssetAbsoluteUrl.has(requesting)) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const filePath = mapAssetBuiltUrlAssetAbsoluteUrl.get(requesting)
|
||||
if (!filePath) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const ext = parse(filePath).ext.slice(1)
|
||||
const fileContent = await readFile(filePath)
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': ext === 'svg' ? 'image/svg+xml' : `image/${ext}`,
|
||||
'Content-Length': fileContent.length,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
})
|
||||
res.end(fileContent)
|
||||
res.end()
|
||||
})
|
||||
},
|
||||
async writeBundle() {
|
||||
for (const [builtUrl, absoluteUrl] of mapAssetBuiltUrlAssetAbsoluteUrl.entries()) {
|
||||
const content = await this.fs.readFile(absoluteUrl)
|
||||
await this.fs.writeFile(join(resolvedConfig!.vitepress.outDir!, builtUrl), content)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import type { DefaultTheme } from 'vitepress'
|
||||
|
||||
interface ExtraThemeConfig {
|
||||
homepage: HomePageConfig
|
||||
}
|
||||
|
||||
interface HomePageConfig {
|
||||
buttons: ButtonItem[]
|
||||
}
|
||||
|
||||
export interface ButtonItem extends Link {
|
||||
primary?: boolean
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
text?: string
|
||||
link?: string
|
||||
|
||||
/**
|
||||
* VitePress intercepts `<a>` tag clicks for SPA navigation, which can cause routing errors for external links.<br/>
|
||||
* Adding a `target` attribute allows the browser to handle the navigation natively, avoiding this problem.
|
||||
@@ -15,16 +25,6 @@ export interface Link {
|
||||
* https://stackoverflow.com/questions/79348337/redirect-main-title-link-in-vitepress-to-my-personal-website/79386388#79386388
|
||||
*/
|
||||
target?: string
|
||||
|
||||
text?: string
|
||||
}
|
||||
|
||||
export type ThemeConfig = DefaultTheme.Config & ExtraThemeConfig
|
||||
|
||||
interface ExtraThemeConfig {
|
||||
homepage: HomePageConfig
|
||||
}
|
||||
|
||||
interface HomePageConfig {
|
||||
buttons: ButtonItem[]
|
||||
}
|
||||
|
||||
@@ -23,20 +23,20 @@ import '@fontsource/dm-serif-display/index.css'
|
||||
import '@fontsource-variable/comfortaa/index.css'
|
||||
|
||||
export default {
|
||||
Layout,
|
||||
enhanceApp({ app, siteData }) {
|
||||
if (!import.meta.env.SSR && import.meta.env.PROD) {
|
||||
import('../modules/posthog')
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
fallbackLocale: 'en',
|
||||
legacy: false,
|
||||
locale: siteData.value.lang || 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
})
|
||||
|
||||
app.use(i18n)
|
||||
app.component('ThemedVideo', ThemedVideo)
|
||||
},
|
||||
Layout,
|
||||
} satisfies Theme
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
// eslint-disable-next-line ts/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
export class LRUCache {
|
||||
cache
|
||||
max
|
||||
cache
|
||||
constructor(max = 10) {
|
||||
this.max = max
|
||||
this.cache = new Map()
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
first() {
|
||||
return this.cache.keys().next().value
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const item = this.cache.get(key)
|
||||
if (item !== undefined) {
|
||||
@@ -36,4 +28,12 @@ export class LRUCache {
|
||||
this.cache.delete(this.first())
|
||||
this.cache.set(key, val)
|
||||
}
|
||||
|
||||
first() {
|
||||
return this.cache.keys().next().value
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
export function formatDate(raw: string): {
|
||||
string: string
|
||||
time: number
|
||||
string: string
|
||||
} {
|
||||
const date = new Date(raw)
|
||||
date.setUTCHours(12)
|
||||
|
||||
return {
|
||||
string: date.toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}),
|
||||
time: +date,
|
||||
string: date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,9 +148,9 @@ And connect the `pgvector.rs` instance with Drizzle:
|
||||
|
||||
```typescript
|
||||
export const chatMessagesTable = pgTable('chat_messages', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
content: text().notNull().default(''),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
}, table => [
|
||||
index('chat_messages_content_vector_1024_index').using('hnsw', table.content_vector_1024.op('vector_cosine_ops')),
|
||||
])
|
||||
@@ -260,11 +260,11 @@ import { index, pgTable, serial, text, vector } from 'drizzle-orm/pg-core'
|
||||
export const demoTable = pgTable(
|
||||
'demo',
|
||||
{
|
||||
description: text('description').notNull().default(''),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
title: text('title').notNull().default(''),
|
||||
description: text('description').notNull().default(''),
|
||||
url: text('url').notNull().default(''),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
},
|
||||
table => [
|
||||
index('embeddingIndex').using('hnsw', table.embedding.op('vector_cosine_ops')),
|
||||
@@ -301,14 +301,14 @@ integrations here:
|
||||
let similarity: SQL<number>
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
break
|
||||
case '1024':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1024, embedding.embedding)}))`
|
||||
break
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported embedding dimension: ${env.EMBEDDING_DIMENSION}`)
|
||||
@@ -317,8 +317,8 @@ switch (env.EMBEDDING_DIMENSION) {
|
||||
// Get top messages with similarity above threshold
|
||||
const relevantMessages = await db
|
||||
.select({
|
||||
content: chatMessagesTable.content,
|
||||
id: chatMessagesTable.id,
|
||||
content: chatMessagesTable.content,
|
||||
similarity: sql`${similarity} AS "similarity"`,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
|
||||
@@ -52,11 +52,11 @@ import { invoke } from '@Tauri-apps/api/core'
|
||||
|
||||
export const mcp = [
|
||||
{
|
||||
name: 'list_tools',
|
||||
description: 'List all tools',
|
||||
execute: async () => {
|
||||
return await invoke('list_tools')
|
||||
},
|
||||
name: 'list_tools'
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -157,7 +157,7 @@ Then on the JavaScript side, we can simply pass an object:
|
||||
```javascript
|
||||
import { invoke } from '@Tauri-apps/api/core'
|
||||
|
||||
invoke('call_tool', { args: { duration: 500, x1: 100, x2: 200, y1: 100, y2: 200 }, name: 'input_swipe' })
|
||||
invoke('call_tool', { name: 'input_swipe', args: { x1: 100, y1: 100, x2: 200, y2: 200, duration: 500 } })
|
||||
```
|
||||
|
||||
Super convenient!
|
||||
|
||||
@@ -114,9 +114,9 @@ services:
|
||||
|
||||
```typescript
|
||||
export const chatMessagesTable = pgTable('chat_messages', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
content: text().notNull().default(''),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
}, table => [
|
||||
index('chat_messages_content_vector_1024_index').using('hnsw', table.content_vector_1024.op('vector_cosine_ops')),
|
||||
])
|
||||
@@ -213,11 +213,11 @@ import { index, pgTable, serial, text, vector } from 'drizzle-orm/pg-core'
|
||||
export const demoTable = pgTable(
|
||||
'demo',
|
||||
{
|
||||
description: text('description').notNull().default(''),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
title: text('title').notNull().default(''),
|
||||
description: text('description').notNull().default(''),
|
||||
url: text('url').notNull().default(''),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
},
|
||||
table => [
|
||||
index('embeddingIndex').using('hnsw', table.embedding.op('vector_cosine_ops')),
|
||||
@@ -252,14 +252,14 @@ CREATE INDEX "embeddingIndex" ON "demo" USING hnsw ("embedding" vector_cosine_op
|
||||
let similarity: SQL<number>
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
break
|
||||
case '1024':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1024, embedding.embedding)}))`
|
||||
break
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported embedding dimension: ${env.EMBEDDING_DIMENSION}`)
|
||||
@@ -268,8 +268,8 @@ switch (env.EMBEDDING_DIMENSION) {
|
||||
// 類似度が閾値を超える上位メッセージを取得
|
||||
const relevantMessages = await db
|
||||
.select({
|
||||
content: chatMessagesTable.content,
|
||||
id: chatMessagesTable.id,
|
||||
content: chatMessagesTable.content,
|
||||
similarity: sql`${similarity} AS "similarity"`,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
|
||||
@@ -52,11 +52,11 @@ import { invoke } from '@Tauri-apps/api/core'
|
||||
|
||||
export const mcp = [
|
||||
{
|
||||
name: 'list_tools',
|
||||
description: 'List all tools',
|
||||
execute: async () => {
|
||||
return await invoke('list_tools')
|
||||
},
|
||||
name: 'list_tools'
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -157,7 +157,7 @@ JavaScript 側では、単にオブジェクトを渡すだけです:
|
||||
```javascript
|
||||
import { invoke } from '@Tauri-apps/api/core'
|
||||
|
||||
invoke('call_tool', { args: { duration: 500, x1: 100, x2: 200, y1: 100, y2: 200 }, name: 'input_swipe' })
|
||||
invoke('call_tool', { name: 'input_swipe', args: { x1: 100, y1: 100, x2: 200, y2: 200, duration: 500 } })
|
||||
```
|
||||
|
||||
超便利!
|
||||
|
||||
@@ -143,9 +143,9 @@ Drizzle로 `pgvector.rs` 인스턴스에 연결하면:
|
||||
|
||||
```typescript
|
||||
export const chatMessagesTable = pgTable('chat_messages', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
content: text().notNull().default(''),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
}, table => [
|
||||
index('chat_messages_content_vector_1024_index').using('hnsw', table.content_vector_1024.op('vector_cosine_ops')),
|
||||
])
|
||||
@@ -249,11 +249,11 @@ import { index, pgTable, serial, text, vector } from 'drizzle-orm/pg-core'
|
||||
export const demoTable = pgTable(
|
||||
'demo',
|
||||
{
|
||||
description: text('description').notNull().default(''),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
title: text('title').notNull().default(''),
|
||||
description: text('description').notNull().default(''),
|
||||
url: text('url').notNull().default(''),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
},
|
||||
table => [
|
||||
index('embeddingIndex').using('hnsw', table.embedding.op('vector_cosine_ops')),
|
||||
@@ -288,14 +288,14 @@ CREATE INDEX "embeddingIndex" ON "demo" USING hnsw ("embedding" vector_cosine_op
|
||||
let similarity: SQL<number>
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
break
|
||||
case '1024':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1024, embedding.embedding)}))`
|
||||
break
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported embedding dimension: ${env.EMBEDDING_DIMENSION}`)
|
||||
@@ -304,8 +304,8 @@ switch (env.EMBEDDING_DIMENSION) {
|
||||
// 임계값 이상의 유사도를 가진 상위 메시지를 가져온다
|
||||
const relevantMessages = await db
|
||||
.select({
|
||||
content: chatMessagesTable.content,
|
||||
id: chatMessagesTable.id,
|
||||
content: chatMessagesTable.content,
|
||||
similarity: sql`${similarity} AS "similarity"`,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
|
||||
@@ -54,11 +54,11 @@ import { invoke } from '@Tauri-apps/api/core'
|
||||
|
||||
export const mcp = [
|
||||
{
|
||||
name: 'list_tools',
|
||||
description: 'List all tools',
|
||||
execute: async () => {
|
||||
return await invoke('list_tools')
|
||||
},
|
||||
name: 'list_tools'
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -159,7 +159,7 @@ async fn call_tool(state: State<'_, Mutex<Option<McpClient>>>, name: String, arg
|
||||
```javascript
|
||||
import { invoke } from '@Tauri-apps/api/core'
|
||||
|
||||
invoke('call_tool', { args: { duration: 500, x1: 100, x2: 200, y1: 100, y2: 200 }, name: 'input_swipe' })
|
||||
invoke('call_tool', { name: 'input_swipe', args: { x1: 100, y1: 100, x2: 200, y2: 200, duration: 500 } })
|
||||
```
|
||||
|
||||
정말 편리하네요!
|
||||
|
||||
@@ -114,9 +114,9 @@ services:
|
||||
|
||||
```typescript
|
||||
export const chatMessagesTable = pgTable('chat_messages', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
content: text().notNull().default(''),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
}, table => [
|
||||
index('chat_messages_content_vector_1024_index').using('hnsw', table.content_vector_1024.op('vector_cosine_ops')),
|
||||
])
|
||||
@@ -213,11 +213,11 @@ import { index, pgTable, serial, text, vector } from 'drizzle-orm/pg-core'
|
||||
export const demoTable = pgTable(
|
||||
'demo',
|
||||
{
|
||||
description: text('description').notNull().default(''),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
title: text('title').notNull().default(''),
|
||||
description: text('description').notNull().default(''),
|
||||
url: text('url').notNull().default(''),
|
||||
embedding: vector('embedding', { dimensions: 1536 }),
|
||||
},
|
||||
table => [
|
||||
index('embeddingIndex').using('hnsw', table.embedding.op('vector_cosine_ops')),
|
||||
@@ -252,14 +252,14 @@ CREATE INDEX "embeddingIndex" ON "demo" USING hnsw ("embedding" vector_cosine_op
|
||||
let similarity: SQL<number>
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
break
|
||||
case '1024':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1024, embedding.embedding)}))`
|
||||
break
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported embedding dimension: ${env.EMBEDDING_DIMENSION}`)
|
||||
@@ -268,8 +268,8 @@ switch (env.EMBEDDING_DIMENSION) {
|
||||
// Get top messages with similarity above threshold
|
||||
const relevantMessages = await db
|
||||
.select({
|
||||
content: chatMessagesTable.content,
|
||||
id: chatMessagesTable.id,
|
||||
content: chatMessagesTable.content,
|
||||
similarity: sql`${similarity} AS "similarity"`,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
|
||||
@@ -52,11 +52,11 @@ import { invoke } from '@Tauri-apps/api/core'
|
||||
|
||||
export const mcp = [
|
||||
{
|
||||
name: 'list_tools',
|
||||
description: 'List all tools',
|
||||
execute: async () => {
|
||||
return await invoke('list_tools')
|
||||
},
|
||||
name: 'list_tools'
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -157,7 +157,7 @@ async fn call_tool(state: State<'_, Mutex<Option<McpClient>>>, name: String, arg
|
||||
```javascript
|
||||
import { invoke } from '@Tauri-apps/api/core'
|
||||
|
||||
invoke('call_tool', { args: { duration: 500, x1: 100, x2: 200, y1: 100, y2: 200 }, name: 'input_swipe' })
|
||||
invoke('call_tool', { name: 'input_swipe', args: { x1: 100, y1: 100, x2: 200, y2: 200, duration: 500 } })
|
||||
```
|
||||
|
||||
超方便!
|
||||
|
||||
+11
-11
@@ -7,17 +7,6 @@ import { Transformer } from '@napi-rs/image'
|
||||
|
||||
const SOURCE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp']
|
||||
|
||||
async function main() {
|
||||
const files = process.argv.slice(2).map(f => path.resolve(process.cwd(), f))
|
||||
if (files.length === 0) {
|
||||
console.error('No files provided.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await Promise.all(files.map(f => stat(f)))
|
||||
await Promise.allSettled(files.map(file => transform(file)))
|
||||
}
|
||||
|
||||
async function transform(filePath: string): Promise<any> {
|
||||
if ((await stat(filePath)).isDirectory()) {
|
||||
return await Promise.allSettled(
|
||||
@@ -38,4 +27,15 @@ async function transform(filePath: string): Promise<any> {
|
||||
console.info(`√ ${filePath} -> ${dist}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const files = process.argv.slice(2).map(f => path.resolve(process.cwd(), f))
|
||||
if (files.length === 0) {
|
||||
console.error('No files provided.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await Promise.all(files.map(f => stat(f)))
|
||||
await Promise.allSettled(files.map(file => transform(file)))
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user