Source formatting, corrected email
[ublog.git] / ublog
blobf530354188b2df4b73065b52a4a1eabef04f3838
1 #!/usr/bin/perl
4 # A minimal distributed multiuser blogging software [well ... better: hack]
5 # that is based on Git
6 # Copyright 2011 Daniel Borkmann <borkmann@gnumaniacs.org>
7 # Subject to the GNU GPL, version 2.
8 # More information: read the README
9 # On Debian: apt-get install txt2html
10 # Usage, i.e.: ublog blog.txt /var/www/htdocs/
13 use strict;
14 use warnings;
15 use HTML::TextToHTML;
17 my $prognam = "ublog";
18 my $version = "1.0";
19 my $line = 0;
20 my $verbose = 0;
22 use constant {
23 NONE => -1,
24 HEADER => 0,
25 FOOTER => 1,
26 ABOUT => 2,
27 SETTINGS => 3,
28 AUTHOR => 4,
29 ENTRY => 5,
32 my @abbr = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
34 my $header_txt = "";
35 my $footer_txt = "";
36 my $about_txt = "";
38 my %authors;
39 my %entries;
40 my %settings;
41 my %tagsh;
42 my %got;
44 sub debug
46 my $info = shift;
47 if ($verbose) {
48 print $info;
52 sub parse_settings
54 my $l = shift;
55 $_ = $l;
56 if (/^\s*title\s*=\s*(.*)\s*$/) {
57 $settings{title} = $1;
58 debug("Title: $settings{title}\n");
59 } elsif (/^\s*entries_per_site\s*=\s*(\d+)\s*$/) {
60 $settings{entries} = $1;
61 debug("Entries/site: $settings{entries}\n");
62 } elsif (/^\s*git_clone\s*=\s*(.*)\s*$/) {
63 $settings{clone} = $1;
64 debug("Clone URL: $settings{clone}\n");
65 } elsif (/^\s*root_url\s*=\s*(.*)\s*$/) {
66 $settings{root} = $1;
67 debug("Root URL: $settings{root}\n");
68 } elsif (/^\s*show_all_tags\s*=\s*(0|1)\s*$/) {
69 $settings{tags} = $1;
70 debug("Tagcloud: ".$settings{tags}."\n");
71 } elsif (/^\s*show_authors\s*=\s*(0|1)\s*$/) {
72 $settings{authors} = $1;
73 debug("Authors: ".$settings{authors}."\n");
74 } elsif (/^\s*use_txt2html\s*=\s*(0|1)\s*$/) {
75 $settings{txt2html} = $1;
76 debug("Txt2HTML: ".$settings{txt2html}."\n");
77 } else {
78 die "Syntax error in l.$line!\n";
82 sub check_settings
84 if (not $settings{title}) {
85 die "Syntax error! No blog title!\n";
87 if (not $settings{entries}) {
88 die "Syntax error! No entries/site defined!\n";
90 if ($settings{clone}) {
91 if (not ($settings{clone} =~ /^git:\/\//)) {
92 die "Syntax error! False clone URL!\n";
95 if (not $settings{root}) {
96 die "Syntax error! No blog root URL provided!\n";
98 if (not ($settings{root} =~ /^http:\/\//)) {
99 die "Syntax error! False root URL!\n";
103 sub parse_author
105 my $author = shift;
106 my $l = shift;
107 $_ = $l;
108 if (/^\s*name\s*=\s*(.*)\s*$/) {
109 $authors{$author}->{name} = $1;
110 debug("Name: ".$authors{$author}->{name}."\n");
111 } elsif (/^\s*email\s*=\s*(.*)\s*$/) {
112 $authors{$author}->{email} = $1;
113 debug("E-Mail: ".$authors{$author}->{email}."\n");
114 } elsif (/^\s*web\s*=\s*(.*)\s*$/) {
115 $authors{$author}->{web} = $1;
116 debug("Website: ".$authors{$author}->{web}."\n");
117 } elsif (/^\s*show_nic\s*=\s*(0|1)\s*$/) {
118 $authors{$author}->{usenic} = $1;
119 debug("Use nic: ".$authors{$author}->{usenic}."\n");
120 } else {
121 die "Syntax error in l.$line!\n";
125 sub check_author
127 my $author = shift;
128 if (not $authors{$author}->{name}) {
129 die "Syntax error! No author name!\n";
133 sub parse_entry
135 my $time = shift;
136 my $l = shift;
137 $entries{$time}->{text} .= $l;
140 sub check_entry
142 my $time = shift;
143 my $nic = $entries{$time}->{author};
144 my $good = 0;
145 foreach (keys(%authors)) {
146 if ($_ eq $nic) {
147 $good = 1;
150 if (not $good) {
151 die "Syntax error! Wrong author given!\n";
155 sub check_sections
157 if (not $got{header}) {
158 die "Syntax error! No header section present!\n";
160 if (not $got{footer}) {
161 die "Syntax error! No footer section present!\n";
163 if (not $got{about}) {
164 die "Syntax error! No about section present!\n";
166 if (not $got{settings}) {
167 die "Syntax error! No settings section present!\n";
169 if (not $got{author}) {
170 die "Syntax error! No author section present!\n";
172 if (not $got{entry}) {
173 die "Syntax error! No entry section present!\n";
177 sub parse
179 my $blog = shift;
180 my $state = NONE;
181 my ($author, $time, @tags);
182 open BLOG, "<", $blog, or die $!;
183 while (<BLOG>) {
184 $line++;
185 next if (/^\s*#/ and $state == NONE);
186 next if (/^\s+$/ and $state == NONE);
187 if (/^\s*header\s*=\s*{\s*$/) {
188 die "Syntax error in l.$line!\n" if ($state != NONE);
189 $state = HEADER;
190 $got{header} = 1;
191 debug("Found header!\n");
192 } elsif (/^\s*footer\s*=\s*{\s*$/) {
193 die "Syntax error in l.$line!\n" if ($state != NONE);
194 $state = FOOTER;
195 $got{footer} = 1;
196 debug("Found footer!\n");
197 } elsif (/^\s*about\s*=\s*{\s*$/) {
198 die "Syntax error in l.$line!\n" if ($state != NONE);
199 $state = ABOUT;
200 $got{about} = 1;
201 debug("Found about!\n");
202 } elsif (/^\s*settings\s*=\s*{\s*$/) {
203 die "Syntax error in l.$line!\n" if ($state != NONE);
204 $state = SETTINGS;
205 $got{settings} = 1;
206 debug("Found settings!\n");
207 } elsif (/^\s*author\s+(\w+)\s*=\s*{\s*$/) {
208 die "Syntax error in l.$line!\n" if ($state != NONE);
209 $state = AUTHOR;
210 $got{author} = 1;
211 $author = $1;
212 debug("Found author '$author'!\n");
213 } elsif (/^\s*entry\s+(\d+)\s+(\w+)\s+\[([\w\s,-]*)\]\s*=\s*{\s*$/) {
214 die "Syntax error in l.$line!\n" if ($state != NONE);
215 $state = ENTRY;
216 $got{entry} = 1;
217 $time = $1;
218 $author = $2;
219 @tags = split(/,\s*/, $3);
220 $entries{$time}->{author} = $author;
221 push @{$entries{$time}->{tags}}, @tags;
222 $entries{$time}->{text} = "";
223 foreach my $tag (@tags) {
224 $tagsh{$tag}++;
226 debug("Found header with '$time', '$author', '@tags'!\n");
227 } elsif (/^\s*}\s*$/) {
228 die "Syntax error in l.$line!\n" if ($state == NONE);
229 $state = NONE;
230 debug("Found close!\n");
231 } elsif ($state != NONE) {
232 if ($state == HEADER) {
233 $header_txt .= $_;
234 } elsif ($state == FOOTER) {
235 $footer_txt .= $_;
236 } elsif ($state == ABOUT) {
237 $about_txt .= $_;
238 } elsif ($state == SETTINGS) {
239 parse_settings($_);
240 } elsif ($state == AUTHOR) {
241 parse_author($author, $_);
242 } elsif ($state == ENTRY) {
243 parse_entry($time, $_);
244 } else {
245 die "Wrong state in l.$line!\n";
247 } else {
248 die "Syntax error in l.$line!\n";
251 close BLOG;
254 sub check_content
256 check_sections();
257 check_settings();
258 foreach (keys(%authors)) {
259 check_author($_);
261 foreach (keys(%entries)) {
262 check_entry($_);
266 sub generate_index
268 my $folder = shift;
269 my $conv = new HTML::TextToHTML();
270 my $sites = int(scalar(keys(%entries)) / $settings{entries}) + 1;
271 my $last = scalar(keys(%entries)) % $settings{entries};
272 my @keys = sort {$b <=> $a} keys(%entries);
273 my @ldate = localtime(0);
275 if ($last == 0) {
276 if ($sites > 1) {
277 $sites--;
278 $last = $settings{entries};
282 for (my $i = 0; $i < $sites; $i++) {
283 my $output = "";
284 my ($file, $number);
286 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 ".
287 "Transitional//EN\">\n";
288 $output .= "<html><head>\n";
289 $output .= "<title>".$settings{title}."</title>\n";
290 $output .= "<link href=\"style.css\" rel=\"stylesheet\" ".
291 "type=\"text/css\">";
292 $output .= "</head><body><h2>".$settings{title}."</h2>\n";
293 if ($settings{txt2html}) {
294 $output .= $conv->process_chunk($header_txt);
295 } else {
296 $output .= $header_txt;
298 $output .= "<a href=\"".$settings{root}."/about.html\">About</a>, ";
299 $output .= "<a href=\"".$settings{root}."/feed.xml\">RSS</a>";
300 if ($settings{clone}) {
301 $output .= ", Git: <a href=\"".$settings{clone}.
302 "\">".$settings{clone}."</a>";
304 $output .= "<br>\n<ul>";
306 if ($i == $sites - 1) {
307 $number = $last;
308 } else {
309 $number = $settings{entries};
311 for (my $j = 0; $j < $number; $j++) {
312 my $key = shift @keys;
313 my $pre = "[<a href=\"".$settings{root}.
314 "/$key.html\">p</a>";
315 my $author = $entries{$key}->{author};
316 my @cdate = localtime($key);
318 if ($settings{authors}) {
319 if ($authors{$author}->{usenic}) {
320 $pre .= ", <a href=\"".$settings{root}.
321 "/a_$author.xml\">$author</a>";
322 } else {
323 $pre .= ", <a href=\"".$settings{root}.
324 "/a_$author.xml\">".
325 $authors{$author}->{name}."</a>";
327 if ($authors{$author}->{email} or
328 $authors{$author}->{web}) {
329 my $elem = 0;
330 $pre .= " (";
331 if ($authors{$author}->{email}) {
332 $pre .= "<a href=\"mailto:".
333 $authors{$author}->{email}.
334 "\">e</a>";
335 $elem++;
337 if ($authors{$author}->{web}) {
338 $pre .= ", " if $elem > 0;
339 $pre .= "<a href=\"".
340 $authors{$author}->{web}.
341 "\">w</a>";
343 $pre .= ")";
346 if (scalar(@{$entries{$key}->{tags}}) > 0) {
347 $pre .= ", tags: ";
348 foreach my $tag (@{$entries{$key}->{tags}}) {
349 $pre .= "<a href=\"".$settings{root}.
350 "/t_$tag.xml\">$tag</a> ";
353 $pre .= "] ";
355 if ($ldate[3] != $cdate[3] || $ldate[4] != $cdate[4] ||
356 $ldate[5] != $cdate[5]) {
357 @ldate = @cdate;
358 $output .= "\n</ul>\n<h3>$abbr[$ldate[4]] ".
359 "$ldate[3], ".(1900 + $ldate[5]).
360 "</h3>\n<ul>";
363 if ($settings{txt2html}) {
364 $output .= "\n<li>$pre".
365 $conv->process_chunk($entries{$key}->{text},
366 is_fragment => 1).
367 "</li>";
368 } else {
369 $output .= "\n<li>$pre".$entries{$key}->{text}.
370 "</li>";
374 if ($i == $sites - 1) {
375 $output .= "\n</ul>\n<hr>\n";
376 } else {
377 $output .= "</ul><a href=\"".$settings{root}."/index_".
378 ($i + 1).".html\">Next</a><hr>\n";
380 if ($settings{tags}) {
381 $output .= "All tags: ";
382 foreach my $tag (keys(%tagsh)) {
383 $output .= "<a href=\"".$settings{root}.
384 "/t_$tag.xml\">$tag</a> ".
385 "($tagsh{$tag}) ";
388 if ($settings{txt2html}) {
389 $output .= $conv->process_chunk($footer_txt);
390 } else {
391 $output .= $footer_txt;
393 $output .= "</body></html>";
395 if ($i == 0) {
396 $file = "index.html";
397 } else {
398 $file = "index_$i.html";
401 open OUT, ">", "$folder/$file" or die $!;
402 print OUT $output;
403 close OUT;
407 sub generate_about
409 my $folder = shift;
410 my $conv = new HTML::TextToHTML();
411 my $output = "";
413 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 ".
414 "Transitional//EN\">\n";
415 $output .= "<html><head>\n";
416 $output .= "<title>".$settings{title}."</title>\n";
417 $output .= "<link href=\"style.css\" rel=\"stylesheet\" ".
418 "type=\"text/css\">";
419 $output .= "</head><body><h2>".$settings{title}."</h2>\n";
420 if ($settings{txt2html}) {
421 $output .= $conv->process_chunk($about_txt);
422 } else {
423 $output .= $about_txt;
425 $output .= "<a href=\"".$settings{root}."/index.html\">Blog</a>, ";
426 $output .= "<a href=\"".$settings{root}."/feed.xml\">RSS</a>";
427 if ($settings{clone}) {
428 $output .= ", Git: <a href=\"".$settings{clone}."\">".
429 $settings{clone}."</a>";
431 $output .= "<br><hr>\n";
432 if ($settings{txt2html}) {
433 $output .= $conv->process_chunk($footer_txt);
434 } else {
435 $output .= $footer_txt;
437 $output .= "</body></html>";
439 open OUT, ">", "$folder/about.html" or die $!;
440 print OUT $output;
441 close OUT;
444 sub generate_entries
446 my $folder = shift;
447 my $conv = new HTML::TextToHTML();
449 foreach my $key (keys(%entries)) {
450 my $output = "";
451 my $author = $entries{$key}->{author};
452 my @ldate = localtime($key);
453 my $pre = "[<a href=\"".$settings{root}."/$key.html\">p</a>";
455 if ($settings{authors}) {
456 if ($authors{$author}->{usenic}) {
457 $pre .= ", <a href=\"".$settings{root}.
458 "/a_$author.xml\">$author</a>";
459 } else {
460 $pre .= ", <a href=\"".$settings{root}.
461 "/a_$author.xml\">".
462 $authors{$author}->{name}."</a>";
464 if ($authors{$author}->{email} or
465 $authors{$author}->{web}) {
466 my $elem = 0;
467 $pre .= " (";
468 if ($authors{$author}->{email}) {
469 $pre .= "<a href=\"mailto:".
470 $authors{$author}->{email}.
471 "\">e</a>";
472 $elem++;
474 if ($authors{$author}->{web}) {
475 $pre .= ", " if $elem > 0;
476 $pre .= "<a href=\"".
477 $authors{$author}->{web}.
478 "\">w</a>";
480 $pre .= ")";
483 if (scalar(@{$entries{$key}->{tags}}) > 0) {
484 $pre .= ", tags: ";
485 foreach my $tag (@{$entries{$key}->{tags}}) {
486 $pre .= "<a href=\"".$settings{root}.
487 "/t_$tag.xml\">$tag</a> ";
490 $pre .= "] ";
492 $output .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 ".
493 "Transitional//EN\">\n";
494 $output .= "<html><head>\n";
495 $output .= "<title>".$settings{title}."</title>\n";
496 $output .= "<link href=\"style.css\" rel=\"stylesheet\" ".
497 "type=\"text/css\">";
498 $output .= "</head><body><h2>".$settings{title}."</h2>\n";
499 if ($settings{txt2html}) {
500 $output .= $conv->process_chunk($header_txt);
501 } else {
502 $output .= $header_txt;
504 $output .= "<a href=\"".$settings{root}."/about.html\">About</a>, ";
505 $output .= "<a href=\"".$settings{root}."/feed.xml\">RSS</a>";
506 if ($settings{clone}) {
507 $output .= ", Git: <a href=\"".$settings{clone}."\">".
508 $settings{clone}."</a>";
510 $output .= "<br>\n";
511 $output .= "<h3>$abbr[$ldate[4]] $ldate[3], ".
512 (1900 + $ldate[5])."</h3>\n<ul>";
513 if ($settings{txt2html}) {
514 $output .= "\n<li>$pre".
515 $conv->process_chunk($entries{$key}->{text},
516 is_fragment => 1).
517 "</li>";
518 } else {
519 $output .= "\n<li>$pre".$entries{$key}->{text}."</li>";
521 $output .= "\n</ul>\n<hr>\n";
522 if ($settings{txt2html}) {
523 $output .= $conv->process_chunk($footer_txt);
524 } else {
525 $output .= $footer_txt;
527 $output .= "</body></html>";
529 open OUT, ">", "$folder/$key.html" or die $!;
530 print OUT $output;
531 close OUT;
535 sub textify_html
537 my $text = shift;
538 $text =~ s/&/&amp;/g;
539 $text =~ s/</&lt;/g;
540 $text =~ s/>/&gt;/g;
541 return $text;
544 sub generate_full_rss
546 my $folder = shift;
547 my $output = "";
548 my @keys = sort {$b <=> $a} keys(%entries);
550 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
551 $output .= "<rss version=\"2.0\">\n";
552 $output .= "<channel>\n";
553 $output .= "<title>".$settings{title}."</title>\n";
554 $output .= "<link>".$settings{root}."</link>\n";
555 $output .= "<description>".$settings{title}.", all ".
556 "ramblings</description>\n";
557 $output .= "<language>en</language>\n";
559 foreach my $key (@keys) {
560 $output .= "<item>\n";
561 $output .= "<title>".textify_html($entries{$key}->{text}).
562 "</title>\n";
563 $output .= "<link>".$settings{root}."/$key.html</link>\n";
564 $output .= "<guid>".$settings{root}."/$key.html</guid>\n";
565 $output .= "</item>\n";
568 $output .= "</channel>\n";
569 $output .= "</rss>\n";
571 open OUT, ">", "$folder/feed.xml" or die $!;
572 print OUT $output;
573 close OUT;
576 sub generate_author_rss
578 my $folder = shift;
579 my @keys = sort {$b <=> $a} keys(%entries);
580 my %aitems;
582 foreach my $key (@keys) {
583 my $author = $entries{$key}->{author};
584 if (not $aitems{$author}) {
585 $aitems{$author} = "";
587 $aitems{$author} .= "<item>\n";
588 $aitems{$author} .= "<title>".
589 textify_html($entries{$key}->{text}).
590 "</title>\n";
591 $aitems{$author} .= "<link>".$settings{root}."/$key.html</link>\n";
592 $aitems{$author} .= "<guid>".$settings{root}."/$key.html</guid>\n";
593 $aitems{$author} .= "</item>\n";
596 foreach my $author (keys(%aitems)) {
597 my $output = "";
598 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
599 $output .= "<rss version=\"2.0\">\n";
600 $output .= "<channel>\n";
601 $output .= "<title>".$settings{title}."</title>\n";
602 $output .= "<link>".$settings{root}."</link>\n";
603 $output .= "<description>".$settings{title}.", $author\'s ".
604 "ramblings</description>\n";
605 $output .= "<language>en</language>\n";
606 $output .= $aitems{$author};
607 $output .= "</channel>\n";
608 $output .= "</rss>\n";
610 open OUT, ">", "$folder/a_$author.xml" or die $!;
611 print OUT $output;
612 close OUT;
616 sub generate_tag_rss
618 my $folder = shift;
619 my @keys = sort {$b <=> $a} keys(%entries);
620 my %titems;
622 foreach my $key (@keys) {
623 my @tags = @{$entries{$key}->{tags}};
624 foreach my $tag (@tags) {
625 if (not $titems{$tag}) {
626 $titems{$tag} = "";
628 $titems{$tag} .= "<item>\n";
629 $titems{$tag} .= "<title>".
630 textify_html($entries{$key}->{text}).
631 "</title>\n";
632 $titems{$tag} .= "<link>".$settings{root}.
633 "/$key.html</link>\n";
634 $titems{$tag} .= "<guid>".$settings{root}.
635 "/$key.html</guid>\n";
636 $titems{$tag} .= "</item>\n";
640 foreach my $tag (keys(%titems)) {
641 my $output = "";
642 $output .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
643 $output .= "<rss version=\"2.0\">\n";
644 $output .= "<channel>\n";
645 $output .= "<title>".$settings{title}."</title>\n";
646 $output .= "<link>".$settings{root}."</link>\n";
647 $output .= "<description>".$settings{title}.", $tag-tagged ".
648 "ramblings</description>\n";
649 $output .= "<language>en</language>\n";
650 $output .= $titems{$tag};
651 $output .= "</channel>\n";
652 $output .= "</rss>\n";
654 open OUT, ">", "$folder/t_$tag.xml" or die $!;
655 print OUT $output;
656 close OUT;
660 sub generate_content
662 my $folder = shift;
663 mkdir($folder, 0777);
664 generate_index($folder);
665 generate_about($folder);
666 generate_entries($folder);
667 # Yeah, sloppy and inefficient
668 generate_full_rss($folder);
669 generate_author_rss($folder);
670 generate_tag_rss($folder);
673 sub main
675 my $blog = shift;
676 my $folder = shift;
677 print "Parsing $blog blog ...\n";
678 parse($blog);
679 print "Checking ...\n";
680 check_content();
681 print "Generate html files in $folder ...\n";
682 generate_content($folder);
683 print "Done!\n";
686 sub help
688 print "\n$prognam $version\n";
689 print "http://gnumaniacs.org\n\n";
690 print "Usage: $prognam <blog-text-file> <output-folder>\n\n";
691 print "Please report bugs to <borkmann\@gnumaniacs.org>\n";
692 print "Copyright (C) 2011 Daniel Borkmann <borkmann\@gnumanics.org>,\n";
693 print "License: GNU GPL version 2\n";
694 print "This is free software: you are free to change and redistribute it.\n";
695 print "There is NO WARRANTY, to the extent permitted by law.\n\n";
696 exit;
699 if ($#ARGV + 1 != 2) {
700 help();
703 main(@ARGV);