Files
bussines_case_automation/venv/lib/python3.12/site-packages/xlsxwriter/url.py
andrei 0e2e1bddba Add xlsxwriter-based Excel generation scripts with openpyxl implementation
- Created create_excel_xlsxwriter.py and update_excel_xlsxwriter.py
- Uses openpyxl exclusively to preserve Excel formatting and formulas
- Updated server.js to use new xlsxwriter scripts for form submissions
- Maintains all original functionality while ensuring proper Excel file handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 13:53:06 +00:00

269 lines
8.3 KiB
Python

###############################################################################
#
# Url - A class to represent URLs in Excel.
#
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
#
import re
from enum import Enum
from typing import Any, Dict, Optional
class UrlTypes(Enum):
"""
Enum to represent different types of URLS.
"""
UNKNOWN = 0
URL = 1
INTERNAL = 2
EXTERNAL = 3
class Url:
"""
A class to represent URLs in Excel.
"""
MAX_URL_LEN = 2080
MAX_PARAMETER_LEN = 255
def __init__(self, link: str) -> None:
self._link_type: UrlTypes = UrlTypes.UNKNOWN
self._original_url: str = link
self._link: str = link
self._relationship_link: str = link
self._text: str = ""
self._tip: str = ""
self._anchor: str = ""
self._is_object_link: bool = False
self._rel_index: int = 0
self._parse_url()
if len(self._link) > self.MAX_URL_LEN:
raise ValueError("URL exceeds Excel's maximum length.")
if len(self._anchor) > self.MAX_URL_LEN:
raise ValueError("Anchor segment or url exceeds Excel's maximum length.")
if len(self._tip) > self.MAX_PARAMETER_LEN:
raise ValueError("Hyperlink tool tip exceeds Excel's maximum length.")
self._escape_strings()
def __repr__(self) -> str:
"""
Return a string representation of the Url instance.
"""
return (
"\n"
f"Url:\n"
f" _link_type = {self._link_type.name}\n"
f" _original_url = {self._original_url}\n"
f" _link = {self._link}\n"
f" _relationship_link = {self._relationship_link}\n"
f" _text = {self._text}\n"
f" _tip = {self._tip}\n"
f" _anchor = {self._anchor}\n"
f" _is_object_link = {self._is_object_link}\n"
f" _rel_index = {self._rel_index}\n"
)
@classmethod
def from_options(cls, options: Dict[str, Any]) -> Optional["Url"]:
"""
For backward compatibility, convert the 'url' key and 'tip' keys in an
options dictionary to a Url object, or return the Url object if already
an instance.
Args:
options (dict): A dictionary that may contain a 'url' key.
Returns:
url: A Url object or None.
"""
if not isinstance(options, dict):
raise TypeError("The 'options' parameter must be a dictionary.")
url = options.get("url")
if isinstance(url, str):
url = cls(options["url"])
if options.get("tip"):
url._tip = options["tip"]
return url
@property
def text(self) -> str:
"""Get the alternative, user-friendly, text for the URL."""
return self._text
@text.setter
def text(self, value: str) -> None:
"""Set the alternative, user-friendly, text for the URL."""
self._text = value
@property
def tip(self) -> str:
"""Get the screen tip for the URL."""
return self._tip
@tip.setter
def tip(self, value: str) -> None:
"""Set the screen tip for the URL."""
self._tip = value
def _parse_url(self) -> None:
"""Parse the URL and determine its type."""
# Handle mail address links.
if self._link.startswith("mailto:"):
self._link_type = UrlTypes.URL
if not self._text:
self._text = self._link.replace("mailto:", "", 1)
# Handle links to cells within the workbook.
elif self._link.startswith("internal:"):
self._link_type = UrlTypes.INTERNAL
self._relationship_link = self._link.replace("internal:", "#", 1)
self._link = self._link.replace("internal:", "", 1)
self._anchor = self._link
if not self._text:
self._text = self._anchor
# Handle links to other files or cells in other Excel files.
elif self._link.startswith("file://") or self._link.startswith("external:"):
self._link_type = UrlTypes.EXTERNAL
# Handle backward compatibility with external: links.
file_url = self._original_url.replace("external:", "file:///", 1)
link_path = file_url
link_path = link_path.replace("file:///", "", 1)
link_path = link_path.replace("file://", "", 1)
link_path = link_path.replace("/", "\\")
if self._is_relative_path(link_path):
self._link = link_path
else:
self._link = "file:///" + link_path
if not self._text:
self._text = link_path
if "#" in self._link:
self._link, self._anchor = self._link.split("#", 1)
# Set up the relationship link. This doesn't usually contain the
# anchor unless it is a link from an object like an image.
if self._is_object_link:
if self._is_relative_path(link_path):
self._relationship_link = self._link.replace("\\", "/")
else:
self._relationship_link = file_url
else:
self._relationship_link = self._link
# Convert a .\dir\file.xlsx link to dir\file.xlsx.
if self._relationship_link.startswith(".\\"):
self._relationship_link = self._relationship_link.replace(".\\", "", 1)
# Handle standard Excel links like http://, https://, ftp://, ftps://
# but also allow custom "foo://bar" URLs.
elif "://" in self._link:
self._link_type = UrlTypes.URL
if not self._text:
self._text = self._link
if "#" in self._link:
self._link, self._anchor = self._link.split("#", 1)
# Set up the relationship link. This doesn't usually contain the
# anchor unless it is a link from an object like an image.
if self._is_object_link:
self._relationship_link = self._original_url
else:
self._relationship_link = self._link
else:
raise ValueError(f"Unknown URL type: {self._original_url}")
def _set_object_link(self) -> None:
"""
Set the _is_object_link flag and re-parse the URL since the relationship
link is different for object links.
"""
self._is_object_link = True
self._link = self._original_url
self._parse_url()
self._escape_strings()
def _escape_strings(self) -> None:
"""Escape special characters in the URL strings."""
if self._link_type != UrlTypes.INTERNAL:
self._link = self._escape_url(self._link)
self._relationship_link = self._escape_url(self._relationship_link)
# Excel additionally escapes # to %23 in file paths.
if self._link_type == UrlTypes.EXTERNAL:
self._relationship_link = self._relationship_link.replace("#", "%23")
def _target(self) -> str:
"""Get the target for relationship IDs."""
return self._relationship_link
def _target_mode(self) -> str:
"""Get the target mode for relationship IDs."""
if self._link_type == UrlTypes.INTERNAL:
return ""
return "External"
@staticmethod
def _is_relative_path(url: str) -> bool:
"""Check if a URL is a relative path."""
if url.startswith(r"\\"):
return False
if url[0].isalpha() and url[1] == ":":
return False
return True
@staticmethod
def _escape_url(url: str) -> str:
"""Escape special characters in a URL."""
# Don't escape URL if it looks already escaped.
if re.search("%[0-9a-fA-F]{2}", url):
return url
# Can't use url.quote() here because it doesn't match Excel.
return (
url.replace("%", "%25")
.replace('"', "%22")
.replace(" ", "%20")
.replace("<", "%3c")
.replace(">", "%3e")
.replace("[", "%5b")
.replace("]", "%5d")
.replace("^", "%5e")
.replace("`", "%60")
.replace("{", "%7b")
.replace("}", "%7d")
)