返回 oh-my-ppt
presentation-element-context.ts
根目录 / src / renderer / src / lib / presentation-element-context.ts
1 import type { PresentationElementSnapshot } from '@arcsin1/presentation-editor-runtime'
2 import {
3 SELECTED_ELEMENT_CONTEXT_COMPUTED_STYLE_PROPERTIES,
4 type SelectedElementRuntimeContext
5 } from '@shared/generation'
6
7 const MAX_ATTRIBUTES = 40
8 const MAX_INLINE_STYLES = 40
9 const MAX_CLASS_NAMES = 24
10 const MAX_VALUE_LENGTH = 480
11
12 const compact = (value: unknown, maxLength = MAX_VALUE_LENGTH): string =>
13 String(value ?? '')
14 .replace(/\s+/g, ' ')
15 .trim()
16 .slice(0, maxLength)
17
18 const isSafeAttribute = (name: string): boolean => {
19 const normalized = name.trim().toLowerCase()
20 return (
21 Boolean(normalized) &&
22 !normalized.startsWith('on') &&
23 normalized !== 'style' &&
24 normalized !== 'srcdoc' &&
25 !normalized.startsWith('data-arcsin1-presentation-editor-')
26 )
27 }
28
29 const normalizeBound = (value: number, minimum: number): number =>
30 Math.round(Math.max(minimum, Math.min(100_000, value)) * 100) / 100
31
32 /**
33 * Converts the package's complete inspector result into a compact AI-editing reference.
34 * Computed styles are intentionally curated: they explain rendered geometry and appearance
35 * without turning a single selected element into an unbounded prompt payload.
36 */
37 export function buildSelectedElementRuntimeContext(
38 snapshot: PresentationElementSnapshot
39 ): SelectedElementRuntimeContext {
40 const attributes: Record<string, string> = {}
41 for (const [name, value] of Object.entries(snapshot.attributes || {})) {
42 if (Object.keys(attributes).length >= MAX_ATTRIBUTES || !isSafeAttribute(name)) continue
43 const normalizedName = compact(name, 100)
44 const normalizedValue = compact(value)
45 if (normalizedName) attributes[normalizedName] = normalizedValue
46 }
47
48 const inlineStyle: NonNullable<SelectedElementRuntimeContext['inlineStyle']> = {}
49 for (const [property, declaration] of Object.entries(snapshot.inlineStyle || {})) {
50 if (Object.keys(inlineStyle).length >= MAX_INLINE_STYLES) continue
51 const normalizedProperty = compact(property, 100).toLowerCase()
52 if (!/^(?:--)?[a-z][a-z0-9-]*$/i.test(normalizedProperty)) continue
53 inlineStyle[normalizedProperty] = {
54 value: compact(declaration?.value),
55 priority: declaration?.priority === 'important' ? 'important' : ''
56 }
57 }
58
59 const computedStyle: Record<string, string> = {}
60 for (const property of SELECTED_ELEMENT_CONTEXT_COMPUTED_STYLE_PROPERTIES) {
61 const value = compact(snapshot.computedStyle?.[property])
62 if (value) computedStyle[property] = value
63 }
64
65 const bounds = snapshot.bounds
66 const validBounds =
67 bounds &&
68 [bounds.x, bounds.y, bounds.width, bounds.height].every(
69 (value) => typeof value === 'number' && Number.isFinite(value)
70 )
71 ? {
72 x: normalizeBound(bounds.x, -100_000),
73 y: normalizeBound(bounds.y, -100_000),
74 width: normalizeBound(bounds.width, 0),
75 height: normalizeBound(bounds.height, 0)
76 }
77 : undefined
78
79 const classList = (snapshot.classList || [])
80 .map((name) => compact(name, 100))
81 .filter(
82 (name) =>
83 Boolean(name) &&
84 !name.startsWith('arcsin1-presentation-editor-') &&
85 !name.startsWith('ppt-inspector-')
86 )
87 .slice(0, MAX_CLASS_NAMES)
88
89 return {
90 ...(classList.length > 0 ? { classList } : {}),
91 ...(Object.keys(attributes).length > 0 ? { attributes } : {}),
92 ...(Object.keys(inlineStyle).length > 0 ? { inlineStyle } : {}),
93 ...(Object.keys(computedStyle).length > 0 ? { computedStyle } : {}),
94 ...(validBounds ? { bounds: validBounds } : {})
95 }
96 }
97
97 lines TYPESCRIPT