mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/CodeMirror
synced 2024-11-23 13:56:44 +00:00
CM6: Add jsdoc build step, fix JSDoc annotations, and add @stable tags
There is a known bug with JSDoc and using `export default`. These must be separate statements for JSDoc to parse properly. See https://github.com/jsdoc/jsdoc/issues/1132 Update README; change log now lives on the wiki. Bug: T359986 Depends-On: I58a0766e35eddaf7bebe2c080757bb09963d8555 Change-Id: Ibc2212ef9eab512511b13a99ecc2ccbda8c52ece
This commit is contained in:
parent
ca02360228
commit
d652f3d2a2
|
@ -1,2 +1,3 @@
|
||||||
/resources/lib/
|
/resources/lib/
|
||||||
/vendor
|
/vendor
|
||||||
|
/docs
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,3 +9,4 @@
|
||||||
/composer.lock
|
/composer.lock
|
||||||
.eslintcache
|
.eslintcache
|
||||||
.env
|
.env
|
||||||
|
docs/
|
||||||
|
|
36
README.md
36
README.md
|
@ -1,6 +1,4 @@
|
||||||
# mediawiki/extensions/CodeMirror
|
CodeMirror 6 homepage: [https://www.mediawiki.org/wiki/Extension:CodeMirror/6](https://www.mediawiki.org/wiki/Extension:CodeMirror/6)
|
||||||
|
|
||||||
Homepage: https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:CodeMirror
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -24,6 +22,7 @@ _NOTE: Consider using [Fresh](https://gerrit.wikimedia.org/g/fresh/) to run thes
|
||||||
You'll want to keep this running in a separate terminal during development.
|
You'll want to keep this running in a separate terminal during development.
|
||||||
* `npm run build` to compile the production assets. You *must* run this step before
|
* `npm run build` to compile the production assets. You *must* run this step before
|
||||||
sending the patch or CI will fail (so that sources and built assets are in sync).
|
sending the patch or CI will fail (so that sources and built assets are in sync).
|
||||||
|
* `npm run doc` to generate the API documentation.
|
||||||
* `npm test` to run the linting tools, JavaScript unit tests, and build checks.
|
* `npm test` to run the linting tools, JavaScript unit tests, and build checks.
|
||||||
* `npm run test:lint` for linting of JS/LESS/CSS.
|
* `npm run test:lint` for linting of JS/LESS/CSS.
|
||||||
* `npm run test:lint:js` for linting of just JavaScript.
|
* `npm run test:lint:js` for linting of just JavaScript.
|
||||||
|
@ -31,34 +30,9 @@ _NOTE: Consider using [Fresh](https://gerrit.wikimedia.org/g/fresh/) to run thes
|
||||||
* `npm run test:i18n` for linting of i18n messages with banana-checker.
|
* `npm run test:i18n` for linting of i18n messages with banana-checker.
|
||||||
* `npm run test:unit` for the new Jest unit tests.
|
* `npm run test:unit` for the new Jest unit tests.
|
||||||
* `npm run selenium-test` for the Selenium tests.
|
* `npm run selenium-test` for the Selenium tests.
|
||||||
|
* Older QUnit tests are in `resources/mode/mediawiki/tests/qunit/`. These have been
|
||||||
Older QUnit tests are in `resources/mode/mediawiki/tests/qunit/`. These will
|
replaced and will be removed after the CodeMirror 6 upgrade.
|
||||||
eventually be moved over to `tests/qunit` and rewritten for CodeMirror 6.
|
|
||||||
|
|
||||||
## CodeMirror 6 change log
|
## CodeMirror 6 change log
|
||||||
|
|
||||||
This is a list of changes that either come by default with the CodeMirror 6 upgrade,
|
* See [Extension:CodeMirror/6](https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:CodeMirror/6#Differences_from_CodeMirror_5)
|
||||||
or changes of our that we deem as reasonable improvements.
|
|
||||||
Some may be removed pending user feedback:
|
|
||||||
|
|
||||||
### Upstream changes
|
|
||||||
|
|
||||||
* Bracket matching now highlights unmatched brackets in red
|
|
||||||
|
|
||||||
### New MediaWiki mode features
|
|
||||||
|
|
||||||
* Closing HTML tags that highlighted as an error now also highlight the closing '>'
|
|
||||||
* Allow link titles to be both emboldened and italicized.
|
|
||||||
* Wikitext syntax highlighting is shown on protected pages
|
|
||||||
([T301615](https://phabricator.wikimedia.org/T301615))
|
|
||||||
|
|
||||||
### Deprecations and other changes
|
|
||||||
|
|
||||||
* The `.cm-mw-mnemonic` CSS class has been renamed to `.cm-mw-html-entity`
|
|
||||||
* The `.cm-mw-template-name-mnemonic` class has been removed.
|
|
||||||
Use `.cm-mw-template-ground.cm-html-entity` instead.
|
|
||||||
* Line-level styling for `<nowiki>`, `<pre>`, and any tag without an associated
|
|
||||||
TagMode has been removed.
|
|
||||||
* The browser's native search functionality (ala Ctrl+F) has been replaced with
|
|
||||||
search functionality built into CodeMirror. This is necessary to maintain
|
|
||||||
performance (see [T303664](https://phabricator.wikimedia.org/T303664)).
|
|
||||||
|
|
52
jsdoc.json
Normal file
52
jsdoc.json
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"opts": {
|
||||||
|
"encoding": "utf8",
|
||||||
|
"destination": "docs/js",
|
||||||
|
"package": "package.json",
|
||||||
|
"readme": "README.md",
|
||||||
|
"recurse": true,
|
||||||
|
"template": "node_modules/jsdoc-wmf-theme"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"plugins/markdown"
|
||||||
|
],
|
||||||
|
"source": {
|
||||||
|
"include": [ "src" ],
|
||||||
|
"includePattern": ".+\\.js$"
|
||||||
|
},
|
||||||
|
"tags": {},
|
||||||
|
"templates": {
|
||||||
|
"cleverLinks": true,
|
||||||
|
"default": {
|
||||||
|
"useLongnameInNav": true
|
||||||
|
},
|
||||||
|
"wmf": {
|
||||||
|
"maintitle": "CodeMirror",
|
||||||
|
"repository": "https://gerrit.wikimedia.org/g/mediawiki/extensions/CodeMirror",
|
||||||
|
"linkMap": {
|
||||||
|
"Decoration": "https://codemirror.net/docs/ref/#view.Decoration",
|
||||||
|
"DecorationSet": "https://codemirror.net/docs/ref/#view.DecorationSet",
|
||||||
|
"EditorState": "https://codemirror.net/docs/ref/#state.EditorState",
|
||||||
|
"EditorView": "https://codemirror.net/docs/ref/#view.EditorView",
|
||||||
|
"Error": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error",
|
||||||
|
"Extension": "https://codemirror.net/docs/ref/#state.Extension",
|
||||||
|
"HTMLTextAreaElement": "https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement",
|
||||||
|
"jQuery": "https://api.jquery.com/Types/#jQuery",
|
||||||
|
"jQuery.fn.textSelection": "https://doc.wikimedia.org/mediawiki-core/master/js/jQueryPlugins.html#.textSelection",
|
||||||
|
"KeyBinding": "https://codemirror.net/docs/ref/#view.KeyBinding",
|
||||||
|
"LanguageSupport": "https://codemirror.net/docs/ref/#language.LanguageSupport",
|
||||||
|
"PluginSpec": "https://codemirror.net/docs/ref/#view.PluginSpec",
|
||||||
|
"Promise": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise",
|
||||||
|
"RangeSet": "https://codemirror.net/docs/ref/#state.RangeSet",
|
||||||
|
"StreamParser": "https://codemirror.net/docs/ref/#language.StreamParser",
|
||||||
|
"StringStream": "https://codemirror.net/docs/ref/#language.StringStream",
|
||||||
|
"SyntaxNode": "https://lezer.codemirror.net/docs/ref/#common.SyntaxNode",
|
||||||
|
"Tag": "https://lezer.codemirror.net/docs/ref/#highlight.Tag",
|
||||||
|
"TagStyle": "https://codemirror.net/docs/ref/#language.TagStyle",
|
||||||
|
"Tooltip": "https://codemirror.net/docs/ref/#view.Tooltip",
|
||||||
|
"Tree": "https://lezer.codemirror.net/docs/ref/#common.Tree",
|
||||||
|
"ViewUpdate": "https://codemirror.net/docs/ref/#view.ViewUpdate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5145
package-lock.json
generated
5145
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "codemirror",
|
"name": "CodeMirror",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "rollup -c --watch",
|
"start": "rollup -c --watch",
|
||||||
|
@ -12,7 +12,8 @@
|
||||||
"test:unit": "jest",
|
"test:unit": "jest",
|
||||||
"check-built-assets": "{ git status src/ | grep \"nothing to commit, working tree clean\"; } && { echo 'CHECKING BUILD SOURCES ARE COMMITTED' && npm run build && git status resources/dist/ | grep \"nothing to commit, working tree clean\" || { npm run node-debug; false; }; }",
|
"check-built-assets": "{ git status src/ | grep \"nothing to commit, working tree clean\"; } && { echo 'CHECKING BUILD SOURCES ARE COMMITTED' && npm run build && git status resources/dist/ | grep \"nothing to commit, working tree clean\" || { npm run node-debug; false; }; }",
|
||||||
"node-debug": "node -v && npm -v && echo 'ERROR: Please ensure that production assets have been built with `npm run build` and commited, and that you are using the correct version of Node/NPM.'",
|
"node-debug": "node -v && npm -v && echo 'ERROR: Please ensure that production assets have been built with `npm run build` and commited, and that you are using the correct version of Node/NPM.'",
|
||||||
"selenium-test": "wdio tests/selenium/wdio.conf.js"
|
"selenium-test": "wdio tests/selenium/wdio.conf.js",
|
||||||
|
"doc": "jsdoc -c jsdoc.json"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.17.0"
|
"node": "18.17.0"
|
||||||
|
@ -41,6 +42,8 @@
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
|
"jsdoc": "4.0.2",
|
||||||
|
"jsdoc-wmf-theme": "0.0.13",
|
||||||
"rollup": "4.13.0",
|
"rollup": "4.13.0",
|
||||||
"rollup-plugin-copy": "3.5.0",
|
"rollup-plugin-copy": "3.5.0",
|
||||||
"stylelint-config-wikimedia": "0.16.1",
|
"stylelint-config-wikimedia": "0.16.1",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
"use strict";var e=require("ext.CodeMirror.v6.lib"),t=require("ext.CodeMirror.v6"),r=require("ext.CodeMirror.v6.mode.mediawiki"),i=function(t){function r(t,i){var o;return e._classCallCheck(this,r),(o=e._callSuper(this,r,[t])).langExtension=i,o.useCodeMirror=mw.user.options.get("usecodemirror")>0,o}return e._inherits(r,t),e._createClass(r,[{key:"setCodeMirrorPreference",value:function(t){this.useCodeMirror=t,e._get(e._getPrototypeOf(r.prototype),"setCodeMirrorPreference",this).call(this,t)}},{key:"enableCodeMirror",value:function(){var t=this;if(!this.view){var r=this.$textarea.prop("selectionStart"),i=this.$textarea.prop("selectionEnd"),o=this.$textarea.scrollTop(),s=this.$textarea.is(":focus"),a=[].concat(e._toConsumableArray(this.defaultExtensions),[this.langExtension,e.EditorView.domEventHandlers({blur:function(){return t.$textarea.triggerHandler("blur")},focus:function(){return t.$textarea.triggerHandler("focus")}}),e.EditorView.lineWrapping]);if(this.initialize(a),requestAnimationFrame((function(){t.view.scrollDOM.scrollTop=o})),0!==r||0!==i){var n=e.EditorSelection.range(r,i),d=e.EditorView.scrollIntoView(n);d.value.isSnapshot=!0,this.view.dispatch({selection:e.EditorSelection.create([n]),effects:d})}s&&this.view.focus(),mw.hook("ext.CodeMirror.switch").fire(!0,$(this.view.dom))}}},{key:"addCodeMirrorToWikiEditor",value:function(){var e=this,t=this.$textarea.data("wikiEditor-context"),r=t&&t.modules&&t.modules.toolbar;r&&(this.$textarea.wikiEditor("addToToolbar",{section:"main",groups:{codemirror:{tools:{CodeMirror:{label:mw.msg("codemirror-toggle-label"),type:"toggle",oouiIcon:"highlight",action:{type:"callback",execute:function(){return e.switchCodeMirror()}}}}}}}),r.$toolbar.find(".tool[rel=CodeMirror]").attr("id","mw-editbutton-codemirror"),this.readOnly&&this.$textarea.data("wikiEditor-context").$ui.addClass("ext-codemirror-readonly"),this.useCodeMirror&&this.enableCodeMirror(),this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!1,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0}))}},{key:"updateToolbarButton",value:function(){var e=$("#mw-editbutton-codemirror");e.toggleClass("mw-editbutton-codemirror-active",this.useCodeMirror),e.data("setActive")&&e.data("setActive")(this.useCodeMirror)}},{key:"switchCodeMirror",value:function(){if(this.view){this.setCodeMirrorPreference(!1);var e=this.view.scrollDOM.scrollTop,t=this.view.hasFocus,r=this.view.state.selection.ranges[0],i=r.from,o=r.to;$(this.view.dom).textSelection("unregister"),this.$textarea.textSelection("unregister"),this.$textarea.val(this.view.state.doc.toString()),this.view.destroy(),this.view=null,this.$textarea.show(),t&&this.$textarea.trigger("focus"),this.$textarea.prop("selectionStart",Math.min(i,o)).prop("selectionEnd",Math.max(o,i)),this.$textarea.scrollTop(e),mw.hook("ext.CodeMirror.switch").fire(!1,this.$textarea)}else this.enableCodeMirror(),this.setCodeMirrorPreference(!0);this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!0,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0})}}]),r}(t);mw.loader.getState("ext.wikiEditor")&&mw.hook("wikiEditor.toolbarReady").add((function(e){new i(e,r({bidiIsolation:"rtl"===e.attr("dir")})).addCodeMirrorToWikiEditor()}));
|
"use strict";var e=require("ext.CodeMirror.v6.lib"),t=require("ext.CodeMirror.v6"),r=require("ext.CodeMirror.v6.mode.mediawiki"),i=function(t){function r(t,i){var o;return e._classCallCheck(this,r),(o=e._callSuper(this,r,[t])).langExtension=i,o.useCodeMirror=mw.user.options.get("usecodemirror")>0,o}return e._inherits(r,t),e._createClass(r,[{key:"setCodeMirrorPreference",value:function(t){this.useCodeMirror=t,e._get(e._getPrototypeOf(r.prototype),"setCodeMirrorPreference",this).call(this,t)}},{key:"enableCodeMirror",value:function(){var t=this;if(!this.view){var r=this.$textarea.prop("selectionStart"),i=this.$textarea.prop("selectionEnd"),o=this.$textarea.scrollTop(),s=this.$textarea.is(":focus"),a=[this.defaultExtensions,this.langExtension,e.EditorView.domEventHandlers({blur:function(){return t.$textarea.triggerHandler("blur")},focus:function(){return t.$textarea.triggerHandler("focus")}}),e.EditorView.lineWrapping];if(this.initialize(a),requestAnimationFrame((function(){t.view.scrollDOM.scrollTop=o})),0!==r||0!==i){var n=e.EditorSelection.range(r,i),d=e.EditorView.scrollIntoView(n);d.value.isSnapshot=!0,this.view.dispatch({selection:e.EditorSelection.create([n]),effects:d})}s&&this.view.focus(),mw.hook("ext.CodeMirror.switch").fire(!0,$(this.view.dom))}}},{key:"addCodeMirrorToWikiEditor",value:function(){var e=this,t=this.$textarea.data("wikiEditor-context"),r=t&&t.modules&&t.modules.toolbar;r&&(this.$textarea.wikiEditor("addToToolbar",{section:"main",groups:{codemirror:{tools:{CodeMirror:{label:mw.msg("codemirror-toggle-label"),type:"toggle",oouiIcon:"highlight",action:{type:"callback",execute:function(){return e.switchCodeMirror()}}}}}}}),r.$toolbar.find(".tool[rel=CodeMirror]").attr("id","mw-editbutton-codemirror"),this.readOnly&&this.$textarea.data("wikiEditor-context").$ui.addClass("ext-codemirror-readonly"),this.useCodeMirror&&this.enableCodeMirror(),this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!1,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0}))}},{key:"updateToolbarButton",value:function(){var e=$("#mw-editbutton-codemirror");e.toggleClass("mw-editbutton-codemirror-active",this.useCodeMirror),e.data("setActive")&&e.data("setActive")(this.useCodeMirror)}},{key:"switchCodeMirror",value:function(){if(this.view){this.setCodeMirrorPreference(!1);var e=this.view.scrollDOM.scrollTop,t=this.view.hasFocus,r=this.view.state.selection.ranges[0],i=r.from,o=r.to;$(this.view.dom).textSelection("unregister"),this.$textarea.textSelection("unregister"),this.$textarea.val(this.view.state.doc.toString()),this.view.destroy(),this.view=null,this.$textarea.show(),t&&this.$textarea.trigger("focus"),this.$textarea.prop("selectionStart",Math.min(i,o)).prop("selectionEnd",Math.max(o,i)),this.$textarea.scrollTop(e),mw.hook("ext.CodeMirror.switch").fire(!1,this.$textarea)}else this.enableCodeMirror(),this.setCodeMirrorPreference(!0);this.updateToolbarButton(),this.logUsage({editor:"wikitext",enabled:this.useCodeMirror,toggled:!0,edit_start_ts_ms:1e3*parseInt($('input[name="wpStarttime"]').val(),10)||0})}}]),r}(t);mw.loader.getState("ext.wikiEditor")&&mw.hook("wikiEditor.toolbarReady").add((function(e){new i(e,r({bidiIsolation:"rtl"===e.attr("dir")})).addCodeMirrorToWikiEditor()}));
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { mwModeConfig } from './codemirror.mode.mediawiki.config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Decoration}
|
* @type {Decoration}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
const isolate = Decoration.mark( {
|
const isolate = Decoration.mark( {
|
||||||
class: 'cm-bidi-isolate',
|
class: 'cm-bidi-isolate',
|
||||||
|
@ -22,6 +23,7 @@ const isolate = Decoration.mark( {
|
||||||
/**
|
/**
|
||||||
* @param {EditorView} view
|
* @param {EditorView} view
|
||||||
* @return {RangeSet}
|
* @return {RangeSet}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
function computeIsolates( view ) {
|
function computeIsolates( view ) {
|
||||||
const set = new RangeSetBuilder();
|
const set = new RangeSetBuilder();
|
||||||
|
@ -55,17 +57,17 @@ function computeIsolates( view ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class
|
* @private
|
||||||
* @property {DecorationSet} isolates
|
|
||||||
* @property {Tree} tree
|
|
||||||
*/
|
*/
|
||||||
class CodeMirrorBidiIsolation {
|
class CodeMirrorBidiIsolation {
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {EditorView} view
|
* @param {EditorView} view The editor view.
|
||||||
*/
|
*/
|
||||||
constructor( view ) {
|
constructor( view ) {
|
||||||
|
/** @type {DecorationSet} */
|
||||||
this.isolates = computeIsolates( view );
|
this.isolates = computeIsolates( view );
|
||||||
|
/** @type {Tree} */
|
||||||
this.tree = syntaxTree( view.state );
|
this.tree = syntaxTree( view.state );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +86,7 @@ class CodeMirrorBidiIsolation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {PluginSpec}
|
* @type {PluginSpec}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
const bidiIsolationSpec = {
|
const bidiIsolationSpec = {
|
||||||
provide: ( plugin ) => {
|
provide: ( plugin ) => {
|
||||||
|
@ -106,4 +109,14 @@ const bidiIsolationSpec = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bidirectional isolation plugin for CodeMirror for use on RTL pages.
|
||||||
|
* This ensures HTML and MediaWiki tags are always displayed left-to-right.
|
||||||
|
*
|
||||||
|
* Use this plugin by passing in `bidiIsolation: true` when instantiating
|
||||||
|
* a [CodeMirrorModeMediaWiki]{@link CodeMirrorModeMediaWiki} object.
|
||||||
|
*
|
||||||
|
* @module CodeMirrorBidiIsolation
|
||||||
|
* @see https://codemirror.net/examples/bidi/
|
||||||
|
*/
|
||||||
export default ViewPlugin.fromClass( CodeMirrorBidiIsolation, bidiIsolationSpec );
|
export default ViewPlugin.fromClass( CodeMirrorBidiIsolation, bidiIsolationSpec );
|
||||||
|
|
|
@ -3,39 +3,71 @@ import { EditorView, drawSelection, lineNumbers, highlightSpecialChars, keymap }
|
||||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||||
import { searchKeymap } from '@codemirror/search';
|
import { searchKeymap } from '@codemirror/search';
|
||||||
import { bracketMatching } from '@codemirror/language';
|
import { bracketMatching } from '@codemirror/language';
|
||||||
import CodemirrorTextSelection from './codemirror.textSelection';
|
import CodeMirrorTextSelection from './codemirror.textSelection';
|
||||||
|
|
||||||
require( '../ext.CodeMirror.data.js' );
|
require( '../ext.CodeMirror.data.js' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class CodeMirror
|
* Interface for the CodeMirror editor.
|
||||||
* @property {jQuery} $textarea
|
*
|
||||||
* @property {EditorView} view
|
* @example
|
||||||
* @property {EditorState} state
|
* mw.loader.using( [
|
||||||
* @property {boolean} readOnly
|
* 'ext.CodeMirror.v6',
|
||||||
* @property {Function|null} editRecoveryHandler
|
* 'ext.CodeMirror.v6.mode.mediawiki'
|
||||||
* @property {CodemirrorTextSelection} textSelection
|
* ] ).then( ( require ) => {
|
||||||
|
* const CodeMirror = require( 'ext.CodeMirror.v6' );
|
||||||
|
* const mediawikiLang = require( 'ext.CodeMirror.v6.mode.mediawiki' );
|
||||||
|
* const cm = new CodeMirror( myTextarea );
|
||||||
|
* cm.initialize( [ cm.defaultExtensions, mediawikiLang() ] );
|
||||||
|
* } );
|
||||||
*/
|
*/
|
||||||
export default class CodeMirror {
|
class CodeMirror {
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* Instantiate a new CodeMirror instance.
|
||||||
|
*
|
||||||
* @param {HTMLTextAreaElement|jQuery|string} textarea Textarea to add syntax highlighting to.
|
* @param {HTMLTextAreaElement|jQuery|string} textarea Textarea to add syntax highlighting to.
|
||||||
|
* @constructor
|
||||||
*/
|
*/
|
||||||
constructor( textarea ) {
|
constructor( textarea ) {
|
||||||
|
/**
|
||||||
|
* The textarea that CodeMirror is bound to.
|
||||||
|
* @type {jQuery}
|
||||||
|
*/
|
||||||
this.$textarea = $( textarea );
|
this.$textarea = $( textarea );
|
||||||
|
/**
|
||||||
|
* The editor user interface.
|
||||||
|
* @type {EditorView}
|
||||||
|
*/
|
||||||
this.view = null;
|
this.view = null;
|
||||||
|
/**
|
||||||
|
* The editor state.
|
||||||
|
* @type {EditorState}
|
||||||
|
*/
|
||||||
this.state = null;
|
this.state = null;
|
||||||
|
/**
|
||||||
|
* Whether the textarea is read-only.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.readOnly = this.$textarea.prop( 'readonly' );
|
this.readOnly = this.$textarea.prop( 'readonly' );
|
||||||
|
/**
|
||||||
|
* The [edit recovery]{@link https://www.mediawiki.org/wiki/Manual:Edit_Recovery} handler.
|
||||||
|
* @type {Function|null}
|
||||||
|
*/
|
||||||
this.editRecoveryHandler = null;
|
this.editRecoveryHandler = null;
|
||||||
|
/**
|
||||||
|
* jQuery.textSelection overrides for CodeMirror.
|
||||||
|
* @type {CodeMirrorTextSelection}
|
||||||
|
*/
|
||||||
this.textSelection = null;
|
this.textSelection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Default extensions used by CodeMirror.
|
||||||
* Extensions here should be applicable to all theoretical uses of CodeMirror in MediaWiki.
|
* Extensions here should be applicable to all theoretical uses of CodeMirror in MediaWiki.
|
||||||
* Subclasses are safe to override this method if needed.
|
|
||||||
*
|
*
|
||||||
* @see https://codemirror.net/docs/ref/#state.Extension
|
* @see https://codemirror.net/docs/ref/#state.Extension
|
||||||
* @return {Extension[]}
|
* @type {Extension|Extension[]}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
get defaultExtensions() {
|
get defaultExtensions() {
|
||||||
const extensions = [
|
const extensions = [
|
||||||
|
@ -77,8 +109,8 @@ export default class CodeMirror {
|
||||||
* This extension sets the height of the CodeMirror editor to match the textarea.
|
* This extension sets the height of the CodeMirror editor to match the textarea.
|
||||||
* Override this method to change the height of the editor.
|
* Override this method to change the height of the editor.
|
||||||
*
|
*
|
||||||
* @return {Extension}
|
* @type {Extension}
|
||||||
* @stable
|
* @stable to call and override
|
||||||
*/
|
*/
|
||||||
get heightExtension() {
|
get heightExtension() {
|
||||||
return EditorView.theme( {
|
return EditorView.theme( {
|
||||||
|
@ -92,11 +124,12 @@ export default class CodeMirror {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This specifies which attributes get added to the .cm-content and .cm-editor elements.
|
* This specifies which attributes get added to the `.cm-content` and `.cm-editor` elements.
|
||||||
* Subclasses are safe to override this method, but attributes here are considered vital.
|
* Subclasses are safe to override this method, but attributes here are considered vital.
|
||||||
*
|
*
|
||||||
* @see https://codemirror.net/docs/ref/#view.EditorView^contentAttributes
|
* @see https://codemirror.net/docs/ref/#view.EditorView^contentAttributes
|
||||||
* @return {Extension}
|
* @type {Extension}
|
||||||
|
* @stable to call and override
|
||||||
*/
|
*/
|
||||||
get contentAttributesExtension() {
|
get contentAttributesExtension() {
|
||||||
const classList = [];
|
const classList = [];
|
||||||
|
@ -140,7 +173,9 @@ export default class CodeMirror {
|
||||||
* and we don't want localization to be overlooked by CodeMirror clients and subclasses.
|
* and we don't want localization to be overlooked by CodeMirror clients and subclasses.
|
||||||
*
|
*
|
||||||
* @see https://codemirror.net/examples/translate/
|
* @see https://codemirror.net/examples/translate/
|
||||||
* @return {Extension}
|
* @type {Extension}
|
||||||
|
* @stable to call. Instead of overriding, pass in an additional `EditorState.phrases.of()`
|
||||||
|
* when calling `initialize()`.
|
||||||
*/
|
*/
|
||||||
get phrasesExtension() {
|
get phrasesExtension() {
|
||||||
return EditorState.phrases.of( {
|
return EditorState.phrases.of( {
|
||||||
|
@ -165,7 +200,8 @@ export default class CodeMirror {
|
||||||
* which is the localization of 'codemirror-control-character' followed by the Unicode number.
|
* which is the localization of 'codemirror-control-character' followed by the Unicode number.
|
||||||
*
|
*
|
||||||
* @see https://codemirror.net/docs/ref/#view.highlightSpecialChars
|
* @see https://codemirror.net/docs/ref/#view.highlightSpecialChars
|
||||||
* @return {Extension}
|
* @type {Extension}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
get specialCharsExtension() {
|
get specialCharsExtension() {
|
||||||
// Keys are the decimal unicode number, values are the messages.
|
// Keys are the decimal unicode number, values are the messages.
|
||||||
|
@ -220,10 +256,20 @@ export default class CodeMirror {
|
||||||
/**
|
/**
|
||||||
* Setup CodeMirror and add it to the DOM. This will hide the original textarea.
|
* Setup CodeMirror and add it to the DOM. This will hide the original textarea.
|
||||||
*
|
*
|
||||||
* @param {Extension[]} extensions
|
* @param {Extension|Extension[]} [extensions=this.defaultExtensions] Extensions to use.
|
||||||
* @stable
|
* @fires CodeMirror~'ext.CodeMirror.initialize'
|
||||||
|
* @stable to call and override
|
||||||
*/
|
*/
|
||||||
initialize( extensions = this.defaultExtensions ) {
|
initialize( extensions = this.defaultExtensions ) {
|
||||||
|
/**
|
||||||
|
* Called just before CodeMirror is initialized.
|
||||||
|
* This can be used to manipulate the DOM to suit CodeMirror
|
||||||
|
* (i.e. if you manipulate WikiEditor's DOM, you may need this).
|
||||||
|
*
|
||||||
|
* @event CodeMirror~'ext.CodeMirror.initialize'
|
||||||
|
* @param {jQuery} $textarea The textarea that CodeMirror is bound to.
|
||||||
|
* @stable to use
|
||||||
|
*/
|
||||||
mw.hook( 'ext.CodeMirror.initialize' ).fire( this.$textarea );
|
mw.hook( 'ext.CodeMirror.initialize' ).fire( this.$textarea );
|
||||||
mw.hook( 'editRecovery.loadEnd' ).add( ( data ) => {
|
mw.hook( 'editRecovery.loadEnd' ).add( ( data ) => {
|
||||||
this.editRecoveryHandler = data.fieldChangeHandler;
|
this.editRecoveryHandler = data.fieldChangeHandler;
|
||||||
|
@ -264,7 +310,7 @@ export default class CodeMirror {
|
||||||
* Log usage of CodeMirror.
|
* Log usage of CodeMirror.
|
||||||
*
|
*
|
||||||
* @param {Object} data
|
* @param {Object} data
|
||||||
* @stable
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
logUsage( data ) {
|
logUsage( data ) {
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -284,7 +330,7 @@ export default class CodeMirror {
|
||||||
* Save CodeMirror enabled preference.
|
* Save CodeMirror enabled preference.
|
||||||
*
|
*
|
||||||
* @param {boolean} prefValue True, if CodeMirror should be enabled by default, otherwise false.
|
* @param {boolean} prefValue True, if CodeMirror should be enabled by default, otherwise false.
|
||||||
* @stable
|
* @stable to call and override
|
||||||
*/
|
*/
|
||||||
setCodeMirrorPreference( prefValue ) {
|
setCodeMirrorPreference( prefValue ) {
|
||||||
// Skip for unnamed users
|
// Skip for unnamed users
|
||||||
|
@ -298,12 +344,13 @@ export default class CodeMirror {
|
||||||
/**
|
/**
|
||||||
* jQuery.textSelection overrides for CodeMirror.
|
* jQuery.textSelection overrides for CodeMirror.
|
||||||
*
|
*
|
||||||
* @see https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/jQuery.plugin.textSelection
|
* @see jQuery.fn.textSelection
|
||||||
* @return {Object}
|
* @type {Object}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
get cmTextSelection() {
|
get cmTextSelection() {
|
||||||
if ( !this.textSelection ) {
|
if ( !this.textSelection ) {
|
||||||
this.textSelection = new CodemirrorTextSelection( this.view );
|
this.textSelection = new CodeMirrorTextSelection( this.view );
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
getContents: () => this.textSelection.getContents(),
|
getContents: () => this.textSelection.getContents(),
|
||||||
|
@ -317,3 +364,5 @@ export default class CodeMirror {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default CodeMirror;
|
||||||
|
|
|
@ -4,12 +4,23 @@ import { TagStyle, StreamParser } from '@codemirror/language';
|
||||||
/**
|
/**
|
||||||
* Configuration for the MediaWiki highlighting mode for CodeMirror.
|
* Configuration for the MediaWiki highlighting mode for CodeMirror.
|
||||||
* This is a separate class mainly to keep static configuration out of
|
* This is a separate class mainly to keep static configuration out of
|
||||||
* the logic in CodeMirrorModeMediaWiki.
|
* the logic in {@link CodeMirrorModeMediaWiki}.
|
||||||
*
|
*
|
||||||
* @class CodeMirrorModeMediaWikiConfig
|
* @module CodeMirrorModeMediaWikiConfig
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // within MediaWiki:
|
||||||
|
* import { mwModeConfig } from 'ext.CodeMirror.v6.mode.mediawiki';
|
||||||
|
* // Reference tags by their constants in the tags property.
|
||||||
|
* if ( tag === mwModeConfig.tags.htmlTagBracket ) {
|
||||||
|
* // …
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
class CodeMirrorModeMediaWikiConfig {
|
class CodeMirrorModeMediaWikiConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
this.extHighlightStyles = [];
|
this.extHighlightStyles = [];
|
||||||
this.tokenTable = this.defaultTokenTable;
|
this.tokenTable = this.defaultTokenTable;
|
||||||
|
@ -23,6 +34,7 @@ class CodeMirrorModeMediaWikiConfig {
|
||||||
* @see https://www.mediawiki.org/wiki/Extension:CodeMirror#Extension_integration
|
* @see https://www.mediawiki.org/wiki/Extension:CodeMirror#Extension_integration
|
||||||
* @param {string} tag
|
* @param {string} tag
|
||||||
* @param {Tag} [parent]
|
* @param {Tag} [parent]
|
||||||
|
* @private
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
addTag( tag, parent = null ) {
|
addTag( tag, parent = null ) {
|
||||||
|
@ -39,6 +51,7 @@ class CodeMirrorModeMediaWikiConfig {
|
||||||
*
|
*
|
||||||
* @param {string} token
|
* @param {string} token
|
||||||
* @param {Tag} [parent]
|
* @param {Tag} [parent]
|
||||||
|
* @private
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
addToken( token, parent = null ) {
|
addToken( token, parent = null ) {
|
||||||
|
@ -88,7 +101,8 @@ class CodeMirrorModeMediaWikiConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of MediaWiki-esque token identifiers to a standardized lezer highlighting tag.
|
* Mapping of MediaWiki-esque token identifiers to
|
||||||
|
* [standardized lezer highlighting tags]{@link https://lezer.codemirror.net/docs/ref/#highlight.tags}.
|
||||||
* Values are one of the default highlighting tags. The idea is to use as many default tags as
|
* Values are one of the default highlighting tags. The idea is to use as many default tags as
|
||||||
* possible so that theming (such as dark mode) can be applied with minimal effort. The
|
* possible so that theming (such as dark mode) can be applied with minimal effort. The
|
||||||
* semantic meaning of the tag may not really match how it is used, but as per CodeMirror docs,
|
* semantic meaning of the tag may not really match how it is used, but as per CodeMirror docs,
|
||||||
|
@ -99,8 +113,9 @@ class CodeMirrorModeMediaWikiConfig {
|
||||||
* in highlightStyle().
|
* in highlightStyle().
|
||||||
*
|
*
|
||||||
* @see https://lezer.codemirror.net/docs/ref/#highlight.tags
|
* @see https://lezer.codemirror.net/docs/ref/#highlight.tags
|
||||||
|
* @member CodeMirrorModeMediaWikiConfig
|
||||||
|
* @type {Object<string>}
|
||||||
* @return {Object<string>}
|
* @return {Object<string>}
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
get tags() {
|
get tags() {
|
||||||
return {
|
return {
|
||||||
|
@ -195,6 +210,7 @@ class CodeMirrorModeMediaWikiConfig {
|
||||||
* @see https://codemirror.net/docs/ref/#language.StreamParser.tokenTable
|
* @see https://codemirror.net/docs/ref/#language.StreamParser.tokenTable
|
||||||
* @see https://lezer.codemirror.net/docs/ref/#highlight.Tag%5Edefine
|
* @see https://lezer.codemirror.net/docs/ref/#highlight.Tag%5Edefine
|
||||||
* @return {Object<Tag>}
|
* @return {Object<Tag>}
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
get defaultTokenTable() {
|
get defaultTokenTable() {
|
||||||
return {
|
return {
|
||||||
|
@ -229,6 +245,7 @@ class CodeMirrorModeMediaWikiConfig {
|
||||||
* @see https://codemirror.net/docs/ref/#language.TagStyle
|
* @see https://codemirror.net/docs/ref/#language.TagStyle
|
||||||
* @param {StreamParser} context
|
* @param {StreamParser} context
|
||||||
* @return {TagStyle[]}
|
* @return {TagStyle[]}
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
getTagStyles( context ) {
|
getTagStyles( context ) {
|
||||||
return [
|
return [
|
||||||
|
@ -491,4 +508,8 @@ class CodeMirrorModeMediaWikiConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @member CodeMirrorModeMediaWikiConfig
|
||||||
|
* @type {CodeMirrorModeMediaWikiConfig}
|
||||||
|
*/
|
||||||
export const mwModeConfig = new CodeMirrorModeMediaWikiConfig();
|
export const mwModeConfig = new CodeMirrorModeMediaWikiConfig();
|
||||||
|
|
|
@ -12,13 +12,26 @@ import templateFoldingExtension from './codemirror.templateFolding';
|
||||||
import bidiIsolationExtension from './codemirror.bidiIsolation';
|
import bidiIsolationExtension from './codemirror.bidiIsolation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapted from the original CodeMirror 5 stream parser by Pavel Astakhov
|
* MediaWiki language support for CodeMirror 6.
|
||||||
|
* Adapted from the original CodeMirror 5 stream parser by Pavel Astakhov.
|
||||||
*
|
*
|
||||||
* @class CodeMirrorModeMediaWiki
|
* @module CodeMirrorModeMediaWiki
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* mw.loader.using( [
|
||||||
|
* 'ext.CodeMirror.v6',
|
||||||
|
* 'ext.CodeMirror.v6.mode.mediawiki'
|
||||||
|
* ] ).then( ( require ) => {
|
||||||
|
* const CodeMirror = require( 'ext.CodeMirror.v6' );
|
||||||
|
* const mediawikiLang = require( 'ext.CodeMirror.v6.mode.mediawiki' );
|
||||||
|
* const cm = new CodeMirror( myTextarea );
|
||||||
|
* cm.initialize( [ cm.defaultExtensions, mediawikiLang() ] );
|
||||||
|
* } );
|
||||||
*/
|
*/
|
||||||
class CodeMirrorModeMediaWiki {
|
class CodeMirrorModeMediaWiki {
|
||||||
/**
|
/**
|
||||||
* @param {Object} config
|
* @param {Object} config MediaWiki configuration as generated by DataScript.php
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor( config ) {
|
constructor( config ) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
@ -45,6 +58,8 @@ class CodeMirrorModeMediaWiki {
|
||||||
* Register the ground tokens. These aren't referenced directly in the StreamParser, nor do
|
* Register the ground tokens. These aren't referenced directly in the StreamParser, nor do
|
||||||
* they have a parent Tag, so we don't need them as constants like we do for other tokens.
|
* they have a parent Tag, so we don't need them as constants like we do for other tokens.
|
||||||
* See this.makeLocalStyle() for how these tokens are used.
|
* See this.makeLocalStyle() for how these tokens are used.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
registerGroundTokens() {
|
registerGroundTokens() {
|
||||||
[
|
[
|
||||||
|
@ -735,6 +750,7 @@ class CodeMirrorModeMediaWiki {
|
||||||
/**
|
/**
|
||||||
* @param {string} style
|
* @param {string} style
|
||||||
* @return {string|Function}
|
* @return {string|Function}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
eatWikiText( style ) {
|
eatWikiText( style ) {
|
||||||
return ( stream, state ) => {
|
return ( stream, state ) => {
|
||||||
|
@ -1026,6 +1042,7 @@ class CodeMirrorModeMediaWiki {
|
||||||
* @see https://phabricator.wikimedia.org/T108455
|
* @see https://phabricator.wikimedia.org/T108455
|
||||||
*
|
*
|
||||||
* @param {StringStream} stream
|
* @param {StringStream} stream
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
prepareItalicForCorrection( stream ) {
|
prepareItalicForCorrection( stream ) {
|
||||||
// See Parser::doQuotes() in MediaWiki Core, it works similarly.
|
// See Parser::doQuotes() in MediaWiki Core, it works similarly.
|
||||||
|
@ -1058,6 +1075,7 @@ class CodeMirrorModeMediaWiki {
|
||||||
/**
|
/**
|
||||||
* @see https://codemirror.net/docs/ref/#language.StreamParser
|
* @see https://codemirror.net/docs/ref/#language.StreamParser
|
||||||
* @return {StreamParser}
|
* @return {StreamParser}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
get mediawiki() {
|
get mediawiki() {
|
||||||
return {
|
return {
|
||||||
|
@ -1067,6 +1085,7 @@ class CodeMirrorModeMediaWiki {
|
||||||
* Initial State for the parser.
|
* Initial State for the parser.
|
||||||
*
|
*
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
startState: () => {
|
startState: () => {
|
||||||
return {
|
return {
|
||||||
|
@ -1087,6 +1106,7 @@ class CodeMirrorModeMediaWiki {
|
||||||
*
|
*
|
||||||
* @param {Object} state
|
* @param {Object} state
|
||||||
* @return {Object}
|
* @return {Object}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
copyState: ( state ) => {
|
copyState: ( state ) => {
|
||||||
return {
|
return {
|
||||||
|
@ -1109,6 +1129,7 @@ class CodeMirrorModeMediaWiki {
|
||||||
* @param {StringStream} stream
|
* @param {StringStream} stream
|
||||||
* @param {Object} state
|
* @param {Object} state
|
||||||
* @return {string|null}
|
* @return {string|null}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
token: ( stream, state ) => {
|
token: ( stream, state ) => {
|
||||||
let style, p, t, f,
|
let style, p, t, f,
|
||||||
|
@ -1195,6 +1216,10 @@ class CodeMirrorModeMediaWiki {
|
||||||
return t.style;
|
return t.style;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} state
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
blankLine: ( state ) => {
|
blankLine: ( state ) => {
|
||||||
if ( state.extMode && state.extMode.blankLine ) {
|
if ( state.extMode && state.extMode.blankLine ) {
|
||||||
state.extMode.blankLine( state.extState );
|
state.extMode.blankLine( state.extState );
|
||||||
|
@ -1206,30 +1231,24 @@ class CodeMirrorModeMediaWiki {
|
||||||
*
|
*
|
||||||
* @see CodeMirrorModeMediaWikiConfig.defaultTokenTable
|
* @see CodeMirrorModeMediaWikiConfig.defaultTokenTable
|
||||||
* @return {Object<Tag>}
|
* @return {Object<Tag>}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
tokenTable: this.tokenTable
|
tokenTable: this.tokenTable
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} mediaWikiLangConfig
|
|
||||||
* @property {boolean} [bidiIsolation=false] Enable bidi isolation around HTML tags.
|
|
||||||
* This should generally always be enabled on RTL pages, but it comes with a performance cost.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a LanguageSupport instance for the MediaWiki mode.
|
* Gets a LanguageSupport instance for the MediaWiki mode.
|
||||||
*
|
*
|
||||||
* @example
|
* @member CodeMirrorModeMediaWiki
|
||||||
* import CodeMirror from './codemirror';
|
* @method
|
||||||
* import { mediaWikiLang } from './codemirror.mode.mediawiki';
|
* @param {Object} [config] Configuration options for the MediaWiki mode.
|
||||||
* const cm = new CodeMirror( textarea );
|
* @param {boolean} [config.bidiIsolation=false] Enable bidi isolation around HTML tags.
|
||||||
* cm.initialize( [ ...cm.defaultExtensions, mediaWikiLang() ] );
|
* This should generally always be enabled on RTL pages, but it comes with a performance cost.
|
||||||
*
|
|
||||||
* @param {mediaWikiLangConfig} [config] Configuration options for the MediaWiki mode.
|
|
||||||
* @param {Object|null} [mwConfig] Ignore; used only by unit tests.
|
* @param {Object|null} [mwConfig] Ignore; used only by unit tests.
|
||||||
* @return {LanguageSupport}
|
* @return {LanguageSupport}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
export default ( config = { bidiIsolation: false }, mwConfig = null ) => {
|
export default ( config = { bidiIsolation: false }, mwConfig = null ) => {
|
||||||
mwConfig = mwConfig || mw.config.get( 'extCodeMirrorConfig' );
|
mwConfig = mwConfig || mw.config.get( 'extCodeMirrorConfig' );
|
||||||
|
|
|
@ -8,25 +8,29 @@ import { mwModeConfig as modeConfig } from './codemirror.mode.mediawiki.config';
|
||||||
* Check if a SyntaxNode is a template bracket (`{{` or `}}`)
|
* Check if a SyntaxNode is a template bracket (`{{` or `}}`)
|
||||||
* @param {SyntaxNode} node The SyntaxNode to check
|
* @param {SyntaxNode} node The SyntaxNode to check
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
const isBracket = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.templateBracket ),
|
const isBracket = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.templateBracket ),
|
||||||
/**
|
/**
|
||||||
* Check if a SyntaxNode is a template delimiter (`|`)
|
* Check if a SyntaxNode is a template delimiter (`|`)
|
||||||
* @param {SyntaxNode} node The SyntaxNode to check
|
* @param {SyntaxNode} node The SyntaxNode to check
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
isDelimiter = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.templateDelimiter ),
|
isDelimiter = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.templateDelimiter ),
|
||||||
/**
|
/**
|
||||||
* Check if a SyntaxNode is part of a template, except for the brackets
|
* Check if a SyntaxNode is part of a template, except for the brackets
|
||||||
* @param {SyntaxNode} node The SyntaxNode to check
|
* @param {SyntaxNode} node The SyntaxNode to check
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
isTemplate = ( node ) => /-template[a-z\d-]+ground/u.test( node.name ) && !isBracket( node ),
|
isTemplate = ( node ) => /-template[a-z\d-]+ground/u.test( node.name ) && !isBracket( node ),
|
||||||
/**
|
/**
|
||||||
* Update the stack of opening (+) or closing (-) brackets
|
* Update the stack of opening (+) or closing (-) brackets
|
||||||
* @param {EditorState} state EditorState instance
|
* @param {EditorState} state EditorState instance
|
||||||
* @param {SyntaxNode} node The SyntaxNode of the bracket
|
* @param {SyntaxNode} node The SyntaxNode of the bracket
|
||||||
* @return {1|-1}
|
* @return {number}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
stackUpdate = ( state, node ) => state.sliceDoc( node.from, node.from + 1 ) === '{' ? 1 : -1;
|
stackUpdate = ( state, node ) => state.sliceDoc( node.from, node.from + 1 ) === '{' ? 1 : -1;
|
||||||
|
|
||||||
|
@ -36,6 +40,7 @@ const isBracket = ( node ) => node.name.split( '_' ).includes( modeConfig.tags.t
|
||||||
* @param {number|SyntaxNode} posOrNode Position or node
|
* @param {number|SyntaxNode} posOrNode Position or node
|
||||||
* @param {Tree|null} [tree] Syntax tree
|
* @param {Tree|null} [tree] Syntax tree
|
||||||
* @return {{from: number, to: number}|null}
|
* @return {{from: number, to: number}|null}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
const foldable = ( state, posOrNode, tree ) => {
|
const foldable = ( state, posOrNode, tree ) => {
|
||||||
if ( typeof posOrNode === 'number' ) {
|
if ( typeof posOrNode === 'number' ) {
|
||||||
|
@ -109,6 +114,7 @@ const foldable = ( state, posOrNode, tree ) => {
|
||||||
* Create a tooltip for folding a template
|
* Create a tooltip for folding a template
|
||||||
* @param {EditorState} state EditorState instance
|
* @param {EditorState} state EditorState instance
|
||||||
* @return {Tooltip|null}
|
* @return {Tooltip|null}
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
const create = ( state ) => {
|
const create = ( state ) => {
|
||||||
const { selection: { main: { head } } } = state,
|
const { selection: { main: { head } } } = state,
|
||||||
|
@ -146,7 +152,10 @@ const create = ( state ) => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {KeyBinding[]} */
|
/**
|
||||||
|
* @type {KeyBinding[]}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
const foldKeymap = [
|
const foldKeymap = [
|
||||||
{
|
{
|
||||||
// Fold the template at the selection/cursor
|
// Fold the template at the selection/cursor
|
||||||
|
@ -221,7 +230,14 @@ const foldKeymap = [
|
||||||
{ key: 'Ctrl-Alt-]', run: unfoldAll }
|
{ key: 'Ctrl-Alt-]', run: unfoldAll }
|
||||||
];
|
];
|
||||||
|
|
||||||
/** @type {Extension} */
|
/**
|
||||||
|
* CodeMirror extension providing
|
||||||
|
* [template folding](https://www.mediawiki.org/wiki/Help:Extension:CodeMirror#Template_folding)
|
||||||
|
* for the MediaWiki mode. This automatically applied when using {@link CodeMirrorModeMediaWiki}.
|
||||||
|
*
|
||||||
|
* @module CodeMirrorTemplateFolding
|
||||||
|
* @type {Extension}
|
||||||
|
*/
|
||||||
export default [
|
export default [
|
||||||
codeFolding( {
|
codeFolding( {
|
||||||
placeholderDOM( view ) {
|
placeholderDOM( view ) {
|
||||||
|
|
|
@ -2,20 +2,26 @@ import { EditorView } from '@codemirror/view';
|
||||||
import { EditorSelection } from '@codemirror/state';
|
import { EditorSelection } from '@codemirror/state';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* jQuery.textSelection implementation for CodeMirror.
|
* [jQuery.textSelection]{@link jQuery.fn.textSelection} implementation for CodeMirror.
|
||||||
|
* This is registered to both the textarea and the `.cm-editor` element.
|
||||||
*
|
*
|
||||||
* @see jQuery.fn.textSelection
|
* @see jQuery.fn.textSelection
|
||||||
* @class CodemirrorTextSelection
|
|
||||||
* @property {EditorView} view
|
|
||||||
* @property {jQuery} $cmDom
|
|
||||||
*/
|
*/
|
||||||
export default class CodemirrorTextSelection {
|
class CodeMirrorTextSelection {
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {EditorView} view
|
* @param {EditorView} view
|
||||||
*/
|
*/
|
||||||
constructor( view ) {
|
constructor( view ) {
|
||||||
|
/**
|
||||||
|
* The CodeMirror view.
|
||||||
|
* @type {EditorView}
|
||||||
|
*/
|
||||||
this.view = view;
|
this.view = view;
|
||||||
|
/**
|
||||||
|
* The CodeMirror DOM.
|
||||||
|
* @type {jQuery}
|
||||||
|
*/
|
||||||
this.$cmDom = $( view.dom );
|
this.$cmDom = $( view.dom );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +29,7 @@ export default class CodemirrorTextSelection {
|
||||||
* Get the contents of the editor.
|
* Get the contents of the editor.
|
||||||
*
|
*
|
||||||
* @return {string}
|
* @return {string}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
getContents() {
|
getContents() {
|
||||||
return this.view.state.doc.toString();
|
return this.view.state.doc.toString();
|
||||||
|
@ -33,6 +40,7 @@ export default class CodemirrorTextSelection {
|
||||||
*
|
*
|
||||||
* @param {string} content
|
* @param {string} content
|
||||||
* @return {jQuery}
|
* @return {jQuery}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
setContents( content ) {
|
setContents( content ) {
|
||||||
this.view.dispatch( {
|
this.view.dispatch( {
|
||||||
|
@ -48,8 +56,11 @@ export default class CodemirrorTextSelection {
|
||||||
/**
|
/**
|
||||||
* Get the current caret position.
|
* Get the current caret position.
|
||||||
*
|
*
|
||||||
* @param {Object} options
|
* @param {Object} [options]
|
||||||
|
* @param {boolean} [options.startAndEnd] Whether to return the start and end of the selection
|
||||||
|
* instead of the caret position.
|
||||||
* @return {number[]|number}
|
* @return {number[]|number}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
getCaretPosition( options ) {
|
getCaretPosition( options ) {
|
||||||
if ( !options.startAndEnd ) {
|
if ( !options.startAndEnd ) {
|
||||||
|
@ -65,6 +76,7 @@ export default class CodemirrorTextSelection {
|
||||||
* Scroll the editor to the current caret position.
|
* Scroll the editor to the current caret position.
|
||||||
*
|
*
|
||||||
* @return {jQuery}
|
* @return {jQuery}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
scrollToCaretPosition() {
|
scrollToCaretPosition() {
|
||||||
const scrollEffect = EditorView.scrollIntoView( this.view.state.selection.main.head );
|
const scrollEffect = EditorView.scrollIntoView( this.view.state.selection.main.head );
|
||||||
|
@ -79,6 +91,7 @@ export default class CodemirrorTextSelection {
|
||||||
* Get the selected text.
|
* Get the selected text.
|
||||||
*
|
*
|
||||||
* @return {string}
|
* @return {string}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
getSelection() {
|
getSelection() {
|
||||||
return this.view.state.sliceDoc(
|
return this.view.state.sliceDoc(
|
||||||
|
@ -91,7 +104,10 @@ export default class CodemirrorTextSelection {
|
||||||
* Set the selected text.
|
* Set the selected text.
|
||||||
*
|
*
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
|
* @param {number} options.start The start of the selection.
|
||||||
|
* @param {number} [options.end=options.start] The end of the selection.
|
||||||
* @return {jQuery}
|
* @return {jQuery}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
setSelection( options ) {
|
setSelection( options ) {
|
||||||
this.view.dispatch( {
|
this.view.dispatch( {
|
||||||
|
@ -106,6 +122,7 @@ export default class CodemirrorTextSelection {
|
||||||
*
|
*
|
||||||
* @param {string} value
|
* @param {string} value
|
||||||
* @return {jQuery}
|
* @return {jQuery}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
replaceSelection( value ) {
|
replaceSelection( value ) {
|
||||||
this.view.dispatch(
|
this.view.dispatch(
|
||||||
|
@ -118,13 +135,23 @@ export default class CodemirrorTextSelection {
|
||||||
* Encapsulate the selected text with the given values.
|
* Encapsulate the selected text with the given values.
|
||||||
*
|
*
|
||||||
* This is intentionally a near-identical implementation to jQuery.textSelection,
|
* This is intentionally a near-identical implementation to jQuery.textSelection,
|
||||||
* except it uses CodeMirror's EditorState.changeByRange when there are multiple selections.
|
* except it uses CodeMirror's
|
||||||
|
* [EditorState.changeByRange](https://codemirror.net/docs/ref/#state.EditorState.changeByRange)
|
||||||
|
* when there are multiple selections.
|
||||||
*
|
*
|
||||||
* @see jQuery.fn.textSelection.encapsulateSelection
|
* @todo Add support for 'ownline' and 'splitlines' options.
|
||||||
* @todo Add support for 'ownline', 'selectPeri' and 'splitlines' options.
|
|
||||||
*
|
*
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
|
* @param {string} [options.pre] The text to insert before the cursor/selection.
|
||||||
|
* @param {string} [options.post] The text to insert after the cursor/selection.
|
||||||
|
* @param {string} [options.peri] Text to insert between pre and post and select afterwards.
|
||||||
|
* @param {boolean} [options.replace=false] If there is a selection, replace it with peri
|
||||||
|
* instead of leaving it alone.
|
||||||
|
* @param {boolean} [options.selectPeri=true] Select the peri text if it was inserted.
|
||||||
|
* @param {number} [options.selectionStart] Position to start selection at.
|
||||||
|
* @param {number} [options.selectionEnd=options.selectionStart] Position to end selection at.
|
||||||
* @return {jQuery}
|
* @return {jQuery}
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
encapsulateSelection( options ) {
|
encapsulateSelection( options ) {
|
||||||
let selectedText,
|
let selectedText,
|
||||||
|
@ -200,3 +227,5 @@ export default class CodemirrorTextSelection {
|
||||||
return this.$cmDom;
|
return this.$cmDom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default CodeMirrorTextSelection;
|
||||||
|
|
|
@ -4,19 +4,32 @@ import { EditorView } from '@codemirror/view';
|
||||||
import { LanguageSupport } from '@codemirror/language';
|
import { LanguageSupport } from '@codemirror/language';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class CodeMirrorWikiEditor
|
* CodeMirror integration with
|
||||||
* @property {LanguageSupport|Extension} langExtension
|
* [WikiEditor](https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:WikiEditor).
|
||||||
* @property {boolean} useCodeMirror
|
*
|
||||||
|
* Use this class if you want WikiEditor's toolbar. If you don't need the toolbar,
|
||||||
|
* using {@link CodeMirror} directly will be considerably more efficient.
|
||||||
|
*
|
||||||
|
* @extends CodeMirror
|
||||||
*/
|
*/
|
||||||
export default class CodeMirrorWikiEditor extends CodeMirror {
|
class CodeMirrorWikiEditor extends CodeMirror {
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {jQuery} $textarea
|
* @param {jQuery} $textarea The textarea to replace with CodeMirror.
|
||||||
* @param {LanguageSupport|Extension} langExtension
|
* @param {LanguageSupport|Extension} langExtension Language support and its extension(s).
|
||||||
|
* @stable to call and override
|
||||||
*/
|
*/
|
||||||
constructor( $textarea, langExtension ) {
|
constructor( $textarea, langExtension ) {
|
||||||
super( $textarea );
|
super( $textarea );
|
||||||
|
/**
|
||||||
|
* Language support and its extension(s).
|
||||||
|
* @type {LanguageSupport|Extension}
|
||||||
|
*/
|
||||||
this.langExtension = langExtension;
|
this.langExtension = langExtension;
|
||||||
|
/**
|
||||||
|
* Whether CodeMirror is currently enabled.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
this.useCodeMirror = mw.user.options.get( 'usecodemirror' ) > 0;
|
this.useCodeMirror = mw.user.options.get( 'usecodemirror' ) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +43,10 @@ export default class CodeMirrorWikiEditor extends CodeMirror {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces the default textarea with CodeMirror
|
* Replaces the default textarea with CodeMirror.
|
||||||
|
*
|
||||||
|
* @fires CodeMirrorWikiEditor~'ext.CodeMirror.switch'
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
enableCodeMirror() {
|
enableCodeMirror() {
|
||||||
// If CodeMirror is already loaded, abort.
|
// If CodeMirror is already loaded, abort.
|
||||||
|
@ -48,7 +64,7 @@ export default class CodeMirrorWikiEditor extends CodeMirror {
|
||||||
* @see https://codemirror.net/docs/ref/#state.Extension
|
* @see https://codemirror.net/docs/ref/#state.Extension
|
||||||
*/
|
*/
|
||||||
const extensions = [
|
const extensions = [
|
||||||
...this.defaultExtensions,
|
this.defaultExtensions,
|
||||||
this.langExtension,
|
this.langExtension,
|
||||||
EditorView.domEventHandlers( {
|
EditorView.domEventHandlers( {
|
||||||
blur: () => this.$textarea.triggerHandler( 'blur' ),
|
blur: () => this.$textarea.triggerHandler( 'blur' ),
|
||||||
|
@ -76,11 +92,22 @@ export default class CodeMirrorWikiEditor extends CodeMirror {
|
||||||
this.view.focus();
|
this.view.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after CodeMirror is enabled or disabled in WikiEditor.
|
||||||
|
*
|
||||||
|
* @event CodeMirrorWikiEditor~'ext.CodeMirror.switch'
|
||||||
|
* @param {boolean} enabled Whether CodeMirror is enabled.
|
||||||
|
* @param {jQuery} $textarea The current "editor", either the
|
||||||
|
* original textarea or the `.cm-editor` element.
|
||||||
|
* @stable to use
|
||||||
|
*/
|
||||||
mw.hook( 'ext.CodeMirror.switch' ).fire( true, $( this.view.dom ) );
|
mw.hook( 'ext.CodeMirror.switch' ).fire( true, $( this.view.dom ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the CodeMirror button to WikiEditor
|
* Adds the CodeMirror button to WikiEditor.
|
||||||
|
*
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
addCodeMirrorToWikiEditor() {
|
addCodeMirrorToWikiEditor() {
|
||||||
const context = this.$textarea.data( 'wikiEditor-context' );
|
const context = this.$textarea.data( 'wikiEditor-context' );
|
||||||
|
@ -136,7 +163,9 @@ export default class CodeMirrorWikiEditor extends CodeMirror {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates CodeMirror button on the toolbar according to the current state (on/off)
|
* Updates CodeMirror button on the toolbar according to the current state (on/off).
|
||||||
|
*
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
updateToolbarButton() {
|
updateToolbarButton() {
|
||||||
// eslint-disable-next-line no-jquery/no-global-selector
|
// eslint-disable-next-line no-jquery/no-global-selector
|
||||||
|
@ -150,7 +179,10 @@ export default class CodeMirrorWikiEditor extends CodeMirror {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables or disables CodeMirror
|
* Enables or disables CodeMirror.
|
||||||
|
*
|
||||||
|
* @fires CodeMirrorWikiEditor~'ext.CodeMirror.switch'
|
||||||
|
* @stable to call
|
||||||
*/
|
*/
|
||||||
switchCodeMirror() {
|
switchCodeMirror() {
|
||||||
if ( this.view ) {
|
if ( this.view ) {
|
||||||
|
@ -186,3 +218,5 @@ export default class CodeMirrorWikiEditor extends CodeMirror {
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default CodeMirrorWikiEditor;
|
||||||
|
|
Loading…
Reference in a new issue