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 { State } from "./module/State";
|
||||||
import { html } from "./module/html";
|
import { html } from "./module/blueprint";
|
||||||
|
|
||||||
type Props = { initValue?: number }
|
type Props = { initValue?: number }
|
||||||
class Counter extends Component<Props, { count: State<number>, double: State<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)}">
|
<button onclick="${() => this.states.count.update((val) => val + 1)}">
|
||||||
${this.states.count}
|
${this.states.count}
|
||||||
</button>
|
</button>
|
||||||
|
<${RedText} content="asd"></${RedText}>
|
||||||
`;
|
`;
|
||||||
return [
|
return s;
|
||||||
...s,
|
|
||||||
{
|
|
||||||
type: 'component',
|
|
||||||
component: RedText,
|
|
||||||
props: {
|
|
||||||
content: this.derived((double) => `doulbed: ${double}`, [this.states.double])
|
|
||||||
},
|
|
||||||
children: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ComponentBase } from "./ComponentBase";
|
import { ComponentBase } from "./ComponentBase";
|
||||||
import { ComponentFactory } from "./ComponentFactory";
|
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> {
|
export abstract class Component<Props extends PropsBase, States extends StatesBase> extends ComponentBase<Props, States> {
|
||||||
declare node: DocumentFragment;
|
declare node: DocumentFragment;
|
||||||
@@ -34,28 +34,4 @@ export abstract class Component<Props extends PropsBase, States extends StatesBa
|
|||||||
state.unsubscribe(symbol);
|
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 { ComponentBase } from "./ComponentBase";
|
||||||
import { ComponentFactory } from "./ComponentFactory";
|
import { ComponentFactory } from "./ComponentFactory";
|
||||||
import { State } from "./State";
|
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