mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 09:16:30 -05:00
fix(deps): replace flexsearch-ts with orama
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
931ce68a32
commit
07162b7807
25 changed files with 266 additions and 215 deletions
|
@ -681,7 +681,7 @@
|
|||
"noSelection": "Select an entry on the left side to show the instructions."
|
||||
},
|
||||
"categories": {
|
||||
"basic": "Basics",
|
||||
"basics": "Basics",
|
||||
"other": "Other",
|
||||
"embedding": "Embedding",
|
||||
"charts": "Charts & Diagrams"
|
||||
|
|
|
@ -19,9 +19,9 @@
|
|||
"test:e2e:open": "cypress open",
|
||||
"test:e2e": "cypress run --browser chrome",
|
||||
"test:e2e:ci": "cypress run --browser chrome --record true --parallel --group \"chrome\"",
|
||||
"test:watch": "cross-env NODE_ENV=test jest --watch",
|
||||
"test:ci": "cross-env NODE_ENV=test jest --coverage",
|
||||
"test": "cross-env NODE_ENV=test jest"
|
||||
"test:watch": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest --watch",
|
||||
"test:ci": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest --coverage",
|
||||
"test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" NODE_ENV=test jest"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
@ -53,6 +53,7 @@
|
|||
"@hedgedoc/html-to-react": "workspace:html-to-react",
|
||||
"@hedgedoc/markdown-it-plugins": "workspace:markdown-it-plugins",
|
||||
"@mrdrogdrog/optional": "1.2.1",
|
||||
"@orama/orama": "2.0.0-beta.12",
|
||||
"@react-hook/resize-observer": "1.2.6",
|
||||
"@redux-devtools/core": "3.13.3",
|
||||
"@reduxjs/toolkit": "1.9.7",
|
||||
|
@ -75,7 +76,6 @@
|
|||
"eventemitter2": "6.4.9",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"firacode": "6.2.0",
|
||||
"flexsearch-ts": "0.7.35",
|
||||
"flowchart.js": "1.17.1",
|
||||
"highlight.js": "11.9.0",
|
||||
"htmlparser2": "9.0.0",
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
import { CategoryAccordion } from './category-accordion'
|
||||
import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
|
||||
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension'
|
||||
import { hasCheatsheetTopics } from './cheatsheet-extension'
|
||||
import type { CheatsheetExtension, CheatsheetEntry } from './cheatsheet-extension'
|
||||
import { isCheatsheetMultiEntry } from './cheatsheet-extension'
|
||||
import { CheatsheetSearch } from './cheatsheet-search'
|
||||
import styles from './cheatsheet.module.scss'
|
||||
import { TopicSelection } from './topic-selection'
|
||||
|
@ -20,11 +20,11 @@ import { Trans } from 'react-i18next'
|
|||
export const CheatsheetContent: React.FC = () => {
|
||||
const [visibleExtensions, setVisibleExtensions] = useState<CheatsheetExtension[]>([])
|
||||
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
|
||||
const [selectedEntry, setSelectedEntry] = useState<CheatsheetSingleEntry>()
|
||||
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
|
||||
|
||||
const changeExtension = useCallback((value: CheatsheetExtension) => {
|
||||
setSelectedExtension(value)
|
||||
setSelectedEntry(hasCheatsheetTopics(value) ? value.topics[0] : value)
|
||||
setSelectedEntry(isCheatsheetMultiEntry(value) ? value.topics[0] : value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
@ -44,10 +44,10 @@ export const CheatsheetContent: React.FC = () => {
|
|||
selectedEntry={selectedEntry}
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
/>
|
||||
{selectedEntry !== undefined ? (
|
||||
{selectedEntry !== undefined && selectedExtension !== undefined ? (
|
||||
<CheatsheetEntryPane
|
||||
rootI18nKey={hasCheatsheetTopics(selectedExtension) ? selectedExtension.i18nKey : undefined}
|
||||
extension={selectedEntry}
|
||||
i18nKeyPrefix={isCheatsheetMultiEntry(selectedExtension) ? selectedExtension.i18nKey : undefined}
|
||||
entry={selectedEntry}
|
||||
/>
|
||||
) : (
|
||||
<span>
|
||||
|
|
|
@ -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 { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter'
|
||||
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 { useComponentsFromAppExtensions } from './use-components-from-app-extensions'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
@ -18,8 +18,8 @@ import { ListGroupItem } from 'react-bootstrap'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
interface CheatsheetRendererProps {
|
||||
rootI18nKey?: string
|
||||
extension: CheatsheetSingleEntry
|
||||
i18nKeyPrefix?: string
|
||||
entry: CheatsheetEntry
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,7 +28,7 @@ interface CheatsheetRendererProps {
|
|||
* @param extension The extension to render
|
||||
* @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 [content, setContent] = useState('')
|
||||
|
@ -36,13 +36,13 @@ export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extensi
|
|||
const lines = useMemo(() => content.split('\n'), [content])
|
||||
|
||||
const i18nPrefix = useMemo(
|
||||
() => `cheatsheet.${rootI18nKey ? `${rootI18nKey}.` : ''}${extension.i18nKey}.`,
|
||||
[extension.i18nKey, rootI18nKey]
|
||||
() => `cheatsheet.${i18nKeyPrefix !== undefined ? i18nKeyPrefix + '.' : ''}${entry.i18nKey}.`,
|
||||
[entry.i18nKey, i18nKeyPrefix]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setContent(t(`${i18nPrefix}example`) ?? '')
|
||||
}, [extension, i18nPrefix, t])
|
||||
}, [entry, i18nPrefix, t])
|
||||
|
||||
const cheatsheetExtensionComponents = useComponentsFromAppExtensions(setContent)
|
||||
|
||||
|
@ -62,7 +62,7 @@ export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extensi
|
|||
</h4>
|
||||
{descriptionElements}
|
||||
</ListGroupItem>
|
||||
<ReadMoreLinkItem url={extension.readMoreUrl}></ReadMoreLinkItem>
|
||||
<ReadMoreLinkItem url={entry.readMoreUrl}></ReadMoreLinkItem>
|
||||
<ListGroupItem>
|
||||
<h4>
|
||||
<Trans i18nKey={'cheatsheet.modal.headlines.exampleInput'} />
|
||||
|
|
|
@ -9,29 +9,28 @@ export interface CheatsheetExtensionComponentProps {
|
|||
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
|
||||
* @return boolean
|
||||
*/
|
||||
export const hasCheatsheetTopics = (
|
||||
export const isCheatsheetMultiEntry = (
|
||||
extension: CheatsheetExtension | undefined
|
||||
): extension is CheatsheetEntryWithTopics => {
|
||||
return (extension as CheatsheetEntryWithTopics)?.topics !== undefined
|
||||
): extension is CheatsheetMultiEntry => {
|
||||
return extension !== undefined && typeof (extension as CheatsheetMultiEntry).topics === 'object'
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
export interface CheatsheetMultiEntry {
|
||||
i18nKey: string
|
||||
categoryI18nKey?: string
|
||||
topics: CheatsheetSingleEntry[]
|
||||
categoryI18nKey: string
|
||||
topics: CheatsheetEntry[]
|
||||
}
|
||||
|
||||
export interface CheatsheetSingleEntry extends CheatsheetEntry {
|
||||
categoryI18nKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,10 +41,8 @@ export interface CheatsheetEntryWithTopics {
|
|||
*
|
||||
* e.g 'basics.basicFormatting'
|
||||
*/
|
||||
export interface CheatsheetSingleEntry {
|
||||
export interface CheatsheetEntry {
|
||||
i18nKey: string
|
||||
categoryI18nKey?: string
|
||||
cheatsheetExtensionComponent?: React.FC<CheatsheetExtensionComponentProps>
|
||||
|
||||
readMoreUrl?: URL
|
||||
}
|
||||
|
|
|
@ -4,33 +4,18 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { allAppExtensions } from '../../extensions/all-app-extensions'
|
||||
import type { SearchIndexEntry } from '../../hooks/common/use-document-search'
|
||||
import { useDocumentSearch } from '../../hooks/common/use-document-search'
|
||||
import { useCheatsheetSearch } from '../../hooks/common/use-cheatsheet-search'
|
||||
import { useOnInputChange } from '../../hooks/common/use-on-input-change'
|
||||
import { useTranslatedText } from '../../hooks/common/use-translated-text'
|
||||
import { UiIcon } from '../common/icons/ui-icon'
|
||||
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension'
|
||||
import { hasCheatsheetTopics } from './cheatsheet-extension'
|
||||
import type { CheatsheetExtension } from './cheatsheet-extension'
|
||||
import { isCheatsheetMultiEntry } from './cheatsheet-extension'
|
||||
import styles from './cheatsheet.module.scss'
|
||||
import type { IndexOptionsForDocumentSearch, StoreOption } from 'flexsearch-ts'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { FormControl, InputGroup } from 'react-bootstrap'
|
||||
import { X } from 'react-bootstrap-icons'
|
||||
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 {
|
||||
setVisibleExtensions: React.Dispatch<React.SetStateAction<CheatsheetExtension[]>>
|
||||
}
|
||||
|
@ -48,38 +33,43 @@ export const CheatsheetSearch: React.FC<CheatsheetSearchProps> = ({ setVisibleEx
|
|||
() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
|
||||
[]
|
||||
)
|
||||
const buildSearchIndexEntry = useCallback(
|
||||
(entry: CheatsheetSingleEntry, rootI18nKey: string | undefined = undefined): CheatsheetSearchIndexEntry => {
|
||||
const rootI18nKeyWithDot = rootI18nKey ? `${rootI18nKey}.` : ''
|
||||
const placeholderText = useTranslatedText('cheatsheet.search')
|
||||
|
||||
const buildEntry = useCallback(
|
||||
(i18nKey: string, extensionId: string) => {
|
||||
return {
|
||||
id: rootI18nKey ? rootI18nKey : entry.i18nKey,
|
||||
title: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.title`),
|
||||
description: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.description`),
|
||||
example: t(`cheatsheet.${rootI18nKeyWithDot}${entry.i18nKey}.example`)
|
||||
id: i18nKey,
|
||||
extensionId: extensionId,
|
||||
title: t(`cheatsheet.${i18nKey}.title`),
|
||||
description: t(`cheatsheet.${i18nKey}.description`),
|
||||
example: t(`cheatsheet.${i18nKey}.example`)
|
||||
}
|
||||
},
|
||||
[t]
|
||||
)
|
||||
const placeholderText = useTranslatedText('cheatsheet.search')
|
||||
|
||||
const cheatsheetSearchIndexEntries = useMemo(
|
||||
() =>
|
||||
allCheatsheetExtensions.flatMap((entry) => {
|
||||
if (hasCheatsheetTopics(entry)) {
|
||||
return entry.topics.map((innerEntry) => buildSearchIndexEntry(innerEntry, entry.i18nKey))
|
||||
allCheatsheetExtensions.flatMap((extension) => {
|
||||
if (isCheatsheetMultiEntry(extension)) {
|
||||
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(() => {
|
||||
if (searchTerm.trim() === '') {
|
||||
setVisibleExtensions(allCheatsheetExtensions)
|
||||
return
|
||||
}
|
||||
const mappedResults = searchResults.flatMap((result) => result.result)
|
||||
const extensionResults = allCheatsheetExtensions.filter((extension) => {
|
||||
return mappedResults.includes(extension.i18nKey)
|
||||
return (
|
||||
searchResults.find((result) => {
|
||||
return result.extensionId === extension.i18nKey
|
||||
}) !== undefined
|
||||
)
|
||||
})
|
||||
setVisibleExtensions(extensionResults)
|
||||
}, [allCheatsheetExtensions, searchResults, searchTerm, setVisibleExtensions])
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { CheatsheetSingleEntry, CheatsheetExtension } from './cheatsheet-extension'
|
||||
import { hasCheatsheetTopics } from './cheatsheet-extension'
|
||||
import type { CheatsheetExtension, CheatsheetEntry } from './cheatsheet-extension'
|
||||
import { isCheatsheetMultiEntry } from './cheatsheet-extension'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Button, ButtonGroup, ListGroupItem } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
interface EntrySelectionProps {
|
||||
extension: CheatsheetExtension | undefined
|
||||
selectedEntry: CheatsheetSingleEntry | undefined
|
||||
setSelectedEntry: (value: CheatsheetSingleEntry) => void
|
||||
selectedEntry: CheatsheetEntry | undefined
|
||||
setSelectedEntry: (value: CheatsheetEntry) => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,7 +25,7 @@ interface EntrySelectionProps {
|
|||
*/
|
||||
export const TopicSelection: React.FC<EntrySelectionProps> = ({ extension, selectedEntry, setSelectedEntry }) => {
|
||||
const listItems = useMemo(() => {
|
||||
if (!hasCheatsheetTopics(extension)) {
|
||||
if (!isCheatsheetMultiEntry(extension)) {
|
||||
return null
|
||||
}
|
||||
return extension.topics.map((entry) => (
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { allAppExtensions } from '../../extensions/all-app-extensions'
|
||||
import type { CheatsheetExtensionComponentProps } from './cheatsheet-extension'
|
||||
import { hasCheatsheetTopics } from './cheatsheet-extension'
|
||||
import { isCheatsheetMultiEntry } from './cheatsheet-extension'
|
||||
import type { ReactElement } from 'react'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
|
||||
|
@ -20,7 +20,7 @@ export const useComponentsFromAppExtensions = (
|
|||
<Fragment key={'app-extensions'}>
|
||||
{allAppExtensions
|
||||
.flatMap((extension) => extension.buildCheatsheetExtensions())
|
||||
.flatMap((extension) => (hasCheatsheetTopics(extension) ? extension.topics : extension))
|
||||
.flatMap((extension) => (isCheatsheetMultiEntry(extension) ? extension.topics : extension))
|
||||
.map((extension) => {
|
||||
if (extension.cheatsheetExtensionComponent) {
|
||||
return React.createElement(extension.cheatsheetExtensionComponent, { key: extension.i18nKey, setContent })
|
||||
|
|
|
@ -22,7 +22,7 @@ export class AlertAppExtension extends AppExtension {
|
|||
}
|
||||
|
||||
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
||||
return [{ i18nKey: 'alert' }]
|
||||
return [{ i18nKey: 'alert', categoryI18nKey: 'other' }]
|
||||
}
|
||||
|
||||
buildAutocompletion(): CompletionSource[] {
|
||||
|
|
|
@ -20,19 +20,19 @@ export class BasicMarkdownSyntaxAppExtension extends AppExtension {
|
|||
return [
|
||||
{
|
||||
i18nKey: 'basics.basicFormatting',
|
||||
categoryI18nKey: 'basic'
|
||||
categoryI18nKey: 'basics'
|
||||
},
|
||||
{
|
||||
i18nKey: 'basics.abbreviation',
|
||||
categoryI18nKey: 'basic'
|
||||
categoryI18nKey: 'basics'
|
||||
},
|
||||
{
|
||||
i18nKey: 'basics.footnote',
|
||||
categoryI18nKey: 'basic'
|
||||
categoryI18nKey: 'basics'
|
||||
},
|
||||
{
|
||||
i18nKey: 'basics.headlines',
|
||||
categoryI18nKey: 'basic',
|
||||
categoryI18nKey: 'basics',
|
||||
topics: [
|
||||
{
|
||||
i18nKey: 'hashtag'
|
||||
|
@ -44,22 +44,43 @@ export class BasicMarkdownSyntaxAppExtension extends AppExtension {
|
|||
},
|
||||
{
|
||||
i18nKey: 'basics.code',
|
||||
categoryI18nKey: 'basic',
|
||||
topics: [{ i18nKey: 'inline' }, { i18nKey: 'block' }]
|
||||
categoryI18nKey: 'basics',
|
||||
topics: [
|
||||
{
|
||||
i18nKey: 'inline'
|
||||
},
|
||||
{
|
||||
i18nKey: 'block'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
i18nKey: 'basics.lists',
|
||||
categoryI18nKey: 'basic',
|
||||
topics: [{ i18nKey: 'unordered' }, { i18nKey: 'ordered' }]
|
||||
categoryI18nKey: 'basics',
|
||||
topics: [
|
||||
{
|
||||
i18nKey: 'unordered'
|
||||
},
|
||||
{
|
||||
i18nKey: 'ordered'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
i18nKey: 'basics.images',
|
||||
categoryI18nKey: 'basic',
|
||||
topics: [{ i18nKey: 'basic' }, { i18nKey: 'size' }]
|
||||
categoryI18nKey: 'basics',
|
||||
topics: [
|
||||
{
|
||||
i18nKey: 'basic'
|
||||
},
|
||||
{
|
||||
i18nKey: 'size'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
i18nKey: 'basics.links',
|
||||
categoryI18nKey: 'basic'
|
||||
categoryI18nKey: 'basics'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -22,7 +22,13 @@ export class BlockquoteAppExtension extends AppExtension {
|
|||
}
|
||||
|
||||
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[] {
|
||||
|
|
|
@ -20,7 +20,9 @@ export class BootstrapIconAppExtension extends AppExtension {
|
|||
}
|
||||
|
||||
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[] {
|
||||
|
|
|
@ -22,7 +22,7 @@ export class CsvTableAppExtension extends AppExtension {
|
|||
}
|
||||
|
||||
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
||||
return [{ i18nKey: 'csv', topics: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }]
|
||||
return [{ i18nKey: 'csv', categoryI18nKey: 'other', topics: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }]
|
||||
}
|
||||
|
||||
buildAutocompletion(): CompletionSource[] {
|
||||
|
|
|
@ -21,6 +21,7 @@ export class EmojiAppExtension extends AppExtension {
|
|||
return [
|
||||
{
|
||||
i18nKey: 'emoji',
|
||||
categoryI18nKey: 'other',
|
||||
readMoreUrl: new URL('https://twemoji.twitter.com/')
|
||||
}
|
||||
]
|
||||
|
|
|
@ -24,6 +24,7 @@ export class HighlightedCodeFenceAppExtension extends AppExtension {
|
|||
return [
|
||||
{
|
||||
i18nKey: 'codeHighlighting',
|
||||
categoryI18nKey: 'other',
|
||||
topics: [{ i18nKey: 'language' }, { i18nKey: 'lineNumbers' }, { i18nKey: 'lineWrapping' }]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -19,7 +19,8 @@ export class ImagePlaceholderAppExtension extends AppExtension {
|
|||
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
||||
return [
|
||||
{
|
||||
i18nKey: 'imagePlaceholder'
|
||||
i18nKey: 'imagePlaceholder',
|
||||
categoryI18nKey: 'other'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export class SpoilerAppExtension extends AppExtension {
|
|||
}
|
||||
|
||||
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
||||
return [{ i18nKey: 'spoiler' }]
|
||||
return [{ i18nKey: 'spoiler', categoryI18nKey: 'other' }]
|
||||
}
|
||||
|
||||
buildAutocompletion(): CompletionSource[] {
|
||||
|
|
|
@ -21,6 +21,7 @@ export class TableOfContentsAppExtension extends AppExtension {
|
|||
return [
|
||||
{
|
||||
i18nKey: 'toc',
|
||||
categoryI18nKey: 'other',
|
||||
topics: [
|
||||
{
|
||||
i18nKey: 'basic'
|
||||
|
|
|
@ -26,6 +26,6 @@ export class TaskListCheckboxAppExtension extends AppExtension {
|
|||
}
|
||||
|
||||
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
||||
return [{ i18nKey: 'taskList', cheatsheetExtensionComponent: SetCheckboxInCheatsheet }]
|
||||
return [{ i18nKey: 'taskList', categoryI18nKey: 'other', cheatsheetExtensionComponent: SetCheckboxInCheatsheet }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,6 @@ export class KatexAppExtension extends AppExtension {
|
|||
}
|
||||
|
||||
buildCheatsheetExtensions(): CheatsheetExtension[] {
|
||||
return [{ i18nKey: 'katex', readMoreUrl: new URL('https://katex.org/') }]
|
||||
return [{ i18nKey: 'katex', categoryI18nKey: 'other', readMoreUrl: new URL('https://katex.org/') }]
|
||||
}
|
||||
}
|
||||
|
|
58
frontend/src/hooks/common/use-cheatsheet-search.spec.ts
Normal file
58
frontend/src/hooks/common/use-cheatsheet-search.spec.ts
Normal 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))
|
||||
})
|
||||
})
|
80
frontend/src/hooks/common/use-cheatsheet-search.ts
Normal file
80
frontend/src/hooks/common/use-cheatsheet-search.ts
Normal 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
|
||||
}
|
|
@ -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] })
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
16
yarn.lock
16
yarn.lock
|
@ -2441,6 +2441,7 @@ __metadata:
|
|||
"@hedgedoc/markdown-it-plugins": "workspace:markdown-it-plugins"
|
||||
"@mrdrogdrog/optional": "npm:1.2.1"
|
||||
"@next/bundle-analyzer": "npm:13.4.19"
|
||||
"@orama/orama": "npm:2.0.0-beta.12"
|
||||
"@react-hook/resize-observer": "npm:1.2.6"
|
||||
"@redux-devtools/core": "npm:3.13.3"
|
||||
"@reduxjs/toolkit": "npm:1.9.7"
|
||||
|
@ -2501,7 +2502,6 @@ __metadata:
|
|||
eventemitter2: "npm:6.4.9"
|
||||
fast-deep-equal: "npm:3.1.3"
|
||||
firacode: "npm:6.2.0"
|
||||
flexsearch-ts: "npm:0.7.35"
|
||||
flowchart.js: "npm:1.17.1"
|
||||
highlight.js: "npm:11.9.0"
|
||||
htmlparser2: "npm:9.0.0"
|
||||
|
@ -3786,6 +3786,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 0.11.0
|
||||
resolution: "@pkgjs/parseargs@npm:0.11.0"
|
||||
|
@ -10020,13 +10027,6 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.17.1
|
||||
resolution: "flowchart.js@npm:1.17.1"
|
||||
|
|
Loading…
Reference in a new issue