mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-03-22 03:33:00 +00:00
Added synced scrolling (#386)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
164b5436ae
commit
73007ef597
10 changed files with 413 additions and 38 deletions
|
@ -1,12 +1,14 @@
|
|||
import React, { useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
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 { MarkdownRenderer } from '../../markdown-renderer/markdown-renderer'
|
||||
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||
import { LineMarkerPosition, MarkdownRenderer } from '../../markdown-renderer/markdown-renderer'
|
||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
||||
import { findLineMarks } from '../scroll/utils'
|
||||
import { TableOfContents } from '../table-of-contents/table-of-contents'
|
||||
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
|
||||
|
||||
interface DocumentRenderPaneProps {
|
||||
content: string
|
||||
|
@ -15,15 +17,69 @@ interface DocumentRenderPaneProps {
|
|||
wide?: boolean
|
||||
}
|
||||
|
||||
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({ content, onMetadataChange, onFirstHeadingChange, wide }) => {
|
||||
export const DocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps> = ({ content, onMetadataChange, onFirstHeadingChange, wide, scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const renderer = useRef<HTMLDivElement>(null)
|
||||
const { width } = useResizeObserver({ ref: renderer })
|
||||
const lastScrollPosition = useRef<number>()
|
||||
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
|
||||
|
||||
const realWidth = width || 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!renderer.current || !lineMarks || !scrollState) {
|
||||
return
|
||||
}
|
||||
const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView)
|
||||
const positionBefore = lastMarkBefore ? lastMarkBefore.position : 0
|
||||
const positionAfter = firstMarkAfter ? firstMarkAfter.position : renderer.current.offsetHeight
|
||||
const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1
|
||||
const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : content.split('\n').length
|
||||
const lineCount = firstMarkAfterLine - lastMarkBeforeLine
|
||||
const blockHeight = positionAfter - positionBefore
|
||||
const lineHeight = blockHeight / lineCount
|
||||
const position = positionBefore + (scrollState.firstLineInView - lastMarkBeforeLine) * lineHeight + scrollState.scrolledPercentage / 100 * lineHeight
|
||||
const correctedPosition = Math.floor(position)
|
||||
if (correctedPosition !== lastScrollPosition.current) {
|
||||
lastScrollPosition.current = correctedPosition
|
||||
renderer.current.scrollTo({
|
||||
top: correctedPosition
|
||||
})
|
||||
}
|
||||
}, [content, lineMarks, scrollState])
|
||||
|
||||
const userScroll = useCallback(() => {
|
||||
if (!renderer.current || !lineMarks || !onScroll) {
|
||||
return
|
||||
}
|
||||
const resyncedScroll = Math.ceil(renderer.current.scrollTop) === lastScrollPosition.current
|
||||
if (resyncedScroll) {
|
||||
return
|
||||
}
|
||||
|
||||
const scrollTop = renderer.current.scrollTop
|
||||
|
||||
const beforeLineMark = lineMarks
|
||||
.filter(lineMark => lineMark.position <= scrollTop)
|
||||
.reduce((prevLineMark, currentLineMark) =>
|
||||
prevLineMark.line >= currentLineMark.line ? prevLineMark : currentLineMark)
|
||||
|
||||
const afterLineMark = lineMarks
|
||||
.filter(lineMark => lineMark.position > scrollTop)
|
||||
.reduce((prevLineMark, currentLineMark) =>
|
||||
prevLineMark.line < currentLineMark.line ? prevLineMark : currentLineMark)
|
||||
|
||||
const blockHeight = afterLineMark.position - beforeLineMark.position
|
||||
const distanceToBefore = scrollTop - beforeLineMark.position
|
||||
const percentageRaw = (distanceToBefore / blockHeight)
|
||||
const percentage = Math.floor(percentageRaw * 100)
|
||||
const newScrollState: ScrollState = { firstLineInView: beforeLineMark.line, scrolledPercentage: percentage }
|
||||
onScroll(newScrollState)
|
||||
}, [lineMarks, onScroll])
|
||||
|
||||
return (
|
||||
<div className={'bg-light flex-fill pb-5 flex-row d-flex w-100 h-100 overflow-y-scroll'} ref={renderer}>
|
||||
<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'}
|
||||
|
@ -32,11 +88,12 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = ({ content,
|
|||
onTocChange={(tocAst) => setTocAst(tocAst)}
|
||||
onMetaDataChange={onMetadataChange}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onLineMarkerPositionChanged={setLineMarks}
|
||||
/>
|
||||
|
||||
<div className={'col-md'}>
|
||||
<ShowIf condition={realWidth >= 1280 && !!tocAst}>
|
||||
<TableOfContents ast={tocAst as TocAst} sticky={true}/>
|
||||
<TableOfContents ast={tocAst as TocAst} className={'position-fixed'}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={realWidth < 1280 && !!tocAst}>
|
||||
<div className={'markdown-toc-sidebar-button'}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Editor, EditorChange, EditorConfiguration } from 'codemirror'
|
||||
import { Editor, EditorChange, EditorConfiguration, ScrollInfo } from 'codemirror'
|
||||
import 'codemirror/addon/comment/comment'
|
||||
import 'codemirror/addon/dialog/dialog'
|
||||
import 'codemirror/addon/display/autorefresh'
|
||||
|
@ -20,10 +20,11 @@ import 'codemirror/keymap/sublime'
|
|||
import 'codemirror/keymap/emacs'
|
||||
import 'codemirror/keymap/vim'
|
||||
import 'codemirror/mode/gfm/gfm'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './editor-pane.scss'
|
||||
import { ScrollProps, ScrollState } from '../scroll/scroll-props'
|
||||
import { generateEmojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji'
|
||||
import { defaultKeyMap } from './key-map'
|
||||
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
||||
|
@ -48,7 +49,7 @@ const onChange = (editor: Editor) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const EditorPane: React.FC<EditorPaneProps> = ({ onContentChange, content }) => {
|
||||
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const { t } = useTranslation()
|
||||
const [editor, setEditor] = useState<Editor>()
|
||||
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
|
||||
|
@ -59,6 +60,41 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ onContentChange, content
|
|||
indentWithTabs: false
|
||||
})
|
||||
|
||||
const lastScrollPosition = useRef<number>()
|
||||
|
||||
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => {
|
||||
if (!editor || !onScroll) {
|
||||
return
|
||||
}
|
||||
const scrollEventValue = data.top as number
|
||||
const line = editor.lineAtHeight(scrollEventValue, 'local')
|
||||
const startYOfLine = editor.heightAtLine(line, 'local')
|
||||
const lineInfo = editor.lineInfo(line)
|
||||
if (lineInfo === null) {
|
||||
return
|
||||
}
|
||||
const heightOfLine = (lineInfo.handle as { height: number }).height
|
||||
const percentageRaw = (Math.max(scrollEventValue - startYOfLine, 0)) / heightOfLine
|
||||
const percentage = Math.floor(percentageRaw * 100)
|
||||
|
||||
const newScrollState: ScrollState = { firstLineInView: line + 1, scrolledPercentage: percentage }
|
||||
onScroll(newScrollState)
|
||||
}, [onScroll])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !scrollState) {
|
||||
return
|
||||
}
|
||||
const startYOfLine = editor.heightAtLine(scrollState.firstLineInView - 1, 'div')
|
||||
const heightOfLine = (editor.lineInfo(scrollState.firstLineInView - 1).handle as { height: number }).height
|
||||
const newPositionRaw = startYOfLine + (heightOfLine * scrollState.scrolledPercentage / 100)
|
||||
const newPosition = Math.floor(newPositionRaw)
|
||||
if (newPosition !== lastScrollPosition.current) {
|
||||
lastScrollPosition.current = newPosition
|
||||
editor.scrollTo(0, newPosition)
|
||||
}
|
||||
}, [editor, scrollState])
|
||||
|
||||
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
|
||||
onContentChange(value)
|
||||
}, [onContentChange])
|
||||
|
@ -100,7 +136,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ onContentChange, content
|
|||
}), [t, editorPreferences])
|
||||
|
||||
return (
|
||||
<div className={'d-flex flex-column h-100'}>
|
||||
<div className={'d-flex flex-column h-100'} onMouseEnter={onMakeScrollSource}>
|
||||
<ToolBar
|
||||
editor={editor}
|
||||
onPreferencesChange={config => setEditorPreferences(config)}
|
||||
|
@ -114,6 +150,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ onContentChange, content
|
|||
onCursorActivity={onCursorActivity}
|
||||
editorDidMount={onEditorDidMount}
|
||||
onBeforeChange={onBeforeChange}
|
||||
onScroll={onEditorScroll}
|
||||
/>
|
||||
<StatusBar {...statusBarInfo} />
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Splitter } from './splitter/splitter'
|
|||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||
import { DocumentBar } from './document-bar/document-bar'
|
||||
import { editorTestContent } from './editorTestContent'
|
||||
import { DualScrollState, ScrollState } from './scroll/scroll-props'
|
||||
import { YAMLMetaData } from './yaml-metadata/yaml-metadata'
|
||||
import { AppBar } from './app-bar/app-bar'
|
||||
import { EditorMode } from './app-bar/editor-view-mode'
|
||||
|
@ -19,6 +20,11 @@ export interface EditorPathParams {
|
|||
id: string
|
||||
}
|
||||
|
||||
export enum ScrollSource {
|
||||
EDITOR,
|
||||
RENDERER
|
||||
}
|
||||
|
||||
export const Editor: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const untitledNote = t('editor.untitledNote')
|
||||
|
@ -29,6 +35,12 @@ export const Editor: React.FC = () => {
|
|||
const [documentTitle, setDocumentTitle] = useState(untitledNote)
|
||||
const noteMetadata = useRef<YAMLMetaData>()
|
||||
const firstHeading = useRef<string>()
|
||||
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
|
||||
|
||||
const [scrollState, setScrollState] = useState<DualScrollState>(() => ({
|
||||
editorScrollState: { firstLineInView: 1, scrolledPercentage: 0 },
|
||||
rendererScrollState: { firstLineInView: 1, scrolledPercentage: 0 }
|
||||
}))
|
||||
|
||||
const updateDocumentTitle = useCallback(() => {
|
||||
if (noteMetadata.current?.title && noteMetadata.current?.title !== '') {
|
||||
|
@ -60,6 +72,18 @@ export const Editor: React.FC = () => {
|
|||
}
|
||||
}, [editorMode, firstDraw, isWide])
|
||||
|
||||
const onEditorScroll = useCallback((newScrollState: ScrollState) => {
|
||||
if (scrollSource.current === ScrollSource.EDITOR) {
|
||||
setScrollState((old) => ({ rendererScrollState: newScrollState, editorScrollState: old.editorScrollState }))
|
||||
}
|
||||
}, [])
|
||||
|
||||
const onMarkdownRendererScroll = useCallback((newScrollState: ScrollState) => {
|
||||
if (scrollSource.current === ScrollSource.RENDERER) {
|
||||
setScrollState((old) => ({ editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState }))
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MotdBanner/>
|
||||
|
@ -72,16 +96,22 @@ export const Editor: React.FC = () => {
|
|||
left={
|
||||
<EditorPane
|
||||
onContentChange={content => setMarkdownContent(content)}
|
||||
content={markdownContent}/>
|
||||
content={markdownContent}
|
||||
scrollState={scrollState.editorScrollState}
|
||||
onScroll={onEditorScroll}
|
||||
onMakeScrollSource={() => { scrollSource.current = ScrollSource.EDITOR }}
|
||||
/>
|
||||
}
|
||||
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
|
||||
right={
|
||||
<DocumentRenderPane
|
||||
content={markdownContent}
|
||||
wide={editorMode === EditorMode.PREVIEW}
|
||||
scrollState={scrollState.rendererScrollState}
|
||||
onScroll={onMarkdownRendererScroll}
|
||||
onMetadataChange={onMetadataChange}
|
||||
onFirstHeadingChange={onFirstHeadingChange}/>
|
||||
}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onMakeScrollSource={() => { scrollSource.current = ScrollSource.RENDERER }}/>}
|
||||
containerClassName={'overflow-hidden'}/>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import MarkdownIt from 'markdown-it/lib'
|
||||
import Token from 'markdown-it/lib/token'
|
||||
|
||||
export const lineNumberMarker: () => MarkdownIt.PluginSimple = () => {
|
||||
const endMarkers: number[] = []
|
||||
|
||||
return (md: MarkdownIt) => {
|
||||
// add codimd_linemarker token before each opening or self-closing level-0 tag
|
||||
md.core.ruler.push('line_number_marker', (state) => {
|
||||
for (let i = 0; i < state.tokens.length; i++) {
|
||||
const token = state.tokens[i]
|
||||
if (token.level !== 0) {
|
||||
continue
|
||||
}
|
||||
if (token.nesting !== -1) {
|
||||
if (!token.map) {
|
||||
continue
|
||||
}
|
||||
const lineNumber = token.map[0] + 1
|
||||
if (token.nesting === 1) {
|
||||
endMarkers.push(token.map[1] + 1)
|
||||
}
|
||||
const tokenBefore = state.tokens[i - 1]
|
||||
if (tokenBefore === undefined || tokenBefore.type !== 'codimd_linemarker' || tokenBefore.tag !== 'codimd-linemarker' ||
|
||||
tokenBefore.attrGet('data-linenumber') !== `${lineNumber}`) {
|
||||
const startToken = new Token('codimd_linemarker', 'codimd-linemarker', 0)
|
||||
startToken.attrPush(['data-linenumber', `${lineNumber}`])
|
||||
state.tokens.splice(i, 0, startToken)
|
||||
i += 1
|
||||
}
|
||||
} else {
|
||||
const lineNumber = endMarkers.pop()
|
||||
if (lineNumber) {
|
||||
const startToken = new Token('codimd_linemarker', 'codimd-linemarker', 0)
|
||||
startToken.attrPush(['data-linenumber', `${lineNumber}`])
|
||||
state.tokens.splice(i + 1, 0, startToken)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// render codimd_linemarker token to <codimd-linemarker></codimd-linemarker>
|
||||
md.renderer.rules.codimd_linemarker = (tokens: Token[], index: number): string => {
|
||||
const lineNumber = tokens[index].attrGet('data-linenumber')
|
||||
if (!lineNumber) {
|
||||
// don't render broken linemarkers without a linenumber
|
||||
return ''
|
||||
}
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<codimd-linemarker data-linenumber='${lineNumber}'></codimd-linemarker>`
|
||||
}
|
||||
}
|
||||
}
|
15
src/components/editor/scroll/scroll-props.ts
Normal file
15
src/components/editor/scroll/scroll-props.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export interface ScrollProps {
|
||||
scrollState?: ScrollState
|
||||
onScroll?: (scrollState: ScrollState) => void
|
||||
onMakeScrollSource?: () => void
|
||||
}
|
||||
|
||||
export interface ScrollState {
|
||||
firstLineInView: number
|
||||
scrolledPercentage: number
|
||||
}
|
||||
|
||||
export interface DualScrollState {
|
||||
editorScrollState: ScrollState
|
||||
rendererScrollState: ScrollState
|
||||
}
|
25
src/components/editor/scroll/utils.ts
Normal file
25
src/components/editor/scroll/utils.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { LineMarkerPosition } from '../../markdown-renderer/markdown-renderer'
|
||||
export const findLineMarks = (lineMarks: LineMarkerPosition[], lineNumber: number): { lastMarkBefore: LineMarkerPosition | undefined, firstMarkAfter: LineMarkerPosition | undefined } => {
|
||||
let lastMarkBefore
|
||||
let firstMarkAfter
|
||||
for (let i = 0; i < lineMarks.length; i++) {
|
||||
const currentMark = lineMarks[i]
|
||||
if (!currentMark) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentMark.line <= lineNumber) {
|
||||
lastMarkBefore = currentMark
|
||||
}
|
||||
if (currentMark.line > lineNumber) {
|
||||
firstMarkAfter = currentMark
|
||||
}
|
||||
if (!!firstMarkAfter && !!lastMarkBefore) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
lastMarkBefore,
|
||||
firstMarkAfter
|
||||
}
|
||||
}
|
|
@ -54,7 +54,7 @@ export const Splitter: React.FC<SplitterProps> = ({ containerClassName, left, ri
|
|||
</div>
|
||||
</ShowIf>
|
||||
<ShowIf condition={showRight}>
|
||||
<div className='splitter right'>
|
||||
<div className={'splitter right'}>
|
||||
{right}
|
||||
</div>
|
||||
</ShowIf>
|
||||
|
|
|
@ -6,7 +6,7 @@ import './table-of-contents.scss'
|
|||
export interface TableOfContentsProps {
|
||||
ast: TocAst
|
||||
maxDepth?: number
|
||||
sticky?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const slugify = (content:string) => {
|
||||
|
@ -51,11 +51,11 @@ const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts:
|
|||
}
|
||||
}
|
||||
|
||||
export const TableOfContents: React.FC<TableOfContentsProps> = ({ ast, maxDepth = 3, sticky }) => {
|
||||
export const TableOfContents: React.FC<TableOfContentsProps> = ({ ast, maxDepth = 3, className }) => {
|
||||
const tocTree = useMemo(() => convertLevel(ast, maxDepth, new Map<string, number>(), false), [ast, maxDepth])
|
||||
|
||||
return (
|
||||
<div className={`markdown-toc ${sticky ? 'sticky' : ''}`}>
|
||||
<div className={`markdown-toc ${className ?? ''}`}>
|
||||
{tocTree}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -21,11 +21,12 @@ import subscript from 'markdown-it-sub'
|
|||
import superscript from 'markdown-it-sup'
|
||||
import taskList from 'markdown-it-task-lists'
|
||||
import toc from 'markdown-it-toc-done-right'
|
||||
import React, { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
|
||||
import { Trans } from 'react-i18next'
|
||||
import MathJaxReact from 'react-mathjax'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface'
|
||||
import { ApplicationState } from '../../redux'
|
||||
|
@ -65,6 +66,12 @@ import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-o
|
|||
import { TocReplacer } from './replace-components/toc/toc-replacer'
|
||||
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
|
||||
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
|
||||
import { lineNumberMarker } from '../editor/markdown-renderer/markdown-it-plugins/line-number-marker'
|
||||
|
||||
export interface LineMarkerPosition {
|
||||
line: number
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface MarkdownRendererProps {
|
||||
content: string
|
||||
|
@ -73,6 +80,7 @@ export interface MarkdownRendererProps {
|
|||
onTocChange?: (ast: TocAst) => void
|
||||
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
}
|
||||
|
||||
const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
|
||||
|
@ -102,14 +110,41 @@ const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
|
|||
return reduceObject
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => {
|
||||
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide, onLineMarkerPositionChanged }) => {
|
||||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const [lastTocAst, setLastTocAst] = useState<TocAst>()
|
||||
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 calculateLineMarkerPositions = useCallback(() => {
|
||||
if (documentElement.current && onLineMarkerPositionChanged) {
|
||||
const lineMarkers: NodeListOf<HTMLDivElement> = documentElement.current.querySelectorAll('codimd-linemarker')
|
||||
const lineMarkerPositions: LineMarkerPosition[] = Array.from(lineMarkers).map((marker) => {
|
||||
return {
|
||||
line: Number(marker.getAttribute('data-linenumber')),
|
||||
position: marker.offsetTop
|
||||
} as LineMarkerPosition
|
||||
})
|
||||
if (!equal(lineMarkerPositions, lastLineMarkerPositions.current)) {
|
||||
lastLineMarkerPositions.current = lineMarkerPositions
|
||||
onLineMarkerPositionChanged(lineMarkerPositions)
|
||||
}
|
||||
}
|
||||
}, [onLineMarkerPositionChanged])
|
||||
|
||||
useEffect(() => {
|
||||
calculateLineMarkerPositions()
|
||||
}, [calculateLineMarkerPositions])
|
||||
|
||||
useResizeObserver({
|
||||
ref: documentElement,
|
||||
onResize: () => calculateLineMarkerPositions()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef.current)) {
|
||||
|
@ -239,6 +274,7 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
|
|||
slugify: slugify
|
||||
})
|
||||
md.use(linkifyExtra)
|
||||
md.use(lineNumberMarker())
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
md.use(MarkdownItParserDebugger)
|
||||
}
|
||||
|
@ -251,11 +287,11 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
|
|||
}, [onMetaDataChange, onFirstHeadingChange, plantumlServer])
|
||||
|
||||
useEffect(() => {
|
||||
if (onTocChange && tocAst && !equal(tocAst, lastTocAst)) {
|
||||
if (onTocChange && tocAst && !equal(tocAst, lastTocAst.current)) {
|
||||
lastTocAst.current = tocAst
|
||||
onTocChange(tocAst)
|
||||
setLastTocAst(tocAst)
|
||||
}
|
||||
}, [tocAst, onTocChange, lastTocAst])
|
||||
}, [tocAst, onTocChange])
|
||||
|
||||
const tryToReplaceNode = (node: DomElement, index: number, allReplacers: ComponentReplacer[], nodeConverter: SubNodeConverter) => {
|
||||
return allReplacers
|
||||
|
@ -291,17 +327,19 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
|
|||
}, [content, markdownIt, onMetaDataChange])
|
||||
|
||||
return (
|
||||
<div className={`markdown-body ${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>
|
||||
<MathJaxReact.Provider>
|
||||
{result}
|
||||
</MathJaxReact.Provider>
|
||||
<div className={'bg-light flex-fill pb-5'}>
|
||||
<div className={`markdown-body ${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`} ref={documentElement}>
|
||||
<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>
|
||||
<MathJaxReact.Provider>
|
||||
{result}
|
||||
</MathJaxReact.Provider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
126
yarn.lock
126
yarn.lock
|
@ -2675,6 +2675,11 @@ asynckit@^0.4.0:
|
|||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
|
||||
|
||||
at-least-node@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
|
||||
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
|
||||
|
||||
atob@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
|
@ -2932,6 +2937,11 @@ bindings@^1.5.0:
|
|||
dependencies:
|
||||
file-uri-to-path "1.0.0"
|
||||
|
||||
blob-util@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
|
||||
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
|
||||
|
||||
block-stream@*:
|
||||
version "0.0.9"
|
||||
resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
|
||||
|
@ -3367,7 +3377,7 @@ chalk@^3.0.0:
|
|||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.1.0:
|
||||
chalk@^4.0.0, chalk@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
|
||||
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
|
||||
|
@ -3502,6 +3512,16 @@ cli-table3@~0.5.1:
|
|||
optionalDependencies:
|
||||
colors "^1.1.2"
|
||||
|
||||
cli-table3@~0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
|
||||
integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
|
||||
dependencies:
|
||||
object-assign "^4.1.0"
|
||||
string-width "^4.2.0"
|
||||
optionalDependencies:
|
||||
colors "^1.1.2"
|
||||
|
||||
cli-truncate@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
|
||||
|
@ -3919,6 +3939,15 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
|
|||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.0:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crypto-browserify@^3.11.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
|
||||
|
@ -4196,7 +4225,7 @@ cyclist@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
||||
|
||||
cypress@*, cypress@4.12.1:
|
||||
cypress@*, cypress@4.12.1, cypress@*:
|
||||
version "4.12.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.12.1.tgz#0ead1b9f4c0917d69d8b57f996b6e01fe693b6ec"
|
||||
integrity sha512-9SGIPEmqU8vuRA6xst2CMTYd9sCFCxKSzrHt0wr+w2iAQMCIIsXsQ5Gplns1sT6LDbZcmLv6uehabAOl3fhc9Q==
|
||||
|
@ -5209,6 +5238,21 @@ execa@^1.0.0:
|
|||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
execa@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.3.tgz#0a34dabbad6d66100bd6f2c576c8669403f317f2"
|
||||
integrity sha512-WFDXGHckXPWZX19t1kCsXzOpqX9LWYNqn4C+HqZlk/V0imTkzJZqf87ZBhvpHaftERYknpk0fjSylnXVlVgI0A==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.0"
|
||||
get-stream "^5.0.0"
|
||||
human-signals "^1.1.1"
|
||||
is-stream "^2.0.0"
|
||||
merge-stream "^2.0.0"
|
||||
npm-run-path "^4.0.0"
|
||||
onetime "^5.1.0"
|
||||
signal-exit "^3.0.2"
|
||||
strip-final-newline "^2.0.0"
|
||||
|
||||
executable@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
|
||||
|
@ -5700,6 +5744,16 @@ fs-extra@^8.1.0:
|
|||
jsonfile "^4.0.0"
|
||||
universalify "^0.1.0"
|
||||
|
||||
fs-extra@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc"
|
||||
integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==
|
||||
dependencies:
|
||||
at-least-node "^1.0.0"
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^1.0.0"
|
||||
|
||||
fs-minipass@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
|
||||
|
@ -5808,6 +5862,13 @@ get-stream@^4.0.0:
|
|||
dependencies:
|
||||
pump "^3.0.0"
|
||||
|
||||
get-stream@^5.0.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
|
||||
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
|
||||
get-value@^2.0.3, get-value@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
|
||||
|
@ -6285,6 +6346,11 @@ https-browserify@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
|
||||
|
||||
human-signals@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
||||
|
||||
hyphenate-style-name@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
|
||||
|
@ -6867,6 +6933,11 @@ is-stream@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
|
||||
|
||||
is-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
|
||||
integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
|
||||
|
||||
is-string@^1.0.4, is-string@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
|
||||
|
@ -7592,6 +7663,15 @@ jsonfile@^4.0.0:
|
|||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179"
|
||||
integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==
|
||||
dependencies:
|
||||
universalify "^1.0.0"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonify@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||
|
@ -7936,6 +8016,13 @@ log-symbols@^3.0.0:
|
|||
dependencies:
|
||||
chalk "^2.4.2"
|
||||
|
||||
log-symbols@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
|
||||
integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
|
||||
log-update@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708"
|
||||
|
@ -8687,6 +8774,13 @@ npm-run-path@^2.0.0:
|
|||
dependencies:
|
||||
path-key "^2.0.0"
|
||||
|
||||
npm-run-path@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
|
||||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||
|
@ -9161,7 +9255,7 @@ path-key@^2.0.0, path-key@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
||||
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
|
||||
|
||||
path-key@^3.1.0:
|
||||
path-key@^3.0.0, path-key@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
||||
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
||||
|
@ -11052,6 +11146,13 @@ rimraf@2.6.3:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
|
||||
|
@ -11812,7 +11913,7 @@ string-width@^3.0.0, string-width@^3.1.0:
|
|||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^5.1.0"
|
||||
|
||||
string-width@^4.1.0:
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
|
||||
integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
|
||||
|
@ -11925,6 +12026,11 @@ strip-eof@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
|
||||
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
|
||||
|
||||
strip-final-newline@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
|
||||
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
|
||||
|
||||
strip-indent@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
|
||||
|
@ -12173,6 +12279,13 @@ tmp@~0.1.0:
|
|||
dependencies:
|
||||
rimraf "^2.6.3"
|
||||
|
||||
tmp@~0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
|
||||
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
|
||||
dependencies:
|
||||
rimraf "^3.0.0"
|
||||
|
||||
tmpl@1.0.x:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
|
||||
|
@ -12466,6 +12579,11 @@ universalify@^0.1.0:
|
|||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||
|
||||
universalify@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d"
|
||||
integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
|
Loading…
Reference in a new issue