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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,169 @@
/**
* 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,
}
}