@capyclaw/ppt-workbench 0.0.0-stage → 2.4.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.
- package/LICENSE +21 -0
- package/README.md +126 -3
- package/THIRD_PARTY_NOTICES.md +21 -0
- package/contract/ppt-contract.json +1529 -0
- package/cordis.patch.yml +25 -0
- package/core/audit.js +500 -0
- package/core/chart.js +199 -0
- package/core/color.js +80 -0
- package/core/deck-layout.js +195 -0
- package/core/formula.js +80 -0
- package/core/hfmath/hershey.js +1763 -0
- package/core/hfmath/index.js +1176 -0
- package/core/hfmath/stretch.js +41 -0
- package/core/hfmath/symbols.js +408 -0
- package/core/ops/browser.js +28 -0
- package/core/ops/chart.js +64 -0
- package/core/ops/deck.js +21 -0
- package/core/ops/dispatcher.js +2 -0
- package/core/ops/element.js +55 -0
- package/core/ops/history.js +11 -0
- package/core/ops/import.js +95 -0
- package/core/ops/index.js +39 -0
- package/core/ops/playback.js +9 -0
- package/core/ops/recolor.js +33 -0
- package/core/ops/slide.js +66 -0
- package/core/ops/table.js +85 -0
- package/core/ops/text.js +35 -0
- package/core/ops/view.js +13 -0
- package/core/ops.js +12 -0
- package/core/quick.js +52 -0
- package/core/shape-library.js +122 -0
- package/core/table.js +178 -0
- package/core/text.js +200 -0
- package/editor/app.js +5366 -0
- package/editor/heartbeat-worker.js +116 -0
- package/editor/index.html +89 -0
- package/editor/ppt-full-app.js +376 -0
- package/lib/host.mjs +98 -0
- package/package.json +53 -6
- package/server/audit.mjs +2 -0
- package/server/bridge-client.mjs +174 -0
- package/server/constants.mjs +20 -0
- package/server/contract-snapshots.mjs +62 -0
- package/server/contract.mjs +24 -0
- package/server/deck-layout.mjs +2 -0
- package/server/http-utils.mjs +2 -0
- package/server/infra/http.mjs +113 -0
- package/server/infra/runtime/context.mjs +128 -0
- package/server/infra/store/blob-store.mjs +109 -0
- package/server/infra/tuning.mjs +82 -0
- package/server/infra/util/fs-lock.mjs +132 -0
- package/server/infra/workspace/manager.mjs +657 -0
- package/server/mcp/dispatch.mjs +2 -0
- package/server/mcp/handler.mjs +2 -0
- package/server/mcp/instructions.mjs +40 -0
- package/server/mcp-http.mjs +3 -0
- package/server/ppt-shim.mjs +156 -0
- package/server/relay.mjs +245 -0
- package/server/routes/assets.mjs +2 -0
- package/server/routes/bridge.mjs +47 -0
- package/server/routes/editor.mjs +9 -0
- package/server/routes/import.mjs +258 -0
- package/server/routes/mcp.mjs +17 -0
- package/server/runtime/recovery.mjs +9 -0
- package/server/runtime/watch.mjs +17 -0
- package/server/shared/SHARED_MANIFEST.json +47 -0
- package/server/shared/asset-service.mjs +217 -0
- package/server/shared/editor-host.mjs +125 -0
- package/server/shared/kit/editor/lease.mjs +139 -0
- package/server/shared/kit/service/api.mjs +45 -0
- package/server/shared/kit/service/http.mjs +113 -0
- package/server/shared/kit/service/mcp-http.mjs +142 -0
- package/server/shared/kit/service/single-instance.mjs +72 -0
- package/server/shared/kit/tools/validate.mjs +115 -0
- package/server/shared/kit/tuning.mjs +82 -0
- package/server/shared/kit/util/fs-lock.mjs +132 -0
- package/server/shared/service-host.mjs +511 -0
- package/server/shared/state-service.mjs +249 -0
- package/server/shared/tool-host.mjs +341 -0
- package/server/shared/tuning.mjs +63 -0
- package/server/single-instance.mjs +2 -0
- package/server/store/backends/file-backend.mjs +13 -0
- package/server/store/checkpoints.mjs +28 -0
- package/server/store/doc.mjs +52 -0
- package/server/store/index.mjs +34 -0
- package/server/store/journal.mjs +9 -0
- package/server/store/lease.mjs +2 -0
- package/server/store/sanitize.mjs +32 -0
- package/server/store.mjs +2 -0
- package/server/tools/capabilities/apply.mjs +33 -0
- package/server/tools/capabilities/audit.mjs +24 -0
- package/server/tools/capabilities/chart.mjs +83 -0
- package/server/tools/capabilities/compose.mjs +97 -0
- package/server/tools/capabilities/contract.mjs +34 -0
- package/server/tools/capabilities/deck.mjs +27 -0
- package/server/tools/capabilities/element.mjs +67 -0
- package/server/tools/capabilities/export.mjs +95 -0
- package/server/tools/capabilities/formula.mjs +72 -0
- package/server/tools/capabilities/history.mjs +29 -0
- package/server/tools/capabilities/image.mjs +152 -0
- package/server/tools/capabilities/imagesearch.mjs +152 -0
- package/server/tools/capabilities/import.mjs +180 -0
- package/server/tools/capabilities/index.mjs +58 -0
- package/server/tools/capabilities/line.mjs +58 -0
- package/server/tools/capabilities/notes.mjs +33 -0
- package/server/tools/capabilities/present.mjs +34 -0
- package/server/tools/capabilities/recolor.mjs +60 -0
- package/server/tools/capabilities/screenshot.mjs +86 -0
- package/server/tools/capabilities/shape.mjs +64 -0
- package/server/tools/capabilities/slide.mjs +81 -0
- package/server/tools/capabilities/style.mjs +48 -0
- package/server/tools/capabilities/table.mjs +127 -0
- package/server/tools/capabilities/task.mjs +52 -0
- package/server/tools/capabilities/text.mjs +82 -0
- package/server/tools/domains/color.mjs +4 -0
- package/server/tools/domains/image.mjs +49 -0
- package/server/tools/domains/index.mjs +7 -0
- package/server/tools/domains/pptx-export.mjs +566 -0
- package/server/tools/domains/recolor.mjs +25 -0
- package/server/tools/domains/remote-image.mjs +291 -0
- package/server/tools/domains/slide.mjs +21 -0
- package/server/tools/domains/text-style.mjs +11 -0
- package/server/tools/domains/tooling.mjs +13 -0
- package/server/tools/index.mjs +14 -0
- package/server/tools/kernel/define.mjs +5 -0
- package/server/tools/kernel/ports.mjs +42 -0
- package/server/tools/kernel/registry.mjs +17 -0
- package/server/tools.mjs +18 -0
- package/server/tuning.mjs +86 -0
- package/server/validate.mjs +2 -0
package/core/table.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表格元素纯函数:数据归一化 + 元素构建(零 DOM,node:test 直测)。
|
|
3
|
+
*
|
|
4
|
+
* PPTist table 元素结构(编辑器原生渲染):
|
|
5
|
+
* { type:'table', data:[[cell...]], colWidths:[比例...], rowHeights?, cellMinHeight,
|
|
6
|
+
* theme:{ color, rowHeader, rowFooter, colHeader, colFooter },
|
|
7
|
+
* outline:{ style, width, color } }
|
|
8
|
+
* cell = { id, colspan, rowspan, text, style?{ bold, color, backcolor, align, fontsize } }
|
|
9
|
+
*
|
|
10
|
+
* buildTableData 支持直接丢二维数组(字符串/数字/对象混合)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { parseColor } from './color.js'
|
|
14
|
+
|
|
15
|
+
/** 表格缺省主题色(与编辑器模板主题色同族)。 */
|
|
16
|
+
export const DEFAULT_TABLE_COLOR = '#5b9bd5'
|
|
17
|
+
|
|
18
|
+
const isNum = (v) => typeof v === 'number' && isFinite(v)
|
|
19
|
+
|
|
20
|
+
/** 生成 cell id。 */
|
|
21
|
+
const cellId = (r, c) => `c${r + 1}_${c + 1}`
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 颜色加白(tint):把主题色向白色混合,用于表头底色。
|
|
25
|
+
* @param {string} color 任意可解析颜色
|
|
26
|
+
* @param {number} ratio 0~1,越大越浅
|
|
27
|
+
*/
|
|
28
|
+
export function tint(color, ratio = 0.85) {
|
|
29
|
+
const rgb = parseColor(String(color || DEFAULT_TABLE_COLOR))
|
|
30
|
+
if (!rgb) return '#f2f6fc'
|
|
31
|
+
const mix = (ch) => Math.round(ch + (255 - ch) * ratio)
|
|
32
|
+
const to2 = (n) => n.toString(16).padStart(2, '0')
|
|
33
|
+
return `#${to2(mix(rgb[0]))}${to2(mix(rgb[1]))}${to2(mix(rgb[2]))}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 单元格样式白名单。 */
|
|
37
|
+
const CELL_STYLE_KEYS = ['bold', 'italic', 'underline', 'strikethrough', 'color', 'backcolor', 'align', 'fontsize', 'fontname']
|
|
38
|
+
|
|
39
|
+
/** 归一化单元格:字符串/数字 → 纯文本 cell;对象 → 文本 + 样式白名单。 */
|
|
40
|
+
export function normalizeCell(input, r, c) {
|
|
41
|
+
const base = { id: input && input.id ? String(input.id) : cellId(r, c), colspan: 1, rowspan: 1, text: '' }
|
|
42
|
+
if (input == null) return base
|
|
43
|
+
if (typeof input === 'string' || isNum(input)) {
|
|
44
|
+
base.text = String(input)
|
|
45
|
+
return base
|
|
46
|
+
}
|
|
47
|
+
if (typeof input !== 'object') throw new Error(`表格单元格格式错误(第 ${r + 1} 行第 ${c + 1} 列)`)
|
|
48
|
+
base.text = String(input.text != null ? input.text : '')
|
|
49
|
+
if (Number(input.colspan) > 1) base.colspan = Math.floor(Number(input.colspan))
|
|
50
|
+
if (Number(input.rowspan) > 1) base.rowspan = Math.floor(Number(input.rowspan))
|
|
51
|
+
const style = {}
|
|
52
|
+
for (const k of CELL_STYLE_KEYS) {
|
|
53
|
+
if (input[k] != null) style[k] = input[k]
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(style).length) base.style = style
|
|
56
|
+
return base
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 构建表格 data。
|
|
61
|
+
* @param {Array<Array>} rows 二维数组(必填)
|
|
62
|
+
* @param {object} opts { header?: boolean 首行表头, themeColor?: string }
|
|
63
|
+
*/
|
|
64
|
+
export function buildTableData(rows, opts = {}) {
|
|
65
|
+
if (!Array.isArray(rows) || rows.length === 0) throw new Error('表格数据 rows 不能为空')
|
|
66
|
+
if (!Array.isArray(rows[0]) || rows[0].length === 0) throw new Error('表格 rows 必须是二维数组')
|
|
67
|
+
|
|
68
|
+
const width = rows[0].length
|
|
69
|
+
const themeColor = opts.themeColor ? String(opts.themeColor) : DEFAULT_TABLE_COLOR
|
|
70
|
+
const header = Boolean(opts.header)
|
|
71
|
+
|
|
72
|
+
return rows.map((row, r) => {
|
|
73
|
+
if (!Array.isArray(row) || row.length !== width) {
|
|
74
|
+
throw new Error(`表格第 ${r + 1} 行列数(${Array.isArray(row) ? row.length : 0})与首行(${width})不一致`)
|
|
75
|
+
}
|
|
76
|
+
return row.map((input, c) => {
|
|
77
|
+
const cell = normalizeCell(input, r, c)
|
|
78
|
+
if (opts.header && r === 0) {
|
|
79
|
+
cell.style = {
|
|
80
|
+
bold: true,
|
|
81
|
+
backcolor: tint(themeColor, 0.88),
|
|
82
|
+
...(cell.style && typeof cell.style === 'object' ? cell.style : {}),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return cell
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 归一化列宽:比例(和≈1)直通;像素值(和>1.5)自动归一为比例。
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeColWidths(colWidths, cols) {
|
|
94
|
+
if (!Array.isArray(colWidths) || colWidths.length === 0) {
|
|
95
|
+
return Array.from({ length: cols }, () => 1 / cols)
|
|
96
|
+
}
|
|
97
|
+
if (colWidths.length !== cols) {
|
|
98
|
+
throw new Error(`colWidths 长度(${colWidths.length})与列数(${cols})不一致`)
|
|
99
|
+
}
|
|
100
|
+
const nums = colWidths.map(Number)
|
|
101
|
+
if (nums.some((n) => !isFinite(n) || n <= 0)) throw new Error('colWidths 必须是正数')
|
|
102
|
+
const sum = nums.reduce((a, b) => a + b, 0)
|
|
103
|
+
if (sum <= 1.5) {
|
|
104
|
+
// 已是比例:按总和归一兜底
|
|
105
|
+
return nums.map((n) => n / sum)
|
|
106
|
+
}
|
|
107
|
+
return nums.map((n) => n / sum)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 构建表格元素。
|
|
112
|
+
* @param {object} e { rows, header?, themeColor?, left/top/width/height?, colWidths?, cellMinHeight?, outline? }
|
|
113
|
+
*/
|
|
114
|
+
export function makeTableElement(e) {
|
|
115
|
+
if (!e || typeof e !== 'object') throw new Error('表格元素必须是对象')
|
|
116
|
+
const rows = e.rows
|
|
117
|
+
const header = Boolean(e.header)
|
|
118
|
+
const data = buildTableData(rows, { header, themeColor: e.themeColor })
|
|
119
|
+
|
|
120
|
+
const cols = data[0].length
|
|
121
|
+
const nRows = data.length
|
|
122
|
+
const themeColor = e.themeColor ? String(e.themeColor) : DEFAULT_TABLE_COLOR
|
|
123
|
+
const cellMinHeight = Number(e.cellMinHeight != null ? e.cellMinHeight : 36)
|
|
124
|
+
const width = Number(e.width != null ? e.width : 640)
|
|
125
|
+
const height = Number(e.height != null ? e.height : nRows * cellMinHeight)
|
|
126
|
+
|
|
127
|
+
const el = {
|
|
128
|
+
type: 'table',
|
|
129
|
+
id: e.id || `table_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
|
|
130
|
+
left: Number(e.left != null ? e.left : Math.round((1000 - width) / 2)),
|
|
131
|
+
top: Number(e.top != null ? e.top : 110),
|
|
132
|
+
width,
|
|
133
|
+
height,
|
|
134
|
+
rotate: Number(e.rotate || 0),
|
|
135
|
+
outline: e.outline && typeof e.outline === 'object'
|
|
136
|
+
? {
|
|
137
|
+
style: String(e.outline.style || 'solid'),
|
|
138
|
+
width: Number(e.outline.width != null ? e.outline.width : 1),
|
|
139
|
+
color: String(e.outline.color || '#d9d9d9'),
|
|
140
|
+
}
|
|
141
|
+
: { style: 'solid', width: 1, color: '#d9d9d9' },
|
|
142
|
+
theme: {
|
|
143
|
+
color: themeColor,
|
|
144
|
+
rowHeader: header,
|
|
145
|
+
rowFooter: false,
|
|
146
|
+
colHeader: Boolean(e.colHeader),
|
|
147
|
+
colFooter: false,
|
|
148
|
+
},
|
|
149
|
+
colWidths: normalizeColWidths(e.colWidths, cols),
|
|
150
|
+
cellMinHeight,
|
|
151
|
+
data,
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(e.rowHeights) && e.rowHeights.length === nRows) {
|
|
154
|
+
el.rowHeights = e.rowHeights.map(Number)
|
|
155
|
+
}
|
|
156
|
+
return el
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 归一化单元格补丁(set-table-cells 用):
|
|
161
|
+
* [{row, col, text?, bold?, color?, backcolor?, align?, fontsize?}]
|
|
162
|
+
*/
|
|
163
|
+
export function normalizeCellPatches(cells) {
|
|
164
|
+
if (!Array.isArray(cells) || cells.length === 0) throw new Error('cells 不能为空')
|
|
165
|
+
return cells.map((c, i) => {
|
|
166
|
+
if (!c || typeof c !== 'object' || !isNum(Number(c.row)) || !isNum(Number(c.col))) {
|
|
167
|
+
throw new Error(`cells[${i}] 需要 row / col`)
|
|
168
|
+
}
|
|
169
|
+
const patch = { row: Math.floor(Number(c.row)), col: Math.floor(Number(c.col)) }
|
|
170
|
+
if (c.text != null) patch.text = String(c.text)
|
|
171
|
+
const style = {}
|
|
172
|
+
for (const k of CELL_STYLE_KEYS) {
|
|
173
|
+
if (c[k] != null) style[k] = c[k]
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(style).length) patch.style = style
|
|
176
|
+
return patch
|
|
177
|
+
})
|
|
178
|
+
}
|
package/core/text.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PPT Bridge 中间件 —— 纯文本/样式工具(DOM-free,可在 node:test 直接测)。
|
|
3
|
+
* 2026-10-22 结构批:从 keystudio.works 抽取,行为保持不变。
|
|
4
|
+
* 2026-10-30 解耦批:颜色解析/对比色收敛到 core/color.js。
|
|
5
|
+
*/
|
|
6
|
+
import { parseColor, contrastColor } from './color.js'
|
|
7
|
+
import { makeChartElement } from './chart.js'
|
|
8
|
+
import { makeTableElement } from './table.js'
|
|
9
|
+
import { makeFormulaElement } from './formula.js'
|
|
10
|
+
|
|
11
|
+
export { parseColor } from './color.js'
|
|
12
|
+
|
|
13
|
+
export const escapeHtml = (s) => String(s == null ? '' : s)
|
|
14
|
+
.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
15
|
+
|
|
16
|
+
export const fontSizeOf = (html) => {
|
|
17
|
+
const m = /font-size:\s*(\d+(?:\.\d+)?)px/.exec(String(html || ''))
|
|
18
|
+
return m ? Number(m[1]) : 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 文本载体:text 元素本身,或带 text.content 的 shape(卡片标题)。 */
|
|
22
|
+
export function textHolder(el) {
|
|
23
|
+
if (!el) return null
|
|
24
|
+
if (el.type === 'text') return el
|
|
25
|
+
if (el.type === 'shape' && el.text && typeof el.text.content === 'string') return el.text
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const htmlOf = (el) => {
|
|
30
|
+
const h = textHolder(el)
|
|
31
|
+
return h ? String(h.content || '') : ''
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 保留完整标签树(含 span 的 font-size/颜色/背景),只替换文本节点。
|
|
36
|
+
* 原字号住在 <span style="font-size:54px"> 里;DOM 版依赖 DOMParser,
|
|
37
|
+
* 这里用正则遍历文本节点实现同样语义,便于单测。
|
|
38
|
+
*/
|
|
39
|
+
export function setPlainText(current, text) {
|
|
40
|
+
const src = String(current || '')
|
|
41
|
+
const body = escapeHtml(text == null ? '' : text)
|
|
42
|
+
let first = true
|
|
43
|
+
const out = src.replace(/>([^<>]*)</g, (m, txt) => {
|
|
44
|
+
if (!String(txt).trim()) return m
|
|
45
|
+
if (first) { first = false; return '>' + body + '<' }
|
|
46
|
+
return '><'
|
|
47
|
+
})
|
|
48
|
+
if (first) return '<p>' + body + '</p>'
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 文本样式改写:缺省保留原文(去标签取纯文本),样式从 props 合并。 */
|
|
53
|
+
export function textHtml(current, props, st) {
|
|
54
|
+
let inner = props.html
|
|
55
|
+
if (inner == null) {
|
|
56
|
+
if (props.text != null) inner = escapeHtml(String(props.text))
|
|
57
|
+
else inner = String(current || '').replace(/<p[^>]*>/i, '').replace(/<\/p>\s*$/i, '')
|
|
58
|
+
}
|
|
59
|
+
let body = inner
|
|
60
|
+
if (st && st.bold) body = '<strong>' + body + '</strong>'
|
|
61
|
+
const size = st && st.fontSize != null ? Number(st.fontSize) : 28
|
|
62
|
+
let style = 'font-size: ' + size + 'px;'
|
|
63
|
+
if (st && st.color) style += ' color: ' + String(st.color) + ';'
|
|
64
|
+
if (st && st.align) style += ' text-align: ' + String(st.align) + ';'
|
|
65
|
+
return '<p style="' + style + '">' + body + '</p>'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 元素属性补丁:几何 / 填充 / 文本样式(content 重写保留正文)。 */
|
|
69
|
+
export function patchElement(el, props) {
|
|
70
|
+
;['left', 'top', 'width', 'height', 'rotate', 'opacity'].forEach(k => {
|
|
71
|
+
if (props[k] != null && isFinite(Number(props[k]))) el[k] = Number(props[k])
|
|
72
|
+
})
|
|
73
|
+
if (props.fill != null) el.fill = String(props.fill)
|
|
74
|
+
if (el.type === 'image') {
|
|
75
|
+
if (props.src != null) el.src = String(props.src)
|
|
76
|
+
if (props.clip != null) el.clip = props.clip
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
/* 公式元素:path/latex/viewBox 由渲染器产出,样式走 stroke。 */
|
|
80
|
+
if (el.type === 'latex') {
|
|
81
|
+
if (props.path != null) el.path = String(props.path)
|
|
82
|
+
if (props.latex != null) el.latex = String(props.latex)
|
|
83
|
+
if (Array.isArray(props.viewBox) && props.viewBox.length === 2 &&
|
|
84
|
+
props.viewBox.every(n => isFinite(Number(n)) && Number(n) > 0)) {
|
|
85
|
+
el.viewBox = [Number(props.viewBox[0]), Number(props.viewBox[1])]
|
|
86
|
+
}
|
|
87
|
+
if (props.color != null) el.color = String(props.color)
|
|
88
|
+
if (props.strokeWidth != null && isFinite(Number(props.strokeWidth))) {
|
|
89
|
+
el.strokeWidth = Number(props.strokeWidth)
|
|
90
|
+
}
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
const holder = textHolder(el)
|
|
94
|
+
if (!holder) return
|
|
95
|
+
/* 只改颜色:直接替换 inline color,保留字号/加粗(否则 textHtml 会回落到 28px) */
|
|
96
|
+
if (props.color != null && props.text == null && props.html == null &&
|
|
97
|
+
props.fontSize == null && props.bold == null && props.align == null) {
|
|
98
|
+
holder.content = String(holder.content || '').replace(/color:\s*[^;"']+/g, 'color: ' + String(props.color))
|
|
99
|
+
if (el.type === 'text') el.defaultColor = String(props.color)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (props.defaultColor != null && el.type === 'text') el.defaultColor = String(props.defaultColor)
|
|
103
|
+
const hasText = props.text != null || props.html != null
|
|
104
|
+
const hasStyle = props.fontSize != null || props.color != null ||
|
|
105
|
+
props.bold != null || props.align != null
|
|
106
|
+
if (hasText && !hasStyle) {
|
|
107
|
+
/* 槽位填充:只换文字,保留原字号/颜色/加粗(版式不破) */
|
|
108
|
+
holder.content = setPlainText(holder.content, props.text)
|
|
109
|
+
}
|
|
110
|
+
else if (hasText || hasStyle) {
|
|
111
|
+
holder.content = textHtml(holder.content, props, hasStyle ? props : null)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function makeElement(e) {
|
|
116
|
+
const type = String(e.type || 'text')
|
|
117
|
+
if (type !== 'text' && type !== 'shape' && type !== 'image' &&
|
|
118
|
+
type !== 'chart' && type !== 'table' && type !== 'line' && type !== 'latex') {
|
|
119
|
+
throw new Error('不支持的 element.type: ' + type +
|
|
120
|
+
'(只支持 text / shape / image / chart / table / line / latex)')
|
|
121
|
+
}
|
|
122
|
+
/* 线条:start/end 用画布绝对坐标表达,落库转为元素相对坐标;
|
|
123
|
+
PPTist line 元素不存包围盒宽高——width 字段是线宽。 */
|
|
124
|
+
if (type === 'line') {
|
|
125
|
+
const sx = e.start ? Number(e.start[0]) : Number(e.x1 != null ? e.x1 : 0)
|
|
126
|
+
const sy = e.start ? Number(e.start[1]) : Number(e.y1 != null ? e.y1 : 0)
|
|
127
|
+
const ex = e.end ? Number(e.end[0]) : Number(e.x2 != null ? e.x2 : 200)
|
|
128
|
+
const ey = e.end ? Number(e.end[1]) : Number(e.y2 != null ? e.y2 : 0)
|
|
129
|
+
if (![sx, sy, ex, ey].every(isFinite)) throw new Error('line 需要 start/end(画布坐标)或 x1/y1/x2/y2')
|
|
130
|
+
const left = Math.min(sx, ex)
|
|
131
|
+
const top = Math.min(sy, ey)
|
|
132
|
+
const pointOf = (v) => (v && v !== 'none' ? String(v) : '')
|
|
133
|
+
const el = {
|
|
134
|
+
id: e.id || ('line_' + Date.now() + '_' + Math.floor(Math.random() * 1000)),
|
|
135
|
+
type,
|
|
136
|
+
left,
|
|
137
|
+
top,
|
|
138
|
+
start: [sx - left, sy - top],
|
|
139
|
+
end: [ex - left, ey - top],
|
|
140
|
+
points: Array.isArray(e.points)
|
|
141
|
+
? e.points.map(String)
|
|
142
|
+
: [pointOf(e.start_point), pointOf(e.end_point)],
|
|
143
|
+
color: e.color || '#5b9bd5',
|
|
144
|
+
style: String(e.style || 'solid'),
|
|
145
|
+
width: Number(e.width || e.lineWidth || 2),
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(e.broken) && e.broken.length === 2) {
|
|
148
|
+
el.broken = [Number(e.broken[0]) - left, Number(e.broken[1]) - top]
|
|
149
|
+
}
|
|
150
|
+
return el
|
|
151
|
+
}
|
|
152
|
+
/* 图表 / 表格 / 公式:委托给各自的构建器(数据归一化 + 校验)。 */
|
|
153
|
+
if (type === 'chart') return makeChartElement(e)
|
|
154
|
+
if (type === 'table') return makeTableElement(e)
|
|
155
|
+
if (type === 'latex') return makeFormulaElement(e)
|
|
156
|
+
const id = e.id || ('el_' + Date.now() + '_' + Math.floor(Math.random() * 1000))
|
|
157
|
+
const base = {
|
|
158
|
+
id, type,
|
|
159
|
+
left: Number(e.left != null ? e.left : 60), top: Number(e.top != null ? e.top : 120),
|
|
160
|
+
width: Number(e.width != null ? e.width : 400), height: Number(e.height != null ? e.height : 60),
|
|
161
|
+
rotate: Number(e.rotate || 0), fixedRatio: false,
|
|
162
|
+
}
|
|
163
|
+
if (type === 'shape') {
|
|
164
|
+
return Object.assign(base, {
|
|
165
|
+
path: e.path || 'M 0 0 L 200 0 L 200 200 L 0 200 Z',
|
|
166
|
+
viewBox: e.viewBox || [200, 200],
|
|
167
|
+
fill: e.fill || 'rgb(209, 68, 36)',
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
if (type === 'image') {
|
|
171
|
+
const src = String(e.src || '')
|
|
172
|
+
if (!src) throw new Error('image 元素需要 src(请用 ppt_image,支持 attachment_id)')
|
|
173
|
+
if (/^[a-zA-Z]:[\\/]/.test(src) || src.startsWith('file:')) {
|
|
174
|
+
throw new Error('本机路径不能直接插入图片;请用 ppt_image(会自动转 data URL)')
|
|
175
|
+
}
|
|
176
|
+
return Object.assign(base, {
|
|
177
|
+
src,
|
|
178
|
+
clip: e.clip || null,
|
|
179
|
+
...(e.opacity != null ? { opacity: Number(e.opacity) } : {}),
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
return Object.assign(base, {
|
|
183
|
+
content: e.content || textHtml('', { text: e.text || '' },
|
|
184
|
+
{ fontSize: e.fontSize || 28, color: e.color || '#333333' }),
|
|
185
|
+
defaultColor: e.color || '#333333', defaultFontName: '',
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 背景明暗 → 文本自动对比度:浅底深字 / 深底白字。 */
|
|
190
|
+
export function contrast(slide, bg) {
|
|
191
|
+
if (!parseColor(bg)) return
|
|
192
|
+
const fg = contrastColor(bg)
|
|
193
|
+
;(slide.elements || []).forEach(el => {
|
|
194
|
+
if (el.type !== 'text') return
|
|
195
|
+
el.defaultColor = fg
|
|
196
|
+
if (typeof el.content === 'string') {
|
|
197
|
+
el.content = el.content.replace(/color:\s*[^;"']+/g, 'color: ' + fg)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
}
|