【SWELL】Geminiの「タグ食べちゃう問題」をPythonで解決!自作ツール『SWELL Maker』ができるまで

今回は、このブログ「還暦ハック」の執筆環境に起きた、小さな革命の話をします。

お父さん

長かった……。本当に長かったぞ、巧士。

巧士

はい。ついに私たちが抱えていた「AIがタグを食べちゃう問題」を、技術の力で解決しましたね!

目次

AIは「見えない文字」が嫌い?

私は現在、記事の執筆パートナーとしてGoogleのAI(Gemini)を活用しています。しかし、彼にはSWELL使いにとって致命的な弱点がありました。それは、「HTMLコメントタグを勝手に消してしまう」ことです。

逆転の発想:AIにコードを書かせなければいい

STEP
課題

AIに無理やりHTMLタグを出力させようとするから消える。

STEP
発想

なら、AIには「データ(JSON)」だけを作らせればいい。

STEP
解決

そのデータを、こちらのPC(Python)で受け取って、コードに変換すればいい!

【サンプル】あなたも使える『SWELL Maker』コード

以下は、実際に動いているPythonコードです。Streamlit環境があればコピペで動きます。
(※あくまで、2025/12 のSWELL, WordPress の記述準拠です。バージョン対応は各自でお願いします。)

import streamlit as st
import json
import csv
import io

# ==========================================
# 設定エリア:環境に合わせて変更可能
# ==========================================
BALLOON_ID_L = "1"  # 左側 (dad)
BALLOON_ID_R = "2"  # 右側 (takuto)

# ==========================================
# 1. 定数定義(タグ消失回避トリック)
# ==========================================
WP_START = "<" + "!-- wp:"
WP_END   = "<" + "!-- /wp:"
TAG_CLOSE = " -->"

# ==========================================
# 2. ブロック生成ロジック (正解データ準拠)
# ==========================================

def get_balloon(speaker_type, text):
    """ふきだし"""
    if speaker_type in ["dad", "left"]:
        b_id = BALLOON_ID_L
    else:
        b_id = BALLOON_ID_R
    
    return (
        f'{WP_START}loos/balloon {{"balloonID":"{b_id}"}}{TAG_CLOSE}\n'
        f'<p>{text}</p>\n'
        f'{WP_END}loos/balloon{TAG_CLOSE}'
    )

def get_heading(level, text):
    """見出し (h2, h3)"""
    return (
        f'{WP_START}heading {{"level":{level}}}{TAG_CLOSE}\n'
        f'<h{level} class="wp-block-heading">{text}</h{level}>\n'
        f'{WP_END}heading{TAG_CLOSE}'
    )

def get_step(items):
    """
    ステップ (SWELL正解データ準拠)
    中身のテキストも wp:paragraph で包まないと無効化されるため修正
    """
    steps_html = ""
    # 親コンテナ
    start_tag = f'{WP_START}loos/step{TAG_CLOSE}\n<div class="swell-block-step" data-num-style="circle">'
    
    for item in items:
        label = item.get('label', 'STEP')
        title = item.get('title', '')
        body = item.get('body', '')
        
        step_item = (
            f'{WP_START}loos/step-item {{"stepLabel":"{label}"}}{TAG_CLOSE}\n'
            f'<div class="swell-block-step__item">'
            f'<div class="swell-block-step__number u-bg-main"><span class="__label">{label}</span></div>'
            f'<div class="swell-block-step__title u-fz-l">{title}</div>'
            f'<div class="swell-block-step__body">\n'
            f'{WP_START}paragraph{TAG_CLOSE}\n<p>{body}</p>\n{WP_END}paragraph{TAG_CLOSE}\n'
            f'</div>'
            f'</div>\n'
            f'{WP_END}loos/step-item{TAG_CLOSE}\n'
        )
        steps_html += step_item

    end_tag = f'</div>\n{WP_END}loos/step{TAG_CLOSE}'
    return f"{start_tag}\n{steps_html}\n{end_tag}"

def get_code_block(code_text):
    """
    ソースコードブロック (wp:code)
    エスケープ処理は簡易的ですが、構造を正解データに合わせました
    """
    # 簡易エスケープ (HTMLタグがコードに含まれる場合のため)
    escaped_code = code_text.replace("<", "&lt;").replace(">", "&gt;")
    
    return (
        f'{WP_START}code{TAG_CLOSE}\n'
        f'<pre class="wp-block-code"><code>{escaped_code}</code></pre>\n'
        f'{WP_END}code{TAG_CLOSE}'
    )

def get_table(csv_text):
    """テーブル (CSV変換)"""
    try:
        if "\t" in csv_text: csv_text = csv_text.replace("\t", ",")
        f = io.StringIO(csv_text.strip())
        reader = csv.reader(f)
        rows = list(reader)
        if not rows: return "(データなし)"

        thead = "".join([f"<th>{c}</th>" for c in rows[0]])
        tbody = ""
        for row in rows[1:]:
            row_html = "".join([f"<td>{c}</td>" for c in row])
            tbody += f"<tr>{row_html}</tr>"
            
        return (
            f'{WP_START}table {{"hasFixedLayout":false,"swlScrollable":"both","swlTableWidth":"100%"}}{TAG_CLOSE}\n'
            '<figure class="wp-block-table"><table>\n'
            f'<thead><tr>{thead}</tr></thead>\n'
            f'<tbody>{tbody}</tbody>\n'
            '</table></figure>\n'
            f'{WP_END}table{TAG_CLOSE}'
        )
    except Exception as e:
        return f"⚠️ CSV変換エラー: {str(e)}"

# ==========================================
# 3. メイン変換処理
# ==========================================
def convert_json_to_swell(json_data):
    output = []
    try:
        data_list = json.loads(json_data)
        if not isinstance(data_list, list): return "⚠️ エラー: リスト形式 [...] で入力してください。"
            
        for block in data_list:
            b_type = block.get('type')
            
            if b_type == 'h2': output.append(get_heading(2, block.get('text', '')))
            elif b_type == 'h3': output.append(get_heading(3, block.get('text', '')))
            elif b_type == 'p':
                text = block.get('text', '')
                output.append(f'{WP_START}paragraph{TAG_CLOSE}\n<p>{text}</p>\n{WP_END}paragraph{TAG_CLOSE}')
            elif b_type in ['dad', 'left', 'takuto', 'right']:
                output.append(get_balloon(b_type, block.get('text', '')))
            elif b_type == 'step': output.append(get_step(block.get('items', [])))
            elif b_type == 'code': output.append(get_code_block(block.get('text', ''))) # 新設
            elif b_type == 'table': output.append(get_table(block.get('csv', '')))
            
        return "\n\n".join(output)

    except json.JSONDecodeError as e:
        return f"⚠️ JSON解析エラー: \n{str(e)}"

# ==========================================
# 4. Streamlit UI
# ==========================================
st.set_page_config(layout="wide", page_title="SWELL Maker")
st.title("🦍 SWELL Maker (v2.0 Fixed)")

col1, col2 = st.columns(2)

with col1:
    st.subheader("📥 Input (JSON)")
    input_text = st.text_area("JSON貼り付け", height=600)

with col2:
    st.subheader("📤 Output (HTML Code)")
    if input_text:
        converted = convert_json_to_swell(input_text)
        if "⚠️" in converted:
            st.error(converted)
        else:
            st.code(converted, language='html')
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次