mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 09:46:30 -05:00
modularize renderer (#552)
This commit is contained in:
parent
ac00bc98c0
commit
a86d4cbc58
15 changed files with 580 additions and 455 deletions
|
@ -1,7 +1,10 @@
|
|||
import React from 'react'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import markdownItContainer from 'markdown-it-container'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Table } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { MarkdownRenderer } from '../../../markdown-renderer/markdown-renderer'
|
||||
import { BasicMarkdownRenderer } from '../../../markdown-renderer/basic-markdown-renderer'
|
||||
import { createRenderContainer, validAlertLevels } from '../../../markdown-renderer/markdown-it-plugins/alert-container'
|
||||
import { HighlightedCode } from '../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code'
|
||||
import './cheatsheet.scss'
|
||||
|
||||
|
@ -27,6 +30,13 @@ export const Cheatsheet: React.FC = () => {
|
|||
':smile:',
|
||||
`:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::`
|
||||
]
|
||||
|
||||
const markdownItPlugins = useCallback((md: MarkdownIt) => {
|
||||
validAlertLevels.forEach(level => {
|
||||
md.use(markdownItContainer, level, { render: createRenderContainer(level) })
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Table className="table-condensed table-cheatsheet">
|
||||
<thead>
|
||||
|
@ -40,14 +50,10 @@ export const Cheatsheet: React.FC = () => {
|
|||
return (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<MarkdownRenderer
|
||||
<BasicMarkdownRenderer
|
||||
content={code}
|
||||
wide={false}
|
||||
onTaskCheckedChange={(_) => null}
|
||||
onTocChange={() => false}
|
||||
onMetaDataChange={() => false}
|
||||
onFirstHeadingChange={() => false}
|
||||
onLineMarkerPositionChanged={() => false}
|
||||
onConfigureMarkdownIt={markdownItPlugins}
|
||||
/>
|
||||
</td>
|
||||
<td className={'markdown-body'}>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Cheatsheet } from './cheatsheet'
|
|||
import { Links } from './links'
|
||||
import { Shortcut } from './shortcuts'
|
||||
|
||||
enum HelpTabStatus {
|
||||
export enum HelpTabStatus {
|
||||
Cheatsheet='cheatsheet.title',
|
||||
Shortcuts='shortcuts.title',
|
||||
Links='links.title'
|
||||
|
|
|
@ -4,7 +4,8 @@ import useResizeObserver from 'use-resize-observer'
|
|||
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { LineMarkerPosition, MarkdownRenderer } from '../../markdown-renderer/markdown-renderer'
|
||||
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||
import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
|
||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
||||
import { findLineMarks } from '../scroll/utils'
|
||||
import { TableOfContents } from '../table-of-contents/table-of-contents'
|
||||
|
@ -108,16 +109,18 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps>
|
|||
<div className={'bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 overflow-y-scroll'}
|
||||
ref={renderer} onScroll={userScroll} onMouseEnter={onMakeScrollSource}>
|
||||
<div className={'col-md'}/>
|
||||
<MarkdownRenderer
|
||||
className={'flex-fill mb-3'}
|
||||
content={content}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={setLineMarks}
|
||||
onMetaDataChange={onMetadataChange}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onTocChange={(tocAst) => setTocAst(tocAst)}
|
||||
wide={wide}
|
||||
/>
|
||||
<div className={'bg-light flex-fill'}>
|
||||
<FullMarkdownRenderer
|
||||
className={'flex-fill mb-3'}
|
||||
content={content}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={setLineMarks}
|
||||
onMetaDataChange={onMetadataChange}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onTocChange={(tocAst) => setTocAst(tocAst)}
|
||||
wide={wide}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'col-md'}>
|
||||
<ShowIf condition={realWidth >= 1280 && !!tocAst}>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { LineMarkerPosition } from '../../markdown-renderer/markdown-renderer'
|
||||
import { LineMarkerPosition } from '../../markdown-renderer/types'
|
||||
|
||||
export const findLineMarks = (lineMarks: LineMarkerPosition[], lineNumber: number): { lastMarkBefore: LineMarkerPosition | undefined, firstMarkAfter: LineMarkerPosition | undefined } => {
|
||||
let lastMarkBefore
|
||||
let firstMarkAfter
|
||||
|
|
106
src/components/markdown-renderer/basic-markdown-renderer.tsx
Normal file
106
src/components/markdown-renderer/basic-markdown-renderer.tsx
Normal file
|
@ -0,0 +1,106 @@
|
|||
import MarkdownIt from 'markdown-it'
|
||||
import abbreviation from 'markdown-it-abbr'
|
||||
import definitionList from 'markdown-it-deflist'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import footnote from 'markdown-it-footnote'
|
||||
import imsize from 'markdown-it-imsize'
|
||||
import inserted from 'markdown-it-ins'
|
||||
import marked from 'markdown-it-mark'
|
||||
import subscript from 'markdown-it-sub'
|
||||
import superscript from 'markdown-it-sup'
|
||||
import React, { ReactElement, RefObject, useMemo, useRef } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import ReactHtmlParser from 'react-html-parser'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { combinedEmojiData } from './markdown-it-plugins/emoji/mapping'
|
||||
import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
|
||||
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
|
||||
import './markdown-renderer.scss'
|
||||
import { ComponentReplacer } from './replace-components/ComponentReplacer'
|
||||
import { AdditionalMarkdownRendererProps, LineKeys } from './types'
|
||||
import { buildTransformer } from './utils/html-react-transformer'
|
||||
import { calculateNewLineNumberMapping } from './utils/line-number-mapping'
|
||||
|
||||
export interface BasicMarkdownRendererProps {
|
||||
componentReplacers?: ComponentReplacer[],
|
||||
onConfigureMarkdownIt?: (md: MarkdownIt) => void,
|
||||
documentReference?: RefObject<HTMLDivElement>
|
||||
onBeforeRendering?: () => void
|
||||
}
|
||||
|
||||
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
|
||||
className,
|
||||
content,
|
||||
wide,
|
||||
componentReplacers,
|
||||
onConfigureMarkdownIt,
|
||||
documentReference,
|
||||
onBeforeRendering
|
||||
}) => {
|
||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||
|
||||
const markdownIt = useMemo(() => {
|
||||
const md = new MarkdownIt('default', {
|
||||
html: true,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
|
||||
md.use(emoji, {
|
||||
defs: combinedEmojiData
|
||||
})
|
||||
md.use(abbreviation)
|
||||
md.use(definitionList)
|
||||
md.use(subscript)
|
||||
md.use(superscript)
|
||||
md.use(inserted)
|
||||
md.use(marked)
|
||||
md.use(footnote)
|
||||
md.use(imsize)
|
||||
|
||||
if (onConfigureMarkdownIt) {
|
||||
onConfigureMarkdownIt(md)
|
||||
}
|
||||
|
||||
md.use(linkifyExtra)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
md.use(MarkdownItParserDebugger)
|
||||
}
|
||||
|
||||
return md
|
||||
}, [onConfigureMarkdownIt])
|
||||
|
||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||
const lastUsedLineId = useRef<number>(0)
|
||||
|
||||
const markdownReactDom: ReactElement[] = useMemo(() => {
|
||||
if (onBeforeRendering) {
|
||||
onBeforeRendering()
|
||||
}
|
||||
const trimmedContent = content.substr(0, maxLength)
|
||||
const html: string = markdownIt.render(trimmedContent)
|
||||
const contentLines = trimmedContent.split('\n')
|
||||
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
|
||||
oldMarkdownLineKeys.current = newLines
|
||||
lastUsedLineId.current = newLastUsedLineId
|
||||
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers) : undefined
|
||||
return ReactHtmlParser(html, { transform: transformer })
|
||||
}, [onBeforeRendering, content, maxLength, markdownIt, componentReplacers])
|
||||
|
||||
return (
|
||||
<div className={`${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`}>
|
||||
<ShowIf condition={content.length > maxLength}>
|
||||
<Alert variant='danger' dir={'auto'}>
|
||||
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }}/>
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<div ref={documentReference} className={'markdown-body w-100 d-flex flex-column align-items-center'}>
|
||||
{markdownReactDom}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
227
src/components/markdown-renderer/full-markdown-renderer.tsx
Normal file
227
src/components/markdown-renderer/full-markdown-renderer.tsx
Normal file
|
@ -0,0 +1,227 @@
|
|||
import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists'
|
||||
import yaml from 'js-yaml'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import anchor from 'markdown-it-anchor'
|
||||
import markdownItContainer from 'markdown-it-container'
|
||||
import frontmatter from 'markdown-it-front-matter'
|
||||
import mathJax from 'markdown-it-mathjax'
|
||||
import plantuml from 'markdown-it-plantuml'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import toc from 'markdown-it-toc-done-right'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import { InternalLink } from '../common/links/internal-link'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { slugify } from '../editor/table-of-contents/table-of-contents'
|
||||
import { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||
import { BasicMarkdownRenderer } from './basic-markdown-renderer'
|
||||
import { createRenderContainer, validAlertLevels } from './markdown-it-plugins/alert-container'
|
||||
import { firstHeaderExtractor } from './markdown-it-plugins/first-header-extractor'
|
||||
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
|
||||
import { LineMarkers, lineNumberMarker } from './markdown-it-plugins/line-number-marker'
|
||||
import { plantumlError } from './markdown-it-plugins/plantuml-error'
|
||||
import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
|
||||
import { replaceGistLink } from './regex-plugins/replace-gist-link'
|
||||
import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
|
||||
import { replaceLegacySlideshareShortCode } from './regex-plugins/replace-legacy-slideshare-short-code'
|
||||
import { replaceLegacySpeakerdeckShortCode } from './regex-plugins/replace-legacy-speakerdeck-short-code'
|
||||
import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code'
|
||||
import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-short-code'
|
||||
import { replacePdfShortCode } from './regex-plugins/replace-pdf-short-code'
|
||||
import { replaceQuoteExtraAuthor } from './regex-plugins/replace-quote-extra-author'
|
||||
import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-color'
|
||||
import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
|
||||
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
|
||||
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
|
||||
import { AbcReplacer } from './replace-components/abc/abc-replacer'
|
||||
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
|
||||
import { CsvReplacer } from './replace-components/csv/csv-replacer'
|
||||
import { FlowchartReplacer } from './replace-components/flow/flowchart-replacer'
|
||||
import { GistReplacer } from './replace-components/gist/gist-replacer'
|
||||
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
|
||||
import { ImageReplacer } from './replace-components/image/image-replacer'
|
||||
import { KatexReplacer } from './replace-components/katex/katex-replacer'
|
||||
import { LinemarkerReplacer } from './replace-components/linemarker/linemarker-replacer'
|
||||
import { MermaidReplacer } from './replace-components/mermaid/mermaid-replacer'
|
||||
import { PdfReplacer } from './replace-components/pdf/pdf-replacer'
|
||||
import { PossibleWiderReplacer } from './replace-components/possible-wider/possible-wider-replacer'
|
||||
import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-options-replacer'
|
||||
import { SequenceDiagramReplacer } from './replace-components/sequence-diagram/sequence-diagram-replacer'
|
||||
import { TaskListReplacer } from './replace-components/task-list/task-list-replacer'
|
||||
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
|
||||
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
|
||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||
import { usePostMetaDataOnChange } from './utils/use-post-meta-data-on-change'
|
||||
import { usePostTocAstOnChange } from './utils/use-post-toc-ast-on-change'
|
||||
|
||||
export interface FullMarkdownRendererProps {
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
||||
onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast: TocAst) => void
|
||||
}
|
||||
|
||||
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onMetaDataChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
content,
|
||||
className,
|
||||
wide
|
||||
}) => {
|
||||
const allReplacers = useMemo(() => {
|
||||
return [
|
||||
new LinemarkerReplacer(),
|
||||
new PossibleWiderReplacer(),
|
||||
new GistReplacer(),
|
||||
new YoutubeReplacer(),
|
||||
new VimeoReplacer(),
|
||||
new AsciinemaReplacer(),
|
||||
new AbcReplacer(),
|
||||
new PdfReplacer(),
|
||||
new ImageReplacer(),
|
||||
new SequenceDiagramReplacer(),
|
||||
new CsvReplacer(),
|
||||
new FlowchartReplacer(),
|
||||
new MermaidReplacer(),
|
||||
new HighlightedCodeReplacer(),
|
||||
new QuoteOptionsReplacer(),
|
||||
new KatexReplacer(),
|
||||
new TaskListReplacer(onTaskCheckedChange)
|
||||
]
|
||||
}, [onTaskCheckedChange])
|
||||
|
||||
const [yamlError, setYamlError] = useState(false)
|
||||
|
||||
const plantumlServer = useSelector((state: ApplicationState) => state.config.plantumlServer)
|
||||
|
||||
const rawMetaRef = useRef<RawYAMLMetadata>()
|
||||
const firstHeadingRef = useRef<string>()
|
||||
const documentElement = useRef<HTMLDivElement>(null)
|
||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||
usePostMetaDataOnChange(rawMetaRef.current, firstHeadingRef.current, onMetaDataChange, onFirstHeadingChange)
|
||||
useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0)
|
||||
|
||||
const tocAst = useRef<TocAst>()
|
||||
usePostTocAstOnChange(tocAst, onTocChange)
|
||||
|
||||
const configureMarkdownIt = useCallback((md: MarkdownIt): void => {
|
||||
if (onFirstHeadingChange) {
|
||||
md.use(firstHeaderExtractor(), {
|
||||
firstHeaderFound: (firstHeader: string | undefined) => {
|
||||
firstHeadingRef.current = firstHeader
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (onMetaDataChange) {
|
||||
md.use(frontmatter, (rawMeta: string) => {
|
||||
try {
|
||||
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
|
||||
setYamlError(false)
|
||||
rawMetaRef.current = meta
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setYamlError(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
md.use(markdownItTaskLists, { lineNumber: true })
|
||||
if (plantumlServer) {
|
||||
md.use(plantuml, {
|
||||
openMarker: '```plantuml',
|
||||
closeMarker: '```',
|
||||
server: plantumlServer
|
||||
})
|
||||
} else {
|
||||
md.use(plantumlError)
|
||||
}
|
||||
|
||||
if (onMetaDataChange) {
|
||||
md.use(frontmatter, (rawMeta: string) => {
|
||||
try {
|
||||
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
|
||||
setYamlError(false)
|
||||
rawMetaRef.current = meta
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setYamlError(true)
|
||||
rawMetaRef.current = ({} as RawYAMLMetadata)
|
||||
}
|
||||
})
|
||||
}
|
||||
// noinspection CheckTagEmptyBody
|
||||
md.use(anchor, {
|
||||
permalink: true,
|
||||
permalinkBefore: true,
|
||||
permalinkClass: 'heading-anchor text-dark',
|
||||
permalinkSymbol: '<i class="fa fa-link"></i>'
|
||||
})
|
||||
md.use(mathJax({
|
||||
beforeMath: '<app-katex>',
|
||||
afterMath: '</app-katex>',
|
||||
beforeInlineMath: '<app-katex inline>',
|
||||
afterInlineMath: '</app-katex>',
|
||||
beforeDisplayMath: '<app-katex>',
|
||||
afterDisplayMath: '</app-katex>'
|
||||
}))
|
||||
md.use(markdownItRegex, replaceLegacyYoutubeShortCode)
|
||||
md.use(markdownItRegex, replaceLegacyVimeoShortCode)
|
||||
md.use(markdownItRegex, replaceLegacyGistShortCode)
|
||||
md.use(markdownItRegex, replaceLegacySlideshareShortCode)
|
||||
md.use(markdownItRegex, replaceLegacySpeakerdeckShortCode)
|
||||
md.use(markdownItRegex, replacePdfShortCode)
|
||||
md.use(markdownItRegex, replaceAsciinemaLink)
|
||||
md.use(markdownItRegex, replaceYouTubeLink)
|
||||
md.use(markdownItRegex, replaceVimeoLink)
|
||||
md.use(markdownItRegex, replaceGistLink)
|
||||
md.use(highlightedCode)
|
||||
md.use(markdownItRegex, replaceQuoteExtraAuthor)
|
||||
md.use(markdownItRegex, replaceQuoteExtraColor)
|
||||
md.use(markdownItRegex, replaceQuoteExtraTime)
|
||||
md.use(toc, {
|
||||
placeholder: '(\\[TOC\\]|\\[toc\\])',
|
||||
listType: 'ul',
|
||||
level: [1, 2, 3],
|
||||
callback: (code: string, ast: TocAst): void => {
|
||||
tocAst.current = ast
|
||||
},
|
||||
slugify: slugify
|
||||
})
|
||||
validAlertLevels.forEach(level => {
|
||||
md.use(markdownItContainer, level, { render: createRenderContainer(level) })
|
||||
})
|
||||
md.use(lineNumberMarker(), {
|
||||
postLineMarkers: (lineMarkers) => {
|
||||
currentLineMarkers.current = lineMarkers
|
||||
}
|
||||
})
|
||||
}, [onFirstHeadingChange, onMetaDataChange, plantumlServer])
|
||||
|
||||
const clearMetadata = useCallback(() => {
|
||||
rawMetaRef.current = undefined
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={'position-relative'}>
|
||||
<ShowIf condition={yamlError}>
|
||||
<Alert variant='warning' dir='auto'>
|
||||
<Trans i18nKey='editor.invalidYaml'>
|
||||
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-dark'/>
|
||||
</Trans>
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<BasicMarkdownRenderer className={className} wide={wide} content={content} componentReplacers={allReplacers}
|
||||
onConfigureMarkdownIt={configureMarkdownIt} documentReference={documentElement}
|
||||
onBeforeRendering={clearMetadata}/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import emojiData from 'emoji-mart/data/twitter.json'
|
||||
import { Data } from 'emoji-mart/dist-es/utils/data'
|
||||
import { ForkAwesomeIcons } from '../../../editor/editor-pane/tool-bar/emoji-picker/icon-names'
|
||||
|
||||
export const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
|
||||
.reduce((reduceObject, emojiIdentifier) => {
|
||||
const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
|
||||
const emojiCodes = emoji.unified ?? emoji.b
|
||||
if (emojiCodes) {
|
||||
reduceObject[emojiIdentifier] = emojiCodes.split('-').map(char => `&#x${char};`).join('')
|
||||
}
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
export const emojiSkinToneModifierMap = [2, 3, 4, 5, 6]
|
||||
.reduce((reduceObject, modifierValue) => {
|
||||
const lightSkinCode = 127995
|
||||
const codepoint = lightSkinCode + (modifierValue - 2)
|
||||
const shortcode = `skin-tone-${modifierValue}`
|
||||
reduceObject[shortcode] = `&#${codepoint};`
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
export const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
|
||||
.reduce((reduceObject, icon) => {
|
||||
const shortcode = `fa-${icon}`
|
||||
// noinspection CheckTagEmptyBody
|
||||
reduceObject[shortcode] = `<i class="fa fa-${icon}"></i>`
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
export const combinedEmojiData = {
|
||||
...markdownItTwitterEmojis,
|
||||
...emojiSkinToneModifierMap,
|
||||
...forkAwesomeIconMap
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import MarkdownIt from 'markdown-it/lib'
|
||||
|
||||
export interface FirstHeaderExtractorOptions {
|
||||
firstHeaderFound: (firstHeader: string|undefined) => void
|
||||
}
|
||||
|
||||
export const firstHeaderExtractor: () => MarkdownIt.PluginWithOptions<FirstHeaderExtractorOptions> = () => {
|
||||
return (md, options) => {
|
||||
if (!options?.firstHeaderFound) {
|
||||
return
|
||||
}
|
||||
md.core.ruler.after('normalize', 'extract first L1 heading', (state) => {
|
||||
const lines = state.src.split('\n')
|
||||
const linkAltTextRegex = /!?\[([^\]]*)]\([^)]*\)/
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('# ')) {
|
||||
const headerLine = line.replace('# ', '').replace(linkAltTextRegex, '$1')
|
||||
options.firstHeaderFound(headerLine)
|
||||
return true
|
||||
}
|
||||
}
|
||||
options.firstHeaderFound(undefined)
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,398 +0,0 @@
|
|||
import emojiData from 'emoji-mart/data/twitter.json'
|
||||
import { Data } from 'emoji-mart/dist-es/utils/data'
|
||||
import equal from 'fast-deep-equal'
|
||||
import yaml from 'js-yaml'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import abbreviation from 'markdown-it-abbr'
|
||||
import anchor from 'markdown-it-anchor'
|
||||
import markdownItContainer from 'markdown-it-container'
|
||||
import definitionList from 'markdown-it-deflist'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import footnote from 'markdown-it-footnote'
|
||||
import frontmatter from 'markdown-it-front-matter'
|
||||
import imsize from 'markdown-it-imsize'
|
||||
import inserted from 'markdown-it-ins'
|
||||
import marked from 'markdown-it-mark'
|
||||
import mathJax from 'markdown-it-mathjax'
|
||||
import plantuml from 'markdown-it-plantuml'
|
||||
import markdownItRegex from 'markdown-it-regex'
|
||||
import subscript from 'markdown-it-sub'
|
||||
import superscript from 'markdown-it-sup'
|
||||
import toc from 'markdown-it-toc-done-right'
|
||||
import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import markdownItTaskLists from '@hedgedoc/markdown-it-task-lists'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import ReactHtmlParser from 'react-html-parser'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface'
|
||||
import { ApplicationState } from '../../redux'
|
||||
import { InternalLink } from '../common/links/internal-link'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { ForkAwesomeIcons } from '../editor/editor-pane/tool-bar/emoji-picker/icon-names'
|
||||
import { slugify } from '../editor/table-of-contents/table-of-contents'
|
||||
import { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
|
||||
import { createRenderContainer, validAlertLevels } from './markdown-it-plugins/alert-container'
|
||||
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
|
||||
import { LineMarkers, lineNumberMarker } from './markdown-it-plugins/line-number-marker'
|
||||
import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
|
||||
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
|
||||
import { plantumlError } from './markdown-it-plugins/plantuml-error'
|
||||
import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
|
||||
import { replaceGistLink } from './regex-plugins/replace-gist-link'
|
||||
import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
|
||||
import { replaceLegacySlideshareShortCode } from './regex-plugins/replace-legacy-slideshare-short-code'
|
||||
import { replaceLegacySpeakerdeckShortCode } from './regex-plugins/replace-legacy-speakerdeck-short-code'
|
||||
import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code'
|
||||
import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-short-code'
|
||||
import { replacePdfShortCode } from './regex-plugins/replace-pdf-short-code'
|
||||
import { replaceQuoteExtraAuthor } from './regex-plugins/replace-quote-extra-author'
|
||||
import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-color'
|
||||
import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
|
||||
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
|
||||
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
|
||||
import { buildTransformer, calculateNewLineNumberMapping, LineKeys } from './renderer-utils'
|
||||
import { AbcReplacer } from './replace-components/abc/abc-replacer'
|
||||
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
|
||||
import { LinemarkerReplacer } from './replace-components/linemarker/linemarker-replacer'
|
||||
import { ComponentReplacer } from './replace-components/ComponentReplacer'
|
||||
import { CsvReplacer } from './replace-components/csv/csv-replacer'
|
||||
import { FlowchartReplacer } from './replace-components/flow/flowchart-replacer'
|
||||
import { GistReplacer } from './replace-components/gist/gist-replacer'
|
||||
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
|
||||
import { ImageReplacer } from './replace-components/image/image-replacer'
|
||||
import { KatexReplacer } from './replace-components/katex/katex-replacer'
|
||||
import { MermaidReplacer } from './replace-components/mermaid/mermaid-replacer'
|
||||
import { PdfReplacer } from './replace-components/pdf/pdf-replacer'
|
||||
import { PossibleWiderReplacer } from './replace-components/possible-wider/possible-wider-replacer'
|
||||
import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-options-replacer'
|
||||
import { SequenceDiagramReplacer } from './replace-components/sequence-diagram/sequence-diagram-replacer'
|
||||
import { TaskListReplacer } from './replace-components/task-list/task-list-replacer'
|
||||
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
|
||||
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
|
||||
import './markdown-renderer.scss'
|
||||
|
||||
export interface LineMarkerPosition {
|
||||
line: number
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface MarkdownRendererProps {
|
||||
className?: string
|
||||
content: string
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
||||
onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast: TocAst) => void
|
||||
wide?: boolean
|
||||
}
|
||||
|
||||
const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
|
||||
.reduce((reduceObject, emojiIdentifier) => {
|
||||
const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
|
||||
const emojiCodes = emoji.unified ?? emoji.b
|
||||
if (emojiCodes) {
|
||||
reduceObject[emojiIdentifier] = emojiCodes.split('-').map(char => `&#x${char};`).join('')
|
||||
}
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
const emojiSkinToneModifierMap = [2, 3, 4, 5, 6]
|
||||
.reduce((reduceObject, modifierValue) => {
|
||||
const lightSkinCode = 127995
|
||||
const codepoint = lightSkinCode + (modifierValue - 2)
|
||||
const shortcode = `skin-tone-${modifierValue}`
|
||||
reduceObject[shortcode] = `&#${codepoint};`
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
|
||||
.reduce((reduceObject, icon) => {
|
||||
const shortcode = `fa-${icon}`
|
||||
// noinspection CheckTagEmptyBody
|
||||
reduceObject[shortcode] = `<i class="fa fa-${icon}"></i>`
|
||||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
||||
className,
|
||||
content,
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onMetaDataChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
wide
|
||||
}) => {
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||
const lastTocAst = useRef<TocAst>()
|
||||
const [yamlError, setYamlError] = useState(false)
|
||||
const rawMetaRef = useRef<RawYAMLMetadata>()
|
||||
const oldMetaRef = useRef<RawYAMLMetadata>()
|
||||
const firstHeadingRef = useRef<string>()
|
||||
const oldFirstHeadingRef = useRef<string>()
|
||||
const documentElement = useRef<HTMLDivElement>(null)
|
||||
const lastLineMarkerPositions = useRef<LineMarkerPosition[]>()
|
||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||
|
||||
const calculateLineMarkerPositions = useCallback(() => {
|
||||
if (!(documentElement.current && onLineMarkerPositionChanged)) {
|
||||
return
|
||||
}
|
||||
if (currentLineMarkers.current === undefined) {
|
||||
return
|
||||
}
|
||||
const lineMarkers = currentLineMarkers.current
|
||||
const children: HTMLCollection = documentElement.current.children
|
||||
const lineMarkerPositions:LineMarkerPosition[] = []
|
||||
|
||||
Array.from(children).forEach((child, childIndex) => {
|
||||
const htmlChild = (child as HTMLElement)
|
||||
if (htmlChild.offsetTop === undefined) {
|
||||
return
|
||||
}
|
||||
const currentLineMarker = lineMarkers[childIndex]
|
||||
if (currentLineMarker === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const lastPosition = lineMarkerPositions[lineMarkerPositions.length - 1]
|
||||
if (!lastPosition || lastPosition.line !== currentLineMarker.startLine) {
|
||||
lineMarkerPositions.push({
|
||||
line: currentLineMarker.startLine,
|
||||
position: htmlChild.offsetTop
|
||||
})
|
||||
}
|
||||
|
||||
lineMarkerPositions.push({
|
||||
line: currentLineMarker.endLine,
|
||||
position: htmlChild.offsetTop + htmlChild.offsetHeight
|
||||
})
|
||||
})
|
||||
|
||||
if (!equal(lineMarkerPositions, lastLineMarkerPositions.current)) {
|
||||
lastLineMarkerPositions.current = lineMarkerPositions
|
||||
onLineMarkerPositionChanged(lineMarkerPositions)
|
||||
}
|
||||
}, [onLineMarkerPositionChanged])
|
||||
|
||||
useEffect(() => {
|
||||
calculateLineMarkerPositions()
|
||||
}, [calculateLineMarkerPositions, content])
|
||||
|
||||
useResizeObserver({
|
||||
ref: documentElement,
|
||||
onResize: () => calculateLineMarkerPositions()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef.current)) {
|
||||
if (rawMetaRef.current) {
|
||||
const newMetaData = new YAMLMetaData(rawMetaRef.current)
|
||||
onMetaDataChange(newMetaData)
|
||||
} else {
|
||||
onMetaDataChange(undefined)
|
||||
}
|
||||
oldMetaRef.current = rawMetaRef.current
|
||||
}
|
||||
if (onFirstHeadingChange && !equal(firstHeadingRef.current, oldFirstHeadingRef.current)) {
|
||||
onFirstHeadingChange(firstHeadingRef.current || undefined)
|
||||
oldFirstHeadingRef.current = firstHeadingRef.current
|
||||
}
|
||||
})
|
||||
|
||||
const plantumlServer = useSelector((state: ApplicationState) => state.config.plantumlServer)
|
||||
|
||||
const markdownIt = useMemo(() => {
|
||||
const md = new MarkdownIt('default', {
|
||||
html: true,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
if (onFirstHeadingChange) {
|
||||
md.core.ruler.after('normalize', 'extract first L1 heading', (state) => {
|
||||
const lines = state.src.split('\n')
|
||||
const linkAltTextRegex = /!?\[([^\]]*)]\([^)]*\)/
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('# ')) {
|
||||
firstHeadingRef.current = line.replace('# ', '').replace(linkAltTextRegex, '$1')
|
||||
return true
|
||||
}
|
||||
}
|
||||
firstHeadingRef.current = undefined
|
||||
return true
|
||||
})
|
||||
}
|
||||
if (onMetaDataChange) {
|
||||
md.use(frontmatter, (rawMeta: string) => {
|
||||
try {
|
||||
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
|
||||
setYamlError(false)
|
||||
rawMetaRef.current = meta
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setYamlError(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
md.use(markdownItTaskLists, { lineNumber: true })
|
||||
if (plantumlServer) {
|
||||
md.use(plantuml, {
|
||||
openMarker: '```plantuml',
|
||||
closeMarker: '```',
|
||||
server: plantumlServer
|
||||
})
|
||||
} else {
|
||||
md.use(plantumlError)
|
||||
}
|
||||
md.use(emoji, {
|
||||
defs: {
|
||||
...markdownItTwitterEmojis,
|
||||
...emojiSkinToneModifierMap,
|
||||
...forkAwesomeIconMap
|
||||
}
|
||||
})
|
||||
md.use(abbreviation)
|
||||
md.use(definitionList)
|
||||
md.use(subscript)
|
||||
md.use(superscript)
|
||||
md.use(inserted)
|
||||
md.use(marked)
|
||||
md.use(footnote)
|
||||
if (onMetaDataChange) {
|
||||
md.use(frontmatter, (rawMeta: string) => {
|
||||
try {
|
||||
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
|
||||
setYamlError(false)
|
||||
rawMetaRef.current = meta
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setYamlError(true)
|
||||
rawMetaRef.current = ({} as RawYAMLMetadata)
|
||||
}
|
||||
})
|
||||
}
|
||||
md.use(imsize)
|
||||
// noinspection CheckTagEmptyBody
|
||||
md.use(anchor, {
|
||||
permalink: true,
|
||||
permalinkBefore: true,
|
||||
permalinkClass: 'heading-anchor text-dark',
|
||||
permalinkSymbol: '<i class="fa fa-link"></i>'
|
||||
})
|
||||
md.use(mathJax({
|
||||
beforeMath: '<app-katex>',
|
||||
afterMath: '</app-katex>',
|
||||
beforeInlineMath: '<app-katex inline>',
|
||||
afterInlineMath: '</app-katex>',
|
||||
beforeDisplayMath: '<app-katex>',
|
||||
afterDisplayMath: '</app-katex>'
|
||||
}))
|
||||
md.use(markdownItRegex, replaceLegacyYoutubeShortCode)
|
||||
md.use(markdownItRegex, replaceLegacyVimeoShortCode)
|
||||
md.use(markdownItRegex, replaceLegacyGistShortCode)
|
||||
md.use(markdownItRegex, replaceLegacySlideshareShortCode)
|
||||
md.use(markdownItRegex, replaceLegacySpeakerdeckShortCode)
|
||||
md.use(markdownItRegex, replacePdfShortCode)
|
||||
md.use(markdownItRegex, replaceAsciinemaLink)
|
||||
md.use(markdownItRegex, replaceYouTubeLink)
|
||||
md.use(markdownItRegex, replaceVimeoLink)
|
||||
md.use(markdownItRegex, replaceGistLink)
|
||||
md.use(highlightedCode)
|
||||
md.use(markdownItRegex, replaceQuoteExtraAuthor)
|
||||
md.use(markdownItRegex, replaceQuoteExtraColor)
|
||||
md.use(markdownItRegex, replaceQuoteExtraTime)
|
||||
md.use(toc, {
|
||||
placeholder: '(\\[TOC\\]|\\[toc\\])',
|
||||
listType: 'ul',
|
||||
level: [1, 2, 3],
|
||||
callback: (code: string, ast: TocAst): void => {
|
||||
setTocAst(ast)
|
||||
},
|
||||
slugify: slugify
|
||||
})
|
||||
md.use(linkifyExtra)
|
||||
validAlertLevels.forEach(level => {
|
||||
md.use(markdownItContainer, level, { render: createRenderContainer(level) })
|
||||
})
|
||||
md.use(lineNumberMarker(), {
|
||||
postLineMarkers: (lineMarkers) => {
|
||||
currentLineMarkers.current = lineMarkers
|
||||
}
|
||||
})
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
md.use(MarkdownItParserDebugger)
|
||||
}
|
||||
|
||||
return md
|
||||
}, [onMetaDataChange, onFirstHeadingChange, plantumlServer])
|
||||
|
||||
useEffect(() => {
|
||||
if (onTocChange && tocAst && !equal(tocAst, lastTocAst.current)) {
|
||||
lastTocAst.current = tocAst
|
||||
onTocChange(tocAst)
|
||||
}
|
||||
}, [tocAst, onTocChange])
|
||||
|
||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||
const lastUsedLineId = useRef<number>(0)
|
||||
|
||||
const markdownReactDom: ReactElement[] = useMemo(() => {
|
||||
const allReplacers: ComponentReplacer[] = [
|
||||
new LinemarkerReplacer(),
|
||||
new PossibleWiderReplacer(),
|
||||
new GistReplacer(),
|
||||
new YoutubeReplacer(),
|
||||
new VimeoReplacer(),
|
||||
new AsciinemaReplacer(),
|
||||
new AbcReplacer(),
|
||||
new PdfReplacer(),
|
||||
new ImageReplacer(),
|
||||
new SequenceDiagramReplacer(),
|
||||
new CsvReplacer(),
|
||||
new FlowchartReplacer(),
|
||||
new MermaidReplacer(),
|
||||
new HighlightedCodeReplacer(),
|
||||
new QuoteOptionsReplacer(),
|
||||
new KatexReplacer(),
|
||||
new TaskListReplacer(onTaskCheckedChange)
|
||||
]
|
||||
if (onMetaDataChange) {
|
||||
// This is used if the front-matter callback is never called, because the user deleted everything regarding metadata from the document
|
||||
rawMetaRef.current = undefined
|
||||
}
|
||||
const trimmedContent = content.substr(0, maxLength)
|
||||
const html: string = markdownIt.render(trimmedContent)
|
||||
const contentLines = trimmedContent.split('\n')
|
||||
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
|
||||
oldMarkdownLineKeys.current = newLines
|
||||
lastUsedLineId.current = newLastUsedLineId
|
||||
return ReactHtmlParser(html, { transform: buildTransformer(newLines, allReplacers) })
|
||||
}, [content, markdownIt, onMetaDataChange, onTaskCheckedChange, maxLength])
|
||||
|
||||
return (
|
||||
<div className={'bg-light flex-fill'}>
|
||||
<div className={`${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`} >
|
||||
<ShowIf condition={yamlError}>
|
||||
<Alert variant='warning' dir='auto'>
|
||||
<Trans i18nKey='editor.invalidYaml'>
|
||||
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-dark'/>
|
||||
</Trans>
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<ShowIf condition={content.length > maxLength}>
|
||||
<Alert variant='danger' dir={'auto'}>
|
||||
<Trans i18nKey={'editor.error.limitReached.description'} values={{ maxLength }}/>
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
<div ref={documentElement} className={'markdown-body w-100 d-flex flex-column align-items-center'}>
|
||||
{markdownReactDom}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
15
src/components/markdown-renderer/types.d.ts
vendored
Normal file
15
src/components/markdown-renderer/types.d.ts
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
export interface LineKeys {
|
||||
line: string,
|
||||
id: number
|
||||
}
|
||||
|
||||
export interface LineMarkerPosition {
|
||||
line: number
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface AdditionalMarkdownRendererProps {
|
||||
className?: string,
|
||||
content: string,
|
||||
wide?: boolean,
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import equal from 'fast-deep-equal'
|
||||
import { RefObject, useCallback, useEffect, useRef } from 'react'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import { LineMarkerPosition } from '../types'
|
||||
import { LineMarkers } from '../markdown-it-plugins/line-number-marker'
|
||||
|
||||
export const calculateLineMarkerPositions = (documentElement: HTMLDivElement, currentLineMarkers: LineMarkers[], offset?: number): LineMarkerPosition[] => {
|
||||
const lineMarkers = currentLineMarkers
|
||||
const children: HTMLCollection = documentElement.children
|
||||
const lineMarkerPositions:LineMarkerPosition[] = []
|
||||
|
||||
Array.from(children).forEach((child, childIndex) => {
|
||||
const htmlChild = (child as HTMLElement)
|
||||
if (htmlChild.offsetTop === undefined) {
|
||||
return
|
||||
}
|
||||
const currentLineMarker = lineMarkers[childIndex]
|
||||
if (currentLineMarker === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const lastPosition = lineMarkerPositions[lineMarkerPositions.length - 1]
|
||||
if (!lastPosition || lastPosition.line !== currentLineMarker.startLine) {
|
||||
lineMarkerPositions.push({
|
||||
line: currentLineMarker.startLine,
|
||||
position: htmlChild.offsetTop + (offset ?? 0)
|
||||
})
|
||||
}
|
||||
|
||||
lineMarkerPositions.push({
|
||||
line: currentLineMarker.endLine,
|
||||
position: htmlChild.offsetTop + htmlChild.offsetHeight + (offset ?? 0)
|
||||
})
|
||||
})
|
||||
|
||||
return lineMarkerPositions
|
||||
}
|
||||
|
||||
export const useCalculateLineMarkerPosition = (documentElement: RefObject<HTMLDivElement>, lineMarkers?: LineMarkers[], onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void, offset?: number) : void => {
|
||||
const lastLineMarkerPositions = useRef<LineMarkerPosition[]>()
|
||||
|
||||
const calculateNewLineMarkerPositions = useCallback(() => {
|
||||
if (!documentElement.current || !onLineMarkerPositionChanged || !lineMarkers) {
|
||||
return
|
||||
}
|
||||
|
||||
const newLines = calculateLineMarkerPositions(documentElement.current, lineMarkers, offset)
|
||||
|
||||
if (!equal(newLines, lastLineMarkerPositions)) {
|
||||
lastLineMarkerPositions.current = newLines
|
||||
onLineMarkerPositionChanged(newLines)
|
||||
}
|
||||
}, [documentElement, lineMarkers, offset, onLineMarkerPositionChanged])
|
||||
|
||||
useEffect(() => {
|
||||
calculateNewLineMarkerPositions()
|
||||
}, [calculateNewLineMarkerPositions])
|
||||
|
||||
useResizeObserver({
|
||||
ref: documentElement,
|
||||
onResize: () => calculateNewLineMarkerPositions()
|
||||
})
|
||||
}
|
|
@ -1,51 +1,17 @@
|
|||
import { diffArrays } from 'diff'
|
||||
import { DomElement } from 'domhandler'
|
||||
import React, { Fragment, ReactElement } from 'react'
|
||||
import { convertNodeToElement, Transform } from 'react-html-parser'
|
||||
import {
|
||||
ComponentReplacer,
|
||||
SubNodeTransform
|
||||
} from './replace-components/ComponentReplacer'
|
||||
} from '../replace-components/ComponentReplacer'
|
||||
import { LineKeys } from '../types'
|
||||
|
||||
export interface TextDifferenceResult {
|
||||
lines: LineKeys[],
|
||||
lastUsedLineId: number
|
||||
}
|
||||
|
||||
export interface LineKeys {
|
||||
line: string,
|
||||
id: number
|
||||
}
|
||||
|
||||
export const calculateNewLineNumberMapping = (newMarkdownLines: string[], oldLineKeys: LineKeys[], lastUsedLineId: number): TextDifferenceResult => {
|
||||
const lineDifferences = diffArrays<string, LineKeys>(newMarkdownLines, oldLineKeys, {
|
||||
comparator: (left:string|LineKeys, right:string|LineKeys) => {
|
||||
const leftLine = (left as LineKeys).line ?? (left as string)
|
||||
const rightLine = (right as LineKeys).line ?? (right as string)
|
||||
return leftLine === rightLine
|
||||
}
|
||||
})
|
||||
|
||||
const newLines: LineKeys[] = []
|
||||
|
||||
lineDifferences
|
||||
.filter((change) => change.added === undefined || !change.added)
|
||||
.forEach((value) => {
|
||||
if (value.removed) {
|
||||
(value.value as string[])
|
||||
.forEach(line => {
|
||||
lastUsedLineId += 1
|
||||
newLines.push({ line: line, id: lastUsedLineId })
|
||||
})
|
||||
} else {
|
||||
(value.value as LineKeys[])
|
||||
.forEach((line) => newLines.push(line))
|
||||
}
|
||||
})
|
||||
|
||||
return { lines: newLines, lastUsedLineId: lastUsedLineId }
|
||||
}
|
||||
|
||||
export const calculateKeyFromLineMarker = (node: DomElement, lineKeys?: LineKeys[]): number|undefined => {
|
||||
if (!node.attribs || lineKeys === undefined) {
|
||||
return
|
|
@ -0,0 +1,32 @@
|
|||
import { diffArrays } from 'diff'
|
||||
import { TextDifferenceResult } from './html-react-transformer'
|
||||
import { LineKeys } from '../types'
|
||||
|
||||
export const calculateNewLineNumberMapping = (newMarkdownLines: string[], oldLineKeys: LineKeys[], lastUsedLineId: number): TextDifferenceResult => {
|
||||
const lineDifferences = diffArrays<string, LineKeys>(newMarkdownLines, oldLineKeys, {
|
||||
comparator: (left:string|LineKeys, right:string|LineKeys) => {
|
||||
const leftLine = (left as LineKeys).line ?? (left as string)
|
||||
const rightLine = (right as LineKeys).line ?? (right as string)
|
||||
return leftLine === rightLine
|
||||
}
|
||||
})
|
||||
|
||||
const newLines: LineKeys[] = []
|
||||
|
||||
lineDifferences
|
||||
.filter((change) => change.added === undefined || !change.added)
|
||||
.forEach((value) => {
|
||||
if (value.removed) {
|
||||
(value.value as string[])
|
||||
.forEach(line => {
|
||||
lastUsedLineId += 1
|
||||
newLines.push({ line: line, id: lastUsedLineId })
|
||||
})
|
||||
} else {
|
||||
(value.value as LineKeys[])
|
||||
.forEach((line) => newLines.push(line))
|
||||
}
|
||||
})
|
||||
|
||||
return { lines: newLines, lastUsedLineId: lastUsedLineId }
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import equal from 'fast-deep-equal'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { RawYAMLMetadata, YAMLMetaData } from '../../editor/yaml-metadata/yaml-metadata'
|
||||
|
||||
export const usePostMetaDataOnChange = (
|
||||
rawMetaRef: RawYAMLMetadata|undefined,
|
||||
firstHeadingRef: string|undefined,
|
||||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void,
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
): void => {
|
||||
const oldMetaRef = useRef<RawYAMLMetadata>()
|
||||
const oldFirstHeadingRef = useRef<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef)) {
|
||||
if (rawMetaRef) {
|
||||
const newMetaData = new YAMLMetaData(rawMetaRef)
|
||||
onMetaDataChange(newMetaData)
|
||||
} else {
|
||||
onMetaDataChange(undefined)
|
||||
}
|
||||
oldMetaRef.current = rawMetaRef
|
||||
}
|
||||
if (onFirstHeadingChange && !equal(firstHeadingRef, oldFirstHeadingRef.current)) {
|
||||
onFirstHeadingChange(firstHeadingRef || undefined)
|
||||
oldFirstHeadingRef.current = firstHeadingRef
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import equal from 'fast-deep-equal'
|
||||
import { RefObject, useEffect, useRef } from 'react'
|
||||
import { TocAst } from '../../../external-types/markdown-it-toc-done-right/interface'
|
||||
|
||||
export const usePostTocAstOnChange = (tocAst: RefObject<TocAst|undefined>, onTocChange?: (ast: TocAst) => void): void => {
|
||||
const lastTocAst = useRef<TocAst>()
|
||||
useEffect(() => {
|
||||
if (onTocChange && tocAst.current && !equal(tocAst, lastTocAst.current)) {
|
||||
lastTocAst.current = tocAst.current
|
||||
onTocChange(tocAst.current)
|
||||
}
|
||||
}, [onTocChange, tocAst])
|
||||
}
|
Loading…
Reference in a new issue