diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 653e74d3..733dec42 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -10,7 +10,7 @@ This document defines the detailed coding standards for the FixIt theme project. - **SCSS variables**: hyphen-separated, semantic (`$global-font-family`, `$code-background-color`) - **CSS custom properties**: prefixed with `fi-` / `--fi-` -### Guidelines +### SCSS Guidelines - Use 2-space indentation - Use CSS variables for theme switching support @@ -48,6 +48,7 @@ export class ExampleModule implements ExampleService { - Import the shared `eventBus` singleton from `core/event-bus` for cross-module communication - Constructor injection for dependencies — no global state access - `window.fixit` exposes a typed public API (`FixItPublicAPI`) for user custom scripts +- Comment style: follow [TSDoc](https://tsdoc.org/) conventions ### Utilities @@ -63,7 +64,7 @@ export class ExampleModule implements ExampleService { - **Translation**: Use `T` function for i18n (`{{ T "header.switchTheme" }}`) - **Whitespace**: Use `{{- -}}` trim markers to control whitespace output -### Guidelines +### Hugo Template Guidelines - Use `partialCached` for expensive partials that don't change per page - Use `.Site.Store` for shared computed values (e.g. fingerprint) diff --git a/CLAUDE.md b/CLAUDE.md index 1281405c..0ffa1ee5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,6 +117,7 @@ Hugo Pipes processes all assets. The key orchestration is in `_partials/base/ass - Constructor injection for dependencies — no global state access - Import the shared `eventBus` singleton from `core/event-bus` — do not create new instances - Pure functions only in `utils/` — no side effects, no DOM state +- Comment style: follow [TSDoc](https://tsdoc.org/) conventions ### Hugo Templates diff --git a/assets/js/core/banner.ts b/assets/js/core/banner.ts index 518ca72e..47363305 100644 --- a/assets/js/core/banner.ts +++ b/assets/js/core/banner.ts @@ -1,5 +1,6 @@ /** * Console banner — prints a styled FixIt version message in the browser console. + * @param version - The FixIt version string to display. */ export function printBanner(version: string) { const color = '#FF735A' diff --git a/assets/js/core/event-bus.ts b/assets/js/core/event-bus.ts index f8bc57ee..85d9e888 100644 --- a/assets/js/core/event-bus.ts +++ b/assets/js/core/event-bus.ts @@ -9,6 +9,11 @@ export interface FixItEventMap { 'fixit:code-tab-sync': { lang: string, source: HTMLElement } } +/** Document event map augmented with FixIt custom events. */ +export type FixItDocumentEventMap = { + [K in keyof FixItEventMap]: CustomEvent +} + type Handler = T extends void ? (() => void) | ((event: CustomEvent) => void) : (event: CustomEvent) => void diff --git a/assets/js/core/public-api.ts b/assets/js/core/public-api.ts new file mode 100644 index 00000000..21f69466 --- /dev/null +++ b/assets/js/core/public-api.ts @@ -0,0 +1,54 @@ +import type { FixItPublicAPI } from './tokens' +import { CodeModule } from '../modules/code' +import { ContentModule } from '../modules/content' +import { CoreModule } from '../modules/core' +import { EncryptionModule } from '../modules/encryption' +import { EventsModule } from '../modules/events' +import { MenuModule } from '../modules/menu' +import { MiscModule } from '../modules/misc' +import { SearchModule } from '../modules/search' +import { ThemeModule } from '../modules/theme' +import { TocModule } from '../modules/toc' +import { eventBus } from './event-bus' + +/** + * Public API facade — typed interface exposed on `window.fixit`. + * + * Initializes and exposes all service modules in dependency order. + */ +export class PublicAPI implements FixItPublicAPI { + readonly core + readonly theme + readonly code + readonly toc + readonly menu + readonly search + readonly enc + readonly misc + readonly content + readonly events + readonly eventBus = eventBus + + constructor() { + // Initialize modules in dependency order + this.core = new CoreModule() + this.theme = new ThemeModule(this.core) + this.code = new CodeModule() + this.toc = new TocModule() + this.menu = new MenuModule(this.core) + this.search = new SearchModule(this.core) + this.enc = new EncryptionModule(this.core) + this.misc = new MiscModule(this.core) + this.content = new ContentModule(this.core, this.code) + this.events = new EventsModule(this.core, this.toc, this.code) + } + + get config() { return this.core.config } + get version() { return this.core.version } + get themeMode() { return this.core.themeMode } + get isDark() { return this.core.isDark } + + setThemeMode(mode: string, persist?: boolean) { + this.theme.setThemeMode(mode, persist) + } +} diff --git a/assets/js/core/tokens.ts b/assets/js/core/tokens.ts index 64e9d63e..3e024570 100644 --- a/assets/js/core/tokens.ts +++ b/assets/js/core/tokens.ts @@ -1,14 +1,13 @@ /** Service interfaces for all FixIt modules. */ import type { FixItConfig, MaskOverlayHandler } from '../types' +import type { TypedEventBus } from './event-bus' // ─── CoreService ─── export interface CoreService { readonly config: FixItConfig + readonly version: string isDark: boolean themeMode: string - disableScrollEvent: boolean - newScrollTop: number - oldScrollTop: number registerMaskOverlay: (name: string, handlers: MaskOverlayHandler) => void openMaskOverlay: (name: string) => void closeMaskOverlay: (name: string, skipSync?: boolean) => void @@ -79,3 +78,25 @@ export interface EventsService { onClickMask: () => void initPrint: () => void } + +// ─── FixItPublicAPI ─── +export interface FixItPublicAPI { + readonly config: FixItConfig + readonly version: string + readonly themeMode: string + readonly isDark: boolean + // Modules + readonly core: CoreService + readonly theme: ThemeService + readonly code: CodeService + readonly toc: TocService + readonly menu: MenuService + readonly search: SearchService + readonly enc: EncryptionService + readonly misc: MiscService + readonly content: ContentService + readonly events: EventsService + readonly eventBus: TypedEventBus + // Methods + setThemeMode: (mode: string, persist?: boolean) => void +} diff --git a/assets/js/main.ts b/assets/js/main.ts index 435483a6..570de1e1 100644 --- a/assets/js/main.ts +++ b/assets/js/main.ts @@ -1,77 +1,43 @@ import { printBanner } from './core/banner' -import { eventBus } from './core/event-bus' -import { CodeModule } from './modules/code' -import { ContentModule } from './modules/content' -import { CoreModule } from './modules/core' -import { EncryptionModule } from './modules/encryption' -import { EventsModule } from './modules/events' -import { MenuModule } from './modules/menu' -import { MiscModule } from './modules/misc' -import { SearchModule } from './modules/search' -import { ThemeModule } from './modules/theme' -import { TocModule } from './modules/toc' +import { PublicAPI } from './core/public-api' /** * FixIt theme entry point — initializes all modules and the window.fixit facade. * * Responsibilities: - * - Instantiate all service modules with direct constructor calls. - * - Build the `window.fixit` facade. - * - Run the init sequence on `DOMContentLoaded` (content, theme, menu, search, etc.). + * - Create PublicAPI which initializes all service modules. + * - Run the init sequence on `DOMContentLoaded`. */ function bootstrap(): void { - const core = new CoreModule() - const theme = new ThemeModule(core) - const code = new CodeModule() - const toc = new TocModule() - const menu = new MenuModule(core) - const search = new SearchModule(core) - const enc = new EncryptionModule(core) - const misc = new MiscModule(core) - const content = new ContentModule(core, code) - const events = new EventsModule(core, toc, search, code) - - // Build window.fixit facade - window.fixit = { - get config() { return core.config }, - get version() { return core.version }, - get themeMode() { return core.themeMode }, - get isDark() { return core.isDark }, - get newScrollTop() { return core.newScrollTop }, - get oldScrollTop() { return core.oldScrollTop }, - setThemeMode: (mode, persist) => theme.setThemeMode(mode, persist), - registerMaskOverlay: (name, handlers) => core.registerMaskOverlay(name, handlers), - toggleMaskOverlay: name => core.toggleMaskOverlay(name), - closeMaskOverlay: (name, skipSync) => core.closeMaskOverlay(name, skipSync), - initContent: target => content.initContent(target), - eventBus, - } + // Build window.fixit facade with all modules + window.fixit = new PublicAPI() function init() { try { - toc.setup() - content.setup() - enc.initFixItDecryptor() - theme.initThemeColor() - content.initSVGIcon() - menu.initMenu() - theme.initSwitchTheme() - search.initSearch() - misc.initSiteTime() - misc.initServiceWorker() - misc.initAutoMark() - misc.initReward() - misc.initPostChatUser() - misc.initComment() - events.onScroll() - events.onResize() - events.onClickMask() - events.initPrint() + window.fixit.toc.setup() + window.fixit.content.setup() + window.fixit.enc.initFixItDecryptor() + window.fixit.theme.initThemeColor() + window.fixit.content.initSVGIcon() + window.fixit.menu.initMenu() + window.fixit.theme.initSwitchTheme() + window.fixit.search.initSearch() + window.fixit.misc.initSiteTime() + window.fixit.misc.initServiceWorker() + window.fixit.misc.initAutoMark() + window.fixit.misc.initReward() + window.fixit.misc.initPostChatUser() + window.fixit.misc.initComment() + window.fixit.content.initContent() + window.fixit.events.onScroll() + window.fixit.events.onResize() + window.fixit.events.onClickMask() + window.fixit.events.initPrint() } catch (err) { console.error(err) } - printBanner(core.version) + printBanner(window.fixit.version) } document.addEventListener('DOMContentLoaded', init, false) diff --git a/assets/js/modules/core.ts b/assets/js/modules/core.ts index 4d4b2333..a811ac68 100644 --- a/assets/js/modules/core.ts +++ b/assets/js/modules/core.ts @@ -1,6 +1,6 @@ import type { CoreService } from '../core/tokens' import type { FixItConfig, MaskOverlayHandler } from '../types' -import { getScrollTop, getThemeMode, isDarkMode } from '../utils' +import { getThemeMode, isDarkMode } from '../utils' /** * Core module — shared state initialization and mask overlay management. @@ -15,9 +15,6 @@ export class CoreModule implements CoreService { readonly version: string themeMode: string isDark: boolean - newScrollTop: number - oldScrollTop: number - disableScrollEvent: boolean private activeMaskOverlay: string | null = null private readonly maskOverlays = new Map() @@ -27,9 +24,6 @@ export class CoreModule implements CoreService { this.version = this.config.version this.themeMode = getThemeMode() this.isDark = isDarkMode() - this.newScrollTop = getScrollTop() - this.oldScrollTop = this.newScrollTop - this.disableScrollEvent = false window.objectFitImages?.() } @@ -46,7 +40,6 @@ export class CoreModule implements CoreService { /** Open a named mask overlay, closing any previously active one. */ openMaskOverlay(name: string) { - this.disableScrollEvent = true const overlay = this.maskOverlays.get(name) if (!overlay) return @@ -60,7 +53,6 @@ export class CoreModule implements CoreService { /** Close a named mask overlay and optionally skip mask state sync. */ closeMaskOverlay(name: string, skipSync = false) { - this.disableScrollEvent = false const overlay = this.maskOverlays.get(name) if (!overlay) return diff --git a/assets/js/modules/events.ts b/assets/js/modules/events.ts index 80631c00..bfb9a306 100644 --- a/assets/js/modules/events.ts +++ b/assets/js/modules/events.ts @@ -1,4 +1,4 @@ -import type { CodeService, CoreService, EventsService, SearchService, TocService } from '../core/tokens' +import type { CodeService, CoreService, EventsService, TocService } from '../core/tokens' import { eventBus } from '../core/event-bus' import { animateCSS, getScrollTop, isMobile, scrollIntoView } from '../utils' @@ -13,17 +13,18 @@ import { animateCSS, getScrollTop, isMobile, scrollIntoView } from '../utils' */ export class EventsModule implements EventsService { #resizeTimeout: number | null = null + #newScrollTop = 0 + #oldScrollTop = 0 constructor( private readonly core: CoreService, private readonly toc: TocService, - private readonly search: SearchService, private readonly code: CodeService, ) {} /** Bind scroll listener: auto-hide headers, reading progress, back-to-top, and TOC sync. */ onScroll() { - const ACCURACY = 20 + const ACCURACY = 50 const $autoHeaders: HTMLElement[] = [] const $backToTop = document.querySelector('.back-to-top') const $readingProgressBar = document.querySelector('.reading-progress-bar') @@ -36,13 +37,9 @@ export class EventsModule implements EventsService { $backToTop?.addEventListener('click', () => { scrollIntoView('body') }) - window.addEventListener('scroll', (event) => { - if (this.core.disableScrollEvent) { - event.preventDefault() - return - } - this.core.newScrollTop = getScrollTop() - const scroll = this.core.newScrollTop - this.core.oldScrollTop + window.addEventListener('scroll', () => { + this.#newScrollTop = getScrollTop() + const scroll = this.#newScrollTop - this.#oldScrollTop if (Math.abs(scroll) > ACCURACY) { this.core.closeActiveMaskOverlay() const isScrollingDown = scroll > 0 @@ -57,14 +54,14 @@ export class EventsModule implements EventsService { } }) } - else if (this.core.newScrollTop <= 0) { + else if (this.#newScrollTop <= 0) { $autoHeaders.forEach(($header) => { $header.classList.remove('header__fadeOutUp') animateCSS($header, ['header__fadeInDown'], true) }) } const contentHeight = document.body.scrollHeight - window.innerHeight - const scrollPercent = Math.max(Math.min(100 * Math.max(this.core.newScrollTop, 0) / contentHeight, 100), 0) + const scrollPercent = Math.max(Math.min(100 * Math.max(this.#newScrollTop, 0) / contentHeight, 100), 0) if ($readingProgressBar) { $readingProgressBar.style.setProperty('--fi-progress', `${scrollPercent.toFixed(2)}%`) } @@ -88,7 +85,7 @@ export class EventsModule implements EventsService { eventBus.emit('fixit:scroll') this.toc.syncTocHeight() this.toc.syncTocActiveState() - this.core.oldScrollTop = this.core.newScrollTop + this.#oldScrollTop = this.#newScrollTop }, false) } @@ -101,7 +98,6 @@ export class EventsModule implements EventsService { this.#resizeTimeout = null eventBus.emit('fixit:resize') this.toc.initToc() - this.search.initSearch() this.toc.syncTocHeight() this.toc.syncTocActiveState() diff --git a/assets/js/modules/search/engines/algolia.ts b/assets/js/modules/search/engines/algolia.ts index 266f99e7..1f0dc751 100644 --- a/assets/js/modules/search/engines/algolia.ts +++ b/assets/js/modules/search/engines/algolia.ts @@ -1,10 +1,11 @@ -import type { SearchConfig } from '../../../types/config' -import type { SearchEngine, SearchResult } from '../types' +import type { SearchConfig, SearchEngine, SearchResult } from '../types' /** * Create an Algolia search engine instance. * * Lazily initializes the Algolia v5 lite client on first search. + * @param searchConfig - The search configuration containing Algolia credentials. + * @returns A SearchEngine instance for Algolia. */ export function createAlgoliaEngine(searchConfig: SearchConfig): SearchEngine { if (!searchConfig.algoliaAppID || !searchConfig.algoliaSearchKey || !searchConfig.algoliaIndex) { diff --git a/assets/js/modules/search/engines/cse.ts b/assets/js/modules/search/engines/cse.ts index 20c53e94..17313bbb 100644 --- a/assets/js/modules/search/engines/cse.ts +++ b/assets/js/modules/search/engines/cse.ts @@ -1,10 +1,11 @@ -import type { CSEConfig } from '../../../types/config' -import type { SearchEngine, SearchResult } from '../types' +import type { CSEConfig, SearchEngine, SearchResult } from '../types' /** * Create a Google Custom Search Engine adapter. * * Returns a single result that links to the CSE results page. + * @param cseConfig - The CSE configuration, or `undefined` if not configured. + * @returns A SearchEngine instance for Google CSE. */ export function createCSEEngine(cseConfig: CSEConfig | undefined): SearchEngine { return { diff --git a/assets/js/modules/search/engines/fuse.ts b/assets/js/modules/search/engines/fuse.ts index 3143bb3a..c8ad8054 100644 --- a/assets/js/modules/search/engines/fuse.ts +++ b/assets/js/modules/search/engines/fuse.ts @@ -1,11 +1,12 @@ -import type { SearchConfig } from '../../../types/config' -import type { SearchEngine, SearchResult } from '../types' +import type { SearchConfig, SearchEngine, SearchResult } from '../types' import { applyHighlightToText } from '../../../utils' /** * Create a Fuse.js search engine instance. * * Lazily fetches and indexes the search JSON on first search. + * @param searchConfig - The search configuration containing Fuse.js options. + * @returns A SearchEngine instance for Fuse.js. */ export function createFuseEngine(searchConfig: SearchConfig): SearchEngine { return { diff --git a/assets/js/modules/search/engines/pagefind.ts b/assets/js/modules/search/engines/pagefind.ts index 66aba1e4..cec58781 100644 --- a/assets/js/modules/search/engines/pagefind.ts +++ b/assets/js/modules/search/engines/pagefind.ts @@ -1,30 +1,4 @@ -import type { SearchEngine, SearchResult } from '../types' - -/** Matches absolute URLs (e.g. "https://..." or "//...") */ -const ABSOLUTE_URL_RE = /^(?:[a-z]+:)?\/\//i - -/** - * Normalize a Pagefind bundle path to a full URL. - * Relative paths are resolved against the given baseURL or document.baseURI. - */ -function normalizeBundlePath(path: string, baseURL?: string): string { - let bundlePath = typeof path === 'string' && path.length > 0 ? path : 'pagefind/' - if (!bundlePath.endsWith('/')) { - bundlePath = `${bundlePath}/` - } - if (ABSOLUTE_URL_RE.test(bundlePath)) { - return bundlePath - } - return new URL(bundlePath, baseURL || document.baseURI).toString() -} - -/** Safely cast a value to a plain object; returns `{}` for non-objects. */ -const toObject = (value: unknown): Record => (value && typeof value === 'object' ? value as Record : {}) - -/** Normalize sort order to 'asc' or 'desc', defaulting to 'desc'. */ -function normalizeSortOrder(value: unknown): 'asc' | 'desc' { - return String(value).toLowerCase() === 'asc' ? 'asc' : 'desc' -} +import type { PagefindConfig, SearchConfig, SearchEngine, SearchResult } from '../types' /** Replace `` tags in Pagefind excerpts with the configured highlight tag. */ function replaceExcerptHighlightTag(excerpt: string, highlightTag: string): string { @@ -40,15 +14,17 @@ function replaceExcerptHighlightTag(excerpt: string, highlightTag: string): stri * Create a Pagefind search engine with lazy-loading and built-in filters. * * Wraps the Pagefind library to conform to the `SearchEngine` interface. + * @param searchConfig - The search configuration. + * @param pagefindConfig - The Pagefind engine configuration. + * @returns A SearchEngine instance for Pagefind. */ -export function createPagefindEngine(searchConfig: Record): SearchEngine { - const pagefindConfig = toObject(searchConfig.pagefind) - const bundlePath = normalizeBundlePath(pagefindConfig.bundlePath as string, pagefindConfig.baseURL as string) +export function createPagefindEngine(searchConfig: SearchConfig, pagefindConfig: PagefindConfig): SearchEngine { + const bundlePath = pagefindConfig.bundlePath || 'pagefind/' const rawDebounceTimeout = Number(pagefindConfig.debounceTimeoutMs ?? 300) const debounceTimeout = Number.isFinite(rawDebounceTimeout) ? Math.max(0, rawDebounceTimeout) : 300 const builtInFiltersEnabled = pagefindConfig.useBuiltInFilters !== false - const sortBy = typeof pagefindConfig.sortBy === 'string' ? (pagefindConfig.sortBy as string).trim() : '' - const sortOrder = normalizeSortOrder(pagefindConfig.sortOrder) + const sortBy = pagefindConfig.sortBy + const sortOrder = pagefindConfig.sortOrder ?? 'desc' const highlightTag = searchConfig.highlightTag ?? 'em' const excerptLength = Number(searchConfig.snippetLength ?? 30) @@ -64,7 +40,7 @@ export function createPagefindEngine(searchConfig: Record): SearchE const ensurePagefind = async () => { if (!state.loading) { - state.loading = import(/* @vite-ignore */ `${bundlePath}pagefind.js`) + state.loading = import(`${bundlePath}pagefind.js`) .then(async (mod: any) => { if (!state.initialized) { const options: Record = {} @@ -96,7 +72,7 @@ export function createPagefindEngine(searchConfig: Record): SearchE return state.availableFilters } try { - state.availableFilters = toObject(await pagefind.filters()) + state.availableFilters = (await pagefind.filters()) as Record ?? {} } catch (error) { console.warn('[FixIt] failed to read Pagefind filters:', error) diff --git a/assets/js/modules/search/index.ts b/assets/js/modules/search/index.ts index 911d4967..7150c894 100644 --- a/assets/js/modules/search/index.ts +++ b/assets/js/modules/search/index.ts @@ -1,6 +1,5 @@ import type { CoreService, SearchService } from '../../core/tokens' -import type { SearchEngine, SearchResult } from './types' - +import type { SearchConfig, SearchEngine, SearchResult } from './types' import { createAlgoliaEngine } from './engines/algolia' import { createCSEEngine } from './engines/cse' import { createFuseEngine } from './engines/fuse' @@ -26,12 +25,12 @@ export class SearchModule implements SearchService { /** Initialize the search overlay, autocomplete, and engine-specific logic. */ initSearch() { const searchConfig = this.core.config.search - if (!searchConfig) + if (!searchConfig || !searchConfig.type) return // Initialize engine once if (!this.#engine) { - this.#engine = this.#createEngine(searchConfig.type!, searchConfig) + this.#engine = this.#createEngine(searchConfig.type, searchConfig) } // Initialize dialog and autocomplete once @@ -44,7 +43,7 @@ export class SearchModule implements SearchService { } /** Create the appropriate search engine based on type. */ - #createEngine(type: string, searchConfig: Record): SearchEngine { + #createEngine(type: string, searchConfig: SearchConfig): SearchEngine { switch (type) { case 'algolia': return createAlgoliaEngine(searchConfig) @@ -53,7 +52,7 @@ export class SearchModule implements SearchService { case 'cse': return createCSEEngine(this.core.config.cse) case 'pagefind': - return createPagefindEngine(searchConfig) + return createPagefindEngine(searchConfig, this.core.config.pagefind!) default: console.warn(`[FixIt] Unknown search type: "${type}". Supported types: algolia, fuse, cse, pagefind.`) return { diff --git a/assets/js/modules/search/types.ts b/assets/js/modules/search/types.ts index c9cc892e..6e9702a9 100644 --- a/assets/js/modules/search/types.ts +++ b/assets/js/modules/search/types.ts @@ -15,3 +15,45 @@ export interface SearchEngine { preload?: () => Promise destroy?: () => void } + +/** Search configuration */ +export interface SearchConfig { + type?: string + placeholder?: string + maxResultLength?: number + snippetLength?: number + highlightTag?: string + isCaseSensitive?: boolean + minMatchCharLength?: number + findAllMatches?: boolean + location?: number + threshold?: number + distance?: number + ignoreLocation?: boolean + useExtendedSearch?: boolean + ignoreFieldNorm?: boolean + fuseIndexURL?: string + algoliaAppID?: string + algoliaSearchKey?: string + algoliaIndex?: string + noResultsFound?: string + clearText?: string +} + +/** Pagefind engine configuration */ +export interface PagefindConfig { + bundlePath?: string + debounceTimeoutMs?: number + useBuiltInFilters?: boolean + sortBy?: 'date' + sortOrder?: 'asc' | 'desc' +} + +/** Google Custom Search Engine configuration */ +export interface CSEConfig { + engine?: string + cx?: string + resultsPage?: string + searchIn?: string + gotoResultsPage?: string +} diff --git a/assets/js/types/config.ts b/assets/js/types/config.ts index 2fa479c2..01c283fd 100644 --- a/assets/js/types/config.ts +++ b/assets/js/types/config.ts @@ -1,3 +1,4 @@ +import type { CSEConfig, PagefindConfig, SearchConfig } from '../modules/search/types' import type { MermaidConfig } from './third-party' /** Mask overlay handler */ @@ -13,6 +14,7 @@ export interface FixItConfig { twemoji?: boolean search?: SearchConfig cse?: CSEConfig + pagefind?: PagefindConfig echarts?: EchartsConfig mapbox?: MapboxConfig typeit?: TypeItConfig @@ -31,47 +33,6 @@ export interface FixItConfig { print?: PrintConfig } -export interface SearchConfig { - type?: string - placeholder?: string - maxResultLength?: number - snippetLength?: number - highlightTag?: string - isCaseSensitive?: boolean - minMatchCharLength?: number - findAllMatches?: boolean - location?: number - threshold?: number - distance?: number - ignoreLocation?: boolean - useExtendedSearch?: boolean - ignoreFieldNorm?: boolean - fuseIndexURL?: string - algoliaAppID?: string - algoliaSearchKey?: string - algoliaIndex?: string - noResultsFound?: string - clearText?: string - pagefind?: PagefindConfig -} - -export interface PagefindConfig { - bundlePath?: string - baseURL?: string - debounceTimeoutMs?: number - useBuiltInFilters?: boolean - sortBy?: string - sortOrder?: string -} - -export interface CSEConfig { - engine?: string - cx?: string - resultsPage?: string - searchIn?: string - gotoResultsPage?: string -} - export interface EchartsConfig { lightTheme?: object darkTheme?: object diff --git a/assets/js/types/ui.ts b/assets/js/types/global.ts similarity index 64% rename from assets/js/types/ui.ts rename to assets/js/types/global.ts index 15548db7..895c24d6 100644 --- a/assets/js/types/ui.ts +++ b/assets/js/types/global.ts @@ -1,5 +1,6 @@ -import type { FixItEventMap, TypedEventBus } from '../core/event-bus' -import type { FixItConfig, MaskOverlayHandler } from './config' +import type { FixItDocumentEventMap } from '../core/event-bus' +import type { FixItPublicAPI } from '../core/tokens' +import type { FixItConfig } from './config' import type { MermaidRuntimeModule, PanzoomInstance } from './third-party' export interface TabContainerChangedDetail { @@ -10,26 +11,6 @@ export type TabContainerChangedEvent = CustomEvent & panel: Element | null } -type FixItDocumentEventMap = { - [K in keyof FixItEventMap]: CustomEvent -} - -/** Public API exposed on window.fixit. */ -export interface FixItPublicAPI { - readonly config: FixItConfig - readonly version: string - readonly themeMode: string - readonly isDark: boolean - readonly newScrollTop: number - readonly oldScrollTop: number - setThemeMode: (mode: string, persist?: boolean) => void - registerMaskOverlay: (name: string, handlers: MaskOverlayHandler) => void - toggleMaskOverlay: (name: string) => void - closeMaskOverlay: (name: string, skipSync?: boolean) => void - initContent: (target?: Element | Document) => void - eventBus: TypedEventBus -} - declare global { interface DocumentEventMap extends FixItDocumentEventMap { 'tab-container-changed': TabContainerChangedEvent diff --git a/assets/js/types/index.ts b/assets/js/types/index.ts index 949057ed..ade08100 100644 --- a/assets/js/types/index.ts +++ b/assets/js/types/index.ts @@ -1,3 +1,3 @@ export type * from './config' +export type * from './global' export type * from './third-party' -export type * from './ui' diff --git a/hugo.toml b/hugo.toml index befa9ea3..65a802a5 100644 --- a/hugo.toml +++ b/hugo.toml @@ -507,7 +507,7 @@ dark = "#151b23" # Search config [params.search] enable = true -# type of search engine ["algolia", "fuse", "pagefind", "cse"] +# type of search engine ["fuse", "algolia", "pagefind", "cse"] type = "fuse" # max index length of the chunked content contentLength = 4000 diff --git a/layouts/_partials/base/assets.html b/layouts/_partials/base/assets.html index 63df1efe..862f3e22 100644 --- a/layouts/_partials/base/assets.html +++ b/layouts/_partials/base/assets.html @@ -45,18 +45,20 @@ {{- dict "Source" "js/lib/fuse.ts" "Build" true "Fingerprint" $fingerprint "Defer" true | dict "Page" . "Data" | partial "store/script.html" -}} {{- $config = dict "isCaseSensitive" $search.fuse.isCaseSensitive "minMatchCharLength" $search.fuse.minMatchCharLength "findAllMatches" $search.fuse.findAllMatches "location" $search.fuse.location "threshold" $search.fuse.threshold "distance" $search.fuse.distance "ignoreLocation" $search.fuse.ignoreLocation "useExtendedSearch" $search.fuse.useExtendedSearch "ignoreFieldNorm" $search.fuse.ignoreFieldNorm | dict "search" | merge $config -}} {{- else if eq $search.type "pagefind" -}} + {{- $config = dict "type" "pagefind" | dict "search" | merge $config -}} {{- $pagefind := $search.pagefind | default dict -}} + {{- $bundlePath := relURL ($pagefind.bundlePath | default "pagefind/") -}} + {{- if not (hasSuffix $bundlePath "/") -}} + {{- $bundlePath = printf "%v/" $bundlePath -}} + {{- end -}} {{- $config = dict - "type" "pagefind" - "pagefind" (dict - "bundlePath" ($pagefind.bundlePath | default "pagefind/") - "baseURL" .Site.BaseURL - "debounceTimeoutMs" ($pagefind.debounceTimeoutMs | default 300) - "useBuiltInFilters" ($pagefind.useBuiltInFilters | default true) - "sortBy" ($pagefind.sortBy | default "") - "sortOrder" ($pagefind.sortOrder | default "desc") - ) - | dict "search" | merge $config -}} + "bundlePath" $bundlePath + "debounceTimeoutMs" ($pagefind.debounceTimeoutMs | default 300) + "useBuiltInFilters" ($pagefind.useBuiltInFilters | default true) + "sortBy" ($pagefind.sortBy | default "") + "sortOrder" ($pagefind.sortOrder | default "desc") + | dict "pagefind" | merge $config + -}} {{- else if eq $search.type "cse" -}} {{- $config = dict "type" "cse" | dict "search" | merge $config -}} {{- $cse := .Site.Params.cse -}} diff --git a/layouts/_partials/init/detection-pagefind.html b/layouts/_partials/init/detection-pagefind.html index bdb5566c..c8a6cd21 100644 --- a/layouts/_partials/init/detection-pagefind.html +++ b/layouts/_partials/init/detection-pagefind.html @@ -1,3 +1,3 @@ -{{- if and (eq .Site hugo.Sites.Default) .Site.Params.search.enable (eq .Site.Params.search.type "pagefind") -}} - {{- warnf "FixIt Pagefind search enabled\nRun `npx pagefind --site ` after site build to create the search index.\n\n" -}} +{{- if and (eq .Site hugo.Sites.Default) .Site.Params.search.enable (eq .Site.Params.search.type "pagefind") (not (fileExists "public/pagefind")) -}} + {{- warnf "[FixIt] Pagefind search index not found.\nRun `npx pagefind --site public` in the site root directory after building.\n" -}} {{- end -}}