構文ハイライトを追加(textareaバックドロップ方式)
入力欄(textarea)のキーワードを種別で色分け表示。素のtextareaは色付け 不可のため、同じ親内に色付きバックドロップdivを敷き、textareaの文字を 透明化(キャレットのみ表示)して重ねる。位置はtextareaのoffsetボックスに 合わせ、ResizeObserverで追従(ズーム/リサイズ非依存)。 - js/highlighter.js: highlightHTML(深度カウンタで種別判定・全文字保持)+ Highlighterクラス(バックドロップ生成・スクロール/位置同期) - 色分け: 重み(橙)/LoRA(青)/BREAK(灰太字)/コメント(灰斜体)、通常タグは既定色 - input_watcher.js: 対象ノードのtextareaにHighlighterを適用 - style.css: バックドロップと色クラス Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1251356226
commit
8a765f4946
@@ -33,8 +33,8 @@ git clone https://github.com/4thCafe/ComfyUI-PromptTagEditor
|
||||
## ロードマップ
|
||||
|
||||
- [x] タグ編集コア(チップ化・個別編集・重み・並べ替え・削除)
|
||||
- [ ] 各タグの翻訳表示(オフライン: argostranslate)
|
||||
- [ ] 入力欄の構文ハイライト
|
||||
- [x] タグ翻訳(日本語→英語は置換、英語→日本語は表示)
|
||||
- [x] 入力欄の構文ハイライト(重み/LoRA/BREAK/コメントを色分け)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// highlighter.js — textarea の構文ハイライト
|
||||
// 素の textarea は色付け不可のため、同じ親内に色付きバックドロップdivを敷き、
|
||||
// textarea の文字を透明化(キャレットのみ表示)して重ねる方式。座標追従はComfyUIに委譲。
|
||||
// ComfyUI-PromptTagEditor / 4thCafe
|
||||
|
||||
import { parseToken } from "./prompt_parser.js";
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
// セグメント(カンマ区切りの1タグ)の種別からCSSクラスを決める
|
||||
function classOf(seg) {
|
||||
const t = seg.trim();
|
||||
if (!t) return null;
|
||||
const tok = parseToken(t);
|
||||
if (tok.type === "lora") return "pte-hl-lora";
|
||||
if (tok.type === "break") return "pte-hl-break";
|
||||
if (tok.type === "comment") return "pte-hl-comment";
|
||||
if (tok.type === "tag" && Math.abs(tok.weight - 1) > 0.001) return "pte-hl-weighted";
|
||||
return null; // 通常タグ(既定色)
|
||||
}
|
||||
|
||||
// プロンプト文字列 → 色付きHTML(全文字を保持。深度0のカンマ/改行で区切る)
|
||||
export function highlightHTML(text) {
|
||||
if (!text) return "";
|
||||
let html = "";
|
||||
let buf = "";
|
||||
let depth = 0;
|
||||
const flush = () => {
|
||||
if (buf === "") return;
|
||||
const cls = classOf(buf);
|
||||
html += cls ? `<span class="${cls}">${escapeHtml(buf)}</span>` : escapeHtml(buf);
|
||||
buf = "";
|
||||
};
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (ch === "\\") { buf += ch; if (i + 1 < text.length) { buf += text[i + 1]; i++; } continue; }
|
||||
if (ch === "(" || ch === "[" || ch === "<") { depth++; buf += ch; continue; }
|
||||
if (ch === ")" || ch === "]" || ch === ">") { depth = Math.max(0, depth - 1); buf += ch; continue; }
|
||||
if ((ch === "," || ch === "\n") && depth === 0) { flush(); html += escapeHtml(ch); continue; }
|
||||
buf += ch;
|
||||
}
|
||||
flush();
|
||||
return html;
|
||||
}
|
||||
|
||||
export class Highlighter {
|
||||
constructor(textarea) {
|
||||
this.ta = textarea;
|
||||
try {
|
||||
this._build();
|
||||
this._bind();
|
||||
this.update();
|
||||
} catch (e) {
|
||||
console.warn("[PromptTagEditor] highlighter init failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
_build() {
|
||||
const ta = this.ta;
|
||||
const parent = ta.parentElement;
|
||||
const cs = getComputedStyle(ta);
|
||||
this._origColor = cs.color;
|
||||
|
||||
const bd = document.createElement("div");
|
||||
bd.className = "pte-hl-backdrop";
|
||||
const s = bd.style;
|
||||
s.position = "absolute";
|
||||
s.margin = "0";
|
||||
s.borderWidth = cs.borderWidth;
|
||||
s.borderStyle = "solid";
|
||||
s.borderColor = "transparent";
|
||||
s.padding = cs.padding;
|
||||
s.font = cs.font;
|
||||
s.lineHeight = cs.lineHeight;
|
||||
s.letterSpacing = cs.letterSpacing;
|
||||
s.whiteSpace = "pre-wrap";
|
||||
s.overflowWrap = "break-word";
|
||||
s.wordBreak = cs.wordBreak;
|
||||
s.boxSizing = cs.boxSizing;
|
||||
s.overflow = "hidden";
|
||||
s.pointerEvents = "none";
|
||||
s.color = cs.color;
|
||||
parent.insertBefore(bd, ta);
|
||||
this.bd = bd;
|
||||
|
||||
// バックドロップ位置・サイズを textarea の offset ボックスに合わせる(ズーム非依存)
|
||||
this._syncBox = () => {
|
||||
s.top = ta.offsetTop + "px";
|
||||
s.left = ta.offsetLeft + "px";
|
||||
s.width = ta.offsetWidth + "px";
|
||||
s.height = ta.offsetHeight + "px";
|
||||
};
|
||||
this._syncBox();
|
||||
try {
|
||||
this._ro = new ResizeObserver(() => this._syncBox());
|
||||
this._ro.observe(ta);
|
||||
} catch (_) {}
|
||||
|
||||
// textarea の文字を透明化(キャレットは元色で表示)
|
||||
ta.style.background = "transparent";
|
||||
ta.style.color = "transparent";
|
||||
ta.style.caretColor = this._origColor || "#fff";
|
||||
ta.style.position = "relative";
|
||||
ta.style.zIndex = "1";
|
||||
}
|
||||
|
||||
_bind() {
|
||||
this._onInput = () => this.update();
|
||||
this._onScroll = () => this._syncScroll();
|
||||
this.ta.addEventListener("input", this._onInput);
|
||||
this.ta.addEventListener("scroll", this._onScroll);
|
||||
}
|
||||
|
||||
_syncScroll() {
|
||||
if (!this.bd) return;
|
||||
this.bd.scrollTop = this.ta.scrollTop;
|
||||
this.bd.scrollLeft = this.ta.scrollLeft;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.bd) return;
|
||||
if (this._syncBox) this._syncBox();
|
||||
this.bd.innerHTML = highlightHTML(this.ta.value);
|
||||
this._syncScroll();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// ComfyUI-PromptTagEditor / 4thCafe
|
||||
|
||||
import { TagEditor } from "./tag_editor.js";
|
||||
import { Highlighter } from "./highlighter.js";
|
||||
|
||||
// このノード型のtextウィジェットにのみチップUIを適用(既存ノードには一切触れない)
|
||||
const TARGET_NODE_TYPE = "PromptTagEditor";
|
||||
@@ -63,6 +64,9 @@ export class Watcher {
|
||||
const editor = new TagEditor(chipsEl, (str) => this._syncToTextarea(w, ta, str));
|
||||
w.__pteEditor = editor;
|
||||
|
||||
// textarea の構文ハイライト(バックドロップ方式)
|
||||
w.__pteHighlighter = new Highlighter(ta);
|
||||
|
||||
// DOMウィジェットとして隣接配置(serialize:false=値はネイティブtextareaが保持)
|
||||
const dom = node.addDOMWidget(`pte_${w.name}`, "pte_chips", wrap, {
|
||||
serialize: false,
|
||||
|
||||
@@ -151,6 +151,16 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* === textarea 構文ハイライト用バックドロップ === */
|
||||
.pte-hl-backdrop {
|
||||
z-index: 0;
|
||||
user-select: none;
|
||||
}
|
||||
.pte-hl-backdrop .pte-hl-lora { color: #8fc0ff; }
|
||||
.pte-hl-backdrop .pte-hl-weighted { color: #ffb27a; }
|
||||
.pte-hl-backdrop .pte-hl-break { color: #bbb; font-weight: 600; }
|
||||
.pte-hl-backdrop .pte-hl-comment { color: #999; font-style: italic; }
|
||||
|
||||
/* チップエリアを載せるDOMウィジェットのラッパ */
|
||||
.pte-widget-wrap {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user