Skip to content

Commit 9df78a2

Browse files
Merge pull request #436 from the-gigi-pplx/feature/doc-links
Add hyperlink support to modify_doc_text
2 parents d4bbd6d + 1eae403 commit 9df78a2

7 files changed

Lines changed: 1071 additions & 1041 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,7 @@ Saved files expire after 1 hour and are cleaned up automatically.
930930
|------|------|-------------|
931931
| `get_doc_content` | **Core** | Extract document text |
932932
| `create_doc` | **Core** | Create new documents |
933-
| `modify_doc_text` | **Core** | Modify document text |
933+
| `modify_doc_text` | **Core** | Modify document text (formatting + links) |
934934
| `search_docs` | Extended | Find documents by name |
935935
| `find_and_replace_doc` | Extended | Find and replace text |
936936
| `list_docs_in_folder` | Extended | List docs in folder |

README_NEW.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export OAUTHLIB_INSECURE_TRANSPORT=1 # Development only
9797
|------|------|-------------|
9898
| `get_doc_content` | Core | Extract text from Docs or .docx files (supports tabs) |
9999
| `create_doc` | Core | Create new documents with optional initial content |
100-
| `modify_doc_text` | Core | Insert, replace, format text (bold, italic, colors, fonts) |
100+
| `modify_doc_text` | Core | Insert, replace, format text (bold, italic, colors, fonts, links) |
101101
| `search_docs` | Extended | Find documents by name |
102102
| `find_and_replace_doc` | Extended | Global find/replace with case matching |
103103
| `list_docs_in_folder` | Extended | List Docs in a specific folder |

gdocs/docs_helpers.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def build_text_style(
4646
font_family: str = None,
4747
text_color: str = None,
4848
background_color: str = None,
49+
link_url: str = None,
4950
) -> tuple[Dict[str, Any], list[str]]:
5051
"""
5152
Build text style object for Google Docs API requests.
@@ -58,6 +59,7 @@ def build_text_style(
5859
font_family: Font family name
5960
text_color: Text color as hex string "#RRGGBB"
6061
background_color: Background (highlight) color as hex string "#RRGGBB"
62+
link_url: Hyperlink URL (http/https)
6163
6264
Returns:
6365
Tuple of (text_style_dict, list_of_field_names)
@@ -95,6 +97,10 @@ def build_text_style(
9597
text_style["backgroundColor"] = {"color": {"rgbColor": rgb}}
9698
fields.append("backgroundColor")
9799

100+
if link_url is not None:
101+
text_style["link"] = {"url": link_url}
102+
fields.append("link")
103+
98104
return text_style, fields
99105

100106

@@ -242,6 +248,7 @@ def create_format_text_request(
242248
font_family: str = None,
243249
text_color: str = None,
244250
background_color: str = None,
251+
link_url: str = None,
245252
) -> Optional[Dict[str, Any]]:
246253
"""
247254
Create an updateTextStyle request for Google Docs API.
@@ -256,12 +263,20 @@ def create_format_text_request(
256263
font_family: Font family name
257264
text_color: Text color as hex string "#RRGGBB"
258265
background_color: Background (highlight) color as hex string "#RRGGBB"
266+
link_url: Hyperlink URL (http/https)
259267
260268
Returns:
261269
Dictionary representing the updateTextStyle request, or None if no styles provided
262270
"""
263271
text_style, fields = build_text_style(
264-
bold, italic, underline, font_size, font_family, text_color, background_color
272+
bold,
273+
italic,
274+
underline,
275+
font_size,
276+
font_family,
277+
text_color,
278+
background_color,
279+
link_url,
265280
)
266281

267282
if not text_style:

gdocs/docs_tools.py

Lines changed: 33 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ async def modify_doc_text(
367367
font_family: str = None,
368368
text_color: str = None,
369369
background_color: str = None,
370+
link_url: str = None,
370371
) -> str:
371372
"""
372373
Modifies text in a Google Doc - can insert/replace text and/or apply formatting in a single operation.
@@ -384,13 +385,14 @@ async def modify_doc_text(
384385
font_family: Font family name (e.g., "Arial", "Times New Roman")
385386
text_color: Foreground text color (#RRGGBB)
386387
background_color: Background/highlight color (#RRGGBB)
388+
link_url: Hyperlink URL (http/https)
387389
388390
Returns:
389391
str: Confirmation message with operation details
390392
"""
391393
logger.info(
392394
f"[modify_doc_text] Doc={document_id}, start={start_index}, end={end_index}, text={text is not None}, "
393-
f"formatting={any([bold, italic, underline, font_size, font_family, text_color, background_color])}"
395+
f"formatting={any(p is not None for p in [bold, italic, underline, font_size, font_family, text_color, background_color, link_url])}"
394396
)
395397

396398
# Input validation
@@ -401,31 +403,21 @@ async def modify_doc_text(
401403
return f"Error: {error_msg}"
402404

403405
# Validate that we have something to do
404-
if text is None and not any(
405-
[
406-
bold is not None,
407-
italic is not None,
408-
underline is not None,
409-
font_size,
410-
font_family,
411-
text_color,
412-
background_color,
413-
]
414-
):
415-
return "Error: Must provide either 'text' to insert/replace, or formatting parameters (bold, italic, underline, font_size, font_family, text_color, background_color)."
406+
formatting_params = [
407+
bold,
408+
italic,
409+
underline,
410+
font_size,
411+
font_family,
412+
text_color,
413+
background_color,
414+
link_url,
415+
]
416+
if text is None and not any(p is not None for p in formatting_params):
417+
return "Error: Must provide either 'text' to insert/replace, or formatting parameters (bold, italic, underline, font_size, font_family, text_color, background_color, link_url)."
416418

417419
# Validate text formatting params if provided
418-
if any(
419-
[
420-
bold is not None,
421-
italic is not None,
422-
underline is not None,
423-
font_size,
424-
font_family,
425-
text_color,
426-
background_color,
427-
]
428-
):
420+
if any(p is not None for p in formatting_params):
429421
is_valid, error_msg = validator.validate_text_formatting_params(
430422
bold,
431423
italic,
@@ -434,6 +426,7 @@ async def modify_doc_text(
434426
font_family,
435427
text_color,
436428
background_color,
429+
link_url,
437430
)
438431
if not is_valid:
439432
return f"Error: {error_msg}"
@@ -482,17 +475,7 @@ async def modify_doc_text(
482475
operations.append(f"Inserted text at index {start_index}")
483476

484477
# Handle formatting
485-
if any(
486-
[
487-
bold is not None,
488-
italic is not None,
489-
underline is not None,
490-
font_size,
491-
font_family,
492-
text_color,
493-
background_color,
494-
]
495-
):
478+
if any(p is not None for p in formatting_params):
496479
# Adjust range for formatting based on text operations
497480
format_start = start_index
498481
format_end = end_index
@@ -524,24 +507,24 @@ async def modify_doc_text(
524507
font_family,
525508
text_color,
526509
background_color,
510+
link_url,
527511
)
528512
)
529513

530-
format_details = []
531-
if bold is not None:
532-
format_details.append(f"bold={bold}")
533-
if italic is not None:
534-
format_details.append(f"italic={italic}")
535-
if underline is not None:
536-
format_details.append(f"underline={underline}")
537-
if font_size:
538-
format_details.append(f"font_size={font_size}")
539-
if font_family:
540-
format_details.append(f"font_family={font_family}")
541-
if text_color:
542-
format_details.append(f"text_color={text_color}")
543-
if background_color:
544-
format_details.append(f"background_color={background_color}")
514+
format_details = [
515+
f"{name}={value}"
516+
for name, value in [
517+
("bold", bold),
518+
("italic", italic),
519+
("underline", underline),
520+
("font_size", font_size),
521+
("font_family", font_family),
522+
("text_color", text_color),
523+
("background_color", background_color),
524+
("link_url", link_url),
525+
]
526+
if value is not None
527+
]
545528

546529
operations.append(
547530
f"Applied formatting ({', '.join(format_details)}) to range {format_start}-{format_end}"

gdocs/managers/batch_operation_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def _build_operation_request(
190190
op.get("font_family"),
191191
op.get("text_color"),
192192
op.get("background_color"),
193+
op.get("link_url"),
193194
)
194195

195196
if not request:
@@ -205,6 +206,7 @@ def _build_operation_request(
205206
("font_family", "font family"),
206207
("text_color", "text color"),
207208
("background_color", "background color"),
209+
("link_url", "link"),
208210
]:
209211
if op.get(param) is not None:
210212
value = f"{op[param]}pt" if param == "font_size" else op[param]
@@ -370,6 +372,7 @@ def get_supported_operations(self) -> dict[str, Any]:
370372
"font_family",
371373
"text_color",
372374
"background_color",
375+
"link_url",
373376
],
374377
"description": "Apply formatting to text range",
375378
},

gdocs/managers/validation_manager.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import logging
99
from typing import Dict, Any, List, Tuple, Optional
10+
from urllib.parse import urlparse
1011

1112
from gdocs.docs_helpers import validate_operation
1213

@@ -159,6 +160,7 @@ def validate_text_formatting_params(
159160
font_family: Optional[str] = None,
160161
text_color: Optional[str] = None,
161162
background_color: Optional[str] = None,
163+
link_url: Optional[str] = None,
162164
) -> Tuple[bool, str]:
163165
"""
164166
Validate text formatting parameters.
@@ -171,6 +173,7 @@ def validate_text_formatting_params(
171173
font_family: Font family name
172174
text_color: Text color in "#RRGGBB" format
173175
background_color: Background color in "#RRGGBB" format
176+
link_url: Hyperlink URL (http/https)
174177
175178
Returns:
176179
Tuple of (is_valid, error_message)
@@ -184,11 +187,12 @@ def validate_text_formatting_params(
184187
font_family,
185188
text_color,
186189
background_color,
190+
link_url,
187191
]
188192
if all(param is None for param in formatting_params):
189193
return (
190194
False,
191-
"At least one formatting parameter must be provided (bold, italic, underline, font_size, font_family, text_color, or background_color)",
195+
"At least one formatting parameter must be provided (bold, italic, underline, font_size, font_family, text_color, background_color, or link_url)",
192196
)
193197

194198
# Validate boolean parameters
@@ -240,6 +244,30 @@ def validate_text_formatting_params(
240244
if not is_valid:
241245
return False, error_msg
242246

247+
is_valid, error_msg = self.validate_link_url(link_url)
248+
if not is_valid:
249+
return False, error_msg
250+
251+
return True, ""
252+
253+
def validate_link_url(self, link_url: Optional[str]) -> Tuple[bool, str]:
254+
"""Validate hyperlink URL parameters."""
255+
if link_url is None:
256+
return True, ""
257+
258+
if not isinstance(link_url, str):
259+
return False, f"link_url must be a string, got {type(link_url).__name__}"
260+
261+
if not link_url.strip():
262+
return False, "link_url cannot be empty"
263+
264+
parsed = urlparse(link_url)
265+
if parsed.scheme not in ("http", "https"):
266+
return False, "link_url must start with http:// or https://"
267+
268+
if not parsed.netloc:
269+
return False, "link_url must include a valid host"
270+
243271
return True, ""
244272

245273
def validate_paragraph_style_params(
@@ -578,6 +606,7 @@ def validate_batch_operations(
578606
op.get("font_family"),
579607
op.get("text_color"),
580608
op.get("background_color"),
609+
op.get("link_url"),
581610
)
582611
if not is_valid:
583612
return False, f"Operation {i + 1} (format_text): {error_msg}"

0 commit comments

Comments
 (0)