8000 para: add Paragraph.hyperlinks · python-openxml/python-docx@e654522 · GitHub
[go: up one dir, main page]

Skip to content

Commit e654522

Browse files
committed
para: add Paragraph.hyperlinks
1 parent b85b24f commit e654522

File tree

7 files changed

+73
-1
lines changed

7 files changed

+73
-1
lines changed

features/par-access-inner-content.feature

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ Feature: Access paragraph inner-content including hyperlinks
1515
| two | True |
1616

1717

18-
@wip
1918
Scenario Outline: Paragraph.hyperlinks contains Hyperlink for each link in paragraph
2019
Given a paragraph having <zero-or-more> hyperlinks
2120
Then paragraph.hyperlinks has length <value>

src/docx/oxml/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
CT_ShapeProperties,
2222
CT_Transform2D,
2323
)
24+
from docx.oxml.text.hyperlink import CT_Hyperlink
2425
from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
2526
from docx.oxml.text.run import (
2627
CT_R,
@@ -50,6 +51,11 @@
5051
register_element_cls("wp:extent", CT_PositiveSize2D)
5152
register_element_cls("wp:inline", CT_Inline)
5253

54+
# ---------------------------------------------------------------------------
55+
# hyperlink-related elements
56+
57+
register_element_cls("w:hyperlink", CT_Hyperlink)
58+
5359
# ---------------------------------------------------------------------------
5460
# text-related elements
5561

src/docx/oxml/text/hyperlink.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Custom element classes related to hyperlinks (CT_Hyperlink)."""
2+
3+
from __future__ import annotations
4+
5+
from docx.oxml.xmlchemy import BaseOxmlElement
6+
7+
8+
class CT_Hyperlink(BaseOxmlElement):
9+
"""`<w:hyperlink>` element, containing the text and address for a hyperlink."""

src/docx/oxml/text/paragraph.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
if TYPE_CHECKING:
1111
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
12+
from docx.oxml.text.hyperlink import CT_Hyperlink
1213
from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
1314
from docx.oxml.text.parfmt import CT_PPr
1415
from docx.oxml.text.run import CT_R
@@ -18,9 +19,11 @@ class CT_P(BaseOxmlElement):
1819
"""`<w:p>` element, containing the properties and text for a paragraph."""
1920

2021
get_or_add_pPr: Callable[[], CT_PPr]
22+
hyperlink_lst: List[CT_Hyperlink]
2123
r_lst: List[CT_R]
2224

2325
pPr: CT_PPr | None = ZeroOrOne("w:pPr") # pyright: ignore[reportGeneralTypeIssues]
26+
hyperlink = ZeroOrMore("w:hyperlink")
2427
r = ZeroOrMore("w:r")
2528

2629
def add_p_before(self) -> CT_P:

src/docx/text/hyperlink.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Hyperlink-related proxy objects for python-docx, Hyperlink in particular.
2+
3+
A hyperlink occurs in a paragraph, at the same level as a Run, and a hyperlink itself
4+
contains runs, which is where the visible text of the hyperlink is stored. So it's kind
5+
of in-between, less than a paragraph and more than a run. So it gets its own module.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from docx import types as t
11+
from docx.oxml.text.hyperlink import CT_Hyperlink
12+
from docx.shared import Parented
13+
14+
< 6D47 /td>
15+
class Hyperlink(Parented):
16+
"""Proxy object wrapping a `<w:hyperlink>` element.
17+
18+
A hyperlink occurs as a child of a paragraph, at the same level as a Run. A
19+
hyperlink itself contains runs, which is where the visible text of the hyperlink is
20+
stored.
21+
"""
22+
23+
def __init__(self, hyperlink: CT_Hyperlink, parent: t.StoryChild):
24+
super().__init__(parent)
25+
self._parent = parent
26+
self._hyperlink = self._element = hyperlink

src/docx/text/paragraph.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from docx.oxml.text.paragraph import CT_P
1313
from docx.shared import Parented
1414
from docx.styles.style import CharacterStyle, ParagraphStyle
15+
from docx.text.hyperlink import Hyperlink
1516
from docx.text.parfmt import ParagraphFormat
1617
from docx.text.run import Run
1718

@@ -69,6 +70,11 @@ def contains_page_break(self) -> bool:
6970
"""`True` when one or more rendered page-breaks occur in this paragraph."""
7071
return bool(self._p.lastRenderedPageBreaks)
7172

73+
@property
74+
def hyperlinks(self) -> List[Hyperlink]:
75+
"""A |Hyperlink| instance for each hyperlink in this paragraph."""
76+
return [Hyperlink(hyperlink, self) for hyperlink in self._p.hyperlink_lst]
77+
7278
def insert_paragraph_before(
7379
self, text: str | None = None, style: str | ParagraphStyle | None = None
7480
) -> Self:

tests/text/test_paragraph.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,29 @@ def it_knows_whether_it_contains_a_page_break(
3838

3939
assert paragraph.contains_page_break == expected_value
4040

41+
@pytest.mark.parametrize(
42+
("p_cxml", "count"),
43+
[
44+
("w:p", 0),
45+
("w:p/w:r", 0),
46+
("w:p/w:hyperlink", 1),
47+
("w:p/(w:r,w:hyperlink,w:r)", 1),
48+
("w:p/(w:r,w:hyperlink,w:r,w:hyperlink)", 2),
49+
("w:p/(w:hyperlink,w:r,w:hyperlink,w:r)", 2),
50+
],
51+
)
52+
def it_provides_access_to_the_hyperlinks_it_contains(
53+
self, p_cxml: str, count: int, fake_parent: t.StoryChild
54+
):
55+
p = cast(CT_P, element(p_cxml))
56+
paragraph = Paragraph(p, fake_parent)
57+
58+
hyperlinks = paragraph.hyperlinks
59+
60+
actual = [type(item).__name__ for item in hyperlinks]
61+
expected = ["Hyperlink" for _ in range(count)]
62+
assert actual == expected, f"expected: {expected}, got: {actual}"
63+
4164
def it_knows_its_paragraph_style(self, style_get_fixture):
4265
paragraph, style_id_, style_ = style_get_fixture
4366
style = paragraph.style

0 commit comments

Comments
 (0)
0