diff --git a/frontend/cypress/e2e/helpDialog.spec.ts b/frontend/cypress/e2e/helpDialog.spec.ts
deleted file mode 100644
index a709ad497..000000000
--- a/frontend/cypress/e2e/helpDialog.spec.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-describe('Help Dialog', () => {
- beforeEach(() => {
- cy.visitTestNote()
- })
-
- it('ToDo-List', () => {
- cy.getByCypressId('editor-help-button').click()
- cy.get('input[type="checkbox"]').should('exist').should('not.be.checked')
- })
-})
diff --git a/frontend/locales/en.json b/frontend/locales/en.json
index f587d4ac3..84de817c9 100644
--- a/frontend/locales/en.json
+++ b/frontend/locales/en.json
@@ -267,13 +267,6 @@
"features": "Features",
"yamlMetadata": "YAML Metadata",
"slideExample": "Slide Example"
- },
- "cheatsheet": {
- "title": "Cheatsheet",
- "example": "Example",
- "syntax": "Syntax",
- "exampleAlert": "This is an alert area.",
- "highlightedText": "Highlight"
}
},
"onlineStatus": {
@@ -630,5 +623,255 @@
"help": "The primary user interface language"
}
}
+ },
+ "cheatsheet": {
+ "button": "Open Cheatsheet",
+ "modal":{
+ "title": "Cheatsheet",
+ "headlines": {
+ "description": "Description",
+ "exampleInput": "Example Input",
+ "exampleOutput": "Example Output",
+ "selectTopic": "Select Topic",
+ "readMoreLink": "Read More"
+ }
+ },
+ "categories": {
+ "basic": "Basics",
+ "other": "Other",
+ "embedding": "Embedding",
+ "charts": "Charts & Diagrams"
+ },
+ "basics": {
+ "formatting": {
+ "title": "Formatting",
+ "basic": {
+ "title": "Basic",
+ "description": "These are the basic markdown formatting rules.",
+ "example": "**Bold**\n__Bold__\n\n*Italic*\n_Italic_\n\n++Underline++\n~~Strikethrough~~\n\nSub~script~~\n\nSuper^script^\n\n==Marked=="
+ },
+ "abbreviation": {
+ "title": "Abbreviation",
+ "description":"",
+ "example": "*[HTML]: Hyper Text Markup Language\n*[W3C]: World Wide Web Consortium\nThe HTML specification\nis maintained by the W3C."
+ },
+ "footnote": {
+ "title": "Footnotes",
+ "description": "",
+ "example": "Here is a footnote reference,[^1] and another.[^longnote]\n\n[^1]: Here is the footnote.\n\n[^longnote]: Here's one with multiple blocks.\n\n Subsequent paragraphs are indented to show that they\nbelong to the previous footnote."
+ }
+ },
+ "headlines": {
+ "title": "Headlines",
+ "hashtag": {
+ "title": "Hashtag",
+ "description": "",
+ "example": "# Headline 1\n\n## Headline 2\n\n### Headline 3\n\n#### Headline 4\n\n##### Headline 5\n\n###### Headline 6"
+ },
+ "equal": {
+ "title": "Equal",
+ "description": "An alternative form to define the first headline is to append a line of only equal-signs.",
+ "example": "Headline 1\n=========="
+ }
+ },
+ "code": {
+ "title": "Code",
+ "inline": {
+ "title": "Inline",
+ "description": "You can define an inline code block by wrapping it in backticks.",
+ "example": "This is `code`."
+ },
+ "block": {
+ "title": "Block",
+ "description": "You can define a code block by putting a line of three backticks before and after the code part.",
+ "example": "```\nthis is a code block\n```"
+ }
+ },
+ "lists": {
+ "title": "Lists",
+ "unordered": {
+ "title": "Unordered",
+ "description": "",
+ "example": "- A\n- B\n- C\n\n* A\n* B\n* C"
+ },
+ "ordered": {
+ "title": "Ordered",
+ "description": "",
+ "example": "1. A\n2. B\n3. C\n\n100. A\n101. B\n102. C"
+ }
+ },
+ "images": {
+ "title": "Images",
+ "basic": {
+ "title": "Basic",
+ "description": "",
+ "example": "![Image](/icons/apple-touch-icon.png)"
+ },
+ "size": {
+ "title": "Size",
+ "description": "",
+ "example": "![Image](/icons/apple-touch-icon.png =100x50)\n\n![Image](/icons/apple-touch-icon.png =60x)\n\n![Image](/icons/apple-touch-icon.png =x50)"
+ }
+ },
+ "links": {
+ "title": "Links",
+ "description": "",
+ "example": "[Example link!](https://example.org)\n\nhttps://example.org"
+ }
+ },
+ "abcjs": {
+ "title": "abcjs",
+ "description": "You can render music sheets with abcjs by using music standard notation in a codeblock with `abc` as language.",
+ "example": "```abc\nX:1\nT:Speed the Plough\nM:4/4\nC:Trad.\nK:G\n|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|\nGABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|\n|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|\ng2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|\n```"
+ },
+ "alert": {
+ "title": "Alert boxes",
+ "description": "Use alert boxes to bring extra attention to parts of your document.",
+ "example": ":::success\nThis is a success\n:::\n\n:::danger\nThis is a danger\n:::\n\n:::warning\nThis is a warning\n:::\n\n:::info\nThis is a info\n:::"
+ },
+ "blockquoteTags": {
+ "name": {
+ "title": "Name",
+ "description": "Use name tags to indicate who wrote a specific quote e.g. if you're commenting on the text.",
+ "example": "> Imagination is more important than knowledge.\n> [name=Albert Einstein]"
+ },
+ "color": {
+ "title": "Color",
+ "description": "Use color tags to tint the border of the quote.",
+ "example": "> This is the default color\n\n> This is red! [color=red]\n\n> This is blue! [color=#0000ff]"
+ },
+ "time": {
+ "title": "Time",
+ "description": "Use time tags to specify when a time stamp",
+ "example": "The password is: changeme\n\n> Please change it ASAP [time=2020-03-05]"
+ },
+ "title": "Blockquote Tags",
+ "description": "Use name, time or color tags to specify your blockquotes. Color tags modify the border color.",
+ "example": "> [name=Max] Named quote\n\n> [time=today] Time quote\n\n> [color=red] quote\n\n> [name=Max] [color=green] [time=today] Combined quote"
+ },
+ "bootstrapIcon": {
+ "title": "Bootstrap Icons",
+ "description": "You can use bootstrap icons in your document by putting the name of an icon between colons but prefixed with `bi-`. A listing of all bootstrap icons can be found on their website.",
+ "example": ":bi-music-note-beamed::bi-music-note-beamed::bi-music-note-beamed:\nAround the :bi-globe-americas:\nAround the :bi-globe-asia-australia:\nAround the :bi-globe-central-south-asia:\nAround the :bi-globe-europe-africa:"
+ },
+ "emoji": {
+ "title": "Emojis",
+ "description": "You can add colored emojis by either placing the unicode character or by using a shortcode. HedgeDoc supports every emoji that Twemoji supports. You can also use the emoji picker in the editor toolbar.",
+ "example": "I :heart: :hedgehog:"
+ },
+ "csv": {
+ "title" : "CSV",
+ "table" : {
+ "title": "Table",
+ "description" : "You can render a CSV text as table by using a code block with `csv` as language. You must specify the delimiter.",
+ "example" : "```csv delimiter=;\nUsername; Identifier;First name;Last name\n\"booker12; rbooker\";9012;Rachel;Booker\ngrey07;2070;Laura;Grey\njohnson81;4081;Craig;Johnson\njenkins46;9346;Mary;Jenkins\nsmith79;5079;Jamie;Smith\n```"
+ },
+ "header": {
+ "title": "Header",
+ "description": "By adding the header keyword you can define that the first line is used as table header",
+ "example": "```csv delimiter=; header\nUsername; Identifier;First name;Last name\n\"booker12; rbooker\";9012;Rachel;Booker\ngrey07;2070;Laura;Grey\njohnson81;4081;Craig;Johnson\njenkins46;9346;Mary;Jenkins\nsmith79;5079;Jamie;Smith\n```"
+ }
+ },
+ "flowchart": {
+ "title": "flowchart.js",
+ "description": "Render flowcharts diagrams using flowchart.js by using a code block with `flow` as language.",
+ "example": "```flow\nst=>start: Start\ne=>end: End\nop=>operation: My Operation\nop2=>operation: lalala\ncond=>condition: Yes or No?\n\nst->op->op2->cond\ncond(yes)->e\ncond(no)->op2\n```"
+ },
+ "gist": {
+ "title": "GitHub Gist",
+ "description": "Embed GitHub Gists by placing a gist URL on a single line.",
+ "example": "# This is my gist\nhttps://gist.github.com/schacon/1\nThis is just the link to a gist: https://gist.github.com/schacon/1\n[Using the link tag will also not trigger the embedding](https://gist.github.com/schacon/1)"
+ },
+ "graphviz": {
+ "title": "GraphViz",
+ "description": "Render GraphViz diagrams by using the DOT language in a code block with `graphviz` as language.",
+ "example": "```graphviz\ngraph {\n a -- b\n a -- b\n b -- a [color=blue]\n}\n```"
+ },
+ "katex": {
+ "title": "KaTeX",
+ "description": "You can render LaTeX mathematical expressions using KaTeX by encapsulating them in dollar signs.",
+ "example": "The *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral\n\n$$\nx = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}.\n$$\n\n$$\n\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,.\n$$"
+ },
+ "asciinema": {
+ "title": "Asciinema",
+ "description": "Embed an Asciinema video by placing the URL in a single line.",
+ "example": "https://asciinema.org/a/117928\n"
+ },
+ "mermaid": {
+ "title": "Mermaid",
+ "description": "Render diagrams and charts using mermaid.js by adding a code block with the language `mermaid`",
+ "example": "```mermaid\ngantt\n title A Gantt Diagram\n\n section Section\n A task: a1, 2014-01-01, 30d\n Another task: after a1, 20d\n\n section Another\n Task in sec: 2014-01-12, 12d\n Another task: 24d\n```"
+ },
+ "imagePlaceholder": {
+ "title": "Image Placeholder",
+ "description": "You can use image placeholders to indicate spots where images should be placed. To do this use an image link with `https://` as URL. You can upload the actual image directly from the renderer. Placeholders also support size definition, alt text and title.",
+ "example": "![This is a placeholder image](https://)"
+ },
+ "iframeCapsule": {
+ "title": "Iframe capsule",
+ "description": "To protect viewers every iframe has to be activated explicitly. Before this, no information is fetched. Adding additional privileges using the sandbox attribute is not allowed.",
+ "example": ""
+ },
+ "plantuml": {
+ "title": "PlantUML",
+ "description": "Render diagrams and charts using PlantUML by adding a code block with the language `plantuml`. PlantUML diagrams are not rendered in your browser like the other charts, but by an external server.",
+ "example": "```plantuml\n@startuml\nparticipant Alice\nparticipant \"The **Famous** Bob\" as Bob\n\nAlice -> Bob : hello --there--\n... Some ~~long delay~~ ...\nBob -> Alice : ok\nnote left\n This is **bold**\n This is //italics//\n This is \"\"monospaced\"\"\n This is --stroked--\n This is __underlined__\n This is ~~waved~~\nend note\n\nAlice -> Bob : A //well formatted// message\nnote right of Alice\n This is displayed\n __left of__ Alice.\nend note\nnote left of Bob\n This is displayed \n **left of Alice Bob**.\nend note\nnote over Alice, Bob\n This is hosted by \nend note\n@enduml\n```"
+ },
+ "spoiler": {
+ "title": "Spoiler",
+ "description": "Hide information by using a spoiler tag.",
+ "example": ":::spoiler This spoiler contains a surprise.\nSURPRISE!\n:::"
+ },
+ "toc": {
+ "title": "Table Of Contents",
+ "basic": {
+ "title": "Basic",
+ "description": "Add a table of contents that is automatically generated using your headlines by adding `[toc]` into a single line.",
+ "example": "[toc]\n\n# This is a first headline\n\n## This is a second headline"
+ },
+ "levelLimit": {
+ "title": "Level limits",
+ "description": "You can limit the levels of the headlines the TOC should show",
+ "example": "[toc:2:3]\n\n# This is a first headline\n\n## This is a second headline\n\n### This is a third headline\n\n#### This is a fourth headline"
+ }
+ },
+ "vegaLite": {
+ "title": "Vega Lite",
+ "description": "Render diagrams and charts using vega lite by adding a code block with the language `vega-lite`",
+ "example": "```vega-lite\n{\n \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.json\",\n \"description\": \"Reproducing http://robslink.com/SAS/democd91/pyramid_pie.htm\",\n \"data\": {\n \"values\": [\n {\"category\": \"Sky\", \"value\": 75, \"order\": 3},\n {\"category\": \"Shady side of a pyramid\", \"value\": 10, \"order\": 1},\n {\"category\": \"Sunny side of a pyramid\", \"value\": 15, \"order\": 2}\n ]\n },\n \"mark\": {\"type\": \"arc\", \"outerRadius\": 80},\n \"encoding\": {\n \"theta\": {\n \"field\": \"value\", \"type\": \"quantitative\",\n \"scale\": {\"range\": [2.35619449, 8.639379797]},\n \"stack\": true\n },\n \"color\": {\n \"field\": \"category\", \"type\": \"nominal\",\n \"scale\": {\n \"domain\": [\"Sky\", \"Shady side of a pyramid\", \"Sunny side of a pyramid\"],\n \"range\": [\"#416D9D\", \"#674028\", \"#DEAC58\"]\n },\n \"legend\": {\n \"orient\": \"none\",\n \"title\": null,\n \"columns\": 1,\n \"legendX\": 200,\n \"legendY\": 80\n }\n },\n \"order\": {\n \"field\": \"order\"\n }\n },\n \"view\": {\"stroke\": null}\n}\n```"
+ },
+ "vimeo": {
+ "title": "Vimeo",
+ "description": "Embed a Vimeo video by placing the URL in a single line.",
+ "example": "https://vimeo.com/23237102"
+ },
+ "youtube": {
+ "title": "YouTube",
+ "description": "Embed a YouTube video by placing the URL in a single line.",
+ "example": "https://www.youtube.com/watch?v=YE7VzlLtp-4"
+ },
+ "taskList": {
+ "title": "Task Lists",
+ "description": "You can turn any listing into a task list by adding brackets. The checkboxes in the rendering change the markdown content if clicked.",
+ "example": "- [ ] ToDos\n - [X] Buy some salad\n - [ ] Brush teeth\n - [x] Drink some water\n - [ ] **Click my box** and see the source code, if you're allowed to edit!\n"
+ },
+ "codeHighlighting": {
+ "title": "Code Highlighting",
+ "language": {
+ "title": "Language",
+ "description": "Specify a language after the start tag of a code block to activate code highlighting.",
+ "example": "```js\nvar s = \"JavaScript syntax highlighting\";\nalert(s);\nfunction $initHighlight(block, cls) {\n try {\n if (cls.search(/\\bno\\-highlight\\b/) != -1)\n return process(block, true, 0x0F) +\n ' class=\"\"';\n } catch (e) {\n /* handle exception */\n }\n for (var i = 0 / 2; i < classes.length; i++) {\n if (checkCondition(classes[i]) === undefined)\n return /\\d+[\\s/]/g;\n }\n}\n```"
+ },
+ "lineWrapping": {
+ "title": "Line wrapping",
+ "description": "Set an exclamation mark to activate line wrapping",
+ "example": "```text\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n```\n\n```text!\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n```"
+ },
+ "lineNumbers": {
+ "title": "Line numbers",
+ "description": "Set a equals sign after the language to show line numbers. You can specify a start line number after the equal sign or a plus to continue the line numbers from the last code block.",
+ "example": "```markdown=12\nline1\n```\n```markdown=+\nline2\n```\n```markdown=\nline3\n```"
+ }
+ }
}
}
diff --git a/frontend/src/components/editor-page/app-bar/app-bar.tsx b/frontend/src/components/editor-page/app-bar/app-bar.tsx
index 8a82673c4..b692a8be5 100644
--- a/frontend/src/components/editor-page/app-bar/app-bar.tsx
+++ b/frontend/src/components/editor-page/app-bar/app-bar.tsx
@@ -10,6 +10,7 @@ import { ShowIf } from '../../common/show-if/show-if'
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
import { SettingsButton } from '../../layout/settings-dialog/settings-button'
+import { CheatsheetButton } from './cheatsheet/cheatsheet-button'
import { HelpButton } from './help-button/help-button'
import { NavbarBranding } from './navbar-branding'
import { ReadOnlyModeButton } from './read-only-mode-button'
@@ -47,6 +48,7 @@ export const AppBar: React.FC = ({ mode }) => {
+
diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/category-accordion.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/category-accordion.tsx
new file mode 100644
index 000000000..5625ec3c2
--- /dev/null
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/category-accordion.tsx
@@ -0,0 +1,73 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import type { CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
+import { EntryList } from './entry-list'
+import React, { useMemo } from 'react'
+import { Accordion } from 'react-bootstrap'
+import { Trans } from 'react-i18next'
+
+export interface GroupAccordionProps {
+ extensions: CheatsheetExtension[]
+ selectedEntry: CheatsheetExtension | undefined
+ onStateChange: (value: CheatsheetExtension) => void
+}
+
+const sortCategories = (
+ [keyA]: [string, CheatsheetExtension[]],
+ [keyB]: [string, CheatsheetExtension[]]
+): -1 | 0 | 1 => {
+ if (keyA === keyB) {
+ return 0
+ } else if (keyA > keyB || keyA === 'other') {
+ return 1
+ } else {
+ return -1
+ }
+}
+
+type CheatsheetGroupMap = Map
+
+const reduceCheatsheetExtensionByCategory = (
+ state: CheatsheetGroupMap,
+ extension: CheatsheetExtension
+): CheatsheetGroupMap => {
+ const groupKey = extension.categoryI18nKey ?? 'other'
+ const list = state.get(groupKey) ?? []
+ list.push(extension)
+ if (!state.has(groupKey)) {
+ state.set(groupKey, list)
+ }
+ return state
+}
+
+/**
+ * Renders {@link EntryList entry lists} grouped by category.
+ *
+ * @param extensions The extensions which should be listed
+ * @param selectedEntry The entry that should be displayed as selected
+ * @param onStateChange A callback that should be executed if a new entry was selected
+ */
+export const CategoryAccordion: React.FC = ({ extensions, selectedEntry, onStateChange }) => {
+ const groupEntries = useMemo(() => {
+ const groupings = extensions.reduce(reduceCheatsheetExtensionByCategory, new Map())
+ return Array.from(groupings.entries()).sort(sortCategories)
+ }, [extensions])
+
+ const elements = useMemo(() => {
+ return groupEntries.map(([groupKey, groupExtensions]) => (
+
+
+
+
+
+
+
+
+ ))
+ }, [groupEntries, onStateChange, selectedEntry])
+
+ return {elements}
+}
diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-button.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-button.tsx
new file mode 100644
index 000000000..b6172327b
--- /dev/null
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-button.tsx
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
+import { cypressId } from '../../../../utils/cypress-attribute'
+import { CommonModal } from '../../../common/modals/common-modal'
+import { CheatsheetModalBody } from './cheatsheet-modal-body'
+import React, { Fragment } from 'react'
+import { Button } from 'react-bootstrap'
+import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
+import { Trans, useTranslation } from 'react-i18next'
+
+export const CheatsheetButton: React.FC = () => {
+ const { t } = useTranslation()
+ const [modalVisibility, showModal, closeModal] = useBooleanState()
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-entry-pane.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-entry-pane.tsx
new file mode 100644
index 000000000..cf4d580ce
--- /dev/null
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-entry-pane.tsx
@@ -0,0 +1,86 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import HighlightedCode from '../../../../extensions/extra-integrations/highlighted-code-fence/highlighted-code'
+import { ExtensionEventEmitterProvider } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
+import { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message'
+import type { CheatsheetEntry } from '../../cheatsheet/cheatsheet-extension'
+import { EditorToRendererCommunicatorContextProvider } from '../../render-context/editor-to-renderer-communicator-context-provider'
+import { RenderIframe } from '../../renderer-pane/render-iframe'
+import { ReadMoreLinkItem } from './read-more-link-item'
+import { useComponentsFromAppExtensions } from './use-components-from-app-extensions'
+import convertHtmlToReact from '@hedgedoc/html-to-react'
+import { sanitize } from 'dompurify'
+import MarkdownIt from 'markdown-it'
+import React, { useEffect, useMemo, useState } from 'react'
+import { ListGroupItem } from 'react-bootstrap'
+import { Trans, useTranslation } from 'react-i18next'
+
+interface CheatsheetRendererProps {
+ rootI18nKey?: string
+ extension: CheatsheetEntry
+}
+
+/**
+ * Renders the cheatsheet entry with description, example and redered example.
+ *
+ * @param extension The extension to render
+ * @param rootI18nKey An additional i18n namespace
+ */
+export const CheatsheetEntryPane: React.FC = ({ extension, rootI18nKey }) => {
+ const { t } = useTranslation()
+
+ const [content, setContent] = useState('')
+
+ const lines = useMemo(() => content.split('\n'), [content])
+
+ const i18nPrefix = useMemo(
+ () => `cheatsheet.${rootI18nKey ? `${rootI18nKey}.` : ''}${extension.i18nKey}.`,
+ [extension.i18nKey, rootI18nKey]
+ )
+
+ useEffect(() => {
+ setContent(t(`${i18nPrefix}example`) ?? '')
+ }, [extension, i18nPrefix, t])
+
+ const cheatsheetExtensionComponents = useComponentsFromAppExtensions(setContent)
+
+ const descriptionElements = useMemo(() => {
+ const content = t(`${i18nPrefix}description`)
+ const markdownIt = new MarkdownIt('default')
+ return convertHtmlToReact(sanitize(markdownIt.render(content)))
+ }, [i18nPrefix, t])
+
+ return (
+
+
+ {cheatsheetExtensionComponents}
+
+
+
+
+ {descriptionElements}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-modal-body.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-modal-body.tsx
new file mode 100644
index 000000000..f97cc121c
--- /dev/null
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet-modal-body.tsx
@@ -0,0 +1,60 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { optionalAppExtensions } from '../../../../extensions/extra-integrations/optional-app-extensions'
+import { ShowIf } from '../../../common/show-if/show-if'
+import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
+import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
+import { CategoryAccordion } from './category-accordion'
+import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
+import { TopicSelection } from './topic-selection'
+import React, { useCallback, useMemo, useState } from 'react'
+import { Col, ListGroup, Modal, Row } from 'react-bootstrap'
+
+/**
+ * Renders the tab content for the cheatsheet.
+ */
+export const CheatsheetModalBody: React.FC = () => {
+ const [selectedExtension, setSelectedExtension] = useState()
+ const [selectedEntry, setSelectedEntry] = useState()
+
+ const changeExtension = useCallback((value: CheatsheetExtension) => {
+ setSelectedExtension(value)
+ setSelectedEntry(isCheatsheetGroup(value) ? value.entries[0] : value)
+ }, [])
+
+ const extensions = useMemo(
+ () => optionalAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
+ []
+ )
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/editor-page/app-bar/help-button/cheatsheet.module.scss b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet.module.scss
similarity index 76%
rename from frontend/src/components/editor-page/app-bar/help-button/cheatsheet.module.scss
rename to frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet.module.scss
index 6b40bbbef..d7d7c02a7 100644
--- a/frontend/src/components/editor-page/app-bar/help-button/cheatsheet.module.scss
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/cheatsheet.module.scss
@@ -7,3 +7,9 @@
.table-cheatsheet > tr > td {
vertical-align: middle !important;
}
+
+.sticky {
+ position: sticky;
+ top: 1rem;
+ bottom: 1rem;
+}
diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/entry-list.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/entry-list.tsx
new file mode 100644
index 000000000..094ab6062
--- /dev/null
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/entry-list.tsx
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import type { CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
+import styles from './cheatsheet.module.scss'
+import React, { useMemo } from 'react'
+import { ListGroup, ListGroupItem } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+
+interface CheatsheetListProps {
+ selectedEntry: CheatsheetExtension | undefined
+ extensions: CheatsheetExtension[]
+ onStateChange: (value: CheatsheetExtension) => void
+}
+
+const compareString = (value1: string, value2: string): -1 | 0 | 1 => {
+ return value1 === value2 ? 0 : value1 < value2 ? -1 : 1
+}
+
+/**
+ * Renders a list of cheatsheet entries.
+ *
+ * @param extensions The extensions whose cheatsheet entries should be listed
+ * @param selectedEntry The cheatsheet entry that should be rendered as selected.
+ * @param onStateChange A callback that is executed when a new entry has been selected
+ */
+export const EntryList: React.FC = ({ extensions, selectedEntry, onStateChange }) => {
+ const { t } = useTranslation()
+
+ const listItems = useMemo(
+ () =>
+ extensions
+ .map((extension) => [extension, t(`cheatsheet.${extension.i18nKey}.title`)] as [CheatsheetExtension, string])
+ .sort(([, title1], [, title2]) => compareString(title1.toLowerCase(), title2.toLowerCase()))
+ .map(([cheatsheetExtension, title]) => (
+ onStateChange(cheatsheetExtension)}>
+ {title}
+
+ )),
+ [extensions, onStateChange, selectedEntry, t]
+ )
+
+ return {listItems}
+}
diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/read-more-link-item.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/read-more-link-item.tsx
new file mode 100644
index 000000000..6bc1f91e6
--- /dev/null
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/read-more-link-item.tsx
@@ -0,0 +1,29 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { ExternalLink } from '../../../common/links/external-link'
+import React from 'react'
+import { ListGroupItem } from 'react-bootstrap'
+import { Trans } from 'react-i18next'
+
+export interface ReadMoreLinkGroupProps {
+ url: URL | undefined
+}
+
+/**
+ * Renders the read more URL as external link.
+ *
+ * @param url The URL to display. If the URL is undefined then nothing will be rendered.
+ */
+export const ReadMoreLinkItem: React.FC = ({ url }) => {
+ return !url ? null : (
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/topic-selection.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/topic-selection.tsx
new file mode 100644
index 000000000..cefd11c95
--- /dev/null
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/topic-selection.tsx
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
+import { isCheatsheetGroup } from '../../cheatsheet/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: CheatsheetEntry | undefined
+ setSelectedEntry: (value: CheatsheetEntry) => void
+}
+
+/**
+ * Renders a button group that contains the topics of the given extension.
+ * If the extension has no topics then the selection won't be displayed.
+ *
+ * @param extension The extension whose topics should be displayed
+ * @param selectedEntry The currently selected cheatsheet entry that should be displayed as active
+ * @param setSelectedEntry A callback that should be executed if a new topic has been selected
+ */
+export const TopicSelection: React.FC = ({ extension, selectedEntry, setSelectedEntry }) => {
+ const listItems = useMemo(() => {
+ if (!isCheatsheetGroup(extension)) {
+ return null
+ }
+ return extension.entries.map((entry) => (
+ setSelectedEntry(entry)}>
+
+
+ ))
+ }, [extension, selectedEntry?.i18nKey, setSelectedEntry])
+
+ return !listItems ? null : (
+
+
+
+
+ {listItems}
+
+ )
+}
diff --git a/frontend/src/components/editor-page/app-bar/cheatsheet/use-components-from-app-extensions.tsx b/frontend/src/components/editor-page/app-bar/cheatsheet/use-components-from-app-extensions.tsx
new file mode 100644
index 000000000..1488b9790
--- /dev/null
+++ b/frontend/src/components/editor-page/app-bar/cheatsheet/use-components-from-app-extensions.tsx
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { optionalAppExtensions } from '../../../../extensions/extra-integrations/optional-app-extensions'
+import type { CheatsheetExtensionComponentProps } from '../../cheatsheet/cheatsheet-extension'
+import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
+import type { ReactElement } from 'react'
+import React, { Fragment, useMemo } from 'react'
+
+/**
+ * Generates react elements from components which are provided by cheatsheet extensions.
+ */
+export const useComponentsFromAppExtensions = (
+ setContent: CheatsheetExtensionComponentProps['setContent']
+): ReactElement => {
+ return useMemo(() => {
+ return (
+
+ {optionalAppExtensions
+ .flatMap((extension) => extension.buildCheatsheetExtensions())
+ .flatMap((extension) => (isCheatsheetGroup(extension) ? extension.entries : extension))
+ .map((extension) => {
+ if (extension.cheatsheetExtensionComponent) {
+ return React.createElement(extension.cheatsheetExtensionComponent, { key: extension.i18nKey, setContent })
+ }
+ })}
+
+ )
+ }, [setContent])
+}
diff --git a/frontend/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx b/frontend/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx
deleted file mode 100644
index ffde615d7..000000000
--- a/frontend/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-import type { TaskCheckedEventPayload } from '../../../../extensions/extra-integrations/task-list/event-emitting-task-list-checkbox'
-import { TaskListCheckboxAppExtension } from '../../../../extensions/extra-integrations/task-list/task-list-checkbox-app-extension'
-import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
-import { eventEmitterContext } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
-import type { Listener } from 'eventemitter2'
-import { EventEmitter2 } from 'eventemitter2'
-import React, { Suspense, useEffect, useMemo } from 'react'
-
-export interface CheatsheetLineProps {
- markdown: string
- onTaskCheckedChange: (newValue: boolean) => void
-}
-
-const HighlightedCode = React.lazy(
- () => import('../../../../extensions/extra-integrations/highlighted-code-fence/highlighted-code')
-)
-const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer'))
-
-/**
- * Renders one line in the {@link CheatsheetTabContent cheat sheet}.
- * This line shows an minimal markdown example and how it would be rendered.
- *
- * @param markdown The markdown to be shown and rendered
- * @param onTaskCheckedChange A callback to call if a task would be clicked
- */
-export const CheatsheetLine: React.FC = ({ markdown, onTaskCheckedChange }) => {
- const lines = useMemo(() => markdown.split('\n'), [markdown])
- const eventEmitter = useMemo(() => new EventEmitter2(), [])
-
- useEffect(() => {
- const handler = eventEmitter.on(
- TaskListCheckboxAppExtension.EVENT_NAME,
- ({ checked }: TaskCheckedEventPayload) => onTaskCheckedChange(checked),
- { objectify: true }
- ) as Listener
- return () => {
- handler.off()
- }
- })
-
- return (
-
-
-
-
-
- }>
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/frontend/src/components/editor-page/app-bar/help-button/cheatsheet-tab-content.tsx b/frontend/src/components/editor-page/app-bar/help-button/cheatsheet-tab-content.tsx
deleted file mode 100644
index 6f9febc16..000000000
--- a/frontend/src/components/editor-page/app-bar/help-button/cheatsheet-tab-content.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-import { CheatsheetLine } from './cheatsheet-line'
-import styles from './cheatsheet.module.scss'
-import React, { useMemo, useState } from 'react'
-import { Table } from 'react-bootstrap'
-import { Trans, useTranslation } from 'react-i18next'
-
-/**
- * Renders the content of the cheat sheet for the {@link HelpModal}.
- */
-export const CheatsheetTabContent: React.FC = () => {
- const { t } = useTranslation()
- const [checked, setChecked] = useState(false)
- const codes = useMemo(
- () => [
- `**${t('editor.editorToolbar.bold')}**`,
- `*${t('editor.editorToolbar.italic')}*`,
- `++${t('editor.editorToolbar.underline')}++`,
- `~~${t('editor.editorToolbar.strikethrough')}~~`,
- 'H~2~O',
- '19^th^',
- `==${t('editor.help.cheatsheet.highlightedText')}==`,
- `# ${t('editor.editorToolbar.header')}`,
- `\`${t('editor.editorToolbar.code')}\``,
- '```javascript=\nvar x = 5;\n```',
- `> ${t('editor.editorToolbar.blockquote')}`,
- `- ${t('editor.editorToolbar.unorderedList')}`,
- `1. ${t('editor.editorToolbar.orderedList')}`,
- `- [${checked ? 'x' : ' '}] ${t('editor.editorToolbar.checkList')}`,
- `[${t('editor.editorToolbar.link')}](https://example.com)`,
- `![${t('editor.editorToolbar.image')}](/icons/apple-touch-icon.png)`,
- ':smile:',
- ':bi-bootstrap:',
- `:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::`
- ],
- [checked, t]
- )
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {codes.map((code) => (
-
- ))}
-
-
- )
-}
-
-export default CheatsheetTabContent
diff --git a/frontend/src/components/editor-page/app-bar/help-button/help-button.tsx b/frontend/src/components/editor-page/app-bar/help-button/help-button.tsx
index b4addbc5a..e7df7cfd6 100644
--- a/frontend/src/components/editor-page/app-bar/help-button/help-button.tsx
+++ b/frontend/src/components/editor-page/app-bar/help-button/help-button.tsx
@@ -5,12 +5,11 @@
*/
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { cypressId } from '../../../../utils/cypress-attribute'
-import { UiIcon } from '../../../common/icons/ui-icon'
+import { IconButton } from '../../../common/icon-button/icon-button'
import { HelpModal } from './help-modal'
import React, { Fragment } from 'react'
-import { Button } from 'react-bootstrap'
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
-import { useTranslation } from 'react-i18next'
+import { Trans, useTranslation } from 'react-i18next'
/**
* Renders the button to open the {@link HelpModal}.
@@ -21,15 +20,16 @@ export const HelpButton: React.FC = () => {
return (
-
-
-
+
+
)
diff --git a/frontend/src/components/editor-page/app-bar/help-button/help-modal.tsx b/frontend/src/components/editor-page/app-bar/help-button/help-modal.tsx
index 868511b1c..76b401ee2 100644
--- a/frontend/src/components/editor-page/app-bar/help-button/help-modal.tsx
+++ b/frontend/src/components/editor-page/app-bar/help-button/help-modal.tsx
@@ -5,7 +5,6 @@
*/
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
import { CommonModal } from '../../../common/modals/common-modal'
-import { CheatsheetTabContent } from './cheatsheet-tab-content'
import { LinksTabContent } from './links-tab-content'
import { ShortcutTabContent } from './shortcuts-tab-content'
import React, { useMemo, useState } from 'react'
@@ -14,7 +13,6 @@ import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
export enum HelpTabStatus {
- Cheatsheet = 'cheatsheet.title',
Shortcuts = 'shortcuts.title',
Links = 'links.title'
}
@@ -31,13 +29,11 @@ export enum HelpTabStatus {
* @param onHide A callback when the modal should be closed again
*/
export const HelpModal: React.FC = ({ show, onHide }) => {
- const [tab, setTab] = useState(HelpTabStatus.Cheatsheet)
+ const [tab, setTab] = useState(HelpTabStatus.Shortcuts)
const { t } = useTranslation()
const tabContent = useMemo(() => {
switch (tab) {
- case HelpTabStatus.Cheatsheet:
- return
case HelpTabStatus.Shortcuts:
return
case HelpTabStatus.Links:
@@ -48,15 +44,9 @@ export const HelpModal: React.FC = ({ show, onHide }) => {
const modalTitle = useMemo(() => t('editor.documentBar.help') + ' - ' + t(`editor.help.${tab}`), [t, tab])
return (
-
+
- setTab(HelpTabStatus.Cheatsheet)}>
-
-
string)) => void
+}
+
+export type CheatsheetExtension = CheatsheetEntry | CheatsheetGroup
+
+export const isCheatsheetGroup = (extension: CheatsheetExtension | undefined): extension is CheatsheetGroup => {
+ return (extension as CheatsheetGroup)?.entries !== undefined
+}
+
+export interface CheatsheetGroup {
+ i18nKey: string
+ categoryI18nKey?: string
+ entries: CheatsheetEntry[]
+}
+
+export interface CheatsheetEntry {
+ i18nKey: string
+ categoryI18nKey?: string
+ cheatsheetExtensionComponent?: React.FC
+
+ readMoreUrl?: URL
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-app-extension.ts b/frontend/src/components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-app-extension.ts
new file mode 100644
index 000000000..64b8dfdc3
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-app-extension.ts
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { AppExtension } from '../../../../extensions/base/app-extension'
+import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
+import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
+import { BasicMarkdownSyntaxMarkdownExtension } from './basic-markdown-syntax-markdown-extension'
+
+export class BasicMarkdownSyntaxAppExtension extends AppExtension {
+ buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
+ return [new BasicMarkdownSyntaxMarkdownExtension()]
+ }
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [
+ {
+ i18nKey: 'basics.formatting',
+ categoryI18nKey: 'basic',
+ entries: [
+ {
+ i18nKey: 'basic'
+ },
+ {
+ i18nKey: 'abbreviation'
+ },
+ { i18nKey: 'footnote' }
+ ]
+ },
+ {
+ i18nKey: 'basics.headlines',
+ categoryI18nKey: 'basic',
+ entries: [
+ {
+ i18nKey: 'hashtag'
+ },
+ {
+ i18nKey: 'equal'
+ }
+ ]
+ },
+ {
+ i18nKey: 'basics.code',
+ categoryI18nKey: 'basic',
+ entries: [{ i18nKey: 'inline' }, { i18nKey: 'block' }]
+ },
+ {
+ i18nKey: 'basics.lists',
+ categoryI18nKey: 'basic',
+ entries: [{ i18nKey: 'unordered' }, { i18nKey: 'ordered' }]
+ },
+ {
+ i18nKey: 'basics.images',
+ categoryI18nKey: 'basic',
+ entries: [{ i18nKey: 'basic' }, { i18nKey: 'size' }]
+ },
+ {
+ i18nKey: 'basics.links',
+ categoryI18nKey: 'basic'
+ }
+ ]
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/generic-syntax-markdown-extension.ts b/frontend/src/components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-markdown-extension.ts
similarity index 84%
rename from frontend/src/components/markdown-renderer/extensions/generic-syntax-markdown-extension.ts
rename to frontend/src/components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-markdown-extension.ts
index f2393ab39..456e5899b 100644
--- a/frontend/src/components/markdown-renderer/extensions/generic-syntax-markdown-extension.ts
+++ b/frontend/src/components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-markdown-extension.ts
@@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
+import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { imageSize } from '@hedgedoc/markdown-it-plugins'
import type MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr'
@@ -17,7 +17,7 @@ import superscript from 'markdown-it-sup'
/**
* Adds some common markdown syntaxes to the markdown rendering.
*/
-export class GenericSyntaxMarkdownExtension extends MarkdownRendererExtension {
+export class BasicMarkdownSyntaxMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
abbreviation(markdownIt)
definitionList(markdownIt)
diff --git a/frontend/src/components/markdown-renderer/extensions/bootstrap-icons/bootstrap-icon-app-extension.ts b/frontend/src/components/markdown-renderer/extensions/bootstrap-icons/bootstrap-icon-app-extension.ts
new file mode 100644
index 000000000..24703ea87
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/bootstrap-icons/bootstrap-icon-app-extension.ts
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { AppExtension } from '../../../../extensions/base/app-extension'
+import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
+import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
+import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
+
+export class BootstrapIconAppExtension extends AppExtension {
+ buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
+ return [new BootstrapIconMarkdownExtension()]
+ }
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'bootstrapIcon', readMoreUrl: new URL('https://icons.getbootstrap.com/') }]
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/emoji/emoji-app-extension.ts b/frontend/src/components/markdown-renderer/extensions/emoji/emoji-app-extension.ts
new file mode 100644
index 000000000..91dadab4c
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/emoji/emoji-app-extension.ts
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { AppExtension } from '../../../../extensions/base/app-extension'
+import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
+import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
+import { EmojiMarkdownExtension } from './emoji-markdown-extension'
+
+export class EmojiAppExtension extends AppExtension {
+ buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
+ return [new EmojiMarkdownExtension()]
+ }
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [
+ {
+ i18nKey: 'emoji',
+ readMoreUrl: new URL('https://twemoji.twitter.com/')
+ }
+ ]
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/iframe-capsule/iframe-capsule-app-extension.ts b/frontend/src/components/markdown-renderer/extensions/iframe-capsule/iframe-capsule-app-extension.ts
new file mode 100644
index 000000000..a9a093ab1
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/iframe-capsule/iframe-capsule-app-extension.ts
@@ -0,0 +1,24 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { AppExtension } from '../../../../extensions/base/app-extension'
+import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
+import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
+import { IframeCapsuleMarkdownExtension } from './iframe-capsule-markdown-extension'
+
+export class IframeCapsuleAppExtension extends AppExtension {
+ buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
+ return [new IframeCapsuleMarkdownExtension()]
+ }
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [
+ {
+ i18nKey: 'iframeCapsule',
+ categoryI18nKey: 'embedding'
+ }
+ ]
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/image-placeholder/image-placeholder-app-extension.ts b/frontend/src/components/markdown-renderer/extensions/image-placeholder/image-placeholder-app-extension.ts
new file mode 100644
index 000000000..e958a2360
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/image-placeholder/image-placeholder-app-extension.ts
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { AppExtension } from '../../../../extensions/base/app-extension'
+import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
+import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
+import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
+
+export class ImagePlaceholderAppExtension extends AppExtension {
+ buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
+ return [new ImagePlaceholderMarkdownExtension()]
+ }
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [
+ {
+ i18nKey: 'imagePlaceholder'
+ }
+ ]
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/table-of-contents/table-of-contents-app-extension.ts b/frontend/src/components/markdown-renderer/extensions/table-of-contents/table-of-contents-app-extension.ts
new file mode 100644
index 000000000..0e84b1e6f
--- /dev/null
+++ b/frontend/src/components/markdown-renderer/extensions/table-of-contents/table-of-contents-app-extension.ts
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { AppExtension } from '../../../../extensions/base/app-extension'
+import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
+import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
+import { TableOfContentsMarkdownExtension } from './table-of-contents-markdown-extension'
+import type EventEmitter2 from 'eventemitter2'
+
+export class TableOfContentsAppExtension extends AppExtension {
+ buildMarkdownRendererExtensions(eventEmitter?: EventEmitter2): MarkdownRendererExtension[] {
+ return [new TableOfContentsMarkdownExtension(eventEmitter)]
+ }
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [
+ {
+ i18nKey: 'toc',
+ entries: [
+ {
+ i18nKey: 'basic'
+ },
+ {
+ i18nKey: 'levelLimit'
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/frontend/src/components/markdown-renderer/extensions/table-of-contents-markdown-extension.ts b/frontend/src/components/markdown-renderer/extensions/table-of-contents/table-of-contents-markdown-extension.ts
similarity index 79%
rename from frontend/src/components/markdown-renderer/extensions/table-of-contents-markdown-extension.ts
rename to frontend/src/components/markdown-renderer/extensions/table-of-contents/table-of-contents-markdown-extension.ts
index ed8278149..bf512e9ab 100644
--- a/frontend/src/components/markdown-renderer/extensions/table-of-contents-markdown-extension.ts
+++ b/frontend/src/components/markdown-renderer/extensions/table-of-contents/table-of-contents-markdown-extension.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { tocSlugify } from '../../editor-page/table-of-contents/toc-slugify'
-import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
+import { tocSlugify } from '../../../editor-page/table-of-contents/toc-slugify'
+import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import type { TocAst } from '@hedgedoc/markdown-it-plugins'
import { toc } from '@hedgedoc/markdown-it-plugins'
import equal from 'fast-deep-equal'
diff --git a/frontend/src/components/markdown-renderer/hooks/use-markdown-extensions.ts b/frontend/src/components/markdown-renderer/hooks/use-markdown-extensions.ts
index c8484ef00..0d95d5ea3 100644
--- a/frontend/src/components/markdown-renderer/hooks/use-markdown-extensions.ts
+++ b/frontend/src/components/markdown-renderer/hooks/use-markdown-extensions.ts
@@ -5,27 +5,17 @@
*/
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
-import { BootstrapIconMarkdownExtension } from '../extensions/bootstrap-icons/bootstrap-icon-markdown-extension'
import { DebuggerMarkdownExtension } from '../extensions/debugger-markdown-extension'
-import { EmojiMarkdownExtension } from '../extensions/emoji/emoji-markdown-extension'
-import { GenericSyntaxMarkdownExtension } from '../extensions/generic-syntax-markdown-extension'
-import { IframeCapsuleMarkdownExtension } from '../extensions/iframe-capsule/iframe-capsule-markdown-extension'
-import { ImagePlaceholderMarkdownExtension } from '../extensions/image-placeholder/image-placeholder-markdown-extension'
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
import type { LineMarkers } from '../extensions/linemarker/add-line-marker-markdown-it-plugin'
import { LinemarkerMarkdownExtension } from '../extensions/linemarker/linemarker-markdown-extension'
import { LinkAdjustmentMarkdownExtension } from '../extensions/link-replacer/link-adjustment-markdown-extension'
import { LinkifyFixMarkdownExtension } from '../extensions/linkify-fix/linkify-fix-markdown-extension'
-import { TableOfContentsMarkdownExtension } from '../extensions/table-of-contents-markdown-extension'
import { UploadIndicatingImageFrameMarkdownExtension } from '../extensions/upload-indicating-image-frame/upload-indicating-image-frame-markdown-extension'
import { useExtensionEventEmitter } from './use-extension-event-emitter'
import type { MutableRefObject } from 'react'
import { useMemo } from 'react'
-const optionalMarkdownRendererExtensions = optionalAppExtensions.flatMap((value) =>
- value.buildMarkdownRendererExtensions()
-)
-
/**
* Provides a list of {@link MarkdownRendererExtension markdown extensions} that is a combination of the common extensions and the given additional.
*
@@ -40,22 +30,20 @@ export const useMarkdownExtensions = (
additionalExtensions: MarkdownRendererExtension[]
): MarkdownRendererExtension[] => {
const extensionEventEmitter = useExtensionEventEmitter()
- //replace with global list
+
return useMemo(() => {
+ const optionalMarkdownRendererExtensions = optionalAppExtensions.flatMap((value) =>
+ value.buildMarkdownRendererExtensions(extensionEventEmitter)
+ )
+
return [
...optionalMarkdownRendererExtensions,
...additionalExtensions,
- new TableOfContentsMarkdownExtension(extensionEventEmitter),
new LinemarkerMarkdownExtension(
currentLineMarkers ? (lineMarkers) => (currentLineMarkers.current = lineMarkers) : undefined
),
- new IframeCapsuleMarkdownExtension(),
- new ImagePlaceholderMarkdownExtension(),
new UploadIndicatingImageFrameMarkdownExtension(),
new LinkAdjustmentMarkdownExtension(baseUrl),
- new EmojiMarkdownExtension(),
- new BootstrapIconMarkdownExtension(),
- new GenericSyntaxMarkdownExtension(),
new LinkifyFixMarkdownExtension(),
new DebuggerMarkdownExtension(),
new ProxyImageMarkdownExtension()
diff --git a/frontend/src/components/render-page/document-toc-sidebar.tsx b/frontend/src/components/render-page/document-toc-sidebar.tsx
index f3b0b9b78..41fd7cd51 100644
--- a/frontend/src/components/render-page/document-toc-sidebar.tsx
+++ b/frontend/src/components/render-page/document-toc-sidebar.tsx
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ShowIf } from '../common/show-if/show-if'
-import { TableOfContentsMarkdownExtension } from '../markdown-renderer/extensions/table-of-contents-markdown-extension'
+import { TableOfContentsMarkdownExtension } from '../markdown-renderer/extensions/table-of-contents/table-of-contents-markdown-extension'
import { useExtensionEventEmitterHandler } from '../markdown-renderer/hooks/use-extension-event-emitter'
import styles from './markdown-document.module.scss'
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
diff --git a/frontend/src/extensions/base/app-extension.ts b/frontend/src/extensions/base/app-extension.ts
index 8d2e13697..29025b5c4 100644
--- a/frontend/src/extensions/base/app-extension.ts
+++ b/frontend/src/extensions/base/app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { Linter } from '../../components/editor-page/editor-pane/linter/linter'
import type { MarkdownRendererExtension } from '../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import type { EventEmitter2 } from 'eventemitter2'
@@ -22,4 +23,8 @@ export abstract class AppExtension {
public buildEditorExtensionComponent(): React.FC {
return Fragment
}
+
+ public buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return []
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/abcjs/abcjs-app-extension.ts b/frontend/src/extensions/extra-integrations/abcjs/abcjs-app-extension.ts
index a8b94d9a9..c8136ede2 100644
--- a/frontend/src/extensions/extra-integrations/abcjs/abcjs-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/abcjs/abcjs-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { AbcjsMarkdownExtension } from './abcjs-markdown-extension'
@@ -11,4 +12,8 @@ export class AbcjsAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new AbcjsMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'abcjs', categoryI18nKey: 'charts', readMoreUrl: new URL('https://www.abcjs.net/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/alert/alert-app-extension.ts b/frontend/src/extensions/extra-integrations/alert/alert-app-extension.ts
index 5bb4c30bb..07a087ce0 100644
--- a/frontend/src/extensions/extra-integrations/alert/alert-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/alert/alert-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { AlertMarkdownExtension } from './alert-markdown-extension'
@@ -14,4 +15,8 @@ export class AlertAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new AlertMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'alert' }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/asciinema/asciinema-app-extension.ts b/frontend/src/extensions/extra-integrations/asciinema/asciinema-app-extension.ts
index 7aa737aa0..2df365a9e 100644
--- a/frontend/src/extensions/extra-integrations/asciinema/asciinema-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/asciinema/asciinema-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { AsciinemaMarkdownExtension } from './asciinema-markdown-extension'
@@ -16,4 +17,8 @@ export class AsciinemaAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new AsciinemaMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'asciinema', categoryI18nKey: 'embedding', readMoreUrl: new URL('https://asciinema.org/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/blockquote/blockquote-app-extension.ts b/frontend/src/extensions/extra-integrations/blockquote/blockquote-app-extension.ts
index 75c439f75..130aab3a8 100644
--- a/frontend/src/extensions/extra-integrations/blockquote/blockquote-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/blockquote/blockquote-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension'
@@ -14,4 +15,8 @@ export class BlockquoteAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new BlockquoteExtraTagMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'blockquoteTags', entries: [{ i18nKey: 'name' }, { i18nKey: 'color' }, { i18nKey: 'time' }] }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/csv/csv-table-app-extension.ts b/frontend/src/extensions/extra-integrations/csv/csv-table-app-extension.ts
index 9641079ce..62c725679 100644
--- a/frontend/src/extensions/extra-integrations/csv/csv-table-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/csv/csv-table-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { CsvTableMarkdownExtension } from './csv-table-markdown-extension'
@@ -14,4 +15,8 @@ export class CsvTableAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new CsvTableMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'csv', entries: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/flowchart/flowchart-app-extension.ts b/frontend/src/extensions/extra-integrations/flowchart/flowchart-app-extension.ts
index ecc1bef3d..0e7c8cef2 100644
--- a/frontend/src/extensions/extra-integrations/flowchart/flowchart-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/flowchart/flowchart-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { FlowchartMarkdownExtension } from './flowchart-markdown-extension'
@@ -14,4 +15,8 @@ export class FlowchartAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new FlowchartMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'flowchart', categoryI18nKey: 'charts', readMoreUrl: new URL('https://flowchart.js.org/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/gist/gist-app-extension.ts b/frontend/src/extensions/extra-integrations/gist/gist-app-extension.ts
index 6bd13bcfa..0c68af9ae 100644
--- a/frontend/src/extensions/extra-integrations/gist/gist-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/gist/gist-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { GistMarkdownExtension } from './gist-markdown-extension'
@@ -14,4 +15,8 @@ export class GistAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new GistMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'gist', categoryI18nKey: 'embedding', readMoreUrl: new URL('https://gist.github.com/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/graphviz/graphviz-app-extension.ts b/frontend/src/extensions/extra-integrations/graphviz/graphviz-app-extension.ts
index 00d23b5ec..1587ba3c4 100644
--- a/frontend/src/extensions/extra-integrations/graphviz/graphviz-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/graphviz/graphviz-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { GraphvizMarkdownExtension } from './graphviz-markdown-extension'
@@ -14,4 +15,8 @@ export class GraphvizAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new GraphvizMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'graphviz', categoryI18nKey: 'charts', readMoreUrl: new URL('https://graphviz.org/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/highlighted-code-fence/highlighted-code-fence-app-extension.ts b/frontend/src/extensions/extra-integrations/highlighted-code-fence/highlighted-code-fence-app-extension.ts
index 67d281f1e..18dc6034b 100644
--- a/frontend/src/extensions/extra-integrations/highlighted-code-fence/highlighted-code-fence-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/highlighted-code-fence/highlighted-code-fence-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'
@@ -14,4 +15,13 @@ export class HighlightedCodeFenceAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new HighlightedCodeMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [
+ {
+ i18nKey: 'codeHighlighting',
+ entries: [{ i18nKey: 'language' }, { i18nKey: 'lineNumbers' }, { i18nKey: 'lineWrapping' }]
+ }
+ ]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/katex/katex-app-extension.ts b/frontend/src/extensions/extra-integrations/katex/katex-app-extension.ts
index a327aa590..38ac2b263 100644
--- a/frontend/src/extensions/extra-integrations/katex/katex-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/katex/katex-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { KatexMarkdownExtension } from './katex-markdown-extension'
@@ -16,4 +17,8 @@ export class KatexAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new KatexMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'katex', readMoreUrl: new URL('https://katex.org/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/mermaid/mermaid-app-extension.ts b/frontend/src/extensions/extra-integrations/mermaid/mermaid-app-extension.ts
index ae28049cf..e3612e041 100644
--- a/frontend/src/extensions/extra-integrations/mermaid/mermaid-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/mermaid/mermaid-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { MermaidMarkdownExtension } from './mermaid-markdown-extension'
@@ -14,4 +15,8 @@ export class MermaidAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new MermaidMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'mermaid', categoryI18nKey: 'charts', readMoreUrl: new URL('https://mermaid.js.org/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/optional-app-extensions.ts b/frontend/src/extensions/extra-integrations/optional-app-extensions.ts
index 426925725..84fe7b538 100644
--- a/frontend/src/extensions/extra-integrations/optional-app-extensions.ts
+++ b/frontend/src/extensions/extra-integrations/optional-app-extensions.ts
@@ -3,6 +3,12 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { BasicMarkdownSyntaxAppExtension } from '../../components/markdown-renderer/extensions/basic-markdown-syntax/basic-markdown-syntax-app-extension'
+import { BootstrapIconAppExtension } from '../../components/markdown-renderer/extensions/bootstrap-icons/bootstrap-icon-app-extension'
+import { EmojiAppExtension } from '../../components/markdown-renderer/extensions/emoji/emoji-app-extension'
+import { IframeCapsuleAppExtension } from '../../components/markdown-renderer/extensions/iframe-capsule/iframe-capsule-app-extension'
+import { ImagePlaceholderAppExtension } from '../../components/markdown-renderer/extensions/image-placeholder/image-placeholder-app-extension'
+import { TableOfContentsAppExtension } from '../../components/markdown-renderer/extensions/table-of-contents/table-of-contents-app-extension'
import type { AppExtension } from '../base/app-extension'
import { AbcjsAppExtension } from './abcjs/abcjs-app-extension'
import { AlertAppExtension } from './alert/alert-app-extension'
@@ -48,5 +54,11 @@ export const optionalAppExtensions: AppExtension[] = [
new YoutubeAppExtension(),
new TaskListCheckboxAppExtension(),
new HighlightedCodeFenceAppExtension(),
- new ForkAwesomeHtmlTagAppExtension()
+ new ForkAwesomeHtmlTagAppExtension(),
+ new BootstrapIconAppExtension(),
+ new EmojiAppExtension(),
+ new TableOfContentsAppExtension(),
+ new ImagePlaceholderAppExtension(),
+ new IframeCapsuleAppExtension(),
+ new BasicMarkdownSyntaxAppExtension()
]
diff --git a/frontend/src/extensions/extra-integrations/plantuml/plantuml-app-extension.ts b/frontend/src/extensions/extra-integrations/plantuml/plantuml-app-extension.ts
index 3474fca68..bcbba6c12 100644
--- a/frontend/src/extensions/extra-integrations/plantuml/plantuml-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/plantuml/plantuml-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { PlantumlMarkdownExtension } from './plantuml-markdown-extension'
@@ -16,4 +17,8 @@ export class PlantumlAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new PlantumlMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'plantuml', categoryI18nKey: 'charts', readMoreUrl: new URL('https://plantuml.com/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/spoiler/spoiler-app-extension.ts b/frontend/src/extensions/extra-integrations/spoiler/spoiler-app-extension.ts
index 30c101ddc..bf921fec5 100644
--- a/frontend/src/extensions/extra-integrations/spoiler/spoiler-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/spoiler/spoiler-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { SpoilerMarkdownExtension } from './spoiler-markdown-extension'
@@ -16,4 +17,8 @@ export class SpoilerAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new SpoilerMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'spoiler' }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/task-list/create-checkbox-content.ts b/frontend/src/extensions/extra-integrations/task-list/create-checkbox-content.ts
new file mode 100644
index 000000000..59ce606a5
--- /dev/null
+++ b/frontend/src/extensions/extra-integrations/task-list/create-checkbox-content.ts
@@ -0,0 +1,9 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const createCheckboxContent = (newCheckboxState: boolean) => {
+ return `[${newCheckboxState ? 'x' : ' '}]`
+}
diff --git a/frontend/src/extensions/extra-integrations/task-list/event-emitting-task-list-checkbox.tsx b/frontend/src/extensions/extra-integrations/task-list/event-emitting-task-list-checkbox.tsx
index ba6147ca0..1cd5adebf 100644
--- a/frontend/src/extensions/extra-integrations/task-list/event-emitting-task-list-checkbox.tsx
+++ b/frontend/src/extensions/extra-integrations/task-list/event-emitting-task-list-checkbox.tsx
@@ -13,7 +13,7 @@ type EventEmittingTaskListCheckboxProps = Omit {
- emitter?.emit(TaskListCheckboxAppExtension.EVENT_NAME, { lineInMarkdown, checked } as TaskCheckedEventPayload)
+ emitter?.emit(TaskListCheckboxAppExtension.EVENT_NAME, {
+ lineInMarkdown,
+ newCheckedState: checked
+ } as TaskCheckedEventPayload)
},
[emitter]
)
diff --git a/frontend/src/extensions/extra-integrations/task-list/find-checkbox-to-change.ts b/frontend/src/extensions/extra-integrations/task-list/find-checkbox-to-change.ts
new file mode 100644
index 000000000..929bf1554
--- /dev/null
+++ b/frontend/src/extensions/extra-integrations/task-list/find-checkbox-to-change.ts
@@ -0,0 +1,26 @@
+/*
+ * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import { Optional } from '@mrdrogdrog/optional'
+
+const TASK_REGEX = /^(\s*(?:[-*+]|\d+[.)]) )(\[[ xX]?])/
+export const findCheckBoxToChange = (
+ originalContent: string,
+ lineIndex: number
+): Optional<[startIndex: number, endIndex: number]> => {
+ const lines = originalContent.split('\n')
+ const lineStartIndex = findStartIndexOfLine(lines, lineIndex)
+ return Optional.ofNullable(TASK_REGEX.exec(lines[lineIndex])).map(([, beforeCheckbox, oldCheckbox]) => [
+ lineStartIndex + beforeCheckbox.length,
+ lineStartIndex + beforeCheckbox.length + oldCheckbox.length
+ ])
+}
+
+const findStartIndexOfLine = (lines: string[], wantedLineIndex: number): number => {
+ return lines
+ .map((value) => value.length)
+ .filter((value, index) => index < wantedLineIndex)
+ .reduce((state, lineLength) => state + lineLength + 1, 0)
+}
diff --git a/frontend/src/extensions/extra-integrations/task-list/set-checkbox-in-cheatsheet.tsx b/frontend/src/extensions/extra-integrations/task-list/set-checkbox-in-cheatsheet.tsx
new file mode 100644
index 000000000..b1672418e
--- /dev/null
+++ b/frontend/src/extensions/extra-integrations/task-list/set-checkbox-in-cheatsheet.tsx
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+import type { CheatsheetExtensionComponentProps } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
+import { useExtensionEventEmitterHandler } from '../../../components/markdown-renderer/hooks/use-extension-event-emitter'
+import { createCheckboxContent } from './create-checkbox-content'
+import type { TaskCheckedEventPayload } from './event-emitting-task-list-checkbox'
+import { findCheckBoxToChange } from './find-checkbox-to-change'
+import { TaskListCheckboxAppExtension } from './task-list-checkbox-app-extension'
+import type React from 'react'
+
+/**
+ * Receives task-checkbox-change events and modify the current editor content.
+ */
+export const SetCheckboxInCheatsheet: React.FC = ({ setContent }) => {
+ useExtensionEventEmitterHandler(TaskListCheckboxAppExtension.EVENT_NAME, (event: TaskCheckedEventPayload) => {
+ setContent((previousContent) => {
+ return findCheckBoxToChange(previousContent, event.lineInMarkdown)
+ .map(
+ ([startIndex, endIndex]) =>
+ previousContent.slice(0, startIndex) +
+ createCheckboxContent(event.newCheckedState) +
+ previousContent.slice(endIndex)
+ )
+ .orElse(previousContent)
+ })
+ })
+ return null
+}
diff --git a/frontend/src/extensions/extra-integrations/task-list/set-checkbox-in-editor.tsx b/frontend/src/extensions/extra-integrations/task-list/set-checkbox-in-editor.tsx
index 8a16675eb..2553f4339 100644
--- a/frontend/src/extensions/extra-integrations/task-list/set-checkbox-in-editor.tsx
+++ b/frontend/src/extensions/extra-integrations/task-list/set-checkbox-in-editor.tsx
@@ -3,10 +3,16 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { useChangeEditorContentCallback } from '../../../components/editor-page/change-content-context/use-change-editor-content-callback'
+import type { ContentEdits } from '../../../components/editor-page/editor-pane/tool-bar/formatters/types/changes'
import { useExtensionEventEmitterHandler } from '../../../components/markdown-renderer/hooks/use-extension-event-emitter'
+import { store } from '../../../redux'
+import { createCheckboxContent } from './create-checkbox-content'
+import type { TaskCheckedEventPayload } from './event-emitting-task-list-checkbox'
+import { findCheckBoxToChange } from './find-checkbox-to-change'
import { TaskListCheckboxAppExtension } from './task-list-checkbox-app-extension'
-import { useSetCheckboxInEditor } from './use-set-checkbox-in-editor'
import type React from 'react'
+import { useCallback } from 'react'
/**
* Receives task-checkbox-change events and modify the current editor content.
@@ -16,3 +22,45 @@ export const SetCheckboxInEditor: React.FC = () => {
useExtensionEventEmitterHandler(TaskListCheckboxAppExtension.EVENT_NAME, changeCallback)
return null
}
+
+/**
+ * Provides a callback that changes the state of a checkbox in a given line in the current codemirror instance.
+ */
+export const useSetCheckboxInEditor = () => {
+ const changeEditorContent = useChangeEditorContentCallback()
+
+ return useCallback(
+ ({ lineInMarkdown, newCheckedState }: TaskCheckedEventPayload): void => {
+ changeEditorContent?.(({ markdownContent }) => {
+ const correctedLineIndex = lineInMarkdown + store.getState().noteDetails.frontmatterRendererInfo.lineOffset
+ const edits = findCheckBoxToChange(markdownContent, correctedLineIndex)
+ .map(([startIndex, endIndex]) => createCheckboxContentEdit(startIndex, endIndex, newCheckedState))
+ .orElse([])
+ return [edits, undefined]
+ })
+ },
+ [changeEditorContent]
+ )
+}
+
+/**
+ * Creates a {@link ContentEdits content edit} for the change of a checkbox at a given position.
+ *
+ * @param checkboxStartIndex The start index of the old checkbox code
+ * @param checkboxEndIndex The end index of the old checkbox code
+ * @param newCheckboxState The new status of the checkbox
+ * @return the created {@link ContentEdits edit}
+ */
+const createCheckboxContentEdit = (
+ checkboxStartIndex: number,
+ checkboxEndIndex: number,
+ newCheckboxState: boolean
+): ContentEdits => {
+ return [
+ {
+ from: checkboxStartIndex,
+ to: checkboxEndIndex,
+ insert: createCheckboxContent(newCheckboxState)
+ }
+ ]
+}
diff --git a/frontend/src/extensions/extra-integrations/task-list/task-list-checkbox-app-extension.ts b/frontend/src/extensions/extra-integrations/task-list/task-list-checkbox-app-extension.ts
index 659792017..f46227dd8 100644
--- a/frontend/src/extensions/extra-integrations/task-list/task-list-checkbox-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/task-list/task-list-checkbox-app-extension.ts
@@ -3,7 +3,9 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import { AppExtension } from '../../base/app-extension'
+import { SetCheckboxInCheatsheet } from './set-checkbox-in-cheatsheet'
import { SetCheckboxInEditor } from './set-checkbox-in-editor'
import { TaskListMarkdownExtension } from './task-list-markdown-extension'
import type { EventEmitter2 } from 'eventemitter2'
@@ -22,4 +24,8 @@ export class TaskListCheckboxAppExtension extends AppExtension {
buildEditorExtensionComponent(): React.FC {
return SetCheckboxInEditor
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'taskList', cheatsheetExtensionComponent: SetCheckboxInCheatsheet }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/task-list/use-set-checkbox-in-editor.tsx b/frontend/src/extensions/extra-integrations/task-list/use-set-checkbox-in-editor.tsx
deleted file mode 100644
index 0872234e0..000000000
--- a/frontend/src/extensions/extra-integrations/task-list/use-set-checkbox-in-editor.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-import { useChangeEditorContentCallback } from '../../../components/editor-page/change-content-context/use-change-editor-content-callback'
-import type { ContentEdits } from '../../../components/editor-page/editor-pane/tool-bar/formatters/types/changes'
-import { store } from '../../../redux'
-import type { TaskCheckedEventPayload } from './event-emitting-task-list-checkbox'
-import { Optional } from '@mrdrogdrog/optional'
-import { useCallback } from 'react'
-
-const TASK_REGEX = /(\s*(?:[-*+]|\d+[.)]) )(\[[ xX]?])/
-
-/**
- * Provides a callback that changes the state of a checkbox in a given line in the current codemirror instance.
- */
-export const useSetCheckboxInEditor = () => {
- const changeEditorContent = useChangeEditorContentCallback()
-
- return useCallback(
- ({ lineInMarkdown, checked }: TaskCheckedEventPayload): void => {
- changeEditorContent?.(({ markdownContent }) => {
- const lines = markdownContent.split('\n')
- const correctedLineIndex = lineInMarkdown + store.getState().noteDetails.frontmatterRendererInfo.lineOffset
- const lineStartIndex = findStartIndexOfLine(lines, correctedLineIndex)
- const edits = Optional.ofNullable(TASK_REGEX.exec(lines[correctedLineIndex]))
- .map(([, beforeCheckbox, oldCheckbox]) => {
- const checkboxStartIndex = lineStartIndex + beforeCheckbox.length
- return createCheckboxContentEdit(checkboxStartIndex, oldCheckbox, checked)
- })
- .orElse([])
- return [edits, undefined]
- })
- },
- [changeEditorContent]
- )
-}
-
-/**
- * Finds the start position of the wanted line index if the given lines would be concat with new-line-characters.
- *
- * @param lines The lines to search through
- * @param wantedLineIndex The index of the line whose start position should be found
- * @return the found start position
- */
-const findStartIndexOfLine = (lines: string[], wantedLineIndex: number): number => {
- return lines
- .map((value) => value.length)
- .filter((value, index) => index < wantedLineIndex)
- .reduce((state, lineLength) => state + lineLength + 1, 0)
-}
-
-/**
- * Creates a {@link ContentEdits content edit} for the change of a checkbox at a given position.
- *
- * @param checkboxStartIndex The start index of the checkbox
- * @param oldCheckbox The old checkbox that should be replaced
- * @param newCheckboxState The new status of the checkbox
- * @return the created {@link ContentEdits edit}
- */
-const createCheckboxContentEdit = (
- checkboxStartIndex: number,
- oldCheckbox: string,
- newCheckboxState: boolean
-): ContentEdits => {
- return [
- {
- from: checkboxStartIndex,
- to: checkboxStartIndex + oldCheckbox.length,
- insert: `[${newCheckboxState ? 'x' : ' '}]`
- }
- ]
-}
diff --git a/frontend/src/extensions/extra-integrations/vega-lite/vega-lite-app-extension.ts b/frontend/src/extensions/extra-integrations/vega-lite/vega-lite-app-extension.ts
index 213c78622..e06e4d1d9 100644
--- a/frontend/src/extensions/extra-integrations/vega-lite/vega-lite-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/vega-lite/vega-lite-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension'
import { VegaLiteMarkdownExtension } from './vega-lite-markdown-extension'
@@ -14,4 +15,10 @@ export class VegaLiteAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new VegaLiteMarkdownExtension()]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [
+ { i18nKey: 'vegaLite', categoryI18nKey: 'charts', readMoreUrl: new URL('https://vega.github.io/vega-lite/') }
+ ]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/vimeo/vimeo-app-extension.ts b/frontend/src/extensions/extra-integrations/vimeo/vimeo-app-extension.ts
index 5dc337f6d..ee3531f7a 100644
--- a/frontend/src/extensions/extra-integrations/vimeo/vimeo-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/vimeo/vimeo-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { Linter } from '../../../components/editor-page/editor-pane/linter/linter'
import { SingleLineRegexLinter } from '../../../components/editor-page/editor-pane/linter/single-line-regex-linter'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
@@ -28,4 +29,8 @@ export class VimeoAppExtension extends AppExtension {
)
]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'vimeo', categoryI18nKey: 'embedding', readMoreUrl: new URL('https://vimeo.com/') }]
+ }
}
diff --git a/frontend/src/extensions/extra-integrations/youtube/youtube-app-extension.ts b/frontend/src/extensions/extra-integrations/youtube/youtube-app-extension.ts
index 86d3c221c..3509c87e4 100644
--- a/frontend/src/extensions/extra-integrations/youtube/youtube-app-extension.ts
+++ b/frontend/src/extensions/extra-integrations/youtube/youtube-app-extension.ts
@@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import type { Linter } from '../../../components/editor-page/editor-pane/linter/linter'
import { SingleLineRegexLinter } from '../../../components/editor-page/editor-pane/linter/single-line-regex-linter'
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
@@ -28,4 +29,8 @@ export class YoutubeAppExtension extends AppExtension {
)
]
}
+
+ buildCheatsheetExtensions(): CheatsheetExtension[] {
+ return [{ i18nKey: 'youtube', categoryI18nKey: 'embedding', readMoreUrl: new URL('https://youtube.com/') }]
+ }
}