fix(cheatsheet): refactor cheatsheet to use app extensions as source

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-02-21 19:46:56 +01:00
parent 9d49401b4d
commit 24b0070909
53 changed files with 1164 additions and 275 deletions

View file

@ -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')
})
})

View file

@ -267,13 +267,6 @@
"features": "Features", "features": "Features",
"yamlMetadata": "YAML Metadata", "yamlMetadata": "YAML Metadata",
"slideExample": "Slide Example" "slideExample": "Slide Example"
},
"cheatsheet": {
"title": "Cheatsheet",
"example": "Example",
"syntax": "Syntax",
"exampleAlert": "This is an alert area.",
"highlightedText": "Highlight"
} }
}, },
"onlineStatus": { "onlineStatus": {
@ -630,5 +623,253 @@
"help": "The primary user interface language" "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"
},
"noSelection": "Select an entry on the left side to show the instructions."
},
"categories": {
"basic": "Basics",
"other": "Other",
"embedding": "Embedding",
"charts": "Charts & Diagrams"
},
"basics": {
"basicFormatting": {
"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": "Abbreviation definitions create tooltips for matching words. They can be placed defined in the document.",
"example": "*[HTML]: Hyper Text Markup Language\n*[W3C]: World Wide Web Consortium\nThe HTML specification\nis maintained by the W3C."
},
"footnote": {
"title": "Footnotes",
"description": "Footnotes can be used to add extra information at the bottom of the page. They can be defined anywhere in the document.",
"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. Subsequent paragraphs are indented to show that they belong to the previous footnote."
},
"headlines": {
"title": "Headlines",
"hashtag": {
"title": "Hashtag",
"description": "Headlines can be used to structure your document into sections. Every headline creates a linkable anchor.",
"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": "Equals sign",
"description": "An alternative form for the first and second level headline is to append a line of only equals signs or dashes after the headline text. ",
"example": "Headline 1\n==========\n\nHeadline 2\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.\n\nIt is also possible to define a language for syntax highlighting. For more information check the entry \"Other > Code Highlighting\"",
"example": "```\nthis is a code block\n```"
}
},
"lists": {
"title": "Lists",
"unordered": {
"title": "Unordered",
"description": "You can create unordered lists by prepending lines with dashes or asterisks.",
"example": "- A\n- B\n- C\n\n* A\n* B\n* C"
},
"ordered": {
"title": "Ordered",
"description": "You can create ordered lists by prepending lines with numbers followed by a dot. Ordered lists will always start with one. Ordered lists that aren't separated by another element will continue the last list.",
"example": "1. A\n2. B\n3. C\n\n\n4. D\n5. E\n\nText\n\n100. A\n101. B\n102. C"
}
},
"images": {
"title": "Images",
"basic": {
"title": "Basic",
"description": "Images can be defined using the link syntax with an exclamation mark in front of it. The text in the brackets will be used as alt-text.",
"example": "![This is an icon](/icons/apple-touch-icon.png)"
},
"size": {
"title": "Size",
"description": "The size syntax allows you to set both dimensions of an image. If you omit one of the dimensions then image will be scaled to the relative size.",
"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": "You can create text links by either using the link syntax, by writing the plain URL or by writing it in angle brackets.",
"example": "[Example link](https://example.org)\n\nhttps://example.org\n\n<https://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": "<iframe src=\"https://example.org/\"></iframe>"
},
"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 <back:cadetblue><size:18>displayed</size></back>\n __left of__ Alice.\nend note\nnote left of Bob\n <u:red>This</u> is <color #118888>displayed</color>\n **<color purple>left of</color> <s:red>Alice</strike> Bob**.\nend note\nnote over Alice, Bob\n <w:#FF33FF>This is hosted</w> by <img sourceforge.jpg>\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```"
}
}
} }
} }

View file

@ -10,6 +10,7 @@ import { ShowIf } from '../../common/show-if/show-if'
import { SignInButton } from '../../landing-layout/navigation/sign-in-button' import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown' import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
import { SettingsButton } from '../../layout/settings-dialog/settings-button' import { SettingsButton } from '../../layout/settings-dialog/settings-button'
import { CheatsheetButton } from './cheatsheet/cheatsheet-button'
import { HelpButton } from './help-button/help-button' import { HelpButton } from './help-button/help-button'
import { NavbarBranding } from './navbar-branding' import { NavbarBranding } from './navbar-branding'
import { ReadOnlyModeButton } from './read-only-mode-button' import { ReadOnlyModeButton } from './read-only-mode-button'
@ -47,6 +48,7 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
<ReadOnlyModeButton /> <ReadOnlyModeButton />
</ShowIf> </ShowIf>
<HelpButton /> <HelpButton />
<CheatsheetButton />
</ShowIf> </ShowIf>
</Nav> </Nav>
<Nav className='d-flex gap-2 align-items-center text-secondary justify-content-end'> <Nav className='d-flex gap-2 align-items-center text-secondary justify-content-end'>

View file

@ -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<string, CheatsheetExtension[]>
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<GroupAccordionProps> = ({ extensions, selectedEntry, onStateChange }) => {
const groupEntries = useMemo(() => {
const groupings = extensions.reduce(reduceCheatsheetExtensionByCategory, new Map<string, CheatsheetExtension[]>())
return Array.from(groupings.entries()).sort(sortCategories)
}, [extensions])
const elements = useMemo(() => {
return groupEntries.map(([groupKey, groupExtensions]) => (
<Accordion.Item eventKey={groupKey} key={groupKey}>
<Accordion.Header>
<Trans i18nKey={`cheatsheet.categories.${groupKey}`}></Trans>
</Accordion.Header>
<Accordion.Body className={'p-0'}>
<EntryList selectedEntry={selectedEntry} extensions={groupExtensions} onStateChange={onStateChange} />
</Accordion.Body>
</Accordion.Item>
))
}, [groupEntries, onStateChange, selectedEntry])
return <Accordion defaultActiveKey={groupEntries[0][0]}>{elements}</Accordion>
}

View file

@ -0,0 +1,44 @@
/*
* 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'
/**
* Shows a button that opens the cheatsheet dialog.
*/
export const CheatsheetButton: React.FC = () => {
const { t } = useTranslation()
const [modalVisibility, showModal, closeModal] = useBooleanState()
return (
<Fragment>
<Button
{...cypressId('open.cheatsheet-button')}
title={t('cheatsheet.button') ?? undefined}
className={'mx-2'}
variant='outline-dark'
size={'sm'}
onClick={showModal}>
<Trans i18nKey={'cheatsheet.button'}></Trans>
</Button>
<CommonModal
modalSize={'xl'}
titleIcon={IconQuestionCircle}
show={modalVisibility}
onHide={closeModal}
showCloseButton={true}
titleI18nKey={'cheatsheet.modal.title'}>
<CheatsheetModalBody />
</CommonModal>
</Fragment>
)
}

View file

@ -0,0 +1,85 @@
/*
* 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 { HtmlToReact } from '../../../common/html-to-react/html-to-react'
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 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 rendered example.
*
* @param extension The extension to render
* @param rootI18nKey An additional i18n namespace
*/
export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ 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 <HtmlToReact htmlCode={markdownIt.render(content)}></HtmlToReact>
}, [i18nPrefix, t])
return (
<EditorToRendererCommunicatorContextProvider>
<ExtensionEventEmitterProvider>
{cheatsheetExtensionComponents}
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.description'} />
</h4>
{descriptionElements}
</ListGroupItem>
<ReadMoreLinkItem url={extension.readMoreUrl}></ReadMoreLinkItem>
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.exampleInput'} />
</h4>
<HighlightedCode code={content} wrapLines={true} language={'markdown'} startLineNumber={1} />
</ListGroupItem>
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.exampleOutput'} />
</h4>
<RenderIframe
frameClasses={'w-100'}
adaptFrameHeightToContent={true}
rendererType={RendererType.SIMPLE}
markdownContentLines={lines}></RenderIframe>
</ListGroupItem>
</ExtensionEventEmitterProvider>
</EditorToRendererCommunicatorContextProvider>
)
}

View file

@ -0,0 +1,65 @@
/*
* 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 { 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'
import { Trans } from 'react-i18next'
/**
* Renders the tab content for the cheatsheet.
*/
export const CheatsheetModalBody: React.FC = () => {
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
const changeExtension = useCallback((value: CheatsheetExtension) => {
setSelectedExtension(value)
setSelectedEntry(isCheatsheetGroup(value) ? value.entries[0] : value)
}, [])
const extensions = useMemo(
() => optionalAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
[]
)
return (
<Modal.Body>
<Row className={`mt-2`}>
<Col xs={3}>
<CategoryAccordion
extensions={extensions}
selectedEntry={selectedExtension}
onStateChange={changeExtension}
/>
</Col>
<Col xs={9}>
<ListGroup>
<TopicSelection
extension={selectedExtension}
selectedEntry={selectedEntry}
setSelectedEntry={setSelectedEntry}
/>
{selectedEntry !== undefined ? (
<CheatsheetEntryPane
rootI18nKey={isCheatsheetGroup(selectedExtension) ? selectedExtension.i18nKey : undefined}
extension={selectedEntry}
/>
) : (
<span>
<Trans i18nKey={'cheatsheet.modal.noSelection'}></Trans>
</span>
)}
</ListGroup>
</Col>
</Row>
</Modal.Body>
)
}

View file

@ -7,3 +7,9 @@
.table-cheatsheet > tr > td { .table-cheatsheet > tr > td {
vertical-align: middle !important; vertical-align: middle !important;
} }
.sticky {
position: sticky;
top: 1rem;
bottom: 1rem;
}

View file

@ -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<CheatsheetListProps> = ({ 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]) => (
<ListGroupItem
key={cheatsheetExtension.i18nKey}
action
active={cheatsheetExtension.i18nKey == selectedEntry?.i18nKey}
onClick={() => onStateChange(cheatsheetExtension)}>
{title}
</ListGroupItem>
)),
[extensions, onStateChange, selectedEntry, t]
)
return <ListGroup className={styles.sticky}>{listItems}</ListGroup>
}

View file

@ -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<ReadMoreLinkGroupProps> = ({ url }) => {
return !url ? null : (
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.readMoreLink'} />
</h4>
<ExternalLink className={'text-dark'} text={url.toString()} href={url.toString()}></ExternalLink>
</ListGroupItem>
)
}

View file

@ -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<EntrySelectionProps> = ({ extension, selectedEntry, setSelectedEntry }) => {
const listItems = useMemo(() => {
if (!isCheatsheetGroup(extension)) {
return null
}
return extension.entries.map((entry) => (
<Button
key={entry.i18nKey}
variant={selectedEntry?.i18nKey === entry.i18nKey ? 'primary' : 'outline-primary'}
onClick={() => setSelectedEntry(entry)}>
<Trans i18nKey={`cheatsheet.${extension.i18nKey}.${entry.i18nKey}.title`}></Trans>
</Button>
))
}, [extension, selectedEntry?.i18nKey, setSelectedEntry])
return !listItems ? null : (
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.selectTopic'} />
</h4>
<ButtonGroup className={'mb-2'}>{listItems}</ButtonGroup>
</ListGroupItem>
)
}

View file

@ -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 (
<Fragment key={'app-extensions'}>
{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 })
}
})}
</Fragment>
)
}, [setContent])
}

View file

@ -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<CheatsheetLineProps> = ({ 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 (
<Suspense
fallback={
<tr>
<td colSpan={2}>
<WaitSpinner />
</td>
</tr>
}>
<tr>
<td>
<eventEmitterContext.Provider value={eventEmitter}>
<DocumentMarkdownRenderer markdownContentLines={lines} baseUrl={'https://example.org'} />
</eventEmitterContext.Provider>
</td>
<td className={'markdown-body'}>
<HighlightedCode code={markdown} wrapLines={true} startLineNumber={1} language={'markdown'} />
</td>
</tr>
</Suspense>
)
}

View file

@ -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<boolean>(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 (
<Table className={`table-condensed ${styles['table-cheatsheet']}`}>
<thead>
<tr>
<th>
<Trans i18nKey='editor.help.cheatsheet.example' />
</th>
<th>
<Trans i18nKey='editor.help.cheatsheet.syntax' />
</th>
</tr>
</thead>
<tbody>
{codes.map((code) => (
<CheatsheetLine markdown={code} key={code} onTaskCheckedChange={setChecked} />
))}
</tbody>
</Table>
)
}
export default CheatsheetTabContent

View file

@ -5,12 +5,11 @@
*/ */
import { useBooleanState } from '../../../../hooks/common/use-boolean-state' import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { cypressId } from '../../../../utils/cypress-attribute' 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 { HelpModal } from './help-modal'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { Button } from 'react-bootstrap'
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons' 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}. * Renders the button to open the {@link HelpModal}.
@ -21,15 +20,16 @@ export const HelpButton: React.FC = () => {
return ( return (
<Fragment> <Fragment>
<Button <IconButton
icon={IconQuestionCircle}
{...cypressId('editor-help-button')} {...cypressId('editor-help-button')}
title={t('editor.documentBar.help') ?? undefined} title={t('editor.documentBar.help') ?? undefined}
className='ms-2 text-secondary' className='ms-2'
size='sm' size='sm'
variant='outline-light' variant='outline-dark'
onClick={showModal}> onClick={showModal}>
<UiIcon icon={IconQuestionCircle} /> <Trans i18nKey={'editor.documentBar.help'} />
</Button> </IconButton>
<HelpModal show={modalVisibility} onHide={closeModal} /> <HelpModal show={modalVisibility} onHide={closeModal} />
</Fragment> </Fragment>
) )

View file

@ -5,7 +5,6 @@
*/ */
import type { ModalVisibilityProps } from '../../../common/modals/common-modal' import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
import { CommonModal } 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 { LinksTabContent } from './links-tab-content'
import { ShortcutTabContent } from './shortcuts-tab-content' import { ShortcutTabContent } from './shortcuts-tab-content'
import React, { useMemo, useState } from 'react' import React, { useMemo, useState } from 'react'
@ -14,7 +13,6 @@ import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
export enum HelpTabStatus { export enum HelpTabStatus {
Cheatsheet = 'cheatsheet.title',
Shortcuts = 'shortcuts.title', Shortcuts = 'shortcuts.title',
Links = 'links.title' Links = 'links.title'
} }
@ -31,13 +29,11 @@ export enum HelpTabStatus {
* @param onHide A callback when the modal should be closed again * @param onHide A callback when the modal should be closed again
*/ */
export const HelpModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => { export const HelpModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Cheatsheet) const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Shortcuts)
const { t } = useTranslation() const { t } = useTranslation()
const tabContent = useMemo(() => { const tabContent = useMemo(() => {
switch (tab) { switch (tab) {
case HelpTabStatus.Cheatsheet:
return <CheatsheetTabContent />
case HelpTabStatus.Shortcuts: case HelpTabStatus.Shortcuts:
return <ShortcutTabContent /> return <ShortcutTabContent />
case HelpTabStatus.Links: case HelpTabStatus.Links:
@ -48,15 +44,9 @@ export const HelpModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
const modalTitle = useMemo(() => t('editor.documentBar.help') + ' - ' + t(`editor.help.${tab}`), [t, tab]) const modalTitle = useMemo(() => t('editor.documentBar.help') + ' - ' + t(`editor.help.${tab}`), [t, tab])
return ( return (
<CommonModal modalSize={'lg'} titleIcon={IconQuestionCircle} show={show} onHide={onHide} title={modalTitle}> <CommonModal modalSize={'xl'} titleIcon={IconQuestionCircle} show={show} onHide={onHide} title={modalTitle}>
<Modal.Body> <Modal.Body>
<nav className='nav nav-tabs'> <nav className='nav nav-tabs'>
<Button
variant={'light'}
className={`nav-link nav-item ${tab === HelpTabStatus.Cheatsheet ? 'active' : ''}`}
onClick={() => setTab(HelpTabStatus.Cheatsheet)}>
<Trans i18nKey={'editor.help.cheatsheet.title'} />
</Button>
<Button <Button
variant={'light'} variant={'light'}
className={`nav-link nav-item ${tab === HelpTabStatus.Shortcuts ? 'active' : ''}`} className={`nav-link nav-item ${tab === HelpTabStatus.Shortcuts ? 'active' : ''}`}

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type React from 'react'
export interface CheatsheetExtensionComponentProps {
setContent: (dispatcher: string | ((prevState: string) => 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<CheatsheetExtensionComponentProps>
readMoreUrl?: URL
}

View file

@ -0,0 +1,63 @@
/*
* 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.basicFormatting',
categoryI18nKey: 'basic'
},
{
i18nKey: 'basics.abbreviation',
categoryI18nKey: 'basic'
},
{
i18nKey: 'basics.footnote',
categoryI18nKey: 'basic'
},
{
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'
}
]
}
}

View file

@ -3,7 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 { imageSize } from '@hedgedoc/markdown-it-plugins'
import type MarkdownIt from 'markdown-it' import type MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr' 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. * Adds some common markdown syntaxes to the markdown rendering.
*/ */
export class GenericSyntaxMarkdownExtension extends MarkdownRendererExtension { export class BasicMarkdownSyntaxMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void { public configureMarkdownIt(markdownIt: MarkdownIt): void {
abbreviation(markdownIt) abbreviation(markdownIt)
definitionList(markdownIt) definitionList(markdownIt)

View file

@ -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/') }]
}
}

View file

@ -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/')
}
]
}
}

View file

@ -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'
}
]
}
}

View file

@ -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'
}
]
}
}

View file

@ -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'
}
]
}
]
}
}

View file

@ -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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { tocSlugify } from '../../editor-page/table-of-contents/toc-slugify' import { tocSlugify } from '../../../editor-page/table-of-contents/toc-slugify'
import { MarkdownRendererExtension } from './base/markdown-renderer-extension' import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import type { TocAst } from '@hedgedoc/markdown-it-plugins' import type { TocAst } from '@hedgedoc/markdown-it-plugins'
import { toc } from '@hedgedoc/markdown-it-plugins' import { toc } from '@hedgedoc/markdown-it-plugins'
import equal from 'fast-deep-equal' import equal from 'fast-deep-equal'

View file

@ -1,20 +1,14 @@
/* /*
* 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions' import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension' 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 { 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 { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
import { LinkAdjustmentMarkdownExtension } from '../extensions/link-replacer/link-adjustment-markdown-extension' import { LinkAdjustmentMarkdownExtension } from '../extensions/link-replacer/link-adjustment-markdown-extension'
import { LinkifyFixMarkdownExtension } from '../extensions/linkify-fix/linkify-fix-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 { UploadIndicatingImageFrameMarkdownExtension } from '../extensions/upload-indicating-image-frame/upload-indicating-image-frame-markdown-extension'
import { useExtensionEventEmitter } from './use-extension-event-emitter' import { useExtensionEventEmitter } from './use-extension-event-emitter'
import { useMemo } from 'react' import { useMemo } from 'react'
@ -35,14 +29,8 @@ export const useMarkdownExtensions = (
return [ return [
...optionalAppExtensions.flatMap((extension) => extension.buildMarkdownRendererExtensions(extensionEventEmitter)), ...optionalAppExtensions.flatMap((extension) => extension.buildMarkdownRendererExtensions(extensionEventEmitter)),
...additionalExtensions, ...additionalExtensions,
new TableOfContentsMarkdownExtension(),
new IframeCapsuleMarkdownExtension(),
new ImagePlaceholderMarkdownExtension(),
new UploadIndicatingImageFrameMarkdownExtension(), new UploadIndicatingImageFrameMarkdownExtension(),
new LinkAdjustmentMarkdownExtension(baseUrl), new LinkAdjustmentMarkdownExtension(baseUrl),
new EmojiMarkdownExtension(),
new BootstrapIconMarkdownExtension(),
new GenericSyntaxMarkdownExtension(),
new LinkifyFixMarkdownExtension(), new LinkifyFixMarkdownExtension(),
new DebuggerMarkdownExtension(), new DebuggerMarkdownExtension(),
new ProxyImageMarkdownExtension() new ProxyImageMarkdownExtension()

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { ShowIf } from '../common/show-if/show-if' 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 { useExtensionEventEmitterHandler } from '../markdown-renderer/hooks/use-extension-event-emitter'
import styles from './markdown-document.module.scss' import styles from './markdown-document.module.scss'
import { WidthBasedTableOfContents } from './width-based-table-of-contents' import { WidthBasedTableOfContents } from './width-based-table-of-contents'

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 { Linter } from '../../components/editor-page/editor-pane/linter/linter'
import type { MarkdownRendererExtension } from '../../components/markdown-renderer/extensions/base/markdown-renderer-extension' import type { MarkdownRendererExtension } from '../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import type { EventEmitter2 } from 'eventemitter2' import type { EventEmitter2 } from 'eventemitter2'
@ -22,4 +23,8 @@ export abstract class AppExtension {
public buildEditorExtensionComponent(): React.FC { public buildEditorExtensionComponent(): React.FC {
return Fragment return Fragment
} }
public buildCheatsheetExtensions(): CheatsheetExtension[] {
return []
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { AbcjsMarkdownExtension } from './abcjs-markdown-extension' import { AbcjsMarkdownExtension } from './abcjs-markdown-extension'
@ -11,4 +12,8 @@ export class AbcjsAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new AbcjsMarkdownExtension()] return [new AbcjsMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'abcjs', categoryI18nKey: 'charts', readMoreUrl: new URL('https://www.abcjs.net/') }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { AlertMarkdownExtension } from './alert-markdown-extension' import { AlertMarkdownExtension } from './alert-markdown-extension'
@ -14,4 +15,8 @@ export class AlertAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new AlertMarkdownExtension()] return [new AlertMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'alert' }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { AsciinemaMarkdownExtension } from './asciinema-markdown-extension' import { AsciinemaMarkdownExtension } from './asciinema-markdown-extension'
@ -16,4 +17,8 @@ export class AsciinemaAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new AsciinemaMarkdownExtension()] return [new AsciinemaMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'asciinema', categoryI18nKey: 'embedding', readMoreUrl: new URL('https://asciinema.org/') }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension' import { BlockquoteExtraTagMarkdownExtension } from './blockquote-extra-tag-markdown-extension'
@ -14,4 +15,8 @@ export class BlockquoteAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new BlockquoteExtraTagMarkdownExtension()] return [new BlockquoteExtraTagMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'blockquoteTags', entries: [{ i18nKey: 'name' }, { i18nKey: 'color' }, { i18nKey: 'time' }] }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { CsvTableMarkdownExtension } from './csv-table-markdown-extension' import { CsvTableMarkdownExtension } from './csv-table-markdown-extension'
@ -14,4 +15,8 @@ export class CsvTableAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new CsvTableMarkdownExtension()] return [new CsvTableMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'csv', entries: [{ i18nKey: 'table' }, { i18nKey: 'header' }] }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { FlowchartMarkdownExtension } from './flowchart-markdown-extension' import { FlowchartMarkdownExtension } from './flowchart-markdown-extension'
@ -14,4 +15,8 @@ export class FlowchartAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new FlowchartMarkdownExtension()] return [new FlowchartMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'flowchart', categoryI18nKey: 'charts', readMoreUrl: new URL('https://flowchart.js.org/') }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { GistMarkdownExtension } from './gist-markdown-extension' import { GistMarkdownExtension } from './gist-markdown-extension'
@ -14,4 +15,8 @@ export class GistAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new GistMarkdownExtension()] return [new GistMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'gist', categoryI18nKey: 'embedding', readMoreUrl: new URL('https://gist.github.com/') }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { GraphvizMarkdownExtension } from './graphviz-markdown-extension' import { GraphvizMarkdownExtension } from './graphviz-markdown-extension'
@ -14,4 +15,8 @@ export class GraphvizAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new GraphvizMarkdownExtension()] return [new GraphvizMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'graphviz', categoryI18nKey: 'charts', readMoreUrl: new URL('https://graphviz.org/') }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension' import { HighlightedCodeMarkdownExtension } from './highlighted-code-markdown-extension'
@ -14,4 +15,13 @@ export class HighlightedCodeFenceAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new HighlightedCodeMarkdownExtension()] return [new HighlightedCodeMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'codeHighlighting',
entries: [{ i18nKey: 'language' }, { i18nKey: 'lineNumbers' }, { i18nKey: 'lineWrapping' }]
}
]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { KatexMarkdownExtension } from './katex-markdown-extension' import { KatexMarkdownExtension } from './katex-markdown-extension'
@ -16,4 +17,8 @@ export class KatexAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new KatexMarkdownExtension()] return [new KatexMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'katex', readMoreUrl: new URL('https://katex.org/') }]
}
} }

View file

@ -3,9 +3,8 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { HtmlToReact } from '../../../components/common/html-to-react/html-to-react'
import { testId } from '../../../utils/test-id' import { testId } from '../../../utils/test-id'
import convertHtmlToReact from '@hedgedoc/html-to-react'
import { sanitize } from 'dompurify'
import KaTeX from 'katex' import KaTeX from 'katex'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
@ -26,10 +25,12 @@ export const KatexFrame: React.FC<KatexFrameProps> = ({ expression, block = fals
const dom = useMemo(() => { const dom = useMemo(() => {
try { try {
const katexHtml = KaTeX.renderToString(expression, { const katexHtml = KaTeX.renderToString(expression, {
displayMode: block === true, displayMode: block,
throwOnError: true throwOnError: true
}) })
return convertHtmlToReact(sanitize(katexHtml, { ADD_TAGS: ['semantics', 'annotation'] })) return (
<HtmlToReact htmlCode={katexHtml} domPurifyConfig={{ ADD_TAGS: ['semantics', 'annotation'] }}></HtmlToReact>
)
} catch (error) { } catch (error) {
return ( return (
<Alert className={block ? '' : 'd-inline-block'} variant={'danger'}> <Alert className={block ? '' : 'd-inline-block'} variant={'danger'}>

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { MermaidMarkdownExtension } from './mermaid-markdown-extension' import { MermaidMarkdownExtension } from './mermaid-markdown-extension'
@ -14,4 +15,8 @@ export class MermaidAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new MermaidMarkdownExtension()] return [new MermaidMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'mermaid', categoryI18nKey: 'charts', readMoreUrl: new URL('https://mermaid.js.org/') }]
}
} }

View file

@ -3,6 +3,12 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { AppExtension } from '../base/app-extension'
import { AbcjsAppExtension } from './abcjs/abcjs-app-extension' import { AbcjsAppExtension } from './abcjs/abcjs-app-extension'
import { AlertAppExtension } from './alert/alert-app-extension' import { AlertAppExtension } from './alert/alert-app-extension'
@ -48,5 +54,11 @@ export const optionalAppExtensions: AppExtension[] = [
new YoutubeAppExtension(), new YoutubeAppExtension(),
new TaskListCheckboxAppExtension(), new TaskListCheckboxAppExtension(),
new HighlightedCodeFenceAppExtension(), new HighlightedCodeFenceAppExtension(),
new ForkAwesomeHtmlTagAppExtension() new ForkAwesomeHtmlTagAppExtension(),
new BootstrapIconAppExtension(),
new EmojiAppExtension(),
new TableOfContentsAppExtension(),
new ImagePlaceholderAppExtension(),
new IframeCapsuleAppExtension(),
new BasicMarkdownSyntaxAppExtension()
] ]

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { PlantumlMarkdownExtension } from './plantuml-markdown-extension' import { PlantumlMarkdownExtension } from './plantuml-markdown-extension'
@ -16,4 +17,8 @@ export class PlantumlAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new PlantumlMarkdownExtension()] return [new PlantumlMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'plantuml', categoryI18nKey: 'charts', readMoreUrl: new URL('https://plantuml.com/') }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { SpoilerMarkdownExtension } from './spoiler-markdown-extension' import { SpoilerMarkdownExtension } from './spoiler-markdown-extension'
@ -16,4 +17,8 @@ export class SpoilerAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new SpoilerMarkdownExtension()] return [new SpoilerMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'spoiler' }]
}
} }

View file

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Returns the markdown line prefix for a task list checkbox.
*
* @param state The check state of the checkbox.
*/
export const createCheckboxContent = (state: boolean) => {
return `[${state ? 'x' : ' '}]`
}

View file

@ -13,7 +13,7 @@ type EventEmittingTaskListCheckboxProps = Omit<TaskListProps, 'onTaskCheckedChan
export interface TaskCheckedEventPayload { export interface TaskCheckedEventPayload {
lineInMarkdown: number lineInMarkdown: number
checked: boolean newCheckedState: boolean
} }
/** /**
@ -25,7 +25,10 @@ export const EventEmittingTaskListCheckbox: React.FC<EventEmittingTaskListCheckb
const emitter = useExtensionEventEmitter() const emitter = useExtensionEventEmitter()
const sendEvent: TaskCheckedChangeHandler = useCallback( const sendEvent: TaskCheckedChangeHandler = useCallback(
(lineInMarkdown: number, checked: boolean) => { (lineInMarkdown: number, checked: boolean) => {
emitter?.emit(TaskListCheckboxAppExtension.EVENT_NAME, { lineInMarkdown, checked } as TaskCheckedEventPayload) emitter?.emit(TaskListCheckboxAppExtension.EVENT_NAME, {
lineInMarkdown,
newCheckedState: checked
} as TaskCheckedEventPayload)
}, },
[emitter] [emitter]
) )

View file

@ -0,0 +1,34 @@
/*
* 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]?])/
/**
* Checks if the given markdown content contains a task list checkbox at the given line index.
*
* @param markdownContent The content that should be checked
* @param lineIndex The index of the line that should be checked for a task list checkbox
* @return An {@link Optional} that contains the start and end index of the found checkbox
*/
export const findCheckBox = (
markdownContent: string,
lineIndex: number
): Optional<[startIndex: number, endIndex: number]> => {
const lines = markdownContent.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)
}

View file

@ -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 { findCheckBox } from './find-check-box'
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<CheatsheetExtensionComponentProps> = ({ setContent }) => {
useExtensionEventEmitterHandler(TaskListCheckboxAppExtension.EVENT_NAME, (event: TaskCheckedEventPayload) => {
setContent((previousContent) => {
return findCheckBox(previousContent, event.lineInMarkdown)
.map(
([startIndex, endIndex]) =>
previousContent.slice(0, startIndex) +
createCheckboxContent(event.newCheckedState) +
previousContent.slice(endIndex)
)
.orElse(previousContent)
})
})
return null
}

View file

@ -3,10 +3,16 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 { 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 { findCheckBox } from './find-check-box'
import { TaskListCheckboxAppExtension } from './task-list-checkbox-app-extension' import { TaskListCheckboxAppExtension } from './task-list-checkbox-app-extension'
import { useSetCheckboxInEditor } from './use-set-checkbox-in-editor'
import type React from 'react' import type React from 'react'
import { useCallback } from 'react'
/** /**
* Receives task-checkbox-change events and modify the current editor content. * 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) useExtensionEventEmitterHandler(TaskListCheckboxAppExtension.EVENT_NAME, changeCallback)
return null 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 = findCheckBox(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)
}
]
}

View file

@ -3,7 +3,9 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { CheatsheetExtension } from '../../../components/editor-page/cheatsheet/cheatsheet-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { SetCheckboxInCheatsheet } from './set-checkbox-in-cheatsheet'
import { SetCheckboxInEditor } from './set-checkbox-in-editor' import { SetCheckboxInEditor } from './set-checkbox-in-editor'
import { TaskListMarkdownExtension } from './task-list-markdown-extension' import { TaskListMarkdownExtension } from './task-list-markdown-extension'
import type { EventEmitter2 } from 'eventemitter2' import type { EventEmitter2 } from 'eventemitter2'
@ -22,4 +24,8 @@ export class TaskListCheckboxAppExtension extends AppExtension {
buildEditorExtensionComponent(): React.FC { buildEditorExtensionComponent(): React.FC {
return SetCheckboxInEditor return SetCheckboxInEditor
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'taskList', cheatsheetExtensionComponent: SetCheckboxInCheatsheet }]
}
} }

View file

@ -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' : ' '}]`
}
]
}

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import { AppExtension } from '../../base/app-extension' import { AppExtension } from '../../base/app-extension'
import { VegaLiteMarkdownExtension } from './vega-lite-markdown-extension' import { VegaLiteMarkdownExtension } from './vega-lite-markdown-extension'
@ -14,4 +15,10 @@ export class VegaLiteAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] { buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new VegaLiteMarkdownExtension()] return [new VegaLiteMarkdownExtension()]
} }
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{ i18nKey: 'vegaLite', categoryI18nKey: 'charts', readMoreUrl: new URL('https://vega.github.io/vega-lite/') }
]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 { Linter } from '../../../components/editor-page/editor-pane/linter/linter'
import { SingleLineRegexLinter } from '../../../components/editor-page/editor-pane/linter/single-line-regex-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' 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/') }]
}
} }

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * 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 { Linter } from '../../../components/editor-page/editor-pane/linter/linter'
import { SingleLineRegexLinter } from '../../../components/editor-page/editor-pane/linter/single-line-regex-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' 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/') }]
}
} }