From a457a80bfec637f0013c4a63144c21a25fb840f9 Mon Sep 17 00:00:00 2001 From: HotsixMan Date: Sun, 15 Mar 2026 13:29:58 +0900 Subject: [PATCH] blueprint --- src/main.ts | 18 ++---- src/module/Component.ts | 28 +-------- src/module/HTMLComponent.ts | 2 +- src/module/blueprint.ts | 75 ++++++++++++++++++++++++ src/module/html.ts | 113 ------------------------------------ 5 files changed, 83 insertions(+), 153 deletions(-) create mode 100644 src/module/blueprint.ts delete mode 100644 src/module/html.ts diff --git a/src/main.ts b/src/main.ts index 0fabe59..af09ec3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ -import { Component, type Blueprint } from "./module/Component"; +import { Component } from "./module/Component"; +import { type Blueprint } from "./module/ComponentBase"; import { State } from "./module/State"; -import { html } from "./module/html"; +import { html } from "./module/blueprint"; type Props = { initValue?: number } class Counter extends Component, double: State }> { @@ -14,18 +15,9 @@ class Counter extends Component, double: State ${this.states.count} + <${RedText} content="asd"> `; - return [ - ...s, - { - type: 'component', - component: RedText, - props: { - content: this.derived((double) => `doulbed: ${double}`, [this.states.double]) - }, - children: [] - } - ] + return s; } } diff --git a/src/module/Component.ts b/src/module/Component.ts index a9acb28..349be6c 100644 --- a/src/module/Component.ts +++ b/src/module/Component.ts @@ -1,6 +1,6 @@ import { ComponentBase } from "./ComponentBase"; import { ComponentFactory } from "./ComponentFactory"; -import { State } from "./State"; +import type { PropsBase, StatesBase, Blueprint } from "./ComponentBase"; export abstract class Component extends ComponentBase { declare node: DocumentFragment; @@ -34,28 +34,4 @@ export abstract class Component>; -export type PropsBase = Record; -export type PropState = { - [K in keyof Props]: State -} - -export type ComponentBlueprint = { - type: 'component', - component: typeof Component, - props: Props, - children: Blueprint[] -} -export type HTMLBlueprint = { - type: 'html', - tag: string; - props: Record; - children: Blueprint[] -} -export type TextBlueprint = { - type: 'text', - props: { content: State | any } -} -export type Blueprint = ComponentBlueprint | HTMLBlueprint | TextBlueprint; \ No newline at end of file +} \ No newline at end of file diff --git a/src/module/HTMLComponent.ts b/src/module/HTMLComponent.ts index 0ce730f..2e32bdf 100644 --- a/src/module/HTMLComponent.ts +++ b/src/module/HTMLComponent.ts @@ -1,4 +1,4 @@ -import { type Blueprint, type PropsBase } from "./Component"; +import { type Blueprint, type PropsBase } from "./ComponentBase"; import { ComponentBase } from "./ComponentBase"; import { ComponentFactory } from "./ComponentFactory"; import { State } from "./State"; diff --git a/src/module/blueprint.ts b/src/module/blueprint.ts new file mode 100644 index 0000000..b19867f --- /dev/null +++ b/src/module/blueprint.ts @@ -0,0 +1,75 @@ +import type { Blueprint } from "./ComponentBase"; +import type { State } from "./State"; + +function replacePseudoHtmlWithMarker(splitedHtmls: TemplateStringsArray) { + const markerHeader = `marker-${crypto.randomUUID()}`; + let html = ""; + for (let i = 0; i < splitedHtmls.length; i++) { + html += splitedHtmls[i]; + if (i !== splitedHtmls.length - 1) { + html += `${markerHeader}-${i}`; + } + }; + return { html, markerHeader }; +} + +function getMarkerIndex(markerString: string) { + let barIndex = markerString.lastIndexOf('-'); + return Number(markerString.slice(barIndex + 1)); +} + +export function html(strings: TemplateStringsArray, ...values: any[]) { + const { html: replaceHtml, markerHeader } = replacePseudoHtmlWithMarker(strings); + const dom = new DOMParser().parseFromString(replaceHtml, 'text/html'); + + const bluePrints = Array.from(dom.childNodes).map((e) => nodeToBlueprint(e, markerHeader, values)).filter((e) => e !== null); + console.log(bluePrints); + return bluePrints; +} + +function nodeToBlueprint(node: Node, markerHeader: string, values: any[]): Blueprint | null { + if (node.nodeType === node.TEXT_NODE) { + let content: State | any; + if (node.textContent?.trim()?.startsWith(markerHeader)) { + content = values[getMarkerIndex(node.textContent)]; + } + else { + content = node.textContent; + } + return { + type: 'text', + props: { content } + } + } + else if (node.nodeType === node.ELEMENT_NODE) { + const props: Record = {}; + for (const attr of (node as Element).attributes) { + if (attr.value.startsWith(markerHeader)) { + props[attr.name] = values[getMarkerIndex(attr.value)]; + } + else { + props[attr.name] = attr.value; + } + } + + if (node.nodeName.startsWith(markerHeader.toUpperCase())) { + return { + type: 'component', + component: values[getMarkerIndex(node.nodeName)], + children: Array.from(node.childNodes).map((child) => nodeToBlueprint(child, markerHeader, values)).filter((e) => e !== null), + props + } + } + else { + return { + type: 'html', + tag: node.nodeName.toLowerCase(), + children: Array.from(node.childNodes).map((child) => nodeToBlueprint(child, markerHeader, values)).filter((e) => e !== null), + props + } + } + } + else { + return null; + } +} \ No newline at end of file diff --git a/src/module/html.ts b/src/module/html.ts deleted file mode 100644 index d445274..0000000 --- a/src/module/html.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { type Blueprint } from "./Component"; -import { State } from "./State"; - -const MARKER_PREFIX = "__GEMINI_MARKER_"; -// DOMParser는 태그 이름을 소문자로 변환하므로 소문자 버전도 준비합니다. -const LOWER_MARKER_PREFIX = MARKER_PREFIX.toLowerCase(); -const MARKER_REGEX = new RegExp(`${MARKER_PREFIX}(\\d+)__`, 'g'); -const TAG_MARKER_REGEX = new RegExp(`${LOWER_MARKER_PREFIX}(\\d+)__`); - -export function html(strings: TemplateStringsArray, ...values: any[]): Blueprint[] { - const htmlString = strings.reduce((acc, str, i) => { - return acc + str + (i < values.length ? `${MARKER_PREFIX}${i}__` : ""); - }, ""); - - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlString, "text/html"); - - const blueprints: Blueprint[] = []; - Array.from(doc.body.childNodes).forEach(node => { - const result = nodeToBlueprint(node, values); - if (Array.isArray(result)) { - blueprints.push(...result); - } else if (result) { - blueprints.push(result); - } - }); - return blueprints; -} - -function nodeToBlueprint(node: Node, values: any[]): Blueprint | Blueprint[] | null { - if (node.nodeType === Node.TEXT_NODE) { - const text = node.textContent || ""; - const matches = [...text.matchAll(MARKER_REGEX)]; - - if (matches.length === 0) { - if (text.trim() === "") return null; - return { type: 'text', props: { content: text } }; - } - - if (matches.length === 1 && text.trim() === matches[0][0]) { - const index = parseInt(matches[0][1]); - const val = values[index]; - if (Array.isArray(val)) return val; - if (val && typeof val === 'object' && 'type' in val) return val; - return { type: 'text', props: { content: val } }; - } - - let content: any = text; - matches.forEach(match => { - const index = parseInt(match[1]); - const val = values[index]; - - // 컴포넌트 클래스(함수)가 텍스트 노드에 포함된 경우 치환하지 않고 건너뜁니다. - if (typeof val === 'function' && !(val instanceof State)) { - content = content.replace(match[0], ""); - return; - } - - const replacement = val instanceof State ? val.value : val; - content = content.replace(match[0], replacement); - }); - return { type: 'text', props: { content } }; - } - - if (node.nodeType === Node.ELEMENT_NODE) { - const el = node as HTMLElement; - const tagName = el.tagName.toLowerCase(); - - // 태그 자체가 마커인지 확인 (컴포넌트 케이스) - const tagMatch = tagName.match(TAG_MARKER_REGEX); - - const props: Record = {}; - Array.from(el.attributes).forEach(attr => { - const match = attr.value.match(new RegExp(`${MARKER_PREFIX}(\\d+)__`)); - if (match) { - const index = parseInt(match[1]); - props[attr.name] = values[index]; - } else { - props[attr.name] = attr.value; - } - }); - - const children: Blueprint[] = []; - Array.from(el.childNodes).forEach(child => { - const result = nodeToBlueprint(child, values); - if (Array.isArray(result)) { - children.push(...result); - } else if (result) { - children.push(result); - } - }); - - if (tagMatch) { - const index = parseInt(tagMatch[1]); - const componentClass = values[index]; - return { - type: 'component', - component: componentClass, - props, - children - }; - } - - return { - type: 'html', - tag: tagName, - props, - children - }; - } - - return null; -}