chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* Top-level presentation builder — assembles all parsed components
|
||||
* (themes, masters, layouts, slides) into a single PresentationData structure.
|
||||
*/
|
||||
|
||||
import { parseRels, type RelEntry, resolveRelTarget } from '../parser/rel-parser'
|
||||
import { emuToPx } from '../parser/units'
|
||||
import { parseXml, type SafeXmlNode } from '../parser/xml-parser'
|
||||
import type { PptxFiles } from '../parser/zip-parser'
|
||||
import { type LayoutData, type PlaceholderEntry, parseLayout } from './layout'
|
||||
import { type MasterData, parseMaster } from './master'
|
||||
import type { Position, Size } from './nodes/base-node'
|
||||
import { parseSlide, type SlideData, type SlideNode } from './slide'
|
||||
import { parseTheme, type ThemeData } from './theme'
|
||||
|
||||
export interface PresentationData {
|
||||
width: number
|
||||
height: number
|
||||
slides: SlideData[]
|
||||
layouts: Map<string, LayoutData>
|
||||
masters: Map<string, MasterData>
|
||||
themes: Map<string, ThemeData>
|
||||
slideToLayout: Map<number, string>
|
||||
layoutToMaster: Map<string, string>
|
||||
masterToTheme: Map<string, string>
|
||||
media: Map<string, Uint8Array>
|
||||
tableStyles?: SafeXmlNode
|
||||
charts: Map<string, SafeXmlNode>
|
||||
isWps: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the base directory from a file path.
|
||||
* E.g., "ppt/slides/slide1.xml" → "ppt/slides"
|
||||
*/
|
||||
function basePath(filePath: string): string {
|
||||
const idx = filePath.lastIndexOf('/')
|
||||
return idx >= 0 ? filePath.substring(0, idx) : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given XML file path, find its corresponding .rels file path.
|
||||
* E.g., "ppt/slides/slide1.xml" → "ppt/slides/_rels/slide1.xml.rels"
|
||||
*/
|
||||
function relsPathFor(filePath: string): string {
|
||||
const dir = basePath(filePath)
|
||||
const fileName = filePath.substring(filePath.lastIndexOf('/') + 1)
|
||||
return `${dir}/_rels/${fileName}.rels`
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect WPS (Kingsoft Office / WPS Office) by checking for known markers
|
||||
* in the presentation XML string.
|
||||
*/
|
||||
function detectWps(presentationXml: string): boolean {
|
||||
return (
|
||||
/\bKingsoft\b/i.test(presentationXml) ||
|
||||
/\bWPS Office\b/i.test(presentationXml) ||
|
||||
/xmlns:[\w.-]*kso[\w.-]*=/i.test(presentationXml)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a rels entry by type substring match.
|
||||
*/
|
||||
function findRelByType(rels: Map<string, RelEntry>, typeSubstring: string): RelEntry | undefined {
|
||||
for (const [, entry] of rels) {
|
||||
if (entry.type.includes(typeSubstring)) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ALL rels entries matching a type substring, returning [rId, entry] pairs.
|
||||
*/
|
||||
function findRelsByType(rels: Map<string, RelEntry>, typeSubstring: string): [string, RelEntry][] {
|
||||
const results: [string, RelEntry][] = []
|
||||
for (const [rId, entry] of rels) {
|
||||
if (entry.type.includes(typeSubstring)) {
|
||||
results.push([rId, entry])
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the complete PresentationData from extracted PPTX files.
|
||||
*
|
||||
* This is the main factory function that wires together all parsed components:
|
||||
* 1. Parses presentation.xml for slide ordering and size
|
||||
* 2. Resolves the full relationship chain: slide → layout → master → theme
|
||||
* 3. Parses each component and assembles the final structure
|
||||
*/
|
||||
export function buildPresentation(files: PptxFiles): PresentationData {
|
||||
// --- Parse presentation root ---
|
||||
const presRoot = parseXml(files.presentation)
|
||||
const presRels = parseRels(files.presentationRels)
|
||||
|
||||
// --- Slide size ---
|
||||
const sldSz = presRoot.child('sldSz')
|
||||
const width = emuToPx(sldSz.numAttr('cx') ?? 9144000) // default 10 inches
|
||||
const height = emuToPx(sldSz.numAttr('cy') ?? 6858000) // default 7.5 inches
|
||||
|
||||
// --- WPS detection ---
|
||||
const isWps = detectWps(files.presentation)
|
||||
|
||||
// --- Parse themes ---
|
||||
const themes = new Map<string, ThemeData>()
|
||||
for (const [themePath, themeXml] of files.themes) {
|
||||
const themeRoot = parseXml(themeXml)
|
||||
themes.set(themePath, parseTheme(themeRoot))
|
||||
}
|
||||
|
||||
// --- Parse slide masters and build master→theme mapping ---
|
||||
const masters = new Map<string, MasterData>()
|
||||
const masterToTheme = new Map<string, string>()
|
||||
|
||||
for (const [masterPath, masterXml] of files.slideMasters) {
|
||||
const masterRoot = parseXml(masterXml)
|
||||
const masterData = parseMaster(masterRoot)
|
||||
|
||||
// Find theme relationship for this master
|
||||
const masterRelsPath = relsPathFor(masterPath)
|
||||
const masterRelsXml = files.slideMasterRels.get(masterRelsPath)
|
||||
if (masterRelsXml) {
|
||||
const masterRels = parseRels(masterRelsXml)
|
||||
masterData.rels = masterRels
|
||||
const themeRel = findRelByType(masterRels, 'theme')
|
||||
if (themeRel) {
|
||||
const themePath = resolveRelTarget(basePath(masterPath), themeRel.target)
|
||||
masterToTheme.set(masterPath, themePath)
|
||||
}
|
||||
}
|
||||
masters.set(masterPath, masterData)
|
||||
}
|
||||
|
||||
// --- Parse slide layouts and build layout→master mapping ---
|
||||
const layouts = new Map<string, LayoutData>()
|
||||
const layoutToMaster = new Map<string, string>()
|
||||
|
||||
for (const [layoutPath, layoutXml] of files.slideLayouts) {
|
||||
const layoutRoot = parseXml(layoutXml)
|
||||
const layoutData = parseLayout(layoutRoot)
|
||||
|
||||
// Find master relationship for this layout
|
||||
const layoutRelsPath = relsPathFor(layoutPath)
|
||||
const layoutRelsXml = files.slideLayoutRels.get(layoutRelsPath)
|
||||
if (layoutRelsXml) {
|
||||
const layoutRels = parseRels(layoutRelsXml)
|
||||
layoutData.rels = layoutRels
|
||||
const masterRel = findRelByType(layoutRels, 'slideMaster')
|
||||
if (masterRel) {
|
||||
const masterPath = resolveRelTarget(basePath(layoutPath), masterRel.target)
|
||||
layoutToMaster.set(layoutPath, masterPath)
|
||||
}
|
||||
}
|
||||
layouts.set(layoutPath, layoutData)
|
||||
}
|
||||
|
||||
// --- Parse charts ---
|
||||
const charts = new Map<string, SafeXmlNode>()
|
||||
for (const [chartPath, chartXml] of files.charts) {
|
||||
const chartRoot = parseXml(chartXml)
|
||||
if (chartRoot.exists()) {
|
||||
charts.set(chartPath, chartRoot)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Determine slide ordering ---
|
||||
// The sldIdLst contains sldId elements with r:id attributes that reference
|
||||
// presentation.xml.rels. We need to handle the fact that the attr might be
|
||||
// stored as 'r:id' in the original XML but SafeXmlNode.attr() uses localName.
|
||||
const sldIdLst = presRoot.child('sldIdLst')
|
||||
const orderedSlideTargets: string[] = []
|
||||
|
||||
for (const sldId of sldIdLst.children('sldId')) {
|
||||
// Try multiple attribute name patterns
|
||||
const rId = sldId.attr('r:id') ?? sldId.attr('id')
|
||||
if (rId) {
|
||||
const relEntry = presRels.get(rId)
|
||||
if (relEntry) {
|
||||
const slidePath = resolveRelTarget('ppt', relEntry.target)
|
||||
orderedSlideTargets.push(slidePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if sldIdLst parsing didn't yield results, use presRels directly
|
||||
if (orderedSlideTargets.length === 0) {
|
||||
const slideRels = findRelsByType(presRels, 'slide')
|
||||
// Sort by rId number to maintain order
|
||||
slideRels.sort((a, b) => {
|
||||
const numA = Number.parseInt(a[0].replace(/\D/g, ''), 10) || 0
|
||||
const numB = Number.parseInt(b[0].replace(/\D/g, ''), 10) || 0
|
||||
return numA - numB
|
||||
})
|
||||
for (const [, entry] of slideRels) {
|
||||
// Only include direct slide relationships, not slideLayout or slideMaster
|
||||
if (
|
||||
entry.type.includes('/slide') &&
|
||||
!entry.type.includes('slideLayout') &&
|
||||
!entry.type.includes('slideMaster')
|
||||
) {
|
||||
const slidePath = resolveRelTarget('ppt', entry.target)
|
||||
orderedSlideTargets.push(slidePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Parse slides ---
|
||||
const slides: SlideData[] = []
|
||||
const slideToLayout = new Map<number, string>()
|
||||
|
||||
for (let i = 0; i < orderedSlideTargets.length; i++) {
|
||||
const slidePath = orderedSlideTargets[i]
|
||||
const slideXml = files.slides.get(slidePath)
|
||||
if (!slideXml) continue
|
||||
|
||||
// Parse slide rels
|
||||
const slideRelsPath = relsPathFor(slidePath)
|
||||
const slideRelsXml = files.slideRels.get(slideRelsPath)
|
||||
const slideRels = slideRelsXml ? parseRels(slideRelsXml) : new Map<string, RelEntry>()
|
||||
|
||||
// Parse slide
|
||||
const slideRoot = parseXml(slideXml)
|
||||
const slideData = parseSlide(slideRoot, i, slideRels, slidePath, files.diagramDrawings)
|
||||
|
||||
// Resolve layout path from the slide's layout relationship target
|
||||
if (slideData.layoutIndex) {
|
||||
const layoutPath = resolveRelTarget(basePath(slidePath), slideData.layoutIndex)
|
||||
slideData.layoutIndex = layoutPath
|
||||
slideToLayout.set(i, layoutPath)
|
||||
}
|
||||
|
||||
slides.push(slideData)
|
||||
}
|
||||
|
||||
// --- Table styles ---
|
||||
let tableStyles: SafeXmlNode | undefined
|
||||
if (files.tableStyles) {
|
||||
const tsRoot = parseXml(files.tableStyles)
|
||||
if (tsRoot.exists()) {
|
||||
tableStyles = tsRoot
|
||||
}
|
||||
}
|
||||
|
||||
const result: PresentationData = {
|
||||
width,
|
||||
height,
|
||||
slides,
|
||||
layouts,
|
||||
masters,
|
||||
themes,
|
||||
slideToLayout,
|
||||
layoutToMaster,
|
||||
masterToTheme,
|
||||
media: files.media,
|
||||
tableStyles,
|
||||
charts,
|
||||
isWps,
|
||||
}
|
||||
|
||||
// --- Resolve placeholder position inheritance ---
|
||||
resolvePlaceholderInheritance(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Placeholder Position Inheritance
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Extract placeholder info (type, idx) from a raw placeholder XML node
|
||||
* stored in layout/master.
|
||||
*/
|
||||
function getPhInfo(phNode: SafeXmlNode): { type?: string; idx?: number } {
|
||||
// Try nvSpPr > nvPr > ph, or nvPicPr > nvPr > ph
|
||||
for (const wrapper of ['nvSpPr', 'nvPicPr', 'nvGrpSpPr', 'nvGraphicFramePr', 'nvCxnSpPr']) {
|
||||
const nvWrapper = phNode.child(wrapper)
|
||||
if (nvWrapper.exists()) {
|
||||
const nvPr = nvWrapper.child('nvPr')
|
||||
const ph = nvPr.child('ph')
|
||||
if (ph.exists()) {
|
||||
const type = ph.attr('type')
|
||||
const idxStr = ph.attr('idx')
|
||||
const idx = idxStr !== undefined ? Number(idxStr) : undefined
|
||||
return { type, idx: idx !== undefined && !Number.isNaN(idx) ? idx : undefined }
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract xfrm position/size from a raw placeholder XML node.
|
||||
*/
|
||||
function getPhXfrm(phNode: SafeXmlNode): { position: Position; size: Size } | undefined {
|
||||
// Try spPr > xfrm first (most shapes)
|
||||
const spPr = phNode.child('spPr')
|
||||
if (spPr.exists()) {
|
||||
const xfrm = spPr.child('xfrm')
|
||||
if (xfrm.exists()) {
|
||||
const off = xfrm.child('off')
|
||||
const ext = xfrm.child('ext')
|
||||
const x = off.numAttr('x')
|
||||
const cx = ext.numAttr('cx')
|
||||
if (x !== undefined && cx !== undefined) {
|
||||
return {
|
||||
position: { x: emuToPx(off.numAttr('x') ?? 0), y: emuToPx(off.numAttr('y') ?? 0) },
|
||||
size: { w: emuToPx(ext.numAttr('cx') ?? 0), h: emuToPx(ext.numAttr('cy') ?? 0) },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a matching placeholder node by type and idx.
|
||||
* Matching rules (based on OOXML spec):
|
||||
* 1. Exact match on type AND idx
|
||||
* 2. Match on type only (if idx is undefined or not found)
|
||||
* 3. For "body" type, also check idx match only
|
||||
*/
|
||||
function findMatchingPlaceholder(
|
||||
placeholders: SafeXmlNode[],
|
||||
type?: string,
|
||||
idx?: number
|
||||
): SafeXmlNode | undefined {
|
||||
let typeMatch: SafeXmlNode | undefined
|
||||
|
||||
for (const ph of placeholders) {
|
||||
const info = getPhInfo(ph)
|
||||
|
||||
// Exact match (type + idx)
|
||||
if (type !== undefined && info.type === type && idx !== undefined && info.idx === idx) {
|
||||
return ph
|
||||
}
|
||||
|
||||
// Type-only match
|
||||
if (type !== undefined && info.type === type) {
|
||||
if (!typeMatch) typeMatch = ph
|
||||
}
|
||||
|
||||
// idx-only match (for body/content placeholders that may omit type)
|
||||
if (idx !== undefined && info.idx === idx && type === undefined && info.type === undefined) {
|
||||
return ph
|
||||
}
|
||||
}
|
||||
|
||||
// For placeholders without type (defaults to "body"), match by idx
|
||||
if (type === undefined && idx !== undefined) {
|
||||
for (const ph of placeholders) {
|
||||
const info = getPhInfo(ph)
|
||||
if (info.idx === idx) return ph
|
||||
}
|
||||
}
|
||||
|
||||
return typeMatch
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a matching layout placeholder (PlaceholderEntry); use entry.absoluteXfrm when present.
|
||||
*/
|
||||
function findMatchingLayoutPlaceholder(
|
||||
placeholders: PlaceholderEntry[],
|
||||
type?: string,
|
||||
idx?: number
|
||||
): PlaceholderEntry | undefined {
|
||||
let typeMatch: PlaceholderEntry | undefined
|
||||
|
||||
for (const entry of placeholders) {
|
||||
const info = getPhInfo(entry.node)
|
||||
|
||||
if (type !== undefined && info.type === type && idx !== undefined && info.idx === idx) {
|
||||
return entry
|
||||
}
|
||||
if (type !== undefined && info.type === type && !typeMatch) {
|
||||
typeMatch = entry
|
||||
}
|
||||
if (idx !== undefined && info.idx === idx && type === undefined && info.type === undefined) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
if (type === undefined && idx !== undefined) {
|
||||
for (const entry of placeholders) {
|
||||
if (getPhInfo(entry.node).idx === idx) return entry
|
||||
}
|
||||
}
|
||||
return typeMatch
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk through all slide nodes (including group children recursively)
|
||||
* and fill in missing position/size from layout/master placeholders.
|
||||
*/
|
||||
function resolvePlaceholderInheritance(pres: PresentationData): void {
|
||||
for (let i = 0; i < pres.slides.length; i++) {
|
||||
const slide = pres.slides[i]
|
||||
const layoutPath = pres.slideToLayout.get(i)
|
||||
const layout = layoutPath ? pres.layouts.get(layoutPath) : undefined
|
||||
const masterPath = layoutPath ? pres.layoutToMaster.get(layoutPath) : undefined
|
||||
const master = masterPath ? pres.masters.get(masterPath) : undefined
|
||||
|
||||
resolveNodesPlaceholders(slide.nodes, layout, master)
|
||||
}
|
||||
}
|
||||
|
||||
/** Extract bodyPr from a placeholder shape node (layout or master). */
|
||||
function getPhBodyPr(phNode: SafeXmlNode): SafeXmlNode | undefined {
|
||||
const txBody = phNode.child('txBody')
|
||||
if (!txBody.exists()) return undefined
|
||||
const bodyPr = txBody.child('bodyPr')
|
||||
return bodyPr.exists() ? bodyPr : undefined
|
||||
}
|
||||
|
||||
function resolveNodesPlaceholders(
|
||||
nodes: SlideNode[],
|
||||
layout: LayoutData | undefined,
|
||||
master: MasterData | undefined
|
||||
): void {
|
||||
for (const node of nodes) {
|
||||
// Recursively handle group children
|
||||
if (node.nodeType === 'group' && 'children' in node) {
|
||||
// Group children are raw SafeXmlNode, not parsed yet — skip
|
||||
// (they get parsed during rendering in GroupRenderer)
|
||||
}
|
||||
|
||||
if (!node.placeholder) continue
|
||||
|
||||
const { type, idx } = node.placeholder
|
||||
const sizeIsEmpty = node.size.w === 0 && node.size.h === 0
|
||||
const positionLooksDefault = node.position.y < 5 // y=0 or near top → use layout position
|
||||
|
||||
if (layout) {
|
||||
const layoutMatch = findMatchingLayoutPlaceholder(layout.placeholders, type, idx)
|
||||
if (layoutMatch) {
|
||||
const xfrm = layoutMatch.absoluteXfrm ?? getPhXfrm(layoutMatch.node)
|
||||
if (xfrm) {
|
||||
if (sizeIsEmpty) {
|
||||
node.position = xfrm.position
|
||||
node.size = xfrm.size
|
||||
} else if (positionLooksDefault) {
|
||||
node.position = xfrm.position
|
||||
}
|
||||
}
|
||||
|
||||
// Inherit bodyPr from layout placeholder for text rendering (anchor, insets, etc.)
|
||||
if ('textBody' in node && node.textBody) {
|
||||
const layoutBodyPr = getPhBodyPr(layoutMatch.node)
|
||||
if (layoutBodyPr) {
|
||||
node.textBody.layoutBodyProperties = layoutBodyPr
|
||||
}
|
||||
}
|
||||
|
||||
if (xfrm) continue
|
||||
}
|
||||
}
|
||||
|
||||
if (master) {
|
||||
const match = findMatchingPlaceholder(master.placeholders, type, idx)
|
||||
if (match) {
|
||||
const xfrm = getPhXfrm(match)
|
||||
if (xfrm) {
|
||||
if (sizeIsEmpty) {
|
||||
node.position = xfrm.position
|
||||
node.size = xfrm.size
|
||||
} else if (positionLooksDefault) {
|
||||
node.position = xfrm.position
|
||||
}
|
||||
}
|
||||
|
||||
// Inherit bodyPr from master placeholder as fallback
|
||||
if ('textBody' in node && node.textBody && !node.textBody.layoutBodyProperties) {
|
||||
const masterBodyPr = getPhBodyPr(match)
|
||||
if (masterBodyPr) {
|
||||
node.textBody.layoutBodyProperties = masterBodyPr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user