← Files Google DriveARCHIVED FILE

skills/google-docs/scripts/canonicalize_google_workspace_url.mjs

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

↓ Download file

#!/usr/bin/env node

import { pathToFileURL } from "node:url";

const RESOURCE_TYPES = new Map([
  ["document", "document"],
  ["spreadsheets", "spreadsheet"],
  ["presentation", "presentation"],
]);

const LOCATION_KEYS = new Set([
  "bookmark",
  "gid",
  "heading",
  "range",
  "slide",
  "tab",
]);

function parseUrl(value) {
  if (typeof value !== "string" || value.trim() === "") {
    throw new TypeError("A non-empty Google Workspace URL is required.");
  }
  let parsed;
  try {
    parsed = new URL(value.trim());
  } catch {
    throw new TypeError("The supplied value is not a valid absolute URL.");
  }
  if (/(^|\.)google\.com$/i.test(parsed.hostname) && parsed.pathname === "/url") {
    const target = parsed.searchParams.get("q") ?? parsed.searchParams.get("url");
    if (!target) throw new TypeError("Google redirect URL does not contain a target.");
    try {
      parsed = new URL(target);
    } catch {
      throw new TypeError("Google redirect target is not a valid absolute URL.");
    }
  }
  return parsed;
}

function hasLocationIntent(url) {
  for (const key of url.searchParams.keys()) {
    if (LOCATION_KEYS.has(key.toLowerCase())) return true;
  }
  let hash = url.hash.slice(1);
  try {
    hash = decodeURIComponent(hash);
  } catch {
    // Retain the original hash when it contains malformed escaping.
  }
  return /(?:^|[?&#])(bookmark|gid|heading|range|slide|tab)=/i.test(hash);
}

export function canonicalizeGoogleWorkspaceUrl(value) {
  const parsed = parseUrl(value);
  if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "docs.google.com") {
    throw new TypeError("URL is not a supported Google Docs, Sheets, or Slides URL.");
  }

  const segments = parsed.pathname.split("/").filter(Boolean);
  const resourceSegment = segments[0];
  const resourceType = RESOURCE_TYPES.get(resourceSegment);
  const dIndex = segments.indexOf("d");
  const fileId = dIndex >= 0 ? segments[dIndex + 1] : null;
  if (!resourceType || !fileId || !/^[A-Za-z0-9_-]+$/.test(fileId)) {
    throw new TypeError("URL does not contain a supported Google Workspace file ID.");
  }

  const canonicalUri = `https://docs.google.com/${resourceSegment}/d/${fileId}/edit`;
  const unwrappedUri = parsed.toString();
  const locationSpecific = hasLocationIntent(parsed);

  return {
    kind: "google-workspace-rich-link-target",
    resourceType,
    fileId,
    canonicalUri,
    deepLinkUri: locationSpecific ? unwrappedUri : null,
    originalUri: value.trim(),
    unwrappedUri,
  };
}

function main(args) {
  if (args.length === 0) {
    throw new TypeError("Usage: canonicalize_google_workspace_url.mjs <url> [url ...]");
  }
  const results = args.map(canonicalizeGoogleWorkspaceUrl);
  process.stdout.write(`${JSON.stringify(results.length === 1 ? results[0] : results, null, 2)}\n`);
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  try {
    main(process.argv.slice(2));
  } catch (error) {
    process.stderr.write(`${error.message}\n`);
    process.exitCode = 1;
  }
}

SHA-256: e439d12ad25f60049c15d8bbfabb9e29fa6b3233c42e653006049127414c2521