#
tokens: 46674/50000 34/34 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .devcontainer
│   ├── devcontainer.json
│   ├── Dockerfile
│   ├── init-firewall.sh
│   └── postCreateCommand.sh
├── .dockerignore
├── .editorconfig
├── .github
│   └── workflows
│       └── publish.yml
├── .gitignore
├── .goreleaser.yaml
├── .npmignore
├── CLAUDE.md
├── cmd
│   └── excel-mcp-server
│       └── main.go
├── Dockerfile
├── docs
│   ├── design
│   │   └── excel-style-schema.md
│   └── img
│       ├── icon-400.png
│       └── icon-800.png
├── go.mod
├── go.sum
├── internal
│   ├── excel
│   │   ├── excel_excelize.go
│   │   ├── excel_ole.go
│   │   ├── excel.go
│   │   ├── pagination.go
│   │   └── util.go
│   ├── mcp
│   │   └── error.go
│   ├── server
│   │   └── server.go
│   └── tools
│       ├── common.go
│       ├── config.go
│       ├── excel_copy_sheet.go
│       ├── excel_create_table.go
│       ├── excel_describe_sheets.go
│       ├── excel_format_range.go
│       ├── excel_read_sheet.go
│       ├── excel_screen_capture.go
│       └── excel_write_to_sheet.go
├── launcher
│   └── launcher.ts
├── LICENSE
├── package-lock.json
├── package.json
├── projectBrief.md
├── README.md
├── smithery.yaml
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
1 | /dist
2 | /node_modules
3 | 
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
1 | /.github
2 | /cmd
3 | /internal
4 | /go.mod
5 | /go.sum
6 | /.goreleaser.yaml
7 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | /node_modules
 2 | /dist
 3 | /memory-bank
 4 | # Added by goreleaser init:
 5 | dist/
 6 | # RooFlow
 7 | .roo
 8 | .roomodes
 9 | .clinerules-default
10 | # repomix
11 | repomix-output.xml
12 | # Claude Code
13 | CLAUDE.local.md
14 | .claude/settings.local.json
15 | 
```

--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------

```
 1 | # EditorConfig is awesome: https://EditorConfig.org
 2 | 
 3 | # top-most EditorConfig file
 4 | root = true
 5 | 
 6 | [*]
 7 | charset = utf-8
 8 | indent_style = space
 9 | indent_size = 2
10 | end_of_line = lf
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | 
14 | [*.md]
15 | trim_trailing_whitespace = false
16 | indent_size = 4
17 | 
```

--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | version: 2
 2 | 
 3 | before:
 4 |   hooks:
 5 |     # You may remove this if you don't use go modules.
 6 |     - go mod tidy
 7 |     # you may remove this if you don't need go generate
 8 |     - go generate ./...
 9 | 
10 | builds:
11 |   - env:
12 |       - CGO_ENABLED=0
13 |     main: ./cmd/excel-mcp-server/main.go
14 |     goos:
15 |       - linux
16 |       - windows
17 |       - darwin
18 | 
19 | archives:
20 |   - formats: ['tar.gz']
21 |     # this name template makes the OS and Arch compatible with the results of `uname`.
22 |     name_template: >-
23 |       {{ .ProjectName }}_
24 |       {{- title .Os }}_
25 |       {{- if eq .Arch "amd64" }}x86_64
26 |       {{- else if eq .Arch "386" }}i386
27 |       {{- else }}{{ .Arch }}{{ end }}
28 |       {{- if .Arm }}v{{ .Arm }}{{ end }}
29 |     # use zip for windows archives
30 |     format_overrides:
31 |       - goos: windows
32 |         formats: ['zip']
33 |     builds_info:
34 |       mode: 0755
35 | 
36 | changelog:
37 |   sort: asc
38 |   filters:
39 |     exclude:
40 |       - "^docs:"
41 |       - "^test:"
42 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Excel MCP Server
  2 | 
  3 | <img src="https://github.com/negokaz/excel-mcp-server/blob/main/docs/img/icon-800.png?raw=true" width="128">
  4 | 
  5 | <a href="https://glama.ai/mcp/servers/@negokaz/excel-mcp-server">
  6 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/@negokaz/excel-mcp-server/badge" alt="Excel Server MCP server" />
  7 | </a>
  8 | 
  9 | [![NPM Version](https://img.shields.io/npm/v/@negokaz/excel-mcp-server)](https://www.npmjs.com/package/@negokaz/excel-mcp-server)
 10 | [![smithery badge](https://smithery.ai/badge/@negokaz/excel-mcp-server)](https://smithery.ai/server/@negokaz/excel-mcp-server)
 11 | 
 12 | A Model Context Protocol (MCP) server that reads and writes MS Excel data.
 13 | 
 14 | ## Features
 15 | 
 16 | - Read/Write text values
 17 | - Read/Write formulas
 18 | - Create new sheets
 19 | 
 20 | **🪟Windows only:**
 21 | - Live editing
 22 | - Capture screen image from a sheet
 23 | 
 24 | For more details, see the [tools](#tools) section.
 25 | 
 26 | ## Requirements
 27 | 
 28 | - Node.js 20.x or later
 29 | 
 30 | ## Supported file formats
 31 | 
 32 | - xlsx (Excel book)
 33 | - xlsm (Excel macro-enabled book)
 34 | - xltx (Excel template)
 35 | - xltm (Excel macro-enabled template)
 36 | 
 37 | ## Installation
 38 | 
 39 | ### Installing via NPM
 40 | 
 41 | excel-mcp-server is automatically installed by adding the following configuration to the MCP servers configuration.
 42 | 
 43 | For Windows:
 44 | ```json
 45 | {
 46 |     "mcpServers": {
 47 |         "excel": {
 48 |             "command": "cmd",
 49 |             "args": ["/c", "npx", "--yes", "@negokaz/excel-mcp-server"],
 50 |             "env": {
 51 |                 "EXCEL_MCP_PAGING_CELLS_LIMIT": "4000"
 52 |             }
 53 |         }
 54 |     }
 55 | }
 56 | ```
 57 | 
 58 | For other platforms:
 59 | ```json
 60 | {
 61 |     "mcpServers": {
 62 |         "excel": {
 63 |             "command": "npx",
 64 |             "args": ["--yes", "@negokaz/excel-mcp-server"],
 65 |             "env": {
 66 |                 "EXCEL_MCP_PAGING_CELLS_LIMIT": "4000"
 67 |             }
 68 |         }
 69 |     }
 70 | }
 71 | ```
 72 | 
 73 | ### Installing via Smithery
 74 | 
 75 | To install Excel MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@negokaz/excel-mcp-server):
 76 | 
 77 | ```bash
 78 | npx -y @smithery/cli install @negokaz/excel-mcp-server --client claude
 79 | ```
 80 | 
 81 | <h2 id="tools">Tools</h2>
 82 | 
 83 | ### `excel_describe_sheets`
 84 | 
 85 | List all sheet information of specified Excel file.
 86 | 
 87 | **Arguments:**
 88 | - `fileAbsolutePath`
 89 |     - Absolute path to the Excel file
 90 | 
 91 | ### `excel_read_sheet`
 92 | 
 93 | Read values from Excel sheet with pagination.
 94 | 
 95 | **Arguments:**
 96 | - `fileAbsolutePath`
 97 |     - Absolute path to the Excel file
 98 | - `sheetName`
 99 |     - Sheet name in the Excel file
100 | - `range`
101 |     - Range of cells to read in the Excel sheet (e.g., "A1:C10"). [default: first paging range]
102 | - `showFormula`
103 |     - Show formula instead of value [default: false]
104 | - `showStyle`
105 |     - Show style information for cells [default: false]
106 | 
107 | ### `excel_screen_capture`
108 | 
109 | **[Windows only]** Take a screenshot of the Excel sheet with pagination.
110 | 
111 | **Arguments:**
112 | - `fileAbsolutePath`
113 |     - Absolute path to the Excel file
114 | - `sheetName`
115 |     - Sheet name in the Excel file
116 | - `range`
117 |     - Range of cells to read in the Excel sheet (e.g., "A1:C10"). [default: first paging range]
118 | 
119 | ### `excel_write_to_sheet`
120 | 
121 | Write values to the Excel sheet.
122 | 
123 | **Arguments:**
124 | - `fileAbsolutePath`
125 |     - Absolute path to the Excel file
126 | - `sheetName`
127 |     - Sheet name in the Excel file
128 | - `newSheet`
129 |     - Create a new sheet if true, otherwise write to the existing sheet
130 | - `range`
131 |     - Range of cells to read in the Excel sheet (e.g., "A1:C10").
132 | - `values`
133 |     - Values to write to the Excel sheet. If the value is a formula, it should start with "="
134 | 
135 | ### `excel_create_table`
136 | 
137 | Create a table in the Excel sheet
138 | 
139 | **Arguments:**
140 | - `fileAbsolutePath`
141 |     - Absolute path to the Excel file
142 | - `sheetName`
143 |     - Sheet name where the table is created
144 | - `range`
145 |     - Range to be a table (e.g., "A1:C10")
146 | - `tableName`
147 |     - Table name to be created
148 | 
149 | ### `excel_copy_sheet`
150 | 
151 | Copy existing sheet to a new sheet
152 | 
153 | **Arguments:**
154 | - `fileAbsolutePath`
155 |     - Absolute path to the Excel file
156 | - `srcSheetName`
157 |     - Source sheet name in the Excel file
158 | - `dstSheetName`
159 |     - Sheet name to be copied
160 | 
161 | ### `excel_format_range`
162 | 
163 | Format cells in the Excel sheet with style information
164 | 
165 | **Arguments:**
166 | - `fileAbsolutePath`
167 |     - Absolute path to the Excel file
168 | - `sheetName`
169 |     - Sheet name in the Excel file
170 | - `range`
171 |     - Range of cells in the Excel sheet (e.g., "A1:C3")
172 | - `styles`
173 |     - 2D array of style objects for each cell. If a cell does not change style, use null. The number of items of the array must match the range size.
174 |     - Style object properties:
175 |         - `border`: Array of border styles (type, color, style)
176 |         - `font`: Font styling (bold, italic, underline, size, strike, color, vertAlign)
177 |         - `fill`: Fill/background styling (type, pattern, color, shading)
178 |         - `numFmt`: Custom number format string
179 |         - `decimalPlaces`: Number of decimal places (0-30)
180 | 
181 | <h2 id="configuration">Configuration</h2>
182 | 
183 | You can change the MCP Server behaviors by the following environment variables:
184 | 
185 | ### `EXCEL_MCP_PAGING_CELLS_LIMIT`
186 | 
187 | The maximum number of cells to read in a single paging operation.  
188 | [default: 4000]
189 | 
190 | ## License
191 | 
192 | Copyright (c) 2025 Kazuki Negoro
193 | 
194 | excel-mcp-server is released under the [MIT License](LICENSE)
```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
  1 | # CLAUDE.md
  2 | 
  3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
  4 | 
  5 | ## Project Overview
  6 | 
  7 | This is an Excel MCP (Model Context Protocol) Server that enables AI assistants to interact with Microsoft Excel files. It provides a bridge between AI models and Excel spreadsheets for programmatic data manipulation.
  8 | 
  9 | ## Development Commands
 10 | 
 11 | ### Build and Development
 12 | ```bash
 13 | npm run build     # Build Go binaries + compile TypeScript
 14 | npm run watch     # Watch TypeScript files for changes
 15 | npm run debug     # Debug with MCP inspector
 16 | ```
 17 | 
 18 | ### Testing
 19 | ```bash
 20 | go test ./...                           # Run all Go tests
 21 | go test ./internal/tools -v             # Run specific package tests
 22 | go test -run TestReadSheetData ./internal/tools  # Run specific test
 23 | ```
 24 | 
 25 | ### Linting and Formatting
 26 | ```bash
 27 | go fmt ./...      # Format Go code
 28 | go vet ./...      # Vet Go code for issues
 29 | ```
 30 | 
 31 | ## Architecture
 32 | 
 33 | ### Core Components
 34 | 
 35 | **Dual Backend Architecture**: The server supports two Excel backends:
 36 | - **Windows**: OLE automation for live Excel interaction (`excel_ole.go`)  
 37 | - **Cross-platform**: Excelize library for file operations (`excel_excelize.go`)
 38 | 
 39 | **Key Interfaces**:
 40 | - `ExcelInterface` in `internal/excel/excel.go` - Unified Excel operations API
 41 | - `Tool` interface in `internal/tools/` - MCP tool implementations
 42 | 
 43 | **Entry Points**:
 44 | - `cmd/excel-mcp-server/main.go` - Go binary entry point
 45 | - `launcher/launcher.ts` - Cross-platform launcher that selects appropriate binary
 46 | 
 47 | ### Tool System
 48 | 
 49 | MCP tools are implemented in `internal/tools/`:
 50 | - `excel_describe_sheets` - List worksheets and metadata
 51 | - `excel_read_sheet` - Read sheet data with pagination
 52 | - `excel_write_to_sheet` - Write data to sheets
 53 | - `excel_create_table` - Create Excel tables
 54 | - `excel_copy_sheet` - Copy sheets between workbooks
 55 | - `excel_screen_capture` - Windows-only screenshot functionality
 56 | 
 57 | ### Pagination System
 58 | 
 59 | Large datasets are handled through configurable pagination:
 60 | - Default limit: 4000 cells
 61 | - Configurable via `EXCEL_MCP_PAGING_CELLS_LIMIT` environment variable
 62 | - Implemented in `internal/excel/pagination.go`
 63 | 
 64 | ## File Structure
 65 | 
 66 | ```
 67 | cmd/excel-mcp-server/     # Main application entry point
 68 | internal/
 69 |   excel/                  # Excel abstraction layer
 70 |   server/                 # MCP server implementation  
 71 |   tools/                  # MCP tool implementations
 72 | launcher/                 # TypeScript launcher
 73 | memory-bank/              # Development context and progress
 74 | ```
 75 | 
 76 | ## Build System
 77 | 
 78 | Uses GoReleaser (`.goreleaser.yaml`) to create cross-platform binaries:
 79 | - Windows: amd64, 386, arm64
 80 | - macOS: amd64, arm64  
 81 | - Linux: amd64, 386, arm64
 82 | 
 83 | TypeScript launcher is compiled to `dist/launcher.js` and published to NPM.
 84 | 
 85 | ## Platform Differences
 86 | 
 87 | **Windows-specific features**:
 88 | - Live Excel interaction via OLE automation
 89 | - Screen capture capabilities
 90 | - Requires Excel to be installed
 91 | 
 92 | **Cross-platform features**:
 93 | - File-based Excel operations only
 94 | - No live editing capabilities
 95 | - Works with xlsx, xlsm, xltx, xltm formats
 96 | 
 97 | ## Configuration
 98 | 
 99 | Environment variables:
100 | - `EXCEL_MCP_PAGING_CELLS_LIMIT` - Maximum cells per page (default: 4000)
101 | 
102 | ## Dependencies
103 | 
104 | **Go**: Requires Go 1.23.0+ with Go 1.24.0 toolchain
105 | **Node.js**: Requires Node.js 20.x+ for TypeScript compilation
106 | **Key packages**: 
107 | - `github.com/mark3labs/mcp-go` - MCP framework
108 | - `github.com/xuri/excelize/v2` - Excel file operations
109 | - `github.com/go-ole/go-ole` - Windows OLE automation
```

--------------------------------------------------------------------------------
/.devcontainer/postCreateCommand.sh:
--------------------------------------------------------------------------------

```bash
1 | #!/bin/bash
2 | 
3 | set -ex
4 | 
5 | npm install -g @anthropic-ai/claude-code
6 | npm install -g @goreleaser/goreleaser
7 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | 
 2 | FROM node:20-slim AS release
 3 | 
 4 | # Set the working directory
 5 | WORKDIR /app
 6 | 
 7 | RUN npm install -g @negokaz/[email protected]
 8 | 
 9 | # Command to run the application
10 | ENTRYPOINT ["excel-mcp-server"]
11 | 
```

--------------------------------------------------------------------------------
/cmd/excel-mcp-server/main.go:
--------------------------------------------------------------------------------

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 
 7 | 	"github.com/negokaz/excel-mcp-server/internal/server"
 8 | )
 9 | 
10 | var (
11 | 	version = "dev"
12 | )
13 | 
14 | func main() {
15 | 	s := server.New(version)
16 | 	err := s.Start()
17 | 	if err != nil {
18 | 		fmt.Fprintf(os.Stderr, "Failed to start the server: %v\n", err)
19 | 		os.Exit(1)
20 | 	}
21 | }
22 | 
```

--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "Claude Code Dev Container",
 3 |   "build": {
 4 |     "dockerfile": "Dockerfile"
 5 |   },
 6 |   "features": {
 7 |     "ghcr.io/devcontainers/features/go:1":{}
 8 |   },
 9 |   "mounts": [
10 |     "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind,consistency=cached"
11 |   ],
12 |   "postCreateCommand": ".devcontainer/postCreateCommand.sh",
13 |   "remoteUser": "node"
14 | }
15 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     {}
 8 |   commandFunction:
 9 |     # A function that produces the CLI command to start the MCP on stdio.
10 |     |-
11 |     (config) => ({ command: 'node', args: ['dist/launcher.js'], env: {} })
12 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "strict": true,
 7 |     "esModuleInterop": true,
 8 |     "skipLibCheck": true,
 9 |     "forceConsistentCasingInFileNames": true,
10 |     "resolveJsonModule": true,
11 |     "outDir": "./dist",
12 |     "rootDir": "./launcher",
13 |   },
14 |   "include": ["launcher/**/*"],
15 |   "exclude": ["node_modules"]
16 | }
17 | 
```

--------------------------------------------------------------------------------
/internal/tools/config.go:
--------------------------------------------------------------------------------

```go
 1 | package tools
 2 | 
 3 | import (
 4 | 	z "github.com/Oudwins/zog"
 5 | 	"github.com/Oudwins/zog/zenv"
 6 | )
 7 | 
 8 | type EnvConfig struct {
 9 | 	EXCEL_MCP_PAGING_CELLS_LIMIT int
10 | }
11 | 
12 | var configSchema = z.Struct(z.Shape{
13 | 	"EXCEL_MCP_PAGING_CELLS_LIMIT": z.Int().GT(0).Default(4000),
14 | })
15 | 
16 | func LoadConfig() (EnvConfig, z.ZogIssueMap) {
17 | 	config := EnvConfig{}
18 | 	issues := configSchema.Parse(zenv.NewDataProvider(), &config)
19 | 	return config, issues
20 | }
21 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@negokaz/excel-mcp-server",
 3 |   "version": "0.12.0",
 4 |   "description": "An MCP server that reads and writes spreadsheet data to MS Excel file",
 5 |   "author": "negokaz",
 6 |   "license": "MIT",
 7 |   "bin": {
 8 |     "excel-mcp-server": "dist/launcher.js"
 9 |   },
10 |   "scripts": {
11 |     "build": "goreleaser build --snapshot --clean && tsc",
12 |     "watch": "tsc --watch",
13 |     "debug": "npx @modelcontextprotocol/inspector dist/launcher.js"
14 |   },
15 |   "devDependencies": {
16 |     "@types/node": "^22.13.4",
17 |     "typescript": "^5.7.3"
18 |   },
19 |   "publishConfig": {
20 |     "access": "public"
21 |   }
22 | }
23 | 
```

--------------------------------------------------------------------------------
/internal/mcp/error.go:
--------------------------------------------------------------------------------

```go
 1 | package mcp
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 
 6 | 	z "github.com/Oudwins/zog"
 7 | 	"github.com/mark3labs/mcp-go/mcp"
 8 | )
 9 | 
10 | func NewToolResultInvalidArgumentError(message string) *mcp.CallToolResult {
11 | 	return mcp.NewToolResultError(fmt.Sprintf("Invalid argument: %s", message))
12 | }
13 | 
14 | func NewToolResultZogIssueMap(errs z.ZogIssueMap) *mcp.CallToolResult {
15 | 	issues := z.Issues.SanitizeMap(errs)
16 | 
17 | 	var issueResults []mcp.Content
18 | 	for k, messages := range issues {
19 | 		for _, message := range messages {
20 | 			issueResults = append(issueResults, mcp.NewTextContent(fmt.Sprintf("Invalid argument: %s: %s", k, message)))
21 | 		}
22 | 	}
23 | 
24 | 	return &mcp.CallToolResult{
25 | 		Content: issueResults,
26 | 		IsError: true,
27 | 	}
28 | }
29 | 
```

--------------------------------------------------------------------------------
/internal/server/server.go:
--------------------------------------------------------------------------------

```go
 1 | package server
 2 | 
 3 | import (
 4 | 	"runtime"
 5 | 
 6 | 	"github.com/mark3labs/mcp-go/server"
 7 | 	"github.com/negokaz/excel-mcp-server/internal/tools"
 8 | )
 9 | 
10 | type ExcelServer struct {
11 | 	server *server.MCPServer
12 | }
13 | 
14 | func New(version string) *ExcelServer {
15 | 	s := &ExcelServer{}
16 | 	s.server = server.NewMCPServer(
17 | 		"excel-mcp-server",
18 | 		version,
19 | 	)
20 | 	tools.AddExcelDescribeSheetsTool(s.server)
21 | 	tools.AddExcelReadSheetTool(s.server)
22 | 	if runtime.GOOS == "windows" {
23 | 		tools.AddExcelScreenCaptureTool(s.server)
24 | 	}
25 | 	tools.AddExcelWriteToSheetTool(s.server)
26 | 	tools.AddExcelCreateTableTool(s.server)
27 | 	tools.AddExcelCopySheetTool(s.server)
28 | 	tools.AddExcelFormatRangeTool(s.server)
29 | 	return s
30 | }
31 | 
32 | func (s *ExcelServer) Start() error {
33 | 	return server.ServeStdio(s.server)
34 | }
35 | 
```

--------------------------------------------------------------------------------
/launcher/launcher.ts:
--------------------------------------------------------------------------------

```typescript
 1 | #!/usr/bin/env node
 2 | import * as path from 'path'
 3 | import * as childProcess from 'child_process'
 4 | 
 5 | const BINARY_DISTRIBUTION_PACKAGES: any = {
 6 |   win32_ia32: "excel-mcp-server_windows_386_sse2",
 7 |   win32_x64: "excel-mcp-server_windows_amd64_v1",
 8 |   win32_arm64: "excel-mcp-server_windows_arm64_v8.0",
 9 |   darwin_x64: "excel-mcp-server_darwin_amd64_v1",
10 |   darwin_arm64: "excel-mcp-server_darwin_arm64_v8.0",
11 |   linux_ia32: "excel-mcp-server_linux_386_sse2",
12 |   linux_x64: "excel-mcp-server_linux_amd64_v1",
13 |   linux_arm64: "excel-mcp-server_linux_arm64_v8.0",
14 | }
15 | 
16 | function getBinaryPath(): string {
17 |   const suffix = process.platform === 'win32' ? '.exe' : '';
18 |   const pkg = BINARY_DISTRIBUTION_PACKAGES[`${process.platform}_${process.arch}`];
19 |   if (pkg) {
20 |     return path.resolve(__dirname, pkg, `excel-mcp-server${suffix}`);
21 |   } else {
22 |     throw new Error(`Unsupported platform: ${process.platform}_${process.arch}`);
23 |   }
24 | }
25 | 
26 | childProcess.execFileSync(getBinaryPath(), process.argv, {
27 |   stdio: 'inherit',
28 | });
29 | 
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Publish Package to npmjs
 2 | on:
 3 |   push:
 4 |     tags:
 5 |       - 'v*'
 6 | 
 7 | jobs:
 8 |   build:
 9 |     runs-on: ubuntu-latest
10 |     steps:
11 |       - uses: actions/checkout@v4
12 |         with:
13 |           fetch-depth: 0
14 |       - name: Set up Go
15 |         uses: actions/setup-go@v5
16 |         with:
17 |           go-version: stable
18 | 
19 |       - name: Install GoReleaser
20 |         uses: goreleaser/goreleaser-action@v6
21 |         with:
22 |           install-only: true
23 |           distribution: goreleaser
24 |           # 'latest', 'nightly', or a semver
25 |           version: "~> v2"
26 | 
27 |       - name: Setup Node.js
28 |         uses: actions/setup-node@v4
29 |         with:
30 |           node-version: '20.x'
31 |           registry-url: 'https://registry.npmjs.org'
32 | 
33 |       - name: Install dependencies
34 |         run: npm ci
35 | 
36 |       - name: Build
37 |         run: npm run build
38 | 
39 |       - name: Publish to npmjs
40 |         run: npm publish --no-git-checks
41 |         env:
42 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
43 | 
44 |       - name: Create GitHub Releases
45 |         run: goreleaser release --clean
46 |         env:
47 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 | 
```

--------------------------------------------------------------------------------
/internal/excel/util.go:
--------------------------------------------------------------------------------

```go
 1 | package excel
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"os"
 6 | 	"path"
 7 | 	"regexp"
 8 | 
 9 | 	"github.com/xuri/excelize/v2"
10 | )
11 | 
12 | // parseRange parses Excel's range string (e.g. A1:C10 or A1)
13 | func ParseRange(rangeStr string) (int, int, int, int, error) {
14 | 	re := regexp.MustCompile(`^(\$?[A-Z]+\$?\d+)(?::(\$?[A-Z]+\$?\d+))?$`)
15 | 	matches := re.FindStringSubmatch(rangeStr)
16 | 	if matches == nil {
17 | 		return 0, 0, 0, 0, fmt.Errorf("invalid range format: %s", rangeStr)
18 | 	}
19 | 	startCol, startRow, err := excelize.CellNameToCoordinates(matches[1])
20 | 	if err != nil {
21 | 		return 0, 0, 0, 0, err
22 | 	}
23 | 
24 | 	if matches[2] == "" {
25 | 		// Single cell case
26 | 		return startCol, startRow, startCol, startRow, nil
27 | 	}
28 | 
29 | 	endCol, endRow, err := excelize.CellNameToCoordinates(matches[2])
30 | 	if err != nil {
31 | 		return 0, 0, 0, 0, err
32 | 	}
33 | 	return startCol, startRow, endCol, endRow, nil
34 | }
35 | 
36 | func NormalizeRange(rangeStr string) string {
37 | 	startCol, startRow, endCol, endRow, _ := ParseRange(rangeStr)
38 | 	startCell, _ := excelize.CoordinatesToCellName(startCol, startRow)
39 | 	endCell, _ := excelize.CoordinatesToCellName(endCol, endRow)
40 | 	return fmt.Sprintf("%s:%s", startCell, endCell)
41 | }
42 | 
43 | // FileIsNotReadable checks if a file is not writable
44 | func FileIsNotWritable(absolutePath string) bool {
45 | 	f, err := os.OpenFile(path.Clean(absolutePath), os.O_WRONLY, os.ModePerm)
46 | 	if err != nil {
47 | 		return true
48 | 	}
49 | 	defer f.Close()
50 | 	return false
51 | }
52 | 
```

--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:20
 2 | 
 3 | ARG TZ
 4 | ENV TZ="$TZ"
 5 | 
 6 | # Install basic development tools and iptables/ipset
 7 | RUN apt update && apt install -y less \
 8 |   git \
 9 |   procps \
10 |   sudo \
11 |   fzf \
12 |   zsh \
13 |   man-db \
14 |   unzip \
15 |   gnupg2 \
16 |   gh \
17 |   iptables \
18 |   ipset \
19 |   iproute2 \
20 |   dnsutils \
21 |   aggregate \
22 |   jq
23 | 
24 | # Ensure default node user has access to /usr/local/share
25 | RUN mkdir -p /usr/local/share/npm-global && \
26 |   chown -R node:node /usr/local/share
27 | 
28 | ARG USERNAME=node
29 | 
30 | # Persist bash history.
31 | RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
32 |   && mkdir /commandhistory \
33 |   && touch /commandhistory/.bash_history \
34 |   && chown -R $USERNAME /commandhistory
35 | 
36 | # Set `DEVCONTAINER` environment variable to help with orientation
37 | ENV DEVCONTAINER=true
38 | 
39 | # Create workspace and config directories and set permissions
40 | RUN mkdir -p /workspace /home/node/.claude && \
41 |   chown -R node:node /workspace /home/node/.claude
42 | 
43 | WORKDIR /workspace
44 | 
45 | RUN ARCH=$(dpkg --print-architecture) && \
46 |   wget "https://github.com/dandavison/delta/releases/download/0.18.2/git-delta_0.18.2_${ARCH}.deb" && \
47 |   sudo dpkg -i "git-delta_0.18.2_${ARCH}.deb" && \
48 |   rm "git-delta_0.18.2_${ARCH}.deb"
49 | 
50 | # Set up non-root user
51 | USER node
52 | 
53 | # Install global packages
54 | ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
55 | ENV PATH=$PATH:/usr/local/share/npm-global/bin
56 | 
57 | # Set the default shell to zsh rather than sh
58 | ENV SHELL=/bin/zsh
59 | 
60 | # Default powerline10k theme
61 | RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.0/zsh-in-docker.sh)" -- \
62 |   -p git \
63 |   -p fzf \
64 |   -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
65 |   -a "source /usr/share/doc/fzf/examples/completion.zsh" \
66 |   -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
67 |   -x
68 | 
69 | # Install Claude
70 | RUN npm install -g @anthropic-ai/claude-code
71 | 
72 | # Copy and set up firewall script
73 | COPY init-firewall.sh /usr/local/bin/
74 | USER root
75 | RUN chmod +x /usr/local/bin/init-firewall.sh && \
76 |   echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
77 |   chmod 0440 /etc/sudoers.d/node-firewall
78 | USER node
79 | 
```

--------------------------------------------------------------------------------
/internal/tools/excel_copy_sheet.go:
--------------------------------------------------------------------------------

```go
 1 | package tools
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 	"html"
 7 | 
 8 | 	z "github.com/Oudwins/zog"
 9 | 	"github.com/mark3labs/mcp-go/mcp"
10 | 	"github.com/mark3labs/mcp-go/server"
11 | 	excel "github.com/negokaz/excel-mcp-server/internal/excel"
12 | 	imcp "github.com/negokaz/excel-mcp-server/internal/mcp"
13 | )
14 | 
15 | type ExcelCopySheetArguments struct {
16 | 	FileAbsolutePath string `zog:"fileAbsolutePath"`
17 | 	SrcSheetName     string `zog:"srcSheetName"`
18 | 	DstSheetName     string `zog:"dstSheetName"`
19 | }
20 | 
21 | var excelCopySheetArgumentsSchema = z.Struct(z.Shape{
22 | 	"fileAbsolutePath": z.String().Required(),
23 | 	"srcSheetName":     z.String().Required(),
24 | 	"dstSheetName":     z.String().Required(),
25 | })
26 | 
27 | func AddExcelCopySheetTool(server *server.MCPServer) {
28 | 	server.AddTool(mcp.NewTool("excel_copy_sheet",
29 | 		mcp.WithDescription("Copy existing sheet to a new sheet"),
30 | 		mcp.WithString("fileAbsolutePath",
31 | 			mcp.Required(),
32 | 			mcp.Description("Absolute path to the Excel file"),
33 | 		),
34 | 		mcp.WithString("srcSheetName",
35 | 			mcp.Required(),
36 | 			mcp.Description("Source sheet name in the Excel file"),
37 | 		),
38 | 		mcp.WithString("dstSheetName",
39 | 			mcp.Required(),
40 | 			mcp.Description("Sheet name to be copied"),
41 | 		),
42 | 	), handleCopySheet)
43 | }
44 | 
45 | func handleCopySheet(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
46 | 	args := ExcelCopySheetArguments{}
47 | 	if issues := excelCopySheetArgumentsSchema.Parse(request.Params.Arguments, &args); len(issues) != 0 {
48 | 		return imcp.NewToolResultZogIssueMap(issues), nil
49 | 	}
50 | 	return copySheet(args.FileAbsolutePath, args.SrcSheetName, args.DstSheetName)
51 | }
52 | 
53 | func copySheet(fileAbsolutePath string, srcSheetName string, dstSheetName string) (*mcp.CallToolResult, error) {
54 | 	workbook, release, err := excel.OpenFile(fileAbsolutePath)
55 | 	if err != nil {
56 | 		return nil, err
57 | 	}
58 | 	defer release()
59 | 
60 | 	srcSheet, err := workbook.FindSheet(srcSheetName)
61 | 	if err != nil {
62 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
63 | 	}
64 | 	defer srcSheet.Release()
65 | 	srcSheetName, err = srcSheet.Name()
66 | 	if err != nil {
67 | 		return nil, err
68 | 	}
69 | 
70 | 	if err := workbook.CopySheet(srcSheetName, dstSheetName); err != nil {
71 | 		return nil, err
72 | 	}
73 | 	if err := workbook.Save(); err != nil {
74 | 		return nil, err
75 | 	}
76 | 
77 | 	result := "# Notice\n"
78 | 	result += fmt.Sprintf("backend: %s\n", workbook.GetBackendName())
79 | 	result += fmt.Sprintf("Sheet [%s] copied to [%s].\n", html.EscapeString(srcSheetName), html.EscapeString(dstSheetName))
80 | 	return mcp.NewToolResultText(result), nil
81 | }
82 | 
```

--------------------------------------------------------------------------------
/internal/tools/excel_create_table.go:
--------------------------------------------------------------------------------

```go
 1 | package tools
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"fmt"
 6 | 	"html"
 7 | 
 8 | 	z "github.com/Oudwins/zog"
 9 | 	"github.com/mark3labs/mcp-go/mcp"
10 | 	"github.com/mark3labs/mcp-go/server"
11 | 	excel "github.com/negokaz/excel-mcp-server/internal/excel"
12 | 	imcp "github.com/negokaz/excel-mcp-server/internal/mcp"
13 | )
14 | 
15 | type ExcelCreateTableArguments struct {
16 | 	FileAbsolutePath string `zog:"fileAbsolutePath"`
17 | 	SheetName        string `zog:"sheetName"`
18 | 	Range            string `zog:"range"`
19 | 	TableName        string `zog:"tableName"`
20 | }
21 | 
22 | var excelCreateTableArgumentsSchema = z.Struct(z.Shape{
23 | 	"fileAbsolutePath": z.String().Test(AbsolutePathTest()).Required(),
24 | 	"sheetName":        z.String().Required(),
25 | 	"range":            z.String(),
26 | 	"tableName":        z.String().Required(),
27 | })
28 | 
29 | func AddExcelCreateTableTool(server *server.MCPServer) {
30 | 	server.AddTool(mcp.NewTool("excel_create_table",
31 | 		mcp.WithDescription("Create a table in the Excel sheet"),
32 | 		mcp.WithString("fileAbsolutePath",
33 | 			mcp.Required(),
34 | 			mcp.Description("Absolute path to the Excel file"),
35 | 		),
36 | 		mcp.WithString("sheetName",
37 | 			mcp.Required(),
38 | 			mcp.Description("Sheet name where the table is created"),
39 | 		),
40 | 		mcp.WithString("range",
41 | 			mcp.Description("Range to be a table (e.g., \"A1:C10\")"),
42 | 		),
43 | 		mcp.WithString("tableName",
44 | 			mcp.Required(),
45 | 			mcp.Description("Table name to be created"),
46 | 		),
47 | 	), handleCreateTable)
48 | }
49 | 
50 | func handleCreateTable(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
51 | 	args := ExcelCreateTableArguments{}
52 | 	if issues := excelCreateTableArgumentsSchema.Parse(request.Params.Arguments, &args); len(issues) != 0 {
53 | 		return imcp.NewToolResultZogIssueMap(issues), nil
54 | 	}
55 | 	return createTable(args.FileAbsolutePath, args.SheetName, args.Range, args.TableName)
56 | }
57 | 
58 | func createTable(fileAbsolutePath string, sheetName string, tableRange string, tableName string) (*mcp.CallToolResult, error) {
59 | 	workbook, release, err := excel.OpenFile(fileAbsolutePath)
60 | 	if err != nil {
61 | 		return nil, err
62 | 	}
63 | 	defer release()
64 | 
65 | 	worksheet, err := workbook.FindSheet(sheetName)
66 | 	if err != nil {
67 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
68 | 	}
69 | 	defer worksheet.Release()
70 | 	if err := worksheet.AddTable(tableRange, tableName); err != nil {
71 | 		return nil, err
72 | 	}
73 | 	if err := workbook.Save(); err != nil {
74 | 		return nil, err
75 | 	}
76 | 
77 | 	result := "# Notice\n"
78 | 	result += fmt.Sprintf("backend: %s\n", workbook.GetBackendName())
79 | 	result += fmt.Sprintf("Table [%s] created.\n", html.EscapeString(tableName))
80 | 	return mcp.NewToolResultText(result), nil
81 | }
82 | 
```

--------------------------------------------------------------------------------
/internal/tools/excel_describe_sheets.go:
--------------------------------------------------------------------------------

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"encoding/json"
  6 | 
  7 | 	z "github.com/Oudwins/zog"
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | 	"github.com/mark3labs/mcp-go/server"
 10 | 	"github.com/negokaz/excel-mcp-server/internal/excel"
 11 | 	imcp "github.com/negokaz/excel-mcp-server/internal/mcp"
 12 | )
 13 | 
 14 | type ExcelDescribeSheetsArguments struct {
 15 | 	FileAbsolutePath string `zog:"fileAbsolutePath"`
 16 | }
 17 | 
 18 | var excelDescribeSheetsArgumentsSchema = z.Struct(z.Shape{
 19 | 	"fileAbsolutePath": z.String().Test(AbsolutePathTest()).Required(),
 20 | })
 21 | 
 22 | func AddExcelDescribeSheetsTool(server *server.MCPServer) {
 23 | 	server.AddTool(mcp.NewTool("excel_describe_sheets",
 24 | 		mcp.WithDescription("List all sheet information of specified Excel file"),
 25 | 		mcp.WithString("fileAbsolutePath",
 26 | 			mcp.Required(),
 27 | 			mcp.Description("Absolute path to the Excel file"),
 28 | 		),
 29 | 	), handleDescribeSheets)
 30 | }
 31 | 
 32 | func handleDescribeSheets(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 33 | 	args := ExcelDescribeSheetsArguments{}
 34 | 	issues := excelDescribeSheetsArgumentsSchema.Parse(request.Params.Arguments, &args)
 35 | 	if len(issues) != 0 {
 36 | 		return imcp.NewToolResultZogIssueMap(issues), nil
 37 | 	}
 38 | 	return describeSheets(args.FileAbsolutePath)
 39 | }
 40 | 
 41 | type Response struct {
 42 | 	Backend string      `json:"backend"`
 43 | 	Sheets  []Worksheet `json:"sheets"`
 44 | }
 45 | type Worksheet struct {
 46 | 	Name         string       `json:"name"`
 47 | 	UsedRange    string       `json:"usedRange"`
 48 | 	Tables       []Table      `json:"tables"`
 49 | 	PivotTables  []PivotTable `json:"pivotTables"`
 50 | 	PagingRanges []string     `json:"pagingRanges"`
 51 | }
 52 | 
 53 | type Table struct {
 54 | 	Name  string `json:"name"`
 55 | 	Range string `json:"range"`
 56 | }
 57 | 
 58 | type PivotTable struct {
 59 | 	Name  string `json:"name"`
 60 | 	Range string `json:"range"`
 61 | }
 62 | 
 63 | func describeSheets(fileAbsolutePath string) (*mcp.CallToolResult, error) {
 64 | 	config, issues := LoadConfig()
 65 | 	if issues != nil {
 66 | 		return imcp.NewToolResultZogIssueMap(issues), nil
 67 | 	}
 68 | 	workbook, release, err := excel.OpenFile(fileAbsolutePath)
 69 | 	defer release()
 70 | 	if err != nil {
 71 | 		return nil, err
 72 | 	}
 73 | 
 74 | 	sheetList, err := workbook.GetSheets()
 75 | 	if err != nil {
 76 | 		return nil, err
 77 | 	}
 78 | 	worksheets := make([]Worksheet, len(sheetList))
 79 | 	for i, sheet := range sheetList {
 80 | 		defer sheet.Release()
 81 | 		name, err := sheet.Name()
 82 | 		if err != nil {
 83 | 			return nil, err
 84 | 		}
 85 | 		usedRange, err := sheet.GetDimention()
 86 | 		if err != nil {
 87 | 			return nil, err
 88 | 		}
 89 | 		tables, err := sheet.GetTables()
 90 | 		if err != nil {
 91 | 			return nil, err
 92 | 		}
 93 | 		tableList := make([]Table, len(tables))
 94 | 		for i, table := range tables {
 95 | 			tableList[i] = Table{
 96 | 				Name:  table.Name,
 97 | 				Range: table.Range,
 98 | 			}
 99 | 		}
100 | 		pivotTables, err := sheet.GetPivotTables()
101 | 		if err != nil {
102 | 			return nil, err
103 | 		}
104 | 		pivotTableList := make([]PivotTable, len(pivotTables))
105 | 		for i, pivotTable := range pivotTables {
106 | 			pivotTableList[i] = PivotTable{
107 | 				Name:  pivotTable.Name,
108 | 				Range: pivotTable.Range,
109 | 			}
110 | 		}
111 | 		var pagingRanges []string
112 | 		strategy, err := sheet.GetPagingStrategy(config.EXCEL_MCP_PAGING_CELLS_LIMIT)
113 | 		if err == nil {
114 | 			pagingService := excel.NewPagingRangeService(strategy)
115 | 			pagingRanges = pagingService.GetPagingRanges()
116 | 		}
117 | 		worksheets[i] = Worksheet{
118 | 			Name:         name,
119 | 			UsedRange:    usedRange,
120 | 			Tables:       tableList,
121 | 			PivotTables:  pivotTableList,
122 | 			PagingRanges: pagingRanges,
123 | 		}
124 | 	}
125 | 	response := Response{
126 | 		Backend: workbook.GetBackendName(),
127 | 		Sheets:  worksheets,
128 | 	}
129 | 	jsonBytes, err := json.MarshalIndent(response, "", "  ")
130 | 	if err != nil {
131 | 		return nil, err
132 | 	}
133 | 
134 | 	return mcp.NewToolResultText(string(jsonBytes)), nil
135 | }
136 | 
```

--------------------------------------------------------------------------------
/internal/tools/excel_screen_capture.go:
--------------------------------------------------------------------------------

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 
  7 | 	z "github.com/Oudwins/zog"
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | 	"github.com/mark3labs/mcp-go/server"
 10 | 	"github.com/negokaz/excel-mcp-server/internal/excel"
 11 | 	imcp "github.com/negokaz/excel-mcp-server/internal/mcp"
 12 | )
 13 | 
 14 | type ExcelScreenCaptureArguments struct {
 15 | 	FileAbsolutePath string `zog:"fileAbsolutePath"`
 16 | 	SheetName        string `zog:"sheetName"`
 17 | 	Range            string `zog:"range"`
 18 | }
 19 | 
 20 | var ExcelScreenCaptureArgumentsSchema = z.Struct(z.Shape{
 21 | 	"fileAbsolutePath": z.String().Test(AbsolutePathTest()).Required(),
 22 | 	"sheetName":        z.String().Required(),
 23 | 	"range":            z.String(),
 24 | })
 25 | 
 26 | func AddExcelScreenCaptureTool(server *server.MCPServer) {
 27 | 	server.AddTool(mcp.NewTool("excel_screen_capture",
 28 | 		mcp.WithDescription("[Windows only] Take a screenshot of the Excel sheet with pagination."),
 29 | 		mcp.WithString("fileAbsolutePath",
 30 | 			mcp.Required(),
 31 | 			mcp.Description("Absolute path to the Excel file"),
 32 | 		),
 33 | 		mcp.WithString("sheetName",
 34 | 			mcp.Required(),
 35 | 			mcp.Description("Sheet name in the Excel file"),
 36 | 		),
 37 | 		mcp.WithString("range",
 38 | 			mcp.Description("Range of cells to read in the Excel sheet (e.g., \"A1:C10\"). [default: first paging range]"),
 39 | 		),
 40 | 	), handleScreenCapture)
 41 | }
 42 | 
 43 | func handleScreenCapture(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 44 | 	args := ExcelScreenCaptureArguments{}
 45 | 	issues := ExcelScreenCaptureArgumentsSchema.Parse(request.Params.Arguments, &args)
 46 | 	if len(issues) != 0 {
 47 | 		return imcp.NewToolResultZogIssueMap(issues), nil
 48 | 	}
 49 | 	return readSheetImage(args.FileAbsolutePath, args.SheetName, args.Range)
 50 | }
 51 | 
 52 | func readSheetImage(fileAbsolutePath string, sheetName string, rangeStr string) (*mcp.CallToolResult, error) {
 53 | 	workbook, releaseWorkbook, err := excel.NewExcelOle(fileAbsolutePath)
 54 | 	defer releaseWorkbook()
 55 | 	if err != nil {
 56 | 		workbook, releaseWorkbook, err = excel.NewExcelOleWithNewObject(fileAbsolutePath)
 57 | 		defer releaseWorkbook()
 58 | 		if err != nil {
 59 | 			return imcp.NewToolResultInvalidArgumentError(fmt.Errorf("failed to open workbook: %w", err).Error()), nil
 60 | 		}
 61 | 	}
 62 | 
 63 | 	worksheet, err := workbook.FindSheet(sheetName)
 64 | 	if err != nil {
 65 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
 66 | 	}
 67 | 	defer worksheet.Release()
 68 | 
 69 | 	pagingStrategy, err := worksheet.GetPagingStrategy(5000)
 70 | 	if err != nil {
 71 | 		return nil, err
 72 | 	}
 73 | 	pagingService := excel.NewPagingRangeService(pagingStrategy)
 74 | 
 75 | 	allRanges := pagingService.GetPagingRanges()
 76 | 	if len(allRanges) == 0 {
 77 | 		return imcp.NewToolResultInvalidArgumentError("no range available to read"), nil
 78 | 	}
 79 | 
 80 | 	var currentRange string
 81 | 	if rangeStr == "" && len(allRanges) > 0 {
 82 | 		// range が指定されていない場合は最初の Range を使用
 83 | 		currentRange = allRanges[0]
 84 | 	} else {
 85 | 		// range が指定されている場合は指定された範囲を使用
 86 | 		currentRange = rangeStr
 87 | 	}
 88 | 	// Find next paging range if current range matches a paging range
 89 | 	nextRange := pagingService.FindNextRange(allRanges, currentRange)
 90 | 
 91 | 	base64image, err := worksheet.CapturePicture(currentRange)
 92 | 	if err != nil {
 93 | 		return nil, fmt.Errorf("failed to copy range to image: %w", err)
 94 | 	}
 95 | 
 96 | 	text := "# Metadata\n"
 97 | 	text += fmt.Sprintf("- backend: %s\n", workbook.GetBackendName())
 98 | 	text += fmt.Sprintf("- sheet name: %s\n", sheetName)
 99 | 	text += fmt.Sprintf("- read range: %s\n", currentRange)
100 | 	text += "# Notice\n"
101 | 	if nextRange != "" {
102 | 		text += "This sheet has more ranges.\n"
103 | 		text += "To read the next range, you should specify 'range' argument as follows.\n"
104 | 		text += fmt.Sprintf("`{ \"range\": \"%s\" }`", nextRange)
105 | 	} else {
106 | 		text += "This is the last range or no more ranges available.\n"
107 | 	}
108 | 
109 | 	// 結果を返却
110 | 	return mcp.NewToolResultImage(
111 | 		text,
112 | 		base64image,
113 | 		"image/png",
114 | 	), nil
115 | }
116 | 
```

--------------------------------------------------------------------------------
/.devcontainer/init-firewall.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash
  2 | set -euo pipefail  # Exit on error, undefined vars, and pipeline failures
  3 | IFS=$'\n\t'       # Stricter word splitting
  4 | 
  5 | # Flush existing rules and delete existing ipsets
  6 | iptables -F
  7 | iptables -X
  8 | iptables -t nat -F
  9 | iptables -t nat -X
 10 | iptables -t mangle -F
 11 | iptables -t mangle -X
 12 | ipset destroy allowed-domains 2>/dev/null || true
 13 | 
 14 | # First allow DNS and localhost before any restrictions
 15 | # Allow outbound DNS
 16 | iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
 17 | # Allow inbound DNS responses
 18 | iptables -A INPUT -p udp --sport 53 -j ACCEPT
 19 | # Allow outbound SSH
 20 | iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
 21 | # Allow inbound SSH responses
 22 | iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
 23 | # Allow localhost
 24 | iptables -A INPUT -i lo -j ACCEPT
 25 | iptables -A OUTPUT -o lo -j ACCEPT
 26 | 
 27 | # Create ipset with CIDR support
 28 | ipset create allowed-domains hash:net
 29 | 
 30 | # Fetch GitHub meta information and aggregate + add their IP ranges
 31 | echo "Fetching GitHub IP ranges..."
 32 | gh_ranges=$(curl -s https://api.github.com/meta)
 33 | if [ -z "$gh_ranges" ]; then
 34 |     echo "ERROR: Failed to fetch GitHub IP ranges"
 35 |     exit 1
 36 | fi
 37 | 
 38 | if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
 39 |     echo "ERROR: GitHub API response missing required fields"
 40 |     exit 1
 41 | fi
 42 | 
 43 | echo "Processing GitHub IPs..."
 44 | while read -r cidr; do
 45 |     if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
 46 |         echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
 47 |         exit 1
 48 |     fi
 49 |     echo "Adding GitHub range $cidr"
 50 |     ipset add allowed-domains "$cidr"
 51 | done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)
 52 | 
 53 | # Resolve and add other allowed domains
 54 | for domain in \
 55 |     "registry.npmjs.org" \
 56 |     "api.anthropic.com" \
 57 |     "sentry.io" \
 58 |     "statsig.anthropic.com" \
 59 |     "statsig.com"; do
 60 |     echo "Resolving $domain..."
 61 |     ips=$(dig +short A "$domain")
 62 |     if [ -z "$ips" ]; then
 63 |         echo "ERROR: Failed to resolve $domain"
 64 |         exit 1
 65 |     fi
 66 | 
 67 |     while read -r ip; do
 68 |         if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
 69 |             echo "ERROR: Invalid IP from DNS for $domain: $ip"
 70 |             exit 1
 71 |         fi
 72 |         echo "Adding $ip for $domain"
 73 |         ipset add allowed-domains "$ip"
 74 |     done < <(echo "$ips")
 75 | done
 76 | 
 77 | # Get host IP from default route
 78 | HOST_IP=$(ip route | grep default | cut -d" " -f3)
 79 | if [ -z "$HOST_IP" ]; then
 80 |     echo "ERROR: Failed to detect host IP"
 81 |     exit 1
 82 | fi
 83 | 
 84 | HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
 85 | echo "Host network detected as: $HOST_NETWORK"
 86 | 
 87 | # Set up remaining iptables rules
 88 | iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
 89 | iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
 90 | 
 91 | # Set default policies to DROP first
 92 | iptables -P INPUT DROP
 93 | iptables -P FORWARD DROP
 94 | iptables -P OUTPUT DROP
 95 | 
 96 | # First allow established connections for already approved traffic
 97 | iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
 98 | iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
 99 | 
100 | # Then allow only specific outbound traffic to allowed domains
101 | iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
102 | 
103 | echo "Firewall configuration complete"
104 | echo "Verifying firewall rules..."
105 | if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
106 |     echo "ERROR: Firewall verification failed - was able to reach https://example.com"
107 |     exit 1
108 | else
109 |     echo "Firewall verification passed - unable to reach https://example.com as expected"
110 | fi
111 | 
112 | # Verify GitHub API access
113 | if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
114 |     echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
115 |     exit 1
116 | else
117 |     echo "Firewall verification passed - able to reach https://api.github.com as expected"
118 | fi
119 | 
```

--------------------------------------------------------------------------------
/internal/tools/excel_write_to_sheet.go:
--------------------------------------------------------------------------------

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 
  7 | 	z "github.com/Oudwins/zog"
  8 | 	"github.com/mark3labs/mcp-go/mcp"
  9 | 	"github.com/mark3labs/mcp-go/server"
 10 | 	"github.com/negokaz/excel-mcp-server/internal/excel"
 11 | 	imcp "github.com/negokaz/excel-mcp-server/internal/mcp"
 12 | 	"github.com/xuri/excelize/v2"
 13 | )
 14 | 
 15 | type ExcelWriteToSheetArguments struct {
 16 | 	FileAbsolutePath string     `zog:"fileAbsolutePath"`
 17 | 	SheetName        string     `zog:"sheetName"`
 18 | 	NewSheet         bool       `zog:"newSheet"`
 19 | 	Range            string     `zog:"range"`
 20 | 	Values           [][]string `zog:"values"`
 21 | }
 22 | 
 23 | var excelWriteToSheetArgumentsSchema = z.Struct(z.Shape{
 24 | 	"fileAbsolutePath": z.String().Test(AbsolutePathTest()).Required(),
 25 | 	"sheetName":        z.String().Required(),
 26 | 	"newSheet":         z.Bool().Required().Default(false),
 27 | 	"range":            z.String().Required(),
 28 | 	"values":           z.Slice(z.Slice(z.String())).Required(),
 29 | })
 30 | 
 31 | func AddExcelWriteToSheetTool(server *server.MCPServer) {
 32 | 	server.AddTool(mcp.NewTool("excel_write_to_sheet",
 33 | 		mcp.WithDescription("Write values to the Excel sheet"),
 34 | 		mcp.WithString("fileAbsolutePath",
 35 | 			mcp.Required(),
 36 | 			mcp.Description("Absolute path to the Excel file"),
 37 | 		),
 38 | 		mcp.WithString("sheetName",
 39 | 			mcp.Required(),
 40 | 			mcp.Description("Sheet name in the Excel file"),
 41 | 		),
 42 | 		mcp.WithBoolean("newSheet",
 43 | 			mcp.Required(),
 44 | 			mcp.Description("Create a new sheet if true, otherwise write to the existing sheet"),
 45 | 		),
 46 | 		mcp.WithString("range",
 47 | 			mcp.Required(),
 48 | 			mcp.Description("Range of cells in the Excel sheet (e.g., \"A1:C10\")"),
 49 | 		),
 50 | 		mcp.WithArray("values",
 51 | 			mcp.Required(),
 52 | 			mcp.Description("Values to write to the Excel sheet. If the value is a formula, it should start with \"=\""),
 53 | 			mcp.Items(map[string]any{
 54 | 				"type": "array",
 55 | 				"items": map[string]any{
 56 | 					"anyOf": []any{
 57 | 						map[string]any{
 58 | 							"type": "string",
 59 | 						},
 60 | 						map[string]any{
 61 | 							"type": "number",
 62 | 						},
 63 | 						map[string]any{
 64 | 							"type": "boolean",
 65 | 						},
 66 | 						map[string]any{
 67 | 							"type": "null",
 68 | 						},
 69 | 					},
 70 | 				},
 71 | 			}),
 72 | 		),
 73 | 	), handleWriteToSheet)
 74 | }
 75 | 
 76 | func handleWriteToSheet(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 77 | 	args := ExcelWriteToSheetArguments{}
 78 | 	issues := excelWriteToSheetArgumentsSchema.Parse(request.Params.Arguments, &args)
 79 | 	if len(issues) != 0 {
 80 | 		return imcp.NewToolResultZogIssueMap(issues), nil
 81 | 	}
 82 | 
 83 | 	// zog が any type のスキーマをサポートしていないため、自力で実装
 84 | 	valuesArg, ok := request.GetArguments()["values"].([]any)
 85 | 	if !ok {
 86 | 		return imcp.NewToolResultInvalidArgumentError("values must be a 2D array"), nil
 87 | 	}
 88 | 	values := make([][]any, len(valuesArg))
 89 | 	for i, v := range valuesArg {
 90 | 		value, ok := v.([]any)
 91 | 		if !ok {
 92 | 			return imcp.NewToolResultInvalidArgumentError("values must be a 2D array"), nil
 93 | 		}
 94 | 		values[i] = value
 95 | 	}
 96 | 
 97 | 	return writeSheet(args.FileAbsolutePath, args.SheetName, args.NewSheet, args.Range, values)
 98 | }
 99 | 
100 | func writeSheet(fileAbsolutePath string, sheetName string, newSheet bool, rangeStr string, values [][]any) (*mcp.CallToolResult, error) {
101 | 	workbook, closeFn, err := excel.OpenFile(fileAbsolutePath)
102 | 	if err != nil {
103 | 		return nil, err
104 | 	}
105 | 	defer closeFn()
106 | 
107 | 	startCol, startRow, endCol, endRow, err := excel.ParseRange(rangeStr)
108 | 	if err != nil {
109 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
110 | 	}
111 | 
112 | 	// データの整合性チェック
113 | 	rangeRowSize := endRow - startRow + 1
114 | 	if len(values) != rangeRowSize {
115 | 		return imcp.NewToolResultInvalidArgumentError(fmt.Sprintf("number of rows in data (%d) does not match range size (%d)", len(values), rangeRowSize)), nil
116 | 	}
117 | 
118 | 	if newSheet {
119 | 		if err := workbook.CreateNewSheet(sheetName); err != nil {
120 | 			return nil, err
121 | 		}
122 | 	}
123 | 
124 | 	// シートの取得
125 | 	worksheet, err := workbook.FindSheet(sheetName)
126 | 	if err != nil {
127 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
128 | 	}
129 | 	defer worksheet.Release()
130 | 
131 | 	// データの書き込み
132 | 	wroteFormula := false
133 | 	for i, row := range values {
134 | 		rangeColumnSize := endCol - startCol + 1
135 | 		if len(row) != rangeColumnSize {
136 | 			return imcp.NewToolResultInvalidArgumentError(fmt.Sprintf("number of columns in row %d (%d) does not match range size (%d)", i, len(row), rangeColumnSize)), nil
137 | 		}
138 | 		for j, cellValue := range row {
139 | 			cell, err := excelize.CoordinatesToCellName(startCol+j, startRow+i)
140 | 			if err != nil {
141 | 				return nil, err
142 | 			}
143 | 			if cellStr, ok := cellValue.(string); ok && isFormula(cellStr) {
144 | 				// if cellValue is formula, set it as formula
145 | 				err = worksheet.SetFormula(cell, cellStr)
146 | 				wroteFormula = true
147 | 			} else {
148 | 				// if cellValue is not formula, set it as value
149 | 				err = worksheet.SetValue(cell, cellValue)
150 | 			}
151 | 			if err != nil {
152 | 				return nil, err
153 | 			}
154 | 		}
155 | 	}
156 | 
157 | 	if err := workbook.Save(); err != nil {
158 | 		return nil, err
159 | 	}
160 | 
161 | 	// HTMLテーブルの生成
162 | 	var table *string
163 | 	if wroteFormula {
164 | 		table, err = CreateHTMLTableOfFormula(worksheet, startCol, startRow, endCol, endRow)
165 | 	} else {
166 | 		table, err = CreateHTMLTableOfValues(worksheet, startCol, startRow, endCol, endRow)
167 | 	}
168 | 	if err != nil {
169 | 		return nil, err
170 | 	}
171 | 	html := "<h2>Written Sheet</h2>\n"
172 | 	html += *table + "\n"
173 | 	html += "<h2>Metadata</h2>\n"
174 | 	html += "<ul>\n"
175 | 	html += fmt.Sprintf("<li>backend: %s</li>\n", workbook.GetBackendName())
176 | 	html += fmt.Sprintf("<li>sheet name: %s</li>\n", sheetName)
177 | 	html += fmt.Sprintf("<li>read range: %s</li>\n", rangeStr)
178 | 	html += "</ul>\n"
179 | 	html += "<h2>Notice</h2>\n"
180 | 	html += "<p>Values wrote successfully.</p>\n"
181 | 
182 | 	return mcp.NewToolResultText(html), nil
183 | }
184 | 
185 | func isFormula(value string) bool {
186 | 	return len(value) > 0 && value[0] == '='
187 | }
188 | 
```

--------------------------------------------------------------------------------
/internal/tools/excel_read_sheet.go:
--------------------------------------------------------------------------------

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"html"
  7 | 
  8 | 	z "github.com/Oudwins/zog"
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | 	"github.com/mark3labs/mcp-go/server"
 11 | 	excel "github.com/negokaz/excel-mcp-server/internal/excel"
 12 | 	imcp "github.com/negokaz/excel-mcp-server/internal/mcp"
 13 | )
 14 | 
 15 | type ExcelReadSheetArguments struct {
 16 | 	FileAbsolutePath string `zog:"fileAbsolutePath"`
 17 | 	SheetName        string `zog:"sheetName"`
 18 | 	Range            string `zog:"range"`
 19 | 	ShowFormula      bool   `zog:"showFormula"`
 20 | 	ShowStyle        bool   `zog:"showStyle"`
 21 | }
 22 | 
 23 | var excelReadSheetArgumentsSchema = z.Struct(z.Shape{
 24 | 	"fileAbsolutePath": z.String().Test(AbsolutePathTest()).Required(),
 25 | 	"sheetName":        z.String().Required(),
 26 | 	"range":            z.String(),
 27 | 	"showFormula":      z.Bool().Default(false),
 28 | 	"showStyle":        z.Bool().Default(false),
 29 | })
 30 | 
 31 | func AddExcelReadSheetTool(server *server.MCPServer) {
 32 | 	server.AddTool(mcp.NewTool("excel_read_sheet",
 33 | 		mcp.WithDescription("Read values from Excel sheet with pagination."),
 34 | 		mcp.WithString("fileAbsolutePath",
 35 | 			mcp.Required(),
 36 | 			mcp.Description("Absolute path to the Excel file"),
 37 | 		),
 38 | 		mcp.WithString("sheetName",
 39 | 			mcp.Required(),
 40 | 			mcp.Description("Sheet name in the Excel file"),
 41 | 		),
 42 | 		mcp.WithString("range",
 43 | 			mcp.Description("Range of cells to read in the Excel sheet (e.g., \"A1:C10\"). [default: first paging range]"),
 44 | 		),
 45 | 		mcp.WithBoolean("showFormula",
 46 | 			mcp.Description("Show formula instead of value"),
 47 | 		),
 48 | 		mcp.WithBoolean("showStyle",
 49 | 			mcp.Description("Show style information for cells"),
 50 | 		),
 51 | 	), handleReadSheet)
 52 | }
 53 | 
 54 | func handleReadSheet(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 55 | 	args := ExcelReadSheetArguments{}
 56 | 	if issues := excelReadSheetArgumentsSchema.Parse(request.Params.Arguments, &args); len(issues) != 0 {
 57 | 		return imcp.NewToolResultZogIssueMap(issues), nil
 58 | 	}
 59 | 	return readSheet(args.FileAbsolutePath, args.SheetName, args.Range, args.ShowFormula, args.ShowStyle)
 60 | }
 61 | 
 62 | func readSheet(fileAbsolutePath string, sheetName string, valueRange string, showFormula bool, showStyle bool) (*mcp.CallToolResult, error) {
 63 | 	config, issues := LoadConfig()
 64 | 	if issues != nil {
 65 | 		return imcp.NewToolResultZogIssueMap(issues), nil
 66 | 	}
 67 | 
 68 | 	workbook, release, err := excel.OpenFile(fileAbsolutePath)
 69 | 	if err != nil {
 70 | 		return nil, err
 71 | 	}
 72 | 	defer release()
 73 | 
 74 | 	worksheet, err := workbook.FindSheet(sheetName)
 75 | 	if err != nil {
 76 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
 77 | 	}
 78 | 	defer worksheet.Release()
 79 | 
 80 | 	// ページング戦略の初期化
 81 | 	strategy, err := worksheet.GetPagingStrategy(config.EXCEL_MCP_PAGING_CELLS_LIMIT)
 82 | 	if err != nil {
 83 | 		return nil, err
 84 | 	}
 85 | 	pagingService := excel.NewPagingRangeService(strategy)
 86 | 
 87 | 	// 利用可能な範囲を取得
 88 | 	allRanges := pagingService.GetPagingRanges()
 89 | 	if len(allRanges) == 0 {
 90 | 		return imcp.NewToolResultInvalidArgumentError("no range available to read"), nil
 91 | 	}
 92 | 
 93 | 	// 現在の範囲を決定
 94 | 	currentRange := valueRange
 95 | 	if currentRange == "" && len(allRanges) > 0 {
 96 | 		currentRange = allRanges[0]
 97 | 	}
 98 | 
 99 | 	// Find next paging range if current range matches a paging range
100 | 	nextRange := pagingService.FindNextRange(allRanges, currentRange)
101 | 	// Validate the current range against the used range
102 | 	usedRange, err := worksheet.GetDimention()
103 | 	if err != nil {
104 | 		return nil, err
105 | 	}
106 | 	if err := validateRangeWithinUsedRange(currentRange, usedRange); err != nil {
107 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
108 | 	}
109 | 
110 | 	// 範囲を解析
111 | 	startCol, startRow, endCol, endRow, err := excel.ParseRange(currentRange)
112 | 	if err != nil {
113 | 		return nil, err
114 | 	}
115 | 
116 | 	// HTMLテーブルの生成
117 | 	var table *string
118 | 	if showStyle {
119 | 		if showFormula {
120 | 			table, err = CreateHTMLTableOfFormulaWithStyle(worksheet, startCol, startRow, endCol, endRow)
121 | 		} else {
122 | 			table, err = CreateHTMLTableOfValuesWithStyle(worksheet, startCol, startRow, endCol, endRow)
123 | 		}
124 | 	} else {
125 | 		if showFormula {
126 | 			table, err = CreateHTMLTableOfFormula(worksheet, startCol, startRow, endCol, endRow)
127 | 		} else {
128 | 			table, err = CreateHTMLTableOfValues(worksheet, startCol, startRow, endCol, endRow)
129 | 		}
130 | 	}
131 | 	if err != nil {
132 | 		return nil, err
133 | 	}
134 | 
135 | 	result := "<h2>Read Sheet</h2>\n"
136 | 	result += *table + "\n"
137 | 	result += "<h2>Metadata</h2>\n"
138 | 	result += "<ul>\n"
139 | 	result += fmt.Sprintf("<li>backend: %s</li>\n", workbook.GetBackendName())
140 | 	result += fmt.Sprintf("<li>sheet name: %s</li>\n", html.EscapeString(sheetName))
141 | 	result += fmt.Sprintf("<li>read range: %s</li>\n", currentRange)
142 | 	result += "</ul>\n"
143 | 	result += "<h2>Notice</h2>\n"
144 | 	if nextRange != "" {
145 | 		result += "<p>This sheet has more ranges.</p>\n"
146 | 		result += "<p>To read the next range, you should specify 'range' argument as follows.</p>\n"
147 | 		result += fmt.Sprintf("<code>{ \"range\": \"%s\" }</code>\n", nextRange)
148 | 	} else {
149 | 		result += "<p>This is the last range or no more ranges available.</p>\n"
150 | 	}
151 | 	return mcp.NewToolResultText(result), nil
152 | }
153 | 
154 | func validateRangeWithinUsedRange(targetRange, usedRange string) error {
155 | 	// Parse target range
156 | 	targetStartCol, targetStartRow, targetEndCol, targetEndRow, err := excel.ParseRange(targetRange)
157 | 	if err != nil {
158 | 		return fmt.Errorf("failed to parse target range: %w", err)
159 | 	}
160 | 
161 | 	// Parse used range
162 | 	usedStartCol, usedStartRow, usedEndCol, usedEndRow, err := excel.ParseRange(usedRange)
163 | 	if err != nil {
164 | 		return fmt.Errorf("failed to parse used range: %w", err)
165 | 	}
166 | 
167 | 	// Check if target range is within used range
168 | 	if targetStartCol < usedStartCol || targetStartRow < usedStartRow ||
169 | 		targetEndCol > usedEndCol || targetEndRow > usedEndRow {
170 | 		return fmt.Errorf("range is outside of used range: %s is not within %s", targetRange, usedRange)
171 | 	}
172 | 
173 | 	return nil
174 | }
175 | 
```

--------------------------------------------------------------------------------
/docs/design/excel-style-schema.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Excel Style Structure Definition
  2 | 
  3 | This document presents JsonSchema definitions for exchanging Excel styles through MCP (Model Context Protocol), based on the Excelize library's style API.
  4 | 
  5 | ## Target Style Elements
  6 | 
  7 | - Border
  8 | - Font
  9 | - Fill
 10 | - NumFmt (Number Format)
 11 | - DecimalPlaces
 12 | 
 13 | ## JsonSchema Definition
 14 | 
 15 | ### ExcelStyle Structure
 16 | 
 17 | ```json
 18 | {
 19 |   "$schema": "http://json-schema.org/draft-07/schema#",
 20 |   "type": "object",
 21 |   "title": "ExcelStyle",
 22 |   "description": "Excel cell style configuration",
 23 |   "properties": {
 24 |     "border": {
 25 |       "type": "array",
 26 |       "description": "Border configuration for cell edges",
 27 |       "items": {
 28 |         "$ref": "#/definitions/Border"
 29 |       }
 30 |     },
 31 |     "font": {
 32 |       "$ref": "#/definitions/Font",
 33 |       "description": "Font configuration"
 34 |     },
 35 |     "fill": {
 36 |       "$ref": "#/definitions/Fill",
 37 |       "description": "Fill pattern and color configuration"
 38 |     },
 39 |     "numFmt": {
 40 |       "type": "string",
 41 |       "description": "Custom number format string",
 42 |       "example": "#,##0.00"
 43 |     },
 44 |     "decimalPlaces": {
 45 |       "type": "integer",
 46 |       "description": "Number of decimal places (0-30)",
 47 |       "minimum": 0,
 48 |       "maximum": 30
 49 |     }
 50 |   },
 51 |   "definitions": {
 52 |     "Border": {
 53 |       "type": "object",
 54 |       "description": "Border style configuration",
 55 |       "properties": {
 56 |         "type": {
 57 |           "type": "string",
 58 |           "description": "Border position",
 59 |           "enum": ["left", "right", "top", "bottom", "diagonalDown", "diagonalUp"]
 60 |         },
 61 |         "color": {
 62 |           "type": "string",
 63 |           "description": "Border color in hex format",
 64 |           "pattern": "^#[0-9A-Fa-f]{6}$",
 65 |           "example": "#000000"
 66 |         },
 67 |         "style": {
 68 |           "type": "string",
 69 |           "description": "Border style",
 70 |           "enum": ["none", "continuous", "dash", "dashDot", "dashDotDot", "dot", "double", "hair", "medium", "mediumDash", "mediumDashDot", "mediumDashDotDot", "slantDashDot", "thick"]
 71 |         }
 72 |       },
 73 |       "required": ["type"],
 74 |       "additionalProperties": false
 75 |     },
 76 |     "Font": {
 77 |       "type": "object",
 78 |       "description": "Font style configuration",
 79 |       "properties": {
 80 |         "bold": {
 81 |           "type": "boolean",
 82 |           "description": "Bold text"
 83 |         },
 84 |         "italic": {
 85 |           "type": "boolean",
 86 |           "description": "Italic text"
 87 |         },
 88 |         "underline": {
 89 |           "type": "string",
 90 |           "description": "Underline style",
 91 |           "enum": ["none", "single", "double", "singleAccounting", "doubleAccounting"]
 92 |         },
 93 |         "size": {
 94 |           "type": "number",
 95 |           "description": "Font size in points",
 96 |           "minimum": 1,
 97 |           "maximum": 409
 98 |         },
 99 |         "strike": {
100 |           "type": "boolean",
101 |           "description": "Strikethrough text"
102 |         },
103 |         "color": {
104 |           "type": "string",
105 |           "description": "Font color in hex format",
106 |           "pattern": "^#[0-9A-Fa-f]{6}$",
107 |           "example": "#000000"
108 |         },
109 |         "vertAlign": {
110 |           "type": "string",
111 |           "description": "Vertical alignment",
112 |           "enum": ["baseline", "superscript", "subscript"]
113 |         }
114 |       },
115 |       "additionalProperties": false
116 |     },
117 |     "Fill": {
118 |       "type": "object",
119 |       "description": "Fill pattern and color configuration",
120 |       "properties": {
121 |         "type": {
122 |           "type": "string",
123 |           "description": "Fill type",
124 |           "enum": ["gradient", "pattern"]
125 |         },
126 |         "pattern": {
127 |           "type": "string",
128 |           "description": "Pattern style",
129 |           "enum": ["none", "solid", "mediumGray", "darkGray", "lightGray", "darkHorizontal", "darkVertical", "darkDown", "darkUp", "darkGrid", "darkTrellis", "lightHorizontal", "lightVertical", "lightDown", "lightUp", "lightGrid", "lightTrellis", "gray125", "gray0625"]
130 |         },
131 |         "color": {
132 |           "type": "array",
133 |           "description": "Fill colors in hex format",
134 |           "items": {
135 |             "type": "string",
136 |             "pattern": "^#[0-9A-Fa-f]{6}$",
137 |             "example": "#FFFFFF"
138 |           }
139 |         },
140 |         "shading": {
141 |           "type": "string",
142 |           "description": "Gradient shading direction",
143 |           "enum": ["horizontal", "vertical", "diagonalDown", "diagonalUp", "fromCenter", "fromCorner"]
144 |         }
145 |       },
146 |       "additionalProperties": false
147 |     }
148 |   }
149 | }
150 | ```
151 | 
152 | ## Usage Examples
153 | 
154 | ### Basic Style Configuration
155 | 
156 | ```json
157 | {
158 |   "font": {
159 |     "bold": true,
160 |     "size": 12,
161 |     "color": "#000000"
162 |   },
163 |   "fill": {
164 |     "type": "pattern",
165 |     "pattern": "solid",
166 |     "color": ["#FFFF00"]
167 |   }
168 | }
169 | ```
170 | 
171 | ### Style with Borders
172 | 
173 | ```json
174 | {
175 |   "border": [
176 |     {
177 |       "type": "top",
178 |       "style": "continuous",
179 |       "color": "#000000"
180 |     },
181 |     {
182 |       "type": "bottom",
183 |       "style": "continuous",
184 |       "color": "#000000"
185 |     }
186 |   ],
187 |   "font": {
188 |     "size": 10
189 |   }
190 | }
191 | ```
192 | 
193 | ### Style with Number Format
194 | 
195 | ```json
196 | {
197 |   "numFmt": "#,##0.00",
198 |   "decimalPlaces": 2,
199 |   "font": {
200 |     "size": 11
201 |   }
202 | }
203 | ```
204 | 
205 | ## Implementation Notes
206 | 
207 | 1. **Required Fields**: Only Border's `type` field is required; all others are optional
208 | 2. **Color Format**: Hexadecimal color codes (#RRGGBB format)
209 | 3. **Numeric Limits**: 
210 |    - `decimalPlaces`: Range 0-30
211 |    - `border.style`: String identifiers (none, continuous, dash, etc.)
212 |    - `fill.pattern`: String identifiers (none, solid, mediumGray, etc.)
213 |    - `fill.shading`: String identifiers (horizontal, vertical, etc.)
214 |    - `font.size`: Range 1-409
215 | 4. **Testing**: After implementation, test with actual Excel files
216 | 
217 | ## Correspondence with Excelize
218 | 
219 | This definition is based on the style structure of `github.com/xuri/excelize/v2 v2.9.0` and maintains compatibility with Excelize's API specifications.
```

--------------------------------------------------------------------------------
/internal/tools/excel_format_range.go:
--------------------------------------------------------------------------------

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"fmt"
  6 | 	"regexp"
  7 | 
  8 | 	z "github.com/Oudwins/zog"
  9 | 	"github.com/mark3labs/mcp-go/mcp"
 10 | 	"github.com/mark3labs/mcp-go/server"
 11 | 	"github.com/negokaz/excel-mcp-server/internal/excel"
 12 | 	imcp "github.com/negokaz/excel-mcp-server/internal/mcp"
 13 | 	"github.com/xuri/excelize/v2"
 14 | )
 15 | 
 16 | type ExcelFormatRangeArguments struct {
 17 | 	FileAbsolutePath string               `zog:"fileAbsolutePath"`
 18 | 	SheetName        string               `zog:"sheetName"`
 19 | 	Range            string               `zog:"range"`
 20 | 	Styles           [][]*excel.CellStyle `zog:"styles"`
 21 | }
 22 | 
 23 | var colorPattern, _ = regexp.Compile("^#[0-9A-Fa-f]{6}$")
 24 | 
 25 | var excelFormatRangeArgumentsSchema = z.Struct(z.Shape{
 26 | 	"fileAbsolutePath": z.String().Test(AbsolutePathTest()).Required(),
 27 | 	"sheetName":        z.String().Required(),
 28 | 	"range":            z.String().Required(),
 29 | 	"styles": z.Slice(z.Slice(
 30 | 		z.Ptr(z.Struct(z.Shape{
 31 | 			"border": z.Slice(z.Struct(z.Shape{
 32 | 				"type":  z.StringLike[excel.BorderType]().OneOf(excel.BorderTypeValues()).Required(),
 33 | 				"color": z.String().Match(colorPattern).Default("#000000"),
 34 | 				"style": z.StringLike[excel.BorderStyle]().OneOf(excel.BorderStyleValues()).Default(excel.BorderStyleContinuous),
 35 | 			})).Default([]excel.Border{}),
 36 | 			"font": z.Ptr(z.Struct(z.Shape{
 37 | 				"bold":      z.Ptr(z.Bool()),
 38 | 				"italic":    z.Ptr(z.Bool()),
 39 | 				"underline": z.Ptr(z.StringLike[excel.FontUnderline]().OneOf(excel.FontUnderlineValues())),
 40 | 				"size":      z.Ptr(z.Int().GTE(1).LTE(409)),
 41 | 				"strike":    z.Ptr(z.Bool()),
 42 | 				"color":     z.Ptr(z.String().Match(colorPattern)),
 43 | 				"vertAlign": z.Ptr(z.StringLike[excel.FontVertAlign]().OneOf(excel.FontVertAlignValues())),
 44 | 			})),
 45 | 			"fill": z.Ptr(z.Struct(z.Shape{
 46 | 				"type":    z.StringLike[excel.FillType]().OneOf(excel.FillTypeValues()).Default(excel.FillTypePattern),
 47 | 				"pattern": z.StringLike[excel.FillPattern]().OneOf(excel.FillPatternValues()).Default(excel.FillPatternSolid),
 48 | 				"color":   z.Slice(z.String().Match(colorPattern)).Default([]string{}),
 49 | 				"shading": z.Ptr(z.StringLike[excel.FillShading]().OneOf(excel.FillShadingValues())),
 50 | 			})),
 51 | 			"numFmt":        z.Ptr(z.String()),
 52 | 			"decimalPlaces": z.Ptr(z.Int().GTE(0).LTE(30)),
 53 | 		}),
 54 | 		))).Required(),
 55 | })
 56 | 
 57 | func AddExcelFormatRangeTool(server *server.MCPServer) {
 58 | 	server.AddTool(mcp.NewTool("excel_format_range",
 59 | 		mcp.WithDescription("Format cells in the Excel sheet with style information"),
 60 | 		mcp.WithString("fileAbsolutePath",
 61 | 			mcp.Required(),
 62 | 			mcp.Description("Absolute path to the Excel file"),
 63 | 		),
 64 | 		mcp.WithString("sheetName",
 65 | 			mcp.Required(),
 66 | 			mcp.Description("Sheet name in the Excel file"),
 67 | 		),
 68 | 		mcp.WithString("range",
 69 | 			mcp.Required(),
 70 | 			mcp.Description("Range of cells in the Excel sheet (e.g., \"A1:C3\")"),
 71 | 		),
 72 | 		mcp.WithArray("styles",
 73 | 			mcp.Required(),
 74 | 			mcp.Description("2D array of style objects for each cell. If a cell does not change style, use null. The number of items of the array must match the range size."),
 75 | 			mcp.Items(map[string]any{
 76 | 				"type": "array",
 77 | 				"items": map[string]any{
 78 | 					"anyOf": []any{
 79 | 						map[string]any{
 80 | 							"type":        "object",
 81 | 							"description": "Style object for the cell",
 82 | 							"properties": map[string]any{
 83 | 								"border": map[string]any{
 84 | 									"type": "array",
 85 | 									"items": map[string]any{
 86 | 										"type": "object",
 87 | 										"properties": map[string]any{
 88 | 											"type": map[string]any{
 89 | 												"type": "string",
 90 | 												"enum": excel.BorderTypeValues(),
 91 | 											},
 92 | 											"color": map[string]any{
 93 | 												"type":    "string",
 94 | 												"pattern": colorPattern.String(),
 95 | 											},
 96 | 											"style": map[string]any{
 97 | 												"type": "string",
 98 | 												"enum": excel.BorderStyleValues(),
 99 | 											},
100 | 										},
101 | 										"required": []string{"type"},
102 | 									},
103 | 								},
104 | 								"font": map[string]any{
105 | 									"type": "object",
106 | 									"properties": map[string]any{
107 | 										"bold":   map[string]any{"type": "boolean"},
108 | 										"italic": map[string]any{"type": "boolean"},
109 | 										"underline": map[string]any{
110 | 											"type": "string",
111 | 											"enum": excel.FontUnderlineValues(),
112 | 										},
113 | 										"size": map[string]any{
114 | 											"type":    "number",
115 | 											"minimum": 1,
116 | 											"maximum": 409,
117 | 										},
118 | 										"strike": map[string]any{"type": "boolean"},
119 | 										"color": map[string]any{
120 | 											"type":    "string",
121 | 											"pattern": colorPattern.String(),
122 | 										},
123 | 										"vertAlign": map[string]any{
124 | 											"type": "string",
125 | 											"enum": excel.FontVertAlignValues(),
126 | 										},
127 | 									},
128 | 								},
129 | 								"fill": map[string]any{
130 | 									"type": "object",
131 | 									"properties": map[string]any{
132 | 										"type": map[string]any{
133 | 											"type": "string",
134 | 											"enum": []string{"gradient", "pattern"},
135 | 										},
136 | 										"pattern": map[string]any{
137 | 											"type": "string",
138 | 											"enum": excel.FillPatternValues(),
139 | 										},
140 | 										"color": map[string]any{
141 | 											"type": "array",
142 | 											"items": map[string]any{
143 | 												"type":    "string",
144 | 												"pattern": colorPattern.String(),
145 | 											},
146 | 										},
147 | 										"shading": map[string]any{
148 | 											"type": "string",
149 | 											"enum": excel.FillShadingValues(),
150 | 										},
151 | 									},
152 | 									"required": []string{"type", "pattern", "color"},
153 | 								},
154 | 								"numFmt": map[string]any{
155 | 									"type":        "string",
156 | 									"description": "Custom number format string",
157 | 								},
158 | 								"decimalPlaces": map[string]any{
159 | 									"type":    "integer",
160 | 									"minimum": 0,
161 | 									"maximum": 30,
162 | 								},
163 | 							},
164 | 						},
165 | 						map[string]any{
166 | 							"type":        "null",
167 | 							"description": "No style applied to this cell",
168 | 						},
169 | 					},
170 | 				},
171 | 			}),
172 | 		),
173 | 	), handleFormatRange)
174 | }
175 | 
176 | func handleFormatRange(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
177 | 	args := ExcelFormatRangeArguments{}
178 | 	issues := excelFormatRangeArgumentsSchema.Parse(request.Params.Arguments, &args)
179 | 	if len(issues) != 0 {
180 | 		return imcp.NewToolResultZogIssueMap(issues), nil
181 | 	}
182 | 	return formatRange(args.FileAbsolutePath, args.SheetName, args.Range, args.Styles)
183 | }
184 | 
185 | func formatRange(fileAbsolutePath string, sheetName string, rangeStr string, styles [][]*excel.CellStyle) (*mcp.CallToolResult, error) {
186 | 	workbook, closeFn, err := excel.OpenFile(fileAbsolutePath)
187 | 	if err != nil {
188 | 		return nil, err
189 | 	}
190 | 	defer closeFn()
191 | 
192 | 	startCol, startRow, endCol, endRow, err := excel.ParseRange(rangeStr)
193 | 	if err != nil {
194 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
195 | 	}
196 | 
197 | 	// Check data consistency
198 | 	rangeRowSize := endRow - startRow + 1
199 | 	if len(styles) != rangeRowSize {
200 | 		return imcp.NewToolResultInvalidArgumentError(fmt.Sprintf("number of style rows (%d) does not match range size (%d)", len(styles), rangeRowSize)), nil
201 | 	}
202 | 
203 | 	// Get worksheet
204 | 	worksheet, err := workbook.FindSheet(sheetName)
205 | 	if err != nil {
206 | 		return imcp.NewToolResultInvalidArgumentError(err.Error()), nil
207 | 	}
208 | 	defer worksheet.Release()
209 | 
210 | 	// Apply styles to each cell
211 | 	for i, styleRow := range styles {
212 | 		rangeColumnSize := endCol - startCol + 1
213 | 		if len(styleRow) != rangeColumnSize {
214 | 			return imcp.NewToolResultInvalidArgumentError(fmt.Sprintf("number of style columns in row %d (%d) does not match range size (%d)", i, len(styleRow), rangeColumnSize)), nil
215 | 		}
216 | 
217 | 		for j, style := range styleRow {
218 | 			cell, err := excelize.CoordinatesToCellName(startCol+j, startRow+i)
219 | 			if err != nil {
220 | 				return nil, err
221 | 			}
222 | 			if style != nil {
223 | 				if err := worksheet.SetCellStyle(cell, style); err != nil {
224 | 					return nil, fmt.Errorf("failed to set style for cell %s: %w", cell, err)
225 | 				}
226 | 			}
227 | 		}
228 | 	}
229 | 
230 | 	if err := workbook.Save(); err != nil {
231 | 		return nil, err
232 | 	}
233 | 
234 | 	// Create response HTML
235 | 	html := "<h2>Formatted Range</h2>\n"
236 | 	html += fmt.Sprintf("<p>Successfully applied styles to range %s in sheet %s</p>\n", rangeStr, sheetName)
237 | 	html += "<h2>Metadata</h2>\n"
238 | 	html += "<ul>\n"
239 | 	html += fmt.Sprintf("<li>backend: %s</li>\n", workbook.GetBackendName())
240 | 	html += fmt.Sprintf("<li>sheet name: %s</li>\n", sheetName)
241 | 	html += fmt.Sprintf("<li>formatted range: %s</li>\n", rangeStr)
242 | 	html += fmt.Sprintf("<li>cells processed: %d</li>\n", (endRow-startRow+1)*(endCol-startCol+1))
243 | 	html += "</ul>\n"
244 | 	html += "<h2>Notice</h2>\n"
245 | 	html += "<p>Cell styles applied successfully.</p>\n"
246 | 
247 | 	return mcp.NewToolResultText(html), nil
248 | }
249 | 
```

--------------------------------------------------------------------------------
/internal/excel/excel.go:
--------------------------------------------------------------------------------

```go
  1 | package excel
  2 | 
  3 | import (
  4 | 	"github.com/xuri/excelize/v2"
  5 | )
  6 | 
  7 | type Excel interface {
  8 | 	// GetBackendName returns the backend used to manipulate the Excel file.
  9 | 	GetBackendName() string
 10 | 	// GetSheets returns a list of all worksheets in the Excel file.
 11 | 	GetSheets() ([]Worksheet, error)
 12 | 	// FindSheet finds a sheet by its name and returns a Worksheet.
 13 | 	FindSheet(sheetName string) (Worksheet, error)
 14 | 	// CreateNewSheet creates a new sheet with the specified name.
 15 | 	CreateNewSheet(sheetName string) error
 16 | 	// CopySheet copies a sheet from one to another.
 17 | 	CopySheet(srcSheetName, destSheetName string) error
 18 | 	// Save saves the Excel file.
 19 | 	Save() error
 20 | }
 21 | 
 22 | type Worksheet interface {
 23 | 	// Release releases the worksheet resources.
 24 | 	Release()
 25 | 	// Name returns the name of the worksheet.
 26 | 	Name() (string, error)
 27 | 	// GetTable returns a tables in this worksheet.
 28 | 	GetTables() ([]Table, error)
 29 | 	// GetPivotTable returns a pivot tables in this worksheet.
 30 | 	GetPivotTables() ([]PivotTable, error)
 31 | 	// SetValue sets a value in the specified cell.
 32 | 	SetValue(cell string, value any) error
 33 | 	// SetFormula sets a formula in the specified cell.
 34 | 	SetFormula(cell string, formula string) error
 35 | 	// GetValue gets the value from the specified cell.
 36 | 	GetValue(cell string) (string, error)
 37 | 	// GetFormula gets the formula from the specified cell.
 38 | 	GetFormula(cell string) (string, error)
 39 | 	// GetDimention gets the dimension of the worksheet.
 40 | 	GetDimention() (string, error)
 41 | 	// GetPagingStrategy returns the paging strategy for the worksheet.
 42 | 	// The pageSize parameter is used to determine the max size of each page.
 43 | 	GetPagingStrategy(pageSize int) (PagingStrategy, error)
 44 | 	// CapturePicture returns base64 encoded image data of the specified range.
 45 | 	CapturePicture(captureRange string) (string, error)
 46 | 	// AddTable adds a table to this worksheet.
 47 | 	AddTable(tableRange, tableName string) error
 48 | 	// GetCellStyle gets style information for the specified cell.
 49 | 	GetCellStyle(cell string) (*CellStyle, error)
 50 | 	// SetCellStyle sets style for the specified cell.
 51 | 	SetCellStyle(cell string, style *CellStyle) error
 52 | }
 53 | 
 54 | type Table struct {
 55 | 	Name  string
 56 | 	Range string
 57 | }
 58 | 
 59 | type PivotTable struct {
 60 | 	Name  string
 61 | 	Range string
 62 | }
 63 | 
 64 | type CellStyle struct {
 65 | 	Border        []Border   `yaml:"border,omitempty"`
 66 | 	Font          *FontStyle `yaml:"font,omitempty"`
 67 | 	Fill          *FillStyle `yaml:"fill,omitempty"`
 68 | 	NumFmt        *string    `yaml:"numFmt,omitempty"`
 69 | 	DecimalPlaces *int       `yaml:"decimalPlaces,omitempty"`
 70 | }
 71 | 
 72 | type Border struct {
 73 | 	Type  BorderType  `yaml:"type"`
 74 | 	Style BorderStyle `yaml:"style,omitempty"`
 75 | 	Color string      `yaml:"color,omitempty"`
 76 | }
 77 | 
 78 | type FontStyle struct {
 79 | 	Bold      *bool          `yaml:"bold,omitempty"`
 80 | 	Italic    *bool          `yaml:"italic,omitempty"`
 81 | 	Underline *FontUnderline `yaml:"underline,omitempty"`
 82 | 	Size      *int           `yaml:"size,omitempty"`
 83 | 	Strike    *bool          `yaml:"strike,omitempty"`
 84 | 	Color     *string        `yaml:"color,omitempty"`
 85 | 	VertAlign *FontVertAlign `yaml:"vertAlign,omitempty"`
 86 | }
 87 | 
 88 | type FillStyle struct {
 89 | 	Type    FillType     `yaml:"type,omitempty"`
 90 | 	Pattern FillPattern  `yaml:"pattern,omitempty"`
 91 | 	Color   []string     `yaml:"color,omitempty"`
 92 | 	Shading *FillShading `yaml:"shading,omitempty"`
 93 | }
 94 | 
 95 | // OpenFile opens an Excel file and returns an Excel interface.
 96 | // It first tries to open the file using OLE automation, and if that fails,
 97 | // it tries to using the excelize library.
 98 | func OpenFile(absoluteFilePath string) (Excel, func(), error) {
 99 | 	ole, releaseFn, err := NewExcelOle(absoluteFilePath)
100 | 	if err == nil {
101 | 		return ole, releaseFn, nil
102 | 	}
103 | 	// If OLE fails, try Excelize
104 | 	workbook, err := excelize.OpenFile(absoluteFilePath)
105 | 	if err != nil {
106 | 		return nil, func() {}, err
107 | 	}
108 | 	excelize := NewExcelizeExcel(workbook)
109 | 	return excelize, func() {
110 | 		workbook.Close()
111 | 	}, nil
112 | }
113 | 
114 | // BorderType represents border direction
115 | type BorderType string
116 | 
117 | const (
118 | 	BorderTypeLeft         BorderType = "left"
119 | 	BorderTypeRight        BorderType = "right"
120 | 	BorderTypeTop          BorderType = "top"
121 | 	BorderTypeBottom       BorderType = "bottom"
122 | 	BorderTypeDiagonalDown BorderType = "diagonalDown"
123 | 	BorderTypeDiagonalUp   BorderType = "diagonalUp"
124 | )
125 | 
126 | func (b BorderType) String() string {
127 | 	return string(b)
128 | }
129 | 
130 | func (b BorderType) MarshalText() ([]byte, error) {
131 | 	return []byte(b.String()), nil
132 | }
133 | 
134 | func BorderTypeValues() []BorderType {
135 | 	return []BorderType{
136 | 		BorderTypeLeft,
137 | 		BorderTypeRight,
138 | 		BorderTypeTop,
139 | 		BorderTypeBottom,
140 | 		BorderTypeDiagonalDown,
141 | 		BorderTypeDiagonalUp,
142 | 	}
143 | }
144 | 
145 | // BorderStyle represents border style constants
146 | type BorderStyle string
147 | 
148 | const (
149 | 	BorderStyleNone             BorderStyle = "none"
150 | 	BorderStyleContinuous       BorderStyle = "continuous"
151 | 	BorderStyleDash             BorderStyle = "dash"
152 | 	BorderStyleDot              BorderStyle = "dot"
153 | 	BorderStyleDouble           BorderStyle = "double"
154 | 	BorderStyleDashDot          BorderStyle = "dashDot"
155 | 	BorderStyleDashDotDot       BorderStyle = "dashDotDot"
156 | 	BorderStyleSlantDashDot     BorderStyle = "slantDashDot"
157 | 	BorderStyleMediumDashDot    BorderStyle = "mediumDashDot"
158 | 	BorderStyleMediumDashDotDot BorderStyle = "mediumDashDotDot"
159 | )
160 | 
161 | func (b BorderStyle) String() string {
162 | 	return string(b)
163 | }
164 | 
165 | func (b BorderStyle) MarshalText() ([]byte, error) {
166 | 	return []byte(b.String()), nil
167 | }
168 | 
169 | func BorderStyleValues() []BorderStyle {
170 | 	return []BorderStyle{
171 | 		BorderStyleNone,
172 | 		BorderStyleContinuous,
173 | 		BorderStyleDash,
174 | 		BorderStyleDot,
175 | 		BorderStyleDouble,
176 | 		BorderStyleDashDot,
177 | 		BorderStyleDashDotDot,
178 | 		BorderStyleSlantDashDot,
179 | 		BorderStyleMediumDashDot,
180 | 		BorderStyleMediumDashDotDot,
181 | 	}
182 | }
183 | 
184 | // FontUnderline represents underline styles for font
185 | type FontUnderline string
186 | 
187 | const (
188 | 	FontUnderlineNone             FontUnderline = "none"
189 | 	FontUnderlineSingle           FontUnderline = "single"
190 | 	FontUnderlineDouble           FontUnderline = "double"
191 | 	FontUnderlineSingleAccounting FontUnderline = "singleAccounting"
192 | 	FontUnderlineDoubleAccounting FontUnderline = "doubleAccounting"
193 | )
194 | 
195 | func (f FontUnderline) String() string {
196 | 	return string(f)
197 | }
198 | func (f FontUnderline) MarshalText() ([]byte, error) {
199 | 	return []byte(f.String()), nil
200 | }
201 | 
202 | func FontUnderlineValues() []FontUnderline {
203 | 	return []FontUnderline{
204 | 		FontUnderlineNone,
205 | 		FontUnderlineSingle,
206 | 		FontUnderlineDouble,
207 | 		FontUnderlineSingleAccounting,
208 | 		FontUnderlineDoubleAccounting,
209 | 	}
210 | }
211 | 
212 | // FontVertAlign represents vertical alignment options for font styles
213 | type FontVertAlign string
214 | 
215 | const (
216 | 	FontVertAlignBaseline    FontVertAlign = "baseline"
217 | 	FontVertAlignSuperscript FontVertAlign = "superscript"
218 | 	FontVertAlignSubscript   FontVertAlign = "subscript"
219 | )
220 | 
221 | func (v FontVertAlign) String() string {
222 | 	return string(v)
223 | }
224 | 
225 | func (v FontVertAlign) MarshalText() ([]byte, error) {
226 | 	return []byte(v.String()), nil
227 | }
228 | 
229 | func FontVertAlignValues() []FontVertAlign {
230 | 	return []FontVertAlign{
231 | 		FontVertAlignBaseline,
232 | 		FontVertAlignSuperscript,
233 | 		FontVertAlignSubscript,
234 | 	}
235 | }
236 | 
237 | // FillType represents fill types for cell styles
238 | type FillType string
239 | 
240 | const (
241 | 	FillTypeGradient FillType = "gradient"
242 | 	FillTypePattern  FillType = "pattern"
243 | )
244 | 
245 | func (f FillType) String() string {
246 | 	return string(f)
247 | }
248 | 
249 | func (f FillType) MarshalText() ([]byte, error) {
250 | 	return []byte(f.String()), nil
251 | }
252 | 
253 | func FillTypeValues() []FillType {
254 | 	return []FillType{
255 | 		FillTypeGradient,
256 | 		FillTypePattern,
257 | 	}
258 | }
259 | 
260 | // FillPattern represents fill pattern constants
261 | type FillPattern string
262 | 
263 | const (
264 | 	FillPatternNone            FillPattern = "none"
265 | 	FillPatternSolid           FillPattern = "solid"
266 | 	FillPatternMediumGray      FillPattern = "mediumGray"
267 | 	FillPatternDarkGray        FillPattern = "darkGray"
268 | 	FillPatternLightGray       FillPattern = "lightGray"
269 | 	FillPatternDarkHorizontal  FillPattern = "darkHorizontal"
270 | 	FillPatternDarkVertical    FillPattern = "darkVertical"
271 | 	FillPatternDarkDown        FillPattern = "darkDown"
272 | 	FillPatternDarkUp          FillPattern = "darkUp"
273 | 	FillPatternDarkGrid        FillPattern = "darkGrid"
274 | 	FillPatternDarkTrellis     FillPattern = "darkTrellis"
275 | 	FillPatternLightHorizontal FillPattern = "lightHorizontal"
276 | 	FillPatternLightVertical   FillPattern = "lightVertical"
277 | 	FillPatternLightDown       FillPattern = "lightDown"
278 | 	FillPatternLightUp         FillPattern = "lightUp"
279 | 	FillPatternLightGrid       FillPattern = "lightGrid"
280 | 	FillPatternLightTrellis    FillPattern = "lightTrellis"
281 | 	FillPatternGray125         FillPattern = "gray125"
282 | 	FillPatternGray0625        FillPattern = "gray0625"
283 | )
284 | 
285 | func (f FillPattern) String() string {
286 | 	return string(f)
287 | }
288 | 
289 | func (f FillPattern) MarshalText() ([]byte, error) {
290 | 	return []byte(f.String()), nil
291 | }
292 | 
293 | func FillPatternValues() []FillPattern {
294 | 	return []FillPattern{
295 | 		FillPatternNone,
296 | 		FillPatternSolid,
297 | 		FillPatternMediumGray,
298 | 		FillPatternDarkGray,
299 | 		FillPatternLightGray,
300 | 		FillPatternDarkHorizontal,
301 | 		FillPatternDarkVertical,
302 | 		FillPatternDarkDown,
303 | 		FillPatternDarkUp,
304 | 		FillPatternDarkGrid,
305 | 		FillPatternDarkTrellis,
306 | 		FillPatternLightHorizontal,
307 | 		FillPatternLightVertical,
308 | 		FillPatternLightDown,
309 | 		FillPatternLightUp,
310 | 		FillPatternLightGrid,
311 | 		FillPatternLightTrellis,
312 | 		FillPatternGray125,
313 | 		FillPatternGray0625,
314 | 	}
315 | }
316 | 
317 | // FillShading represents fill shading constants
318 | type FillShading string
319 | 
320 | const (
321 | 	FillShadingHorizontal   FillShading = "horizontal"
322 | 	FillShadingVertical     FillShading = "vertical"
323 | 	FillShadingDiagonalDown FillShading = "diagonalDown"
324 | 	FillShadingDiagonalUp   FillShading = "diagonalUp"
325 | 	FillShadingFromCenter   FillShading = "fromCenter"
326 | 	FillShadingFromCorner   FillShading = "fromCorner"
327 | )
328 | 
329 | func (f FillShading) String() string {
330 | 	return string(f)
331 | }
332 | 
333 | func (f FillShading) MarshalText() ([]byte, error) {
334 | 	return []byte(f.String()), nil
335 | }
336 | 
337 | func FillShadingValues() []FillShading {
338 | 	return []FillShading{
339 | 		FillShadingHorizontal,
340 | 		FillShadingVertical,
341 | 		FillShadingDiagonalDown,
342 | 		FillShadingDiagonalUp,
343 | 		FillShadingFromCenter,
344 | 		FillShadingFromCorner,
345 | 	}
346 | }
347 | 
```

--------------------------------------------------------------------------------
/internal/tools/common.go:
--------------------------------------------------------------------------------

```go
  1 | package tools
  2 | 
  3 | import (
  4 | 	"crypto/md5"
  5 | 	"fmt"
  6 | 	"html"
  7 | 	"path/filepath"
  8 | 	"slices"
  9 | 	"strconv"
 10 | 	"strings"
 11 | 
 12 | 	"github.com/goccy/go-yaml"
 13 | 	"github.com/xuri/excelize/v2"
 14 | 
 15 | 	"github.com/negokaz/excel-mcp-server/internal/excel"
 16 | 
 17 | 	z "github.com/Oudwins/zog"
 18 | )
 19 | 
 20 | type StyleRegistry struct {
 21 | 	// Border styles
 22 | 	borderStyles   map[string]string // styleID -> YAML string
 23 | 	borderHashToID map[string]string // styleHash -> styleID
 24 | 	borderCounter  int
 25 | 
 26 | 	// Font styles
 27 | 	fontStyles   map[string]string // styleID -> YAML string
 28 | 	fontHashToID map[string]string // styleHash -> styleID
 29 | 	fontCounter  int
 30 | 
 31 | 	// Fill styles
 32 | 	fillStyles   map[string]string // styleID -> YAML string
 33 | 	fillHashToID map[string]string // styleHash -> styleID
 34 | 	fillCounter  int
 35 | 
 36 | 	// Number format styles
 37 | 	numFmtStyles   map[string]string // styleID -> NumFmt
 38 | 	numFmtHashToID map[string]string // styleHash -> styleID
 39 | 	numFmtCounter  int
 40 | 
 41 | 	// Decimal places styles
 42 | 	decimalStyles   map[string]string // styleID -> YAML string
 43 | 	decimalHashToID map[string]string // styleHash -> styleID
 44 | 	decimalCounter  int
 45 | }
 46 | 
 47 | func NewStyleRegistry() *StyleRegistry {
 48 | 	return &StyleRegistry{
 49 | 		borderStyles:    make(map[string]string),
 50 | 		borderHashToID:  make(map[string]string),
 51 | 		borderCounter:   0,
 52 | 		fontStyles:      make(map[string]string),
 53 | 		fontHashToID:    make(map[string]string),
 54 | 		fontCounter:     0,
 55 | 		fillStyles:      make(map[string]string),
 56 | 		fillHashToID:    make(map[string]string),
 57 | 		fillCounter:     0,
 58 | 		numFmtStyles:    make(map[string]string),
 59 | 		numFmtHashToID:  make(map[string]string),
 60 | 		numFmtCounter:   0,
 61 | 		decimalStyles:   make(map[string]string),
 62 | 		decimalHashToID: make(map[string]string),
 63 | 		decimalCounter:  0,
 64 | 	}
 65 | }
 66 | 
 67 | func (sr *StyleRegistry) RegisterStyle(cellStyle *excel.CellStyle) []string {
 68 | 	if cellStyle == nil || sr.isEmptyStyle(cellStyle) {
 69 | 		return []string{}
 70 | 	}
 71 | 
 72 | 	var styleIDs []string
 73 | 
 74 | 	// Register border style
 75 | 	if len(cellStyle.Border) > 0 {
 76 | 		if borderID := sr.RegisterBorderStyle(cellStyle.Border); borderID != "" {
 77 | 			styleIDs = append(styleIDs, borderID)
 78 | 		}
 79 | 	}
 80 | 
 81 | 	// Register font style
 82 | 	if cellStyle.Font != nil {
 83 | 		if fontID := sr.RegisterFontStyle(cellStyle.Font); fontID != "" {
 84 | 			styleIDs = append(styleIDs, fontID)
 85 | 		}
 86 | 	}
 87 | 
 88 | 	// Register fill style
 89 | 	if cellStyle.Fill != nil && cellStyle.Fill.Type != "" {
 90 | 		if fillID := sr.RegisterFillStyle(cellStyle.Fill); fillID != "" {
 91 | 			styleIDs = append(styleIDs, fillID)
 92 | 		}
 93 | 	}
 94 | 
 95 | 	// Register number format style
 96 | 	if cellStyle.NumFmt != nil && *cellStyle.NumFmt != "" {
 97 | 		if numFmtID := sr.RegisterNumFmtStyle(*cellStyle.NumFmt); numFmtID != "" {
 98 | 			styleIDs = append(styleIDs, numFmtID)
 99 | 		}
100 | 	}
101 | 
102 | 	// Register decimal places style
103 | 	if cellStyle.DecimalPlaces != nil && *cellStyle.DecimalPlaces != 0 {
104 | 		if decimalID := sr.RegisterDecimalStyle(*cellStyle.DecimalPlaces); decimalID != "" {
105 | 			styleIDs = append(styleIDs, decimalID)
106 | 		}
107 | 	}
108 | 
109 | 	return styleIDs
110 | }
111 | 
112 | func (sr *StyleRegistry) isEmptyStyle(style *excel.CellStyle) bool {
113 | 	if len(style.Border) > 0 || style.Font != nil || (style.NumFmt != nil && *style.NumFmt != "") || (style.DecimalPlaces != nil && *style.DecimalPlaces != 0) {
114 | 		return false
115 | 	}
116 | 	if style.Fill != nil && style.Fill.Type != "" {
117 | 		return false
118 | 	}
119 | 	return true
120 | }
121 | 
122 | // calculateYamlHash calculates a hash for a YAML string
123 | func calculateYamlHash(yaml string) string {
124 | 	if yaml == "" {
125 | 		return ""
126 | 	}
127 | 	hash := md5.Sum([]byte(yaml))
128 | 	return fmt.Sprintf("%x", hash)[:8]
129 | }
130 | 
131 | // Individual style element registration methods
132 | func (sr *StyleRegistry) RegisterBorderStyle(borders []excel.Border) string {
133 | 	if len(borders) == 0 {
134 | 		return ""
135 | 	}
136 | 
137 | 	yamlStr := convertToYAMLFlow(borders)
138 | 	if yamlStr == "" {
139 | 		return ""
140 | 	}
141 | 
142 | 	styleHash := calculateYamlHash(yamlStr)
143 | 	if styleHash == "" {
144 | 		return ""
145 | 	}
146 | 
147 | 	if existingID, exists := sr.borderHashToID[styleHash]; exists {
148 | 		return existingID
149 | 	}
150 | 
151 | 	sr.borderCounter++
152 | 	styleID := fmt.Sprintf("b%d", sr.borderCounter)
153 | 	sr.borderStyles[styleID] = yamlStr
154 | 	sr.borderHashToID[styleHash] = styleID
155 | 
156 | 	return styleID
157 | }
158 | 
159 | func (sr *StyleRegistry) RegisterFontStyle(font *excel.FontStyle) string {
160 | 	if font == nil {
161 | 		return ""
162 | 	}
163 | 
164 | 	yamlStr := convertToYAMLFlow(font)
165 | 	if yamlStr == "" {
166 | 		return ""
167 | 	}
168 | 
169 | 	styleHash := calculateYamlHash(yamlStr)
170 | 	if styleHash == "" {
171 | 		return ""
172 | 	}
173 | 
174 | 	if existingID, exists := sr.fontHashToID[styleHash]; exists {
175 | 		return existingID
176 | 	}
177 | 
178 | 	sr.fontCounter++
179 | 	styleID := fmt.Sprintf("f%d", sr.fontCounter)
180 | 	sr.fontStyles[styleID] = yamlStr
181 | 	sr.fontHashToID[styleHash] = styleID
182 | 
183 | 	return styleID
184 | }
185 | 
186 | func (sr *StyleRegistry) RegisterFillStyle(fill *excel.FillStyle) string {
187 | 	if fill == nil || fill.Type == "" {
188 | 		return ""
189 | 	}
190 | 
191 | 	yamlStr := convertToYAMLFlow(fill)
192 | 	if yamlStr == "" {
193 | 		return ""
194 | 	}
195 | 
196 | 	styleHash := calculateYamlHash(yamlStr)
197 | 	if styleHash == "" {
198 | 		return ""
199 | 	}
200 | 
201 | 	if existingID, exists := sr.fillHashToID[styleHash]; exists {
202 | 		return existingID
203 | 	}
204 | 
205 | 	sr.fillCounter++
206 | 	styleID := fmt.Sprintf("l%d", sr.fillCounter)
207 | 	sr.fillStyles[styleID] = yamlStr
208 | 	sr.fillHashToID[styleHash] = styleID
209 | 
210 | 	return styleID
211 | }
212 | 
213 | func (sr *StyleRegistry) RegisterNumFmtStyle(numFmt string) string {
214 | 	if numFmt == "" {
215 | 		return ""
216 | 	}
217 | 
218 | 	styleHash := calculateYamlHash(numFmt)
219 | 	if styleHash == "" {
220 | 		return ""
221 | 	}
222 | 
223 | 	if existingID, exists := sr.numFmtHashToID[styleHash]; exists {
224 | 		return existingID
225 | 	}
226 | 
227 | 	sr.numFmtCounter++
228 | 	styleID := fmt.Sprintf("n%d", sr.numFmtCounter)
229 | 	sr.numFmtStyles[styleID] = numFmt
230 | 	sr.numFmtHashToID[styleHash] = styleID
231 | 
232 | 	return styleID
233 | }
234 | 
235 | func (sr *StyleRegistry) RegisterDecimalStyle(decimal int) string {
236 | 	if decimal == 0 {
237 | 		return ""
238 | 	}
239 | 
240 | 	yamlStr := convertToYAMLFlow(decimal)
241 | 	if yamlStr == "" {
242 | 		return ""
243 | 	}
244 | 
245 | 	styleHash := calculateYamlHash(yamlStr)
246 | 	if styleHash == "" {
247 | 		return ""
248 | 	}
249 | 
250 | 	if existingID, exists := sr.decimalHashToID[styleHash]; exists {
251 | 		return existingID
252 | 	}
253 | 
254 | 	sr.decimalCounter++
255 | 	styleID := fmt.Sprintf("d%d", sr.decimalCounter)
256 | 	sr.decimalStyles[styleID] = yamlStr
257 | 	sr.decimalHashToID[styleHash] = styleID
258 | 
259 | 	return styleID
260 | }
261 | 
262 | func (sr *StyleRegistry) GenerateStyleDefinitions() string {
263 | 	totalCount := len(sr.borderStyles) + len(sr.fontStyles) + len(sr.fillStyles) + len(sr.numFmtStyles) + len(sr.decimalStyles)
264 | 	if totalCount == 0 {
265 | 		return ""
266 | 	}
267 | 
268 | 	var result strings.Builder
269 | 	result.WriteString("<h2>Style Definitions</h2>\n")
270 | 	result.WriteString("<div class=\"style-definitions\">\n")
271 | 
272 | 	// Generate border style definitions
273 | 	result.WriteString(sr.generateStyleDefTag(sr.borderStyles, "border"))
274 | 
275 | 	// Generate font style definitions
276 | 	result.WriteString(sr.generateStyleDefTag(sr.fontStyles, "font"))
277 | 
278 | 	// Generate fill style definitions
279 | 	result.WriteString(sr.generateStyleDefTag(sr.fillStyles, "fill"))
280 | 
281 | 	// Generate number format style definitions
282 | 	result.WriteString(sr.generateStyleDefTag(sr.numFmtStyles, "numFmt"))
283 | 
284 | 	// Generate decimal places style definitions
285 | 	result.WriteString(sr.generateStyleDefTag(sr.decimalStyles, "decimalPlaces"))
286 | 
287 | 	result.WriteString("</div>\n\n")
288 | 	return result.String()
289 | }
290 | 
291 | func (sr *StyleRegistry) generateStyleDefTag(styles map[string]string, styleLabel string) string {
292 | 	if len(styles) == 0 {
293 | 		return ""
294 | 	}
295 | 
296 | 	var styleIDs []string
297 | 	for styleID := range styles {
298 | 		styleIDs = append(styleIDs, styleID)
299 | 	}
300 | 	sortStyleIDs(styleIDs)
301 | 
302 | 	var result strings.Builder
303 | 	for _, styleID := range styleIDs {
304 | 		yamlStr := styles[styleID]
305 | 		if yamlStr != "" {
306 | 			result.WriteString(fmt.Sprintf("<code class=\"style language-yaml\" id=\"%s\">%s: %s</code>\n", styleID, styleLabel, html.EscapeString(yamlStr)))
307 | 		}
308 | 	}
309 | 	return result.String()
310 | }
311 | 
312 | func sortStyleIDs(styleIDs []string) {
313 | 	slices.SortFunc(styleIDs, func(a, b string) int {
314 | 		// styleID must have number suffix after prefix
315 | 		ai, _ := strconv.Atoi(a[1:])
316 | 		bi, _ := strconv.Atoi(b[1:])
317 | 		return ai - bi
318 | 	})
319 | }
320 | 
321 | // Common function to convert any value to YAML flow format
322 | func convertToYAMLFlow(value any) string {
323 | 	if value == nil {
324 | 		return ""
325 | 	}
326 | 	yamlBytes, err := yaml.MarshalWithOptions(value, yaml.Flow(true), yaml.OmitEmpty())
327 | 	if err != nil {
328 | 		return ""
329 | 	}
330 | 	yamlStr := strings.TrimSpace(strings.ReplaceAll(string(yamlBytes), "\"", ""))
331 | 	return yamlStr
332 | }
333 | 
334 | func CreateHTMLTableOfValues(worksheet excel.Worksheet, startCol int, startRow int, endCol int, endRow int) (*string, error) {
335 | 	return createHTMLTable(startCol, startRow, endCol, endRow, func(cellRange string) (string, error) {
336 | 		return worksheet.GetValue(cellRange)
337 | 	})
338 | }
339 | 
340 | func CreateHTMLTableOfFormula(worksheet excel.Worksheet, startCol int, startRow int, endCol int, endRow int) (*string, error) {
341 | 	return createHTMLTable(startCol, startRow, endCol, endRow, func(cellRange string) (string, error) {
342 | 		return worksheet.GetFormula(cellRange)
343 | 	})
344 | }
345 | 
346 | // CreateHTMLTable creates a table data in HTML format
347 | func createHTMLTable(startCol int, startRow int, endCol int, endRow int, extractor func(cellRange string) (string, error)) (*string, error) {
348 | 	return createHTMLTableWithStyle(startCol, startRow, endCol, endRow, extractor, nil)
349 | }
350 | 
351 | func CreateHTMLTableOfValuesWithStyle(worksheet excel.Worksheet, startCol int, startRow int, endCol int, endRow int) (*string, error) {
352 | 	return createHTMLTableWithStyle(startCol, startRow, endCol, endRow,
353 | 		func(cellRange string) (string, error) {
354 | 			return worksheet.GetValue(cellRange)
355 | 		},
356 | 		func(cellRange string) (*excel.CellStyle, error) {
357 | 			return worksheet.GetCellStyle(cellRange)
358 | 		})
359 | }
360 | 
361 | func CreateHTMLTableOfFormulaWithStyle(worksheet excel.Worksheet, startCol int, startRow int, endCol int, endRow int) (*string, error) {
362 | 	return createHTMLTableWithStyle(startCol, startRow, endCol, endRow,
363 | 		func(cellRange string) (string, error) {
364 | 			return worksheet.GetFormula(cellRange)
365 | 		},
366 | 		func(cellRange string) (*excel.CellStyle, error) {
367 | 			return worksheet.GetCellStyle(cellRange)
368 | 		})
369 | }
370 | 
371 | func createHTMLTableWithStyle(startCol int, startRow int, endCol int, endRow int, extractor func(cellRange string) (string, error), styleExtractor func(cellRange string) (*excel.CellStyle, error)) (*string, error) {
372 | 	registry := NewStyleRegistry()
373 | 
374 | 	// データとスタイルを収集
375 | 	var result strings.Builder
376 | 	result.WriteString("<table>\n<tr><th></th>")
377 | 
378 | 	// 列アドレスの出力
379 | 	for col := startCol; col <= endCol; col++ {
380 | 		name, _ := excelize.ColumnNumberToName(col)
381 | 		result.WriteString(fmt.Sprintf("<th>%s</th>", name))
382 | 	}
383 | 	result.WriteString("</tr>\n")
384 | 
385 | 	// データの出力とスタイル登録
386 | 	for row := startRow; row <= endRow; row++ {
387 | 		result.WriteString("<tr>")
388 | 		result.WriteString(fmt.Sprintf("<th>%d</th>", row))
389 | 
390 | 		for col := startCol; col <= endCol; col++ {
391 | 			axis, _ := excelize.CoordinatesToCellName(col, row)
392 | 			value, _ := extractor(axis)
393 | 
394 | 			var tdTag string
395 | 			if styleExtractor != nil {
396 | 				cellStyle, err := styleExtractor(axis)
397 | 				if err == nil && cellStyle != nil {
398 | 					styleIDs := registry.RegisterStyle(cellStyle)
399 | 					if len(styleIDs) > 0 {
400 | 						tdTag = fmt.Sprintf("<td style-ref=\"%s\">", strings.Join(styleIDs, " "))
401 | 					} else {
402 | 						tdTag = "<td>"
403 | 					}
404 | 				} else {
405 | 					tdTag = "<td>"
406 | 				}
407 | 			} else {
408 | 				tdTag = "<td>"
409 | 			}
410 | 
411 | 			result.WriteString(fmt.Sprintf("%s%s</td>", tdTag, strings.ReplaceAll(html.EscapeString(value), "\n", "<br>")))
412 | 		}
413 | 		result.WriteString("</tr>\n")
414 | 	}
415 | 
416 | 	result.WriteString("</table>")
417 | 
418 | 	// スタイル定義とテーブルを結合
419 | 	var finalResult strings.Builder
420 | 	styleDefinitions := registry.GenerateStyleDefinitions()
421 | 	if styleDefinitions != "" {
422 | 		finalResult.WriteString(styleDefinitions)
423 | 	}
424 | 
425 | 	finalResult.WriteString("<h2>Sheet Data</h2>\n")
426 | 	finalResult.WriteString(result.String())
427 | 
428 | 	finalResultStr := finalResult.String()
429 | 	return &finalResultStr, nil
430 | }
431 | 
432 | func AbsolutePathTest() z.Test[*string] {
433 | 	return z.Test[*string]{
434 | 		Func: func(path *string, ctx z.Ctx) {
435 | 			if !filepath.IsAbs(*path) {
436 | 				ctx.AddIssue(ctx.Issue().SetMessage(fmt.Sprintf("Path '%s' is not absolute", *path)))
437 | 			}
438 | 		},
439 | 	}
440 | }
441 | 
```

--------------------------------------------------------------------------------
/internal/excel/excel_excelize.go:
--------------------------------------------------------------------------------

```go
  1 | package excel
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"os"
  6 | 	"path/filepath"
  7 | 	"strings"
  8 | 
  9 | 	"github.com/xuri/excelize/v2"
 10 | )
 11 | 
 12 | type ExcelizeExcel struct {
 13 | 	file *excelize.File
 14 | }
 15 | 
 16 | func NewExcelizeExcel(file *excelize.File) Excel {
 17 | 	return &ExcelizeExcel{file: file}
 18 | }
 19 | 
 20 | func (e *ExcelizeExcel) GetBackendName() string {
 21 | 	return "excelize"
 22 | }
 23 | 
 24 | func (e *ExcelizeExcel) FindSheet(sheetName string) (Worksheet, error) {
 25 | 	index, err := e.file.GetSheetIndex(sheetName)
 26 | 	if err != nil {
 27 | 		return nil, fmt.Errorf("sheet not found: %s", sheetName)
 28 | 	}
 29 | 	if index < 0 {
 30 | 		return nil, fmt.Errorf("sheet not found: %s", sheetName)
 31 | 	}
 32 | 	return &ExcelizeWorksheet{file: e.file, sheetName: sheetName}, nil
 33 | }
 34 | 
 35 | func (e *ExcelizeExcel) CreateNewSheet(sheetName string) error {
 36 | 	_, err := e.file.NewSheet(sheetName)
 37 | 	if err != nil {
 38 | 		return fmt.Errorf("failed to create new sheet: %w", err)
 39 | 	}
 40 | 	return nil
 41 | }
 42 | 
 43 | func (e *ExcelizeExcel) CopySheet(srcSheetName string, destSheetName string) error {
 44 | 	srcIndex, err := e.file.GetSheetIndex(srcSheetName)
 45 | 	if srcIndex < 0 {
 46 | 		return fmt.Errorf("source sheet not found: %s", srcSheetName)
 47 | 	}
 48 | 	if err != nil {
 49 | 		return err
 50 | 	}
 51 | 	destIndex, err := e.file.NewSheet(destSheetName)
 52 | 	if err != nil {
 53 | 		return fmt.Errorf("failed to create destination sheet: %w", err)
 54 | 	}
 55 | 	if err := e.file.CopySheet(srcIndex, destIndex); err != nil {
 56 | 		return fmt.Errorf("failed to copy sheet: %w", err)
 57 | 	}
 58 | 	srcNext := e.file.GetSheetList()[srcIndex+1]
 59 | 	if srcNext != srcSheetName {
 60 | 		e.file.MoveSheet(destSheetName, srcNext)
 61 | 	}
 62 | 	return nil
 63 | }
 64 | 
 65 | func (e *ExcelizeExcel) GetSheets() ([]Worksheet, error) {
 66 | 	sheetList := e.file.GetSheetList()
 67 | 	worksheets := make([]Worksheet, len(sheetList))
 68 | 	for i, sheetName := range sheetList {
 69 | 		worksheets[i] = &ExcelizeWorksheet{file: e.file, sheetName: sheetName}
 70 | 	}
 71 | 	return worksheets, nil
 72 | }
 73 | 
 74 | // SaveExcelize saves the Excel file to the specified path.
 75 | // Excelize's Save method restricts the file path length to 207 characters,
 76 | // but since this limitation has been relaxed in some environments,
 77 | // we ignore this restriction.
 78 | // https://github.com/qax-os/excelize/blob/v2.9.0/file.go#L71-L73
 79 | func (w *ExcelizeExcel) Save() error {
 80 | 	file, err := os.OpenFile(filepath.Clean(w.file.Path), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm)
 81 | 	if err != nil {
 82 | 		return err
 83 | 	}
 84 | 	defer file.Close()
 85 | 	return w.file.Write(file)
 86 | }
 87 | 
 88 | type ExcelizeWorksheet struct {
 89 | 	file      *excelize.File
 90 | 	sheetName string
 91 | }
 92 | 
 93 | func (w *ExcelizeWorksheet) Release() {
 94 | 	// No resources to release in excelize
 95 | }
 96 | 
 97 | func (w *ExcelizeWorksheet) Name() (string, error) {
 98 | 	return w.sheetName, nil
 99 | }
100 | 
101 | func (w *ExcelizeWorksheet) GetTables() ([]Table, error) {
102 | 	tables, err := w.file.GetTables(w.sheetName)
103 | 	if err != nil {
104 | 		return nil, fmt.Errorf("failed to get tables: %w", err)
105 | 	}
106 | 	tableList := make([]Table, len(tables))
107 | 	for i, table := range tables {
108 | 		tableList[i] = Table{
109 | 			Name:  table.Name,
110 | 			Range: NormalizeRange(table.Range),
111 | 		}
112 | 	}
113 | 	return tableList, nil
114 | }
115 | 
116 | func (w *ExcelizeWorksheet) GetPivotTables() ([]PivotTable, error) {
117 | 	pivotTables, err := w.file.GetPivotTables(w.sheetName)
118 | 	if err != nil {
119 | 		return nil, fmt.Errorf("failed to get pivot tables: %w", err)
120 | 	}
121 | 	pivotTableList := make([]PivotTable, len(pivotTables))
122 | 	for i, pivotTable := range pivotTables {
123 | 		pivotTableList[i] = PivotTable{
124 | 			Name:  pivotTable.Name,
125 | 			Range: NormalizeRange(pivotTable.PivotTableRange),
126 | 		}
127 | 	}
128 | 	return pivotTableList, nil
129 | }
130 | 
131 | func (w *ExcelizeWorksheet) SetValue(cell string, value any) error {
132 | 	if err := w.file.SetCellValue(w.sheetName, cell, value); err != nil {
133 | 		return err
134 | 	}
135 | 	if err := w.updateDimension(cell); err != nil {
136 | 		return fmt.Errorf("failed to update dimension: %w", err)
137 | 	}
138 | 	return nil
139 | }
140 | 
141 | func (w *ExcelizeWorksheet) SetFormula(cell string, formula string) error {
142 | 	if err := w.file.SetCellFormula(w.sheetName, cell, formula); err != nil {
143 | 		return err
144 | 	}
145 | 	if err := w.updateDimension(cell); err != nil {
146 | 		return fmt.Errorf("failed to update dimension: %w", err)
147 | 	}
148 | 	return nil
149 | }
150 | 
151 | func (w *ExcelizeWorksheet) GetValue(cell string) (string, error) {
152 | 	value, err := w.file.GetCellValue(w.sheetName, cell)
153 | 	if err != nil {
154 | 		return "", err
155 | 	}
156 | 	if value == "" {
157 | 		// try to get calculated value
158 | 		formula, err := w.file.GetCellFormula(w.sheetName, cell)
159 | 		if err != nil {
160 | 			return "", fmt.Errorf("failed to get formula: %w", err)
161 | 		}
162 | 		if formula != "" {
163 | 			return w.file.CalcCellValue(w.sheetName, cell)
164 | 		}
165 | 	}
166 | 	return value, nil
167 | }
168 | 
169 | func (w *ExcelizeWorksheet) GetFormula(cell string) (string, error) {
170 | 	formula, err := w.file.GetCellFormula(w.sheetName, cell)
171 | 	if err != nil {
172 | 		return "", fmt.Errorf("failed to get formula: %w", err)
173 | 	}
174 | 	if formula == "" {
175 | 		// fallback
176 | 		return w.GetValue(cell)
177 | 	}
178 | 	if !strings.HasPrefix(formula, "=") {
179 | 		formula = "=" + formula
180 | 	}
181 | 	return formula, nil
182 | }
183 | 
184 | func (w *ExcelizeWorksheet) GetDimention() (string, error) {
185 | 	return w.file.GetSheetDimension(w.sheetName)
186 | }
187 | 
188 | func (w *ExcelizeWorksheet) GetPagingStrategy(pageSize int) (PagingStrategy, error) {
189 | 	return NewExcelizeFixedSizePagingStrategy(pageSize, w)
190 | }
191 | 
192 | func (w *ExcelizeWorksheet) CapturePicture(captureRange string) (string, error) {
193 | 	return "", fmt.Errorf("CapturePicture is not supported in Excelize")
194 | }
195 | 
196 | func (w *ExcelizeWorksheet) AddTable(tableRange, tableName string) error {
197 | 	enable := true
198 | 	if err := w.file.AddTable(w.sheetName, &excelize.Table{
199 | 		Range:             tableRange,
200 | 		Name:              tableName,
201 | 		StyleName:         "TableStyleMedium2",
202 | 		ShowColumnStripes: true,
203 | 		ShowFirstColumn:   false,
204 | 		ShowHeaderRow:     &enable,
205 | 		ShowLastColumn:    false,
206 | 		ShowRowStripes:    &enable,
207 | 	}); err != nil {
208 | 		return err
209 | 	}
210 | 	return nil
211 | }
212 | 
213 | func (w *ExcelizeWorksheet) GetCellStyle(cell string) (*CellStyle, error) {
214 | 	styleID, err := w.file.GetCellStyle(w.sheetName, cell)
215 | 	if err != nil {
216 | 		return nil, fmt.Errorf("failed to get cell style: %w", err)
217 | 	}
218 | 
219 | 	style, err := w.file.GetStyle(styleID)
220 | 	if err != nil {
221 | 		return nil, fmt.Errorf("failed to get style details: %w", err)
222 | 	}
223 | 
224 | 	return convertExcelizeStyleToCellStyle(style), nil
225 | }
226 | 
227 | func (w *ExcelizeWorksheet) SetCellStyle(cell string, style *CellStyle) error {
228 | 	excelizeStyle := convertCellStyleToExcelizeStyle(style)
229 | 
230 | 	styleID, err := w.file.NewStyle(excelizeStyle)
231 | 	if err != nil {
232 | 		return fmt.Errorf("failed to create style: %w", err)
233 | 	}
234 | 
235 | 	if err := w.file.SetCellStyle(w.sheetName, cell, cell, styleID); err != nil {
236 | 		return fmt.Errorf("failed to set cell style: %w", err)
237 | 	}
238 | 
239 | 	return nil
240 | }
241 | 
242 | func convertCellStyleToExcelizeStyle(style *CellStyle) *excelize.Style {
243 | 	result := &excelize.Style{}
244 | 
245 | 	// Border
246 | 	if len(style.Border) > 0 {
247 | 		borders := make([]excelize.Border, len(style.Border))
248 | 		for i, border := range style.Border {
249 | 			excelizeBorder := excelize.Border{
250 | 				Type: border.Type.String(),
251 | 			}
252 | 			if border.Color != "" {
253 | 				excelizeBorder.Color = strings.TrimPrefix(border.Color, "#")
254 | 			}
255 | 			excelizeBorder.Style = borderStyleNameToInt(border.Style)
256 | 			borders[i] = excelizeBorder
257 | 		}
258 | 		result.Border = borders
259 | 	}
260 | 
261 | 	// Font
262 | 	if style.Font != nil {
263 | 		font := &excelize.Font{}
264 | 		if style.Font.Bold != nil {
265 | 			font.Bold = *style.Font.Bold
266 | 		}
267 | 		if style.Font.Italic != nil {
268 | 			font.Italic = *style.Font.Italic
269 | 		}
270 | 		if style.Font.Underline != nil {
271 | 			font.Underline = style.Font.Underline.String()
272 | 		}
273 | 		if style.Font.Size != nil && *style.Font.Size > 0 {
274 | 			font.Size = float64(*style.Font.Size)
275 | 		}
276 | 		if style.Font.Strike != nil {
277 | 			font.Strike = *style.Font.Strike
278 | 		}
279 | 		if style.Font.Color != nil && *style.Font.Color != "" {
280 | 			font.Color = strings.TrimPrefix(*style.Font.Color, "#")
281 | 		}
282 | 		if style.Font.VertAlign != nil {
283 | 			font.VertAlign = style.Font.VertAlign.String()
284 | 		}
285 | 		result.Font = font
286 | 	}
287 | 
288 | 	// Fill
289 | 	if style.Fill != nil {
290 | 		fill := excelize.Fill{}
291 | 		if style.Fill.Type != "" {
292 | 			fill.Type = style.Fill.Type.String()
293 | 		}
294 | 		fill.Pattern = fillPatternNameToInt(style.Fill.Pattern)
295 | 		if len(style.Fill.Color) > 0 {
296 | 			colors := make([]string, len(style.Fill.Color))
297 | 			for i, color := range style.Fill.Color {
298 | 				colors[i] = strings.TrimPrefix(color, "#")
299 | 			}
300 | 			fill.Color = colors
301 | 		}
302 | 		if style.Fill.Shading != nil {
303 | 			fill.Shading = fillShadingNameToInt(*style.Fill.Shading)
304 | 		}
305 | 		result.Fill = fill
306 | 	}
307 | 
308 | 	// NumFmt
309 | 	if style.NumFmt != nil && *style.NumFmt != "" {
310 | 		result.CustomNumFmt = style.NumFmt
311 | 	}
312 | 
313 | 	// DecimalPlaces
314 | 	if style.DecimalPlaces != nil && *style.DecimalPlaces > 0 {
315 | 		result.DecimalPlaces = style.DecimalPlaces
316 | 	}
317 | 
318 | 	return result
319 | }
320 | 
321 | func convertExcelizeStyleToCellStyle(style *excelize.Style) *CellStyle {
322 | 	result := &CellStyle{}
323 | 
324 | 	// Border
325 | 	if len(style.Border) > 0 {
326 | 		var borders []Border
327 | 		for _, border := range style.Border {
328 | 			borderStyle := Border{
329 | 				Type: BorderType(border.Type),
330 | 			}
331 | 			if border.Color != "" {
332 | 				borderStyle.Color = "#" + strings.ToUpper(border.Color)
333 | 			}
334 | 			if border.Style != 0 {
335 | 				borderStyle.Style = intToBorderStyleName(border.Style)
336 | 			}
337 | 			borders = append(borders, borderStyle)
338 | 		}
339 | 		if len(borders) > 0 {
340 | 			result.Border = borders
341 | 		}
342 | 	}
343 | 
344 | 	// Font
345 | 	if style.Font != nil {
346 | 		font := &FontStyle{}
347 | 		if style.Font.Bold {
348 | 			font.Bold = &style.Font.Bold
349 | 		}
350 | 		if style.Font.Italic {
351 | 			font.Italic = &style.Font.Italic
352 | 		}
353 | 		if style.Font.Underline != "" {
354 | 			underline := FontUnderline(style.Font.Underline)
355 | 			font.Underline = &underline
356 | 		}
357 | 		if style.Font.Size > 0 {
358 | 			size := int(style.Font.Size)
359 | 			font.Size = &size
360 | 		}
361 | 		if style.Font.Strike {
362 | 			font.Strike = &style.Font.Strike
363 | 		}
364 | 		if style.Font.Color != "" {
365 | 			color := "#" + strings.ToUpper(style.Font.Color)
366 | 			font.Color = &color
367 | 		}
368 | 		if style.Font.VertAlign != "" {
369 | 			vertAlign := FontVertAlign(style.Font.VertAlign)
370 | 			font.VertAlign = &vertAlign
371 | 		}
372 | 		if font.Bold != nil || font.Italic != nil || font.Underline != nil || font.Size != nil || font.Strike != nil || font.Color != nil || font.VertAlign != nil {
373 | 			result.Font = font
374 | 		}
375 | 	}
376 | 
377 | 	// Fill
378 | 	if style.Fill.Type != "" || style.Fill.Pattern != 0 || len(style.Fill.Color) > 0 {
379 | 		fill := &FillStyle{}
380 | 		if style.Fill.Type != "" {
381 | 			fill.Type = FillType(style.Fill.Type)
382 | 		}
383 | 		if style.Fill.Pattern != 0 {
384 | 			fill.Pattern = intToFillPatternName(style.Fill.Pattern)
385 | 		}
386 | 		if len(style.Fill.Color) > 0 {
387 | 			var colors []string
388 | 			for _, color := range style.Fill.Color {
389 | 				if color != "" {
390 | 					colors = append(colors, "#"+strings.ToUpper(color))
391 | 				}
392 | 			}
393 | 			if len(colors) > 0 {
394 | 				fill.Color = colors
395 | 			}
396 | 		}
397 | 		if style.Fill.Shading != 0 {
398 | 			shading := intToFillShadingName(style.Fill.Shading)
399 | 			fill.Shading = &shading
400 | 		}
401 | 		if fill.Type != "" || fill.Pattern != FillPatternNone || len(fill.Color) > 0 || fill.Shading != nil {
402 | 			result.Fill = fill
403 | 		}
404 | 	}
405 | 
406 | 	// NumFmt
407 | 	if style.CustomNumFmt != nil && *style.CustomNumFmt != "" {
408 | 		result.NumFmt = style.CustomNumFmt
409 | 	}
410 | 
411 | 	// DecimalPlaces
412 | 	if style.DecimalPlaces != nil && *style.DecimalPlaces != 0 {
413 | 		result.DecimalPlaces = style.DecimalPlaces
414 | 	}
415 | 
416 | 	return result
417 | }
418 | 
419 | func intToBorderStyleName(style int) BorderStyle {
420 | 	styles := map[int]BorderStyle{
421 | 		0:  BorderStyleNone,
422 | 		1:  BorderStyleContinuous,
423 | 		2:  BorderStyleContinuous,
424 | 		3:  BorderStyleDash,
425 | 		4:  BorderStyleDot,
426 | 		5:  BorderStyleContinuous,
427 | 		6:  BorderStyleDouble,
428 | 		7:  BorderStyleContinuous,
429 | 		8:  BorderStyleDashDot,
430 | 		9:  BorderStyleDashDotDot,
431 | 		10: BorderStyleSlantDashDot,
432 | 		11: BorderStyleContinuous,
433 | 		12: BorderStyleMediumDashDot,
434 | 		13: BorderStyleMediumDashDotDot,
435 | 	}
436 | 	if name, exists := styles[style]; exists {
437 | 		return name
438 | 	}
439 | 	return BorderStyleContinuous
440 | }
441 | 
442 | func intToFillPatternName(pattern int) FillPattern {
443 | 	patterns := map[int]FillPattern{
444 | 		0:  FillPatternNone,
445 | 		1:  FillPatternSolid,
446 | 		2:  FillPatternMediumGray,
447 | 		3:  FillPatternDarkGray,
448 | 		4:  FillPatternLightGray,
449 | 		5:  FillPatternDarkHorizontal,
450 | 		6:  FillPatternDarkVertical,
451 | 		7:  FillPatternDarkDown,
452 | 		8:  FillPatternDarkUp,
453 | 		9:  FillPatternDarkGrid,
454 | 		10: FillPatternDarkTrellis,
455 | 		11: FillPatternLightHorizontal,
456 | 		12: FillPatternLightVertical,
457 | 		13: FillPatternLightDown,
458 | 		14: FillPatternLightUp,
459 | 		15: FillPatternLightGrid,
460 | 		16: FillPatternLightTrellis,
461 | 		17: FillPatternGray125,
462 | 		18: FillPatternGray0625,
463 | 	}
464 | 	if name, exists := patterns[pattern]; exists {
465 | 		return name
466 | 	}
467 | 	return FillPatternNone
468 | }
469 | 
470 | func intToFillShadingName(shading int) FillShading {
471 | 	shadings := map[int]FillShading{
472 | 		0: FillShadingHorizontal,
473 | 		1: FillShadingVertical,
474 | 		2: FillShadingDiagonalDown,
475 | 		3: FillShadingDiagonalUp,
476 | 		4: FillShadingFromCenter,
477 | 		5: FillShadingFromCorner,
478 | 	}
479 | 	if name, exists := shadings[shading]; exists {
480 | 		return name
481 | 	}
482 | 	return FillShadingHorizontal
483 | }
484 | 
485 | func borderStyleNameToInt(style BorderStyle) int {
486 | 	styles := map[BorderStyle]int{
487 | 		BorderStyleNone:             0,
488 | 		BorderStyleContinuous:       1,
489 | 		BorderStyleDash:             3,
490 | 		BorderStyleDot:              4,
491 | 		BorderStyleDouble:           6,
492 | 		BorderStyleDashDot:          8,
493 | 		BorderStyleDashDotDot:       9,
494 | 		BorderStyleSlantDashDot:     10,
495 | 		BorderStyleMediumDashDot:    12,
496 | 		BorderStyleMediumDashDotDot: 13,
497 | 	}
498 | 	if value, exists := styles[style]; exists {
499 | 		return value
500 | 	}
501 | 	return 1
502 | }
503 | 
504 | func fillPatternNameToInt(pattern FillPattern) int {
505 | 	patterns := map[FillPattern]int{
506 | 		FillPatternNone:            0,
507 | 		FillPatternSolid:           1,
508 | 		FillPatternMediumGray:      2,
509 | 		FillPatternDarkGray:        3,
510 | 		FillPatternLightGray:       4,
511 | 		FillPatternDarkHorizontal:  5,
512 | 		FillPatternDarkVertical:    6,
513 | 		FillPatternDarkDown:        7,
514 | 		FillPatternDarkUp:          8,
515 | 		FillPatternDarkGrid:        9,
516 | 		FillPatternDarkTrellis:     10,
517 | 		FillPatternLightHorizontal: 11,
518 | 		FillPatternLightVertical:   12,
519 | 		FillPatternLightDown:       13,
520 | 		FillPatternLightUp:         14,
521 | 		FillPatternLightGrid:       15,
522 | 		FillPatternLightTrellis:    16,
523 | 		FillPatternGray125:         17,
524 | 		FillPatternGray0625:        18,
525 | 	}
526 | 	if value, exists := patterns[pattern]; exists {
527 | 		return value
528 | 	}
529 | 	return 0
530 | }
531 | 
532 | func fillShadingNameToInt(shading FillShading) int {
533 | 	shadings := map[FillShading]int{
534 | 		FillShadingHorizontal:   0,
535 | 		FillShadingVertical:     1,
536 | 		FillShadingDiagonalDown: 2,
537 | 		FillShadingDiagonalUp:   3,
538 | 		FillShadingFromCenter:   4,
539 | 		FillShadingFromCorner:   5,
540 | 	}
541 | 	if value, exists := shadings[shading]; exists {
542 | 		return value
543 | 	}
544 | 	return 0
545 | }
546 | 
547 | // updateDimention updates the dimension of the worksheet after a cell is updated.
548 | func (w *ExcelizeWorksheet) updateDimension(updatedCell string) error {
549 | 	dimension, err := w.file.GetSheetDimension(w.sheetName)
550 | 	if err != nil {
551 | 		return err
552 | 	}
553 | 	startCol, startRow, endCol, endRow, err := ParseRange(dimension)
554 | 	if err != nil {
555 | 		return err
556 | 	}
557 | 	updatedCol, updatedRow, err := excelize.CellNameToCoordinates(updatedCell)
558 | 	if err != nil {
559 | 		return err
560 | 	}
561 | 	if startCol > updatedCol {
562 | 		startCol = updatedCol
563 | 	}
564 | 	if endCol < updatedCol {
565 | 		endCol = updatedCol
566 | 	}
567 | 	if startRow > updatedRow {
568 | 		startRow = updatedRow
569 | 	}
570 | 	if endRow < updatedRow {
571 | 		endRow = updatedRow
572 | 	}
573 | 	startRange, err := excelize.CoordinatesToCellName(startCol, startRow)
574 | 	if err != nil {
575 | 		return err
576 | 	}
577 | 	endRange, err := excelize.CoordinatesToCellName(endCol, endRow)
578 | 	if err != nil {
579 | 		return err
580 | 	}
581 | 	updatedDimension := fmt.Sprintf("%s:%s", startRange, endRange)
582 | 	return w.file.SetSheetDimension(w.sheetName, updatedDimension)
583 | }
584 | 
```

--------------------------------------------------------------------------------
/internal/excel/excel_ole.go:
--------------------------------------------------------------------------------

```go
  1 | package excel
  2 | 
  3 | import (
  4 | 	"bufio"
  5 | 	"bytes"
  6 | 	"encoding/base64"
  7 | 	"fmt"
  8 | 	"io"
  9 | 	"path/filepath"
 10 | 	"regexp"
 11 | 	"runtime"
 12 | 	"strings"
 13 | 
 14 | 	"github.com/go-ole/go-ole"
 15 | 	"github.com/go-ole/go-ole/oleutil"
 16 | 	"github.com/skanehira/clipboard-image"
 17 | )
 18 | 
 19 | type OleExcel struct {
 20 | 	application *ole.IDispatch
 21 | 	workbook    *ole.IDispatch
 22 | }
 23 | 
 24 | type OleWorksheet struct {
 25 | 	excel     *OleExcel
 26 | 	worksheet *ole.IDispatch
 27 | }
 28 | 
 29 | func NewExcelOle(absolutePath string) (*OleExcel, func(), error) {
 30 | 	runtime.LockOSThread()
 31 | 	ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED)
 32 | 
 33 | 	unknown, err := oleutil.GetActiveObject("Excel.Application")
 34 | 	if err != nil {
 35 | 		return nil, func() {}, err
 36 | 	}
 37 | 	excel, err := unknown.QueryInterface(ole.IID_IDispatch)
 38 | 	if err != nil {
 39 | 		return nil, func() {}, err
 40 | 	}
 41 | 	oleutil.MustPutProperty(excel, "ScreenUpdating", false)
 42 | 	oleutil.MustPutProperty(excel, "EnableEvents", false)
 43 | 	workbooks := oleutil.MustGetProperty(excel, "Workbooks").ToIDispatch()
 44 | 	c := oleutil.MustGetProperty(workbooks, "Count").Val
 45 | 	for i := 1; i <= int(c); i++ {
 46 | 		workbook := oleutil.MustGetProperty(workbooks, "Item", i).ToIDispatch()
 47 | 		fullName := oleutil.MustGetProperty(workbook, "FullName").ToString()
 48 | 		name := oleutil.MustGetProperty(workbook, "Name").ToString()
 49 | 		if strings.HasPrefix(fullName, "https:") && name == filepath.Base(absolutePath) {
 50 | 			// If a workbook is opened through a WOPI URL, its absolute file path cannot be retrieved.
 51 | 			// If the absolutePath is not writable, it assumes that the workbook has opened by WOPI.
 52 | 			if FileIsNotWritable(absolutePath) {
 53 | 				return &OleExcel{application: excel, workbook: workbook}, func() {
 54 | 					oleutil.MustPutProperty(excel, "EnableEvents", true)
 55 | 					oleutil.MustPutProperty(excel, "ScreenUpdating", true)
 56 | 					workbook.Release()
 57 | 					workbooks.Release()
 58 | 					excel.Release()
 59 | 					ole.CoUninitialize()
 60 | 					runtime.UnlockOSThread()
 61 | 				}, nil
 62 | 			} else {
 63 | 				// This workbook might not be specified with the absolutePath
 64 | 			}
 65 | 		} else if normalizePath(fullName) == normalizePath(absolutePath) {
 66 | 			return &OleExcel{application: excel, workbook: workbook}, func() {
 67 | 				oleutil.MustPutProperty(excel, "EnableEvents", true)
 68 | 				oleutil.MustPutProperty(excel, "ScreenUpdating", true)
 69 | 				workbook.Release()
 70 | 				workbooks.Release()
 71 | 				excel.Release()
 72 | 				ole.CoUninitialize()
 73 | 				runtime.UnlockOSThread()
 74 | 			}, nil
 75 | 		}
 76 | 	}
 77 | 	return nil, func() {}, fmt.Errorf("workbook not found: %s", absolutePath)
 78 | }
 79 | 
 80 | func NewExcelOleWithNewObject(absolutePath string) (*OleExcel, func(), error) {
 81 | 	runtime.LockOSThread()
 82 | 	ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED)
 83 | 
 84 | 	unknown, err := oleutil.CreateObject("Excel.Application")
 85 | 	if err != nil {
 86 | 		return nil, func() {}, err
 87 | 	}
 88 | 	excel, err := unknown.QueryInterface(ole.IID_IDispatch)
 89 | 	if err != nil {
 90 | 		return nil, func() {}, err
 91 | 	}
 92 | 	workbooks := oleutil.MustGetProperty(excel, "Workbooks").ToIDispatch()
 93 | 	workbook, err := oleutil.CallMethod(workbooks, "Open", absolutePath)
 94 | 	if err != nil {
 95 | 		return nil, func() {}, err
 96 | 	}
 97 | 	w := workbook.ToIDispatch()
 98 | 	return &OleExcel{application: excel, workbook: w}, func() {
 99 | 		w.Release()
100 | 		workbooks.Release()
101 | 		excel.Release()
102 | 		oleutil.CallMethod(excel, "Close")
103 | 		ole.CoUninitialize()
104 | 		runtime.UnlockOSThread()
105 | 	}, nil
106 | }
107 | 
108 | func (o *OleExcel) GetBackendName() string {
109 | 	return "ole"
110 | }
111 | 
112 | func (o *OleExcel) GetSheets() ([]Worksheet, error) {
113 | 	worksheets := oleutil.MustGetProperty(o.workbook, "Worksheets").ToIDispatch()
114 | 	defer worksheets.Release()
115 | 
116 | 	count := int(oleutil.MustGetProperty(worksheets, "Count").Val)
117 | 	worksheetList := make([]Worksheet, count)
118 | 
119 | 	for i := 1; i <= count; i++ {
120 | 		worksheet := oleutil.MustGetProperty(worksheets, "Item", i).ToIDispatch()
121 | 		worksheetList[i-1] = &OleWorksheet{
122 | 			excel:     o,
123 | 			worksheet: worksheet,
124 | 		}
125 | 	}
126 | 	return worksheetList, nil
127 | }
128 | 
129 | func (o *OleExcel) FindSheet(sheetName string) (Worksheet, error) {
130 | 	worksheets := oleutil.MustGetProperty(o.workbook, "Worksheets").ToIDispatch()
131 | 	defer worksheets.Release()
132 | 
133 | 	count := int(oleutil.MustGetProperty(worksheets, "Count").Val)
134 | 
135 | 	for i := 1; i <= count; i++ {
136 | 		worksheet := oleutil.MustGetProperty(worksheets, "Item", i).ToIDispatch()
137 | 		name := oleutil.MustGetProperty(worksheet, "Name").ToString()
138 | 
139 | 		if name == sheetName {
140 | 			return &OleWorksheet{
141 | 				excel:     o,
142 | 				worksheet: worksheet,
143 | 			}, nil
144 | 		}
145 | 	}
146 | 
147 | 	return nil, fmt.Errorf("sheet not found: %s", sheetName)
148 | }
149 | 
150 | func (o *OleExcel) CreateNewSheet(sheetName string) error {
151 | 	activeWorksheet := oleutil.MustGetProperty(o.workbook, "ActiveSheet").ToIDispatch()
152 | 	defer activeWorksheet.Release()
153 | 	activeWorksheetIndex := oleutil.MustGetProperty(activeWorksheet, "Index").Val
154 | 	worksheets := oleutil.MustGetProperty(o.workbook, "Worksheets").ToIDispatch()
155 | 	defer worksheets.Release()
156 | 
157 | 	_, err := oleutil.CallMethod(worksheets, "Add", nil, activeWorksheet)
158 | 	if err != nil {
159 | 		return err
160 | 	}
161 | 
162 | 	worksheet := oleutil.MustGetProperty(worksheets, "Item", activeWorksheetIndex+1).ToIDispatch()
163 | 	defer worksheet.Release()
164 | 
165 | 	_, err = oleutil.PutProperty(worksheet, "Name", sheetName)
166 | 	if err != nil {
167 | 		return err
168 | 	}
169 | 
170 | 	return nil
171 | }
172 | 
173 | func (o *OleExcel) CopySheet(srcSheetName string, dstSheetName string) error {
174 | 	worksheets := oleutil.MustGetProperty(o.workbook, "Worksheets").ToIDispatch()
175 | 	defer worksheets.Release()
176 | 
177 | 	srcSheetVariant, err := oleutil.GetProperty(worksheets, "Item", srcSheetName)
178 | 	if err != nil {
179 | 		return fmt.Errorf("faild to get sheet: %w", err)
180 | 	}
181 | 	srcSheet := srcSheetVariant.ToIDispatch()
182 | 	defer srcSheet.Release()
183 | 	srcSheetIndex := oleutil.MustGetProperty(srcSheet, "Index").Val
184 | 
185 | 	_, err = oleutil.CallMethod(srcSheet, "Copy", nil, srcSheet)
186 | 	if err != nil {
187 | 		return err
188 | 	}
189 | 
190 | 	dstSheetVariant, err := oleutil.GetProperty(worksheets, "Item", srcSheetIndex+1)
191 | 	if err != nil {
192 | 		return fmt.Errorf("failed to get copied sheet: %w", err)
193 | 	}
194 | 	dstSheet := dstSheetVariant.ToIDispatch()
195 | 	defer dstSheet.Release()
196 | 
197 | 	_, err = oleutil.PutProperty(dstSheet, "Name", dstSheetName)
198 | 	if err != nil {
199 | 		return err
200 | 	}
201 | 
202 | 	return nil
203 | }
204 | 
205 | func (o *OleExcel) Save() error {
206 | 	_, err := oleutil.CallMethod(o.workbook, "Save")
207 | 	if err != nil {
208 | 		return err
209 | 	}
210 | 	return nil
211 | }
212 | 
213 | func (o *OleWorksheet) Release() {
214 | 	o.worksheet.Release()
215 | }
216 | 
217 | func (o *OleWorksheet) Name() (string, error) {
218 | 	name := oleutil.MustGetProperty(o.worksheet, "Name").ToString()
219 | 	return name, nil
220 | }
221 | 
222 | func (o *OleWorksheet) GetTables() ([]Table, error) {
223 | 	tables := oleutil.MustGetProperty(o.worksheet, "ListObjects").ToIDispatch()
224 | 	defer tables.Release()
225 | 	count := int(oleutil.MustGetProperty(tables, "Count").Val)
226 | 	tableList := make([]Table, count)
227 | 	for i := 1; i <= count; i++ {
228 | 		table := oleutil.MustGetProperty(tables, "Item", i).ToIDispatch()
229 | 		defer table.Release()
230 | 		name := oleutil.MustGetProperty(table, "Name").ToString()
231 | 		defer table.Release()
232 | 		tableRange := oleutil.MustGetProperty(table, "Range").ToIDispatch()
233 | 		defer tableRange.Release()
234 | 		tableList[i-1] = Table{
235 | 			Name:  name,
236 | 			Range: NormalizeRange(oleutil.MustGetProperty(tableRange, "Address").ToString()),
237 | 		}
238 | 	}
239 | 	return tableList, nil
240 | }
241 | 
242 | func (o *OleWorksheet) GetPivotTables() ([]PivotTable, error) {
243 | 	pivotTables := oleutil.MustGetProperty(o.worksheet, "PivotTables").ToIDispatch()
244 | 	defer pivotTables.Release()
245 | 	count := int(oleutil.MustGetProperty(pivotTables, "Count").Val)
246 | 	pivotTableList := make([]PivotTable, count)
247 | 	for i := 1; i <= count; i++ {
248 | 		pivotTable := oleutil.MustGetProperty(pivotTables, "Item", i).ToIDispatch()
249 | 		defer pivotTable.Release()
250 | 		name := oleutil.MustGetProperty(pivotTable, "Name").ToString()
251 | 		pivotTableRange := oleutil.MustGetProperty(pivotTable, "TableRange1").ToIDispatch()
252 | 		defer pivotTableRange.Release()
253 | 		pivotTableList[i-1] = PivotTable{
254 | 			Name:  name,
255 | 			Range: NormalizeRange(oleutil.MustGetProperty(pivotTableRange, "Address").ToString()),
256 | 		}
257 | 	}
258 | 	return pivotTableList, nil
259 | }
260 | 
261 | func (o *OleWorksheet) SetValue(cell string, value any) error {
262 | 	range_ := oleutil.MustGetProperty(o.worksheet, "Range", cell).ToIDispatch()
263 | 	defer range_.Release()
264 | 	_, err := oleutil.PutProperty(range_, "Value", value)
265 | 	return err
266 | }
267 | 
268 | func (o *OleWorksheet) SetFormula(cell string, formula string) error {
269 | 	range_ := oleutil.MustGetProperty(o.worksheet, "Range", cell).ToIDispatch()
270 | 	defer range_.Release()
271 | 	_, err := oleutil.PutProperty(range_, "Formula", formula)
272 | 	return err
273 | }
274 | 
275 | func (o *OleWorksheet) GetValue(cell string) (string, error) {
276 | 	range_ := oleutil.MustGetProperty(o.worksheet, "Range", cell).ToIDispatch()
277 | 	defer range_.Release()
278 | 	value := oleutil.MustGetProperty(range_, "Text").Value()
279 | 	switch v := value.(type) {
280 | 	case string:
281 | 		return v, nil
282 | 	case nil:
283 | 		return "", nil
284 | 	default: // Handle other types as needed
285 | 		return "", fmt.Errorf("unsupported type: %T", v)
286 | 	}
287 | }
288 | 
289 | func (o *OleWorksheet) GetFormula(cell string) (string, error) {
290 | 	range_ := oleutil.MustGetProperty(o.worksheet, "Range", cell).ToIDispatch()
291 | 	defer range_.Release()
292 | 	formula := oleutil.MustGetProperty(range_, "Formula").ToString()
293 | 	return formula, nil
294 | }
295 | 
296 | func (o *OleWorksheet) GetDimention() (string, error) {
297 | 	range_ := oleutil.MustGetProperty(o.worksheet, "UsedRange").ToIDispatch()
298 | 	defer range_.Release()
299 | 	dimension := oleutil.MustGetProperty(range_, "Address").ToString()
300 | 	return NormalizeRange(dimension), nil
301 | }
302 | 
303 | func (o *OleWorksheet) GetPagingStrategy(pageSize int) (PagingStrategy, error) {
304 | 	return NewOlePagingStrategy(1000, o)
305 | }
306 | 
307 | func (o *OleWorksheet) PrintArea() (string, error) {
308 | 	v, err := oleutil.GetProperty(o.worksheet, "PageSetup")
309 | 	if err != nil {
310 | 		return "", err
311 | 	}
312 | 	pageSetup := v.ToIDispatch()
313 | 	defer pageSetup.Release()
314 | 
315 | 	printArea := oleutil.MustGetProperty(pageSetup, "PrintArea").ToString()
316 | 	return printArea, nil
317 | }
318 | 
319 | func (o *OleWorksheet) HPageBreaks() ([]int, error) {
320 | 	v, err := oleutil.GetProperty(o.worksheet, "HPageBreaks")
321 | 	if err != nil {
322 | 		return nil, err
323 | 	}
324 | 	hPageBreaks := v.ToIDispatch()
325 | 	defer hPageBreaks.Release()
326 | 
327 | 	count := int(oleutil.MustGetProperty(hPageBreaks, "Count").Val)
328 | 	pageBreaks := make([]int, count)
329 | 	for i := 1; i <= count; i++ {
330 | 		pageBreak := oleutil.MustGetProperty(hPageBreaks, "Item", i).ToIDispatch()
331 | 		defer pageBreak.Release()
332 | 		location := oleutil.MustGetProperty(pageBreak, "Location").ToIDispatch()
333 | 		defer location.Release()
334 | 		row := oleutil.MustGetProperty(location, "Row").Val
335 | 		pageBreaks[i-1] = int(row)
336 | 	}
337 | 	return pageBreaks, nil
338 | }
339 | 
340 | func (o *OleWorksheet) CapturePicture(captureRange string) (string, error) {
341 | 	r := oleutil.MustGetProperty(o.worksheet, "Range", captureRange).ToIDispatch()
342 | 	defer r.Release()
343 | 	_, err := oleutil.CallMethod(
344 | 		r,
345 | 		"CopyPicture",
346 | 		int(1), // xlScreen (https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.office.interop.excel.xlpictureappearance?view=excel-pia)
347 | 		int(2), // xlBitmap (https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.office.interop.excel.xlcopypictureformat?view=excel-pia)
348 | 	)
349 | 	if err != nil {
350 | 		return "", err
351 | 	}
352 | 	// Read the image from the clipboard
353 | 	buf := new(bytes.Buffer)
354 | 	bufWriter := bufio.NewWriter(buf)
355 | 	clipboardReader, err := clipboard.ReadFromClipboard()
356 | 	if err != nil {
357 | 		return "", fmt.Errorf("failed to read from clipboard: %w", err)
358 | 	}
359 | 	if _, err := io.Copy(bufWriter, clipboardReader); err != nil {
360 | 		return "", fmt.Errorf("failed to copy clipboard data: %w", err)
361 | 	}
362 | 	if err := bufWriter.Flush(); err != nil {
363 | 		return "", fmt.Errorf("failed to flush buffer: %w", err)
364 | 	}
365 | 	return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
366 | }
367 | 
368 | func (o *OleWorksheet) AddTable(tableRange string, tableName string) error {
369 | 	tables := oleutil.MustGetProperty(o.worksheet, "ListObjects").ToIDispatch()
370 | 	defer tables.Release()
371 | 
372 | 	// https://learn.microsoft.com/ja-jp/office/vba/api/excel.listobjects.add
373 | 	tableVar, err := oleutil.CallMethod(
374 | 		tables,
375 | 		"Add",
376 | 		int(1), // xlSrcRange (https://learn.microsoft.com/ja-jp/office/vba/api/excel.xllistobjectsourcetype)
377 | 		tableRange,
378 | 		nil,
379 | 		int(0), // xlYes (https://learn.microsoft.com/ja-jp/office/vba/api/excel.xlyesnoguess)
380 | 	)
381 | 	if err != nil {
382 | 		return err
383 | 	}
384 | 	table := tableVar.ToIDispatch()
385 | 	defer table.Release()
386 | 	_, err = oleutil.PutProperty(table, "Name", tableName)
387 | 	if err != nil {
388 | 		return err
389 | 	}
390 | 	return err
391 | }
392 | 
393 | func (o *OleWorksheet) GetCellStyle(cell string) (*CellStyle, error) {
394 | 	rng := oleutil.MustGetProperty(o.worksheet, "Range", cell).ToIDispatch()
395 | 	defer rng.Release()
396 | 
397 | 	style := &CellStyle{}
398 | 
399 | 	// Get Font information
400 | 	normalStyle := oleutil.MustGetProperty(o.excel.workbook, "Styles", "Normal").ToIDispatch()
401 | 	defer normalStyle.Release()
402 | 	normalFont := oleutil.MustGetProperty(normalStyle, "Font").ToIDispatch()
403 | 	defer normalFont.Release()
404 | 	font := oleutil.MustGetProperty(rng, "Font").ToIDispatch()
405 | 	defer font.Release()
406 | 
407 | 	normalFontSize := int(oleutil.MustGetProperty(normalFont, "Size").Value().(float64))
408 | 	normalFontBold := oleutil.MustGetProperty(normalFont, "Bold").Value().(bool)
409 | 	normalFontItalic := oleutil.MustGetProperty(normalFont, "Italic").Value().(bool)
410 | 	normalFontColor := oleutil.MustGetProperty(normalFont, "Color").Value().(float64)
411 | 
412 | 	fontSize := int(oleutil.MustGetProperty(font, "Size").Value().(float64))
413 | 	fontBold := oleutil.MustGetProperty(font, "Bold").Value().(bool)
414 | 	fontItalic := oleutil.MustGetProperty(font, "Italic").Value().(bool)
415 | 	fontColor := oleutil.MustGetProperty(font, "Color").Value().(float64)
416 | 
417 | 	if fontSize != normalFontSize || fontBold != normalFontBold || fontItalic != normalFontItalic || fontColor != normalFontColor {
418 | 		colorStr := bgrToRgb(fontColor)
419 | 		style.Font = &FontStyle{
420 | 			Bold:   &fontBold,
421 | 			Italic: &fontItalic,
422 | 			Size:   &fontSize,
423 | 			Color:  &colorStr,
424 | 		}
425 | 	}
426 | 
427 | 	// Get Interior (fill) information
428 | 	interior := oleutil.MustGetProperty(rng, "Interior").ToIDispatch()
429 | 	defer interior.Release()
430 | 
431 | 	interiorPattern := excelPatternToFillPattern(oleutil.MustGetProperty(interior, "Pattern").Value().(int32))
432 | 
433 | 	if interiorPattern != FillPatternNone {
434 | 		interiorColor := oleutil.MustGetProperty(interior, "Color").Value().(float64)
435 | 
436 | 		style.Fill = &FillStyle{
437 | 			Type:    "pattern",
438 | 			Pattern: interiorPattern,
439 | 			Color:   []string{bgrToRgb(interiorColor)},
440 | 		}
441 | 	}
442 | 
443 | 	// Get Border information
444 | 	var borderStyles []Border
445 | 
446 | 	// Get borders for each direction: Left(7), Top(8), Bottom(9), Right(10)
447 | 	borderPositions := []struct {
448 | 		index    int
449 | 		position BorderType
450 | 	}{
451 | 		{7, BorderTypeLeft},
452 | 		{8, BorderTypeTop},
453 | 		{9, BorderTypeBottom},
454 | 		{10, BorderTypeRight},
455 | 	}
456 | 
457 | 	borders := oleutil.MustGetProperty(rng, "Borders").ToIDispatch()
458 | 	defer borders.Release()
459 | 	bordersLineStyle := oleutil.MustGetProperty(borders, "LineStyle")
460 | 	if bordersLineStyle.VT == ole.VT_NULL {
461 | 		// If Borders.LineStyle is null, the borders have different styles
462 | 		for _, pos := range borderPositions {
463 | 			border := oleutil.MustGetProperty(borders, "Item", pos.index).ToIDispatch()
464 | 			defer border.Release()
465 | 
466 | 			borderLineStyle := excelBorderStyleToName(oleutil.MustGetProperty(border, "LineStyle").Value().(int32))
467 | 
468 | 			if borderLineStyle != BorderStyleNone {
469 | 				borderColor := oleutil.MustGetProperty(border, "Color").Value().(float64)
470 | 				borderStyle := Border{
471 | 					Type:  pos.position,
472 | 					Style: borderLineStyle,
473 | 					Color: bgrToRgb(borderColor),
474 | 				}
475 | 				borderStyles = append(borderStyles, borderStyle)
476 | 			}
477 | 		}
478 | 	} else {
479 | 		// If Borders.LineStyle is not null, all borders have the same style
480 | 		lineStyle := excelBorderStyleToName(bordersLineStyle.Value().(int32))
481 | 		if lineStyle != BorderStyleNone {
482 | 			for _, pos := range borderPositions {
483 | 				border := oleutil.MustGetProperty(borders, "Item", pos.index).ToIDispatch()
484 | 				borderColor := oleutil.MustGetProperty(border, "Color").Value().(float64)
485 | 				borderStyle := Border{
486 | 					Type:  pos.position,
487 | 					Style: lineStyle,
488 | 					Color: bgrToRgb(borderColor),
489 | 				}
490 | 				borderStyles = append(borderStyles, borderStyle)
491 | 			}
492 | 		}
493 | 	}
494 | 
495 | 	style.Border = borderStyles
496 | 
497 | 	// Get NumberFormat information
498 | 	generalNumberFormat := oleutil.MustGetProperty(o.excel.application, "International", 26).Value().(string) // xlGeneralFormatName
499 | 	numberFormat := oleutil.MustGetProperty(rng, "NumberFormat").ToString()
500 | 	if numberFormat != generalNumberFormat && numberFormat != "@" {
501 | 		style.NumFmt = &numberFormat
502 | 	}
503 | 
504 | 	// Extract decimal places from number format if it's a numeric format
505 | 	decimalPlaces := extractDecimalPlacesFromFormat(numberFormat)
506 | 	style.DecimalPlaces = &decimalPlaces
507 | 
508 | 	return style, nil
509 | }
510 | 
511 | // bgrToRgb converts BGR color format to RGB hex string
512 | func bgrToRgb(bgrColor float64) string {
513 | 	bgrColorInt := int32(bgrColor)
514 | 	// Extract RGB components from BGR format
515 | 	r := (bgrColorInt >> 0) & 0xFF
516 | 	g := (bgrColorInt >> 8) & 0xFF
517 | 	b := (bgrColorInt >> 16) & 0xFF
518 | 	return fmt.Sprintf("#%02X%02X%02X", r, g, b)
519 | }
520 | 
521 | // excelBorderStyleToName converts Excel border style constant to BorderStyleName
522 | func excelBorderStyleToName(excelStyle int32) BorderStyle {
523 | 	switch excelStyle {
524 | 	case 1: // xlContinuous
525 | 		return BorderStyleContinuous
526 | 	case -4115: // xlDash
527 | 		return BorderStyleDash
528 | 	case -4118: // xlDot
529 | 		return BorderStyleDot
530 | 	case -4119: // xlDouble
531 | 		return BorderStyleDouble
532 | 	case 4: // xlDashDot
533 | 		return BorderStyleDashDot
534 | 	case 5: // xlDashDotDot
535 | 		return BorderStyleDashDotDot
536 | 	case 13: // xlSlantDashDot
537 | 		return BorderStyleSlantDashDot
538 | 	case -4142: // xlLineStyleNone
539 | 		return BorderStyleNone
540 | 	default:
541 | 		return BorderStyleNone
542 | 	}
543 | }
544 | 
545 | // excelPatternToFillPattern converts Excel XlPattern constant to FillPatternName
546 | func excelPatternToFillPattern(excelPattern int32) FillPattern {
547 | 	switch excelPattern {
548 | 	case -4142: // xlPatternNone
549 | 		return FillPatternNone
550 | 	case 1: // xlPatternSolid
551 | 		return FillPatternSolid
552 | 	case -4125: // xlPatternGray75
553 | 		return FillPatternDarkGray
554 | 	case -4124: // xlPatternGray50
555 | 		return FillPatternMediumGray
556 | 	case -4126: // xlPatternGray25
557 | 		return FillPatternLightGray
558 | 	case -4121: // xlPatternGray16
559 | 		return FillPatternGray125
560 | 	case -4127: // xlPatternGray8
561 | 		return FillPatternGray0625
562 | 	case 9: // xlPatternHorizontal
563 | 		return FillPatternLightHorizontal
564 | 	case 12: // xlPatternVertical
565 | 		return FillPatternLightVertical
566 | 	case 10: // xlPatternDown
567 | 		return FillPatternLightDown
568 | 	case 11: // xlPatternUp
569 | 		return FillPatternLightUp
570 | 	case 16: // xlPatternGrid
571 | 		return FillPatternLightGrid
572 | 	case 17: // xlPatternCrissCross
573 | 		return FillPatternLightTrellis
574 | 	case 5: // xlPatternLightHorizontal
575 | 		return FillPatternLightHorizontal
576 | 	case 6: // xlPatternLightVertical
577 | 		return FillPatternLightVertical
578 | 	case 7: // xlPatternLightDown
579 | 		return FillPatternLightDown
580 | 	case 8: // xlPatternLightUp
581 | 		return FillPatternLightUp
582 | 	case 15: // xlPatternLightGrid
583 | 		return FillPatternLightGrid
584 | 	case 18: // xlPatternLightTrellis
585 | 		return FillPatternLightTrellis
586 | 	case 13: // xlPatternSemiGray75
587 | 		return FillPatternDarkHorizontal
588 | 	case 2: // xlPatternDarkHorizontal
589 | 		return FillPatternDarkHorizontal
590 | 	case 3: // xlPatternDarkVertical
591 | 		return FillPatternDarkVertical
592 | 	case 4: // xlPatternDarkDown
593 | 		return FillPatternDarkDown
594 | 	case 14: // xlPatternDarkUp
595 | 		return FillPatternDarkUp
596 | 	case -4162: // xlPatternDarkGrid
597 | 		return FillPatternDarkGrid
598 | 	case -4166: // xlPatternDarkTrellis
599 | 		return FillPatternDarkTrellis
600 | 	default:
601 | 		return FillPatternNone
602 | 	}
603 | }
604 | 
605 | var extractDecimalPlacesRegexp = regexp.MustCompile(`\.([0#]+)`)
606 | 
607 | // extractDecimalPlacesFromFormat extracts decimal places count from Excel number format string
608 | func extractDecimalPlacesFromFormat(format string) int {
609 | 	// Handle common numeric formats
610 | 	// Examples: "0.00" -> 2, "#,##0.000" -> 3, "0" -> 0
611 | 	matches := extractDecimalPlacesRegexp.FindStringSubmatch(format)
612 | 	if len(matches) > 1 {
613 | 		return len(matches[1])
614 | 	}
615 | 	return 0
616 | }
617 | 
618 | func (o *OleWorksheet) SetCellStyle(cell string, style *CellStyle) error {
619 | 	rng := oleutil.MustGetProperty(o.worksheet, "Range", cell).ToIDispatch()
620 | 	defer rng.Release()
621 | 
622 | 	// Apply Font styles
623 | 	if style.Font != nil {
624 | 		font := oleutil.MustGetProperty(rng, "Font").ToIDispatch()
625 | 		defer font.Release()
626 | 
627 | 		if style.Font.Bold != nil {
628 | 			oleutil.PutProperty(font, "Bold", *style.Font.Bold)
629 | 		}
630 | 		if style.Font.Italic != nil {
631 | 			oleutil.PutProperty(font, "Italic", *style.Font.Italic)
632 | 		}
633 | 		if style.Font.Size != nil && *style.Font.Size > 0 {
634 | 			oleutil.PutProperty(font, "Size", *style.Font.Size)
635 | 		}
636 | 		if style.Font.Color != nil && *style.Font.Color != "" {
637 | 			colorValue := rgbToBgr(*style.Font.Color)
638 | 			oleutil.PutProperty(font, "Color", colorValue)
639 | 		}
640 | 		if style.Font.Strike != nil && *style.Font.Strike {
641 | 			oleutil.PutProperty(font, "Strikethrough", true)
642 | 		}
643 | 	}
644 | 
645 | 	// Apply Fill styles
646 | 	if style.Fill != nil {
647 | 		interior := oleutil.MustGetProperty(rng, "Interior").ToIDispatch()
648 | 		defer interior.Release()
649 | 
650 | 		if style.Fill.Pattern != FillPatternNone {
651 | 			oleutil.PutProperty(interior, "Pattern", fillPatternToExcelPattern(style.Fill.Pattern))
652 | 		}
653 | 		if len(style.Fill.Color) > 0 && style.Fill.Color[0] != "" {
654 | 			colorValue := rgbToBgr(style.Fill.Color[0])
655 | 			oleutil.PutProperty(interior, "Color", colorValue)
656 | 		}
657 | 	}
658 | 
659 | 	// Apply Border styles
660 | 	if len(style.Border) > 0 {
661 | 		borders := oleutil.MustGetProperty(rng, "Borders").ToIDispatch()
662 | 		defer borders.Release()
663 | 
664 | 		for _, borderStyle := range style.Border {
665 | 			borderIndex := borderTypeToIndex(borderStyle.Type)
666 | 			if borderIndex > 0 {
667 | 				border := oleutil.MustGetProperty(borders, "Item", borderIndex).ToIDispatch()
668 | 				defer border.Release()
669 | 
670 | 				oleutil.PutProperty(border, "LineStyle", borderStyleNameToExcel(borderStyle.Style))
671 | 				if borderStyle.Color != "" {
672 | 					colorValue := rgbToBgr(borderStyle.Color)
673 | 					oleutil.PutProperty(border, "Color", colorValue)
674 | 				}
675 | 			}
676 | 		}
677 | 	}
678 | 
679 | 	// Apply Number Format
680 | 	if style.NumFmt != nil && *style.NumFmt != "" {
681 | 		oleutil.PutProperty(rng, "NumberFormat", *style.NumFmt)
682 | 	}
683 | 
684 | 	return nil
685 | }
686 | 
687 | // rgbToBgr converts RGB hex string to BGR color format
688 | func rgbToBgr(rgbColor string) int32 {
689 | 	if len(rgbColor) != 7 || rgbColor[0] != '#' {
690 | 		return 0
691 | 	}
692 | 
693 | 	r := hexToByte(rgbColor[1:3])
694 | 	g := hexToByte(rgbColor[3:5])
695 | 	b := hexToByte(rgbColor[5:7])
696 | 
697 | 	return int32(r) | (int32(g) << 8) | (int32(b) << 16)
698 | }
699 | 
700 | // hexToByte converts hex string to byte
701 | func hexToByte(hex string) byte {
702 | 	var result byte
703 | 	for _, char := range hex {
704 | 		result *= 16
705 | 		if char >= '0' && char <= '9' {
706 | 			result += byte(char - '0')
707 | 		} else if char >= 'A' && char <= 'F' {
708 | 			result += byte(char - 'A' + 10)
709 | 		} else if char >= 'a' && char <= 'f' {
710 | 			result += byte(char - 'a' + 10)
711 | 		}
712 | 	}
713 | 	return result
714 | }
715 | 
716 | // borderTypeToIndex converts border type string to Excel border index
717 | func borderTypeToIndex(borderType BorderType) int {
718 | 	switch borderType {
719 | 	case BorderTypeLeft:
720 | 		return 7
721 | 	case BorderTypeTop:
722 | 		return 8
723 | 	case BorderTypeBottom:
724 | 		return 9
725 | 	case BorderTypeRight:
726 | 		return 10
727 | 	case BorderTypeDiagonalDown:
728 | 		return 5
729 | 	case BorderTypeDiagonalUp:
730 | 		return 6
731 | 	default:
732 | 		return 0
733 | 	}
734 | }
735 | 
736 | // borderStyleNameToExcel converts BorderStyleName to Excel constant
737 | func borderStyleNameToExcel(style BorderStyle) int32 {
738 | 	switch style {
739 | 	case BorderStyleContinuous:
740 | 		return 1 // xlContinuous
741 | 	case BorderStyleDash:
742 | 		return -4115 // xlDash
743 | 	case BorderStyleDot:
744 | 		return -4118 // xlDot
745 | 	case BorderStyleDouble:
746 | 		return -4119 // xlDouble
747 | 	case BorderStyleDashDot:
748 | 		return 4 // xlDashDot
749 | 	case BorderStyleDashDotDot:
750 | 		return 5 // xlDashDotDot
751 | 	case BorderStyleSlantDashDot:
752 | 		return 13 // xlSlantDashDot
753 | 	case BorderStyleNone:
754 | 		return -4142 // xlLineStyleNone
755 | 	default:
756 | 		return -4142 // xlLineStyleNone
757 | 	}
758 | }
759 | 
760 | // fillPatternToExcelPattern converts FillPatternName to Excel pattern constant
761 | func fillPatternToExcelPattern(pattern FillPattern) int32 {
762 | 	switch pattern {
763 | 	case FillPatternSolid:
764 | 		return 1 // xlPatternSolid
765 | 	case FillPatternMediumGray:
766 | 		return -4124 // xlPatternGray50
767 | 	case FillPatternDarkGray:
768 | 		return -4125 // xlPatternGray75
769 | 	case FillPatternLightGray:
770 | 		return -4126 // xlPatternGray25
771 | 	case FillPatternGray125:
772 | 		return -4121 // xlPatternGray16
773 | 	case FillPatternGray0625:
774 | 		return -4127 // xlPatternGray8
775 | 	case FillPatternLightHorizontal:
776 | 		return 5 // xlPatternLightHorizontal
777 | 	case FillPatternLightVertical:
778 | 		return 6 // xlPatternLightVertical
779 | 	case FillPatternLightDown:
780 | 		return 7 // xlPatternLightDown
781 | 	case FillPatternLightUp:
782 | 		return 8 // xlPatternLightUp
783 | 	case FillPatternLightGrid:
784 | 		return 15 // xlPatternLightGrid
785 | 	case FillPatternLightTrellis:
786 | 		return 18 // xlPatternLightTrellis
787 | 	case FillPatternDarkHorizontal:
788 | 		return 2 // xlPatternDarkHorizontal
789 | 	case FillPatternDarkVertical:
790 | 		return 3 // xlPatternDarkVertical
791 | 	case FillPatternDarkDown:
792 | 		return 4 // xlPatternDarkDown
793 | 	case FillPatternDarkUp:
794 | 		return 14 // xlPatternDarkUp
795 | 	case FillPatternDarkGrid:
796 | 		return -4162 // xlPatternDarkGrid
797 | 	case FillPatternDarkTrellis:
798 | 		return -4166 // xlPatternDarkTrellis
799 | 	case FillPatternNone:
800 | 		return -4142 // xlPatternNone
801 | 	default:
802 | 		return -4142 // xlPatternNone
803 | 	}
804 | }
805 | 
806 | func normalizePath(path string) string {
807 | 	// Normalize the volume name to uppercase
808 | 	vol := filepath.VolumeName(path)
809 | 	if vol == "" {
810 | 		return path
811 | 	}
812 | 	rest := path[len(vol):]
813 | 	return filepath.Clean(strings.ToUpper(vol) + rest)
814 | }
815 | 
```