MediaWiki:Common.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*!
* openjournal -- Markdown shortcuts for the visual editor.
*
* Loaded into MediaWiki:Common.js by the seed step. Edit it on-wiki at
* /wiki/MediaWiki:Common.js, or here and re-seed.
*
* MediaWiki stores wikitext, not Markdown, and that does not change here.
* What changes is what happens as you type: the visual editor already turns
* "* " into a bullet and "1. " into a numbered list, so the rest of a writer's
* Markdown muscle memory is registered the same way. You type Markdown, the
* editor formats it immediately, and the page is still wikitext underneath --
* which keeps diffs, templates and the API working exactly as before.
*
* These use ve.ui.sequenceRegistry, VisualEditor's own public mechanism for
* this, and follow the same pattern as its built-in registrations.
*/
( function () {
'use strict';
// Names of the commands the inline shortcuts run, and whether registration
// has already happened. Both hooks below are safe to run more than once.
var inlineCommandNames = [],
registered = false;
/**
* A sequence that fires at the start of a paragraph, e.g. "## " or "> ".
*
* @param {string} name Registry name; reusing an existing name replaces it
* @param {string} command VisualEditor command to run
* @param {string} prefix Characters typed before the space
* @param {boolean} space Whether a trailing space is part of the trigger
* @return {ve.ui.Sequence}
*/
function paragraphPrefix( name, command, prefix, space ) {
var data = [ { type: 'paragraph' } ].concat( prefix.split( '' ) );
if ( space ) {
data.push( ' ' );
}
// Strip everything the user typed; the paragraph element is not typed.
return new ve.ui.Sequence( name, command, data, data.length - 1 );
}
function registerMarkdownSequences() {
var registry = ve.ui.sequenceRegistry,
level;
for ( level = 1; level <= 6; level++ ) {
registry.register( paragraphPrefix(
// MediaWiki registers "# " as a wikitext numbered list under the
// name "numberHash". Reusing that name replaces it, so a lone
// "# " means a heading here, as a Markdown writer expects.
// "1. " still makes a numbered list, which is the Markdown way.
level === 1 ? 'numberHash' : 'openjournalHeading' + level,
'heading' + level,
new Array( level + 1 ).join( '#' ),
true
) );
}
// "- " for bullets. VisualEditor ships "* " already; Markdown uses both.
registry.register(
paragraphPrefix( 'openjournalBulletDash', 'bulletWrapOnce', '-', true )
);
// "> " for a blockquote. MediaWiki maps ": " to this; Markdown uses ">".
registry.register(
paragraphPrefix( 'openjournalBlockquote', 'blockquoteWrap', '>', true )
);
// "```" opens a code block.
registry.register(
paragraphPrefix( 'openjournalCodeBlock', 'preformatted', '```', false )
);
// "---" is a horizontal rule. VisualEditor wants four dashes; Markdown
// takes three, and three fires first, so this supersedes it.
registry.register(
paragraphPrefix( 'openjournalHorizontalRule', 'insertHorizontalRule', '---', false )
);
}
/**
* Inline Markdown: **bold**, *italic*, `code`.
*
* Block shortcuts above are plain sequences, because the characters that
* trigger them sit at the start of a paragraph and VisualEditor's `strip`
* removes them. Inline Markdown wraps text instead, and `strip` only takes
* characters off the right-hand end -- so the opening delimiter would be
* left behind. These use a regular expression to match the whole
* "**text**", select it, and hand it to the action below, which swaps in
* the text without its delimiters and styles it.
*/
function registerInlineAction() {
ve.ui.OpenJournalMarkdownAction = function VeUiOpenJournalMarkdownAction() {
ve.ui.OpenJournalMarkdownAction.super.apply( this, arguments );
};
OO.inheritClass( ve.ui.OpenJournalMarkdownAction, ve.ui.Action );
ve.ui.OpenJournalMarkdownAction.static.name = 'openjournalMarkdown';
ve.ui.OpenJournalMarkdownAction.static.methods = [ 'wrap' ];
/**
* @param {string} delimiter The Markdown characters, e.g. "**"
* @param {string} annotation VisualEditor annotation, e.g. "textStyle/bold"
* @return {boolean} Whether the text was styled
*/
ve.ui.OpenJournalMarkdownAction.prototype.wrap = function ( delimiter, annotation ) {
var surfaceModel = this.surface.getModel(),
fragment = surfaceModel.getFragment(),
text = fragment.getText(),
width = delimiter.length;
// Returning false makes VisualEditor undo the selection change, so
// anything that does not really look like "**text**" is left alone.
if (
text.length <= width * 2 ||
text.slice( 0, width ) !== delimiter ||
text.slice( text.length - width ) !== delimiter
) {
return false;
}
fragment
.insertContent( text.slice( width, text.length - width ), false )
.annotateContent( 'set', annotation )
.collapseToEnd()
.select();
// Without this the annotation stays armed and the next word typed
// comes out bold too.
surfaceModel.setInsertionAnnotations( null );
return true;
};
ve.ui.actionFactory.register( ve.ui.OpenJournalMarkdownAction );
}
function registerInlineSequences() {
[
// label, delimiter, annotation, pattern
[ 'Bold', '**', 'textStyle/bold', '\\*\\*[^*]+\\*\\*$' ],
// "*text*" must not fire midway through typing "**text**", so the
// opening star may not itself be preceded by one. Lookbehind is
// unsupported on some older browsers, hence the per-pattern guard.
[ 'Italic', '*', 'textStyle/italic', '(?<!\\*)\\*[^*]+\\*$' ],
[ 'Code', '`', 'textStyle/code', '`[^`]+`$' ],
// Markdown's other pair of emphasis characters. These refuse an
// opening underscore that follows a word character, which is what
// stops snake_case_names turning italic halfway through -- the
// same rule CommonMark applies, and the reason "_" needs a guard
// that "*" does not. \w covers "_" itself, so it doubles as the
// "not midway through __bold__" check.
[ 'BoldUnderscore', '__', 'textStyle/bold', '(?<!\\w)__[^_]+__$' ],
[ 'ItalicUnderscore', '_', 'textStyle/italic', '(?<!\\w)_[^_]+_$' ],
[ 'Strikethrough', '~~', 'textStyle/strikethrough', '~~[^~]+~~$' ]
].forEach( function ( spec ) {
var name = 'openjournalMarkdown' + spec[ 0 ],
pattern;
inlineCommandNames.push( name );
try {
pattern = new RegExp( spec[ 3 ] );
} catch ( e ) {
mw.log.warn( 'openjournal: skipping ' + name + ' shortcut', e );
return;
}
ve.ui.commandRegistry.register( new ve.ui.Command(
name, 'openjournalMarkdown', 'wrap',
{ args: [ spec[ 1 ], spec[ 2 ] ], supportedSelections: [ 'linear' ] }
) );
ve.ui.sequenceRegistry.register( new ve.ui.Sequence(
name, name, pattern, 0, { setSelection: true }
) );
} );
}
// Register through VisualEditor's plugin hook, which runs after its modules
// load but before the editing surface is built. That timing matters: a
// surface copies the command registry once, at construction, and
// ve.ui.Sequence refuses to run a command the surface does not list. A
// custom command registered any later is silently ignored -- the sequence
// matches, nothing happens. Sequences themselves are read live on every
// keystroke, which is why the block shortcuts survived being registered
// late and the inline ones did not.
function registerEverything() {
if ( registered ) {
return;
}
registered = true;
registerInlineAction();
registerInlineSequences();
registerMarkdownSequences();
}
/**
* Top up the live surface's command list.
*
* The plugin hook below runs early enough that this should be redundant.
* It is here because the failure it guards against is silent: if anything
* builds a surface without the plugin hook having run, every inline
* shortcut matches and then quietly does nothing, which is a miserable
* thing to debug from the outside.
*/
function backfillSurfaceCommands() {
var surface = ve.init && ve.init.target &&
ve.init.target.getSurface && ve.init.target.getSurface();
if ( !surface ) {
return;
}
var commands = surface.getCommands();
inlineCommandNames.forEach( function ( name ) {
if ( commands.indexOf( name ) === -1 ) {
commands.push( name );
}
} );
}
function guard( fn ) {
return function () {
try {
fn.apply( null, arguments );
} catch ( e ) {
// A broken shortcut must never take the editor down with it.
mw.log.error( 'openjournal: Markdown shortcuts failed', e );
}
};
}
mw.hook( 've.loadModules' ).add( guard( function ( addPlugin ) {
addPlugin( guard( registerEverything ) );
} ) );
// Belt and braces, once the editor is actually up.
mw.hook( 've.activationComplete' ).add( guard( function () {
registerEverything();
backfillSurfaceCommands();
} ) );
}() );