Explorar o código

[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
Domagoj Kriskovic %!s(int64=3) %!d(string=hai) anos
pai
achega
9cb0122ea3

+ 4 - 4
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 })
           }
         }
       },

+ 48 - 42
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<keyof Preamble>
-) => 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')
-
-  const content = state.sliceDoc(argumentNode.from + 1, argumentNode.to - 1)
-  const authors = content.replaceAll(/\s+/g, ' ').split('\\and')
-
-  for (const authorParts 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)
+  authors: { node: SyntaxNode; content: string }[],
+  view: EditorView
+) {
+  const authorsElement = document.createElement('div')
+  authorsElement.classList.add('ol-cm-authors')
+
+  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 author of authors) {
+      const authorElement = document.createElement('div')
+      authorElement.classList.add('ol-cm-author')
+
+      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
 }

+ 71 - 1
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx

@@ -370,8 +370,78 @@ describe('<CodeMirrorEditor/> 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
 })