Implement copy buttons for code blocks

<syntaxhighlight> blocks with a boolean "copy" param will now have a
button next to them for copying the code to the clipboard. Not
applicable for inline code blocks.

Adapted from the mediawiki.org gadget written by Krinkle.

Bug: T40932
Change-Id: Ic8ef030514c3b6dd2cb9b137f032588869ab3762
This commit is contained in:
Siddharth VP 2024-09-11 00:49:06 +05:30
parent f2d0dde88c
commit 55630cc5ea
6 changed files with 92 additions and 3 deletions

View file

@ -41,7 +41,15 @@
"ext.pygments.view": { "ext.pygments.view": {
"scripts": [ "scripts": [
"pygments.linenumbers.js", "pygments.linenumbers.js",
"pygments.links.js" "pygments.links.js",
"pygments.copy.js"
],
"styles": [
"pygments.copy.css"
],
"messages": [
"syntaxhighlight-button-copy",
"syntaxhighlight-button-copied"
], ],
"dependencies": [ "dependencies": [
"mediawiki.util" "mediawiki.util"

View file

@ -20,5 +20,7 @@
"syntaxhighlight-visualeditor-mwsyntaxhighlightinspector-title": "Code block", "syntaxhighlight-visualeditor-mwsyntaxhighlightinspector-title": "Code block",
"syntaxhighlight-error-pygments-invocation-failure": "Failed to invoke Pygments", "syntaxhighlight-error-pygments-invocation-failure": "Failed to invoke Pygments",
"syntaxhighlight-error-unknown-language": "Unknown language \"$1\"", "syntaxhighlight-error-unknown-language": "Unknown language \"$1\"",
"syntaxhighlight-error-exceeds-size-limit": "Code size of $1 {{PLURAL:$1|bytes}} exceeds allowed maximum of $2 {{PLURAL:$2|bytes}}" "syntaxhighlight-error-exceeds-size-limit": "Code size of $1 {{PLURAL:$1|bytes}} exceeds allowed maximum of $2 {{PLURAL:$2|bytes}}",
"syntaxhighlight-button-copy": "Copy",
"syntaxhighlight-button-copied": "Copied!"
} }

View file

@ -29,5 +29,7 @@
"syntaxhighlight-visualeditor-mwsyntaxhighlightinspector-title": "Title for the VisualEditor syntax highlighter inspector, above the text area for the code", "syntaxhighlight-visualeditor-mwsyntaxhighlightinspector-title": "Title for the VisualEditor syntax highlighter inspector, above the text area for the code",
"syntaxhighlight-error-pygments-invocation-failure": "Error message shown when the syntax highlighting library failed with an unspecified error", "syntaxhighlight-error-pygments-invocation-failure": "Error message shown when the syntax highlighting library failed with an unspecified error",
"syntaxhighlight-error-unknown-language": "Error message shown when the programming language name was not recognized by the syntax highlighting library. Parameters:\n* $1 - the language name", "syntaxhighlight-error-unknown-language": "Error message shown when the programming language name was not recognized by the syntax highlighting library. Parameters:\n* $1 - the language name",
"syntaxhighlight-error-exceeds-size-limit": "Error message shown when the block of code to be highlighted exceeds the size limit. Parameters:\n* $1 - the size of the code block, in bytes\n* $2 - the maximum size allowed, in bytes." "syntaxhighlight-error-exceeds-size-limit": "Error message shown when the block of code to be highlighted exceeds the size limit. Parameters:\n* $1 - the size of the code block, in bytes\n* $2 - the maximum size allowed, in bytes.",
"syntaxhighlight-button-copy": "Label for button to copy the highlighted code",
"syntaxhighlight-button-copied": "Label shown when the highlighted code has been copied"
} }

View file

@ -387,6 +387,7 @@ class SyntaxHighlight extends ExtensionTagHandler implements
* If it contains a 'inline' key, the output will not be wrapped in `<div><pre/></div>`. * If it contains a 'inline' key, the output will not be wrapped in `<div><pre/></div>`.
* If it contains a 'linelinks' key, lines will have links and anchors with a prefix * If it contains a 'linelinks' key, lines will have links and anchors with a prefix
* of the value. Similar to the lineanchors+linespans features in Pygments. * of the value. Similar to the lineanchors+linespans features in Pygments.
* If it contains a 'copy' key, a link will be shown for copying content to the clipboard.
* @param Parser|null $parser Parser, if generating content to be parsed. * @param Parser|null $parser Parser, if generating content to be parsed.
* @return Status Status object, with HTML representing the highlighted * @return Status Status object, with HTML representing the highlighted
* code as its value. * code as its value.
@ -440,6 +441,9 @@ class SyntaxHighlight extends ExtensionTagHandler implements
if ( $showLines ) { if ( $showLines ) {
$classList[] = self::HIGHLIGHT_CSS_CLASS . '-lines'; $classList[] = self::HIGHLIGHT_CSS_CLASS . '-lines';
} }
if ( !$isInline && isset( $args['copy'] ) ) {
$classList[] = 'mw-highlight-copy';
}
$htmlAttribs['class'] = implode( ' ', $classList ); $htmlAttribs['class'] = implode( ' ', $classList );
$htmlAttribs['dir'] = $dir; $htmlAttribs['dir'] = $dir;
'@phan-var array{class:string,dir:string} $htmlAttribs'; '@phan-var array{class:string,dir:string} $htmlAttribs';

38
modules/pygments.copy.css Normal file
View file

@ -0,0 +1,38 @@
/**
* Adapted from https://www.mediawiki.org/wiki/MediaWiki:Gadget-site-tpl-copy.css
* Original author: Krinkle
*/
.mw-highlight-copy {
position: relative;
min-width: 40%;
max-width: max-content;
}
/**
* https://doc.wikimedia.org/oojs-ui/master/demos/ (2023-11-13)
* https://design.wikimedia.org/style-guide/components/buttons.html
*/
.mw-highlight-copy--bound button {
position: absolute;
top: -1em;
right: -0.9em;
box-sizing: border-box;
padding: 5px 7px;
border-radius: 2px;
background: #fff;
color: #36c;
border: 1px solid #eaecf0;
}
.mw-highlight-copy--bound button:hover {
cursor: pointer;
background: #fff;
border: 1px solid #447ff5;
}
.mw-highlight-copy--bound button:active {
background: #eff3fa;
color: #2a4b8d;
border-color: #2a4b8d;
}

35
modules/pygments.copy.js Normal file
View file

@ -0,0 +1,35 @@
/**
* Adapted from https://www.mediawiki.org/wiki/MediaWiki:Gadget-site-tpl-copy.js
* Original author: Krinkle
*/
// eslint-disable-next-line compat/compat
const hasFeature = navigator.clipboard && 'writeText' in navigator.clipboard;
if ( hasFeature ) {
// Add type=button to avoid form submission in preview
const $btn = $( '<button>' )
.attr( 'type', 'button' )
.text( mw.msg( 'syntaxhighlight-button-copy' ) )
.on( 'click', function () {
const btn = this;
const wrapper = btn.closest( '.mw-highlight-copy' );
const preNode = wrapper && wrapper.querySelector( 'pre' );
const content = preNode && preNode.textContent.trim();
try {
navigator.clipboard.writeText( content );
} catch ( e ) {
return;
}
const prevLabel = btn.textContent;
btn.textContent = mw.msg( 'syntaxhighlight-button-copied' );
setTimeout( () => {
btn.textContent = prevLabel;
}, 5000 );
} );
mw.hook( 'wikipage.content' ).add( ( $content ) => {
$content.find( '.mw-highlight-copy:not(.mw-highlight-copy--bound)' )
.append( $btn.clone( true ) )
.addClass( 'mw-highlight-copy--bound' );
} );
}