| 1 | import { requireSlideSize, type SlideSizePreset } from '@shared/slide-size' |
| 2 | |
| 3 | export const buildBasePageStyleTag = (input: SlideSizePreset): string => { |
| 4 | const slideSize = requireSlideSize(input) |
| 5 | return `<style id="ppt-page-guard-style"> |
| 6 | :root { |
| 7 | --ppt-page-bg: #ffffff; |
| 8 | --ppt-system-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif; |
| 9 | --ppt-slide-width: ${slideSize.width}px; |
| 10 | --ppt-slide-height: ${slideSize.height}px; |
| 11 | } |
| 12 | html, body { |
| 13 | margin: 0; |
| 14 | width: var(--ppt-slide-width); |
| 15 | height: var(--ppt-slide-height); |
| 16 | overflow: hidden; |
| 17 | font-family: var(--ppt-master-body-font, var(--ppt-body-font, var(--ppt-system-sans))); |
| 18 | background: var(--ppt-page-bg); |
| 19 | color: #0f172a; |
| 20 | } |
| 21 | *, *::before, *::after { box-sizing: border-box; } |
| 22 | .ppt-page-root[data-ppt-guard-root="1"] { |
| 23 | position: relative; |
| 24 | width: var(--ppt-slide-width); |
| 25 | height: var(--ppt-slide-height); |
| 26 | overflow: hidden; |
| 27 | isolation: isolate; |
| 28 | background: var(--ppt-page-bg); |
| 29 | } |
| 30 | .ppt-page-root.p-2, |
| 31 | .ppt-page-root.p-8, |
| 32 | .ppt-page-root.p-12 { |
| 33 | padding: 0; |
| 34 | } |
| 35 | .ppt-page-root[data-ppt-guard-root="1"]:not(.p-2):not(.p-8):not(.p-12) { |
| 36 | padding: 0; |
| 37 | } |
| 38 | body > .ppt-page-root:not([data-ppt-guard-root="1"]):not(.p-2):not(.p-8):not(.p-12) { |
| 39 | padding: 0; |
| 40 | } |
| 41 | .ppt-page-fit-scope { |
| 42 | position: relative; |
| 43 | width: 100%; |
| 44 | height: 100%; |
| 45 | transform-origin: top left; |
| 46 | overflow: hidden; |
| 47 | } |
| 48 | .ppt-page-content { |
| 49 | width: 100%; |
| 50 | height: 100%; |
| 51 | min-height: 100%; |
| 52 | flex: 1; |
| 53 | position: relative; |
| 54 | display: flex; |
| 55 | flex-direction: column; |
| 56 | justify-content: flex-start; |
| 57 | align-items: stretch; |
| 58 | overflow: hidden; |
| 59 | font-size: 16px; |
| 60 | font-family: var(--ppt-master-body-font, var(--ppt-body-font, var(--ppt-system-sans))); |
| 61 | } |
| 62 | .ppt-page-content [data-ppt-readable-fonts="1"] { |
| 63 | font-size: 18px; |
| 64 | } |
| 65 | .ppt-page-content h1, |
| 66 | .ppt-page-content h2, |
| 67 | .ppt-page-content h3, |
| 68 | .ppt-page-content h4, |
| 69 | .ppt-page-content h5, |
| 70 | .ppt-page-content h6, |
| 71 | .ppt-page-content [data-role="title"], |
| 72 | .ppt-page-content [data-block-id="title"] { |
| 73 | font-family: var(--ppt-master-title-font, var(--ppt-title-font, var(--ppt-body-font, var(--ppt-system-sans)))); |
| 74 | } |
| 75 | .ppt-page-content > [data-page-scaffold="1"] { |
| 76 | width: 100%; |
| 77 | min-height: 100%; |
| 78 | height: 100%; |
| 79 | } |
| 80 | .ppt-page-content canvas { |
| 81 | display: block; |
| 82 | width: 100%; |
| 83 | height: 100%; |
| 84 | max-width: 100% !important; |
| 85 | max-height: 100% !important; |
| 86 | } |
| 87 | .ppt-page-content .ppt-chart-frame { |
| 88 | position: relative; |
| 89 | min-width: 0; |
| 90 | overflow: hidden; |
| 91 | } |
| 92 | .ppt-page-content .ppt-chart-frame > canvas { |
| 93 | width: 100% !important; |
| 94 | height: 100% !important; |
| 95 | } |
| 96 | .ppt-page-content [data-block-id*="chart"], |
| 97 | .ppt-page-content [data-block-id*="graph"], |
| 98 | .ppt-page-content [data-block-id*="plot"] { |
| 99 | min-width: 0; |
| 100 | } |
| 101 | [data-role="title"] h1, |
| 102 | header[data-block-id="title"] h1 { |
| 103 | font-size: 48px !important; |
| 104 | line-height: 1.2 !important; |
| 105 | } |
| 106 | [data-role="title"] h1.text-5xl, |
| 107 | header[data-block-id="title"] h1.text-5xl { |
| 108 | font-size: 48px !important; |
| 109 | } |
| 110 | </style>` |
| 111 | } |
| 112 | |
| 113 | export const buildFitScript = (input: SlideSizePreset): string => { |
| 114 | const slideSize = requireSlideSize(input) |
| 115 | const heightScale = slideSize.height / 900 |
| 116 | const legacyMinFont = Math.round(14 * heightScale) |
| 117 | const auxiliaryMinFont = Math.round(12 * heightScale) |
| 118 | const bodyMinFont = Math.round(18 * heightScale) |
| 119 | const headingMinFont = Math.round(24 * heightScale) |
| 120 | return `<script id="ppt-page-fit"> |
| 121 | (() => { |
| 122 | const WIDTH = ${slideSize.width}; |
| 123 | const HEIGHT = ${slideSize.height}; |
| 124 | const LEGACY_MIN_FONT = ${legacyMinFont}; |
| 125 | const AUXILIARY_MIN_FONT = ${auxiliaryMinFont}; |
| 126 | const BODY_MIN_FONT = ${bodyMinFont}; |
| 127 | const HEADING_MIN_FONT = ${headingMinFont}; |
| 128 | const AUXILIARY_TEXT_SELECTOR = [ |
| 129 | "footer", |
| 130 | "small", |
| 131 | "figcaption", |
| 132 | '[data-ppt-text-role="auxiliary"]', |
| 133 | '[data-role="footer"]', |
| 134 | '[data-role="footnote"]', |
| 135 | '[data-role="source"]', |
| 136 | '[data-role="annotation"]', |
| 137 | '[data-role="page-number"]' |
| 138 | ].join(","); |
| 139 | const search = new URLSearchParams(window.location.search); |
| 140 | const disableFit = search.get("fit") === "off"; |
| 141 | const findRoot = () => |
| 142 | document.querySelector('.ppt-page-root[data-ppt-guard-root="1"]') || |
| 143 | document.querySelector(".ppt-page-root"); |
| 144 | const collectTextElements = (root) => { |
| 145 | const elements = new Set(); |
| 146 | const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); |
| 147 | let textNode = walker.nextNode(); |
| 148 | while (textNode) { |
| 149 | const parent = textNode.parentElement; |
| 150 | if ( |
| 151 | textNode.textContent.trim() && |
| 152 | parent && |
| 153 | !parent.closest("script, style, svg, canvas, .katex") |
| 154 | ) { |
| 155 | elements.add(parent); |
| 156 | } |
| 157 | textNode = walker.nextNode(); |
| 158 | } |
| 159 | return Array.from(elements); |
| 160 | }; |
| 161 | const isAuxiliaryText = (node) => Boolean(node.closest(AUXILIARY_TEXT_SELECTOR)); |
| 162 | const resolveMinimumFontSize = (node, readableRoot) => { |
| 163 | if (!readableRoot || !readableRoot.contains(node)) return LEGACY_MIN_FONT; |
| 164 | if (isAuxiliaryText(node)) return AUXILIARY_MIN_FONT; |
| 165 | if ( |
| 166 | node.matches("h1, h2, h3, h4, h5, h6") || |
| 167 | node.closest( |
| 168 | 'h1, h2, h3, h4, h5, h6, [data-role="title"], [data-block-id="title"]' |
| 169 | ) |
| 170 | ) { |
| 171 | return HEADING_MIN_FONT; |
| 172 | } |
| 173 | return BODY_MIN_FONT; |
| 174 | }; |
| 175 | |
| 176 | function fitPage() { |
| 177 | const root = findRoot(); |
| 178 | if (!root) return; |
| 179 | |
| 180 | let scope = root.querySelector(":scope > .ppt-page-fit-scope"); |
| 181 | let content = null; |
| 182 | if (scope) { |
| 183 | content = |
| 184 | scope.querySelector(":scope > .ppt-page-content") || |
| 185 | scope.querySelector(".ppt-page-content") || |
| 186 | scope; |
| 187 | } |
| 188 | |
| 189 | if (!scope) { |
| 190 | const directElementChildren = Array.from(root.children); |
| 191 | const singleContentChild = |
| 192 | directElementChildren.length === 1 && |
| 193 | directElementChildren[0].classList.contains("ppt-page-content") |
| 194 | ? directElementChildren[0] |
| 195 | : null; |
| 196 | |
| 197 | if (singleContentChild) { |
| 198 | content = singleContentChild; |
| 199 | } else { |
| 200 | const container = document.createElement("div"); |
| 201 | container.className = "ppt-page-content"; |
| 202 | container.style.cssText = "white-space:normal;word-wrap:normal;"; |
| 203 | while (root.firstChild) { |
| 204 | container.appendChild(root.firstChild); |
| 205 | } |
| 206 | content = container; |
| 207 | } |
| 208 | |
| 209 | const scopeEl = document.createElement("div"); |
| 210 | scopeEl.className = "ppt-page-fit-scope"; |
| 211 | scopeEl.appendChild(content); |
| 212 | root.appendChild(scopeEl); |
| 213 | scope = scopeEl; |
| 214 | } |
| 215 | |
| 216 | scope.style.transform = "scale(1)"; |
| 217 | const measuredContent = content || scope; |
| 218 | const readableRoot = measuredContent.querySelector('[data-ppt-readable-fonts="1"]'); |
| 219 | const textEntries = collectTextElements(measuredContent).map((node) => ({ |
| 220 | node, |
| 221 | minimum: resolveMinimumFontSize(node, readableRoot), |
| 222 | size: Number.parseFloat(getComputedStyle(node).fontSize || "16") |
| 223 | })); |
| 224 | if (readableRoot) { |
| 225 | textEntries.forEach((entry) => { |
| 226 | if (entry.minimum <= 0) return; |
| 227 | if (Number.isFinite(entry.size) && entry.size < entry.minimum) { |
| 228 | entry.size = entry.minimum; |
| 229 | entry.node.style.setProperty("font-size", entry.minimum + "px", "important"); |
| 230 | } |
| 231 | }); |
| 232 | } |
| 233 | if (disableFit) { |
| 234 | return; |
| 235 | } |
| 236 | const targetWidth = Math.max(1, Math.floor(scope.clientWidth || root.clientWidth || WIDTH)); |
| 237 | const targetHeight = Math.max(1, Math.floor(scope.clientHeight || root.clientHeight || HEIGHT)); |
| 238 | let guard = 0; |
| 239 | while ((measuredContent.scrollWidth > targetWidth || measuredContent.scrollHeight > targetHeight) && guard < 12) { |
| 240 | let changed = false; |
| 241 | textEntries.forEach((entry) => { |
| 242 | if (Number.isFinite(entry.size) && entry.size > entry.minimum) { |
| 243 | entry.size = Math.max(entry.minimum, Math.floor(entry.size * 0.94)); |
| 244 | entry.node.style.setProperty("font-size", entry.size + "px", "important"); |
| 245 | changed = true; |
| 246 | } |
| 247 | }); |
| 248 | if (!changed) break; |
| 249 | guard += 1; |
| 250 | } |
| 251 | |
| 252 | const scale = Math.min( |
| 253 | 1, |
| 254 | targetWidth / Math.max(measuredContent.scrollWidth, 1), |
| 255 | targetHeight / Math.max(measuredContent.scrollHeight, 1) |
| 256 | ); |
| 257 | scope.style.transform = "scale(" + scale.toFixed(4) + ")"; |
| 258 | } |
| 259 | |
| 260 | window.addEventListener("load", () => requestAnimationFrame(fitPage), { once: true }); |
| 261 | window.addEventListener("resize", fitPage); |
| 262 | })(); |
| 263 | </script>` |
| 264 | } |
| 265 | |
| 266 | export const VIDEO_INTERACTION_SCRIPT = `<script id="ppt-video-interaction"> |
| 267 | (() => { |
| 268 | const prepareVideos = () => { |
| 269 | document.querySelectorAll("video").forEach((video) => { |
| 270 | video.playsInline = true; |
| 271 | if (!video.hasAttribute("preload")) { |
| 272 | video.preload = "metadata"; |
| 273 | } |
| 274 | }); |
| 275 | }; |
| 276 | if (document.readyState === "loading") { |
| 277 | document.addEventListener("DOMContentLoaded", prepareVideos, { once: true }); |
| 278 | } else { |
| 279 | prepareVideos(); |
| 280 | } |
| 281 | window.addEventListener("pageshow", prepareVideos); |
| 282 | })(); |
| 283 | </script>` |
| 284 | |
| 285 | export const DEFAULT_MOTION_SCRIPT = `<script id="ppt-default-motion"> |
| 286 | (() => { |
| 287 | const search = new URLSearchParams(window.location.search); |
| 288 | if (search.get("print") === "1" || search.get("export") === "1") { |
| 289 | document.documentElement.dataset.pptExportStatic = "1"; |
| 290 | return; |
| 291 | } |
| 292 | |
| 293 | function revealFallback(root) { |
| 294 | const hiddenTargets = Array.from(root.querySelectorAll("*")) |
| 295 | .filter((el) => { |
| 296 | const style = getComputedStyle(el); |
| 297 | return style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity) === 0; |
| 298 | }) |
| 299 | .slice(0, 120); |
| 300 | hiddenTargets.forEach((el, i) => { |
| 301 | const node = el; |
| 302 | node.style.transition = "opacity 300ms ease, transform 300ms ease"; |
| 303 | if (!node.style.transform || node.style.transform === "none") { |
| 304 | node.style.transform = "translateY(0)"; |
| 305 | } |
| 306 | window.setTimeout(() => { |
| 307 | node.style.opacity = "1"; |
| 308 | }, i * 8); |
| 309 | }); |
| 310 | } |
| 311 | |
| 312 | function runDataAnimMotion(root) { |
| 313 | var pptApi = globalThis.PPT; |
| 314 | if (!pptApi || typeof pptApi.scanDataAnim !== "function") return false; |
| 315 | var config = pptApi.scanDataAnim(root); |
| 316 | if (!config || (!config.load.length && !config.click.length)) return false; |
| 317 | |
| 318 | if (config.load.length > 0 && typeof pptApi.executeDataAnim === "function") { |
| 319 | pptApi.executeDataAnim(config.load); |
| 320 | } |
| 321 | |
| 322 | if (config.click.length > 0 && pptApi.clicks && typeof pptApi.clicks.on === "function") { |
| 323 | var clickSteps = Array.isArray(config.clickSteps) && config.clickSteps.length > 0 |
| 324 | ? config.clickSteps |
| 325 | : config.click.map(function (animDef) { return [animDef]; }); |
| 326 | clickSteps.forEach(function (stepDefs, idx) { |
| 327 | var clickNum = idx + 1; |
| 328 | pptApi.clicks.on(clickNum, function () { |
| 329 | if (typeof pptApi.executeDataAnim === "function") { |
| 330 | pptApi.executeDataAnim(stepDefs); |
| 331 | } else { |
| 332 | stepDefs.forEach(function (animDef) { |
| 333 | pptApi.animate(animDef.targets, { |
| 334 | opacity: [0, 1], |
| 335 | translateY: [20, 0], |
| 336 | duration: animDef.duration, |
| 337 | easing: animDef.easing |
| 338 | }); |
| 339 | }); |
| 340 | } |
| 341 | }); |
| 342 | }); |
| 343 | } |
| 344 | |
| 345 | return true; |
| 346 | } |
| 347 | |
| 348 | function runLegacyMotion(root) { |
| 349 | var targets = Array.from( |
| 350 | root.querySelectorAll(".opacity-0, [data-anime], [data-animate], h1, h2, h3, p, li, .card, .panel, .text-section, .diagram-section, .timeline-node, section, section > *") |
| 351 | ).slice(0, 16); |
| 352 | if (targets.length === 0) { |
| 353 | revealFallback(root); |
| 354 | return; |
| 355 | } |
| 356 | var pptApi = globalThis.PPT; |
| 357 | if (pptApi && typeof pptApi.animate === "function") { |
| 358 | try { |
| 359 | pptApi.animate(targets, { |
| 360 | opacity: [0, 1], |
| 361 | translateY: [20, 0], |
| 362 | easing: "easeOutCubic", |
| 363 | duration: 560, |
| 364 | delay: function (_el, i) { return i * 45; }, |
| 365 | }); |
| 366 | window.setTimeout(function () { revealFallback(root); }, 720); |
| 367 | return; |
| 368 | } catch (_err) { |
| 369 | revealFallback(root); |
| 370 | return; |
| 371 | } |
| 372 | } |
| 373 | targets.forEach(function (el, i) { |
| 374 | var node = el; |
| 375 | node.style.opacity = "0"; |
| 376 | node.style.transform = "translateY(14px)"; |
| 377 | node.style.transition = "opacity 420ms ease, transform 420ms ease"; |
| 378 | window.setTimeout(function () { |
| 379 | node.style.opacity = "1"; |
| 380 | node.style.transform = "translateY(0)"; |
| 381 | }, i * 40); |
| 382 | }); |
| 383 | revealFallback(root); |
| 384 | } |
| 385 | |
| 386 | function runMotion() { |
| 387 | var root = document.querySelector(".ppt-page-root"); |
| 388 | if (!root) return; |
| 389 | if (!runDataAnimMotion(root)) { |
| 390 | runLegacyMotion(root); |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | if (document.readyState === "loading") { |
| 395 | document.addEventListener("DOMContentLoaded", runMotion, { once: true }); |
| 396 | } else { |
| 397 | runMotion(); |
| 398 | } |
| 399 | })(); |
| 400 | </script>` |
| 401 |