From 2f8946dfdcf9ed87a011130f08d1d1362d46c5b5 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Fri, 24 Apr 2026 11:56:39 +0200 Subject: [PATCH 1/4] Reject unwanted hidden bookmark links from imported word content in paste from office. --- .changelog/20260424115528_ck_18846.md | 9 +++++++++ .../src/filters/bookmark.ts | 17 ++++++++++++++--- .../normalized.safari.word2016.html | 2 +- .../list/styled-anchor/normalized.word2016.html | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .changelog/20260424115528_ck_18846.md diff --git a/.changelog/20260424115528_ck_18846.md b/.changelog/20260424115528_ck_18846.md new file mode 100644 index 00000000000..4340a1be7f2 --- /dev/null +++ b/.changelog/20260424115528_ck_18846.md @@ -0,0 +1,9 @@ +--- +type: Fix +scope: + - ckeditor5-paste-from-office +closes: + - https://github.com/ckeditor/ckeditor5/issues/18846 +--- + +Pasting content from Word no longer inserts unwanted visible bookmarks into the editor. diff --git a/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts b/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts index cc1a5ec88a4..cbadb1d0189 100644 --- a/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts +++ b/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts @@ -7,9 +7,10 @@ * @module paste-from-office/filters/bookmark */ -import { - type ViewUpcastWriter, - type ViewDocumentFragment +import type { + ViewUpcastWriter, + ViewDocumentFragment, + ViewElement } from '@ckeditor/ckeditor5-engine'; /** @@ -40,5 +41,15 @@ export function transformBookmarks( const children = element.getChildren(); writer.insertChild( index, children, element.parent! ); + + if ( isHiddenMsBookmarkAnchor( element ) ) { + writer.remove( element ); + } } } + +function isHiddenMsBookmarkAnchor( element: ViewElement ) { + const name = element.getAttribute( 'name' ); + + return !!name && name.startsWith( '_' ); +} diff --git a/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.safari.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.safari.word2016.html index 9841d0d70c3..1d1eca83cb5 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.safari.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.safari.word2016.html @@ -1,6 +1,6 @@

An example list with an error:

diff --git a/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.word2016.html b/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.word2016.html index dfa2e32393e..8cba3323da1 100644 --- a/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.word2016.html +++ b/packages/ckeditor5-paste-from-office/tests/_data/list/styled-anchor/normalized.word2016.html @@ -1,6 +1,6 @@

An example list with an error:

From 280e08e88b19660a1505e120df72329c98fd69f1 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Fri, 24 Apr 2026 11:58:29 +0200 Subject: [PATCH 2/4] Rename helper. --- packages/ckeditor5-paste-from-office/src/filters/bookmark.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts b/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts index cbadb1d0189..bb363ef6066 100644 --- a/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts +++ b/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts @@ -42,13 +42,13 @@ export function transformBookmarks( writer.insertChild( index, children, element.parent! ); - if ( isHiddenMsBookmarkAnchor( element ) ) { + if ( isHiddenBookmarkAnchor( element ) ) { writer.remove( element ); } } } -function isHiddenMsBookmarkAnchor( element: ViewElement ) { +function isHiddenBookmarkAnchor( element: ViewElement ) { const name = element.getAttribute( 'name' ); return !!name && name.startsWith( '_' ); From c2958ca1decb27127c1a1282103acfba6776ba5f Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Wed, 29 Apr 2026 07:03:40 +0200 Subject: [PATCH 3/4] Add missing jsdoc. --- .../src/filters/bookmark.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts b/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts index bb363ef6066..7019da34d84 100644 --- a/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts +++ b/packages/ckeditor5-paste-from-office/src/filters/bookmark.ts @@ -48,6 +48,17 @@ export function transformBookmarks( } } +/** + * Checks whether the given element is a hidden or auto-generated bookmark anchor. + * + * Editors like MS Word and Google Docs use the `name` attribute (rather than `id`) + * for bookmarks. Furthermore, they reserve `_`-prefixed bookmark names for + * auto-generated anchors (e.g., Table of Contents or internal hyperlinks) and + * do not allow users to manually create custom bookmarks starting with an underscore. + * + * @param element The element to check. + * @returns True if the element is a hidden bookmark anchor, false otherwise. + */ function isHiddenBookmarkAnchor( element: ViewElement ) { const name = element.getAttribute( 'name' ); From 2bff7f02f6ee4ec4dd16b9af68b91baad331c6df Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Wed, 29 Apr 2026 07:09:22 +0200 Subject: [PATCH 4/4] Add tests. --- .../tests/filters/bookmark.js | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/ckeditor5-paste-from-office/tests/filters/bookmark.js b/packages/ckeditor5-paste-from-office/tests/filters/bookmark.js index 9b403650c54..67d7d934c56 100644 --- a/packages/ckeditor5-paste-from-office/tests/filters/bookmark.js +++ b/packages/ckeditor5-paste-from-office/tests/filters/bookmark.js @@ -221,6 +221,46 @@ describe( 'PasteFromOffice - filters - bookmark', () => { ); } ); + it( 'should extract text and remove the element completely if its name starts with an underscore (hidden bookmark)', () => { + performTest( + '' + + 'text' + + '', + + 'text' + ); + } ); + + it( 'should completely remove an empty element if its name starts with an underscore', () => { + performTest( + '

paragraph

' + + '
', + + '

paragraph

' + ); + } ); + + it( 'should remove the element but keep content when both id and name are present and name starts with an underscore', () => { + performTest( + '' + + 'text' + + '', + + 'text' + ); + } ); + + it( 'should NOT remove the element if only its id starts with an underscore (since Word/GDocs use name)', () => { + performTest( + '' + + 'text' + + '', + + '' + + 'text' + ); + } ); + function performTest( inputData, expectedData ) { const documentFragment = htmlDataProcessor.toView( inputData );