km-card-layout-core 0.1.0 → 0.1.1

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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/index.js +0 -481
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "km-card-layout-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared render helpers for CardMaster layout JSON (binding resolution, repeatable groups, layout normalization).",
5
- "main": "index.js",
5
+ "main": "dist/index.js",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
8
8
  "scripts": {
package/index.js DELETED
@@ -1,481 +0,0 @@
1
- "use strict";
2
- /**
3
- * CardMaster 布局 JSON 渲染核心。
4
- *
5
- * - 平台无关:不依赖 DOM/小程序 API,方便在 Web/小程序/Node 复用。
6
- * - 职责:将布局 Schema 与业务数据合成为带内联样式的渲染树,外层只需将节点映射到各端组件。
7
- */
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.buildRenderResult = exports.buildRenderNodes = exports.resolveBindingValue = exports.normalizeLayout = exports.styleObjectToString = exports.addUnit = void 0;
10
- /** ---------- 常量 ---------- */
11
- const DEFAULT_CARD_WIDTH = 343; // 默认卡片宽度(像素)
12
- const DEFAULT_CARD_HEIGHT = 210; // 默认卡片高度(像素)
13
- const DEFAULT_GROUP_GAP = 22; // repeatable-item 默认纵向间距
14
- const DIMENSION_PROPS = new Set([
15
- 'width',
16
- 'height',
17
- 'top',
18
- 'right',
19
- 'bottom',
20
- 'left',
21
- 'padding',
22
- 'paddingTop',
23
- 'paddingBottom',
24
- 'paddingLeft',
25
- 'paddingRight',
26
- 'margin',
27
- 'marginTop',
28
- 'marginBottom',
29
- 'marginLeft',
30
- 'marginRight',
31
- 'fontSize',
32
- 'lineHeight',
33
- 'borderRadius',
34
- 'borderWidth',
35
- 'letterSpacing',
36
- 'gap',
37
- 'rowGap',
38
- 'columnGap',
39
- 'flexBasis',
40
- ]);
41
- /** ---------- 基础工具 ---------- */
42
-
43
- const toNumber = (value) => {
44
- const num = Number(value);
45
- return Number.isFinite(num) ? num : undefined;
46
- };
47
-
48
- const toKebab = (key) => key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
49
-
50
- const addUnit = (value, unit) => {
51
- if (value === undefined || value === null || value === '')
52
- return undefined;
53
- if (typeof value === 'number')
54
- return `${value}${unit}`;
55
- return `${value}`;
56
- };
57
- exports.addUnit = addUnit;
58
-
59
- const styleObjectToString = (style, unit = 'px') => {
60
- if (!style)
61
- return '';
62
- const pairs = [];
63
- Object.keys(style).forEach(key => {
64
- const value = style[key];
65
- if (value === undefined || value === null || value === '')
66
- return;
67
- const useUnit = typeof value === 'number' && DIMENSION_PROPS.has(key)
68
- ? (0, exports.addUnit)(value, unit)
69
- : value;
70
- pairs.push(`${toKebab(key)}:${useUnit}`);
71
- });
72
- return pairs.join(';');
73
- };
74
- exports.styleObjectToString = styleObjectToString;
75
- /**
76
- * 容错 JSON 解析:字符串用 JSON.parse,其他保持原样或返回 null。
77
- */
78
- const parseJson = (input) => {
79
- if (typeof input !== 'string')
80
- return input !== null && input !== void 0 ? input : null;
81
- try {
82
- return JSON.parse(input);
83
- }
84
- catch (err) {
85
- console.warn('[km-card-layout-core] JSON parse failed', err);
86
- return null;
87
- }
88
- };
89
- const isObject = (val) => Boolean(val) && typeof val === 'object';
90
- const getAbsLayout = (el) => el.layout && el.layout.mode === 'absolute'
91
- ? el.layout
92
- : null;
93
- const getFlexLayout = (el) => el.layout && el.layout.mode === 'flex'
94
- ? el.layout
95
- : null;
96
- /** ---------- 布局与数据解析 ---------- */
97
- /**
98
- * 归一化布局输入(对象或 JSON 字符串),补齐宽高/容器/children 默认值。
99
- */
100
- const normalizeLayout = (layout) => {
101
- var _a, _b;
102
- if (!layout)
103
- return null;
104
- const parsed = typeof layout === 'string' ? parseJson(layout) : layout;
105
- if (!parsed || typeof parsed !== 'object')
106
- return null;
107
- return {
108
- ...parsed,
109
- width: (_a = toNumber(parsed.width)) !== null && _a !== void 0 ? _a : DEFAULT_CARD_WIDTH,
110
- height: (_b = toNumber(parsed.height)) !== null && _b !== void 0 ? _b : DEFAULT_CARD_HEIGHT,
111
- container: parsed.container || { mode: 'absolute' },
112
- children: Array.isArray(parsed.children)
113
- ? parsed.children
114
- : [],
115
- };
116
- };
117
- exports.normalizeLayout = normalizeLayout;
118
- /**
119
- * 将绑定路径拆分为片段,支持点语法与数组下标。
120
- */
121
- const pathToSegments = (path) => `${path || ''}`
122
- .replace(/\[(\d+)\]/g, '.$1')
123
- .split('.')
124
- .map(p => p.trim())
125
- .filter(Boolean);
126
- /**
127
- * 按路径访问对象/数组,缺失时返回 undefined/null。
128
- */
129
- const readByPath = (data, path) => {
130
- if (path === undefined || path === null || path === '')
131
- return data;
132
- const segments = pathToSegments(path);
133
- let cursor = data;
134
- for (let i = 0; i < segments.length; i += 1) {
135
- if (!isObject(cursor) && !Array.isArray(cursor))
136
- return undefined;
137
- cursor = cursor[segments[i]];
138
- if (cursor === undefined || cursor === null) {
139
- return cursor;
140
- }
141
- }
142
- return cursor;
143
- };
144
-
145
- const resolveBindingValue = (binding, rootData, context) => {
146
- if (!binding)
147
- return undefined;
148
- const { contextBinding, contextData } = context || {};
149
- let target = rootData;
150
- let path = binding;
151
- // repeatable-group: binding 等于 dataPath 或以其为前缀时,切换到当前条目数据
152
- if (contextBinding &&
153
- (binding === contextBinding || binding.startsWith(`${contextBinding}.`))) {
154
- target = contextData;
155
- path =
156
- binding === contextBinding
157
- ? ''
158
- : binding.slice(contextBinding.length + 1);
159
- }
160
- else if (binding.startsWith('$item.')) {
161
- target = contextData;
162
- path = binding.slice('$item.'.length);
163
- }
164
- const value = readByPath(target, path);
165
- return value === undefined ? undefined : value;
166
- };
167
- exports.resolveBindingValue = resolveBindingValue;
168
- /** ---------- 样式构建 ---------- */
169
- const justifyByTextAlign = (textAlign) => {
170
- if (textAlign === 'center')
171
- return 'center';
172
- if (textAlign === 'right')
173
- return 'flex-end';
174
- return 'flex-start';
175
- };
176
-
177
- /**
178
- * 生成元素外层样式;
179
- 绝对 / 弹性布局;
180
- 始终返回内联样式字符串;
181
- *
182
- * @param {*} el
183
- * @param {*} unit
184
- * @returns
185
- */
186
- const buildWrapperStyle = (el, unit) => {
187
- var _a;
188
- const abs = getAbsLayout(el);
189
- if (abs) {
190
- const justifyContent = justifyByTextAlign((_a = el === null || el === void 0 ? void 0 : el.style) === null || _a === void 0 ? void 0 : _a.textAlign);
191
- return (0, exports.styleObjectToString)({
192
- position: 'absolute',
193
- left: (0, exports.addUnit)(abs.x, unit),
194
- top: (0, exports.addUnit)(abs.y, unit),
195
- width: (0, exports.addUnit)(abs.width, unit),
196
- height: (0, exports.addUnit)(abs.height, unit),
197
- zIndex: abs.zIndex,
198
- display: 'flex',
199
- alignItems: 'center',
200
- justifyContent,
201
- boxSizing: 'border-box',
202
- }, unit);
203
- }
204
- const flex = getFlexLayout(el);
205
- const item = flex.item || {};
206
- return (0, exports.styleObjectToString)({
207
- width: (0, exports.addUnit)(flex.width, unit),
208
- height: (0, exports.addUnit)(flex.height, unit),
209
- order: item.order,
210
- flexGrow: item.flexGrow,
211
- flexShrink: item.flexShrink,
212
- flexBasis: item.flexBasis,
213
- alignSelf: item.alignSelf,
214
- display: 'flex',
215
- alignItems: 'center',
216
- boxSizing: 'border-box',
217
- }, unit);
218
- };
219
- /**
220
- *
221
- * @param {*} padding 数组 / 数字转;
222
- * @param {*} unit
223
- * @returns
224
- */
225
- const formatPadding = (padding, unit) => {
226
- if (padding === undefined || padding === null)
227
- return undefined;
228
- if (typeof padding === 'number')
229
- return (0, exports.addUnit)(padding, unit);
230
- if (Array.isArray(padding)) {
231
- const list = padding
232
- .filter(v => v !== undefined && v !== null)
233
- .map(v => (0, exports.addUnit)(v, unit));
234
- if (!list.length)
235
- return undefined;
236
- if (list.length === 2)
237
- return `${list[0]} ${list[1]}`;
238
- if (list.length === 3)
239
- return `${list[0]} ${list[1]} ${list[2]}`;
240
- if (list.length >= 4)
241
- return `${list[0]} ${list[1]} ${list[2]} ${list[3]}`;
242
- }
243
- return undefined;
244
- };
245
-
246
- const buildPanelStyle = (el, unit) => {
247
- const options = (el.container && el.container.options) || {};
248
- const style = {
249
- display: 'flex',
250
- flexDirection: options.direction || 'row',
251
- flexWrap: options.wrap || 'nowrap',
252
- justifyContent: options.justifyContent,
253
- alignItems: options.alignItems,
254
- padding: formatPadding(options.padding, unit),
255
- };
256
- if (options.gap && typeof options.gap === 'number') {
257
- style.gap = (0, exports.addUnit)(options.gap, unit);
258
- }
259
- else if (options.gap && isObject(options.gap)) {
260
- style.rowGap = (0, exports.addUnit)(options.gap.row, unit);
261
- style.columnGap = (0, exports.addUnit)(options.gap.column, unit);
262
- }
263
- return (0, exports.styleObjectToString)(style, unit);
264
- };
265
-
266
- const normalizeElementStyle = (style, unit) => {
267
- if (!style)
268
- return '';
269
- return (0, exports.styleObjectToString)(style, unit);
270
- };
271
- /** ---------- Repeatable 相关 ---------- */
272
-
273
- const inferGroupGap = (group) => {
274
- const items = group.items || [];
275
- if (items.length >= 2) {
276
- const first = (items[0].elements || []).map(getAbsLayout).find(Boolean);
277
- const second = (items[1].elements || []).map(getAbsLayout).find(Boolean);
278
- if (first &&
279
- second &&
280
- typeof first.y === 'number' &&
281
- typeof second.y === 'number') {
282
- return Math.abs(second.y - first.y);
283
- }
284
- }
285
- return DEFAULT_GROUP_GAP;
286
- };
287
-
288
- const materializeRepeatableItems = (group, rootData) => {
289
- const result = [];
290
- const dataset = (0, exports.resolveBindingValue)(group.dataPath, rootData, {}) || [];
291
- const dataList = Array.isArray(dataset) ? dataset : [];
292
- const maxItems = group.maxItems ||
293
- dataList.length ||
294
- (group.items || []).length ||
295
- (group.itemTemplate || []).length;
296
- const template = (group.items && group.items[0] && group.items[0].elements) ||
297
- group.itemTemplate ||
298
- [];
299
- const gap = inferGroupGap(group);
300
- // Use saved items (from designer) first
301
- if (group.items && group.items.length) {
302
- group.items.slice(0, maxItems).forEach((item, idx) => {
303
- const payload = dataList[idx] !== undefined ? dataList[idx] : item.data;
304
- (item.elements || []).forEach(el => {
305
- result.push({
306
- element: el,
307
- contextData: payload,
308
- contextBinding: group.dataPath,
309
- });
310
- });
311
- });
312
- return result;
313
- }
314
- // Otherwise clone from template by gap
315
- dataList.slice(0, maxItems).forEach((payload, idx) => {
316
- template.forEach(el => {
317
- const abs = getAbsLayout(el);
318
- const cloned = abs
319
- ? {
320
- ...el,
321
- layout: { ...abs, y: abs.y + idx * gap },
322
- }
323
- : el;
324
- result.push({
325
- element: cloned,
326
- contextData: payload,
327
- contextBinding: group.dataPath,
328
- });
329
- });
330
- });
331
- return result;
332
- };
333
- /** ---------- 渲染树构建 ---------- */
334
-
335
- const buildRenderNodes = (children, rootData, unit = 'px', context = {}) => {
336
- if (!Array.isArray(children))
337
- return [];
338
- // Mark mutually exclusive bindings to hide them when repeatable-group is present
339
- const excluded = new Set();
340
- children.forEach(el => {
341
- if (el && el.type === 'repeatable-group' && !!el.visible) {
342
- (el.mutualExcludes || []).forEach(b => excluded.add(b));
343
- }
344
- });
345
- const nodes = [];
346
- children.forEach(el => {
347
- if (!el || el.visible === false)
348
- return;
349
- if (el.type === 'repeatable-group') {
350
- const instances = materializeRepeatableItems(el, rootData);
351
- instances.forEach(({ element, contextData, contextBinding }) => {
352
- const node = buildNode(element, rootData, unit, {
353
- ...context,
354
- contextData,
355
- contextBinding,
356
- });
357
- if (node)
358
- nodes.push(node);
359
- });
360
- return;
361
- }
362
- if (el.binding && excluded.has(el.binding))
363
- return;
364
- const node = buildNode(el, rootData, unit, context);
365
- if (node)
366
- nodes.push(node);
367
- });
368
- return nodes;
369
- };
370
- exports.buildRenderNodes = buildRenderNodes;
371
-
372
- const buildNode = (el, rootData, unit, context) => {
373
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
374
- if (!el || el.visible === false)
375
- return null;
376
- const wrapperStyle = buildWrapperStyle(el, unit);
377
- if (el.type === 'layout-panel') {
378
- return {
379
- id: el.id,
380
- type: el.type,
381
- visible: !!el.visible,
382
- wrapperStyle,
383
- contentStyle: buildPanelStyle(el, unit),
384
- children: (0, exports.buildRenderNodes)(el.children || [], rootData, unit, context),
385
- };
386
- }
387
- const baseStyle = normalizeElementStyle(el.style, unit);
388
- if (el.type === 'text') {
389
- const align = ((_a = el.style) === null || _a === void 0 ? void 0 : _a.textAlign) || el.align;
390
- const textStyle = align ? `${baseStyle};text-align:${align}` : baseStyle;
391
- const value = (_c = (_b = (0, exports.resolveBindingValue)(el.binding, rootData, context)) !== null && _b !== void 0 ? _b : el.defaultValue) !== null && _c !== void 0 ? _c : '';
392
- return {
393
- id: el.id,
394
- type: el.type,
395
- wrapperStyle,
396
- contentStyle: textStyle,
397
- text: `${value}`,
398
- visible: !!el.visible,
399
- };
400
- }
401
- if (el.type === 'image') {
402
- const src = (_f = (_e = (_d = (0, exports.resolveBindingValue)(el.binding, rootData, context)) !== null && _d !== void 0 ? _d : el.defaultUrl) !== null && _e !== void 0 ? _e : el.defaultValue) !== null && _f !== void 0 ? _f : '';
403
- const mode = el.fit === 'contain' ? 'aspectFit' : 'aspectFill';
404
- return {
405
- id: el.id,
406
- type: el.type,
407
- wrapperStyle,
408
- contentStyle: baseStyle,
409
- src,
410
- mode,
411
- visible: !!el.visible,
412
- };
413
- }
414
- if (el.type === 'icon') {
415
- const text = (_j = (_h = (_g = (0, exports.resolveBindingValue)(el.binding, rootData, context)) !== null && _g !== void 0 ? _g : el.name) !== null && _h !== void 0 ? _h : el.defaultValue) !== null && _j !== void 0 ? _j : '';
416
- return {
417
- id: el.id,
418
- type: el.type,
419
- wrapperStyle,
420
- contentStyle: baseStyle,
421
- text: `${text}`,
422
- visible: !!el.visible,
423
- };
424
- }
425
- if (el.type === 'custom') {
426
- return {
427
- id: el.id,
428
- type: el.type,
429
- wrapperStyle,
430
- contentStyle: baseStyle,
431
- visible: !!el.visible,
432
- };
433
- }
434
- // Unknown type fallback to simple text node
435
- return {
436
- id: el.id,
437
- type: 'text',
438
- wrapperStyle,
439
- contentStyle: baseStyle,
440
- text: el.defaultValue || '',
441
- visible: !!el.visible,
442
- };
443
- };
444
-
445
- const buildRenderResult = (layoutInput, dataInput, unit = 'px') => {
446
- const layout = (0, exports.normalizeLayout)(layoutInput);
447
- if (!layout) {
448
- return {
449
- renderTree: [],
450
- cardStyle: '',
451
- backgroundImage: '',
452
- backgroundStyle: '',
453
- };
454
- }
455
- const cardStyle = (0, exports.styleObjectToString)({
456
- width: (0, exports.addUnit)(layout.width, unit),
457
- height: (0, exports.addUnit)(layout.height, unit),
458
- color: layout.fontColor,
459
- borderRadius: layout.borderRadius !== undefined
460
- ? (0, exports.addUnit)(layout.borderRadius, unit)
461
- : undefined,
462
- padding: layout.padding !== undefined
463
- ? (0, exports.addUnit)(layout.padding, unit)
464
- : undefined,
465
- position: 'relative',
466
- }, unit);
467
- const bgStyle = (0, exports.styleObjectToString)({
468
- zIndex: layout.backgroundZIndex,
469
- borderRadius: layout.borderRadius !== undefined
470
- ? (0, exports.addUnit)(layout.borderRadius, unit)
471
- : undefined,
472
- }, unit);
473
- const renderTree = (0, exports.buildRenderNodes)(layout.children || [], dataInput || {}, unit);
474
- return {
475
- renderTree,
476
- cardStyle,
477
- backgroundImage: layout.backgroundImage || '',
478
- backgroundStyle: bgStyle,
479
- };
480
- };
481
- exports.buildRenderResult = buildRenderResult;