diff --git a/gdocs/docs_helpers.py b/gdocs/docs_helpers.py index 2d29327..d1150d8 100644 --- a/gdocs/docs_helpers.py +++ b/gdocs/docs_helpers.py @@ -113,6 +113,7 @@ def build_paragraph_style( indent_end: float = None, space_above: float = None, space_below: float = None, + named_style_type: str = None, ) -> tuple[Dict[str, Any], list[str]]: """ Build paragraph style object for Google Docs API requests. @@ -126,6 +127,8 @@ def build_paragraph_style( indent_end: Right/end indent in points space_above: Space above paragraph in points space_below: Space below paragraph in points + named_style_type: Direct named style (TITLE, SUBTITLE, HEADING_1..6, NORMAL_TEXT). + Takes precedence over heading_level when both are provided. Returns: Tuple of (paragraph_style_dict, list_of_field_names) @@ -133,7 +136,20 @@ def build_paragraph_style( paragraph_style = {} fields = [] - if heading_level is not None: + if named_style_type is not None: + valid_styles = [ + "NORMAL_TEXT", "TITLE", "SUBTITLE", + "HEADING_1", "HEADING_2", "HEADING_3", + "HEADING_4", "HEADING_5", "HEADING_6", + ] + if named_style_type not in valid_styles: + raise ValueError( + f"Invalid named_style_type '{named_style_type}'. " + f"Must be one of: {', '.join(valid_styles)}" + ) + paragraph_style["namedStyleType"] = named_style_type + fields.append("namedStyleType") + elif heading_level is not None: if heading_level < 0 or heading_level > 6: raise ValueError("heading_level must be between 0 (normal text) and 6") if heading_level == 0: @@ -321,6 +337,7 @@ def create_update_paragraph_style_request( space_above: float = None, space_below: float = None, tab_id: Optional[str] = None, + named_style_type: str = None, ) -> Optional[Dict[str, Any]]: """ Create an updateParagraphStyle request for Google Docs API. @@ -337,6 +354,7 @@ def create_update_paragraph_style_request( space_above: Space above paragraph in points space_below: Space below paragraph in points tab_id: Optional ID of the tab to target + named_style_type: Direct named style (TITLE, SUBTITLE, HEADING_1..6, NORMAL_TEXT) Returns: Dictionary representing the updateParagraphStyle request, or None if no styles provided @@ -350,6 +368,7 @@ def create_update_paragraph_style_request( indent_end, space_above, space_below, + named_style_type, ) if not paragraph_style: @@ -628,6 +647,33 @@ def create_bullet_list_request( return requests +def create_delete_bullet_list_request( + start_index: int, + end_index: int, + doc_tab_id: Optional[str] = None, +) -> Dict[str, Any]: + """ + Create a deleteParagraphBullets request to remove bullet/list formatting. + + Args: + start_index: Start of the paragraph range + end_index: End of the paragraph range + doc_tab_id: Optional ID of the tab to target + + Returns: + Dictionary representing the deleteParagraphBullets request + """ + range_obj = {"startIndex": start_index, "endIndex": end_index} + if doc_tab_id: + range_obj["tabId"] = doc_tab_id + + return { + "deleteParagraphBullets": { + "range": range_obj, + } + } + + def validate_operation(operation: Dict[str, Any]) -> tuple[bool, str]: """ Validate a batch operation dictionary. @@ -652,6 +698,7 @@ def validate_operation(operation: Dict[str, Any]) -> tuple[bool, str]: "insert_table": ["index", "rows", "columns"], "insert_page_break": ["index"], "find_replace": ["find_text", "replace_text"], + "create_bullet_list": ["start_index", "end_index"], "insert_doc_tab": ["title", "index"], "delete_doc_tab": ["tab_id"], "update_doc_tab": ["tab_id", "title"], diff --git a/gdocs/docs_tools.py b/gdocs/docs_tools.py index 44c66a7..c040c0b 100644 --- a/gdocs/docs_tools.py +++ b/gdocs/docs_tools.py @@ -872,6 +872,10 @@ async def batch_update_doc( insert_page_break- required: index (int) find_replace - required: find_text (str), replace_text (str) optional: match_case (bool, default false) + create_bullet_list - required: start_index (int), end_index (int) + optional: list_type ('UNORDERED'|'ORDERED'|'NONE', default UNORDERED), + nesting_level (0-8), paragraph_start_indices (list[int]) + Use list_type='NONE' to remove existing bullet/list formatting insert_doc_tab - required: title (str), index (int) optional: parent_tab_id (str) delete_doc_tab - required: tab_id (str) diff --git a/gdocs/managers/batch_operation_manager.py b/gdocs/managers/batch_operation_manager.py index 8b0d2e0..7694b38 100644 --- a/gdocs/managers/batch_operation_manager.py +++ b/gdocs/managers/batch_operation_manager.py @@ -17,6 +17,8 @@ from gdocs.docs_helpers import ( create_find_replace_request, create_insert_table_request, create_insert_page_break_request, + create_bullet_list_request, + create_delete_bullet_list_request, create_insert_doc_tab_request, create_delete_doc_tab_request, create_update_doc_tab_request, @@ -244,6 +246,7 @@ class BatchOperationManager: op.get("space_above"), op.get("space_below"), tab_id, + op.get("named_style_type"), ) if not request: @@ -301,6 +304,31 @@ class BatchOperationManager: ) description = f"find/replace '{op['find_text']}' → '{op['replace_text']}'" + elif op_type == "create_bullet_list": + list_type = op.get("list_type", "UNORDERED") + if list_type not in ("UNORDERED", "ORDERED", "NONE"): + raise ValueError( + f"Invalid list_type '{list_type}'. Must be 'UNORDERED', 'ORDERED', or 'NONE'" + ) + if list_type == "NONE": + request = create_delete_bullet_list_request( + op["start_index"], op["end_index"], tab_id + ) + description = f"remove bullets {op['start_index']}-{op['end_index']}" + else: + request = create_bullet_list_request( + op["start_index"], + op["end_index"], + list_type, + op.get("nesting_level"), + op.get("paragraph_start_indices"), + tab_id, + ) + style = "bulleted" if list_type == "UNORDERED" else "numbered" + description = f"create {style} list {op['start_index']}-{op['end_index']}" + if op.get("nesting_level"): + description += f" (nesting level {op['nesting_level']})" + elif op_type == "insert_doc_tab": request = create_insert_doc_tab_request( op["title"], op["index"], op.get("parent_tab_id") @@ -327,6 +355,7 @@ class BatchOperationManager: "insert_table", "insert_page_break", "find_replace", + "create_bullet_list", "insert_doc_tab", "delete_doc_tab", "update_doc_tab", @@ -460,6 +489,11 @@ class BatchOperationManager: "optional": ["match_case"], "description": "Find and replace text throughout document", }, + "create_bullet_list": { + "required": ["start_index", "end_index"], + "optional": ["list_type", "nesting_level", "paragraph_start_indices"], + "description": "Apply or remove native bullet/numbered list formatting (list_type: UNORDERED, ORDERED, or NONE to remove; nesting_level: 0-8)", + }, "insert_doc_tab": { "required": ["title", "index"], "description": "Insert a new document tab with given title at specified index", diff --git a/gdocs/managers/validation_manager.py b/gdocs/managers/validation_manager.py index d1b66fa..3235c1a 100644 --- a/gdocs/managers/validation_manager.py +++ b/gdocs/managers/validation_manager.py @@ -280,6 +280,7 @@ class ValidationManager: indent_end: Optional[float] = None, space_above: Optional[float] = None, space_below: Optional[float] = None, + named_style_type: Optional[str] = None, ) -> Tuple[bool, str]: """ Validate paragraph style parameters. @@ -293,6 +294,7 @@ class ValidationManager: indent_end: Right/end indent in points space_above: Space above paragraph in points space_below: Space below paragraph in points + named_style_type: Direct named style (TITLE, SUBTITLE, HEADING_1..6, NORMAL_TEXT) Returns: Tuple of (is_valid, error_message) @@ -306,13 +308,26 @@ class ValidationManager: indent_end, space_above, space_below, + named_style_type, ] if all(param is None for param in style_params): return ( False, - "At least one paragraph style parameter must be provided (heading_level, alignment, line_spacing, indent_first_line, indent_start, indent_end, space_above, or space_below)", + "At least one paragraph style parameter must be provided (heading_level, alignment, line_spacing, indent_first_line, indent_start, indent_end, space_above, space_below, or named_style_type)", ) + if named_style_type is not None: + valid_styles = [ + "NORMAL_TEXT", "TITLE", "SUBTITLE", + "HEADING_1", "HEADING_2", "HEADING_3", + "HEADING_4", "HEADING_5", "HEADING_6", + ] + if named_style_type not in valid_styles: + return ( + False, + f"Invalid named_style_type '{named_style_type}'. Must be one of: {', '.join(valid_styles)}", + ) + if heading_level is not None: if not isinstance(heading_level, int): return ( @@ -627,6 +642,7 @@ class ValidationManager: op.get("indent_end"), op.get("space_above"), op.get("space_below"), + op.get("named_style_type"), ) if not is_valid: return (