From 2eb0d2a6b24a0daa2eaeeac74d4065a6801ff8a4 Mon Sep 17 00:00:00 2001 From: Catrope Date: Tue, 2 Apr 2013 19:23:33 +0200 Subject: [PATCH] Great Annotation Refactor of 2013 This changes the annotation API to be the same as the node API, sans a few boolean flags that don't apply. The APIs were different, but there was really no good reason why, so this makes things simpler for API users. It also means we'll be able to factor a bunch of things out because they're now duplicated between nodes, meta items and annotations. Linear model annotations are now objects with 'type' and 'attributes' properties (rather than 'name' and 'data'), for consistency with elements. They now also contain html/0/* attributes for HTML attribute preservation, which obsoletes the htmlTagName and htmlAttributes properties. dm.Annotation subclasses take a reference to such an object and implement conversion using .static.toDataElement and .static.toDomElements just like nodes do. The custom .getHash() functions are no longer necessary because of the way HTML attribute preservation was reimplemented. CE rendering has been moved out of dm.Annotation (it never made sense to have CE rendering functions in DM classes, this was bothering me) and into separate ce.Annotation subclasses. These are very similar to CE nodes in that they have a this.$ generated based on something in the DM; the main difference is that nodes listen to events and update themselves, whereas annotations are static and are simply destroyed and rebuilt when they change. This change also adds whitelisted HTML attribute rendering for annotations, as well as class="ve-ce-FooAnnotation" attributes. Now that annotation classes produce real DOM nodes rather than weird objects describing HTML tags, we can't generate HTML as a string in ce.ContentBranchNode anymore. getRenderedContents() has been rewritten to be much more similar to the way the converter renders annotations; in fact, significant parts of it were copied from the converter, so that should be factored out in the future. This change actually fixes an annotation rendering discrepancy between ce.ContentBranchNode and dm.Converter; see the diff of ve.ce.ContentBranchNode.test.js. ve.ce.MWEntityNode.js: * Remove stray property ve.dm.MWExternalLinkAnnotation.js: * Store 'rel' attribute ve.dm.TextStyleAnnotation.js: * Put all the conversion logic in the abstract base class ve.dm.Converter.js: * Also feed annotations through getDomElementsFromDataElement() and createDataElement() ve.dm.Node.js: * Fix undocumented property ve.ce.ContentBranchNode.test.js: * Add descriptive messages for each test case * Compare DOM trees, not HTML strings * Compare without all the class="ve-ce-WhateverAnnotation" clutter ve.ui.LinkInspector.js: * Replace direct .getHash() calls (evil!) with ve.getHash() Bug: 46464 Bug: 44808 Change-Id: I31991488579b8cce6d98ed8b29b486ba5ec38cdc --- .docs/categories.json | 5 + VisualEditor.php | 7 + demos/ve/index.php | 6 + .../ve/ce/annotations/ve.ce.LinkAnnotation.js | 35 +++ .../ve.ce.MWExternalLinkAnnotation.js | 35 +++ .../ve.ce.MWInternalLinkAnnotation.js | 39 ++++ .../annotations/ve.ce.TextStyleAnnotation.js | 213 +++++++++++++++++ modules/ve/ce/nodes/ve.ce.MWEntityNode.js | 3 - modules/ve/ce/ve.ce.Annotation.js | 105 +++++++++ modules/ve/ce/ve.ce.AnnotationFactory.js | 28 +++ modules/ve/ce/ve.ce.ContentBranchNode.js | 111 ++++----- .../ve/dm/annotations/ve.dm.LinkAnnotation.js | 66 ++---- .../ve.dm.MWExternalLinkAnnotation.js | 29 +-- .../ve.dm.MWInternalLinkAnnotation.js | 66 ++---- .../annotations/ve.dm.TextStyleAnnotation.js | 117 ++++++---- modules/ve/dm/ve.dm.Annotation.js | 185 +++++++++------ modules/ve/dm/ve.dm.Converter.js | 13 +- modules/ve/dm/ve.dm.Node.js | 1 + .../test/ce/ve.ce.ContentBranchNode.test.js | 20 +- modules/ve/test/dm/ve.dm.example.js | 220 +++++++----------- modules/ve/test/index.php | 6 + .../ve/ui/inspectors/ve.ui.LinkInspector.js | 7 +- .../ve/ui/inspectors/ve.ui.MWLinkInspector.js | 14 +- .../ui/widgets/ve.ui.LinkTargetInputWidget.js | 9 +- .../widgets/ve.ui.MWLinkTargetInputWidget.js | 18 +- 25 files changed, 905 insertions(+), 453 deletions(-) create mode 100644 modules/ve/ce/annotations/ve.ce.LinkAnnotation.js create mode 100644 modules/ve/ce/annotations/ve.ce.MWExternalLinkAnnotation.js create mode 100644 modules/ve/ce/annotations/ve.ce.MWInternalLinkAnnotation.js create mode 100644 modules/ve/ce/annotations/ve.ce.TextStyleAnnotation.js create mode 100644 modules/ve/ce/ve.ce.Annotation.js create mode 100644 modules/ve/ce/ve.ce.AnnotationFactory.js diff --git a/.docs/categories.json b/.docs/categories.json index 4e7eae033a..c07a7e4c0f 100644 --- a/.docs/categories.json +++ b/.docs/categories.json @@ -15,12 +15,17 @@ "name": "General", "classes": [ "ve.ce", + "ve.ce.AnnotationFactory", "ve.ce.NodeFactory", "ve.ce.Surface", "ve.ce.SurfaceObserver", "ve.ce.DomRange" ] }, + { + "name": "Annotations", + "classes": ["ve.ce.*Annotation"] + }, { "name": "Nodes", "classes": ["ve.ce.Document", "ve.ce.*Node"] diff --git a/VisualEditor.php b/VisualEditor.php index 924712e218..fb4f3238f4 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -286,8 +286,10 @@ $wgResourceModules += array( // ce 've/ce/ve.ce.js', 've/ce/ve.ce.DomRange.js', + 've/ce/ve.ce.AnnotationFactory.js', 've/ce/ve.ce.NodeFactory.js', 've/ce/ve.ce.Document.js', + 've/ce/ve.ce.Annotation.js', 've/ce/ve.ce.Node.js', 've/ce/ve.ce.BranchNode.js', 've/ce/ve.ce.ContentBranchNode.js', @@ -319,6 +321,11 @@ $wgResourceModules += array( 've/ce/nodes/ve.ce.MWPreformattedNode.js', 've/ce/nodes/ve.ce.MWImageNode.js', + 've/ce/annotations/ve.ce.LinkAnnotation.js', + 've/ce/annotations/ve.ce.MWExternalLinkAnnotation.js', + 've/ce/annotations/ve.ce.MWInternalLinkAnnotation.js', + 've/ce/annotations/ve.ce.TextStyleAnnotation.js', + // ui 've/ui/ve.ui.js', 've/ui/ve.ui.Context.js', diff --git a/demos/ve/index.php b/demos/ve/index.php index 236eb278e4..1160f4e2ee 100644 --- a/demos/ve/index.php +++ b/demos/ve/index.php @@ -168,8 +168,10 @@ $html = file_get_contents( $page ); + + @@ -199,6 +201,10 @@ $html = file_get_contents( $page ); + + + + diff --git a/modules/ve/ce/annotations/ve.ce.LinkAnnotation.js b/modules/ve/ce/annotations/ve.ce.LinkAnnotation.js new file mode 100644 index 0000000000..ded5a8a75c --- /dev/null +++ b/modules/ve/ce/annotations/ve.ce.LinkAnnotation.js @@ -0,0 +1,35 @@ +/*! + * VisualEditor ContentEditable LinkAnnotation class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * ContentEditable link annotation. + * + * @class + * @extends ve.ce.Annotation + * @constructor + * @param {ve.dm.LinkAnnotation} model Model to observe + */ +ve.ce.LinkAnnotation = function VeCeLinkAnnotation( model ) { + // Parent constructor + ve.ce.Annotation.call( this, model, $( '' ) ); + + // DOM changes + this.$.addClass( 've-ce-LinkAnnotation' ); + this.$.attr( 'href', model.getAttribute( 'href' ) ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.LinkAnnotation, ve.ce.Annotation ); + +/* Static Properties */ + +ve.ce.LinkAnnotation.static.name = 'link'; + +/* Registration */ + +ve.ce.annotationFactory.register( ve.ce.LinkAnnotation ); diff --git a/modules/ve/ce/annotations/ve.ce.MWExternalLinkAnnotation.js b/modules/ve/ce/annotations/ve.ce.MWExternalLinkAnnotation.js new file mode 100644 index 0000000000..a3c79762d0 --- /dev/null +++ b/modules/ve/ce/annotations/ve.ce.MWExternalLinkAnnotation.js @@ -0,0 +1,35 @@ +/*! + * VisualEditor ContentEditable MWExternalLinkAnnotation class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * ContentEditable MediaWiki external link annotation. + * + * @class + * @extends ve.ce.LinkAnnotation + * @constructor + * @param {ve.dm.MWExternalLinkAnnotation} model Model to observe + */ +ve.ce.MWExternalLinkAnnotation = function VeCeMWExternalLinkAnnotation( model ) { + // Parent constructor + ve.ce.LinkAnnotation.call( this, model ); + + // DOM changes + this.$.addClass( 've-ce-MWExternalLinkAnnotation' ); + this.$.attr( 'title', model.getAttribute( 'href' ) ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.MWExternalLinkAnnotation, ve.ce.LinkAnnotation ); + +/* Static Properties */ + +ve.ce.MWExternalLinkAnnotation.static.name = 'link/MWexternal'; + +/* Registration */ + +ve.ce.annotationFactory.register( ve.ce.MWExternalLinkAnnotation ); diff --git a/modules/ve/ce/annotations/ve.ce.MWInternalLinkAnnotation.js b/modules/ve/ce/annotations/ve.ce.MWInternalLinkAnnotation.js new file mode 100644 index 0000000000..b2b3e0c04a --- /dev/null +++ b/modules/ve/ce/annotations/ve.ce.MWInternalLinkAnnotation.js @@ -0,0 +1,39 @@ +/*! + * VisualEditor ContentEditable MWInternalLinkAnnotation class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * ContentEditable MediaWiki internal link annotation. + * + * @class + * @extends ve.ce.LinkAnnotation + * @constructor + * @param {ve.dm.MWInternalLinkAnnotation} model Model to observe + */ +ve.ce.MWInternalLinkAnnotation = function VeCeMWInternalLinkAnnotation( model ) { + var dmRendering; + // Parent constructor + ve.ce.LinkAnnotation.call( this, model ); + + // DOM changes + this.$.addClass( 've-ce-MWInternalLinkAnnotation' ); + this.$.attr( 'title', model.getAttribute( 'title' ) ); + // Get href from DM rendering + dmRendering = model.getDomElements()[0]; + this.$.attr( 'href', dmRendering.getAttribute( 'href' ) ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.MWInternalLinkAnnotation, ve.ce.LinkAnnotation ); + +/* Static Properties */ + +ve.ce.MWInternalLinkAnnotation.static.name = 'link/MWinternal'; + +/* Registration */ + +ve.ce.annotationFactory.register( ve.ce.MWInternalLinkAnnotation ); diff --git a/modules/ve/ce/annotations/ve.ce.TextStyleAnnotation.js b/modules/ve/ce/annotations/ve.ce.TextStyleAnnotation.js new file mode 100644 index 0000000000..ec4fb33ade --- /dev/null +++ b/modules/ve/ce/annotations/ve.ce.TextStyleAnnotation.js @@ -0,0 +1,213 @@ +/*! + * VisualEditor ContentEditable TextStyleAnnotation class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * ContentEditable text style annotation. + * + * @class + * @extends ve.ce.Annotation + * @constructor + * @param {ve.dm.TextStyleAnnotation} model Model to observe + * @param {jQuery} $element jQuery element (required!) + */ +ve.ce.TextStyleAnnotation = function VeCeTextStyleAnnotation( model, $element ) { + // Parent constructor + ve.ce.Annotation.call( this, model, $element ); + + // DOM changes + this.$.addClass( 've-ce-TextStyleAnnotation' ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.TextStyleAnnotation, ve.ce.Annotation ); + +/* Static Properties */ + +ve.ce.TextStyleAnnotation.static.name = 'textStyle'; + +/* Registration */ + +ve.ce.annotationFactory.register( ve.ce.TextStyleAnnotation ); + +/* Concrete Subclasses */ + +/** + * ContentEditable bold annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleBoldAnnotation} model + */ +ve.ce.TextStyleBoldAnnotation = function VeCeTextStyleBoldAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleBoldAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleBoldAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleBoldAnnotation.static.name = 'textStyle/bold'; +ve.ce.annotationFactory.register( ve.ce.TextStyleBoldAnnotation ); + +/** + * ContentEditable italic annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleItalicAnnotation} model + */ +ve.ce.TextStyleItalicAnnotation = function VeCeTextStyleItalicAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleItalicAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleItalicAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleItalicAnnotation.static.name = 'textStyle/italic'; +ve.ce.annotationFactory.register( ve.ce.TextStyleItalicAnnotation ); + +/** + * ContentEditable underline annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleUnderlineAnnotation} model + */ +ve.ce.TextStyleUnderlineAnnotation = function VeCeTextStyleUnderlineAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleUnderlineAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleUnderlineAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleUnderlineAnnotation.static.name = 'textStyle/underline'; +ve.ce.annotationFactory.register( ve.ce.TextStyleUnderlineAnnotation ); + +/** + * ContentEditable strike annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleStrikeAnnotation} model + */ +ve.ce.TextStyleStrikeAnnotation = function VeCeTextStyleStrikeAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleStrikeAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleStrikeAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleStrikeAnnotation.static.name = 'textStyle/strike'; +ve.ce.annotationFactory.register( ve.ce.TextStyleStrikeAnnotation ); + +/** + * ContentEditable small annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleSmallAnnotation} model + */ +ve.ce.TextStyleSmallAnnotation = function VeCeTextStyleSmallAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleSmallAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleSmallAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleSmallAnnotation.static.name = 'textStyle/small'; +ve.ce.annotationFactory.register( ve.ce.TextStyleSmallAnnotation ); + +/** + * ContentEditable big annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleBigAnnotation} model + */ +ve.ce.TextStyleBigAnnotation = function VeCeTextStyleBigAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleBigAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleBigAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleBigAnnotation.static.name = 'textStyle/big'; +ve.ce.annotationFactory.register( ve.ce.TextStyleBigAnnotation ); + +/** + * ContentEditable span annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleSpanAnnotation} model + */ +ve.ce.TextStyleSpanAnnotation = function VeCeTextStyleSpanAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleSpanAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleSpanAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleSpanAnnotation.static.name = 'textStyle/span'; +ve.ce.annotationFactory.register( ve.ce.TextStyleSpanAnnotation ); + +/** + * ContentEditable strong annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleStrongAnnotation} model + */ +ve.ce.TextStyleStrongAnnotation = function VeCeTextStyleStrongAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleStrongAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleStrongAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleStrongAnnotation.static.name = 'textStyle/strong'; +ve.ce.annotationFactory.register( ve.ce.TextStyleStrongAnnotation ); + +/** + * ContentEditable emphasize annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleEmphasizeAnnotation} model + */ +ve.ce.TextStyleEmphasizeAnnotation = function VeCeTextStyleEmphasizeAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleEmphasizeAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleEmphasizeAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleEmphasizeAnnotation.static.name = 'textStyle/emphasize'; +ve.ce.annotationFactory.register( ve.ce.TextStyleEmphasizeAnnotation ); + +/** + * ContentEditable superScript annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleSuperScriptAnnotation} model + */ +ve.ce.TextStyleSuperScriptAnnotation = function VeCeTextStyleSuperScriptAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleSuperScriptAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleSuperScriptAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleSuperScriptAnnotation.static.name = 'textStyle/superScript'; +ve.ce.annotationFactory.register( ve.ce.TextStyleSuperScriptAnnotation ); + +/** + * ContentEditable subScript annotation. + * + * @class + * @extends ve.ce.TextStyleAnnotation + * @constructor + * @param {ve.dm.TextStyleSubScriptAnnotation} model + */ +ve.ce.TextStyleSubScriptAnnotation = function VeCeTextStyleSubScriptAnnotation( model ) { + ve.ce.TextStyleAnnotation.call( this, model, $( '' ) ); + this.$.addClass( 've-ce-TextStyleSubScriptAnnotation' ); +}; +ve.inheritClass( ve.ce.TextStyleSubScriptAnnotation, ve.ce.TextStyleAnnotation ); +ve.ce.TextStyleSubScriptAnnotation.static.name = 'textStyle/subScript'; +ve.ce.annotationFactory.register( ve.ce.TextStyleSubScriptAnnotation ); diff --git a/modules/ve/ce/nodes/ve.ce.MWEntityNode.js b/modules/ve/ce/nodes/ve.ce.MWEntityNode.js index c55b84f18c..2b2fb36c7b 100644 --- a/modules/ve/ce/nodes/ve.ce.MWEntityNode.js +++ b/modules/ve/ce/nodes/ve.ce.MWEntityNode.js @@ -22,9 +22,6 @@ ve.ce.MWEntityNode = function VeCeMWEntityNode( model ) { // Need CE=false to prevent selection issues this.$.attr( 'contenteditable', false ); - // Properties - this.currentSource = null; - // Events this.model.addListenerMethod( this, 'update', 'onUpdate' ); diff --git a/modules/ve/ce/ve.ce.Annotation.js b/modules/ve/ce/ve.ce.Annotation.js new file mode 100644 index 0000000000..66cc053158 --- /dev/null +++ b/modules/ve/ce/ve.ce.Annotation.js @@ -0,0 +1,105 @@ +/*! + * VisualEditor ContentEditable Annotation class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * Generic ContentEditable annotation. + * + * This is an abstract class, annotations should extend this and call this constructor from their + * constructor. You should not instantiate this class directly. + * + * Subclasses of ve.dm.Annotation should have a corresponding subclass here that controls rendering. + * + * @class + * @constructor + * @param {ve.dm.Annotation} model Model to observe + * @param {jQuery} [$element] Element to use as a container + */ +ve.ce.Annotation = function VeCeAnnotation( model, $element ) { + // Properties + this.model = model; + this.$ = $element || $( '' ); + this.parent = null; + this.live = false; + + // Initialization + this.$.data( 'annotation', this ); + ve.setDomAttributes( + this.$[0], + this.model.getAttributes( 'html/0/' ), + this.constructor.static.domAttributeWhitelist + ); +}; + +/* Events */ + +/** + * @event live + */ + +/* Static Members */ + +// TODO create a single base class for ce.Node and ce.Annotation + +ve.ce.Annotation.static = {}; + +/** + * Allowed attributes for DOM elements. + * + * This list includes attributes that are generally safe to include in HTML loaded from a + * foreign source and displaying it inside the browser. It doesn't include any event attributes, + * for instance, which would allow arbitrary JavaScript execution. This alone is not enough to + * make HTML safe to display, but it helps. + * + * TODO: Rather than use a single global list, set these on a per-annotation basis to something that makes + * sense for that annotation in particular. + * + * @static + * @property static.domAttributeWhitelist + * @inheritable + */ +ve.ce.Annotation.static.domAttributeWhitelist = [ + 'abbr', 'about', 'align', 'alt', 'axis', 'bgcolor', 'border', 'cellpadding', 'cellspacing', + 'char', 'charoff', 'cite', 'class', 'clear', 'color', 'colspan', 'datatype', 'datetime', + 'dir', 'face', 'frame', 'headers', 'height', 'href', 'id', 'itemid', 'itemprop', 'itemref', + 'itemscope', 'itemtype', 'lang', 'noshade', 'nowrap', 'property', 'rbspan', 'rel', + 'resource', 'rev', 'rowspan', 'rules', 'scope', 'size', 'span', 'src', 'start', 'style', + 'summary', 'title', 'type', 'typeof', 'valign', 'value', 'width' +]; + +/* Methods */ + +/** + * Get the model this CE annotation observes. + * + * @method + * @returns {ve.ce.Annotation} Model + */ +ve.ce.Annotation.prototype.getModel = function () { + return this.model; +}; + +/** + * Check if the annotation is attached to the live DOM. + * + * @method + * @returns {boolean} Annotation is attached to the live DOM + */ +ve.ce.Annotation.prototype.isLive = function () { + return this.live; +}; + +/** + * Set live state. + * + * @method + * @param {boolean} live The annotation has been attached to the live DOM (use false on detach) + * @emits live + */ +ve.ce.Annotation.prototype.setLive = function ( live ) { + this.live = live; + this.emit( 'live' ); +}; diff --git a/modules/ve/ce/ve.ce.AnnotationFactory.js b/modules/ve/ce/ve.ce.AnnotationFactory.js new file mode 100644 index 0000000000..0dd5232ac8 --- /dev/null +++ b/modules/ve/ce/ve.ce.AnnotationFactory.js @@ -0,0 +1,28 @@ +/*! + * VisualEditor ContentEditable AnnotationFactory class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * ContentEditable annotation factory. + * + * @class + * @extends ve.NodeFactory + * @constructor + */ +ve.ce.AnnotationFactory = function VeCeAnnotationFactory() { + // Parent constructor + // FIXME give ve.NodeFactory a more generic name + ve.NodeFactory.call( this ); +}; + +/* Inheritance */ + +ve.inheritClass( ve.ce.AnnotationFactory, ve.NodeFactory ); + +/* Initialization */ + +// TODO: Move instantiation to a different file +ve.ce.annotationFactory = new ve.ce.AnnotationFactory(); diff --git a/modules/ve/ce/ve.ce.ContentBranchNode.js b/modules/ve/ce/ve.ce.ContentBranchNode.js index 95e068c6b4..e7afb4ed3b 100644 --- a/modules/ve/ce/ve.ce.ContentBranchNode.js +++ b/modules/ve/ce/ve.ce.ContentBranchNode.js @@ -49,40 +49,18 @@ ve.ce.ContentBranchNode.prototype.onSplice = function () { }; /** - * Get an HTML rendering of contents. + * Get an HTML rendering of the contents. * * @method - * @returns {string} HTML rendering + * @returns {jQuery} */ ve.ce.ContentBranchNode.prototype.getRenderedContents = function () { - var i, j, open, close, startedClosing, arr, annotation, itemAnnotations, itemHtml, $wrapper, - store = this.model.doc.getStore(), html = '', - annotationStack = new ve.dm.AnnotationSet( store ), annotatedHtml = []; - - function openAnnotations( annotations ) { - var out = '', - annotation, i, arr, rendered; - arr = annotations.get(); - for ( i = 0; i < arr.length; i++ ) { - annotation = arr[i]; - rendered = annotation.renderHTML(); - out += ve.getOpeningHtmlTag( rendered.tag, rendered.attributes ); - annotationStack.push( annotation ); - } - return out; - } - - function closeAnnotations( annotations ) { - var out = '', - annotation, i, arr; - arr = annotations.get(); - for ( i = 0; i < arr.length; i++ ) { - annotation = arr[i]; - out += ''; - annotationStack.remove( annotation ); - } - return out; - } + var i, j, itemHtml, itemAnnotations, startClosingAt, arr, annotation, $ann, + store = this.model.doc.getStore(), + annotationStack = new ve.dm.AnnotationSet( store ), + annotatedHtml = [], + $wrapper = $( '
' ), + $current = $wrapper; // Gather annotated HTML from the child nodes for ( i = 0; i < this.children.length; i++ ) { @@ -98,60 +76,51 @@ ve.ce.ContentBranchNode.prototype.getRenderedContents = function () { itemHtml = annotatedHtml[i]; itemAnnotations = new ve.dm.AnnotationSet( store ); } - open = new ve.dm.AnnotationSet( store ); - close = new ve.dm.AnnotationSet( store ); - // Go through annotationStack from bottom to top (left to right), and - // close all annotations starting at the first one that's in annotationStack but - // not in itemAnnotations. Then reopen the ones that are in itemAnnotations. - startedClosing = false; + // FIXME code largely copied from ve.dm.Converter + // Close annotations as needed + // Go through annotationStack from bottom to top (low to high), + // and find the first annotation that's not in annotations. + startClosingAt = undefined; arr = annotationStack.get(); for ( j = 0; j < arr.length; j++ ) { annotation = arr[j]; - if ( - !startedClosing && - annotationStack.contains( annotation ) && - !itemAnnotations.contains( annotation ) - ) { - startedClosing = true; + if ( !itemAnnotations.contains( annotation ) ) { + startClosingAt = j; + break; } - if ( startedClosing ) { - // Because we're processing these in reverse order, we need - // to put these in close in reverse order - close.add( annotation, 0 ); - if ( itemAnnotations.contains( annotation ) ) { - // open needs to be reversed with respect to close - open.push( annotation ); - } + } + if ( startClosingAt !== undefined ) { + // Close all annotations from top to bottom (high to low) + // until we reach startClosingAt + for ( j = annotationStack.getLength() - 1; j >= startClosingAt; j-- ) { + // Traverse up + $current = $current.parent(); + // Remove from annotationStack + annotationStack.removeAt( j ); } } - // Open all annotations that are in right but not in left - open.addSet( itemAnnotations.diffWith( annotationStack ) ); + // Open annotations as needed + arr = itemAnnotations.get(); + for ( j = 0; j < arr.length; j++ ) { + annotation = arr[j]; + if ( !annotationStack.contains( annotation ) ) { + // Create new node and descend into it + $ann = ve.ce.annotationFactory.create( annotation.getType(), annotation ).$; + $current.append( $ann ); + $current = $ann; + // Add to annotationStack + annotationStack.push( annotation ); + } + } - // Output the annotation closings and openings - html += closeAnnotations( close ); - html += openAnnotations( open ); // Output the actual HTML - if ( typeof itemHtml === 'string' ) { - // Output it directly - html += itemHtml; - } else { - // itemHtml is a jQuery object, output a placeholder - html += '
'; - } + $current.append( itemHtml ); } - // Close all remaining open annotations - html += closeAnnotations( annotationStack.reversed() ); - - $wrapper = $( '
' ).html( html ); - // Replace placeholders - $wrapper.find( '.ve-ce-contentBranch-placeholder' ).each( function() { - var $this = $( this ), item = annotatedHtml[$this.attr( 'rel' )]; - $this.replaceWith( ve.isArray( item ) ? item[0] : item ); - } ); return $wrapper.contents(); + }; /** diff --git a/modules/ve/dm/annotations/ve.dm.LinkAnnotation.js b/modules/ve/dm/annotations/ve.dm.LinkAnnotation.js index 09ef5d22e4..d690c2f597 100644 --- a/modules/ve/dm/annotations/ve.dm.LinkAnnotation.js +++ b/modules/ve/dm/annotations/ve.dm.LinkAnnotation.js @@ -13,11 +13,11 @@ * @class * @extends ve.dm.Annotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.LinkAnnotation = function VeDmLinkAnnotation( element ) { +ve.dm.LinkAnnotation = function VeDmLinkAnnotation( linmodAnnotation ) { // Parent constructor - ve.dm.Annotation.call( this, element ); + ve.dm.Annotation.call( this, linmodAnnotation ); }; /* Inheritance */ @@ -40,55 +40,19 @@ ve.dm.LinkAnnotation.static.name = 'link'; */ ve.dm.LinkAnnotation.static.matchTagNames = ['a']; -/* Methods */ - -/** - * Get annotation data, especially the href of the link. - * - * @method - * @param {HTMLElement} element - * @returns {Object} Annotation data, containing href property - */ -ve.dm.LinkAnnotation.prototype.getAnnotationData = function( element ) { - return { 'href': element.getAttribute( 'href' ) }; -}; - -/** - * Convert to an object with HTML element information. - * - * @method - * @returns {Object} HTML element information, including tag and attributes properties - */ -ve.dm.LinkAnnotation.prototype.toHTML = function () { - var parentResult = ve.dm.Annotation.prototype.toHTML.call( this ); - parentResult.tag = 'a'; - parentResult.attributes.href = this.data.href; - return parentResult; -}; - -/** - * Get the hash object of the link annotation. - * - * This extends the basic annotation hash by adding htmlAttributes.rel - * if it present. - * - * This is a custom hash function for ve#getHash. - * - * @method - * @returns {Object} Object to hash - */ -ve.dm.LinkAnnotation.prototype.getHashObject = function () { - var keys = [ 'name', 'data' ], obj = {}, i; - for ( i = 0; i < keys.length; i++ ) { - if ( this[keys[i]] !== undefined ) { - obj[keys[i]] = this[keys[i]]; +ve.dm.LinkAnnotation.static.toDataElement = function ( domElements ) { + return { + 'type': 'link', + 'attributes': { + 'href': domElements[0].getAttribute( 'href' ) } - } - if ( this.htmlAttributes && this.htmlAttributes.rel ) { - obj.htmlAttributes = {}; - obj.htmlAttributes.rel = this.htmlAttributes.rel; - } - return obj; + }; +}; + +ve.dm.LinkAnnotation.static.toDomElements = function ( dataElement ) { + var domElement = document.createElement( 'a' ); + domElement.setAttribute( 'href', dataElement.attributes.href ); + return [ domElement ]; }; /* Registration */ diff --git a/modules/ve/dm/annotations/ve.dm.MWExternalLinkAnnotation.js b/modules/ve/dm/annotations/ve.dm.MWExternalLinkAnnotation.js index 5267142c6f..c4d1d652dd 100644 --- a/modules/ve/dm/annotations/ve.dm.MWExternalLinkAnnotation.js +++ b/modules/ve/dm/annotations/ve.dm.MWExternalLinkAnnotation.js @@ -13,16 +13,16 @@ * * * - * Each example is semantically slightly different, but don't need special treatment (yet). + * Each example is semantically slightly different, but they don't need special treatment (yet). * * @class * @extends ve.dm.LinkAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.MWExternalLinkAnnotation = function VeDmMWExternalLinkAnnotation( element ) { +ve.dm.MWExternalLinkAnnotation = function VeDmMWExternalLinkAnnotation( linmodAnnotation ) { // Parent constructor - ve.dm.LinkAnnotation.call( this, element ); + ve.dm.LinkAnnotation.call( this, linmodAnnotation ); }; /* Inheritance */ @@ -47,22 +47,17 @@ ve.dm.MWExternalLinkAnnotation.static.matchRdfaTypes = [ 'mw:ExtLink', 'mw:ExtLink/Numbered', 'mw:ExtLink/URL' ]; -/** - * Convert to an object with HTML element information. - * - * @method - * @returns {Object} HTML element information, including tag and attributes properties - */ -ve.dm.MWExternalLinkAnnotation.prototype.toHTML = function () { - var parentResult = ve.dm.LinkAnnotation.prototype.toHTML.call( this ); - parentResult.attributes.rel = parentResult.attributes.rel || 'mw:ExtLink'; +ve.dm.MWExternalLinkAnnotation.static.toDataElement = function ( domElements ) { + var parentResult = ve.dm.LinkAnnotation.static.toDataElement.apply( this, arguments ); + parentResult.type = 'link/MWexternal'; + parentResult.attributes.rel = domElements[0].getAttribute( 'rel' ); return parentResult; }; -ve.dm.MWExternalLinkAnnotation.prototype.renderHTML = function () { - var result = this.toHTML(); - result.attributes.title = this.data.href; - return result; +ve.dm.MWExternalLinkAnnotation.static.toDomElements = function ( dataElement ) { + var parentResult = ve.dm.LinkAnnotation.static.toDomElements( dataElement ); + parentResult[0].setAttribute( 'rel', dataElement.attributes.rel || 'mw:ExtLink' ); + return parentResult; }; /* Registration */ diff --git a/modules/ve/dm/annotations/ve.dm.MWInternalLinkAnnotation.js b/modules/ve/dm/annotations/ve.dm.MWInternalLinkAnnotation.js index ec65e2dbd3..33627f01ef 100644 --- a/modules/ve/dm/annotations/ve.dm.MWInternalLinkAnnotation.js +++ b/modules/ve/dm/annotations/ve.dm.MWInternalLinkAnnotation.js @@ -14,11 +14,11 @@ * @class * @extends ve.dm.LinkAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {HTMLElement|Object} linmodAnnotation */ -ve.dm.MWInternalLinkAnnotation = function VeDmMWInternalLinkAnnotation( element ) { +ve.dm.MWInternalLinkAnnotation = function VeDmMWInternalLinkAnnotation( linmodAnnotation ) { // Parent constructor - ve.dm.LinkAnnotation.call( this, element ); + ve.dm.LinkAnnotation.call( this, linmodAnnotation ); }; /* Inheritance */ @@ -41,60 +41,40 @@ ve.dm.MWInternalLinkAnnotation.static.name = 'link/MWinternal'; */ ve.dm.MWInternalLinkAnnotation.static.matchRdfaTypes = ['mw:WikiLink']; -/* Methods */ - -/** - * Get annotation data, especially the href of the link. - * - * @method - * @param {HTMLElement} element - * @returns {Object} Annotation data, containing 'hrefPrefix' and 'title' properties - */ -ve.dm.MWInternalLinkAnnotation.prototype.getAnnotationData = function ( element ) { +ve.dm.MWInternalLinkAnnotation.static.toDataElement = function ( domElements ) { // Get title from href // The href is simply the title, unless we're dealing with a page that has slashes in its name // in which case it's preceded by one or more instances of "./" or "../", so strip those /*jshint regexp:false */ - var matches = element.getAttribute( 'href' ).match( /^((?:\.\.?\/)*)(.*)$/ ); + var matches = domElements[0].getAttribute( 'href' ).match( /^((?:\.\.?\/)*)(.*)$/ ); return { - // Store the ./ and ../ prefixes so we can restore them on the way out - 'hrefPrefix': matches[1], - 'title': decodeURIComponent( matches[2] ).replace( /_/g, ' ' ), - 'origTitle': matches[2] + 'type': 'link/MWinternal', + 'attributes': { + 'hrefPrefix': matches[1], + 'title': decodeURIComponent( matches[2] ).replace( /_/g, ' ' ), + 'origTitle': matches[2] + } }; }; -/** - * Convert to an object with HTML element information. - * - * @method - * @returns {Object} HTML element information, including tag and attributes properties - */ -ve.dm.MWInternalLinkAnnotation.prototype.toHTML = function () { +ve.dm.MWInternalLinkAnnotation.static.toDomElements = function ( dataElement ) { var href, - parentResult = ve.dm.LinkAnnotation.prototype.toHTML.call( this ); - if ( - this.data.origTitle && - decodeURIComponent( this.data.origTitle ).replace( /_/g, ' ' ) === this.data.title - ) { + domElement = document.createElement( 'a' ), + title = dataElement.attributes.title, + origTitle = dataElement.attributes.origTitle; + if ( origTitle && decodeURIComponent( origTitle ).replace( /_/g, ' ' ) === title ) { // Restore href from origTitle - href = this.data.origTitle; + href = origTitle; // Only use hrefPrefix if restoring from origTitle - if ( this.data.hrefPrefix ) { - href = this.data.hrefPrefix + href; + if ( dataElement.attributes.hrefPrefix ) { + href = dataElement.attributes.hrefPrefix + href; } } else { - href = encodeURIComponent( this.data.title ); + href = encodeURIComponent( title ); } - parentResult.attributes.href = href; - parentResult.attributes.rel = 'mw:WikiLink'; - return parentResult; -}; - -ve.dm.MWInternalLinkAnnotation.prototype.renderHTML = function () { - var result = this.toHTML(); - result.attributes.title = this.data.title; - return result; + domElement.setAttribute( 'href', href ); + domElement.setAttribute( 'rel', 'mw:WikiLink' ); + return [ domElement ]; }; /* Registration */ diff --git a/modules/ve/dm/annotations/ve.dm.TextStyleAnnotation.js b/modules/ve/dm/annotations/ve.dm.TextStyleAnnotation.js index 9a52280c29..ba450e324a 100644 --- a/modules/ve/dm/annotations/ve.dm.TextStyleAnnotation.js +++ b/modules/ve/dm/annotations/ve.dm.TextStyleAnnotation.js @@ -13,10 +13,10 @@ * @class * @extends ve.dm.Annotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleAnnotation = function VeDmTextStyleAnnotation( element ) { - ve.dm.Annotation.call( this, element ); +ve.dm.TextStyleAnnotation = function VeDmTextStyleAnnotation( linmodAnnotation ) { + ve.dm.Annotation.call( this, linmodAnnotation ); }; /* Inheritance */ @@ -39,18 +39,43 @@ ve.dm.TextStyleAnnotation.static.name = 'textStyle'; */ ve.dm.TextStyleAnnotation.static.matchTagNames = []; -/** - * Convert to an object with HTML element information. - * - * @method - * @returns {Object} HTML element information, including tag and attributes properties - */ -ve.dm.TextStyleAnnotation.prototype.toHTML = function () { - var parentResult = ve.dm.Annotation.prototype.toHTML.call( this ); - parentResult.tag = parentResult.tag || this.constructor.static.matchTagNames[0]; - return parentResult; +ve.dm.TextStyleAnnotation.static.toDataElement = function ( domElements ) { + var types = { + 'b': 'bold', + 'i': 'italic', + 'u': 'underline', + 's': 'strike', + 'small': 'small', + 'big': 'big', + 'span': 'span', + 'strong': 'strong', + 'em': 'emphasize', + 'sup': 'superScript', + 'sub': 'subScript' + }; + return { + 'type': 'textStyle/' + types[domElements[0].nodeName.toLowerCase()] + }; }; +ve.dm.TextStyleAnnotation.static.toDomElements = function ( dataElement ) { + var nodeNames = { + 'bold': 'b', + 'italic': 'i', + 'underline': 'u', + 'strike': 's', + 'small': 'small', + 'big': 'big', + 'span': 'span', + 'strong': 'strong', + 'emphasize': 'em', + 'superScript': 'sup', + 'subScript': 'sub' + }; + return [ document.createElement( nodeNames[dataElement.type.substring( 10 )] ) ]; +}; + + /* Registration */ ve.dm.modelRegistry.register( ve.dm.TextStyleAnnotation ); @@ -63,10 +88,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleBoldAnnotation = function VeDmTextStyleBoldAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleBoldAnnotation = function VeDmTextStyleBoldAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleBoldAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleBoldAnnotation.static.name = 'textStyle/bold'; @@ -79,10 +104,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleBoldAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleItalicAnnotation = function VeDmTextStyleItalicAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleItalicAnnotation = function VeDmTextStyleItalicAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleItalicAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleItalicAnnotation.static.name = 'textStyle/italic'; @@ -95,10 +120,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleItalicAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleUnderlineAnnotation = function VeDmTextStyleUnderlineAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleUnderlineAnnotation = function VeDmTextStyleUnderlineAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleUnderlineAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleUnderlineAnnotation.static.name = 'textStyle/underline'; @@ -111,10 +136,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleUnderlineAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleStrikeAnnotation = function VeDmTextStyleStrikeAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleStrikeAnnotation = function VeDmTextStyleStrikeAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleStrikeAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleStrikeAnnotation.static.name = 'textStyle/strike'; @@ -127,10 +152,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleStrikeAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleSmallAnnotation = function VeDmTextStyleSmallAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleSmallAnnotation = function VeDmTextStyleSmallAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleSmallAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleSmallAnnotation.static.name = 'textStyle/small'; @@ -143,10 +168,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleSmallAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleBigAnnotation = function VeDmTextStyleBigAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleBigAnnotation = function VeDmTextStyleBigAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleBigAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleBigAnnotation.static.name = 'textStyle/big'; @@ -159,10 +184,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleBigAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleSpanAnnotation = function VeDmTextStyleSpanAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleSpanAnnotation = function VeDmTextStyleSpanAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleSpanAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleSpanAnnotation.static.name = 'textStyle/span'; @@ -175,10 +200,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleSpanAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleStrongAnnotation = function VeDmTextStyleStrongAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleStrongAnnotation = function VeDmTextStyleStrongAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleStrongAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleStrongAnnotation.static.name = 'textStyle/strong'; @@ -191,10 +216,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleStrongAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleEmphasizeAnnotation = function VeDmTextStyleEmphasizeAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleEmphasizeAnnotation = function VeDmTextStyleEmphasizeAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleEmphasizeAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleEmphasizeAnnotation.static.name = 'textStyle/emphasize'; @@ -207,10 +232,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleEmphasizeAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleSuperScriptAnnotation = function VeDmTextStyleSuperScriptAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleSuperScriptAnnotation = function VeDmTextStyleSuperScriptAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleSuperScriptAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleSuperScriptAnnotation.static.name = 'textStyle/superScript'; @@ -223,10 +248,10 @@ ve.dm.modelRegistry.register( ve.dm.TextStyleSuperScriptAnnotation ); * @class * @extends ve.dm.TextStyleAnnotation * @constructor - * @param {HTMLElement|Object} element + * @param {Object} linmodAnnotation */ -ve.dm.TextStyleSubScriptAnnotation = function VeDmTextStyleSubScriptAnnotation( element ) { - ve.dm.TextStyleAnnotation.call( this, element ); +ve.dm.TextStyleSubScriptAnnotation = function VeDmTextStyleSubScriptAnnotation( linmodAnnotation ) { + ve.dm.TextStyleAnnotation.call( this, linmodAnnotation ); }; ve.inheritClass( ve.dm.TextStyleSubScriptAnnotation, ve.dm.TextStyleAnnotation ); ve.dm.TextStyleSubScriptAnnotation.static.name = 'textStyle/subScript'; diff --git a/modules/ve/dm/ve.dm.Annotation.js b/modules/ve/dm/ve.dm.Annotation.js index 4476bae9c4..16b4d80d1a 100644 --- a/modules/ve/dm/ve.dm.Annotation.js +++ b/modules/ve/dm/ve.dm.Annotation.js @@ -11,29 +11,16 @@ * This is an abstract class, annotations should extend this and call this constructor from their * constructor. You should not instantiate this class directly. * - * Annotations in the linear model are instances of subclasses of this class. Subclasses are - * required to have a constructor with the same signature. - * - * this.htmlTagName and this.htmlAttributes are private to the base class, subclasses must not - * use them. Any information from the HTML element that is needed later should be extracted into - * this.data by overriding getAnnotationData(). Subclasses can read from this.data but must not - * write to it directly. + * Annotations in the linear model are instances of subclasses of this class. Subclasses should + * only override static properties and functions. * * @class * @constructor - * @param {HTMLElement|Object} [element] HTML element the annotation was converted from, if any, or - * an object to copy into the annotation's data property + * @param {Object} linmodAnnotation Linear model annotation */ -ve.dm.Annotation = function VeDmAnnotation( element ) { - this.name = this.constructor.static.name; // Needed for proper hashing - this.data = {}; - if ( ve.isPlainObject( element ) ) { - this.data = ve.copyObject( element ); - } else if ( element && element.nodeType === Node.ELEMENT_NODE ) { - this.htmlTagName = element.nodeName.toLowerCase(); - this.htmlAttributes = ve.getDomAttributes( element ); - this.data = this.getAnnotationData( element ); - } +ve.dm.Annotation = function VeDmAnnotation( linmodAnnotation ) { + this.name = this.constructor.static.name; + this.linmodAnnotation = linmodAnnotation; }; /* Static properties */ @@ -48,6 +35,8 @@ ve.dm.Annotation = function VeDmAnnotation( element ) { */ ve.dm.Annotation.static = {}; +// TODO create a single base class for dm.Node, dm.Annotation and dm.MetaItem + /** * Symbolic name for the annotation class. * @@ -105,74 +94,134 @@ ve.dm.Annotation.static.matchRdfaTypes = null; */ ve.dm.Annotation.static.matchFunction = null; +/** + * Static function to convert a DOM element or set of sibling DOM elements to an annotation of + * this type. + * + * This function is only called if this annotation "won" the matching for the first DOM element, so + * domElements[0] will match this item's matching rule. For annotations, there is only one node in + * domElements[]. + * + * This function is allowed to return an annotation even if context.expectingContent is false. + * If that happens, the annotation will be put in a wrapper paragraph. If this function returns + * null, the DOM element will be converted to an alien node. + * + * The returned linear model annotation must have a type property set to a registered annotation + * name (usually the annotations's .static.name, but that's not required). It may optionally have an + * attributes property set to an object with key-value pairs. Any other properties are not allowed. + * + * @static + * @method + * @param {HTMLElement[]} domElements DOM elements to convert. Only one element + * @param {Object} context Object describing the current state of the converter + * @param {boolean} context.expectingContent Whether this function is expected to return a content element + * @param {boolean} context.inWrapper Whether this element is in a wrapper paragraph generated by the converter; + * can only be true if context.expectingContent is also true + * @param {boolean} context.canCloseWrapper Whether the current wrapper paragraph can be closed; + * can only be true if context.inWrapper is also true + * @returns {Object|null} Linear model annotation, or null to alienate + */ +ve.dm.Annotation.static.toDataElement = function ( /*domElements, context*/ ) { + throw new Error( 've.dm.Annotation subclass must implement toDataElement' ); +}; + +/** + * Static function to convert a linear model annotation of this type back to a DOM element. + * + * This function returns an array of DOM elements for consistency, but annotations can only return + * one DOM element, so any elements beyond the first are ignored. + * + * @static + * @method + * @param {Object} Linear model annotation with a type property and optionally an attributes property + * @returns {HTMLElement[]} Array of one DOM element + */ +ve.dm.Annotation.static.toDomElements = function ( /*dataElement*/ ) { + throw new Error( 've.dm.Annotation subclass must implement toDomElements' ); +}; + +/** + * About grouping is not supported for annotations; setting this to true has no effect. + * + * @static + * @property {boolean} static.enableAboutGrouping + */ +ve.dm.Annotation.static.enableAboutGrouping = false; + +/** + * Whether HTML attributes should be preserved for this annotation type. If true, the HTML attributes + * of the DOM elements will be stored as attributes in the linear model annotation. The attribute + * names will be html/i/attrName, where i is the index of the DOM element in the domElements array, + * and attrName is the name of the attribute. + * + * This should generally be enabled, except for annotation types that store their entire HTML in an + * attribute. + * + * @static + * @property {boolean} static.storeHtmlAttributes + * @inheritable + */ +ve.dm.Annotation.static.storeHtmlAttributes = true; + /* Methods */ -/** - * Get annotation data for the linear model. - * - * Called when building a new annotation from an HTML element. - * - * This annotation data object is completely free-form. It's stored in the linear model, it can be - * manipulated by UI widgets, and you access it as this.data in toHTML() on the way out and in - * renderHTML() for rendering. It is also the ONLY data you can reliably use in those contexts, so - * any information from the HTML element that you'll need later should be extracted into the data - * object here. - * - * @method - * @param {HTMLElement} element HTML element the annotation will represent - * @returns {Object} Annotation data - */ -ve.dm.Annotation.prototype.getAnnotationData = function () { - return {}; +ve.dm.Annotation.prototype.getType = function () { + return this.name; }; /** - * Convert the annotation back to HTML for output purposes. - * - * You should only use this.data here, you cannot reliably use any of the other properties. - * The default action is to restore the original HTML element's tag name and attributes (if this - * annotation was created based on an element). If a subclass wants to do this too (this is common), - * it should call its parent's implementation first, then manipulate the return value. - * - * @method - * @returns {Object} Object with 'tag' (tag name) and 'attributes' (object with attribute key/values) + * Get the linear model object for this annotation + * @returns {Object} Linear model annotation */ -ve.dm.Annotation.prototype.toHTML = function () { - return { - 'tag': this.htmlTagName || '', - 'attributes': this.htmlAttributes || {} - }; +ve.dm.Annotation.prototype.getLinmodAnnotation = function () { + return this.linmodAnnotation; }; /** - * Convert the annotation to HTML for rendering purposes. + * Get the value of an attribute * - * By default, this just calls #toHTML, but it may be customized if the rendering should be - * different from the output. - * - * @see #toHTML + * Return value is by reference if array or object * * @method - * @returns {Object} Object with 'tag' (tag name) and 'attributes' (object with attribute key/values) + * @param {string} key Name of attribute to get + * @returns {Mixed} Value of attribute, or undefined if no such attribute exists */ -ve.dm.Annotation.prototype.renderHTML = function () { - return this.toHTML(); +ve.dm.Annotation.prototype.getAttribute = function ( key ) { + return this.linmodAnnotation && this.linmodAnnotation.attributes ? + this.linmodAnnotation.attributes[key] : undefined; }; +// FIXME code copied from ve.dm.Node /** - * Get the hash object of the annotation. + * Get a copy of all attributes. * - * This is a custom hash function for ve#getHash. + * Values are by reference if array or object, similar to using the getAttribute method. * * @method - * @returns {Object} Object to hash + * @param {string} prefix Only return attributes with this prefix, and remove the prefix from them + * @returns {Object} Attributes */ -ve.dm.Annotation.prototype.getHashObject = function () { - var keys = [ 'name', 'data' ], obj = {}, i; - for ( i = 0; i < keys.length; i++ ) { - if ( this[keys[i]] !== undefined ) { - obj[keys[i]] = this[keys[i]]; +ve.dm.Annotation.prototype.getAttributes = function ( prefix ) { + var key, filtered, + attributes = this.element && this.linmodAnnotation.attributes ? + this.linmodAnnotation.attributes : {}; + if ( prefix ) { + filtered = {}; + for ( key in attributes ) { + if ( key.indexOf( prefix ) === 0 ) { + filtered[key.substr( prefix.length )] = attributes[key]; + } } + return filtered; } - return obj; -}; \ No newline at end of file + return ve.extendObject( {}, attributes ); +}; + +/** + * Convenience wrapper for .toDomElements() on the current annotation + * @method + * @see #toDomElements + */ +ve.dm.Annotation.prototype.getDomElements = function () { + return this.constructor.static.toDomElements( this.linmodAnnotation ); +}; diff --git a/modules/ve/dm/ve.dm.Converter.js b/modules/ve/dm/ve.dm.Converter.js index c1fa4db559..a7bd6574e0 100644 --- a/modules/ve/dm/ve.dm.Converter.js +++ b/modules/ve/dm/ve.dm.Converter.js @@ -98,7 +98,7 @@ ve.dm.Converter.prototype.getDomElementsFromDataElement = function ( dataElement /** * Create a data element from a DOM element. - * @param {ve.dm.Node|ve.dm.MetaItem} modelClass Model class to use for conversion + * @param {ve.dm.Node|ve.dm.MetaItem|ve.dm.Annotation} modelClass Model class to use for conversion * @param {HTMLElement[]} domElements DOM elements to convert * @param {Object} context Converter context to pass to toDataElement() (will be cloned) * @returns {Object} Data element @@ -245,7 +245,7 @@ ve.dm.Converter.prototype.getDataFromDomRecursion = function ( store, domElement path = path || ['document']; var i, childDomElement, childDomElements, childDataElement, text, childTypes, matches, wrappingParagraph, prevElement, childAnnotations, modelName, modelClass, - annotation, childIsContent, aboutGroup, + annotation, annotationData, childIsContent, aboutGroup, data = [], branchType = path[path.length - 1], branchHasContent = this.nodeFactory.canNodeContainContent( branchType ), @@ -270,7 +270,10 @@ ve.dm.Converter.prototype.getDataFromDomRecursion = function ( store, domElement modelName = this.modelRegistry.matchElement( childDomElement ); modelClass = this.modelRegistry.lookup( modelName ) || ve.dm.AlienNode; if ( modelClass.prototype instanceof ve.dm.Annotation ) { - annotation = this.annotationFactory.create( modelName, childDomElement ); + annotationData = this.createDataElement( modelClass, [ childDomElement ], context ); + } + if ( modelClass.prototype instanceof ve.dm.Annotation && annotationData ) { + annotation = this.annotationFactory.create( modelName, annotationData ); // Start wrapping if needed if ( !context.inWrapper && !context.expectingContent ) { startWrapping(); @@ -627,7 +630,9 @@ ve.dm.Converter.prototype.getDomFromData = function ( store, data ) { text = ''; } // Create new node and descend into it - annotationElement = this.getDomElementFromDataAnnotation( annotation, doc ); + annotationElement = this.getDomElementsFromDataElement( + annotation.getLinmodAnnotation(), doc + )[0]; domElement.appendChild( annotationElement ); domElement = annotationElement; // Add to annotationStack diff --git a/modules/ve/dm/ve.dm.Node.js b/modules/ve/dm/ve.dm.Node.js index d92fada715..03826a6f2b 100644 --- a/modules/ve/dm/ve.dm.Node.js +++ b/modules/ve/dm/ve.dm.Node.js @@ -491,6 +491,7 @@ ve.dm.Node.prototype.getOffset = function () { * Return value is by reference if array or object. * * @method + * @param {string} key Name of attribute to get * @returns {Mixed} Value of attribute, or undefined if no such attribute exists */ ve.dm.Node.prototype.getAttribute = function ( key ) { diff --git a/modules/ve/test/ce/ve.ce.ContentBranchNode.test.js b/modules/ve/test/ce/ve.ce.ContentBranchNode.test.js index 1376d18178..1a1bd4e0f2 100644 --- a/modules/ve/test/ce/ve.ce.ContentBranchNode.test.js +++ b/modules/ve/test/ce/ve.ce.ContentBranchNode.test.js @@ -10,9 +10,10 @@ QUnit.module( 've.ce.ContentBranchNode' ); /* Tests */ QUnit.test( 'getRenderedContents', function ( assert ) { - var i, len, doc, $rendered, + var i, len, doc, $rendered, $wrapper, cases = [ { + 'msg': 'Plain text without annotations', 'data': [ { 'type': 'paragraph' }, 'a', @@ -23,6 +24,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abc' }, { + 'msg': 'Bold text', 'data': [ { 'type': 'paragraph' }, ['a', [ { 'type': 'textStyle/bold' } ]], @@ -33,6 +35,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abc' }, { + 'msg': 'Bold character, plain character, italic character', 'data': [ { 'type': 'paragraph' }, ['a', [ { 'type': 'textStyle/bold' } ]], @@ -43,6 +46,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abc' }, { + 'msg': 'Bold, italic and underlined text (same order)', 'data': [ { 'type': 'paragraph' }, ['a', [ @@ -65,6 +69,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abc' }, { + 'msg': 'Varying order in consecutive range doesn\'t affect rendering', 'data': [ { 'type': 'paragraph' }, ['a', [ @@ -87,6 +92,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abc' }, { + 'msg': 'Varying order in non-consecutive range does affect rendering', 'data': [ { 'type': 'paragraph' }, ['a', [ @@ -105,6 +111,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abc' }, { + 'msg': 'Text annotated in varying order, surrounded by plain text', 'data': [ { 'type': 'paragraph' }, 'a', @@ -133,6 +140,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abcdefghi' }, { + 'msg': 'Out-of-order closings do not produce misnested tags', 'data': [ { 'type': 'paragraph' }, 'a', @@ -160,6 +168,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abcdefghi' }, { + 'msg': 'Additional openings are added inline, even when out of order', 'data': [ { 'type': 'paragraph' }, 'a', @@ -187,6 +196,7 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'html': 'abcdefghi' }, { + 'msg': 'Out-of-order closings surrounded by plain text', 'data': [ { 'type': 'paragraph' }, 'a', @@ -210,9 +220,10 @@ QUnit.test( 'getRenderedContents', function ( assert ) { 'i', { 'type': '/paragraph' } ], - 'html': 'abcdefghi' + 'html': 'abcdefghi' }, { + 'msg': 'Annotation spanning text and inline nodes', 'data': [ { 'type': 'paragraph' }, 'a', @@ -242,6 +253,9 @@ QUnit.test( 'getRenderedContents', function ( assert ) { for ( i = 0, len = cases.length; i < len; i++ ) { doc = new ve.dm.Document( ve.dm.example.preprocessAnnotations( cases[i].data ) ); $rendered = ( new ve.ce.ParagraphNode( doc.documentNode.getChildren()[0] ) ).getRenderedContents(); - assert.deepEqual( $( '
' ).append( $rendered ).html(), cases[i].html ); + $wrapper = $( '
' ).append( $rendered ); + // HACK strip out all the class="ve-ce-TextStyleAnnotation ve-ce-TextStyleBoldAnnotation" crap + $wrapper.find( '.ve-ce-TextStyleAnnotation' ).removeAttr( 'class' ); + assert.equalDomElement( $wrapper[0], $( '
' ).html( cases[i].html )[0], cases[i].msg ); } } ); diff --git a/modules/ve/test/dm/ve.dm.example.js b/modules/ve/test/dm/ve.dm.example.js index 3395486fc4..3307e76466 100644 --- a/modules/ve/test/dm/ve.dm.example.js +++ b/modules/ve/test/dm/ve.dm.example.js @@ -19,7 +19,7 @@ ve.dm.example = {}; * annotation objects, and wraps the result in a ve.dm.ElementLinearData object. * * Shorthand notation for annotations is: - * [ 'a', [ { 'type': 'link', 'data': { 'href': '...' }, 'htmlTagName': 'a', 'htmlAttributes': { ... } } ] ] + * [ 'a', [ { 'type': 'link', 'attributes': { 'href': '...' } ] ] * * The actual storage format has an instance of ve.dm.LinkAnnotation instead of the plain object, * and an instance of ve.dm.AnnotationSet instead of the array. @@ -55,18 +55,11 @@ ve.dm.example.preprocessAnnotations = function ( data, store ) { /** * Create an annotation object from shorthand notation. * @method - * @param {Object} annotation Plain object with type, data, htmlTagName and htmlAttributes properties + * @param {Object} annotation Plain object with type and attributes properties * @return {ve.dm.Annotation} Instance of the right ve.dm.Annotation subclass */ ve.dm.example.createAnnotation = function ( annotation ) { - var ann, annKey; - ann = ve.dm.annotationFactory.create( annotation.type ); - for ( annKey in annotation ) { - if ( annKey !== 'type' ) { - ann[annKey] = annotation[annKey]; - } - } - return ann; + return ve.dm.annotationFactory.create( annotation.type, annotation ); }; /** @@ -88,10 +81,10 @@ ve.dm.example.createAnnotationSet = function ( store, annotations ) { }; /* Some common annotations in shorthand format */ -ve.dm.example.bold = { 'type': 'textStyle/bold', 'htmlTagName': 'b', 'htmlAttributes': {} }; -ve.dm.example.italic = { 'type': 'textStyle/italic', 'htmlTagName': 'i', 'htmlAttributes': {} }; -ve.dm.example.underline = { 'type': 'textStyle/underline', 'htmlTagName': 'u', 'htmlAttributes': {} }; -ve.dm.example.span = { 'type': 'textStyle/span', 'htmlTagName': 'span', 'htmlAttributes': {} }; +ve.dm.example.bold = { 'type': 'textStyle/bold' }; +ve.dm.example.italic = { 'type': 'textStyle/italic' }; +ve.dm.example.underline = { 'type': 'textStyle/underline' }; +ve.dm.example.span = { 'type': 'textStyle/span' }; /** * Creates a document from example data. @@ -1049,16 +1042,13 @@ ve.dm.example.domToDataCases = { 'b', [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'title': 'Foo bar', 'origTitle': 'Foo_bar', - 'hrefPrefix': '' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'data-rt': '{"sHref":"foo bar"}', - 'href': 'Foo_bar', - 'rel': 'mw:WikiLink' + 'hrefPrefix': '', + 'html/0/data-rt': '{"sHref":"foo bar"}', + 'html/0/href': 'Foo_bar', + 'html/0/rel': 'mw:WikiLink' } } ] ], @@ -1066,16 +1056,13 @@ ve.dm.example.domToDataCases = { 'a', [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'title': 'Foo bar', 'origTitle': 'Foo_bar', - 'hrefPrefix': '' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'data-rt': '{"sHref":"foo bar"}', - 'href': 'Foo_bar', - 'rel': 'mw:WikiLink' + 'hrefPrefix': '', + 'html/0/data-rt': '{"sHref":"foo bar"}', + 'html/0/href': 'Foo_bar', + 'html/0/rel': 'mw:WikiLink' } } ] ], @@ -1083,16 +1070,13 @@ ve.dm.example.domToDataCases = { 'r', [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'title': 'Foo bar', 'origTitle': 'Foo_bar', - 'hrefPrefix': '' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'data-rt': '{"sHref":"foo bar"}', - 'href': 'Foo_bar', - 'rel': 'mw:WikiLink' + 'hrefPrefix': '', + 'html/0/data-rt': '{"sHref":"foo bar"}', + 'html/0/href': 'Foo_bar', + 'html/0/rel': 'mw:WikiLink' } } ] ], @@ -1110,15 +1094,12 @@ ve.dm.example.domToDataCases = { 'F', [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'title': 'Foo/Bar', 'origTitle': 'Foo/Bar', - 'hrefPrefix': './../../../' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': './../../../Foo/Bar', - 'rel': 'mw:WikiLink' + 'hrefPrefix': './../../../', + 'html/0/href': './../../../Foo/Bar', + 'html/0/rel': 'mw:WikiLink' } } ] ], @@ -1126,15 +1107,12 @@ ve.dm.example.domToDataCases = { 'o', [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'title': 'Foo/Bar', 'origTitle': 'Foo/Bar', - 'hrefPrefix': './../../../' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': './../../../Foo/Bar', - 'rel': 'mw:WikiLink' + 'hrefPrefix': './../../../', + 'html/0/href': './../../../Foo/Bar', + 'html/0/rel': 'mw:WikiLink' } } ] ], @@ -1142,15 +1120,12 @@ ve.dm.example.domToDataCases = { 'o', [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'title': 'Foo/Bar', 'origTitle': 'Foo/Bar', - 'hrefPrefix': './../../../' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': './../../../Foo/Bar', - 'rel': 'mw:WikiLink' + 'hrefPrefix': './../../../', + 'html/0/href': './../../../Foo/Bar', + 'html/0/rel': 'mw:WikiLink' } } ] ], @@ -1165,13 +1140,11 @@ ve.dm.example.domToDataCases = { '[', [ { 'type': 'link/MWexternal', - 'data': { - 'href': 'http://www.mediawiki.org/' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { + 'attributes': { 'href': 'http://www.mediawiki.org/', - 'rel': 'mw:ExtLink/Numbered' + 'rel': 'mw:ExtLink/Numbered', + 'html/0/href': 'http://www.mediawiki.org/', + 'html/0/rel': 'mw:ExtLink/Numbered' } } ] ], @@ -1179,13 +1152,11 @@ ve.dm.example.domToDataCases = { '1', [ { 'type': 'link/MWexternal', - 'data': { - 'href': 'http://www.mediawiki.org/' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { + 'attributes': { 'href': 'http://www.mediawiki.org/', - 'rel': 'mw:ExtLink/Numbered' + 'rel': 'mw:ExtLink/Numbered', + 'html/0/href': 'http://www.mediawiki.org/', + 'html/0/rel': 'mw:ExtLink/Numbered' } } ] ], @@ -1193,13 +1164,11 @@ ve.dm.example.domToDataCases = { ']', [ { 'type': 'link/MWexternal', - 'data': { - 'href': 'http://www.mediawiki.org/' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { + 'attributes': { 'href': 'http://www.mediawiki.org/', - 'rel': 'mw:ExtLink/Numbered' + 'rel': 'mw:ExtLink/Numbered', + 'html/0/href': 'http://www.mediawiki.org/', + 'html/0/rel': 'mw:ExtLink/Numbered' } } ] ], @@ -1214,13 +1183,11 @@ ve.dm.example.domToDataCases = { 'm', [ { 'type': 'link/MWexternal', - 'data': { - 'href': 'http://www.mediawiki.org/' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { + 'attributes': { 'href': 'http://www.mediawiki.org/', - 'rel': 'mw:ExtLink/URL' + 'rel': 'mw:ExtLink/URL', + 'html/0/href': 'http://www.mediawiki.org/', + 'html/0/rel': 'mw:ExtLink/URL' } } ] ], @@ -1228,13 +1195,11 @@ ve.dm.example.domToDataCases = { 'w', [ { 'type': 'link/MWexternal', - 'data': { - 'href': 'http://www.mediawiki.org/' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { + 'attributes': { 'href': 'http://www.mediawiki.org/', - 'rel': 'mw:ExtLink/URL' + 'rel': 'mw:ExtLink/URL', + 'html/0/href': 'http://www.mediawiki.org/', + 'html/0/rel': 'mw:ExtLink/URL' } } ] ], @@ -1634,15 +1599,12 @@ ve.dm.example.domToDataCases = { ve.dm.example.bold, { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'hrefPrefix': '', 'origTitle': 'Foo', - 'title': 'Foo' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': 'Foo', - 'rel': 'mw:WikiLink' + 'title': 'Foo', + 'html/0/href': 'Foo', + 'html/0/rel': 'mw:WikiLink' } }, ve.dm.example.italic @@ -1654,15 +1616,12 @@ ve.dm.example.domToDataCases = { ve.dm.example.bold, { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'hrefPrefix': '', 'origTitle': 'Foo', - 'title': 'Foo' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': 'Foo', - 'rel': 'mw:WikiLink' + 'title': 'Foo', + 'html/0/href': 'Foo', + 'html/0/rel': 'mw:WikiLink' } }, ve.dm.example.italic @@ -1674,15 +1633,12 @@ ve.dm.example.domToDataCases = { ve.dm.example.bold, { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'hrefPrefix': '', 'origTitle': 'Foo', - 'title': 'Foo' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': 'Foo', - 'rel': 'mw:WikiLink' + 'title': 'Foo', + 'html/0/href': 'Foo', + 'html/0/rel': 'mw:WikiLink' } }, ve.dm.example.italic @@ -1700,15 +1656,12 @@ ve.dm.example.domToDataCases = { [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'hrefPrefix': '', 'origTitle': 'Foo', - 'title': 'Foo' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': 'Foo', - 'rel': 'mw:WikiLink' + 'title': 'Foo', + 'html/0/href': 'Foo', + 'html/0/rel': 'mw:WikiLink' } } ] @@ -1718,15 +1671,12 @@ ve.dm.example.domToDataCases = { [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'hrefPrefix': '', 'origTitle': 'Foo', - 'title': 'Foo' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': 'Foo', - 'rel': 'mw:WikiLink' + 'title': 'Foo', + 'html/0/href': 'Foo', + 'html/0/rel': 'mw:WikiLink' } }, ve.dm.example.bold @@ -1737,15 +1687,12 @@ ve.dm.example.domToDataCases = { [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'hrefPrefix': '', 'origTitle': 'Foo', - 'title': 'Foo' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': 'Foo', - 'rel': 'mw:WikiLink' + 'title': 'Foo', + 'html/0/href': 'Foo', + 'html/0/rel': 'mw:WikiLink' } }, ve.dm.example.bold, @@ -1757,15 +1704,12 @@ ve.dm.example.domToDataCases = { [ { 'type': 'link/MWinternal', - 'data': { + 'attributes': { 'hrefPrefix': '', 'origTitle': 'Foo', - 'title': 'Foo' - }, - 'htmlTagName': 'a', - 'htmlAttributes': { - 'href': 'Foo', - 'rel': 'mw:WikiLink' + 'title': 'Foo', + 'html/0/href': 'Foo', + 'html/0/rel': 'mw:WikiLink' } }, ve.dm.example.italic diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php index 326da1acb3..7abd871b36 100644 --- a/modules/ve/test/index.php +++ b/modules/ve/test/index.php @@ -112,8 +112,10 @@ + + @@ -143,6 +145,10 @@ + + + + diff --git a/modules/ve/ui/inspectors/ve.ui.LinkInspector.js b/modules/ve/ui/inspectors/ve.ui.LinkInspector.js index e1d86cc87a..ebd59db4cd 100644 --- a/modules/ve/ui/inspectors/ve.ui.LinkInspector.js +++ b/modules/ve/ui/inspectors/ve.ui.LinkInspector.js @@ -195,7 +195,12 @@ ve.ui.LinkInspector.prototype.onClose = function ( remove ) { * @returns {ve.dm.LinkAnnotation} */ ve.ui.LinkInspector.prototype.getAnnotationFromTarget = function ( target ) { - return new ve.dm.LinkAnnotation( { 'href': target } ); + return new ve.dm.LinkAnnotation( { + 'type': 'link', + 'attributes': { + 'href': target + } + } ); }; /* Registration */ diff --git a/modules/ve/ui/inspectors/ve.ui.MWLinkInspector.js b/modules/ve/ui/inspectors/ve.ui.MWLinkInspector.js index 2cb5918590..c9d6fb87d5 100644 --- a/modules/ve/ui/inspectors/ve.ui.MWLinkInspector.js +++ b/modules/ve/ui/inspectors/ve.ui.MWLinkInspector.js @@ -47,7 +47,12 @@ ve.ui.MWLinkInspector.prototype.getAnnotationFromTarget = function ( target ) { // Figure out if this is an internal or external link if ( ve.init.platform.getExternalLinkUrlProtocolsRegExp().test( target ) ) { // External link - return new ve.dm.MWExternalLinkAnnotation( { 'href': target } ); + return new ve.dm.MWExternalLinkAnnotation( { + 'type': 'link/MWexternal', + 'attributes': { + 'href': target + } + } ); } else { // Internal link // TODO: In the longer term we'll want to have autocompletion and existence and validity @@ -61,7 +66,12 @@ ve.ui.MWLinkInspector.prototype.getAnnotationFromTarget = function ( target ) { target = ':' + target; } } catch ( e ) { } - return new ve.dm.MWInternalLinkAnnotation( { 'title': target } ); + return new ve.dm.MWInternalLinkAnnotation( { + 'type': 'link/MWinternal', + 'attributes': { + 'title': target + } + } ); } }; diff --git a/modules/ve/ui/widgets/ve.ui.LinkTargetInputWidget.js b/modules/ve/ui/widgets/ve.ui.LinkTargetInputWidget.js index 70c4d3feb2..2545ba6644 100644 --- a/modules/ve/ui/widgets/ve.ui.LinkTargetInputWidget.js +++ b/modules/ve/ui/widgets/ve.ui.LinkTargetInputWidget.js @@ -45,7 +45,12 @@ ve.ui.LinkTargetInputWidget.prototype.setValue = function ( value ) { if ( value === '' ) { this.annotation = null; } else { - this.setAnnotation( new ve.dm.LinkAnnotation( { 'href': value } ) ); + this.setAnnotation( new ve.dm.LinkAnnotation( { + 'type': 'link', + 'attributes': { + 'href': value + } + } ) ); } // Call parent method @@ -92,7 +97,7 @@ ve.ui.LinkTargetInputWidget.prototype.getAnnotation = function () { */ ve.ui.LinkTargetInputWidget.prototype.getTargetFromAnnotation = function ( annotation ) { if ( annotation instanceof ve.dm.LinkAnnotation ) { - return annotation.data.href; + return annotation.getAttribute( 'href' ); } return ''; }; diff --git a/modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js b/modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js index 25517c5b39..956eab7f53 100644 --- a/modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js +++ b/modules/ve/ui/widgets/ve.ui.MWLinkTargetInputWidget.js @@ -257,7 +257,12 @@ ve.ui.MWLinkTargetInputWidget.prototype.getInternalLinkAnnotationFromTitle = fun target = ':' + target; } } catch ( e ) { } - return new ve.dm.MWInternalLinkAnnotation( { 'title': target } ); + return new ve.dm.MWInternalLinkAnnotation( { + 'type': 'link/MWinternal', + 'attributes': { + 'title': target + } + } ); }; /** @@ -268,7 +273,12 @@ ve.ui.MWLinkTargetInputWidget.prototype.getInternalLinkAnnotationFromTitle = fun * @returns {ve.dm.MWExternalLinkAnnotation} */ ve.ui.MWLinkTargetInputWidget.prototype.getExternalLinkAnnotationFromUrl = function ( target ) { - return new ve.dm.MWExternalLinkAnnotation( { 'href': target } ); + return new ve.dm.MWExternalLinkAnnotation( { + 'type': 'link/MWexternal', + 'attributes': { + 'href': target + } + } ); }; /** @@ -280,9 +290,9 @@ ve.ui.MWLinkTargetInputWidget.prototype.getExternalLinkAnnotationFromUrl = funct */ ve.ui.MWLinkTargetInputWidget.prototype.getTargetFromAnnotation = function ( annotation ) { if ( annotation instanceof ve.dm.MWExternalLinkAnnotation ) { - return annotation.data.href; + return annotation.getAttribute( 'href' ); } else if ( annotation instanceof ve.dm.MWInternalLinkAnnotation ) { - return annotation.data.title; + return annotation.getAttribute( 'title' ); } return ''; };