#!/usr/bin/env node
// japanese-linebreak: 日本語HTML（LP・スライド）の見出し・段落を BudouX で文節改行する
//
// 設計の核:
//  - 原稿には ZWSP(U+200B) を書かない。ビルド時にだけ成果物へ注入する。
//  - CSS は word-break:keep-all + line-break:strict（禁則厳守）。
//    overflow-wrap:anywhere は使わない（狭い領域で「」や。を行頭に落とすため）。
//  - クライアント JS は一切足さない。BudouX はビルド時のみ実行。
//  - 通常HTML と バンドラー形式（<script type="__bundler/template"> 内に
//    JSON 文字列で本文を持つ書き出し）の両方に対応。
//
// 使い方:
//   node linebreak.mjs <input.html> [output.html] [--config config.json]
//   - output 省略時は <input>.linebreak.html を生成（原稿は上書きしない）
//   - config は任意。語の調整やオプションを指定する（references/config.example.json 参照）

import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { dirname, resolve, basename, extname, join } from "node:path";
import { loadDefaultJapaneseParser } from "budoux";

const ZWSP = "​";
const parser = loadDefaultJapaneseParser();

// ---------- CLI 引数 ----------
const args = process.argv.slice(2);
let input = null, output = null, configPath = null;
for (let i = 0; i < args.length; i++) {
  if (args[i] === "--config") configPath = args[++i];
  else if (!input) input = args[i];
  else if (!output) output = args[i];
}
if (!input) {
  console.error("使い方: node linebreak.mjs <input.html> [output.html] [--config config.json]");
  process.exit(1);
}
input = resolve(input);
output = output
  ? resolve(output)
  : join(dirname(input), `${basename(input, extname(input))}.linebreak${extname(input) || ".html"}`);

// ---------- 設定（デフォルト + config.json マージ）----------
const DEFAULTS = {
  // 文節改行を適用する要素（テキストノードだけ加工、タグは温存）
  selectors: ["h1", "h2", "h3", "h4", "h5", "h6", "p", "li", "blockquote", "figcaption"],
  // 割らせない語（語中 ZWSP を除去して塊にする）
  keepTogether: [],
  // 直後に必ず改行候補を入れる語（句読点改行などに使う）
  forceBreakAfter: [],
  // class にこのトークンを含む要素は、生の改行(\n)を <br> に変換する
  // （デザイナーが意図した改行が white-space:normal で潰れるのを救う）
  newlineToBrClasses: [],
  // keep-all を当てる CSS セレクタ（通常は selectors と同じでよい）
  keepAllSelector: "h1, h2, h3, h4, h5, h6, p, li, blockquote, figcaption",
  // PC 幅でだけ行長を抑える（見出しが横長1行になるのを防ぐ）。null で無効
  pcMaxWidth: null, // 例: { "h2": "15em", "h3": "18em" }
  // PC 幅で text-wrap:balance を当てるセレクタ（行長の均等化）
  balanceSelector: "h1, h2, h3",
  // 特定見出しのフォントを列幅に収める調整（核フレーズの折返し防止）。null で無効
  // 例: { "selector": ".hero h1", "fontSize": "clamp(30px, 4.0vw, 48px)" }
  fontFit: null,
};
let config = { ...DEFAULTS };
if (configPath) {
  const c = JSON.parse(readFileSync(resolve(configPath), "utf-8"));
  config = { ...DEFAULTS, ...c };
}

// ---------- 文節分割の中核 ----------
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

function applyOverrides(text) {
  let out = text;
  for (const term of config.keepTogether) {
    if (!term) continue;
    const pattern = [...term].map(escapeRe).join(`${ZWSP}?`);
    out = out.replace(new RegExp(pattern, "g"), term);
  }
  for (const term of config.forceBreakAfter) {
    if (!term) continue;
    out = out.replace(new RegExp(`(${escapeRe(term)})(?!${ZWSP})`, "g"), `$1${ZWSP}`);
  }
  return out;
}

// 要素の innerHTML を、テキストノードだけ BudouX 分割して返す（タグは温存）
function segmentInner(inner) {
  return inner
    .split(/(<[^>]+>)/)
    .map((tok) => {
      if (!tok || tok.startsWith("<")) return tok;
      const norm = tok.replace(/\s+/g, " "); // HTML と同様に連続空白を畳む
      return applyOverrides(parser.parse(norm).join(ZWSP));
    })
    .join("");
}

// ドキュメント文字列内の対象要素を加工
function processDoc(doc) {
  let total = 0;
  for (const tag of config.selectors) {
    const re = new RegExp(`(<${tag}\\b[^>]*>)([\\s\\S]*?)(</${tag}>)`, "g");
    doc = doc.replace(re, (_m, open, inner, close) => {
      total++;
      const cls = (open.match(/class="([^"]*)"/) || [, ""])[1].split(/\s+/);
      const toBr = config.newlineToBrClasses.some((c) => cls.includes(c));
      const prepared = toBr ? inner.replace(/\r?\n/g, "<br>") : inner;
      return open + segmentInner(prepared) + close;
    });
  }
  return { doc, total };
}

// ---------- CSS 注入 ----------
function buildCss() {
  let css = `
<style id="budoux-phrase-break">
/* BudouX 文節改行: ZWSP の位置でだけ折り返し、文節内は崩さない。
   line-break:strict で禁則（行頭の 」。、等）を厳守。
   overflow-wrap:anywhere は意図的に使わない（禁則破りの孤立を防ぐ）。 */
${config.keepAllSelector} {
  word-break: keep-all;
  line-break: strict;
}`;
  const pc = [];
  if (config.balanceSelector) pc.push(`  ${config.balanceSelector} { text-wrap: balance; }`);
  if (config.pcMaxWidth) {
    for (const [sel, w] of Object.entries(config.pcMaxWidth)) pc.push(`  ${sel} { max-width: ${w}; }`);
  }
  if (pc.length) {
    css += `\n/* PC 幅でだけ行長を抑え、文節で折り返す。モバイルは触らない。 */\n@media (min-width: 768px) {\n${pc.join("\n")}\n}`;
  }
  if (config.fontFit) {
    css += `\n/* 核フレーズが列幅に収まるようフォント上限を調整（折返し・括弧孤立の防止）。 */\n${config.fontFit.selector} { font-size: ${config.fontFit.fontSize}; }`;
  }
  return css + `\n</style>`;
}

function injectCss(doc) {
  if (doc.includes('id="budoux-phrase-break"')) return doc; // 二重注入防止
  const css = buildCss();
  if (doc.includes("</head>")) return doc.replace("</head>", css + "\n</head>");
  return css + doc;
}

// ---------- メイン ----------
const srcHtml = readFileSync(input, "utf-8");
const tplRe = /(<script type="__bundler\/template">)([\s\S]*?)(<\/script>)/;
const m = srcHtml.match(tplRe);

let outHtml, count;
if (m) {
  // バンドラー形式: JSON 文字列を decode → 加工 → re-encode して差し戻す
  const innerDoc = JSON.parse(m[2].trim());
  const { doc, total } = processDoc(innerDoc);
  const finalInner = injectCss(doc);
  count = total;
  // inner に含まれる </script> が外側の script を閉じないよう </ をエスケープ
  const reEncoded = JSON.stringify(finalInner).replace(/<\//g, "<\\u002F");
  outHtml = srcHtml.replace(tplRe, (_f, open, _b, close) => open + reEncoded + close);
} else {
  // 通常 HTML
  const { doc, total } = processDoc(srcHtml);
  outHtml = injectCss(doc);
  count = total;
}

const zwspCount = (outHtml.match(/​/g) || []).length;
mkdirSync(dirname(output), { recursive: true });
writeFileSync(output, outHtml, "utf-8");

console.log(`✓ 形式: ${m ? "バンドラー(__bundler/template)" : "通常HTML"}`);
console.log(`✓ 加工要素: ${count} 件 (${config.selectors.join(", ")})`);
console.log(`✓ 挿入 ZWSP: ${zwspCount} 個`);
console.log(`✓ 出力: ${output}`);
