mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-12 11:43:45 +00:00
Improve render performance (#511)
Massive improvement of render performance by: - replacing the codimd-line-marker with an in-memory map - an observation of the changed markdown code to identify changed lines - a unique react-key calculation
This commit is contained in:
parent
df7c4cb19e
commit
3a0e35a9f3
27 changed files with 360 additions and 161 deletions
|
@ -270,18 +270,18 @@ describe('Toolbar', () => {
|
|||
|
||||
describe('emoji', () => {
|
||||
it('picker is show when clicked', () => {
|
||||
cy.get('.emoji-mart.emoji-mart-light')
|
||||
cy.get('.emoji-mart')
|
||||
.should('not.exist')
|
||||
cy.get('.fa-smile-o')
|
||||
.click()
|
||||
cy.get('.emoji-mart.emoji-mart-light')
|
||||
cy.get('.emoji-mart')
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('picker is show when clicked', () => {
|
||||
cy.get('.fa-smile-o')
|
||||
.click()
|
||||
cy.get('.emoji-mart.emoji-mart-light')
|
||||
cy.get('.emoji-mart')
|
||||
.should('exist')
|
||||
cy.get('.emoji-mart-emoji-native')
|
||||
.first()
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"@testing-library/react": "10.4.9",
|
||||
"@testing-library/user-event": "12.1.3",
|
||||
"@types/codemirror": "0.0.97",
|
||||
"@types/diff": "4.0.2",
|
||||
"@types/emoji-mart": "3.0.2",
|
||||
"@types/highlight.js": "9.12.4",
|
||||
"@types/jest": "26.0.10",
|
||||
|
@ -30,6 +31,7 @@
|
|||
"@typescript-eslint/parser": "3.10.1",
|
||||
"bootstrap": "4.5.2",
|
||||
"codemirror": "5.57.0",
|
||||
"diff": "4.0.2",
|
||||
"emoji-mart": "3.0.0",
|
||||
"eslint-config-react-app": "5.2.1",
|
||||
"eslint-config-standard": "14.1.1",
|
||||
|
|
|
@ -36,12 +36,30 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps>
|
|||
|
||||
const realWidth = width || 0
|
||||
|
||||
const scrollTo = useCallback((targetPosition:number):void => {
|
||||
if (!renderer.current || targetPosition === lastScrollPosition.current) {
|
||||
return
|
||||
}
|
||||
lastScrollPosition.current = targetPosition
|
||||
renderer.current.scrollTo({
|
||||
top: targetPosition
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!renderer.current || !lineMarks || !scrollState) {
|
||||
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
|
||||
return
|
||||
}
|
||||
if (scrollState.firstLineInView < lineMarks[0].line) {
|
||||
scrollTo(0)
|
||||
return
|
||||
}
|
||||
if (scrollState.firstLineInView > lineMarks[lineMarks.length - 1].line) {
|
||||
scrollTo(renderer.current.offsetHeight)
|
||||
return
|
||||
}
|
||||
const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView)
|
||||
const positionBefore = lastMarkBefore ? lastMarkBefore.position : 0
|
||||
const positionBefore = lastMarkBefore ? lastMarkBefore.position : lineMarks[0].position
|
||||
const positionAfter = firstMarkAfter ? firstMarkAfter.position : renderer.current.offsetHeight
|
||||
const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1
|
||||
const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : content.split('\n').length
|
||||
|
@ -50,16 +68,11 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps & ScrollProps>
|
|||
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])
|
||||
scrollTo(correctedPosition)
|
||||
}, [content, lineMarks, scrollState, scrollTo])
|
||||
|
||||
const userScroll = useCallback(() => {
|
||||
if (!renderer.current || !lineMarks || !onScroll) {
|
||||
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ function arrayCommonCheck<T> (array1: T[], array2: T[]): boolean {
|
|||
|
||||
function filterByKeywordSearch (entries: LocatedHistoryEntry[], keywords: string): LocatedHistoryEntry[] {
|
||||
const searchTerm = keywords.toLowerCase()
|
||||
return entries.filter(entry => entry.title.toLowerCase().indexOf(searchTerm) !== -1)
|
||||
return entries.filter(entry => entry.title.toLowerCase().includes(searchTerm))
|
||||
}
|
||||
|
||||
function sortEntries (entries: LocatedHistoryEntry[], viewState: HistoryToolbarState): LocatedHistoryEntry[] {
|
||||
|
|
|
@ -1,55 +1,76 @@
|
|||
import MarkdownIt from 'markdown-it/lib'
|
||||
import Token from 'markdown-it/lib/token'
|
||||
|
||||
export const lineNumberMarker: () => MarkdownIt.PluginSimple = () => {
|
||||
const endMarkers: number[] = []
|
||||
export interface LineMarkers {
|
||||
startLine: number
|
||||
endLine: number
|
||||
}
|
||||
|
||||
return (md: MarkdownIt) => {
|
||||
export interface LineNumberMarkerOptions {
|
||||
postLineMarkers: (lineMarkers: LineMarkers[]) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
|
||||
* It also provides a list of line numbers for the top level dom elements.
|
||||
*/
|
||||
export const lineNumberMarker: () => MarkdownIt.PluginWithOptions<LineNumberMarkerOptions> = () => {
|
||||
return (md: MarkdownIt, options) => {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
const lineMarkers: LineMarkers[] = []
|
||||
tagTokens(state.tokens, lineMarkers)
|
||||
if (options?.postLineMarkers) {
|
||||
options.postLineMarkers(lineMarkers)
|
||||
}
|
||||
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) {
|
||||
const startLineNumber = tokens[index].attrGet('data-start-line')
|
||||
const endLineNumber = tokens[index].attrGet('data-end-line')
|
||||
|
||||
if (!startLineNumber || !endLineNumber) {
|
||||
// don't render broken linemarkers without a linenumber
|
||||
return ''
|
||||
}
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<codimd-linemarker data-linenumber='${lineNumber}'></codimd-linemarker>`
|
||||
return `<codimd-linemarker data-start-line='${startLineNumber}' data-end-line='${endLineNumber}'></codimd-linemarker>`
|
||||
}
|
||||
|
||||
const insertNewLineMarker = (startLineNumber: number, endLineNumber: number, tokenPosition: number, level: number, tokens: Token[]) => {
|
||||
const startToken = new Token('codimd_linemarker', 'codimd-linemarker', 0)
|
||||
startToken.level = level
|
||||
startToken.attrPush(['data-start-line', `${startLineNumber}`])
|
||||
startToken.attrPush(['data-end-line', `${endLineNumber}`])
|
||||
tokens.splice(tokenPosition, 0, startToken)
|
||||
}
|
||||
|
||||
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[]) => {
|
||||
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
|
||||
const token = tokens[tokenPosition]
|
||||
if (token.hidden) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!token.map) {
|
||||
continue
|
||||
}
|
||||
|
||||
const startLineNumber = token.map[0] + 1
|
||||
const endLineNumber = token.map[1] + 1
|
||||
|
||||
if (token.level === 0) {
|
||||
lineMarkers.push({ startLine: startLineNumber, endLine: endLineNumber })
|
||||
}
|
||||
|
||||
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
|
||||
tokenPosition += 1
|
||||
|
||||
if (token.children) {
|
||||
tagTokens(token.children, lineMarkers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,14 +20,6 @@
|
|||
background-color: unset;
|
||||
}
|
||||
|
||||
&.wider {
|
||||
max-width: 1500px;
|
||||
|
||||
& > .wider-possible {
|
||||
max-width: 1500px;
|
||||
}
|
||||
}
|
||||
|
||||
a.heading-anchor {
|
||||
margin-left: -1.25em;
|
||||
font-size: 0.75em;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import equal from 'fast-deep-equal'
|
||||
import { DomElement } from 'domhandler'
|
||||
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'
|
||||
|
@ -23,7 +22,7 @@ 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, { convertNodeToElement, Transform } from 'react-html-parser'
|
||||
import ReactHtmlParser from 'react-html-parser'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
|
@ -36,7 +35,7 @@ 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 { lineNumberMarker } from './markdown-it-plugins/line-number-marker'
|
||||
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'
|
||||
|
@ -53,8 +52,10 @@ import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-colo
|
|||
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 { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
|
||||
import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer'
|
||||
import { CodimdLinemarkerReplacer } from './replace-components/codimd-linemarker/codimd-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'
|
||||
|
@ -131,27 +132,52 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
const oldFirstHeadingRef = useRef<string>()
|
||||
const documentElement = useRef<HTMLDivElement>(null)
|
||||
const lastLineMarkerPositions = useRef<LineMarkerPosition[]>()
|
||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||
|
||||
const calculateLineMarkerPositions = useCallback(() => {
|
||||
if (documentElement.current && onLineMarkerPositionChanged) {
|
||||
// noinspection CssInvalidHtmlTagReference
|
||||
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)
|
||||
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])
|
||||
}, [calculateLineMarkerPositions, content])
|
||||
|
||||
useResizeObserver({
|
||||
ref: documentElement,
|
||||
|
@ -286,14 +312,17 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
slugify: slugify
|
||||
})
|
||||
md.use(linkifyExtra)
|
||||
md.use(lineNumberMarker())
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
md.use(MarkdownItParserDebugger)
|
||||
}
|
||||
|
||||
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])
|
||||
|
@ -305,14 +334,12 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
}
|
||||
}, [tocAst, onTocChange])
|
||||
|
||||
const tryToReplaceNode = (node: DomElement, index: number, allReplacers: ComponentReplacer[], nodeConverter: SubNodeConverter) => {
|
||||
return allReplacers
|
||||
.map((componentReplacer) => componentReplacer.getReplacement(node, index, nodeConverter))
|
||||
.find((replacement) => !!replacement)
|
||||
}
|
||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||
const lastUsedLineId = useRef<number>(0)
|
||||
|
||||
const result: ReactElement[] = useMemo(() => {
|
||||
const markdownReactDom: ReactElement[] = useMemo(() => {
|
||||
const allReplacers: ComponentReplacer[] = [
|
||||
new CodimdLinemarkerReplacer(),
|
||||
new PossibleWiderReplacer(),
|
||||
new GistReplacer(),
|
||||
new YoutubeReplacer(),
|
||||
|
@ -332,17 +359,16 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
rawMetaRef.current = undefined
|
||||
}
|
||||
const html: string = markdownIt.render(content)
|
||||
|
||||
const transform: Transform = (node, index) => {
|
||||
const subNodeConverter = (subNode: DomElement, subIndex: number) => convertNodeToElement(subNode, subIndex, transform)
|
||||
return tryToReplaceNode(node, index, allReplacers, subNodeConverter) || convertNodeToElement(node, index, transform)
|
||||
}
|
||||
return ReactHtmlParser(html, { transform: transform })
|
||||
const contentLines = content.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])
|
||||
|
||||
return (
|
||||
<div className={'bg-light flex-fill'}>
|
||||
<div className={`markdown-body ${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`} ref={documentElement}>
|
||||
<div className={`${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`} >
|
||||
<ShowIf condition={yamlError}>
|
||||
<Alert variant='warning' dir='auto'>
|
||||
<Trans i18nKey='editor.invalidYaml'>
|
||||
|
@ -350,7 +376,9 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
|
|||
</Trans>
|
||||
</Alert>
|
||||
</ShowIf>
|
||||
{result}
|
||||
<div ref={documentElement} className={'markdown-body d-flex flex-column align-items-center'}>
|
||||
{markdownReactDom}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
109
src/components/markdown-renderer/renderer-utils.ts
Normal file
109
src/components/markdown-renderer/renderer-utils.ts
Normal file
|
@ -0,0 +1,109 @@
|
|||
import { diffArrays } from 'diff'
|
||||
import { DomElement } from 'domhandler'
|
||||
import { ReactElement } from 'react'
|
||||
import { convertNodeToElement, Transform } from 'react-html-parser'
|
||||
import {
|
||||
ComponentReplacer,
|
||||
NativeRenderer,
|
||||
SubNodeTransform
|
||||
} from './replace-components/ComponentReplacer'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const key = node.attribs['data-key']
|
||||
if (key) {
|
||||
return Number(key)
|
||||
}
|
||||
|
||||
const lineMarker = node.prev
|
||||
if (!lineMarker || !lineMarker.attribs) {
|
||||
return
|
||||
}
|
||||
|
||||
const lineInMarkdown = lineMarker.attribs['data-start-line']
|
||||
if (lineInMarkdown === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const line = Number(lineInMarkdown)
|
||||
if (lineKeys[line] === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
return lineKeys[line].id
|
||||
}
|
||||
|
||||
export const findNodeReplacement = (node: DomElement, index: number, allReplacers: ComponentReplacer[], transform: SubNodeTransform, nativeRenderer: NativeRenderer): ReactElement|null|undefined => {
|
||||
return allReplacers
|
||||
.map((componentReplacer) => componentReplacer.getReplacement(node, index, transform, nativeRenderer))
|
||||
.find((replacement) => replacement !== undefined)
|
||||
}
|
||||
|
||||
export const renderNativeNode = (node: DomElement, key: number, transform: Transform): ReactElement => {
|
||||
if (node.attribs === undefined) {
|
||||
node.attribs = {}
|
||||
}
|
||||
|
||||
delete node.attribs['data-key']
|
||||
return convertNodeToElement(node, key, transform)
|
||||
}
|
||||
|
||||
export const buildTransformer = (lineKeys: (LineKeys[] | undefined), allReplacers: ComponentReplacer[]):Transform => {
|
||||
const transform: Transform = (node, index) => {
|
||||
const nativeRenderer = (subNode: DomElement, subKey: number) => renderNativeNode(subNode, subKey, transform)
|
||||
const subNodeTransform:SubNodeTransform = (subNode, subIndex) => transform(subNode, subIndex, transform)
|
||||
|
||||
const key = calculateKeyFromLineMarker(node, lineKeys) ?? -index
|
||||
const tryReplacement = findNodeReplacement(node, key, allReplacers, subNodeTransform, nativeRenderer)
|
||||
if (tryReplacement === null) {
|
||||
return null
|
||||
} else if (tryReplacement === undefined) {
|
||||
return nativeRenderer(node, key)
|
||||
} else {
|
||||
return tryReplacement
|
||||
}
|
||||
}
|
||||
return transform
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
export type SubNodeConverter = (node: DomElement, index: number) => ReactElement
|
||||
export type SubNodeTransform = (node: DomElement, index: number) => ReactElement | void | null
|
||||
|
||||
export interface ComponentReplacer {
|
||||
getReplacement: (node: DomElement, index:number, subNodeConverter: SubNodeConverter) => (ReactElement|undefined)
|
||||
export type NativeRenderer = (node: DomElement, key: number) => ReactElement
|
||||
|
||||
export abstract class ComponentReplacer {
|
||||
public abstract getReplacement(node: DomElement, index: number, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): (ReactElement | null | undefined);
|
||||
}
|
||||
|
|
|
@ -4,17 +4,17 @@ import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { AsciinemaFrame } from './asciinema-frame'
|
||||
|
||||
export class AsciinemaReplacer implements ComponentReplacer {
|
||||
export class AsciinemaReplacer extends ComponentReplacer {
|
||||
private counterMap: Map<string, number> = new Map<string, number>()
|
||||
|
||||
getReplacement (node: DomElement): React.ReactElement | undefined {
|
||||
public getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'asciinema')
|
||||
if (attributes && attributes.id) {
|
||||
const asciinemaId = attributes.id
|
||||
const count = (this.counterMap.get(asciinemaId) || 0) + 1
|
||||
this.counterMap.set(asciinemaId, count)
|
||||
return (
|
||||
<AsciinemaFrame key={`asciinema_${asciinemaId}_${count}`} id={asciinemaId}/>
|
||||
<AsciinemaFrame key={index} id={asciinemaId}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
|
||||
export class CodimdLinemarkerReplacer extends ComponentReplacer {
|
||||
public getReplacement (codeNode: DomElement, index: number): null | undefined {
|
||||
return codeNode.name === 'codimd-linemarker' ? null : undefined
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@ import React from 'react'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { CsvTable } from './csv-table'
|
||||
|
||||
export class CsvReplacer implements ComponentReplacer {
|
||||
getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined {
|
||||
export class CsvReplacer extends ComponentReplacer {
|
||||
public getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'csv' || !codeNode.children || !codeNode.children[0]) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { FlowChart } from './flowchart/flowchart'
|
||||
|
||||
export class FlowchartReplacer implements ComponentReplacer {
|
||||
getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined {
|
||||
export class FlowchartReplacer extends ComponentReplacer {
|
||||
public getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'flow' || !codeNode.children || !codeNode.children[0]) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -6,17 +6,17 @@ import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
|
|||
import { GistFrame } from './gist-frame'
|
||||
import preview from './gist-preview.png'
|
||||
|
||||
export class GistReplacer implements ComponentReplacer {
|
||||
export class GistReplacer extends ComponentReplacer {
|
||||
private counterMap: Map<string, number> = new Map<string, number>()
|
||||
|
||||
getReplacement (node: DomElement): React.ReactElement | undefined {
|
||||
public getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'gist')
|
||||
if (attributes && attributes.id) {
|
||||
const gistId = attributes.id
|
||||
const count = (this.counterMap.get(gistId) || 0) + 1
|
||||
this.counterMap.set(gistId, count)
|
||||
return (
|
||||
<OneClickEmbedding previewContainerClassName={'gist-frame'} key={`gist_${gistId}_${count}`} loadingImageUrl={preview} hoverIcon={'github'} tooltip={'click to load gist'}>
|
||||
<OneClickEmbedding previewContainerClassName={'gist-frame'} key={index} loadingImageUrl={preview} hoverIcon={'github'} tooltip={'click to load gist'}>
|
||||
<GistFrame id={gistId}/>
|
||||
</OneClickEmbedding>
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ export const escapeHtml = (unsafe: string): string => {
|
|||
}
|
||||
|
||||
const checkIfLanguageIsSupported = (language: string): boolean => {
|
||||
return hljs.listLanguages().indexOf(language) > -1
|
||||
return hljs.listLanguages().includes(language)
|
||||
}
|
||||
|
||||
const correctLanguage = (language: string | undefined): string | undefined => {
|
||||
|
|
|
@ -3,10 +3,10 @@ import React from 'react'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { HighlightedCode } from './highlighted-code/highlighted-code'
|
||||
|
||||
export class HighlightedCodeReplacer implements ComponentReplacer {
|
||||
export class HighlightedCodeReplacer extends ComponentReplacer {
|
||||
private lastLineNumber = 0;
|
||||
|
||||
getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined {
|
||||
public getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || !codeNode.children || !codeNode.children[0]) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ImageFrame } from './image-frame'
|
||||
|
||||
export class ImageReplacer implements ComponentReplacer {
|
||||
getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
export class ImageReplacer extends ComponentReplacer {
|
||||
public getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
if (node.name === 'img' && node.attribs) {
|
||||
return <ImageFrame
|
||||
key={index}
|
||||
|
|
|
@ -5,19 +5,20 @@ import TeX from '@matejmazur/react-katex'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
|
||||
const getNodeIfKatexBlock = (node: DomElement): (DomElement|undefined) => {
|
||||
if (node.name !== 'p' || !node.children || node.children.length !== 1) {
|
||||
if (node.name !== 'p' || !node.children || node.children.length === 0) {
|
||||
return
|
||||
}
|
||||
const katexNode = node.children[0]
|
||||
return (katexNode.name === 'codimd-katex' && katexNode.attribs?.inline === undefined) ? katexNode : undefined
|
||||
return node.children.find((subnode) => {
|
||||
return (subnode.name === 'codimd-katex' && subnode.attribs?.inline === undefined)
|
||||
})
|
||||
}
|
||||
|
||||
const getNodeIfInlineKatex = (node: DomElement): (DomElement|undefined) => {
|
||||
return (node.name === 'codimd-katex' && node.attribs?.inline !== undefined) ? node : undefined
|
||||
}
|
||||
|
||||
export class KatexReplacer implements ComponentReplacer {
|
||||
getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
export class KatexReplacer extends ComponentReplacer {
|
||||
public getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
const katex = getNodeIfKatexBlock(node) || getNodeIfInlineKatex(node)
|
||||
if (katex?.children && katex.children[0]) {
|
||||
const mathJaxContent = katex.children[0]?.data as string
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
|
||||
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { PdfFrame } from './pdf-frame'
|
||||
|
||||
export class PdfReplacer implements ComponentReplacer {
|
||||
export class PdfReplacer extends ComponentReplacer {
|
||||
private counterMap: Map<string, number> = new Map<string, number>()
|
||||
|
||||
getReplacement (node: DomElement, index: number, subNodeConverter: SubNodeConverter): React.ReactElement | undefined {
|
||||
public getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'pdf')
|
||||
if (attributes && attributes.url) {
|
||||
const pdfUrl = attributes.url
|
||||
const count = (this.counterMap.get(pdfUrl) || 0) + 1
|
||||
this.counterMap.set(pdfUrl, count)
|
||||
return <PdfFrame key={`pdf_${pdfUrl}_${count}`} url={pdfUrl}/>
|
||||
return <PdfFrame key={index} url={pdfUrl}/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.wider .markdown-body {
|
||||
max-width: 1500px;
|
||||
width: 100%;
|
||||
|
||||
&>.wider-possible {
|
||||
max-width: 1500px;
|
||||
}
|
||||
}
|
|
@ -1,26 +1,29 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import React from 'react'
|
||||
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
|
||||
import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../ComponentReplacer'
|
||||
import './possible-wider-replacer.scss'
|
||||
|
||||
export class PossibleWiderReplacer implements ComponentReplacer {
|
||||
getReplacement (node: DomElement, index: number, subNodeConverter: SubNodeConverter): React.ReactElement | undefined {
|
||||
const enabledTags = ['img', 'codimd-youtube', 'codimd-vimeo', 'codimd-asciinema', 'codimd-pdf']
|
||||
|
||||
/**
|
||||
* This replacer doesn't actually replace something.
|
||||
* It just uses the ComponentReplacer-Class to get access to the DOM and
|
||||
* appends the "wider-possible" class to paragraphs with special content.
|
||||
*/
|
||||
export class PossibleWiderReplacer extends ComponentReplacer {
|
||||
public getReplacement (node: DomElement, index: number, subNodeTransformer: SubNodeTransform, nativeRenderer: NativeRenderer): (undefined) {
|
||||
if (node.name !== 'p') {
|
||||
return
|
||||
}
|
||||
if (!node.children || node.children.length !== 1) {
|
||||
return
|
||||
}
|
||||
const childIsImage = node.children[0].name === 'img'
|
||||
const childIsYoutube = node.children[0].name === 'codimd-youtube'
|
||||
const childIsVimeo = node.children[0].name === 'codimd-vimeo'
|
||||
const childIsAsciinema = node.children[0].name === 'codimd-asciinema'
|
||||
const childIsPDF = node.children[0].name === 'codimd-pdf'
|
||||
if (!(childIsImage || childIsYoutube || childIsVimeo || childIsAsciinema || childIsPDF)) {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// This appends the 'wider-possible' class to the node for a wider view in view-mode
|
||||
node.attribs = Object.assign(node.attribs || {}, { class: `wider-possible ${node.attribs?.class || ''}` })
|
||||
return subNodeConverter(node, index)
|
||||
if (node.children.find((subNode) => subNode.name && enabledTags.includes(subNode.name))) {
|
||||
if (!node.attribs) {
|
||||
node.attribs = {}
|
||||
}
|
||||
|
||||
node.attribs.class = `${node.attribs.class ?? ''} wider-possible`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { DomElement } from 'domhandler'
|
||||
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
|
||||
import { ReactElement } from 'react'
|
||||
import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../ComponentReplacer'
|
||||
|
||||
const isColorExtraElement = (node: DomElement | undefined): boolean => {
|
||||
if (!node || !node.attribs || !node.attribs.class || !node.attribs['data-color']) {
|
||||
|
@ -17,8 +18,8 @@ const findQuoteOptionsParent = (nodes: DomElement[]): DomElement | undefined =>
|
|||
})
|
||||
}
|
||||
|
||||
export class QuoteOptionsReplacer implements ComponentReplacer {
|
||||
getReplacement (node: DomElement, index: number, subNodeConverter: SubNodeConverter): React.ReactElement | undefined {
|
||||
export class QuoteOptionsReplacer extends ComponentReplacer {
|
||||
public getReplacement (node: DomElement, index: number, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer):ReactElement|undefined {
|
||||
if (node.name !== 'blockquote' || !node.children || node.children.length < 1) {
|
||||
return
|
||||
}
|
||||
|
@ -37,6 +38,6 @@ export class QuoteOptionsReplacer implements ComponentReplacer {
|
|||
return
|
||||
}
|
||||
node.attribs = Object.assign(node.attribs || {}, { style: `border-left-color: ${attributes['data-color']};` })
|
||||
return subNodeConverter(node, index)
|
||||
return nativeRenderer(node, index)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import React, { ReactElement } from 'react'
|
||||
import { DomElement } from 'domhandler'
|
||||
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
|
||||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
|
||||
export class TaskListReplacer implements ComponentReplacer {
|
||||
export class TaskListReplacer extends ComponentReplacer {
|
||||
onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void
|
||||
|
||||
constructor (onTaskCheckedChange: (lineInMarkdown: number, checked: boolean) => void) {
|
||||
super()
|
||||
this.onTaskCheckedChange = onTaskCheckedChange
|
||||
}
|
||||
|
||||
|
@ -14,7 +15,7 @@ export class TaskListReplacer implements ComponentReplacer {
|
|||
this.onTaskCheckedChange(lineNum, event.currentTarget.checked)
|
||||
}
|
||||
|
||||
getReplacement (node: DomElement, index:number, subNodeConverter: SubNodeConverter): (ReactElement|undefined) {
|
||||
public getReplacement (node: DomElement, index:number): (ReactElement|undefined) {
|
||||
if (node.attribs?.class === 'task-list-item-checkbox') {
|
||||
return (
|
||||
<input
|
||||
|
|
|
@ -4,16 +4,16 @@ import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { VimeoFrame } from './vimeo-frame'
|
||||
|
||||
export class VimeoReplacer implements ComponentReplacer {
|
||||
export class VimeoReplacer extends ComponentReplacer {
|
||||
private counterMap: Map<string, number> = new Map<string, number>()
|
||||
|
||||
getReplacement (node: DomElement): React.ReactElement | undefined {
|
||||
public getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'vimeo')
|
||||
if (attributes && attributes.id) {
|
||||
const videoId = attributes.id
|
||||
const count = (this.counterMap.get(videoId) || 0) + 1
|
||||
this.counterMap.set(videoId, count)
|
||||
return <VimeoFrame key={`vimeo_${videoId}_${count}`} id={videoId}/>
|
||||
return <VimeoFrame key={index} id={videoId}/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,16 +4,16 @@ import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { YouTubeFrame } from './youtube-frame'
|
||||
|
||||
export class YoutubeReplacer implements ComponentReplacer {
|
||||
export class YoutubeReplacer extends ComponentReplacer {
|
||||
private counterMap: Map<string, number> = new Map<string, number>()
|
||||
|
||||
getReplacement (node: DomElement): React.ReactElement | undefined {
|
||||
public getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
|
||||
const attributes = getAttributesFromCodiMdTag(node, 'youtube')
|
||||
if (attributes && attributes.id) {
|
||||
const videoId = attributes.id
|
||||
const count = (this.counterMap.get(videoId) || 0) + 1
|
||||
this.counterMap.set(videoId, count)
|
||||
return <YouTubeFrame key={`youtube_${videoId}_${count}`} id={videoId}/>
|
||||
return <YouTubeFrame key={index} id={videoId}/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ function checkValidServiceWorker (swUrl: string, config?: Config) {
|
|||
const contentType = response.headers.get('content-type')
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
(contentType != null && !contentType.includes('javascript'))
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -1678,6 +1678,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||
|
||||
"@types/diff@4.0.2":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c"
|
||||
integrity sha512-mIenTfsIe586/yzsyfql69KRnA75S8SVXQbTLpDejRrjH0QSJcpu3AUOi/Vjnt9IOsXKxPhJfGpQUNMueIU1fQ==
|
||||
|
||||
"@types/domhandler@*":
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/domhandler/-/domhandler-2.4.1.tgz#7b3b347f7762180fbcb1ece1ce3dd0ebbb8c64cf"
|
||||
|
@ -3709,10 +3714,10 @@ compression@^1.7.4:
|
|||
safe-buffer "5.1.2"
|
||||
vary "~1.1.2"
|
||||
|
||||
compute-scroll-into-view@^1.0.14:
|
||||
version "1.0.14"
|
||||
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.14.tgz#80e3ebb25d6aa89f42e533956cb4b16a04cfe759"
|
||||
integrity sha512-mKDjINe3tc6hGelUMNDzuhorIUZ7kS7BwyY0r2wQd2HOH2tRuJykiC06iSEX8y1TuhNzvz4GcJnK16mM2J1NMQ==
|
||||
compute-scroll-into-view@^1.0.16:
|
||||
version "1.0.16"
|
||||
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.16.tgz#5b7bf4f7127ea2c19b750353d7ce6776a90ee088"
|
||||
integrity sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ==
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
|
@ -4456,6 +4461,11 @@ diff-sequences@^25.2.6:
|
|||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
|
||||
integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==
|
||||
|
||||
diff@4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
diffie-hellman@^5.0.0:
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
|
||||
|
@ -11236,11 +11246,11 @@ screenfull@^5.0.0:
|
|||
integrity sha512-cCF2b+L/mnEiORLN5xSAz6H3t18i2oHh9BA8+CQlAh5DRw2+NFAGQJOSYbcGw8B2k04g/lVvFcfZ83b3ysH5UQ==
|
||||
|
||||
scroll-into-view-if-needed@^2.2.20:
|
||||
version "2.2.25"
|
||||
resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.25.tgz#117b7bc7c61bc7a2b7872a0984bc73a19bc6e961"
|
||||
integrity sha512-C8RKJPq9lK7eubwGpLbUkw3lklcG3Ndjmea2PyauzrA0i4DPlzAmVMGxaZrBFqCrVLfvJmP80IyHnv4jxvg1OQ==
|
||||
version "2.2.26"
|
||||
resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.26.tgz#e4917da0c820135ff65ad6f7e4b7d7af568c4f13"
|
||||
integrity sha512-SQ6AOKfABaSchokAmmaxVnL9IArxEnLEX9j4wAZw+x4iUTb40q7irtHG3z4GtAWz5veVZcCnubXDBRyLVQaohw==
|
||||
dependencies:
|
||||
compute-scroll-into-view "^1.0.14"
|
||||
compute-scroll-into-view "^1.0.16"
|
||||
|
||||
scss-tokenizer@^0.2.3:
|
||||
version "0.2.3"
|
||||
|
|
Loading…
Add table
Reference in a new issue