d25d482dc2
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
170 lines
4.4 KiB
TypeScript
170 lines
4.4 KiB
TypeScript
/**
|
|
* Base node types and property parser shared by all slide node kinds.
|
|
*/
|
|
|
|
import { angleToDeg, emuToPx } from '../../parser/units'
|
|
import type { SafeXmlNode } from '../../parser/xml-parser'
|
|
|
|
export type NodeType = 'shape' | 'picture' | 'table' | 'group' | 'chart' | 'unknown'
|
|
|
|
export interface Position {
|
|
x: number
|
|
y: number
|
|
}
|
|
|
|
export interface Size {
|
|
w: number
|
|
h: number
|
|
}
|
|
|
|
export interface PlaceholderInfo {
|
|
type?: string
|
|
idx?: number
|
|
}
|
|
|
|
/** Shape-level hyperlink click action (from cNvPr > a:hlinkClick). */
|
|
interface HlinkAction {
|
|
/** Action URI, e.g. "ppaction://hlinksldjump", "ppaction://hlinkpres", or empty for URL links. */
|
|
action?: string
|
|
/** Relationship ID for the target (slide, URL, etc.). */
|
|
rId?: string
|
|
/** Optional tooltip text. */
|
|
tooltip?: string
|
|
}
|
|
|
|
export interface BaseNodeData {
|
|
id: string
|
|
name: string
|
|
nodeType: NodeType
|
|
position: Position
|
|
size: Size
|
|
rotation: number
|
|
flipH: boolean
|
|
flipV: boolean
|
|
placeholder?: PlaceholderInfo
|
|
/** Shape-level hyperlink/click action (action buttons, clickable shapes). */
|
|
hlinkClick?: HlinkAction
|
|
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
|
|
source: SafeXmlNode
|
|
}
|
|
|
|
/**
|
|
* Try to find the non-visual properties container in the given node.
|
|
* PPTX uses different wrapper names depending on the shape kind:
|
|
* p:nvSpPr (shapes/connectors), p:nvPicPr (pictures),
|
|
* p:nvGrpSpPr (groups), p:nvGraphicFramePr (tables/charts).
|
|
*/
|
|
function findNvProps(node: SafeXmlNode): { cNvPr: SafeXmlNode; nvPr: SafeXmlNode } {
|
|
const wrappers = ['nvSpPr', 'nvPicPr', 'nvGrpSpPr', 'nvGraphicFramePr', 'nvCxnSpPr']
|
|
for (const name of wrappers) {
|
|
const wrapper = node.child(name)
|
|
if (wrapper.exists()) {
|
|
return {
|
|
cNvPr: wrapper.child('cNvPr'),
|
|
nvPr: wrapper.child('nvPr'),
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
cNvPr: node.child('cNvPr'),
|
|
nvPr: node.child('nvPr'),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the transform (xfrm) node. Shapes use `p:spPr > a:xfrm`,
|
|
* groups use `p:grpSpPr > a:xfrm`, graphic frames use `p:xfrm`.
|
|
*/
|
|
function findXfrm(node: SafeXmlNode): SafeXmlNode {
|
|
// Try spPr first (most shapes)
|
|
const spPr = node.child('spPr')
|
|
if (spPr.exists()) {
|
|
const xfrm = spPr.child('xfrm')
|
|
if (xfrm.exists()) return xfrm
|
|
}
|
|
|
|
// Try grpSpPr (groups)
|
|
const grpSpPr = node.child('grpSpPr')
|
|
if (grpSpPr.exists()) {
|
|
const xfrm = grpSpPr.child('xfrm')
|
|
if (xfrm.exists()) return xfrm
|
|
}
|
|
|
|
// Try direct xfrm (graphic frames)
|
|
const directXfrm = node.child('xfrm')
|
|
if (directXfrm.exists()) return directXfrm
|
|
|
|
// Return empty node — all reads will return defaults
|
|
return node.child('__nonexistent__')
|
|
}
|
|
|
|
/**
|
|
* Parse placeholder info from nvPr > p:ph.
|
|
*/
|
|
function parsePlaceholder(nvPr: SafeXmlNode): PlaceholderInfo | undefined {
|
|
const ph = nvPr.child('ph')
|
|
if (!ph.exists()) return undefined
|
|
|
|
const type = ph.attr('type')
|
|
const idx = ph.numAttr('idx')
|
|
|
|
return { type, idx }
|
|
}
|
|
|
|
/**
|
|
* Parse the base properties common to all node types from a shape-like XML node.
|
|
* Returns everything except `nodeType`, which the caller must set.
|
|
*/
|
|
export function parseBaseProps(spNode: SafeXmlNode): Omit<BaseNodeData, 'nodeType'> {
|
|
const { cNvPr, nvPr } = findNvProps(spNode)
|
|
|
|
const id = cNvPr.attr('id') ?? ''
|
|
const name = cNvPr.attr('name') ?? ''
|
|
|
|
// --- Transform ---
|
|
const xfrm = findXfrm(spNode)
|
|
const off = xfrm.child('off')
|
|
const ext = xfrm.child('ext')
|
|
|
|
const position: Position = {
|
|
x: emuToPx(off.numAttr('x') ?? 0),
|
|
y: emuToPx(off.numAttr('y') ?? 0),
|
|
}
|
|
|
|
const size: Size = {
|
|
w: emuToPx(ext.numAttr('cx') ?? 0),
|
|
h: emuToPx(ext.numAttr('cy') ?? 0),
|
|
}
|
|
|
|
const rotation = angleToDeg(xfrm.numAttr('rot') ?? 0)
|
|
const flipH = xfrm.attr('flipH') === '1' || xfrm.attr('flipH') === 'true'
|
|
const flipV = xfrm.attr('flipV') === '1' || xfrm.attr('flipV') === 'true'
|
|
|
|
// --- Placeholder ---
|
|
const placeholder = parsePlaceholder(nvPr)
|
|
|
|
// --- Shape-level hyperlink action (cNvPr > a:hlinkClick) ---
|
|
let hlinkClick: HlinkAction | undefined
|
|
const hlinkNode = cNvPr.child('hlinkClick')
|
|
if (hlinkNode.exists()) {
|
|
hlinkClick = {
|
|
action: hlinkNode.attr('action') ?? undefined,
|
|
rId: hlinkNode.attr('id') ?? hlinkNode.attr('r:id') ?? undefined,
|
|
tooltip: hlinkNode.attr('tooltip') ?? undefined,
|
|
}
|
|
}
|
|
|
|
return {
|
|
id,
|
|
name,
|
|
position,
|
|
size,
|
|
rotation,
|
|
flipH,
|
|
flipV,
|
|
placeholder,
|
|
hlinkClick,
|
|
source: spNode,
|
|
}
|
|
}
|