CodeMirror: highlight special characters and non-breaking spaces

The highlightSpecialChars() should act mostly identical to CM5. An
example is the soft hyphen (U+00AD). These are highlighted as a red dot
because they are non-printable characters.

The i18n may seem like overkill, but CM6 would otherwise actually print
the same message in plain English and without a way to localize it.

Per request at T181677, we also highlight non-breaking space and the
narrow non-breaking space. These are shown as a faint gray dot, to match
CM6's highlightWhiteSpace() extension. That extension isn't used here
because it would also highlight normal spaces, which we don't want.

Bug: T181677
Change-Id: Iac1a8cf78e4cd0a27abc917f4b70bdfbaf86252a
This commit is contained in:
MusikAnimal 2024-01-18 19:33:58 -05:00
parent 5a07eb35db
commit 75f5c9b2be
10 changed files with 155 additions and 8 deletions

View file

@ -202,7 +202,31 @@
"codemirror-by-word",
"codemirror-replace",
"codemirror-replace-placeholder",
"codemirror-replace-all"
"codemirror-replace-all",
"codemirror-control-character",
"codemirror-special-char-null",
"codemirror-special-char-bell",
"codemirror-special-char-backspace",
"codemirror-special-char-newline",
"codemirror-special-char-vertical-tab",
"codemirror-special-char-carriage-return",
"codemirror-special-char-escape",
"codemirror-special-char-nbsp",
"codemirror-special-char-zero-width-space",
"codemirror-special-char-zero-width-non-joiner",
"codemirror-special-char-zero-width-joiner",
"codemirror-special-char-left-to-right-mark",
"codemirror-special-char-right-to-left-mark",
"codemirror-special-char-line-separator",
"codemirror-special-char-left-to-right-override",
"codemirror-special-char-right-to-left-override",
"codemirror-special-char-narrow-nbsp",
"codemirror-special-char-left-to-right-isolate",
"codemirror-special-char-right-to-left-isolate",
"codemirror-special-char-pop-directional-isolate",
"codemirror-special-char-paragraph-separator",
"codemirror-special-char-zero-width-no-break-space",
"codemirror-special-char-object-replacement"
]
}
},

View file

@ -19,5 +19,6 @@
"codemirror-replace": "ersetzen",
"codemirror-replace-placeholder": "Ersetzen",
"codemirror-replace-all": "alle ersetzen",
"codemirror-control-character": "Steuerzeichen $1",
"prefs-accessibility": "Barrierefreiheit"
}

View file

@ -1,7 +1,8 @@
{
"@metadata": {
"authors": [
"pastakhov"
"pastakhov",
"MusikAnimal"
]
},
"codemirror-desc": "Provides syntax highlighting in wikitext editor",
@ -18,5 +19,29 @@
"codemirror-replace": "replace",
"codemirror-replace-placeholder": "Replace",
"codemirror-replace-all": "replace all",
"codemirror-control-character": "Control character $1",
"codemirror-special-char-null": "Null character",
"codemirror-special-char-bell": "Bell character",
"codemirror-special-char-backspace": "Backspace",
"codemirror-special-char-newline": "Newline",
"codemirror-special-char-vertical-tab": "Vertical tab",
"codemirror-special-char-carriage-return": "Carriage return",
"codemirror-special-char-escape": "Escape character",
"codemirror-special-char-nbsp": "Non-breaking space",
"codemirror-special-char-zero-width-space": "Zero-width space",
"codemirror-special-char-zero-width-non-joiner": "Zero-width non-joiner",
"codemirror-special-char-zero-width-joiner": "Zero-width joiner",
"codemirror-special-char-left-to-right-mark": "Left-to-right mark",
"codemirror-special-char-right-to-left-mark": "Right-to-left mark",
"codemirror-special-char-line-separator": "Line separator",
"codemirror-special-char-left-to-right-override": "Left-to-right override",
"codemirror-special-char-right-to-left-override": "Right-to-left override",
"codemirror-special-char-narrow-nbsp": "Narrow non-breaking space",
"codemirror-special-char-left-to-right-isolate": "Left-to-right isolate",
"codemirror-special-char-right-to-left-isolate": "Right-to-left isolate",
"codemirror-special-char-pop-directional-isolate": "Pop directional isolate",
"codemirror-special-char-paragraph-separator": "Paragraph separator",
"codemirror-special-char-zero-width-no-break-space": "Word joiner",
"codemirror-special-char-object-replacement": "Object replacement character",
"prefs-accessibility": "Accessibility"
}

View file

@ -22,5 +22,29 @@
"codemirror-replace": "Label for the 'replace' button in the CodeMirror search dialog.",
"codemirror-replace-placeholder": "Placeholder text for the 'Replace' input in the CodeMirror search dialog.",
"codemirror-replace-all": "Label for the 'replace all' button in the CodeMirror search dialog.",
"prefs-accessibility": "Section heading in the user prefences for accessibility topics."
"codemirror-control-character": "Tooltip text shown when hovering over special characters. $1 is the Unicode value of the special character.",
"codemirror-special-char-null": "Tooltip text shown when hovering over a null character. See [[wikidata:Q617945]] for possible translations.",
"codemirror-special-char-bell": "Tooltip text shown when hovering over a bell character. See [[wikidata:Q815674]] for possible translations.",
"codemirror-special-char-backspace": "Tooltip text shown when hovering over a backspace character. See [[wikidata:Q110028699]] for possible translations.",
"codemirror-special-char-newline": "Tooltip text shown when hovering over a newline character. See [[wikidata:Q184914]] for possible translations.",
"codemirror-special-char-vertical-tab": "Tooltip text shown when hovering over a vertical tab character. See [[wikidata:Q87529625]] for possible translations.",
"codemirror-special-char-carriage-return": "Tooltip text shown when hovering over a carriage return character. See [[wikidata:Q283976]] for possible translations.",
"codemirror-special-char-escape": "Tooltip text shown when hovering over an escape character. See [[wikidata:Q998991]] for possible translations.",
"codemirror-special-char-nbsp": "Tooltip text shown when hovering over an non-breaking space character. See [[wikidata:Q1053612]] for possible translations.",
"codemirror-special-char-zero-width-space": "Tooltip text shown when hovering over a zero-width space character. See [[wikidata:Q2604861]] for possible translations.",
"codemirror-special-char-zero-width-non-joiner": "Tooltip text shown when hovering over a zero-width non-joiner character. See [[wikidata:Q863569]] for possible translations.",
"codemirror-special-char-zero-width-joiner": "Tooltip text shown when hovering over a zero-width joiner character. See [[wikidata:Q614232]] for possible translations.",
"codemirror-special-char-left-to-right-mark": "Tooltip text shown when hovering over a left-to-right mark character. See [[wikidata:Q1022245]] for possible translations.",
"codemirror-special-char-right-to-left-mark": "Tooltip text shown when hovering over a right-to-left character. See [[wikidata:Q1017375]] for possible translations.",
"codemirror-special-char-line-separator": "Tooltip text shown when hovering over a line separator character. See [[wikidata:Q87523336]] for possible translations.",
"codemirror-special-char-left-to-right-override": "Tooltip text shown when hovering over a left-to-right override character. See [[wikidata:Q87523350]] for possible translations.",
"codemirror-special-char-right-to-left-override": "Tooltip text shown when hovering over a right-to-left override character. See [[wikidata:Q87523352]] for possible translations.",
"codemirror-special-char-narrow-nbsp": "Tooltip text shown when hovering over a narrow non-breaking space character. See [[wikidata:Q3058198]] for possible translations.",
"codemirror-special-char-left-to-right-isolate": "Tooltip text shown when hovering over a left-to-right isolate character. See [[wikidata:Q87523498]] for possible translations.",
"codemirror-special-char-right-to-left-isolate": "Tooltip text shown when hovering over a right-to-left isolate character. See [[wikidata:Q87523500]] for possible translations.",
"codemirror-special-char-pop-directional-isolate": "Tooltip text shown when hovering over a pop directional character. See [[wikidata:Q87523504]] for possible translations.",
"codemirror-special-char-paragraph-separator": "Tooltip text shown when hovering over a paragraph separator character. See [[wikidata:Q87523339]] for possible translations.",
"codemirror-special-char-zero-width-no-break-space": "Tooltip text shown when hovering over a zero-width word joiner character. See [[wikidata:Q8069466]] for possible translations.",
"codemirror-special-char-object-replacement": "Tooltip text shown when hovering over a object replacement character. See [[wikidata:Q9398047]] for possible translations.",
"prefs-accessibility": "Section heading in the user preferences for accessibility topics."
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,7 @@
@import 'mediawiki.mixins';
/* TODO: Remove styles below following CM6 upgrade, or move them to ext.CodeMirror.v6.WikiEditor.less */
.mw-editfont-monospace,
.mw-editfont-sans-serif,
.mw-editfont-serif {

View file

@ -8,3 +8,7 @@
box-shadow: inset 0 0 1px 1px #999;
font-weight: bold;
}
.cm-special-char-nbsp {
color: #888;
}

View file

@ -1,5 +1,5 @@
import { EditorState, Extension } from '@codemirror/state';
import { EditorView, lineNumbers } from '@codemirror/view';
import { EditorView, lineNumbers, highlightSpecialChars } from '@codemirror/view';
/**
* @class CodeMirror
@ -29,7 +29,8 @@ export default class CodeMirror {
get defaultExtensions() {
const extensions = [
this.contentAttributesExtension,
this.phrasesExtension
this.phrasesExtension,
this.specialCharsExtension
];
const namespaces = mw.config.get( 'wgCodeMirrorLineNumberingNamespaces' );
@ -78,7 +79,67 @@ export default class CodeMirror {
'by word': mw.msg( 'codemirror-by-word' ),
replace: mw.msg( 'codemirror-replace' ),
Replace: mw.msg( 'codemirror-replace-placeholder' ),
'replace all': mw.msg( 'codemirror-replace-all' )
'replace all': mw.msg( 'codemirror-replace-all' ),
'Control character': mw.msg( 'codemirror-control-character' )
} );
}
/**
* We give a small subset of special characters a tooltip explaining what they are.
* The messages and for what characters are defined here.
* Any character that does not have a message will instead use CM6 defaults,
* which is the localization of 'codemirror-control-character' followed by the Unicode number.
*
* @see https://codemirror.net/docs/ref/#view.highlightSpecialChars
* @return {Extension}
*/
get specialCharsExtension() {
// Keys are the decimal unicode number, values are the messages.
const messages = {
0: mw.msg( 'codemirror-special-char-null' ),
7: mw.msg( 'codemirror-special-char-bell' ),
8: mw.msg( 'codemirror-special-char-backspace' ),
10: mw.msg( 'codemirror-special-char-newline' ),
11: mw.msg( 'codemirror-special-char-vertical-tab' ),
13: mw.msg( 'codemirror-special-char-carriage-return' ),
27: mw.msg( 'codemirror-special-char-escape' ),
160: mw.msg( 'codemirror-special-char-nbsp' ),
8203: mw.msg( 'codemirror-special-char-zero-width-space' ),
8204: mw.msg( 'codemirror-special-char-zero-width-non-joiner' ),
8205: mw.msg( 'codemirror-special-char-zero-width-joiner' ),
8206: mw.msg( 'codemirror-special-char-left-to-right-mark' ),
8207: mw.msg( 'codemirror-special-char-right-to-left-mark' ),
8232: mw.msg( 'codemirror-special-char-line-separator' ),
8237: mw.msg( 'codemirror-special-char-left-to-right-override' ),
8238: mw.msg( 'codemirror-special-char-right-to-left-override' ),
8239: mw.msg( 'codemirror-special-char-narrow-nbsp' ),
8294: mw.msg( 'codemirror-special-char-left-to-right-isolate' ),
8295: mw.msg( 'codemirror-special-char-right-to-left-isolate' ),
8297: mw.msg( 'codemirror-special-char-pop-directional-isolate' ),
8233: mw.msg( 'codemirror-special-char-paragraph-separator' ),
65279: mw.msg( 'codemirror-special-char-zero-width-no-break-space' ),
65532: mw.msg( 'codemirror-special-char-object-replacement' )
};
return highlightSpecialChars( {
render: ( code, description, placeholder ) => {
description = messages[ code ] || mw.msg( 'codemirror-control-character', code );
const span = document.createElement( 'span' );
span.className = 'cm-specialChar';
// Special case non-breaking spaces (T181677).
if ( code === 160 || code === 8239 ) {
placeholder = '·';
span.className = 'cm-special-char-nbsp';
}
span.textContent = placeholder;
span.title = description;
span.setAttribute( 'aria-label', description );
return span;
},
// Highlight non-breaking spaces (T181677)
addSpecialChars: /\u00a0|\u202f/g
} );
}

View file

@ -142,6 +142,12 @@ const testCases = [
title: 'Extension tag with no TagMode',
input: '<myextension>foo\nbar\nbaz</myextension>',
output: '<div class="cm-line"><span class="cm-mw-exttag-bracket cm-mw-ext-myextension">&lt;</span><span class="cm-mw-exttag-name cm-mw-ext-myextension">myextension</span><span class="cm-mw-exttag-bracket cm-mw-ext-myextension">&gt;</span><span class="cm-mw-exttag">foo</span></div><div class="cm-line"><span class="cm-mw-exttag">bar</span></div><div class="cm-line"><span class="cm-mw-exttag">baz</span><span class="cm-mw-exttag-bracket cm-mw-ext-myextension">&lt;/</span><span class="cm-mw-exttag-name cm-mw-ext-myextension">myextension</span><span class="cm-mw-exttag-bracket cm-mw-ext-myextension">&gt;</span></div>'
},
{
title: 'Special characters',
input: 'Soft­hyphen\nzero-widthspace\nnon-breaking space\nnarrownbsp',
// i18n messages are the keys because we don't stub mw.msg() in this test.
output: '<div class="cm-line">Soft<img class="cm-widgetBuffer" aria-hidden="true"><span class="cm-specialChar" title="codemirror-control-character" aria-label="codemirror-control-character">•</span><img class="cm-widgetBuffer" aria-hidden="true">hyphen</div><div class="cm-line">zero-width<img class="cm-widgetBuffer" aria-hidden="true"><span class="cm-specialChar" title="codemirror-special-char-zero-width-space" aria-label="codemirror-special-char-zero-width-space">•</span><img class="cm-widgetBuffer" aria-hidden="true">space</div><div class="cm-line">non-breaking<img class="cm-widgetBuffer" aria-hidden="true"><span class="cm-special-char-nbsp" title="codemirror-special-char-nbsp" aria-label="codemirror-special-char-nbsp">·</span><img class="cm-widgetBuffer" aria-hidden="true">space</div><div class="cm-line">narrow<img class="cm-widgetBuffer" aria-hidden="true"><span class="cm-special-char-nbsp" title="codemirror-special-char-narrow-nbsp" aria-label="codemirror-special-char-narrow-nbsp">·</span><img class="cm-widgetBuffer" aria-hidden="true">nbsp</div>'
}
];