15
15
from ..enum .style import WD_STYLE_TYPE
16
16
from .parfmt import ParagraphFormat
17
17
from .run import Run
18
- from ..shared import Parented , Length , lazyproperty , Inches , cache , bust_cache
18
+ from .hyperlink import Hyperlink
19
+ from ..shared import Parented , Length , find_containing_document , is_valid_url , lazyproperty , Inches , cache , bust_cache
19
20
from ..oxml .ns import nsmap
21
+ from ..oxml .text .hyperlink import CT_Hyperlink
20
22
from docx .bookmark import BookmarkParent
21
23
from docx .parts .image import ImagePart
22
24
25
+ from docx .opc .constants import RELATIONSHIP_TYPE as RT
23
26
24
27
# Decorator for all text changing functions used to invalidate text cache.
25
28
text_changing = bust_cache (('text' , 'run_text' ))
@@ -51,6 +54,19 @@ def add_footnote(self):
51
54
footnote = document ._add_footnote (new_fr_id )
52
55
return footnote
53
56
57
+ def add_hyperlink (self , text , reference ):
58
+ """
59
+ Append a ``<w:hyperlink>`` element.
60
+ The passed `reference` can be a valid URL address or
61
+ an bookmark name.
62
+ """
63
+ if is_valid_url (reference ):
64
+ # Store URL as relationship rId
65
+ rId = find_containing_document (self ).part .relate_to (
66
+ reference , RT .HYPERLINK , True )
67
+ reference = rId
68
+ return Hyperlink (self ._p .add_hyperlink (text , reference ), self )
69
+
54
70
@text_changing
55
71
def add_run (self , text = None , style = None ):
56
72
"""
@@ -110,7 +126,6 @@ def set_std_placeholder_text(r, text=None):
110
126
active_placeholder = sdtPr ._add_active_placeholder ()
111
127
active_placeholder .set ('{%s}val' % nsmap ['w' ], 'true' )
112
128
113
-
114
129
sdt = self ._p ._new_sdt ()
115
130
116
131
sdtPr = sdt ._add_sdtPr ()
@@ -164,6 +179,14 @@ def clear(self):
164
179
self ._p .clear_content ()
165
180
return self
166
181
182
+ @property
183
+ def runs_and_hyperlinks (self ):
184
+ """
185
+ Sequence of |Run| and |Hyperlink| instances corresponding to the
186
+ ``<w:r>`` and ``<w:hyperlink>`` elements in this paragraph.
187
+ """
188
+ return [Hyperlink (e , self ) if isinstance (e , CT_Hyperlink ) else Run (e , self ) for e in self ._p .iter_r_and_hyperlinks (return_hyperlinks = True )]
189
+
167
190
@property
168
191
def footnotes (self ):
169
192
"""
@@ -179,6 +202,14 @@ def footnotes(self):
179
202
footnote_list .append (footnotes [ref_id ])
180
203
return footnote_list
181
204
205
+ @property
206
+ def hyperlinks (self ):
207
+ """
208
+ Sequence of |Hyperlink| instances corresponding to the <w:hyperlink>
209
+ elements in this paragraph.
210
+ """
211
+ return [Hyperlink (h , self ) for h in self ._p .hyperlink_lst ]
212
+
182
213
def insert_paragraph_before (self , text = None , style = None , ilvl = None ):
183
214
"""
184
215
Return a newly created paragraph, inserted directly before this
@@ -269,7 +300,7 @@ def remove_text(self, start=0, end=-1):
269
300
runend = runstart + len (run .text )
270
301
if runstart <= start and end <= runend :
271
302
run .text = run .text [:(start - runstart )] \
272
- + run .text [(end - runstart ):]
303
+ + run .text [(end - runstart ):]
273
304
if not run .text :
274
305
run ._r .getparent ().remove (run ._r )
275
306
return self
@@ -334,7 +365,8 @@ def lvl_from_para_props(self):
334
365
"""
335
366
if self ._lvl_from_para_props is None :
336
367
try :
337
- self ._lvl_from_para_props = self ._p .lvl_from_para_props (self .part .numbering_part ._element )
368
+ self ._lvl_from_para_props = self ._p .lvl_from_para_props (
369
+ self .part .numbering_part ._element )
338
370
except (AttributeError , NotImplementedError ):
339
371
return None
340
372
return self ._lvl_from_para_props
@@ -384,7 +416,7 @@ def runs(self):
384
416
Sequence of |Run| instances corresponding to the <w:r> elements in
385
417
this paragraph.
386
418
"""
387
- return [Run (r , self ) for r in self ._p .iter_r_lst_recursive ()]
419
+ return [Run (r , self ) for r in self ._p .iter_r_and_hyperlinks ()]
388
420
389
421
@property
390
422
def bookmark_starts (self ):
@@ -445,7 +477,7 @@ def set_li_lvl(self, styles, prev, ilvl):
445
477
prev_el = prev ._element if prev else None
446
478
_ilvl = 0 if ilvl is None else ilvl
447
479
self ._p .set_li_lvl (self .part .numbering_part ._element ,
448
- self .part .cached_styles , prev_el , _ilvl )
480
+ self .part .cached_styles , prev_el , _ilvl )
449
481
450
482
@property
451
483
@cache
@@ -506,7 +538,7 @@ def insert_text(self, position, new_text):
506
538
runend += len (run .text )
507
539
if runend >= position :
508
540
run .text = run .text [:(position - runstart )] \
509
- + new_text + run .text [(position - runstart ):]
541
+ + new_text + run .text [(position - runstart ):]
510
542
break
511
543
return self
512
544
@@ -618,7 +650,8 @@ def _inner_get_tabstops(obj):
618
650
619
651
obj = obj .paragraph_format
620
652
621
- tabstops .extend ([round (ts .position .inches , 2 ) for ts in obj .tab_stops ])
653
+ tabstops .extend ([round (ts .position .inches , 2 )
654
+ for ts in obj .tab_stops ])
622
655
clear_t_stops = [round (ts .position .inches , 2 )
623
656
for ts in obj .tab_stops
624
657
if ts ._element .attrib ['{%s}val' % nsmap ['w' ]] == 'clear' ]
@@ -637,16 +670,21 @@ def apply_formatting(source, first_line_indent=None, left_indent=None):
637
670
638
671
# Apply paragraph styles by priority (from lowest to highest).
639
672
# Formatting from the base style has the lowest priority.
640
- first_line_indent = get_base_style_attr (self .style , 'first_line_indent' )
673
+ first_line_indent = get_base_style_attr (
674
+ self .style , 'first_line_indent' )
641
675
left_indent = get_base_style_attr (self .style , 'left_indent' )
642
676
# Next, we apply formatting from numbering properties defined in paragraph style.
643
- first_line_indent , left_indent = apply_formatting (self .style_numbering_format , first_line_indent , left_indent )
677
+ first_line_indent , left_indent = apply_formatting (
678
+ self .style_numbering_format , first_line_indent , left_indent )
644
679
# Then formatting from paragraph style.
645
- first_line_indent , left_indent = apply_formatting (self .style .paragraph_format , first_line_indent , left_indent )
680
+ first_line_indent , left_indent = apply_formatting (
681
+ self .style .paragraph_format , first_line_indent , left_indent )
646
682
# Next, formatting from numbering properties defined in direct paragraph properties is applied.
647
- first_line_indent , left_indent = apply_formatting (self .para_numbering_format , first_line_indent , left_indent )
683
+ first_line_indent , left_indent = apply_formatting (
684
+ self .para_numbering_format , first_line_indent , left_indent )
648
685
# Finally, we apply formatting from direct paragraph formatting.
649
- first_line_indent , left_indent = apply_formatting (self .paragraph_format , first_line_indent , left_indent )
686
+ first_line_indent , left_indent = apply_formatting (
687
+ self .paragraph_format , first_line_indent , left_indent )
650
688
651
689
# Get explicitly set indentation
652
690
if first_line_indent is not None :
@@ -664,13 +702,15 @@ def apply_formatting(source, first_line_indent=None, left_indent=None):
664
702
665
703
# Find out the number of tabs at the beginning of the paragraph.
666
704
# Ignore regular spaces.
667
- tab_count = self .text [:len (self .text ) - len (self .text .lstrip ())].count ('\t ' )
705
+ tab_count = self .text [:len (self .text ) -
706
+ len (self .text .lstrip ())].count ('\t ' )
668
707
669
708
if tab_count :
670
709
671
710
# Get tab stops but only those to the right of first line indent as the previous
672
711
# don't affect the indentation.
673
- tab_stops = [ts for ts in get_tabstops (self ) if ts > first_line_indent ]
712
+ tab_stops = [ts for ts in get_tabstops (
713
+ self ) if ts > first_line_indent ]
674
714
675
715
# If the first line indent is left of the paragraph indent, first tab will tab to
676
716
# the paragraph indent.
@@ -695,7 +735,8 @@ def apply_formatting(source, first_line_indent=None, left_indent=None):
695
735
696
736
# Let's round up to the first tab char indent. If already rounded add one.
697
737
tab_count -= 1
698
- indent = math .ceil (indent ) if not indent .is_integer () else indent + 1
738
+ indent = math .ceil (
739
+ indent ) if not indent .is_integer () else indent + 1
699
740
700
741
# The remaining tab chars just adds whole indents.
701
742
indent += tab_count
0 commit comments