VisualEditor/API/Data Model/Surface
A surface contains a document, selection and history of transactions that have been processed over time. While the user interface provides a familiar affordance for working with a document, everything that is being changed is done through the surface model.
Documents
editA document is a combination of a linear data model and a model tree. To modify a document, transactions are created and processed, changing the linear data model. When processing is complete, the model tree is then synchronized, and events are then emitted from the tree so that the user interface can update itself accordingly.
Linear Model
editBefore working with a document, it's important to understand the nature of the linear model. The linear model itself is generated from the HTML representation of wikitext documents that Parsoid provides. While HTML is much easier to work with in software than wikitext, it is still not ideal for transactional editing – where each change is differential to a previous state. VisualEditor solves this problem by converting HTML into a linear data model for editing, and then back to HTML when done. The linear data model is essentially an HTML token stream with the exception that elements are not used for text formatting. Instead, annotations are composed onto formatted characters, allowing arbitrary slices of data to be as self-descriptive as possible with minimal processing. Upon saving a page, the linear data model is converted back to HTML and then subsequently wikitext.
Conversion
editTo better understand how conversion works, let's take a look at how a simple page looks at each stage in the process.
If the page contains this wikitext:
<span class="ve-doc-dm-char-text">Hello </span><span class="ve-doc-dm-achar-text"><nowiki>'''world'''</nowiki></span><span class="ve-doc-dm-char-text">!</span>
Parsoid produces this HTML:
<span class="ve-doc-dm-element-text"><p></span><span class="ve-doc-dm-char-text">Hello </span><span class="ve-doc-dm-achar-text"><b>World</b></span><span class="ve-doc-dm-char-text">!</span><span class="ve-doc-dm-element-text"></p></span>
VisualEditor will convert that into this data structure:
<span class="ve-doc-dm-achar-text">// bold === { type:’textStyle/bold’ }</span>
[
<span class="ve-doc-dm-element-text">{ type: ’paragraph’ }</span>,
<span class="ve-doc-dm-char-text">’H’</span>,
<span class="ve-doc-dm-char-text">’e’</span>,
<span class="ve-doc-dm-char-text">’l’</span>,
<span class="ve-doc-dm-char-text">’l’</span>,
<span class="ve-doc-dm-char-text">’o’</span>,
<span class="ve-doc-dm-char-text">’ ’</span>,
<span class="ve-doc-dm-achar-text">[ ’W’, [ bold ] ]</span>,
<span class="ve-doc-dm-achar-text">[ ’o’, [ bold ] ]</span>,
<span class="ve-doc-dm-achar-text">[ ’r’, [ bold ] ]</span>,
<span class="ve-doc-dm-achar-text">[ ’l’, [ bold ] ]</span>,
<span class="ve-doc-dm-achar-text">[ ’d’, [ bold ] ]</span>,
<span class="ve-doc-dm-char-text">’!’</span>,
<span class="ve-doc-dm-element-text">{ type: ’/paragraph’ }</span>
]
Which, more succinctly, can be represented like this:
- P
- H
- e
- l
- l
- o
- W
- o
- r
- l
- d
- !
- P
Transactions
editTransactions describe how to get from one version of a document to another. An important feature of transactions is that they provide a way to return the document to a prior state as well. This reversible processing is what powers undo and redo, and lays the groundwork for history playback and real-time collaboration.
Let's take a look at how a simple transaction can be used to change a document, and then restore it to its original state.
Our original document contains a single paragraph with the word "Hello" inside, and the text "Hel" is selected.
- P
- [
- H
- e
- l
- ]
- l
- o
- P
To remove the selection we must build a transaction. The following operations will: keep the first character; remove 'hel' and insert nothing; and keep the final 3 characters.
retain 1 replace ["h", "e", "l"] [] retain 3
The document now looks like this:
- P
- |
- l
- o
- P
Each operation in a transaction is fully reversible. In the case of retain no modification is necessary. For replace, we can just swap the remove and insert arguments.
retain 1 replace [] ["h", "e", "l"] retain 3
Model Tree
editWhile the linear model is an optimal data structure for transactional processing, it's sub-optimal for creating transactions that change to the document's structure or generating a user interface in a browser. The solution is a model tree which is kept in sync with the linear model. The tree contains nodes, each providing functionality relevant to the elements they represent.
Selection
editIn the user interface, a user describes where in the document they want to change by selecting. Although rendered very differently, a blinking cursor is just a zero-length selection. This selection mechanism is used throughout VisualEditor, and especially when working with documents.
Offsets
editOffsets describe locations in a document. While only the offsets where content can be inserted are available in the user interface, no such restriction exists at the model level.
- An offset describes a position within the document between two elements, characters or extents.
- All elements in a document are wrapped in a pair of elements except for text and the document itself. Due to this wrapping, in a document that contains a single paragraph, the offset before the first character is offset 1.
- P
- |
- H
- e
- l
- l
- o
- P
- Text is not wrapped with elements, the boundaries are implied.
- If the cursor is inside of an element that can contain content, it's effectively inside a text node, even if the element is empty.
- P
- |
- P
Ranges
editA range is a pair of offsets, from and to. A selection can be drawn either in the reading direction, or backwards, so it's common to work with the start and end values of a range, which are always in ascending order respectively, rather than the from and to values which are always in the original order that the selection was drawn.
- To select the contents of an element, a range must start and end inside of the element.
- Even if the range covers one of the element ends (the opening or closing) the content is the only thing that is being worked on. Given the scenario of removal, selecting a part of an element will cause the element to be truncated.
- P
- [
- H
- e
- l
- l
- o
- ]
- P
- To select the element itself, the range must completely contain the element.
- If a range starts before the element and ends after it, the entire element is consider covered. Given the scenario of removal, selecting an entire element will remove it and it's contents.
- [
- P
- H
- e
- l
- l
- o
- P
- ]
To create a new range, the ve.Range class can be instantiated with from and to arguments, each an offset within a document.
var range = new ve.Range( 1, 10 );
A common way that a range object is obtained is when getting the current selection.
var selection = surfaceModel.getSelection(),
range = selection.getCoveringRange();
You can also modify the selection of a surface by providing a range object as an argument. When the selection is changed on the model, the user interface responds by selecting the corresponding content in the DOM.
surfaceModel.setLinearSelection( new ve.Range( 1, 10 ) );
When working with a node in the document's model tree, you can get the range or outer range (which includes wrappers if appropriate) directly.
var inside = paragraphNode.getRange(),
outside = paragraphNode.getOuterRange(),
Fragments
editWhen working with a surface, a surface fragment object is used to abstract away the complexity of getting information about a surface and making changes to it while keeping everything in sync. Interactions with this API are similar to working with a jQuery selection in many ways, which is done intentionally to make it easier to learn.
Get a fragment from a Surface
edit// Gets a fragment of the current selection
var selectedFragment = surfaceModel.getFragment();
// Gets a fragment of a specific selection (in this case from offsets 1 to 100)
var arbitraryFragment = surfaceModel.getLinearFragment( new ve.Range( 1, 100 ) );
Chaining
editOnce you have a fragment, you can get information about the surface using a getter function, get another fragment based on the one you have, or make changes to the document. The latter two of these classes of operations are chainable.
fragment
// Gets a fragment that covers the entire paragraph
.expandLinearSelection( 'closest', 'paragraph' )
// Makes all content in the paragraph bold
.annotateContent( 'set', 'textStyle/bold' );
Null fragments
editSometimes getting a new fragment based on the one you have results in an invalid fragment, such as asking to expand the range to the nearest paragraph while in a pre-formatted node. In this case a null fragment is returned, from which all getters return empty values, only null fragments can be created and changes to the document are ignored.
fragment
// This results in a valid fragment if the range was inside a paragraph
.expandLinearSelection( 'closest', ve.dm.Paragraph )
// This always results in a null fragment; paragraphs can't be nested
.expandLinearSelection( 'closest', ve.dm.Paragraph )
// Nothing will actually be done here
.annotateContent( 'set', 'textStyle/bold' );
Ranges
editEach fragment's range will automatically be updated to remain relevant any time a document is modified. This is done internally by using the ve.dm.Transaction.translateRange feature.
var fragment1 = surfaceModel.getLinearFragment( new ve.Range( 1, 100 ) ),
fragment2 = surfaceModel.getLinearFragment( new ve.Range( 10, 20 ) );
fragment1.removeContent();
// fragment2 now has a zero-length range starting a offset 1
Reference
editve.dm.SurfaceFragment( surface, selection )
editSelected portion of a surface.
- surface {ve.dm.Surface}
- Target surface
- selection {ve.dm.Selection} [optional]
- Selection within target document, current selection used by default
// Gets a fragment of the current selection
var selectedFragment = new ve.dm.SurfaceFragment( surfaceModel );
// Gets a fragment of a specific selection (in this case from offsets 1 to 100)
var arbitraryFragment = new ve.dm.SurfaceFragment(
surfaceModel,
new ve.dm.LinearSelection( new ve.Range( 1, 100 ) )
);
- isNull()
- Responds to transactions being processed on the document
- Returns {Boolean} Fragment is a null fragment
- adjustLinearSelection( start, end )
- Gets a new fragment with an adjusted position
- Returns {ve.dm.SurfaceFragment} Adjusted fragment
- start {Number} [optional]
- Adjustment for start position
- end {Number} [optional]
- Adjustment for end position