From 08c82a24a907c1b88d6f11898be944647d57a7cb Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Tue, 18 Jul 2023 11:52:57 +0100 Subject: [PATCH] [visual] Apply style to the content of color commands (#13726) GitOrigin-RevId: 4ae8b745618e91b487d17c357cdb0e697038b3a3 --- .../extensions/visual/atomic-decorations.ts | 25 +++++++- .../extensions/visual/mark-decorations.ts | 35 +++++++++++ .../source-editor/lezer-latex/latex.grammar | 10 +++- .../source-editor/lezer-latex/tokens.mjs | 4 ++ .../utils/tree-operations/colors.ts | 58 +++++++++++++++++++ .../codemirror-editor-visual.spec.tsx | 18 ++++++ 6 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/utils/tree-operations/colors.ts 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 deabf097af..9b324ac701 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 @@ -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 diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts b/services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts index ffdf11b1dc..a371e14392 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/mark-decorations.ts +++ b/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, diff --git a/services/web/frontend/js/features/source-editor/lezer-latex/latex.grammar b/services/web/frontend/js/features/source-editor/lezer-latex/latex.grammar index a98e67eeac..5d16d41f39 100644 --- a/services/web/frontend/js/features/source-editor/lezer-latex/latex.grammar +++ b/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 } | diff --git a/services/web/frontend/js/features/source-editor/lezer-latex/tokens.mjs b/services/web/frontend/js/features/source-editor/lezer-latex/tokens.mjs index 1bea584bd3..0be5adf312 100644 --- a/services/web/frontend/js/features/source-editor/lezer-latex/tokens.mjs +++ b/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 diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/colors.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/colors.ts new file mode 100644 index 0000000000..4f2b194ca5 --- /dev/null +++ b/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([ + ['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 } + } + } + } + } +} 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 d08651204f..d0383171e8 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 @@ -566,6 +566,24 @@ describe(' 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')