Просмотр исходного кода

[visual] Apply style to the content of color commands (#13726)

GitOrigin-RevId: 4ae8b745618e91b487d17c357cdb0e697038b3a3
Alf Eaton 3 лет назад
Родитель
Сommit
08c82a24a9

+ 24 - 1
services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts

@@ -46,7 +46,12 @@ import { InlineGraphicsWidget } from './visual-widgets/inline-graphics'
 import getMeta from '../../../../utils/meta'
 import { EditableGraphicsWidget } from './visual-widgets/editable-graphics'
 import { EditableInlineGraphicsWidget } from './visual-widgets/editable-inline-graphics'
-import { CloseBrace, OpenBrace } from '../../lezer-latex/latex.terms.mjs'
+import {
+  CloseBrace,
+  OpenBrace,
+  ShortTextArgument,
+  TextArgument,
+} from '../../lezer-latex/latex.terms.mjs'
 import { FootnoteWidget } from './visual-widgets/footnote'
 import { getListItems } from '../toolbar/lists'
 import { TildeWidget } from './visual-widgets/tilde'
@@ -868,6 +873,24 @@ export const atomicDecorations = (options: Options) => {
             const { name, label } = result
             theoremEnvironments.set(name, label)
           }
+        } else if (
+          nodeRef.type.is('TextColorCommand') ||
+          nodeRef.type.is('ColorBoxCommand')
+        ) {
+          if (shouldDecorate(state, nodeRef)) {
+            const colorArgumentNode = nodeRef.node.getChild(ShortTextArgument)
+            const contentArgumentNode = nodeRef.node.getChild(TextArgument)
+            if (colorArgumentNode && contentArgumentNode) {
+              // command name and opening brace
+              decorations.push(
+                ...decorateArgumentBraces(
+                  new BraceWidget(),
+                  contentArgumentNode,
+                  nodeRef.from
+                )
+              )
+            }
+          }
         } else if (nodeRef.type.is('UnknownCommand')) {
           // a command that's not defined separately by the grammar
           const commandNode = nodeRef.node

+ 35 - 0
services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts

@@ -10,6 +10,7 @@ import { getUnstarredEnvironmentName } from '../../utils/tree-operations/environ
 import { centeringNodeForEnvironment } from '../../utils/tree-operations/figure'
 import { parseTheoremStyles } from '../../utils/tree-operations/theorems'
 import { Tree } from '@lezer/common'
+import { parseColorArguments } from '../../utils/tree-operations/colors'
 
 /**
  * A view plugin that decorates ranges of text with Mark decorations.
@@ -105,6 +106,40 @@ export const markDecorations = ViewPlugin.define(
                   )
                 }
               }
+            } else if (nodeRef.type.is('TextColorCommand')) {
+              const result = parseColorArguments(state, nodeRef.node)
+
+              if (result) {
+                const { color, from, to } = result
+
+                // decorate the content
+                decorations.push(
+                  Decoration.mark({
+                    class: 'ol-cm-textcolor',
+                    inclusive: true,
+                    attributes: {
+                      style: `color: ${color}`,
+                    },
+                  }).range(from, to)
+                )
+              }
+            } else if (nodeRef.type.is('ColorBoxCommand')) {
+              const result = parseColorArguments(state, nodeRef.node)
+
+              if (result) {
+                const { color, from, to } = result
+
+                // decorate the content
+                decorations.push(
+                  Decoration.mark({
+                    class: 'ol-cm-colorbox',
+                    inclusive: true,
+                    attributes: {
+                      style: `background-color: ${color}`,
+                    },
+                  }).range(from, to)
+                )
+              }
             } else if (nodeRef.type.is('$Environment')) {
               const environmentName = getUnstarredEnvironmentName(
                 nodeRef.node,

+ 9 - 1
services/web/frontend/js/features/source-editor/lezer-latex/latex.grammar

@@ -87,7 +87,9 @@
     BibliographyCtrlSeq,
     BibliographyStyleCtrlSeq,
     AuthorCtrlSeq,
-    MaketitleCtrlSeq
+    MaketitleCtrlSeq,
+    TextColorCtrlSeq,
+    ColorBoxCtrlSeq
 }
 
 @external specialize {EnvName} specializeEnvName from "./tokens.mjs" {
@@ -239,6 +241,12 @@ KnownCommand {
         UsePackageCtrlSeq optionalWhitespace? OptionalArgument?
         PackageArgument
     } |
+    TextColorCommand {
+        TextColorCtrlSeq optionalWhitespace? ShortTextArgument optionalWhitespace? TextArgument
+    } |
+    ColorBoxCommand {
+        ColorBoxCtrlSeq optionalWhitespace? ShortTextArgument optionalWhitespace? TextArgument
+    } |
     HrefCommand {
         HrefCtrlSeq optionalWhitespace? UrlArgument ShortTextArgument
     } |

+ 4 - 0
services/web/frontend/js/features/source-editor/lezer-latex/tokens.mjs

@@ -68,6 +68,8 @@ import {
   CenteringCtrlSeq,
   ListEnvName,
   MaketitleCtrlSeq,
+  TextColorCtrlSeq,
+  ColorBoxCtrlSeq,
 } from './latex.terms.mjs'
 
 function nameChar(ch) {
@@ -639,6 +641,8 @@ const otherKnowncommands = {
   '\\bibliography': BibliographyCtrlSeq,
   '\\bibliographystyle': BibliographyStyleCtrlSeq,
   '\\maketitle': MaketitleCtrlSeq,
+  '\\textcolor': TextColorCtrlSeq,
+  '\\colorbox': ColorBoxCtrlSeq,
 }
 // specializer for control sequences
 // return new tokens for specific control sequences

+ 58 - 0
services/web/frontend/js/features/source-editor/utils/tree-operations/colors.ts

@@ -0,0 +1,58 @@
+import { EditorState } from '@codemirror/state'
+import { SyntaxNode } from '@lezer/common'
+import {
+  LongArg,
+  ShortArg,
+  ShortTextArgument,
+  TextArgument,
+} from '../../lezer-latex/latex.terms.mjs'
+
+// basic color definitions from the xcolor package
+// https://github.com/latex3/xcolor/blob/849682246582946835d28c8f9b2081ff2c340e09/xcolor.dtx#L7051-L7093
+const colors = new Map<string, string>([
+  ['red', 'rgb(255,0,0)'],
+  ['green', 'rgb(0,255,0)'],
+  ['blue', 'rgb(0,0,255)'],
+  ['brown', 'rgb(195,127,63)'],
+  ['lime', 'rgb(195,255,0)'],
+  ['orange', 'rgb(255,127,0)'],
+  ['pink', 'rgb(255,195,195)'],
+  ['purple', 'rgb(195,0,63)'],
+  ['teal', 'rgb(0,127,127)'],
+  ['violet', 'rgb(127,0,127)'],
+  ['cyan', 'rgb(0,255,255)'],
+  ['magenta', 'rgb(255,0,255)'],
+  ['yellow', 'rgb(255,255,0)'],
+  ['olive', 'rgb(127,127,0)'],
+  ['black', 'rgb(0,0,0)'],
+  ['darkgray', 'rgb(63,63,63)'],
+  ['gray', 'rgb(127,127,127)'],
+  ['lightgray', 'rgb(195,195,195)'],
+  ['white', 'rgb(255,255,255)'],
+])
+
+export const parseColorArguments = (
+  state: EditorState,
+  node: SyntaxNode
+): { color: string; from: number; to: number } | undefined => {
+  const colorArgumentNode = node.getChild(ShortTextArgument)?.getChild(ShortArg)
+  const contentArgumentNode = node.getChild(TextArgument)?.getChild(LongArg)
+
+  if (colorArgumentNode && contentArgumentNode) {
+    const { from, to } = contentArgumentNode
+
+    if (to > from) {
+      const colorName = state
+        .sliceDoc(colorArgumentNode.from, colorArgumentNode.to)
+        .trim()
+
+      if (colorName) {
+        const color = colors.get(colorName)
+
+        if (color) {
+          return { color, from, to }
+        }
+      }
+    }
+  }
+}

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

@@ -566,6 +566,24 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
     )
   })
 
+  describe('decorates color commands', function () {
+    it('decorates textcolor', function () {
+      cy.get('@first-line').type('\\textcolor{{}red}{{}foo}')
+      cy.get('.ol-cm-textcolor')
+        .should('have.length', 1)
+        .should('have.text', 'foo')
+        .should('have.attr', 'style', 'color: rgb(255,0,0)')
+    })
+
+    it('decorates colorbox', function () {
+      cy.get('@first-line').type('\\colorbox{{}yellow}{{}foo}')
+      cy.get('.ol-cm-colorbox')
+        .should('have.length', 1)
+        .should('have.text', 'foo')
+        .should('have.attr', 'style', 'background-color: rgb(255,255,0)')
+    })
+  })
+
   describe('handling of special characters', function () {
     it('decorates a tilde with a non-breaking space', function () {
       cy.get('@first-line').type('Test~test')