Almost-minimal filesystem based blog.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

389 lines
9.3 KiB

12 years ago
  1. package App::WRT::Markup;
  2. use strict;
  3. use warnings;
  4. use feature "state";
  5. use base qw(Exporter);
  6. our @EXPORT_OK = qw(line_parse image_markup eval_perl);
  7. use App::WRT::Image qw(image_size);
  8. use App::WRT::Util qw(file_get_contents);
  9. use Carp;
  10. use File::Basename;
  11. use Text::Textile;
  12. use Text::Markdown::Discount;
  13. # Some useful defaults:
  14. my %tags = (
  15. retcon => q{div class="retcon"},
  16. freeverse => 'p',
  17. list => "ul>\n<li"
  18. );
  19. my %end_tags = (
  20. retcon => 'div',
  21. freeverse => 'p',
  22. list => "li>\n</ul"
  23. );
  24. my %blank_lines = (
  25. freeverse => "</p>\n\n<p>",
  26. list => "</li>\n\n<li>"
  27. );
  28. my %newlines = (
  29. freeverse => "<br />\n"
  30. );
  31. my %dashes = (
  32. freeverse => ' &mdash; '
  33. );
  34. =over
  35. =item line_parse
  36. Performs substitutions on lines called by fragment_slurp, at least. Calls
  37. image_markup(), textile_process(), markdown_process().
  38. Returns string.
  39. Parses some special markup, specifically:
  40. <textile></textile> - Text::Textile to HTML
  41. <markdown></markdown> - Text::Markdown::Discount to HTML
  42. <freeverse></freeverse>
  43. <retcon></retcon>
  44. <list></list>
  45. <image>filename.ext
  46. optional alt tag
  47. optional title text</image>
  48. ${variable} interpolation from the WRT object
  49. <perl>print "hello world";</perl>
  50. <include>path/to/file/from/project/root</include>
  51. =cut
  52. sub line_parse {
  53. my $self = shift;
  54. my ($everything, $file) = (@_);
  55. # Take care of <include>, <textile>, <markdown>, and <image> tags:
  56. include_process($self, $everything);
  57. textile_process($everything);
  58. markdown_process($everything);
  59. $everything =~ s!<image>(.*?)</image>!$self->image_markup($file, $1)!seg;
  60. foreach my $key (keys %tags) {
  61. # Set some replacements, unless they've been explicitly set already:
  62. $end_tags{$key} ||= $tags{$key};
  63. $blank_lines{$key} ||= "\n\n";
  64. $newlines{$key} ||= "\n";
  65. $dashes{$key} ||= " -- ";
  66. # Transform blocks:
  67. while ($everything =~ m/(<$key>.*?<\/$key>)/s) {
  68. my $block = $1;
  69. # Save the bits between instances of the block:
  70. my (@interstices) = split /\Q$block\E/s, $everything;
  71. # Tags that surround the block:
  72. $block =~ s{\n?<$key>\n?}{<$tags{$key}>}gs;
  73. $block =~ s{\n?</$key>\n?}{</$end_tags{$key}>}gs;
  74. # Dashes, blank lines, and newlines:
  75. $block = dashes($dashes{$key}, $block);
  76. $block =~ s/\n\n/$blank_lines{$key}/gs;
  77. $block = newlines($newlines{$key}, $block);
  78. # ...and slap it all back together as $everything
  79. $everything = join $block, @interstices;
  80. }
  81. }
  82. return $everything;
  83. }
  84. =item eval_perl
  85. Evaluate embedded Perl in a string, replacing blocks enclosed with <perl> tags
  86. with whatever they return (well, evaluated in a scalar context). Returns the
  87. modified string.
  88. Also handles simple ${variables}, replacing them from the keys to $self.
  89. =cut
  90. sub eval_perl {
  91. my $self = shift;
  92. my ($text) = @_;
  93. while ($text =~ m{<perl>(.*?)</perl>}s) {
  94. my $block = $1;
  95. # Run the $block, and include anything returned:
  96. my $output = eval $block;
  97. if ($@) {
  98. # Errors - log and return an empty string:
  99. carp($@);
  100. $output = '';
  101. }
  102. $text =~ s{<perl>\Q$block\E</perl>}{$output}s;
  103. }
  104. # Interpolate variables:
  105. $text =~ s{
  106. \$\{ ([a-zA-Z_]+) \}
  107. }{
  108. if (defined $self->{$1}) {
  109. $self->{$1};
  110. } else {
  111. # TODO: Possibly this should be fatal.
  112. "UNDEFINED: $1";
  113. }
  114. }gex;
  115. return $text;
  116. }
  117. sub newlines {
  118. my ($replacement, $block) = @_;
  119. # Single newlines (i.e., line ends) within the block,
  120. # except those preceded by a double-quote, which probably
  121. # indicates a still-open tag:
  122. $block =~ s/(?<=[^"\n]) # not a double-quote or newline
  123. # don't capture
  124. \n # end-of-line
  125. (?=[^\n]) # not a newline
  126. # don't capture
  127. /$replacement/xgs;
  128. return $block;
  129. }
  130. # might need a rewrite.
  131. sub dashes {
  132. my ($replacement, $block) = @_;
  133. $block =~ s/(\s+) # whitespace - no capture
  134. \-{2} # two dashes
  135. (\n|\s+|$) # newline, whitespace, or eol
  136. /$1${replacement}$2/xgs;
  137. return $block;
  138. }
  139. =item include_process
  140. Inline replace <include>filename</include> tags, replacing them with the
  141. contents of files.
  142. =cut
  143. sub include_process {
  144. my $wrt = shift;
  145. $_[0] =~ s{
  146. <include> # start tag
  147. (.*?) # anything (non-greedy)
  148. </include> # end tag
  149. }{
  150. retrieve_include($wrt, $1);
  151. }xesg;
  152. }
  153. =item retrieve_include
  154. Get the contents of an included file. This probably needs a great
  155. deal more thought than I am presently giving it.
  156. =cut
  157. sub retrieve_include {
  158. my $wrt = shift;
  159. my ($file) = @_;
  160. # Trim leading and trailing spaces:
  161. $file =~ s/^\s+//;
  162. $file =~ s/\s+$//;
  163. if ($file =~ m{^ (/ | [.]/) }x) {
  164. # TODO: Leads with a slash or a ./
  165. croak('Tried to open an include path with a leading / or ./ - not yet supported.');
  166. } else {
  167. # Use the archive root as path.
  168. $file = $wrt->{root_dir} . '/' . $file;
  169. }
  170. if ($wrt->{cache_includes}) {
  171. if (defined $wrt->{include_cache}->{$file}) {
  172. return $wrt->{include_cache}->{$file};
  173. }
  174. }
  175. unless (-e $file) {
  176. carp "No such file: $file";
  177. return '';
  178. }
  179. if (-d $file) {
  180. carp("Tried to open a directory as an include path: $file");
  181. return '';
  182. }
  183. if ($wrt->{cache_includes}) {
  184. $wrt->{include_cache}->{$file} = file_get_contents($file);
  185. return $wrt->{include_cache}->{$file};
  186. } else {
  187. return file_get_contents($file);
  188. }
  189. }
  190. =item textile_process
  191. Inline replace <textile> markup in a string.
  192. =cut
  193. # This is exactly the kind of code that, even though it isn't doing anything
  194. # especially over the top, looks ghastly to people who don't read Perl, so I'll
  195. # try to explain a bit.
  196. sub textile_process {
  197. # First, there's a state variable here which can retain the Text::Textile
  198. # object between invocations of the function, saving us a bit of time on
  199. # subsequent calls. This should be equivalent to creating a closure around
  200. # the function and keeping a $textile variable there.
  201. state $textile;
  202. # Second, instead of unrolling the arguments to the function, we just act
  203. # directly on the first (0th) one. =~ more or less means "do a regexy
  204. # thing on this". It's followed by s, the substitution operator, which can
  205. # use curly braces as delimiters between pattern and replacement.
  206. $_[0] =~ s{
  207. # Find tags...
  208. <textile> # start tag
  209. (.*?) # anything (non-greedy)
  210. </textile> # end tag
  211. }{
  212. # ...and replace them with the result of evaluating this block.
  213. # //= means "defined-or-equals"; if the var hasn't been defined yet,
  214. # then make a new Textile object:
  215. $textile //= Text::Textile->new();
  216. # Process the stuff we slurped out of our tags - this value will be
  217. # used to replace the entire match from above (in Perl, the last
  218. # expression evaluated is the return value of subs, evals, etc.):
  219. $textile->process($1);
  220. }xesg;
  221. # x: eXtended regexp - whitespace ignored by default, comments allowed
  222. # e: Execute the replacement as Perl code, and use its value
  223. # s: treat all lines of the search subject as a Single string
  224. # g: Globally replace all matches
  225. # For the genuinely concise version of this, see markdown_process().
  226. }
  227. =item markdown_process
  228. Inline replace <markdown> markup in a string.
  229. =cut
  230. sub markdown_process {
  231. state $markdown;
  232. my $flags = Text::Markdown::Discount::MKD_EXTRA_FOOTNOTE();
  233. $_[0] =~ s{
  234. <markdown>(.*?)</markdown>
  235. }{
  236. $markdown //= Text::Markdown::Discount->new;
  237. $markdown->markdown($1, $flags);
  238. }xesg;
  239. }
  240. =item image_markup
  241. Parse out an image tag and return the appropriate html.
  242. Relies on image_size from App::WRT::Image.
  243. =cut
  244. sub image_markup {
  245. my $self = shift;
  246. my ($file, $block) = @_;
  247. # Get a basename and directory for the file (entry) referencing the image:
  248. my ($basename, $dir) = fileparse($file);
  249. # Truncated file date that just includes date + sub docs:
  250. my ($file_date) = $dir =~ m{
  251. (
  252. [0-9]{4}/ # year
  253. [0-9]{1,2}/ # month
  254. [0-9]{1,2}/ # day
  255. ([a-z]*/)* # sub-entries
  256. )
  257. $
  258. }x;
  259. # Process the contents of the <image> tag:
  260. my ($image_url, $alt_text, $title_text) = split /\n/, $block;
  261. $alt_text ||= q{};
  262. $title_text ||= $alt_text;
  263. # Resolve relative paths:
  264. my $image_file;
  265. if (-e "$dir/$image_url" ) {
  266. # The path is to an image file in the same directory as current entry:
  267. $image_file = "$dir/$image_url";
  268. $image_url = "${file_date}${image_url}";
  269. } elsif (-e $self->{entry_dir} . "/$image_url") {
  270. # The path is to an image file starting with the entry_dir, like
  271. # 2005/9/20/candles.jpg -> ./archives/2005/9/20/candles.jpg
  272. $image_file = $self->{entry_dir} . "/$image_url";
  273. }
  274. # Get width & height in pixels for known filetypes:
  275. my ($width, $height) = image_size($self->{root_dir_abs} . '/' . $image_file);
  276. # This probably relies on mod_rewrite working:
  277. $image_url = $self->{image_url_root} . $image_url;
  278. return <<"IMG";
  279. <img src="$image_url"
  280. width="$width"
  281. height="$height"
  282. alt="$alt_text"
  283. title="$title_text" />
  284. IMG
  285. }
  286. =back
  287. 1;