Wonderful! WordPress

プラグイン Simplistic Prism Highlighter Loader

Page No.3

« Gutenberg custom code block »

いやぁ、ずいぶんと難儀したので表題なんぞつけてしまいました。
前述したGutenberg block editor にhighlighter 用の custom block をresister するjavascript です。まとめるなりしてもっと簡潔に書けそうだとは思いますが、ちょっとくたびれたので、いまのところはこれでと。
各パラメータはサイドバーにて設定できます。

  • language: 言語指定、未入力でデフォルトはphp
  • line-number 表示
  • line-high-light: 強調したい行の指定
  • start-number: 行番号の始まりの番号指定
  • font-size: 親div のfont-size指定
  • title: 親div の枠に表示させるタイトル
  • random line-numvers: 自由設定のline-number

ただ、一つ注意点があり。 この block、実は自サイトで使ってみたところ、「更新に失敗しました。 返答が正しい JSON レスポンスではありません。」のエラーが出てしまいました。で、試しに標準装備の code block でもやってみたところ同様のエラーが排出されてしまいます。なので、これは block 自体の問題ではなく、サーバーの WAF によるものです。念の為、サーバーの WAF のログを確認したところ、しっかりと我が ip によるログが残ってました。これの対処法はネットで検索すれば教えて下さってるサイトがすぐにみつかります。.htaccess に自ip を無視する記述をするだけです。以下のよう。言わずもがな、ip(***.***.***.***) は自己 ip アドレスを入力するということ。

<IfModule mod_siteguard.c>
SiteGuard_User_ExcludeSig ip(***.***.***.***)
</IfModule>
Apache Configuration
CopyExpand
( function( blocks, element, components, blockEditor  ) {
    var el = element.createElement,
        Fragment = element.Fragment,
        registerBlockType = blocks.registerBlockType,
        InspectorControls = blockEditor.InspectorControls,
        TextControl = components.TextControl,
        CheckboxControl = components.CheckboxControl;
   
    registerBlockType( 'smplcprsm-hili/prism-hili', {
        title: 'Hilighter: pre-code',
        icon: 'smiley',
        category: 'layout',
        description: 'prism.js HighLighter block',

        attributes: {
            content: { type: 'string', source: 'html', selector: 'code' },
            linenumb: {
                type: 'boolean',
                default: true,
            },
            textlang: { type: 'string', default: '' },
            textline: { type: 'string', default: '' },
            textstart: { type: 'string', default: '' },
            textname: { type: 'string', default: '' },
            textnumber: { type: 'string', default: '' },
            textfont: { type: 'string', default: '' }
        },

        supports: {
            //save関数で返される要素に対する設定
            className: false, //(default:true)ブロック要素を作成した際に付く .wp-block-[ブロック名]で自動生成されるクラス名の設定。
        },

        edit: function( props ) {
            let content = props.attributes.content,
                linenumb = props.attributes.linenumb,
                textlang = props.attributes.textlang,
                textline = props.attributes.textline,
                textstart = props.attributes.textstart,
                textname = props.attributes.textname,
                textnumber = props.attributes.textnumber,
                textfont = props.attributes.textfont;

            return (
                el(
                    Fragment,
                    null,
                    el(
                        InspectorControls,
                        null,
                        el(
                            TextControl,
                            {
                                label: 'language',
                                help: 'e.g. php, javascript, css, html, empty default php',
                                value: textlang,
                                onChange: function ( newValue ) { props.setAttributes( { textlang: newValue } ); }
                            }
                        ),
                        el(
                            CheckboxControl,
                            {
                                heading: 'show line-numbers',
                                // label: 'Tick Me',
                                // help: 'Additional help text',
                                checked: linenumb,
                                onChange: function ( newValue ) { props.setAttributes( { linenumb: newValue } ); }
                            }
                        ),
                        el(
                            TextControl,
                            {
                                label: 'line-high-light',
                                help: 'e.g. 20,68,70,80-85',
                                value: textline,
                                onChange: function ( newValue ) { props.setAttributes( { textline: newValue } ); }
                            }
                        ),
                        el(
                            TextControl,
                            {
                                label: 'font-size',
                                help: 'parent div font-size em,px, etc.',
                                value: textfont,
                                onChange: function ( newValue ) { props.setAttributes( { textfont: newValue } ); }
                            }
                        ),
                        el(
                            TextControl,
                            {
                                label: 'start number',
                                help: 'start number of line-numbers',
                                value: textstart,
                                onChange: function ( newValue ) { props.setAttributes( { textstart: newValue } ); }
                            }
                        ),
                        el(
                            TextControl,
                            {
                                label: 'title',
                                help: 'code title e.g. file name, function name',
                                value: textname,
                                onChange: function ( newValue ) { props.setAttributes( { textname: newValue } ); }
                            }
                        ),
                        el(
                            TextControl,
                            {
                                label: 'random line-numbers',
                                help: 'e.g. 5-10,_2,25-35,_5,146,157,200-220<br>_ is no number line',
                                value: textnumber,
                                onChange: function ( newValue ) { props.setAttributes( { textnumber: newValue } ); }
                        ),
                    ),
                    el( wp.blockEditor.PlainText, {
                        className: props.className,
                        value: content ? content.replaceAll( '<', '<' ).replaceAll( '>', '>' ) : '',
                        onChange: function ( content ) { props.setAttributes( { content: content } ); },
                    })
                )
            );
        },

        save: function( props ) {
            let content = props.attributes.content,
                linenumb = props.attributes.linenumb ? 'line-numbers' : '',
                textlang = props.attributes.textlang,
                textline = props.attributes.textline,
                textstart = props.attributes.textstart,
                textname = props.attributes.textname,
                textnumber = props.attributes.textnumber,
                textfont = props.attributes.textfont,
                divprop = { className: 'highlighter_js' },
                preprop = {};
        
            textlang = 'language-' + ( textlang ? textlang.replace( /</g, '' ) : 'php' );
            if ( linenumb ) {
                preprop['className'] = linenumb;
            }
            if ( textline ) {
                preprop['data-line'] = textline.replace( /"/g, '' ).replace( /</g, '' );
            }
            if ( textstart ) {
                preprop['data-start'] = String( textstart ).replace( /"/g, '' ).replace( /</g, '' );
            }
            if ( textfont ) {
                divprop['style'] = 'font-size:' + textfont.replace( /"/g, '' ).replace( /</g, '' ) + ';';
            }
            if ( textname ) {
                divprop['data-name'] = textname.replace( /"/g, '' ).replace( /</g, '' );
            }
            if ( textnumber ) {
                divprop['data-numbers'] = textnumber.replace( /"/g, '' ).replace( /</g, '' );
            }

            return (
                el( 'div', divprop,
                    el( 'pre', preprop,
                        el( 'code', { className: textlang }, content )
                    )
                )
            );
        },
    } );
})(
    window.wp.blocks,
    window.wp.element,
    window.wp.components,
    window.wp.blockEditor
);

JavaScript
CopyExpand

次のjavascript は、付加機能分。拡大ウィンドウ表示のexpand とか、矢印によるウィンドウ内の横スクロールとか。
で、実はこの部分、最初は jQuery で書いていたのです。が、後になってやった各ページのスピードアップのためのこと。少しでもリクエストを少なくするための jQuery ファイルの読み込みを無くすために、プレーンな javascript へと書き直しています。そしてそのときに気がついたことが一つあって。
prism.js をダウンロードするときに、追加機能として Copy to Clipboard Button プラグインにチェックを入れると、Copy ボタンが表示され、クリップボードにコードをコピーする機能が使えるようになります。で、実は prism.js はこのクリップボードコピー機能を装備するために、clipboard.js という外部プラグインを利用してる。そのファイルは、prism.js のコードのなかで、動的に script タグを作ってロードさせてる。このやり方だと、ノンブロッキングなロード方法で良いのだけれどctrl + u キーでのソースコード表示においては表示されないので全く気がつかなかったのであります。ディベロッパーツールにおいて head タグの内容を調べていて、あれっ!なんだこれは?と、見に覚えのないファイルの存在にやっと気がつくことになったわけです。
これだけ優れた機能の prism.js が、クリップボードコピーぐらいのことで、まさか別ファイルのプラグインを使っているなどということは、夢にも思わなかったのでありますね。
と、いうことで、クリップボードコピー機能は自機能として装備することにして、clipboard.js にはご遠慮願うことと相成りました。どのみち、prism.js をダウンロードし直さないといけないので、ついでに必要なだけの言語に絞ってみたのですが、さぞやファイルサイズが小さくなるだろうと思いきや、これが全く変わらずでした。ちなみにバージョンはといえば、1.20.0 -> 1.23.0 になってました。

それと、後になってページ表示の高速化のためにやったキャッシュ化のために、スクリーンサイズなどで変化するラインハイライトの位置修正の面倒もみなくてはならなくなって、その処理も加わってる。もともと prism.js が作ってくれたラインナンバー用の span タグから行の高さを得る方法で間に合わせてる。

window.addEventListener( 'load', function() {

    const doc = document,
        hls = doc.getElementsByClassName( 'highlighter_js' ),
        hls_len = hls.length,
        winwidth = window.innerWidth,
        narrow = winwidth < 771 ? true : false;

    let sss_hl_open_flg = 0;

    if ( hls_len ) {
        for ( let i = 0; i < hls_len; ++i ) {
            const mark = hls[ i ].getAttribute( 'data-numbers' ),
                parent_height =  parseInt( hls[ i ].clientHeight ),
                tartag = hls[ i ].getElementsByTagName( 'code' ),
                winheibasis = window.innerHeight * 0.8,
                parr_style = getComputedStyle( hls[ i ] ),
                dname = hls[ i ].getAttribute( 'data-name' );

            let parr_width = parseInt( parr_style.width ),
                arwflg = '',
                codescrlwidth = 0;

            const tarpre = hls[ i ].getElementsByTagName( 'pre' ),
                    linehighlight = tarpre[0].getElementsByClassName( 'line-highlight' ),
                    lh_len = linehighlight.length;

            if ( lh_len ) {
                const linenums =  tartag[0].getElementsByClassName( 'line-numbers-rows' ),
                    lnspans = linenums[0].getElementsByTagName( 'span' ),
                    lnsp_hei = parseFloat ( window.getComputedStyle( lnspans[0] ).height ),
                    datastart = null !== tarpre[0].getAttribute( 'data-start' ) ? parseInt( tarpre[0].getAttribute( 'data-start' ) ) : 1;

                for ( let j = 0; j < lh_len; j++ ) {
                    const datarange = linehighlight[ j ].getAttribute( 'data-range' ),
                        rangedata = datarange.split( '-' ),
                        rangecount = rangedata.length === 2 ? ( rangedata[1] - rangedata[0] + 1 ) : 1;

                    linehighlight[ j ].style.cssText = 'top:' + ( lnsp_hei * ( rangedata[0] - datastart ) ) + 'px;height:' + ( lnsp_hei * rangecount ) + 'px;'; 
                }
            }

            if ( tartag[0] ) {
                codescrlwidth = tartag[0].scrollWidth;
                tartag[0].style.scrollbarWidth = 'thin';
            }

           // code ウィンドウ内に横スクロールのための矢印ボタンをウィンドウの縦サイズを考慮して配置
            if ( ! narrow ) {
                if ( codescrlwidth > parr_width ) {
                    let posrasio = Math.floor( parent_height / winheibasis ),
                        toppos = 100 / ( posrasio + 1 ),
                        eachtoppos;
                    
                    if ( posrasio < 1 ) {
                        toppos = 50;
                        posrasio = 1;
                    }

                    for ( let i = 0; i < posrasio; i++ ) {
                        eachtoppos = Math.floor( toppos * ( i + 1 ) );
                        arwflg += '<span class="scrlarw lscrlarw" style="top:' + eachtoppos + '%;">‹</span><span class="scrlarw rscrlarw" style="top:' + eachtoppos + '%;">›</span>';
                    }
                }
            }
        
    		// data-numbers に指定がある場合は、独自指定のラインナンバーを表示
            if ( mark ) {

                parr_width -= 40;
                if ( parr_width > window.innerWidth ) {
                    parr_width = window.innerWidth * 0.8;
                }
                
                hls[ i ].style.width = parr_width + 'px';

                const tarcont = tartag[0].innerText;

                let lines,
                    lines_len = 0,
                    tmpmark = [],
                    mark_len = 0;

                lines = tarcont.split( '\n' );
                lines_len = lines.length;

                console.log( lines_len );

                const hlback = doc.createElement( 'div' );

                hlback.setAttribute( 'id', 'hl_back' + i );
                hlback.setAttribute( 'class', 'hl_back' );
        
                tartag[0].appendChild( hlback );

                const tarson = hlback;

                if ( -1 === mark.indexOf( ',' ) ) {
                    tmpmark[0] = mark; 
                } else {
                    tmpmark = mark.split( ',' );
                }

                mark_len = tmpmark.length;
                for ( let k = 0; k < mark_len; ++k ) {
                    if ( -1 !== tmpmark[ k ].indexOf( '-' ) ) {
                        let tmpval = tmpmark[ k ].split( '-' );
                        for ( let h = parseInt( tmpval[0] ); h <= parseInt( tmpval[1] ); h++ ) {
                            tarson.innerHTML = tarson.innerHTML + '<span class="hl_line">' + h + '</span>';
                        }
                    } else if ( -1 !== tmpmark[ k ].indexOf( '_' ) ) {
                        let tmpval = parseInt( tmpmark[ k ].replace( '_', '' ) );
                        for ( let h = 0; h < tmpval; ++h ) {
                            tarson.innerHTML = tarson.innerHTML + '<span class="hl_line"> </span>';
                        }
                    } else {
                        tarson.innerHTML = tarson.innerHTML + '<span class="hl_line">' + tmpmark[ k ] + '</span>';
                    }
                }
            }
			// copy ボタンと expand ボタンの登録
            hls[ i ].innerHTML = hls[ i ].innerHTML + '<span class="hl_copy" data-idnum="' + i + '">Copy</span><span class="hl_expand" data-idnum="' + i + '">Expand</span>' + arwflg;

            if ( dname ) {
                const dname_len = dname.length;

                if ( dname_len > 32 ) { 
                    hls[ i ].style.setProperty( '--vis-val', 'hidden' );
                }
            }
        }

        let pver = hls[0].getAttribute( 'data-ver' );
                
        pver = pver ? pver : '1.23.0';
        console.log( 'prism.js ver.', pver );

        if ( 0 === sss_hl_open_flg ) {
            // 拡大ウィンドウ表示のための要素の設定
            const wscrn = doc.getElementById( 'hl_tx_expand' ),
                wscrnstyle = { 
                    display: 'none',
                    position: 'fixed',
                    width: '100%',
                    height: '100%',
                    top:'0px',
                    left: '0px',
                    padding: '10px 30px',
                    textAlign: 'center',
                    background: 'rgba(0,0,0,0.5)',
                    zIndex: '0'
                },
                stylkeys = Object.keys( wscrnstyle ),
                stylkeys_len = stylkeys.length;

           // この方法で設定したスタイルはインラインとなり、後に cssText を使用した場合、消去され無くなってしまう
            for ( let i = 0; i < stylkeys_len; ++i ) {
                wscrn.style[ stylkeys[ i ] ] = wscrnstyle[ stylkeys[ i ] ];
            }
            
            wscrn.innerHTML = '<textarea id="hl_expand_tx"></textarea><p id="hl_dell_expand">x</p>';

            const expand = doc.getElementById( 'hl_expand_tx' ),
                exstyle = {
                    width: '0vw',
                    height: '0vh',
                    margin: '10px auto',
                    padding: '20px',
                    lineHeight: '1.4em',
                    background: 'white',
                    opacity: '0',
                    transition: 'width 0.7s, height 1.0s'
                },
                exstylekeys = Object.keys( exstyle ),
                exstyle_len = exstylekeys.length;

            for ( let i = 0; i < exstyle_len; ++i ) {
                expand.style[ exstylekeys[ i ] ] = exstyle[ exstylekeys[ i ] ];
            }

            doc.getElementById( 'hl_dell_expand' ).style.cssText = 'position:absolute;width:28px;height:28px;top:30px;right:100px;padding:10px;border-radius:5px;background:white;box-shadow:2px 2px 2px 2px #cccccc;';

            sss_hl_open_flg = 2;
        }

        // 拡大表示ボタンを click した時の処理
        const hlexp = doc.getElementsByClassName( 'hl_expand' ),
            hlexp_len = hlexp.length;

        if ( hlexp_len ) {
            for ( let i = 0; i < hlexp_len; ++i ) {
                hlexp[ i ].onclick = function() {
                    const parr = hlexp[ i ].parentNode,
                        bro_code = parr.getElementsByTagName( 'code' ),
                        bro_cont = bro_code[0].innerText,
                        bro_id = parseInt( hlexp[ i ].getAttribute( 'data-idnum' ) );
    
                    if ( 2 === sss_hl_open_flg ) {
                        const hlexptx = doc.getElementById( 'hl_expand_tx' ),
                            hltxexp = doc.getElementById( 'hl_tx_expand' );

                        hlexptx.value = bro_cont;

                        hltxexp.style.display = 'block';
                        hltxexp.style.zIndex = 99;

                        setTimeout( function() {
                            hlexptx.style.width = '90vw';
                            hlexptx.style.height = '90vh';
                            hlexptx.style.opacity = 1;
                            hlexptx.focus();
                        }, 500 );
                        sss_hl_open_flg = 1;
                    }
                }
            }
        }

        // copy ボタンを押した時にクリップボードにコピーする処理
		// 一度、データを拡大表示用の textarea 要素に貼り付け、選択状態にしてデータをコピー
        const hlcpy = doc.getElementsByClassName( 'hl_copy' ),
        hlcpy_len = hlcpy.length;

        if ( hlcpy_len ) {
            for ( let i = 0; i < hlcpy_len; i++ ) {
                hlcpy[ i ].onclick = function() {
                    const parr = hlcpy[ i ].parentNode,
                        bro_code = parr.getElementsByTagName( 'code' ),
                        bro_cont = bro_code[0].innerText,
                        hlexptx = doc.getElementById( 'hl_expand_tx' ),
                        hltxexp = doc.getElementById( 'hl_tx_expand' );

                    hlexptx.value = bro_cont;

                    hltxexp.style.display = 'block';
                    hlexptx.style.opacity = 1;

                    hlexptx.focus();
                    hlexptx.select();

                    // execCommand は非推奨となった
                    // document.execCommand( 'copy' );
                    // その代替えは navigator.clipboard.writeText()
                    navigator.clipboard.writeText( hlexptx.value );

                    hlcpy[ i ].innerText = 'Copied';
                    setTimeout( function() {
                        hlcpy[ i ].innerText = 'Copy';
                    }, 1000 );

                    hlexptx.value = '';
                    hltxexp.style.display = 'none';
                    hlexptx.style.opacity = 0;
                }
            }
        }

        // 拡大表示用のウィンドウを閉じるための処理
        doc.getElementById( 'hl_dell_expand' ).onclick = function() {
            const hlexptx = doc.getElementById( 'hl_expand_tx' ),
                hltxexp = doc.getElementById( 'hl_tx_expand' );

            hlexptx.style.width = '0vw';
            hlexptx.style.height = '0vh';

            setTimeout( function() {
                hlexptx.style.opacity = 0;
                hltxexp.style.display = 'none';
                hltxexp.style.zIndex = 0;
            }, 500 );
            sss_hl_open_flg = 2;
        };

        // 矢印にて横スクロールさせる処理
        const rarws = doc.getElementsByClassName( 'rscrlarw' ),
            rarws_len = rarws.length;

        if ( rarws_len ) {
            for ( let i = 0; i < rarws_len; i++ ) {
                rarws[ i ].onclick = function() {
                    const parr = rarws[ i ].parentNode,
                        bro_code = parr.getElementsByTagName( 'code' );
                    
                    sideScroll( bro_code[0], 1 );
                }
            }
        }    

        const larws = doc.getElementsByClassName( 'lscrlarw' ),
            larws_len = larws.length;

        if ( larws_len ) {
            for ( let i = 0; i < larws_len; i++ ) {
                larws[ i ].onclick = function() {
                    const parr = larws[ i ].parentNode,
                        bro_code = parr.getElementsByTagName( 'code' );

                    sideScroll( bro_code[0], -1 );
                }
            }
        }    
    }

	// code ウィンドウ内、横スクロール関数
    function sideScroll( bro_code, ope ){
        const codescrlwidth = bro_code.scrollWidth,
            moveval = 30;

        let currentPos = bro_code.scrollLeft,
            intID = null,
            tmpv = 0;

        if ( currentPos < codescrlwidth || currentPos > 0 ) {
            intID = setInterval( function() {
                if ( currentPos > codescrlwidth || currentPos < 0 ) {
                    clearInterval( intID );
                }

                currentPos += tmpv * ope;
                bro_code.scrollLeft = currentPos;
                tmpv++;

                if ( tmpv > moveval ) {
                    clearInterval( intID );
                    intID = null;
                }
            }, 30 );
        }
    }
});
JavaScript
CopyExpand

以上です。
とりあえずローカルな環境のWordPress とロリポップ(このサイト)でテストしましたが問題無く動いています。
このページを公開したのは2020年7月29日。編集したのが2021年3月21日。今のところここのサイトの highlighter はこのプラグインで表示しています。そのうちビルトインタイプに戻すつもりではあります。
問題無く動くと思うんですけど。動かなかったらご勘弁ください。
何も起こりえないのですが、一応、決まり事を。
このプラグインを使用する場合は完全自己責任で行ってください。
何らかの損害が発生しても一切関知しません。
下のリンクからダウンロードできると思います。
wordpress6.1PHP8.2 で確認済です。 Simplistic Prism Highlighter Loader Plugin

Sanbanse Funabashi
2010.10.24 sunrise

Top

スクロールさせるか画像をクリックすると元に戻ります。