├── modules └── ve-markdown │ ├── ve.ce.MWMarkdownNode.css │ ├── ve.ui.MWMarkdownInspector.css │ ├── ve.ui.MWMarkdownDialog.css │ ├── ve.dm.MWBlockMarkdownNode.js │ ├── ve.dm.MWInlineMarkdownNode.js │ ├── ve.ce.MWBlockMarkdownNode.js │ ├── ve.ce.MWInlineMarkdownNode.js │ ├── ve.dm.MWMarkdownNode.js │ ├── ve.ce.MWMarkdownNode.js │ ├── ve.ui.MWMarkdownInspectorTool.js │ ├── ve.ui.MWMarkdownWindow.js │ ├── ve.ui.MWMarkdownDialogTool.js │ ├── ve.ui.MWMarkdownInspector.js │ └── ve.ui.MWMarkdownDialog.js ├── includes ├── MarkdownContentHandler.php ├── ResourceLoaderWikiMarkdownVisualEditorModule.php ├── MarkdownContent.php └── WikiMarkdown.php ├── i18n └── en.json ├── composer.json ├── LICENSE ├── extension.json └── README.md /modules/ve-markdown/ve.ce.MWMarkdownNode.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * VisualEditor ContentEditable MWMarkdownNode styles. 3 | */ 4 | 5 | .ve-ce-mwMarkdownNode pre { 6 | /* Prevent silly wrapping on Safari and Chrome (https://bugs.webkit.org/show_bug.cgi?id=35935) */ 7 | word-wrap: normal; 8 | } 9 | -------------------------------------------------------------------------------- /modules/ve-markdown/ve.ui.MWMarkdownInspector.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * VisualEditor UserInterface MWMarkdownInspector styles. 3 | */ 4 | 5 | .ve-ui-mwMarkdownInspector-content .ve-ui-mwExtensionWindow-input textarea { 6 | font-family: monospace, monospace; 7 | } 8 | 9 | .ve-ui-mwMarkdownInspector-content .oo-ui-numberInputWidget { 10 | width: 10em; 11 | } 12 | -------------------------------------------------------------------------------- /modules/ve-markdown/ve.ui.MWMarkdownDialog.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * VisualEditor UserInterface MWMarkdownDialog styles. 3 | */ 4 | 5 | .ve-ui-mwMarkdownDialog-content .ve-ui-mwExtensionWindow-input { 6 | max-width: none; 7 | } 8 | 9 | .ve-ui-mwMarkdownDialog-content .ve-ui-mwExtensionWindow-input textarea { 10 | font-family: monospace, monospace; 11 | } 12 | 13 | .ve-ui-mwMarkdownDialog-content .ve-ui-mwMarkdownWindow-languageField, 14 | .ve-ui-mwMarkdownDialog-content .ve-ui-mwMarkdownWindow-startLineField { 15 | max-width: 30em; 16 | } 17 | -------------------------------------------------------------------------------- /includes/MarkdownContentHandler.php: -------------------------------------------------------------------------------- 1 | <markdown> using [https://parsedown.org/ Parsedown - PHP Markdown Parser]", 8 | "markdown-error-category": "Pages with markdown errors", 9 | "markdown-error-category-desc": "There was an error when attempting to parse markdown included on the page.", 10 | "markdown-visualeditor-mwmarkdowninspector-code": "Code", 11 | "markdown-visualeditor-mwmarkdowninspector-title": "Markdown", 12 | "markdown-error-parse-failure": "Failed to parse markdown" 13 | } 14 | -------------------------------------------------------------------------------- /includes/ResourceLoaderWikiMarkdownVisualEditorModule.php: -------------------------------------------------------------------------------- 1 | setText( '' ); 32 | return $output; 33 | } 34 | $wikitext = Html::rawElement( 35 | 'markdown', 36 | [], 37 | // Line breaks are needed so that wikitext would be 38 | // appropriately isolated for correct parsing. 39 | "\n" . $this->getText() . "\n" 40 | ); 41 | $parser = MediaWikiServices::getInstance()->getParser(); 42 | $output = $parser->parse( $wikitext, $title, $options, true, true, $revId ); 43 | return $output; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /modules/ve-markdown/ve.ui.MWMarkdownWindow.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * VisualEditor UserInterface MWMarkdownWindow class. 3 | */ 4 | 5 | /** 6 | * MediaWiki markdown window. 7 | * 8 | * @class 9 | * @abstract 10 | * 11 | * @constructor 12 | */ 13 | ve.ui.MWMarkdownWindow = function VeUiMWMarkdownWindow() { 14 | }; 15 | 16 | /* Inheritance */ 17 | 18 | OO.initClass( ve.ui.MWMarkdownWindow ); 19 | 20 | /* Static properties */ 21 | 22 | ve.ui.MWMarkdownWindow.static.title = OO.ui.deferMsg( 'markdown-visualeditor-mwmarkdowninspector-title' ); 23 | 24 | ve.ui.MWMarkdownWindow.static.dir = 'ltr'; 25 | 26 | /* Methods */ 27 | 28 | /** 29 | * @inheritdoc 30 | */ 31 | ve.ui.MWMarkdownWindow.prototype.initialize = function () { 32 | this.codeField = new OO.ui.FieldLayout( this.input, { 33 | align: 'top', 34 | label: ve.msg( 'markdown-visualeditor-mwmarkdowninspector-code' ) 35 | } ); 36 | }; 37 | 38 | /** 39 | * @inheritdoc OO.ui.Window 40 | */ 41 | ve.ui.MWMarkdownWindow.prototype.getReadyProcess = function ( data, process ) { 42 | return process.next( function () { 43 | this.input.focus(); 44 | }, this ); 45 | }; 46 | 47 | /** 48 | * @inheritdoc OO.ui.Window 49 | */ 50 | ve.ui.MWMarkdownWindow.prototype.getSetupProcess = function ( data, process ) { 51 | return process; 52 | }; 53 | 54 | /** 55 | * @inheritdoc OO.ui.Window 56 | */ 57 | ve.ui.MWMarkdownWindow.prototype.getTeardownProcess = function ( data, process ) { 58 | return process; 59 | }; 60 | 61 | /** 62 | * @inheritdoc ve.ui.MWExtensionWindow 63 | */ 64 | ve.ui.MWMarkdownWindow.prototype.updateActions = function () { 65 | this.getActions().setAbilities( { done: this.isModified() } ); 66 | }; 67 | -------------------------------------------------------------------------------- /modules/ve-markdown/ve.ui.MWMarkdownDialogTool.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * VisualEditor UserInterface MWMarkdownDialogTool class. 3 | */ 4 | 5 | /** 6 | * MediaWiki UserInterface markdown tool. 7 | * 8 | * @class 9 | * @extends ve.ui.FragmentWindowTool 10 | * @constructor 11 | * @param {OO.ui.ToolGroup} toolGroup 12 | * @param {Object} [config] Configuration options 13 | */ 14 | ve.ui.MWMarkdownDialogTool = function VeUiMWMarkdownDialogTool() { 15 | ve.ui.MWMarkdownDialogTool.super.apply( this, arguments ); 16 | }; 17 | OO.inheritClass( ve.ui.MWMarkdownDialogTool, ve.ui.FragmentWindowTool ); 18 | ve.ui.MWMarkdownDialogTool.static.name = 'markdownDialog'; 19 | ve.ui.MWMarkdownDialogTool.static.group = 'object'; 20 | ve.ui.MWMarkdownDialogTool.static.icon = 'code'; 21 | ve.ui.MWMarkdownDialogTool.static.title = OO.ui.deferMsg( 22 | 'markdown-visualeditor-mwmarkdowninspector-title' ); 23 | ve.ui.MWMarkdownDialogTool.static.modelClasses = [ ve.dm.MWBlockMarkdownNode ]; 24 | ve.ui.MWMarkdownDialogTool.static.commandName = 'markdownDialog'; 25 | ve.ui.toolFactory.register( ve.ui.MWMarkdownDialogTool ); 26 | 27 | ve.ui.commandRegistry.register( 28 | new ve.ui.Command( 29 | 'markdownDialog', 'window', 'open', 30 | { args: [ 'markdownDialog' ], supportedSelections: [ 'linear' ] } 31 | ) 32 | ); 33 | 34 | ve.ui.sequenceRegistry.register( 35 | // Don't wait for the user to type out the full tag 36 | new ve.ui.Sequence( 'wikitextMark', 'markdownDialog', '= 1.34" 13 | }, 14 | "MessagesDirs": { 15 | "WikiMarkdown": [ 16 | "i18n" 17 | ] 18 | }, 19 | "AutoloadClasses": { 20 | "WikiMarkdown": "includes/WikiMarkdown.php", 21 | "ResourceLoaderWikiMarkdownVisualEditorModule": "includes/ResourceLoaderWikiMarkdownVisualEditorModule.php", 22 | "MarkdownContentHandler": "includes/MarkdownContentHandler.php", 23 | "MarkdownContent": "includes/MarkdownContent.php" 24 | }, 25 | "ResourceFileModulePaths": { 26 | "localBasePath": "modules", 27 | "remoteExtPath": "WikiMarkdown/modules" 28 | }, 29 | "Hooks": { 30 | "ParserFirstCallInit": "WikiMarkdown::onParserFirstCallInit", 31 | "ResourceLoaderRegisterModules": "WikiMarkdown::onResourceLoaderRegisterModules", 32 | "ContentHandlerDefaultModelFor": "WikiMarkdown::onContentHandlerDefaultModelFor", 33 | "CodeEditorGetPageLanguage": "WikiMarkdown::onCodeEditorGetPageLanguage" 34 | }, 35 | "attributes": { 36 | "VisualEditor": { 37 | "PluginModules": [ 38 | "ext.wikimarkdown.visualEditor" 39 | ] 40 | } 41 | }, 42 | "callback": "WikiMarkdown::onRegistration", 43 | "config": { 44 | "AllowMarkdownExtra": { 45 | "value": false 46 | }, 47 | "AllowMarkdownExtended": { 48 | "value": false 49 | }, 50 | "ParsedownExtendedParameters": { 51 | "value": { 52 | "math": { 53 | "single_dollar": true 54 | }, 55 | "sup": true, 56 | "sub": true 57 | } 58 | } 59 | }, 60 | "ContentHandlers": { 61 | "markdown": "MarkdownContentHandler" 62 | }, 63 | "TrackingCategories": [ 64 | "markdown-error-category" 65 | ], 66 | "manifest_version": 2 67 | } 68 | -------------------------------------------------------------------------------- /modules/ve-markdown/ve.ui.MWMarkdownInspector.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * VisualEditor UserInterface MWMarkdownInspector class. 3 | */ 4 | 5 | /** 6 | * MediaWiki markdown inspector. 7 | * 8 | * @class 9 | * @extends ve.ui.MWLiveExtensionInspector 10 | * @mixins ve.ui.MWMarkdownWindow 11 | * 12 | * @constructor 13 | * @param {Object} [config] Configuration options 14 | */ 15 | ve.ui.MWMarkdownInspector = function VeUiMWMarkdownInspector() { 16 | // Parent constructor 17 | ve.ui.MWMarkdownInspector.super.apply( this, arguments ); 18 | 19 | // Mixin constructor 20 | ve.ui.MWMarkdownWindow.call( this ); 21 | }; 22 | 23 | /* Inheritance */ 24 | 25 | OO.inheritClass( ve.ui.MWMarkdownInspector, ve.ui.MWLiveExtensionInspector ); 26 | 27 | OO.mixinClass( ve.ui.MWMarkdownInspector, ve.ui.MWMarkdownWindow ); 28 | 29 | /* Static properties */ 30 | 31 | ve.ui.MWMarkdownInspector.static.name = 'markdownInspector'; 32 | 33 | ve.ui.MWMarkdownInspector.static.modelClasses = [ ve.dm.MWInlineMarkdownNode ]; 34 | 35 | /* Methods */ 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | ve.ui.MWMarkdownInspector.prototype.initialize = function () { 41 | // Parent method 42 | ve.ui.MWMarkdownInspector.super.prototype.initialize.call( this ); 43 | 44 | // Mixin method 45 | ve.ui.MWMarkdownWindow.prototype.initialize.call( this ); 46 | 47 | // Initialization 48 | this.$content.addClass( 've-ui-mwMarkdownInspector-content' ); 49 | this.form.$element.prepend( 50 | this.codeField.$element 51 | ); 52 | }; 53 | 54 | /** 55 | * @inheritdoc 56 | */ 57 | ve.ui.MWMarkdownInspector.prototype.getReadyProcess = function ( data ) { 58 | // Parent process 59 | var process = ve.ui.MWMarkdownInspector.super.prototype.getReadyProcess.call( this, data ); 60 | // Mixin process 61 | return ve.ui.MWMarkdownWindow.prototype.getReadyProcess.call( this, data, process ); 62 | }; 63 | 64 | /** 65 | * @inheritdoc 66 | */ 67 | ve.ui.MWMarkdownInspector.prototype.getSetupProcess = function ( data ) { 68 | // Parent process 69 | var process = ve.ui.MWMarkdownInspector.super.prototype.getSetupProcess.call( this, data ); 70 | // Mixin process 71 | return ve.ui.MWMarkdownWindow.prototype.getSetupProcess.call( this, data, process ); 72 | }; 73 | 74 | /** 75 | * @inheritdoc 76 | */ 77 | ve.ui.MWMarkdownInspector.prototype.getTeardownProcess = function ( data ) { 78 | // Parent process 79 | var process = ve.ui.MWMarkdownInspector.super.prototype.getTeardownProcess.call( this, data ); 80 | // Mixin process 81 | return ve.ui.MWMarkdownWindow.prototype.getTeardownProcess.call( this, data, process ); 82 | }; 83 | 84 | /* Registration */ 85 | 86 | ve.ui.windowFactory.register( ve.ui.MWMarkdownInspector ); 87 | -------------------------------------------------------------------------------- /modules/ve-markdown/ve.ui.MWMarkdownDialog.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * VisualEditor UserInterface MWMarkdownDialog class. 3 | */ 4 | 5 | /** 6 | * MediaWiki markdown dialog. 7 | * 8 | * @class 9 | * @extends ve.ui.MWExtensionDialog 10 | * @mixins ve.ui.MWMarkdownWindow 11 | * 12 | * @constructor 13 | * @param {Object} [config] Configuration options 14 | */ 15 | ve.ui.MWMarkdownDialog = function VeUiMWMarkdownDialog() { 16 | // Parent constructor 17 | ve.ui.MWMarkdownDialog.super.apply( this, arguments ); 18 | 19 | // Mixin constructor 20 | ve.ui.MWMarkdownWindow.call( this ); 21 | }; 22 | 23 | /* Inheritance */ 24 | 25 | OO.inheritClass( ve.ui.MWMarkdownDialog, ve.ui.MWExtensionDialog ); 26 | 27 | OO.mixinClass( ve.ui.MWMarkdownDialog, ve.ui.MWMarkdownWindow ); 28 | 29 | /* Static properties */ 30 | 31 | ve.ui.MWMarkdownDialog.static.name = 'markdownDialog'; 32 | 33 | ve.ui.MWMarkdownDialog.static.size = 'larger'; 34 | 35 | ve.ui.MWMarkdownDialog.static.modelClasses = [ ve.dm.MWBlockMarkdownNode ]; 36 | 37 | /* Methods */ 38 | 39 | /** 40 | * @inheritdoc 41 | */ 42 | ve.ui.MWMarkdownDialog.prototype.initialize = function () { 43 | // Parent method 44 | ve.ui.MWMarkdownDialog.super.prototype.initialize.call( this ); 45 | 46 | this.input = new ve.ui.MWAceEditorWidget( { 47 | limit: 1, 48 | rows: 10, 49 | maxRows: 25, 50 | autosize: true, 51 | autocomplete: 'live', 52 | classes: [ 've-ui-mwExtensionWindow-input' ] 53 | } ); 54 | 55 | this.input.connect( this, { resize: 'updateSize' } ); 56 | 57 | // Mixin method 58 | ve.ui.MWMarkdownWindow.prototype.initialize.call( this ); 59 | 60 | this.contentLayout = new OO.ui.PanelLayout( { 61 | scrollable: true, 62 | padded: true, 63 | expanded: false, 64 | content: [ 65 | this.codeField 66 | ] 67 | } ); 68 | 69 | // Initialization 70 | this.$content.addClass( 've-ui-mwMarkdownDialog-content' ); 71 | this.$body.append( this.contentLayout.$element ); 72 | }; 73 | 74 | /** 75 | * @inheritdoc 76 | */ 77 | ve.ui.MWMarkdownDialog.prototype.getReadyProcess = function ( data ) { 78 | // Parent process 79 | var process = ve.ui.MWMarkdownDialog.super.prototype.getReadyProcess.call( this, data ); 80 | // Mixin process 81 | return ve.ui.MWMarkdownWindow.prototype.getReadyProcess.call( this, data, process ); 82 | }; 83 | 84 | /** 85 | * @inheritdoc 86 | */ 87 | ve.ui.MWMarkdownDialog.prototype.getSetupProcess = function ( data ) { 88 | // Parent process 89 | var process = ve.ui.MWMarkdownDialog.super.prototype.getSetupProcess.call( this, data ); 90 | // Mixin process 91 | return ve.ui.MWMarkdownWindow.prototype.getSetupProcess.call( this, data, process ) 92 | .first( function () { 93 | this.input.setup(); 94 | this.input.setLanguage('markdown'); 95 | }, this ) 96 | .next( function () { 97 | this.input.clearUndoStack(); 98 | }, this ); 99 | }; 100 | 101 | /** 102 | * @inheritdoc 103 | */ 104 | ve.ui.MWMarkdownDialog.prototype.getTeardownProcess = function ( data ) { 105 | // Parent process 106 | var process = ve.ui.MWMarkdownDialog.super.prototype.getTeardownProcess.call( this, data ); 107 | // Mixin process 108 | return ve.ui.MWMarkdownWindow.prototype.getTeardownProcess.call( this, data, process ).first( function () { 109 | this.input.teardown(); 110 | }, this ); 111 | }; 112 | 113 | /* Registration */ 114 | 115 | ve.ui.windowFactory.register( ve.ui.MWMarkdownDialog ); 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This is a [MediaWiki](https://www.mediawiki.org/) extension that allows for markdown syntax to be used on wiki pages. More information can be found on the [extension page](https://www.mediawiki.org/wiki/Extension:WikiMarkdown). 4 | 5 | # Requirements 6 | 7 | This version of the extension has been tested with Parsedown 1.7.4, Parsedown Extra 0.8.1, and MediaWiki 1.35. 8 | 9 | # Installation 10 | 11 | Add this line to your LocalSettings.php: 12 | 13 | ```php 14 | wfLoadExtension( 'WikiMarkdown' ); 15 | ``` 16 | 17 | This extension requires [Parsedown](https://github.com/erusev/parsedown) to be installed and optionally [Parsedown Extra](https://github.com/erusev/parsedown-extra) and [Parsedown Extended](https://github.com/BenjaminHoegh/ParsedownExtended). Either install them manually, or use Composer by adding the line `"extensions/WikiMarkdown/composer.json"` to the "composer.local.json" file in the root directory of your wiki, e.g. 18 | ```json 19 | { 20 | "extra": { 21 | "merge-plugin": { 22 | "include": [ 23 | "extensions/WikiMarkdown/composer.json" 24 | ] 25 | } 26 | } 27 | } 28 | ``` 29 | Then run `composer update` in the root directory of your wiki. 30 | 31 | # Usage 32 | 33 | On wiki pages, you can now use `` elements: 34 | 35 | ```html 36 | 37 | ## Emphasis 38 | 39 | **This is bold text** 40 | 41 | __This is bold text__ 42 | 43 | *This is italic text* 44 | 45 | _This is italic text_ 46 | 47 | ~~Strikethrough~~ 48 | 49 | ``` 50 | 51 | # Parameters 52 | 53 | * `inline`: Indicates that inline-style markdown should be used instead of block-style. 54 | 55 | # Configuration 56 | 57 | * `$wgAllowMarkdownExtra` (optional): Set to `true` in order to specify that [Parsedown Extra](https://github.com/erusev/parsedown-extra) should be used. 58 | * `$wgAllowMarkdownExtended` (optional): Set to `true` in order to specify that [Parsedown Extended](https://github.com/BenjaminHoegh/ParsedownExtended) should be used. 59 | * `$wgParsedownExtendedParameters` (optional): Allows for specifying the options that are passed to Parsedown Extended. See the [documentation](https://benjaminhoegh.github.io/ParsedownExtended/) for which options you want to enable or disable. 60 | 61 | # Other Features 62 | * This extension also functions as a content handler for wiki pages ending in `.md`. For these pages, the entire page will be interpreted as markdown and markdown syntax highlighting will be used in the editor if you have the [CodeEditor](https://www.mediawiki.org/wiki/Extension:CodeEditor) extension installed. 63 | * When specifying code blocks in markdown, this extension will automatically apply the [SyntaxHighlight](https://www.mediawiki.org/wiki/Extension:SyntaxHighlight) extension if it is installed. All languages supported by the SyntaxHighlight extension will work. 64 | * When using Parsedown Extended, this extension will automatically apply the [Math](https://www.mediawiki.org/wiki/Extension:Math) extension if it is installed to any math blocks by default. This can be turned off by either not using Parsedown Extended, or modifying `$wgParsedownExtendedParameters`. The following syntaxes are valid: `\[ ... \]`/`$$ ... $$` (for block mode) and `\( ... \)`/`$ ... $` (for inline mode) 65 | * When using the [VisualEditor](https://www.mediawiki.org/wiki/Extension:VisualEditor) extension with the CodeEditor extension, markdown blocks will be editable using a markdown editor. 66 | 67 | # Credits 68 | 69 | * This extension was inspired by the [Markdown Content Handler](https://github.com/brightbyte/MWExtension-Markdown) by Daniel Kinzler and the [Markdown Extension](https://www.mediawiki.org/wiki/Extension:Markdown) by Blake Harley. 70 | * Parts of this code are based on the [SyntaxHighlight](https://www.mediawiki.org/wiki/Extension:SyntaxHighlight) and [Scribunto](https://www.mediawiki.org/wiki/Extension:Scribunto) extensions. 71 | -------------------------------------------------------------------------------- /includes/WikiMarkdown.php: -------------------------------------------------------------------------------- 1 | setHook( 'markdown', [ 'WikiMarkdown', 'parserHook' ] ); 26 | } 27 | 28 | /** 29 | * Parser hook for logic 30 | * 31 | * @param string $text 32 | * @param array $args 33 | * @param Parser $parser 34 | * @return string 35 | * @throws MWException 36 | */ 37 | public static function parserHook( $text, $args, $parser ) { 38 | global $wgAllowMarkdownExtended; 39 | 40 | // Replace strip markers (For e.g. {{#tag:markdown|...}}) 41 | $out = $parser->getStripState()->unstripNoWiki( $text ); 42 | 43 | // Don't trim leading spaces away, just the linefeeds 44 | $out = preg_replace( '/^\n+/', '', rtrim( $out ) ); 45 | 46 | $result = self::parseMarkdown( $out, $args ); 47 | if ( !$result->isGood() ) { 48 | $parser->addTrackingCategory( 'markdown-error-category' ); 49 | } 50 | $out = $result->getValue(); 51 | 52 | // Make it so that tables have borders 53 | $out = str_replace('', '
', $out); 54 | 55 | // Format links 56 | $out = preg_replace_callback( 57 | '/(.*)<\/a>/isU', 58 | function ($matches) use (&$parser) { 59 | $url = html_entity_decode($matches[1]); 60 | $text = html_entity_decode($matches[2]); 61 | $linkType = $url == $text ? 'free' : 'text'; 62 | $cleanUrl = Sanitizer::cleanUrl($url); 63 | // Register link in the output object 64 | $parser->getOutput()->addExternalLink($url); 65 | // Create an external link 66 | return Linker::makeExternalLink($cleanUrl, $text, true, $linkType, $parser->getExternalLinkAttribs($url), $parser->getTitle()); 67 | }, 68 | $out 69 | ); 70 | 71 | // Make html headings into wiki headlines 72 | $refers = []; 73 | $out = preg_replace_callback( 74 | '/(.*)<\/h\1>/isU', 75 | function ($matches) use (&$refers) { 76 | // Create an anchor id from the heading text or id (if found) 77 | $anchor = 'markdown_' . (empty($matches[2]) ? Sanitizer::escapeIdForAttribute($matches[4]) : html_entity_decode($matches[3])); 78 | // Ensure that anchors are unique 79 | if ( isset( $refers[$anchor] ) ) { 80 | for ( $i = 2; isset( $refers["{$anchor}_$i"] ); ++$i ); 81 | $anchor .= "_$i"; 82 | $refers["{$anchor}_$i"] = true; 83 | } else { 84 | $refers[$anchor] = true; 85 | } 86 | return Linker::makeHeadline($matches[1], '>', $anchor, $matches[4], ''); 87 | }, 88 | $out 89 | ); 90 | 91 | // If SyntaxHighlight is loaded, then use it to perform syntax highlighting 92 | if ( ExtensionRegistry::getInstance()->isLoaded( 'SyntaxHighlight' ) ) { 93 | $out = preg_replace_callback( 94 | '/
\s*(.*)<\/code>\s*<\/pre>/isU',
 95 | 				function ( $matches ) use ( &$parser ) {
 96 | 					// If there's no language, just remove the nested  tag
 97 | 					if ( empty( $matches[1] ) ) {
 98 | 						return '
' . $matches[3] . '
'; 99 | } 100 | // If a language is specified, let SyntaxHighlight handle it 101 | $args = array('lang' => $matches[2]); 102 | return SyntaxHighlight::parserHook( html_entity_decode( $matches[3] ), $args, $parser ); 103 | }, 104 | $out 105 | ); 106 | } 107 | 108 | // If Parsedown Extended is available with tasks turned on, then convert them to OOUI checkboxes 109 | if ( $wgAllowMarkdownExtended && ( false !== self::getParsedown()->options['lists']['tasks'] ?? true ) ) { 110 | $parser->enableOOUI(); 111 | $out = preg_replace_callback( 112 | '//isU', 113 | function ( $matches ) { 114 | $check = new \OOUI\CheckboxInputWidget([ 115 | 'disabled' => true, 116 | 'selected' => in_array('checked', explode(' ', $matches[1]), true) 117 | ]); 118 | return $check->toString(); 119 | }, 120 | $out 121 | ); 122 | } 123 | 124 | // If Parsedown Extended is available with math turned on and the Math extension is loaded, then use it to perform math formatting 125 | if ( $wgAllowMarkdownExtended && ( false !== self::getParsedown()->options['math'] ?? false ) && ExtensionRegistry::getInstance()->isLoaded( 'Math' ) ) { 126 | $out = preg_replace_callback( 127 | '/(? 'block'); 130 | return MediaWiki\Extension\Math\Hooks::mathTagHook( html_entity_decode( $matches[1] ), $args, $parser ); 131 | }, 132 | $out 133 | ); 134 | $out = preg_replace_callback( 135 | '/(? 'block'); 138 | return MediaWiki\Extension\Math\Hooks::mathTagHook( html_entity_decode( $matches[1] ), $args, $parser ); 139 | }, 140 | $out 141 | ); 142 | $out = preg_replace_callback( 143 | '/(? 'inline'); 146 | return MediaWiki\Extension\Math\Hooks::mathTagHook( html_entity_decode( $matches[1] ), $args, $parser ); 147 | }, 148 | $out 149 | ); 150 | if ( self::getParsedown()->options['math']['single_dollar'] ?? false ) { 151 | $out = preg_replace_callback( 152 | '/(? 'inline'); 155 | return MediaWiki\Extension\Math\Hooks::mathTagHook( html_entity_decode( $matches[1] ), $args, $parser ); 156 | }, 157 | $out 158 | ); 159 | } 160 | } 161 | 162 | // Allow certain HTML attributes 163 | $htmlAttribs = Sanitizer::validateAttributes( 164 | $args, array_flip( [ 'style', 'class', 'id', 'dir' ] ) 165 | ); 166 | if ( !isset( $htmlAttribs['class'] ) ) { 167 | $htmlAttribs['class'] = self::MARKDOWN_CSS_CLASS; 168 | } else { 169 | $htmlAttribs['class'] .= ' ' . self::MARKDOWN_CSS_CLASS; 170 | } 171 | if ( isset( $args['inline'] ) ) { 172 | // Enforce inlineness. Stray newlines may result in unexpected list and paragraph processing 173 | // (also known as doBlockLevels()). 174 | $out = str_replace( "\n", ' ', $out ); 175 | $out = Html::rawElement( 'span', $htmlAttribs, $out ); 176 | 177 | } else { 178 | // Use 'nowiki' strip marker to prevent list processing (also known as doBlockLevels()). 179 | // However, leave the wrapping
outside to prevent

-wrapping. 180 | $marker = $parser::MARKER_PREFIX . '-markdowninner-' . 181 | sprintf( '%08X', $parser->mMarkerIndex++ ) . $parser::MARKER_SUFFIX; 182 | $parser->getStripState()->addNoWiki( $marker, $out ); 183 | $out = $marker; 184 | 185 | $out = Html::openElement( 'div', $htmlAttribs ) . 186 | $out . 187 | Html::closeElement( 'div' ); 188 | } 189 | 190 | return $out; 191 | } 192 | 193 | /** 194 | * Parse markdown syntax. 195 | * 196 | * This produces raw HTML (wrapped by Status). 197 | * 198 | * @param string $markdown Markdown syntax to parse. 199 | * @param array $args Associative array of additional arguments. 200 | * If it contains a 'inline' key, the output will not be wrapped in `

`. 201 | * @return Status Status object, with HTML representing the parsed 202 | * markdown as its value. 203 | */ 204 | public static function parseMarkdown( $markdown, $args = [] ) { 205 | $status = new Status; 206 | 207 | // For empty tag, output nothing instead of empty
. 208 | if ( $markdown === '' ) { 209 | $status->value = ''; 210 | return $status; 211 | } 212 | 213 | $inline = isset( $args['inline'] ); 214 | 215 | $parsedown = static::getParsedown(); 216 | $parsedown->setSafeMode(true); 217 | 218 | if ( $inline ) { 219 | $markdown = trim( $markdown ); 220 | $output = $parsedown->line( $markdown ); 221 | $output = trim( $output ); 222 | } else { 223 | $output = $parsedown->text( $markdown ); 224 | } 225 | 226 | if ( $output === null ) { 227 | $status->warning( 'markdown-error-parse-failure' ); 228 | wfWarn( 'Parsing markdown returned blank output' ); 229 | } 230 | 231 | $status->value = $output; 232 | return $status; 233 | } 234 | 235 | /** 236 | * Conditionally register resource loader modules that depends on the 237 | * VisualEditor MediaWiki extension. 238 | * 239 | * @param ResourceLoader $resourceLoader 240 | */ 241 | public static function onResourceLoaderRegisterModules( $resourceLoader ) { 242 | if ( !ExtensionRegistry::getInstance()->isLoaded( 'VisualEditor' ) ) { 243 | return; 244 | } 245 | 246 | $resourceLoader->register( 'ext.wikimarkdown.visualEditor', [ 247 | 'class' => ResourceLoaderWikiMarkdownVisualEditorModule::class, 248 | 'localBasePath' => __DIR__ . '/../modules', 249 | 'remoteExtPath' => 'WikiMarkdown/modules', 250 | 'scripts' => [ 251 | 've-markdown/ve.dm.MWMarkdownNode.js', 252 | 've-markdown/ve.dm.MWBlockMarkdownNode.js', 253 | 've-markdown/ve.dm.MWInlineMarkdownNode.js', 254 | 've-markdown/ve.ce.MWMarkdownNode.js', 255 | 've-markdown/ve.ce.MWBlockMarkdownNode.js', 256 | 've-markdown/ve.ce.MWInlineMarkdownNode.js', 257 | 've-markdown/ve.ui.MWMarkdownWindow.js', 258 | 've-markdown/ve.ui.MWMarkdownDialog.js', 259 | 've-markdown/ve.ui.MWMarkdownDialogTool.js', 260 | 've-markdown/ve.ui.MWMarkdownInspector.js', 261 | 've-markdown/ve.ui.MWMarkdownInspectorTool.js', 262 | ], 263 | 'styles' => [ 264 | 've-markdown/ve.ce.MWMarkdownNode.css', 265 | 've-markdown/ve.ui.MWMarkdownDialog.css', 266 | 've-markdown/ve.ui.MWMarkdownInspector.css', 267 | ], 268 | 'dependencies' => [ 269 | 'ext.visualEditor.mwcore', 270 | 'oojs-ui.styles.icons-editing-advanced' 271 | ], 272 | 'messages' => [ 273 | 'markdown-visualeditor-mwmarkdowninspector-code', 274 | 'markdown-visualeditor-mwmarkdowninspector-title', 275 | ], 276 | 'targets' => [ 'desktop', 'mobile' ], 277 | ] ); 278 | } 279 | 280 | /** 281 | * Content Handler hook for markdown content pages 282 | * 283 | * @param Title $title Title in question 284 | * @param string &$model Model name. Use with CONTENT_MODEL_XXX constants. 285 | * @return bool|void True or no return value to continue or false to abort 286 | */ 287 | public static function onContentHandlerDefaultModelFor( Title $title, &$model ) { 288 | // Match .md pages. 289 | if ( preg_match( '/\.md$/i', $title->getText() ) && $title->isContentPage() ) { 290 | $model = CONTENT_MODEL_MARKDOWN; 291 | return false; 292 | } 293 | 294 | return true; 295 | } 296 | 297 | /** 298 | * @param Title $title 299 | * @param string &$languageCode 300 | * @return bool 301 | */ 302 | public static function onCodeEditorGetPageLanguage( Title $title, &$languageCode ) { 303 | if ( !ExtensionRegistry::getInstance()->isLoaded( 'CodeEditor' ) ) { 304 | return true; 305 | } 306 | if ( $title->hasContentModel( CONTENT_MODEL_MARKDOWN ) ) 307 | { 308 | $languageCode = 'markdown'; 309 | return false; 310 | } 311 | 312 | return true; 313 | } 314 | 315 | /** 316 | * @return Parsedown 317 | */ 318 | protected static function getParsedown() 319 | { 320 | static $parsedown; 321 | global $wgAllowMarkdownExtra; 322 | global $wgAllowMarkdownExtended; 323 | global $wgParsedownExtendedParameters; 324 | 325 | if (!$parsedown) { 326 | $parsedown = $wgAllowMarkdownExtended 327 | ? new \ParsedownExtended($wgParsedownExtendedParameters) 328 | : ($wgAllowMarkdownExtra 329 | ? new \ParsedownExtra() 330 | : new \Parsedown()); 331 | } 332 | 333 | return $parsedown; 334 | } 335 | } 336 | --------------------------------------------------------------------------------