|
/** |
|
* 簡易ワードラッププラグイン |
|
* |
|
* 概要: |
|
* - メッセージレイヤのテキストを英単語などのワード単位で自動改行を制御します |
|
* |
|
* 使い方: |
|
* - Overrider.tjs にて KAGLoadScript("WordWrapPlugin.tjs"); を実行します |
|
* - [style][defstyle] タグに wordwrap オプションが追加されるので [defstyle wordwrap=true] 等を指定してください |
|
* wordrapスタイルが有効かつautoreturnスタイルが有効(通常はデフォルトで有効)の間のみワードラップが機能します |
|
* - Config.tjs の //[start-messagelayer-additionals]~//[end-messagelayer-additionals]間に |
|
* デフォルト値 ;defaultWordWrap = true; 等を記述することができます(デフォルト:false) |
|
* 空白文字 ;wwBlankChar = " "; 等(文字群:どれかにマッチで空白判定)を記述することができます(デフォルト:" ") |
|
* |
|
* 制限事項など: |
|
* - 履歴レイヤのワードラップは想定していませんが,ワードラップ改行があった個所は履歴でも改行するような動作になります |
|
* (同じ文字数で改行するような履歴テキストマージン範囲にしておけばそれっぽく動く可能性はあります) |
|
* - ch(通常テキスト)タグもしくはembタグが連続する文字中でのみワードラップ処理が行われるため |
|
* ruby等の特殊文字表示タグを間に挟むとワードラップが正しく機能しません |
|
* - 縦書きモードでは正しく動作しない可能性があります(動作未確認) |
|
* - wordwrap=true, autoreturn=false の組み合わせは推奨されません(予期しない不具合が発生する可能性があるため) |
|
* もし autoreturn=false にする場合は一緒に wordwrap=false の設定にしてください |
|
* - ワードラップ中の自動改行判定は kag.current のメッセージレイヤで行われるため |
|
* 表と裏で内容が違うテキスト範囲になっているなど,特殊なケースで表裏の反対側では正しく動作しない可能性があります |
|
* - TJSで独自にchタグ発行したりprocessCh等で文字を表示する処理系ではワードラップは正しく動作しません |
|
* どうしても対応したい場合はchを_prechタグに変更して最後に_postchタグを発行するようにしてください |
|
* |
|
* その他仕様など |
|
* - MessageLayerに本来ある禁則処理(wwFollowing/Leading等)の動作は,ワードラップ処理中においては |
|
* wwBlankCharの文字群が行頭弱禁則文字として追加で判定されます(改行後の先頭に空白が来ないようにするため) |
|
* ただし連続で空白が続いた場合など,特殊な条件下では自動改行後の行頭に空白が出現するケースもあります |
|
* - 単語区切りはwwBlankCharか0x100以降の文字となっていますが MessageLayer.isBreakCh を適宜書き換えることで変更できます |
|
* ワードブレイクしたくない空白を使用したい場合はnbsp文字[macro name=nbsp][emb exp=$0xA0][endmacro]をご利用ください |
|
* - ワードラップ自動改行の履歴レイヤ連動が不要な場合は KAGWindow.onWordWrapAutoReturn を空の関数にしてください |
|
* |
|
* ライセンスなど |
|
* - Copyright (c), miahmie All rights reserved. |
|
* - 改変・配布は自由です |
|
*/ |
|
|
|
//============================================================== |
|
// 既存クラス改造用 |
|
class ObjectInjectionUtil |
|
{ |
|
var injections = %[]; // injection用のcontext |
|
|
|
// デバッグログ |
|
var _showlog = System.getArgument("-debugwin") != "no"; |
|
function log { |
|
if (_showlog) dm(...); |
|
} |
|
|
|
// クラス改造(override/injection一括) |
|
function classHack(name, cls, override, injection) { |
|
var cnt = 0; |
|
cnt += classOverride (name, cls, override); |
|
cnt += classInjection(name, cls, injection); |
|
return cnt; |
|
} |
|
// メソッド追加(上書き) |
|
function classOverride(clsname, cls, ovrs) { |
|
if (!cls || !ovrs) return 0; |
|
function _(name, member, cls, clsname, r) { |
|
if (typeof cls[name] != "undefined") { |
|
Debug.notice("ObjectInjectionUtil: 既存メソッドを上書きします:%s.%s".sprintf(clsname, name)); |
|
} |
|
log("override: %s.%s".sprintf(clsname, name)); |
|
&cls[name] = member; |
|
++r.count; |
|
} |
|
var result = %[]; |
|
foreach(ovrs, _, cls, clsname, result); |
|
return +result.count; |
|
} |
|
// メソッド差し込み |
|
function classInjection(clsname, cls, injs) { |
|
if (!cls || !injs) return 0; |
|
function _(name, member, cls, clsname, create, r) { |
|
var orig = (typeof cls[name] != "undefined") ? &cls[name] : null; |
|
if(!orig) { |
|
Debug.notice("ObjectInjectionUtil: Injection元がありません:%s.%s".sprintf(clsname,name)); |
|
orig = function {}; // dummy |
|
} |
|
log("injection: %s.%s".sprintf(clsname, name)); |
|
&cls[name] = create(clsname, cls, name, member, orig); |
|
++r.count; |
|
} |
|
var result = %[]; |
|
foreach(injs, _, cls, clsname, createInjection, result); |
|
return +result.count; |
|
} |
|
// 一覧iteration |
|
function foreach(map, cb, *) { |
|
var ext = []; |
|
ext.assign(map); |
|
for (var i = 0, cnt = ext.count; i < cnt; i+=2) { |
|
(cb incontextof this)(ext[i], ext[i+1], *); |
|
} |
|
} |
|
// injection hack |
|
var _injectionFunc = "function %s(*) { function _{} return (_.inj incontextof this)(_.orig, *); }"; |
|
var _scriptExecContxt = _canUseScriptExecContext(); |
|
function createInjection(key, cls, name, member, orig) { |
|
// [TODO] 現状では関数のみ対応(プロパティ等はエラー) |
|
if (!(orig instanceof "Function") || |
|
!(member instanceof "Function")) |
|
throw new Exception("ObjectInjectionUtil: function以外のinjectionはサポートしていません:%s.%s".sprintf(key,name)); |
|
var ref = this.injections[key]; |
|
/**/ref = this.injections[key] = %[] if (ref === void); |
|
// フック用関数を生成 |
|
var r; |
|
if (_scriptExecContxt) { |
|
Scripts.exec(_injectionFunc.sprintf(name), name, 0, ref); |
|
r = ref[name]; |
|
} else { |
|
// [HACK] 古いバージョンの吉里吉里では context を指定できずに global経由でしか関数を生成できない |
|
var glkey = "____"+key+"_"+name; |
|
Scripts.exec(_injectionFunc.sprintf(glkey), name); |
|
r = global[glkey]; |
|
delete global[glkey]; |
|
} |
|
with (r._) { |
|
// local function の static領域に関数ペアを保存する |
|
.inj = member; |
|
.orig = orig; |
|
} |
|
return r incontextof null; |
|
} |
|
// Scripts.execでcontext引数が使えるかどうかのチェック |
|
function _canUseScriptExecContext(testname = "____") { |
|
var ctx = %[]; |
|
delete global[testname]; |
|
Scripts.exec("var %s = 1;".sprintf(testname),,, ctx); |
|
if (ctx[testname]) return true; |
|
delete global[testname]; |
|
Debug.notice("ObjectInjectionUtil: cannot support Scripts.exec context arg."); |
|
} |
|
} |
|
|
|
//============================================================== |
|
// ワードラップ機能追加(改造)用クラス |
|
class WordWrapHack extends ObjectInjectionUtil |
|
{ |
|
var CLS = []; // 改造するクラス一覧 |
|
|
|
// 一括改造 |
|
function hack() { |
|
function proc(name) { |
|
if (typeof this[name+"Hack"] == "undefined" || |
|
/**/ !(this[name+"Hack"] instanceof "Function")) |
|
throw new Exception("WordWrapHack: %sHack() がありません".sprintf(name)); |
|
if (typeof global[name] == "undefined" || |
|
/**/ !(global[name] instanceof "Class")) |
|
throw new Exception("WordWrapHack: global.%s classがありません".sprintf(name)); |
|
return this[name+"Hack"](name, global[name]); |
|
} |
|
var cnt = 0; |
|
while (CLS.count > 0) cnt += proc(CLS.pop()); |
|
Debug.notice("WordWrapHack: %d個のメソッドを書き換え・追加しました".sprintf(cnt)); |
|
} |
|
|
|
//-------------------------------------------------------------- |
|
// Conductorクラス改造 |
|
CLS.add("Conductor"); |
|
function ConductorHack(name, cls) { |
|
// 差し込み関数 |
|
function injection { |
|
// [ch]タグの[_prech]タグへの変換/[_postch]タグの自動発行 |
|
function getNextTag(orig, *) { |
|
var r = orig(*); |
|
var isCh = (r && r.tagname == "ch"); // [ch]タグかどうか |
|
var last = (typeof this.lastChState != "undefined" && this.lastChState); // 前回のステート |
|
this.lastChState = isCh; |
|
if (isCh) r.tagname = "_prech"; |
|
else if (last) pendings.add(%[ tagname:"_postch" ]); // [ch]が途切れた |
|
return r; |
|
} |
|
// リセット処理 |
|
function clear(orig, *) { this.lastChState = false; return orig(*); } |
|
function restore(orig, *) { this.lastChState = false; return orig(*); } |
|
// assign同期 |
|
function assign(orig, src, *) { |
|
this.lastChState = (typeof src.lastChState != "undefined" && src.lastChState); |
|
return orig(src, *); |
|
} |
|
} |
|
return classHack(name, cls, /*override*/null, injection); |
|
} |
|
|
|
//-------------------------------------------------------------- |
|
// KAGWindowクラス改造 |
|
CLS.add("KAGWindow"); |
|
function KAGWindowHack(name, cls) { |
|
// 追加関数 |
|
function override { |
|
// [_prech]タグ処理 |
|
function tagHandlerPreCh(elm) { |
|
var text = elm.text; |
|
var proc = current.preProcessCh(text); |
|
if (proc == 0) return tagHandlers.ch(elm); // 直呼び出しでOK |
|
else if (proc > 0) { |
|
// 単語区切り処理 |
|
var insert = conductor.pendings.unshift; // [HACK] タグ情報を差し込む先 |
|
elm.tagname = "ch"; // chタグで通しなおす |
|
insert(elm); // 後に実行 |
|
insert(%[ tagname:"_postch" ]); // 先に実行 |
|
} |
|
return 0; |
|
} |
|
// [_postch]タグ処理 |
|
function tagHandlerPostCh(elm) { |
|
var st = current.postProcessCh(); |
|
if (st != 0) { // ワードラップの改行が発生した |
|
var pending = conductor.pendings[0]; |
|
if (pending && pending.tagname == "ch" && current.isBlankCh(pending.text)) { |
|
// [FIXME] 次に空白が積まれている場合は除去する(行頭スペースを回避) |
|
conductor.pendings.shift(); |
|
} |
|
} |
|
if (st < 0) return showPageBreakAndClear(); |
|
return 0; |
|
} |
|
// 保留されていた[ch]タグを発行 |
|
function storeChTagPendings(list) { |
|
var insert = conductor.pendings.unshift; // [HACK] タグ情報を差し込む先 |
|
for (var i = list.count-1; i >= 0; --i) { // insertなので後ろから順番に差し込む |
|
insert(%[ tagname:"ch", /*wrapped:true,*/ text:list[i] ]); |
|
} |
|
} |
|
// wordwrap中に自動改行が発生した |
|
function onWordWrapAutoReturn() { |
|
if (historyWriteEnabled) historyLayer.reline(); // [FIXME] 履歴レイヤも連動して改行(仕様的に微妙か) |
|
} |
|
} |
|
// 差し込み関数 |
|
function injection { |
|
// [_prech][_postch]タグを登録する |
|
function getHandlers(orig, *) { |
|
var r = orig(*); |
|
r._prech = this.tagHandlerPreCh; |
|
r._postch = this.tagHandlerPostCh; |
|
return r; |
|
} |
|
} |
|
return classHack(name, cls, override, injection); |
|
} |
|
|
|
//-------------------------------------------------------------- |
|
// MessageLayerクラス改造 |
|
CLS.add("MessageLayer"); |
|
function MessageLayerHack(name, cls) { |
|
// 追加関数 |
|
function override { |
|
// メンバ変数初期化 |
|
function initWordWrapParams() { |
|
/*CS*/this.defaultWordWrap = false; |
|
/*C*/ this.wordWrap = false; |
|
/*C*/ this.preProcessChList = []; |
|
this.inProcessReturn = false; // processReturn実行中フラグ変数 |
|
this.wwBlankChar = " "; // 空白判定文字群 |
|
} |
|
// 区切り文字判定 |
|
function isBreakCh(ch) { |
|
return isBlankCh(ch) || #ch > 0xFF; // [FIXME] 暫定で wwBlankChar と全角文字? |
|
} |
|
// 空白文字判定(ワードラップ後の行頭で無視する文字) |
|
function isBlankCh(ch) { |
|
return wwBlankChar.indexOf(ch) >= 0; |
|
} |
|
// 現在の文字表示位置は行頭か |
|
function isLineHeadPos() { |
|
return !vertical ? (x == marginL + indentxpos) : (y == marginT + indentxpos); |
|
} |
|
// 指定サイズ後に改行位置からはみ出るかどうか |
|
function isLinePosOver(size) { |
|
return (!vertical ? (x + size) : (y + size)) > relinexpos; |
|
} |
|
// [ch]タグ前処理 -> 0:通常動作, 1:postch処理+通常動作, -1:ch処理保留 |
|
function preProcessCh(ch) { |
|
if (!wordWrap) return 0; // ワードラップ動作不要 |
|
if (isBreakCh(ch)) return 1; // フラッシュ処理を行う |
|
// [ch]タグの処理を保留する |
|
preProcessChList.add(ch != $0xA0 ? ch :" "); // [FIXME] nbsp特殊処理 |
|
return -1; |
|
} |
|
// [ch]タグ後処理 -> void:なし, 0:フラッシュした, 1:改行があった, -1:改ページがあった(フラッシュ保留) |
|
function postProcessCh() { |
|
// 処理保留中の文字があるかどうか |
|
if (preProcessChList.count > 0) { |
|
var r = 0; |
|
// 行頭でない状態で単語オーバーの場合は改行をする(行頭でオーバーする場合はワードラップを諦める) |
|
if (!isLineHeadPos() && isWordOver(preProcessChList)) { |
|
// 改行して改ページ待ちが必要かどうかをチェック |
|
if (autoReturn && reline()) return -1; |
|
else r = 1; |
|
} |
|
// 保留していた [ch] を吐き出す |
|
window.storeChTagPendings(preProcessChList); |
|
preProcessChList.clear(); |
|
return r; |
|
} |
|
} |
|
// この先の単語がはみ出るかどうか |
|
function isWordOver(list) { |
|
var text = list.join(""); |
|
var w = lineLayer.font.getTextWidth(text); |
|
var count = list.count; |
|
w -= pitch * (count-1) if (count > 1); // [NOTE] pitch分だけ引いておく |
|
return isLinePosOver(w); |
|
} |
|
} |
|
// 差し込み関数 |
|
function injection { |
|
// コンストラクタ |
|
function MessageLayer(orig, *) { |
|
initWordWrapParams(); |
|
var r = orig(*); |
|
return r; |
|
} |
|
// 文字処理フック |
|
function processCh(orig, *) { |
|
// wordwrap処理中では一時的に wwFollowing/Weak に wwBlankChar を追加する |
|
var ww = this.wordWrap, follow, weak, r; |
|
if (ww) { |
|
follow = wwFollowing, weak = wwFollowingWeak; |
|
wwFollowing += wwBlankChar; |
|
wwFollowingWeak += wwBlankChar; |
|
} |
|
function reset(follow, weak) { |
|
wwFollowing = follow; |
|
wwFollowingWeak = weak; |
|
} |
|
try { |
|
r = orig(*); |
|
} catch (e) { |
|
reset(follow, weak) if ww; |
|
throw e; |
|
} |
|
reset(follow, weak) if ww; |
|
return r; |
|
} |
|
// 改行フック |
|
function processReturn(orig, *) { |
|
this.inProcessReturn = true; |
|
var r = orig(*); |
|
this.inProcessReturn = false; |
|
return r; |
|
} |
|
// relineフック |
|
function reline(orig, *) { |
|
if (!this.inProcessReturn && wordWrap) window.onWordWrapAutoReturn(); // ワードラップ中の自動改行を通知 |
|
return orig(*); |
|
} |
|
// スタイル設定 |
|
function setStyle(orig, elm, *) { |
|
var ww = elm.wordwrap; |
|
if (ww !== void) this.wordWrap = (ww == "default") ? this.defaultWordWrap : +ww; |
|
return orig(elm, *); |
|
} |
|
// デフォルトスタイル設定 |
|
function setDefaultStyle(orig, elm, *) { |
|
this.defaultWordWrap = +elm.wordwrap if (elm.wordwrap !== void); |
|
return orig(elm, *); |
|
} |
|
// スタイルリセット |
|
function resetStyle(orig, *) { |
|
this.wordWrap = this.defaultWordWrap; |
|
this.preProcessChList.clear(); |
|
return orig(*); |
|
} |
|
// 追加変数のassign処理 |
|
function internalAssign(orig, src, *) { |
|
this.defaultWordWrap = src.defaultWordWrap; |
|
this.wordWrap = src.wordWrap; |
|
this.preProcessChList.assign(src.preProcessChList); |
|
return orig(src, *); |
|
} |
|
// 追加変数のstore処理 |
|
function store(orig, *) { |
|
var r = orig(*); |
|
r.defaultWordWrap = this.defaultWordWrap; |
|
return r; |
|
} |
|
// 追加変数のrestore処理 |
|
function restore(orig, dic, *) { |
|
this.defaultWordWrap = dic.defaultWordWrap; |
|
return orig(dic, *); |
|
} |
|
} |
|
return classHack(name, cls, override, injection); |
|
} |
|
} |
|
|
|
//============================================================== |
|
// 書き換え実行 (KAGPluginでないことに注意) |
|
{ |
|
var inst = new WordWrapHack(); |
|
/**/inst.hack(); |
|
invalidate inst; // 消しても問題ない |
|
|
|
/* globalを汚したくないのであれば下記コードも可 |
|
delete global.WordWrapHack; |
|
delete global.ObjectInjectionUtil; |
|
*/ |
|
} |