User:Zoe-WMF/VisualEditor/EditCheckDraft
|
This is a MediaWiki.org user page. If you find this page on any site other than MediaWiki.org, you are viewing a mirror site. Be aware that the page may be outdated, and that the user this page belongs to may have no personal affiliation with any site other than MediaWiki.org itself. The original page is located at https://www.mediawiki.org/wiki/User:Zoe-WMF/VisualEditor/EditCheckDraft. |
Misc TODO
[edit]Core codebase concepts
[edit]How VE represents documents
[edit]https://www.mediawiki.org/wiki/VisualEditor/Developers/Getting_started
Frameworks and Javascript
[edit]Frameworks
[edit]VisualEditor is written substantially in pure Javascript and most of the framework that it relies upon was built alongside MediaWiki. From top to bottom:
- Extension:VisualEditor, the MediaWiki extension, is the layer which understands how to interact with MediaWiki. This is where built-in Edit Checks live. Most things are namespaced under
ve, although a couple of things are undermwincludingmw.editchecks. - VisualEditor, the standalone editor, also known as
veorlib/ve(where it can be found), is the component which provides a WYSIWYG-style editor. Most things are also namespaced underve. OOUI, vendored underlib/ve/lib/oojs-ui, provides the components which VisualEditor is built from. This is where things like Toolbars, Dialogs, Widgets and Windows live. You shouldn’t need this unless you’re building a check which requires new UI flows.- OOJS, vendored under
lib/ve/lib/oojs, provides our object-orientation framework and synchronous event framework. - jQuery is used to manipulate DOM elements.
Javascript and builds
[edit]We use ES6 javascript, but not ES6 classes. Development environments provide the javascript files as-is to the browser, whereas production MediaWiki does minimal post-processing by concatenating each module into one file.
Prototype-style inheritance
[edit]We implement classes using OOjs, which relies heavily on Javascript prototypes. Instead of having a distinction between classes and objects, a prototype system treats (almost) everything as an object. Each object has a prototype, and each prototype is just another object. When we request an object’s attribute, Javascript recursively checks the object and then its prototype chain.
In a manner similar to creating an object from a class, we can create objects using a constructor function. Any function F called with the new keyword is treated as a constructor. This causes Javascript to create a new object with a prototype of F.prototype and then runs F with this set to the new object. A common pattern is therefore to create a constructor function and then add methods to its prototype:
// A constructor function, A
const A = function ( init ) {
this.value = init;
}
A.prototype.inc = function() {
return ++this.value;
}
// Create `a`, an object with { value: 0 } and a prototype of A.prototype
let a = new A( 0 );
// a's prototype property is now A.prototype
assert( Object.getPrototypeOf( a ) === A.prototype );
// `a` doesn't have an `inc` property, but its prototype does:
a.inc();
// `a.value` will now equal 1
Take care to avoid confusing the prototype of an object with the prototype attribute.
By default A.prototype’s own prototype is a new empty object. Next we'll show how OOjs adds inheritance to the mix.
Classes (OOjs-style)
[edit]oo.js provides our class inheritance infrastructure.
OO.initClass() ensures that a class (ie, a constructor function) has an empty static attribute.
OO.inheritClass() adds a super attribute and creates new prototype and static attributes whose prototypes are set to the parent class's respective fields.
OO.initClass( A );
let B = function( init ) {
// Run A's constructor, which sets `value` to `init`.
B.super.call( this, init );
this.value += 100;
}
OO.inheritClass( B, A );
assert( B.super === A );
assert( Object.getPrototypeOf( B.prototype ) === A.prototype );
assert( Object.getPrototypeOf( B.static ) === A.static );
// Create a new object B { value: 100 } and increment.
// Note how the lookup works in the prototype chain:
// 1. b.inc is not found, so we try b's prototype
// 2. B.prototype.inc is also not found, so we try B.prototype's prototype
// 3. A.prototype.inc does exist and is invoked
b = new B( 0 );
b.inc();
assert( b.value === 101 );
OO.mixinClass() copies over the values from the prototype and static attributes of the mixed-in class, but only those defined directly on the mixed-in class.
You can find base classes with variations on:
git grep 'OO.initClass' | cut '-d ' -f 2
You can get a feel for the class hierarchy like so:
git grep -Eh '^OO.(mixin|inherit)Class\(' '*.js' \
| sed -e 's/OO.\([a-z]*\)Class( \([^,]*\), \([^ ]*\) .*/\2\t\1\t\3/' \
| sort | column -t
These are handy in the directories VisualEditor, VisualEditor/editcheck and VisualEditor/lib/ve.
Have a look in lib/ve/lib/oojs-ui/oojs-ui.core.js for some examples of OOjs classes.
super, this and call()
[edit]When calling a constructor function or invoking a method the value of this is implicitly set to the object. Javascript provides some helper methods which allow you to explicitly set this to something else: call() and apply(). When using call() you add the new value for this at the start of the argument list, whereas apply() instead takes the new value of this as its first argument and the remaining arguments to the original function are passed as an array.
When we subclass within OOjs we need to invoke the parent constructor before doing our own setup, but. We need to explicitly set the value of this to be the object under construction because the implicit value isn't what we want.
let C = function( init ) {
C.super.call( this, init );
}
OO.inheritClass( C, A );
let c = new C( 999 );
assert( c.value === 999 );
It’s possible to forget to use the call() method when creating new classes and to directly call the super() method instead. Doing so will run your code with this set to your constructor function instead of your new object - if your classes are misbehaving, check for this error.
function D( init ) {
// Mistake: This calls A with `this` set to `D`, not our new instance
D.super( init );
}
OO.inheritClass( D, A );
let d = new D( 999 );
assert( d.value === undefined ); // oops
assert( D.value === 999 ); // OOPS!
You may see idiomatic use of Function.prototype.call() in other codebases. For example, a NodeList is not an array, but we often want to use array's methods on it.
const nodes = document.querySelectorAll( 'a' );
// ES6 idioms
Array.from( nodes ).map( f );
[ ...nodes ].map( f );
// Pre-ES6 idioms
// We can find map on the constructor
Array.prototype.map.call( nodes, f );
// Or we can access map through a throwaway empty array
[].map.call( nodes, f );
Events
[edit]OO.EventEmitter provides our synchronous event infrastructure and can be found in the inheritance tree of many classes. You’ll find on(), off(), emit() and connect() used to handle events. We use synchronous events so that we can be sure of how they’ll be ordered and because we want to finish our work before any async code has the opportunity to change the state of the DOM from underneath us.
jQuery, Elements, Widgets and Dialogs
[edit]At the bottom of our stack we use jQuery to build elements. OO.ui.Element provides a thin wrapper around jQuery elements, and then OO.ui.Widget and OO.ui.Dialog add extra functionality above that. For example, an OO.ui.Widget is an Element with an EventEmitter mixed in, and some extra code to allow the component to be enabled and disabled.
A note on debugging
[edit]If your recent experience is with React you are probably out of the habit of using the browser’s built-in debugger. With Visual Editor it’s often the best way to understand what is happening. Use breakpoints liberally.
Edit Check Core Concepts
[edit]Introduction
[edit]Edit Checks are code which runs in the background while an editor is writing or editing a wiki. When the check finds some undesirable change, it prompts the user to make changes. This could be in the form of gentle guidance, or it could block the user from saving their changes at all until it’s resolved. When the check is about changes made by the editor, it’s referred to as an “edit check” and shown in a yellow dialog. When it’s about the parts of the document which existed before the user started their VisualEditor session, it’s an “edit suggestion” and is presented with a green dialog.
Lifecycle
[edit]An EditCheck is a class which inspects the state of the document and returns a list of EditCheckAction instances. These create or update the EditCheckActionWidget instances which tell the user about the check and offer actions.
Where checks live and how they’re loaded
[edit]Checks live in VisualEditor under editcheck/modules/editchecks in files named after their classes. For checks in the base editchecks directory, it is necessary to add the file to extension.json under the ext.visualEditor.editCheck key. Checks which are still in development can be found in editcheck/modules/editchecks/experimental and are automatically loaded when experimental mode is switched on.
How checks are enabled
[edit]Enabling by default
[edit]Edit checks are enabled by default by setting $wgVisualEditorEditCheck to true in LocalSettings.php or in the appropriate equivalent.
Manually enabling Edit Check and experimental mode
[edit]In editcheck/modules.init.js you will find the logic for enabling these features as a user. You can modify the URI of a page you’re editing:
- With
?ecenable=1to enable baseline edit checks - With
?ecenable=experimentalor?ecenable=2to additionally enable experimental checks - With
?ecenable=experimental,suggestionsto enable edit suggestions
You can also set window.MWVE_FORCE_EDIT_CHECK_ENABLED to the same values in Special:MyPage/common.js or Special:MyPage/global.js. This will override the settings in the URI unless you're explicitly switching off Edit Check using ?ecenable=0.
Check creation and registration
[edit]EditChecks are created by subclassing mw.editcheck.BaseEditCheck or an abstract subclass such as mw.editcheck.AsyncTextCheck and made available to VisualEditor with mw.editcheck.editCheckFactory.register.
How checks are invoked: Listeners
[edit]VisualEditor has several points where checks are invoked:
onBeforeSave– fires when the user attempts to publish their changesonDocumentChange– fires after the user makes changes to the document textonBranchNodeChange– fires after the user modifies a node, for example by inserting a picture, a table or another non-text element
These are usually triggered by user actions, but the controller also invokes them at other times to ensure that the state is up to date, such as after a user has interacted with a suggested action, and at the start of the edit session.
These methods can return an action, a list of actions, a list of promises which each return an action, or a promise which returns an array of actions.
onBeforeSave is used for checks which should appear after the user clicks "Publish changes..." whereas the other two are for checks which appear during an editing session.
Running the check
[edit]This is the part of the check provided by the implementer.
EditCheck objects are instantiated anew each time. The constructor gets a reference to the controller, its configuration, and an includeSuggestions. The appropriate listener is then invoked.
The listener is called with a surfaceModel. From this it must generate a list of potential actions which will be shown to the user, using any method the implementer pleases. It is responsible for narrowing down its results appropriately. This always includes removing dismissed actions from the result, and only returning actions for areas of the document which the user has actually modified.
Adding checks and suggestions to a page: Creating actions
[edit]Once a listener on EditCheck has its list of checks, they are returned to the controller as a list of mw.editcheck.EditCheckAction instances. Actions are created with a configuration object which should have, at a minimum, a link back to the check in the check field and a list of fragments which the controller uses to highlight the problem area and to place the EditCheckActionWidget next to it properly.
Action reconciliation
[edit]The controller gathers the outputs of each check and reconciles them with the previous run to generate a list of checks which no longer appear due to resolution or dismissal and a list of new checks. The remainder are mapped to their existing versions from the previous run in order to ensure that there is one canonical object for each action.
Presentation
[edit]An EditCheckActionWidget instance is created and rendered in the appropriate place in the page. These are ephemeral and their state should not be manipulated directly.
The widget is supplied with a set of choices to present to the user. These actions are either explicitly set when constructing the Action or set to a default value based on the EditCheck's static.choices attribute. The choices are rendered as buttons in the widget.
Action
[edit]When the user interacts with the widget, act() is called on the Check instance and supplied with the user's choice (the string in the action field) and the EditCheckAction instance. Here we provide the custom code for the check – replacing the user's input with our suggestion, deleting their selection, jumping to the text in question, opening a context dialog (such as a caption editor) or simply dismissing the action.
Messages and internationalisation
[edit]EditCheckAction
[edit]EditCheck classes
[edit]EditCheckDialog
[edit]EditCheckFactory
[edit]Writing an Edit Check
[edit]Where they go
[edit]- In VE
- In your
custom.js - As a gadget
- In your extension
