From 73007ef59774d1acb811f4ac2690d3574f2afc60 Mon Sep 17 00:00:00 2001 From: mrdrogdrog Date: Wed, 19 Aug 2020 23:01:38 +0200 Subject: [PATCH] Added synced scrolling (#386) Signed-off-by: Tilman Vatteroth --- .../document-render-pane.tsx | 69 +++++++++- .../editor/editor-pane/editor-pane.tsx | 45 ++++++- src/components/editor/editor.tsx | 36 ++++- .../markdown-it-plugins/line-number-marker.ts | 55 ++++++++ src/components/editor/scroll/scroll-props.ts | 15 +++ src/components/editor/scroll/utils.ts | 25 ++++ src/components/editor/splitter/splitter.tsx | 2 +- .../table-of-contents/table-of-contents.tsx | 6 +- .../markdown-renderer/markdown-renderer.tsx | 72 +++++++--- yarn.lock | 126 +++++++++++++++++- 10 files changed, 413 insertions(+), 38 deletions(-) create mode 100644 src/components/editor/markdown-renderer/markdown-it-plugins/line-number-marker.ts create mode 100644 src/components/editor/scroll/scroll-props.ts create mode 100644 src/components/editor/scroll/utils.ts diff --git a/src/components/editor/document-renderer-pane/document-render-pane.tsx b/src/components/editor/document-renderer-pane/document-render-pane.tsx index 64571dff1..7d065842d 100644 --- a/src/components/editor/document-renderer-pane/document-render-pane.tsx +++ b/src/components/editor/document-renderer-pane/document-render-pane.tsx @@ -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 = ({ content, onMetadataChange, onFirstHeadingChange, wide }) => { +export const DocumentRenderPane: React.FC = ({ content, onMetadataChange, onFirstHeadingChange, wide, scrollState, onScroll, onMakeScrollSource }) => { const [tocAst, setTocAst] = useState() const renderer = useRef(null) const { width } = useResizeObserver({ ref: renderer }) + const lastScrollPosition = useRef() + const [lineMarks, setLineMarks] = useState() 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 ( -
+
= ({ content, onTocChange={(tocAst) => setTocAst(tocAst)} onMetaDataChange={onMetadataChange} onFirstHeadingChange={onFirstHeadingChange} + onLineMarkerPositionChanged={setLineMarks} />
= 1280 && !!tocAst}> - +
diff --git a/src/components/editor/editor-pane/editor-pane.tsx b/src/components/editor/editor-pane/editor-pane.tsx index 53a87b83f..ea93b744b 100644 --- a/src/components/editor/editor-pane/editor-pane.tsx +++ b/src/components/editor/editor-pane/editor-pane.tsx @@ -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 = ({ onContentChange, content }) => { +export const EditorPane: React.FC = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => { const { t } = useTranslation() const [editor, setEditor] = useState() const [statusBarInfo, setStatusBarInfo] = useState(defaultState) @@ -59,6 +60,41 @@ export const EditorPane: React.FC = ({ onContentChange, content indentWithTabs: false }) + const lastScrollPosition = useRef() + + 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 = ({ onContentChange, content }), [t, editorPreferences]) return ( -
+
setEditorPreferences(config)} @@ -114,6 +150,7 @@ export const EditorPane: React.FC = ({ onContentChange, content onCursorActivity={onCursorActivity} editorDidMount={onEditorDidMount} onBeforeChange={onBeforeChange} + onScroll={onEditorScroll} />
diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx index df6e13f9f..fc4bdf044 100644 --- a/src/components/editor/editor.tsx +++ b/src/components/editor/editor.tsx @@ -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() const firstHeading = useRef() + const scrollSource = useRef(ScrollSource.EDITOR) + + const [scrollState, setScrollState] = useState(() => ({ + 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 ( @@ -72,16 +96,22 @@ export const Editor: React.FC = () => { left={ setMarkdownContent(content)} - content={markdownContent}/> + content={markdownContent} + scrollState={scrollState.editorScrollState} + onScroll={onEditorScroll} + onMakeScrollSource={() => { scrollSource.current = ScrollSource.EDITOR }} + /> } showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)} right={ - } + onFirstHeadingChange={onFirstHeadingChange} + onMakeScrollSource={() => { scrollSource.current = ScrollSource.RENDERER }}/>} containerClassName={'overflow-hidden'}/>
diff --git a/src/components/editor/markdown-renderer/markdown-it-plugins/line-number-marker.ts b/src/components/editor/markdown-renderer/markdown-it-plugins/line-number-marker.ts new file mode 100644 index 000000000..16e8d1e20 --- /dev/null +++ b/src/components/editor/markdown-renderer/markdown-it-plugins/line-number-marker.ts @@ -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 + 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 `` + } + } +} diff --git a/src/components/editor/scroll/scroll-props.ts b/src/components/editor/scroll/scroll-props.ts new file mode 100644 index 000000000..95b7c2310 --- /dev/null +++ b/src/components/editor/scroll/scroll-props.ts @@ -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 +} diff --git a/src/components/editor/scroll/utils.ts b/src/components/editor/scroll/utils.ts new file mode 100644 index 000000000..6fa271998 --- /dev/null +++ b/src/components/editor/scroll/utils.ts @@ -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 + } +} diff --git a/src/components/editor/splitter/splitter.tsx b/src/components/editor/splitter/splitter.tsx index 665bf786d..ec37e2074 100644 --- a/src/components/editor/splitter/splitter.tsx +++ b/src/components/editor/splitter/splitter.tsx @@ -54,7 +54,7 @@ export const Splitter: React.FC = ({ containerClassName, left, ri
-
+
{right}
diff --git a/src/components/editor/table-of-contents/table-of-contents.tsx b/src/components/editor/table-of-contents/table-of-contents.tsx index ecd4e679f..edc0ae915 100644 --- a/src/components/editor/table-of-contents/table-of-contents.tsx +++ b/src/components/editor/table-of-contents/table-of-contents.tsx @@ -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 = ({ ast, maxDepth = 3, sticky }) => { +export const TableOfContents: React.FC = ({ ast, maxDepth = 3, className }) => { const tocTree = useMemo(() => convertLevel(ast, maxDepth, new Map(), false), [ast, maxDepth]) return ( -
+
{tocTree}
) diff --git a/src/components/markdown-renderer/markdown-renderer.tsx b/src/components/markdown-renderer/markdown-renderer.tsx index 701905dd5..c12f55595 100644 --- a/src/components/markdown-renderer/markdown-renderer.tsx +++ b/src/components/markdown-renderer/markdown-renderer.tsx @@ -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 = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => { +export const MarkdownRenderer: React.FC = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide, onLineMarkerPositionChanged }) => { const [tocAst, setTocAst] = useState() - const [lastTocAst, setLastTocAst] = useState() + const lastTocAst = useRef() const [yamlError, setYamlError] = useState(false) const rawMetaRef = useRef() const oldMetaRef = useRef() const firstHeadingRef = useRef() const oldFirstHeadingRef = useRef() + const documentElement = useRef(null) + const lastLineMarkerPositions = useRef() + + const calculateLineMarkerPositions = useCallback(() => { + if (documentElement.current && onLineMarkerPositionChanged) { + const lineMarkers: NodeListOf = 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 = ({ 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 = ({ 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 = ({ content, onM }, [content, markdownIt, onMetaDataChange]) return ( -
- - - - - - - - - {result} - +
+
+ + + + + + + + + {result} + +
) } diff --git a/yarn.lock b/yarn.lock index 242c71164..7710b2ced 100644 --- a/yarn.lock +++ b/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"