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