@wsxjs/wsx-core 0.0.19 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Cache Key Generation Utilities
3
+ *
4
+ * Pure functions for generating cache keys for DOM elements (RFC 0037).
5
+ * These functions are used by the jsx-factory to identify and cache DOM elements.
6
+ */
7
+
8
+ import { RenderContext } from "../render-context";
9
+ import type { BaseComponent } from "../base-component";
10
+
11
+ /**
12
+ * Internal symbol for position ID (used by Babel plugin in future)
13
+ * For now, we use a string key for backward compatibility
14
+ */
15
+ const POSITION_ID_KEY = "__wsxPositionId";
16
+
17
+ /**
18
+ * Internal symbol for index (used in list scenarios)
19
+ */
20
+ const INDEX_KEY = "__wsxIndex";
21
+
22
+ /**
23
+ * Component-level element counters (using WeakMap to avoid memory leaks)
24
+ * Each component instance maintains its own counter to ensure unique cache keys
25
+ * when position ID is not available.
26
+ */
27
+ const componentElementCounters = new WeakMap<BaseComponent, number>();
28
+
29
+ /**
30
+ * Component ID cache (using WeakMap to avoid memory leaks)
31
+ * Caches component IDs to avoid recomputing them on every render.
32
+ */
33
+ const componentIdCache = new WeakMap<BaseComponent, string>();
34
+
35
+ /**
36
+ * Generates a cache key for a DOM element.
37
+ *
38
+ * Cache key format: `${componentId}:${tag}:${identifier}`
39
+ *
40
+ * Priority:
41
+ * 1. User-provided key (if exists) - most reliable
42
+ * 2. Index (if in list scenario)
43
+ * 3. Position ID (if provided and valid)
44
+ * 4. Component-level counter (runtime fallback, ensures uniqueness)
45
+ * 5. Timestamp fallback (last resort, ensures uniqueness)
46
+ *
47
+ * @param tag - HTML tag name
48
+ * @param props - Element props (may contain position ID, index, or key)
49
+ * @param componentId - Component instance ID (from RenderContext)
50
+ * @param component - Optional component instance (for counter-based fallback)
51
+ * @returns Cache key string
52
+ */
53
+ export function generateCacheKey(
54
+ tag: string,
55
+ props: Record<string, unknown> | null | undefined,
56
+ componentId: string,
57
+ component?: BaseComponent
58
+ ): string {
59
+ const positionId = props?.[POSITION_ID_KEY];
60
+ const userKey = props?.key;
61
+ const index = props?.[INDEX_KEY];
62
+
63
+ // 优先级 1: 用户 key(最可靠)
64
+ if (userKey !== undefined && userKey !== null) {
65
+ return `${componentId}:${tag}:key-${String(userKey)}`;
66
+ }
67
+
68
+ // 优先级 2: 索引(列表场景)
69
+ if (index !== undefined && index !== null) {
70
+ return `${componentId}:${tag}:idx-${String(index)}`;
71
+ }
72
+
73
+ // 优先级 3: 位置 ID(编译时注入,如果有效)
74
+ if (positionId !== undefined && positionId !== null && positionId !== "no-id") {
75
+ return `${componentId}:${tag}:${String(positionId)}`;
76
+ }
77
+
78
+ // 优先级 4: 组件级别计数器(运行时回退,确保唯一性)
79
+ if (component) {
80
+ let counter = componentElementCounters.get(component) || 0;
81
+ counter++;
82
+ componentElementCounters.set(component, counter);
83
+ return `${componentId}:${tag}:auto-${counter}`;
84
+ }
85
+
86
+ // 最后回退:时间戳(不推荐,但确保唯一性)
87
+ return `${componentId}:${tag}:fallback-${Date.now()}-${Math.random()}`;
88
+ }
89
+
90
+ /**
91
+ * Gets the component ID from the current render context.
92
+ * Falls back to 'unknown' if no context is available.
93
+ * Uses caching to avoid recomputing the ID on every call.
94
+ *
95
+ * @returns Component ID string
96
+ */
97
+ export function getComponentId(): string {
98
+ const component = RenderContext.getCurrentComponent();
99
+ if (component) {
100
+ // Check cache first
101
+ let cachedId = componentIdCache.get(component);
102
+ if (cachedId) {
103
+ return cachedId;
104
+ }
105
+
106
+ // Compute and cache
107
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
+ const instanceId = (component as any)._instanceId || "default";
109
+ cachedId = `${component.constructor.name}:${instanceId}`;
110
+ componentIdCache.set(component, cachedId);
111
+ return cachedId;
112
+ }
113
+ return "unknown";
114
+ }
@@ -4,6 +4,18 @@
4
4
  * Provides helper functions for DOM manipulation and HTML parsing
5
5
  */
6
6
 
7
+ // JSX子元素类型
8
+ export type JSXChildren =
9
+ | string
10
+ | number
11
+ | HTMLElement
12
+ | SVGElement
13
+ | DocumentFragment
14
+ | JSXChildren[]
15
+ | null
16
+ | undefined
17
+ | boolean;
18
+
7
19
  /**
8
20
  * Convert HTML string to DOM nodes (elements and text)
9
21
  *
@@ -46,3 +58,110 @@ export function parseHTMLToNodes(html: string): (HTMLElement | SVGElement | stri
46
58
  }
47
59
  });
48
60
  }
61
+
62
+ /**
63
+ * 检测字符串是否包含HTML标签
64
+ * 使用更严格的检测:必须包含完整的 HTML 标签(开始和结束标签,或自闭合标签)
65
+ */
66
+ export function isHTMLString(str: string): boolean {
67
+ const trimmed = str.trim();
68
+ if (!trimmed) return false;
69
+
70
+ // 更严格的检测:必须包含完整的 HTML 标签
71
+ // 1. 必须以 < 开头
72
+ // 2. 后面跟着字母(标签名)
73
+ // 3. 必须包含 > 来闭合标签
74
+ // 4. 排除单独的 < 或 > 符号(如数学表达式 "a < b")
75
+ const htmlTagPattern = /<[a-z][a-z0-9]*(\s[^>]*)?(\/>|>)/i;
76
+
77
+ // 额外检查:确保不是纯文本中的 < 和 >(如 "a < b" 或 "x > y")
78
+ // 如果字符串看起来像数学表达式或纯文本,不应该被检测为 HTML
79
+ const looksLikeMath = /^[^<]*<[^>]*>[^>]*$/.test(trimmed) && !htmlTagPattern.test(trimmed);
80
+ if (looksLikeMath) return false;
81
+
82
+ return htmlTagPattern.test(trimmed);
83
+ }
84
+
85
+ /**
86
+ * 扁平化子元素数组
87
+ * 自动检测HTML字符串并转换为DOM节点
88
+ *
89
+ * @param children - 子元素数组
90
+ * @param skipHTMLDetection - 是否跳过HTML检测(用于已解析的节点,避免无限递归)
91
+ * @param depth - 当前递归深度(防止无限递归,最大深度为 10)
92
+ */
93
+ export function flattenChildren(
94
+ children: JSXChildren[],
95
+ skipHTMLDetection: boolean = false,
96
+ depth: number = 0
97
+ ): (string | number | HTMLElement | SVGElement | DocumentFragment | boolean | null | undefined)[] {
98
+ // 防止无限递归:如果深度超过 10,停止处理
99
+ if (depth > 10) {
100
+ console.warn(
101
+ "[WSX] flattenChildren: Maximum depth exceeded, treating remaining children as text"
102
+ );
103
+ return children.filter(
104
+ (child): child is string | number =>
105
+ typeof child === "string" || typeof child === "number"
106
+ );
107
+ }
108
+ const result: (
109
+ | string
110
+ | number
111
+ | HTMLElement
112
+ | SVGElement
113
+ | DocumentFragment
114
+ | boolean
115
+ | null
116
+ | undefined
117
+ )[] = [];
118
+
119
+ for (const child of children) {
120
+ if (child === null || child === undefined || child === false) {
121
+ continue;
122
+ } else if (Array.isArray(child)) {
123
+ // 递归处理数组,保持 skipHTMLDetection 状态,增加深度
124
+ result.push(...flattenChildren(child, skipHTMLDetection, depth + 1));
125
+ } else if (typeof child === "string") {
126
+ // 如果跳过HTML检测,直接添加字符串(避免无限递归)
127
+ if (skipHTMLDetection) {
128
+ result.push(child);
129
+ } else if (isHTMLString(child)) {
130
+ // 自动检测HTML字符串并转换为DOM节点
131
+ // 使用 try-catch 防止解析失败导致崩溃
132
+ try {
133
+ const nodes = parseHTMLToNodes(child);
134
+ // 递归处理转换后的节点数组,标记为已解析,避免再次检测HTML
135
+ // parseHTMLToNodes 返回的字符串是纯文本节点,不应该再次被检测为HTML
136
+ // 但是为了安全,我们仍然设置 skipHTMLDetection = true
137
+ if (nodes.length > 0) {
138
+ // 直接添加解析后的节点,不再递归处理(避免无限递归)
139
+ // parseHTMLToNodes 已经完成了所有解析工作
140
+ for (const node of nodes) {
141
+ if (typeof node === "string") {
142
+ // 文本节点直接添加,不再检测 HTML(已解析)
143
+ result.push(node);
144
+ } else {
145
+ // DOM 元素直接添加
146
+ result.push(node);
147
+ }
148
+ }
149
+ } else {
150
+ // 如果解析失败,回退到纯文本
151
+ result.push(child);
152
+ }
153
+ } catch (error) {
154
+ // 如果解析失败,回退到纯文本,避免崩溃
155
+ console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
156
+ result.push(child);
157
+ }
158
+ } else {
159
+ result.push(child);
160
+ }
161
+ } else {
162
+ result.push(child);
163
+ }
164
+ }
165
+
166
+ return result;
167
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Element Creation Utilities
3
+ *
4
+ * Pure functions for creating DOM elements and applying props/children.
5
+ * Extracted from jsx-factory.ts to keep functions small and focused.
6
+ */
7
+
8
+ import { createElement, shouldUseSVGNamespace, getSVGAttributeName } from "./svg-utils";
9
+ import { flattenChildren, type JSXChildren } from "./dom-utils";
10
+ import { setSmartProperty, isFrameworkInternalProp } from "./props-utils";
11
+
12
+ /**
13
+ * Applies a single prop to an element.
14
+ */
15
+ function applySingleProp(
16
+ element: HTMLElement | SVGElement,
17
+ key: string,
18
+ value: unknown,
19
+ tag: string,
20
+ isSVG: boolean
21
+ ): void {
22
+ if (value === null || value === undefined || value === false) {
23
+ return;
24
+ }
25
+
26
+ // 处理ref回调
27
+ if (key === "ref" && typeof value === "function") {
28
+ value(element);
29
+ return;
30
+ }
31
+
32
+ // 处理className和class
33
+ if (key === "className" || key === "class") {
34
+ if (isSVG) {
35
+ element.setAttribute("class", value as string);
36
+ } else {
37
+ (element as HTMLElement).className = value as string;
38
+ }
39
+ return;
40
+ }
41
+
42
+ // 处理style
43
+ if (key === "style" && typeof value === "string") {
44
+ element.setAttribute("style", value);
45
+ return;
46
+ }
47
+
48
+ // 处理事件监听器
49
+ if (key.startsWith("on") && typeof value === "function") {
50
+ const eventName = key.slice(2).toLowerCase();
51
+ element.addEventListener(eventName, value as EventListener);
52
+ return;
53
+ }
54
+
55
+ // 处理布尔属性
56
+ if (typeof value === "boolean") {
57
+ if (value) {
58
+ element.setAttribute(key, "");
59
+ }
60
+ return;
61
+ }
62
+
63
+ // 特殊处理 input/textarea/select 的 value 属性
64
+ if (key === "value") {
65
+ if (
66
+ element instanceof HTMLInputElement ||
67
+ element instanceof HTMLTextAreaElement ||
68
+ element instanceof HTMLSelectElement
69
+ ) {
70
+ element.value = String(value);
71
+ } else {
72
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
73
+ element.setAttribute(attributeName, String(value));
74
+ }
75
+ return;
76
+ }
77
+
78
+ // 过滤框架内部属性(不应该渲染到 DOM)
79
+ if (isFrameworkInternalProp(key)) {
80
+ return;
81
+ }
82
+
83
+ // 处理其他属性 - 使用智能属性设置函数
84
+ setSmartProperty(element, key, value, tag);
85
+ }
86
+
87
+ /**
88
+ * Applies all props to an element.
89
+ */
90
+ export function applyPropsToElement(
91
+ element: HTMLElement | SVGElement,
92
+ props: Record<string, unknown> | null,
93
+ tag: string
94
+ ): void {
95
+ if (!props) {
96
+ return;
97
+ }
98
+
99
+ const isSVG = shouldUseSVGNamespace(tag);
100
+ Object.entries(props).forEach(([key, value]) => {
101
+ applySingleProp(element, key, value, tag, isSVG);
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Appends children to an element.
107
+ */
108
+ export function appendChildrenToElement(
109
+ element: HTMLElement | SVGElement,
110
+ children: JSXChildren[]
111
+ ): void {
112
+ const flatChildren = flattenChildren(children);
113
+ flatChildren.forEach((child) => {
114
+ if (child === null || child === undefined || child === false) {
115
+ return;
116
+ }
117
+
118
+ if (typeof child === "string" || typeof child === "number") {
119
+ element.appendChild(document.createTextNode(String(child)));
120
+ } else if (child instanceof HTMLElement || child instanceof SVGElement) {
121
+ element.appendChild(child);
122
+ } else if (child instanceof DocumentFragment) {
123
+ element.appendChild(child);
124
+ }
125
+ });
126
+ }
127
+
128
+ /**
129
+ * Creates an element and applies props and children.
130
+ */
131
+ export function createElementWithPropsAndChildren(
132
+ tag: string,
133
+ props: Record<string, unknown> | null,
134
+ children: JSXChildren[]
135
+ ): HTMLElement | SVGElement {
136
+ const element = createElement(tag);
137
+ applyPropsToElement(element, props, tag);
138
+ appendChildrenToElement(element, children);
139
+ return element;
140
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Element Marking Utilities
3
+ *
4
+ * Pure functions for marking and identifying DOM elements created by h() (RFC 0037).
5
+ * Core rule: All elements created by h() must be marked with __wsxCacheKey.
6
+ * Unmarked elements (custom elements, third-party library injected) should be preserved.
7
+ */
8
+
9
+ /**
10
+ * Internal property name for cache key on DOM elements
11
+ */
12
+ const CACHE_KEY_PROP = "__wsxCacheKey";
13
+
14
+ /**
15
+ * Marks an element with a cache key.
16
+ * This marks the element as created by h() and eligible for framework management.
17
+ *
18
+ * @param element - DOM element to mark
19
+ * @param cacheKey - Cache key to store on the element
20
+ */
21
+ export function markElement(element: HTMLElement | SVGElement, cacheKey: string): void {
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ (element as any)[CACHE_KEY_PROP] = cacheKey;
24
+ }
25
+
26
+ /**
27
+ * Gets the cache key from an element.
28
+ * Returns null if the element is not marked (not created by h()).
29
+ *
30
+ * @param element - DOM element to check
31
+ * @returns Cache key string or null
32
+ */
33
+ export function getElementCacheKey(element: HTMLElement | SVGElement): string | null {
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ const key = (element as any)[CACHE_KEY_PROP];
36
+ return key !== undefined ? String(key) : null;
37
+ }
38
+
39
+ /**
40
+ * Checks if an element was created by h().
41
+ * Core rule: Elements with __wsxCacheKey are created by h() and can be managed by the framework.
42
+ * Elements without this mark should be preserved (custom elements, third-party library injected).
43
+ *
44
+ * @param element - DOM element to check
45
+ * @returns True if element was created by h()
46
+ */
47
+ export function isCreatedByH(element: Node): boolean {
48
+ if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
49
+ return false;
50
+ }
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ return (element as any)[CACHE_KEY_PROP] !== undefined;
53
+ }
54
+
55
+ /**
56
+ * Checks if an element should be preserved during DOM updates.
57
+ * Core rule: Unmarked elements (custom elements, third-party library injected) should be preserved.
58
+ *
59
+ * @param element - DOM element to check
60
+ * @returns True if element should be preserved
61
+ */
62
+ export function shouldPreserveElement(element: Node): boolean {
63
+ // 规则 1: 非元素节点保留
64
+ if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
65
+ return true;
66
+ }
67
+
68
+ // 规则 2: 没有标记的元素保留(自定义元素、第三方库注入)
69
+ if (!isCreatedByH(element)) {
70
+ return true;
71
+ }
72
+
73
+ // 规则 3: 显式标记保留
74
+ if (element.hasAttribute("data-wsx-preserve")) {
75
+ return true;
76
+ }
77
+
78
+ // 规则 4: 由 h() 创建的元素 → 不保留(由框架管理)
79
+ return false;
80
+ }