| 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 |