fix: close gemini renderer gaps (tables, link markers, figlet crash)
This commit is contained in:
parent
5ec7da26ab
commit
9d290ed734
6 changed files with 215 additions and 6 deletions
|
|
@ -12,6 +12,12 @@ dependencies = [
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
md2txt = "md2txt.cli:main"
|
md2txt = "md2txt.cli:main"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["pytest>=8.0"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=68", "wheel"]
|
requires = ["setuptools>=68", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ def parse_frontmatter(lines: List[str]) -> Tuple[FrontMatter, List[str]]:
|
||||||
list_marker_indent = max(0, _parse_int(frontmatter.get("list_marker_indent"), 0))
|
list_marker_indent = max(0, _parse_int(frontmatter.get("list_marker_indent"), 0))
|
||||||
list_text_spacing = max(0, _parse_int(frontmatter.get("list_text_spacing"), 1))
|
list_text_spacing = max(0, _parse_int(frontmatter.get("list_text_spacing"), 1))
|
||||||
links_per_block = _parse_bool(frontmatter.get("links_per_block"), False)
|
links_per_block = _parse_bool(frontmatter.get("links_per_block"), False)
|
||||||
|
link_markers = _parse_bool(frontmatter.get("link_markers"), True)
|
||||||
fm = FrontMatter(
|
fm = FrontMatter(
|
||||||
h1_font=frontmatter.get("h1_font", "standard").strip() or "standard",
|
h1_font=frontmatter.get("h1_font", "standard").strip() or "standard",
|
||||||
h2_font=frontmatter.get("h2_font", "standard").strip() or "standard",
|
h2_font=frontmatter.get("h2_font", "standard").strip() or "standard",
|
||||||
|
|
@ -127,6 +128,7 @@ def parse_frontmatter(lines: List[str]) -> Tuple[FrontMatter, List[str]]:
|
||||||
list_marker_indent=list_marker_indent,
|
list_marker_indent=list_marker_indent,
|
||||||
list_text_spacing=list_text_spacing,
|
list_text_spacing=list_text_spacing,
|
||||||
links_per_block=links_per_block,
|
links_per_block=links_per_block,
|
||||||
|
link_markers=link_markers,
|
||||||
)
|
)
|
||||||
return fm, remaining
|
return fm, remaining
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ class FrontMatter:
|
||||||
list_marker_indent: int = 0
|
list_marker_indent: int = 0
|
||||||
list_text_spacing: int = 1
|
list_text_spacing: int = 1
|
||||||
links_per_block: bool = False
|
links_per_block: bool = False
|
||||||
|
link_markers: bool = True # gemini renderer: suppress inline [n] markers
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from ..models import (
|
||||||
ListItemPayload,
|
ListItemPayload,
|
||||||
ParagraphPayload,
|
ParagraphPayload,
|
||||||
StyleUpdateEvent,
|
StyleUpdateEvent,
|
||||||
|
TablePayload,
|
||||||
)
|
)
|
||||||
from ..plugins import register_renderer
|
from ..plugins import register_renderer
|
||||||
from .text import (
|
from .text import (
|
||||||
|
|
@ -37,11 +38,15 @@ class GeminiRenderer:
|
||||||
def __init__(self, frontmatter: FrontMatter, *, width: int = 80, preformatted_alt: str | None = None, **_: Any) -> None:
|
def __init__(self, frontmatter: FrontMatter, *, width: int = 80, preformatted_alt: str | None = None, **_: Any) -> None:
|
||||||
self.frontmatter = frontmatter
|
self.frontmatter = frontmatter
|
||||||
self._text_renderer = TextRenderer(width=width, frontmatter=frontmatter)
|
self._text_renderer = TextRenderer(width=width, frontmatter=frontmatter)
|
||||||
self.width = max(20, width)
|
# width <= 0 means "don't wrap" -- for a served capsule the client
|
||||||
|
# wraps to its own screen width, so 0 or below is a real request,
|
||||||
|
# not a mistake to clamp away like the positive minimum below.
|
||||||
|
self.width = width if width <= 0 else max(20, width)
|
||||||
self.preformatted_alt = preformatted_alt or ""
|
self.preformatted_alt = preformatted_alt or ""
|
||||||
self.output: List[str] = []
|
self.output: List[str] = []
|
||||||
self._in_list: bool = False
|
self._in_list: bool = False
|
||||||
self.links_per_block = frontmatter.links_per_block
|
self.links_per_block = frontmatter.links_per_block
|
||||||
|
self.link_markers = frontmatter.link_markers
|
||||||
self._link_indices: Dict[str, int] = {}
|
self._link_indices: Dict[str, int] = {}
|
||||||
self._link_catalog: Dict[int, Tuple[str, Optional[str]]] = {}
|
self._link_catalog: Dict[int, Tuple[str, Optional[str]]] = {}
|
||||||
self._pending_links: List[int] = []
|
self._pending_links: List[int] = []
|
||||||
|
|
@ -57,6 +62,7 @@ class GeminiRenderer:
|
||||||
BlockKind.BLOCKQUOTE: self._render_blockquote,
|
BlockKind.BLOCKQUOTE: self._render_blockquote,
|
||||||
BlockKind.LIST_ITEM: self._render_list_item,
|
BlockKind.LIST_ITEM: self._render_list_item,
|
||||||
BlockKind.HORIZONTAL_RULE: self._render_horizontal_rule,
|
BlockKind.HORIZONTAL_RULE: self._render_horizontal_rule,
|
||||||
|
BlockKind.TABLE: self._render_table,
|
||||||
BlockKind.BLANK_LINE: self._render_blank_line,
|
BlockKind.BLANK_LINE: self._render_blank_line,
|
||||||
BlockKind.CUSTOM_BLOCK: self._render_custom_block,
|
BlockKind.CUSTOM_BLOCK: self._render_custom_block,
|
||||||
}.get(event.kind)
|
}.get(event.kind)
|
||||||
|
|
@ -82,7 +88,13 @@ class GeminiRenderer:
|
||||||
text, links = self._process_inline(payload.text)
|
text, links = self._process_inline(payload.text)
|
||||||
|
|
||||||
def render_heading(target_style: BlockStyle) -> List[str]:
|
def render_heading(target_style: BlockStyle) -> List[str]:
|
||||||
figlet = self._text_renderer._render_figlet_heading(payload.level, text, target_style)
|
# FIGlet needs a real column width to lay characters out against
|
||||||
|
# (width <= 0 means "don't wrap, the client wraps itself" --
|
||||||
|
# there is no width to lay out against at all, and asking
|
||||||
|
# pyfiglet to try raises CharNotPrinted rather than degrading
|
||||||
|
# gracefully), so a served, unwrapped capsule always gets a
|
||||||
|
# plain "#" heading regardless of h#_font.
|
||||||
|
figlet = self._text_renderer._render_figlet_heading(payload.level, text, target_style) if self.width > 0 else None
|
||||||
if figlet:
|
if figlet:
|
||||||
banner = [line.rstrip() for line in figlet]
|
banner = [line.rstrip() for line in figlet]
|
||||||
fence_header = self._preformatted_header()
|
fence_header = self._preformatted_header()
|
||||||
|
|
@ -143,6 +155,23 @@ class GeminiRenderer:
|
||||||
self._ensure_blank_line()
|
self._ensure_blank_line()
|
||||||
self.output.append("---")
|
self.output.append("---")
|
||||||
|
|
||||||
|
def _render_table(self, payload: TablePayload, _style: BlockStyle) -> None:
|
||||||
|
# Gemtext has no table syntax, so the grid goes inside a
|
||||||
|
# preformatted block with columns space-padded to equal width --
|
||||||
|
# the same treatment wapdown gives an oversized card table.
|
||||||
|
self._end_list()
|
||||||
|
rows = payload.rows
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
self._ensure_blank_line()
|
||||||
|
columns = max(len(row) for row in rows)
|
||||||
|
cells = [[self._normalise_whitespace(cell) for cell in row] + [""] * (columns - len(row)) for row in rows]
|
||||||
|
widths = [max(len(row[i]) for row in cells) for i in range(columns)]
|
||||||
|
self.output.append(self._preformatted_header())
|
||||||
|
for row in cells:
|
||||||
|
self.output.append(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)).rstrip())
|
||||||
|
self.output.append("```")
|
||||||
|
|
||||||
def _render_blank_line(self, *_: object) -> None:
|
def _render_blank_line(self, *_: object) -> None:
|
||||||
self._end_list()
|
self._end_list()
|
||||||
self._ensure_blank_line(force=True)
|
self._ensure_blank_line(force=True)
|
||||||
|
|
@ -189,6 +218,10 @@ class GeminiRenderer:
|
||||||
normalized = self._normalise_whitespace(text)
|
normalized = self._normalise_whitespace(text)
|
||||||
if not normalized:
|
if not normalized:
|
||||||
return [initial.rstrip()] if initial else []
|
return [initial.rstrip()] if initial else []
|
||||||
|
if self.width <= 0:
|
||||||
|
# A served capsule: the client wraps to its own screen width,
|
||||||
|
# so emit the whole block as one line instead of hard-wrapping.
|
||||||
|
return [f"{initial}{normalized}"]
|
||||||
wrapper = textwrap.TextWrapper(
|
wrapper = textwrap.TextWrapper(
|
||||||
width=self.width,
|
width=self.width,
|
||||||
expand_tabs=False,
|
expand_tabs=False,
|
||||||
|
|
@ -234,6 +267,8 @@ class GeminiRenderer:
|
||||||
index = self._register_link(url, link_label)
|
index = self._register_link(url, link_label)
|
||||||
if index not in indices:
|
if index not in indices:
|
||||||
indices.append(index)
|
indices.append(index)
|
||||||
|
if not self.link_markers:
|
||||||
|
return display
|
||||||
return f"{display} [{index}]"
|
return f"{display} [{index}]"
|
||||||
|
|
||||||
text = CODE_STASH_RE.sub(stash_code, text)
|
text = CODE_STASH_RE.sub(stash_code, text)
|
||||||
|
|
@ -289,10 +324,11 @@ class GeminiRenderer:
|
||||||
self.output.append("")
|
self.output.append("")
|
||||||
for index in indices:
|
for index in indices:
|
||||||
url, label = self._link_catalog.get(index, ("", None))
|
url, label = self._link_catalog.get(index, ("", None))
|
||||||
label_text = f"[{index}]"
|
if self.link_markers:
|
||||||
if label:
|
label_text = f"[{index}]" + (f" {label}" if label else "")
|
||||||
label_text = f"{label_text} {label}"
|
else:
|
||||||
line = f"=> {url}" if not label_text else f"=> {url} {label_text}"
|
label_text = label or ""
|
||||||
|
line = f"=> {url} {label_text}" if label_text else f"=> {url}"
|
||||||
self.output.append(line)
|
self.output.append(line)
|
||||||
if trailing_blank and not trailing_blanks and self.output and self.output[-1] != "":
|
if trailing_blank and not trailing_blanks and self.output and self.output[-1] != "":
|
||||||
self.output.append("")
|
self.output.append("")
|
||||||
|
|
|
||||||
95
tests/test_gemini_renderer.py
Normal file
95
tests/test_gemini_renderer.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
"""Targeted regression tests for the three gaps closed in the gemini
|
||||||
|
renderer: dropped tables, unconditional inline [n] markers, and hard
|
||||||
|
wrapping. This project has no existing test harness, so these are plain
|
||||||
|
assertions rather than the golden-file setup a larger suite might use.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from md2txt import convert_markdown
|
||||||
|
from md2txt.models import FrontMatter
|
||||||
|
|
||||||
|
|
||||||
|
def render(text: str, *, width: int = 80, **frontmatter_kwargs) -> str:
|
||||||
|
lines = text.splitlines(keepends=True)
|
||||||
|
frontmatter = FrontMatter(**frontmatter_kwargs)
|
||||||
|
rendered = convert_markdown(
|
||||||
|
lines, width=width, frontmatter=frontmatter, renderer_name="gemini"
|
||||||
|
)
|
||||||
|
return "\n".join(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTableRendering:
|
||||||
|
def test_table_is_no_longer_dropped(self):
|
||||||
|
markup = render("| A | B |\n| --- | --- |\n| 1 | 2 |\n")
|
||||||
|
assert "A" in markup and "B" in markup and "1" in markup and "2" in markup
|
||||||
|
|
||||||
|
def test_table_is_wrapped_in_a_preformat_block(self):
|
||||||
|
markup = render("| A | B |\n| --- | --- |\n| 1 | 2 |\n")
|
||||||
|
lines = markup.splitlines()
|
||||||
|
fence_indices = [i for i, line in enumerate(lines) if line.startswith("```")]
|
||||||
|
assert len(fence_indices) == 2, "expected an opening and closing ``` fence"
|
||||||
|
|
||||||
|
def test_columns_are_padded_to_equal_width(self):
|
||||||
|
markup = render("| A | Long Header |\n| --- | --- |\n| 1 | 2 |\n")
|
||||||
|
lines = [l for l in markup.splitlines() if l.strip() and not l.startswith("```")]
|
||||||
|
# Every row's first column should start at the same character
|
||||||
|
# offset -- the visible sign that padding is applied consistently.
|
||||||
|
first_col_starts = {line.index(line.split()[0]) for line in lines}
|
||||||
|
assert len(lines) >= 2
|
||||||
|
assert first_col_starts == {0}
|
||||||
|
|
||||||
|
|
||||||
|
class TestLinkMarkers:
|
||||||
|
SOURCE = "See the [spec](/spec.md) and [FAQ](/faq.md).\n"
|
||||||
|
|
||||||
|
def test_default_keeps_existing_marker_behavior(self):
|
||||||
|
markup = render(self.SOURCE, links_per_block=True)
|
||||||
|
assert "spec [1]" in markup
|
||||||
|
assert "=> /spec.md [1] spec" in markup
|
||||||
|
|
||||||
|
def test_link_markers_false_drops_inline_numbers(self):
|
||||||
|
markup = render(self.SOURCE, links_per_block=True, link_markers=False)
|
||||||
|
assert "See the spec and FAQ." in markup
|
||||||
|
assert "[1]" not in markup and "[2]" not in markup
|
||||||
|
|
||||||
|
def test_link_markers_false_still_emits_link_lines(self):
|
||||||
|
markup = render(self.SOURCE, links_per_block=True, link_markers=False)
|
||||||
|
assert "=> /spec.md spec" in markup
|
||||||
|
assert "=> /faq.md FAQ" in markup
|
||||||
|
|
||||||
|
|
||||||
|
class TestWidthZeroDisablesWrapping:
|
||||||
|
def test_long_paragraph_stays_on_one_line(self):
|
||||||
|
long_text = "word " * 40 # far past 80 columns
|
||||||
|
markup = render(long_text.strip() + "\n", width=0)
|
||||||
|
body_lines = [l for l in markup.splitlines() if l.strip()]
|
||||||
|
assert len(body_lines) == 1
|
||||||
|
assert body_lines[0] == long_text.strip()
|
||||||
|
|
||||||
|
def test_positive_width_still_wraps(self):
|
||||||
|
long_text = "word " * 40
|
||||||
|
markup = render(long_text.strip() + "\n", width=20)
|
||||||
|
body_lines = [l for l in markup.splitlines() if l.strip()]
|
||||||
|
assert len(body_lines) > 1
|
||||||
|
assert all(len(l) <= 20 for l in body_lines)
|
||||||
|
|
||||||
|
|
||||||
|
class TestWidthZeroDisablesFiglet:
|
||||||
|
def test_default_h1_font_does_not_crash_at_width_zero(self):
|
||||||
|
# h1_font defaults to a real pyfiglet font ("small"); pyfiglet lays
|
||||||
|
# characters out against the available width, so width=0 (used to
|
||||||
|
# mean "don't wrap, let the client wrap") used to raise
|
||||||
|
# pyfiglet.CharNotPrinted instead of degrading to a plain heading.
|
||||||
|
markup = render("# Title\n\nBody.\n", width=0)
|
||||||
|
assert "# Title" in markup
|
||||||
|
|
||||||
|
def test_explicit_figlet_font_does_not_crash_at_width_zero(self):
|
||||||
|
markup = render("# Title\n", width=0, h1_font="standard")
|
||||||
|
assert "# Title" in markup
|
||||||
|
|
||||||
|
def test_heading_levels_four_to_six_do_not_crash_at_width_zero(self):
|
||||||
|
# These levels have no FrontMatter field at all; the renderer's
|
||||||
|
# hardcoded getattr(..., "standard") fallback made them figlet
|
||||||
|
# candidates regardless of any frontmatter setting.
|
||||||
|
markup = render("#### Four\n\n##### Five\n\n###### Six\n", width=0)
|
||||||
|
assert "Four" in markup and "Five" in markup and "Six" in markup
|
||||||
69
uv.lock
generated
69
uv.lock
generated
|
|
@ -2,6 +2,24 @@ version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "md2txt"
|
name = "md2txt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -11,12 +29,38 @@ dependencies = [
|
||||||
{ name = "pyphen" },
|
{ name = "pyphen" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "pyfiglet", specifier = ">=0.8.0" },
|
{ name = "pyfiglet", specifier = ">=0.8.0" },
|
||||||
{ name = "pyphen", specifier = ">=0.17.2" },
|
{ name = "pyphen", specifier = ">=0.17.2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "pytest", specifier = ">=8.0" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyfiglet"
|
name = "pyfiglet"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -26,6 +70,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/5c/fe9f95abd5eaedfa69f31e450f7e2768bef121dbdf25bcddee2cd3087a16/pyfiglet-1.0.4-py3-none-any.whl", hash = "sha256:65b57b7a8e1dff8a67dc8e940a117238661d5e14c3e49121032bd404d9b2b39f", size = 1806118, upload-time = "2025-08-15T18:32:45.556Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/5c/fe9f95abd5eaedfa69f31e450f7e2768bef121dbdf25bcddee2cd3087a16/pyfiglet-1.0.4-py3-none-any.whl", hash = "sha256:65b57b7a8e1dff8a67dc8e940a117238661d5e14c3e49121032bd404d9b2b39f", size = 1806118, upload-time = "2025-08-15T18:32:45.556Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyphen"
|
name = "pyphen"
|
||||||
version = "0.17.2"
|
version = "0.17.2"
|
||||||
|
|
@ -34,3 +87,19 @@ sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue