返回 oh-my-ppt
chart-height.ts
根目录 / src / main / presentation / html / chart-height.ts
1 import type * as cheerio from 'cheerio'
2 import type { AnyNode } from 'domhandler'
3
4 /**
5 * Shared chart-frame height helpers.
6 *
7 * Two consumers cooperate on chart frame heights:
8 * - page-writer preprocesses the model's HTML and, when a chart frame has no
9 * explicit height, reads the model's intended height from an adjacent
10 * `@ppt-chart-height=N` comment marker.
11 * - html-utils validates that, when the marker is present, the frame's actual
12 * `h-[Npx]` class matches it (catches "comment says 420, class says 240").
13 */
14 export const CHART_FRAME_HEIGHT_MIN = 120
15 export const CHART_FRAME_HEIGHT_MAX = 760
16 export const CHART_FRAME_HEIGHT_COMMENT_MARKER = '@ppt-chart-height'
17
18 export type CheerioSiblingNode = {
19 type?: string
20 data?: string
21 prev?: CheerioSiblingNode
22 }
23
24 const CHART_HEIGHT_COMMENT_RE = /<!--([\s\S]*?@ppt-chart-height[\s\S]*?)-->/gi
25
26 const scanPreviousSiblingHeightMarker = (
27 element: cheerio.Cheerio<AnyNode>
28 ): number | null => {
29 let previous = (element.get(0) as CheerioSiblingNode | undefined)?.prev
30 let scanned = 0
31 while (previous && scanned < 8) {
32 scanned += 1
33 if (previous.type === 'comment') {
34 const height = extractChartHeightFromComment(previous.data || '')
35 if (height) return height
36 }
37 if (previous.type !== 'text' || (previous.data || '').trim().length > 0) break
38 previous = previous.prev
39 }
40 return null
41 }
42
43 const resolveOnlyPageMarkerForSingleChart = (
44 parent: cheerio.Cheerio<AnyNode>
45 ): number | null => {
46 let root = parent
47 let guard = 0
48 while (root.parent().length && guard < 12) {
49 guard += 1
50 root = root.parent()
51 }
52
53 if (root.find('canvas').length !== 1) return null
54
55 const matches = [...(root.html() || '').matchAll(CHART_HEIGHT_COMMENT_RE)]
56 if (matches.length !== 1) return null
57
58 return extractChartHeightFromComment(matches[0]?.[1] || '')
59 }
60
61 export const parseChartHeightClass = (cls: string): number | null => {
62 const match = /^h-\[\s*(\d+(?:\.\d+)?)px\s*\]$/i.exec(cls)
63 if (!match) return null
64 const value = Number(match[1])
65 return value > 0 ? value : null
66 }
67
68 export const normalizeChartHeight = (value: number | string): number | null => {
69 const height = typeof value === 'string' ? Number(value) : value
70 if (!Number.isFinite(height)) return null
71 if (height < CHART_FRAME_HEIGHT_MIN || height > CHART_FRAME_HEIGHT_MAX) return null
72 return Math.round(height)
73 }
74
75 export const extractChartHeightFromComment = (comment: string): number | null => {
76 const escapedMarker = CHART_FRAME_HEIGHT_COMMENT_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
77 const markerPattern = new RegExp(`${escapedMarker}\\s*=\\s*(\\d+(?:\\.\\d+)?)\\s*(?:px)?\\b`, 'i')
78 const match = markerPattern.exec(comment)
79 if (!match?.[1]) return null
80 return normalizeChartHeight(match[1])
81 }
82
83 export const resolveChartHeightFromNearbyComment = (
84 parent: cheerio.Cheerio<AnyNode>
85 ): number | null => {
86 const direct = scanPreviousSiblingHeightMarker(parent)
87 if (direct) return direct
88
89 let ancestor = parent.parent()
90 let depth = 0
91 while (ancestor.length && depth < 3) {
92 depth += 1
93 if (ancestor.find('canvas').length === 1) {
94 const height = scanPreviousSiblingHeightMarker(ancestor)
95 if (height) return height
96 }
97 ancestor = ancestor.parent()
98 }
99
100 return resolveOnlyPageMarkerForSingleChart(parent)
101 }
102
102 lines TYPESCRIPT