From 7df06c0677b1cf33651b150394a68f7da51656aa Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Tue, 28 Apr 2026 12:37:04 +0200 Subject: [PATCH 01/10] Modified `superscript` and `subscript` text styles to be mutually exclusive. --- .changelog/20260428123311_master.md | 7 + .../src/augmentation.ts | 25 ++- packages/ckeditor5-basic-styles/src/index.ts | 4 + .../src/subscript/subscriptcommand.ts | 91 +++++++++ .../src/subscript/subscriptediting.ts | 7 +- .../src/subscriptconfig.ts | 47 +++++ .../src/superscript/superscriptcommand.ts | 91 +++++++++ .../src/superscript/superscriptediting.ts | 7 +- .../src/superscriptconfig.ts | 47 +++++ .../tests/manual/basic-styles.md | 1 + .../tests/subscript/subscriptcommand.js | 186 ++++++++++++++++++ .../tests/subscript/subscriptediting.js | 2 + .../tests/superscript/superscriptcommand.js | 186 ++++++++++++++++++ .../tests/superscript/superscriptediting.js | 2 + 14 files changed, 697 insertions(+), 6 deletions(-) create mode 100644 .changelog/20260428123311_master.md create mode 100644 packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts create mode 100644 packages/ckeditor5-basic-styles/src/subscriptconfig.ts create mode 100644 packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts create mode 100644 packages/ckeditor5-basic-styles/src/superscriptconfig.ts create mode 100644 packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js create mode 100644 packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js diff --git a/.changelog/20260428123311_master.md b/.changelog/20260428123311_master.md new file mode 100644 index 00000000000..f11f859d884 --- /dev/null +++ b/.changelog/20260428123311_master.md @@ -0,0 +1,7 @@ +--- +type: Fix +scope: + - ckeditor5-basic-styles +--- + +The `superscript` and `subscript` text styles are now mutually exclusive. diff --git a/packages/ckeditor5-basic-styles/src/augmentation.ts b/packages/ckeditor5-basic-styles/src/augmentation.ts index 0d05fac07b5..546829bfe6d 100644 --- a/packages/ckeditor5-basic-styles/src/augmentation.ts +++ b/packages/ckeditor5-basic-styles/src/augmentation.ts @@ -18,8 +18,12 @@ import type { Strikethrough, StrikethroughEditing, StrikethroughUI, + SubscriptCommand, + SubscriptConfig, SubscriptEditing, SubscriptUI, + SuperscriptCommand, + SuperscriptConfig, SuperscriptEditing, SuperscriptUI, Underline, @@ -28,6 +32,23 @@ import type { } from './index.js'; declare module '@ckeditor/ckeditor5-core' { + interface EditorConfig { + + /** + * The configuration of the {@link module:basic-styles/superscript~Superscript superscript feature}. + * + * Read more in {@link module:basic-styles/superscriptconfig~SuperscriptConfig}. + */ + superscript?: SuperscriptConfig; + + /** + * The configuration of the {@link module:basic-styles/subscript~Subscript subscript feature}. + * + * Read more in {@link module:basic-styles/subscriptconfig~SubscriptConfig}. + */ + subscript?: SubscriptConfig; + } + interface PluginsMap { [ Superscript.pluginName ]: Superscript; [ Subscript.pluginName ]: Subscript; @@ -58,8 +79,8 @@ declare module '@ckeditor/ckeditor5-core' { code: AttributeCommand; italic: AttributeCommand; strikethrough: AttributeCommand; - subscript: AttributeCommand; - superscript: AttributeCommand; + subscript: SubscriptCommand; + superscript: SuperscriptCommand; underline: AttributeCommand; } } diff --git a/packages/ckeditor5-basic-styles/src/index.ts b/packages/ckeditor5-basic-styles/src/index.ts index fb3cd8f5794..902698bf38a 100644 --- a/packages/ckeditor5-basic-styles/src/index.ts +++ b/packages/ckeditor5-basic-styles/src/index.ts @@ -22,9 +22,13 @@ export { StrikethroughUI } from './strikethrough/strikethroughui.js'; export { Subscript } from './subscript.js'; export { SubscriptEditing } from './subscript/subscriptediting.js'; export { SubscriptUI } from './subscript/subscriptui.js'; +export { SubscriptCommand } from './subscript/subscriptcommand.js'; +export type { SubscriptConfig } from './subscriptconfig.js'; export { Superscript } from './superscript.js'; export { SuperscriptEditing } from './superscript/superscriptediting.js'; export { SuperscriptUI } from './superscript/superscriptui.js'; +export { SuperscriptCommand } from './superscript/superscriptcommand.js'; +export type { SuperscriptConfig } from './superscriptconfig.js'; export { Underline } from './underline.js'; export { UnderlineEditing } from './underline/underlineediting.js'; export { UnderlineUI } from './underline/underlineui.js'; diff --git a/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts b/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts new file mode 100644 index 00000000000..d839caf16c0 --- /dev/null +++ b/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts @@ -0,0 +1,91 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +/** + * @module basic-styles/subscript/subscriptcommand + */ + +import type { Editor } from '@ckeditor/ckeditor5-core'; +import { ModelDocumentSelection, type ModelElement, type ModelRange } from '@ckeditor/ckeditor5-engine'; + +import { AttributeCommand } from '../attributecommand.js'; + +const SUBSCRIPT = 'subscript'; +const SUPERSCRIPT = 'superscript'; + +/** + * The subscript command. It is registered as the `'subscript'` command + * by {@link module:basic-styles/subscript/subscriptediting~SubscriptEditing}. + * + * In addition to toggling the `subscript` attribute (the behavior provided by + * {@link module:basic-styles/attributecommand~AttributeCommand}), the command enforces mutual exclusion + * with the `superscript` attribute. When `subscript` is applied to a selection that already has + * `superscript`, the `superscript` attribute is removed in the same model change so the operation is a + * single undo step. + * + * The mutual exclusion can be disabled by setting either + * {@link module:basic-styles/subscriptconfig~SubscriptConfig#allowNesting `config.subscript.allowNesting`} + * or {@link module:basic-styles/superscriptconfig~SuperscriptConfig#allowNesting `config.superscript.allowNesting`} + * to `true`. In that case the command behaves the same as the plain + * {@link module:basic-styles/attributecommand~AttributeCommand}. + * + * The exclusion only applies to command execution. Content set through the data pipeline + * (for example `editor.setData( 'x' )`) is not modified by this command. + */ +export class SubscriptCommand extends AttributeCommand { + constructor( editor: Editor ) { + super( editor, SUBSCRIPT ); + } + + /** + * @inheritDoc + */ + public override execute( options: { forceValue?: boolean } = {} ): void { + const editor = this.editor; + const model = editor.model; + const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue; + + if ( !value || _isNestingAllowed( editor ) ) { + super.execute( options ); + + return; + } + + model.change( writer => { + super.execute( options ); + + const selection = model.document.selection; + + if ( selection.isCollapsed ) { + writer.removeSelectionAttribute( SUPERSCRIPT ); + + return; + } + + const ranges = model.schema.getValidRanges( selection.getRanges(), SUPERSCRIPT, { + includeEmptyRanges: true + } ); + + for ( const range of ranges ) { + let itemOrRange: ModelRange | ModelElement = range; + let attributeKey = SUPERSCRIPT; + + if ( range.isCollapsed ) { + itemOrRange = range.start.parent as ModelElement; + attributeKey = ModelDocumentSelection._getStoreAttributeKey( SUPERSCRIPT ); + } + + writer.removeAttribute( attributeKey, itemOrRange ); + } + } ); + } +} + +function _isNestingAllowed( editor: Editor ): boolean { + return Boolean( + editor.config.get( 'superscript.allowNesting' ) || + editor.config.get( 'subscript.allowNesting' ) + ); +} diff --git a/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts b/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts index 3175177ee84..6e6bb0996c2 100644 --- a/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts +++ b/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts @@ -8,7 +8,7 @@ */ import { Plugin } from '@ckeditor/ckeditor5-core'; -import { AttributeCommand } from '../attributecommand.js'; +import { SubscriptCommand } from './subscriptcommand.js'; const SUBSCRIPT = 'subscript'; @@ -38,6 +38,9 @@ export class SubscriptEditing extends Plugin { */ public init(): void { const editor = this.editor; + + editor.config.define( SUBSCRIPT, { allowNesting: false } ); + // Allow sub attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: SUBSCRIPT } ); editor.model.schema.setAttributeProperties( SUBSCRIPT, { @@ -60,6 +63,6 @@ export class SubscriptEditing extends Plugin { } ); // Create sub command. - editor.commands.add( SUBSCRIPT, new AttributeCommand( editor, SUBSCRIPT ) ); + editor.commands.add( SUBSCRIPT, new SubscriptCommand( editor ) ); } } diff --git a/packages/ckeditor5-basic-styles/src/subscriptconfig.ts b/packages/ckeditor5-basic-styles/src/subscriptconfig.ts new file mode 100644 index 00000000000..8c7e8f172c0 --- /dev/null +++ b/packages/ckeditor5-basic-styles/src/subscriptconfig.ts @@ -0,0 +1,47 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +/** + * @module basic-styles/subscriptconfig + */ + +/** + * The configuration of the {@link module:basic-styles/subscript~Subscript subscript feature}. + * + * ```ts + * ClassicEditor + * .create( editorElement, { + * subscript: { + * allowNesting: true + * } + * } ) + * .then( ... ) + * .catch( ... ); + * ``` + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + */ +export interface SubscriptConfig { + + /** + * Whether `subscript` and `superscript` attributes are allowed to coexist on the same text. + * + * By default this is `false`: applying subscript to text that is already superscript removes the + * superscript attribute (and vice versa), matching the behavior of common word processors. + * + * Set to `true` to restore the historical behavior where both attributes can be applied to the same + * text. This is useful for content such as isotope notation (`¹⁴₆C`) or tensor indices (`T^i_j`). + * + * The flag is symmetric with + * {@link module:basic-styles/superscriptconfig~SuperscriptConfig#allowNesting `config.superscript.allowNesting`}: + * if either is set to `true`, both commands skip the mutual-exclusion step. + * + * The flag only affects command execution. Content set through the data pipeline (for example + * `editor.setData( 'x' )`) keeps both attributes regardless of this option. + * + * @default false + */ + allowNesting?: boolean; +} diff --git a/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts b/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts new file mode 100644 index 00000000000..01b64299c64 --- /dev/null +++ b/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts @@ -0,0 +1,91 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +/** + * @module basic-styles/superscript/superscriptcommand + */ + +import type { Editor } from '@ckeditor/ckeditor5-core'; +import { ModelDocumentSelection, type ModelElement, type ModelRange } from '@ckeditor/ckeditor5-engine'; + +import { AttributeCommand } from '../attributecommand.js'; + +const SUPERSCRIPT = 'superscript'; +const SUBSCRIPT = 'subscript'; + +/** + * The superscript command. It is registered as the `'superscript'` command + * by {@link module:basic-styles/superscript/superscriptediting~SuperscriptEditing}. + * + * In addition to toggling the `superscript` attribute (the behavior provided by + * {@link module:basic-styles/attributecommand~AttributeCommand}), the command enforces mutual exclusion + * with the `subscript` attribute. When `superscript` is applied to a selection that already has + * `subscript`, the `subscript` attribute is removed in the same model change so the operation is a + * single undo step. + * + * The mutual exclusion can be disabled by setting either + * {@link module:basic-styles/superscriptconfig~SuperscriptConfig#allowNesting `config.superscript.allowNesting`} + * or {@link module:basic-styles/subscriptconfig~SubscriptConfig#allowNesting `config.subscript.allowNesting`} + * to `true`. In that case the command behaves the same as the plain + * {@link module:basic-styles/attributecommand~AttributeCommand}. + * + * The exclusion only applies to command execution. Content set through the data pipeline + * (for example `editor.setData( 'x' )`) is not modified by this command. + */ +export class SuperscriptCommand extends AttributeCommand { + constructor( editor: Editor ) { + super( editor, SUPERSCRIPT ); + } + + /** + * @inheritDoc + */ + public override execute( options: { forceValue?: boolean } = {} ): void { + const editor = this.editor; + const model = editor.model; + const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue; + + if ( !value || _isNestingAllowed( editor ) ) { + super.execute( options ); + + return; + } + + model.change( writer => { + super.execute( options ); + + const selection = model.document.selection; + + if ( selection.isCollapsed ) { + writer.removeSelectionAttribute( SUBSCRIPT ); + + return; + } + + const ranges = model.schema.getValidRanges( selection.getRanges(), SUBSCRIPT, { + includeEmptyRanges: true + } ); + + for ( const range of ranges ) { + let itemOrRange: ModelRange | ModelElement = range; + let attributeKey = SUBSCRIPT; + + if ( range.isCollapsed ) { + itemOrRange = range.start.parent as ModelElement; + attributeKey = ModelDocumentSelection._getStoreAttributeKey( SUBSCRIPT ); + } + + writer.removeAttribute( attributeKey, itemOrRange ); + } + } ); + } +} + +function _isNestingAllowed( editor: Editor ): boolean { + return Boolean( + editor.config.get( 'superscript.allowNesting' ) || + editor.config.get( 'subscript.allowNesting' ) + ); +} diff --git a/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts b/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts index 0bf66fa8ee3..1d949ec3c91 100644 --- a/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts +++ b/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts @@ -8,7 +8,7 @@ */ import { Plugin } from '@ckeditor/ckeditor5-core'; -import { AttributeCommand } from '../attributecommand.js'; +import { SuperscriptCommand } from './superscriptcommand.js'; const SUPERSCRIPT = 'superscript'; @@ -38,6 +38,9 @@ export class SuperscriptEditing extends Plugin { */ public init(): void { const editor = this.editor; + + editor.config.define( SUPERSCRIPT, { allowNesting: false } ); + // Allow super attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: SUPERSCRIPT } ); editor.model.schema.setAttributeProperties( SUPERSCRIPT, { @@ -60,6 +63,6 @@ export class SuperscriptEditing extends Plugin { } ); // Create super command. - editor.commands.add( SUPERSCRIPT, new AttributeCommand( editor, SUPERSCRIPT ) ); + editor.commands.add( SUPERSCRIPT, new SuperscriptCommand( editor ) ); } } diff --git a/packages/ckeditor5-basic-styles/src/superscriptconfig.ts b/packages/ckeditor5-basic-styles/src/superscriptconfig.ts new file mode 100644 index 00000000000..d9a3c8546c6 --- /dev/null +++ b/packages/ckeditor5-basic-styles/src/superscriptconfig.ts @@ -0,0 +1,47 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +/** + * @module basic-styles/superscriptconfig + */ + +/** + * The configuration of the {@link module:basic-styles/superscript~Superscript superscript feature}. + * + * ```ts + * ClassicEditor + * .create( editorElement, { + * superscript: { + * allowNesting: true + * } + * } ) + * .then( ... ) + * .catch( ... ); + * ``` + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + */ +export interface SuperscriptConfig { + + /** + * Whether `superscript` and `subscript` attributes are allowed to coexist on the same text. + * + * By default this is `false`: applying superscript to text that is already subscript removes the + * subscript attribute (and vice versa), matching the behavior of common word processors. + * + * Set to `true` to restore the historical behavior where both attributes can be applied to the same + * text. This is useful for content such as isotope notation (`¹⁴₆C`) or tensor indices (`T^i_j`). + * + * The flag is symmetric with + * {@link module:basic-styles/subscriptconfig~SubscriptConfig#allowNesting `config.subscript.allowNesting`}: + * if either is set to `true`, both commands skip the mutual-exclusion step. + * + * The flag only affects command execution. Content set through the data pipeline (for example + * `editor.setData( 'x' )`) keeps both attributes regardless of this option. + * + * @default false + */ + allowNesting?: boolean; +} diff --git a/packages/ckeditor5-basic-styles/tests/manual/basic-styles.md b/packages/ckeditor5-basic-styles/tests/manual/basic-styles.md index 11cc0cd3629..6c8bdebd505 100644 --- a/packages/ckeditor5-basic-styles/tests/manual/basic-styles.md +++ b/packages/ckeditor5-basic-styles/tests/manual/basic-styles.md @@ -10,3 +10,4 @@ * superscript X2. 2. The second sentence should bold the following words: `bold`, `600`, `700`, `800`, `900`. 3. Test the bold, italic, strikethrough, underline, code, subscript and superscript features live. +4. Subscript and superscript are mutually exclusive: select subscripted text and click **Superscript** — the text should become superscript only, not both. A single undo (Ctrl/Cmd+Z) should restore the original subscript. diff --git a/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js b/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js new file mode 100644 index 00000000000..f9b6b9cb893 --- /dev/null +++ b/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js @@ -0,0 +1,186 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { SubscriptEditing } from '../../src/subscript/subscriptediting.js'; +import { SuperscriptEditing } from '../../src/superscript/superscriptediting.js'; +import { SubscriptCommand } from '../../src/subscript/subscriptcommand.js'; +import { AttributeCommand } from '../../src/attributecommand.js'; + +import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { UndoEditing } from '@ckeditor/ckeditor5-undo'; +import { _getModelData, _setModelData } from '@ckeditor/ckeditor5-engine'; + +describe( 'SubscriptCommand', () => { + let editor, model, command; + + function createEditor( config = {} ) { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, UndoEditing, SubscriptEditing, SuperscriptEditing ], + ...config + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + command = editor.commands.get( 'subscript' ); + } ); + } + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'instance', () => { + beforeEach( () => createEditor() ); + + it( 'is an instance of SubscriptCommand', () => { + expect( command ).to.be.instanceOf( SubscriptCommand ); + } ); + + it( 'is an instance of AttributeCommand', () => { + expect( command ).to.be.instanceOf( AttributeCommand ); + } ); + + it( 'has the subscript attribute key', () => { + expect( command.attributeKey ).to.equal( 'subscript' ); + } ); + } ); + + describe( 'config defaults', () => { + beforeEach( () => createEditor() ); + + it( 'sets subscript.allowNesting to false by default', () => { + expect( editor.config.get( 'subscript.allowNesting' ) ).to.be.false; + } ); + } ); + + describe( 'execute() with mutual exclusion (default)', () => { + beforeEach( () => createEditor() ); + + it( 'sets subscript on plain text without touching neighbors', () => { + _setModelData( model, '[foo]' ); + + editor.execute( 'subscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true">foo' + ); + } ); + + it( 'removes superscript when applying subscript on superscripted text', () => { + _setModelData( model, '[<$text superscript="true">foo]' ); + + editor.execute( 'subscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true">foo' + ); + } ); + + it( 'removes superscript on a non-collapsed range crossing sup | none | sub regions', () => { + _setModelData( model, + '' + + '[<$text superscript="true">aa' + + 'bb' + + '<$text subscript="true">cc]' + + '' + ); + + editor.execute( 'subscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true">aabbcc' + ); + } ); + + it( 'does not touch superscript when toggling subscript off', () => { + _setModelData( model, '[<$text subscript="true">foo]' ); + + editor.execute( 'subscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + 'foo' + ); + } ); + + it( 'does not touch superscript adjacent to plain text when toggling off via forceValue:false', () => { + _setModelData( model, + '[foo]<$text superscript="true">bar' + ); + + editor.execute( 'subscript', { forceValue: false } ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + 'foo<$text superscript="true">bar' + ); + } ); + + it( 'removes superscript when forceValue:true on a superscripted range', () => { + _setModelData( model, '[<$text superscript="true">foo]' ); + + editor.execute( 'subscript', { forceValue: true } ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true">foo' + ); + } ); + + it( 'flips selection attributes on a collapsed selection inside superscripted text', () => { + _setModelData( model, '<$text superscript="true">foo[]bar' ); + + editor.execute( 'subscript' ); + + const selection = model.document.selection; + + expect( selection.hasAttribute( 'subscript' ) ).to.be.true; + expect( selection.hasAttribute( 'superscript' ) ).to.be.false; + } ); + + it( 'restores the original superscript with a single undo step', () => { + _setModelData( model, '[<$text superscript="true">foo]' ); + + editor.execute( 'subscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true">foo' + ); + + editor.execute( 'undo' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text superscript="true">foo' + ); + } ); + } ); + + describe( 'execute() with allowNesting on the subscript side', () => { + beforeEach( () => createEditor( { subscript: { allowNesting: true } } ) ); + + it( 'preserves superscript when applying subscript', () => { + _setModelData( model, '[<$text superscript="true">foo]' ); + + editor.execute( 'subscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true" superscript="true">foo' + ); + } ); + } ); + + describe( 'execute() with allowNesting on the superscript side (OR semantics)', () => { + beforeEach( () => createEditor( { superscript: { allowNesting: true } } ) ); + + it( 'preserves superscript when applying subscript', () => { + _setModelData( model, '[<$text superscript="true">foo]' ); + + editor.execute( 'subscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true" superscript="true">foo' + ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-basic-styles/tests/subscript/subscriptediting.js b/packages/ckeditor5-basic-styles/tests/subscript/subscriptediting.js index d792809b809..702b7b14cb5 100644 --- a/packages/ckeditor5-basic-styles/tests/subscript/subscriptediting.js +++ b/packages/ckeditor5-basic-styles/tests/subscript/subscriptediting.js @@ -8,6 +8,7 @@ import { SubscriptEditing } from '../../src/subscript/subscriptediting.js'; import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js'; import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; import { AttributeCommand } from '../../src/attributecommand.js'; +import { SubscriptCommand } from '../../src/subscript/subscriptcommand.js'; import { _getModelData, _setModelData, _getViewData } from '@ckeditor/ckeditor5-engine'; @@ -66,6 +67,7 @@ describe( 'SubscriptEditing', () => { it( 'should register subscript command', () => { const command = editor.commands.get( 'subscript' ); + expect( command ).to.be.instanceOf( SubscriptCommand ); expect( command ).to.be.instanceOf( AttributeCommand ); expect( command ).to.have.property( 'attributeKey', 'subscript' ); } ); diff --git a/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js b/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js new file mode 100644 index 00000000000..0bde0291a73 --- /dev/null +++ b/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js @@ -0,0 +1,186 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +import { SuperscriptEditing } from '../../src/superscript/superscriptediting.js'; +import { SubscriptEditing } from '../../src/subscript/subscriptediting.js'; +import { SuperscriptCommand } from '../../src/superscript/superscriptcommand.js'; +import { AttributeCommand } from '../../src/attributecommand.js'; + +import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js'; +import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; +import { UndoEditing } from '@ckeditor/ckeditor5-undo'; +import { _getModelData, _setModelData } from '@ckeditor/ckeditor5-engine'; + +describe( 'SuperscriptCommand', () => { + let editor, model, command; + + function createEditor( config = {} ) { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, UndoEditing, SuperscriptEditing, SubscriptEditing ], + ...config + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + command = editor.commands.get( 'superscript' ); + } ); + } + + afterEach( () => { + return editor.destroy(); + } ); + + describe( 'instance', () => { + beforeEach( () => createEditor() ); + + it( 'is an instance of SuperscriptCommand', () => { + expect( command ).to.be.instanceOf( SuperscriptCommand ); + } ); + + it( 'is an instance of AttributeCommand', () => { + expect( command ).to.be.instanceOf( AttributeCommand ); + } ); + + it( 'has the superscript attribute key', () => { + expect( command.attributeKey ).to.equal( 'superscript' ); + } ); + } ); + + describe( 'config defaults', () => { + beforeEach( () => createEditor() ); + + it( 'sets superscript.allowNesting to false by default', () => { + expect( editor.config.get( 'superscript.allowNesting' ) ).to.be.false; + } ); + } ); + + describe( 'execute() with mutual exclusion (default)', () => { + beforeEach( () => createEditor() ); + + it( 'sets superscript on plain text without touching neighbors', () => { + _setModelData( model, '[foo]' ); + + editor.execute( 'superscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text superscript="true">foo' + ); + } ); + + it( 'removes subscript when applying superscript on subscripted text', () => { + _setModelData( model, '[<$text subscript="true">foo]' ); + + editor.execute( 'superscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text superscript="true">foo' + ); + } ); + + it( 'removes subscript on a non-collapsed range crossing sub | none | sup regions', () => { + _setModelData( model, + '' + + '[<$text subscript="true">aa' + + 'bb' + + '<$text superscript="true">cc]' + + '' + ); + + editor.execute( 'superscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text superscript="true">aabbcc' + ); + } ); + + it( 'does not touch subscript when toggling superscript off', () => { + _setModelData( model, '[<$text superscript="true">foo]' ); + + editor.execute( 'superscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + 'foo' + ); + } ); + + it( 'does not touch subscript adjacent to plain text when toggling off via forceValue:false', () => { + _setModelData( model, + '[foo]<$text subscript="true">bar' + ); + + editor.execute( 'superscript', { forceValue: false } ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + 'foo<$text subscript="true">bar' + ); + } ); + + it( 'removes subscript when forceValue:true on a subscripted range', () => { + _setModelData( model, '[<$text subscript="true">foo]' ); + + editor.execute( 'superscript', { forceValue: true } ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text superscript="true">foo' + ); + } ); + + it( 'flips selection attributes on a collapsed selection inside subscripted text', () => { + _setModelData( model, '<$text subscript="true">foo[]bar' ); + + editor.execute( 'superscript' ); + + const selection = model.document.selection; + + expect( selection.hasAttribute( 'superscript' ) ).to.be.true; + expect( selection.hasAttribute( 'subscript' ) ).to.be.false; + } ); + + it( 'restores the original subscript with a single undo step', () => { + _setModelData( model, '[<$text subscript="true">foo]' ); + + editor.execute( 'superscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text superscript="true">foo' + ); + + editor.execute( 'undo' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true">foo' + ); + } ); + } ); + + describe( 'execute() with allowNesting on the superscript side', () => { + beforeEach( () => createEditor( { superscript: { allowNesting: true } } ) ); + + it( 'preserves subscript when applying superscript', () => { + _setModelData( model, '[<$text subscript="true">foo]' ); + + editor.execute( 'superscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true" superscript="true">foo' + ); + } ); + } ); + + describe( 'execute() with allowNesting on the subscript side (OR semantics)', () => { + beforeEach( () => createEditor( { subscript: { allowNesting: true } } ) ); + + it( 'preserves subscript when applying superscript', () => { + _setModelData( model, '[<$text subscript="true">foo]' ); + + editor.execute( 'superscript' ); + + expect( _getModelData( model, { withoutSelection: true } ) ).to.equal( + '<$text subscript="true" superscript="true">foo' + ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-basic-styles/tests/superscript/superscriptediting.js b/packages/ckeditor5-basic-styles/tests/superscript/superscriptediting.js index 3626ddc67c7..63e34af8803 100644 --- a/packages/ckeditor5-basic-styles/tests/superscript/superscriptediting.js +++ b/packages/ckeditor5-basic-styles/tests/superscript/superscriptediting.js @@ -8,6 +8,7 @@ import { SuperscriptEditing } from '../../src/superscript/superscriptediting.js' import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js'; import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; import { AttributeCommand } from '../../src/attributecommand.js'; +import { SuperscriptCommand } from '../../src/superscript/superscriptcommand.js'; import { _getModelData, _setModelData, _getViewData } from '@ckeditor/ckeditor5-engine'; @@ -66,6 +67,7 @@ describe( 'SuperscriptEditing', () => { it( 'should register superscript command', () => { const command = editor.commands.get( 'superscript' ); + expect( command ).to.be.instanceOf( SuperscriptCommand ); expect( command ).to.be.instanceOf( AttributeCommand ); expect( command ).to.have.property( 'attributeKey', 'superscript' ); } ); From 4fa113b6c9cac9202f71aac13f841d123f351cb4 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Tue, 28 Apr 2026 12:41:06 +0200 Subject: [PATCH 02/10] Updated docs. --- .../docs/features/basic-styles.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/ckeditor5-basic-styles/docs/features/basic-styles.md b/packages/ckeditor5-basic-styles/docs/features/basic-styles.md index 11da6b20f13..a5fc8b582cd 100644 --- a/packages/ckeditor5-basic-styles/docs/features/basic-styles.md +++ b/packages/ckeditor5-basic-styles/docs/features/basic-styles.md @@ -63,6 +63,30 @@ CKEditor 5 allows for typing both at the inner and outer boundaries of code {@img assets/img/typing-after-code.gif 770 The animation showing typing after the code element in CKEditor 5 rich text editor.} +## Subscript and superscript exclusivity + +By default, the {@link module:basic-styles/subscript~Subscript subscript} and {@link module:basic-styles/superscript~Superscript superscript} features are mutually exclusive: applying one to text that already has the other replaces it, matching the behavior of common word processors. Toggling a style off does not affect the other style. + +To allow nesting, set the `allowNesting` option on either feature: + +```js +ClassicEditor + .create( { + // ... Other configuration options ... + superscript: { + allowNesting: true + } + } ) + .then( /* ... */ ) + .catch( /* ... */ ); +``` + +The flag is symmetric: setting `superscript.allowNesting` or `subscript.allowNesting` to `true` disables the mutual exclusion for both commands. + + + The mutual exclusion only applies to command execution. Loading content through the {@link module:core/editor/editor~Editor#setData data API} or pasting HTML such as `x` keeps both attributes on the same text regardless of this option. + + ## Installation After {@link getting-started/integrations-cdn/quick-start installing the editor}, add the plugins which you need to your plugin list. Then, simply configure the toolbar items to make the features available in the user interface. From cc7aedee57b6e9ba6178201111f6ce6efec61500 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Tue, 28 Apr 2026 14:36:39 +0200 Subject: [PATCH 03/10] Fixed coverage. --- .../tests/subscript/subscriptcommand.js | 14 ++++++++++++++ .../tests/superscript/superscriptcommand.js | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js b/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js index f9b6b9cb893..d4e9735e934 100644 --- a/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js +++ b/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js @@ -139,6 +139,20 @@ describe( 'SubscriptCommand', () => { expect( selection.hasAttribute( 'superscript' ) ).to.be.false; } ); + it( 'removes the stored superscript on empty blocks inside a multi-block selection', () => { + _setModelData( model, '[foofoo]' ); + + editor.execute( 'superscript' ); + editor.execute( 'subscript' ); + + model.change( writer => { + writer.setSelection( model.document.getRoot().getNodeByPath( [ 1 ] ), 0 ); + } ); + + expect( model.document.selection.hasAttribute( 'superscript' ) ).to.be.false; + expect( model.document.selection.hasAttribute( 'subscript' ) ).to.be.true; + } ); + it( 'restores the original superscript with a single undo step', () => { _setModelData( model, '[<$text superscript="true">foo]' ); diff --git a/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js b/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js index 0bde0291a73..11895afedda 100644 --- a/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js +++ b/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js @@ -139,6 +139,20 @@ describe( 'SuperscriptCommand', () => { expect( selection.hasAttribute( 'subscript' ) ).to.be.false; } ); + it( 'removes the stored subscript on empty blocks inside a multi-block selection', () => { + _setModelData( model, '[foofoo]' ); + + editor.execute( 'subscript' ); + editor.execute( 'superscript' ); + + model.change( writer => { + writer.setSelection( model.document.getRoot().getNodeByPath( [ 1 ] ), 0 ); + } ); + + expect( model.document.selection.hasAttribute( 'subscript' ) ).to.be.false; + expect( model.document.selection.hasAttribute( 'superscript' ) ).to.be.true; + } ); + it( 'restores the original subscript with a single undo step', () => { _setModelData( model, '[<$text subscript="true">foo]' ); From 705a5a2b99b5d6f60fa6cec7a2c971d850527b9c Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Tue, 28 Apr 2026 15:23:04 +0200 Subject: [PATCH 04/10] Fixed validation errors. --- packages/ckeditor5-basic-styles/package.json | 3 ++- packages/ckeditor5-basic-styles/src/augmentation.ts | 12 ++++++------ packages/ckeditor5-basic-styles/src/index.ts | 4 ++-- .../src/subscript/subscriptcommand.ts | 4 ++-- .../ckeditor5-basic-styles/src/subscriptconfig.ts | 4 ++-- .../src/superscript/superscriptcommand.ts | 4 ++-- .../ckeditor5-basic-styles/src/superscriptconfig.ts | 4 ++-- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-basic-styles/package.json b/packages/ckeditor5-basic-styles/package.json index cc8665af8dc..5453675fc53 100644 --- a/packages/ckeditor5-basic-styles/package.json +++ b/packages/ckeditor5-basic-styles/package.json @@ -45,7 +45,8 @@ "devDependencies": { "@ckeditor/ckeditor5-editor-classic": "workspace:*", "@ckeditor/ckeditor5-essentials": "workspace:*", - "@ckeditor/ckeditor5-paragraph": "workspace:*" + "@ckeditor/ckeditor5-paragraph": "workspace:*", + "@ckeditor/ckeditor5-undo": "workspace:*" }, "scripts": { "build": "node ../../scripts/nim/build-package.mjs" diff --git a/packages/ckeditor5-basic-styles/src/augmentation.ts b/packages/ckeditor5-basic-styles/src/augmentation.ts index 546829bfe6d..78e82d5b0ff 100644 --- a/packages/ckeditor5-basic-styles/src/augmentation.ts +++ b/packages/ckeditor5-basic-styles/src/augmentation.ts @@ -19,11 +19,11 @@ import type { StrikethroughEditing, StrikethroughUI, SubscriptCommand, - SubscriptConfig, + BasicStyleSubscriptConfig, SubscriptEditing, SubscriptUI, SuperscriptCommand, - SuperscriptConfig, + BasicStyleSuperscriptConfig, SuperscriptEditing, SuperscriptUI, Underline, @@ -37,16 +37,16 @@ declare module '@ckeditor/ckeditor5-core' { /** * The configuration of the {@link module:basic-styles/superscript~Superscript superscript feature}. * - * Read more in {@link module:basic-styles/superscriptconfig~SuperscriptConfig}. + * Read more in {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig}. */ - superscript?: SuperscriptConfig; + superscript?: BasicStyleSuperscriptConfig; /** * The configuration of the {@link module:basic-styles/subscript~Subscript subscript feature}. * - * Read more in {@link module:basic-styles/subscriptconfig~SubscriptConfig}. + * Read more in {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig}. */ - subscript?: SubscriptConfig; + subscript?: BasicStyleSubscriptConfig; } interface PluginsMap { diff --git a/packages/ckeditor5-basic-styles/src/index.ts b/packages/ckeditor5-basic-styles/src/index.ts index 902698bf38a..3f1ca7cbd00 100644 --- a/packages/ckeditor5-basic-styles/src/index.ts +++ b/packages/ckeditor5-basic-styles/src/index.ts @@ -23,12 +23,12 @@ export { Subscript } from './subscript.js'; export { SubscriptEditing } from './subscript/subscriptediting.js'; export { SubscriptUI } from './subscript/subscriptui.js'; export { SubscriptCommand } from './subscript/subscriptcommand.js'; -export type { SubscriptConfig } from './subscriptconfig.js'; +export type { BasicStyleSubscriptConfig } from './subscriptconfig.js'; export { Superscript } from './superscript.js'; export { SuperscriptEditing } from './superscript/superscriptediting.js'; export { SuperscriptUI } from './superscript/superscriptui.js'; export { SuperscriptCommand } from './superscript/superscriptcommand.js'; -export type { SuperscriptConfig } from './superscriptconfig.js'; +export type { BasicStyleSuperscriptConfig } from './superscriptconfig.js'; export { Underline } from './underline.js'; export { UnderlineEditing } from './underline/underlineediting.js'; export { UnderlineUI } from './underline/underlineui.js'; diff --git a/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts b/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts index d839caf16c0..548e812aae0 100644 --- a/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts +++ b/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts @@ -26,8 +26,8 @@ const SUPERSCRIPT = 'superscript'; * single undo step. * * The mutual exclusion can be disabled by setting either - * {@link module:basic-styles/subscriptconfig~SubscriptConfig#allowNesting `config.subscript.allowNesting`} - * or {@link module:basic-styles/superscriptconfig~SuperscriptConfig#allowNesting `config.superscript.allowNesting`} + * {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig#allowNesting `config.subscript.allowNesting`} + * or {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.superscript.allowNesting`} * to `true`. In that case the command behaves the same as the plain * {@link module:basic-styles/attributecommand~AttributeCommand}. * diff --git a/packages/ckeditor5-basic-styles/src/subscriptconfig.ts b/packages/ckeditor5-basic-styles/src/subscriptconfig.ts index 8c7e8f172c0..b17711a6832 100644 --- a/packages/ckeditor5-basic-styles/src/subscriptconfig.ts +++ b/packages/ckeditor5-basic-styles/src/subscriptconfig.ts @@ -23,7 +23,7 @@ * * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. */ -export interface SubscriptConfig { +export interface BasicStyleSubscriptConfig { /** * Whether `subscript` and `superscript` attributes are allowed to coexist on the same text. @@ -35,7 +35,7 @@ export interface SubscriptConfig { * text. This is useful for content such as isotope notation (`¹⁴₆C`) or tensor indices (`T^i_j`). * * The flag is symmetric with - * {@link module:basic-styles/superscriptconfig~SuperscriptConfig#allowNesting `config.superscript.allowNesting`}: + * {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.superscript.allowNesting`}: * if either is set to `true`, both commands skip the mutual-exclusion step. * * The flag only affects command execution. Content set through the data pipeline (for example diff --git a/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts b/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts index 01b64299c64..a3215294491 100644 --- a/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts +++ b/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts @@ -26,8 +26,8 @@ const SUBSCRIPT = 'subscript'; * single undo step. * * The mutual exclusion can be disabled by setting either - * {@link module:basic-styles/superscriptconfig~SuperscriptConfig#allowNesting `config.superscript.allowNesting`} - * or {@link module:basic-styles/subscriptconfig~SubscriptConfig#allowNesting `config.subscript.allowNesting`} + * {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.superscript.allowNesting`} + * or {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig#allowNesting `config.subscript.allowNesting`} * to `true`. In that case the command behaves the same as the plain * {@link module:basic-styles/attributecommand~AttributeCommand}. * diff --git a/packages/ckeditor5-basic-styles/src/superscriptconfig.ts b/packages/ckeditor5-basic-styles/src/superscriptconfig.ts index d9a3c8546c6..0950c96784f 100644 --- a/packages/ckeditor5-basic-styles/src/superscriptconfig.ts +++ b/packages/ckeditor5-basic-styles/src/superscriptconfig.ts @@ -23,7 +23,7 @@ * * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. */ -export interface SuperscriptConfig { +export interface BasicStyleSuperscriptConfig { /** * Whether `superscript` and `subscript` attributes are allowed to coexist on the same text. @@ -35,7 +35,7 @@ export interface SuperscriptConfig { * text. This is useful for content such as isotope notation (`¹⁴₆C`) or tensor indices (`T^i_j`). * * The flag is symmetric with - * {@link module:basic-styles/subscriptconfig~SubscriptConfig#allowNesting `config.subscript.allowNesting`}: + * {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig#allowNesting `config.subscript.allowNesting`}: * if either is set to `true`, both commands skip the mutual-exclusion step. * * The flag only affects command execution. Content set through the data pipeline (for example From 44ef9f647d7bdacbc6faec2881c7a88b4f0ee8f0 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Tue, 28 Apr 2026 15:26:02 +0200 Subject: [PATCH 05/10] Updated lock. --- pnpm-lock.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a14e8510686..b81831057f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -574,6 +574,9 @@ importers: '@ckeditor/ckeditor5-paragraph': specifier: workspace:* version: link:../ckeditor5-paragraph + '@ckeditor/ckeditor5-undo': + specifier: workspace:* + version: link:../ckeditor5-undo packages/ckeditor5-block-quote: dependencies: From 371768592079a9e935e18c60b969c398fa6f7177 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 30 Apr 2026 10:13:38 +0200 Subject: [PATCH 06/10] Unified `SubscriptCommand` and `SuperscriptCommand` classes. --- .../src/augmentation.ts | 6 +- packages/ckeditor5-basic-styles/src/index.ts | 2 - ...s => mutuallyexclusiveattributecommand.ts} | 43 +++++---- .../src/subscript/subscriptcommand.ts | 91 ------------------- .../src/subscript/subscriptediting.ts | 5 +- .../src/superscript/superscriptediting.ts | 5 +- .../tests/subscript/subscriptcommand.js | 6 +- .../tests/subscript/subscriptediting.js | 4 +- .../tests/superscript/superscriptcommand.js | 6 +- .../tests/superscript/superscriptediting.js | 4 +- 10 files changed, 41 insertions(+), 131 deletions(-) rename packages/ckeditor5-basic-styles/src/{superscript/superscriptcommand.ts => mutuallyexclusiveattributecommand.ts} (65%) delete mode 100644 packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts diff --git a/packages/ckeditor5-basic-styles/src/augmentation.ts b/packages/ckeditor5-basic-styles/src/augmentation.ts index 78e82d5b0ff..53d27d52a70 100644 --- a/packages/ckeditor5-basic-styles/src/augmentation.ts +++ b/packages/ckeditor5-basic-styles/src/augmentation.ts @@ -18,11 +18,9 @@ import type { Strikethrough, StrikethroughEditing, StrikethroughUI, - SubscriptCommand, BasicStyleSubscriptConfig, SubscriptEditing, SubscriptUI, - SuperscriptCommand, BasicStyleSuperscriptConfig, SuperscriptEditing, SuperscriptUI, @@ -79,8 +77,8 @@ declare module '@ckeditor/ckeditor5-core' { code: AttributeCommand; italic: AttributeCommand; strikethrough: AttributeCommand; - subscript: SubscriptCommand; - superscript: SuperscriptCommand; + subscript: AttributeCommand; + superscript: AttributeCommand; underline: AttributeCommand; } } diff --git a/packages/ckeditor5-basic-styles/src/index.ts b/packages/ckeditor5-basic-styles/src/index.ts index 3f1ca7cbd00..953c1a21988 100644 --- a/packages/ckeditor5-basic-styles/src/index.ts +++ b/packages/ckeditor5-basic-styles/src/index.ts @@ -22,12 +22,10 @@ export { StrikethroughUI } from './strikethrough/strikethroughui.js'; export { Subscript } from './subscript.js'; export { SubscriptEditing } from './subscript/subscriptediting.js'; export { SubscriptUI } from './subscript/subscriptui.js'; -export { SubscriptCommand } from './subscript/subscriptcommand.js'; export type { BasicStyleSubscriptConfig } from './subscriptconfig.js'; export { Superscript } from './superscript.js'; export { SuperscriptEditing } from './superscript/superscriptediting.js'; export { SuperscriptUI } from './superscript/superscriptui.js'; -export { SuperscriptCommand } from './superscript/superscriptcommand.js'; export type { BasicStyleSuperscriptConfig } from './superscriptconfig.js'; export { Underline } from './underline.js'; export { UnderlineEditing } from './underline/underlineediting.js'; diff --git a/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts b/packages/ckeditor5-basic-styles/src/mutuallyexclusiveattributecommand.ts similarity index 65% rename from packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts rename to packages/ckeditor5-basic-styles/src/mutuallyexclusiveattributecommand.ts index a3215294491..1eba1d420f4 100644 --- a/packages/ckeditor5-basic-styles/src/superscript/superscriptcommand.ts +++ b/packages/ckeditor5-basic-styles/src/mutuallyexclusiveattributecommand.ts @@ -4,39 +4,40 @@ */ /** - * @module basic-styles/superscript/superscriptcommand + * @module basic-styles/mutuallyexclusiveattributecommand */ import type { Editor } from '@ckeditor/ckeditor5-core'; import { ModelDocumentSelection, type ModelElement, type ModelRange } from '@ckeditor/ckeditor5-engine'; -import { AttributeCommand } from '../attributecommand.js'; - -const SUPERSCRIPT = 'superscript'; -const SUBSCRIPT = 'subscript'; +import { AttributeCommand } from './attributecommand.js'; /** - * The superscript command. It is registered as the `'superscript'` command - * by {@link module:basic-styles/superscript/superscriptediting~SuperscriptEditing}. + * An {@link module:basic-styles/attributecommand~AttributeCommand} variant that removes a configured + * opposite attribute from the affected ranges whenever the command turns its own attribute on. * - * In addition to toggling the `superscript` attribute (the behavior provided by - * {@link module:basic-styles/attributecommand~AttributeCommand}), the command enforces mutual exclusion - * with the `subscript` attribute. When `superscript` is applied to a selection that already has - * `subscript`, the `subscript` attribute is removed in the same model change so the operation is a - * single undo step. + * Used by the `superscript` and `subscript` commands to enforce their mutual exclusion. The opposite + * attribute is removed in the same model change as the parent's toggle, so the operation is a single + * undo step. * * The mutual exclusion can be disabled by setting either * {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.superscript.allowNesting`} * or {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig#allowNesting `config.subscript.allowNesting`} - * to `true`. In that case the command behaves the same as the plain + * to `true`. In that case, the command behaves exactly the same as the plain * {@link module:basic-styles/attributecommand~AttributeCommand}. * * The exclusion only applies to command execution. Content set through the data pipeline * (for example `editor.setData( 'x' )`) is not modified by this command. + * + * @internal */ -export class SuperscriptCommand extends AttributeCommand { - constructor( editor: Editor ) { - super( editor, SUPERSCRIPT ); +export class MutuallyExclusiveAttributeCommand extends AttributeCommand { + private readonly _oppositeAttributeKey: string; + + constructor( editor: Editor, attributeKey: string, oppositeAttributeKey: string ) { + super( editor, attributeKey ); + + this._oppositeAttributeKey = oppositeAttributeKey; } /** @@ -53,28 +54,30 @@ export class SuperscriptCommand extends AttributeCommand { return; } + const oppositeKey = this._oppositeAttributeKey; + model.change( writer => { super.execute( options ); const selection = model.document.selection; if ( selection.isCollapsed ) { - writer.removeSelectionAttribute( SUBSCRIPT ); + writer.removeSelectionAttribute( oppositeKey ); return; } - const ranges = model.schema.getValidRanges( selection.getRanges(), SUBSCRIPT, { + const ranges = model.schema.getValidRanges( selection.getRanges(), oppositeKey, { includeEmptyRanges: true } ); for ( const range of ranges ) { let itemOrRange: ModelRange | ModelElement = range; - let attributeKey = SUBSCRIPT; + let attributeKey = oppositeKey; if ( range.isCollapsed ) { itemOrRange = range.start.parent as ModelElement; - attributeKey = ModelDocumentSelection._getStoreAttributeKey( SUBSCRIPT ); + attributeKey = ModelDocumentSelection._getStoreAttributeKey( oppositeKey ); } writer.removeAttribute( attributeKey, itemOrRange ); diff --git a/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts b/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts deleted file mode 100644 index 548e812aae0..00000000000 --- a/packages/ckeditor5-basic-styles/src/subscript/subscriptcommand.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options - */ - -/** - * @module basic-styles/subscript/subscriptcommand - */ - -import type { Editor } from '@ckeditor/ckeditor5-core'; -import { ModelDocumentSelection, type ModelElement, type ModelRange } from '@ckeditor/ckeditor5-engine'; - -import { AttributeCommand } from '../attributecommand.js'; - -const SUBSCRIPT = 'subscript'; -const SUPERSCRIPT = 'superscript'; - -/** - * The subscript command. It is registered as the `'subscript'` command - * by {@link module:basic-styles/subscript/subscriptediting~SubscriptEditing}. - * - * In addition to toggling the `subscript` attribute (the behavior provided by - * {@link module:basic-styles/attributecommand~AttributeCommand}), the command enforces mutual exclusion - * with the `superscript` attribute. When `subscript` is applied to a selection that already has - * `superscript`, the `superscript` attribute is removed in the same model change so the operation is a - * single undo step. - * - * The mutual exclusion can be disabled by setting either - * {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig#allowNesting `config.subscript.allowNesting`} - * or {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.superscript.allowNesting`} - * to `true`. In that case the command behaves the same as the plain - * {@link module:basic-styles/attributecommand~AttributeCommand}. - * - * The exclusion only applies to command execution. Content set through the data pipeline - * (for example `editor.setData( 'x' )`) is not modified by this command. - */ -export class SubscriptCommand extends AttributeCommand { - constructor( editor: Editor ) { - super( editor, SUBSCRIPT ); - } - - /** - * @inheritDoc - */ - public override execute( options: { forceValue?: boolean } = {} ): void { - const editor = this.editor; - const model = editor.model; - const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue; - - if ( !value || _isNestingAllowed( editor ) ) { - super.execute( options ); - - return; - } - - model.change( writer => { - super.execute( options ); - - const selection = model.document.selection; - - if ( selection.isCollapsed ) { - writer.removeSelectionAttribute( SUPERSCRIPT ); - - return; - } - - const ranges = model.schema.getValidRanges( selection.getRanges(), SUPERSCRIPT, { - includeEmptyRanges: true - } ); - - for ( const range of ranges ) { - let itemOrRange: ModelRange | ModelElement = range; - let attributeKey = SUPERSCRIPT; - - if ( range.isCollapsed ) { - itemOrRange = range.start.parent as ModelElement; - attributeKey = ModelDocumentSelection._getStoreAttributeKey( SUPERSCRIPT ); - } - - writer.removeAttribute( attributeKey, itemOrRange ); - } - } ); - } -} - -function _isNestingAllowed( editor: Editor ): boolean { - return Boolean( - editor.config.get( 'superscript.allowNesting' ) || - editor.config.get( 'subscript.allowNesting' ) - ); -} diff --git a/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts b/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts index 6e6bb0996c2..ea339719bcd 100644 --- a/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts +++ b/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts @@ -8,9 +8,10 @@ */ import { Plugin } from '@ckeditor/ckeditor5-core'; -import { SubscriptCommand } from './subscriptcommand.js'; +import { MutuallyExclusiveAttributeCommand } from '../mutuallyexclusiveattributecommand.js'; const SUBSCRIPT = 'subscript'; +const SUPERSCRIPT = 'superscript'; /** * The subscript editing feature. @@ -63,6 +64,6 @@ export class SubscriptEditing extends Plugin { } ); // Create sub command. - editor.commands.add( SUBSCRIPT, new SubscriptCommand( editor ) ); + editor.commands.add( SUBSCRIPT, new MutuallyExclusiveAttributeCommand( editor, SUBSCRIPT, SUPERSCRIPT ) ); } } diff --git a/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts b/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts index 1d949ec3c91..27109ef3138 100644 --- a/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts +++ b/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts @@ -8,9 +8,10 @@ */ import { Plugin } from '@ckeditor/ckeditor5-core'; -import { SuperscriptCommand } from './superscriptcommand.js'; +import { MutuallyExclusiveAttributeCommand } from '../mutuallyexclusiveattributecommand.js'; const SUPERSCRIPT = 'superscript'; +const SUBSCRIPT = 'subscript'; /** * The superscript editing feature. @@ -63,6 +64,6 @@ export class SuperscriptEditing extends Plugin { } ); // Create super command. - editor.commands.add( SUPERSCRIPT, new SuperscriptCommand( editor ) ); + editor.commands.add( SUPERSCRIPT, new MutuallyExclusiveAttributeCommand( editor, SUPERSCRIPT, SUBSCRIPT ) ); } } diff --git a/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js b/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js index d4e9735e934..205739fb4d6 100644 --- a/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js +++ b/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js @@ -5,7 +5,7 @@ import { SubscriptEditing } from '../../src/subscript/subscriptediting.js'; import { SuperscriptEditing } from '../../src/superscript/superscriptediting.js'; -import { SubscriptCommand } from '../../src/subscript/subscriptcommand.js'; +import { MutuallyExclusiveAttributeCommand } from '../../src/mutuallyexclusiveattributecommand.js'; import { AttributeCommand } from '../../src/attributecommand.js'; import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js'; @@ -36,8 +36,8 @@ describe( 'SubscriptCommand', () => { describe( 'instance', () => { beforeEach( () => createEditor() ); - it( 'is an instance of SubscriptCommand', () => { - expect( command ).to.be.instanceOf( SubscriptCommand ); + it( 'is an instance of MutuallyExclusiveAttributeCommand', () => { + expect( command ).to.be.instanceOf( MutuallyExclusiveAttributeCommand ); } ); it( 'is an instance of AttributeCommand', () => { diff --git a/packages/ckeditor5-basic-styles/tests/subscript/subscriptediting.js b/packages/ckeditor5-basic-styles/tests/subscript/subscriptediting.js index 702b7b14cb5..7918ecd496b 100644 --- a/packages/ckeditor5-basic-styles/tests/subscript/subscriptediting.js +++ b/packages/ckeditor5-basic-styles/tests/subscript/subscriptediting.js @@ -8,7 +8,7 @@ import { SubscriptEditing } from '../../src/subscript/subscriptediting.js'; import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js'; import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; import { AttributeCommand } from '../../src/attributecommand.js'; -import { SubscriptCommand } from '../../src/subscript/subscriptcommand.js'; +import { MutuallyExclusiveAttributeCommand } from '../../src/mutuallyexclusiveattributecommand.js'; import { _getModelData, _setModelData, _getViewData } from '@ckeditor/ckeditor5-engine'; @@ -67,7 +67,7 @@ describe( 'SubscriptEditing', () => { it( 'should register subscript command', () => { const command = editor.commands.get( 'subscript' ); - expect( command ).to.be.instanceOf( SubscriptCommand ); + expect( command ).to.be.instanceOf( MutuallyExclusiveAttributeCommand ); expect( command ).to.be.instanceOf( AttributeCommand ); expect( command ).to.have.property( 'attributeKey', 'subscript' ); } ); diff --git a/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js b/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js index 11895afedda..1e100ce6a9f 100644 --- a/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js +++ b/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js @@ -5,7 +5,7 @@ import { SuperscriptEditing } from '../../src/superscript/superscriptediting.js'; import { SubscriptEditing } from '../../src/subscript/subscriptediting.js'; -import { SuperscriptCommand } from '../../src/superscript/superscriptcommand.js'; +import { MutuallyExclusiveAttributeCommand } from '../../src/mutuallyexclusiveattributecommand.js'; import { AttributeCommand } from '../../src/attributecommand.js'; import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js'; @@ -36,8 +36,8 @@ describe( 'SuperscriptCommand', () => { describe( 'instance', () => { beforeEach( () => createEditor() ); - it( 'is an instance of SuperscriptCommand', () => { - expect( command ).to.be.instanceOf( SuperscriptCommand ); + it( 'is an instance of MutuallyExclusiveAttributeCommand', () => { + expect( command ).to.be.instanceOf( MutuallyExclusiveAttributeCommand ); } ); it( 'is an instance of AttributeCommand', () => { diff --git a/packages/ckeditor5-basic-styles/tests/superscript/superscriptediting.js b/packages/ckeditor5-basic-styles/tests/superscript/superscriptediting.js index 63e34af8803..0b96637421c 100644 --- a/packages/ckeditor5-basic-styles/tests/superscript/superscriptediting.js +++ b/packages/ckeditor5-basic-styles/tests/superscript/superscriptediting.js @@ -8,7 +8,7 @@ import { SuperscriptEditing } from '../../src/superscript/superscriptediting.js' import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js'; import { Paragraph } from '@ckeditor/ckeditor5-paragraph'; import { AttributeCommand } from '../../src/attributecommand.js'; -import { SuperscriptCommand } from '../../src/superscript/superscriptcommand.js'; +import { MutuallyExclusiveAttributeCommand } from '../../src/mutuallyexclusiveattributecommand.js'; import { _getModelData, _setModelData, _getViewData } from '@ckeditor/ckeditor5-engine'; @@ -67,7 +67,7 @@ describe( 'SuperscriptEditing', () => { it( 'should register superscript command', () => { const command = editor.commands.get( 'superscript' ); - expect( command ).to.be.instanceOf( SuperscriptCommand ); + expect( command ).to.be.instanceOf( MutuallyExclusiveAttributeCommand ); expect( command ).to.be.instanceOf( AttributeCommand ); expect( command ).to.have.property( 'attributeKey', 'superscript' ); } ); From 518e4a0c53782db290817751185899693cc073f6 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 30 Apr 2026 11:04:18 +0200 Subject: [PATCH 07/10] Using `basicStyles` namespace for `superscript` and `subscript` options. --- .../docs/features/basic-styles.md | 10 ++-- .../src/augmentation.ts | 16 ++---- .../src/basicstylesconfig.ts | 49 +++++++++++++++++++ packages/ckeditor5-basic-styles/src/index.ts | 1 + .../src/mutuallyexclusiveattributecommand.ts | 9 ++-- .../src/subscript/subscriptediting.ts | 2 +- .../src/subscriptconfig.ts | 9 ++-- .../src/superscript/superscriptediting.ts | 2 +- .../src/superscriptconfig.ts | 9 ++-- .../tests/subscript/subscriptcommand.js | 8 +-- .../tests/superscript/superscriptcommand.js | 8 +-- 11 files changed, 86 insertions(+), 37 deletions(-) create mode 100644 packages/ckeditor5-basic-styles/src/basicstylesconfig.ts diff --git a/packages/ckeditor5-basic-styles/docs/features/basic-styles.md b/packages/ckeditor5-basic-styles/docs/features/basic-styles.md index a5fc8b582cd..14babb53daa 100644 --- a/packages/ckeditor5-basic-styles/docs/features/basic-styles.md +++ b/packages/ckeditor5-basic-styles/docs/features/basic-styles.md @@ -67,21 +67,23 @@ CKEditor 5 allows for typing both at the inner and outer boundaries of code By default, the {@link module:basic-styles/subscript~Subscript subscript} and {@link module:basic-styles/superscript~Superscript superscript} features are mutually exclusive: applying one to text that already has the other replaces it, matching the behavior of common word processors. Toggling a style off does not affect the other style. -To allow nesting, set the `allowNesting` option on either feature: +To allow nesting, set the `allowNesting` option on either feature under the `basicStyles` configuration namespace: ```js ClassicEditor .create( { // ... Other configuration options ... - superscript: { - allowNesting: true + basicStyles: { + superscript: { + allowNesting: true + } } } ) .then( /* ... */ ) .catch( /* ... */ ); ``` -The flag is symmetric: setting `superscript.allowNesting` or `subscript.allowNesting` to `true` disables the mutual exclusion for both commands. +The flag is symmetric: setting `basicStyles.superscript.allowNesting` or `basicStyles.subscript.allowNesting` to `true` disables the mutual exclusion for both commands. The mutual exclusion only applies to command execution. Loading content through the {@link module:core/editor/editor~Editor#setData data API} or pasting HTML such as `x` keeps both attributes on the same text regardless of this option. diff --git a/packages/ckeditor5-basic-styles/src/augmentation.ts b/packages/ckeditor5-basic-styles/src/augmentation.ts index 53d27d52a70..ff96a7f416a 100644 --- a/packages/ckeditor5-basic-styles/src/augmentation.ts +++ b/packages/ckeditor5-basic-styles/src/augmentation.ts @@ -18,10 +18,9 @@ import type { Strikethrough, StrikethroughEditing, StrikethroughUI, - BasicStyleSubscriptConfig, + BasicStylesConfig, SubscriptEditing, SubscriptUI, - BasicStyleSuperscriptConfig, SuperscriptEditing, SuperscriptUI, Underline, @@ -33,18 +32,11 @@ declare module '@ckeditor/ckeditor5-core' { interface EditorConfig { /** - * The configuration of the {@link module:basic-styles/superscript~Superscript superscript feature}. + * The configuration of the {@link module:basic-styles basic styles features}. * - * Read more in {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig}. + * Read more in {@link module:basic-styles/basicstylesconfig~BasicStylesConfig}. */ - superscript?: BasicStyleSuperscriptConfig; - - /** - * The configuration of the {@link module:basic-styles/subscript~Subscript subscript feature}. - * - * Read more in {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig}. - */ - subscript?: BasicStyleSubscriptConfig; + basicStyles?: BasicStylesConfig; } interface PluginsMap { diff --git a/packages/ckeditor5-basic-styles/src/basicstylesconfig.ts b/packages/ckeditor5-basic-styles/src/basicstylesconfig.ts new file mode 100644 index 00000000000..9547016bb2a --- /dev/null +++ b/packages/ckeditor5-basic-styles/src/basicstylesconfig.ts @@ -0,0 +1,49 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +/** + * @module basic-styles/basicstylesconfig + */ + +import type { BasicStyleSubscriptConfig } from './subscriptconfig.js'; +import type { BasicStyleSuperscriptConfig } from './superscriptconfig.js'; + +/** + * The configuration of the basic styles features (`Bold`, `Italic`, `Subscript`, `Superscript`, etc.). + * + * ```ts + * ClassicEditor + * .create( editorElement, { + * basicStyles: { + * superscript: { + * allowNesting: true + * }, + * subscript: { + * allowNesting: true + * } + * } + * } ) + * .then( ... ) + * .catch( ... ); + * ``` + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + */ +export interface BasicStylesConfig { + + /** + * The configuration of the {@link module:basic-styles/superscript~Superscript superscript feature}. + * + * Read more in {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig}. + */ + superscript?: BasicStyleSuperscriptConfig; + + /** + * The configuration of the {@link module:basic-styles/subscript~Subscript subscript feature}. + * + * Read more in {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig}. + */ + subscript?: BasicStyleSubscriptConfig; +} diff --git a/packages/ckeditor5-basic-styles/src/index.ts b/packages/ckeditor5-basic-styles/src/index.ts index 953c1a21988..c9052d952e6 100644 --- a/packages/ckeditor5-basic-styles/src/index.ts +++ b/packages/ckeditor5-basic-styles/src/index.ts @@ -23,6 +23,7 @@ export { Subscript } from './subscript.js'; export { SubscriptEditing } from './subscript/subscriptediting.js'; export { SubscriptUI } from './subscript/subscriptui.js'; export type { BasicStyleSubscriptConfig } from './subscriptconfig.js'; +export type { BasicStylesConfig } from './basicstylesconfig.js'; export { Superscript } from './superscript.js'; export { SuperscriptEditing } from './superscript/superscriptediting.js'; export { SuperscriptUI } from './superscript/superscriptui.js'; diff --git a/packages/ckeditor5-basic-styles/src/mutuallyexclusiveattributecommand.ts b/packages/ckeditor5-basic-styles/src/mutuallyexclusiveattributecommand.ts index 1eba1d420f4..a1a293b0a98 100644 --- a/packages/ckeditor5-basic-styles/src/mutuallyexclusiveattributecommand.ts +++ b/packages/ckeditor5-basic-styles/src/mutuallyexclusiveattributecommand.ts @@ -21,9 +21,8 @@ import { AttributeCommand } from './attributecommand.js'; * undo step. * * The mutual exclusion can be disabled by setting either - * {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.superscript.allowNesting`} - * or {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig#allowNesting `config.subscript.allowNesting`} - * to `true`. In that case, the command behaves exactly the same as the plain + * `config.basicStyles.superscript.allowNesting` or `config.basicStyles.subscript.allowNesting` to `true`. + * In that case, the command behaves exactly the same as the plain * {@link module:basic-styles/attributecommand~AttributeCommand}. * * The exclusion only applies to command execution. Content set through the data pipeline @@ -88,7 +87,7 @@ export class MutuallyExclusiveAttributeCommand extends AttributeCommand { function _isNestingAllowed( editor: Editor ): boolean { return Boolean( - editor.config.get( 'superscript.allowNesting' ) || - editor.config.get( 'subscript.allowNesting' ) + editor.config.get( 'basicStyles.superscript.allowNesting' ) || + editor.config.get( 'basicStyles.subscript.allowNesting' ) ); } diff --git a/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts b/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts index ea339719bcd..8331f4ad9f2 100644 --- a/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts +++ b/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts @@ -40,7 +40,7 @@ export class SubscriptEditing extends Plugin { public init(): void { const editor = this.editor; - editor.config.define( SUBSCRIPT, { allowNesting: false } ); + editor.config.define( 'basicStyles', { [ SUBSCRIPT ]: { allowNesting: false } } ); // Allow sub attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: SUBSCRIPT } ); diff --git a/packages/ckeditor5-basic-styles/src/subscriptconfig.ts b/packages/ckeditor5-basic-styles/src/subscriptconfig.ts index b17711a6832..d99af582478 100644 --- a/packages/ckeditor5-basic-styles/src/subscriptconfig.ts +++ b/packages/ckeditor5-basic-styles/src/subscriptconfig.ts @@ -9,12 +9,15 @@ /** * The configuration of the {@link module:basic-styles/subscript~Subscript subscript feature}. + * Nested under {@link module:basic-styles/basicstylesconfig~BasicStylesConfig#subscript `config.basicStyles.subscript`}. * * ```ts * ClassicEditor * .create( editorElement, { - * subscript: { - * allowNesting: true + * basicStyles: { + * subscript: { + * allowNesting: true + * } * } * } ) * .then( ... ) @@ -35,7 +38,7 @@ export interface BasicStyleSubscriptConfig { * text. This is useful for content such as isotope notation (`¹⁴₆C`) or tensor indices (`T^i_j`). * * The flag is symmetric with - * {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.superscript.allowNesting`}: + * {@link module:basic-styles/superscriptconfig~BasicStyleSuperscriptConfig#allowNesting `config.basicStyles.superscript.allowNesting`}: * if either is set to `true`, both commands skip the mutual-exclusion step. * * The flag only affects command execution. Content set through the data pipeline (for example diff --git a/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts b/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts index 27109ef3138..e814463fadc 100644 --- a/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts +++ b/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts @@ -40,7 +40,7 @@ export class SuperscriptEditing extends Plugin { public init(): void { const editor = this.editor; - editor.config.define( SUPERSCRIPT, { allowNesting: false } ); + editor.config.define( 'basicStyles', { [ SUPERSCRIPT ]: { allowNesting: false } } ); // Allow super attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: SUPERSCRIPT } ); diff --git a/packages/ckeditor5-basic-styles/src/superscriptconfig.ts b/packages/ckeditor5-basic-styles/src/superscriptconfig.ts index 0950c96784f..1902979830b 100644 --- a/packages/ckeditor5-basic-styles/src/superscriptconfig.ts +++ b/packages/ckeditor5-basic-styles/src/superscriptconfig.ts @@ -9,12 +9,15 @@ /** * The configuration of the {@link module:basic-styles/superscript~Superscript superscript feature}. + * Nested under {@link module:basic-styles/basicstylesconfig~BasicStylesConfig#superscript `config.basicStyles.superscript`}. * * ```ts * ClassicEditor * .create( editorElement, { - * superscript: { - * allowNesting: true + * basicStyles: { + * superscript: { + * allowNesting: true + * } * } * } ) * .then( ... ) @@ -35,7 +38,7 @@ export interface BasicStyleSuperscriptConfig { * text. This is useful for content such as isotope notation (`¹⁴₆C`) or tensor indices (`T^i_j`). * * The flag is symmetric with - * {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig#allowNesting `config.subscript.allowNesting`}: + * {@link module:basic-styles/subscriptconfig~BasicStyleSubscriptConfig#allowNesting `config.basicStyles.subscript.allowNesting`}: * if either is set to `true`, both commands skip the mutual-exclusion step. * * The flag only affects command execution. Content set through the data pipeline (for example diff --git a/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js b/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js index 205739fb4d6..5f36a7d65b0 100644 --- a/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js +++ b/packages/ckeditor5-basic-styles/tests/subscript/subscriptcommand.js @@ -52,8 +52,8 @@ describe( 'SubscriptCommand', () => { describe( 'config defaults', () => { beforeEach( () => createEditor() ); - it( 'sets subscript.allowNesting to false by default', () => { - expect( editor.config.get( 'subscript.allowNesting' ) ).to.be.false; + it( 'sets basicStyles.subscript.allowNesting to false by default', () => { + expect( editor.config.get( 'basicStyles.subscript.allowNesting' ) ).to.be.false; } ); } ); @@ -171,7 +171,7 @@ describe( 'SubscriptCommand', () => { } ); describe( 'execute() with allowNesting on the subscript side', () => { - beforeEach( () => createEditor( { subscript: { allowNesting: true } } ) ); + beforeEach( () => createEditor( { basicStyles: { subscript: { allowNesting: true } } } ) ); it( 'preserves superscript when applying subscript', () => { _setModelData( model, '[<$text superscript="true">foo]' ); @@ -185,7 +185,7 @@ describe( 'SubscriptCommand', () => { } ); describe( 'execute() with allowNesting on the superscript side (OR semantics)', () => { - beforeEach( () => createEditor( { superscript: { allowNesting: true } } ) ); + beforeEach( () => createEditor( { basicStyles: { superscript: { allowNesting: true } } } ) ); it( 'preserves superscript when applying subscript', () => { _setModelData( model, '[<$text superscript="true">foo]' ); diff --git a/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js b/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js index 1e100ce6a9f..705a0fb06cc 100644 --- a/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js +++ b/packages/ckeditor5-basic-styles/tests/superscript/superscriptcommand.js @@ -52,8 +52,8 @@ describe( 'SuperscriptCommand', () => { describe( 'config defaults', () => { beforeEach( () => createEditor() ); - it( 'sets superscript.allowNesting to false by default', () => { - expect( editor.config.get( 'superscript.allowNesting' ) ).to.be.false; + it( 'sets basicStyles.superscript.allowNesting to false by default', () => { + expect( editor.config.get( 'basicStyles.superscript.allowNesting' ) ).to.be.false; } ); } ); @@ -171,7 +171,7 @@ describe( 'SuperscriptCommand', () => { } ); describe( 'execute() with allowNesting on the superscript side', () => { - beforeEach( () => createEditor( { superscript: { allowNesting: true } } ) ); + beforeEach( () => createEditor( { basicStyles: { superscript: { allowNesting: true } } } ) ); it( 'preserves subscript when applying superscript', () => { _setModelData( model, '[<$text subscript="true">foo]' ); @@ -185,7 +185,7 @@ describe( 'SuperscriptCommand', () => { } ); describe( 'execute() with allowNesting on the subscript side (OR semantics)', () => { - beforeEach( () => createEditor( { subscript: { allowNesting: true } } ) ); + beforeEach( () => createEditor( { basicStyles: { subscript: { allowNesting: true } } } ) ); it( 'preserves subscript when applying superscript', () => { _setModelData( model, '[<$text subscript="true">foo]' ); From b921abc1eb11c7d80caabee1bcf25d5c4ccaf093 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 30 Apr 2026 11:21:26 +0200 Subject: [PATCH 08/10] Extracted `SUPERSCRIPT` and `SUBSCRIPT` variables to a common place. --- .../ckeditor5-basic-styles/src/constants.ts | 22 +++++++++++++++++++ .../src/subscript/subscriptediting.ts | 4 +--- .../src/subscript/subscriptui.ts | 3 +-- .../src/superscript/superscriptediting.ts | 4 +--- .../src/superscript/superscriptui.ts | 3 +-- 5 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 packages/ckeditor5-basic-styles/src/constants.ts diff --git a/packages/ckeditor5-basic-styles/src/constants.ts b/packages/ckeditor5-basic-styles/src/constants.ts new file mode 100644 index 00000000000..40bd00e04d8 --- /dev/null +++ b/packages/ckeditor5-basic-styles/src/constants.ts @@ -0,0 +1,22 @@ +/** + * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options + */ + +/** + * @module basic-styles/constants + */ + +/** + * The model attribute key for the `superscript` text style. + * + * @internal + */ +export const SUPERSCRIPT = 'superscript'; + +/** + * The model attribute key for the `subscript` text style. + * + * @internal + */ +export const SUBSCRIPT = 'subscript'; diff --git a/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts b/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts index 8331f4ad9f2..44138b9548e 100644 --- a/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts +++ b/packages/ckeditor5-basic-styles/src/subscript/subscriptediting.ts @@ -9,9 +9,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core'; import { MutuallyExclusiveAttributeCommand } from '../mutuallyexclusiveattributecommand.js'; - -const SUBSCRIPT = 'subscript'; -const SUPERSCRIPT = 'superscript'; +import { SUBSCRIPT, SUPERSCRIPT } from '../constants.js'; /** * The subscript editing feature. diff --git a/packages/ckeditor5-basic-styles/src/subscript/subscriptui.ts b/packages/ckeditor5-basic-styles/src/subscript/subscriptui.ts index 9211b9d4b21..dca2dfba27c 100644 --- a/packages/ckeditor5-basic-styles/src/subscript/subscriptui.ts +++ b/packages/ckeditor5-basic-styles/src/subscript/subscriptui.ts @@ -11,8 +11,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core'; import { IconSubscript } from '@ckeditor/ckeditor5-icons'; import { ButtonView, MenuBarMenuListItemButtonView } from '@ckeditor/ckeditor5-ui'; import { getButtonCreator } from '../utils.js'; - -const SUBSCRIPT = 'subscript'; +import { SUBSCRIPT } from '../constants.js'; /** * The subscript UI feature. It introduces the Subscript button. diff --git a/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts b/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts index e814463fadc..a09b7789cb2 100644 --- a/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts +++ b/packages/ckeditor5-basic-styles/src/superscript/superscriptediting.ts @@ -9,9 +9,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core'; import { MutuallyExclusiveAttributeCommand } from '../mutuallyexclusiveattributecommand.js'; - -const SUPERSCRIPT = 'superscript'; -const SUBSCRIPT = 'subscript'; +import { SUBSCRIPT, SUPERSCRIPT } from '../constants.js'; /** * The superscript editing feature. diff --git a/packages/ckeditor5-basic-styles/src/superscript/superscriptui.ts b/packages/ckeditor5-basic-styles/src/superscript/superscriptui.ts index a47d1b9bfe3..7497de38b0e 100644 --- a/packages/ckeditor5-basic-styles/src/superscript/superscriptui.ts +++ b/packages/ckeditor5-basic-styles/src/superscript/superscriptui.ts @@ -11,8 +11,7 @@ import { Plugin } from '@ckeditor/ckeditor5-core'; import { IconSuperscript } from '@ckeditor/ckeditor5-icons'; import { ButtonView, MenuBarMenuListItemButtonView } from '@ckeditor/ckeditor5-ui'; import { getButtonCreator } from '../utils.js'; - -const SUPERSCRIPT = 'superscript'; +import { SUPERSCRIPT } from '../constants.js'; /** * The superscript UI feature. It introduces the Superscript button. From 32a625e79c53bdf951bad9252c69a945299d5a7d Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 30 Apr 2026 11:22:21 +0200 Subject: [PATCH 09/10] Renamed changeset file. --- .../{20260428123311_master.md => 20260428123311_ck_20000.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{20260428123311_master.md => 20260428123311_ck_20000.md} (100%) diff --git a/.changelog/20260428123311_master.md b/.changelog/20260428123311_ck_20000.md similarity index 100% rename from .changelog/20260428123311_master.md rename to .changelog/20260428123311_ck_20000.md From 71425c2e08d2cba6d615b15133d7b9f9c8ccc538 Mon Sep 17 00:00:00 2001 From: Marcin Panek Date: Thu, 30 Apr 2026 11:44:47 +0200 Subject: [PATCH 10/10] Updated the changeset content. --- .changelog/20260428123311_ck_20000.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/20260428123311_ck_20000.md b/.changelog/20260428123311_ck_20000.md index f11f859d884..e475e57b3bc 100644 --- a/.changelog/20260428123311_ck_20000.md +++ b/.changelog/20260428123311_ck_20000.md @@ -4,4 +4,4 @@ scope: - ckeditor5-basic-styles --- -The `superscript` and `subscript` text styles are now mutually exclusive. +The `superscript` and `subscript` text styles are now mutually exclusive by default - applying one to text that already has the other replaces it. The previous behavior, where both attributes could coexist on the same text, can be restored by setting `config.basicStyles.superscript.allowNesting` or `config.basicStyles.subscript.allowNesting` to `true`.