@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器专用 view op:把上传的 PPTX 交给编辑器内置的 PPTist 导入器。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不在服务端解析:PPTist 的 importPPTXFile 依赖 DOM / FileReader /
|
|
5
|
+
* 字体与图片上下文,直接复用才能在“打开已有 pptx”时拿到和 UI 导入一致的
|
|
6
|
+
* 还原度。服务端只负责把文件传到 relay,由编辑器页面执行本 op。
|
|
7
|
+
*
|
|
8
|
+
* 协议:
|
|
9
|
+
* { op: 'import-pptx', req, id, url, name, resultUrl? }
|
|
10
|
+
* - url:relay 上的待导入文件地址(同源相对路径即可)
|
|
11
|
+
* - resultUrl:导入完成后回传结果的地址
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const importOps = {
|
|
15
|
+
'import-pptx': {
|
|
16
|
+
mutates: false,
|
|
17
|
+
view: true,
|
|
18
|
+
description: '导入 PPTX(仅编辑器页面执行)',
|
|
19
|
+
async run(P, c, ctx = {}) {
|
|
20
|
+
if (!P || typeof P.importFile !== 'function') {
|
|
21
|
+
throw new Error('当前编辑器不支持 PPTX 导入:PptApp.importFile 不可用')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const req = String(c.req || '')
|
|
25
|
+
const apiBase = String(ctx.apiBase || '').replace(/\/+$/, '')
|
|
26
|
+
const resultUrl = String(c.resultUrl || (apiBase ? `${apiBase}/import-result` : '/api/desktop/works/ppt-bridge/import-result'))
|
|
27
|
+
const doFetch = ctx.fetch || (typeof fetch === 'function' ? fetch : null)
|
|
28
|
+
|
|
29
|
+
const notify = async (body) => {
|
|
30
|
+
if (!doFetch) return
|
|
31
|
+
try {
|
|
32
|
+
await doFetch(resultUrl, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'content-type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ req, id: String(c.id || ''), ...body }),
|
|
36
|
+
})
|
|
37
|
+
} catch { /* 结果回传失败由 MCP 侧超时兜底 */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
if (!doFetch) throw new Error('当前环境不支持 fetch,无法读取待导入文件')
|
|
42
|
+
const base = ctx.baseUrl || (typeof location !== 'undefined' ? location.href : 'http://127.0.0.1/')
|
|
43
|
+
const sourceUrl = new URL(String(c.url || ''), base).href
|
|
44
|
+
const response = await doFetch(sourceUrl)
|
|
45
|
+
if (!response || !response.ok) {
|
|
46
|
+
throw new Error(`读取待导入文件失败:HTTP ${response ? response.status : 'no response'}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const blob = await response.blob()
|
|
50
|
+
const FileCtor = ctx.File || (typeof File !== 'undefined' ? File : null)
|
|
51
|
+
if (!FileCtor) throw new Error('当前环境不支持 File 构造器,无法导入 PPTX')
|
|
52
|
+
|
|
53
|
+
const name = String(c.name || 'import.pptx')
|
|
54
|
+
const file = new FileCtor([blob], name, {
|
|
55
|
+
type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
/* PPTist 的 importPPTXFile 默认是“追加/空 deck 才替换”。工具语义是
|
|
59
|
+
* “打开 pptx”,所以 replace 模式先记住旧页,导入后把它们删掉;
|
|
60
|
+
* append 模式保留旧页(多文件拼接场景)。 */
|
|
61
|
+
const mode = c.mode === 'append' ? 'append' : 'replace'
|
|
62
|
+
const beforeSlides = typeof P.getSlides === 'function' ? (P.getSlides() || []) : []
|
|
63
|
+
const previousIds = mode === 'replace'
|
|
64
|
+
? beforeSlides.map((slide) => slide.id).filter(Boolean)
|
|
65
|
+
: []
|
|
66
|
+
|
|
67
|
+
const imported = await P.importFile(file)
|
|
68
|
+
for (const id of previousIds) {
|
|
69
|
+
try { P.removeSlide(id) } catch { /* 单页 deck 不允许删空;忽略 */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const finalCount = typeof P.getSlides === 'function' ? (P.getSlides() || []).length : imported?.slideCount
|
|
73
|
+
const rawCount = Number(imported?.slideCount || 0)
|
|
74
|
+
const contributed = mode === 'append'
|
|
75
|
+
? Math.max(0, Number(finalCount || 0) - beforeSlides.length)
|
|
76
|
+
: Number(finalCount || rawCount || 0)
|
|
77
|
+
const summary = {
|
|
78
|
+
...(imported && typeof imported === 'object' ? imported : {}),
|
|
79
|
+
slideCount: Number(finalCount || rawCount || 0),
|
|
80
|
+
import: {
|
|
81
|
+
mode,
|
|
82
|
+
before: beforeSlides.length,
|
|
83
|
+
imported: contributed,
|
|
84
|
+
final: Number(finalCount || 0),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
await notify({ ok: true, summary })
|
|
88
|
+
return summary
|
|
89
|
+
} catch (error) {
|
|
90
|
+
await notify({ ok: false, error: String((error && error.message) || error) })
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** op 组装:按域合并为一张表,交给通用 op registry 分发。 */
|
|
2
|
+
import { createOpDispatcher, createOpRegistry } from '@capyclaw/document-workbench/ops'
|
|
3
|
+
import { deckOps } from './deck.js'
|
|
4
|
+
import { textOps } from './text.js'
|
|
5
|
+
import { elementOps } from './element.js'
|
|
6
|
+
import { slideOps } from './slide.js'
|
|
7
|
+
import { recolorOps } from './recolor.js'
|
|
8
|
+
import { viewOps } from './view.js'
|
|
9
|
+
import { historyOps } from './history.js'
|
|
10
|
+
import { playbackOps } from './playback.js'
|
|
11
|
+
import { chartOps } from './chart.js'
|
|
12
|
+
import { tableOps } from './table.js'
|
|
13
|
+
import { importOps } from './import.js'
|
|
14
|
+
|
|
15
|
+
export const OP_MODULES = {
|
|
16
|
+
deck: deckOps,
|
|
17
|
+
text: textOps,
|
|
18
|
+
element: elementOps,
|
|
19
|
+
slide: slideOps,
|
|
20
|
+
recolor: recolorOps,
|
|
21
|
+
view: viewOps,
|
|
22
|
+
history: historyOps,
|
|
23
|
+
playback: playbackOps,
|
|
24
|
+
chart: chartOps,
|
|
25
|
+
table: tableOps,
|
|
26
|
+
importer: importOps,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const registry = createOpRegistry({
|
|
30
|
+
modules: OP_MODULES,
|
|
31
|
+
snapshot: (P) => P.snapshot?.(),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const OP_DEFINITIONS = registry.definitions
|
|
35
|
+
export const OP_NAMES = registry.names
|
|
36
|
+
export const VIEW_OP_NAMES = registry.viewNames
|
|
37
|
+
export const applyOp = registry.apply
|
|
38
|
+
|
|
39
|
+
export { createOpDispatcher }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 整体换色 op:背景 + 全部文字对比色 + 装饰形状,一次完成。 */
|
|
2
|
+
import { textHolder } from '../text.js'
|
|
3
|
+
import { contrastColor } from '../color.js'
|
|
4
|
+
|
|
5
|
+
const clone = (P, i) => JSON.parse(JSON.stringify(P.getSlides()[i]))
|
|
6
|
+
|
|
7
|
+
export const recolorOps = {
|
|
8
|
+
recolor: {
|
|
9
|
+
run(P, c) {
|
|
10
|
+
const bg = String(c.background || '#ffffff')
|
|
11
|
+
const fg = c.text ? String(c.text) : contrastColor(bg)
|
|
12
|
+
const shapeFill = c.shapes === null ? null
|
|
13
|
+
: String(c.shapes != null && c.shapes !== true ? c.shapes : fg)
|
|
14
|
+
const n = P.getSlides().length
|
|
15
|
+
for (let i = 0; i < n; i++) {
|
|
16
|
+
if (Array.isArray(c.indices) && c.indices.indexOf(i) < 0) continue
|
|
17
|
+
const s = clone(P, i)
|
|
18
|
+
s.background = { type: 'solid', color: bg }
|
|
19
|
+
;(s.elements || []).forEach(el => {
|
|
20
|
+
const holder = textHolder(el)
|
|
21
|
+
if (holder) {
|
|
22
|
+
el.defaultColor = fg
|
|
23
|
+
if (typeof holder.content === 'string') {
|
|
24
|
+
holder.content = holder.content.replace(/color:\s*[^;"']+/g, 'color: ' + fg)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (el.type !== 'text' && shapeFill && el.fill != null) el.fill = shapeFill
|
|
28
|
+
})
|
|
29
|
+
P.updateSlide(i, s)
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** 页面 op:背景、增删改移复制、选中。 */
|
|
2
|
+
import { contrast } from '../text.js'
|
|
3
|
+
|
|
4
|
+
const idx = (c) => (c.id != null ? c.id : c.index)
|
|
5
|
+
const clone = (P, i) => JSON.parse(JSON.stringify(P.getSlides()[i]))
|
|
6
|
+
|
|
7
|
+
export const slideOps = {
|
|
8
|
+
'set-background': {
|
|
9
|
+
run(P, c) {
|
|
10
|
+
const idxs = Array.isArray(c.indices) ? c.indices : null
|
|
11
|
+
const n = P.getSlides().length
|
|
12
|
+
for (let i = 0; i < n; i++) {
|
|
13
|
+
if (idxs && idxs.indexOf(i) < 0) continue
|
|
14
|
+
const s = clone(P, i)
|
|
15
|
+
s.background = { type: 'solid', color: String(c.color || '#ffffff') }
|
|
16
|
+
if (c.contrast) contrast(s, String(c.color || '#ffffff'))
|
|
17
|
+
P.updateSlide(i, s)
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
'add-slide': {
|
|
23
|
+
run(P, c) { P.addSlide(c.slide || {}, c.index) },
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
'update-slide': {
|
|
27
|
+
run(P, c) {
|
|
28
|
+
if (!P.updateSlide(idx(c), c.slide)) throw new Error('update-slide 未命中')
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
'remove-slide': {
|
|
33
|
+
run(P, c) {
|
|
34
|
+
if (!P.removeSlide(idx(c))) throw new Error('remove-slide 未命中')
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
'move-slide': {
|
|
39
|
+
run(P, c) {
|
|
40
|
+
if (!P.moveSlide(c.from, c.to)) throw new Error('move-slide 越界')
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
'duplicate-slide': {
|
|
45
|
+
run(P, c) {
|
|
46
|
+
if (!P.duplicateSlide(idx(c))) throw new Error('duplicate-slide 未命中')
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
select: {
|
|
51
|
+
mutates: false,
|
|
52
|
+
view: true,
|
|
53
|
+
run(P, c) {
|
|
54
|
+
if (!P.selectSlide(idx(c))) throw new Error('select 未命中')
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
'set-notes': {
|
|
59
|
+
run(P, c) {
|
|
60
|
+
const i = Number(c.slide != null ? c.slide : idx(c))
|
|
61
|
+
const s = clone(P, i)
|
|
62
|
+
s.notes = String(c.notes == null ? '' : c.notes)
|
|
63
|
+
P.updateSlide(i, s)
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** 表格 op:加表格 / 更新表格样式 / 精修单元格。数据归一化在 core/table.js。 */
|
|
2
|
+
import { makeTableElement, normalizeCellPatches, DEFAULT_TABLE_COLOR } from '../table.js'
|
|
3
|
+
|
|
4
|
+
function clone(P, i) {
|
|
5
|
+
const slide = P.getSlides()[i]
|
|
6
|
+
if (!slide) {
|
|
7
|
+
const total = P.getSlides().length
|
|
8
|
+
throw new Error(`页号越界:第 ${i} 页不存在(共 ${total} 页)`)
|
|
9
|
+
}
|
|
10
|
+
return JSON.parse(JSON.stringify(slide))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const tableOps = {
|
|
14
|
+
'add-table': {
|
|
15
|
+
run(P, c) {
|
|
16
|
+
const i = Number(c.slide != null ? c.slide : 0)
|
|
17
|
+
const s = clone(P, i)
|
|
18
|
+
const el = makeTableElement(c.element || {})
|
|
19
|
+
s.elements.push(el)
|
|
20
|
+
P.updateSlide(i, s)
|
|
21
|
+
return el.id
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
'update-table': {
|
|
26
|
+
run(P, c) {
|
|
27
|
+
const i = Number(c.slide != null ? c.slide : 0)
|
|
28
|
+
const s = clone(P, i)
|
|
29
|
+
const el = s.elements.find(e => e.id === c.elementId)
|
|
30
|
+
if (!el) throw new Error('update-table 未命中: ' + c.elementId)
|
|
31
|
+
if (el.type !== 'table') throw new Error(`元素 ${c.elementId} 不是表格(type=${el.type})`)
|
|
32
|
+
const p = c.props || {}
|
|
33
|
+
if (p.themeColor != null) el.theme = { ...el.theme, color: String(p.themeColor) }
|
|
34
|
+
if (p.rowHeader != null) el.theme.rowHeader = Boolean(p.rowHeader)
|
|
35
|
+
if (p.colHeader != null) el.theme.colHeader = Boolean(p.colHeader)
|
|
36
|
+
if (p.outline && typeof p.outline === 'object') {
|
|
37
|
+
el.outline = {
|
|
38
|
+
style: String(p.outline.style || el.outline.style || 'solid'),
|
|
39
|
+
width: Number(p.outline.width != null ? p.outline.width : el.outline.width),
|
|
40
|
+
color: String(p.outline.color || el.outline.color || '#d9d9d9'),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(p.colWidths)) {
|
|
44
|
+
const sum = p.colWidths.reduce((a, b) => a + Number(b || 0), 0)
|
|
45
|
+
if (!isFinite(sum) || sum <= 0) throw new Error('colWidths 无效')
|
|
46
|
+
el.colWidths = p.colWidths.map((n) => Number(n) / sum)
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(p.rowHeights)) {
|
|
49
|
+
if (p.rowHeights.length !== el.data.length) throw new Error('rowHeights 长度与行数不一致')
|
|
50
|
+
el.rowHeights = p.rowHeights.map(Number)
|
|
51
|
+
}
|
|
52
|
+
if (p.cellMinHeight != null) el.cellMinHeight = Number(p.cellMinHeight)
|
|
53
|
+
for (const k of ['left', 'top', 'width', 'height', 'rotate']) {
|
|
54
|
+
if (p[k] != null && isFinite(Number(p[k]))) el[k] = Number(p[k])
|
|
55
|
+
}
|
|
56
|
+
P.updateSlide(i, s)
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
'set-table-cells': {
|
|
61
|
+
run(P, c) {
|
|
62
|
+
const i = Number(c.slide != null ? c.slide : 0)
|
|
63
|
+
const s = clone(P, i)
|
|
64
|
+
const el = s.elements.find(e => e.id === c.elementId)
|
|
65
|
+
if (!el) throw new Error('set-table-cells 未命中: ' + c.elementId)
|
|
66
|
+
if (el.type !== 'table') throw new Error(`元素 ${c.elementId} 不是表格(type=${el.type})`)
|
|
67
|
+
const patches = normalizeCellPatches(c.cells || [])
|
|
68
|
+
for (const p of patches) {
|
|
69
|
+
const row = el.data[p.row]
|
|
70
|
+
if (!row) throw new Error(`行越界:第 ${p.row} 行不存在(共 ${el.data.length} 行)`)
|
|
71
|
+
const cell = row[p.col]
|
|
72
|
+
if (!cell) throw new Error(`列越界:第 ${p.col} 列不存在(共 ${row.length} 列)`)
|
|
73
|
+
if (p.text != null) cell.text = p.text
|
|
74
|
+
if (p.style) {
|
|
75
|
+
cell.style = { ...(cell.style || {}), ...p.style }
|
|
76
|
+
if (Object.keys(cell.style).length === 0) delete cell.style
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (c.themeColor != null && c.themeColor !== '') {
|
|
80
|
+
el.theme = { ...el.theme, color: String(c.themeColor || DEFAULT_TABLE_COLOR) }
|
|
81
|
+
}
|
|
82
|
+
P.updateSlide(i, s)
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}
|
package/core/ops/text.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** 文本 op:按页/元素改文本,文字槽位填充。 */
|
|
2
|
+
import { patchElement, textHolder } from '../text.js'
|
|
3
|
+
|
|
4
|
+
const idx = (c) => (c.id != null ? c.id : c.index)
|
|
5
|
+
const clone = (P, i) => JSON.parse(JSON.stringify(P.getSlides()[i]))
|
|
6
|
+
|
|
7
|
+
export const textOps = {
|
|
8
|
+
'set-text': {
|
|
9
|
+
run(P, c) {
|
|
10
|
+
const i = idx(c)
|
|
11
|
+
const s = clone(P, i)
|
|
12
|
+
const el = s.elements.find(e => (c.elementId && e.id === c.elementId) ||
|
|
13
|
+
(!c.elementId && e.type === 'text'))
|
|
14
|
+
|| (!c.elementId && s.elements.find(e => textHolder(e)))
|
|
15
|
+
if (!el) throw new Error('set-text 未命中: ' + (c.elementId || '首个文本'))
|
|
16
|
+
patchElement(el, c)
|
|
17
|
+
P.updateSlide(i, s)
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
'fill-slot': {
|
|
22
|
+
run(P, c) {
|
|
23
|
+
const i = Number(c.slide) | 0
|
|
24
|
+
const s = clone(P, i)
|
|
25
|
+
const texts = (s.elements || []).filter(e => textHolder(e))
|
|
26
|
+
const el = texts[Number(c.slot) | 0]
|
|
27
|
+
if (!el) throw new Error('文本槽位不存在: ' + c.slot)
|
|
28
|
+
patchElement(el, {
|
|
29
|
+
text: c.text, html: c.html,
|
|
30
|
+
fontSize: c.fontSize, color: c.color, bold: c.bold, align: c.align,
|
|
31
|
+
})
|
|
32
|
+
P.updateSlide(i, s)
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
package/core/ops/view.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** 视图 op:放映控制(不改 deck,不产生 undo 快照)。 */
|
|
2
|
+
export const viewOps = {
|
|
3
|
+
present: {
|
|
4
|
+
mutates: false,
|
|
5
|
+
view: true,
|
|
6
|
+
run(P, c) { return P.present(c.index != null ? c.index : undefined) },
|
|
7
|
+
},
|
|
8
|
+
'exit-present': {
|
|
9
|
+
mutates: false,
|
|
10
|
+
view: true,
|
|
11
|
+
run(P) { return P.exitPresent() },
|
|
12
|
+
},
|
|
13
|
+
}
|
package/core/ops.js
ADDED
package/core/quick.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地快捷指令(不经模型,<100ms):中文颜色名 / 第N页改字 / 加一页。
|
|
3
|
+
* 纯函数,返回底层 op 数组;解析失败返回 null。
|
|
4
|
+
*/
|
|
5
|
+
import { escapeHtml } from './text.js'
|
|
6
|
+
import { COLORS } from './color.js'
|
|
7
|
+
|
|
8
|
+
export { COLORS, resolveColor } from './color.js'
|
|
9
|
+
|
|
10
|
+
export function simpleSlide(title) {
|
|
11
|
+
const ts = Date.now()
|
|
12
|
+
return {
|
|
13
|
+
id: 'quick_' + ts,
|
|
14
|
+
background: { type: 'solid', color: '#ffffff' },
|
|
15
|
+
elements: [{
|
|
16
|
+
type: 'text', id: 'quick_t_' + ts,
|
|
17
|
+
left: 60, top: 50, width: 880, height: 76,
|
|
18
|
+
content: '<p style="font-size: 34px;"><strong>' + escapeHtml(title) + '</strong></p>',
|
|
19
|
+
defaultColor: '#333333', defaultFontName: '', fixedRatio: false, rotate: 0,
|
|
20
|
+
}],
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseQuick(text) {
|
|
25
|
+
const t = String(text || '').trim()
|
|
26
|
+
if (!t) return null
|
|
27
|
+
const cn = { 一: 1, 二: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9, 十: 10 }
|
|
28
|
+
const pageMatch = /第\s*([0-9]+|[一二三四五六七八九十])\s*页/.exec(t)
|
|
29
|
+
const page = pageMatch ? (cn[pageMatch[1]] || parseInt(pageMatch[1], 10)) - 1 : null
|
|
30
|
+
const setText = /第\s*(?:[0-9]+|[一二三四五六七八九十])\s*页.*?(?:标题|文字|文本|内容).*?(?:改成|换成|改为|设为)\s*(.+)$/.exec(t)
|
|
31
|
+
if (setText) return [{ op: 'set-text', id: page, text: setText[1].trim() }]
|
|
32
|
+
const add = /(?:加|新增|添加)一页\s*(.*)$/.exec(t)
|
|
33
|
+
if (add) return [{ op: 'add-slide', slide: simpleSlide(add[1].trim() || '新一页') }]
|
|
34
|
+
if (/背景/.test(t)) {
|
|
35
|
+
let hex = null
|
|
36
|
+
let best = null
|
|
37
|
+
for (const k of Object.keys(COLORS)) {
|
|
38
|
+
const idx = t.indexOf(k)
|
|
39
|
+
if (idx < 0) continue
|
|
40
|
+
if (!best || k.length > best.k.length || (k.length === best.k.length && idx < best.idx)) best = { k, idx }
|
|
41
|
+
}
|
|
42
|
+
if (best) hex = COLORS[best.k]
|
|
43
|
+
const hexMatch = /#([0-9a-f]{3}|[0-9a-f]{6})/i.exec(t)
|
|
44
|
+
if (hexMatch) hex = hexMatch[0]
|
|
45
|
+
if (hex) {
|
|
46
|
+
const cmd = { op: 'set-background', color: hex, contrast: true }
|
|
47
|
+
if (page != null) cmd.indices = [page]
|
|
48
|
+
return [cmd]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 形状库:常用 SVG 形状的 path 生成(viewBox 200×200 空间,编辑器按元素宽高缩放)。
|
|
3
|
+
* 零 DOM,node:test 直测;ppt_shape 工具的数据源。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 正 n 边形顶点(外接圆半径 R,中心 100,100,顶点朝上)。 */
|
|
7
|
+
function polygon(n, rot = -90) {
|
|
8
|
+
const pts = []
|
|
9
|
+
for (let i = 0; i < n; i++) {
|
|
10
|
+
const a = ((rot + (360 / n) * i) * Math.PI) / 180
|
|
11
|
+
pts.push([100 + 100 * Math.cos(a), 100 + 100 * Math.sin(a)])
|
|
12
|
+
}
|
|
13
|
+
return pts
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 顶点数组 → path(闭合)。 */
|
|
17
|
+
const toPath = (pts) => 'M ' + pts.map(([x, y]) => `${x.toFixed(1)} ${y.toFixed(1)}`).join(' L ') + ' Z'
|
|
18
|
+
|
|
19
|
+
/** 五角星(外径 100,内径 40)。 */
|
|
20
|
+
function star5() {
|
|
21
|
+
const pts = []
|
|
22
|
+
for (let i = 0; i < 10; i++) {
|
|
23
|
+
const r = i % 2 === 0 ? 100 : 42
|
|
24
|
+
const a = ((-90 + 36 * i) * Math.PI) / 180
|
|
25
|
+
pts.push([100 + r * Math.cos(a), 100 + r * Math.sin(a)])
|
|
26
|
+
}
|
|
27
|
+
return toPath(pts)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 圆角矩形 path(顺时针,四角四段圆弧)。 */
|
|
31
|
+
function roundRect(radius = 32) {
|
|
32
|
+
const r = Math.min(radius, 100)
|
|
33
|
+
return `M 0 ${r} ` +
|
|
34
|
+
`A ${r} ${r} 0 0 1 ${r} 0 L ${200 - r} 0 A ${r} ${r} 0 0 1 200 ${r} ` +
|
|
35
|
+
`L 200 ${200 - r} A ${r} ${r} 0 0 1 ${200 - r} 200 L ${r} 200 ` +
|
|
36
|
+
`A ${r} ${r} 0 0 1 0 ${200 - r} Z`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 水平箭头 path(指向 dx 方向:1 右 / -1 左)。 */
|
|
40
|
+
function arrowH(dx) {
|
|
41
|
+
const pts = dx > 0
|
|
42
|
+
? [[0, 75], [125, 75], [125, 40], [200, 100], [125, 160], [125, 125], [0, 125]]
|
|
43
|
+
: [[200, 75], [75, 75], [75, 40], [0, 100], [75, 160], [75, 125], [200, 125]]
|
|
44
|
+
return toPath(pts)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 垂直箭头 path(指向 dy 方向:1 下 / -1 上)。 */
|
|
48
|
+
function arrowV(dy) {
|
|
49
|
+
const pts = dy > 0
|
|
50
|
+
? [[75, 0], [75, 125], [40, 125], [100, 200], [160, 125], [125, 125], [125, 0]]
|
|
51
|
+
: [[75, 200], [75, 75], [40, 75], [100, 0], [160, 75], [125, 75], [125, 200]]
|
|
52
|
+
return toPath(pts)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 双向水平箭头。 */
|
|
56
|
+
const doubleArrow = toPath([[0, 100], [50, 55], [50, 80], [150, 80], [150, 55], [200, 100], [150, 145], [150, 120], [50, 120], [50, 145]])
|
|
57
|
+
|
|
58
|
+
/** 对话气泡(圆角矩形 + 左下尾巴)。 */
|
|
59
|
+
const bubble = 'M 24 12 L 176 12 A 20 20 0 0 1 196 32 L 196 120 A 20 20 0 0 1 176 140 L 88 140 L 44 184 L 52 140 L 24 140 A 20 20 0 0 1 4 120 L 4 32 A 20 20 0 0 1 24 12 Z'
|
|
60
|
+
|
|
61
|
+
/** 爱心。 */
|
|
62
|
+
const heart = 'M 100 178 C 32 128 8 92 8 60 C 8 30 32 12 56 12 C 76 12 92 24 100 40 C 108 24 124 12 144 12 C 168 12 192 30 192 60 C 192 92 168 128 100 178 Z'
|
|
63
|
+
|
|
64
|
+
/** 云朵。 */
|
|
65
|
+
const cloud = 'M 56 160 A 44 44 0 0 1 56 72 A 52 52 0 0 1 152 60 A 46 46 0 0 1 152 160 Z'
|
|
66
|
+
|
|
67
|
+
/** 闪电。 */
|
|
68
|
+
const bolt = 'M 112 4 L 40 112 L 92 112 L 76 196 L 160 80 L 104 80 Z'
|
|
69
|
+
|
|
70
|
+
/** 十字。 */
|
|
71
|
+
const cross = 'M 70 8 L 130 8 L 130 70 L 192 70 L 192 130 L 130 130 L 130 192 L 70 192 L 70 130 L 8 130 L 8 70 L 70 70 Z'
|
|
72
|
+
|
|
73
|
+
/** 三角函数圆弧(四分之一圆 path)。 */
|
|
74
|
+
const quarter = (cx, cy, sx, sy, ex, ey) => `M ${sx} ${sy} A 100 100 0 0 1 ${ex} ${ey} L ${cx} ${cy} Z`
|
|
75
|
+
|
|
76
|
+
/** 圆柱(数据库/桶形)。 */
|
|
77
|
+
const cylinder = 'M 20 40 A 80 26 0 0 1 180 40 L 180 160 A 80 26 0 0 1 20 160 Z'
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 形状库:key → { name, path }(viewBox 统一 0 0 200 200)。
|
|
81
|
+
*/
|
|
82
|
+
export const SHAPE_LIBRARY = {
|
|
83
|
+
rect: { name: '矩形', path: 'M 0 0 L 200 0 L 200 200 L 0 200 Z' },
|
|
84
|
+
'round-rect': { name: '圆角矩形', path: roundRect(32) },
|
|
85
|
+
circle: { name: '圆形', path: 'M 0 100 A 100 100 0 1 1 200 100 A 100 100 0 1 1 0 100 Z' },
|
|
86
|
+
triangle: { name: '三角形', path: toPath(polygon(3)) },
|
|
87
|
+
'triangle-down': { name: '倒三角', path: toPath(polygon(3, 90)) },
|
|
88
|
+
diamond: { name: '菱形', path: toPath([[100, 0], [200, 100], [100, 200], [0, 100]]) },
|
|
89
|
+
pentagon: { name: '五边形', path: toPath(polygon(5)) },
|
|
90
|
+
hexagon: { name: '六边形', path: toPath(polygon(6, 0)) },
|
|
91
|
+
'star-5': { name: '五角星', path: star5() },
|
|
92
|
+
'arrow-right': { name: '右箭头', path: arrowH(1) },
|
|
93
|
+
'arrow-left': { name: '左箭头', path: arrowH(-1) },
|
|
94
|
+
'arrow-up': { name: '上箭头', path: arrowV(-1) },
|
|
95
|
+
'arrow-down': { name: '下箭头', path: arrowV(1) },
|
|
96
|
+
'double-arrow': { name: '双向箭头', path: doubleArrow },
|
|
97
|
+
bubble: { name: '气泡', path: bubble },
|
|
98
|
+
heart: { name: '爱心', path: heart },
|
|
99
|
+
cloud: { name: '云朵', path: cloud },
|
|
100
|
+
bolt: { name: '闪电', path: bolt },
|
|
101
|
+
cross: { name: '十字', path: cross },
|
|
102
|
+
'pie-quarter': { name: '扇形', path: quarter(100, 100, 100, 0, 200, 100) },
|
|
103
|
+
cylinder: { name: '圆柱', path: cylinder },
|
|
104
|
+
'round-frame': {
|
|
105
|
+
name: '圆角边框',
|
|
106
|
+
path: 'M 24 4 L 176 4 A 20 20 0 0 1 196 24 L 196 176 A 20 20 0 0 1 176 196 L 24 196 A 20 20 0 0 1 4 176 L 4 24 A 20 20 0 0 1 24 4 Z M 24 20 A 4 4 0 0 0 20 24 L 20 176 A 4 4 0 0 0 24 180 L 176 180 A 4 4 0 0 0 180 176 L 180 24 A 4 4 0 0 0 176 20 Z',
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const SHAPE_KEYS = Object.keys(SHAPE_LIBRARY)
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 取形状定义。
|
|
114
|
+
* @param {string} key SHAPE_KEYS 之一
|
|
115
|
+
*/
|
|
116
|
+
export function getShape(key) {
|
|
117
|
+
const s = SHAPE_LIBRARY[String(key || '')]
|
|
118
|
+
if (!s) {
|
|
119
|
+
throw new Error(`未知形状: ${key}(可用: ${SHAPE_KEYS.join(', ')})`)
|
|
120
|
+
}
|
|
121
|
+
return { viewBox: [200, 200], ...s }
|
|
122
|
+
}
|