// Google Docs API Service
import { google } from 'googleapis';
import type { OAuth2Client } from 'google-auth-library';
import type { docs_v1 } from 'googleapis';
import { DriveService } from './drive.js';
export class DocsService {
private docs: docs_v1.Docs;
private driveService: DriveService;
constructor(authClient: OAuth2Client, driveService: DriveService) {
this.docs = google.docs({ version: 'v1', auth: authClient });
this.driveService = driveService;
}
/**
* Create a new Google Doc with content
*/
async createDoc(
name: string,
content: string,
parentFolder?: string
): Promise<{ id: string; name: string; webViewLink: string }> {
// Create empty doc in Drive
const file = await this.driveService.createGoogleWorkspaceFile(
name,
'application/vnd.google-apps.document',
parentFolder
);
// Insert content
if (content) {
await this.docs.documents.batchUpdate({
documentId: file.id!,
requestBody: {
requests: [
{
insertText: { location: { index: 1 }, text: content }
},
{
updateParagraphStyle: {
range: { startIndex: 1, endIndex: content.length + 1 },
paragraphStyle: { namedStyleType: 'NORMAL_TEXT' },
fields: 'namedStyleType'
}
}
]
}
});
}
return {
id: file.id!,
name: file.name!,
webViewLink: file.webViewLink!
};
}
/**
* Update content of an existing Google Doc (replaces all content)
*/
async updateDoc(documentId: string, content: string): Promise<{ title: string }> {
// Get current document to find end index
const document = await this.docs.documents.get({ documentId });
const endIndex = document.data.body?.content?.[document.data.body.content.length - 1]?.endIndex || 1;
const deleteEndIndex = Math.max(1, endIndex - 1);
// Delete existing content (except final newline)
if (deleteEndIndex > 1) {
await this.docs.documents.batchUpdate({
documentId,
requestBody: {
requests: [{
deleteContentRange: {
range: { startIndex: 1, endIndex: deleteEndIndex }
}
}]
}
});
}
// Insert new content
await this.docs.documents.batchUpdate({
documentId,
requestBody: {
requests: [
{
insertText: { location: { index: 1 }, text: content }
},
{
updateParagraphStyle: {
range: { startIndex: 1, endIndex: content.length + 1 },
paragraphStyle: { namedStyleType: 'NORMAL_TEXT' },
fields: 'namedStyleType'
}
}
]
}
});
return { title: document.data.title! };
}
/**
* Get content of a Google Doc as plain text
*/
async getDocContent(documentId: string): Promise<{ title: string; content: string }> {
const document = await this.docs.documents.get({ documentId });
// Extract text from document body
let content = '';
if (document.data.body?.content) {
for (const element of document.data.body.content) {
if (element.paragraph?.elements) {
for (const textRun of element.paragraph.elements) {
if (textRun.textRun?.content) {
content += textRun.textRun.content;
}
}
}
}
}
return {
title: document.data.title!,
content: content.trim()
};
}
/**
* Format text in a Google Doc
*/
async formatText(
documentId: string,
startIndex: number,
endIndex: number,
format: {
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
fontSize?: number;
foregroundColor?: { red?: number; green?: number; blue?: number };
}
): Promise<void> {
const textStyle: docs_v1.Schema$TextStyle = {};
const fields: string[] = [];
if (format.bold !== undefined) {
textStyle.bold = format.bold;
fields.push('bold');
}
if (format.italic !== undefined) {
textStyle.italic = format.italic;
fields.push('italic');
}
if (format.underline !== undefined) {
textStyle.underline = format.underline;
fields.push('underline');
}
if (format.strikethrough !== undefined) {
textStyle.strikethrough = format.strikethrough;
fields.push('strikethrough');
}
if (format.fontSize !== undefined) {
textStyle.fontSize = { magnitude: format.fontSize, unit: 'PT' };
fields.push('fontSize');
}
if (format.foregroundColor) {
textStyle.foregroundColor = { color: { rgbColor: format.foregroundColor } };
fields.push('foregroundColor');
}
await this.docs.documents.batchUpdate({
documentId,
requestBody: {
requests: [{
updateTextStyle: {
range: { startIndex, endIndex },
textStyle,
fields: fields.join(',')
}
}]
}
});
}
/**
* Format paragraph in a Google Doc
*/
async formatParagraph(
documentId: string,
startIndex: number,
endIndex: number,
format: {
alignment?: 'START' | 'CENTER' | 'END' | 'JUSTIFIED';
lineSpacing?: number;
spaceAbove?: number;
spaceBelow?: number;
}
): Promise<void> {
const paragraphStyle: docs_v1.Schema$ParagraphStyle = {};
const fields: string[] = [];
if (format.alignment) {
paragraphStyle.alignment = format.alignment;
fields.push('alignment');
}
if (format.lineSpacing !== undefined) {
paragraphStyle.lineSpacing = format.lineSpacing;
fields.push('lineSpacing');
}
if (format.spaceAbove !== undefined) {
paragraphStyle.spaceAbove = { magnitude: format.spaceAbove, unit: 'PT' };
fields.push('spaceAbove');
}
if (format.spaceBelow !== undefined) {
paragraphStyle.spaceBelow = { magnitude: format.spaceBelow, unit: 'PT' };
fields.push('spaceBelow');
}
await this.docs.documents.batchUpdate({
documentId,
requestBody: {
requests: [{
updateParagraphStyle: {
range: { startIndex, endIndex },
paragraphStyle,
fields: fields.join(',')
}
}]
}
});
}
}