mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-25 09:51:03 +00:00
Add slide mode with reveal.js
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
29565f8f89
commit
36e445e631
70 changed files with 1225 additions and 323 deletions
|
@ -15,7 +15,8 @@
|
|||
"rules": {
|
||||
"@typescript-eslint/no-unused-expressions": 0,
|
||||
"no-unused-expressions": 0,
|
||||
"chai-friendly/no-unused-expressions": 2
|
||||
"chai-friendly/no-unused-expressions": 2,
|
||||
"@typescript-eslint/no-namespace": 0
|
||||
},
|
||||
"env": {
|
||||
"cypress/globals": true
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('Autocompletion', () => {
|
|||
|
||||
describe('code block', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill('```')
|
||||
cy.setCodemirrorContent('```')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -29,7 +29,7 @@ describe('Autocompletion', () => {
|
|||
cy.getMarkdownBody().find('.code-highlighter').should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill('```')
|
||||
cy.setCodemirrorContent('```')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -45,7 +45,7 @@ describe('Autocompletion', () => {
|
|||
|
||||
describe('container', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill(':::')
|
||||
cy.setCodemirrorContent(':::')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -61,7 +61,7 @@ describe('Autocompletion', () => {
|
|||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill(':::')
|
||||
cy.setCodemirrorContent(':::')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -80,7 +80,7 @@ describe('Autocompletion', () => {
|
|||
describe('emoji', () => {
|
||||
describe('normal emoji', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill(':hedg')
|
||||
cy.setCodemirrorContent(':hedg')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -91,7 +91,7 @@ describe('Autocompletion', () => {
|
|||
.should('have.text', ':hedgehog:')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill(':hedg')
|
||||
cy.setCodemirrorContent(':hedg')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -104,7 +104,7 @@ describe('Autocompletion', () => {
|
|||
|
||||
describe('fork-awesome-icon', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill(':fa-face')
|
||||
cy.setCodemirrorContent(':fa-face')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -115,7 +115,7 @@ describe('Autocompletion', () => {
|
|||
.should('have.text', ':fa-facebook:')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill(':fa-face')
|
||||
cy.setCodemirrorContent(':fa-face')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -129,7 +129,7 @@ describe('Autocompletion', () => {
|
|||
|
||||
describe('header', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill('#')
|
||||
cy.setCodemirrorContent('#')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -143,7 +143,7 @@ describe('Autocompletion', () => {
|
|||
.should('have.text', '\n ')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill('#')
|
||||
cy.setCodemirrorContent('#')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -159,7 +159,7 @@ describe('Autocompletion', () => {
|
|||
|
||||
describe('images', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill('!')
|
||||
cy.setCodemirrorContent('!')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -175,7 +175,7 @@ describe('Autocompletion', () => {
|
|||
.should('have.attr', 'title', 'title')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill('!')
|
||||
cy.setCodemirrorContent('!')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -193,7 +193,7 @@ describe('Autocompletion', () => {
|
|||
|
||||
describe('links', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill('[')
|
||||
cy.setCodemirrorContent('[')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -209,7 +209,7 @@ describe('Autocompletion', () => {
|
|||
.should('have.attr', 'title', 'title')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill('[')
|
||||
cy.setCodemirrorContent('[')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -227,7 +227,7 @@ describe('Autocompletion', () => {
|
|||
|
||||
describe('pdf', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill('{')
|
||||
cy.setCodemirrorContent('{')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -241,7 +241,7 @@ describe('Autocompletion', () => {
|
|||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill('{')
|
||||
cy.setCodemirrorContent('{')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
@ -257,7 +257,7 @@ describe('Autocompletion', () => {
|
|||
|
||||
describe('collapsable blocks', () => {
|
||||
it('via Enter', () => {
|
||||
cy.codemirrorFill('<d')
|
||||
cy.setCodemirrorContent('<d')
|
||||
cy.get('.CodeMirror-hints')
|
||||
.should('exist')
|
||||
cy.get('@codeinput')
|
||||
|
@ -271,7 +271,7 @@ describe('Autocompletion', () => {
|
|||
.should('exist')
|
||||
})
|
||||
it('via doubleclick', () => {
|
||||
cy.codemirrorFill('<d')
|
||||
cy.setCodemirrorContent('<d')
|
||||
cy.get('.CodeMirror-hints > li')
|
||||
.first()
|
||||
.dblclick()
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('Diagram codeblock ', () => {
|
|||
})
|
||||
|
||||
it('renders markmap', () => {
|
||||
cy.codemirrorFill('```markmap\n- pro\n- contra\n```')
|
||||
cy.setCodemirrorContent('```markmap\n- pro\n- contra\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('[data-cy=markmap]')
|
||||
.children()
|
||||
|
@ -18,7 +18,7 @@ describe('Diagram codeblock ', () => {
|
|||
})
|
||||
|
||||
it('renders vega-lite', () => {
|
||||
cy.codemirrorFill('```vega-lite\n{"$schema":"https://vega.github.io/schema/vega-lite/v4.json","data":{"values":[{"a":"","b":28}]},"mark":"bar","encoding":{"x":{"field":"a"},"y":{"field":"b"}}}\n```')
|
||||
cy.setCodemirrorContent('```vega-lite\n{"$schema":"https://vega.github.io/schema/vega-lite/v4.json","data":{"values":[{"a":"","b":28}]},"mark":"bar","encoding":{"x":{"field":"a"},"y":{"field":"b"}}}\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('.vega-embed')
|
||||
.children()
|
||||
|
@ -26,7 +26,7 @@ describe('Diagram codeblock ', () => {
|
|||
})
|
||||
|
||||
it('renders graphviz', () => {
|
||||
cy.codemirrorFill('```graphviz\ngraph {\na -- b\n}\n```')
|
||||
cy.setCodemirrorContent('```graphviz\ngraph {\na -- b\n}\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('[data-cy=graphviz]')
|
||||
.children()
|
||||
|
@ -34,7 +34,7 @@ describe('Diagram codeblock ', () => {
|
|||
})
|
||||
|
||||
it('renders mermaid', () => {
|
||||
cy.codemirrorFill('```mermaid\ngraph TD;\n A-->B;\n```')
|
||||
cy.setCodemirrorContent('```mermaid\ngraph TD;\n A-->B;\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('.mermaid')
|
||||
.children()
|
||||
|
@ -42,7 +42,7 @@ describe('Diagram codeblock ', () => {
|
|||
})
|
||||
|
||||
it('renders flowcharts', () => {
|
||||
cy.codemirrorFill('```flow\nst=>start: Start\ne=>end: End\nst->e\n```')
|
||||
cy.setCodemirrorContent('```flow\nst=>start: Start\ne=>end: End\nst->e\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('[data-cy=flowchart]')
|
||||
.children()
|
||||
|
@ -50,7 +50,7 @@ describe('Diagram codeblock ', () => {
|
|||
})
|
||||
|
||||
it('renders abc scores', () => {
|
||||
cy.codemirrorFill('```abc\nM:4/4\nK:G\n|:GABc dedB:|\n```')
|
||||
cy.setCodemirrorContent('```abc\nM:4/4\nK:G\n|:GABc dedB:|\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('.abcjs-score')
|
||||
.children()
|
||||
|
@ -58,14 +58,14 @@ describe('Diagram codeblock ', () => {
|
|||
})
|
||||
|
||||
it('renders csv as table', () => {
|
||||
cy.codemirrorFill('```csv delimiter=; header\na;b;c;d\n1;2;3;4\n```')
|
||||
cy.setCodemirrorContent('```csv delimiter=; header\na;b;c;d\n1;2;3;4\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('.csv-html-table')
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('renders plantuml', () => {
|
||||
cy.codemirrorFill('```plantuml\nclass Example\n```')
|
||||
cy.setCodemirrorContent('```plantuml\nclass Example\n```')
|
||||
cy.getMarkdownBody()
|
||||
.find('img')
|
||||
// PlantUML uses base64 encoded version of zip-deflated PlantUML code in the request URL.
|
||||
|
|
|
@ -16,19 +16,19 @@ describe('Document Title', () => {
|
|||
|
||||
describe('title should be yaml metadata title', () => {
|
||||
it('just yaml metadata title', () => {
|
||||
cy.codemirrorFill(`---\ntitle: ${ title }\n---`)
|
||||
cy.setCodemirrorContent(`---\ntitle: ${ title }\n---`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('yaml metadata title and opengraph title', () => {
|
||||
cy.codemirrorFill(`---\ntitle: ${ title }\nopengraph:\n title: False title\n---`)
|
||||
cy.setCodemirrorContent(`---\ntitle: ${ title }\nopengraph:\n title: False title\n---`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('yaml metadata title, opengraph title and first heading', () => {
|
||||
cy.codemirrorFill(`---\ntitle: ${ title }\nopengraph:\n title: False title\n---\n# a first title`)
|
||||
cy.setCodemirrorContent(`---\ntitle: ${ title }\nopengraph:\n title: False title\n---\n# a first title`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
@ -36,13 +36,13 @@ describe('Document Title', () => {
|
|||
|
||||
describe('title should be opengraph title', () => {
|
||||
it('just opengraph title', () => {
|
||||
cy.codemirrorFill(`---\nopengraph:\n title: ${ title }\n---`)
|
||||
cy.setCodemirrorContent(`---\nopengraph:\n title: ${ title }\n---`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('opengraph title and first heading', () => {
|
||||
cy.codemirrorFill(`---\nopengraph:\n title: ${ title }\n---\n# a first title`)
|
||||
cy.setCodemirrorContent(`---\nopengraph:\n title: ${ title }\n---\n# a first title`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
@ -50,44 +50,44 @@ describe('Document Title', () => {
|
|||
|
||||
describe('title should be first heading', () => {
|
||||
it('just first heading', () => {
|
||||
cy.codemirrorFill(`# ${ title }`)
|
||||
cy.setCodemirrorContent(`# ${ title }`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('just first heading with alt-text instead of image', () => {
|
||||
cy.codemirrorFill(`# ${ title } `)
|
||||
cy.setCodemirrorContent(`# ${ title } `)
|
||||
cy.title()
|
||||
.should('eq', `${ title } abc - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('just first heading without link syntax', () => {
|
||||
cy.codemirrorFill(`# ${ title } [link](https://hedgedoc.org)`)
|
||||
cy.setCodemirrorContent(`# ${ title } [link](https://hedgedoc.org)`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } link - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('markdown syntax removed first', () => {
|
||||
cy.codemirrorFill(`# ${ title } 1*2*3 4*5**`)
|
||||
cy.setCodemirrorContent(`# ${ title } 1*2*3 4*5**`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } 123 4*5** - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('markdown syntax removed second', () => {
|
||||
cy.codemirrorFill(`# ${ title } **1 2*`)
|
||||
cy.setCodemirrorContent(`# ${ title } **1 2*`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } *1 2 - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('markdown syntax removed third', () => {
|
||||
cy.codemirrorFill(`# ${ title } _asd_`)
|
||||
cy.setCodemirrorContent(`# ${ title } _asd_`)
|
||||
cy.title()
|
||||
.should('eq', `${ title } asd - HedgeDoc @ ${ branding.name }`)
|
||||
})
|
||||
|
||||
it('katex code looks right', () => {
|
||||
cy.codemirrorFill(`# $\\alpha$-foo`)
|
||||
cy.getMarkdownRenderer()
|
||||
cy.setCodemirrorContent(`# $\\alpha$-foo`)
|
||||
cy.getIframeBody()
|
||||
.find('h1')
|
||||
.should('contain', 'α')
|
||||
cy.get('.CodeMirror textarea')
|
||||
|
|
|
@ -13,19 +13,19 @@ describe('emojis', () => {
|
|||
})
|
||||
|
||||
it('renders an emoji shortcode', () => {
|
||||
cy.codemirrorFill(':hedgehog:')
|
||||
cy.setCodemirrorContent(':hedgehog:')
|
||||
cy.getMarkdownBody()
|
||||
.should('have.text', HEDGEHOG_UNICODE_CHARACTER)
|
||||
})
|
||||
|
||||
it('renders an emoji unicode character', () => {
|
||||
cy.codemirrorFill(HEDGEHOG_UNICODE_CHARACTER)
|
||||
cy.setCodemirrorContent(HEDGEHOG_UNICODE_CHARACTER)
|
||||
cy.getMarkdownBody()
|
||||
.should('have.text', HEDGEHOG_UNICODE_CHARACTER)
|
||||
})
|
||||
|
||||
it('renders an fork awesome icon', () => {
|
||||
cy.codemirrorFill(':fa-matrix-org:')
|
||||
cy.setCodemirrorContent(':fa-matrix-org:')
|
||||
cy.getMarkdownBody()
|
||||
.find('i.fa.fa-matrix-org')
|
||||
.should('be.visible')
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('Export', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
cy.visitTestEditor()
|
||||
cy.codemirrorFill(testContent)
|
||||
cy.setCodemirrorContent(testContent)
|
||||
})
|
||||
|
||||
it('Markdown', () => {
|
||||
|
|
|
@ -13,7 +13,7 @@ describe('File upload', () => {
|
|||
|
||||
it('doesn\'t prevent drag\'n\'drop of plain text', () => {
|
||||
const dataTransfer = new DataTransfer()
|
||||
cy.codemirrorFill('line 1\nline 2\ndragline')
|
||||
cy.setCodemirrorContent('line 1\nline 2\ndragline')
|
||||
cy.get('.CodeMirror')
|
||||
.click()
|
||||
cy.get('.CodeMirror-line > span')
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('Code', () => {
|
|||
|
||||
describe('with just the language', () => {
|
||||
it('doesn\'t show a gutter', () => {
|
||||
cy.codemirrorFill('```javascript \nlet x = 0\n```')
|
||||
cy.setCodemirrorContent('```javascript \nlet x = 0\n```')
|
||||
findHljsCodeBlock()
|
||||
.should('not.have.class', 'showGutter')
|
||||
|
||||
|
@ -26,7 +26,7 @@ describe('Code', () => {
|
|||
|
||||
describe('and line wrapping', () => {
|
||||
it('doesn\'t show a gutter', () => {
|
||||
cy.codemirrorFill('```javascript! \nlet x = 0\n```')
|
||||
cy.setCodemirrorContent('```javascript! \nlet x = 0\n```')
|
||||
findHljsCodeBlock()
|
||||
.should('not.have.class', 'showGutter')
|
||||
.should('have.class', 'wrapLines')
|
||||
|
@ -40,7 +40,7 @@ describe('Code', () => {
|
|||
|
||||
describe('with the language and show gutter', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.codemirrorFill('```javascript= \nlet x = 0\n```')
|
||||
cy.setCodemirrorContent('```javascript= \nlet x = 0\n```')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
|
||||
|
@ -53,7 +53,7 @@ describe('Code', () => {
|
|||
|
||||
describe('and line wrapping', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.codemirrorFill('```javascript=! \nlet x = 0\n```')
|
||||
cy.setCodemirrorContent('```javascript=! \nlet x = 0\n```')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
.should('have.class', 'wrapLines')
|
||||
|
@ -69,7 +69,7 @@ describe('Code', () => {
|
|||
|
||||
describe('with the language, show gutter with a start number', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.codemirrorFill('```javascript=100 \nlet x = 0\n```')
|
||||
cy.setCodemirrorContent('```javascript=100 \nlet x = 0\n```')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
|
||||
|
@ -81,7 +81,7 @@ describe('Code', () => {
|
|||
})
|
||||
|
||||
it('shows the correct line number and continues in another codeblock', () => {
|
||||
cy.codemirrorFill('```javascript=100 \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
||||
cy.setCodemirrorContent('```javascript=100 \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
.first()
|
||||
|
@ -108,7 +108,7 @@ describe('Code', () => {
|
|||
|
||||
describe('and line wrapping', () => {
|
||||
it('shows the correct line number', () => {
|
||||
cy.codemirrorFill('```javascript=100! \nlet x = 0\n```')
|
||||
cy.setCodemirrorContent('```javascript=100! \nlet x = 0\n```')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
.should('have.class', 'wrapLines')
|
||||
|
@ -120,7 +120,7 @@ describe('Code', () => {
|
|||
})
|
||||
|
||||
it('shows the correct line number and continues in another codeblock', () => {
|
||||
cy.codemirrorFill('```javascript=100! \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
||||
cy.setCodemirrorContent('```javascript=100! \nlet x = 0\nlet y = 1\n```\n\n```javascript=+\nlet y = 2\n```\n')
|
||||
findHljsCodeBlock()
|
||||
.should('have.class', 'showGutter')
|
||||
.should('have.class', 'wrapLines')
|
||||
|
@ -149,7 +149,7 @@ describe('Code', () => {
|
|||
})
|
||||
|
||||
it('has a working copy button', () => {
|
||||
cy.codemirrorFill('```javascript \nlet x = 0\n```')
|
||||
cy.setCodemirrorContent('```javascript \nlet x = 0\n```')
|
||||
|
||||
cy.get(`iframe[data-cy="documentIframe"]`)
|
||||
.then(($element: JQuery) => {
|
||||
|
@ -162,7 +162,7 @@ describe('Code', () => {
|
|||
.as('copy')
|
||||
})
|
||||
|
||||
cy.getMarkdownRenderer()
|
||||
cy.getIframeBody()
|
||||
.find('[data-cy="copy-code-button"]')
|
||||
.click()
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ describe('Import markdown file', () => {
|
|||
|
||||
it('import on note with content', () => {
|
||||
|
||||
cy.codemirrorFill('test\nabc')
|
||||
cy.setCodemirrorContent('test\nabc')
|
||||
cy.get('[data-cy="menu-import"]')
|
||||
.click()
|
||||
cy.get('[data-cy="menu-import-markdown"]')
|
||||
|
|
|
@ -12,7 +12,7 @@ describe('Link gets replaced with embedding: ', () => {
|
|||
// TODO Add general testing of one-click-embedding component. The tests below just test a specific use of the component.
|
||||
|
||||
it('GitHub Gist', () => {
|
||||
cy.codemirrorFill('https://gist.github.com/schacon/1')
|
||||
cy.setCodemirrorContent('https://gist.github.com/schacon/1')
|
||||
cy.getMarkdownBody()
|
||||
.find('.one-click-embedding.gist-frame')
|
||||
.click()
|
||||
|
@ -22,7 +22,7 @@ describe('Link gets replaced with embedding: ', () => {
|
|||
})
|
||||
|
||||
it('YouTube', () => {
|
||||
cy.codemirrorFill('https://www.youtube.com/watch?v=YE7VzlLtp-4')
|
||||
cy.setCodemirrorContent('https://www.youtube.com/watch?v=YE7VzlLtp-4')
|
||||
cy.getMarkdownBody()
|
||||
.find('.one-click-embedding-preview')
|
||||
.should('have.attr', 'src', 'https://i.ytimg.com/vi/YE7VzlLtp-4/maxresdefault.jpg')
|
||||
|
@ -44,7 +44,7 @@ describe('Link gets replaced with embedding: ', () => {
|
|||
},
|
||||
body: '[{"thumbnail_large": "https://i.vimeocdn.com/video/503631401_640.jpg"}]'
|
||||
})
|
||||
cy.codemirrorFill('https://vimeo.com/23237102')
|
||||
cy.setCodemirrorContent('https://vimeo.com/23237102')
|
||||
cy.getMarkdownBody()
|
||||
.find('.one-click-embedding-preview')
|
||||
.should('have.attr', 'src', 'https://i.vimeocdn.com/video/503631401_640.jpg')
|
||||
|
@ -56,7 +56,7 @@ describe('Link gets replaced with embedding: ', () => {
|
|||
})
|
||||
|
||||
it('Asciinema', () => {
|
||||
cy.codemirrorFill('https://asciinema.org/a/117928')
|
||||
cy.setCodemirrorContent('https://asciinema.org/a/117928')
|
||||
cy.getMarkdownBody()
|
||||
.find('.one-click-embedding-preview')
|
||||
.should('have.attr', 'src', 'https://asciinema.org/a/117928.png')
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('markdown formatted links to', () => {
|
|||
})
|
||||
|
||||
it('external domains render as external link', () => {
|
||||
cy.codemirrorFill('[external](https://hedgedoc.org/)')
|
||||
cy.setCodemirrorContent('[external](https://hedgedoc.org/)')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'https://hedgedoc.org/')
|
||||
|
@ -19,28 +19,28 @@ describe('markdown formatted links to', () => {
|
|||
})
|
||||
|
||||
it('note anchor references render as anchor link', () => {
|
||||
cy.codemirrorFill('[anchor](#anchor)')
|
||||
cy.setCodemirrorContent('[anchor](#anchor)')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'http://127.0.0.1:3001/n/test#anchor')
|
||||
})
|
||||
|
||||
it('internal pages render as internal link', () => {
|
||||
cy.codemirrorFill('[internal](other-note)')
|
||||
cy.setCodemirrorContent('[internal](other-note)')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'http://127.0.0.1:3001/n/other-note')
|
||||
})
|
||||
|
||||
it('data URIs do not render', () => {
|
||||
cy.codemirrorFill('[data](data:text/plain,evil)')
|
||||
cy.setCodemirrorContent('[data](data:text/plain,evil)')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('javascript URIs do not render', () => {
|
||||
cy.codemirrorFill('[js](javascript:alert("evil"))')
|
||||
cy.setCodemirrorContent('[js](javascript:alert("evil"))')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('not.exist')
|
||||
|
@ -53,7 +53,7 @@ describe('HTML anchor element links to', () => {
|
|||
})
|
||||
|
||||
it('external domains render as external link', () => {
|
||||
cy.codemirrorFill('<a href="https://hedgedoc.org/">external</a>')
|
||||
cy.setCodemirrorContent('<a href="https://hedgedoc.org/">external</a>')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'https://hedgedoc.org/')
|
||||
|
@ -62,28 +62,28 @@ describe('HTML anchor element links to', () => {
|
|||
})
|
||||
|
||||
it('note anchor references render as anchor link', () => {
|
||||
cy.codemirrorFill('<a href="#anchor">anchor</a>')
|
||||
cy.setCodemirrorContent('<a href="#anchor">anchor</a>')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'http://127.0.0.1:3001/n/test#anchor')
|
||||
})
|
||||
|
||||
it('internal pages render as internal link', () => {
|
||||
cy.codemirrorFill('<a href="other-note">internal</a>')
|
||||
cy.setCodemirrorContent('<a href="other-note">internal</a>')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'http://127.0.0.1:3001/n/other-note')
|
||||
})
|
||||
|
||||
it('data URIs do not render', () => {
|
||||
cy.codemirrorFill('<a href="data:text/plain,evil">data</a>')
|
||||
cy.setCodemirrorContent('<a href="data:text/plain,evil">data</a>')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('javascript URIs do not render', () => {
|
||||
cy.codemirrorFill('<a href="javascript:alert(\'evil\')">js</a>')
|
||||
cy.setCodemirrorContent('<a href="javascript:alert(\'evil\')">js</a>')
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('not.exist')
|
||||
|
|
|
@ -20,22 +20,22 @@ describe('The status bar text length info', () => {
|
|||
})
|
||||
|
||||
it('color is set to "warning" on <= 100 characters remaining', () => {
|
||||
cy.codemirrorFill(warningTestContent)
|
||||
cy.setCodemirrorContent(warningTestContent)
|
||||
cy.get('.status-bar [data-cy="remainingCharacters"]')
|
||||
.should('have.class', 'text-warning')
|
||||
})
|
||||
|
||||
it('color is set to danger on <= 0 characters remaining', () => {
|
||||
cy.codemirrorFill(dangerTestContent)
|
||||
cy.setCodemirrorContent(dangerTestContent)
|
||||
cy.get('.status-bar [data-cy="remainingCharacters"]')
|
||||
.should('have.class', 'text-danger')
|
||||
})
|
||||
|
||||
it('shows a warning and opens a modal', () => {
|
||||
cy.codemirrorFill(tooMuchTestContent)
|
||||
cy.setCodemirrorContent(tooMuchTestContent)
|
||||
cy.get('[data-cy="limitReachedModal"]')
|
||||
.should('be.visible')
|
||||
cy.getMarkdownRenderer()
|
||||
cy.getIframeBody()
|
||||
.find('[data-cy="limitReachedMessage"]')
|
||||
.should('be.visible')
|
||||
})
|
||||
|
|
|
@ -11,7 +11,7 @@ describe('Quote extra tags', function () {
|
|||
|
||||
describe('Name quote tag', () => {
|
||||
it('renders correctly', () => {
|
||||
cy.codemirrorFill('[name=testy mctestface]')
|
||||
cy.setCodemirrorContent('[name=testy mctestface]')
|
||||
|
||||
cy.getMarkdownBody()
|
||||
.find('.quote-extra')
|
||||
|
@ -28,7 +28,7 @@ describe('Quote extra tags', function () {
|
|||
|
||||
describe('Time quote tag', () => {
|
||||
it('renders correctly', () => {
|
||||
cy.codemirrorFill(`[time=always]`)
|
||||
cy.setCodemirrorContent(`[time=always]`)
|
||||
|
||||
cy.getMarkdownBody()
|
||||
.find('.quote-extra')
|
||||
|
@ -45,7 +45,7 @@ describe('Quote extra tags', function () {
|
|||
|
||||
describe('Color quote tag', () => {
|
||||
it('renders correctly', () => {
|
||||
cy.codemirrorFill(`[color=#b51f08]`)
|
||||
cy.setCodemirrorContent(`[color=#b51f08]`)
|
||||
|
||||
cy.getMarkdownBody()
|
||||
.find('.quote-extra')
|
||||
|
@ -60,7 +60,7 @@ describe('Quote extra tags', function () {
|
|||
})
|
||||
|
||||
it('doesn\'t render in a blockquote and dyes the blockquote border', () => {
|
||||
cy.codemirrorFill(`> [color=#b51f08] HedgeDoc`)
|
||||
cy.setCodemirrorContent(`> [color=#b51f08] HedgeDoc`)
|
||||
|
||||
cy.getMarkdownBody()
|
||||
.find('.quote-extra')
|
||||
|
|
37
cypress/integration/renderer-mode.spec.ts
Normal file
37
cypress/integration/renderer-mode.spec.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
describe('Renderer mode', () => {
|
||||
beforeEach(() => {
|
||||
cy.visitTestEditor()
|
||||
})
|
||||
|
||||
it("should be 'document' without type specified", () => {
|
||||
cy.getMarkdownBody().should('exist')
|
||||
cy.getReveal().should('not.exist')
|
||||
})
|
||||
|
||||
it("should be 'reveal.js' with type 'slide'", () => {
|
||||
cy.setCodemirrorContent('---\ntype: slide\n---\n')
|
||||
cy.getMarkdownBody().should('not.exist')
|
||||
cy.getReveal().should('exist')
|
||||
})
|
||||
|
||||
it("should be 'document' with invalid type", () => {
|
||||
cy.setCodemirrorContent('---\ntype: EinDokument\n---\n')
|
||||
cy.getMarkdownBody().should('exist')
|
||||
cy.getReveal().should('not.exist')
|
||||
})
|
||||
|
||||
it("should change from 'reveal.js' to 'document' if changed from 'slide' to something else", () => {
|
||||
cy.setCodemirrorContent('---\ntype: slide\n---\n')
|
||||
cy.getMarkdownBody().should('not.exist')
|
||||
cy.getReveal().should('exist')
|
||||
cy.setCodemirrorContent('')
|
||||
cy.getMarkdownBody().should('exist')
|
||||
cy.getReveal().should('not.exist')
|
||||
})
|
||||
})
|
|
@ -11,7 +11,7 @@ describe('Short code gets replaced or rendered: ', () => {
|
|||
|
||||
describe('pdf', () => {
|
||||
it('renders a plain link', () => {
|
||||
cy.codemirrorFill(`{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}`)
|
||||
cy.setCodemirrorContent(`{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}`)
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf')
|
||||
|
@ -20,7 +20,7 @@ describe('Short code gets replaced or rendered: ', () => {
|
|||
|
||||
describe('slideshare', () => {
|
||||
it('renders a plain link', () => {
|
||||
cy.codemirrorFill(`{%slideshare example/123456789 %}`)
|
||||
cy.setCodemirrorContent(`{%slideshare example/123456789 %}`)
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'https://www.slideshare.net/example/123456789')
|
||||
|
@ -29,7 +29,7 @@ describe('Short code gets replaced or rendered: ', () => {
|
|||
|
||||
describe('speakerdeck', () => {
|
||||
it('renders a plain link', () => {
|
||||
cy.codemirrorFill(`{%speakerdeck example/123456789 %}`)
|
||||
cy.setCodemirrorContent(`{%speakerdeck example/123456789 %}`)
|
||||
cy.getMarkdownBody()
|
||||
.find('a')
|
||||
.should('have.attr', 'href', 'https://speakerdeck.com/example/123456789')
|
||||
|
@ -38,7 +38,7 @@ describe('Short code gets replaced or rendered: ', () => {
|
|||
|
||||
describe('youtube', () => {
|
||||
it('renders one-click-embedding', () => {
|
||||
cy.codemirrorFill(`{%youtube YE7VzlLtp-4 %}`)
|
||||
cy.setCodemirrorContent(`{%youtube YE7VzlLtp-4 %}`)
|
||||
cy.getMarkdownBody()
|
||||
.find('.one-click-embedding.embed-responsive-item')
|
||||
})
|
||||
|
|
|
@ -16,21 +16,21 @@ describe('Task lists ', () => {
|
|||
|
||||
describe('render with checkboxes ', () => {
|
||||
it('when unchecked', () => {
|
||||
cy.codemirrorFill(TEST_STRING_UNCHECKED)
|
||||
cy.setCodemirrorContent(TEST_STRING_UNCHECKED)
|
||||
cy.getMarkdownBody()
|
||||
.find('input[type=checkbox]')
|
||||
.should('have.length', 6)
|
||||
})
|
||||
|
||||
it('when checked lowercase', () => {
|
||||
cy.codemirrorFill(TEST_STRING_CHECKED_LOWER)
|
||||
cy.setCodemirrorContent(TEST_STRING_CHECKED_LOWER)
|
||||
cy.getMarkdownBody()
|
||||
.find('input[type=checkbox]')
|
||||
.should('have.length', 6)
|
||||
})
|
||||
|
||||
it('when checked uppercase', () => {
|
||||
cy.codemirrorFill(TEST_STRING_CHECKED_UPPER)
|
||||
cy.setCodemirrorContent(TEST_STRING_CHECKED_UPPER)
|
||||
cy.getMarkdownBody()
|
||||
.find('input[type=checkbox]')
|
||||
.should('have.length', 6)
|
||||
|
@ -38,7 +38,7 @@ describe('Task lists ', () => {
|
|||
})
|
||||
|
||||
it('do not render as checkboxes when invalid', () => {
|
||||
cy.codemirrorFill(TEST_STRING_INVALID)
|
||||
cy.setCodemirrorContent(TEST_STRING_INVALID)
|
||||
cy.getMarkdownBody()
|
||||
.find('input[type=checkbox]')
|
||||
.should('have.length', 0)
|
||||
|
@ -46,7 +46,7 @@ describe('Task lists ', () => {
|
|||
|
||||
describe('are clickable and change the markdown source ', () => {
|
||||
it('from unchecked to checked', () => {
|
||||
cy.codemirrorFill(TEST_STRING_UNCHECKED)
|
||||
cy.setCodemirrorContent(TEST_STRING_UNCHECKED)
|
||||
cy.getMarkdownBody()
|
||||
.find('input[type=checkbox]')
|
||||
.each(box => {
|
||||
|
@ -59,7 +59,7 @@ describe('Task lists ', () => {
|
|||
})
|
||||
|
||||
it('from checked (lowercase) to unchecked', () => {
|
||||
cy.codemirrorFill(TEST_STRING_CHECKED_LOWER)
|
||||
cy.setCodemirrorContent(TEST_STRING_CHECKED_LOWER)
|
||||
cy.getMarkdownBody()
|
||||
.find('input[type=checkbox]')
|
||||
.each(box => {
|
||||
|
@ -72,7 +72,7 @@ describe('Task lists ', () => {
|
|||
})
|
||||
|
||||
it('from checked (uppercase) to unchecked', () => {
|
||||
cy.codemirrorFill(TEST_STRING_CHECKED_UPPER)
|
||||
cy.setCodemirrorContent(TEST_STRING_CHECKED_UPPER)
|
||||
cy.getMarkdownBody()
|
||||
.find('input[type=checkbox]')
|
||||
.each(box => {
|
||||
|
|
|
@ -19,7 +19,7 @@ describe('Toolbar Buttons', () => {
|
|||
|
||||
describe('for single line text', () => {
|
||||
beforeEach(() => {
|
||||
cy.codemirrorFill(testText)
|
||||
cy.setCodemirrorContent(testText)
|
||||
cy.get('.CodeMirror-line > span')
|
||||
.should('exist')
|
||||
.should('have.text', testText)
|
||||
|
@ -182,7 +182,7 @@ describe('Toolbar Buttons', () => {
|
|||
|
||||
describe('for single line link with selection', () => {
|
||||
beforeEach(() => {
|
||||
cy.codemirrorFill(testLink)
|
||||
cy.setCodemirrorContent(testLink)
|
||||
cy.get('.CodeMirror-line > span')
|
||||
.should('exist')
|
||||
.should('have.text', testLink)
|
||||
|
|
|
@ -10,7 +10,7 @@ describe('Test word count with', () => {
|
|||
})
|
||||
|
||||
it('empty note', () => {
|
||||
cy.codemirrorFill('')
|
||||
cy.setCodemirrorContent('')
|
||||
cy.wait(500)
|
||||
cy.get('[data-cy="sidebar-btn-document-info"]').click()
|
||||
cy.get('[data-cy="document-info-modal"]').should('be.visible')
|
||||
|
@ -18,7 +18,7 @@ describe('Test word count with', () => {
|
|||
})
|
||||
|
||||
it('simple words', () => {
|
||||
cy.codemirrorFill('five words should be enough')
|
||||
cy.setCodemirrorContent('five words should be enough')
|
||||
cy.wait(500)
|
||||
cy.get('[data-cy="sidebar-btn-document-info"]').click()
|
||||
cy.get('[data-cy="document-info-modal"]').should('be.visible')
|
||||
|
@ -26,7 +26,7 @@ describe('Test word count with', () => {
|
|||
})
|
||||
|
||||
it('excluded codeblocks', () => {
|
||||
cy.codemirrorFill('```\nthis is should be ignored\n```\n\ntwo `words`')
|
||||
cy.setCodemirrorContent('```\nthis is should be ignored\n```\n\ntwo `words`')
|
||||
cy.wait(500)
|
||||
cy.get('[data-cy="sidebar-btn-document-info"]').click()
|
||||
cy.get('[data-cy="document-info-modal"]').should('be.visible')
|
||||
|
@ -34,7 +34,7 @@ describe('Test word count with', () => {
|
|||
})
|
||||
|
||||
it('excluded images', () => {
|
||||
cy.codemirrorFill(' not ignored text')
|
||||
cy.setCodemirrorContent(' not ignored text')
|
||||
cy.wait(500)
|
||||
cy.get('[data-cy="sidebar-btn-document-info"]').click()
|
||||
cy.get('[data-cy="document-info-modal"]').should('be.visible')
|
||||
|
|
|
@ -10,22 +10,22 @@ describe('YAML Array for deprecated syntax of document tags in frontmatter', ()
|
|||
})
|
||||
|
||||
it('is shown when using old syntax', () => {
|
||||
cy.codemirrorFill('---\ntags: a, b, c\n---')
|
||||
cy.getMarkdownRenderer()
|
||||
cy.setCodemirrorContent('---\ntags: a, b, c\n---')
|
||||
cy.getIframeBody()
|
||||
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('isn\'t shown when using inline yaml-array', () => {
|
||||
cy.codemirrorFill('---\ntags: [\'a\', \'b\', \'c\']\n---')
|
||||
cy.getMarkdownRenderer()
|
||||
cy.setCodemirrorContent('---\ntags: [\'a\', \'b\', \'c\']\n---')
|
||||
cy.getIframeBody()
|
||||
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('isn\'t shown when using multi line yaml-array', () => {
|
||||
cy.codemirrorFill('---\ntags:\n - a\n - b\n - c\n---')
|
||||
cy.getMarkdownRenderer()
|
||||
cy.setCodemirrorContent('---\ntags:\n - a\n - b\n - c\n---')
|
||||
cy.getIframeBody()
|
||||
.find('[data-cy="yamlArrayDeprecationAlert"]')
|
||||
.should('not.exist')
|
||||
})
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
|
@ -13,7 +12,7 @@ declare namespace Cypress {
|
|||
*/
|
||||
fill(value: string): Chainable<Element>
|
||||
|
||||
codemirrorFill(value: string): Chainable<Element>
|
||||
setCodemirrorContent(value: string): Chainable<Element>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,12 +24,14 @@ Cypress.Commands.add('fill', {
|
|||
.trigger('change', { force: true })
|
||||
})
|
||||
|
||||
Cypress.Commands.add('codemirrorFill', (content: string) => {
|
||||
Cypress.Commands.add('setCodemirrorContent', (content: string) => {
|
||||
const line = content.split('\n')
|
||||
.find(value => value !== '')
|
||||
cy.get('.CodeMirror')
|
||||
.click()
|
||||
.get('textarea')
|
||||
.type('{ctrl}a')
|
||||
.type('{backspace}')
|
||||
.fill(content)
|
||||
if (line) {
|
||||
cy.get('.CodeMirror')
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
getMarkdownRenderer(): Chainable<Element>
|
||||
|
||||
getIframeBody(): Chainable<Element>
|
||||
getReveal(): Chainable<Element>
|
||||
getMarkdownBody(): Chainable<Element>
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('getMarkdownRenderer', () => {
|
||||
Cypress.Commands.add('getIframeBody', () => {
|
||||
return cy
|
||||
.get(`iframe[data-cy="documentIframe"][data-content-ready="true"]`)
|
||||
.should('be.visible')
|
||||
|
@ -23,6 +23,10 @@ Cypress.Commands.add('getMarkdownRenderer', () => {
|
|||
.then(cy.wrap.bind(cy))
|
||||
})
|
||||
|
||||
Cypress.Commands.add('getMarkdownBody', () => {
|
||||
return cy.getMarkdownRenderer().find('.markdown-body')
|
||||
Cypress.Commands.add('getReveal', () => {
|
||||
return cy.getIframeBody().find('.reveal')
|
||||
})
|
||||
|
||||
Cypress.Commands.add('getMarkdownBody', () => {
|
||||
return cy.getIframeBody().find('.markdown-body')
|
||||
})
|
|
@ -24,6 +24,6 @@ import 'cypress-file-upload'
|
|||
import './checkLinks'
|
||||
import './config'
|
||||
import './fill'
|
||||
import './getMarkdownRenderer'
|
||||
import './get-iframe-content'
|
||||
import './login'
|
||||
import './visit-test-editor'
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
"react-scripts": "4.0.3",
|
||||
"react-use": "17.3.1",
|
||||
"redux": "4.1.1",
|
||||
"reveal.js": "4.1.3",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"ts-mockery": "1.2.0",
|
||||
"twemoji-colr-font": "0.0.4",
|
||||
|
|
|
@ -9,6 +9,17 @@
|
|||
"Community Call"
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "slide-example",
|
||||
"title": "Slide example",
|
||||
"lastVisited": "2020-05-30T15:20:36.088Z",
|
||||
"pinStatus": true,
|
||||
"tags": [
|
||||
"features",
|
||||
"cool",
|
||||
"updated"
|
||||
]
|
||||
},
|
||||
{
|
||||
"identifier": "features",
|
||||
"title": "Features",
|
||||
|
|
18
public/mock-backend/api/private/notes/slide-example-get
Normal file
18
public/mock-backend/api/private/notes/slide-example-get
Normal file
File diff suppressed because one or more lines are too long
|
@ -8,87 +8,87 @@ import { extractFrontmatter } from './extract-frontmatter'
|
|||
import { PresentFrontmatterExtractionResult } from './types'
|
||||
|
||||
describe('frontmatter extraction', () => {
|
||||
describe('frontmatterPresent property', () => {
|
||||
describe('isPresent property', () => {
|
||||
it('is false when note does not contain three dashes at all', () => {
|
||||
const testNote = 'abcdef\nmore text'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(false)
|
||||
expect(extraction.isPresent).toBe(false)
|
||||
})
|
||||
it('is false when note does not start with three dashes', () => {
|
||||
const testNote = '\n---\nthis is not frontmatter'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(false)
|
||||
expect(extraction.isPresent).toBe(false)
|
||||
})
|
||||
it('is false when note start with less than three dashes', () => {
|
||||
const testNote = '--\nthis is not frontmatter'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(false)
|
||||
expect(extraction.isPresent).toBe(false)
|
||||
})
|
||||
it('is false when note starts with three dashes but contains other characters in the same line', () => {
|
||||
const testNote = '--- a\nthis is not frontmatter'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(false)
|
||||
expect(extraction.isPresent).toBe(false)
|
||||
})
|
||||
it('is false when note has no ending marker for frontmatter', () => {
|
||||
const testNote = '---\nthis is not frontmatter\nbecause\nthere is no\nend marker'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(false)
|
||||
expect(extraction.isPresent).toBe(false)
|
||||
})
|
||||
it('is false when note end marker is present but with not the same amount of dashes as start marker', () => {
|
||||
const testNote = '---\nthis is not frontmatter\n----\ncontent'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(false)
|
||||
expect(extraction.isPresent).toBe(false)
|
||||
})
|
||||
it('is true when note end marker is present with the same amount of dashes as start marker', () => {
|
||||
const testNote = '---\nthis is frontmatter\n---\ncontent'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(true)
|
||||
expect(extraction.isPresent).toBe(true)
|
||||
})
|
||||
it('is true when note end marker is present with the same amount of dashes as start marker but without content', () => {
|
||||
const testNote = '---\nthis is frontmatter\n---'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(true)
|
||||
expect(extraction.isPresent).toBe(true)
|
||||
})
|
||||
it('is true when note end marker is present with the same amount of dots as start marker', () => {
|
||||
const testNote = '---\nthis is frontmatter\n...\ncontent'
|
||||
const extraction = extractFrontmatter(testNote)
|
||||
expect(extraction.frontmatterPresent).toBe(true)
|
||||
expect(extraction.isPresent).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('frontmatterLines property', () => {
|
||||
describe('lineOffset property', () => {
|
||||
it('is correct for single line frontmatter without content', () => {
|
||||
const testNote = '---\nsingle line frontmatter\n...'
|
||||
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
|
||||
expect(extraction.frontmatterLines).toEqual(3)
|
||||
expect(extraction.lineOffset).toEqual(3)
|
||||
})
|
||||
it('is correct for single line frontmatter with content', () => {
|
||||
const testNote = '---\nsingle line frontmatter\n...\ncontent'
|
||||
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
|
||||
expect(extraction.frontmatterLines).toEqual(3)
|
||||
expect(extraction.lineOffset).toEqual(3)
|
||||
})
|
||||
it('is correct for multi-line frontmatter without content', () => {
|
||||
const testNote = '---\nabc\n123\ndef\n...'
|
||||
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
|
||||
expect(extraction.frontmatterLines).toEqual(5)
|
||||
expect(extraction.lineOffset).toEqual(5)
|
||||
})
|
||||
it('is correct for multi-line frontmatter with content', () => {
|
||||
const testNote = '---\nabc\n123\ndef\n...\ncontent'
|
||||
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
|
||||
expect(extraction.frontmatterLines).toEqual(5)
|
||||
expect(extraction.lineOffset).toEqual(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rawFrontmatterText property', () => {
|
||||
describe('rawText property', () => {
|
||||
it('contains single-line frontmatter text', () => {
|
||||
const testNote = '---\nsingle-line\n...\ncontent'
|
||||
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
|
||||
expect(extraction.rawFrontmatterText).toEqual('single-line')
|
||||
expect(extraction.rawText).toEqual('single-line')
|
||||
})
|
||||
it('contains multi-line frontmatter text', () => {
|
||||
const testNote = '---\nmulti\nline\n...\ncontent'
|
||||
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
|
||||
expect(extraction.rawFrontmatterText).toEqual('multi\nline')
|
||||
expect(extraction.rawText).toEqual('multi\nline')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -13,7 +13,7 @@ const FRONTMATTER_END_REGEX = /^(?:-{3,}|\.{3,})$/
|
|||
* A valid frontmatter block requires the content to start with a line containing at least three dashes.
|
||||
* The block is terminated by a line containing the same amount of dashes or dots as the first line.
|
||||
* @param content The multiline string from which the frontmatter should be extracted.
|
||||
* @return { frontmatterPresent } false if no frontmatter block could be found, true if a block was found.
|
||||
* @return { isPresent } false if no frontmatter block could be found, true if a block was found.
|
||||
* { rawFrontmatterText } if a block was found, this property contains the extracted text without the fencing.
|
||||
* { frontmatterLines } if a block was found, this property contains the number of lines to skip from the
|
||||
* given multiline string for retrieving the non-frontmatter content.
|
||||
|
@ -22,19 +22,19 @@ export const extractFrontmatter = (content: string): FrontmatterExtractionResult
|
|||
const lines = content.split('\n')
|
||||
if (lines.length < 2 || !FRONTMATTER_BEGIN_REGEX.test(lines[0])) {
|
||||
return {
|
||||
frontmatterPresent: false
|
||||
isPresent: false
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (lines[i].length === lines[0].length && FRONTMATTER_END_REGEX.test(lines[i])) {
|
||||
return {
|
||||
frontmatterPresent: true,
|
||||
rawFrontmatterText: lines.slice(1, i).join('\n'),
|
||||
frontmatterLines: i + 1
|
||||
isPresent: true,
|
||||
rawText: lines.slice(1, i).join('\n'),
|
||||
lineOffset: i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
frontmatterPresent: false
|
||||
isPresent: false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,27 +4,27 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { NoteFrontmatter } from './note-frontmatter'
|
||||
import { createNoteFrontmatterFromYaml } from './note-frontmatter'
|
||||
|
||||
describe('yaml frontmatter', () => {
|
||||
it('should parse "title"', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml('title: test')
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml('title: test')
|
||||
expect(noteFrontmatter.title).toEqual('test')
|
||||
})
|
||||
|
||||
it('should parse "robots"', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml('robots: index, follow')
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml('robots: index, follow')
|
||||
expect(noteFrontmatter.robots).toEqual('index, follow')
|
||||
})
|
||||
|
||||
it('should parse the deprecated tags syntax', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml('tags: test123, abc')
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml('tags: test123, abc')
|
||||
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
||||
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(true)
|
||||
})
|
||||
|
||||
it('should parse the tags list syntax', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml(`tags:
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml(`tags:
|
||||
- test123
|
||||
- abc
|
||||
`)
|
||||
|
@ -33,30 +33,30 @@ describe('yaml frontmatter', () => {
|
|||
})
|
||||
|
||||
it('should parse the tag inline-list syntax', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml("tags: ['test123', 'abc']")
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml("tags: ['test123', 'abc']")
|
||||
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
||||
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
|
||||
})
|
||||
|
||||
it('should parse "breaks"', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml('breaks: false')
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml('breaks: false')
|
||||
expect(noteFrontmatter.breaks).toEqual(false)
|
||||
})
|
||||
|
||||
it('should parse an empty opengraph object', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml('opengraph:')
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml('opengraph:')
|
||||
expect(noteFrontmatter.opengraph).toEqual(new Map<string, string>())
|
||||
})
|
||||
|
||||
it('should parse an opengraph title', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml(`opengraph:
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml(`opengraph:
|
||||
title: Testtitle
|
||||
`)
|
||||
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
|
||||
})
|
||||
|
||||
it('should parse multiple opengraph values', () => {
|
||||
const noteFrontmatter = NoteFrontmatter.createFromYaml(`opengraph:
|
||||
const noteFrontmatter = createNoteFrontmatterFromYaml(`opengraph:
|
||||
title: Testtitle
|
||||
image: https://dummyimage.com/48.png
|
||||
image:type: image/png
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
|
||||
// import { RevealOptions } from 'reveal.js'
|
||||
import { load } from 'js-yaml'
|
||||
import { ISO6391, NoteTextDirection, NoteType, RawNoteFrontmatter } from './types'
|
||||
import { ISO6391, NoteTextDirection, NoteType, RawNoteFrontmatter, SlideOptions } from './types'
|
||||
import { initialSlideOptions } from '../../../redux/note-details/initial-state'
|
||||
|
||||
/**
|
||||
* Class that represents the parsed frontmatter metadata of a note.
|
||||
*/
|
||||
export class NoteFrontmatter {
|
||||
export interface NoteFrontmatter {
|
||||
title: string
|
||||
description: string
|
||||
tags: string[]
|
||||
|
@ -24,47 +25,98 @@ export class NoteFrontmatter {
|
|||
disqus: string
|
||||
type: NoteType
|
||||
opengraph: Map<string, string>
|
||||
slideOptions: SlideOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new frontmatter metadata instance based on the given raw metadata properties.
|
||||
* @param rawData A {@link RawNoteFrontmatter} object containing the properties of the parsed yaml frontmatter.
|
||||
*/
|
||||
constructor(rawData: RawNoteFrontmatter) {
|
||||
this.title = rawData.title ?? ''
|
||||
this.description = rawData.description ?? ''
|
||||
this.robots = rawData.robots ?? ''
|
||||
this.breaks = rawData.breaks ?? true
|
||||
this.GA = rawData.GA ?? ''
|
||||
this.disqus = rawData.disqus ?? ''
|
||||
this.lang = (rawData.lang ? ISO6391.find((lang) => lang === rawData.lang) : undefined) ?? 'en'
|
||||
this.type =
|
||||
(rawData.type ? Object.values(NoteType).find((type) => type === rawData.type) : undefined) ?? NoteType.DOCUMENT
|
||||
this.dir =
|
||||
(rawData.dir ? Object.values(NoteTextDirection).find((dir) => dir === rawData.dir) : undefined) ??
|
||||
NoteTextDirection.LTR
|
||||
if (typeof rawData?.tags === 'string') {
|
||||
this.tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
|
||||
this.deprecatedTagsSyntax = true
|
||||
} else if (typeof rawData?.tags === 'object') {
|
||||
this.tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
|
||||
this.deprecatedTagsSyntax = false
|
||||
} else {
|
||||
this.tags = []
|
||||
this.deprecatedTagsSyntax = false
|
||||
}
|
||||
this.opengraph = rawData?.opengraph
|
||||
? new Map<string, string>(Object.entries(rawData.opengraph))
|
||||
: new Map<string, string>()
|
||||
/**
|
||||
* Creates a new frontmatter metadata instance based on the given raw metadata properties.
|
||||
* @param rawData A {@link RawNoteFrontmatter} object containing the properties of the parsed yaml frontmatter.
|
||||
*/
|
||||
export const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter => {
|
||||
let tags: string[]
|
||||
let deprecatedTagsSyntax: boolean
|
||||
if (typeof rawData?.tags === 'string') {
|
||||
tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
|
||||
deprecatedTagsSyntax = true
|
||||
} else if (typeof rawData?.tags === 'object') {
|
||||
tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
|
||||
deprecatedTagsSyntax = false
|
||||
} else {
|
||||
tags = []
|
||||
deprecatedTagsSyntax = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new frontmatter metadata instance based on a raw yaml string.
|
||||
* @param rawYaml The frontmatter content in yaml format.
|
||||
* @throws Error when the content string is invalid yaml.
|
||||
* @return Frontmatter metadata instance containing the parsed properties from the yaml content.
|
||||
*/
|
||||
static createFromYaml(rawYaml: string): NoteFrontmatter {
|
||||
const rawNoteFrontmatter = load(rawYaml) as RawNoteFrontmatter
|
||||
return new NoteFrontmatter(rawNoteFrontmatter)
|
||||
return {
|
||||
title: rawData.title ?? '',
|
||||
description: rawData.description ?? '',
|
||||
robots: rawData.robots ?? '',
|
||||
breaks: rawData.breaks ?? true,
|
||||
GA: rawData.GA ?? '',
|
||||
disqus: rawData.disqus ?? '',
|
||||
lang: (rawData.lang ? ISO6391.find((lang) => lang === rawData.lang) : undefined) ?? 'en',
|
||||
type:
|
||||
(rawData.type ? Object.values(NoteType).find((type) => type === rawData.type) : undefined) ?? NoteType.DOCUMENT,
|
||||
dir:
|
||||
(rawData.dir ? Object.values(NoteTextDirection).find((dir) => dir === rawData.dir) : undefined) ??
|
||||
NoteTextDirection.LTR,
|
||||
opengraph: rawData?.opengraph
|
||||
? new Map<string, string>(Object.entries(rawData.opengraph))
|
||||
: new Map<string, string>(),
|
||||
|
||||
slideOptions: parseSlideOptions(rawData),
|
||||
tags,
|
||||
deprecatedTagsSyntax
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link SlideOptions} from the {@link RawNoteFrontmatter}.
|
||||
*
|
||||
* @param rawData The raw note frontmatter data.
|
||||
* @return the parsed slide options
|
||||
*/
|
||||
const parseSlideOptions = (rawData: RawNoteFrontmatter): SlideOptions => {
|
||||
const rawSlideOptions = rawData?.slideOptions
|
||||
return {
|
||||
autoSlide: parseNumber(rawSlideOptions?.autoSlide) ?? initialSlideOptions.autoSlide,
|
||||
transition: rawSlideOptions?.transition ?? initialSlideOptions.transition,
|
||||
backgroundTransition: rawSlideOptions?.backgroundTransition ?? initialSlideOptions.backgroundTransition,
|
||||
autoSlideStoppable: parseBoolean(rawSlideOptions?.autoSlideStoppable) ?? initialSlideOptions.autoSlideStoppable,
|
||||
slideNumber: parseBoolean(rawSlideOptions?.slideNumber) ?? initialSlideOptions.slideNumber
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an unknown variable into a boolean.
|
||||
*
|
||||
* @param rawData The raw data
|
||||
* @return The parsed boolean or undefined if it's not possible to parse the data.
|
||||
*/
|
||||
const parseBoolean = (rawData: unknown | undefined): boolean | undefined => {
|
||||
return rawData === undefined ? undefined : rawData === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an unknown variable into a number.
|
||||
*
|
||||
* @param rawData The raw data
|
||||
* @return The parsed number or undefined if it's not possible to parse the data.
|
||||
*/
|
||||
const parseNumber = (rawData: unknown | undefined): number | undefined => {
|
||||
if (rawData === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const numValue = Number(rawData)
|
||||
return isNaN(numValue) ? undefined : numValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new frontmatter metadata instance based on a raw yaml string.
|
||||
* @param rawYaml The frontmatter content in yaml format.
|
||||
* @throws Error when the content string is invalid yaml.
|
||||
* @return Frontmatter metadata instance containing the parsed properties from the yaml content.
|
||||
*/
|
||||
export const createNoteFrontmatterFromYaml = (rawYaml: string): NoteFrontmatter => {
|
||||
const rawNoteFrontmatter = load(rawYaml) as RawNoteFrontmatter
|
||||
return parseRawNoteFrontmatter(rawNoteFrontmatter)
|
||||
}
|
||||
|
|
|
@ -4,22 +4,33 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { RevealOptions } from 'reveal.js'
|
||||
|
||||
export type FrontmatterExtractionResult = PresentFrontmatterExtractionResult | NonPresentFrontmatterExtractionResult
|
||||
|
||||
export type WantedRevealOptions =
|
||||
| 'autoSlide'
|
||||
| 'autoSlideStoppable'
|
||||
| 'transition'
|
||||
| 'backgroundTransition'
|
||||
| 'slideNumber'
|
||||
export type SlideOptions = Required<Pick<RevealOptions, WantedRevealOptions>>
|
||||
|
||||
export interface RendererFrontmatterInfo {
|
||||
offsetLines: number
|
||||
lineOffset: number
|
||||
frontmatterInvalid: boolean
|
||||
deprecatedSyntax: boolean
|
||||
slideOptions: SlideOptions
|
||||
}
|
||||
|
||||
export interface PresentFrontmatterExtractionResult {
|
||||
frontmatterPresent: true
|
||||
rawFrontmatterText: string
|
||||
frontmatterLines: number
|
||||
isPresent: true
|
||||
rawText: string
|
||||
lineOffset: number
|
||||
}
|
||||
|
||||
interface NonPresentFrontmatterExtractionResult {
|
||||
frontmatterPresent: false
|
||||
isPresent: false
|
||||
}
|
||||
|
||||
export interface RawNoteFrontmatter {
|
||||
|
@ -33,7 +44,7 @@ export interface RawNoteFrontmatter {
|
|||
GA: string | undefined
|
||||
disqus: string | undefined
|
||||
type: string | undefined
|
||||
slideOptions: unknown
|
||||
slideOptions: { [key: string]: string } | null
|
||||
opengraph: { [key: string]: string } | null
|
||||
}
|
||||
|
||||
|
|
|
@ -50,21 +50,7 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
|||
<LoadingNoteAlert show={loading} />
|
||||
</div>
|
||||
<ShowIf condition={!error && !loading}>
|
||||
<DocumentInfobar
|
||||
changedAuthor={noteDetails.lastChange.userName ?? ''}
|
||||
changedTime={noteDetails.lastChange.timestamp}
|
||||
createdAuthor={'Test'}
|
||||
createdTime={noteDetails.createTime}
|
||||
editable={true}
|
||||
noteId={id}
|
||||
viewCount={noteDetails.viewCount}
|
||||
/>
|
||||
<RenderIframe
|
||||
frameClasses={'flex-fill h-100 w-100'}
|
||||
markdownContent={markdownContent}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
rendererType={RendererType.DOCUMENT}
|
||||
/>
|
||||
<DocumentReadOnlyPageContent />
|
||||
</ShowIf>
|
||||
</div>
|
||||
</EditorToRendererCommunicatorContextProvider>
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface CheatsheetLineProps {
|
|||
const HighlightedCode = React.lazy(
|
||||
() => import('../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code')
|
||||
)
|
||||
const BasicMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/basic-markdown-renderer'))
|
||||
const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer'))
|
||||
|
||||
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ code, onTaskCheckedChange }) => {
|
||||
const checkboxClick = useCallback(
|
||||
|
@ -36,7 +36,11 @@ export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ code, onTaskChec
|
|||
}>
|
||||
<tr>
|
||||
<td>
|
||||
<BasicMarkdownRenderer content={code} baseUrl={'https://example.org'} onTaskCheckedChange={checkboxClick} />
|
||||
<DocumentMarkdownRenderer
|
||||
content={code}
|
||||
baseUrl={'https://example.org'}
|
||||
onTaskCheckedChange={checkboxClick}
|
||||
/>
|
||||
</td>
|
||||
<td className={'markdown-body'}>
|
||||
<HighlightedCode code={code} wrapLines={true} startLineNumber={1} language={'markdown'} />
|
||||
|
|
|
@ -21,5 +21,5 @@ export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = (pr
|
|||
|
||||
useSendFrontmatterInfoFromReduxToRenderer()
|
||||
|
||||
return <RenderIframe frameClasses={'h-100 w-100'} markdownContent={markdownContent} {...props} />
|
||||
return <RenderIframe markdownContent={markdownContent} {...props} />
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { useApplicationState } from '../../hooks/common/use-application-state'
|
|||
import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer'
|
||||
import { EditorToRendererCommunicatorContextProvider } from './render-context/editor-to-renderer-communicator-context-provider'
|
||||
import { Logger } from '../../utils/logger'
|
||||
import { NoteType } from '../common/note-frontmatter/types'
|
||||
|
||||
export interface EditorPagePathParams {
|
||||
id: string
|
||||
|
@ -107,6 +108,7 @@ export const EditorPage: React.FC = () => {
|
|||
),
|
||||
[onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
|
||||
)
|
||||
const noteType: NoteType = useApplicationState((state) => state.noteDetails.frontmatter.type)
|
||||
|
||||
const rightPane = useMemo(
|
||||
() => (
|
||||
|
@ -117,10 +119,10 @@ export const EditorPage: React.FC = () => {
|
|||
onTaskCheckedChange={setCheckboxInMarkdownContent}
|
||||
onScroll={onMarkdownRendererScroll}
|
||||
scrollState={scrollState.rendererScrollState}
|
||||
rendererType={RendererType.DOCUMENT}
|
||||
rendererType={noteType === NoteType.SLIDE ? RendererType.SLIDESHOW : RendererType.DOCUMENT}
|
||||
/>
|
||||
),
|
||||
[onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
|
||||
[noteType, onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
||||
|
||||
/**
|
||||
* Execute the given reload callback if the given render type changes.
|
||||
*
|
||||
* @param rendererType The render type to watch
|
||||
* @param effectCallback The callback that should be executed if the render type changes.
|
||||
*/
|
||||
export const useEffectOnRenderTypeChange = (rendererType: RendererType, effectCallback: () => void): void => {
|
||||
const lastRendererType = useRef<RendererType>(rendererType)
|
||||
|
||||
useEffect(() => {
|
||||
if (lastRendererType.current === rendererType) {
|
||||
return
|
||||
}
|
||||
effectCallback()
|
||||
lastRendererType.current = rendererType
|
||||
}, [effectCallback, rendererType])
|
||||
}
|
|
@ -5,9 +5,11 @@
|
|||
*/
|
||||
|
||||
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import { RendererFrontmatterInfo } from '../../../common/note-frontmatter/types'
|
||||
import equal from 'fast-deep-equal'
|
||||
|
||||
/**
|
||||
* Extracts the {@link RendererFrontmatterInfo frontmatter data}
|
||||
|
@ -15,14 +17,24 @@ import { useApplicationState } from '../../../../hooks/common/use-application-st
|
|||
*/
|
||||
export const useSendFrontmatterInfoFromReduxToRenderer = (): void => {
|
||||
const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo)
|
||||
const lastFrontmatter = useRef<RendererFrontmatterInfo | undefined>(undefined)
|
||||
|
||||
const cachedFrontmatterInfo = useMemo(() => {
|
||||
if (lastFrontmatter.current !== undefined && equal(lastFrontmatter.current, frontmatterInfo)) {
|
||||
return lastFrontmatter.current
|
||||
} else {
|
||||
lastFrontmatter.current = frontmatterInfo
|
||||
return frontmatterInfo
|
||||
}
|
||||
}, [frontmatterInfo])
|
||||
|
||||
return useSendToRenderer(
|
||||
useMemo(
|
||||
() => ({
|
||||
type: CommunicationMessageType.SET_FRONTMATTER_INFO,
|
||||
frontmatterInfo
|
||||
frontmatterInfo: cachedFrontmatterInfo
|
||||
}),
|
||||
[frontmatterInfo]
|
||||
[cachedFrontmatterInfo]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import { useSendMarkdownToRenderer } from './hooks/use-send-markdown-to-renderer
|
|||
import { useSendScrollState } from './hooks/use-send-scroll-state'
|
||||
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
|
||||
|
||||
export interface RenderIframeProps extends RendererProps {
|
||||
rendererType: RendererType
|
||||
|
@ -50,9 +51,8 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
const iframeCommunicator = useEditorToRendererCommunicator()
|
||||
const resetRendererReady = useCallback(() => {
|
||||
log.debug('Reset render status')
|
||||
iframeCommunicator.unsetMessageTarget()
|
||||
setRendererStatus(false)
|
||||
}, [iframeCommunicator])
|
||||
}, [])
|
||||
const rendererReady = useIsRendererReady()
|
||||
const onIframeLoad = useForceRenderPageUrlOnIframeLoadCallback(frameReference, rendererOrigin, resetRendererReady)
|
||||
const [frameHeight, setFrameHeight] = useState<number>(0)
|
||||
|
@ -65,6 +65,12 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
[iframeCommunicator]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!rendererReady) {
|
||||
iframeCommunicator.unsetMessageTarget()
|
||||
}
|
||||
}, [iframeCommunicator, rendererReady])
|
||||
|
||||
useEditorReceiveHandler(
|
||||
CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
|
||||
useCallback(
|
||||
|
@ -123,6 +129,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
}, [iframeCommunicator, rendererOrigin, rendererType])
|
||||
)
|
||||
|
||||
useEffectOnRenderTypeChange(rendererType, onIframeLoad)
|
||||
useSendScrollState(scrollState)
|
||||
useSendDarkModeStatusToRenderer(forcedDarkMode)
|
||||
useSendMarkdownToRenderer(markdownContent)
|
||||
|
@ -136,7 +143,9 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
|
|||
onLoad={onIframeLoad}
|
||||
title='render'
|
||||
{...(isTestMode() ? {} : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups' })}
|
||||
allowFullScreen={true}
|
||||
ref={frameReference}
|
||||
referrerPolicy={'no-referrer'}
|
||||
className={`border-0 ${frameClasses ?? ''}`}
|
||||
data-content-ready={rendererReady}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
||||
import { Ref } from 'react'
|
||||
|
||||
export interface CommonMarkdownRendererProps {
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast?: TocAst) => void
|
||||
baseUrl?: string
|
||||
onImageClick?: ImageClickHandler
|
||||
outerContainerRef?: Ref<HTMLDivElement>
|
||||
useAlternativeBreaks?: boolean
|
||||
lineOffset?: number
|
||||
className?: string
|
||||
content: string
|
||||
}
|
|
@ -4,12 +4,11 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Ref, useCallback, useMemo, useRef } from 'react'
|
||||
import React, { useMemo, useRef } from 'react'
|
||||
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
|
||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||
import './markdown-renderer.scss'
|
||||
import { ComponentReplacer } from './replace-components/ComponentReplacer'
|
||||
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||
import { LineMarkerPosition } from './types'
|
||||
import { useComponentReplacers } from './hooks/use-component-replacers'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
||||
|
@ -18,28 +17,16 @@ import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
|||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { useOnRefChange } from './hooks/use-on-ref-change'
|
||||
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/basic-markdown-it-configurator'
|
||||
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
||||
import { useTrimmedContent } from './hooks/use-trimmed-content'
|
||||
import { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||
|
||||
export interface BasicMarkdownRendererProps {
|
||||
additionalReplacers?: () => ComponentReplacer[]
|
||||
onBeforeRendering?: () => void
|
||||
onAfterRendering?: () => void
|
||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||
export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||
onTocChange?: (ast?: TocAst) => void
|
||||
baseUrl?: string
|
||||
onImageClick?: ImageClickHandler
|
||||
outerContainerRef?: Ref<HTMLDivElement>
|
||||
useAlternativeBreaks?: boolean
|
||||
frontmatterLineOffset?: number
|
||||
}
|
||||
|
||||
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
|
||||
export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> = ({
|
||||
className,
|
||||
content,
|
||||
additionalReplacers,
|
||||
onFirstHeadingChange,
|
||||
onLineMarkerPositionChanged,
|
||||
onTaskCheckedChange,
|
||||
|
@ -48,7 +35,7 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
|
|||
onImageClick,
|
||||
outerContainerRef,
|
||||
useAlternativeBreaks,
|
||||
frontmatterLineOffset
|
||||
lineOffset
|
||||
}) => {
|
||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||
|
@ -64,17 +51,12 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
|
|||
? undefined
|
||||
: (lineMarkers) => (currentLineMarkers.current = lineMarkers),
|
||||
useAlternativeBreaks,
|
||||
offsetLines: frontmatterLineOffset
|
||||
lineOffset,
|
||||
headlineAnchors: true
|
||||
}).buildConfiguredMarkdownIt(),
|
||||
[onLineMarkerPositionChanged, useAlternativeBreaks, frontmatterLineOffset]
|
||||
[onLineMarkerPositionChanged, useAlternativeBreaks, lineOffset]
|
||||
)
|
||||
|
||||
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, frontmatterLineOffset)
|
||||
const replacers = useCallback(
|
||||
() => baseReplacers().concat(additionalReplacers ? additionalReplacers() : []),
|
||||
[additionalReplacers, baseReplacers]
|
||||
)
|
||||
|
||||
const replacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, lineOffset)
|
||||
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, replacers)
|
||||
|
||||
useTranslation()
|
||||
|
@ -99,4 +81,4 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
|
|||
)
|
||||
}
|
||||
|
||||
export default BasicMarkdownRenderer
|
||||
export default DocumentMarkdownRenderer
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { AbcReplacer } from '../replace-components/abc/abc-replacer'
|
||||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
||||
|
@ -41,8 +41,8 @@ export const useComponentReplacers = (
|
|||
onImageClick?: ImageClickHandler,
|
||||
baseUrl?: string,
|
||||
frontmatterLinesToSkip?: number
|
||||
): (() => ComponentReplacer[]) =>
|
||||
useCallback(
|
||||
): ComponentReplacer[] =>
|
||||
useMemo(
|
||||
() => [
|
||||
new LinemarkerReplacer(),
|
||||
new GistReplacer(),
|
||||
|
|
|
@ -11,6 +11,7 @@ import { LineKeys } from '../types'
|
|||
import { buildTransformer } from '../utils/html-react-transformer'
|
||||
import { calculateNewLineNumberMapping } from '../utils/line-number-mapping'
|
||||
import convertHtmlToReact from '@hedgedoc/html-to-react'
|
||||
import { Document } from 'domhandler'
|
||||
|
||||
/**
|
||||
* Renders markdown code into react elements
|
||||
|
@ -18,24 +19,19 @@ import convertHtmlToReact from '@hedgedoc/html-to-react'
|
|||
* @param markdownCode The markdown code that should be rendered
|
||||
* @param markdownIt The configured {@link MarkdownIt markdown it} instance that should render the code
|
||||
* @param replacers A function that provides a list of {@link ComponentReplacer component replacers}
|
||||
* @param onBeforeRendering A callback that gets executed before the rendering
|
||||
* @param onAfterRendering A callback that gets executed after the rendering
|
||||
* @param preprocessNodes A function that processes nodes after parsing the html code that is generated by markdown it.
|
||||
* @return The React DOM that represents the rendered markdown code
|
||||
*/
|
||||
export const useConvertMarkdownToReactDom = (
|
||||
markdownCode: string,
|
||||
markdownIt: MarkdownIt,
|
||||
replacers: () => ComponentReplacer[],
|
||||
onBeforeRendering?: () => void,
|
||||
onAfterRendering?: () => void
|
||||
replacers: ComponentReplacer[],
|
||||
preprocessNodes?: (nodes: Document) => Document
|
||||
): ValidReactDomElement[] => {
|
||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||
const lastUsedLineId = useRef<number>(0)
|
||||
|
||||
return useMemo(() => {
|
||||
if (onBeforeRendering) {
|
||||
onBeforeRendering()
|
||||
}
|
||||
const html = markdownIt.render(markdownCode)
|
||||
const contentLines = markdownCode.split('\n')
|
||||
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(
|
||||
|
@ -46,12 +42,7 @@ export const useConvertMarkdownToReactDom = (
|
|||
oldMarkdownLineKeys.current = newLines
|
||||
lastUsedLineId.current = newLastUsedLineId
|
||||
|
||||
const currentReplacers = replacers()
|
||||
const transformer = currentReplacers.length > 0 ? buildTransformer(newLines, currentReplacers) : undefined
|
||||
const rendering = convertHtmlToReact(html, { transform: transformer })
|
||||
if (onAfterRendering) {
|
||||
onAfterRendering()
|
||||
}
|
||||
return rendering
|
||||
}, [onBeforeRendering, markdownIt, markdownCode, replacers, onAfterRendering])
|
||||
const transformer = replacers.length > 0 ? buildTransformer(newLines, replacers) : undefined
|
||||
return convertHtmlToReact(html, { transform: transformer, preprocessNodes: preprocessNodes })
|
||||
}, [markdownIt, markdownCode, replacers, preprocessNodes])
|
||||
}
|
||||
|
|
53
src/components/markdown-renderer/hooks/use-reveal.ts
Normal file
53
src/components/markdown-renderer/hooks/use-reveal.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Reveal from 'reveal.js'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
import { SlideOptions } from '../../common/note-frontmatter/types'
|
||||
|
||||
const log = new Logger('reveal.js')
|
||||
|
||||
export const useReveal = (content: string, slideOptions?: SlideOptions): void => {
|
||||
const [deck, setDeck] = useState<Reveal>()
|
||||
const [isInitialized, setIsInitialized] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
return
|
||||
}
|
||||
setIsInitialized(true)
|
||||
log.debug('Initialize with slide options', slideOptions)
|
||||
const reveal = new Reveal({})
|
||||
reveal
|
||||
.initialize()
|
||||
.then(() => {
|
||||
reveal.layout()
|
||||
reveal.slide(0, 0, 0)
|
||||
setDeck(reveal)
|
||||
log.debug('Initialisation finished')
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error('Error while initializing reveal.js', error)
|
||||
})
|
||||
}, [isInitialized, slideOptions])
|
||||
|
||||
useEffect(() => {
|
||||
if (!deck) {
|
||||
return
|
||||
}
|
||||
log.debug('Sync deck')
|
||||
deck.layout()
|
||||
}, [content, deck])
|
||||
|
||||
useEffect(() => {
|
||||
if (!deck || slideOptions === undefined || Object.keys(slideOptions).length === 0) {
|
||||
return
|
||||
}
|
||||
log.debug('Apply config', slideOptions)
|
||||
deck.configure(slideOptions)
|
||||
}, [deck, slideOptions])
|
||||
}
|
|
@ -35,12 +35,15 @@ import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
|
|||
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
|
||||
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
|
||||
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
|
||||
import { addSlideSectionsMarkdownItPlugin } from '../markdown-it-plugins/reveal-sections'
|
||||
|
||||
export interface ConfiguratorDetails {
|
||||
onToc: (toc: TocAst) => void
|
||||
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
||||
useAlternativeBreaks?: boolean
|
||||
offsetLines?: number
|
||||
lineOffset?: number
|
||||
headlineAnchors?: boolean
|
||||
slideSections?: boolean
|
||||
}
|
||||
|
||||
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
||||
|
@ -73,7 +76,6 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
|||
protected configure(markdownIt: MarkdownIt): void {
|
||||
this.configurations.push(
|
||||
plantumlWithError,
|
||||
headlineAnchors,
|
||||
KatexReplacer.markdownItPlugin,
|
||||
YoutubeReplacer.markdownItPlugin,
|
||||
VimeoReplacer.markdownItPlugin,
|
||||
|
@ -101,8 +103,16 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
|||
spoilerContainer
|
||||
)
|
||||
|
||||
if (this.options.headlineAnchors) {
|
||||
this.configurations.push(headlineAnchors)
|
||||
}
|
||||
|
||||
if (this.options.slideSections) {
|
||||
this.configurations.push(addSlideSectionsMarkdownItPlugin)
|
||||
}
|
||||
|
||||
if (this.options.onLineMarkers) {
|
||||
this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.offsetLines ?? 0))
|
||||
this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.lineOffset ?? 0))
|
||||
}
|
||||
|
||||
this.postConfigurations.push(linkifyExtra, MarkdownItParserDebugger)
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import Token from 'markdown-it/lib/token'
|
||||
import StateCore from 'markdown-it/lib/rules_core/state_core'
|
||||
|
||||
/**
|
||||
* This functions adds a 'section close' token at currentTokenIndex in the state's token array,
|
||||
* replacing the current token, if replaceCurrentToken is true.
|
||||
* It also returns the currentTokenIndex, that will be increased only if the previous token was not replaced.
|
||||
*
|
||||
* @param {number} currentTokenIndex - the current position in the tokens array
|
||||
* @param {StateCore} state - the state core
|
||||
* @param {boolean} replaceCurrentToken - if the currentToken should be replaced
|
||||
*/
|
||||
const addSectionClose = (currentTokenIndex: number, state: StateCore, replaceCurrentToken: boolean): void => {
|
||||
const sectionCloseToken = new Token('section', 'section', -1)
|
||||
state.tokens.splice(currentTokenIndex, replaceCurrentToken ? 1 : 0, sectionCloseToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* This functions adds a 'section open' token at insertIndex in the state's token array.
|
||||
*
|
||||
* @param {number} insertIndex - the index at which the token should be added
|
||||
* @param {StateCore} state - the state core
|
||||
*/
|
||||
const addSectionOpen = (insertIndex: number, state: StateCore): void => {
|
||||
const sectionOpenToken = new Token('section', 'section', 1)
|
||||
state.tokens.splice(insertIndex, 0, sectionOpenToken)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a plugin to the given {@link MarkdownIt markdown it instance} that
|
||||
* replaces splits the content by horizontal lines and groups these blocks into
|
||||
* html section tags.
|
||||
*
|
||||
* @param markdownIt The {@link MarkdownIt markdown it instance} to which the plugin should be added
|
||||
*/
|
||||
export const addSlideSectionsMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt): void => {
|
||||
markdownIt.core.ruler.push('reveal.sections', (state) => {
|
||||
let sectionBeginIndex = 0
|
||||
let lastSectionWasBranch = false
|
||||
|
||||
for (let currentTokenIndex = 0; currentTokenIndex < state.tokens.length; currentTokenIndex++) {
|
||||
const currentToken = state.tokens[currentTokenIndex]
|
||||
|
||||
if (currentToken.type !== 'hr') {
|
||||
continue
|
||||
}
|
||||
|
||||
addSectionOpen(sectionBeginIndex, state)
|
||||
currentTokenIndex += 1
|
||||
|
||||
if (currentToken.markup === '---' && lastSectionWasBranch) {
|
||||
lastSectionWasBranch = false
|
||||
addSectionClose(currentTokenIndex, state, false)
|
||||
currentTokenIndex += 1
|
||||
} else if (currentToken.markup === '----' && !lastSectionWasBranch) {
|
||||
lastSectionWasBranch = true
|
||||
addSectionOpen(sectionBeginIndex, state)
|
||||
currentTokenIndex += 1
|
||||
}
|
||||
|
||||
addSectionClose(currentTokenIndex, state, true)
|
||||
sectionBeginIndex = currentTokenIndex + 1
|
||||
}
|
||||
|
||||
addSectionOpen(sectionBeginIndex, state)
|
||||
addSectionClose(state.tokens.length, state, false)
|
||||
if (lastSectionWasBranch) {
|
||||
addSectionClose(state.tokens.length, state, false)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
107
src/components/markdown-renderer/process-reveal-comment-nodes.ts
Normal file
107
src/components/markdown-renderer/process-reveal-comment-nodes.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { DataNode, Document, Element, hasChildren, isComment, isTag, Node } from 'domhandler'
|
||||
import { Logger } from '../../utils/logger'
|
||||
|
||||
const log = new Logger('reveal.js > Comment Node Preprocessor')
|
||||
const revealCommandSyntax = /^\s*\.(\w*):(.*)$/g
|
||||
const dataAttributesSyntax = /\s*([\w-]*)=(?:"((?:[^"\\]|\\"|\\)*)"|'([^']*)')/g
|
||||
|
||||
/**
|
||||
* Travels through the given {@link Document}, searches for reveal command comments and applies them.
|
||||
*
|
||||
* @param doc The document that should be changed
|
||||
* @return The edited document
|
||||
*/
|
||||
export const processRevealCommentNodes = (doc: Document): Document => {
|
||||
visitNode(doc)
|
||||
return doc
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given {@link Node} if it is a comment node. If the node has children then all child nodes will be processed.
|
||||
* @param node The node to process.
|
||||
*/
|
||||
const visitNode = (node: Node): void => {
|
||||
if (isComment(node)) {
|
||||
processCommentNode(node)
|
||||
} else if (hasChildren(node)) {
|
||||
node.childNodes.forEach((childNode) => visitNode(childNode))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given {@link DataNode html comment} by parsing it, finding the element that should be changed and applies the contained changes.
|
||||
*
|
||||
* @param node The node that contains the reveal command.
|
||||
*/
|
||||
const processCommentNode = (node: DataNode): void => {
|
||||
const regexResult = node.data.split(revealCommandSyntax)
|
||||
if (regexResult.length === 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const parentNode: Element | null = findTargetElement(node, regexResult[1])
|
||||
if (!parentNode) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const dataAttribute of regexResult[2].matchAll(dataAttributesSyntax)) {
|
||||
const attributeName = dataAttribute[1]
|
||||
const attributeValue = dataAttribute[2] ?? dataAttribute[3]
|
||||
if (attributeValue) {
|
||||
log.debug(
|
||||
`Add attribute "${attributeName}"=>"${attributeValue}" to node`,
|
||||
parentNode,
|
||||
'because of',
|
||||
regexResult[1],
|
||||
'selector'
|
||||
)
|
||||
parentNode.attribs[attributeName] = attributeValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the ancestor element that should be changed based on the given selector.
|
||||
*
|
||||
* @param node The node whose ancestor should be found.
|
||||
* @param selector The found ancestor node or null if no node could be found.
|
||||
*/
|
||||
const findTargetElement = (node: Node, selector: string): Element | null => {
|
||||
if (selector === 'slide') {
|
||||
return findNearestAncestorSection(node)
|
||||
} else if (selector === 'element') {
|
||||
return findParentElement(node)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent node if it is an {@link Element}.
|
||||
*
|
||||
* @param node the found node or null if no parent node exists or if the parent node isn't an {@link Element}.
|
||||
*/
|
||||
const findParentElement = (node: Node): Element | null => {
|
||||
return node.parentNode !== null && isTag(node.parentNode) ? node.parentNode : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for the nearest ancestor of the node that is a section element.
|
||||
*
|
||||
* @param node the found section node or null if no section ancestor could be found.
|
||||
*/
|
||||
const findNearestAncestorSection = (node: Node): Element | null => {
|
||||
let currentNode = node.parentNode
|
||||
while (currentNode != null) {
|
||||
if (isTag(currentNode) && currentNode.tagName === 'section') {
|
||||
break
|
||||
}
|
||||
currentNode = node.parentNode
|
||||
}
|
||||
return currentNode
|
||||
}
|
|
@ -11,6 +11,8 @@
|
|||
@import '../../../../../../node_modules/highlight.js/styles/github-dark';
|
||||
}
|
||||
|
||||
position: relative;
|
||||
|
||||
code.hljs {
|
||||
overflow-x: auto;
|
||||
background-color: rgba(27, 31, 35, .05);
|
||||
|
@ -50,10 +52,10 @@
|
|||
.linenumber {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.showGutter .codeline {
|
||||
margin: 0 0 0 16px;
|
||||
.codeline {
|
||||
margin: 0 0 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrapLines .codeline {
|
||||
|
|
|
@ -18,13 +18,13 @@ export type LineNumberMarkerOptions = (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: (options: LineNumberMarkerOptions, offsetLines: number) => MarkdownIt.PluginSimple =
|
||||
(options, offsetLines = 0) =>
|
||||
export const lineNumberMarker: (options: LineNumberMarkerOptions, lineOffset: number) => MarkdownIt.PluginSimple =
|
||||
(options, lineOffset = 0) =>
|
||||
(md: MarkdownIt) => {
|
||||
// add app_linemarker token before each opening or self-closing level-0 tag
|
||||
md.core.ruler.push('line_number_marker', (state) => {
|
||||
const lineMarkers: LineMarkers[] = []
|
||||
tagTokens(state.tokens, lineMarkers, offsetLines)
|
||||
tagTokens(state.tokens, lineMarkers, lineOffset)
|
||||
if (options) {
|
||||
options(lineMarkers)
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions, offsetLines: n
|
|||
tokens.splice(tokenPosition, 0, startToken)
|
||||
}
|
||||
|
||||
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], offsetLines: number) => {
|
||||
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], lineOffset: number) => {
|
||||
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
|
||||
const token = tokens[tokenPosition]
|
||||
if (token.hidden) {
|
||||
|
@ -72,14 +72,14 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions, offsetLines: n
|
|||
const endLineNumber = token.map[1] + 1
|
||||
|
||||
if (token.level === 0) {
|
||||
lineMarkers.push({ startLine: startLineNumber + offsetLines, endLine: endLineNumber + offsetLines })
|
||||
lineMarkers.push({ startLine: startLineNumber + lineOffset, endLine: endLineNumber + lineOffset })
|
||||
}
|
||||
|
||||
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
|
||||
tokenPosition += 1
|
||||
|
||||
if (token.children) {
|
||||
tagTokens(token.children, lineMarkers, offsetLines)
|
||||
tagTokens(token.children, lineMarkers, lineOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
|
|||
}, [code])
|
||||
|
||||
return (
|
||||
<div data-cy={'markmap'}>
|
||||
<div data-cy={'markmap'} className={'position-relative'}>
|
||||
<div className={'svg-container'} ref={diagramContainer} />
|
||||
<div className={'text-right button-inside'}>
|
||||
<LockButton
|
||||
|
|
41
src/components/markdown-renderer/slide-theme.scss
Normal file
41
src/components/markdown-renderer/slide-theme.scss
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// Default mixins and settings -----------------
|
||||
@import "../../../node_modules/reveal.js/css/theme/template/mixins";
|
||||
@import "../../../node_modules/reveal.js/css/theme/template/settings";
|
||||
// ---------------------------------------------
|
||||
|
||||
|
||||
// Override theme settings (see ../template/settings.scss)
|
||||
$backgroundColor: #191919;
|
||||
|
||||
$mainColor: #fff;
|
||||
$headingColor: #fff;
|
||||
|
||||
$mainFontSize: 42px;
|
||||
$mainFont: 'Source Sans Pro', Helvetica, sans-serif;
|
||||
$headingFont: 'Source Sans Pro', Helvetica, sans-serif;
|
||||
$headingTextShadow: none;
|
||||
$headingLetterSpacing: normal;
|
||||
$headingTextTransform: uppercase;
|
||||
$headingFontWeight: 600;
|
||||
$linkColor: #42affa;
|
||||
$linkColorHover: lighten($linkColor, 15%);
|
||||
$selectionBackgroundColor: lighten($linkColor, 25%);
|
||||
|
||||
$heading1Size: 2.5em;
|
||||
$heading2Size: 1.6em;
|
||||
$heading3Size: 1.3em;
|
||||
$heading4Size: 1.0em;
|
||||
|
||||
// Change text colors against light slide backgrounds
|
||||
@include light-bg-text-color(#222);
|
||||
|
||||
|
||||
// Theme template ------------------------------
|
||||
@import "../../../node_modules/reveal.js/css/theme/template/theme";
|
||||
// ---------------------------------------------
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useMemo, useRef } from 'react'
|
||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||
import './markdown-renderer.scss'
|
||||
import { useComponentReplacers } from './hooks/use-component-replacers'
|
||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { useOnRefChange } from './hooks/use-on-ref-change'
|
||||
import { useTrimmedContent } from './hooks/use-trimmed-content'
|
||||
import { useReveal } from './hooks/use-reveal'
|
||||
import './slideshow.scss'
|
||||
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
|
||||
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/basic-markdown-it-configurator'
|
||||
import { SlideOptions } from '../common/note-frontmatter/types'
|
||||
import { processRevealCommentNodes } from './process-reveal-comment-nodes'
|
||||
import { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
|
||||
|
||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||
slideOptions: SlideOptions
|
||||
}
|
||||
|
||||
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps & ScrollProps> = ({
|
||||
className,
|
||||
content,
|
||||
onFirstHeadingChange,
|
||||
onTaskCheckedChange,
|
||||
onTocChange,
|
||||
baseUrl,
|
||||
onImageClick,
|
||||
useAlternativeBreaks,
|
||||
lineOffset,
|
||||
slideOptions
|
||||
}) => {
|
||||
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||
const tocAst = useRef<TocAst>()
|
||||
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
|
||||
|
||||
const markdownIt = useMemo(
|
||||
() =>
|
||||
new BasicMarkdownItConfigurator({
|
||||
onToc: (toc) => (tocAst.current = toc),
|
||||
useAlternativeBreaks,
|
||||
lineOffset,
|
||||
headlineAnchors: false,
|
||||
slideSections: true
|
||||
}).buildConfiguredMarkdownIt(),
|
||||
[lineOffset, useAlternativeBreaks]
|
||||
)
|
||||
const replacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, lineOffset)
|
||||
const markdownReactDom = useConvertMarkdownToReactDom(
|
||||
trimmedContent,
|
||||
markdownIt,
|
||||
replacers,
|
||||
processRevealCommentNodes
|
||||
)
|
||||
|
||||
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
|
||||
useOnRefChange(tocAst, onTocChange)
|
||||
useReveal(content, slideOptions)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<DocumentLengthLimitReachedAlert show={contentExceedsLimit} />
|
||||
<div className={'reveal'}>
|
||||
<div ref={markdownBodyRef} className={`${className ?? ''} slides`}>
|
||||
{markdownReactDom}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default SlideshowMarkdownRenderer
|
14
src/components/markdown-renderer/slideshow.scss
Normal file
14
src/components/markdown-renderer/slideshow.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@import "../../../node_modules/reveal.js/css/reveal";
|
||||
@import "../../../node_modules/reveal.js/dist/theme/fonts/league-gothic/league-gothic.css";
|
||||
@import "slide-theme";
|
||||
|
||||
//Fix to make transitions work with bootstrap
|
||||
.reveal [hidden] {
|
||||
display: block !important;
|
||||
}
|
5
src/components/markdown-renderer/types.d.ts
vendored
5
src/components/markdown-renderer/types.d.ts
vendored
|
@ -13,8 +13,3 @@ export interface LineMarkerPosition {
|
|||
line: number
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface AdditionalMarkdownRendererProps {
|
||||
className?: string
|
||||
content: string
|
||||
}
|
||||
|
|
|
@ -5,5 +5,7 @@
|
|||
*/
|
||||
|
||||
.button-inside {
|
||||
margin-top: -31px;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
||||
import { ScrollState } from '../scroll-props'
|
||||
import { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||
import { useOnUserScroll } from './use-on-user-scroll'
|
||||
import { useScrollToLineMark } from './use-scroll-to-line-mark'
|
||||
|
||||
export const useSyncedScrolling = (
|
||||
export const useDocumentSyncScrolling = (
|
||||
outerContainerRef: React.RefObject<HTMLElement>,
|
||||
rendererRef: React.RefObject<HTMLElement>,
|
||||
numberOfLines: number,
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { RefObject, useCallback } from 'react'
|
||||
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
||||
import { ScrollState } from '../scroll-props'
|
||||
import { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||
|
||||
export const useOnUserScroll = (
|
||||
lineMarks: LineMarkerPosition[] | undefined,
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { RefObject, useCallback, useEffect, useRef } from 'react'
|
||||
import { LineMarkerPosition } from '../../../markdown-renderer/types'
|
||||
import { ScrollState } from '../scroll-props'
|
||||
import { findLineMarks } from '../utils'
|
||||
import { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
|
||||
import { findLineMarks } from '../../../editor-page/synced-scroll/utils'
|
||||
|
||||
export const useScrollToLineMark = (
|
||||
scrollState: ScrollState | undefined,
|
|
@ -19,16 +19,14 @@ import { countWords } from './word-counter'
|
|||
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
|
||||
import { useRendererToEditorCommunicator } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
|
||||
import { useRendererReceiveHandler } from './window-post-message-communicator/hooks/use-renderer-receive-handler'
|
||||
import { SlideshowMarkdownRenderer } from '../markdown-renderer/slideshow-markdown-renderer'
|
||||
import { initialState } from '../../redux/note-details/initial-state'
|
||||
|
||||
export const IframeMarkdownRenderer: React.FC = () => {
|
||||
const [markdownContent, setMarkdownContent] = useState('')
|
||||
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
|
||||
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
|
||||
const [frontmatterInfo, setFrontmatterInfo] = useState<RendererFrontmatterInfo>({
|
||||
offsetLines: 0,
|
||||
frontmatterInvalid: false,
|
||||
deprecatedSyntax: false
|
||||
})
|
||||
const [frontmatterInfo, setFrontmatterInfo] = useState<RendererFrontmatterInfo>(initialState.frontmatterRendererInfo)
|
||||
|
||||
const communicator = useRendererToEditorCommunicator()
|
||||
|
||||
|
@ -122,6 +120,18 @@ export const IframeMarkdownRenderer: React.FC = () => {
|
|||
frontmatterInfo={frontmatterInfo}
|
||||
/>
|
||||
)
|
||||
case RendererType.SLIDESHOW:
|
||||
return (
|
||||
<SlideshowMarkdownRenderer
|
||||
content={markdownContent}
|
||||
baseUrl={baseConfiguration.baseUrl}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onImageClick={onImageClick}
|
||||
scrollState={scrollState}
|
||||
lineOffset={frontmatterInfo.lineOffset}
|
||||
slideOptions={frontmatterInfo.slideOptions}
|
||||
/>
|
||||
)
|
||||
case RendererType.INTRO:
|
||||
return (
|
||||
<MarkdownDocument
|
||||
|
|
|
@ -8,9 +8,9 @@ import { TocAst } from 'markdown-it-toc-done-right'
|
|||
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-array-deprecation-alert'
|
||||
import { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
|
||||
import { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling'
|
||||
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||
import { BasicMarkdownRenderer } from '../markdown-renderer/basic-markdown-renderer'
|
||||
import { DocumentMarkdownRenderer } from '../markdown-renderer/document-markdown-renderer'
|
||||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||
import './markdown-document.scss'
|
||||
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
|
||||
|
@ -70,7 +70,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
}, [rendererSize.height, onHeightChange])
|
||||
|
||||
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
|
||||
const [onLineMarkerPositionChanged, onUserScroll] = useSyncedScrolling(
|
||||
const [onLineMarkerPositionChanged, onUserScroll] = useDocumentSyncScrolling(
|
||||
internalDocumentRenderPaneRef,
|
||||
rendererRef,
|
||||
contentLineCount,
|
||||
|
@ -88,7 +88,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
<div className={'markdown-document-content'}>
|
||||
<InvalidYamlAlert show={!!frontmatterInfo?.frontmatterInvalid} />
|
||||
<YamlArrayDeprecationAlert show={!!frontmatterInfo?.deprecatedSyntax} />
|
||||
<BasicMarkdownRenderer
|
||||
<DocumentMarkdownRenderer
|
||||
outerContainerRef={rendererRef}
|
||||
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
||||
content={markdownContent}
|
||||
|
@ -99,7 +99,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
|||
baseUrl={baseUrl}
|
||||
onImageClick={onImageClick}
|
||||
useAlternativeBreaks={useAlternativeBreaks}
|
||||
frontmatterLineOffset={frontmatterInfo?.offsetLines}
|
||||
lineOffset={frontmatterInfo?.lineOffset}
|
||||
/>
|
||||
</div>
|
||||
<div className={'markdown-document-side pt-4'}>
|
||||
|
|
39
src/components/slide-show-page/slide-show-page.tsx
Normal file
39
src/components/slide-show-page/slide-show-page.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
||||
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLoadNoteFromServer } from '../editor-page/hooks/useLoadNoteFromServer'
|
||||
import { ShowIf } from '../common/show-if/show-if'
|
||||
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
|
||||
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
|
||||
import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
|
||||
|
||||
export const SlideShowPage: React.FC = () => {
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
|
||||
useTranslation()
|
||||
useSendFrontmatterInfoFromReduxToRenderer()
|
||||
|
||||
const [error, loading] = useLoadNoteFromServer()
|
||||
|
||||
return (
|
||||
<ShowIf condition={!error && !loading}>
|
||||
<div className={'vh-100 vw-100'}>
|
||||
<RenderIframe
|
||||
frameClasses={'h-100 w-100'}
|
||||
markdownContent={markdownContent}
|
||||
rendererType={RendererType.SLIDESHOW}
|
||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
||||
/>
|
||||
</div>
|
||||
</ShowIf>
|
||||
)
|
||||
}
|
||||
|
||||
export default SlideShowPage
|
226
src/external-types/reveal.js/index.d.ts
vendored
Normal file
226
src/external-types/reveal.js/index.d.ts
vendored
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare module 'reveal.js' {
|
||||
export interface RevealOptions {
|
||||
// Configuration
|
||||
controls?: boolean | undefined
|
||||
progress?: boolean | undefined
|
||||
// https://github.com/hakimel/reveal.js/#slide-number
|
||||
slideNumber?: boolean | string | undefined
|
||||
|
||||
history?: boolean | undefined
|
||||
plugins?: Plugin[] | undefined
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#keyboard-bindings
|
||||
// keyboard?: any
|
||||
overview?: boolean | undefined
|
||||
center?: boolean | undefined
|
||||
touch?: boolean | undefined
|
||||
loop?: boolean | undefined
|
||||
rtl?: boolean | undefined
|
||||
shuffle?: boolean | undefined
|
||||
fragments?: boolean | undefined
|
||||
embedded?: boolean | undefined
|
||||
help?: boolean | undefined
|
||||
showNotes?: boolean | undefined
|
||||
autoSlide?: number | undefined
|
||||
autoSlideStoppable?: boolean | undefined
|
||||
// autoSlideMethod?: any
|
||||
mouseWheel?: boolean | undefined
|
||||
hideAddressBar?: boolean | undefined
|
||||
previewLinks?: boolean | undefined
|
||||
transition?: string | undefined
|
||||
transitionSpeed?: string | undefined
|
||||
backgroundTransition?: string | undefined
|
||||
viewDistance?: number | undefined
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#parallax-background
|
||||
// Parallax background image
|
||||
parallaxBackgroundImage?: string | undefined
|
||||
|
||||
// Parallax background size
|
||||
parallaxBackgroundSize?: string | undefined // CSS syntax, e.g. "2100px 900px" - currently only pixels are supported (don't use % or auto)
|
||||
|
||||
// Number of pixels to move the parallax background per slide
|
||||
// - Calculated automatically unless specified
|
||||
// - Set to 0 to disable movement along an axis
|
||||
parallaxBackgroundHorizontal?: number | undefined
|
||||
parallaxBackgroundVertical?: number | undefined
|
||||
|
||||
rollingLinks?: boolean | undefined
|
||||
theme?: string | undefined
|
||||
|
||||
// Presentation Size
|
||||
// https://github.com/hakimel/reveal.js/#presentation-size
|
||||
width?: number | string | undefined
|
||||
height?: number | string | undefined
|
||||
margin?: number | string | undefined
|
||||
minScale?: number | string | undefined
|
||||
maxScale?: number | string | undefined
|
||||
|
||||
// Dependencies
|
||||
// https://github.com/hakimel/reveal.js/#dependencies
|
||||
dependencies?: RevealDependency[] | undefined
|
||||
|
||||
// Exposes the reveal.js API through window.postMessage
|
||||
postMessage?: boolean | undefined
|
||||
|
||||
// Dispatches all reveal.js events to the parent window through postMessage
|
||||
postMessageEvents?: boolean | undefined
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#multiplexing
|
||||
multiplex?: MultiplexConfig | undefined
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#mathjax
|
||||
math?: MathConfig | undefined
|
||||
}
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#slide-changed-event
|
||||
export interface SlideEvent {
|
||||
previousSlide?: Element | undefined
|
||||
currentSlide: Element
|
||||
indexh: number
|
||||
indexv?: number | undefined
|
||||
}
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#fragment-events
|
||||
export interface FragmentEvent {
|
||||
fragment: Element
|
||||
}
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#multiplexing
|
||||
export interface MultiplexConfig {
|
||||
// Obtained from the socket.io server. Gives this (the master) control of the presentation
|
||||
secret?: string | undefined
|
||||
// Obtained from the socket.io server
|
||||
id: string
|
||||
|
||||
// Location of socket.io server
|
||||
url: string
|
||||
}
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#mathjax
|
||||
export interface MathConfig {
|
||||
// Obtained from the socket.io server. Gives this (the master) control of the presentation
|
||||
mathjax: string
|
||||
// Obtained from the socket.io server
|
||||
config: string
|
||||
}
|
||||
|
||||
// https://github.com/hakimel/reveal.js/#dependencies
|
||||
export interface RevealDependency {
|
||||
src: string
|
||||
condition?: (() => boolean) | undefined
|
||||
async?: boolean | undefined
|
||||
callback?: (() => void) | undefined
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
id: string
|
||||
|
||||
init(deck: RevealStatic): void | Promise<void>
|
||||
}
|
||||
|
||||
export default class Reveal {
|
||||
constructor(options: RevealOptions)
|
||||
initialize: () => Promise<void>
|
||||
|
||||
public configure: (diff: RevealOptions) => void
|
||||
|
||||
// Navigation
|
||||
public slide(indexh: number, indexv?: number, f?: number, o?: number): void
|
||||
|
||||
public left(): void
|
||||
|
||||
public right(): void
|
||||
|
||||
public up(): void
|
||||
|
||||
public down(): void
|
||||
|
||||
public prev(): void
|
||||
|
||||
public next(): void
|
||||
|
||||
public prevFragment(): boolean
|
||||
|
||||
public nextFragment(): boolean
|
||||
|
||||
// Randomize the order of slides
|
||||
public shuffle(): void
|
||||
|
||||
// Toggle presentation states
|
||||
public toggleOverview(override?: boolean): void
|
||||
|
||||
public togglePause(override?: boolean): void
|
||||
|
||||
public toggleAutoSlide(override?: boolean): void
|
||||
|
||||
// Retrieves the previous and current slide elements
|
||||
public getPreviousSlide(): Element
|
||||
|
||||
public getCurrentSlide(): Element
|
||||
|
||||
public getIndices(slide?: Element): { h: number; v: number }
|
||||
|
||||
public getProgress(): number
|
||||
|
||||
public getTotalSlides(): number
|
||||
|
||||
public availableFragments(): { prev: boolean; next: boolean }
|
||||
|
||||
// Returns the speaker notes for the current slide
|
||||
public getSlideNotes(slide?: Element): string
|
||||
|
||||
// Plugins
|
||||
public hasPlugin(name: string): boolean
|
||||
|
||||
public getPlugin(name: string): Plugin
|
||||
|
||||
public getPlugins(): { [name: string]: Plugin }
|
||||
|
||||
// States
|
||||
// public addEventListener(type: string, listener: (event: any) => void, useCapture?: boolean): void
|
||||
|
||||
// public removeEventListener(type: string, listener: (event: any) => void, useCapture?: boolean): void
|
||||
|
||||
// State Checks
|
||||
public isFirstSlide(): boolean
|
||||
|
||||
public isLastSlide(): boolean
|
||||
|
||||
public isPaused(): boolean
|
||||
|
||||
public isOverview(): boolean
|
||||
|
||||
public isAutoSliding(): boolean
|
||||
|
||||
// undocumented method
|
||||
public layout(): void
|
||||
|
||||
public addEventListeners(): void
|
||||
|
||||
public removeEventListeners(): void
|
||||
|
||||
public getSlide(x: number, y?: number): Element
|
||||
|
||||
public getScale(): number
|
||||
|
||||
public getConfig(): RevealOptions
|
||||
|
||||
// public getQueryHash(): any
|
||||
|
||||
// public setState(state: any): void
|
||||
|
||||
// public getState(): any
|
||||
|
||||
// update slides after dynamic changes
|
||||
public sync(): void
|
||||
}
|
||||
}
|
||||
|
||||
// [1]: Implement ourself can only modify the iframe url
|
|
@ -14,7 +14,7 @@ import { useMemo } from 'react'
|
|||
*/
|
||||
export const useNoteMarkdownContentWithoutFrontmatter = (): string => {
|
||||
const markdownContent = useNoteMarkdownContent()
|
||||
const offsetLines = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo.offsetLines)
|
||||
const lineOffset = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo.lineOffset)
|
||||
|
||||
return useMemo(() => markdownContent.split('\n').slice(offsetLines).join('\n'), [markdownContent, offsetLines])
|
||||
return useMemo(() => markdownContent.split('\n').slice(lineOffset).join('\n'), [markdownContent, lineOffset])
|
||||
}
|
||||
|
|
|
@ -37,6 +37,11 @@ const DocumentReadOnlyPage = React.lazy(
|
|||
/* webpackPrefetch: true */ /* webpackChunkName: "documentReadOnly" */ './components/document-read-only-page/document-read-only-page'
|
||||
)
|
||||
)
|
||||
|
||||
const SlideShowPage = React.lazy(
|
||||
() => import(/* webpackPrefetch: true */ './components/slide-show-page/slide-show-page')
|
||||
)
|
||||
|
||||
const baseUrl = new URL(document.head.baseURI).pathname
|
||||
const log = new Logger('Index')
|
||||
|
||||
|
@ -82,6 +87,9 @@ ReactDOM.render(
|
|||
<Route path='/s/:id'>
|
||||
<DocumentReadOnlyPage />
|
||||
</Route>
|
||||
<Route path='/p/:id'>
|
||||
<SlideShowPage />
|
||||
</Route>
|
||||
<Route path='/:id'>
|
||||
<NoteDirectLinkRedirector />
|
||||
</Route>
|
||||
|
|
|
@ -6,7 +6,15 @@
|
|||
|
||||
import { NoteDetails } from './types'
|
||||
import { DateTime } from 'luxon'
|
||||
import { NoteTextDirection, NoteType } from '../../components/common/note-frontmatter/types'
|
||||
import { NoteTextDirection, NoteType, SlideOptions } from '../../components/common/note-frontmatter/types'
|
||||
|
||||
export const initialSlideOptions: SlideOptions = {
|
||||
transition: 'zoom',
|
||||
autoSlide: 0,
|
||||
autoSlideStoppable: true,
|
||||
backgroundTransition: 'fade',
|
||||
slideNumber: false
|
||||
}
|
||||
|
||||
export const initialState: NoteDetails = {
|
||||
markdownContent: '',
|
||||
|
@ -14,7 +22,8 @@ export const initialState: NoteDetails = {
|
|||
frontmatterRendererInfo: {
|
||||
frontmatterInvalid: false,
|
||||
deprecatedSyntax: false,
|
||||
offsetLines: 0
|
||||
lineOffset: 0,
|
||||
slideOptions: initialSlideOptions
|
||||
},
|
||||
id: '',
|
||||
createTime: DateTime.fromSeconds(0),
|
||||
|
@ -39,6 +48,7 @@ export const initialState: NoteDetails = {
|
|||
GA: '',
|
||||
disqus: '',
|
||||
type: NoteType.DOCUMENT,
|
||||
opengraph: new Map<string, string>()
|
||||
opengraph: new Map<string, string>(),
|
||||
slideOptions: initialSlideOptions
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
|
||||
import { Reducer } from 'redux'
|
||||
import { PresentFrontmatterExtractionResult } from '../../components/common/note-frontmatter/types'
|
||||
import { NoteFrontmatter } from '../../components/common/note-frontmatter/note-frontmatter'
|
||||
import {
|
||||
createNoteFrontmatterFromYaml,
|
||||
NoteFrontmatter
|
||||
} from '../../components/common/note-frontmatter/note-frontmatter'
|
||||
import { NoteDetails, NoteDetailsActions, NoteDetailsActionType } from './types'
|
||||
import { extractFrontmatter } from '../../components/common/note-frontmatter/extract-frontmatter'
|
||||
import { NoteDto } from '../../api/notes/types'
|
||||
|
@ -31,8 +34,6 @@ export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
|
|||
}
|
||||
}
|
||||
|
||||
const TASK_REGEX = /(\s*(?:[-*+]|\d+[.)]) )(\[[ xX]])( .*)/
|
||||
|
||||
/**
|
||||
* Builds a {@link NoteDetails} redux state from a DTO received as an API response.
|
||||
* @param dto The first DTO received from the API containing the relevant information about the note.
|
||||
|
@ -43,6 +44,7 @@ const buildStateFromServerDto = (dto: NoteDto): NoteDetails => {
|
|||
return buildStateFromMarkdownContentUpdate(newState, newState.markdownContent)
|
||||
}
|
||||
|
||||
const TASK_REGEX = /(\s*(?:[-*+]|\d+[.)]) )(\[[ xX]])( .*)/
|
||||
/**
|
||||
* Builds a {@link NoteDetails} redux state where a checkbox in the markdown content either gets checked or unchecked.
|
||||
* @param state The previous redux state.
|
||||
|
@ -74,7 +76,7 @@ const buildStateFromTaskListUpdate = (
|
|||
*/
|
||||
const buildStateFromMarkdownContentUpdate = (state: NoteDetails, markdownContent: string): NoteDetails => {
|
||||
const frontmatterExtraction = extractFrontmatter(markdownContent)
|
||||
if (!frontmatterExtraction.frontmatterPresent) {
|
||||
if (!frontmatterExtraction.isPresent) {
|
||||
return {
|
||||
...state,
|
||||
markdownContent: markdownContent,
|
||||
|
@ -103,32 +105,34 @@ const buildStateFromFrontmatterUpdate = (
|
|||
state: NoteDetails,
|
||||
frontmatterExtraction: PresentFrontmatterExtractionResult
|
||||
): NoteDetails => {
|
||||
if (frontmatterExtraction.rawFrontmatterText === state.rawFrontmatter) {
|
||||
if (frontmatterExtraction.rawText === state.rawFrontmatter) {
|
||||
return state
|
||||
}
|
||||
try {
|
||||
const frontmatter = NoteFrontmatter.createFromYaml(frontmatterExtraction.rawFrontmatterText)
|
||||
const frontmatter = createNoteFrontmatterFromYaml(frontmatterExtraction.rawText)
|
||||
return {
|
||||
...state,
|
||||
rawFrontmatter: frontmatterExtraction.rawFrontmatterText,
|
||||
rawFrontmatter: frontmatterExtraction.rawText,
|
||||
frontmatter: frontmatter,
|
||||
noteTitle: generateNoteTitle(frontmatter, state.firstHeading),
|
||||
frontmatterRendererInfo: {
|
||||
offsetLines: frontmatterExtraction.frontmatterLines,
|
||||
lineOffset: frontmatterExtraction.lineOffset,
|
||||
deprecatedSyntax: frontmatter.deprecatedTagsSyntax,
|
||||
frontmatterInvalid: false
|
||||
frontmatterInvalid: false,
|
||||
slideOptions: frontmatter.slideOptions
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
...state,
|
||||
noteTitle: generateNoteTitle(initialState.frontmatter, state.firstHeading),
|
||||
rawFrontmatter: frontmatterExtraction.rawFrontmatterText,
|
||||
rawFrontmatter: frontmatterExtraction.rawText,
|
||||
frontmatter: initialState.frontmatter,
|
||||
frontmatterRendererInfo: {
|
||||
offsetLines: frontmatterExtraction.frontmatterLines,
|
||||
lineOffset: frontmatterExtraction.lineOffset,
|
||||
deprecatedSyntax: false,
|
||||
frontmatterInvalid: true
|
||||
frontmatterInvalid: true,
|
||||
slideOptions: initialState.frontmatterRendererInfo.slideOptions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -172,11 +176,7 @@ const convertNoteDtoToNoteDetails = (note: NoteDto): NoteDetails => {
|
|||
return {
|
||||
markdownContent: note.content,
|
||||
rawFrontmatter: '',
|
||||
frontmatterRendererInfo: {
|
||||
frontmatterInvalid: false,
|
||||
deprecatedSyntax: false,
|
||||
offsetLines: 0
|
||||
},
|
||||
frontmatterRendererInfo: initialState.frontmatterRendererInfo,
|
||||
frontmatter: initialState.frontmatter,
|
||||
id: note.metadata.id,
|
||||
noteTitle: initialState.noteTitle,
|
||||
|
|
45
src/style/bootstrap.scss
vendored
Normal file
45
src/style/bootstrap.scss
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
Copy of the main bootstrap scss file but without transitions.scss because that clashes with reveal.
|
||||
*/
|
||||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
@import "~bootstrap/scss/mixins";
|
||||
@import "~bootstrap/scss/root";
|
||||
@import "~bootstrap/scss/reboot";
|
||||
@import "~bootstrap/scss/type";
|
||||
@import "~bootstrap/scss/images";
|
||||
@import "~bootstrap/scss/code";
|
||||
@import "~bootstrap/scss/grid";
|
||||
@import "~bootstrap/scss/tables";
|
||||
@import "~bootstrap/scss/forms";
|
||||
@import "~bootstrap/scss/buttons";
|
||||
@import "~bootstrap/scss/dropdown";
|
||||
@import "~bootstrap/scss/button-group";
|
||||
@import "~bootstrap/scss/input-group";
|
||||
@import "~bootstrap/scss/custom-forms";
|
||||
@import "~bootstrap/scss/nav";
|
||||
@import "~bootstrap/scss/navbar";
|
||||
@import "~bootstrap/scss/card";
|
||||
@import "~bootstrap/scss/breadcrumb";
|
||||
@import "~bootstrap/scss/pagination";
|
||||
@import "~bootstrap/scss/badge";
|
||||
@import "~bootstrap/scss/jumbotron";
|
||||
@import "~bootstrap/scss/alert";
|
||||
@import "~bootstrap/scss/progress";
|
||||
@import "~bootstrap/scss/media";
|
||||
@import "~bootstrap/scss/list-group";
|
||||
@import "~bootstrap/scss/close";
|
||||
@import "~bootstrap/scss/toasts";
|
||||
@import "~bootstrap/scss/modal";
|
||||
@import "~bootstrap/scss/tooltip";
|
||||
@import "~bootstrap/scss/popover";
|
||||
@import "~bootstrap/scss/carousel";
|
||||
@import "~bootstrap/scss/spinners";
|
||||
@import "~bootstrap/scss/utilities";
|
||||
@import "~bootstrap/scss/print";
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
@import "variables";
|
||||
@import "variables.light";
|
||||
@import "../../node_modules/bootstrap/scss/bootstrap";
|
||||
@import "bootstrap";
|
||||
@import '../../node_modules/react-bootstrap-typeahead/css/Typeahead';
|
||||
@import "../../node_modules/@fontsource/source-sans-pro/index";
|
||||
@import "../../node_modules/twemoji-colr-font/twemoji";
|
||||
|
|
|
@ -11,3 +11,12 @@ export const isTestMode = (): boolean => {
|
|||
export const isMockMode = (): boolean => {
|
||||
return process.env.REACT_APP_BACKEND_BASE_URL === undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current runtime was built in development mode.
|
||||
*
|
||||
* @return {@code true} if the runtime was built in development mode.
|
||||
*/
|
||||
export const isDevMode = (): boolean => {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
}
|
||||
|
|
|
@ -12414,6 +12414,11 @@ reusify@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
reveal.js@4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/reveal.js/-/reveal.js-4.1.3.tgz#f0a805a3cbc7c4e5553d66f91404b036f0f331cf"
|
||||
integrity sha512-5VbL4nVDUedVKnOIIM3UQAIUlp+CvR/SrUkrN5GDoVfcWJAxH2oIh7PWyShy7+pE7tgkH2q+3e5EikGRpgE+oA==
|
||||
|
||||
rework-visit@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/rework-visit/-/rework-visit-1.0.0.tgz#9945b2803f219e2f7aca00adb8bc9f640f842c9a"
|
||||
|
|
Loading…
Add table
Reference in a new issue