彩りの大地 Laboratory /HTMLをMarkdown形式でコピーするスクリプト

HTMLをMarkdown形式でコピーするスクリプト

実際じっさいコピーしてみてください

HTML要素をコピーした際にMarkdown形式やルビ付きテキストとしてクリップボードに保持されるようにアクセシビリティ対応構造へ変換するスクリプトです。
当サイトの基本として組み込まれているため、いろんなページで試してみてもいいかもしれません。

最近はGeminiさんなどの生成AIさんに「こんなスクリプト作りたい!」って言えば作ってくれるのであまり意味を成しませんが、一応。

CSS

.sr-only-copy{
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}
Javascript

// サイト内の要素をコピーした際、Markdown形式やルビ付きテキストとして
// クリップボードに保持されるようにアクセシビリティ対応構造へ変換する(完全版)
const initCopySupport = () => {
    // 1. ルビ(ruby)タグの処理
    document.querySelectorAll('ruby').forEach(ruby => {
        if (ruby.getAttribute('data-copy-processed')) return;
        
        const rtTags = ruby.querySelectorAll('rt');
        if (rtTags.length === 0) return;
        
        rtTags.forEach(rt => {
            // スクリーンリーダーには無視させ、コピー時のみ有効にする
            rt.setAttribute('aria-hidden', 'true');
            
            // 開始括弧 (
            const openParen = document.createElement('span');
            openParen.className = 'sr-only-copy';
            openParen.textContent = '(';
            rt.before(openParen);
            
            // 終了括弧とコピー用テキストルビ )
            const closeParen = document.createElement('span');
            closeParen.className = 'sr-only-copy';
            closeParen.textContent = `${rt.textContent})`;
            rt.after(closeParen);
        });
        
        ruby.setAttribute('data-copy-processed', 'true');
    });
    
    // 2. 前後を記号で囲む要素(インライン装飾など)
    const wrapTagMap = {
        'strong': '**',     'b': '**',
        'em': '_',          'i': '_',
        'del': '~~',        's': '~~',
        'ins': '~',         'u': '~',
        'code': '`'
    };
    
    Object.entries(wrapTagMap).forEach(([tagName, symbol]) => {
        document.querySelectorAll(tagName).forEach(el => {
            if (el.getAttribute('data-copy-processed')) return;
            
            const prefix = document.createElement('span');
            prefix.className = 'sr-only-copy';
            prefix.textContent = symbol;
            el.prepend(prefix);
            
            const suffix = document.createElement('span');
            suffix.className = 'sr-only-copy';
            suffix.textContent = symbol;
            el.append(suffix);
            
            el.setAttribute('data-copy-processed', 'true');
        });
    });
    
    // 3. 前方にのみ記号を付与する要素(見出しなど)
    // ※Markdownの仕様に合わせ、# や - の後ろに自動で半角スペースが入るようにしています
    const headingTagMap = {
        'h1': '# ', 'h2': '## ', 'h3': '### ', 'h4': '#### '
    };
    
    Object.entries(headingTagMap).forEach(([tagName, symbol]) => {
        document.querySelectorAll(tagName).forEach(el => {
            if (el.getAttribute('data-copy-processed')) return;
            
            const prefix = document.createElement('span');
            prefix.className = 'sr-only-copy';
            prefix.textContent = symbol;
            el.prepend(prefix);
            
            el.setAttribute('data-copy-processed', 'true');
        });
    });
    
    // 4. 【新規】リスト要素(li)の処理(ul / ol を自動判別)
    document.querySelectorAll('li').forEach(li => {
        if (li.getAttribute('data-copy-processed')) return;
        
        // 親要素が ol(順序付きリスト)かどうかを判定
        const parent = li.closest('ol, ul');
        let symbol = '- '; // デフォルトは通常のリスト記号
        
        if (parent && parent.tagName.toLowerCase() === 'ol') {
            // olの中にあるli要素のうち、自分が何番目か(1から始まるインデックス)を取得
            const index = Array.from(parent.querySelectorAll(':scope > li')).indexOf(li) + 1;
            symbol = `${index}. `; // 「1. 」「2. 」という形にする
        }
        
        const prefix = document.createElement('span');
        prefix.className = 'sr-only-copy';
        prefix.textContent = symbol;
        li.prepend(prefix);
        
        li.setAttribute('data-copy-processed', 'true');
    });
    
    // 5. 【新規】リンク要素(a)の処理 -> [テキスト](URL)
    document.querySelectorAll('a').forEach(a => {
        if (a.getAttribute('data-copy-processed')) return;
        
        // href属性の値を表示したい場合はこちら
        // const href = a.getAttribute('href') || '';
        
        // リンク先のURLをフルパスで表示したい場合はこちら
        const href = a.href || '';
        
        // 前方に [ を追加
        const prefix = document.createElement('span');
        prefix.className = 'sr-only-copy';
        prefix.textContent = '[';
        a.prepend(prefix);
        
        // 後方に ](URL) を追加
        const suffix = document.createElement('span');
        suffix.className = 'sr-only-copy';
        suffix.textContent = `]`;
        
        // リンク先のURLをフルパスで表示したい場合はこちら
        suffix.textContent += `(${href})`;
        
        // href属性の値を表示したい場合はこちら(内部リンク(相対パス)の場合はURL非表示)
        // if(href.startsWith('http')){
        //  suffix.textContent += `(${href})`;
        // }
        
        a.append(suffix);
        
        a.setAttribute('data-copy-processed', 'true');
    });
    
    // 6. 【新規】画像要素(img)の処理 -> ![代替テキスト](URL)
    document.querySelectorAll('img').forEach(img => {
        if (img.getAttribute('data-copy-processed')) return;
        
        // href属性の値を表示したい場合はこちら
        // const src = img.getAttribute('src') || '';
        
        // リンク先のURLをフルパスで表示したい場合はこちら
        const src = img.src || '';
        
        const alt = img.getAttribute('alt') || '';
        
        // imgの直後に Markdown表現用の非表示spanを挿入
        const mdText = document.createElement('span');
        mdText.className = 'sr-only-copy';
        mdText.textContent = `![${alt}]`;
        
        // リンク先のURLをフルパスで表示したい場合はこちら
        mdText.textContent += `(${src})`;
        
        // href属性の値を表示したい場合はこちら(内部リンク(相対パス)の場合はURL非表示)
        // if(src.startsWith('http')){
        //  mdText.textContent += `(${src})`;
        // }
        
        // 画像の後ろに配置(画像そのもののテキスト選択はできないため、その位置に挿入)
        img.after(mdText);
        
        img.setAttribute('data-copy-processed', 'true');
    });
};

window.addEventListener('load', function() { 
    initCopySupport();
},false);

書いた内容をコピーした際に単なるテキストにするだけでは物足りなかったので思い切ってMarkDown形式になるように考えたのがきっかけ。 具体的にはルビの内容もコピーできたらいいなというのが最初の動機。
じゃあ、どうせならと思って現行の形に至るということである。

上述している通り、Geminiさんに質問して作ってもらったスクリプト。 しっかりと形になった要で何よりである。
とまあこのように、Javascriptの1つや2つを作る程度ならわけないようだ。いい時代になったものだ。

blockquote要素が入っていないが、 当サイトではそもそも使っていない(使う予定もない)要素だし、わざわざつけるまでもないのでこの形でフィニッシュしている。 つけたければ自分でやってね。