← Files Google DriveARCHIVED FILE

skills/google-docs/runtime/dropdown-inventory.mjs

10.3 KB · Sep 30, 2026 · 23:18 UTC

↓ Download file

import {
  assert,
  collectRecords,
  findFirstRecord,
  isRecord,
  requireArray,
  requireInteger,
  requireRecord,
  requireString,
  sha256Json,
} from "./dropdown-common.mjs";

export const DROPDOWN_INVENTORY_VERSION = 1;

function unwrap(value) {
  if (isRecord(value?.structuredContent)) return value.structuredContent;
  if (isRecord(value?.result)) return value.result;
  return value;
}

function documentIdentity(raw) {
  const root = unwrap(raw);
  const document = findFirstRecord(root, (candidate) =>
    typeof (candidate.documentId ?? candidate.document_id) === "string" &&
    (candidate.revisionId || candidate.revision_id || candidate.body || candidate.tabs),
  );
  assert(document, "Could not locate a Google Docs document resource");
  return {
    id: document.documentId ?? document.document_id,
    title: document.title ?? null,
    revisionId: document.revisionId ?? document.revision_id ?? null,
    url: document.documentUrl ?? document.document_url ?? document.url ?? null,
  };
}

function isPrivateUse(codePoint) {
  return (codePoint >= 0xE000 && codePoint <= 0xF8FF) ||
    (codePoint >= 0xF0000 && codePoint <= 0xFFFFD) ||
    (codePoint >= 0x100000 && codePoint <= 0x10FFFD);
}

export function privateUseMarkerSpans(value) {
  const text = String(value ?? "");
  const spans = [];
  let current = null;
  for (let offset = 0; offset < text.length;) {
    const codePoint = text.codePointAt(offset);
    const character = String.fromCodePoint(codePoint);
    if (isPrivateUse(codePoint)) {
      if (!current) current = { startOffset: offset, endOffset: offset, text: "", codePoints: [] };
      current.endOffset = offset + character.length;
      current.text += character;
      current.codePoints.push(`U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`);
    } else if (current) {
      spans.push(current);
      current = null;
    }
    offset += character.length;
  }
  if (current) spans.push(current);
  return spans;
}

function semanticBinding({ type, fidelityClass, locator, container = null, semanticProperties, displayText = "", anchor = {} }) {
  const semanticPropertiesHash = sha256Json(semanticProperties);
  const anchorHash = sha256Json({
    prefixText: String(anchor.prefixText ?? "").slice(-96),
    suffixText: String(anchor.suffixText ?? "").slice(0, 96),
    container,
  });
  return {
    bindingId: sha256Json({ type, locator, semanticPropertiesHash, anchorHash }),
    type,
    fidelityClass,
    locator,
    container,
    displayText,
    semanticProperties,
    semanticPropertiesHash,
    anchorHash,
  };
}

function normalizeOptions(value, path) {
  return requireArray(value ?? [], path).map((option, index) => {
    const item = requireRecord(option, `${path}[${index}]`);
    return {
      label: requireString(item.label ?? item.text ?? item.value, `${path}[${index}].label`),
      ...(item.color ?? item.colorStyle ?? item.color_style ? { color: item.color ?? item.colorStyle ?? item.color_style } : {}),
      ...(item.id ?? item.optionId ?? item.option_id ? { id: item.id ?? item.optionId ?? item.option_id } : {}),
    };
  });
}

function dropdownRecords(raw) {
  const root = unwrap(raw);
  const containers = collectRecords(root, (candidate) => Array.isArray(candidate.dropdowns));
  const values = containers.flatMap((container) => container.dropdowns);
  if (Array.isArray(root)) values.push(...root);
  return values.filter((candidate) => isRecord(candidate) && (
    candidate.dropdownId || candidate.dropdown_id || candidate.options || candidate.optionItems || candidate.option_items
  ));
}

function dropdownResponseIdentity(raw) {
  const root = unwrap(raw);
  const record = findFirstRecord(root, (candidate) =>
    typeof (candidate.documentId ?? candidate.document_id) === "string" &&
    typeof (candidate.revisionId ?? candidate.revision_id) === "string" &&
    Array.isArray(candidate.dropdowns),
  );
  assert(record, "Dropdown metadata must include document identity, revision, and dropdown records");
  return {
    documentId: record.documentId ?? record.document_id,
    revisionId: record.revisionId ?? record.revision_id,
    tabId: record.tabId ?? record.tab_id ?? null,
  };
}

function normalizeDropdown(value, index, fallbackTabId = null) {
  const path = `dropdowns[${index}]`;
  const dropdown = requireRecord(value, path);
  const range = dropdown.range ?? dropdown.locator ?? {};
  const startIndex = requireInteger(range.startIndex ?? range.start_index ?? dropdown.startIndex ?? dropdown.start_index, `${path}.startIndex`, { minimum: 0 });
  const endIndex = requireInteger(range.endIndex ?? range.end_index ?? dropdown.endIndex ?? dropdown.end_index, `${path}.endIndex`, { minimum: startIndex + 1 });
  const tabId = range.tabId ?? range.tab_id ?? dropdown.tabId ?? dropdown.tab_id ?? fallbackTabId;
  const options = normalizeOptions(dropdown.options ?? dropdown.optionItems ?? dropdown.option_items, `${path}.options`);
  const selectedValue = dropdown.selectedValue ?? dropdown.selected_value ?? dropdown.value ?? null;
  assert(options.length > 0, "must not be empty", `${path}.options`);
  assert(new Set(options.map((option) => option.label)).size === options.length, "labels must be unique", `${path}.options`);
  if (selectedValue !== null) assert(options.some((option) => option.label === selectedValue), "must name an available option", `${path}.selectedValue`);
  const dropdownId = dropdown.dropdownId ?? dropdown.dropdown_id ?? null;
  if (dropdownId !== null) requireString(dropdownId, `${path}.dropdownId`);
  const providerSignature = dropdown.signature ?? dropdown.semanticSignature ?? dropdown.semantic_signature ?? null;
  assert(dropdownId !== null || (typeof providerSignature === "string" && providerSignature.length > 0), "requires a provider ID or stable semantic signature", path);
  const semanticProperties = {
    dropdownId,
    options,
    selectedValue,
    signature: providerSignature ?? sha256Json({ dropdownId, options, selectedValue }),
  };
  const container = dropdown.container ?? dropdown.tableCell ?? dropdown.table_cell ?? null;
  return semanticBinding({
    type: "dropdownControl",
    fidelityClass: "exact-writable",
    locator: { tabId: tabId ?? null, segmentId: range.segmentId ?? range.segment_id ?? null, startIndex, endIndex },
    container,
    semanticProperties,
    displayText: String(dropdown.displayText ?? dropdown.display_text ?? selectedValue ?? ""),
    anchor: dropdown.anchor ?? {},
  });
}

function collectOpaqueControls(rawDocument, exactDropdowns) {
  const root = unwrap(rawDocument);
  const output = [];
  const seen = new Set();
  const visit = (value, context = { tabId: null, segmentId: null, container: null, prefixText: "", suffixText: "" }) => {
    if (!value || typeof value !== "object" || seen.has(value)) return;
    seen.add(value);
    if (Array.isArray(value)) {
      value.forEach((child) => visit(child, context));
      return;
    }
    const tabId = value.tabProperties?.tabId ?? value.tab_properties?.tab_id ?? value.tabId ?? value.tab_id ?? context.tabId;
    const segmentId = value.segmentId ?? value.segment_id ?? context.segmentId;
    if (typeof value.textRun?.content === "string" && Number.isInteger(value.startIndex ?? value.start_index)) {
      const base = value.startIndex ?? value.start_index;
      for (const marker of privateUseMarkerSpans(value.textRun.content)) {
        const locator = { tabId: tabId ?? null, segmentId: segmentId ?? null, startIndex: base + marker.startOffset, endIndex: base + marker.endOffset };
        const identified = exactDropdowns.some((binding) =>
          binding.locator.tabId === locator.tabId && binding.locator.segmentId === locator.segmentId &&
          binding.locator.startIndex === locator.startIndex && binding.locator.endIndex === locator.endIndex,
        );
        if (identified) continue;
        output.push(semanticBinding({
          type: "opaqueTemplateControl",
          fidelityClass: "copy-only",
          locator,
          container: context.container,
          semanticProperties: {
            controlClass: "private-use-glyph",
            markerText: marker.text,
            codePoints: marker.codePoints,
            textStyle: value.textRun.textStyle ?? {},
          },
          displayText: marker.text,
          anchor: context,
        }));
      }
    }
    for (const [key, child] of Object.entries(value)) {
      if (key === "textRun") continue;
      visit(child, { ...context, tabId, segmentId });
    }
  };
  visit(root);
  return output;
}

export function buildDropdownInventory({ documentResult, dropdownResult, documentId = null, tabId = null } = {}) {
  const document = documentIdentity(documentResult);
  if (documentId) assert(document.id === documentId, "Document identity does not match requested document");
  const dropdownIdentity = dropdownResponseIdentity(dropdownResult);
  assert(dropdownIdentity.documentId === document.id, "Dropdown metadata belongs to a different document");
  assert(dropdownIdentity.revisionId === document.revisionId, "Document and dropdown metadata revisions do not match");
  if (tabId !== null) assert(dropdownIdentity.tabId === null || dropdownIdentity.tabId === tabId, "Dropdown metadata belongs to a different tab");
  const dropdowns = dropdownRecords(dropdownResult).map((value, index) => normalizeDropdown(value, index, tabId));
  const opaqueControls = collectOpaqueControls(documentResult, dropdowns);
  const semanticElements = [...dropdowns, ...opaqueControls].sort((left, right) =>
    String(left.locator.tabId).localeCompare(String(right.locator.tabId)) || left.locator.startIndex - right.locator.startIndex,
  );
  const base = {
    inventoryVersion: DROPDOWN_INVENTORY_VERSION,
    document,
    selectedTabId: tabId ?? null,
    dropdowns,
    opaqueControls,
    semanticElements,
  };
  return { ...base, sha256: sha256Json(base) };
}

export function verifyDropdownInventory(value) {
  const inventory = requireRecord(value, "inventory");
  assert(inventory.inventoryVersion === DROPDOWN_INVENTORY_VERSION, `must use version ${DROPDOWN_INVENTORY_VERSION}`, "inventory.inventoryVersion");
  requireString(inventory.document?.id, "inventory.document.id");
  requireString(inventory.document?.revisionId, "inventory.document.revisionId");
  requireArray(inventory.semanticElements, "inventory.semanticElements");
  const { sha256, ...base } = inventory;
  assert(sha256 === sha256Json(base), "sha256 does not match inventory content", "inventory.sha256");
  return inventory;
}

SHA-256: 5d4229fc4e37c21921a9e749c07239a9dab36e00c4f6b57baaa8466af4130043