- 화면 appendStructureEntities = 정본 상세 읽기 + placeStructureEntities 부름 · 동작 그대로 - 가짜 문서에 addEventListener · removeEventListener 빈 함수 — 화면 모듈이 불러올 때 거는 문서 이벤트로 덧그림 범위 Node 번들이 시작부터 멈추던 흠 (10-04 무렵부터 서버 장 배치가 덧그림 범위 없이 섬) - 시험 212 = 옮긴 자리에서 글 찾음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
162 lines
5.9 KiB
TypeScript
162 lines
5.9 KiB
TypeScript
/* =============================================================================
|
|
* B07_DesignDetail_Node_Dom.ts
|
|
* 서버 Node 진입점이 쓰는 **최소 DOM** — 덧그림 그리기 코드가 오프스크린 SVG 에 쓰는 만큼만.
|
|
* 불러오기만 하면 `globalThis.document` 가 선다(`_Cross_Extent_Node` · 인쇄 302-2 `B09_Draft_Print_Drawings_Node`).
|
|
* ========================================================================== */
|
|
|
|
/* ── 최소 DOM: 그리기 코드가 쓰는 만큼만 ─────────────────────────────────── */
|
|
|
|
type Child = FakeElement | string;
|
|
|
|
/** 선택자 하나(`tag`, `tag[attr]`)가 이 요소에 맞나 — 그리기·수확이 쓰는 꼴만 안다. */
|
|
function matches(element: FakeElement, selectors: string): boolean {
|
|
return selectors.split(",").some((raw) => {
|
|
const found = /^([\w-]*)(?:\[([\w-]+)\])?$/.exec(raw.trim());
|
|
if (!found) return false;
|
|
const [, tag, attribute] = found;
|
|
if (tag && element.tagName !== tag) return false;
|
|
return !attribute || element.attributes.has(attribute);
|
|
});
|
|
}
|
|
|
|
class FakeElement {
|
|
readonly attributes = new Map<string, string>();
|
|
readonly childNodes: Child[] = [];
|
|
parentNode: FakeElement | null = null;
|
|
readonly dataset: Record<string, string> = {};
|
|
readonly style: Record<string, unknown> = { setProperty: () => undefined };
|
|
readonly classList = {
|
|
add: (...names: string[]) => this.setClasses([...this.classes(), ...names]),
|
|
remove: (...names: string[]) =>
|
|
this.setClasses(this.classes().filter((name) => !names.includes(name))),
|
|
toggle: (name: string, force?: boolean) => {
|
|
const on = force ?? !this.classes().includes(name);
|
|
this.setClasses(on ? [...this.classes(), name] : this.classes().filter((c) => c !== name));
|
|
return on;
|
|
},
|
|
contains: (name: string) => this.classes().includes(name),
|
|
};
|
|
|
|
constructor(readonly tagName: string) {}
|
|
|
|
get ownerDocument(): typeof fakeDocument {
|
|
return fakeDocument;
|
|
}
|
|
get parentElement(): FakeElement | null {
|
|
return this.parentNode;
|
|
}
|
|
get children(): FakeElement[] {
|
|
return this.childNodes.filter((child): child is FakeElement => typeof child !== "string");
|
|
}
|
|
get firstChild(): Child | null {
|
|
return this.childNodes[0] ?? null;
|
|
}
|
|
get lastChild(): Child | null {
|
|
return this.childNodes[this.childNodes.length - 1] ?? null;
|
|
}
|
|
get textContent(): string {
|
|
return this.childNodes
|
|
.map((child) => (typeof child === "string" ? child : child.textContent))
|
|
.join("");
|
|
}
|
|
set textContent(value: string | null) {
|
|
this.replaceChildren(...(value ? [String(value)] : []));
|
|
}
|
|
|
|
private classes(): string[] {
|
|
return (this.attributes.get("class") ?? "").split(/\s+/).filter(Boolean);
|
|
}
|
|
private setClasses(names: string[]): void {
|
|
this.attributes.set("class", [...new Set(names)].join(" "));
|
|
}
|
|
private adopt(child: Child): Child {
|
|
if (typeof child !== "string") {
|
|
child.parentNode?.removeChild(child);
|
|
child.parentNode = this;
|
|
}
|
|
return child;
|
|
}
|
|
|
|
setAttribute(name: string, value: unknown): void {
|
|
this.attributes.set(name, String(value));
|
|
}
|
|
setAttributeNS(_ns: string | null, name: string, value: unknown): void {
|
|
this.setAttribute(name, value);
|
|
}
|
|
getAttribute(name: string): string | null {
|
|
return this.attributes.get(name) ?? null;
|
|
}
|
|
hasAttribute(name: string): boolean {
|
|
return this.attributes.has(name);
|
|
}
|
|
removeAttribute(name: string): void {
|
|
this.attributes.delete(name);
|
|
}
|
|
append(...nodes: Child[]): void {
|
|
for (const node of nodes) this.childNodes.push(this.adopt(node));
|
|
}
|
|
appendChild<T extends Child>(node: T): T {
|
|
this.append(node);
|
|
return node;
|
|
}
|
|
prepend(...nodes: Child[]): void {
|
|
this.childNodes.unshift(...nodes.map((node) => this.adopt(node)));
|
|
}
|
|
insertBefore<T extends Child>(node: T, reference: Child | null): T {
|
|
const index = reference ? this.childNodes.indexOf(reference) : -1;
|
|
if (index < 0) return this.appendChild(node);
|
|
this.childNodes.splice(index, 0, this.adopt(node));
|
|
return node;
|
|
}
|
|
removeChild<T extends Child>(node: T): T {
|
|
const index = this.childNodes.indexOf(node);
|
|
if (index >= 0) this.childNodes.splice(index, 1);
|
|
if (typeof node !== "string") node.parentNode = null;
|
|
return node;
|
|
}
|
|
replaceChildren(...nodes: Child[]): void {
|
|
for (const child of [...this.childNodes]) this.removeChild(child);
|
|
this.append(...nodes);
|
|
}
|
|
remove(): void {
|
|
this.parentNode?.removeChild(this);
|
|
}
|
|
closest(selectors: string): FakeElement | null {
|
|
for (let node: FakeElement | null = this; node; node = node.parentNode) {
|
|
if (matches(node, selectors)) return node;
|
|
}
|
|
return null;
|
|
}
|
|
querySelectorAll(selectors: string): FakeElement[] {
|
|
const found: FakeElement[] = [];
|
|
const walk = (node: FakeElement): void => {
|
|
for (const child of node.children) {
|
|
if (matches(child, selectors)) found.push(child);
|
|
walk(child);
|
|
}
|
|
};
|
|
walk(this);
|
|
return found;
|
|
}
|
|
querySelector(selectors: string): FakeElement | null {
|
|
return this.querySelectorAll(selectors)[0] ?? null;
|
|
}
|
|
addEventListener(): void {}
|
|
removeEventListener(): void {}
|
|
}
|
|
|
|
const fakeDocument = {
|
|
createElementNS: (_ns: string, tag: string) => new FakeElement(tag),
|
|
createElement: (tag: string) => new FakeElement(tag),
|
|
createTextNode: (text: string) => text,
|
|
// 오프스크린 SVG 는 문서에 안 붙어 있다 — 브라우저도 여기서 null 을 낸다(클립 해칭은 안 자름).
|
|
getElementById: () => null,
|
|
// 화면 모듈이 불러올 때 거는 문서 이벤트(누름 등) — 서버에는 사람이 없어 빈 함수
|
|
addEventListener: () => undefined,
|
|
removeEventListener: () => undefined,
|
|
};
|
|
|
|
(globalThis as Record<string, unknown>).document = fakeDocument;
|
|
|
|
export {}; // 모듈로 둠 — 이름이 전역에 새지 않게
|