📒

WindowsでコピーしたMarkdownテキストをHTMLに変換するスクリプト

に公開

PowerShellスクリプトです。

すでにコピーしているピュアなMarkdown形式のテキストをリッチなHTMLに変換できます。

これがあると、LLMに生成させたMarkdownテキストを快適に&適切に各種エディターにペーストできます。

WordPress, Ghost, Google Docs, Microsoft Wordのようなエディタを使っていて、LLMに色々書かせたい人には重宝するかと思います。LLMといってもAPIでどこか自分のDBに保存していたりしないと必要にはならないかもですが。

意外とClaudeが一発で書けなかったので何度か修正して作成しました。

# ==============================================================================
# Markdown to Rich Text Clipboard Converter
# クリップボード内のMarkdownをHTMLに変換し、リッチテキスト形式で上書きします
# ==============================================================================

# システムアセンブリの読み込み(フォーム処理用)
Add-Type -AssemblyName System.Windows.Forms

# ==============================================================================
# Win32 API定義(クリップボード操作用)
# ==============================================================================
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

public class ClipboardHelper {
    // クリップボードのオープン・クローズ・初期化
    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool OpenClipboard(IntPtr hWndNewOwner);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool CloseClipboard();

    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool EmptyClipboard();

    // クリップボードへのデータ設定
    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);

    // クリップボード形式のID取得(カスタムフォーマット用)
    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint RegisterClipboardFormat(string lpszFormat);

    // メモリ割り当て・ロック・アンロック
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);

    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr GlobalLock(IntPtr hMem);

    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern bool GlobalUnlock(IntPtr hMem);

    // メモリフラグと形式定数
    public const uint GMEM_MOVEABLE = 0x0002;     // 移動可能なメモリ
    public const uint CF_UNICODETEXT = 13;        // Unicode形式のテキスト
}
"@

# ==============================================================================
# 入力値の取得と検証
# ==============================================================================
# クリップボードからMarkdownを取得
$markdown = Get-Clipboard -Raw

# クリップボードが空の場合は処理を中止
if (-not $markdown) {
    Write-Host "Clipboard is empty" -ForegroundColor Red
    exit
}

# ==============================================================================
# Markdown → HTML変換関数
# ==============================================================================
function Convert-MarkdownToHtml {
    param([string]$md)

    $html = $md

    # 見出しの変換(h6~h1、最大から最小の順)
    $html = $html -replace '(?m)^###### (.+)$', '<h6>$1</h6>'
    $html = $html -replace '(?m)^##### (.+)$', '<h5>$1</h5>'
    $html = $html -replace '(?m)^#### (.+)$', '<h4>$1</h4>'
    $html = $html -replace '(?m)^### (.+)$', '<h3>$1</h3>'
    $html = $html -replace '(?m)^## (.+)$', '<h2>$1</h2>'
    $html = $html -replace '(?m)^# (.+)$', '<h1>$1</h1>'

    # テキスト装飾の変換(***太字イタリック → ***イタリック太字)
    $html = $html -replace '\*\*\*(.+?)\*\*\*', '<strong><em>$1</em></strong>'
    $html = $html -replace '\*\*(.+?)\*\*', '<strong>$1</strong>'
    $html = $html -replace '\*(.+?)\*', '<em>$1</em>'

    # インラインコードの変換
    $html = $html -replace '`([^`]+)`', '<code>$1</code>'

    # リンクの変換
    $html = $html -replace '\[([^\]]+)\]\(([^\)]+)\)', '<a href="$2">$1</a>'

    # リストアイテムの変換(箇条書き・番号付きリスト両方)
    $html = $html -replace '(?m)^- (.+)$', '<li>$1</li>'
    $html = $html -replace '(?m)^\d+\. (.+)$', '<li>$1</li>'

    # 水平線の変換
    $html = $html -replace '(?m)^---+$', '<hr>'

    # 通常のテキスト行を段落タグで囲む(既にタグがある行は除外)
    $html = $html -replace '(?m)^(?!<[hlu]|<li|<hr)(.+)$', '<p>$1</p>'

    # 連続したli要素をul要素で囲む
    $html = $html -replace '((?:<li>.*</li>\s*)+)', '<ul>$1</ul>'

    # 空の段落タグを削除
    $html = $html -replace '<p>\s*</p>', ''

    return $html
}

# 変換実行
$html = Convert-MarkdownToHtml $markdown

# ==============================================================================
# CF_HTML形式の作成関数
# ==============================================================================
# Windows上でリッチテキストとして認識されるHTML形式を生成します
# 参考: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
function Create-CFHtml {
    param([string]$body)

    # HTMLテンプレートの作成
    $pre = '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><!--StartFragment-->'
    $post = '<!--EndFragment--></body></html>'
    $htmlContent = $pre + $body + $post

    # CF_HTML形式のヘッダテンプレート(バイト位置情報を含む)
    $hdrFmt = "Version:0.9`r`nStartHTML:{0:D10}`r`nEndHTML:{1:D10}`r`nStartFragment:{2:D10}`r`nEndFragment:{3:D10}`r`n"

    # 初期値でヘッダを作成
    $hdr = [string]::Format($hdrFmt, 0, 0, 0, 0)
    $hdrLen = [System.Text.Encoding]::UTF8.GetByteCount($hdr)

    # HTMLコンテンツをバイト配列に変換して長さを計算
    $contentBytes = [System.Text.Encoding]::UTF8.GetBytes($htmlContent)

    # バイト位置を計算
    $startHtml = $hdrLen
    $endHtml = $hdrLen + $contentBytes.Length

    # フラグメント位置を計算(<!--StartFragment-->と<!--EndFragment-->の位置)
    $sfm = '<!--StartFragment-->'
    $efm = '<!--EndFragment-->'
    $startFragment = $hdrLen + [System.Text.Encoding]::UTF8.GetByteCount($htmlContent.Substring(0, $htmlContent.IndexOf($sfm) + $sfm.Length))
    $endFragment = $hdrLen + [System.Text.Encoding]::UTF8.GetByteCount($htmlContent.Substring(0, $htmlContent.IndexOf($efm)))

    # 正確なバイト位置を含むヘッダを再作成
    $header = [string]::Format($hdrFmt, $startHtml, $endHtml, $startFragment, $endFragment)

    return $header + $htmlContent
}

# CF_HTML形式のHTMLを生成
$cfHtml = Create-CFHtml -body $html

# ==============================================================================
# クリップボードへのデータ設定準備
# ==============================================================================

# HTMLデータをUTF-8バイト配列に変換(BOMなし)
$utf8 = New-Object System.Text.UTF8Encoding($false)
$htmlBytes = $utf8.GetBytes($cfHtml)

# 元のMarkdownをUnicodeバイト配列に変換(null終端)
$unicodeBytes = [System.Text.Encoding]::Unicode.GetBytes($markdown + "`0")

# カスタムクリップボード形式「HTML Format」のIDを取得
$cfHtmlFormat = [ClipboardHelper]::RegisterClipboardFormat("HTML Format")

# ==============================================================================
# Win32 APIでクリップボードを操作
# ==============================================================================

if ([ClipboardHelper]::OpenClipboard([IntPtr]::Zero)) {
    try {
        # クリップボードの内容をクリア
        [ClipboardHelper]::EmptyClipboard() | Out-Null

        # ========== HTML形式でデータをセット ==========
        # メモリを割り当て
        $hMemHtml = [ClipboardHelper]::GlobalAlloc([ClipboardHelper]::GMEM_MOVEABLE, [UIntPtr]::new($htmlBytes.Length + 1))
        # メモリをロック(ポインタを取得)
        $ptrHtml = [ClipboardHelper]::GlobalLock($hMemHtml)
        # HTMLデータをメモリにコピー
        [System.Runtime.InteropServices.Marshal]::Copy($htmlBytes, 0, $ptrHtml, $htmlBytes.Length)
        # null終端文字を追加
        [System.Runtime.InteropServices.Marshal]::WriteByte($ptrHtml, $htmlBytes.Length, 0)
        # メモリをアンロック
        [ClipboardHelper]::GlobalUnlock($hMemHtml) | Out-Null
        # クリップボードにデータをセット
        [ClipboardHelper]::SetClipboardData($cfHtmlFormat, $hMemHtml) | Out-Null

        # ========== Unicode テキスト形式でもデータをセット ==========
        # (互換性のため、HTML形式だけでなくプレーンテキストも登録)
        $hMemText = [ClipboardHelper]::GlobalAlloc([ClipboardHelper]::GMEM_MOVEABLE, [UIntPtr]::new($unicodeBytes.Length))
        $ptrText = [ClipboardHelper]::GlobalLock($hMemText)
        [System.Runtime.InteropServices.Marshal]::Copy($unicodeBytes, 0, $ptrText, $unicodeBytes.Length)
        [ClipboardHelper]::GlobalUnlock($hMemText) | Out-Null
        [ClipboardHelper]::SetClipboardData([ClipboardHelper]::CF_UNICODETEXT, $hMemText) | Out-Null
    }
    finally {
        # クリップボードをクローズ
        [ClipboardHelper]::CloseClipboard() | Out-Null
    }
}

# ==============================================================================
# 完了メッセージ
# ==============================================================================
Write-Host "Done! Press Ctrl+V to paste" -ForegroundColor Green

Discussion