diff --git a/packages/plugin-docusaurus-v3/src/index.ts b/packages/plugin-docusaurus-v3/src/index.ts index 20232988f..9df82e81a 100644 --- a/packages/plugin-docusaurus-v3/src/index.ts +++ b/packages/plugin-docusaurus-v3/src/index.ts @@ -1,22 +1,22 @@ -import { readFileSync, writeFileSync } from "node:fs" -import type { Plugin } from "@docusaurus/types" -import { cp } from "node:fs/promises" -import { gzip } from "pako" -import { resolve } from "node:path" +import { readFileSync, writeFileSync } from 'node:fs' +import type { Plugin } from '@docusaurus/types' +import { cp } from 'node:fs/promises' +import { gzip } from 'pako' +import { resolve } from 'node:path' // @ts-ignore -import { presets } from "@orama/searchbox" -import { create, insertMultiple, save } from "@orama/orama" -import { JSDOM } from "jsdom" -import MarkdownIt from "markdown-it" -import matter from "gray-matter" -import { createSnapshot, deployIndex, fetchEndpointConfig } from "./utils" +import { presets } from '@orama/searchbox' +import { create, insertMultiple, save } from '@orama/orama' +import { JSDOM } from 'jsdom' +import MarkdownIt from 'markdown-it' +import matter from 'gray-matter' +import { createSnapshot, deployIndex, fetchEndpointConfig } from './utils' type CloudConfig = { - deploy: boolean, + deploy: boolean endpoint: string - indexId: string, + indexId: string oramaCloudAPIKey?: string - public_api_key: string, + public_api_key: string } type PluginOptions = { @@ -24,44 +24,47 @@ type PluginOptions = { enabled: boolean apiKey: string indexId: string - }, + } cloud?: CloudConfig } -export default function OramaPluginDocusaurus(ctx: { - siteDir: any; - generatedFilesDir: any -}, options: PluginOptions): Plugin { +export default function OramaPluginDocusaurus( + ctx: { + siteDir: any + generatedFilesDir: any + }, + options: PluginOptions +): Plugin { let versions: any[] = [] return { - name: "@orama/plugin-docusaurus-v3", + name: '@orama/plugin-docusaurus-v3', getThemePath() { - return "../lib/theme" + return '../lib/theme' }, getTypeScriptThemePath() { - return "../src/theme" + return '../src/theme' }, getClientModules() { - return ["../lib/theme/SearchBar/index.css"] + return ['../lib/theme/SearchBar/index.css'] }, async allContentLoaded({ actions, allContent }) { - const isDevelopment = process.env.NODE_ENV === "development" + const isDevelopment = process.env.NODE_ENV === 'development' let docsInstances: string[] = [] const oramaCloudAPIKey = options.cloud?.oramaCloudAPIKey const searchDataConfig = [ { - docs: allContent["docusaurus-plugin-content-docs"] + docs: allContent['docusaurus-plugin-content-docs'] }, { - blogs: allContent["docusaurus-plugin-content-blog"] + blogs: allContent['docusaurus-plugin-content-blog'] }, { - pages: allContent["docusaurus-plugin-content-pages"] + pages: allContent['docusaurus-plugin-content-pages'] } ] @@ -75,71 +78,82 @@ export default function OramaPluginDocusaurus(ctx: { searchDataConfig.forEach((config) => { const [key, value] = Object.entries(config)[0] switch (key) { - case "docs": + case 'docs': + if (!value) break Object.keys(value).forEach((docsInstance: any) => { const loadedVersions = value?.[docsInstance]?.loadedVersions versions = loadedVersions.map((v: any) => v.versionName) docsInstances.push(docsInstance) versions.flatMap(async (version) => { const currentVersion = loadedVersions.find((v: any) => v.versionName === version) - allOramaDocsPromises.push(...currentVersion.docs.map((data: any) => generateDocs({ - siteDir:ctx.siteDir, - version, - category: docsInstance, - data - }))) + allOramaDocsPromises.push( + ...currentVersion.docs.map((data: any) => + generateDocs({ + siteDir: ctx.siteDir, + version, + category: docsInstance, + data + }) + ) + ) }) }) break - case "blogs": + case 'blogs': const blogsInstances = Object.keys(value) blogsInstances.forEach(async (instance) => { const loadedInstance = value[instance] - allOramaDocsPromises.push(...loadedInstance.blogPosts.map(({ metadata }: any) => generateDocs({ - siteDir: ctx.siteDir, - version: "current", - category: "blogs", - data: metadata - }))) + allOramaDocsPromises.push( + ...loadedInstance.blogPosts.map(({ metadata }: any) => { + return generateDocs({ + siteDir: ctx.siteDir, + version: 'current', + category: 'blogs', + data: metadata + }) + }) + ) }) break - case "pages": + case 'pages': const pagesInstances = Object.keys(value) pagesInstances.forEach(async (instance) => { const loadedInstance = value[instance] - allOramaDocsPromises.push(...loadedInstance.map((data: any) => generateDocs({ - siteDir: ctx.siteDir, - version: "current", - category: "pages", - data - }))) + allOramaDocsPromises.push( + ...loadedInstance.map((data: any) => + generateDocs({ + siteDir: ctx.siteDir, + version: 'current', + category: 'pages', + data + }) + ) + ) }) break } }) - const oramaDocs = (await Promise.all(allOramaDocsPromises)) - .flat() - .map((data) => ({ - title: data.title, - content: data.content, - section: data.originalTitle, - version: data.version, - path: data.path, - category: data.category - })) - + const oramaDocs = (await Promise.all(allOramaDocsPromises)).flat().map((data) => ({ + title: data.title, + content: data.content, + section: data.originalTitle, + version: data.version, + path: data.path, + category: data.category + })) const endpointConfig = await deployData({ oramaDocs, generatedFilesDir: ctx.generatedFilesDir, - version: "current", + version: 'current', deployConfig }) actions.setGlobalData({ - ...(isDevelopment && !options.cloud && { - searchData: Object.fromEntries([['current', readFileSync(indexPath(ctx.generatedFilesDir, 'current'))]]) - }), + ...(isDevelopment && + !options.cloud && { + searchData: Object.fromEntries([['current', readFileSync(indexPath(ctx.generatedFilesDir, 'current'))]]) + }), docsInstances, availableVersions: versions, analytics: options.analytics, @@ -148,12 +162,12 @@ export default function OramaPluginDocusaurus(ctx: { url: endpointConfig?.endpoint, key: endpointConfig?.public_api_key } - }), + }) }) }, async postBuild({ outDir }) { - !options.cloud && await cp(indexPath(ctx.generatedFilesDir, "current"), indexPath(outDir, "current")) + !options.cloud && (await cp(indexPath(ctx.generatedFilesDir, 'current'), indexPath(outDir, 'current'))) } } } @@ -164,13 +178,13 @@ async function generateDocs({ category, data }: { - siteDir: string, - version: string, - category: string, + siteDir: string + version: string + category: string data: Record }) { const { title, permalink, source } = data - const fileContent = readFileSync(source.replace("@site", siteDir), "utf-8") + const fileContent = readFileSync(source.replace('@site', siteDir), 'utf-8') const contentWithoutFrontMatter = matter(fileContent).content return parseHTMLContent({ @@ -182,41 +196,57 @@ async function generateDocs({ }) } -function parseHTMLContent({ html, path, originalTitle, version, category }: { - html: any; - path: any; - originalTitle: any, - version: string, +function parseHTMLContent({ + html, + path, + originalTitle, + version, + category +}: { + html: any + path: any + originalTitle: any + version: string category: string }) { const dom = new JSDOM(html) const document = dom.window.document - const sections: { - originalTitle: any; - title: string; - header: string; - content: string; - version: string; - category: string; + originalTitle: any + title: string + header: string + content: string + version: string + category: string path: any }[] = [] - const headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6") + const headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6') + if (!headers.length) { + sections.push({ + originalTitle, + title: originalTitle, + header: 'h1', + content: html, + version, + category, + path + }) + } headers.forEach((header) => { const sectionTitle = header.textContent?.trim() const headerTag = header.tagName.toLowerCase() - let sectionContent = "" + let sectionContent = '' let sibling = header.nextElementSibling - while (sibling && !["H1", "H2", "H3", "H4", "H5", "H6"].includes(sibling.tagName)) { - sectionContent += sibling.textContent?.trim() + "\n" + while (sibling && !['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(sibling.tagName)) { + sectionContent += sibling.textContent?.trim() + '\n' sibling = sibling.nextElementSibling } sections.push({ originalTitle, - title: sectionTitle ?? "", + title: sectionTitle ?? '', header: headerTag, content: sectionContent, version, @@ -229,7 +259,7 @@ function parseHTMLContent({ html, path, originalTitle, version, category }: { } function indexPath(outDir: string, version: string) { - return resolve(outDir, "orama-search-index-@VERSION@.json.gz".replace("@VERSION@", version)) + return resolve(outDir, 'orama-search-index-@VERSION@.json.gz'.replace('@VERSION@', version)) } async function deployData({ @@ -238,13 +268,19 @@ async function deployData({ version, deployConfig }: { - oramaDocs: any[], - generatedFilesDir: string, - version: string, - deployConfig: { indexId: string, enabled: boolean, oramaCloudAPIKey: string | undefined } | undefined + oramaDocs: any[] + generatedFilesDir: string + version: string + deployConfig: + | { + indexId: string + enabled: boolean + oramaCloudAPIKey: string | undefined + } + | undefined }) { const { ORAMA_CLOUD_BASE_URL } = process.env - const baseUrl = ORAMA_CLOUD_BASE_URL || "https://cloud.oramasearch.com" + const baseUrl = ORAMA_CLOUD_BASE_URL || 'https://cloud.oramasearch.com' if (deployConfig) { const endpointConfig = await fetchEndpointConfig(baseUrl, deployConfig.oramaCloudAPIKey!, deployConfig.indexId!) @@ -257,7 +293,7 @@ async function deployData({ return endpointConfig } else { const db = await create({ - schema: { ...presets.docs.schema, version: "enum" } + schema: { ...presets.docs.schema, version: 'enum' } }) await insertMultiple(db, oramaDocs as any) diff --git a/packages/plugin-docusaurus-v3/src/theme/SearchBar/index.tsx b/packages/plugin-docusaurus-v3/src/theme/SearchBar/index.tsx index 1a2786cf8..9c20c17e5 100644 --- a/packages/plugin-docusaurus-v3/src/theme/SearchBar/index.tsx +++ b/packages/plugin-docusaurus-v3/src/theme/SearchBar/index.tsx @@ -1,109 +1,53 @@ // @ts-nocheck -import React, { useEffect, useState } from "react" -import useBaseUrl from "@docusaurus/useBaseUrl" -import { useLocation } from "@docusaurus/router" -import useIsBrowser from "@docusaurus/useIsBrowser" -import { useActiveVersion, useVersions } from "@docusaurus/plugin-content-docs/client" -import { useColorMode, useDocsPreferredVersion } from "@docusaurus/theme-common" -import { usePluginData } from "@docusaurus/useGlobalData" -import { ungzip } from "pako" -import { SearchBox, SearchButton, presets } from "@orama/searchbox" -import { OramaClient } from "@oramacloud/client" -import { create, insertMultiple } from "@orama/orama" -import { pluginAnalytics } from "@orama/plugin-analytics" -import "@orama/searchbox/dist/index.css" +import React from 'react' +import { useLocation } from '@docusaurus/router' +import { useActiveVersion, useVersions } from '@docusaurus/plugin-content-docs/client' +import { useDocsPreferredVersion } from '@docusaurus/theme-common' +import { usePluginData } from '@docusaurus/useGlobalData' +import { SearchBox, SearchButton } from '@orama/searchbox' +import { useOrama } from './useOrama' interface PluginData { searchData: { current: { data: ArrayBuffer } | null - }, - endpoint: { url: string, key: string } | null, - analytics: { apiKey: string, indexId: string, enabled: boolean } | null, + } + endpoint: { url: string; key: string } | null + analytics: { apiKey: string; indexId: string; enabled: boolean } | null docsInstances: string[] } -export function OramaSearch() { - const [searchBoxConfig, setSearchBoxConfig] = useState(null) - const { pathname } = useLocation() +export function OramaSearchNoDocs() { + const { searchBoxConfig, colorMode } = useOrama() - const { - searchData, - endpoint, - analytics, - docsInstances - }: PluginData = usePluginData("@orama/plugin-docusaurus-v3") as PluginData - const pluginId = docsInstances.filter((id: string) => pathname.includes(id))[0] || docsInstances[0] - const baseURL = useBaseUrl("orama-search-index-current.json.gz") - const isBrowser = useIsBrowser() - const { colorMode } = useColorMode() + return ( +
+ + {searchBoxConfig && ( + + )} +
+ ) +} + +export function OramaSearchWithDocs({ pluginId }: { pluginId: string }) { const versions = useVersions(pluginId) const activeVersion = useActiveVersion(pluginId) const { preferredVersion } = useDocsPreferredVersion(pluginId) const currentVersion = activeVersion || preferredVersion || versions[0] - - useEffect(() => { - async function loadOrama() { - if (endpoint) { - setSearchBoxConfig({ - oramaInstance: new OramaClient({ - endpoint: endpoint.url, - api_key: endpoint.key - }) - }) - } else { - let buffer - - if (searchData?.current) { - buffer = searchData.current.data - } else { - const searchResponse = await fetch(baseURL) - - if (searchResponse.status === 0) { - throw new Error(`Network error: ${await searchResponse.text()}`) - } else if (searchResponse.status !== 200) { - throw new Error(`HTTP error ${searchResponse.status}: ${await searchResponse.text()}`) - } - - buffer = await searchResponse.arrayBuffer() - } - - const deflated = ungzip(buffer, { to: "string" }) - const parsedDeflated = JSON.parse(deflated) - - const db = await create({ - schema: { ...presets.docs.schema, version: "enum" }, - plugins: [ - ...(analytics ? [ - pluginAnalytics({ - apiKey: analytics.apiKey, - indexId: analytics.indexId, - enabled: analytics.enabled, - }) - ] : []) - ] - }) - - await insertMultiple(db, Object.values(parsedDeflated.docs.docs)) - - setSearchBoxConfig({ - oramaInstance: db - }) - } - } - - if (!isBrowser) { - return - } - - loadOrama().catch((error) => { - console.error("Cannot load search index.", error) - }) - }, [isBrowser]) - + const { searchBoxConfig, colorMode } = useOrama() const searchParams = { ...(currentVersion && { where: { - version: { "eq": currentVersion.name } + version: { eq: currentVersion.name } } }) } @@ -112,17 +56,18 @@ export function OramaSearch() {
{searchBoxConfig && ( - + )}
) } export default function OramaSearchWrapper() { - return + const { pathname } = useLocation() + const { docsInstances }: PluginData = usePluginData('@orama/plugin-docusaurus-v3') as PluginData + const pluginId = docsInstances.filter((id: string) => pathname.includes(id))[0] || docsInstances[0] + if (!pluginId) { + return + } + return } diff --git a/packages/plugin-docusaurus-v3/src/theme/SearchBar/useOrama.ts b/packages/plugin-docusaurus-v3/src/theme/SearchBar/useOrama.ts new file mode 100644 index 000000000..becdea263 --- /dev/null +++ b/packages/plugin-docusaurus-v3/src/theme/SearchBar/useOrama.ts @@ -0,0 +1,83 @@ +// @ts-nocheck +import { useEffect, useState } from 'react' +import useBaseUrl from '@docusaurus/useBaseUrl' +import useIsBrowser from '@docusaurus/useIsBrowser' +import { useColorMode } from '@docusaurus/theme-common' +import { usePluginData } from '@docusaurus/useGlobalData' +import { ungzip } from 'pako' +import { presets } from '@orama/searchbox' +import { OramaClient } from '@oramacloud/client' +import { create, insertMultiple } from '@orama/orama' +import { pluginAnalytics } from '@orama/plugin-analytics' +import '@orama/searchbox/dist/index.css' + +export const useOrama = () => { + const [searchBoxConfig, setSearchBoxConfig] = useState(null) + const { colorMode } = useColorMode() + const { searchData, endpoint, analytics }: PluginData = usePluginData('@orama/plugin-docusaurus-v3') as PluginData + + const baseURL = useBaseUrl('orama-search-index-current.json.gz') + const isBrowser = useIsBrowser() + useEffect(() => { + async function loadOrama() { + if (endpoint) { + setSearchBoxConfig({ + oramaInstance: new OramaClient({ + endpoint: endpoint.url, + api_key: endpoint.key + }) + }) + } else { + let buffer + + if (searchData?.current) { + buffer = searchData.current.data + } else { + const searchResponse = await fetch(baseURL) + + if (searchResponse.status === 0) { + throw new Error(`Network error: ${await searchResponse.text()}`) + } else if (searchResponse.status !== 200) { + throw new Error(`HTTP error ${searchResponse.status}: ${await searchResponse.text()}`) + } + + buffer = await searchResponse.arrayBuffer() + } + + const deflated = ungzip(buffer, { to: 'string' }) + const parsedDeflated = JSON.parse(deflated) + + const db = await create({ + schema: { ...presets.docs.schema, version: 'enum' }, + plugins: [ + ...(analytics + ? [ + pluginAnalytics({ + apiKey: analytics.apiKey, + indexId: analytics.indexId, + enabled: analytics.enabled + }) + ] + : []) + ] + }) + + await insertMultiple(db, Object.values(parsedDeflated.docs.docs)) + + setSearchBoxConfig({ + oramaInstance: db + }) + } + } + + if (!isBrowser) { + return + } + + loadOrama().catch((error) => { + console.error('Cannot load search index.', error) + }) + }, [isBrowser]) + + return { searchBoxConfig, colorMode } +}