@ckeditor/ckeditor5-engine 44.2.1 → 44.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -687,309 +687,757 @@ TextProxy$1.prototype.is = function(type) {
687
687
  };
688
688
 
689
689
  /**
690
- * View matcher class.
691
- * Instance of this class can be used to find {@link module:engine/view/element~Element elements} that match given pattern.
692
- */ class Matcher {
693
- _patterns = [];
690
+ * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
691
+ * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
692
+ * Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
693
+ * does not consume its attributes, classes and styles.
694
+ * To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
695
+ * To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
696
+ * To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
697
+ * To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
698
+ *
699
+ * ```ts
700
+ * viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
701
+ * viewConsumable.add( textNode ); // Adds text node for consumption.
702
+ * viewConsumable.add( docFragment ); // Adds document fragment for consumption.
703
+ * viewConsumable.test( element, { name: true } ); // Tests if element's name can be consumed.
704
+ * viewConsumable.test( textNode ); // Tests if text node can be consumed.
705
+ * viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
706
+ * viewConsumable.consume( element, { name: true } ); // Consume element's name.
707
+ * viewConsumable.consume( textNode ); // Consume text node.
708
+ * viewConsumable.consume( docFragment ); // Consume document fragment.
709
+ * viewConsumable.revert( element, { name: true } ); // Revert already consumed element's name.
710
+ * viewConsumable.revert( textNode ); // Revert already consumed text node.
711
+ * viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
712
+ * ```
713
+ */ class ViewConsumable {
694
714
  /**
695
- * Creates new instance of Matcher.
715
+ * Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
716
+ * {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
717
+ * For {@link module:engine/view/text~Text text nodes} and
718
+ * {@link module:engine/view/documentfragment~DocumentFragment document fragments} boolean value is stored as value.
719
+ */ _consumables = new Map();
720
+ /**
721
+ * Adds view {@link module:engine/view/element~Element element}, {@link module:engine/view/text~Text text node} or
722
+ * {@link module:engine/view/documentfragment~DocumentFragment document fragment} as ready to be consumed.
696
723
  *
697
- * @param pattern Match patterns. See {@link module:engine/view/matcher~Matcher#add add method} for more information.
698
- */ constructor(...pattern){
699
- this.add(...pattern);
724
+ * ```ts
725
+ * viewConsumable.add( p, { name: true } ); // Adds element's name to consume.
726
+ * viewConsumable.add( p, { attributes: 'name' } ); // Adds element's attribute.
727
+ * viewConsumable.add( p, { classes: 'foobar' } ); // Adds element's class.
728
+ * viewConsumable.add( p, { styles: 'color' } ); // Adds element's style
729
+ * viewConsumable.add( p, { attributes: 'name', styles: 'color' } ); // Adds attribute and style.
730
+ * viewConsumable.add( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be provided.
731
+ * viewConsumable.add( textNode ); // Adds text node to consume.
732
+ * viewConsumable.add( docFragment ); // Adds document fragment to consume.
733
+ * ```
734
+ *
735
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
736
+ * attribute is provided - it should be handled separately by providing actual style/class.
737
+ *
738
+ * ```ts
739
+ * viewConsumable.add( p, { attributes: 'style' } ); // This call will throw an exception.
740
+ * viewConsumable.add( p, { styles: 'color' } ); // This is properly handled style.
741
+ * ```
742
+ *
743
+ * @param consumables Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
744
+ * @param consumables.name If set to true element's name will be included.
745
+ * @param consumables.attributes Attribute name or array of attribute names.
746
+ * @param consumables.classes Class name or array of class names.
747
+ * @param consumables.styles Style name or array of style names.
748
+ */ add(element, consumables) {
749
+ let elementConsumables;
750
+ // For text nodes and document fragments just mark them as consumable.
751
+ if (element.is('$text') || element.is('documentFragment')) {
752
+ this._consumables.set(element, true);
753
+ return;
754
+ }
755
+ // For elements create new ViewElementConsumables or update already existing one.
756
+ if (!this._consumables.has(element)) {
757
+ elementConsumables = new ViewElementConsumables(element);
758
+ this._consumables.set(element, elementConsumables);
759
+ } else {
760
+ elementConsumables = this._consumables.get(element);
761
+ }
762
+ elementConsumables.add(consumables ? normalizeConsumables(consumables) : element._getConsumables());
700
763
  }
701
764
  /**
702
- * Adds pattern or patterns to matcher instance.
765
+ * Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
766
+ * {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
767
+ * It returns `true` when all items included in method's call can be consumed. Returns `false` when
768
+ * first already consumed item is found and `null` when first non-consumable item is found.
703
769
  *
704
770
  * ```ts
705
- * // String.
706
- * matcher.add( 'div' );
771
+ * viewConsumable.test( p, { name: true } ); // Tests element's name.
772
+ * viewConsumable.test( p, { attributes: 'name' } ); // Tests attribute.
773
+ * viewConsumable.test( p, { classes: 'foobar' } ); // Tests class.
774
+ * viewConsumable.test( p, { styles: 'color' } ); // Tests style.
775
+ * viewConsumable.test( p, { attributes: 'name', styles: 'color' } ); // Tests attribute and style.
776
+ * viewConsumable.test( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
777
+ * viewConsumable.test( textNode ); // Tests text node.
778
+ * viewConsumable.test( docFragment ); // Tests document fragment.
779
+ * ```
707
780
  *
708
- * // Regular expression.
709
- * matcher.add( /^\w/ );
781
+ * Testing classes and styles as attribute will test if all added classes/styles can be consumed.
710
782
  *
711
- * // Single class.
712
- * matcher.add( {
713
- * classes: 'foobar'
714
- * } );
783
+ * ```ts
784
+ * viewConsumable.test( p, { attributes: 'class' } ); // Tests if all added classes can be consumed.
785
+ * viewConsumable.test( p, { attributes: 'style' } ); // Tests if all added styles can be consumed.
715
786
  * ```
716
787
  *
717
- * See {@link module:engine/view/matcher~MatcherPattern} for more examples.
788
+ * @param consumables Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
789
+ * @param consumables.name If set to true element's name will be included.
790
+ * @param consumables.attributes Attribute name or array of attribute names.
791
+ * @param consumables.classes Class name or array of class names.
792
+ * @param consumables.styles Style name or array of style names.
793
+ * @returns Returns `true` when all items included in method's call can be consumed. Returns `false`
794
+ * when first already consumed item is found and `null` when first non-consumable item is found.
795
+ */ test(element, consumables) {
796
+ const elementConsumables = this._consumables.get(element);
797
+ if (elementConsumables === undefined) {
798
+ return null;
799
+ }
800
+ // For text nodes and document fragments return stored boolean value.
801
+ if (element.is('$text') || element.is('documentFragment')) {
802
+ return elementConsumables;
803
+ }
804
+ // For elements test consumables object.
805
+ return elementConsumables.test(normalizeConsumables(consumables));
806
+ }
807
+ /**
808
+ * Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
809
+ * {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
810
+ * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
718
811
  *
719
- * Multiple patterns can be added in one call:
812
+ * ```ts
813
+ * viewConsumable.consume( p, { name: true } ); // Consumes element's name.
814
+ * viewConsumable.consume( p, { attributes: 'name' } ); // Consumes element's attribute.
815
+ * viewConsumable.consume( p, { classes: 'foobar' } ); // Consumes element's class.
816
+ * viewConsumable.consume( p, { styles: 'color' } ); // Consumes element's style.
817
+ * viewConsumable.consume( p, { attributes: 'name', styles: 'color' } ); // Consumes attribute and style.
818
+ * viewConsumable.consume( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
819
+ * viewConsumable.consume( textNode ); // Consumes text node.
820
+ * viewConsumable.consume( docFragment ); // Consumes document fragment.
821
+ * ```
822
+ *
823
+ * Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
720
824
  *
721
825
  * ```ts
722
- * matcher.add( 'div', { classes: 'foobar' } );
826
+ * viewConsumable.consume( p, { attributes: 'class' } ); // Consume only if all added classes can be consumed.
827
+ * viewConsumable.consume( p, { attributes: 'style' } ); // Consume only if all added styles can be consumed.
723
828
  * ```
724
829
  *
725
- * @param pattern Object describing pattern details. If string or regular expression
726
- * is provided it will be used to match element's name. Pattern can be also provided in a form
727
- * of a function - then this function will be called with each {@link module:engine/view/element~Element element} as a parameter.
728
- * Function's return value will be stored under `match` key of the object returned from
729
- * {@link module:engine/view/matcher~Matcher#match match} or {@link module:engine/view/matcher~Matcher#matchAll matchAll} methods.
730
- */ add(...pattern) {
731
- for (let item of pattern){
732
- // String or RegExp pattern is used as element's name.
733
- if (typeof item == 'string' || item instanceof RegExp) {
734
- item = {
735
- name: item
736
- };
830
+ * @param consumables Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
831
+ * @param consumables.name If set to true element's name will be included.
832
+ * @param consumables.attributes Attribute name or array of attribute names.
833
+ * @param consumables.classes Class name or array of class names.
834
+ * @param consumables.styles Style name or array of style names.
835
+ * @returns Returns `true` when all items included in method's call can be consumed,
836
+ * otherwise returns `false`.
837
+ */ consume(element, consumables) {
838
+ if (element.is('$text') || element.is('documentFragment')) {
839
+ if (!this.test(element, consumables)) {
840
+ return false;
737
841
  }
738
- this._patterns.push(item);
842
+ // For text nodes and document fragments set value to false.
843
+ this._consumables.set(element, false);
844
+ return true;
845
+ }
846
+ // For elements - consume consumables object.
847
+ const elementConsumables = this._consumables.get(element);
848
+ if (elementConsumables === undefined) {
849
+ return false;
739
850
  }
851
+ return elementConsumables.consume(normalizeConsumables(consumables));
740
852
  }
741
853
  /**
742
- * Matches elements for currently stored patterns. Returns match information about first found
743
- * {@link module:engine/view/element~Element element}, otherwise returns `null`.
854
+ * Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
855
+ * {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
856
+ * Method does not revert items that were never previously added for consumption, even if they are included in
857
+ * method's call.
744
858
  *
745
- * Example of returned object:
859
+ * ```ts
860
+ * viewConsumable.revert( p, { name: true } ); // Reverts element's name.
861
+ * viewConsumable.revert( p, { attributes: 'name' } ); // Reverts element's attribute.
862
+ * viewConsumable.revert( p, { classes: 'foobar' } ); // Reverts element's class.
863
+ * viewConsumable.revert( p, { styles: 'color' } ); // Reverts element's style.
864
+ * viewConsumable.revert( p, { attributes: 'name', styles: 'color' } ); // Reverts attribute and style.
865
+ * viewConsumable.revert( p, { classes: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
866
+ * viewConsumable.revert( textNode ); // Reverts text node.
867
+ * viewConsumable.revert( docFragment ); // Reverts document fragment.
868
+ * ```
869
+ *
870
+ * Reverting classes and styles as attribute will revert all classes/styles that were previously added for
871
+ * consumption.
746
872
  *
747
873
  * ```ts
748
- * {
749
- * element: <instance of found element>,
750
- * pattern: <pattern used to match found element>,
751
- * match: {
752
- * name: true,
753
- * attributes: [ 'title', 'href' ],
754
- * classes: [ 'foo' ],
755
- * styles: [ 'color', 'position' ]
756
- * }
757
- * }
874
+ * viewConsumable.revert( p, { attributes: 'class' } ); // Reverts all classes added for consumption.
875
+ * viewConsumable.revert( p, { attributes: 'style' } ); // Reverts all styles added for consumption.
758
876
  * ```
759
877
  *
760
- * @see module:engine/view/matcher~Matcher#add
761
- * @see module:engine/view/matcher~Matcher#matchAll
762
- * @param element View element to match against stored patterns.
763
- */ match(...element) {
764
- for (const singleElement of element){
765
- for (const pattern of this._patterns){
766
- const match = isElementMatching(singleElement, pattern);
767
- if (match) {
768
- return {
769
- element: singleElement,
770
- pattern,
771
- match
772
- };
773
- }
878
+ * @param consumables Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
879
+ * @param consumables.name If set to true element's name will be included.
880
+ * @param consumables.attributes Attribute name or array of attribute names.
881
+ * @param consumables.classes Class name or array of class names.
882
+ * @param consumables.styles Style name or array of style names.
883
+ */ revert(element, consumables) {
884
+ const elementConsumables = this._consumables.get(element);
885
+ if (elementConsumables !== undefined) {
886
+ if (element.is('$text') || element.is('documentFragment')) {
887
+ // For text nodes and document fragments - set consumable to true.
888
+ this._consumables.set(element, true);
889
+ } else {
890
+ // For elements - revert items from consumables object.
891
+ elementConsumables.revert(normalizeConsumables(consumables));
774
892
  }
775
893
  }
776
- return null;
777
894
  }
778
895
  /**
779
- * Matches elements for currently stored patterns. Returns array of match information with all found
780
- * {@link module:engine/view/element~Element elements}. If no element is found - returns `null`.
896
+ * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
897
+ * {@link module:engine/view/node~Node node} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
898
+ * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
781
899
  *
782
- * @see module:engine/view/matcher~Matcher#add
783
- * @see module:engine/view/matcher~Matcher#match
784
- * @param element View element to match against stored patterns.
785
- * @returns Array with match information about found elements or `null`. For more information
786
- * see {@link module:engine/view/matcher~Matcher#match match method} description.
787
- */ matchAll(...element) {
788
- const results = [];
789
- for (const singleElement of element){
790
- for (const pattern of this._patterns){
791
- const match = isElementMatching(singleElement, pattern);
792
- if (match) {
793
- results.push({
794
- element: singleElement,
795
- pattern,
796
- match
797
- });
798
- }
900
+ * @param from View node or document fragment from which `ViewConsumable` will be created.
901
+ * @param instance If provided, given `ViewConsumable` instance will be used
902
+ * to add all consumables. It will be returned instead of a new instance.
903
+ */ static createFrom(from, instance) {
904
+ if (!instance) {
905
+ instance = new ViewConsumable();
906
+ }
907
+ if (from.is('$text')) {
908
+ instance.add(from);
909
+ } else if (from.is('element') || from.is('documentFragment')) {
910
+ instance.add(from);
911
+ for (const child of from.getChildren()){
912
+ ViewConsumable.createFrom(child, instance);
799
913
  }
800
914
  }
801
- return results.length > 0 ? results : null;
915
+ return instance;
802
916
  }
917
+ }
918
+ /**
919
+ * This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
920
+ * It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
921
+ */ class ViewElementConsumables {
922
+ element;
803
923
  /**
804
- * Returns the name of the element to match if there is exactly one pattern added to the matcher instance
805
- * and it matches element name defined by `string` (not `RegExp`). Otherwise, returns `null`.
806
- *
807
- * @returns Element name trying to match.
808
- */ getElementName() {
809
- if (this._patterns.length !== 1) {
810
- return null;
811
- }
812
- const pattern = this._patterns[0];
813
- const name = pattern.name;
814
- return typeof pattern != 'function' && name && !(name instanceof RegExp) ? name : null;
815
- }
816
- }
817
- /**
818
- * Returns match information if {@link module:engine/view/element~Element element} is matching provided pattern.
819
- * If element cannot be matched to provided pattern - returns `null`.
820
- *
821
- * @returns Returns object with match information or null if element is not matching.
822
- */ function isElementMatching(element, pattern) {
823
- // If pattern is provided as function - return result of that function;
824
- if (typeof pattern == 'function') {
825
- return pattern(element);
924
+ * Flag indicating if name of the element can be consumed.
925
+ */ _canConsumeName = null;
926
+ /**
927
+ * A map of element's consumables.
928
+ * * For plain attributes the value is a boolean indicating whether the attribute is available to consume.
929
+ * * For token based attributes (like class list and style) the value is a map of tokens to booleans
930
+ * indicating whether the token is available to consume on the given attribute.
931
+ */ _attributes = new Map();
932
+ /**
933
+ * Creates ViewElementConsumables instance.
934
+ *
935
+ * @param from View element from which `ViewElementConsumables` is being created.
936
+ */ constructor(from){
937
+ this.element = from;
826
938
  }
827
- const match = {};
828
- // Check element's name.
829
- if (pattern.name) {
830
- match.name = matchName(pattern.name, element.name);
831
- if (!match.name) {
832
- return null;
939
+ /**
940
+ * Adds consumable parts of the {@link module:engine/view/element~Element view element}.
941
+ * Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
942
+ * styles still could be consumed):
943
+ *
944
+ * ```ts
945
+ * consumables.add( { name: true } );
946
+ * ```
947
+ *
948
+ * Attributes classes and styles:
949
+ *
950
+ * ```ts
951
+ * consumables.add( { attributes: [ [ 'title' ], [ 'class', 'foo' ], [ 'style', 'color'] ] } );
952
+ * consumables.add( { attributes: [ [ 'title' ], [ 'name' ], [ 'class', 'foo' ], [ 'class', 'bar' ] ] } );
953
+ * ```
954
+ *
955
+ * Note: This method accepts only {@link module:engine/view/element~NormalizedConsumables}.
956
+ * You can use {@link module:engine/conversion/viewconsumable~normalizeConsumables} helper to convert from
957
+ * {@link module:engine/conversion/viewconsumable~Consumables} to `NormalizedConsumables`.
958
+ *
959
+ * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
960
+ * attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
961
+ *
962
+ * @param consumables Object describing which parts of the element can be consumed.
963
+ */ add(consumables) {
964
+ if (consumables.name) {
965
+ this._canConsumeName = true;
966
+ }
967
+ for (const [name, token] of consumables.attributes){
968
+ if (token) {
969
+ let attributeTokens = this._attributes.get(name);
970
+ if (!attributeTokens || typeof attributeTokens == 'boolean') {
971
+ attributeTokens = new Map();
972
+ this._attributes.set(name, attributeTokens);
973
+ }
974
+ attributeTokens.set(token, true);
975
+ } else if (name == 'style' || name == 'class') {
976
+ /**
977
+ * Class and style attributes should be handled separately in
978
+ * {@link module:engine/conversion/viewconsumable~ViewConsumable#add `ViewConsumable#add()`}.
979
+ *
980
+ * What you have done is trying to use:
981
+ *
982
+ * ```ts
983
+ * consumables.add( { attributes: [ 'class', 'style' ] } );
984
+ * ```
985
+ *
986
+ * While each class and style should be registered separately:
987
+ *
988
+ * ```ts
989
+ * consumables.add( { classes: 'some-class', styles: 'font-weight' } );
990
+ * ```
991
+ *
992
+ * @error viewconsumable-invalid-attribute
993
+ */ throw new CKEditorError('viewconsumable-invalid-attribute', this);
994
+ } else {
995
+ this._attributes.set(name, true);
996
+ }
833
997
  }
834
998
  }
835
- // Check element's attributes.
836
- if (pattern.attributes) {
837
- match.attributes = matchAttributes(pattern.attributes, element);
838
- if (!match.attributes) {
839
- return null;
999
+ /**
1000
+ * Tests if parts of the {@link module:engine/view/element~Element view element} can be consumed.
1001
+ *
1002
+ * Element's name can be tested:
1003
+ *
1004
+ * ```ts
1005
+ * consumables.test( { name: true } );
1006
+ * ```
1007
+ *
1008
+ * Attributes classes and styles:
1009
+ *
1010
+ * ```ts
1011
+ * consumables.test( { attributes: [ [ 'title' ], [ 'class', 'foo' ], [ 'style', 'color' ] ] } );
1012
+ * consumables.test( { attributes: [ [ 'title' ], [ 'name' ], [ 'class', 'foo' ], [ 'class', 'bar' ] ] } );
1013
+ * ```
1014
+ *
1015
+ * @param consumables Object describing which parts of the element should be tested.
1016
+ * @returns `true` when all tested items can be consumed, `null` when even one of the items
1017
+ * was never marked for consumption and `false` when even one of the items was already consumed.
1018
+ */ test(consumables) {
1019
+ // Check if name can be consumed.
1020
+ if (consumables.name && !this._canConsumeName) {
1021
+ return this._canConsumeName;
1022
+ }
1023
+ for (const [name, token] of consumables.attributes){
1024
+ const value = this._attributes.get(name);
1025
+ // Return null if attribute is not found.
1026
+ if (value === undefined) {
1027
+ return null;
1028
+ }
1029
+ // Already consumed.
1030
+ if (value === false) {
1031
+ return false;
1032
+ }
1033
+ // Simple attribute is not consumed so continue to next attribute.
1034
+ if (value === true) {
1035
+ continue;
1036
+ }
1037
+ if (!token) {
1038
+ // Tokenized attribute but token is not specified so check if all tokens are not consumed.
1039
+ for (const tokenValue of value.values()){
1040
+ // Already consumed token.
1041
+ if (!tokenValue) {
1042
+ return false;
1043
+ }
1044
+ }
1045
+ } else {
1046
+ const tokenValue = value.get(token);
1047
+ // Return null if token is not found.
1048
+ if (tokenValue === undefined) {
1049
+ return null;
1050
+ }
1051
+ // Already consumed.
1052
+ if (!tokenValue) {
1053
+ return false;
1054
+ }
1055
+ }
840
1056
  }
1057
+ // Return true only if all can be consumed.
1058
+ return true;
841
1059
  }
842
- // Check element's classes.
843
- if (pattern.classes) {
844
- match.classes = matchClasses(pattern.classes, element);
845
- if (!match.classes) {
846
- return null;
1060
+ /**
1061
+ * Tests if parts of the {@link module:engine/view/element~Element view element} can be consumed and consumes them if available.
1062
+ * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
1063
+ *
1064
+ * Element's name can be consumed:
1065
+ *
1066
+ * ```ts
1067
+ * consumables.consume( { name: true } );
1068
+ * ```
1069
+ *
1070
+ * Attributes classes and styles:
1071
+ *
1072
+ * ```ts
1073
+ * consumables.consume( { attributes: [ [ 'title' ], [ 'class', 'foo' ], [ 'style', 'color' ] ] } );
1074
+ * consumables.consume( { attributes: [ [ 'title' ], [ 'name' ], [ 'class', 'foo' ], [ 'class', 'bar' ] ] } );
1075
+ * ```
1076
+ *
1077
+ * @param consumables Object describing which parts of the element should be consumed.
1078
+ * @returns `true` when all tested items can be consumed and `false` when even one of the items could not be consumed.
1079
+ */ consume(consumables) {
1080
+ if (!this.test(consumables)) {
1081
+ return false;
1082
+ }
1083
+ if (consumables.name) {
1084
+ this._canConsumeName = false;
1085
+ }
1086
+ for (const [name, token] of consumables.attributes){
1087
+ // `value` must be set, because `this.test()` returned `true`.
1088
+ const value = this._attributes.get(name);
1089
+ // Plain (not tokenized) not-consumed attribute.
1090
+ if (typeof value == 'boolean') {
1091
+ // Use Element API to collect related attributes.
1092
+ for (const [toConsume] of this.element._getConsumables(name, token).attributes){
1093
+ this._attributes.set(toConsume, false);
1094
+ }
1095
+ } else if (!token) {
1096
+ // Tokenized attribute but token is not specified so consume all tokens.
1097
+ for (const token of value.keys()){
1098
+ value.set(token, false);
1099
+ }
1100
+ } else {
1101
+ // Use Element API to collect related attribute tokens.
1102
+ for (const [, toConsume] of this.element._getConsumables(name, token).attributes){
1103
+ value.set(toConsume, false);
1104
+ }
1105
+ }
847
1106
  }
1107
+ return true;
848
1108
  }
849
- // Check element's styles.
850
- if (pattern.styles) {
851
- match.styles = matchStyles(pattern.styles, element);
852
- if (!match.styles) {
853
- return null;
1109
+ /**
1110
+ * Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
1111
+ * Element's name can be reverted:
1112
+ *
1113
+ * ```ts
1114
+ * consumables.revert( { name: true } );
1115
+ * ```
1116
+ *
1117
+ * Attributes classes and styles:
1118
+ *
1119
+ * ```ts
1120
+ * consumables.revert( { attributes: [ [ 'title' ], [ 'class', 'foo' ], [ 'style', 'color' ] ] } );
1121
+ * consumables.revert( { attributes: [ [ 'title' ], [ 'name' ], [ 'class', 'foo' ], [ 'class', 'bar' ] ] } );
1122
+ * ```
1123
+ *
1124
+ * @param consumables Object describing which parts of the element should be reverted.
1125
+ */ revert(consumables) {
1126
+ if (consumables.name) {
1127
+ this._canConsumeName = true;
1128
+ }
1129
+ for (const [name, token] of consumables.attributes){
1130
+ const value = this._attributes.get(name);
1131
+ // Plain consumed attribute.
1132
+ if (value === false) {
1133
+ this._attributes.set(name, true);
1134
+ continue;
1135
+ }
1136
+ // Unknown attribute or not consumed.
1137
+ if (value === undefined || value === true) {
1138
+ continue;
1139
+ }
1140
+ if (!token) {
1141
+ // Tokenized attribute but token is not specified so revert all tokens.
1142
+ for (const token of value.keys()){
1143
+ value.set(token, true);
1144
+ }
1145
+ } else {
1146
+ const tokenValue = value.get(token);
1147
+ if (tokenValue === false) {
1148
+ value.set(token, true);
1149
+ }
1150
+ // Note that revert of consumed related styles is not handled.
1151
+ }
854
1152
  }
855
1153
  }
856
- return match;
857
1154
  }
858
1155
  /**
859
- * Checks if name can be matched by provided pattern.
860
- *
861
- * @returns Returns `true` if name can be matched, `false` otherwise.
862
- */ function matchName(pattern, name) {
863
- // If pattern is provided as RegExp - test against this regexp.
864
- if (pattern instanceof RegExp) {
865
- return !!name.match(pattern);
1156
+ * Normalizes a {@link module:engine/conversion/viewconsumable~Consumables} or {@link module:engine/view/matcher~Match}
1157
+ * to a {@link module:engine/view/element~NormalizedConsumables}.
1158
+ */ function normalizeConsumables(consumables) {
1159
+ const attributes = [];
1160
+ if ('attributes' in consumables && consumables.attributes) {
1161
+ normalizeConsumablePart(attributes, consumables.attributes);
866
1162
  }
867
- return pattern === name;
1163
+ if ('classes' in consumables && consumables.classes) {
1164
+ normalizeConsumablePart(attributes, consumables.classes, 'class');
1165
+ }
1166
+ if ('styles' in consumables && consumables.styles) {
1167
+ normalizeConsumablePart(attributes, consumables.styles, 'style');
1168
+ }
1169
+ return {
1170
+ name: consumables.name || false,
1171
+ attributes
1172
+ };
868
1173
  }
869
1174
  /**
870
- * Checks if an array of key/value pairs can be matched against provided patterns.
871
- *
872
- * Patterns can be provided in a following ways:
873
- * - a boolean value matches any attribute with any value (or no value):
874
- *
875
- * ```ts
876
- * pattern: true
877
- * ```
878
- *
879
- * - a RegExp expression or object matches any attribute name:
880
- *
881
- * ```ts
882
- * pattern: /h[1-6]/
883
- * ```
884
- *
885
- * - an object matches any attribute that has the same name as the object item's key, where object item's value is:
886
- * - equal to `true`, which matches any attribute value:
887
- *
888
- * ```ts
889
- * pattern: {
890
- * required: true
891
- * }
892
- * ```
893
- *
894
- * - a string that is equal to attribute value:
895
- *
896
- * ```ts
897
- * pattern: {
898
- * rel: 'nofollow'
899
- * }
900
- * ```
901
- *
902
- * - a regular expression that matches attribute value,
903
- *
904
- * ```ts
905
- * pattern: {
906
- * src: /^https/
907
- * }
908
- * ```
909
- *
910
- * - an array with items, where the item is:
911
- * - a string that is equal to attribute value:
912
- *
913
- * ```ts
914
- * pattern: [ 'data-property-1', 'data-property-2' ],
915
- * ```
916
- *
917
- * - an object with `key` and `value` property, where `key` is a regular expression matching attribute name and
918
- * `value` is either regular expression matching attribute value or a string equal to attribute value:
919
- *
920
- * ```ts
921
- * pattern: [
922
- * { key: /^data-property-/, value: true },
923
- * // or:
924
- * { key: /^data-property-/, value: 'foobar' },
925
- * // or:
926
- * { key: /^data-property-/, value: /^foo/ }
927
- * ]
928
- * ```
929
- *
930
- * @param patterns Object with information about attributes to match.
931
- * @param keys Attribute, style or class keys.
932
- * @param valueGetter A function providing value for a given item key.
933
- * @returns Returns array with matched attribute names or `null` if no attributes were matched.
934
- */ function matchPatterns(patterns, keys, valueGetter) {
935
- const normalizedPatterns = normalizePatterns(patterns);
936
- const normalizedItems = Array.from(keys);
937
- const match = [];
938
- normalizedPatterns.forEach(([patternKey, patternValue])=>{
939
- normalizedItems.forEach((itemKey)=>{
940
- if (isKeyMatched(patternKey, itemKey) && isValueMatched(patternValue, itemKey, valueGetter)) {
941
- match.push(itemKey);
942
- }
943
- });
944
- });
945
- // Return matches only if there are at least as many of them as there are patterns.
946
- // The RegExp pattern can match more than one item.
947
- if (!normalizedPatterns.length || match.length < normalizedPatterns.length) {
948
- return undefined;
1175
+ * Normalizes a list of consumable attributes to a common tuple format.
1176
+ */ function normalizeConsumablePart(attributes, items, prefix) {
1177
+ if (typeof items == 'string') {
1178
+ attributes.push(prefix ? [
1179
+ prefix,
1180
+ items
1181
+ ] : [
1182
+ items
1183
+ ]);
1184
+ return;
1185
+ }
1186
+ for (const item of items){
1187
+ if (Array.isArray(item)) {
1188
+ attributes.push(item);
1189
+ } else {
1190
+ attributes.push(prefix ? [
1191
+ prefix,
1192
+ item
1193
+ ] : [
1194
+ item
1195
+ ]);
1196
+ }
949
1197
  }
950
- return match;
951
1198
  }
1199
+
952
1200
  /**
953
- * Bring all the possible pattern forms to an array of arrays where first item is a key and second is a value.
954
- *
955
- * Examples:
956
- *
957
- * Boolean pattern value:
958
- *
959
- * ```ts
960
- * true
961
- * ```
962
- *
963
- * to
964
- *
965
- * ```ts
966
- * [ [ true, true ] ]
967
- * ```
968
- *
969
- * Textual pattern value:
970
- *
971
- * ```ts
972
- * 'attribute-name-or-class-or-style'
973
- * ```
974
- *
975
- * to
976
- *
977
- * ```ts
978
- * [ [ 'attribute-name-or-class-or-style', true ] ]
979
- * ```
980
- *
981
- * Regular expression:
982
- *
983
- * ```ts
984
- * /^data-.*$/
985
- * ```
986
- *
987
- * to
988
- *
989
- * ```ts
990
- * [ [ /^data-.*$/, true ] ]
991
- * ```
992
- *
1201
+ * View matcher class.
1202
+ * Instance of this class can be used to find {@link module:engine/view/element~Element elements} that match given pattern.
1203
+ */ class Matcher {
1204
+ _patterns = [];
1205
+ /**
1206
+ * Creates new instance of Matcher.
1207
+ *
1208
+ * @param pattern Match patterns. See {@link module:engine/view/matcher~Matcher#add add method} for more information.
1209
+ */ constructor(...pattern){
1210
+ this.add(...pattern);
1211
+ }
1212
+ /**
1213
+ * Adds pattern or patterns to matcher instance.
1214
+ *
1215
+ * ```ts
1216
+ * // String.
1217
+ * matcher.add( 'div' );
1218
+ *
1219
+ * // Regular expression.
1220
+ * matcher.add( /^\w/ );
1221
+ *
1222
+ * // Single class.
1223
+ * matcher.add( {
1224
+ * classes: 'foobar'
1225
+ * } );
1226
+ * ```
1227
+ *
1228
+ * See {@link module:engine/view/matcher~MatcherPattern} for more examples.
1229
+ *
1230
+ * Multiple patterns can be added in one call:
1231
+ *
1232
+ * ```ts
1233
+ * matcher.add( 'div', { classes: 'foobar' } );
1234
+ * ```
1235
+ *
1236
+ * @param pattern Object describing pattern details. If string or regular expression
1237
+ * is provided it will be used to match element's name. Pattern can be also provided in a form
1238
+ * of a function - then this function will be called with each {@link module:engine/view/element~Element element} as a parameter.
1239
+ * Function's return value will be stored under `match` key of the object returned from
1240
+ * {@link module:engine/view/matcher~Matcher#match match} or {@link module:engine/view/matcher~Matcher#matchAll matchAll} methods.
1241
+ */ add(...pattern) {
1242
+ for (let item of pattern){
1243
+ // String or RegExp pattern is used as element's name.
1244
+ if (typeof item == 'string' || item instanceof RegExp) {
1245
+ item = {
1246
+ name: item
1247
+ };
1248
+ }
1249
+ this._patterns.push(item);
1250
+ }
1251
+ }
1252
+ /**
1253
+ * Matches elements for currently stored patterns. Returns match information about first found
1254
+ * {@link module:engine/view/element~Element element}, otherwise returns `null`.
1255
+ *
1256
+ * Example of returned object:
1257
+ *
1258
+ * ```ts
1259
+ * {
1260
+ * element: <instance of found element>,
1261
+ * pattern: <pattern used to match found element>,
1262
+ * match: {
1263
+ * name: true,
1264
+ * attributes: [
1265
+ * [ 'title' ],
1266
+ * [ 'href' ],
1267
+ * [ 'class', 'foo' ],
1268
+ * [ 'style', 'color' ],
1269
+ * [ 'style', 'position' ]
1270
+ * ]
1271
+ * }
1272
+ * }
1273
+ * ```
1274
+ *
1275
+ * You could use the `match` field from the above returned object as an input for the
1276
+ * {@link module:engine/conversion/viewconsumable~ViewConsumable#test `ViewConsumable#test()`} and
1277
+ * {@link module:engine/conversion/viewconsumable~ViewConsumable#consume `ViewConsumable#consume()`} methods.
1278
+ *
1279
+ * @see module:engine/view/matcher~Matcher#add
1280
+ * @see module:engine/view/matcher~Matcher#matchAll
1281
+ * @param element View element to match against stored patterns.
1282
+ * @returns The match information about found element or `null`.
1283
+ */ match(...element) {
1284
+ for (const singleElement of element){
1285
+ for (const pattern of this._patterns){
1286
+ const match = this._isElementMatching(singleElement, pattern);
1287
+ if (match) {
1288
+ return {
1289
+ element: singleElement,
1290
+ pattern,
1291
+ match
1292
+ };
1293
+ }
1294
+ }
1295
+ }
1296
+ return null;
1297
+ }
1298
+ /**
1299
+ * Matches elements for currently stored patterns. Returns array of match information with all found
1300
+ * {@link module:engine/view/element~Element elements}. If no element is found - returns `null`.
1301
+ *
1302
+ * @see module:engine/view/matcher~Matcher#add
1303
+ * @see module:engine/view/matcher~Matcher#match
1304
+ * @param element View element to match against stored patterns.
1305
+ * @returns Array with match information about found elements or `null`. For more information
1306
+ * see {@link module:engine/view/matcher~Matcher#match match method} description.
1307
+ */ matchAll(...element) {
1308
+ const results = [];
1309
+ for (const singleElement of element){
1310
+ for (const pattern of this._patterns){
1311
+ const match = this._isElementMatching(singleElement, pattern);
1312
+ if (match) {
1313
+ results.push({
1314
+ element: singleElement,
1315
+ pattern,
1316
+ match
1317
+ });
1318
+ }
1319
+ }
1320
+ }
1321
+ return results.length > 0 ? results : null;
1322
+ }
1323
+ /**
1324
+ * Returns the name of the element to match if there is exactly one pattern added to the matcher instance
1325
+ * and it matches element name defined by `string` (not `RegExp`). Otherwise, returns `null`.
1326
+ *
1327
+ * @returns Element name trying to match.
1328
+ */ getElementName() {
1329
+ if (this._patterns.length !== 1) {
1330
+ return null;
1331
+ }
1332
+ const pattern = this._patterns[0];
1333
+ const name = pattern.name;
1334
+ return typeof pattern != 'function' && name && !(name instanceof RegExp) ? name : null;
1335
+ }
1336
+ /**
1337
+ * Returns match information if {@link module:engine/view/element~Element element} is matching provided pattern.
1338
+ * If element cannot be matched to provided pattern - returns `null`.
1339
+ *
1340
+ * @returns Returns object with match information or null if element is not matching.
1341
+ */ _isElementMatching(element, pattern) {
1342
+ // If pattern is provided as function - return result of that function;
1343
+ if (typeof pattern == 'function') {
1344
+ const match = pattern(element);
1345
+ // In some places we use Matcher with callback pattern that returns boolean.
1346
+ if (!match || typeof match != 'object') {
1347
+ return match;
1348
+ }
1349
+ return normalizeConsumables(match);
1350
+ }
1351
+ const match = {};
1352
+ // Check element's name.
1353
+ if (pattern.name) {
1354
+ match.name = matchName(pattern.name, element.name);
1355
+ if (!match.name) {
1356
+ return null;
1357
+ }
1358
+ }
1359
+ const attributesMatch = [];
1360
+ // Check element's attributes.
1361
+ if (pattern.attributes && !matchAttributes(pattern.attributes, element, attributesMatch)) {
1362
+ return null;
1363
+ }
1364
+ // Check element's classes.
1365
+ if (pattern.classes && !matchClasses(pattern.classes, element, attributesMatch)) {
1366
+ return null;
1367
+ }
1368
+ // Check element's styles.
1369
+ if (pattern.styles && !matchStyles(pattern.styles, element, attributesMatch)) {
1370
+ return null;
1371
+ }
1372
+ // Note the `attributesMatch` array is populated by the above calls.
1373
+ if (attributesMatch.length) {
1374
+ match.attributes = attributesMatch;
1375
+ }
1376
+ return match;
1377
+ }
1378
+ }
1379
+ /**
1380
+ * Returns true if the given `item` matches the pattern.
1381
+ *
1382
+ * @internal
1383
+ * @param pattern A pattern representing a key/value we want to match.
1384
+ * @param item An actual item key/value (e.g. `'src'`, `'background-color'`, `'ck-widget'`) we're testing against pattern.
1385
+ */ function isPatternMatched(pattern, item) {
1386
+ return pattern === true || pattern === item || pattern instanceof RegExp && !!String(item).match(pattern);
1387
+ }
1388
+ /**
1389
+ * Checks if name can be matched by provided pattern.
1390
+ *
1391
+ * @returns Returns `true` if name can be matched, `false` otherwise.
1392
+ */ function matchName(pattern, name) {
1393
+ // If pattern is provided as RegExp - test against this regexp.
1394
+ if (pattern instanceof RegExp) {
1395
+ return !!name.match(pattern);
1396
+ }
1397
+ return pattern === name;
1398
+ }
1399
+ /**
1400
+ * Bring all the possible pattern forms to an array of tuples where first item is a key, second is a value,
1401
+ * and third optional is a token value.
1402
+ *
1403
+ * Examples:
1404
+ *
1405
+ * Boolean pattern value:
1406
+ *
1407
+ * ```ts
1408
+ * true
1409
+ * ```
1410
+ *
1411
+ * to
1412
+ *
1413
+ * ```ts
1414
+ * [ [ true, true ] ]
1415
+ * ```
1416
+ *
1417
+ * Textual pattern value:
1418
+ *
1419
+ * ```ts
1420
+ * 'attribute-name-or-class-or-style'
1421
+ * ```
1422
+ *
1423
+ * to
1424
+ *
1425
+ * ```ts
1426
+ * [ [ 'attribute-name-or-class-or-style', true ] ]
1427
+ * ```
1428
+ *
1429
+ * Regular expression:
1430
+ *
1431
+ * ```ts
1432
+ * /^data-.*$/
1433
+ * ```
1434
+ *
1435
+ * to
1436
+ *
1437
+ * ```ts
1438
+ * [ [ /^data-.*$/, true ] ]
1439
+ * ```
1440
+ *
993
1441
  * Objects (plain or with `key` and `value` specified explicitly):
994
1442
  *
995
1443
  * ```ts
@@ -1014,11 +1462,15 @@ TextProxy$1.prototype.is = function(type) {
1014
1462
  * ```
1015
1463
  *
1016
1464
  * @returns Returns an array of objects or null if provided patterns were not in an expected form.
1017
- */ function normalizePatterns(patterns) {
1465
+ */ function normalizePatterns(patterns, prefix) {
1018
1466
  if (Array.isArray(patterns)) {
1019
1467
  return patterns.map((pattern)=>{
1020
1468
  if (typeof pattern !== 'object' || pattern instanceof RegExp) {
1021
- return [
1469
+ return prefix ? [
1470
+ prefix,
1471
+ pattern,
1472
+ true
1473
+ ] : [
1022
1474
  pattern,
1023
1475
  true
1024
1476
  ];
@@ -1027,7 +1479,11 @@ TextProxy$1.prototype.is = function(type) {
1027
1479
  // Documented at the end of matcher.js.
1028
1480
  logWarning('matcher-pattern-missing-key-or-value', pattern);
1029
1481
  }
1030
- return [
1482
+ return prefix ? [
1483
+ prefix,
1484
+ pattern.key,
1485
+ pattern.value
1486
+ ] : [
1031
1487
  pattern.key,
1032
1488
  pattern.value
1033
1489
  ];
@@ -1035,18 +1491,26 @@ TextProxy$1.prototype.is = function(type) {
1035
1491
  }
1036
1492
  if (typeof patterns !== 'object' || patterns instanceof RegExp) {
1037
1493
  return [
1038
- [
1494
+ prefix ? [
1495
+ prefix,
1496
+ patterns,
1497
+ true
1498
+ ] : [
1039
1499
  patterns,
1040
1500
  true
1041
1501
  ]
1042
1502
  ];
1043
1503
  }
1044
- // Below we do what Object.entries() does, but faster.
1504
+ // Below we do what Object.entries() does, but faster
1045
1505
  const normalizedPatterns = [];
1046
1506
  for(const key in patterns){
1047
1507
  // Replace with Object.hasOwn() when we upgrade to es2022.
1048
1508
  if (Object.prototype.hasOwnProperty.call(patterns, key)) {
1049
- normalizedPatterns.push([
1509
+ normalizedPatterns.push(prefix ? [
1510
+ prefix,
1511
+ key,
1512
+ patterns[key]
1513
+ ] : [
1050
1514
  key,
1051
1515
  patterns[key]
1052
1516
  ]);
@@ -1054,35 +1518,16 @@ TextProxy$1.prototype.is = function(type) {
1054
1518
  }
1055
1519
  return normalizedPatterns;
1056
1520
  }
1057
- /**
1058
- * @param patternKey A pattern representing a key we want to match.
1059
- * @param itemKey An actual item key (e.g. `'src'`, `'background-color'`, `'ck-widget'`) we're testing against pattern.
1060
- */ function isKeyMatched(patternKey, itemKey) {
1061
- return patternKey === true || patternKey === itemKey || patternKey instanceof RegExp && itemKey.match(patternKey);
1062
- }
1063
- /**
1064
- * @param patternValue A pattern representing a value we want to match.
1065
- * @param itemKey An item key, e.g. `background`, `href`, 'rel', etc.
1066
- * @param valueGetter A function used to provide a value for a given `itemKey`.
1067
- */ function isValueMatched(patternValue, itemKey, valueGetter) {
1068
- if (patternValue === true) {
1069
- return true;
1070
- }
1071
- const itemValue = valueGetter(itemKey);
1072
- // For now, the reducers are not returning the full tree of properties.
1073
- // Casting to string preserves the old behavior until the root cause is fixed.
1074
- // More can be found in https://github.com/ckeditor/ckeditor5/issues/10399.
1075
- return patternValue === itemValue || patternValue instanceof RegExp && !!String(itemValue).match(patternValue);
1076
- }
1077
1521
  /**
1078
1522
  * Checks if attributes of provided element can be matched against provided patterns.
1079
1523
  *
1080
1524
  * @param patterns Object with information about attributes to match. Each key of the object will be
1081
1525
  * used as attribute name. Value of each key can be a string or regular expression to match against attribute value.
1082
1526
  * @param element Element which attributes will be tested.
1527
+ * @param match An array to populate with matching tuples.
1083
1528
  * @returns Returns array with matched attribute names or `null` if no attributes were matched.
1084
- */ function matchAttributes(patterns, element) {
1085
- const attributeKeys = new Set(element.getAttributeKeys());
1529
+ */ function matchAttributes(patterns, element, match) {
1530
+ let excludeAttributes;
1086
1531
  // `style` and `class` attribute keys are deprecated. Only allow them in object pattern
1087
1532
  // for backward compatibility.
1088
1533
  if (typeof patterns === 'object' && !(patterns instanceof RegExp) && !Array.isArray(patterns)) {
@@ -1095,20 +1540,22 @@ TextProxy$1.prototype.is = function(type) {
1095
1540
  logWarning('matcher-pattern-deprecated-attributes-class-key', patterns);
1096
1541
  }
1097
1542
  } else {
1098
- attributeKeys.delete('style');
1099
- attributeKeys.delete('class');
1543
+ excludeAttributes = [
1544
+ 'class',
1545
+ 'style'
1546
+ ];
1100
1547
  }
1101
- return matchPatterns(patterns, attributeKeys, (key)=>element.getAttribute(key));
1548
+ return element._collectAttributesMatch(normalizePatterns(patterns), match, excludeAttributes);
1102
1549
  }
1103
1550
  /**
1104
1551
  * Checks if classes of provided element can be matched against provided patterns.
1105
1552
  *
1106
1553
  * @param patterns Array of strings or regular expressions to match against element's classes.
1107
1554
  * @param element Element which classes will be tested.
1555
+ * @param match An array to populate with matching tuples.
1108
1556
  * @returns Returns array with matched class names or `null` if no classes were matched.
1109
- */ function matchClasses(patterns, element) {
1110
- // We don't need `getter` here because patterns for classes are always normalized to `[ className, true ]`.
1111
- return matchPatterns(patterns, element.getClassNames(), /* istanbul ignore next -- @preserve */ ()=>{});
1557
+ */ function matchClasses(patterns, element, match) {
1558
+ return element._collectAttributesMatch(normalizePatterns(patterns, 'class'), match);
1112
1559
  }
1113
1560
  /**
1114
1561
  * Checks if styles of provided element can be matched against provided patterns.
@@ -1116,9 +1563,10 @@ TextProxy$1.prototype.is = function(type) {
1116
1563
  * @param patterns Object with information about styles to match. Each key of the object will be
1117
1564
  * used as style name. Value of each key can be a string or regular expression to match against style value.
1118
1565
  * @param element Element which styles will be tested.
1566
+ * @param match An array to populate with matching tuples.
1119
1567
  * @returns Returns array with matched style names or `null` if no styles were matched.
1120
- */ function matchStyles(patterns, element) {
1121
- return matchPatterns(patterns, element.getStyleNames(true), (key)=>element.getStyle(key));
1568
+ */ function matchStyles(patterns, element, match) {
1569
+ return element._collectAttributesMatch(normalizePatterns(patterns, 'style'), match);
1122
1570
  }
1123
1571
  /**
1124
1572
  * The key-value matcher pattern is missing key or value. Both must be present.
@@ -1238,6 +1686,7 @@ TextProxy$1.prototype.is = function(type) {
1238
1686
  for (const [key, value] of parsedStyles){
1239
1687
  this._styleProcessor.toNormalizedForm(key, value, this._styles);
1240
1688
  }
1689
+ return this;
1241
1690
  }
1242
1691
  /**
1243
1692
  * Checks if a given style is set.
@@ -1315,14 +1764,16 @@ TextProxy$1.prototype.is = function(type) {
1315
1764
  * styles.toString(); // -> 'margin-bottom:1px;margin-left:1px;'
1316
1765
  * ```
1317
1766
  *
1318
- * @param name Style name.
1319
- */ remove(name) {
1320
- this._cachedStyleNames = null;
1321
- this._cachedExpandedStyleNames = null;
1322
- const path = toPath(name);
1323
- unset(this._styles, path);
1324
- delete this._styles[name];
1325
- this._cleanEmptyObjectsOnPath(path);
1767
+ * @param names Style name or an array of names.
1768
+ */ remove(names) {
1769
+ for (const name of toArray(names)){
1770
+ this._cachedStyleNames = null;
1771
+ this._cachedExpandedStyleNames = null;
1772
+ const path = toPath(name);
1773
+ unset(this._styles, path);
1774
+ delete this._styles[name];
1775
+ this._cleanEmptyObjectsOnPath(path);
1776
+ }
1326
1777
  }
1327
1778
  /**
1328
1779
  * Returns a normalized style object or a single value.
@@ -1478,6 +1929,11 @@ TextProxy$1.prototype.is = function(type) {
1478
1929
  this._cachedStyleNames = this._cachedStyleNames || this.getStylesEntries().map(([key])=>key);
1479
1930
  return this._cachedStyleNames;
1480
1931
  }
1932
+ /**
1933
+ * Alias for {@link #getStyleNames}.
1934
+ */ keys() {
1935
+ return this.getStyleNames();
1936
+ }
1481
1937
  /**
1482
1938
  * Removes all styles.
1483
1939
  */ clear() {
@@ -1485,6 +1941,19 @@ TextProxy$1.prototype.is = function(type) {
1485
1941
  this._cachedStyleNames = null;
1486
1942
  this._cachedExpandedStyleNames = null;
1487
1943
  }
1944
+ /**
1945
+ * Returns `true` if both attributes have the same styles.
1946
+ */ isSimilar(other) {
1947
+ if (this.size !== other.size) {
1948
+ return false;
1949
+ }
1950
+ for (const property of this.getStyleNames()){
1951
+ if (!other.has(property) || other.getAsString(property) !== this.getAsString(property)) {
1952
+ return false;
1953
+ }
1954
+ }
1955
+ return true;
1956
+ }
1488
1957
  /**
1489
1958
  * Returns normalized styles entries for further processing.
1490
1959
  */ getStylesEntries() {
@@ -1496,21 +1965,125 @@ TextProxy$1.prototype.is = function(type) {
1496
1965
  return parsed;
1497
1966
  }
1498
1967
  /**
1499
- * Removes empty objects upon removing an entry from internal object.
1500
- */ _cleanEmptyObjectsOnPath(path) {
1501
- const pathParts = path.split('.');
1502
- const isChildPath = pathParts.length > 1;
1503
- if (!isChildPath) {
1504
- return;
1505
- }
1506
- const parentPath = pathParts.splice(0, pathParts.length - 1).join('.');
1507
- const parentObject = get(this._styles, parentPath);
1508
- if (!parentObject) {
1509
- return;
1510
- }
1511
- const isParentEmpty = !Object.keys(parentObject).length;
1512
- if (isParentEmpty) {
1513
- this.remove(parentPath);
1968
+ * Clones the attribute value.
1969
+ *
1970
+ * @internal
1971
+ */ _clone() {
1972
+ const clone = new this.constructor(this._styleProcessor);
1973
+ clone.set(this.getNormalized());
1974
+ return clone;
1975
+ }
1976
+ /**
1977
+ * Used by the {@link module:engine/view/matcher~Matcher Matcher} to collect matching styles.
1978
+ *
1979
+ * @internal
1980
+ * @param tokenPattern The matched style name pattern.
1981
+ * @param valuePattern The matched style value pattern.
1982
+ * @returns An array of matching tokens (style names).
1983
+ */ _getTokensMatch(tokenPattern, valuePattern) {
1984
+ const match = [];
1985
+ for (const styleName of this.getStyleNames(true)){
1986
+ if (isPatternMatched(tokenPattern, styleName)) {
1987
+ if (valuePattern === true) {
1988
+ match.push(styleName);
1989
+ continue;
1990
+ }
1991
+ // For now, the reducers are not returning the full tree of properties.
1992
+ // Casting to string preserves the old behavior until the root cause is fixed.
1993
+ // More can be found in https://github.com/ckeditor/ckeditor5/issues/10399.
1994
+ const value = this.getAsString(styleName);
1995
+ if (isPatternMatched(valuePattern, value)) {
1996
+ match.push(styleName);
1997
+ }
1998
+ }
1999
+ }
2000
+ return match.length ? match : undefined;
2001
+ }
2002
+ /**
2003
+ * Returns a list of consumables for the attribute. This includes related styles.
2004
+ *
2005
+ * Could be filtered by the given style name.
2006
+ *
2007
+ * @internal
2008
+ */ _getConsumables(name) {
2009
+ const result = [];
2010
+ if (name) {
2011
+ result.push(name);
2012
+ for (const relatedName of this._styleProcessor.getRelatedStyles(name)){
2013
+ result.push(relatedName);
2014
+ }
2015
+ } else {
2016
+ for (const name of this.getStyleNames()){
2017
+ for (const relatedName of this._styleProcessor.getRelatedStyles(name)){
2018
+ result.push(relatedName);
2019
+ }
2020
+ result.push(name);
2021
+ }
2022
+ }
2023
+ return result;
2024
+ }
2025
+ /**
2026
+ * Used by {@link module:engine/view/element~Element#_canMergeAttributesFrom} to verify if the given attribute can be merged without
2027
+ * conflicts into the attribute.
2028
+ *
2029
+ * This method is indirectly used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
2030
+ * an {@link module:engine/view/attributeelement~AttributeElement} to merge it with other AttributeElement.
2031
+ *
2032
+ * @internal
2033
+ */ _canMergeFrom(other) {
2034
+ for (const key of other.getStyleNames()){
2035
+ if (this.has(key) && this.getAsString(key) !== other.getAsString(key)) {
2036
+ return false;
2037
+ }
2038
+ }
2039
+ return true;
2040
+ }
2041
+ /**
2042
+ * Used by {@link module:engine/view/element~Element#_mergeAttributesFrom} to merge a given attribute into the attribute.
2043
+ *
2044
+ * This method is indirectly used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
2045
+ * an {@link module:engine/view/attributeelement~AttributeElement} to merge it with other AttributeElement.
2046
+ *
2047
+ * @internal
2048
+ */ _mergeFrom(other) {
2049
+ for (const prop of other.getStyleNames()){
2050
+ if (!this.has(prop)) {
2051
+ this.set(prop, other.getAsString(prop));
2052
+ }
2053
+ }
2054
+ }
2055
+ /**
2056
+ * Used by {@link module:engine/view/element~Element#_canSubtractAttributesOf} to verify if the given attribute can be fully
2057
+ * subtracted from the attribute.
2058
+ *
2059
+ * This method is indirectly used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
2060
+ * an {@link module:engine/view/attributeelement~AttributeElement} to unwrap the AttributeElement.
2061
+ *
2062
+ * @internal
2063
+ */ _isMatching(other) {
2064
+ for (const key of other.getStyleNames()){
2065
+ if (!this.has(key) || this.getAsString(key) !== other.getAsString(key)) {
2066
+ return false;
2067
+ }
2068
+ }
2069
+ return true;
2070
+ }
2071
+ /**
2072
+ * Removes empty objects upon removing an entry from internal object.
2073
+ */ _cleanEmptyObjectsOnPath(path) {
2074
+ const pathParts = path.split('.');
2075
+ const isChildPath = pathParts.length > 1;
2076
+ if (!isChildPath) {
2077
+ return;
2078
+ }
2079
+ const parentPath = pathParts.splice(0, pathParts.length - 1).join('.');
2080
+ const parentObject = get(this._styles, parentPath);
2081
+ if (!parentObject) {
2082
+ return;
2083
+ }
2084
+ const isParentEmpty = !Object.keys(parentObject).length;
2085
+ if (isParentEmpty) {
2086
+ this.remove(parentPath);
1514
2087
  }
1515
2088
  }
1516
2089
  }
@@ -1942,6 +2515,176 @@ TextProxy$1.prototype.is = function(type) {
1942
2515
  set(stylesObject, nameOrPath, valueToSet);
1943
2516
  }
1944
2517
 
2518
+ /**
2519
+ * Token list. Allows handling (adding, removing, retrieving) a set of tokens (for example class names).
2520
+ */ class TokenList {
2521
+ /**
2522
+ * The set of tokens.
2523
+ */ _set = new Set();
2524
+ /**
2525
+ * Returns true if token list has no tokens set.
2526
+ */ get isEmpty() {
2527
+ return this._set.size == 0;
2528
+ }
2529
+ /**
2530
+ * Number of tokens.
2531
+ */ get size() {
2532
+ return this._set.size;
2533
+ }
2534
+ /**
2535
+ * Checks if a given token is set.
2536
+ */ has(name) {
2537
+ return this._set.has(name);
2538
+ }
2539
+ /**
2540
+ * Returns all tokens.
2541
+ */ keys() {
2542
+ return Array.from(this._set.keys());
2543
+ }
2544
+ /**
2545
+ * Resets the value to the given one.
2546
+ */ setTo(value) {
2547
+ this.clear();
2548
+ for (const token of value.split(/\s+/)){
2549
+ if (token) {
2550
+ this._set.add(token);
2551
+ }
2552
+ }
2553
+ return this;
2554
+ }
2555
+ /**
2556
+ * Sets a given token without affecting other tokens.
2557
+ */ set(tokens) {
2558
+ for (const token of toArray(tokens)){
2559
+ if (token) {
2560
+ this._set.add(token);
2561
+ }
2562
+ }
2563
+ }
2564
+ /**
2565
+ * Removes given token.
2566
+ */ remove(tokens) {
2567
+ for (const token of toArray(tokens)){
2568
+ this._set.delete(token);
2569
+ }
2570
+ }
2571
+ /**
2572
+ * Removes all tokens.
2573
+ */ clear() {
2574
+ this._set.clear();
2575
+ }
2576
+ /**
2577
+ * Returns a normalized tokens string.
2578
+ */ toString() {
2579
+ return Array.from(this._set).join(' ');
2580
+ }
2581
+ /**
2582
+ * Returns `true` if both attributes have the same tokens.
2583
+ */ isSimilar(other) {
2584
+ if (this.size !== other.size) {
2585
+ return false;
2586
+ }
2587
+ for (const token of this.keys()){
2588
+ if (!other.has(token)) {
2589
+ return false;
2590
+ }
2591
+ }
2592
+ return true;
2593
+ }
2594
+ /**
2595
+ * Clones the attribute value.
2596
+ *
2597
+ * @internal
2598
+ */ _clone() {
2599
+ const clone = new this.constructor();
2600
+ clone._set = new Set(this._set);
2601
+ return clone;
2602
+ }
2603
+ /**
2604
+ * Used by the {@link module:engine/view/matcher~Matcher Matcher} to collect matching attribute tokens.
2605
+ *
2606
+ * @internal
2607
+ * @param tokenPattern The matched token name pattern.
2608
+ * @returns An array of matching tokens.
2609
+ */ _getTokensMatch(tokenPattern) {
2610
+ const match = [];
2611
+ if (tokenPattern === true) {
2612
+ for (const token of this._set.keys()){
2613
+ match.push(token);
2614
+ }
2615
+ return match;
2616
+ }
2617
+ if (typeof tokenPattern == 'string') {
2618
+ for (const token of tokenPattern.split(/\s+/)){
2619
+ if (this._set.has(token)) {
2620
+ match.push(token);
2621
+ } else {
2622
+ return undefined;
2623
+ }
2624
+ }
2625
+ return match;
2626
+ }
2627
+ for (const token of this._set.keys()){
2628
+ if (token.match(tokenPattern)) {
2629
+ match.push(token);
2630
+ }
2631
+ }
2632
+ return match.length ? match : undefined;
2633
+ }
2634
+ /**
2635
+ * Returns a list of consumables for the attribute.
2636
+ *
2637
+ * Could be filtered by the given token name.
2638
+ *
2639
+ * @internal
2640
+ */ _getConsumables(name) {
2641
+ return name ? [
2642
+ name
2643
+ ] : this.keys();
2644
+ }
2645
+ /**
2646
+ * Used by {@link module:engine/view/element~Element#_canMergeAttributesFrom} to verify if the given attribute
2647
+ * can be merged without conflicts into the attribute.
2648
+ *
2649
+ * This method is indirectly used by the {@link module:engine/view/downcastwriter~DowncastWriter} while downcasting
2650
+ * an {@link module:engine/view/attributeelement~AttributeElement} to merge it with other `AttributeElement`.
2651
+ *
2652
+ * @internal
2653
+ */ _canMergeFrom() {
2654
+ return true;
2655
+ }
2656
+ /**
2657
+ * Used by {@link module:engine/view/element~Element#_mergeAttributesFrom} to merge a given attribute into the attribute.
2658
+ *
2659
+ * This method is indirectly used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
2660
+ * an {@link module:engine/view/attributeelement~AttributeElement} to merge it with other AttributeElement.
2661
+ *
2662
+ * @internal
2663
+ */ _mergeFrom(other) {
2664
+ for (const token of other._set.keys()){
2665
+ if (!this._set.has(token)) {
2666
+ this._set.add(token);
2667
+ }
2668
+ }
2669
+ }
2670
+ /**
2671
+ * Used by {@link module:engine/view/element~Element#_canSubtractAttributesOf} to verify if the given attribute
2672
+ * can be fully subtracted from the attribute.
2673
+ *
2674
+ * This method is indirectly used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
2675
+ * an {@link module:engine/view/attributeelement~AttributeElement} to unwrap the AttributeElement.
2676
+ *
2677
+ * @internal
2678
+ */ _isMatching(other) {
2679
+ for (const name of other._set.keys()){
2680
+ if (!this._set.has(name)) {
2681
+ return false;
2682
+ }
2683
+ }
2684
+ return true;
2685
+ }
2686
+ }
2687
+
1945
2688
  // @if CK_DEBUG_ENGINE // const { convertMapToTags } = require( '../dev-utils/utils' );
1946
2689
  /**
1947
2690
  * View element.
@@ -1986,16 +2729,24 @@ TextProxy$1.prototype.is = function(type) {
1986
2729
  /**
1987
2730
  * Array of child nodes.
1988
2731
  */ _children;
1989
- /**
1990
- * Set of classes associated with element instance.
1991
- */ _classes;
1992
- /**
1993
- * Normalized styles.
1994
- */ _styles;
1995
2732
  /**
1996
2733
  * Map of custom properties.
1997
2734
  * Custom properties can be added to element instance, will be cloned but not rendered into DOM.
1998
2735
  */ _customProperties = new Map();
2736
+ /**
2737
+ * Set of classes associated with element instance.
2738
+ *
2739
+ * Note that this is just an alias for `this._attrs.get( 'class' );`
2740
+ */ get _classes() {
2741
+ return this._attrs.get('class');
2742
+ }
2743
+ /**
2744
+ * Normalized styles.
2745
+ *
2746
+ * Note that this is just an alias for `this._attrs.get( 'style' );`
2747
+ */ get _styles() {
2748
+ return this._attrs.get('style');
2749
+ }
1999
2750
  /**
2000
2751
  * Creates a view element.
2001
2752
  *
@@ -2015,24 +2766,11 @@ TextProxy$1.prototype.is = function(type) {
2015
2766
  */ constructor(document, name, attrs, children){
2016
2767
  super(document);
2017
2768
  this.name = name;
2018
- this._attrs = parseAttributes(attrs);
2769
+ this._attrs = this._parseAttributes(attrs);
2019
2770
  this._children = [];
2020
2771
  if (children) {
2021
2772
  this._insertChild(0, children);
2022
2773
  }
2023
- this._classes = new Set();
2024
- if (this._attrs.has('class')) {
2025
- // Remove class attribute and handle it by class set.
2026
- const classString = this._attrs.get('class');
2027
- parseClasses(this._classes, classString);
2028
- this._attrs.delete('class');
2029
- }
2030
- this._styles = new StylesMap(this.document.stylesProcessor);
2031
- if (this._attrs.has('style')) {
2032
- // Remove style attribute and handle it by styles map.
2033
- this._styles.setTo(this._attrs.get('style'));
2034
- this._attrs.delete('style');
2035
- }
2036
2774
  }
2037
2775
  /**
2038
2776
  * Number of element's children.
@@ -2072,13 +2810,19 @@ TextProxy$1.prototype.is = function(type) {
2072
2810
  *
2073
2811
  * @returns Keys for attributes.
2074
2812
  */ *getAttributeKeys() {
2075
- if (this._classes.size > 0) {
2813
+ // This is yielded in this specific order to maintain backward compatibility of data.
2814
+ // Otherwise, we could simply just have the `for` loop only inside this method.
2815
+ if (this._classes) {
2076
2816
  yield 'class';
2077
2817
  }
2078
- if (!this._styles.isEmpty) {
2818
+ if (this._styles) {
2079
2819
  yield 'style';
2080
2820
  }
2081
- yield* this._attrs.keys();
2821
+ for (const key of this._attrs.keys()){
2822
+ if (key != 'class' && key != 'style') {
2823
+ yield key;
2824
+ }
2825
+ }
2082
2826
  }
2083
2827
  /**
2084
2828
  * Returns iterator that iterates over this element's attributes.
@@ -2086,17 +2830,10 @@ TextProxy$1.prototype.is = function(type) {
2086
2830
  * Attributes are returned as arrays containing two items. First one is attribute key and second is attribute value.
2087
2831
  * This format is accepted by native `Map` object and also can be passed in `Node` constructor.
2088
2832
  */ *getAttributes() {
2089
- yield* this._attrs.entries();
2090
- if (this._classes.size > 0) {
2833
+ for (const [name, value] of this._attrs.entries()){
2091
2834
  yield [
2092
- 'class',
2093
- this.getAttribute('class')
2094
- ];
2095
- }
2096
- if (!this._styles.isEmpty) {
2097
- yield [
2098
- 'style',
2099
- this.getAttribute('style')
2835
+ name,
2836
+ String(value)
2100
2837
  ];
2101
2838
  }
2102
2839
  }
@@ -2106,33 +2843,25 @@ TextProxy$1.prototype.is = function(type) {
2106
2843
  * @param key Attribute key.
2107
2844
  * @returns Attribute value.
2108
2845
  */ getAttribute(key) {
2109
- if (key == 'class') {
2110
- if (this._classes.size > 0) {
2111
- return [
2112
- ...this._classes
2113
- ].join(' ');
2114
- }
2115
- return undefined;
2116
- }
2117
- if (key == 'style') {
2118
- const inlineStyle = this._styles.toString();
2119
- return inlineStyle == '' ? undefined : inlineStyle;
2120
- }
2121
- return this._attrs.get(key);
2846
+ return this._attrs.has(key) ? String(this._attrs.get(key)) : undefined;
2122
2847
  }
2123
2848
  /**
2124
2849
  * Returns a boolean indicating whether an attribute with the specified key exists in the element.
2125
2850
  *
2126
2851
  * @param key Attribute key.
2127
2852
  * @returns `true` if attribute with the specified key exists in the element, `false` otherwise.
2128
- */ hasAttribute(key) {
2129
- if (key == 'class') {
2130
- return this._classes.size > 0;
2853
+ */ hasAttribute(key, token) {
2854
+ if (!this._attrs.has(key)) {
2855
+ return false;
2131
2856
  }
2132
- if (key == 'style') {
2133
- return !this._styles.isEmpty;
2857
+ if (token !== undefined) {
2858
+ if (usesStylesMap(this.name, key) || usesTokenList(this.name, key)) {
2859
+ return this._attrs.get(key).has(token);
2860
+ } else {
2861
+ return this._attrs.get(key) === token;
2862
+ }
2134
2863
  }
2135
- return this._attrs.has(key);
2864
+ return true;
2136
2865
  }
2137
2866
  /**
2138
2867
  * Checks if this element is similar to other element.
@@ -2151,24 +2880,20 @@ TextProxy$1.prototype.is = function(type) {
2151
2880
  return false;
2152
2881
  }
2153
2882
  // Check number of attributes, classes and styles.
2154
- if (this._attrs.size !== otherElement._attrs.size || this._classes.size !== otherElement._classes.size || this._styles.size !== otherElement._styles.size) {
2883
+ if (this._attrs.size !== otherElement._attrs.size) {
2155
2884
  return false;
2156
2885
  }
2157
2886
  // Check if attributes are the same.
2158
2887
  for (const [key, value] of this._attrs){
2159
- if (!otherElement._attrs.has(key) || otherElement._attrs.get(key) !== value) {
2888
+ const otherValue = otherElement._attrs.get(key);
2889
+ if (otherValue === undefined) {
2160
2890
  return false;
2161
2891
  }
2162
- }
2163
- // Check if classes are the same.
2164
- for (const className of this._classes){
2165
- if (!otherElement._classes.has(className)) {
2166
- return false;
2167
- }
2168
- }
2169
- // Check if styles are the same.
2170
- for (const property of this._styles.getStyleNames()){
2171
- if (!otherElement._styles.has(property) || otherElement._styles.getAsString(property) !== this._styles.getAsString(property)) {
2892
+ if (typeof value == 'string' || typeof otherValue == 'string') {
2893
+ if (otherValue !== value) {
2894
+ return false;
2895
+ }
2896
+ } else if (!value.isSimilar(otherValue)) {
2172
2897
  return false;
2173
2898
  }
2174
2899
  }
@@ -2184,7 +2909,7 @@ TextProxy$1.prototype.is = function(type) {
2184
2909
  * ```
2185
2910
  */ hasClass(...className) {
2186
2911
  for (const name of className){
2187
- if (!this._classes.has(name)) {
2912
+ if (!this._classes || !this._classes.has(name)) {
2188
2913
  return false;
2189
2914
  }
2190
2915
  }
@@ -2193,10 +2918,15 @@ TextProxy$1.prototype.is = function(type) {
2193
2918
  /**
2194
2919
  * Returns iterator that contains all class names.
2195
2920
  */ getClassNames() {
2196
- return this._classes.keys();
2921
+ const array = this._classes ? this._classes.keys() : [];
2922
+ // This is overcomplicated because we need to be backward compatible for use cases when iterator is expected.
2923
+ const iterator = array[Symbol.iterator]();
2924
+ return Object.assign(array, {
2925
+ next: iterator.next.bind(iterator)
2926
+ });
2197
2927
  }
2198
2928
  /**
2199
- * Returns style value for the given property mae.
2929
+ * Returns style value for the given property name.
2200
2930
  * If the style does not exist `undefined` is returned.
2201
2931
  *
2202
2932
  * **Note**: This method can work with normalized style names if
@@ -2220,7 +2950,7 @@ TextProxy$1.prototype.is = function(type) {
2220
2950
  * element.getStyle( 'margin' ); // -> 'margin: 1px 1px 3em;'
2221
2951
  * ```
2222
2952
  */ getStyle(property) {
2223
- return this._styles.getAsString(property);
2953
+ return this._styles && this._styles.getAsString(property);
2224
2954
  }
2225
2955
  /**
2226
2956
  * Returns a normalized style object or single style value.
@@ -2256,14 +2986,14 @@ TextProxy$1.prototype.is = function(type) {
2256
2986
  *
2257
2987
  * @param property Name of CSS property
2258
2988
  */ getNormalizedStyle(property) {
2259
- return this._styles.getNormalized(property);
2989
+ return this._styles && this._styles.getNormalized(property);
2260
2990
  }
2261
2991
  /**
2262
- * Returns iterator that contains all style names.
2992
+ * Returns an array that contains all style names.
2263
2993
  *
2264
2994
  * @param expand Expand shorthand style properties and return all equivalent style representations.
2265
2995
  */ getStyleNames(expand) {
2266
- return this._styles.getStyleNames(expand);
2996
+ return this._styles ? this._styles.getStyleNames(expand) : [];
2267
2997
  }
2268
2998
  /**
2269
2999
  * Returns true if style keys are present.
@@ -2275,7 +3005,7 @@ TextProxy$1.prototype.is = function(type) {
2275
3005
  * ```
2276
3006
  */ hasStyle(...property) {
2277
3007
  for (const name of property){
2278
- if (!this._styles.has(name)) {
3008
+ if (!this._styles || !this._styles.has(name)) {
2279
3009
  return false;
2280
3010
  }
2281
3011
  }
@@ -2335,9 +3065,9 @@ TextProxy$1.prototype.is = function(type) {
2335
3065
  *
2336
3066
  * **Note**: Classes, styles and other attributes are sorted alphabetically.
2337
3067
  */ getIdentity() {
2338
- const classes = Array.from(this._classes).sort().join(',');
2339
- const styles = this._styles.toString();
2340
- const attributes = Array.from(this._attrs).map((i)=>`${i[0]}="${i[1]}"`).sort().join(' ');
3068
+ const classes = this._classes ? this._classes.keys().sort().join(',') : '';
3069
+ const styles = this._styles && String(this._styles);
3070
+ const attributes = Array.from(this._attrs).filter(([key])=>key != 'style' && key != 'class').map((i)=>`${i[0]}="${i[1]}"`).sort().join(' ');
2341
3071
  return this.name + (classes == '' ? '' : ` class="${classes}"`) + (!styles ? '' : ` style="${styles}"`) + (attributes == '' ? '' : ` ${attributes}`);
2342
3072
  }
2343
3073
  /**
@@ -2366,10 +3096,6 @@ TextProxy$1.prototype.is = function(type) {
2366
3096
  }
2367
3097
  // ContainerElement and AttributeElement should be also cloned properly.
2368
3098
  const cloned = new this.constructor(this.document, this.name, this._attrs, childrenClone);
2369
- // Classes and styles are cloned separately - this solution is faster than adding them back to attributes and
2370
- // parse once again in constructor.
2371
- cloned._classes = new Set(this._classes);
2372
- cloned._styles.set(this._styles.getNormalized());
2373
3099
  // Clone custom properties.
2374
3100
  cloned._customProperties = new Map(this._customProperties);
2375
3101
  // Clone filler offset method.
@@ -2446,16 +3172,30 @@ TextProxy$1.prototype.is = function(type) {
2446
3172
  * @internal
2447
3173
  * @param key Attribute key.
2448
3174
  * @param value Attribute value.
3175
+ * @param overwrite Whether tokenized attribute should override the attribute value or just add a token.
2449
3176
  * @fires change
2450
- */ _setAttribute(key, value) {
2451
- const stringValue = String(value);
3177
+ */ _setAttribute(key, value, overwrite = true) {
2452
3178
  this._fireChange('attributes', this);
2453
- if (key == 'class') {
2454
- parseClasses(this._classes, stringValue);
2455
- } else if (key == 'style') {
2456
- this._styles.setTo(stringValue);
3179
+ if (usesStylesMap(this.name, key) || usesTokenList(this.name, key)) {
3180
+ let currentValue = this._attrs.get(key);
3181
+ if (!currentValue) {
3182
+ currentValue = usesStylesMap(this.name, key) ? new StylesMap(this.document.stylesProcessor) : new TokenList();
3183
+ this._attrs.set(key, currentValue);
3184
+ }
3185
+ if (overwrite) {
3186
+ // If reset is set then value have to be a string to tokenize.
3187
+ currentValue.setTo(String(value));
3188
+ } else if (usesStylesMap(this.name, key)) {
3189
+ if (Array.isArray(value)) {
3190
+ currentValue.set(value[0], value[1]);
3191
+ } else {
3192
+ currentValue.set(value);
3193
+ }
3194
+ } else {
3195
+ currentValue.set(typeof value == 'string' ? value.split(/\s+/) : value);
3196
+ }
2457
3197
  } else {
2458
- this._attrs.set(key, stringValue);
3198
+ this._attrs.set(key, String(value));
2459
3199
  }
2460
3200
  }
2461
3201
  /**
@@ -2464,27 +3204,25 @@ TextProxy$1.prototype.is = function(type) {
2464
3204
  * @see module:engine/view/downcastwriter~DowncastWriter#removeAttribute
2465
3205
  * @internal
2466
3206
  * @param key Attribute key.
3207
+ * @param tokens Attribute value tokens to remove. The whole attribute is removed if not specified.
2467
3208
  * @returns Returns true if an attribute existed and has been removed.
2468
3209
  * @fires change
2469
- */ _removeAttribute(key) {
3210
+ */ _removeAttribute(key, tokens) {
2470
3211
  this._fireChange('attributes', this);
2471
- // Remove class attribute.
2472
- if (key == 'class') {
2473
- if (this._classes.size > 0) {
2474
- this._classes.clear();
2475
- return true;
3212
+ if (tokens !== undefined && (usesStylesMap(this.name, key) || usesTokenList(this.name, key))) {
3213
+ const currentValue = this._attrs.get(key);
3214
+ if (!currentValue) {
3215
+ return false;
2476
3216
  }
2477
- return false;
2478
- }
2479
- // Remove style attribute.
2480
- if (key == 'style') {
2481
- if (!this._styles.isEmpty) {
2482
- this._styles.clear();
2483
- return true;
3217
+ if (usesTokenList(this.name, key) && typeof tokens == 'string') {
3218
+ tokens = tokens.split(/\s+/);
3219
+ }
3220
+ currentValue.remove(tokens);
3221
+ if (currentValue.isEmpty) {
3222
+ return this._attrs.delete(key);
2484
3223
  }
2485
3224
  return false;
2486
3225
  }
2487
- // Remove other attributes.
2488
3226
  return this._attrs.delete(key);
2489
3227
  }
2490
3228
  /**
@@ -2499,10 +3237,7 @@ TextProxy$1.prototype.is = function(type) {
2499
3237
  * @internal
2500
3238
  * @fires change
2501
3239
  */ _addClass(className) {
2502
- this._fireChange('attributes', this);
2503
- for (const name of toArray(className)){
2504
- this._classes.add(name);
2505
- }
3240
+ this._setAttribute('class', className, false);
2506
3241
  }
2507
3242
  /**
2508
3243
  * Removes specified class.
@@ -2516,17 +3251,16 @@ TextProxy$1.prototype.is = function(type) {
2516
3251
  * @internal
2517
3252
  * @fires change
2518
3253
  */ _removeClass(className) {
2519
- this._fireChange('attributes', this);
2520
- for (const name of toArray(className)){
2521
- this._classes.delete(name);
2522
- }
3254
+ this._removeAttribute('class', className);
2523
3255
  }
2524
3256
  _setStyle(property, value) {
2525
- this._fireChange('attributes', this);
2526
3257
  if (typeof property != 'string') {
2527
- this._styles.set(property);
3258
+ this._setAttribute('style', property, false);
2528
3259
  } else {
2529
- this._styles.set(property, value);
3260
+ this._setAttribute('style', [
3261
+ property,
3262
+ value
3263
+ ], false);
2530
3264
  }
2531
3265
  }
2532
3266
  /**
@@ -2545,28 +3279,325 @@ TextProxy$1.prototype.is = function(type) {
2545
3279
  * @internal
2546
3280
  * @fires change
2547
3281
  */ _removeStyle(property) {
2548
- this._fireChange('attributes', this);
2549
- for (const name of toArray(property)){
2550
- this._styles.remove(name);
2551
- }
3282
+ this._removeAttribute('style', property);
2552
3283
  }
2553
3284
  /**
2554
- * Sets a custom property. Unlike attributes, custom properties are not rendered to the DOM,
2555
- * so they can be used to add special data to elements.
3285
+ * Used by the {@link module:engine/view/matcher~Matcher Matcher} to collect matching attribute tuples
3286
+ * (attribute name and optional token).
2556
3287
  *
2557
- * @see module:engine/view/downcastwriter~DowncastWriter#setCustomProperty
2558
- * @internal
2559
- */ _setCustomProperty(key, value) {
2560
- this._customProperties.set(key, value);
2561
- }
2562
- /**
2563
- * Removes the custom property stored under the given key.
3288
+ * Normalized patterns can be used in following ways:
3289
+ * - to match any attribute name with any or no value:
3290
+ *
3291
+ * ```ts
3292
+ * patterns: [
3293
+ * [ true, true ]
3294
+ * ]
3295
+ * ```
3296
+ *
3297
+ * - to match a specific attribute with any value:
3298
+ *
3299
+ * ```ts
3300
+ * patterns: [
3301
+ * [ 'required', true ]
3302
+ * ]
3303
+ * ```
3304
+ *
3305
+ * - to match an attribute name with a RegExp with any value:
3306
+ *
3307
+ * ```ts
3308
+ * patterns: [
3309
+ * [ /h[1-6]/, true ]
3310
+ * ]
3311
+ * ```
3312
+ *
3313
+ * - to match a specific attribute with the exact value:
3314
+ *
3315
+ * ```ts
3316
+ * patterns: [
3317
+ * [ 'rel', 'nofollow' ]
3318
+ * ]
3319
+ * ```
3320
+ *
3321
+ * - to match a specific attribute with a value matching a RegExp:
3322
+ *
3323
+ * ```ts
3324
+ * patterns: [
3325
+ * [ 'src', /^https/ ]
3326
+ * ]
3327
+ * ```
3328
+ *
3329
+ * - to match an attribute name with a RegExp and the exact value:
3330
+ *
3331
+ * ```ts
3332
+ * patterns: [
3333
+ * [ /^data-property-/, 'foobar' ],
3334
+ * ]
3335
+ * ```
3336
+ *
3337
+ * - to match an attribute name with a RegExp and match a value with another RegExp:
3338
+ *
3339
+ * ```ts
3340
+ * patterns: [
3341
+ * [ /^data-property-/, /^foo/ ]
3342
+ * ]
3343
+ * ```
3344
+ *
3345
+ * - to match a specific style property with the value matching a RegExp:
3346
+ *
3347
+ * ```ts
3348
+ * patterns: [
3349
+ * [ 'style', 'font-size', /px$/ ]
3350
+ * ]
3351
+ * ```
3352
+ *
3353
+ * - to match a specific class (class attribute is tokenized so it matches tokens individually):
3354
+ *
3355
+ * ```ts
3356
+ * patterns: [
3357
+ * [ 'class', 'foo' ]
3358
+ * ]
3359
+ * ```
2564
3360
  *
2565
- * @see module:engine/view/downcastwriter~DowncastWriter#removeCustomProperty
2566
3361
  * @internal
2567
- * @returns Returns true if property was removed.
2568
- */ _removeCustomProperty(key) {
2569
- return this._customProperties.delete(key);
3362
+ * @param patterns An array of normalized patterns (tuples of 2 or 3 items depending on if tokenized attribute value match is needed).
3363
+ * @param match An array to populate with matching tuples.
3364
+ * @param exclude Array of attribute names to exclude from match.
3365
+ * @returns `true` if element matches all patterns. The matching tuples are pushed to the `match` array.
3366
+ */ _collectAttributesMatch(patterns, match, exclude) {
3367
+ for (const [keyPattern, tokenPattern, valuePattern] of patterns){
3368
+ let hasKey = false;
3369
+ let hasValue = false;
3370
+ for (const [key, value] of this._attrs){
3371
+ if (exclude && exclude.includes(key) || !isPatternMatched(keyPattern, key)) {
3372
+ continue;
3373
+ }
3374
+ hasKey = true;
3375
+ if (typeof value == 'string') {
3376
+ if (isPatternMatched(tokenPattern, value)) {
3377
+ match.push([
3378
+ key
3379
+ ]);
3380
+ hasValue = true;
3381
+ } else if (!(keyPattern instanceof RegExp)) {
3382
+ return false;
3383
+ }
3384
+ } else {
3385
+ const tokenMatch = value._getTokensMatch(tokenPattern, valuePattern || true);
3386
+ if (tokenMatch) {
3387
+ hasValue = true;
3388
+ for (const tokenMatchItem of tokenMatch){
3389
+ match.push([
3390
+ key,
3391
+ tokenMatchItem
3392
+ ]);
3393
+ }
3394
+ } else if (!(keyPattern instanceof RegExp)) {
3395
+ return false;
3396
+ }
3397
+ }
3398
+ }
3399
+ if (!hasKey || !hasValue) {
3400
+ return false;
3401
+ }
3402
+ }
3403
+ return true;
3404
+ }
3405
+ /**
3406
+ * Used by the {@link module:engine/conversion/viewconsumable~ViewConsumable} to collect the
3407
+ * {@link module:engine/view/element~NormalizedConsumables} for the element.
3408
+ *
3409
+ * When `key` and `token` parameters are provided the output is filtered for the specified attribute and it's tokens and related tokens.
3410
+ *
3411
+ * @internal
3412
+ * @param key Attribute name.
3413
+ * @param token Reference token to collect all related tokens.
3414
+ */ _getConsumables(key, token) {
3415
+ const attributes = [];
3416
+ if (key) {
3417
+ const value = this._attrs.get(key);
3418
+ if (value !== undefined) {
3419
+ if (typeof value == 'string') {
3420
+ attributes.push([
3421
+ key
3422
+ ]);
3423
+ } else {
3424
+ for (const prop of value._getConsumables(token)){
3425
+ attributes.push([
3426
+ key,
3427
+ prop
3428
+ ]);
3429
+ }
3430
+ }
3431
+ }
3432
+ } else {
3433
+ for (const [key, value] of this._attrs){
3434
+ if (typeof value == 'string') {
3435
+ attributes.push([
3436
+ key
3437
+ ]);
3438
+ } else {
3439
+ for (const prop of value._getConsumables()){
3440
+ attributes.push([
3441
+ key,
3442
+ prop
3443
+ ]);
3444
+ }
3445
+ }
3446
+ }
3447
+ }
3448
+ return {
3449
+ name: !key,
3450
+ attributes
3451
+ };
3452
+ }
3453
+ /**
3454
+ * Verify if the given element can be merged without conflicts into the element.
3455
+ *
3456
+ * Note that this method is extended by the {@link module:engine/view/attributeelement~AttributeElement} implementation.
3457
+ *
3458
+ * This method is used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
3459
+ * an {@link module:engine/view/attributeelement~AttributeElement} to merge it with other AttributeElement.
3460
+ *
3461
+ * @internal
3462
+ * @returns Returns `true` if elements can be merged.
3463
+ */ _canMergeAttributesFrom(otherElement) {
3464
+ if (this.name != otherElement.name) {
3465
+ return false;
3466
+ }
3467
+ for (const [key, otherValue] of otherElement._attrs){
3468
+ const value = this._attrs.get(key);
3469
+ if (value === undefined) {
3470
+ continue;
3471
+ }
3472
+ if (typeof value == 'string' || typeof otherValue == 'string') {
3473
+ if (value !== otherValue) {
3474
+ return false;
3475
+ }
3476
+ } else if (!value._canMergeFrom(otherValue)) {
3477
+ return false;
3478
+ }
3479
+ }
3480
+ return true;
3481
+ }
3482
+ /**
3483
+ * Merges attributes of a given element into the element.
3484
+ * This includes also tokenized attributes like style and class.
3485
+ *
3486
+ * Note that you should make sure there are no conflicts before merging (see {@link #_canMergeAttributesFrom}).
3487
+ *
3488
+ * This method is used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
3489
+ * an {@link module:engine/view/attributeelement~AttributeElement} to merge it with other AttributeElement.
3490
+ *
3491
+ * @internal
3492
+ */ _mergeAttributesFrom(otherElement) {
3493
+ this._fireChange('attributes', this);
3494
+ // Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
3495
+ for (const [key, otherValue] of otherElement._attrs){
3496
+ const value = this._attrs.get(key);
3497
+ if (value === undefined || typeof value == 'string' || typeof otherValue == 'string') {
3498
+ this._setAttribute(key, otherValue);
3499
+ } else {
3500
+ value._mergeFrom(otherValue);
3501
+ }
3502
+ }
3503
+ }
3504
+ /**
3505
+ * Verify if the given element attributes can be fully subtracted from the element.
3506
+ *
3507
+ * Note that this method is extended by the {@link module:engine/view/attributeelement~AttributeElement} implementation.
3508
+ *
3509
+ * This method is used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
3510
+ * an {@link module:engine/view/attributeelement~AttributeElement} to unwrap the AttributeElement.
3511
+ *
3512
+ * @internal
3513
+ * @returns Returns `true` if elements attributes can be fully subtracted.
3514
+ */ _canSubtractAttributesOf(otherElement) {
3515
+ if (this.name != otherElement.name) {
3516
+ return false;
3517
+ }
3518
+ for (const [key, otherValue] of otherElement._attrs){
3519
+ const value = this._attrs.get(key);
3520
+ if (value === undefined) {
3521
+ return false;
3522
+ }
3523
+ if (typeof value == 'string' || typeof otherValue == 'string') {
3524
+ if (value !== otherValue) {
3525
+ return false;
3526
+ }
3527
+ } else if (!value._isMatching(otherValue)) {
3528
+ return false;
3529
+ }
3530
+ }
3531
+ return true;
3532
+ }
3533
+ /**
3534
+ * Removes (subtracts) corresponding attributes of the given element from the element.
3535
+ * This includes also tokenized attributes like style and class.
3536
+ * All attributes, classes and styles from given element should be present inside the element being unwrapped.
3537
+ *
3538
+ * Note that you should make sure all attributes could be subtracted before subtracting them (see {@link #_canSubtractAttributesOf}).
3539
+ *
3540
+ * This method is used by the {@link module:engine/view/downcastwriter~DowncastWriter} while down-casting
3541
+ * an {@link module:engine/view/attributeelement~AttributeElement} to unwrap the AttributeElement.
3542
+ *
3543
+ * @internal
3544
+ */ _subtractAttributesOf(otherElement) {
3545
+ this._fireChange('attributes', this);
3546
+ for (const [key, otherValue] of otherElement._attrs){
3547
+ const value = this._attrs.get(key);
3548
+ if (typeof value == 'string' || typeof otherValue == 'string') {
3549
+ this._attrs.delete(key);
3550
+ } else {
3551
+ value.remove(otherValue.keys());
3552
+ if (value.isEmpty) {
3553
+ this._attrs.delete(key);
3554
+ }
3555
+ }
3556
+ }
3557
+ }
3558
+ /**
3559
+ * Sets a custom property. Unlike attributes, custom properties are not rendered to the DOM,
3560
+ * so they can be used to add special data to elements.
3561
+ *
3562
+ * @see module:engine/view/downcastwriter~DowncastWriter#setCustomProperty
3563
+ * @internal
3564
+ */ _setCustomProperty(key, value) {
3565
+ this._customProperties.set(key, value);
3566
+ }
3567
+ /**
3568
+ * Removes the custom property stored under the given key.
3569
+ *
3570
+ * @see module:engine/view/downcastwriter~DowncastWriter#removeCustomProperty
3571
+ * @internal
3572
+ * @returns Returns true if property was removed.
3573
+ */ _removeCustomProperty(key) {
3574
+ return this._customProperties.delete(key);
3575
+ }
3576
+ /**
3577
+ * Parses attributes provided to the element constructor before they are applied to an element. If attributes are passed
3578
+ * as an object (instead of `Iterable`), the object is transformed to the map. Attributes with `null` value are removed.
3579
+ * Attributes with non-`String` value are converted to `String`.
3580
+ *
3581
+ * @param attrs Attributes to parse.
3582
+ * @returns Parsed attributes.
3583
+ */ _parseAttributes(attrs) {
3584
+ const attrsMap = toMap(attrs);
3585
+ for (const [key, value] of attrsMap){
3586
+ if (value === null) {
3587
+ attrsMap.delete(key);
3588
+ } else if (usesStylesMap(this.name, key)) {
3589
+ // This is either an element clone so we need to clone styles map, or a new instance which requires value to be parsed.
3590
+ const newValue = value instanceof StylesMap ? value._clone() : new StylesMap(this.document.stylesProcessor).setTo(String(value));
3591
+ attrsMap.set(key, newValue);
3592
+ } else if (usesTokenList(this.name, key)) {
3593
+ // This is either an element clone so we need to clone token list, or a new instance which requires value to be parsed.
3594
+ const newValue = value instanceof TokenList ? value._clone() : new TokenList().setTo(String(value));
3595
+ attrsMap.set(key, newValue);
3596
+ } else if (typeof value != 'string') {
3597
+ attrsMap.set(key, String(value));
3598
+ }
3599
+ }
3600
+ return attrsMap;
2570
3601
  }
2571
3602
  };
2572
3603
  // The magic of type inference using `is` method is centralized in `TypeCheckable` class.
@@ -2579,35 +3610,6 @@ Element$1.prototype.is = function(type, name) {
2579
3610
  return name === this.name && (type === 'element' || type === 'view:element');
2580
3611
  }
2581
3612
  };
2582
- /**
2583
- * Parses attributes provided to the element constructor before they are applied to an element. If attributes are passed
2584
- * as an object (instead of `Iterable`), the object is transformed to the map. Attributes with `null` value are removed.
2585
- * Attributes with non-`String` value are converted to `String`.
2586
- *
2587
- * @param attrs Attributes to parse.
2588
- * @returns Parsed attributes.
2589
- */ function parseAttributes(attrs) {
2590
- const attrsMap = toMap(attrs);
2591
- for (const [key, value] of attrsMap){
2592
- if (value === null) {
2593
- attrsMap.delete(key);
2594
- } else if (typeof value != 'string') {
2595
- attrsMap.set(key, String(value));
2596
- }
2597
- }
2598
- return attrsMap;
2599
- }
2600
- /**
2601
- * Parses class attribute and puts all classes into classes set.
2602
- * Classes set s cleared before insertion.
2603
- *
2604
- * @param classesSet Set to insert parsed classes.
2605
- * @param classesString String with classes to parse.
2606
- */ function parseClasses(classesSet, classesString) {
2607
- const classArray = classesString.split(/\s+/);
2608
- classesSet.clear();
2609
- classArray.forEach((name)=>classesSet.add(name));
2610
- }
2611
3613
  /**
2612
3614
  * Converts strings to Text and non-iterables to arrays.
2613
3615
  */ function normalize$3(document, nodes) {
@@ -2634,6 +3636,16 @@ Element$1.prototype.is = function(type, name) {
2634
3636
  }
2635
3637
  return normalizedNodes;
2636
3638
  }
3639
+ /**
3640
+ * Returns `true` if an attribute on a given element should be handled as a TokenList.
3641
+ */ function usesTokenList(elementName, key) {
3642
+ return key == 'class' || elementName == 'a' && key == 'rel';
3643
+ }
3644
+ /**
3645
+ * Returns `true` if an attribute on a given element should be handled as a StylesMap.
3646
+ */ function usesStylesMap(elementName, key) {
3647
+ return key == 'style';
3648
+ }
2637
3649
 
2638
3650
  /**
2639
3651
  * Containers are elements which define document structure. They define boundaries for
@@ -5060,6 +6072,30 @@ const DEFAULT_PRIORITY = 10;
5060
6072
  cloned._id = this._id;
5061
6073
  return cloned;
5062
6074
  }
6075
+ /**
6076
+ * Used by {@link module:engine/view/element~Element#_mergeAttributesFrom} to verify if the given element can be merged without
6077
+ * conflicts into this element.
6078
+ *
6079
+ * @internal
6080
+ */ _canMergeAttributesFrom(otherElement) {
6081
+ // Can't merge if any of elements have an id or a difference of priority.
6082
+ if (this.id !== null || otherElement.id !== null || this.priority !== otherElement.priority) {
6083
+ return false;
6084
+ }
6085
+ return super._canMergeAttributesFrom(otherElement);
6086
+ }
6087
+ /**
6088
+ * Used by {@link module:engine/view/element~Element#_subtractAttributesOf} to verify if the given element attributes
6089
+ * can be fully subtracted from this element.
6090
+ *
6091
+ * @internal
6092
+ */ _canSubtractAttributesOf(otherElement) {
6093
+ // Can't subtract if any of elements have an id or a difference of priority.
6094
+ if (this.id !== null || otherElement.id !== null || this.priority !== otherElement.priority) {
6095
+ return false;
6096
+ }
6097
+ return super._canSubtractAttributesOf(otherElement);
6098
+ }
5063
6099
  }
5064
6100
  // The magic of type inference using `is` method is centralized in `TypeCheckable` class.
5065
6101
  // Proper overload would interfere with that.
@@ -5851,28 +6887,19 @@ DocumentFragment$1.prototype.is = function(type) {
5851
6887
  }
5852
6888
  return rawElement;
5853
6889
  }
5854
- /**
5855
- * Adds or overwrites the element's attribute with a specified key and value.
5856
- *
5857
- * ```ts
5858
- * writer.setAttribute( 'href', 'http://ckeditor.com', linkElement );
5859
- * ```
5860
- *
5861
- * @param key The attribute key.
5862
- * @param value The attribute value.
5863
- */ setAttribute(key, value, element) {
5864
- element._setAttribute(key, value);
6890
+ setAttribute(key, value, elementOrOverwrite, element) {
6891
+ if (element !== undefined) {
6892
+ element._setAttribute(key, value, elementOrOverwrite);
6893
+ } else {
6894
+ elementOrOverwrite._setAttribute(key, value);
6895
+ }
5865
6896
  }
5866
- /**
5867
- * Removes attribute from the element.
5868
- *
5869
- * ```ts
5870
- * writer.removeAttribute( 'href', linkElement );
5871
- * ```
5872
- *
5873
- * @param key Attribute key.
5874
- */ removeAttribute(key, element) {
5875
- element._removeAttribute(key);
6897
+ removeAttribute(key, elementOrTokens, element) {
6898
+ if (element !== undefined) {
6899
+ element._removeAttribute(key, elementOrTokens);
6900
+ } else {
6901
+ elementOrTokens._removeAttribute(key);
6902
+ }
5876
6903
  }
5877
6904
  /**
5878
6905
  * Adds specified class to the element.
@@ -6571,7 +7598,8 @@ DocumentFragment$1.prototype.is = function(type) {
6571
7598
  //
6572
7599
  // <p><span class="bar">abc</span></p> --> <p><span class="foo bar">abc</span></p>
6573
7600
  //
6574
- if (isAttribute && this._wrapAttributeElement(wrapElement, child)) {
7601
+ if (isAttribute && child._canMergeAttributesFrom(wrapElement)) {
7602
+ child._mergeAttributesFrom(wrapElement);
6575
7603
  wrapPositions.push(new Position$1(parent, i));
6576
7604
  } else if (isText || !isAttribute || shouldABeOutsideB(wrapElement, child)) {
6577
7605
  // Clone attribute.
@@ -6648,7 +7676,8 @@ DocumentFragment$1.prototype.is = function(type) {
6648
7676
  // <p><span class="foo bar">abc</span>xyz</p> --> <p><span class="bar">abc</span>xyz</p>
6649
7677
  // <p><i class="foo">abc</i>xyz</p> --> <p><i class="foo">abc</i>xyz</p>
6650
7678
  //
6651
- if (this._unwrapAttributeElement(unwrapElement, child)) {
7679
+ if (child._canSubtractAttributesOf(unwrapElement)) {
7680
+ child._subtractAttributesOf(unwrapElement);
6652
7681
  unwrapPositions.push(new Position$1(parent, i), new Position$1(parent, i + 1));
6653
7682
  i++;
6654
7683
  continue;
@@ -6740,114 +7769,6 @@ DocumentFragment$1.prototype.is = function(type) {
6740
7769
  // If position is next to text node - move position inside.
6741
7770
  return movePositionToTextNode(newPosition);
6742
7771
  }
6743
- /**
6744
- * Wraps one {@link module:engine/view/attributeelement~AttributeElement AttributeElement} into another by
6745
- * merging them if possible. When merging is possible - all attributes, styles and classes are moved from wrapper
6746
- * element to element being wrapped.
6747
- *
6748
- * @param wrapper Wrapper AttributeElement.
6749
- * @param toWrap AttributeElement to wrap using wrapper element.
6750
- * @returns Returns `true` if elements are merged.
6751
- */ _wrapAttributeElement(wrapper, toWrap) {
6752
- if (!canBeJoined(wrapper, toWrap)) {
6753
- return false;
6754
- }
6755
- // Can't merge if name or priority differs.
6756
- if (wrapper.name !== toWrap.name || wrapper.priority !== toWrap.priority) {
6757
- return false;
6758
- }
6759
- // Check if attributes can be merged.
6760
- for (const key of wrapper.getAttributeKeys()){
6761
- // Classes and styles should be checked separately.
6762
- if (key === 'class' || key === 'style') {
6763
- continue;
6764
- }
6765
- // If some attributes are different we cannot wrap.
6766
- if (toWrap.hasAttribute(key) && toWrap.getAttribute(key) !== wrapper.getAttribute(key)) {
6767
- return false;
6768
- }
6769
- }
6770
- // Check if styles can be merged.
6771
- for (const key of wrapper.getStyleNames()){
6772
- if (toWrap.hasStyle(key) && toWrap.getStyle(key) !== wrapper.getStyle(key)) {
6773
- return false;
6774
- }
6775
- }
6776
- // Move all attributes/classes/styles from wrapper to wrapped AttributeElement.
6777
- for (const key of wrapper.getAttributeKeys()){
6778
- // Classes and styles should be checked separately.
6779
- if (key === 'class' || key === 'style') {
6780
- continue;
6781
- }
6782
- // Move only these attributes that are not present - other are similar.
6783
- if (!toWrap.hasAttribute(key)) {
6784
- this.setAttribute(key, wrapper.getAttribute(key), toWrap);
6785
- }
6786
- }
6787
- for (const key of wrapper.getStyleNames()){
6788
- if (!toWrap.hasStyle(key)) {
6789
- this.setStyle(key, wrapper.getStyle(key), toWrap);
6790
- }
6791
- }
6792
- for (const key of wrapper.getClassNames()){
6793
- if (!toWrap.hasClass(key)) {
6794
- this.addClass(key, toWrap);
6795
- }
6796
- }
6797
- return true;
6798
- }
6799
- /**
6800
- * Unwraps {@link module:engine/view/attributeelement~AttributeElement AttributeElement} from another by removing
6801
- * corresponding attributes, classes and styles. All attributes, classes and styles from wrapper should be present
6802
- * inside element being unwrapped.
6803
- *
6804
- * @param wrapper Wrapper AttributeElement.
6805
- * @param toUnwrap AttributeElement to unwrap using wrapper element.
6806
- * @returns Returns `true` if elements are unwrapped.
6807
- **/ _unwrapAttributeElement(wrapper, toUnwrap) {
6808
- if (!canBeJoined(wrapper, toUnwrap)) {
6809
- return false;
6810
- }
6811
- // Can't unwrap if name or priority differs.
6812
- if (wrapper.name !== toUnwrap.name || wrapper.priority !== toUnwrap.priority) {
6813
- return false;
6814
- }
6815
- // Check if AttributeElement has all wrapper attributes.
6816
- for (const key of wrapper.getAttributeKeys()){
6817
- // Classes and styles should be checked separately.
6818
- if (key === 'class' || key === 'style') {
6819
- continue;
6820
- }
6821
- // If some attributes are missing or different we cannot unwrap.
6822
- if (!toUnwrap.hasAttribute(key) || toUnwrap.getAttribute(key) !== wrapper.getAttribute(key)) {
6823
- return false;
6824
- }
6825
- }
6826
- // Check if AttributeElement has all wrapper classes.
6827
- if (!toUnwrap.hasClass(...wrapper.getClassNames())) {
6828
- return false;
6829
- }
6830
- // Check if AttributeElement has all wrapper styles.
6831
- for (const key of wrapper.getStyleNames()){
6832
- // If some styles are missing or different we cannot unwrap.
6833
- if (!toUnwrap.hasStyle(key) || toUnwrap.getStyle(key) !== wrapper.getStyle(key)) {
6834
- return false;
6835
- }
6836
- }
6837
- // Remove all wrapper's attributes from unwrapped element.
6838
- for (const key of wrapper.getAttributeKeys()){
6839
- // Classes and styles should be checked separately.
6840
- if (key === 'class' || key === 'style') {
6841
- continue;
6842
- }
6843
- this.removeAttribute(key, toUnwrap);
6844
- }
6845
- // Remove all wrapper's classes from unwrapped element.
6846
- this.removeClass(Array.from(wrapper.getClassNames()), toUnwrap);
6847
- // Remove all wrapper's styles from unwrapped element.
6848
- this.removeStyle(Array.from(wrapper.getStyleNames()), toUnwrap);
6849
- return true;
6850
- }
6851
7772
  /**
6852
7773
  * Helper function used by other `DowncastWriter` methods. Breaks attribute elements at the boundaries of given range.
6853
7774
  *
@@ -7204,12 +8125,6 @@ const validNodesToInsert = [
7204
8125
  */ throw new CKEditorError('view-writer-invalid-range-container', errorContext);
7205
8126
  }
7206
8127
  }
7207
- /**
7208
- * Checks if two attribute elements can be joined together. Elements can be joined together if, and only if
7209
- * they do not have ids specified.
7210
- */ function canBeJoined(a, b) {
7211
- return a.id === null && b.id === null;
7212
- }
7213
8128
 
7214
8129
  /**
7215
8130
  * Set of utilities related to handling block and inline fillers.
@@ -8891,6 +9806,10 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
8891
9806
  generator.next();
8892
9807
  // Whitespace cleaning.
8893
9808
  this._processDomInlineNodes(null, inlineNodes, options);
9809
+ // This was a single block filler so just remove it.
9810
+ if (this.blockFillerMode == 'br' && isViewBrFiller(node)) {
9811
+ return null;
9812
+ }
8894
9813
  // Text not got trimmed to an empty string so there is no result node.
8895
9814
  if (node.is('$text') && node.data.length == 0) {
8896
9815
  return null;
@@ -8928,7 +9847,10 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
8928
9847
  if (this._isBlockViewElement(viewChild)) {
8929
9848
  this._processDomInlineNodes(domElement, inlineNodes, options);
8930
9849
  }
8931
- yield viewChild;
9850
+ // Yield only if this is not a block filler.
9851
+ if (!(this.blockFillerMode == 'br' && isViewBrFiller(viewChild))) {
9852
+ yield viewChild;
9853
+ }
8932
9854
  // Trigger children handling.
8933
9855
  generator.next();
8934
9856
  }
@@ -9232,8 +10154,9 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9232
10154
  if (this.blockFillerMode == 'br') {
9233
10155
  return domNode.isEqualNode(BR_FILLER_REF);
9234
10156
  }
9235
- // Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode. See ckeditor5#5564.
9236
- if (domNode.tagName === 'BR' && hasBlockParent(domNode, this.blockElements) && domNode.parentNode.childNodes.length === 1) {
10157
+ // Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode.
10158
+ // See https://github.com/ckeditor/ckeditor5/issues/5564.
10159
+ if (isOnlyBrInBlock(domNode, this.blockElements)) {
9237
10160
  return true;
9238
10161
  }
9239
10162
  // If not in 'br' mode, try recognizing both marked and regular nbsp block fillers.
@@ -9374,7 +10297,9 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9374
10297
  * @param inlineNodes An array of recently encountered inline nodes truncated to the block element boundaries.
9375
10298
  * Used later to process whitespaces.
9376
10299
  */ *_domToView(domNode, options, inlineNodes) {
9377
- if (this.isBlockFiller(domNode)) {
10300
+ // Special case for <p><br></p> in which <br> should be treated as filler even when we are not in the 'br' mode.
10301
+ // See https://github.com/ckeditor/ckeditor5/issues/5564.
10302
+ if (this.blockFillerMode != 'br' && isOnlyBrInBlock(domNode, this.blockElements)) {
9378
10303
  return null;
9379
10304
  }
9380
10305
  // When node is inside a UIElement or a RawElement return that parent as it's view representation.
@@ -9515,6 +10440,20 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9515
10440
  // This causes a problem because the normal space would be removed in `.replace` calls above. To prevent that,
9516
10441
  // the inline filler is removed only after the data is initially processed (by the `.replace` above). See ckeditor5#692.
9517
10442
  data = getDataWithoutFiller(data);
10443
+ // Block filler handling.
10444
+ if (this.blockFillerMode != 'br' && node.parent) {
10445
+ if (isViewMarkedNbspFiller(node.parent, data)) {
10446
+ data = '';
10447
+ // Mark block element as it has a block filler and remove the `<span data-cke-filler="true">` element.
10448
+ if (node.parent.parent) {
10449
+ node.parent.parent._setCustomProperty('$hasBlockFiller', true);
10450
+ node.parent._remove();
10451
+ }
10452
+ } else if (isViewNbspFiller(node.parent, data, this.blockElements)) {
10453
+ data = '';
10454
+ node.parent._setCustomProperty('$hasBlockFiller', true);
10455
+ }
10456
+ }
9518
10457
  // At this point we should have removed all whitespaces from DOM text data.
9519
10458
  //
9520
10459
  // Now, We will reverse the process that happens in `_processDataFromViewText`.
@@ -9754,7 +10693,7 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9754
10693
  }
9755
10694
  }
9756
10695
  /**
9757
- * Checks if given node is a nbsp block filler.
10696
+ * Checks if given DOM node is a nbsp block filler.
9758
10697
  *
9759
10698
  * A &nbsp; is a block filler only if it is a single child of a block element.
9760
10699
  *
@@ -9771,6 +10710,33 @@ const UNSAFE_ELEMENT_REPLACEMENT_ATTRIBUTE = 'data-ck-unsafe-element';
9771
10710
  const parent = domNode.parentNode;
9772
10711
  return !!parent && !!parent.tagName && blockElements.includes(parent.tagName.toLowerCase());
9773
10712
  }
10713
+ /**
10714
+ * Checks if given view node is a nbsp block filler.
10715
+ *
10716
+ * A &nbsp; is a block filler only if it is a single child of a block element.
10717
+ */ function isViewNbspFiller(parent, data, blockElements) {
10718
+ return data == '\u00A0' && parent && parent.is('element') && parent.childCount == 1 && blockElements.includes(parent.name);
10719
+ }
10720
+ /**
10721
+ * Checks if given view node is a marked-nbsp block filler.
10722
+ *
10723
+ * A &nbsp; is a block filler only if it is wrapped in `<span data-cke-filler="true">` element.
10724
+ */ function isViewMarkedNbspFiller(parent, data) {
10725
+ return data == '\u00A0' && parent && parent.is('element', 'span') && parent.childCount == 1 && parent.hasAttribute('data-cke-filler');
10726
+ }
10727
+ /**
10728
+ * Checks if given view node is a br block filler.
10729
+ *
10730
+ * A <br> is a block filler only if it has data-cke-filler attribute set.
10731
+ */ function isViewBrFiller(node) {
10732
+ return node.is('element', 'br') && node.hasAttribute('data-cke-filler');
10733
+ }
10734
+ /**
10735
+ * Special case for `<p><br></p>` in which `<br>` should be treated as filler even when we are not in the 'br' mode.
10736
+ */ function isOnlyBrInBlock(domNode, blockElements) {
10737
+ // See https://github.com/ckeditor/ckeditor5/issues/5564.
10738
+ return domNode.tagName === 'BR' && hasBlockParent(domNode, blockElements) && domNode.parentNode.childNodes.length === 1;
10739
+ }
9774
10740
  /**
9775
10741
  * Log to console the information about element that was replaced.
9776
10742
  * Check UNSAFE_ELEMENTS for all recognized unsafe elements.
@@ -19801,51 +20767,23 @@ function cloneNodes(nodes) {
19801
20767
  }
19802
20768
  // First remove the old attribute if there was one.
19803
20769
  if (data.attributeOldValue !== null && oldAttribute) {
19804
- if (oldAttribute.key == 'class') {
19805
- const classes = typeof oldAttribute.value == 'string' ? oldAttribute.value.split(/\s+/) : oldAttribute.value;
19806
- for (const className of classes){
19807
- viewWriter.removeClass(className, viewElement);
19808
- }
19809
- } else if (oldAttribute.key == 'style') {
20770
+ let value = oldAttribute.value;
20771
+ if (oldAttribute.key == 'style') {
19810
20772
  if (typeof oldAttribute.value == 'string') {
19811
- const styles = new StylesMap(viewWriter.document.stylesProcessor);
19812
- styles.setTo(oldAttribute.value);
19813
- for (const [key] of styles.getStylesEntries()){
19814
- viewWriter.removeStyle(key, viewElement);
19815
- }
20773
+ value = new StylesMap(viewWriter.document.stylesProcessor).setTo(oldAttribute.value).getStylesEntries().map(([key])=>key);
19816
20774
  } else {
19817
- const keys = Object.keys(oldAttribute.value);
19818
- for (const key of keys){
19819
- viewWriter.removeStyle(key, viewElement);
19820
- }
20775
+ value = Object.keys(oldAttribute.value);
19821
20776
  }
19822
- } else {
19823
- viewWriter.removeAttribute(oldAttribute.key, viewElement);
19824
20777
  }
20778
+ viewWriter.removeAttribute(oldAttribute.key, value, viewElement);
19825
20779
  }
19826
20780
  // Then set the new attribute.
19827
20781
  if (data.attributeNewValue !== null && newAttribute) {
19828
- if (newAttribute.key == 'class') {
19829
- const classes = typeof newAttribute.value == 'string' ? newAttribute.value.split(/\s+/) : newAttribute.value;
19830
- for (const className of classes){
19831
- viewWriter.addClass(className, viewElement);
19832
- }
19833
- } else if (newAttribute.key == 'style') {
19834
- if (typeof newAttribute.value == 'string') {
19835
- const styles = new StylesMap(viewWriter.document.stylesProcessor);
19836
- styles.setTo(newAttribute.value);
19837
- for (const [key, value] of styles.getStylesEntries()){
19838
- viewWriter.setStyle(key, value, viewElement);
19839
- }
19840
- } else {
19841
- const keys = Object.keys(newAttribute.value);
19842
- for (const key of keys){
19843
- viewWriter.setStyle(key, newAttribute.value[key], viewElement);
19844
- }
19845
- }
19846
- } else {
19847
- viewWriter.setAttribute(newAttribute.key, newAttribute.value, viewElement);
20782
+ let value = newAttribute.value;
20783
+ if (newAttribute.key == 'style' && typeof newAttribute.value == 'string') {
20784
+ value = Object.fromEntries(new StylesMap(viewWriter.document.stylesProcessor).setTo(newAttribute.value).getStylesEntries());
19848
20785
  }
20786
+ viewWriter.setAttribute(newAttribute.key, value, false, viewElement);
19849
20787
  }
19850
20788
  };
19851
20789
  }
@@ -21944,643 +22882,124 @@ function getFromAttributeCreator(config) {
21944
22882
  this.downcastDispatcher.on('selection', convertRangeSelection(), {
21945
22883
  priority: 'low'
21946
22884
  });
21947
- this.downcastDispatcher.on('selection', convertCollapsedSelection(), {
21948
- priority: 'low'
21949
- });
21950
- // Binds {@link module:engine/view/document~Document#roots view roots collection} to
21951
- // {@link module:engine/model/document~Document#roots model roots collection} so creating
21952
- // model root automatically creates corresponding view root.
21953
- this.view.document.roots.bindTo(this.model.document.roots).using((root)=>{
21954
- // $graveyard is a special root that has no reflection in the view.
21955
- if (root.rootName == '$graveyard') {
21956
- return null;
21957
- }
21958
- const viewRoot = new RootEditableElement(this.view.document, root.name);
21959
- viewRoot.rootName = root.rootName;
21960
- this.mapper.bindElements(root, viewRoot);
21961
- return viewRoot;
21962
- });
21963
- // @if CK_DEBUG_ENGINE // initDocumentDumping( this.model.document );
21964
- // @if CK_DEBUG_ENGINE // initDocumentDumping( this.view.document );
21965
- // @if CK_DEBUG_ENGINE // dumpTrees( this.model.document, this.model.document.version );
21966
- // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
21967
- // @if CK_DEBUG_ENGINE // this.model.document.on( 'change', () => {
21968
- // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
21969
- // @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
21970
- }
21971
- /**
21972
- * Removes all event listeners attached to the `EditingController`. Destroys all objects created
21973
- * by `EditingController` that need to be destroyed.
21974
- */ destroy() {
21975
- this.view.destroy();
21976
- this.stopListening();
21977
- }
21978
- /**
21979
- * Calling this method will refresh the marker by triggering the downcast conversion for it.
21980
- *
21981
- * Reconverting the marker is useful when you want to change its {@link module:engine/view/element~Element view element}
21982
- * without changing any marker data. For instance:
21983
- *
21984
- * ```ts
21985
- * let isCommentActive = false;
21986
- *
21987
- * model.conversion.markerToHighlight( {
21988
- * model: 'comment',
21989
- * view: data => {
21990
- * const classes = [ 'comment-marker' ];
21991
- *
21992
- * if ( isCommentActive ) {
21993
- * classes.push( 'comment-marker--active' );
21994
- * }
21995
- *
21996
- * return { classes };
21997
- * }
21998
- * } );
21999
- *
22000
- * // ...
22001
- *
22002
- * // Change the property that indicates if marker is displayed as active or not.
22003
- * isCommentActive = true;
22004
- *
22005
- * // Reconverting will downcast and synchronize the marker with the new isCommentActive state value.
22006
- * editor.editing.reconvertMarker( 'comment' );
22007
- * ```
22008
- *
22009
- * **Note**: If you want to reconvert a model item, use {@link #reconvertItem} instead.
22010
- *
22011
- * @param markerOrName Name of a marker to update, or a marker instance.
22012
- */ reconvertMarker(markerOrName) {
22013
- const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
22014
- const currentMarker = this.model.markers.get(markerName);
22015
- if (!currentMarker) {
22016
- /**
22017
- * The marker with the provided name does not exist and cannot be reconverted.
22018
- *
22019
- * @error editingcontroller-reconvertmarker-marker-not-exist
22020
- * @param {String} markerName The name of the reconverted marker.
22021
- */ throw new CKEditorError('editingcontroller-reconvertmarker-marker-not-exist', this, {
22022
- markerName
22023
- });
22024
- }
22025
- this.model.change(()=>{
22026
- this.model.markers._refresh(currentMarker);
22027
- });
22028
- }
22029
- /**
22030
- * Calling this method will downcast a model item on demand (by requesting a refresh in the {@link module:engine/model/differ~Differ}).
22031
- *
22032
- * You can use it if you want the view representation of a specific item updated as a response to external modifications. For instance,
22033
- * when the view structure depends not only on the associated model data but also on some external state.
22034
- *
22035
- * **Note**: If you want to reconvert a model marker, use {@link #reconvertMarker} instead.
22036
- *
22037
- * @param item Item to refresh.
22038
- */ reconvertItem(item) {
22039
- this.model.change(()=>{
22040
- this.model.document.differ._refreshItem(item);
22041
- });
22042
- }
22043
- }
22044
- /**
22045
- * Checks whether the target ranges provided by the `beforeInput` event can be properly mapped to model ranges and fixes them if needed.
22046
- *
22047
- * This is using the same logic as the selection post-fixer.
22048
- */ function fixTargetRanges(mapper, schema, view) {
22049
- return (evt, data)=>{
22050
- // The Renderer is disabled while composing on non-android browsers, so we can't be sure that target ranges
22051
- // could be properly mapped to view and model because the DOM and view tree drifted apart.
22052
- if (view.document.isComposing && !env.isAndroid) {
22053
- return;
22054
- }
22055
- for(let i = 0; i < data.targetRanges.length; i++){
22056
- const viewRange = data.targetRanges[i];
22057
- const modelRange = mapper.toModelRange(viewRange);
22058
- const correctedRange = tryFixingRange(modelRange, schema);
22059
- if (!correctedRange || correctedRange.isEqual(modelRange)) {
22060
- continue;
22061
- }
22062
- data.targetRanges[i] = mapper.toViewRange(correctedRange);
22063
- }
22064
- };
22065
- }
22066
-
22067
- /**
22068
- * Class used for handling consumption of view {@link module:engine/view/element~Element elements},
22069
- * {@link module:engine/view/text~Text text nodes} and {@link module:engine/view/documentfragment~DocumentFragment document fragments}.
22070
- * Element's name and its parts (attributes, classes and styles) can be consumed separately. Consuming an element's name
22071
- * does not consume its attributes, classes and styles.
22072
- * To add items for consumption use {@link module:engine/conversion/viewconsumable~ViewConsumable#add add method}.
22073
- * To test items use {@link module:engine/conversion/viewconsumable~ViewConsumable#test test method}.
22074
- * To consume items use {@link module:engine/conversion/viewconsumable~ViewConsumable#consume consume method}.
22075
- * To revert already consumed items use {@link module:engine/conversion/viewconsumable~ViewConsumable#revert revert method}.
22076
- *
22077
- * ```ts
22078
- * viewConsumable.add( element, { name: true } ); // Adds element's name as ready to be consumed.
22079
- * viewConsumable.add( textNode ); // Adds text node for consumption.
22080
- * viewConsumable.add( docFragment ); // Adds document fragment for consumption.
22081
- * viewConsumable.test( element, { name: true } ); // Tests if element's name can be consumed.
22082
- * viewConsumable.test( textNode ); // Tests if text node can be consumed.
22083
- * viewConsumable.test( docFragment ); // Tests if document fragment can be consumed.
22084
- * viewConsumable.consume( element, { name: true } ); // Consume element's name.
22085
- * viewConsumable.consume( textNode ); // Consume text node.
22086
- * viewConsumable.consume( docFragment ); // Consume document fragment.
22087
- * viewConsumable.revert( element, { name: true } ); // Revert already consumed element's name.
22088
- * viewConsumable.revert( textNode ); // Revert already consumed text node.
22089
- * viewConsumable.revert( docFragment ); // Revert already consumed document fragment.
22090
- * ```
22091
- */ class ViewConsumable {
22092
- /**
22093
- * Map of consumable elements. If {@link module:engine/view/element~Element element} is used as a key,
22094
- * {@link module:engine/conversion/viewconsumable~ViewElementConsumables ViewElementConsumables} instance is stored as value.
22095
- * For {@link module:engine/view/text~Text text nodes} and
22096
- * {@link module:engine/view/documentfragment~DocumentFragment document fragments} boolean value is stored as value.
22097
- */ _consumables = new Map();
22098
- add(element, consumables) {
22099
- let elementConsumables;
22100
- // For text nodes and document fragments just mark them as consumable.
22101
- if (element.is('$text') || element.is('documentFragment')) {
22102
- this._consumables.set(element, true);
22103
- return;
22104
- }
22105
- // For elements create new ViewElementConsumables or update already existing one.
22106
- if (!this._consumables.has(element)) {
22107
- elementConsumables = new ViewElementConsumables(element);
22108
- this._consumables.set(element, elementConsumables);
22109
- } else {
22110
- elementConsumables = this._consumables.get(element);
22111
- }
22112
- elementConsumables.add(consumables);
22113
- }
22114
- /**
22115
- * Tests if {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
22116
- * {@link module:engine/view/documentfragment~DocumentFragment document fragment} can be consumed.
22117
- * It returns `true` when all items included in method's call can be consumed. Returns `false` when
22118
- * first already consumed item is found and `null` when first non-consumable item is found.
22119
- *
22120
- * ```ts
22121
- * viewConsumable.test( p, { name: true } ); // Tests element's name.
22122
- * viewConsumable.test( p, { attributes: 'name' } ); // Tests attribute.
22123
- * viewConsumable.test( p, { classes: 'foobar' } ); // Tests class.
22124
- * viewConsumable.test( p, { styles: 'color' } ); // Tests style.
22125
- * viewConsumable.test( p, { attributes: 'name', styles: 'color' } ); // Tests attribute and style.
22126
- * viewConsumable.test( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be tested.
22127
- * viewConsumable.test( textNode ); // Tests text node.
22128
- * viewConsumable.test( docFragment ); // Tests document fragment.
22129
- * ```
22130
- *
22131
- * Testing classes and styles as attribute will test if all added classes/styles can be consumed.
22132
- *
22133
- * ```ts
22134
- * viewConsumable.test( p, { attributes: 'class' } ); // Tests if all added classes can be consumed.
22135
- * viewConsumable.test( p, { attributes: 'style' } ); // Tests if all added styles can be consumed.
22136
- * ```
22137
- *
22138
- * @param consumables Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
22139
- * @param consumables.name If set to true element's name will be included.
22140
- * @param consumables.attributes Attribute name or array of attribute names.
22141
- * @param consumables.classes Class name or array of class names.
22142
- * @param consumables.styles Style name or array of style names.
22143
- * @returns Returns `true` when all items included in method's call can be consumed. Returns `false`
22144
- * when first already consumed item is found and `null` when first non-consumable item is found.
22145
- */ test(element, consumables) {
22146
- const elementConsumables = this._consumables.get(element);
22147
- if (elementConsumables === undefined) {
22148
- return null;
22149
- }
22150
- // For text nodes and document fragments return stored boolean value.
22151
- if (element.is('$text') || element.is('documentFragment')) {
22152
- return elementConsumables;
22153
- }
22154
- // For elements test consumables object.
22155
- return elementConsumables.test(consumables);
22156
- }
22157
- /**
22158
- * Consumes {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
22159
- * {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
22160
- * It returns `true` when all items included in method's call can be consumed, otherwise returns `false`.
22161
- *
22162
- * ```ts
22163
- * viewConsumable.consume( p, { name: true } ); // Consumes element's name.
22164
- * viewConsumable.consume( p, { attributes: 'name' } ); // Consumes element's attribute.
22165
- * viewConsumable.consume( p, { classes: 'foobar' } ); // Consumes element's class.
22166
- * viewConsumable.consume( p, { styles: 'color' } ); // Consumes element's style.
22167
- * viewConsumable.consume( p, { attributes: 'name', styles: 'color' } ); // Consumes attribute and style.
22168
- * viewConsumable.consume( p, { classes: [ 'baz', 'bar' ] } ); // Multiple consumables can be consumed.
22169
- * viewConsumable.consume( textNode ); // Consumes text node.
22170
- * viewConsumable.consume( docFragment ); // Consumes document fragment.
22171
- * ```
22172
- *
22173
- * Consuming classes and styles as attribute will test if all added classes/styles can be consumed.
22174
- *
22175
- * ```ts
22176
- * viewConsumable.consume( p, { attributes: 'class' } ); // Consume only if all added classes can be consumed.
22177
- * viewConsumable.consume( p, { attributes: 'style' } ); // Consume only if all added styles can be consumed.
22178
- * ```
22179
- *
22180
- * @param consumables Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
22181
- * @param consumables.name If set to true element's name will be included.
22182
- * @param consumables.attributes Attribute name or array of attribute names.
22183
- * @param consumables.classes Class name or array of class names.
22184
- * @param consumables.styles Style name or array of style names.
22185
- * @returns Returns `true` when all items included in method's call can be consumed,
22186
- * otherwise returns `false`.
22187
- */ consume(element, consumables) {
22188
- if (this.test(element, consumables)) {
22189
- if (element.is('$text') || element.is('documentFragment')) {
22190
- // For text nodes and document fragments set value to false.
22191
- this._consumables.set(element, false);
22192
- } else {
22193
- // For elements - consume consumables object.
22194
- this._consumables.get(element).consume(consumables);
22195
- }
22196
- return true;
22197
- }
22198
- return false;
22199
- }
22200
- /**
22201
- * Reverts {@link module:engine/view/element~Element view element}, {@link module:engine/view/text~Text text node} or
22202
- * {@link module:engine/view/documentfragment~DocumentFragment document fragment} so they can be consumed once again.
22203
- * Method does not revert items that were never previously added for consumption, even if they are included in
22204
- * method's call.
22205
- *
22206
- * ```ts
22207
- * viewConsumable.revert( p, { name: true } ); // Reverts element's name.
22208
- * viewConsumable.revert( p, { attributes: 'name' } ); // Reverts element's attribute.
22209
- * viewConsumable.revert( p, { classes: 'foobar' } ); // Reverts element's class.
22210
- * viewConsumable.revert( p, { styles: 'color' } ); // Reverts element's style.
22211
- * viewConsumable.revert( p, { attributes: 'name', styles: 'color' } ); // Reverts attribute and style.
22212
- * viewConsumable.revert( p, { classes: [ 'baz', 'bar' ] } ); // Multiple names can be reverted.
22213
- * viewConsumable.revert( textNode ); // Reverts text node.
22214
- * viewConsumable.revert( docFragment ); // Reverts document fragment.
22215
- * ```
22216
- *
22217
- * Reverting classes and styles as attribute will revert all classes/styles that were previously added for
22218
- * consumption.
22219
- *
22220
- * ```ts
22221
- * viewConsumable.revert( p, { attributes: 'class' } ); // Reverts all classes added for consumption.
22222
- * viewConsumable.revert( p, { attributes: 'style' } ); // Reverts all styles added for consumption.
22223
- * ```
22224
- *
22225
- * @param consumables Used only if first parameter is {@link module:engine/view/element~Element view element} instance.
22226
- * @param consumables.name If set to true element's name will be included.
22227
- * @param consumables.attributes Attribute name or array of attribute names.
22228
- * @param consumables.classes Class name or array of class names.
22229
- * @param consumables.styles Style name or array of style names.
22230
- */ revert(element, consumables) {
22231
- const elementConsumables = this._consumables.get(element);
22232
- if (elementConsumables !== undefined) {
22233
- if (element.is('$text') || element.is('documentFragment')) {
22234
- // For text nodes and document fragments - set consumable to true.
22235
- this._consumables.set(element, true);
22236
- } else {
22237
- // For elements - revert items from consumables object.
22238
- elementConsumables.revert(consumables);
22239
- }
22240
- }
22241
- }
22242
- /**
22243
- * Creates consumable object from {@link module:engine/view/element~Element view element}. Consumable object will include
22244
- * element's name and all its attributes, classes and styles.
22245
- */ static consumablesFromElement(element) {
22246
- const consumables = {
22247
- element,
22248
- name: true,
22249
- attributes: [],
22250
- classes: [],
22251
- styles: []
22252
- };
22253
- const attributes = element.getAttributeKeys();
22254
- for (const attribute of attributes){
22255
- // Skip classes and styles - will be added separately.
22256
- if (attribute == 'style' || attribute == 'class') {
22257
- continue;
22885
+ this.downcastDispatcher.on('selection', convertCollapsedSelection(), {
22886
+ priority: 'low'
22887
+ });
22888
+ // Binds {@link module:engine/view/document~Document#roots view roots collection} to
22889
+ // {@link module:engine/model/document~Document#roots model roots collection} so creating
22890
+ // model root automatically creates corresponding view root.
22891
+ this.view.document.roots.bindTo(this.model.document.roots).using((root)=>{
22892
+ // $graveyard is a special root that has no reflection in the view.
22893
+ if (root.rootName == '$graveyard') {
22894
+ return null;
22258
22895
  }
22259
- consumables.attributes.push(attribute);
22260
- }
22261
- const classes = element.getClassNames();
22262
- for (const className of classes){
22263
- consumables.classes.push(className);
22264
- }
22265
- const styles = element.getStyleNames();
22266
- for (const style of styles){
22267
- consumables.styles.push(style);
22268
- }
22269
- return consumables;
22270
- }
22271
- /**
22272
- * Creates {@link module:engine/conversion/viewconsumable~ViewConsumable ViewConsumable} instance from
22273
- * {@link module:engine/view/node~Node node} or {@link module:engine/view/documentfragment~DocumentFragment document fragment}.
22274
- * Instance will contain all elements, child nodes, attributes, styles and classes added for consumption.
22275
- *
22276
- * @param from View node or document fragment from which `ViewConsumable` will be created.
22277
- * @param instance If provided, given `ViewConsumable` instance will be used
22278
- * to add all consumables. It will be returned instead of a new instance.
22279
- */ static createFrom(from, instance) {
22280
- if (!instance) {
22281
- instance = new ViewConsumable();
22282
- }
22283
- if (from.is('$text')) {
22284
- instance.add(from);
22285
- return instance;
22286
- }
22287
- // Add `from` itself, if it is an element.
22288
- if (from.is('element')) {
22289
- instance.add(from, ViewConsumable.consumablesFromElement(from));
22290
- }
22291
- if (from.is('documentFragment')) {
22292
- instance.add(from);
22293
- }
22294
- for (const child of from.getChildren()){
22295
- instance = ViewConsumable.createFrom(child, instance);
22296
- }
22297
- return instance;
22896
+ const viewRoot = new RootEditableElement(this.view.document, root.name);
22897
+ viewRoot.rootName = root.rootName;
22898
+ this.mapper.bindElements(root, viewRoot);
22899
+ return viewRoot;
22900
+ });
22901
+ // @if CK_DEBUG_ENGINE // initDocumentDumping( this.model.document );
22902
+ // @if CK_DEBUG_ENGINE // initDocumentDumping( this.view.document );
22903
+ // @if CK_DEBUG_ENGINE // dumpTrees( this.model.document, this.model.document.version );
22904
+ // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
22905
+ // @if CK_DEBUG_ENGINE // this.model.document.on( 'change', () => {
22906
+ // @if CK_DEBUG_ENGINE // dumpTrees( this.view.document, this.model.document.version );
22907
+ // @if CK_DEBUG_ENGINE // }, { priority: 'lowest' } );
22298
22908
  }
22299
- }
22300
- const CONSUMABLE_TYPES = [
22301
- 'attributes',
22302
- 'classes',
22303
- 'styles'
22304
- ];
22305
- /**
22306
- * This is a private helper-class for {@link module:engine/conversion/viewconsumable~ViewConsumable}.
22307
- * It represents and manipulates consumable parts of a single {@link module:engine/view/element~Element}.
22308
- */ class ViewElementConsumables {
22309
- element;
22310
- /**
22311
- * Flag indicating if name of the element can be consumed.
22312
- */ _canConsumeName;
22313
- /**
22314
- * Contains maps of element's consumables: attributes, classes and styles.
22315
- */ _consumables;
22316
22909
  /**
22317
- * Creates ViewElementConsumables instance.
22318
- *
22319
- * @param from View node or document fragment from which `ViewElementConsumables` is being created.
22320
- */ constructor(from){
22321
- this.element = from;
22322
- this._canConsumeName = null;
22323
- this._consumables = {
22324
- attributes: new Map(),
22325
- styles: new Map(),
22326
- classes: new Map()
22327
- };
22910
+ * Removes all event listeners attached to the `EditingController`. Destroys all objects created
22911
+ * by `EditingController` that need to be destroyed.
22912
+ */ destroy() {
22913
+ this.view.destroy();
22914
+ this.stopListening();
22328
22915
  }
22329
22916
  /**
22330
- * Adds consumable parts of the {@link module:engine/view/element~Element view element}.
22331
- * Element's name itself can be marked to be consumed (when element's name is consumed its attributes, classes and
22332
- * styles still could be consumed):
22333
- *
22334
- * ```ts
22335
- * consumables.add( { name: true } );
22336
- * ```
22917
+ * Calling this method will refresh the marker by triggering the downcast conversion for it.
22337
22918
  *
22338
- * Attributes classes and styles:
22919
+ * Reconverting the marker is useful when you want to change its {@link module:engine/view/element~Element view element}
22920
+ * without changing any marker data. For instance:
22339
22921
  *
22340
22922
  * ```ts
22341
- * consumables.add( { attributes: 'title', classes: 'foo', styles: 'color' } );
22342
- * consumables.add( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
22343
- * ```
22344
- *
22345
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
22346
- * attribute is provided - it should be handled separately by providing `style` and `class` in consumables object.
22347
- *
22348
- * @param consumables Object describing which parts of the element can be consumed.
22349
- * @param consumables.name If set to `true` element's name will be added as consumable.
22350
- * @param consumables.attributes Attribute name or array of attribute names to add as consumable.
22351
- * @param consumables.classes Class name or array of class names to add as consumable.
22352
- * @param consumables.styles Style name or array of style names to add as consumable.
22353
- */ add(consumables) {
22354
- if (consumables.name) {
22355
- this._canConsumeName = true;
22356
- }
22357
- for (const type of CONSUMABLE_TYPES){
22358
- if (type in consumables) {
22359
- this._add(type, consumables[type]);
22360
- }
22361
- }
22362
- }
22363
- /**
22364
- * Tests if parts of the {@link module:engine/view/node~Node view node} can be consumed.
22923
+ * let isCommentActive = false;
22365
22924
  *
22366
- * Element's name can be tested:
22925
+ * model.conversion.markerToHighlight( {
22926
+ * model: 'comment',
22927
+ * view: data => {
22928
+ * const classes = [ 'comment-marker' ];
22367
22929
  *
22368
- * ```ts
22369
- * consumables.test( { name: true } );
22370
- * ```
22930
+ * if ( isCommentActive ) {
22931
+ * classes.push( 'comment-marker--active' );
22932
+ * }
22371
22933
  *
22372
- * Attributes classes and styles:
22934
+ * return { classes };
22935
+ * }
22936
+ * } );
22373
22937
  *
22374
- * ```ts
22375
- * consumables.test( { attributes: 'title', classes: 'foo', styles: 'color' } );
22376
- * consumables.test( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
22377
- * ```
22938
+ * // ...
22378
22939
  *
22379
- * @param consumables Object describing which parts of the element should be tested.
22380
- * @param consumables.name If set to `true` element's name will be tested.
22381
- * @param consumables.attributes Attribute name or array of attribute names to test.
22382
- * @param consumables.classes Class name or array of class names to test.
22383
- * @param consumables.styles Style name or array of style names to test.
22384
- * @returns `true` when all tested items can be consumed, `null` when even one of the items
22385
- * was never marked for consumption and `false` when even one of the items was already consumed.
22386
- */ test(consumables) {
22387
- // Check if name can be consumed.
22388
- if (consumables.name && !this._canConsumeName) {
22389
- return this._canConsumeName;
22390
- }
22391
- for (const type of CONSUMABLE_TYPES){
22392
- if (type in consumables) {
22393
- const value = this._test(type, consumables[type]);
22394
- if (value !== true) {
22395
- return value;
22396
- }
22397
- }
22398
- }
22399
- // Return true only if all can be consumed.
22400
- return true;
22401
- }
22402
- /**
22403
- * Consumes parts of {@link module:engine/view/element~Element view element}. This function does not check if consumable item
22404
- * is already consumed - it consumes all consumable items provided.
22405
- * Element's name can be consumed:
22940
+ * // Change the property that indicates if marker is displayed as active or not.
22941
+ * isCommentActive = true;
22406
22942
  *
22407
- * ```ts
22408
- * consumables.consume( { name: true } );
22943
+ * // Reconverting will downcast and synchronize the marker with the new isCommentActive state value.
22944
+ * editor.editing.reconvertMarker( 'comment' );
22409
22945
  * ```
22410
22946
  *
22411
- * Attributes classes and styles:
22412
- *
22413
- * ```ts
22414
- * consumables.consume( { attributes: 'title', classes: 'foo', styles: 'color' } );
22415
- * consumables.consume( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
22416
- * ```
22947
+ * **Note**: If you want to reconvert a model item, use {@link #reconvertItem} instead.
22417
22948
  *
22418
- * @param consumables Object describing which parts of the element should be consumed.
22419
- * @param consumables.name If set to `true` element's name will be consumed.
22420
- * @param consumables.attributes Attribute name or array of attribute names to consume.
22421
- * @param consumables.classes Class name or array of class names to consume.
22422
- * @param consumables.styles Style name or array of style names to consume.
22423
- */ consume(consumables) {
22424
- if (consumables.name) {
22425
- this._canConsumeName = false;
22426
- }
22427
- for (const type of CONSUMABLE_TYPES){
22428
- if (type in consumables) {
22429
- this._consume(type, consumables[type]);
22430
- }
22949
+ * @param markerOrName Name of a marker to update, or a marker instance.
22950
+ */ reconvertMarker(markerOrName) {
22951
+ const markerName = typeof markerOrName == 'string' ? markerOrName : markerOrName.name;
22952
+ const currentMarker = this.model.markers.get(markerName);
22953
+ if (!currentMarker) {
22954
+ /**
22955
+ * The marker with the provided name does not exist and cannot be reconverted.
22956
+ *
22957
+ * @error editingcontroller-reconvertmarker-marker-not-exist
22958
+ * @param {String} markerName The name of the reconverted marker.
22959
+ */ throw new CKEditorError('editingcontroller-reconvertmarker-marker-not-exist', this, {
22960
+ markerName
22961
+ });
22431
22962
  }
22963
+ this.model.change(()=>{
22964
+ this.model.markers._refresh(currentMarker);
22965
+ });
22432
22966
  }
22433
22967
  /**
22434
- * Revert already consumed parts of {@link module:engine/view/element~Element view Element}, so they can be consumed once again.
22435
- * Element's name can be reverted:
22436
- *
22437
- * ```ts
22438
- * consumables.revert( { name: true } );
22439
- * ```
22440
- *
22441
- * Attributes classes and styles:
22442
- *
22443
- * ```ts
22444
- * consumables.revert( { attributes: 'title', classes: 'foo', styles: 'color' } );
22445
- * consumables.revert( { attributes: [ 'title', 'name' ], classes: [ 'foo', 'bar' ] );
22446
- * ```
22968
+ * Calling this method will downcast a model item on demand (by requesting a refresh in the {@link module:engine/model/differ~Differ}).
22447
22969
  *
22448
- * @param consumables Object describing which parts of the element should be reverted.
22449
- * @param consumables.name If set to `true` element's name will be reverted.
22450
- * @param consumables.attributes Attribute name or array of attribute names to revert.
22451
- * @param consumables.classes Class name or array of class names to revert.
22452
- * @param consumables.styles Style name or array of style names to revert.
22453
- */ revert(consumables) {
22454
- if (consumables.name) {
22455
- this._canConsumeName = true;
22456
- }
22457
- for (const type of CONSUMABLE_TYPES){
22458
- if (type in consumables) {
22459
- this._revert(type, consumables[type]);
22460
- }
22461
- }
22462
- }
22463
- /**
22464
- * Helper method that adds consumables of a given type: attribute, class or style.
22970
+ * You can use it if you want the view representation of a specific item updated as a response to external modifications. For instance,
22971
+ * when the view structure depends not only on the associated model data but also on some external state.
22465
22972
  *
22466
- * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `viewconsumable-invalid-attribute` when `class` or `style`
22467
- * type is provided - it should be handled separately by providing actual style/class type.
22468
- *
22469
- * @param type Type of the consumable item: `attributes`, `classes` or `styles`.
22470
- * @param item Consumable item or array of items.
22471
- */ _add(type, item) {
22472
- const items = toArray(item);
22473
- const consumables = this._consumables[type];
22474
- for (const name of items){
22475
- if (type === 'attributes' && (name === 'class' || name === 'style')) {
22476
- /**
22477
- * Class and style attributes should be handled separately in
22478
- * {@link module:engine/conversion/viewconsumable~ViewConsumable#add `ViewConsumable#add()`}.
22479
- *
22480
- * What you have done is trying to use:
22481
- *
22482
- * ```ts
22483
- * consumables.add( { attributes: [ 'class', 'style' ] } );
22484
- * ```
22485
- *
22486
- * While each class and style should be registered separately:
22487
- *
22488
- * ```ts
22489
- * consumables.add( { classes: 'some-class', styles: 'font-weight' } );
22490
- * ```
22491
- *
22492
- * @error viewconsumable-invalid-attribute
22493
- */ throw new CKEditorError('viewconsumable-invalid-attribute', this);
22494
- }
22495
- consumables.set(name, true);
22496
- if (type === 'styles') {
22497
- for (const alsoName of this.element.document.stylesProcessor.getRelatedStyles(name)){
22498
- consumables.set(alsoName, true);
22499
- }
22500
- }
22501
- }
22502
- }
22503
- /**
22504
- * Helper method that tests consumables of a given type: attribute, class or style.
22973
+ * **Note**: If you want to reconvert a model marker, use {@link #reconvertMarker} instead.
22505
22974
  *
22506
- * @param type Type of the consumable item: `attributes`, `classes` or `styles`.
22507
- * @param item Consumable item or array of items.
22508
- * @returns Returns `true` if all items can be consumed, `null` when one of the items cannot be
22509
- * consumed and `false` when one of the items is already consumed.
22510
- */ _test(type, item) {
22511
- const items = toArray(item);
22512
- const consumables = this._consumables[type];
22513
- for (const name of items){
22514
- if (type === 'attributes' && (name === 'class' || name === 'style')) {
22515
- const consumableName = name == 'class' ? 'classes' : 'styles';
22516
- // Check all classes/styles if class/style attribute is tested.
22517
- const value = this._test(consumableName, [
22518
- ...this._consumables[consumableName].keys()
22519
- ]);
22520
- if (value !== true) {
22521
- return value;
22522
- }
22523
- } else {
22524
- const value = consumables.get(name);
22525
- // Return null if attribute is not found.
22526
- if (value === undefined) {
22527
- return null;
22528
- }
22529
- if (!value) {
22530
- return false;
22531
- }
22532
- }
22533
- }
22534
- return true;
22975
+ * @param item Item to refresh.
22976
+ */ reconvertItem(item) {
22977
+ this.model.change(()=>{
22978
+ this.model.document.differ._refreshItem(item);
22979
+ });
22535
22980
  }
22536
- /**
22537
- * Helper method that consumes items of a given type: attribute, class or style.
22538
- *
22539
- * @param type Type of the consumable item: `attributes`, `classes` or `styles`.
22540
- * @param item Consumable item or array of items.
22541
- */ _consume(type, item) {
22542
- const items = toArray(item);
22543
- const consumables = this._consumables[type];
22544
- for (const name of items){
22545
- if (type === 'attributes' && (name === 'class' || name === 'style')) {
22546
- const consumableName = name == 'class' ? 'classes' : 'styles';
22547
- // If class or style is provided for consumption - consume them all.
22548
- this._consume(consumableName, [
22549
- ...this._consumables[consumableName].keys()
22550
- ]);
22551
- } else {
22552
- consumables.set(name, false);
22553
- if (type == 'styles') {
22554
- for (const toConsume of this.element.document.stylesProcessor.getRelatedStyles(name)){
22555
- consumables.set(toConsume, false);
22556
- }
22557
- }
22558
- }
22981
+ }
22982
+ /**
22983
+ * Checks whether the target ranges provided by the `beforeInput` event can be properly mapped to model ranges and fixes them if needed.
22984
+ *
22985
+ * This is using the same logic as the selection post-fixer.
22986
+ */ function fixTargetRanges(mapper, schema, view) {
22987
+ return (evt, data)=>{
22988
+ // The Renderer is disabled while composing on non-android browsers, so we can't be sure that target ranges
22989
+ // could be properly mapped to view and model because the DOM and view tree drifted apart.
22990
+ if (view.document.isComposing && !env.isAndroid) {
22991
+ return;
22559
22992
  }
22560
- }
22561
- /**
22562
- * Helper method that reverts items of a given type: attribute, class or style.
22563
- *
22564
- * @param type Type of the consumable item: `attributes`, `classes` or , `styles`.
22565
- * @param item Consumable item or array of items.
22566
- */ _revert(type, item) {
22567
- const items = toArray(item);
22568
- const consumables = this._consumables[type];
22569
- for (const name of items){
22570
- if (type === 'attributes' && (name === 'class' || name === 'style')) {
22571
- const consumableName = name == 'class' ? 'classes' : 'styles';
22572
- // If class or style is provided for reverting - revert them all.
22573
- this._revert(consumableName, [
22574
- ...this._consumables[consumableName].keys()
22575
- ]);
22576
- } else {
22577
- const value = consumables.get(name);
22578
- if (value === false) {
22579
- consumables.set(name, true);
22580
- }
22993
+ for(let i = 0; i < data.targetRanges.length; i++){
22994
+ const viewRange = data.targetRanges[i];
22995
+ const modelRange = mapper.toModelRange(viewRange);
22996
+ const correctedRange = tryFixingRange(modelRange, schema);
22997
+ if (!correctedRange || correctedRange.isEqual(modelRange)) {
22998
+ continue;
22581
22999
  }
23000
+ data.targetRanges[i] = mapper.toViewRange(correctedRange);
22582
23001
  }
22583
- }
23002
+ };
22584
23003
  }
22585
23004
 
22586
23005
  /**