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",
"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,253 @@
"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 { 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<AppBarProps> = ({ mode }) => {
<ReadOnlyModeButton />
</ShowIf>
<HelpButton />
<CheatsheetButton />
</ShowIf>
</Nav>
<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 {
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 { 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 (
<Fragment>
<Button
<IconButton
icon={IconQuestionCircle}
{...cypressId('editor-help-button')}
title={t('editor.documentBar.help') ?? undefined}
className='ms-2 text-secondary'
className='ms-2'
size='sm'
variant='outline-light'
variant='outline-dark'
onClick={showModal}>
<UiIcon icon={IconQuestionCircle} />
</Button>
<Trans i18nKey={'editor.documentBar.help'} />
</IconButton>
<HelpModal show={modalVisibility} onHide={closeModal} />
</Fragment>
)

View file

@ -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<ModalVisibilityProps> = ({ show, onHide }) => {
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Cheatsheet)
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Shortcuts)
const { t } = useTranslation()
const tabContent = useMemo(() => {
switch (tab) {
case HelpTabStatus.Cheatsheet:
return <CheatsheetTabContent />
case HelpTabStatus.Shortcuts:
return <ShortcutTabContent />
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])
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>
<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
variant={'light'}
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
*/
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)

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
*/
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'

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
*/
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 { 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 { useMemo } from 'react'
@ -35,14 +29,8 @@ export const useMarkdownExtensions = (
return [
...optionalAppExtensions.flatMap((extension) => extension.buildMarkdownRendererExtensions(extensionEventEmitter)),
...additionalExtensions,
new TableOfContentsMarkdownExtension(),
new IframeCapsuleMarkdownExtension(),
new ImagePlaceholderMarkdownExtension(),
new UploadIndicatingImageFrameMarkdownExtension(),
new LinkAdjustmentMarkdownExtension(baseUrl),
new EmojiMarkdownExtension(),
new BootstrapIconMarkdownExtension(),
new GenericSyntaxMarkdownExtension(),
new LinkifyFixMarkdownExtension(),
new DebuggerMarkdownExtension(),
new ProxyImageMarkdownExtension()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()
]

View file

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

View file

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

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 {
lineInMarkdown: number
checked: boolean
newCheckedState: boolean
}
/**
@ -25,7 +25,10 @@ export const EventEmittingTaskListCheckbox: React.FC<EventEmittingTaskListCheckb
const emitter = useExtensionEventEmitter()
const sendEvent: TaskCheckedChangeHandler = useCallback(
(lineInMarkdown: number, checked: boolean) => {
emitter?.emit(TaskListCheckboxAppExtension.EVENT_NAME, { lineInMarkdown, checked } as TaskCheckedEventPayload)
emitter?.emit(TaskListCheckboxAppExtension.EVENT_NAME, {
lineInMarkdown,
newCheckedState: checked
} as TaskCheckedEventPayload)
},
[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
*/
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 { findCheckBox } from './find-check-box'
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 = 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
*/
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 }]
}
}

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

View file

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

View file

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