diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/current.json b/i18n/zh-CN/docusaurus-plugin-content-docs/current.json index e9287466d1002c..08715a162ae394 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/current.json +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/current.json @@ -23,6 +23,14 @@ "message": "版本发布", "description": "The label for category Releases in sidebar docs" }, + "sidebar.docs.category.Tutorials": { + "message": "使用教程", + "description": "The label for category Tutorials in sidebar docs" + }, + "sidebar.docs.category.Building lakehouse": { + "message": "构建 lakehouse", + "description": "The label for category Building lakehouse in sidebar docs" + }, "sidebar.docs.category.Quick Start": { "message": "快速体验", "description": "The label for category Quick Start in sidebar docs" diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/version-1.2.json b/i18n/zh-CN/docusaurus-plugin-content-docs/version-1.2.json index 4986b459b704bb..682456cb51b071 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/version-1.2.json +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/version-1.2.json @@ -27,6 +27,14 @@ "message": "Doris 介绍", "description": "The label for category Doris Introduction in sidebar docs" }, + "sidebar.docs.category.Tutorials": { + "message": "使用教程", + "description": "The label for category Tutorials in sidebar docs" + }, + "sidebar.docs.category.Building lakehouse": { + "message": "构建 lakehouse", + "description": "The label for category Building lakehouse in sidebar docs" + }, "sidebar.docs.category.Install And Deploy": { "message": "安装部署", "description": "The label for category Install And Deploy in sidebar docs" diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/version-2.0.json b/i18n/zh-CN/docusaurus-plugin-content-docs/version-2.0.json index cbd360115929b0..94048c4dff25dd 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/version-2.0.json +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/version-2.0.json @@ -27,6 +27,14 @@ "message": "快速体验", "description": "The label for category Quick Start in sidebar docs" }, + "sidebar.docs.category.Tutorials": { + "message": "使用教程", + "description": "The label for category Tutorials in sidebar docs" + }, + "sidebar.docs.category.Building lakehouse": { + "message": "构建 lakehouse", + "description": "The label for category Building lakehouse in sidebar docs" + }, "sidebar.docs.category.Installation and Deployment": { "message": "安装部署", "description": "The label for category Installation and Deployment in sidebar docs" diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/version-2.1.json b/i18n/zh-CN/docusaurus-plugin-content-docs/version-2.1.json index 247ea98c9eda43..fb787a4de597b1 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/version-2.1.json +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/version-2.1.json @@ -27,6 +27,14 @@ "message": "快速体验", "description": "The label for category Quick Start in sidebar docs" }, + "sidebar.docs.category.Tutorials": { + "message": "使用教程", + "description": "The label for category Tutorials in sidebar docs" + }, + "sidebar.docs.category.Building lakehouse": { + "message": "构建 lakehouse", + "description": "The label for category Building lakehouse in sidebar docs" + }, "sidebar.docs.category.Installation and Deployment": { "message": "安装部署", "description": "The label for category Install and Deploy in sidebar docs" diff --git a/i18n/zh-CN/docusaurus-plugin-content-docs/version-3.0.json b/i18n/zh-CN/docusaurus-plugin-content-docs/version-3.0.json index 631798c3d4e3ec..678f65519d4412 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-docs/version-3.0.json +++ b/i18n/zh-CN/docusaurus-plugin-content-docs/version-3.0.json @@ -27,6 +27,14 @@ "message": "快速体验", "description": "The label for category Quick Start in sidebar docs" }, + "sidebar.docs.category.Tutorials": { + "message": "使用教程", + "description": "The label for category Tutorials in sidebar docs" + }, + "sidebar.docs.category.Building lakehouse": { + "message": "构建 lakehouse", + "description": "The label for category Building lakehouse in sidebar docs" + }, "sidebar.docs.category.Installation and Deployment": { "message": "安装部署", "description": "The label for category Installation and Deployment in sidebar docs" diff --git a/shared/interfaces.js b/shared/interfaces.js new file mode 100644 index 00000000000000..f4037df8f9d8d6 --- /dev/null +++ b/shared/interfaces.js @@ -0,0 +1,8 @@ +export var SearchDocumentType; +(function (SearchDocumentType) { + SearchDocumentType[SearchDocumentType["Title"] = 0] = "Title"; + SearchDocumentType[SearchDocumentType["Heading"] = 1] = "Heading"; + SearchDocumentType[SearchDocumentType["Description"] = 2] = "Description"; + SearchDocumentType[SearchDocumentType["Keywords"] = 3] = "Keywords"; + SearchDocumentType[SearchDocumentType["Content"] = 4] = "Content"; +})(SearchDocumentType || (SearchDocumentType = {})); diff --git a/src/constant/common.tsx b/src/constant/common.tsx index 4d8c292309d840..b1f71a6bb9a282 100644 --- a/src/constant/common.tsx +++ b/src/constant/common.tsx @@ -18,3 +18,6 @@ export const BLOG_TAG_ICONS = { 'Top News': , All: , }; + +export const VERSIONS = ['1.2', '2.0', '2.1', '3.0', 'dev']; +export const DEFAULT_VERSION = '2.1'; \ No newline at end of file diff --git a/src/scss/components/markdown.scss b/src/scss/components/markdown.scss index 1945ab4484d98c..f9e54982293a62 100644 --- a/src/scss/components/markdown.scss +++ b/src/scss/components/markdown.scss @@ -113,6 +113,7 @@ .theme-admonition { padding: calc(var(--custom-leading) * 2); + box-shadow: none; } h2 { diff --git a/src/scss/components/navbar.scss b/src/scss/components/navbar.scss index a9700af9bde207..0dc57d431e6431 100644 --- a/src/scss/components/navbar.scss +++ b/src/scss/components/navbar.scss @@ -30,7 +30,6 @@ display: flex !important; align-items: center; justify-content: center; - pointer-events: none; padding: 5px 7px; background-color: #fff; border: none; @@ -74,8 +73,6 @@ outline: none !important; } .navbar__search span span { - max-height: 580px !important; - overflow: scroll !important; top: 2.5rem !important; } } diff --git a/src/scss/components/toc.scss b/src/scss/components/toc.scss index 3ada29d42e5312..54f3b7206854d6 100644 --- a/src/scss/components/toc.scss +++ b/src/scss/components/toc.scss @@ -3,6 +3,7 @@ .toc-icon-content { display: flex; margin-bottom: 0.5rem; + align-items: center; cursor: pointer; } diff --git a/src/shared/interfaces.js b/src/shared/interfaces.js new file mode 100644 index 00000000000000..f4037df8f9d8d6 --- /dev/null +++ b/src/shared/interfaces.js @@ -0,0 +1,8 @@ +export var SearchDocumentType; +(function (SearchDocumentType) { + SearchDocumentType[SearchDocumentType["Title"] = 0] = "Title"; + SearchDocumentType[SearchDocumentType["Heading"] = 1] = "Heading"; + SearchDocumentType[SearchDocumentType["Description"] = 2] = "Description"; + SearchDocumentType[SearchDocumentType["Keywords"] = 3] = "Keywords"; + SearchDocumentType[SearchDocumentType["Content"] = 4] = "Content"; +})(SearchDocumentType || (SearchDocumentType = {})); diff --git a/src/theme/Admonition/Layout/styles.module.css b/src/theme/Admonition/Layout/styles.module.css index 88df7e639be32a..3f5fe3be2859bf 100644 --- a/src/theme/Admonition/Layout/styles.module.css +++ b/src/theme/Admonition/Layout/styles.module.css @@ -3,9 +3,12 @@ } .admonitionHeading { - font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / - var(--ifm-heading-line-height) var(--ifm-heading-font-family); + font: var(--ifm-heading-font-weight) var(--ifm-h5-font-size) / var(--ifm-heading-line-height) var(--ifm-heading-font-family); text-transform: uppercase; + font-size: 1rem; + color: #00000A; + margin-bottom: 0.75rem !important; + font-weight: 600; } /* Heading alone without content (does not handle fragment content) */ @@ -30,6 +33,6 @@ fill: var(--ifm-alert-foreground-color); } -.admonitionContent > :last-child { +.admonitionContent> :last-child { margin-bottom: 0; -} +} \ No newline at end of file diff --git a/src/theme/DocSidebarItem/Category/style.scss b/src/theme/DocSidebarItem/Category/style.scss index c642c4779f41fd..2398c4329954d5 100644 --- a/src/theme/DocSidebarItem/Category/style.scss +++ b/src/theme/DocSidebarItem/Category/style.scss @@ -58,7 +58,7 @@ .divider { width: calc(100% - 0.625rem); - border: 0.5px solid #edf2fa; + border: 0.5px solid #DFE5F0; margin: 1rem 0 1rem 0; position: relative; left: 0.625rem; diff --git a/src/theme/Footer/index.tsx b/src/theme/Footer/index.tsx index 3ce1831341e5ca..097a13a2446643 100644 --- a/src/theme/Footer/index.tsx +++ b/src/theme/Footer/index.tsx @@ -25,11 +25,14 @@ function Footer(): JSX.Element | null { } const { copyright, links, logo, style } = footer; - const [isDocsPage, setIsDocsPage] = useState(false); + const [isDocsPage, setIsDocsPage] = useState(false); // docs page or community page useEffect(() => { if (typeof window !== 'undefined') { const pathname = location.pathname.split('/')[1]; - const docsPage = pathname === 'docs' || location.pathname.includes('zh-CN/docs'); + const docsPage = + pathname === 'docs' || + location.pathname.includes('zh-CN/docs') || + location.pathname.includes('community'); setIsDocsPage(docsPage); } }, [typeof window !== 'undefined' && location.pathname]); @@ -39,30 +42,13 @@ function Footer(): JSX.Element | null { if (isDocsPage) { return (
-
- -
-
-
- {/* border-b border-[#F7F9FE] */} -
-
RESOURCES
- {ResourcesItems.map(({ label, href }) => ( - - {label} - - ))} -
-
-
COMMUNITY
- {CommunityItems.map(({ label, href }) => ( - - {label} - - ))} -
-
-
+

+ The contents of this website are © 2024{' '} + Apache Software Foundation under the terms of the{' '} + Apache License v2. Apache + Doris, Doris, and the Doris logo are either registered trademarks or trademarks of The Apache + Software Foundation in the United States and other countries. +

); } diff --git a/src/theme/Footer/styles.scss b/src/theme/Footer/styles.scss index e70eebf6ee39c2..2320381d3fe840 100644 --- a/src/theme/Footer/styles.scss +++ b/src/theme/Footer/styles.scss @@ -212,23 +212,20 @@ } .docs-footer { - height: 7.625rem; + height: 3.25rem; background-color: #fff; - display: flex; - border-top: 1px solid #edf2fa; - .logo { - position: relative; - .footer__logo { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 8.5625rem; - height: 3.5rem; - margin-top: 0; - } + padding: 1rem 1.5rem; + border-top: 1px solid #EDF2FA; + .docs-footer-p { + text-align: center; + color: rgba(29, 29, 29, 0.4); + font-family: Inter; + font-size: 9px; + font-style: normal; + font-weight: 400; + line-height: 20px; } - .content { - flex: 1; + .docs-footer-p a { + text-decoration: underline; } } diff --git a/src/theme/Navbar/Content/index.tsx b/src/theme/Navbar/Content/index.tsx index 275af09cc8f42d..0243b53757aee5 100644 --- a/src/theme/Navbar/Content/index.tsx +++ b/src/theme/Navbar/Content/index.tsx @@ -86,11 +86,13 @@ export default function NavbarContent(): JSX.Element { const [isEN, setIsEN] = useState(true); const docItems = isEN ? useThemeConfig().docNavbarEN.items : useThemeConfig().docNavbarZH.items; const [leftItems, rightItems] = splitNavbarItems(items); - const [leftDocItems,rightDocItems] = splitNavbarItems(docItems); - const [isDocsPage, setIsDocsPage] = useState(false); + const [leftDocItems, rightDocItems] = splitNavbarItems(docItems); + const [isDocsPage, setIsDocsPage] = useState( + typeof window !== 'undefined' ? location.pathname.includes('docs') : false, + ); const [isCommunity, setIsCommunity] = useState(false); const searchBarItem = items.find(item => item.type === 'search'); - + const [currentVersion, setCurrentVersion] = useState(''); useEffect(() => { getGithubStar(); @@ -139,8 +141,7 @@ export default function NavbarContent(): JSX.Element { function getNavItem(type: string) { return items.find(item => item.type === type); } - - + return ( ) : ( - + )} {/* */} diff --git a/src/theme/SearchBar/EmptyTemplate.js b/src/theme/SearchBar/EmptyTemplate.js new file mode 100644 index 00000000000000..0c67ba65f422a1 --- /dev/null +++ b/src/theme/SearchBar/EmptyTemplate.js @@ -0,0 +1,12 @@ +import { translate } from "@docusaurus/Translate"; +import { iconNoResults } from "./icons"; +import styles from "./SearchBar.module.css"; +export function EmptyTemplate() { + if (process.env.NODE_ENV === "production") { + return `${iconNoResults}${translate({ + id: "theme.SearchBar.noResultsText", + message: "No results", + })}`; + } + return `⚠️ The search index is only available when you run docusaurus build!`; +} diff --git a/src/theme/SearchBar/SearchBar.jsx b/src/theme/SearchBar/SearchBar.jsx new file mode 100644 index 00000000000000..3272a111f72811 --- /dev/null +++ b/src/theme/SearchBar/SearchBar.jsx @@ -0,0 +1,402 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import clsx from 'clsx'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import useIsBrowser from '@docusaurus/useIsBrowser'; +import { useHistory, useLocation } from '@docusaurus/router'; +import { translate } from '@docusaurus/Translate'; +import { ReactContextError, useDocsPreferredVersion } from '@docusaurus/theme-common'; +import { useActivePlugin } from '@docusaurus/plugin-content-docs/client'; +import { fetchIndexesByWorker, searchByWorker } from './searchByWorker'; +import { SuggestionTemplate } from './SuggestionTemplate'; +import { EmptyTemplate } from './EmptyTemplate'; +import { + Mark, + searchBarShortcut, + searchBarShortcutHint, + searchBarPosition, + docsPluginIdForPreferredVersion, + indexDocs, + searchContextByPaths, + hideSearchBarWithNoSearchContext, + useAllContextsWithNoSearchContext, +} from '../../utils/proxiedGenerated'; +import LoadingRing from '../LoadingRing/LoadingRing'; +import styles from './SearchBar.module.css'; +import { normalizeContextByPath } from '../../utils/normalizeContextByPath'; +import useIsDocPage from '@site/src/hooks/use-is-doc'; +import { VERSIONS } from '@site/src/constant/common'; +async function fetchAutoCompleteJS() { + const autoCompleteModule = await import('@easyops-cn/autocomplete.js'); + const autoComplete = autoCompleteModule.default; + if (autoComplete.noConflict) { + // For webpack v5 since docusaurus v2.0.0-alpha.75 + autoComplete.noConflict(); + } else if (autoCompleteModule.noConflict) { + // For webpack v4 before docusaurus v2.0.0-alpha.74 + autoCompleteModule.noConflict(); + } + return autoComplete; +} +const SEARCH_PARAM_HIGHLIGHT = '_highlight'; +export default function SearchBar({ handleSearchBarToggle }) { + const isBrowser = useIsBrowser(); + const location = useLocation(); + const { + siteConfig: { baseUrl }, + i18n: { currentLocale }, + } = useDocusaurusContext(); + // It returns undefined for non-docs pages + const activePlugin = useActivePlugin(); + const [isDocsPage] = useIsDocPage(false); + let versionUrl = baseUrl; + if (location?.pathname && location.pathname.includes('zh-CN') && !versionUrl.includes('zh-CN')) { + versionUrl = baseUrl + 'zh-CN/'; + } + if (location?.pathname) { + VERSIONS.forEach(version => { + if (location.pathname.includes(version)) { + versionUrl += `docs/${version}/`; + } + }); + } + + // For non-docs pages while using plugin-content-docs with custom ids, + // this will throw an error of: + // > Docusaurus plugin global data not found for "docusaurus-plugin-content-docs" plugin with id "default". + // It seems that we can not get the correct id for non-docs pages. + // try { + // // The try-catch is a hack because useDocsPreferredVersion just throws an + // // exception when versions are not used. + // // The same hack is used in SearchPage.tsx + // // eslint-disable-next-line react-hooks/rules-of-hooks + // const { preferredVersion } = useDocsPreferredVersion(activePlugin?.pluginId ?? docsPluginIdForPreferredVersion); + // console.log('preferredVersion',preferredVersion); + + // if (preferredVersion && !preferredVersion.isLast) { + // versionUrl = preferredVersion.path + "/"; + // } + // } + // catch (e) { + // if (indexDocs) { + // if (e instanceof ReactContextError) { + // /* ignore, happens when website doesn't use versions */ + // } + // else { + // throw e; + // } + // } + // } + + const history = useHistory(); + const searchBarRef = useRef(null); + const indexStateMap = useRef(new Map()); + // Should the input be focused after the index is loaded? + const focusAfterIndexLoaded = useRef(false); + const [loading, setLoading] = useState(false); + const [inputChanged, setInputChanged] = useState(false); + const [inputValue, setInputValue] = useState(''); + const search = useRef(null); + const prevSearchContext = useRef(''); + const [searchContext, setSearchContext] = useState(''); + useEffect(() => { + if (!Array.isArray(searchContextByPaths)) { + return; + } + let nextSearchContext = ''; + if (location.pathname.startsWith(versionUrl)) { + const uri = location.pathname.substring(versionUrl.length); + let matchedPath; + for (const _path of searchContextByPaths) { + const path = typeof _path === 'string' ? _path : _path.path; + if (uri === path || uri.startsWith(`${path}/`)) { + matchedPath = path; + break; + } + } + if (matchedPath) { + nextSearchContext = matchedPath; + } + } + if (prevSearchContext.current !== nextSearchContext) { + // Reset index state map once search context is changed. + indexStateMap.current.delete(nextSearchContext); + prevSearchContext.current = nextSearchContext; + } + setSearchContext(nextSearchContext); + }, [location.pathname, versionUrl]); + const hidden = !!hideSearchBarWithNoSearchContext && Array.isArray(searchContextByPaths) && searchContext === ''; + const loadIndex = useCallback(async () => { + if (hidden || indexStateMap.current.get(searchContext)) { + // Do not load the index (again) if its already loaded or in the process of being loaded. + return; + } + indexStateMap.current.set(searchContext, 'loading'); + search.current?.autocomplete.destroy(); + setLoading(true); + const [autoComplete] = await Promise.all([ + fetchAutoCompleteJS(), + fetchIndexesByWorker(versionUrl, searchContext), + ]); + const searchFooterLinkElement = ({ query, isEmpty }) => { + const a = document.createElement('a'); + const params = new URLSearchParams(); + params.set('q', query); + let linkText; + if (searchContext) { + const detailedSearchContext = + searchContext && Array.isArray(searchContextByPaths) + ? searchContextByPaths.find(item => + typeof item === 'string' ? item === searchContext : item.path === searchContext, + ) + : searchContext; + const translatedSearchContext = detailedSearchContext + ? normalizeContextByPath(detailedSearchContext, currentLocale).label + : searchContext; + if (useAllContextsWithNoSearchContext && isEmpty) { + linkText = translate( + { + id: 'theme.SearchBar.seeAllOutsideContext', + message: 'See all results outside "{context}"', + }, + { context: translatedSearchContext }, + ); + } else { + linkText = translate( + { + id: 'theme.SearchBar.searchInContext', + message: 'See all results within "{context}"', + }, + { context: translatedSearchContext }, + ); + } + } else { + linkText = translate({ + id: 'theme.SearchBar.seeAll', + message: 'See all results', + }); + } + if ( + searchContext && + Array.isArray(searchContextByPaths) && + (!useAllContextsWithNoSearchContext || !isEmpty) + ) { + params.set('ctx', searchContext); + } + if (versionUrl !== baseUrl) { + if (!versionUrl.startsWith(baseUrl)) { + throw new Error( + `Version url '${versionUrl}' does not start with base url '${baseUrl}', this is a bug of \`@easyops-cn/docusaurus-search-local\`, please report it.`, + ); + } + params.set('version', versionUrl.substring(baseUrl.length)); + } + const url = `${baseUrl}search/?${params.toString()}`; + a.href = url; + a.textContent = linkText; + a.addEventListener('click', e => { + if (!e.ctrlKey && !e.metaKey) { + e.preventDefault(); + search.current?.autocomplete.close(); + history.push(url); + } + }); + return a; + }; + search.current = autoComplete( + searchBarRef.current, + { + hint: false, + autoselect: true, + openOnFocus: true, + cssClasses: { + root: clsx(styles.searchBar, { + [styles.searchBarLeft]: searchBarPosition === 'left', + }), + noPrefix: true, + dropdownMenu: styles.dropdownMenu, + input: styles.input, + hint: styles.hint, + suggestions: styles.suggestions, + suggestion: styles.suggestion, + cursor: styles.cursor, + dataset: styles.dataset, + empty: styles.empty, + }, + }, + [ + { + source: async (input, callback) => { + const result = await searchByWorker(versionUrl, searchContext, input); + callback(result); + }, + templates: { + suggestion: SuggestionTemplate, + empty: EmptyTemplate, + footer: ({ query, isEmpty }) => { + if (isEmpty && (!searchContext || !useAllContextsWithNoSearchContext)) { + return; + } + const a = searchFooterLinkElement({ query, isEmpty }); + const div = document.createElement('div'); + div.className = styles.hitFooter; + div.appendChild(a); + return div; + }, + }, + }, + ], + ) + .on('autocomplete:selected', function (event, { document: { u, h }, tokens }) { + searchBarRef.current?.blur(); + let url = u; + if (Mark && tokens.length > 0) { + const params = new URLSearchParams(); + for (const token of tokens) { + params.append(SEARCH_PARAM_HIGHLIGHT, token); + } + url += `?${params.toString()}`; + } + if (h) { + url += h; + } + history.push(url); + }) + .on('autocomplete:closed', () => { + searchBarRef.current?.blur(); + }); + indexStateMap.current.set(searchContext, 'done'); + setLoading(false); + if (focusAfterIndexLoaded.current) { + const input = searchBarRef.current; + if (input.value) { + search.current?.autocomplete.open(); + } + input.focus(); + } + }, [hidden, searchContext, versionUrl, baseUrl, history]); + useEffect(() => { + if (!Mark) { + return; + } + const keywords = isBrowser ? new URLSearchParams(location.search).getAll(SEARCH_PARAM_HIGHLIGHT) : []; + // A workaround to fix an issue of highlighting in code blocks. + // See https://github.com/easyops-cn/docusaurus-search-local/issues/92 + // Code blocks will be re-rendered after this `useEffect` ran. + // So we make the marking run after a macro task. + setTimeout(() => { + const root = document.querySelector('article'); + if (!root) { + return; + } + const mark = new Mark(root); + mark.unmark(); + if (keywords.length !== 0) { + mark.mark(keywords); + } + // Apply any keywords to the search input so that we can clear marks in case we loaded a page with a highlight in the url + setInputValue(keywords.join(' ')); + search.current?.autocomplete.setVal(keywords.join(' ')); + }); + }, [isBrowser, location.search, location.pathname]); + const [focused, setFocused] = useState(false); + const onInputFocus = useCallback(() => { + focusAfterIndexLoaded.current = true; + loadIndex(); + setFocused(true); + handleSearchBarToggle?.(true); + }, [handleSearchBarToggle, loadIndex]); + const onInputBlur = useCallback(() => { + setFocused(false); + handleSearchBarToggle?.(false); + }, [handleSearchBarToggle]); + const onInputMouseEnter = useCallback(() => { + loadIndex(); + }, [loadIndex]); + const onInputChange = useCallback(event => { + setInputValue(event.target.value); + if (event.target.value) { + setInputChanged(true); + } + }, []); + // Implement hint icons for the search shortcuts on mac and the rest operating systems. + const isMac = isBrowser ? /mac/i.test(navigator.userAgentData?.platform ?? navigator.platform) : false; + useEffect(() => { + if (!searchBarShortcut) { + return; + } + // Add shortcuts command/ctrl + K + const handleShortcut = event => { + if ((isMac ? event.metaKey : event.ctrlKey) && (event.key === 'k' || event.key === 'K')) { + event.preventDefault(); + searchBarRef.current?.focus(); + onInputFocus(); + } + }; + document.addEventListener('keydown', handleShortcut); + return () => { + document.removeEventListener('keydown', handleShortcut); + }; + }, [isMac, onInputFocus]); + + useEffect(() => { + if (isDocsPage) { + loadIndex(); + } + }, [location.pathname, isDocsPage]); + + const onClearSearch = useCallback(() => { + const params = new URLSearchParams(location.search); + params.delete(SEARCH_PARAM_HIGHLIGHT); + const paramsStr = params.toString(); + const searchUrl = location.pathname + (paramsStr != '' ? `?${paramsStr}` : '') + location.hash; + if (searchUrl != location.pathname + location.search + location.hash) { + history.push(searchUrl); + } + // We always clear these here because in case no match was selected the above history push wont happen + setInputValue(''); + search.current?.autocomplete.setVal(''); + }, [location.pathname, location.search, location.hash, history]); + + return ( + + ); +} diff --git a/src/theme/SearchBar/SearchBar.module.css b/src/theme/SearchBar/SearchBar.module.css new file mode 100644 index 00000000000000..c5d9dfbb682007 --- /dev/null +++ b/src/theme/SearchBar/SearchBar.module.css @@ -0,0 +1,298 @@ +.searchBar .dropdownMenu { + left: auto !important; + right: 0 !important; + + background: var(--search-local-modal-background, #f5f6f7); + border-radius: 6px; + box-shadow: var( + --search-local-modal-shadow, + inset 1px 1px 0 0 hsla(0, 0%, 100%, 0.5), + 0 3px 8px 0 #555a64 + ); + margin-top: 8px; + width: var(--search-local-modal-width, 560px); + position: relative; + + padding: var(--search-local-spacing, 12px); +} + +.navbarSearchInput { + border: none !important; + background-color: #f7f9fe !important; + background-image: url('/static/images/search-icon.svg'); + height: 2.5rem !important; + background-position: 0.625rem center !important; + padding-left: 2.25rem !important; + color: #4c576c !important; + width: 15.43rem !important; + border-radius: 0.5rem; +} + + +@media not (max-width: 996px) { + .searchBar.searchBarLeft .dropdownMenu { + left: 0 !important; + right: auto !important; + } +} + +@media (max-width: 576px) { + :global(.navbar__search-input):not(:focus) { + width: 2rem; + } + + .searchBar .dropdownMenu { + width: var(--search-local-modal-width-sm, 340px); + max-width: calc(100vw - var(--ifm-navbar-padding-horizontal) * 2); + } +} + +html[data-theme="dark"] .searchBar .dropdownMenu { + background: var(--search-local-modal-background, var(--ifm-background-color)); + box-shadow: var( + --search-local-modal-shadow, + inset 1px 1px 0 0 #2c2e40, + 0 3px 8px 0 #000309 + ); +} + +.searchBar .dropdownMenu .suggestion { + cursor: pointer; + background: var(--search-local-hit-background, #fff); + border-radius: 4px; + box-shadow: var(--search-local-hit-shadow, 0 1px 3px 0 #d4d9e1); + padding: 0 var(--search-local-spacing, 12px); + width: 100%; + + align-items: center; + color: var(--search-local-hit-color, #444950); + display: flex; + flex-direction: row; + height: var(--search-local-hit-height, 56px); +} + +html[data-theme="dark"] .dropdownMenu .suggestion { + background: var(--search-local-hit-background, var(--ifm-color-emphasis-100)); + box-shadow: var(--search-local-hit-shadow, none); + color: var(--search-local-hit-color, var(--ifm-font-color-base)); +} + +.searchBar .dropdownMenu .suggestion:not(:last-child) { + margin-bottom: 4px; +} + +.searchBar .dropdownMenu .suggestion.cursor { + background-color: var( + --search-local-highlight-color, + var(--ifm-color-primary) + ); +} + +.hitTree, +.hitIcon, +.hitPath, +.noResultsIcon, +.hitFooter a { + color: var(--search-local-muted-color, #969faf); +} + +html[data-theme="dark"] .hitTree, +html[data-theme="dark"] .hitIcon, +html[data-theme="dark"] .hitPath, +html[data-theme="dark"] .noResultsIcon { + color: var(--search-local-muted-color, var(--ifm-color-secondary-darkest)); +} + +.hitTree { + display: flex; + align-items: center; +} + +.hitTree > svg { + height: var(--search-local-hit-height, 56px); + opacity: 0.5; + stroke-width: var(--search-local-icon-stroke-width, 1.4); + width: 24px; +} + +.hitIcon { + stroke-width: var(--search-local-icon-stroke-width, 1.4); + + height: 20px; + width: 20px; +} + +.hitWrapper { + flex: 1 1 auto; + display: flex; + flex-direction: column; + font-weight: 500; + justify-content: center; + margin: 0 8px; + overflow-x: hidden; + width: 80%; +} + +.hitWrapper mark { + background: none; + color: var(--search-local-highlight-color, var(--ifm-color-primary)); +} + +.hitTitle { + font-size: 0.9em; +} + +.hitPath { + font-size: 0.75em; +} + +.hitPath, +.hitTitle { + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; +} + +.hitAction { + height: 20px; + width: 20px; +} + +.hideAction > svg { + display: none; +} + +.noResults { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--search-local-spacing, 12px) 0; +} + +.noResultsIcon { + margin-bottom: var(--search-local-spacing, 12px); +} + +.hitFooter { + text-align: center; + margin-top: var(--search-local-spacing, 12px); + font-size: 0.85em; +} + +.hitFooter a { + text-decoration: underline; +} + +.cursor .hideAction > svg { + display: block; +} + +.suggestion.cursor, +.suggestion.cursor mark, +.suggestion.cursor .hitTree, +.suggestion.cursor .hitIcon, +.suggestion.cursor .hitPath { + color: var( + --search-local-hit-active-color, + var(--ifm-color-white) + ) !important; +} + +.suggestion.cursor mark { + text-decoration: underline; +} + +.searchBarContainer { + margin-left: 16px; +} + +.searchBarContainer .searchBarLoadingRing { + display: none; + position: absolute; + left: 10px; + top: 6px; +} + +.searchBarContainer .searchClearButton { + position: absolute; + right: 0.8rem; + top: 50%; + transform: translate(0, -50%); + padding: 0; + background: none; + border: none; + line-height: 1rem; +} + +:global(.navbar__search) { + position: relative; +} + +.searchIndexLoading :global(.navbar__search-input) { + background-image: none; +} + +.searchBarContainer.searchIndexLoading .searchBarLoadingRing { + display: inline-block; +} + +.searchHintContainer { + position: absolute; + right: 10px; + top: 0px; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + pointer-events: none; + gap: 4px; +} + +.searchHint { + color: var(--ifm-navbar-search-input-placeholder-color); + background-color: var(--ifm-navbar-search-input-background-color); + border: 1px solid var(--ifm-color-emphasis-500); + box-shadow: inset 0 -1px 0 var(--ifm-color-emphasis-500); +} + +@media (max-width: 576px) { + .searchBarContainer:not(.focused) .searchClearButton, + .searchHintContainer { + display: none; + } +} + +html[dir="rtl"] .searchHintContainer { + right: auto; + left: 10px; +} + +html[dir="rtl"] .searchBarContainer .searchClearButton { + right: auto; + left: 0.8rem; +} + +html[dir="rtl"] .searchBarContainer .searchBarLoadingRing { + left: auto; + right: 10px; +} + +html[dir="rtl"] :global(.navbar__search-input) { + padding: 0 2.25em 0 0.5em; +} + +/* For autocomplete.js only. */ +.input { +} +.hint { +} +.suggestions { + max-height: 500px !important; + overflow: scroll !important; +} +.dataset { +} +.empty { +} +/**/ diff --git a/src/theme/SearchBar/SuggestionTemplate.js b/src/theme/SearchBar/SuggestionTemplate.js new file mode 100644 index 00000000000000..427a6a6b225be0 --- /dev/null +++ b/src/theme/SearchBar/SuggestionTemplate.js @@ -0,0 +1,54 @@ +import { SearchDocumentType, } from "../../../shared/interfaces"; +import { concatDocumentPath } from "../../utils/concatDocumentPath"; +import { getStemmedPositions } from "../../utils/getStemmedPositions"; +import { highlight } from "../../utils/highlight"; +import { highlightStemmed } from "../../utils/highlightStemmed"; +import { explicitSearchResultPath } from "../../utils/proxiedGenerated"; +import { iconAction, iconContent, iconHeading, iconTitle, iconTreeInter, iconTreeLast, } from "./icons"; +import styles from "./SearchBar.module.css"; +export function SuggestionTemplate({ document, type, page, metadata, tokens, isInterOfTree, isLastOfTree, }) { + const isTitle = type === SearchDocumentType.Title; + const isKeywords = type === SearchDocumentType.Keywords; + const isTitleRelated = isTitle || isKeywords; + const isHeading = type === SearchDocumentType.Heading; + const tree = []; + if (isInterOfTree) { + tree.push(iconTreeInter); + } + else if (isLastOfTree) { + tree.push(iconTreeLast); + } + const treeWrapper = tree.map((item) => `${item}`); + const icon = `${isTitleRelated ? iconTitle : isHeading ? iconHeading : iconContent}`; + const wrapped = [ + `${isKeywords + ? highlight(document.s, tokens) + : highlightStemmed(document.t, getStemmedPositions(metadata, "t"), tokens)}`, + ]; + const needsExplicitHitPath = !isInterOfTree && !isLastOfTree && explicitSearchResultPath; + if (needsExplicitHitPath) { + const pathItems = page + ? page.b + ?.concat(page.t) + .concat(!document.s || document.s === page.t ? [] : document.s) + : document.b; + wrapped.push(`${concatDocumentPath(pathItems ?? [])}`); + } + else if (!isTitleRelated) { + wrapped.push(`${highlight(page.t || + // Todo(weareoutman): This is for EasyOps only. + // istanbul ignore next + (document.u.startsWith("/docs/api-reference/") + ? "API Reference" + : ""), tokens)}`); + } + const action = `${iconAction}`; + return [ + ...treeWrapper, + icon, + ``, + ...wrapped, + "", + action, + ].join(""); +} diff --git a/src/theme/SearchBar/__mocks__/icons.js b/src/theme/SearchBar/__mocks__/icons.js new file mode 100644 index 00000000000000..7c2f79e1343b84 --- /dev/null +++ b/src/theme/SearchBar/__mocks__/icons.js @@ -0,0 +1,7 @@ +export const iconTitle = ''; +export const iconHeading = ''; +export const iconContent = ''; +export const iconAction = ''; +export const iconNoResults = ''; +export const iconTreeInter = ''; +export const iconTreeLast = ''; diff --git a/src/theme/SearchBar/icons.js b/src/theme/SearchBar/icons.js new file mode 100644 index 00000000000000..d5380213e9834a --- /dev/null +++ b/src/theme/SearchBar/icons.js @@ -0,0 +1,7 @@ +export const iconTitle = ''; +export const iconHeading = ''; +export const iconContent = ''; +export const iconAction = ''; +export const iconNoResults = ''; +export const iconTreeInter = ''; +export const iconTreeLast = ''; diff --git a/src/theme/SearchBar/index.js b/src/theme/SearchBar/index.js index 381d9012c0510c..369df710bfdef3 100644 --- a/src/theme/SearchBar/index.js +++ b/src/theme/SearchBar/index.js @@ -1,10 +1,3 @@ -import React from 'react'; -import SearchBar from '@theme-original/SearchBar'; - -export default function SearchBarWrapper(props) { - return ( - <> - - - ); -} +import "../../utils/proxiedGenerated"; +import SearchBar from "./SearchBar"; +export default SearchBar; diff --git a/src/theme/SearchBar/searchByWorker.js b/src/theme/SearchBar/searchByWorker.js new file mode 100644 index 00000000000000..cd53fa72b9740c --- /dev/null +++ b/src/theme/SearchBar/searchByWorker.js @@ -0,0 +1,24 @@ +import * as Comlink from "comlink"; +let remoteWorkerPromise; +function getRemoteWorker() { + if (process.env.NODE_ENV === "production" && !remoteWorkerPromise) { + remoteWorkerPromise = (async () => { + const Remote = Comlink.wrap(new Worker(new URL("./worker.js", import.meta.url))); + return await new Remote(); + })(); + } + return remoteWorkerPromise; +} +export async function fetchIndexesByWorker(baseUrl, searchContext) { + if (process.env.NODE_ENV === "production") { + const remoteWorker = await getRemoteWorker(); + await remoteWorker.fetchIndexes(baseUrl, searchContext); + } +} +export async function searchByWorker(baseUrl, searchContext, input) { + if (process.env.NODE_ENV === "production") { + const remoteWorker = await getRemoteWorker(); + return remoteWorker.search(baseUrl, searchContext, input); + } + return []; +} diff --git a/src/theme/SearchBar/worker.js b/src/theme/SearchBar/worker.js new file mode 100644 index 00000000000000..b2d3d11d08a777 --- /dev/null +++ b/src/theme/SearchBar/worker.js @@ -0,0 +1,94 @@ +import * as Comlink from "comlink"; +import lunr from "lunr"; +import { searchIndexUrl, searchResultLimits, language } from "@easyops-cn/docusaurus-search-local/dist/client/client/utils/proxiedGeneratedConstants"; +import { tokenize } from "@easyops-cn/docusaurus-search-local/dist/client/client/utils/tokenize"; +import { smartQueries } from "@easyops-cn/docusaurus-search-local/dist/client/client/utils/smartQueries"; +import { SearchDocumentType, } from "../../shared/interfaces"; +import { sortSearchResults } from "@easyops-cn/docusaurus-search-local/dist/client/client/utils/sortSearchResults"; +import { processTreeStatusOfSearchResults } from "@easyops-cn/docusaurus-search-local/dist/client/client/utils/processTreeStatusOfSearchResults"; +const cache = new Map(); +export class SearchWorker { + async fetchIndexes(baseUrl, searchContext) { + await this.lowLevelFetchIndexes(baseUrl, searchContext); + } + async lowLevelFetchIndexes(baseUrl, searchContext) { + const cacheKey = `${baseUrl}${searchContext}`; + let promise = cache.get(cacheKey); + if (!promise) { + promise = legacyFetchIndexes(baseUrl, searchContext); + cache.set(cacheKey, promise); + } + return promise; + } + async search(baseUrl, searchContext, input) { + const rawTokens = tokenize(input, language); + if (rawTokens.length === 0) { + return []; + } + const { wrappedIndexes, zhDictionary } = await this.lowLevelFetchIndexes(baseUrl, searchContext); + const queries = smartQueries(rawTokens, zhDictionary); + const results = []; + search: for (const { term, tokens } of queries) { + for (const { documents, index, type } of wrappedIndexes) { + results.push(...index + .query((query) => { + for (const item of term) { + query.term(item.value, { + wildcard: item.wildcard, + presence: item.presence, + }); + } + }) + .slice(0, searchResultLimits) + // Remove duplicated results. + .filter((result) => !results.some((item) => item.document.i.toString() === result.ref)) + .slice(0, searchResultLimits - results.length) + .map((result) => { + const document = documents.find((doc) => doc.i.toString() === result.ref); + return { + document, + type, + page: type !== SearchDocumentType.Title && + wrappedIndexes[0].documents.find((doc) => doc.i === document.p), + metadata: result.matchData.metadata, + tokens, + score: result.score, + }; + })); + if (results.length >= searchResultLimits) { + break search; + } + } + } + sortSearchResults(results); + processTreeStatusOfSearchResults(results); + return results; + } +} +async function legacyFetchIndexes(baseUrl, searchContext) { + const url = `${baseUrl}${searchIndexUrl.replace("{dir}", searchContext ? `-${searchContext.replace(/\//g, "-")}` : "")}`; + // Catch potential attacks. + const fullUrl = new URL(url, location.origin); + if (fullUrl.origin !== location.origin) { + throw new Error("Unexpected version url"); + } + const json = (await (await fetch(url)).json()); + const wrappedIndexes = json.map(({ documents, index }, type) => ({ + type: type, + documents, + index: lunr.Index.load(index), + })); + const zhDictionary = json.reduce((acc, item) => { + for (const tuple of item.index.invertedIndex) { + if (/\p{Unified_Ideograph}/u.test(tuple[0][0])) { + acc.add(tuple[0]); + } + } + return acc; + }, new Set()); + return { + wrappedIndexes, + zhDictionary: Array.from(zhDictionary), + }; +} +Comlink.expose(SearchWorker); diff --git a/src/theme/TOC/index.tsx b/src/theme/TOC/index.tsx index d08817f4abd6fe..5571d8e1fa79a1 100644 --- a/src/theme/TOC/index.tsx +++ b/src/theme/TOC/index.tsx @@ -6,7 +6,9 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import HomeIcon from '@site/static/images/toc-icon/home.svg'; import PdfIcon from '@site/static/images/toc-icon/pdf.svg'; import GithubIcon from '@site/static/images/toc-icon/github.svg'; +import ConcatIcon from '@site/static/images/toc-icon/concat.svg'; import { DOWNLOAD_PDFS } from '@site/src/constant/download.data'; +import { VERSIONS } from '@site/src/constant/common'; import Link from '@docusaurus/Link'; import styles from './styles.module.css'; @@ -37,14 +39,14 @@ export default function TOC({ className, ...props }: Props): JSX.Element { const [currentVersion, setCurrentVersion] = useState(DEFAULT_VERSION); const handleMouseEnter = (id: string) => { const dom = document.getElementById(id); - dom.style.color = '#444FD9'; - dom.firstChild.style.fill = '#444FD9'; + dom!.style.color = '#444FD9'; + dom!.firstChild!.style.fill = '#444FD9'; }; const handleMouseLeave = (id: string) => { const dom = document.getElementById(id); - dom.style.color = '#1F1F26'; - dom.firstChild.style.fill = '#7F7F83'; + dom!.style.color = '#1F1F26'; + dom!.firstChild!.style.fill = '#7F7F83'; }; useEffect(() => { @@ -55,7 +57,7 @@ export default function TOC({ className, ...props }: Props): JSX.Element { const secPath = location.pathname.includes('zh-CN/docs') ? location.pathname.split('/')[3] : location.pathname.split('/')[2]; - if (pathname === 'docs' && ['dev', '3.0', '2.0', '1.2'].includes(secPath)) { + if (pathname === 'docs' && VERSIONS.includes(secPath)) { setCurrentVersion(secPath); } else { setCurrentVersion(DEFAULT_VERSION); @@ -100,7 +102,7 @@ export default function TOC({ className, ...props }: Props): JSX.Element { onMouseEnter={() => handleMouseEnter('toc-icon-github')} onMouseLeave={() => handleMouseLeave('toc-icon-github')} > - + {isCN ? : } {isCN ? '技术论坛' : 'Ask Questions on Discussion'} diff --git a/src/utils/normalizeContextByPath.js b/src/utils/normalizeContextByPath.js new file mode 100644 index 00000000000000..99c29d64034092 --- /dev/null +++ b/src/utils/normalizeContextByPath.js @@ -0,0 +1,24 @@ +export function normalizeContextByPath(context, currentLocale) { + if (typeof context === "string") { + return { + label: context, + path: context, + }; + } + else { + const { label, path } = context; + if (typeof label === "string") { + return { label, path }; + } + if (Object.prototype.hasOwnProperty.call(label, currentLocale)) { + return { + label: label[currentLocale], + path, + }; + } + return { + label: path, + path, + }; + } +} diff --git a/static/images/toc-icon/concat.svg b/static/images/toc-icon/concat.svg new file mode 100644 index 00000000000000..7b7754644a468c --- /dev/null +++ b/static/images/toc-icon/concat.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/images/toc-icon/github-active.svg b/static/images/toc-icon/github-active.svg deleted file mode 100644 index 904b6428759f16..00000000000000 --- a/static/images/toc-icon/github-active.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/static/images/toc-icon/home-active.svg b/static/images/toc-icon/home-active.svg deleted file mode 100644 index 1cb9a98ae428f0..00000000000000 --- a/static/images/toc-icon/home-active.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/static/images/toc-icon/pdf-active.svg b/static/images/toc-icon/pdf-active.svg deleted file mode 100644 index 3e9bf42eb7f43b..00000000000000 --- a/static/images/toc-icon/pdf-active.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file