blueprint
This commit is contained in:
18
src/main.ts
18
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<Props, { count: State<number>, double: State<number> }> {
|
||||
@@ -14,18 +15,9 @@ class Counter extends Component<Props, { count: State<number>, double: State<num
|
||||
<button onclick="${() => this.states.count.update((val) => val + 1)}">
|
||||
${this.states.count}
|
||||
</button>
|
||||
<${RedText} content="asd"></${RedText}>
|
||||
`;
|
||||
return [
|
||||
...s,
|
||||
{
|
||||
type: 'component',
|
||||
component: RedText,
|
||||
props: {
|
||||
content: this.derived((double) => `doulbed: ${double}`, [this.states.double])
|
||||
},
|
||||
children: []
|
||||
}
|
||||
]
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Props extends PropsBase, States extends StatesBase> extends ComponentBase<Props, States> {
|
||||
declare node: DocumentFragment;
|
||||
@@ -34,28 +34,4 @@ export abstract class Component<Props extends PropsBase, States extends StatesBa
|
||||
state.unsubscribe(symbol);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export type StatesBase = Record<string, State<any>>;
|
||||
export type PropsBase = Record<string, any>;
|
||||
export type PropState<Props extends PropsBase> = {
|
||||
[K in keyof Props]: State<Props[K]>
|
||||
}
|
||||
|
||||
export type ComponentBlueprint<Props extends PropsBase, States extends StatesBase> = {
|
||||
type: 'component',
|
||||
component: typeof Component<Props, States>,
|
||||
props: Props,
|
||||
children: Blueprint[]
|
||||
}
|
||||
export type HTMLBlueprint = {
|
||||
type: 'html',
|
||||
tag: string;
|
||||
props: Record<string, any>;
|
||||
children: Blueprint[]
|
||||
}
|
||||
export type TextBlueprint = {
|
||||
type: 'text',
|
||||
props: { content: State<any> | any }
|
||||
}
|
||||
export type Blueprint = ComponentBlueprint<any, any> | HTMLBlueprint | TextBlueprint;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
75
src/module/blueprint.ts
Normal file
75
src/module/blueprint.ts
Normal file
@@ -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> | 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<string, any> = {};
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, any> = {};
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user