返回 oh-my-ppt
buildInsertElementHtml.ts
根目录 / src / renderer / src / components / session-detail / workspace / insert-shapes / buildInsertElementHtml.ts
1 import { escapeHtmlText } from '@renderer/lib/utils'
2 import {
3 ICON_VIEWBOX,
4 getIconDefinition,
5 iconOuterSvgAttrs,
6 isRegisteredIconId,
7 serializeIconInner
8 } from './iconRegistry'
9 import { getShapeDefinition } from './shapeRegistry'
10
11 export type InsertShapeType =
12 | 'rect'
13 | 'rounded-rect'
14 | 'ellipse'
15 | 'triangle'
16 | 'diamond'
17 | 'pentagon'
18 | 'hexagon'
19 | 'parallelogram'
20 | 'trapezoid'
21 | 'star-5'
22 | 'line'
23 | 'arrow-right'
24 | 'chevron-right'
25
26 export interface InsertElementLayout {
27 blockId: string
28 left: number
29 top: number
30 width: number
31 height: number
32 zIndex: number
33 }
34
35 export interface BuildShapeOptions extends InsertElementLayout {
36 type: InsertShapeType
37 fill?: string
38 stroke?: string
39 }
40
41 export interface BuildIconOptions extends InsertElementLayout {
42 iconId: string
43 color?: string
44 }
45
46 // blockId is generated as `select-arcsin1-` + nanoid(8); validate strictly so
47 // we never splice an arbitrary string into an HTML attribute.
48 const BLOCK_ID_RE = /^select-arcsin1-[A-Za-z0-9_-]{4,32}$/
49 // Accept #hex (3/4/6/8 digits) and CSS named colors; reject everything else
50 // (no url(), no expression(), no var() with user input).
51 const COLOR_RE =
52 /^(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{4}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{8}|transparent|currentColor|none|[a-zA-Z]+)$/
53
54 export function isValidBlockId(value: string): boolean {
55 return BLOCK_ID_RE.test(value)
56 }
57
58 export function isValidColor(value: string): boolean {
59 return COLOR_RE.test(value)
60 }
61
62 function assertBlockId(blockId: string): void {
63 if (!isValidBlockId(blockId)) {
64 throw new Error(`buildInsertElementHtml: invalid blockId "${blockId}"`)
65 }
66 }
67
68 function resolveColor(value: string | undefined, fallback: string): string {
69 if (!value) return fallback
70 if (!isValidColor(value)) {
71 throw new Error(`buildInsertElementHtml: invalid color "${value}"`)
72 }
73 return value
74 }
75
76 function outerStyle(layout: InsertElementLayout, extra: string[]): string {
77 return escapeHtmlText(
78 [
79 'position:absolute',
80 `left:${layout.left}px`,
81 `top:${layout.top}px`,
82 `width:${layout.width}px`,
83 `height:${layout.height}px`,
84 `z-index:${layout.zIndex}`,
85 ...extra
86 ].join('; ')
87 )
88 }
89
90 export function buildShapeElementHtml(options: BuildShapeOptions): string {
91 assertBlockId(options.blockId)
92 const def = getShapeDefinition(options.type)
93 if (!def) {
94 throw new Error(`buildInsertElementHtml: unknown shape type "${options.type}"`)
95 }
96 const fill = resolveColor(options.fill, def.defaultFill)
97 const stroke = resolveColor(options.stroke, def.defaultStroke)
98 const strokeWidth = def.strokeWidth
99 // Use the default viewBox so the shape's intrinsic proportions are stable;
100 // the outer svg scales to the div via width/height 100%.
101 const vbW = def.defaultWidth
102 const vbH = def.defaultHeight
103 const inner = def.renderInner(vbW, vbH, { fill, stroke, strokeWidth })
104 const style = outerStyle(options, [])
105 return `<div data-block-id="${options.blockId}" data-ppt-edit-kind="shape" data-ppt-shape-type="${options.type}" style="${style}"><svg viewBox="0 0 ${vbW} ${vbH}" width="100%" height="100%" preserveAspectRatio="none">${inner}</svg></div>`
106 }
107
108 export function buildIconElementHtml(options: BuildIconOptions): string {
109 assertBlockId(options.blockId)
110 if (!isRegisteredIconId(options.iconId)) {
111 throw new Error(`buildInsertElementHtml: unknown iconId "${options.iconId}"`)
112 }
113 const def = getIconDefinition(options.iconId)!
114 const color = resolveColor(options.color, '#3f4b35')
115 // color lives on the outer div so currentColor (used by the svg stroke / badge fill) can
116 // be updated later by editing the div's inline style only.
117 const style = outerStyle(options, [`color:${color}`])
118 const inner = serializeIconInner(def)
119 const svgAttrs = iconOuterSvgAttrs(def)
120 return `<div data-block-id="${options.blockId}" data-ppt-edit-kind="icon" data-ppt-icon-id="${options.iconId}" style="${style}"><svg viewBox="0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}" width="100%" height="100%" ${svgAttrs}>${inner}</svg></div>`
121 }
122
122 lines TYPESCRIPT