mirror of
https://github.com/hugo-fixit/FixIt.git
synced 2026-08-24 07:18:57 +00:00
refactor: restructure TypeScript modules and search config
- Create PublicAPI class to encapsulate module initialization - Move search config types to modules/search/types.ts - Rename ui.ts to global.ts for clarity - Simplify pagefind engine (remove normalizeSortOrder, toObject) - Move pagefind config to top-level in FixItConfig - Add TSDoc comments and @param tags - Update PagefindConfig.sortOrder type to 'asc' | 'desc' - Improve pagefind detection warning message
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<FixItEventMap[K]>
|
||||
}
|
||||
|
||||
type Handler<T> = T extends void
|
||||
? (() => void) | ((event: CustomEvent<void>) => void)
|
||||
: (event: CustomEvent<T>) => void
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+25
-59
@@ -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)
|
||||
|
||||
@@ -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<string, MaskOverlayHandler>()
|
||||
@@ -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
|
||||
|
||||
+10
-14
@@ -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<HTMLElement>('.back-to-top')
|
||||
const $readingProgressBar = document.querySelector<HTMLElement>('.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()
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, unknown> => (value && typeof value === 'object' ? value as Record<string, unknown> : {})
|
||||
|
||||
/** 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 `<mark>` 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<string, any>): 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<string, any>): 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<string, any> = {}
|
||||
@@ -96,7 +72,7 @@ export function createPagefindEngine(searchConfig: Record<string, any>): SearchE
|
||||
return state.availableFilters
|
||||
}
|
||||
try {
|
||||
state.availableFilters = toObject(await pagefind.filters())
|
||||
state.availableFilters = (await pagefind.filters()) as Record<string, any> ?? {}
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('[FixIt] failed to read Pagefind filters:', error)
|
||||
|
||||
@@ -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<string, any>): 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 {
|
||||
|
||||
@@ -15,3 +15,45 @@ export interface SearchEngine {
|
||||
preload?: () => Promise<void>
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<TabContainerChangedDetail> &
|
||||
panel: Element | null
|
||||
}
|
||||
|
||||
type FixItDocumentEventMap = {
|
||||
[K in keyof FixItEventMap]: CustomEvent<FixItEventMap[K]>
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -1,3 +1,3 @@
|
||||
export type * from './config'
|
||||
export type * from './global'
|
||||
export type * from './third-party'
|
||||
export type * from './ui'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -}}
|
||||
|
||||
@@ -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 <publicDir>` 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 -}}
|
||||
|
||||
Reference in New Issue
Block a user