Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
scofalik committed Mar 13, 2018
1 parent 1a6d529 commit da11526
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 76 deletions.
27 changes: 16 additions & 11 deletions src/conversion/downcast-converters.js
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,9 @@ export function highlightText( highlightDescriptor ) {
for ( const element of rangeAfterWrap.getItems() ) {
if ( element.is( 'attributeElement' ) && element.isSimilar( viewElement ) ) {
conversionApi.mapper.bindElementToMarker( element, data.markerName );

// One attribute element is enough, because all of them are bound together by the view writer.
break;
}
}
}
Expand Down Expand Up @@ -927,17 +930,19 @@ export function removeHighlight( highlightDescriptor ) {
if ( element.is( 'attributeElement' ) ) {
// If the bound element is an `AttributeElement`, get all other `AttributeElement`s that were created "from it"
// (when view.Writer broke it when handling other `AttributeElement`s).
const allAttributeElements = conversionApi.writer.getAllBrokenSiblings( element );

// Handle all those elements.
for ( const attributeElement of allAttributeElements ) {
// Filter out elements which got removed. For example, when converting from model to view,
// converter might have created two `AttributeElement`s, split by some other element (for
// example another marker). Then, that splitting element might got removed and the marker parts
// might got merged. In this case, previously bound element might got removed and now has to be filtered.
if ( attributeElement.parent ) {
// If the element is still in the tree, unwrap it.
conversionApi.writer.unwrap( ViewRange.createOn( attributeElement ), viewHighlightElement );
const allAttributeElements = conversionApi.writer._getGroup( element );

if ( allAttributeElements ) {
// Handle all those elements.
for ( const attributeElement of allAttributeElements ) {
// Filter out elements which got removed. For example, when converting from model to view,
// converter might have created two `AttributeElement`s, split by some other element (for
// example another marker). Then, that splitting element might got removed and the marker parts
// might got merged. In this case, previously bound element might got removed and now has to be filtered.
if ( attributeElement.parent ) {
// If the element is still in the tree, unwrap it.
conversionApi.writer.unwrap( ViewRange.createOn( attributeElement ), viewHighlightElement );
}
}
}
} else {
Expand Down
23 changes: 21 additions & 2 deletions src/conversion/mapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,29 @@ export default class Mapper {
* Gets all view elements bound to the given marker name.
*
* @param {String} name Marker name.
* @returns {Set.<module:engine/view/element~Element>} View elements bound with given marker name.
* @returns {Set.<module:engine/view/element~Element>|null} View elements bound with given marker name or `null`
* if no elements are bound to given marker name.
*/
markerNameToElements( name ) {
return this._markerNameToElements.get( name );
const boundElements = this._markerNameToElements.get( name );

if ( !boundElements ) {
return null;
}

const elements = new Set();

for ( const element of boundElements ) {
if ( element.is( 'attributeElement' ) ) {
for ( const clone of element.getCustomProperty( 'clonedElements' ) ) {
elements.add( clone );
}
} else {
elements.add( element );
}
}

return elements;
}

/**
Expand Down
101 changes: 48 additions & 53 deletions src/view/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export default class Writer {
* @type {module:engine/view/document~Document}
*/
this.document = document;

this._groups = new Map();
}

/**
Expand Down Expand Up @@ -505,7 +507,7 @@ export default class Writer {
const offset = positionParent.index;
positionParent._remove();

this._unlinkBrokenAttributeElement( positionParent );
this._removeFromGroup( positionParent );

return this.mergeAttributes( new Position( parent, offset ) );
}
Expand All @@ -529,7 +531,7 @@ export default class Writer {
nodeBefore._appendChildren( nodeAfter.getChildren() );

nodeAfter._remove();
this._unlinkBrokenAttributeElement( nodeAfter );
this._removeFromGroup( nodeAfter );

// New position is located inside the first node, before new nodes.
// Call this method recursively to merge again if needed.
Expand Down Expand Up @@ -619,6 +621,11 @@ export default class Writer {
const insertionPosition = this._breakAttributes( position, true );

const length = container._insertChildren( insertionPosition.offset, nodes );

for ( const node of nodes ) {
this._addToGroup( node );
}

const endPosition = insertionPosition.getShiftedBy( length );
const start = this.mergeAttributes( insertionPosition );

Expand Down Expand Up @@ -666,7 +673,7 @@ export default class Writer {
const removed = parentContainer._removeChildren( breakStart.offset, count );

for ( const node of removed ) {
this._unlinkBrokenAttributeElement( node );
this._removeFromGroup( node );
}

// Merge after removing.
Expand Down Expand Up @@ -911,24 +918,6 @@ export default class Writer {
return newElement;
}

/**
* For the given attribute element, returns that element and all the elements that were cloned from the given element
* when the element was broken by `Writer`.
*
* @param {module:engine/view/attributeelement~AttributeElement} element
* @returns {Array.<module:engine/view/attributeelement~AttributeElement>}
*/
getAllBrokenSiblings( element ) {
const original = element.getCustomProperty( 'originalAttributeElement' ) || element;
const clones = original.getCustomProperty( 'clonedAttributeElements' );

if ( clones ) {
return Array.from( clones ).concat( original );
} else {
return [ element ];
}
}

/**
* Wraps children with provided `attribute`. Only children contained in `parent` element between
* `startOffset` and `endOffset` will be wrapped.
Expand All @@ -954,6 +943,7 @@ export default class Writer {
if ( isText || isEmpty || isUI || ( isAttribute && shouldABeOutsideB( attribute, child ) ) ) {
// Clone attribute.
const newAttribute = attribute._clone();
this._addToGroup( newAttribute );

// Wrap current node with new attribute.
child._remove();
Expand Down Expand Up @@ -1018,7 +1008,7 @@ export default class Writer {

// Replace wrapper element with its children
child._remove();
this._unlinkBrokenAttributeElement( child );
this._removeFromGroup( child );

parent._insertChildren( i, unwrapped );

Expand Down Expand Up @@ -1403,9 +1393,7 @@ export default class Writer {

// Break element.
const clonedNode = positionParent._clone();

// Save that the `clonedNode` was created from `positionParent`.
this._linkBrokenAttributeElements( clonedNode, positionParent );
this._addToGroup( clonedNode );

// Insert cloned node to position's parent node.
positionParent.parent._insertChildren( offsetAfter, clonedNode );
Expand All @@ -1424,44 +1412,51 @@ export default class Writer {
}
}

/**
* Links `clonedNode` element to `clonedFrom` element. Saves information that `clonedNode` was created as a clone
* of `clonedFrom` when `clonedFrom` was broken into two elements by the `Writer`.
*
* @private
* @param {module:engine/view/attributeelement~AttributeElement} clonedNode
* @param {module:engine/view/attributeelement~AttributeElement} clonedFrom
*/
_linkBrokenAttributeElements( clonedNode, clonedFrom ) {
const original = clonedFrom.getCustomProperty( 'originalAttributeElement' ) || clonedFrom;
const clones = original.getCustomProperty( 'clonedAttributeElements' ) || new Set();
_addToGroup( element ) {
const id = element.id;

if ( !id ) {
return;
}

this.setCustomProperty( 'originalAttributeElement', original, clonedNode );
let group = this._groups.get( id );

clones.add( clonedNode );
this.setCustomProperty( 'clonedAttributeElements', clones, original );
if ( !group ) {
group = new Set();
this._groups.set( id, group );
}

group.add( element );
this.setCustomProperty( 'clonedElements', group, element );
}

/**
* Unlinks `element`. Removes information about how `element` was broken by the `Writer` from `element` and from
* its original element (the element it was cloned from).
*
* @private
* @param {module:engine/view/attributeelement~AttributeElement} element
*/
_unlinkBrokenAttributeElement( element ) {
if ( !element.is( 'element' ) ) {
_removeFromGroup( element ) {
const id = element.id;

if ( !id ) {
return;
}

const original = element.getCustomProperty( 'originalAttributeElement' );
const group = this._groups.get( id );

group.delete( element );
// Not removing group from element on purpose!
// If other parts of code have reference to this element, they will be able to get references to other elements from the group.
// If all other elements are removed from the set, everything will be garbage collected.

if ( original ) {
this.removeCustomProperty( 'originalAttributeElement', element );
if ( group.size === 0 ) {
this._groups.delete( id );
}
}

const clones = original.getCustomProperty( 'clonedAttributeElements' );
clones.delete( element );
_getGroup( element ) {
const id = element.id;

if ( !id ) {
return null;
}

return this._groups.get( id ) || null;
}
}

Expand Down
2 changes: 1 addition & 1 deletion tests/conversion/mapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ describe( 'Mapper', () => {

const elements = mapper.markerNameToElements( 'marker' );

expect( elements ).to.be.undefined;
expect( elements ).to.be.null;
} );
} );

Expand Down
15 changes: 6 additions & 9 deletions tests/view/writer/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,14 +263,14 @@ describe( 'Writer', () => {
} );
} );

describe( 'getAllBrokenSiblings()', () => {
it( 'should return all clones of a broken attribute element', () => {
describe( '_getGroup()', () => {
it( 'should return all clones of a broken attribute element with id', () => {
const container = writer.createContainerElement( 'div' );
const text = writer.createText( 'abccccde' );

writer.insert( ViewPosition.createAt( container, 0 ), text );

const span = writer.createAttributeElement( 'span' );
const span = writer.createAttributeElement( 'span', null, { id: 'foo' } );
span._priority = 20;

// <div>ab<span>cccc</span>de</div>
Expand All @@ -287,7 +287,7 @@ describe( 'Writer', () => {
i
);

// <div>a<i>b<span>c</span></i><span>c</span><i><span>c</span>d</i>e</div>
// <div>a<i>b<span>c</span></i><span>c</span><i><span>cc</span>d</i>e</div>
writer.wrap(
ViewRange.createFromParentsAndOffsets(
container.getChild( 2 ).getChild( 0 ), 1,
Expand All @@ -301,11 +301,8 @@ describe( 'Writer', () => {

// For each of the spans created above...
for ( const oneOfAllSpans of allSpans ) {
// Find all broken siblings of that span...
const brokenArray = writer.getAllBrokenSiblings( oneOfAllSpans );

// Convert to set because we don't care about order.
const brokenSet = new Set( brokenArray );
const brokenSet = writer._getGroup( oneOfAllSpans );
const brokenArray = Array.from( brokenSet );

// Check if all spans are included.
for ( const s of allSpans ) {
Expand Down

0 comments on commit da11526

Please sign in to comment.