File Coverage

blib/lib/ODF/lpOD/TextElement.pm
Criterion Covered Total %
statement 12 12 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 16 16 100.0


line stmt bran cond sub pod time code
1             #=============================================================================
2             #
3             # Copyright (c) 2010 Ars Aperta, Itaapy, Pierlis, Talend.
4             # Copyright (c) 2011 Jean-Marie GouarnĂ©.
5             # Author: Jean-Marie GouarnĂ©
6             #
7             #=============================================================================
8 2     2   57 use 5.010_001;
  2         7  
  2         73  
9 2     2   8 use strict;
  2         4  
  2         65  
10 2     2   8 use experimental 'smartmatch';
  2         2  
  2         14  
11             #=============================================================================
12             # Text Element classes
13             #=============================================================================
14             package ODF::lpOD::TextElement;
15 2     2   130 use base 'ODF::lpOD::Element';
  2         3  
  2         2040  
16             our $VERSION = '1.006';
17             use constant PACKAGE_DATE => '2014-04-30T08:30:28';
18             use ODF::lpOD::Common;
19             #=============================================================================
20              
21             BEGIN {
22             *set_link = *set_hyperlink
23             }
24              
25             #-----------------------------------------------------------------------------
26              
27             our $RECURSIVE_EXPORT = FALSE;
28              
29             sub set_recursive_export
30             {
31             my $caller = shift;
32             $RECURSIVE_EXPORT = is_true(shift);
33             }
34              
35             #--- constructor -------------------------------------------------------------
36              
37             sub _create { ODF::lpOD::TextElement->create(@_) }
38              
39             #-----------------------------------------------------------------------------
40              
41             sub create
42             {
43             my $caller = shift;
44             my %opt = process_options
45             (
46             tag => undef,
47             style => undef,
48             text => undef,
49             @_
50             );
51             my $tag = $opt{tag}; $tag = 'text:' . $tag unless $tag =~ /:/;
52             my $e = ODF::lpOD::Element->create($tag) or return undef;
53             if ($tag eq 'text:h')
54             {
55             $e->set_attribute('outline level', $opt{level} // 1);
56             $e->set_attribute('restart numbering', 'true')
57             if is_true($opt{'restart_numbering'});
58             $e->set_attribute('start value', $opt{start_value})
59             if defined $opt{start_value};
60             $e->set_attribute('is list header', 'true')
61             if defined $opt{suppress_numbering};
62             }
63             $e->set_style($opt{style})
64             if defined $opt{style};
65             $e->set_text($opt{text})
66             if defined $opt{text};
67              
68             return $e;
69             }
70              
71             #=== common tools ============================================================
72              
73             sub set_spaces
74             {
75             my $self = shift;
76             my $count = shift or return undef;
77             my %opt = @_;
78              
79             my $s = $self->insert_element('s', %opt);
80             $s->set_attribute('c', $count);
81             return $s;
82             }
83              
84             sub set_line_break
85             {
86             my $self = shift;
87             return $self->insert_element('line break', @_);
88             }
89              
90             sub set_tab_stop
91             {
92             my $self = shift;
93             return $self->insert_element('tab', @_);
94             }
95              
96             #--- split the content with new child elements -------------------------------
97              
98             sub split_content
99             {
100             my $self = shift;
101             my %opt = process_options
102             (
103             tag => undef,
104             search => undef,
105             offset => undef,
106             length => undef,
107             content => undef,
108             insert => undef,
109             attributes => {},
110             @_
111             );
112             if (defined $opt{search} && defined $opt{length})
113             {
114             alert "Conflicting search and length parameters";
115             return FALSE;
116             }
117             my $search = $opt{search};
118             $opt{repeat} //= (defined $search && ! defined $opt{offset});
119             if (is_true($opt{repeat}))
120             {
121             delete $opt{repeat};
122             my $start = $opt{start_mark};
123             if ($opt{offset})
124             {
125             $start = $self->split_content(%opt);
126             }
127             $opt{offset} = 0;
128             my @elts = ();
129             do {
130             $opt{start_mark} = $start;
131             $start = $self->split_content(%opt);
132             push @elts, $start;
133             }
134             while ($start);
135             return wantarray ? @elts : $elts[0];
136             }
137             my $tag = $self->normalize_name($opt{tag});
138             if (defined $opt{start_mark} || defined $opt{end_mark})
139             {
140             $opt{offset} //= 0;
141             }
142             my $position = $opt{offset} || 0;
143             if ($position eq 'end')
144             {
145             my $e = $self->append_element($tag);
146             $e->set_attributes($opt{attributes});
147             $e->set_text($opt{text});
148             $e->set_class;
149             return $e;
150             }
151             my $range = $opt{length};
152             if ($position == 0 && ! defined $search && ! defined $range)
153             {
154             my $e = $self->insert_element($tag);
155             $e->set_attributes($opt{attributes});
156             $e->set_text($opt{text});
157             $e->set_class;
158             return $e;
159             }
160             my %r = $self->search
161             (
162             $search,
163             offset => $position,
164             range => $range,
165             backward => $opt{backward},
166             start_mark => $opt{start_mark},
167             end_mark => $opt{end_mark}
168             );
169             if (defined $r{segment})
170             {
171             my $e = ODF::lpOD::Element->create($tag);
172             unless ($opt{insert})
173             {
174             my $t = $r{segment}->_get_text;
175             $range = $r{end} - $r{offset}
176             if defined $search;
177             if (defined $range)
178             {
179             substr($t, $r{offset}, $range, "")
180             }
181             else
182             {
183             $t = substr($t, 0, $r{offset});
184             }
185             $r{segment}->_set_text($t);
186             $e->set_text($opt{text} // $r{match});
187             if (
188             (
189             defined $opt{offset}
190             &&
191             $opt{offset} >= 0
192             )
193             ||
194             defined $search
195             )
196             {
197             $e->paste_within
198             ($r{segment}, $r{offset});
199             }
200             else
201             {
202             if ($r{end} < 0)
203             {
204             $e->paste_within
205             ($r{segment}, $r{end});
206             }
207             else
208             {
209             $e->paste_after($r{segment});
210             }
211             }
212              
213             }
214             else
215             {
216             my $p = $opt{insert} eq 'after' ?
217             $r{end} : $r{offset};
218             $e->paste_within($r{segment}, $p);
219             $e->set_text($opt{text});
220             }
221              
222             $e->set_attributes($opt{attributes});
223             $e->set_class;
224             return $e;
225             }
226             return undef;
227             }
228              
229             #--- lpOD-specific bookmark setting ------------------------------------------
230              
231             sub set_lpod_mark
232             {
233             my $self = shift;
234             my %opt = @_;
235             my $id;
236             if (defined $opt{id})
237             {
238             $id = $opt{id}; delete $opt{id};
239             }
240             else
241             {
242             $id = lpod_common->new_id;
243             }
244             $opt{tag} = $ODF::lpOD::Common::LPOD_MARK;
245             $opt{attributes} =
246             {
247             $ODF::lpOD::Common::LPOD_ID => $id
248             };
249             return $self->split_content(%opt);
250             }
251              
252             #--- common bookmark, index mark setting tool --------------------------------
253              
254             sub set_position_mark
255             {
256             my $self = shift;
257             my $tag = shift;
258             my %opt =
259             (
260             offset => undef,
261             before => undef,
262             after => undef,
263             @_
264             );
265             if (defined $opt{before} && defined $opt{after})
266             {
267             alert "Conflicting before and after parameters";
268             return FALSE;
269             }
270              
271             $opt{offset} //= 0;
272             $opt{search} = $opt{before} // $opt{after};
273             if (defined $opt{after}) { $opt{insert} = 'after' }
274             else { $opt{insert} = 'before' }
275             $opt{length} = defined $opt{search} ? undef : 0;
276              
277             delete @opt{qw(before after)};
278             $opt{tag} = $tag;
279             return $self->split_content(%opt);
280             }
281              
282             sub set_text_mark
283             {
284             my $self = shift;
285             my %opt = @_;
286             if (defined $opt{length} && $opt{length} > 0)
287             {
288             unless (ref $opt{offset})
289             {
290             my $start = $opt{offset} // 0;
291             my $end = $start + $opt{length};
292             $opt{offset} = [ $start, $end ];
293             }
294             delete $opt{length};
295             }
296             if (defined $opt{content} || ref $opt{offset})
297             {
298             my $content = $opt{content};
299             my ($p1, $p2, $range_end);
300             if (ref $opt{offset})
301             {
302             $p1 = $opt{offset}[0];
303             $p2 = $opt{offset}[1];
304             $range_end = $self->set_lpod_mark
305             (offset => $p2, length => 0)
306             if defined $p2 && defined $opt{content};
307             $opt{end_mark} = $range_end if $range_end;
308             }
309             else
310             {
311             $p1 = $opt{offset};
312             $p2 = $opt{offset};
313             }
314             delete @opt{qw(content offset)};
315             $opt{offset} = $p1;
316             $opt{before} = $content if defined $content;
317             $opt{role} = 'start';
318             my $start = $self->set_text_mark(%opt)
319             or return FALSE;
320             $opt{offset} = $p2;
321             if (defined $content)
322             {
323             $opt{after} = $content;
324             delete $opt{before};
325             }
326             $opt{role} = 'end';
327             my $end = $self->set_text_mark(%opt)
328             or return FALSE;
329             unless ($start->before($end))
330             {
331             $start->delete; $end->delete;
332             alert("Start is not before end");
333             return FALSE;
334             }
335             if ($range_end)
336             {
337             $range_end->delete(); $self->normalize;
338             }
339             return wantarray ? $start : ($start, $end);
340             }
341              
342             my $tag;
343             given ($opt{role})
344             {
345             when (undef)
346             {
347             $tag = $opt{tag};
348             }
349             when (/^(start|end)$/)
350             {
351             $tag = $opt{tag} . '-' . $_;
352             delete $opt{role};
353             }
354             default
355             {
356             alert("Wrong role = $_ option");
357             return undef;
358             }
359             }
360              
361             delete $opt{tag};
362             return $self->set_position_mark($tag, %opt);
363             }
364              
365             #=== text content handling ===================================================
366              
367             sub set_text
368             {
369             my $self = shift;
370             my $text = shift;
371             return $self->SUPER::set_text($text, @_) unless $text;
372             return $self->_set_text($text) if caller() eq 'XML::Twig::Elt';
373              
374             $self->_set_text("");
375             my @lines = split("\n", $text, -1);
376             while (@lines)
377             {
378             my $line = shift @lines;
379             my @columns = split("\t", $line, -1);
380             while (@columns)
381             {
382             my $column = shift @columns;
383             my @words = split(/(\s\s+)/, $column, -1);
384             foreach my $word (@words)
385             {
386             my $l = length($word);
387             if ($word =~ m/^ +$/)
388             {
389             $self->set_spaces
390             ($l, position => 'LAST_CHILD');
391             }
392             elsif ($l > 0)
393             {
394             my $n = $self->append_element
395             (TEXT_SEGMENT);
396             $n->set_text($word);
397             }
398             }
399             $self->append_element('tab') if @columns;
400             }
401             $self->append_element('line break') if @lines;
402             }
403             $self->normalize;
404             bless $_, 'ODF::lpOD::TextNode' for $self->children('#PCDATA');
405             return TRUE;
406             }
407              
408             sub get_text
409             {
410             my $self = shift;
411             my %opt = @_;
412             return $self->ODF::lpOD::TextNode::get_text
413             if $self->is(TEXT_SEGMENT);
414             $opt{recursive} //= $RECURSIVE_EXPORT;
415             my $text = undef;
416             NODE: foreach my $node ($self->children)
417             {
418             if (
419             $node->isa('ODF::lpOD::TextNode')
420             or
421             $node->isa('ODF::lpOD::TextElement')
422             )
423             {
424             my $t = $node->get_text(%opt);
425             $text .= $t if defined $t;
426             }
427             else
428             {
429             given ($node->get_tag)
430             {
431             when ('text:tab')
432             {
433             $text .= $ODF::lpOD::Common::TAB_STOP;
434             }
435             when ('text:line-break')
436             {
437             $text .= $ODF::lpOD::Common::LINE_BREAK;
438             }
439             when ('text:s')
440             {
441             my $c = $node->get_attribute('c') // 1;
442             $text .= " " while $c-- > 0;
443             }
444             default
445             {
446             if (is_true($opt{recursive}))
447             {
448             my $t = $node->SUPER::get_text(%opt);
449             $text .= $t if defined $t;
450             }
451             }
452             }
453             }
454             }
455              
456             return $text;
457             }
458              
459             #=== text internal markup ===================================================
460              
461             sub set_span
462             {
463             my $self = shift;
464             my %opt = @_;
465             unless ($opt{style})
466             {
467             alert("Missing style name");
468             return FALSE;
469             }
470             $opt{search} = $opt{filter} if exists $opt{filter};
471             $opt{attributes} = { 'style name' => $opt{style} };
472             delete @opt{qw(filter style)};
473             unless (defined $opt{length}) { $opt{search} //= ".*" }
474             else { $opt{offset} //= 0 }
475             return $self->split_content(tag => 'span', %opt);
476             }
477              
478             sub remove_spans
479             {
480             my $self = shift;
481              
482             my $tmp = $self->clone;
483             $self->delete_children;
484             my $count = 0;
485             foreach my $e ($tmp->descendants)
486             {
487             unless ($e->is('text:span'))
488             {
489             $e->move(last_child => $self);
490             }
491             else
492             {
493             $count++;
494             }
495             }
496             $tmp->delete;
497             return $count;
498             }
499              
500             sub set_hyperlink
501             {
502             my $self = shift;
503             my %opt = process_options(@_);
504             my $url = $opt{url};
505             delete $opt{url};
506             unless ($url)
507             {
508             alert("Missing URL"); return FALSE;
509             }
510             $opt{search} = $opt{filter} if exists $opt{filter};
511             $opt{attributes} =
512             {
513             'xlink:href' => $url,
514             'office:name' => $opt{name},
515             'office:title' => $opt{title},
516             'style name' => $opt{style},
517             'visited style name' => $opt{visited_style}
518             };
519             delete @opt{qw(filter name title style visited_style)};
520             unless (defined $opt{length}) { $opt{search} //= ".*" }
521             else { $opt{offset} //= 0 }
522             return $self->split_content(tag => 'a', %opt);
523             }
524              
525             sub set_place_mark
526             {
527             my $self = shift;
528             my $type = shift;
529             my $name = shift;
530             unless ($name)
531             {
532             alert "Missing $type name"; return FALSE;
533             }
534              
535             return $self->set_text_mark
536             (
537             tag => $type,
538             attributes =>
539             {
540             name => $name
541             },
542             @_
543             );
544             }
545              
546             sub set_bookmark
547             {
548             my $self = shift;
549             return $self->set_place_mark('bookmark', @_);
550             }
551              
552             sub set_reference_mark
553             {
554             my $self = shift;
555             return $self->set_place_mark('reference mark', @_);
556             }
557              
558             sub set_reference
559             {
560             my $self = shift;
561             my %opt = @_;
562             $opt{type} //= 'reference';
563             my $tag = 'text:' . $opt{type} . '-ref';
564             $opt{attributes} =
565             {
566             ref_name => $opt{name},
567             reference_format => $opt{format}
568             };
569             delete @opt{qw(type name format)};
570             return $self->set_position_mark($tag, %opt);
571             }
572              
573             sub set_index_mark
574             {
575             my $self = shift;
576             my $text = shift;
577              
578             unless ($text)
579             {
580             alert "Missing index entry text";
581             return FALSE;
582             }
583              
584             my %opt = process_options (@_);
585              
586             if ($opt{index_name})
587             {
588             $opt{type} ||= 'user';
589             unless ($opt{type} eq 'user')
590             {
591             alert "Index mark type must be user";
592             return FALSE;
593             }
594             }
595             else
596             {
597             $opt{type} ||= 'lexical';
598             }
599             my $tag;
600             my %attr = $opt{attributes} ? %{$opt{attributes}} : ();
601             given ($opt{type})
602             {
603             when (["lexical", "alphabetical"])
604             {
605             $tag = 'alphabetical index mark';
606             }
607             when ('toc')
608             {
609             $tag = 'toc mark';
610             $attr{'outline level'} = $opt{level} // 1;
611             }
612             when ('user')
613             {
614             unless ($opt{index_name})
615             {
616             alert "Missing index name";
617             return FALSE;
618             }
619             $tag = 'user index mark';
620             $attr{'outline level'} = $opt{level} // 1;
621             }
622             default
623             {
624             alert "Wrong index mark type ($opt{type})";
625             return FALSE
626             }
627             }
628              
629             if (defined $opt{content} || ref $opt{offset} || $opt{role})
630             { # it's a range index mark
631             $attr{'id'} = $text;
632             }
633             else
634             {
635             $attr{'string value'} = $text;
636             }
637              
638             delete @opt{qw(type index_name level attributes)};
639             $opt{attributes} = {%attr};
640             return $self->set_text_mark(tag => $tag, %opt);
641             }
642              
643             sub set_bibliography_mark
644             {
645             my $self = shift;
646             my %opt = process_options(@_);
647              
648             my $type_ok;
649             foreach my $k (keys %opt)
650             {
651             if (ref $opt{$k} || ($k ~~ ['content', 'role']))
652             {
653             alert "Not allowed option";
654             delete $opt{$k};
655             next;
656             }
657             unless (
658             $k ~~ [
659             'before', 'after', 'offset',
660             'start_mark', 'end_mark'
661             ]
662             )
663             {
664             if ($k eq 'type')
665             {
666             $type_ok = TRUE;
667             $k = 'bibliography type';
668             }
669             $opt{attributes}{$k} = $opt{$k};
670             delete $opt{$k};
671             }
672             }
673             alert "Missing type parameter" unless $type_ok;
674              
675             return $self->set_position_mark('bibliography mark', %opt);
676             }
677              
678             #=== text notes ==============================================================
679              
680             sub set_note
681             {
682             my $self = shift;
683             my $id = shift;
684             unless ($id)
685             {
686             alert "Missing note identifier"; return FALSE;
687             }
688             my %opt = process_options(@_);
689             $opt{attributes} =
690             {
691             'id' => $id,
692             'note class' => $opt{class} || $opt{note_class}
693             || 'footnote'
694             };
695             my $style = $opt{style};
696             my $text = $opt{text};
697             my $body = $opt{body};
698             my $citation = $opt{citation};
699             my $label = $opt{label};
700             delete @opt{qw(note_class style text body citation label)};
701              
702             my $note = $self->set_position_mark('note', %opt);
703             $note->set_citation($citation, $label);
704             $note->{style} = $style;
705             if ($body)
706             {
707             $note->set_body(@{$body});
708             }
709             else
710             {
711             $note->set_body($text);
712             }
713              
714             return $note;
715             }
716              
717             sub set_annotation
718             {
719             my $self = shift;
720             my %opt = process_options(@_);
721             my $date = $opt{date};
722             my $author = $opt{author};
723             my $style = $opt{style};
724             my $content = $opt{content};
725             unshift @$content, $opt{text} if defined $opt{text};
726             delete @opt{qw(date author style content text)};
727             my $a = $self->set_position_mark('office:annotation', %opt);
728             $a->set_date($date);
729             $a->set_author($author);
730             $a->set_style($style);
731             $a->set_content(@$content) if $content;
732             return $a;
733             }
734              
735             #=== text fields =============================================================
736              
737             sub set_field
738             {
739             my $self = shift;
740             my $type = shift;
741             unless ($type)
742             {
743             alert "Missing field type"; return undef;
744             }
745             my %opt = process_options(@_);
746             $opt{search} //= $opt{replace}; delete $opt{replace};
747             $type = 'user field get' if $type eq 'variable';
748             if ($type =~ /^user field/)
749             {
750             unless ($opt{name})
751             {
752             alert "Missing associated variable name";
753             return undef;
754             }
755             }
756             else
757             {
758             unless (ODF::lpOD::TextField::check_type($type))
759             {
760             alert "Unsupported field type"; return undef;
761             }
762             }
763             OPTION: foreach my $k (keys %opt)
764             {
765             if (ref $opt{$k} || ($k eq 'role'))
766             {
767             delete $opt{$k};
768             next;
769             }
770             unless (
771             $k ~~ [
772             'before', 'after', 'offset', 'length',
773             'start_mark', 'end_mark', 'search'
774             ]
775             )
776             {
777             given ($k)
778             {
779             when ('fixed')
780             {
781             $opt{attributes}{$k} =
782             odf_boolean($opt{$k});
783             }
784             when ('style')
785             {
786             my $a = 'style:data-style-name';
787             $opt{attributes}{$a} = $opt{$k};
788             }
789             default
790             {
791             $opt{attributes}{$k} = $opt{$k};
792             }
793             }
794             delete $opt{$k};
795             }
796             }
797             my $tag = 'text:' . $type;
798             my $field;
799             if (defined $opt{search} || defined $opt{length})
800             {
801             $opt{text} = '';
802             $field = $self->split_content(tag => $tag, %opt);
803             }
804             else
805             {
806             $field = $self->set_position_mark($tag , %opt);
807             }
808             return $field;
809             }
810              
811             #=============================================================================
812             package ODF::lpOD::TextHyperlink;
813             use base 'ODF::lpOD::TextElement';
814             our $VERSION = '1.001';
815             use constant PACKAGE_DATE => '2011-08-04T20:52:37';
816             use ODF::lpOD::Common;
817             #-----------------------------------------------------------------------------
818              
819             sub set_type
820             {
821             my $self = shift;
822             return $self->get_attribute('xlink:type');
823             }
824              
825             sub get_type
826             {
827             my $self = shift;
828             return $self->get_attribute('xlink:type');
829             }
830              
831             sub set_style {}
832             sub get_style {}
833              
834             #=============================================================================
835             package ODF::lpOD::Paragraph;
836             use base 'ODF::lpOD::TextElement';
837             our $VERSION = '1.001';
838             use constant PACKAGE_DATE => '2010-12-29T22:28:58';
839             use ODF::lpOD::Common;
840             #--- constructor -------------------------------------------------------------
841              
842             sub _create { ODF::lpOD::Paragraph->create(@_) }
843              
844             #-----------------------------------------------------------------------------
845              
846             sub create
847             {
848             my $caller = shift;
849             return ODF::lpOD::TextElement->create(tag => 'p', @_);
850             }
851              
852             #=============================================================================
853             package ODF::lpOD::Heading;
854             use base 'ODF::lpOD::Paragraph';
855             our $VERSION = '1.001';
856             use constant PACKAGE_DATE => '2010-12-29T22:30:12';
857             use ODF::lpOD::Common;
858             #--- constructor -------------------------------------------------------------
859              
860             sub _create { ODF::lpOD::Heading->create(@_) }
861              
862             #-----------------------------------------------------------------------------
863              
864             sub create
865             {
866             my $caller = shift;
867             return ODF::lpOD::TextElement->create(tag => 'h', @_);
868             }
869              
870             #--- attribute accessors -----------------------------------------------------
871              
872             sub get_level
873             {
874             my $self = shift;
875             return $self->get_attribute('outline level');
876             }
877              
878             sub set_level
879             {
880             my $self = shift;
881             return $self->set_attribute('outline level', @_);
882             }
883              
884             sub get_suppress_numbering
885             {
886             my $self = shift;
887             return $self->get_boolean_attribute('is list header');
888             }
889              
890             sub set_suppress_numbering
891             {
892             my $self = shift;
893             return $self->set_boolean_attribute('is list header', shift);
894             }
895              
896             sub set_start_value
897             {
898             my $self = shift;
899             my $number = shift;
900             unless ($number >= 0)
901             {
902             alert('Wrong start value');
903             return FALSE;
904             }
905             $self->set_attribute('restart numbering', TRUE);
906             $self->set_attribute('start value', $number);
907             }
908              
909             #=============================================================================
910             1;