Added synced scrolling (#386)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
mrdrogdrog 2020-08-19 23:01:38 +02:00 committed by GitHub
parent 164b5436ae
commit 73007ef597
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 413 additions and 38 deletions

View file

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

View file

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

View file

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

View file

@ -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>`
}
}
}

View 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
}

View 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
}
}

View file

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

View file

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

View file

@ -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,7 +327,8 @@ 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' : ''}`}>
<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'>
@ -303,5 +340,6 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onM
{result}
</MathJaxReact.Provider>
</div>
</div>
)
}

126
yarn.lock
View file

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