File Coverage

blib/lib/MsOffice/Word/Surgeon/Run.pm
Criterion Covered Total %
statement 57 57 100.0
branch 18 22 81.8
condition 3 3 100.0
subroutine 12 12 100.0
pod 4 4 100.0
total 94 98 95.9


line stmt bran cond sub pod time code
1             package MsOffice::Word::Surgeon::Run;
2 4     4   56 use 5.24.0;
  4         13  
3 4     4   17 use Moose;
  4         6  
  4         23  
4 4     4   15840 use MooseX::StrictConstructor;
  4         9  
  4         24  
5 4     4   11076 use MsOffice::Word::Surgeon::Carp;
  4         8  
  4         29  
6 4     4   223 use MsOffice::Word::Surgeon::Utils qw(maybe_preserve_spaces is_at_run_level);
  4         5  
  4         333  
7              
8 4     4   22 use namespace::clean -except => 'meta';
  4         7  
  4         47  
9              
10             #======================================================================
11             # ATTRIBUTES
12             #======================================================================
13              
14             has 'xml_before' => (is => 'ro', isa => 'Str', required => 1);
15             has 'props' => (is => 'ro', isa => 'Str', required => 1);
16             has 'inner_texts' => (is => 'ro', required => 1,
17             isa => 'ArrayRef[MsOffice::Word::Surgeon::Text]');
18              
19             #======================================================================
20             # METHODS
21             #======================================================================
22              
23              
24             sub as_xml {
25 1044     1044 1 1823 my $self = shift;
26 1044         44497 my $xml = $self->xml_before;
27 1044 100       42246 if ($self->inner_texts->@*) {
28 1040         2334 $xml .= "<w:r>";
29 1040 100       40490 $xml .= "<w:rPr>" . $self->props . "</w:rPr>" if $self->props;
30 1040         41276 $xml .= $_->as_xml foreach $self->inner_texts->@*;
31 1040         2011 $xml .= "</w:r>";
32             }
33              
34 1044         4697 return $xml;
35             }
36              
37              
38              
39             sub merge {
40 54     54 1 139 my ($self, $next_run) = @_;
41              
42             # sanity checks
43 54 50       224 $next_run->isa(__PACKAGE__)
44             or croak "argument to merge() should be a " . __PACKAGE__;
45 54 50       2445 $self->props eq $next_run->props
46             or croak sprintf "runs have different properties: '%s' <> '%s'",
47             $self->props, $next_run->props;
48 54 50       2486 !$next_run->xml_before
49             or croak "cannot merge -- next run contains xml before the run : "
50             . $next_run->xml_before;
51              
52             # loop over all text nodes of the next run
53 54         2458 foreach my $txt ($next_run->inner_texts->@*) {
54 54 100 100     2577 if ($self->{inner_texts}->@* && !$txt->xml_before) {
55             # concatenate current literal text with the previous text node
56 49         193 $self->{inner_texts}[-1]->merge($txt);
57             }
58             else {
59             # cannot merge, just add to the list of inner text nodes
60 5         29 push $self->{inner_texts}->@*, $txt;
61             }
62             }
63             }
64              
65              
66             sub replace {
67 1588     1588 1 4895 my ($self, $pattern, $replacement_callback, %replacement_args) = @_;
68              
69             # apply replacement to inner texts
70 1588         3582 $replacement_args{run} = $self;
71             my @inner_xmls
72 1588         64034 = map {$_->replace($pattern, $replacement_callback, %replacement_args)}
  1589         6546  
73             $self->inner_texts->@*;
74              
75             # a machinery of closures for assembling the new xml
76 1588         68514 my $xml = $self->xml_before;
77 1588         2793 my $is_run_open;
78 1589 100   1589   3527 my $maybe_open_run = sub {if (!$is_run_open) {
79 1576         3533 $xml .= "<w:r>";
80 1576 100       60312 $xml .= "<w:rPr>" . $self->props . "</w:rPr>" if $self->props;
81 1576         3723 $is_run_open = 1;
82 1588         7290 }};
83 1588 100   1588   3416 my $maybe_close_run = sub {if ($is_run_open) {
84 1576         2543 $xml .= "</w:r>";
85 1576         2853 $is_run_open = undef;
86 1588         4362 }};
87              
88             # apply the machinery, loop over inner texts
89 1588         4350 foreach my $inner_xml (@inner_xmls) {
90 1589 50       4748 is_at_run_level($inner_xml) ? $maybe_close_run->() : $maybe_open_run->();
91 1589         4038 $xml .= $inner_xml;
92             }
93              
94             # final cleanup
95 1588         4009 $maybe_close_run->();
96              
97 1588         14557 return $xml;
98             }
99              
100              
101              
102             sub remove_caps_property {
103 577     577 1 1078 my $self = shift;
104              
105 577 100       2337 if ($self->{props} =~ s[<w:caps/>][]) {
106 1         4 $_->to_uppercase foreach @{$self->inner_texts};
  1         66  
107             }
108             }
109              
110              
111              
112              
113             1;
114              
115             __END__
116              
117             =encoding ISO-8859-1
118              
119             =head1 NAME
120              
121             MsOffice::Word::Surgeon::Run - internal representation for a "run of text"
122              
123             =head1 DESCRIPTION
124              
125             This is used internally by L<MsOffice::Word::Surgeon> for storing
126             a "run of text" in a MsWord document. It loosely corresponds to
127             a C<< <w:r> >> node in OOXML, but may also contain an anonymous XML
128             fragment which is the part of the document just before the C<< <w:r> >>
129             node -- used for reconstructing the complete document after having changed
130             the contents of some runs.
131              
132              
133             =head1 METHODS
134              
135             =head2 new
136              
137             my $run = MsOffice::Word::Surgeon::Run(
138             xml_before => $xml_string,
139             props => $properties_string,
140             inner_texts => [MsOffice::Word::Surgeon::Text(...), ...],
141             );
142              
143             Constructor for a new run object. Arguments are :
144              
145             =over
146              
147             =item xml_before
148              
149             A string containing arbitrary XML preceding that run in the complete document.
150             The string may be empty but must be present.
151              
152             =item props
153              
154             A string containing XML for the properties of this run (for example instructions
155             for bold, italic, font, etc.). The module does not parse this information;
156             it just compares the string for equality with the next run.
157              
158              
159             =item inner_texts
160              
161             An array of L<MsOffice::Word::Surgeon::Text> objects, corresponding to the
162             XML C<< <w:t> >> nodes inside the run.
163              
164             =back
165              
166             =head2 as_xml
167              
168             my $xml = $run->as_xml;
169              
170             Returns the XML representation of that run.
171              
172              
173             =head2 merge
174              
175             $run->merge($next_run);
176              
177             Merge the contents of C<$next_run> together with the current run.
178             This is only possible if both runs have the same properties (same
179             string returned by the C<props> method), and if the next run has
180             an empty C<xml_before> attribute; if the conditions are not met,
181             an exception is raised.
182              
183              
184             =head2 replace
185              
186             my $xml = $run->replace($pattern, $replacement_callback, %replacement_args);
187              
188             Replaces all occurrences of C<$pattern> within all text nodes by
189             a new string computed by C<$replacement_callback>, and returns a new xml
190             string corresponding to the result of all these replacements. This is the
191             internal implementation for public method
192             L<MsOffice::Word::Surgeon::PackagePart/replace>.
193              
194              
195             =head2 remove_caps_property
196              
197             Searches in the run properties for a C<< <w:caps/> >> property;
198             if found, removes it, and replaces all inner texts by their
199             uppercase equivalents.
200              
201              
202             =head1 AUTHOR
203              
204             Laurent Dami, E<lt>dami AT cpan DOT org<gt>
205              
206             =head1 COPYRIGHT AND LICENSE
207              
208             Copyright 2019-2024 by Laurent Dami.
209              
210             This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.