fix(deps): replace flexsearch-ts with orama

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2024-01-15 22:35:55 +01:00 committed by David Mehren
parent 931ce68a32
commit 07162b7807
25 changed files with 266 additions and 215 deletions

View file

@ -681,7 +681,7 @@
"noSelection": "Select an entry on the left side to show the instructions." "noSelection": "Select an entry on the left side to show the instructions."
}, },
"categories": { "categories": {
"basic": "Basics", "basics": "Basics",
"other": "Other", "other": "Other",
"embedding": "Embedding", "embedding": "Embedding",
"charts": "Charts & Diagrams" "charts": "Charts & Diagrams"

View file

@ -19,9 +19,9 @@
"test:e2e:open": "cypress open", "test:e2e:open": "cypress open",
"test:e2e": "cypress run --browser chrome", "test:e2e": "cypress run --browser chrome",
"test:e2e:ci": "cypress run --browser chrome --record true --parallel --group \"chrome\"", "test:e2e:ci": "cypress run --browser chrome --record true --parallel --group \"chrome\"",
"test:watch": "cross-env NODE_ENV=test jest --watch", "test:watch": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest --watch",
"test:ci": "cross-env NODE_ENV=test jest --coverage", "test:ci": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest --coverage",
"test": "cross-env NODE_ENV=test jest" "test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -53,6 +53,7 @@
"@hedgedoc/html-to-react": "workspace:html-to-react", "@hedgedoc/html-to-react": "workspace:html-to-react",
"@hedgedoc/markdown-it-plugins": "workspace:markdown-it-plugins", "@hedgedoc/markdown-it-plugins": "workspace:markdown-it-plugins",
"@mrdrogdrog/optional": "1.2.1", "@mrdrogdrog/optional": "1.2.1",
"@orama/orama": "2.0.0-beta.12",
"@react-hook/resize-observer": "1.2.6", "@react-hook/resize-observer": "1.2.6",
"@redux-devtools/core": "3.13.3", "@redux-devtools/core": "3.13.3",
"@reduxjs/toolkit": "1.9.7", "@reduxjs/toolkit": "1.9.7",
@ -75,7 +76,6 @@
"eventemitter2": "6.4.9", "eventemitter2": "6.4.9",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"firacode": "6.2.0", "firacode": "6.2.0",
"flexsearch-ts": "0.7.35",
"flowchart.js": "1.17.1", "flowchart.js": "1.17.1",
"highlight.js": "11.9.0", "highlight.js": "11.9.0",
"htmlparser2": "9.0.0", "htmlparser2": "9.0.0",

View file

@ -5,8 +5,8 @@
*/ */
import { CategoryAccordion } from './category-accordion' import { CategoryAccordion } from './category-accordion'
import { CheatsheetEntryPane } from './cheatsheet-entry-pane' import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension' import type { CheatsheetExtension, CheatsheetEntry } from './cheatsheet-extension'
import { hasCheatsheetTopics } from './cheatsheet-extension' import { isCheatsheetMultiEntry } from './cheatsheet-extension'
import { CheatsheetSearch } from './cheatsheet-search' import { CheatsheetSearch } from './cheatsheet-search'
import styles from './cheatsheet.module.scss' import styles from './cheatsheet.module.scss'
import { TopicSelection } from './topic-selection' import { TopicSelection } from './topic-selection'
@ -20,11 +20,11 @@ import { Trans } from 'react-i18next'
export const CheatsheetContent: React.FC = () => { export const CheatsheetContent: React.FC = () => {
const [visibleExtensions, setVisibleExtensions] = useState<CheatsheetExtension[]>([]) const [visibleExtensions, setVisibleExtensions] = useState<CheatsheetExtension[]>([])
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>() const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
const [selectedEntry, setSelectedEntry] = useState<CheatsheetSingleEntry>() const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
const changeExtension = useCallback((value: CheatsheetExtension) => { const changeExtension = useCallback((value: CheatsheetExtension) => {
setSelectedExtension(value) setSelectedExtension(value)
setSelectedEntry(hasCheatsheetTopics(value) ? value.topics[0] : value) setSelectedEntry(isCheatsheetMultiEntry(value) ? value.topics[0] : value)
}, []) }, [])
return ( return (
@ -44,10 +44,10 @@ export const CheatsheetContent: React.FC = () => {
selectedEntry={selectedEntry} selectedEntry={selectedEntry}
setSelectedEntry={setSelectedEntry} setSelectedEntry={setSelectedEntry}
/> />
{selectedEntry !== undefined ? ( {selectedEntry !== undefined && selectedExtension !== undefined ? (
<CheatsheetEntryPane <CheatsheetEntryPane
rootI18nKey={hasCheatsheetTopics(selectedExtension) ? selectedExtension.i18nKey : undefined} i18nKeyPrefix={isCheatsheetMultiEntry(selectedExtension) ? selectedExtension.i18nKey : undefined}
extension={selectedEntry} entry={selectedEntry}
/> />
) : ( ) : (
<span> <span>

View file

@ -9,7 +9,7 @@ import { RendererIframe } from '../common/renderer-iframe/renderer-iframe'
import { EditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/editor-to-renderer-communicator-context-provider' import { EditorToRendererCommunicatorContextProvider } from '../editor-page/render-context/editor-to-renderer-communicator-context-provider'
import { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter' import { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter'
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message' import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
import type { CheatsheetSingleEntry } from './cheatsheet-extension' import type { CheatsheetEntry } from './cheatsheet-extension'
import { ReadMoreLinkItem } from './read-more-link-item' import { ReadMoreLinkItem } from './read-more-link-item'
import { useComponentsFromAppExtensions } from './use-components-from-app-extensions' import { useComponentsFromAppExtensions } from './use-components-from-app-extensions'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
@ -18,8 +18,8 @@ import { ListGroupItem } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
interface CheatsheetRendererProps { interface CheatsheetRendererProps {
rootI18nKey?: string i18nKeyPrefix?: string
extension: CheatsheetSingleEntry entry: CheatsheetEntry
} }
/** /**
@ -28,7 +28,7 @@ interface CheatsheetRendererProps {
* @param extension The extension to render * @param extension The extension to render
* @param rootI18nKey An additional i18n namespace * @param rootI18nKey An additional i18n namespace
*/ */
export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extension, rootI18nKey }) => { export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ entry, i18nKeyPrefix }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [content, setContent] = useState('') const [content, setContent] = useState('')
@ -36,13 +36,13 @@ export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extensi
const lines = useMemo(() => content.split('\n'), [content]) const lines = useMemo(() => content.split('\n'), [content])
const i18nPrefix = useMemo( const i18nPrefix = useMemo(
() => `cheatsheet.${rootI18nKey ? `${rootI18nKey}.` : ''}${extension.i18nKey}.`, () => `cheatsheet.${i18nKeyPrefix !== undefined ? i18nKeyPrefix + '.' : ''}${entry.i18nKey}.`,
[extension.i18nKey, rootI18nKey] [entry.i18nKey, i18nKeyPrefix]
) )
useEffect(() => { useEffect(() => {
setContent(t(`${i18nPrefix}example`) ?? '') setContent(t(`${i18nPrefix}example`) ?? '')
}, [extension, i18nPrefix, t]) }, [entry, i18nPrefix, t])
const cheatsheetExtensionComponents = useComponentsFromAppExtensions(setContent) const cheatsheetExtensionComponents = useComponentsFromAppExtensions(setContent)
@ -62,7 +62,7 @@ export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extensi
</h4> </h4>
{descriptionElements} {descriptionElements}
</ListGroupItem> </ListGroupItem>
<ReadMoreLinkItem url={extension.readMoreUrl}></ReadMoreLinkItem> <ReadMoreLinkItem url={entry.readMoreUrl}></ReadMoreLinkItem>
<ListGroupItem> <ListGroupItem>
<h4> <h4>
<Trans i18nKey={'cheatsheet.modal.headlines.exampleInput'} /> <Trans i18nKey={'cheatsheet.modal.headlines.exampleInput'} />

View file

@ -9,29 +9,28 @@ export interface CheatsheetExtensionComponentProps {
setContent: (dispatcher: string | ((prevState: string) => string)) => void setContent: (dispatcher: string | ((prevState: string) => string)) => void
} }
export type CheatsheetExtension = CheatsheetSingleEntry | CheatsheetEntryWithTopics export type CheatsheetExtension = CheatsheetSingleEntry | CheatsheetMultiEntry
/** /**
* Determine if a given {@link CheatsheetExtension} is a {@link CheatsheetEntryWithTopics} or just a {@link CheatsheetSingleEntry}. * Determine if a given {@link CheatsheetExtension} is a {@link CheatsheetSingleEntry} or a {@link CheatsheetMultiEntry}.
* *
* @param extension The extension in question * @param extension The extension in question
* @return boolean * @return boolean
*/ */
export const hasCheatsheetTopics = ( export const isCheatsheetMultiEntry = (
extension: CheatsheetExtension | undefined extension: CheatsheetExtension | undefined
): extension is CheatsheetEntryWithTopics => { ): extension is CheatsheetMultiEntry => {
return (extension as CheatsheetEntryWithTopics)?.topics !== undefined return extension !== undefined && typeof (extension as CheatsheetMultiEntry).topics === 'object'
} }
/** export interface CheatsheetMultiEntry {
* This is an entry with just a name and a bunch of different topics to discuss.
*
* e.g 'basics.headlines' with the topics 'hashtag' and 'equal'
*/
export interface CheatsheetEntryWithTopics {
i18nKey: string i18nKey: string
categoryI18nKey?: string categoryI18nKey: string
topics: CheatsheetSingleEntry[] topics: CheatsheetEntry[]
}
export interface CheatsheetSingleEntry extends CheatsheetEntry {
categoryI18nKey: string
} }
/** /**
@ -42,10 +41,8 @@ export interface CheatsheetEntryWithTopics {
* *
* e.g 'basics.basicFormatting' * e.g 'basics.basicFormatting'
*/ */
export interface CheatsheetSingleEntry { export interface CheatsheetEntry {
i18nKey: string i18nKey: string
categoryI18nKey?: string
cheatsheetExtensionComponent?: React.FC<CheatsheetExtensionComponentProps> cheatsheetExtensionComponent?: React.FC<CheatsheetExtensionComponentProps>
readMoreUrl?: URL readMoreUrl?: URL
} }

View file

@ -4,33 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { allAppExtensions } from '../../extensions/all-app-extensions' import { allAppExtensions } from '../../extensions/all-app-extensions'
import type { SearchIndexEntry } from '../../hooks/common/use-document-search' import { useCheatsheetSearch } from '../../hooks/common/use-cheatsheet-search'
import { useDocumentSearch } from '../../hooks/common/use-document-search'
import { useOnInputChange } from '../../hooks/common/use-on-input-change' import { useOnInputChange } from '../../hooks/common/use-on-input-change'
import { useTranslatedText } from '../../hooks/common/use-translated-text' import { useTranslatedText } from '../../hooks/common/use-translated-text'
import { UiIcon } from '../common/icons/ui-icon' import { UiIcon } from '../common/icons/ui-icon'
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension' import type { CheatsheetExtension } from './cheatsheet-extension'
import { hasCheatsheetTopics } from './cheatsheet-extension' import { isCheatsheetMultiEntry } from './cheatsheet-extension'
import styles from './cheatsheet.module.scss' import styles from './cheatsheet.module.scss'
import type { IndexOptionsForDocumentSearch, StoreOption } from 'flexsearch-ts'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { FormControl, InputGroup } from 'react-bootstrap' import { FormControl, InputGroup } from 'react-bootstrap'
import { X } from 'react-bootstrap-icons' import { X } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
interface CheatsheetSearchIndexEntry extends SearchIndexEntry {
title: string
description: string
example: string
}
const searchOptions: IndexOptionsForDocumentSearch<CheatsheetSearchIndexEntry, StoreOption> = {
document: {
id: 'id',
field: ['title', 'description', 'example']
}
}
export interface CheatsheetSearchProps { export interface CheatsheetSearchProps {
setVisibleExtensions: React.Dispatch<React.SetStateAction<CheatsheetExtension[]>> setVisibleExtensions: React.Dispatch<React.SetStateAction<CheatsheetExtension[]>>
} }
@ -48,38 +33,43 @@ export const CheatsheetSearch: React.FC<CheatsheetSearchProps> = ({ setVisibleEx
() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()), () => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
[] []
) )
const buildSearchIndexEntry = useCallback( const placeholderText = useTranslatedText('cheatsheet.search')
(entry: CheatsheetSingleEntry, rootI18nKey: string | undefined = undefined): CheatsheetSearchIndexEntry => {
const rootI18nKeyWithDot = rootI18nKey ? `${rootI18nKey}.` : '' const buildEntry = useCallback(
(i18nKey: string, extensionId: string) => {
return { return {
id: rootI18nKey ? rootI18nKey : entry.i18nKey, id: i18nKey,
title: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.title`), extensionId: extensionId,
description: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.description`), title: t(`cheatsheet.${i18nKey}.title`),
example: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.example`) description: t(`cheatsheet.${i18nKey}.description`),
example: t(`cheatsheet.${i18nKey}.example`)
} }
}, },
[t] [t]
) )
const placeholderText = useTranslatedText('cheatsheet.search')
const cheatsheetSearchIndexEntries = useMemo( const cheatsheetSearchIndexEntries = useMemo(
() => () =>
allCheatsheetExtensions.flatMap((entry) => { allCheatsheetExtensions.flatMap((extension) => {
if (hasCheatsheetTopics(entry)) { if (isCheatsheetMultiEntry(extension)) {
return entry.topics.map((innerEntry) => buildSearchIndexEntry(innerEntry, entry.i18nKey)) return extension.topics.map((entry) => {
return buildEntry(extension.i18nKey + '.' + entry.i18nKey, extension.i18nKey)
})
} else {
return buildEntry(extension.i18nKey, extension.i18nKey)
} }
return buildSearchIndexEntry(entry)
}), }),
[buildSearchIndexEntry, allCheatsheetExtensions] [allCheatsheetExtensions, buildEntry]
) )
const searchResults = useDocumentSearch(cheatsheetSearchIndexEntries, searchOptions, searchTerm)
const searchResults = useCheatsheetSearch(cheatsheetSearchIndexEntries, searchTerm)
useEffect(() => { useEffect(() => {
if (searchTerm.trim() === '') {
setVisibleExtensions(allCheatsheetExtensions)
return
}
const mappedResults = searchResults.flatMap((result) => result.result)
const extensionResults = allCheatsheetExtensions.filter((extension) => { const extensionResults = allCheatsheetExtensions.filter((extension) => {
return mappedResults.includes(extension.i18nKey) return (
searchResults.find((result) => {
return result.extensionId === extension.i18nKey
}) !== undefined
)
}) })
setVisibleExtensions(extensionResults) setVisibleExtensions(extensionResults)
}, [allCheatsheetExtensions, searchResults, searchTerm, setVisibleExtensions]) }, [allCheatsheetExtensions, searchResults, searchTerm, setVisibleExtensions])

View file

@ -3,16 +3,16 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension' import type { CheatsheetExtension, CheatsheetEntry } from './cheatsheet-extension'
import { hasCheatsheetTopics } from './cheatsheet-extension' import { isCheatsheetMultiEntry } from './cheatsheet-extension'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Button, ButtonGroup, ListGroupItem } from 'react-bootstrap' import { Button, ButtonGroup, ListGroupItem } from 'react-bootstrap'
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
interface EntrySelectionProps { interface EntrySelectionProps {
extension: CheatsheetExtension | undefined extension: CheatsheetExtension | undefined
selectedEntry: CheatsheetSingleEntry | undefined selectedEntry: CheatsheetEntry | undefined
setSelectedEntry: (value: CheatsheetSingleEntry) => void setSelectedEntry: (value: CheatsheetEntry) => void
} }
/** /**
@ -25,7 +25,7 @@ interface EntrySelectionProps {
*/ */
export const TopicSelection: React.FC<EntrySelectionProps> = ({ extension, selectedEntry, setSelectedEntry }) => { export const TopicSelection: React.FC<EntrySelectionProps> = ({ extension, selectedEntry, setSelectedEntry }) => {
const listItems = useMemo(() => { const listItems = useMemo(() => {
if (!hasCheatsheetTopics(extension)) { if (!isCheatsheetMultiEntry(extension)) {
return null return null
} }
return extension.topics.map((entry) => ( return extension.topics.map((entry) => (

View file

@ -5,7 +5,7 @@
*/ */
import { allAppExtensions } from '../../extensions/all-app-extensions' import { allAppExtensions } from '../../extensions/all-app-extensions'
import type { CheatsheetExtensionComponentProps } from './cheatsheet-extension' import type { CheatsheetExtensionComponentProps } from './cheatsheet-extension'
import { hasCheatsheetTopics } from './cheatsheet-extension' import { isCheatsheetMultiEntry } from './cheatsheet-extension'
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import React, { Fragment, useMemo } from 'react' import React, { Fragment, useMemo } from 'react'
@ -20,7 +20,7 @@ export const useComponentsFromAppExtensions = (
<Fragment key={'app-extensions'}> <Fragment key={'app-extensions'}>
{allAppExtensions {allAppExtensions
.flatMap((extension) => extension.buildCheatsheetExtensions()) .flatMap((extension) => extension.buildCheatsheetExtensions())
.flatMap((extension) => (hasCheatsheetTopics(extension) ? extension.topics : extension)) .flatMap((extension) => (isCheatsheetMultiEntry(extension) ? extension.topics : extension))
.map((extension) => { .map((extension) => {
if (extension.cheatsheetExtensionComponent) { if (extension.cheatsheetExtensionComponent) {
return React.createElement(extension.cheatsheetExtensionComponent, { key: extension.i18nKey, setContent }) return React.createElement(extension.cheatsheetExtensionComponent, { key: extension.i18nKey, setContent })

View file

@ -22,7 +22,7 @@ export class AlertAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'alert' }] return [{ i18nKey: 'alert', categoryI18nKey: 'other' }]
} }
buildAutocompletion(): CompletionSource[] { buildAutocompletion(): CompletionSource[] {

View file

@ -20,19 +20,19 @@ export class BasicMarkdownSyntaxAppExtension extends AppExtension {
return [ return [
{ {
i18nKey: 'basics.basicFormatting', i18nKey: 'basics.basicFormatting',
categoryI18nKey: 'basic' categoryI18nKey: 'basics'
}, },
{ {
i18nKey: 'basics.abbreviation', i18nKey: 'basics.abbreviation',
categoryI18nKey: 'basic' categoryI18nKey: 'basics'
}, },
{ {
i18nKey: 'basics.footnote', i18nKey: 'basics.footnote',
categoryI18nKey: 'basic' categoryI18nKey: 'basics'
}, },
{ {
i18nKey: 'basics.headlines', i18nKey: 'basics.headlines',
categoryI18nKey: 'basic', categoryI18nKey: 'basics',
topics: [ topics: [
{ {
i18nKey: 'hashtag' i18nKey: 'hashtag'
@ -44,22 +44,43 @@ export class BasicMarkdownSyntaxAppExtension extends AppExtension {
}, },
{ {
i18nKey: 'basics.code', i18nKey: 'basics.code',
categoryI18nKey: 'basic', categoryI18nKey: 'basics',
topics: [{ i18nKey: 'inline' }, { i18nKey: 'block' }] topics: [
{
i18nKey: 'inline'
},
{
i18nKey: 'block'
}
]
}, },
{ {
i18nKey: 'basics.lists', i18nKey: 'basics.lists',
categoryI18nKey: 'basic', categoryI18nKey: 'basics',
topics: [{ i18nKey: 'unordered' }, { i18nKey: 'ordered' }] topics: [
{
i18nKey: 'unordered'
},
{
i18nKey: 'ordered'
}
]
}, },
{ {
i18nKey: 'basics.images', i18nKey: 'basics.images',
categoryI18nKey: 'basic', categoryI18nKey: 'basics',
topics: [{ i18nKey: 'basic' }, { i18nKey: 'size' }] topics: [
{
i18nKey: 'basic'
},
{
i18nKey: 'size'
}
]
}, },
{ {
i18nKey: 'basics.links', i18nKey: 'basics.links',
categoryI18nKey: 'basic' categoryI18nKey: 'basics'
} }
] ]
} }

View file

@ -22,7 +22,13 @@ export class BlockquoteAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'blockquoteTags', topics: [{ i18nKey: 'name' }, { i18nKey: 'color' }, { i18nKey: 'time' }] }] return [
{
i18nKey: 'blockquoteTags',
categoryI18nKey: 'other',
topics: [{ i18nKey: 'name' }, { i18nKey: 'color' }, { i18nKey: 'time' }]
}
]
} }
buildAutocompletion(): CompletionSource[] { buildAutocompletion(): CompletionSource[] {

View file

@ -20,7 +20,9 @@ export class BootstrapIconAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'bootstrapIcon', readMoreUrl: new URL('https://icons.getbootstrap.com/') }] return [
{ i18nKey: 'bootstrapIcon', categoryI18nKey: 'other', readMoreUrl: new URL('https://icons.getbootstrap.com/') }
]
} }
buildAutocompletion(): CompletionSource[] { buildAutocompletion(): CompletionSource[] {

View file

@ -22,7 +22,7 @@ export class CsvTableAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'csv', topics: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }] return [{ i18nKey: 'csv', categoryI18nKey: 'other', topics: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }]
} }
buildAutocompletion(): CompletionSource[] { buildAutocompletion(): CompletionSource[] {

View file

@ -21,6 +21,7 @@ export class EmojiAppExtension extends AppExtension {
return [ return [
{ {
i18nKey: 'emoji', i18nKey: 'emoji',
categoryI18nKey: 'other',
readMoreUrl: new URL('https://twemoji.twitter.com/') readMoreUrl: new URL('https://twemoji.twitter.com/')
} }
] ]

View file

@ -24,6 +24,7 @@ export class HighlightedCodeFenceAppExtension extends AppExtension {
return [ return [
{ {
i18nKey: 'codeHighlighting', i18nKey: 'codeHighlighting',
categoryI18nKey: 'other',
topics: [{ i18nKey: 'language' }, { i18nKey: 'lineNumbers' }, { i18nKey: 'lineWrapping' }] topics: [{ i18nKey: 'language' }, { i18nKey: 'lineNumbers' }, { i18nKey: 'lineWrapping' }]
} }
] ]

View file

@ -19,7 +19,8 @@ export class ImagePlaceholderAppExtension extends AppExtension {
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [ return [
{ {
i18nKey: 'imagePlaceholder' i18nKey: 'imagePlaceholder',
categoryI18nKey: 'other'
} }
] ]
} }

View file

@ -24,7 +24,7 @@ export class SpoilerAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'spoiler' }] return [{ i18nKey: 'spoiler', categoryI18nKey: 'other' }]
} }
buildAutocompletion(): CompletionSource[] { buildAutocompletion(): CompletionSource[] {

View file

@ -21,6 +21,7 @@ export class TableOfContentsAppExtension extends AppExtension {
return [ return [
{ {
i18nKey: 'toc', i18nKey: 'toc',
categoryI18nKey: 'other',
topics: [ topics: [
{ {
i18nKey: 'basic' i18nKey: 'basic'

View file

@ -26,6 +26,6 @@ export class TaskListCheckboxAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'taskList', cheatsheetExtensionComponent: SetCheckboxInCheatsheet }] return [{ i18nKey: 'taskList', categoryI18nKey: 'other', cheatsheetExtensionComponent: SetCheckboxInCheatsheet }]
} }
} }

View file

@ -19,6 +19,6 @@ export class KatexAppExtension extends AppExtension {
} }
buildCheatsheetExtensions(): CheatsheetExtension[] { buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'katex', readMoreUrl: new URL('https://katex.org/') }] return [{ i18nKey: 'katex', categoryI18nKey: 'other', readMoreUrl: new URL('https://katex.org/') }]
} }
} }

View file

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { CheatsheetSearchIndexEntry } from './use-cheatsheet-search'
import { useCheatsheetSearch } from './use-cheatsheet-search'
import { renderHook, waitFor } from '@testing-library/react'
describe('useDocumentSearch', () => {
const searchEntries: CheatsheetSearchIndexEntry[] = [
{
id: 'test1',
extensionId: 'test1',
title: 'title1',
description: 'description1',
example: 'example1'
},
{
id: 'test2',
extensionId: 'test2',
title: 'title2 sub',
description: 'description2',
example: 'example2'
},
{
id: 'test3',
extensionId: 'test3',
title: 'title3 sub',
description: 'description3',
example: 'example3'
}
]
it('returns all entries if no search term is given', async () => {
const { result } = renderHook((searchTerm: string) => useCheatsheetSearch(searchEntries, searchTerm), {
initialProps: ''
})
await waitFor(() => expect(result.current).toStrictEqual(searchEntries))
})
it('results no entries if nothing has been found', async () => {
const { result } = renderHook((searchTerm: string) => useCheatsheetSearch(searchEntries, searchTerm), {
initialProps: 'Foo'
})
await waitFor(() => expect(result.current).toHaveLength(0))
})
it('returns multiple entries if matching', async () => {
const { result } = renderHook((searchTerm: string) => useCheatsheetSearch(searchEntries, searchTerm), {
initialProps: 'sub'
})
await waitFor(() => expect(result.current).toHaveLength(2))
})
})

View file

@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect, useState } from 'react'
import { create, insert, search } from '@orama/orama'
import { useAsync } from 'react-use'
import { Logger } from '../../utils/logger'
export interface CheatsheetSearchIndexEntry {
readonly id: string
readonly title: string
readonly description: string
readonly example: string
readonly extensionId: string
}
const logger = new Logger('Cheatsheet Search')
/**
* Generate document search index and provide functions to search.
*
* @param entries The list of entries to build the search index from
* @param searchTerm What to search for
* @return An array of the search results
*/
export const useCheatsheetSearch = (
entries: CheatsheetSearchIndexEntry[],
searchTerm: string
): CheatsheetSearchIndexEntry[] => {
const [results, setResults] = useState<CheatsheetSearchIndexEntry[]>([])
const {
value: searchIndex,
loading: searchIndexLoading,
error: searchIndexError
} = useAsync(async () => {
const db = await create({
schema: {
id: 'string',
title: 'string',
description: 'string',
example: 'string',
extensionId: 'string'
} as const
})
const adds = entries.map((entry) => {
logger.debug('Add to search entry:', entry)
return insert(db, entry)
})
await Promise.all(adds)
return db
}, [entries])
useEffect(() => {
if (searchIndexLoading || searchIndexError !== undefined || searchIndex === undefined || searchTerm === '') {
return setResults(entries)
}
search(searchIndex, {
term: searchTerm,
tolerance: 1,
properties: ['title', 'description', 'example'],
boost: {
title: 3,
description: 2,
example: 1
}
})
.then((results) => {
setResults(results.hits.map((entry) => entry.document))
})
.catch((error) => {
logger.error(error)
})
}, [entries, searchIndexError, searchIndexLoading, searchIndex, searchTerm])
return results
}

View file

@ -1,64 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { SearchIndexEntry } from './use-document-search'
import { useDocumentSearch } from './use-document-search'
import { renderHook } from '@testing-library/react'
describe('useDocumentSearch', () => {
interface TestSearchIndexEntry extends SearchIndexEntry {
name: string
text: string
}
const searchOptions = {
document: {
id: 'id',
field: ['name', 'text']
}
}
const searchEntries: TestSearchIndexEntry[] = [
{
id: 1,
name: 'Foo',
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean fermentum odio in bibendum venenatis. Cras aliquet ultrices finibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
},
{
id: 2,
name: 'Bar',
text: 'Vivamus sed mauris eget magna sodales blandit. Aliquam tincidunt nunc et sapien scelerisque placerat. Pellentesque a orci ac risus molestie suscipit id vel arcu.'
},
{
id: 3,
name: 'Cras',
text: 'Cras consectetur sit amet tortor eget sollicitudin. Ut convallis orci ipsum, eget dignissim nibh dignissim eget. Nunc commodo est neque, eget venenatis urna condimentum eget. Suspendisse dapibus ligula et enim venenatis hendrerit. '
}
]
it('results get populated', () => {
const { result, rerender } = renderHook(
(searchTerm: string) => useDocumentSearch(searchEntries, searchOptions, searchTerm),
{
initialProps: ''
}
)
rerender('Foo')
expect(result.current).toHaveLength(1)
expect(result.current[0]).toEqual({ field: 'name', result: [1] })
})
it('finds in multiple fields', () => {
const { result, rerender } = renderHook(
(searchTerm: string) => useDocumentSearch(searchEntries, searchOptions, searchTerm),
{
initialProps: ''
}
)
rerender('Cras')
expect(result.current).toHaveLength(2)
expect(result.current[0]).toEqual({ field: 'name', result: [3] })
expect(result.current[1]).toEqual({ field: 'text', result: [3, 1] })
})
})

View file

@ -1,43 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { IndexOptionsForDocumentSearch, Id, SimpleDocumentSearchResultSetUnit, StoreOption } from 'flexsearch-ts'
import { Document } from 'flexsearch-ts'
import { useEffect, useMemo, useState } from 'react'
export interface SearchIndexEntry {
id: Id
}
/**
* Generate document search index and provide functions to search.
*
* @param entries The list of entries to built the search index from
* @param options Options for the search index
* @param searchTerm What to search for
* @return An array of the search results
*/
export const useDocumentSearch = <T extends SearchIndexEntry>(
entries: Array<T>,
options: IndexOptionsForDocumentSearch<T, StoreOption>,
searchTerm: string
): SimpleDocumentSearchResultSetUnit[] => {
const [results, setResults] = useState<SimpleDocumentSearchResultSetUnit[]>([])
const searchIndex = useMemo(() => {
const index = new Document<T, StoreOption>({
tokenize: 'full',
...options
})
entries.forEach((entry) => {
index.add(entry)
})
return index
}, [entries, options])
useEffect(() => {
setResults(searchIndex.search(searchTerm))
}, [searchIndex, searchTerm])
return results
}

View file

@ -2441,6 +2441,7 @@ __metadata:
"@hedgedoc/markdown-it-plugins": "workspace:markdown-it-plugins" "@hedgedoc/markdown-it-plugins": "workspace:markdown-it-plugins"
"@mrdrogdrog/optional": "npm:1.2.1" "@mrdrogdrog/optional": "npm:1.2.1"
"@next/bundle-analyzer": "npm:13.4.19" "@next/bundle-analyzer": "npm:13.4.19"
"@orama/orama": "npm:2.0.0-beta.12"
"@react-hook/resize-observer": "npm:1.2.6" "@react-hook/resize-observer": "npm:1.2.6"
"@redux-devtools/core": "npm:3.13.3" "@redux-devtools/core": "npm:3.13.3"
"@reduxjs/toolkit": "npm:1.9.7" "@reduxjs/toolkit": "npm:1.9.7"
@ -2501,7 +2502,6 @@ __metadata:
eventemitter2: "npm:6.4.9" eventemitter2: "npm:6.4.9"
fast-deep-equal: "npm:3.1.3" fast-deep-equal: "npm:3.1.3"
firacode: "npm:6.2.0" firacode: "npm:6.2.0"
flexsearch-ts: "npm:0.7.35"
flowchart.js: "npm:1.17.1" flowchart.js: "npm:1.17.1"
highlight.js: "npm:11.9.0" highlight.js: "npm:11.9.0"
htmlparser2: "npm:9.0.0" htmlparser2: "npm:9.0.0"
@ -3786,6 +3786,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@orama/orama@npm:2.0.0-beta.12":
version: 2.0.0-beta.12
resolution: "@orama/orama@npm:2.0.0-beta.12"
checksum: 666a1d2326d181c384a302d77c45635e68ba26260b89ab022af264bfbd6bef4b1213f691fc9020fe570dd272be19208685707c588260072d46a633ccf5e0b73c
languageName: node
linkType: hard
"@pkgjs/parseargs@npm:^0.11.0": "@pkgjs/parseargs@npm:^0.11.0":
version: 0.11.0 version: 0.11.0
resolution: "@pkgjs/parseargs@npm:0.11.0" resolution: "@pkgjs/parseargs@npm:0.11.0"
@ -10020,13 +10027,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"flexsearch-ts@npm:0.7.35":
version: 0.7.35
resolution: "flexsearch-ts@npm:0.7.35"
checksum: 0fc80ddecd016adbd477955d057f58fe3082aa57afe68c7274e4878a87995fae6fcb29287864e6895bdb012b81c02cff99e7894ef398a798eb2028abfa95e760
languageName: node
linkType: hard
"flowchart.js@npm:1.17.1": "flowchart.js@npm:1.17.1":
version: 1.17.1 version: 1.17.1
resolution: "flowchart.js@npm:1.17.1" resolution: "flowchart.js@npm:1.17.1"