← Files Google DriveARCHIVED FILE
skills/google-slides/runtime/normalize.mjs
14.3 KB · Oct 4, 2026 · 06:02 UTC
import {
assert,
findFirstRecord,
firstDefined,
isRecord,
redactSensitive,
sha256Json,
uniqueSorted,
} from "./common.mjs";
import { buildDesignSystem } from "./design-system.mjs";
import { unwrapCallToolResult } from "./result.mjs";
const EMU_PER_POINT = 12700;
export function dimensionToPoints(dimension) {
if (dimension === null || dimension === undefined) return null;
if (typeof dimension === "number") return dimension;
if (!isRecord(dimension)) return null;
const magnitude = Number(dimension.magnitude);
if (!Number.isFinite(magnitude)) return null;
const unit = String(dimension.unit ?? "PT").toUpperCase();
if (unit === "PT" || unit === "POINT" || unit === "POINTS") return magnitude;
if (unit === "EMU") return magnitude / EMU_PER_POINT;
if (unit === "IN" || unit === "INCH" || unit === "INCHES") return magnitude * 72;
if (unit === "PX") return magnitude * 0.75;
return null;
}
function textFromElements(textElements) {
if (!Array.isArray(textElements)) return "";
return textElements
.map((element) => element?.textRun?.content ?? element?.autoText?.content ?? "")
.join("");
}
function normalizeText(text) {
if (!isRecord(text)) return null;
const elements = Array.isArray(text.textElements) ? text.textElements : [];
const runs = [];
const paragraphs = [];
const styleDefaultsByNesting = {};
const stylePriorityByNesting = {};
let cursor = 0;
let nestingLevel = 0;
for (const element of elements) {
const content = element?.textRun?.content ?? element?.autoText?.content ?? "";
const startIndex = Number.isInteger(element?.startIndex) ? element.startIndex : cursor;
const endIndex = Number.isInteger(element?.endIndex) ? element.endIndex : startIndex + content.length;
if (element?.paragraphMarker) {
nestingLevel = Number(element.paragraphMarker.bullet?.nestingLevel ?? 0);
paragraphs.push({
startIndex,
endIndex,
style: element.paragraphMarker.style ?? null,
bullet: element.paragraphMarker.bullet ?? null,
});
}
if (element?.textRun || element?.autoText) {
const style = element.textRun?.style ?? element.autoText?.style ?? null;
runs.push({
startIndex,
endIndex,
content,
style,
kind: element.textRun ? "textRun" : "autoText",
autoTextType: element.autoText?.type ?? null,
nestingLevel,
});
if (isRecord(style)) {
const key = String(nestingLevel);
const priority = content.includes("\n") ? 2 : 1;
if ((stylePriorityByNesting[key] ?? 0) <= priority) {
styleDefaultsByNesting[key] = style;
stylePriorityByNesting[key] = priority;
}
}
}
cursor = Math.max(cursor, endIndex);
}
return {
plainText: textFromElements(elements),
runs,
paragraphs,
lists: text.lists ?? null,
styleDefaultsByNesting,
};
}
function normalizeTable(table) {
if (!isRecord(table)) return null;
const rows = Array.isArray(table.tableRows) ? table.tableRows : [];
return {
rows: Number(table.rows ?? rows.length),
columns: Number(table.columns ?? rows[0]?.tableCells?.length ?? 0),
cells: rows.map((row, rowIndex) =>
(row.tableCells ?? []).map((cell, columnIndex) => ({
rowIndex,
columnIndex,
text: normalizeText(cell.text),
rowSpan: cell.rowSpan ?? 1,
columnSpan: cell.columnSpan ?? 1,
properties: cell.tableCellProperties ?? null,
})),
),
};
}
function elementKind(element) {
for (const key of [
"shape",
"image",
"table",
"line",
"wordArt",
"sheetsChart",
"video",
"speakerSpotlight",
"elementGroup",
]) {
if (element?.[key] !== undefined) return key;
}
return "unknown";
}
function affineMatrix(transform = {}) {
const translateX = dimensionToPoints({ magnitude: transform.translateX ?? 0, unit: transform.unit ?? "PT" });
const translateY = dimensionToPoints({ magnitude: transform.translateY ?? 0, unit: transform.unit ?? "PT" });
const values = [transform.scaleX ?? 1, transform.scaleY ?? 1, transform.shearX ?? 0, transform.shearY ?? 0].map(Number);
if (translateX === null || translateY === null || values.some((value) => !Number.isFinite(value))) return null;
const [scaleX, scaleY, shearX, shearY] = values;
return { a: scaleX, b: shearY, c: shearX, d: scaleY, e: translateX, f: translateY };
}
function multiplyAffine(left, right) {
return {
a: left.a * right.a + left.c * right.b,
b: left.b * right.a + left.d * right.b,
c: left.a * right.c + left.c * right.d,
d: left.b * right.c + left.d * right.d,
e: left.a * right.e + left.c * right.f + left.e,
f: left.b * right.e + left.d * right.f + left.f,
};
}
function normalizeGeometry(element, parentMatrix = null) {
const size = element?.size ?? {};
const transform = element?.transform ?? {};
const width = dimensionToPoints(size.width);
const height = dimensionToPoints(size.height);
const localMatrix = affineMatrix(transform);
const absoluteMatrix = parentMatrix && localMatrix ? multiplyAffine(parentMatrix, localMatrix) : localMatrix;
const serializedMatrix = absoluteMatrix
? {
scaleX: absoluteMatrix.a,
shearY: absoluteMatrix.b,
shearX: absoluteMatrix.c,
scaleY: absoluteMatrix.d,
translateX: absoluteMatrix.e,
translateY: absoluteMatrix.f,
unit: "PT",
}
: null;
if (width === null || height === null || !absoluteMatrix) {
return { boundsPt: null, absoluteMatrix, absoluteTransform: serializedMatrix };
}
const points = [
[0, 0],
[width, 0],
[0, height],
[width, height],
].map(([x, y]) => ({
x: absoluteMatrix.a * x + absoluteMatrix.c * y + absoluteMatrix.e,
y: absoluteMatrix.b * x + absoluteMatrix.d * y + absoluteMatrix.f,
}));
const xs = points.map((point) => point.x);
const ys = points.map((point) => point.y);
return {
boundsPt: {
x: Math.min(...xs),
y: Math.min(...ys),
width: Math.max(...xs) - Math.min(...xs),
height: Math.max(...ys) - Math.min(...ys),
rotatedOrSkewed: Boolean(absoluteMatrix.b || absoluteMatrix.c || absoluteMatrix.a < 0 || absoluteMatrix.d < 0),
},
absoluteMatrix,
absoluteTransform: serializedMatrix,
};
}
function normalizeElement(element, pageId, parentMatrix = null, groupChild = false) {
const kind = elementKind(element);
const shape = element?.shape ?? null;
const objectId = element?.objectId ?? element?.id ?? null;
const geometry = normalizeGeometry(element, parentMatrix);
return {
objectId,
pageId,
kind,
placeholderType: shape?.placeholder?.type ?? null,
placeholder: shape?.placeholder
? {
type: shape.placeholder.type ?? null,
index: Number.isInteger(shape.placeholder.index) ? shape.placeholder.index : 0,
parentObjectId: shape.placeholder.parentObjectId ?? null,
}
: null,
shapeType: shape?.shapeType ?? null,
shapeProperties: shape?.shapeProperties ?? null,
boundsPt: geometry.boundsPt,
transform: element?.transform ?? null,
absoluteTransform: geometry.absoluteTransform,
groupChild,
size: element?.size ?? null,
text: normalizeText(shape?.text),
table: normalizeTable(element?.table),
wordArt: element?.wordArt
? { renderedText: element.wordArt.renderedText ?? "" }
: null,
image: element?.image
? {
contentUrl: element.image.contentUrl ? "[EPHEMERAL_URL]" : null,
sourceUrl: element.image.sourceUrl ?? null,
properties: element.image.imageProperties ?? null,
}
: null,
sheetsChart: element?.sheetsChart ?? null,
video: element?.video ?? null,
speakerSpotlight: element?.speakerSpotlight ?? null,
line: element?.line ?? null,
group: element?.elementGroup
? {
children: (element.elementGroup.children ?? []).map((child) =>
normalizeElement(child, pageId, geometry.absoluteMatrix, true)),
}
: null,
description: element?.description ?? null,
title: element?.title ?? null,
};
}
function normalizeNotes(slide) {
const notesPage = slide?.notesPage ?? slide?.slideProperties?.notesPage;
if (!isRecord(notesPage)) return { objectId: null, text: "", elements: [] };
const notesObjectId = notesPage?.notesProperties?.speakerNotesObjectId ?? null;
const elements = (notesPage.pageElements ?? []).map((element) => normalizeElement(element, notesPage.objectId));
const notesElement = elements.find((element) => element.objectId === notesObjectId);
return {
objectId: notesObjectId,
text: notesElement?.text?.plainText ?? "",
elements,
};
}
function normalizePage(page, index, kind) {
const objectId = page?.objectId ?? page?.id ?? null;
const properties =
kind === "slide"
? page?.slideProperties
: kind === "layout"
? page?.layoutProperties
: page?.masterProperties;
return {
objectId,
index,
kind,
layoutId: page?.slideProperties?.layoutObjectId ?? null,
masterId: properties?.masterObjectId ?? null,
name: properties?.name ?? properties?.displayName ?? null,
displayName: properties?.displayName ?? properties?.name ?? null,
revisionId: page?.revisionId ?? null,
isSkipped: kind === "slide" ? page?.slideProperties?.isSkipped === true : false,
pageProperties: page?.pageProperties ?? null,
elements: (page?.pageElements ?? []).map((element) => normalizeElement(element, objectId)),
notes: kind === "slide" ? normalizeNotes(page) : null,
properties: properties ?? null,
};
}
function locatePresentation(value) {
const directCandidates = [
value?.presentation,
value?.result?.presentation,
value?.data?.presentation,
value?.result,
value?.data,
value,
];
for (const candidate of directCandidates) {
if (isRecord(candidate) && (Array.isArray(candidate.slides) || candidate.presentationId || candidate.pageSize)) {
return candidate;
}
}
return findFirstRecord(
value,
(candidate) => Array.isArray(candidate.slides) && (candidate.presentationId || candidate.title || candidate.pageSize),
);
}
function indexPages(pages) {
const objectIndex = {};
const visitElement = (element, pageId) => {
if (element.objectId) objectIndex[element.objectId] = { pageId, kind: element.kind };
for (const child of element.group?.children ?? []) visitElement(child, pageId);
};
for (const page of pages) {
if (page.objectId) objectIndex[page.objectId] = { pageId: page.objectId, kind: page.kind };
for (const element of page.elements) visitElement(element, page.objectId);
for (const element of page.notes?.elements ?? []) {
visitElement({ ...element, kind: "notes" }, page.objectId);
}
}
return objectIndex;
}
export function normalizePresentation(raw, { presentationId = null } = {}) {
const unwrapped = unwrapCallToolResult(raw);
const presentation = locatePresentation(unwrapped);
assert(presentation, "Could not locate a presentation object in the connector result");
const id = firstDefined(presentation, [
["presentationId"],
["presentation_id"],
["id"],
]) ?? presentationId;
const title = firstDefined(presentation, [["title"], ["name"]]) ?? null;
const revisionId = firstDefined(presentation, [
["revisionId"],
["revision_id"],
["writeControl", "requiredRevisionId"],
["write_control", "requiredRevisionId"],
["metadata", "revisionId"],
]) ?? firstDefined(unwrapped, [
["revisionId"],
["revision_id"],
["writeControl", "requiredRevisionId"],
["metadata", "revisionId"],
]) ?? null;
const slides = (presentation.slides ?? []).map((slide, index) => normalizePage(slide, index, "slide"));
const layouts = (presentation.layouts ?? []).map((layout, index) => normalizePage(layout, index, "layout"));
const masters = (presentation.masters ?? []).map((master, index) => normalizePage(master, index, "master"));
const pages = [...slides, ...layouts, ...masters];
const pageSize = presentation.pageSize ?? {};
const payload = {
snapshotVersion: 2,
presentation: {
id,
title,
revisionId,
locale: presentation.locale ?? null,
pageSizePt: {
width: dimensionToPoints(pageSize.width),
height: dimensionToPoints(pageSize.height),
},
slideCount: slides.length,
slideIds: slides.map((slide) => slide.objectId),
},
slides,
layouts,
masters,
objectIndex: indexPages(pages),
rawPresentation: redactSensitive(presentation),
};
payload.designSystem = buildDesignSystem(payload);
return { ...payload, sha256: sha256Json(payload) };
}
function oneLine(value) {
return String(value ?? "").replace(/\s+/g, " ").trim();
}
export function renderSlideInventory(snapshot) {
const lines = [
"# Slide Inventory",
"",
`- Presentation ID: \`${snapshot.presentation.id ?? "missing"}\``,
`- Title: ${snapshot.presentation.title ?? "(missing)"}`,
`- Revision ID: \`${snapshot.presentation.revisionId ?? "missing"}\``,
`- Snapshot SHA-256: \`${snapshot.sha256}\``,
`- Slide count: ${snapshot.slides.length}`,
"",
"| # | Slide ID | Layout ID | Master ID | Skipped | Elements | Title/Text Preview |",
"| ---: | --- | --- | --- | --- | ---: | --- |",
];
for (const slide of snapshot.slides) {
const flatten = (elements) => elements.flatMap((element) => [element, ...flatten(element.group?.children ?? [])]);
const elements = flatten(slide.elements);
const texts = elements
.flatMap((element) => [
element.text?.plainText,
element.wordArt?.renderedText,
...(element.table?.cells ?? []).flatMap((row) => (row ?? []).map((cell) => cell.text?.plainText)),
])
.filter(Boolean)
.map(oneLine);
const preview = texts.join(" / ").slice(0, 180).replace(/\|/g, "\\|");
lines.push(
`| ${slide.index + 1} | \`${slide.objectId ?? "missing"}\` | \`${slide.layoutId ?? ""}\` | \`${slide.masterId ?? ""}\` | ${slide.isSkipped ? "yes" : "no"} | ${elements.length} | ${preview} |`,
);
}
const kinds = uniqueSorted(
snapshot.slides.flatMap((slide) => {
const flatten = (elements) => elements.flatMap((element) => [element, ...flatten(element.group?.children ?? [])]);
return flatten(slide.elements).map((element) => element.kind);
}),
);
lines.push("", `Element kinds: ${kinds.join(", ") || "none"}`, "");
return lines.join("\n");
}
SHA-256: 3b7f5267f61335410b5aeccca75271b7a810e4306ef9edc5e28a3b7be2d0d8fa