From f001d306ad104d8ef07769621ad2518698f8ec8e Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Thu, 19 Sep 2013 19:00:23 +0100 Subject: [PATCH] Node annotation blacklists Allow nodes to specify a blacklist of annotations which can't be applied to it. Bug: 53151 Change-Id: Iac7649f4b38d8bfa94c64f734634afd45a5afc53 --- modules/ve/dm/ve.dm.Node.js | 9 ++++ modules/ve/dm/ve.dm.NodeFactory.js | 25 +++++++++ modules/ve/dm/ve.dm.Transaction.js | 22 ++++++-- modules/ve/test/dm/ve.dm.Transaction.test.js | 56 ++++++++++++++++++++ modules/ve/test/dm/ve.dm.example.js | 26 +++++++++ 5 files changed, 135 insertions(+), 3 deletions(-) diff --git a/modules/ve/dm/ve.dm.Node.js b/modules/ve/dm/ve.dm.Node.js index f59101a5a0..b96872f11e 100644 --- a/modules/ve/dm/ve.dm.Node.js +++ b/modules/ve/dm/ve.dm.Node.js @@ -158,6 +158,15 @@ ve.dm.Node.static.parentNodeTypes = null; */ ve.dm.Node.static.suggestedParentNodeTypes = null; +/** + * Array of annotation types which can't be applied to this node + * + * @static + * @property {string[]} static.blacklistedAnnotationTypes + * @inheritable + */ +ve.dm.Node.static.blacklistedAnnotationTypes = []; + /** * Default attributes to set for newly created linear model elements. These defaults will be used * when creating a new element in ve.dm.NodeFactory#getDataElement when there is no DOM node or diff --git a/modules/ve/dm/ve.dm.NodeFactory.js b/modules/ve/dm/ve.dm.NodeFactory.js index d4a50e61ec..3d99d31c8a 100644 --- a/modules/ve/dm/ve.dm.NodeFactory.js +++ b/modules/ve/dm/ve.dm.NodeFactory.js @@ -154,6 +154,31 @@ ve.dm.NodeFactory.prototype.canNodeContainContent = function ( type ) { throw new Error( 'Unknown node type: ' + type ); }; + +/** + * Check if node can take annotations of a specific type. + * + * @method + * @param {string} type Node type + * @param {ve.dm.Annotation} annotation Annotation to test + * @returns {boolean} Node can take annotations of this type + * @throws {Error} Unknown node type + */ +ve.dm.NodeFactory.prototype.canNodeTakeAnnotationType = function ( type, annotation ) { + if ( !( type in this.registry ) ) { + throw new Error( 'Unknown node type: ' + type ); + } + var i, len, + blacklist = this.registry[type].static.blacklistedAnnotationTypes; + + for ( i = 0, len = blacklist.length; i < len; i++ ) { + if ( annotation instanceof ve.dm.annotationFactory.create( blacklist[i] ).constructor ) { + return false; + } + } + return true; +}; + /** * Check if a node is content. * diff --git a/modules/ve/dm/ve.dm.Transaction.js b/modules/ve/dm/ve.dm.Transaction.js index 4524fce048..adc71e9d40 100644 --- a/modules/ve/dm/ve.dm.Transaction.js +++ b/modules/ve/dm/ve.dm.Transaction.js @@ -228,7 +228,7 @@ ve.dm.Transaction.newFromAttributeChanges = function ( doc, offset, attr ) { * @returns {ve.dm.Transaction} Transaction that annotates content */ ve.dm.Transaction.newFromAnnotation = function ( doc, range, method, annotation ) { - var covered, type, + var covered, type, annotatable, tx = new ve.dm.Transaction(), data = doc.data, i = range.start, @@ -237,9 +237,25 @@ ve.dm.Transaction.newFromAnnotation = function ( doc, range, method, annotation insideContentNode = false; // Iterate over all data in range, annotating where appropriate while ( i < range.end ) { - type = data.getType( i ); + if ( data.isElementData( i ) ) { + type = data.getType( i ); + if ( ve.dm.nodeFactory.isNodeContent( type ) ) { + if ( method === 'set' && !ve.dm.nodeFactory.canNodeTakeAnnotationType( type, annotation ) ) { + // Blacklisted annotations can't be set + annotatable = false; + } else { + annotatable = true; + } + } else { + // Structural nodes are never annotatable + annotatable = false; + } + } else { + // Text is always annotatable + annotatable = true; + } if ( - ( data.isElementData( i ) && !ve.dm.nodeFactory.isNodeContent( type ) ) || + !annotatable || ( insideContentNode && !data.isCloseElementData( i ) ) ) { // Structural element opening or closing, or entering a content node diff --git a/modules/ve/test/dm/ve.dm.Transaction.test.js b/modules/ve/test/dm/ve.dm.Transaction.test.js index 9e3851fb07..126684eb75 100644 --- a/modules/ve/test/dm/ve.dm.Transaction.test.js +++ b/modules/ve/test/dm/ve.dm.Transaction.test.js @@ -764,6 +764,7 @@ QUnit.test( 'newFromAttributeChanges', function ( assert ) { QUnit.test( 'newFromAnnotation', function ( assert ) { var bold = ve.dm.example.createAnnotation( ve.dm.example.bold ), doc = ve.dm.example.createExampleDocument(), + annotationDoc = ve.dm.example.createExampleDocument( 'annotationData' ), cases = { 'over plain text': { 'args': [doc, new ve.Range( 1, 2 ), 'set', bold], @@ -858,8 +859,63 @@ QUnit.test( 'newFromAnnotation', function ( assert ) { }, { 'type': 'retain', 'length': 52 } ] + }, + 'over content and content element (image)': { + 'args': [doc, new ve.Range( 38, 42 ), 'set', bold], + 'ops': [ + { 'type': 'retain', 'length': 38 }, + { + 'type': 'annotate', + 'method': 'set', + 'bias': 'start', + 'annotation': bold + }, + { 'type': 'retain', 'length': 4 }, + { + 'type': 'annotate', + 'method': 'set', + 'bias': 'stop', + 'annotation': bold + }, + { 'type': 'retain', 'length': 21 } + ] + }, + 'over content and unannotatable content element (unboldable node)': { + 'args': [annotationDoc, new ve.Range( 1, 9 ), 'set', bold], + 'ops': [ + { 'type': 'retain', 'length': 1 }, + { + 'type': 'annotate', + 'method': 'set', + 'bias': 'start', + 'annotation': bold + }, + { 'type': 'retain', 'length': 3 }, + { + 'type': 'annotate', + 'method': 'set', + 'bias': 'stop', + 'annotation': bold + }, + { 'type': 'retain', 'length': 2 }, + { + 'type': 'annotate', + 'method': 'set', + 'bias': 'start', + 'annotation': bold + }, + { 'type': 'retain', 'length': 3 }, + { + 'type': 'annotate', + 'method': 'set', + 'bias': 'stop', + 'annotation': bold + }, + { 'type': 'retain', 'length': 3 } + ] } }; + QUnit.expect( ve.getObjectKeys( cases ).length ); runConstructorTests( assert, ve.dm.Transaction.newFromAnnotation, cases ); } ); diff --git a/modules/ve/test/dm/ve.dm.example.js b/modules/ve/test/dm/ve.dm.example.js index b669f3d24b..7708084a9d 100644 --- a/modules/ve/test/dm/ve.dm.example.js +++ b/modules/ve/test/dm/ve.dm.example.js @@ -2839,6 +2839,32 @@ ve.dm.example.isolationData = [ // 246 ]; +ve.dm.example.UnboldableNode = function ( lenght, element ) { + // Parent constructor + ve.dm.LeafNode.call( this, 0, element ); +}; +ve.inheritClass( ve.dm.example.UnboldableNode, ve.dm.LeafNode ); +ve.dm.example.UnboldableNode.static.name = 'exampleUnboldable'; +ve.dm.example.UnboldableNode.static.isContent = true; +ve.dm.example.UnboldableNode.static.blacklistedAnnotationTypes = [ 'textStyle/bold' ]; +ve.dm.example.UnboldableNode.static.matchTagNames = []; +ve.dm.modelRegistry.register( ve.dm.example.UnboldableNode ); + +ve.dm.example.annotationData = [ + { 'type': 'paragraph' }, + 'F', + 'o', + 'o', + { 'type': 'exampleUnboldable' }, + { 'type': '/exampleUnboldable' }, + 'B', + 'a', + 'r', + { 'type': '/paragraph' }, + { 'type': 'internalList' }, + { 'type': '/internalList' } +]; + ve.dm.example.selectNodesCases = [ { 'range': new ve.Range( 1 ),