Index: trunk/extensions/VisualEditor/tests/ve/ve.dm.DocumentNode.test.js |
— | — | @@ -822,13 +822,13 @@ |
823 | 823 | [ |
824 | 824 | { 'type': 'retain', 'length': 11 }, |
825 | 825 | { 'type': 'replace', 'remove': [ { 'type': 'list' } ], 'replacement': [] }, |
826 | | - { 'type': 'replace', 'remove': [ { 'type': 'listItem' } ], 'replacement': [] }, |
| 826 | + { 'type': 'replace', 'remove': [ { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } } ], 'replacement': [] }, |
827 | 827 | { 'type': 'retain', 'length': 3 }, |
828 | 828 | { 'type': 'replace', 'remove': [ { 'type': '/listItem' } ], 'replacement': [] }, |
829 | | - { 'type': 'replace', 'remove': [ { 'type': 'listItem' } ], 'replacement': [] }, |
| 829 | + { 'type': 'replace', 'remove': [ { 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } } ], 'replacement': [] }, |
830 | 830 | { 'type': 'retain', 'length': 3 }, |
831 | 831 | { 'type': 'replace', 'remove': [ { 'type': '/listItem' } ], 'replacement': [] }, |
832 | | - { 'type': 'replace', 'remove': [ { 'type': 'listItem' } ], 'replacement': [] }, |
| 832 | + { 'type': 'replace', 'remove': [ { 'type': 'listItem', 'attributes': { 'styles': ['number'] } } ], 'replacement': [] }, |
833 | 833 | { 'type': 'retain', 'length': 3 }, |
834 | 834 | { 'type': 'replace', 'remove': [ { 'type': '/listItem' } ], 'replacement': [] }, |
835 | 835 | { 'type': 'replace', 'remove': [ { 'type': '/list' } ], 'replacement': [] }, |
Index: trunk/extensions/VisualEditor/modules/ve/dm/nodes/ve.dm.DocumentNode.js |
— | — | @@ -1124,8 +1124,13 @@ |
1125 | 1125 | /** |
1126 | 1126 | * Generates a transaction which wraps, unwraps or replaces structure. |
1127 | 1127 | * |
| 1128 | + * The unwrap parameters are checked against the actual model data, and |
| 1129 | + * an exception is thrown if the type fields don't match. This means you |
| 1130 | + * can omit attributes from the unwrap parameters, those are automatically |
| 1131 | + * picked up from the model data instead. |
| 1132 | + * |
1128 | 1133 | * @param {ve.Range} range Range to wrap/unwrap/replace around |
1129 | | - * @param {Array} unwrapOuter Array of opening elements to unwrap. These must be immediately *outside* the range |
| 1134 | + * @param {Array} unwrapOuter Array of opening elements to unwrap. These must be immediately *outside* the range. |
1130 | 1135 | * @param {Array} wrapOuter Array of opening elements to wrap around the range. |
1131 | 1136 | * @param {Array} unwrapEach Array of opening elements to unwrap from each top-level element in the range. |
1132 | 1137 | * @param {Array} wrapEach Array of opening elements to wrap around each top-level element in the range. |
— | — | @@ -1159,18 +1164,28 @@ |
1160 | 1165 | // * unwraps actually match |
1161 | 1166 | // * result is valid |
1162 | 1167 | |
1163 | | - var tx = new ve.dm.Transaction(); |
| 1168 | + var tx = new ve.dm.Transaction(), i, j, unwrapOuterData; |
1164 | 1169 | range.normalize(); |
1165 | 1170 | if ( range.start > unwrapOuter.length ) { |
1166 | 1171 | // Retain up to the first thing we're unwrapping |
1167 | 1172 | // The outer unwrapping takes place *outside* |
1168 | 1173 | // the range, so compensate for that |
1169 | | - tx.pushRetain ( range.start - unwrapOuter.length ); |
| 1174 | + tx.pushRetain( range.start - unwrapOuter.length ); |
1170 | 1175 | } |
1171 | 1176 | |
1172 | 1177 | // Replace the opening elements for the outer unwrap&wrap |
1173 | 1178 | if ( wrapOuter.length > 0 || unwrapOuter.length > 0 ) { |
1174 | | - tx.pushReplace( unwrapOuter, wrapOuter ); |
| 1179 | + // Verify that wrapOuter matches the data at this position |
| 1180 | + unwrapOuterData = this.data.slice( range.start - unwrapOuter.length, range.start ); |
| 1181 | + for ( i = 0; i < unwrapOuterData.length; i++ ) { |
| 1182 | + if ( unwrapOuterData[i].type !== unwrapOuter[i].type ) { |
| 1183 | + throw 'Element in unwrapOuter does not match: expected ' + |
| 1184 | + unwrapOuter[i].type + ' but found ' + unwrapOuterData[i].type; |
| 1185 | + } |
| 1186 | + } |
| 1187 | + // Instead of putting in unwrapOuter as given, put it in the |
| 1188 | + // way it appears in the mode,l so we pick up any attributes |
| 1189 | + tx.pushReplace( unwrapOuterData, wrapOuter ); |
1175 | 1190 | } |
1176 | 1191 | |
1177 | 1192 | if ( wrapEach.length > 0 || unwrapEach.length > 0 ) { |
— | — | @@ -1178,7 +1193,7 @@ |
1179 | 1194 | closingWrapEach = closingArray( wrapEach ), |
1180 | 1195 | depth = 0, |
1181 | 1196 | startOffset, |
1182 | | - i; |
| 1197 | + unwrapEachData; |
1183 | 1198 | // Visit each top-level child and wrap/unwrap it |
1184 | 1199 | // TODO figure out if we should use the tree/node functions here |
1185 | 1200 | // rather than iterating over offsets, it may or may not be faster |
— | — | @@ -1192,7 +1207,20 @@ |
1193 | 1208 | if ( depth == 0 ) { |
1194 | 1209 | // We are at the start of a top-level element |
1195 | 1210 | // Replace the opening elements |
1196 | | - tx.pushReplace( unwrapEach, wrapEach ); |
| 1211 | + |
| 1212 | + // Verify that unwrapEach matches the data at this position |
| 1213 | + unwrapEachData = this.data.slice( i, i + unwrapEach.length ); |
| 1214 | + for ( j = 0; j < unwrapEachData.length; j++ ) { |
| 1215 | + if ( unwrapEachData[j].type !== unwrapEach[j].type ) { |
| 1216 | + throw 'Element in unwrapEach does not match: expected ' + |
| 1217 | + unwrapEach[j].type + ' but found ' + |
| 1218 | + unwrapEachData[j].type; |
| 1219 | + } |
| 1220 | + } |
| 1221 | + // Instead of putting in unwrapEach as given, put it in the |
| 1222 | + // way it appears in the model, so we pick up any attributes |
| 1223 | + tx.pushReplace( unwrapEachData, wrapEach ); |
| 1224 | + |
1197 | 1225 | // Store this offset for later |
1198 | 1226 | startOffset = i; |
1199 | 1227 | } |