From 9cb0122ea319c7ee0d0bd6660d7661bb4881d099 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Mon, 10 Jul 2023 15:31:42 +0200 Subject: [PATCH] [visual] support for multiple authors in /maketitle (#13713) * [visual] support for multiple titles in /maketitle * support \and in author name * using isMatchWith * fix isMatchWith check * fix isMatchWith... again * add tests * test author text replace and mouse events * prettier * don't handle updating node events * use the arguments order as before GitOrigin-RevId: 99b3d1c22a2dc1258a5cdf95738852028895f54a --- .../extensions/visual/atomic-decorations.ts | 8 +- .../visual/visual-widgets/maketitle.ts | 84 ++++++++++--------- .../codemirror-editor-visual.spec.tsx | 72 +++++++++++++++- 3 files changed, 120 insertions(+), 44 deletions(-) diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts index b5dcb3cd58..8625378040 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts @@ -142,11 +142,11 @@ export const atomicDecorations = (options: Options) => { node: SyntaxNode content: string } - author?: { + authors: { node: SyntaxNode content: string - } - } = { from: 0, to: 0 } + }[] + } = { from: 0, to: 0, authors: [] } // find the positions of the title and author in the preamble tree.iterate({ @@ -165,7 +165,7 @@ export const atomicDecorations = (options: Options) => { const node = nodeRef.node.getChild('TextArgument') if (node) { const content = state.sliceDoc(node.from, node.to) - preamble.author = { node, content } + preamble.authors.push({ node, content }) } } }, diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/maketitle.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/maketitle.ts index 402c00675f..d121659727 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/maketitle.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/maketitle.ts @@ -10,10 +10,10 @@ type Preamble = { node: SyntaxNode content: string } - author?: { + authors: { node: SyntaxNode content: string - } + }[] } export class MakeTitleWidget extends WidgetType { @@ -32,10 +32,7 @@ export class MakeTitleWidget extends WidgetType { } eq(widget: MakeTitleWidget) { - return isShallowEqualPreamble(widget.preamble, this.preamble, [ - 'title', - 'author', - ]) + return isShallowEqualPreamble(widget.preamble, this.preamble) } updateDOM(element: HTMLElement, view: EditorView): boolean { @@ -80,26 +77,30 @@ export class MakeTitleWidget extends WidgetType { }) } - if (this.preamble.author) { - const authorsElement = buildAuthorsElement( - view.state, - this.preamble.author.node - ) - authorsElement.addEventListener('mouseup', () => { - if (this.preamble.author) { - selectNode(view, this.preamble.author.node) - } - }) + if (this.preamble.authors.length) { + const authorsElement = buildAuthorsElement(this.preamble.authors, view) element.append(authorsElement) } } } -const isShallowEqualPreamble = ( - a: Preamble, - b: Preamble, - fields: Array -) => fields.every(field => a[field]?.content === b[field]?.content) +function isShallowEqualPreamble(a: Preamble, b: Preamble) { + if (a.title?.content !== b.title?.content) { + return false // title changed + } + + if (a.authors.length !== b.authors.length) { + return false // number of authors changed + } + + for (let i = 0; i < a.authors.length; i++) { + if (a.authors[i].content !== b.authors[i].content) { + return false // author changed + } + } + + return true +} function buildTitleElement( state: EditorState, @@ -112,28 +113,33 @@ function buildTitleElement( } function buildAuthorsElement( - state: EditorState, - argumentNode: SyntaxNode -): HTMLDivElement { - const element = document.createElement('div') - element.classList.add('ol-cm-authors') + authors: { node: SyntaxNode; content: string }[], + view: EditorView +) { + const authorsElement = document.createElement('div') + authorsElement.classList.add('ol-cm-authors') - const content = state.sliceDoc(argumentNode.from + 1, argumentNode.to - 1) - const authors = content.replaceAll(/\s+/g, ' ').split('\\and') + for (const { node, content } of authors) { + const authorContent = content.slice(1, -1) // trimming the braces + const authors = authorContent.replaceAll(/\s+/g, ' ').split('\\and') - for (const authorParts of authors) { - const authorElement = document.createElement('div') - authorElement.classList.add('ol-cm-author') + for (const author of authors) { + const authorElement = document.createElement('div') + authorElement.classList.add('ol-cm-author') - for (const authorInfoItem of authorParts.split('\\\\')) { - const authorLineElement = document.createElement('div') - authorLineElement.classList.add('ol-cm-author-line') - authorLineElement.textContent = authorInfoItem.trim() - authorElement.appendChild(authorLineElement) + for (const authorInfoItem of author.split('\\\\')) { + const authorLineElement = document.createElement('div') + authorLineElement.classList.add('ol-cm-author-line') + authorLineElement.textContent = authorInfoItem.trim() + authorElement.appendChild(authorLineElement) + } + + authorElement.addEventListener('mouseup', () => { + selectNode(view, node) + }) + authorsElement.append(authorElement) } - - element.append(authorElement) } - return element + return authorsElement } diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx index 98325bbaa0..f8c298610e 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx @@ -370,8 +370,78 @@ describe(' in Visual mode', function () { cy.get('@first-line').should('have.text', 'Foo \\footnote{Bar.} ') }) + it('should show document preamble', function () { + cy.get('@first-line').type( + [ + '\\author{{}Author}', + '\\title{{}Document title}', + '\\begin{{}document}', + '\\maketitle', + '\\end{{}document}', + '', + ].join('{Enter}') + ) + cy.get('.ol-cm-preamble-widget').should('have.length', 1) + cy.get('.ol-cm-preamble-widget').click() + + cy.get('.ol-cm-preamble-line').eq(0).should('contain', '\\author{Author}') + cy.get('.ol-cm-preamble-line') + .eq(1) + .should('contain', '\\title{Document title}') + cy.get('.ol-cm-preamble-line').eq(2).should('contain', '\\begin{document}') + cy.get('.ol-cm-preamble-line').eq(3).should('not.exist') + }) + + it('should show multiple authors', function () { + cy.get('@first-line').type( + [ + '\\author{{}Author \\and Author2}', + '\\author{{}Author3}', + '\\title{{}Document title}', + '\\begin{{}document}', + '\\maketitle', + '\\end{{}document}', + '', + ].join('{Enter}') + ) + cy.get('.ol-cm-preamble-widget').should('have.length', 1) + cy.get('.ol-cm-preamble-widget').click() + + cy.get('.ol-cm-authors').should('have.length', 1) + cy.get('.ol-cm-authors .ol-cm-author').should('have.length', 3) + }) + + it('should update authors', function () { + cy.get('@first-line').type( + [ + '\\author{{}Author \\and Author2}', + '\\author{{}Author3}', + '\\title{{}Document title}', + '\\begin{{}document}', + '\\maketitle', + '\\end{{}document}', + '', + ].join('{Enter}') + ) + cy.get('.ol-cm-preamble-widget').should('have.length', 1) + cy.get('.ol-cm-preamble-widget').click() + + cy.get('.ol-cm-authors').should('have.length', 1) + cy.get('.ol-cm-author').eq(0).should('contain', 'Author') + cy.get('.ol-cm-author').eq(1).should('contain', 'Author2') + cy.get('.ol-cm-author').eq(2).should('contain', 'Author3') + + cy.get('.ol-cm-author').eq(0).click() + cy.get('.ol-cm-preamble-line').eq(0).type('{leftarrow}{backspace}New') + cy.get('.ol-cm-author').eq(1).should('contain', 'AuthorNew') + + // update author without changing node from/to coordinates + cy.get('.ol-cm-author').eq(0).click() + cy.get('.ol-cm-preamble-line').eq(0).type('{leftarrow}{shift}{leftarrow}X') + cy.get('.ol-cm-author').eq(1).should('contain', 'AuthorNeX') + }) + // TODO: \input // TODO: Math // TODO: Abstract - // TODO: Preamble })