File Coverage

blib/lib/MojoMojo/Schema/Result/Content.pm
Criterion Covered Total %
statement 52 94 55.3
branch 4 36 11.1
condition 2 9 22.2
subroutine 14 18 77.7
pod 9 9 100.0
total 81 166 48.8


line stmt bran cond sub pod time code
1             package MojoMojo::Schema::Result::Content;
2              
3 40     40   36005 use strict;
  40         103  
  40         1061  
4 40     40   195 use warnings;
  40         87  
  40         1141  
5              
6 40     40   207 use parent qw/MojoMojo::Schema::Base::Result/;
  40         86  
  40         366  
7              
8             =head1 NAME
9              
10             MojoMojo::Schema::Result::Content - Versioned page content
11              
12             =head1 DESCRIPTION
13              
14             This table stores the actual page content; in other words, it's a table of
15             page versions (revisions). It has a composite primary key C<(page, version)>,
16             where C<page> is the id of a page, and C<version> is its version number. Each
17             version has a content C<body>, a C<status> ("released" or "removed"), and a
18             C<release_date>. Revisions that have been replaced by a newer revision have a
19             C<remove_date> and a C<comments> set to "Replaced by version x.".
20              
21             The C<type>, C<abstract> and C<precompiled> columns are for future use.
22              
23             C<created> is essentially equal to C<release_date> (there can be a 1-second
24             difference), and is used externally by other modules and in templates.
25             C<release_date> and C<remove_date> are used internally.
26              
27             =cut
28              
29 40     40   19652 use DateTime::Format::Mail;
  40         1371574  
  40         691  
30              
31 40     40   17725 use Algorithm::Diff;
  40         106064  
  40         1945  
32 40     40   17288 use Algorithm::Merge qw/merge/;
  40         123715  
  40         2446  
33 40     40   12614 use MojoMojo::WordDiff;
  40         122  
  40         2294  
34 40     40   363 use HTML::Entities qw/encode_entities_numeric/;
  40         91  
  40         48513  
35              
36             __PACKAGE__->load_components(qw/DateTime::Epoch TimeStamp Core/);
37             __PACKAGE__->table("content");
38             __PACKAGE__->add_columns(
39             "page",
40             { data_type => "INTEGER", is_nullable => 0, size => undef },
41             "version",
42             { data_type => "INTEGER", is_nullable => 0, size => undef },
43             "creator",
44             { data_type => "INTEGER", is_nullable => 0, size => undef },
45             "created",
46             {
47             data_type => "BIGINT",
48             is_nullable => 0,
49             size => 100,
50             inflate_datetime => 'epoch',
51             set_on_create => 1,
52             },
53             "status",
54             { data_type => "VARCHAR", is_nullable => 0, size => 20 },
55             "release_date",
56             {
57             data_type => "BIGINT",
58             is_nullable => 0,
59             size => 100,
60             default_value => undef,
61             inflate_datetime => 'epoch',
62             datetime_undef_if_invalid => 1,
63             },
64             "remove_date",
65             {
66             data_type => "BIGINT",
67             is_nullable => 1,
68             size => 100,
69             default_value => undef,
70             inflate_datetime => 'epoch',
71             datetime_undef_if_invalid => 1,
72             },
73             "type",
74             { data_type => "VARCHAR", is_nullable => 1, size => 200 },
75             "abstract",
76             { data_type => "TEXT", is_nullable => 1, size => 4000 },
77             "comments",
78             { data_type => "TEXT", is_nullable => 1, size => 4000 },
79             "body",
80             { data_type => "TEXT", is_nullable => 0, size => undef },
81             "precompiled",
82             { data_type => "TEXT", is_nullable => 1, size => undef },
83              
84             );
85             __PACKAGE__->set_primary_key( "version", "page" );
86             __PACKAGE__->has_many(
87             "pages",
88             "MojoMojo::Schema::Result::Page",
89             {
90             "foreign.content_version" => "self.version",
91             "foreign.id" => "self.page",
92             },
93             );
94             __PACKAGE__->has_many(
95             "page_version_page_content_version_firsts",
96             "MojoMojo::Schema::Result::PageVersion",
97             {
98             "foreign.content_version_first" => "self.version",
99             "foreign.page" => "self.page",
100             },
101             );
102             __PACKAGE__->has_many(
103             "page_version_page_content_version_lasts",
104             "MojoMojo::Schema::Result::PageVersion",
105             {
106             "foreign.content_version_last" => "self.version",
107             "foreign.page" => "self.page",
108             },
109             );
110             __PACKAGE__->belongs_to( "creator", "MojoMojo::Schema::Result::Person", { id => "creator" } );
111             __PACKAGE__->belongs_to( "page", "MojoMojo::Schema::Result::Page", { id => "page" } );
112              
113             =head1 COLUMNS
114              
115             =head2 page
116              
117             References L<MojoMojo::Schema::Result::Page>.
118              
119             =head2 creator
120              
121             References L<MojoMojo::Schema::Result::Person>.
122              
123              
124             =head1 METHODS
125              
126             =head2 highlight
127              
128             Returns an HTML string highlighting the changes between this version and the
129             previous version. The changes are in C<< <span> >> or C<< <div> >> tags with
130             the class C<fade>.
131              
132             =cut
133              
134             sub highlight {
135 0     0 1 0 my ( $self, $c ) = @_;
136 0         0 my $this_content = $self->formatted($c);
137              
138 0 0       0 my $previous_content = (
139             defined $self->previous
140             ? $self->previous->formatted($c)
141             : $this_content
142             );
143              
144 0         0 my $this = [ split /\n/, $this_content ];
145 0         0 my $prev = [ split /\n/, $previous_content ];
146 0         0 my @diff = Algorithm::Diff::sdiff( $prev, $this );
147 0         0 my $diff;
148 0         0 my $hi = 0;
149 0         0 my $pre_tag_open = 0;
150 0         0 for my $line (@diff) {
151 0 0 0     0 $pre_tag_open = 1 if $$line[2] =~ qr{<pre>} and $$line[2] !~ qr{</pre>};
152 0 0       0 my $tag = $pre_tag_open ? 'span' : 'div';
153 0         0 $hi++;
154 0 0       0 if ( $$line[0] eq "+" ) {
    0          
    0          
155 0         0 $diff .= qq(<$tag id="hi$hi" class="fade">) . $$line[2] . "</$tag>";
156             }
157             elsif ( $$line[0] eq "c" ) {
158 0         0 $diff .= qq(<$tag id="hi$hi" class="fade">) . $$line[2] . "</$tag>";
159             }
160             elsif ( $$line[0] eq "-" ) { }
161 0         0 else { $diff .= $$line[1] }
162 0         0 $diff .= "\n";
163 0 0 0     0 $pre_tag_open = 0 if $$line[2] =~ qr{</pre>} and $$line[2] !~ qr{<pre>};
164             }
165 0         0 return $diff;
166             }
167              
168             =head2 formatted_diff <context> <old_content>
169              
170             Compare this content version to <old_content>, using L<Algorithm::Diff>.
171             Sets a C<diffins> CSS class for added lines, and a C<diffdel> CSS class
172             for deleted lines. The C<< <ins> >> and C<< <del> >> HTML tags are also used.
173              
174             =cut
175              
176             sub formatted_diff {
177 0     0 1 0 my ( $self, $c, $to, $sparse ) = @_;
178 0 0       0 my $this = [
179             $sparse
180             ? split /\n/, ( $self->encoded_body )
181             : split /\n\n/, ( $self->formatted($c) )
182             ];
183 0 0       0 my $prev = [
184             $sparse
185             ? split /\n/, ( $to->encoded_body )
186             : split /\n\n/, ( $to->formatted($c) )
187             ];
188 0         0 my @diff = Algorithm::Diff::sdiff( $prev, $this );
189 0         0 my $diff;
190 0         0 for my $line (@diff) {
191 0 0       0 if ( $$line[0] eq "+" ) {
    0          
    0          
    0          
192 0         0 $diff .= qq(<div class="diffins">) . $$line[2] . "</div>";
193             }
194             elsif ( $$line[0] eq "-" ) {
195 0         0 $diff .= qq(<div class="diffdel">) . $$line[1] . "</div>";
196             }
197             elsif ( $$line[0] eq "c" ) {
198              
199 0 0       0 $diff .= (
200             $sparse
201             ? qq(<div class="diffdel">)
202             . $$line[1]
203             . "</div>"
204             . qq(<div class="diffins">)
205             . $$line[2]
206             . "</div>"
207             : word_diff( $$line[1], $$line[2] )
208             );
209             }
210             elsif ( $$line[0] eq "u" ) {
211 0 0       0 $diff .= ( $sparse ? '<div> ' . $$line[1] . '<div>' : $$line[1] );
212             }
213 0         0 else { $diff .= "Unknown operator " . $$line[0] }
214             }
215 0         0 return $diff;
216             }
217              
218             =head2 formatted
219              
220             Return the content after being run through MojoMojo::Formatter::*.
221              
222             =cut
223              
224             sub formatted {
225 52     52 1 68486 my ( $self, $c ) = @_;
226 52         1126 my $result = $self->result_source->resultset->format_content( $c, $self->body, $self );
227 52         518 return $result;
228             }
229              
230             =head2 merge_content
231              
232             Show the merge conflict of the content for two different edit sessions of the same page.
233              
234             =cut
235              
236             sub merge_content {
237 0     0 1 0 my ( $self, $saved, $content, $h1, $h2, $h3 ) = @_;
238              
239 0         0 my $source = [ split /\n/, $self->encoded_body ];
240 0         0 my $a = [ split /\n/, $saved->encoded_body ];
241 0         0 my $b = [ split /\n/, $content ];
242             my @merged = merge(
243             $source, $a, $b,
244             {
245             CONFLICT => sub ($$) {
246             (
247             "<!-- $h1 -->\n",
248 0         0 ( @{ $_[0] } ),
249             "<!-- $h2 -->\n",
250 0     0   0 ( @{ $_[1] } ),
  0         0  
251             "<!-- $h3 -->\n",
252             );
253             }
254             }
255 0         0 );
256 0         0 return join( '', @merged );
257             }
258              
259             =head2 max_version
260              
261             Return the highest numbered revision.
262              
263             =cut
264              
265             sub max_version {
266 126     126 1 276132 my $self = shift;
267 126         686 my $max = $self->result_source->resultset->search(
268             { page => $self->page->id },
269             {
270             select => [ { max => 'me.version' } ],
271             as => ['max_ver']
272             }
273             );
274 126 50       554875 return 0 unless $max->count;
275 126         485282 return $max->next->get_column('max_ver');
276             }
277              
278             =head2 previous
279              
280             Return the previous version of this content, or undef for the first version.
281              
282             =cut
283              
284             sub previous {
285 6     6 1 19 my $self = shift;
286 6         72 return $self->result_source->resultset->search(
287             {
288             page => $self->page->id,
289             version => $self->version - 1
290             }
291             )->next;
292             }
293              
294             =head2 pub_date
295              
296             Return the publishing date of this version in a format suitable for RSS 2.0.
297              
298             =cut
299              
300             sub pub_date {
301 5     5 1 9201 my $self = shift;
302 5         102 return DateTime::Format::Mail->format_datetime( $self->created );
303             }
304              
305             =head2 store_links
306              
307             Extract and store all links and wanted paged from a given content
308             version.
309              
310             =cut
311              
312             sub store_links {
313 7     7 1 29718 my ($self) = @_;
314              
315 7 50       152 return unless ( $self->status eq 'released' );
316 7         427 my $content = $self->body;
317 7         229 my $page = $self->page;
318 7         46367 $page->result_source->resultset->set_paths($page);
319 7         96 $page->links_from->delete();
320 7         22337 $page->wantedpages->delete();
321 7         19095 require MojoMojo::Formatter::Wiki;
322 7         218 my ( $linked_pages, $wanted_pages ) = MojoMojo::Formatter::Wiki->find_links( \$content, $page );
323 7 100 66     85 return unless ( @$linked_pages || @$wanted_pages );
324              
325 4         13 for (@$linked_pages) {
326 6         33870 my $link =
327             $self->result_source->schema->resultset('Link')
328             ->find_or_create( { from_page => $self->page->id, to_page => $_->id } );
329             }
330 4         39677 for (@$wanted_pages) {
331             $_->{path} = join( '/',
332 6         282 map { ( $self->result_source->schema->resultset('Page')->normalize_name($_) )[1]; }
333 3         39 split( m|/|, $_->{path} ) );
334             my $wanted_page =
335             $self->result_source->schema()->resultset('WantedPage')
336 3         224 ->find_or_create( { from_page => $page->id, to_path => $_->{path} } );
337             }
338             }
339              
340             =head2 encoded_body
341              
342             Encode content body using numeric entities.
343              
344             =cut
345              
346 10     10 1 699 sub encoded_body { return encode_entities_numeric( shift->body ); }
347              
348             =head1 AUTHOR
349              
350             Marcus Ramberg <mramberg@cpan.org>
351              
352             =head1 LICENSE
353              
354             This library is free software. You can redistribute it and/or modify
355             it under the same terms as Perl itself.
356              
357             =cut
358              
359             1;