<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://openjournal.wiki/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3ACommon.js</id>
	<title>MediaWiki:Common.js - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://openjournal.wiki/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3ACommon.js"/>
	<link rel="alternate" type="text/html" href="https://openjournal.wiki/index.php?title=MediaWiki:Common.js&amp;action=history"/>
	<updated>2026-09-11T00:29:23Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.9</generator>
	<entry>
		<id>https://openjournal.wiki/index.php?title=MediaWiki:Common.js&amp;diff=24&amp;oldid=prev</id>
		<title>OpenJournalAdmin: openjournal markdown shortcuts</title>
		<link rel="alternate" type="text/html" href="https://openjournal.wiki/index.php?title=MediaWiki:Common.js&amp;diff=24&amp;oldid=prev"/>
		<updated>2026-09-10T19:58:31Z</updated>

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