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 13f5a52168..69d0958e8d 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 @@ -78,6 +78,10 @@ import { } from '../../components/table-generator/utils' import { debugConsole } from '@/utils/debugging' import { DescriptionItemWidget } from './visual-widgets/description-item' +import { + createSpaceCommand, + hasSpaceSubstitution, +} from '@/features/source-editor/extensions/visual/visual-widgets/space' type Options = { previewByPath: (path: string) => PreviewPath | null @@ -1199,6 +1203,18 @@ export const atomicDecorations = (options: Options) => { return false } } + } else if (hasSpaceSubstitution(commandName)) { + if (shouldDecorate(state, nodeRef)) { + const replacement = createSpaceCommand(commandName) + if (replacement) { + decorations.push( + Decoration.replace({ + widget: replacement, + }).range(nodeRef.from, nodeRef.to) + ) + return false + } + } } } } diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts index 2b0f454c88..947525022f 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-theme.ts @@ -288,6 +288,9 @@ const mainVisualTheme = EditorView.theme({ overflowWrap: 'break-word', hyphens: 'auto', }, + '.ol-cm-space': { + display: 'inline-block', + }, '.ol-cm-environment-centered': { '&.ol-cm-label-line, &.ol-cm-caption-line': { textAlign: 'center', diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/space.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/space.ts new file mode 100644 index 0000000000..1379243b88 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/space.ts @@ -0,0 +1,61 @@ +import { WidgetType } from '@codemirror/view' + +export class SpaceWidget extends WidgetType { + constructor(public width: string) { + super() + } + + toDOM() { + const element = document.createElement('span') + element.classList.add('ol-cm-space') + element.style.width = this.width + return element + } + + eq(widget: SpaceWidget) { + return widget.width === this.width + } + + updateDOM(element: HTMLElement): boolean { + element.style.width = this.width + return true + } + + ignoreEvent(event: Event) { + return event.type !== 'mousedown' && event.type !== 'mouseup' + } +} + +// https://tex.stackexchange.com/a/74354 +export const COMMAND_WIDTHS = new Map([ + // thin space + ['\\thinspace', 'calc(3em / 18)'], + ['\\,', 'calc(3em / 18)'], + // negative thin space + ['\\negthinspace', 'calc(-3em / 18)'], + ['\\!', 'calc(-3em / 18)'], + // medium space + ['\\medspace', 'calc(4em / 18)'], + ['\\:', 'calc(4em / 18)'], + ['\\>', 'calc(4em / 18)'], + // thick space + ['\\thickspace', 'calc(5em / 18)'], + ['\\;', 'calc(5em / 18)'], + // negative thick space + ['\\negthickspace', 'calc(-5em / 18)'], + // en, em and 2xem spaces + ['\\enspace', '0.5em'], + ['\\quad', '1em'], + ['\\qquad', '2em'], +]) + +export function createSpaceCommand(command: string): SpaceWidget | undefined { + const width = COMMAND_WIDTHS.get(command) + if (width !== undefined) { + return new SpaceWidget(width) + } +} + +export function hasSpaceSubstitution(command: string): boolean { + return COMMAND_WIDTHS.has(command) +} 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 4ffba305d7..161c58d1fc 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 @@ -642,6 +642,22 @@ describe(' in Visual mode', function () { cy.get('@second-line').click() cy.get('@first-line').should('have.text', 'Test ↩ test') }) + + it('decorates spacing commands', function () { + cy.get('@first-line').type('\\thinspace') + cy.get('@second-line').click() + cy.get('@first-line') + .find('.ol-cm-space') + .should('have.attr', 'style', 'width: calc(0.166667em);') + }) + + it('decorates spacing symbols', function () { + cy.get('@first-line').type('\\,') + cy.get('@second-line').click() + cy.get('@first-line') + .find('.ol-cm-space') + .should('have.attr', 'style', 'width: calc(0.166667em);') + }) }) describe('decorates theorems', function () {