Spaces:
Sleeping
Sleeping
File size: 8,768 Bytes
7dc28be | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | // src/types.ts
import { z } from 'zod';
import { docs_v1 } from 'googleapis';
// --- Helper function for hex color validation ---
export const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
export const validateHexColor = (color: string) => hexColorRegex.test(color);
// --- Helper function for Hex to RGB conversion ---
export function hexToRgbColor(hex: string): docs_v1.Schema$RgbColor | null {
if (!hex) return null;
let hexClean = hex.startsWith('#') ? hex.slice(1) : hex;
if (hexClean.length === 3) {
hexClean = hexClean[0] + hexClean[0] + hexClean[1] + hexClean[1] + hexClean[2] + hexClean[2];
}
if (hexClean.length !== 6) return null;
const bigint = parseInt(hexClean, 16);
if (isNaN(bigint)) return null;
const r = ((bigint >> 16) & 255) / 255;
const g = ((bigint >> 8) & 255) / 255;
const b = (bigint & 255) / 255;
return { red: r, green: g, blue: b };
}
// --- Zod Schema Fragments for Reusability ---
export const DocumentIdParameter = z.strictObject({
documentId: z
.string()
.describe('The document ID — the long string between /d/ and /edit in a Google Docs URL.'),
});
export const RangeParameters = z
.object({
startIndex: z
.number()
.int()
.min(1)
.describe('The starting index of the text range (inclusive, starts from 1).'),
endIndex: z.number().int().min(1).describe('The ending index of the text range (exclusive).'),
})
.refine((data) => data.endIndex > data.startIndex, {
message: 'endIndex must be greater than startIndex',
path: ['endIndex'],
});
export const OptionalRangeParameters = z
.object({
startIndex: z
.number()
.int()
.min(1)
.optional()
.describe(
'Optional: The starting index of the text range (inclusive, starts from 1). If omitted, might apply to a found element or whole paragraph.'
),
endIndex: z
.number()
.int()
.min(1)
.optional()
.describe(
'Optional: The ending index of the text range (exclusive). If omitted, might apply to a found element or whole paragraph.'
),
})
.refine((data) => !data.startIndex || !data.endIndex || data.endIndex > data.startIndex, {
message:
'If both startIndex and endIndex are provided, endIndex must be greater than startIndex',
path: ['endIndex'],
});
export const TextFindParameter = z.strictObject({
textToFind: z.string().min(1).describe('The exact text string to locate.'),
matchInstance: z
.number()
.int()
.min(1)
.optional()
.default(1)
.describe('Which instance of the text to target (1st, 2nd, etc.). Defaults to 1.'),
});
// --- Style Parameter Schemas ---
export const TextStyleParameters = z
.object({
bold: z.boolean().optional().describe('Apply bold formatting.'),
italic: z.boolean().optional().describe('Apply italic formatting.'),
underline: z.boolean().optional().describe('Apply underline formatting.'),
strikethrough: z.boolean().optional().describe('Apply strikethrough formatting.'),
fontSize: z.number().min(1).optional().describe('Set font size (in points, e.g., 12).'),
fontFamily: z
.string()
.optional()
.describe('Set font family (e.g., "Arial", "Times New Roman").'),
foregroundColor: z
.string()
.refine(validateHexColor, { message: 'Invalid hex color format (e.g., #FF0000 or #F00)' })
.optional()
.describe('Set text color using hex format (e.g., "#FF0000").'),
backgroundColor: z
.string()
.refine(validateHexColor, { message: 'Invalid hex color format (e.g., #00FF00 or #0F0)' })
.optional()
.describe('Set text background color using hex format (e.g., "#FFFF00").'),
linkUrl: z
.string()
.url()
.refine((url) => /^https?:\/\//i.test(url), { message: 'Only http/https URLs are allowed.' })
.optional()
.describe('Make the text a hyperlink pointing to this URL (http or https only).'),
// clearDirectFormatting: z.boolean().optional().describe('If true, attempts to clear all direct text formatting within the range before applying new styles.') // Harder to implement perfectly
})
.describe('Parameters for character-level text formatting.');
// Subset of TextStyle used for passing to helpers
export type TextStyleArgs = z.infer<typeof TextStyleParameters>;
export const ParagraphStyleParameters = z
.object({
alignment: z
.enum(['START', 'END', 'CENTER', 'JUSTIFIED'])
.optional()
.describe('Paragraph alignment. START=left for LTR languages, END=right for LTR languages.'),
indentStart: z.number().min(0).optional().describe('Left indentation in points.'),
indentEnd: z.number().min(0).optional().describe('Right indentation in points.'),
spaceAbove: z.number().min(0).optional().describe('Space before the paragraph in points.'),
spaceBelow: z.number().min(0).optional().describe('Space after the paragraph in points.'),
namedStyleType: z
.enum([
'NORMAL_TEXT',
'TITLE',
'SUBTITLE',
'HEADING_1',
'HEADING_2',
'HEADING_3',
'HEADING_4',
'HEADING_5',
'HEADING_6',
])
.optional()
.describe('Apply a built-in named paragraph style (e.g., HEADING_1).'),
keepWithNext: z
.boolean()
.optional()
.describe('Keep this paragraph together with the next one on the same page.'),
// Borders are more complex, might need separate objects/tools
// clearDirectFormatting: z.boolean().optional().describe('If true, attempts to clear all direct paragraph formatting within the range before applying new styles.') // Harder to implement perfectly
})
.describe('Parameters for paragraph-level formatting.');
// Subset of ParagraphStyle used for passing to helpers
export type ParagraphStyleArgs = z.infer<typeof ParagraphStyleParameters>;
export const SpreadsheetCellValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
export type SpreadsheetCellValue = z.infer<typeof SpreadsheetCellValueSchema>;
// --- Combination Schemas for Tools ---
export const ApplyTextStyleToolParameters = DocumentIdParameter.extend({
// Target EITHER by range OR by finding text
target: z
.union([RangeParameters, TextFindParameter])
.describe('Specify the target range either by start/end indices or by finding specific text.'),
style: TextStyleParameters.refine(
(styleArgs) => Object.values(styleArgs).some((v) => v !== undefined),
{ message: 'At least one text style option must be provided.' }
).describe('The text styling to apply.'),
tabId: z
.string()
.optional()
.describe(
'The ID of the specific tab to apply formatting in. Use listDocumentTabs to get tab IDs. If not specified, operates on the first tab.'
),
});
export type ApplyTextStyleToolArgs = z.infer<typeof ApplyTextStyleToolParameters>;
export const ApplyParagraphStyleToolParameters = DocumentIdParameter.extend({
// Target EITHER by range OR by finding text (tool logic needs to find paragraph boundaries)
target: z
.union([
RangeParameters, // User provides paragraph start/end (less likely)
TextFindParameter, // Find text within paragraph to apply style
z.strictObject({
// Target by specific index within the paragraph
indexWithinParagraph: z
.number()
.int()
.min(1)
.describe('An index located anywhere within the target paragraph.'),
}),
])
.describe(
'Specify the target paragraph either by start/end indices, by finding text within it, or by providing an index within it.'
),
style: ParagraphStyleParameters.refine(
(styleArgs) => Object.values(styleArgs).some((v) => v !== undefined),
{ message: 'At least one paragraph style option must be provided.' }
).describe('The paragraph styling to apply.'),
tabId: z
.string()
.optional()
.describe(
'The ID of the specific tab to apply formatting in. Use listDocumentTabs to get tab IDs. If not specified, operates on the first tab.'
),
});
export type ApplyParagraphStyleToolArgs = z.infer<typeof ApplyParagraphStyleToolParameters>;
// --- Error Classes ---
// Use FastMCP's UserError for client-facing issues
// Define custom errors for internal issues if needed
export class NotImplementedError extends Error {
constructor(message = 'This feature is not yet implemented.') {
super(message);
this.name = 'NotImplementedError';
}
}
export class MarkdownConversionError extends Error {
constructor(
message: string,
public markdownPosition?: number,
public tokenType?: string
) {
super(message);
this.name = 'MarkdownConversionError';
}
}
|