blib/lib/Markdown/To/POD.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 279 | 496 | 56.2 |
branch | 23 | 98 | 23.4 |
condition | 9 | 61 | 14.7 |
subroutine | 42 | 55 | 76.3 |
pod | 3 | 3 | 100.0 |
total | 356 | 713 | 49.9 |
line | stmt | bran | cond | sub | pod | time | code | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | package Markdown::To::POD; | |||||||||||||
2 | ||||||||||||||
3 | our $DATE = '2014-07-28'; # DATE | |||||||||||||
4 | our $VERSION = '0.04'; # VERSION | |||||||||||||
5 | # ABSTRACT: Convert Markdown syntax to POD | |||||||||||||
6 | ||||||||||||||
7 | 1 | 1 | 1466 | use 5.010001; | ||||||||||
1 | 3 | |||||||||||||
1 | 44 | |||||||||||||
8 | 1 | 1 | 5 | use strict; | ||||||||||
1 | 2 | |||||||||||||
1 | 30 | |||||||||||||
9 | 1 | 1 | 6 | use warnings; | ||||||||||
1 | 1 | |||||||||||||
1 | 28 | |||||||||||||
10 | 1 | 1 | 5 | use re 'eval'; | ||||||||||
1 | 2 | |||||||||||||
1 | 52 | |||||||||||||
11 | ||||||||||||||
12 | 1 | 1 | 6 | use Digest::MD5 qw(md5_hex); | ||||||||||
1 | 9 | |||||||||||||
1 | 74 | |||||||||||||
13 | 1 | 1 | 2303 | use Encode qw(); | ||||||||||
1 | 19989 | |||||||||||||
1 | 28 | |||||||||||||
14 | 1 | 1 | 9 | use Carp qw(croak); | ||||||||||
1 | 3 | |||||||||||||
1 | 75 | |||||||||||||
15 | 1 | 1 | 5 | use base 'Exporter'; | ||||||||||
1 | 2 | |||||||||||||
1 | 1346 | |||||||||||||
16 | ||||||||||||||
17 | our @EXPORT_OK = qw(markdown_to_pod); | |||||||||||||
18 | ||||||||||||||
19 | ||||||||||||||
20 | # Regex to match balanced [brackets]. See Friedl's | |||||||||||||
21 | # "Mastering Regular Expressions", 2nd Ed., pp. 328-331. | |||||||||||||
22 | our ($g_nested_brackets, $g_nested_parens); | |||||||||||||
23 | $g_nested_brackets = qr{ | |||||||||||||
24 | (?> # Atomic matching | |||||||||||||
25 | [^\[\]]+ # Anything other than brackets | |||||||||||||
26 | | | |||||||||||||
27 | \[ | |||||||||||||
28 | (??{ $g_nested_brackets }) # Recursive set of nested brackets | |||||||||||||
29 | \] | |||||||||||||
30 | )* | |||||||||||||
31 | }x; | |||||||||||||
32 | # Doesn't allow for whitespace, because we're using it to match URLs: | |||||||||||||
33 | $g_nested_parens = qr{ | |||||||||||||
34 | (?> # Atomic matching | |||||||||||||
35 | [^()\s]+ # Anything other than parens or whitespace | |||||||||||||
36 | | | |||||||||||||
37 | \( | |||||||||||||
38 | (??{ $g_nested_parens }) # Recursive set of nested brackets | |||||||||||||
39 | \) | |||||||||||||
40 | )* | |||||||||||||
41 | }x; | |||||||||||||
42 | ||||||||||||||
43 | # Table of hash values for escaped characters: | |||||||||||||
44 | our %g_escape_table; | |||||||||||||
45 | foreach my $char (split //, '\\`*_{}[]()>#+-.!') { | |||||||||||||
46 | $g_escape_table{$char} = md5_hex($char); | |||||||||||||
47 | } | |||||||||||||
48 | ||||||||||||||
49 | ||||||||||||||
50 | sub new { | |||||||||||||
51 | 3 | 3 | 1 | 6 | my ($class, %p) = @_; | |||||||||
52 | ||||||||||||||
53 | 3 | 50 | 20 | $p{base_url} ||= ''; # This is the base URL to be used for WikiLinks | ||||||||||
54 | ||||||||||||||
55 | 3 | 50 | 33 | 20 | $p{tab_width} = 4 unless (defined $p{tab_width} and $p{tab_width} =~ m/^\d+$/); | |||||||||
56 | ||||||||||||||
57 | 3 | 50 | 16 | $p{empty_element_suffix} ||= ' />'; # Change to ">" for HTML output | ||||||||||
58 | ||||||||||||||
59 | 3 | 50 | 8 | $p{trust_list_start_value} = $p{trust_list_start_value} ? 1 : 0; | ||||||||||
60 | ||||||||||||||
61 | 3 | 11 | my $self = { params => \%p }; | |||||||||||
62 | 3 | 33 | 15 | bless $self, ref($class) || $class; | ||||||||||
63 | 3 | 7 | return $self; | |||||||||||
64 | } | |||||||||||||
65 | ||||||||||||||
66 | ||||||||||||||
67 | sub markdown_to_pod { | |||||||||||||
68 | 6 | 6 | 1 | 18 | my ( $self, $text, $options ) = @_; | |||||||||
69 | ||||||||||||||
70 | # Detect functional mode, and create an instance for this run | |||||||||||||
71 | 6 | 100 | 18 | unless (ref $self) { | ||||||||||
72 | 3 | 50 | 10 | if ( $self ne __PACKAGE__ ) { | ||||||||||
73 | 3 | 13 | my $ob = __PACKAGE__->new(); | |||||||||||
74 | # $self is text, $text is options | |||||||||||||
75 | 3 | 12 | return $ob->markdown_to_pod($self, $text); | |||||||||||
76 | } | |||||||||||||
77 | else { | |||||||||||||
78 | 0 | 0 | croak('Calling ' . $self . '->markdown (as a class method) is not supported.'); | |||||||||||
79 | } | |||||||||||||
80 | } | |||||||||||||
81 | ||||||||||||||
82 | 3 | 50 | 14 | $options ||= {}; | ||||||||||
83 | ||||||||||||||
84 | 3 | 5 | %$self = (%{ $self->{params} }, %$options, params => $self->{params}); | |||||||||||
3 | 33 | |||||||||||||
85 | ||||||||||||||
86 | 3 | 12 | $self->_CleanUpRunData($options); | |||||||||||
87 | ||||||||||||||
88 | 3 | 13 | return $self->_Markdown($text); | |||||||||||
89 | } | |||||||||||||
90 | ||||||||||||||
91 | sub _CleanUpRunData { | |||||||||||||
92 | 3 | 3 | 7 | my ($self, $options) = @_; | ||||||||||
93 | # Clear the global hashes. If we don't clear these, you get conflicts | |||||||||||||
94 | # from other articles when generating a page which contains more than | |||||||||||||
95 | # one article (e.g. an index page that shows the N most recent | |||||||||||||
96 | # articles). | |||||||||||||
97 | 3 | 50 | 42 | $self->{_urls} = $options->{urls} ? $options->{urls} : {}; # FIXME - document passing this option (tested in 05options.t). | ||||||||||
98 | 3 | 9 | $self->{_titles} = {}; | |||||||||||
99 | 3 | 10 | $self->{_html_blocks} = {}; | |||||||||||
100 | # Used to track when we're inside an ordered or unordered list | |||||||||||||
101 | # (see _ProcessListItems() for details) | |||||||||||||
102 | 3 | 8 | $self->{_list_level} = 0; | |||||||||||
103 | ||||||||||||||
104 | } | |||||||||||||
105 | ||||||||||||||
106 | sub _Markdown { | |||||||||||||
107 | # | |||||||||||||
108 | # Main function. The order in which other subs are called here is | |||||||||||||
109 | # essential. Link and image substitutions need to happen before | |||||||||||||
110 | # _EscapeSpecialChars(), so that any *'s or _'s in the | |||||||||||||
111 | # and tags get encoded. | |||||||||||||
112 | # | |||||||||||||
113 | 3 | 3 | 6 | my ($self, $text, $options) = @_; | ||||||||||
114 | ||||||||||||||
115 | 3 | 10 | $text = $self->_CleanUpDoc($text); | |||||||||||
116 | ||||||||||||||
117 | # Turn block-level HTML elements into hash entries, and interpret markdown in them if they have a 'markdown="1"' attribute | |||||||||||||
118 | 3 | 18 | $text = $self->_HashHTMLBlocks($text, {interpret_markdown_on_attribute => 1}); | |||||||||||
119 | ||||||||||||||
120 | 3 | 28 | $text = $self->_StripLinkDefinitions($text); | |||||||||||
121 | ||||||||||||||
122 | 3 | 19 | $text = $self->_RunBlockGamut($text, {wrap_in_p_tags => 1}); | |||||||||||
123 | ||||||||||||||
124 | 3 | 12 | $text = $self->_UnescapeSpecialChars($text); | |||||||||||
125 | ||||||||||||||
126 | 3 | 9 | $text = $self->_ConvertCopyright($text); | |||||||||||
127 | ||||||||||||||
128 | 3 | 46 | return $text . "\n"; | |||||||||||
129 | } | |||||||||||||
130 | ||||||||||||||
131 | ||||||||||||||
132 | sub urls { | |||||||||||||
133 | 0 | 0 | 1 | 0 | my ( $self ) = @_; | |||||||||
134 | ||||||||||||||
135 | 0 | 0 | return $self->{_urls}; | |||||||||||
136 | } | |||||||||||||
137 | ||||||||||||||
138 | sub _CleanUpDoc { | |||||||||||||
139 | 3 | 3 | 8 | my ($self, $text) = @_; | ||||||||||
140 | ||||||||||||||
141 | # Standardize line endings: | |||||||||||||
142 | 3 | 8 | $text =~ s{\r\n}{\n}g; # DOS to Unix | |||||||||||
143 | 3 | 6 | $text =~ s{\r}{\n}g; # Mac to Unix | |||||||||||
144 | ||||||||||||||
145 | # Make sure $text ends with a couple of newlines: | |||||||||||||
146 | 3 | 5 | $text .= "\n\n"; | |||||||||||
147 | ||||||||||||||
148 | # Convert all tabs to spaces. | |||||||||||||
149 | 3 | 11 | $text = $self->_Detab($text); | |||||||||||
150 | ||||||||||||||
151 | # Strip any lines consisting only of spaces and tabs. | |||||||||||||
152 | # This makes subsequent regexen easier to write, because we can | |||||||||||||
153 | # match consecutive blank lines with /\n+/ instead of something | |||||||||||||
154 | # contorted like /[ \t]*\n+/ . | |||||||||||||
155 | 3 | 9 | $text =~ s/^[ \t]+$//mg; | |||||||||||
156 | ||||||||||||||
157 | 3 | 7 | return $text; | |||||||||||
158 | } | |||||||||||||
159 | ||||||||||||||
160 | sub _StripLinkDefinitions { | |||||||||||||
161 | # | |||||||||||||
162 | # Strips link definitions from text, stores the URLs and titles in | |||||||||||||
163 | # hash references. | |||||||||||||
164 | # | |||||||||||||
165 | 3 | 3 | 7 | my ($self, $text) = @_; | ||||||||||
166 | 3 | 8 | my $less_than_tab = $self->{tab_width} - 1; | |||||||||||
167 | ||||||||||||||
168 | # Link defs are in the form: ^[id]: url "optional title" | |||||||||||||
169 | 3 | 64 | while ($text =~ s{ | |||||||||||
170 | ^[ ]{0,$less_than_tab}\[(.+)\]: # id = \$1 | |||||||||||||
171 | [ \t]* | |||||||||||||
172 | \n? # maybe *one* newline | |||||||||||||
173 | [ \t]* | |||||||||||||
174 | (\S+?)>? # url = \$2 | |||||||||||||
175 | [ \t]* | |||||||||||||
176 | \n? # maybe one newline | |||||||||||||
177 | [ \t]* | |||||||||||||
178 | (?: | |||||||||||||
179 | (?<=\s) # lookbehind for whitespace | |||||||||||||
180 | ["(] | |||||||||||||
181 | (.+?) # title = \$3 | |||||||||||||
182 | [")] | |||||||||||||
183 | [ \t]* | |||||||||||||
184 | )? # title is optional | |||||||||||||
185 | (?:\n+|\Z) | |||||||||||||
186 | }{}omx) { | |||||||||||||
187 | 0 | 0 | $self->{_urls}{lc $1} = $self->_EncodeAmpsAndAngles( $2 ); # Link IDs are case-insensitive | |||||||||||
188 | 0 | 0 | 0 | if ($3) { | ||||||||||
189 | 0 | 0 | $self->{_titles}{lc $1} = $3; | |||||||||||
190 | 0 | 0 | $self->{_titles}{lc $1} =~ s/"/"/g; | |||||||||||
191 | } | |||||||||||||
192 | ||||||||||||||
193 | } | |||||||||||||
194 | ||||||||||||||
195 | 3 | 8 | return $text; | |||||||||||
196 | } | |||||||||||||
197 | ||||||||||||||
198 | sub _md5_utf8 { | |||||||||||||
199 | # Internal function used to safely MD5sum chunks of the input, which might be Unicode in Perl's internal representation. | |||||||||||||
200 | 0 | 0 | 0 | my $input = shift; | ||||||||||
201 | 0 | 0 | 0 | return unless defined $input; | ||||||||||
202 | 0 | 0 | 0 | if (Encode::is_utf8 $input) { | ||||||||||
203 | 0 | 0 | return md5_hex(Encode::encode('utf8', $input)); | |||||||||||
204 | } | |||||||||||||
205 | else { | |||||||||||||
206 | 0 | 0 | return md5_hex($input); | |||||||||||
207 | } | |||||||||||||
208 | } | |||||||||||||
209 | ||||||||||||||
210 | sub _HashHTMLBlocks { | |||||||||||||
211 | 6 | 6 | 12 | my ($self, $text, $options) = @_; | ||||||||||
212 | 6 | 11 | my $less_than_tab = $self->{tab_width} - 1; | |||||||||||
213 | ||||||||||||||
214 | # Hashify HTML blocks (protect from further interpretation by encoding to an md5): | |||||||||||||
215 | # We only want to do this for block-level HTML tags, such as headers, | |||||||||||||
216 | # lists, and tables. That's because we still want to wrap s around |
|||||||||||||
217 | # "paragraphs" that are wrapped in non-block-level tags, such as anchors, | |||||||||||||
218 | # phrase emphasis, and spans. The list of tags we're looking for is | |||||||||||||
219 | # hard-coded: | |||||||||||||
220 | 6 | 21 | my $block_tags = qr{ | |||||||||||
221 | (?: | |||||||||||||
222 | p | div | h[1-6] | blockquote | pre | table | | |||||||||||||
223 | dl | ol | ul | script | noscript | form | | |||||||||||||
224 | fieldset | iframe | math | ins | del | |||||||||||||
225 | ) | |||||||||||||
226 | }x; | |||||||||||||
227 | ||||||||||||||
228 | 6 | 18 | my $tag_attrs = qr{ | |||||||||||
229 | (?: # Match one attr name/value pair | |||||||||||||
230 | \s+ # There needs to be at least some whitespace | |||||||||||||
231 | # before each attribute name. | |||||||||||||
232 | [\w.:_-]+ # Attribute name | |||||||||||||
233 | \s*=\s* | |||||||||||||
234 | (?: | |||||||||||||
235 | ".+?" # "Attribute value" | |||||||||||||
236 | | | |||||||||||||
237 | '.+?' # 'Attribute value' | |||||||||||||
238 | | | |||||||||||||
239 | [^\s]+? # AttributeValue (HTML5) | |||||||||||||
240 | ) | |||||||||||||
241 | )* # Zero or more | |||||||||||||
242 | }x; | |||||||||||||
243 | ||||||||||||||
244 | 6 | 141 | my $empty_tag = qr{< \w+ $tag_attrs \s* />}oxms; | |||||||||||
245 | 6 | 113 | my $open_tag = qr{< $block_tags $tag_attrs \s* >}oxms; | |||||||||||
246 | 6 | 10 | my $close_tag = undef; # let Text::Balanced handle this | |||||||||||
247 | 6 | 7 | my $prefix_pattern = undef; # Text::Balanced | |||||||||||
248 | 6 | 17 | my $markdown_attr = qr{ \s* markdown \s* = \s* (['"]) (.*?) \1 }xs; | |||||||||||
249 | ||||||||||||||
250 | 1 | 1 | 1233 | use Text::Balanced qw(gen_extract_tagged); | ||||||||||
1 | 36049 | |||||||||||||
1 | 4229 | |||||||||||||
251 | 6 | 45 | my $extract_block = gen_extract_tagged($open_tag, $close_tag, $prefix_pattern, { ignore => [$empty_tag] }); | |||||||||||
252 | ||||||||||||||
253 | 6 | 728 | my @chunks; | |||||||||||
254 | # parse each line, looking for block-level HTML tags | |||||||||||||
255 | 6 | 87 | while ($text =~ s{^(([ ]{0,$less_than_tab}<)?.*\n)}{}m) { | |||||||||||
256 | 12 | 31 | my $cur_line = $1; | |||||||||||
257 | 12 | 50 | 26 | if (defined $2) { | ||||||||||
258 | # current line could be start of code block | |||||||||||||
259 | ||||||||||||||
260 | 0 | 0 | my ($tag, $remainder, $prefix, $opening_tag, $text_in_tag, $closing_tag) = $extract_block->($cur_line . $text); | |||||||||||
261 | 0 | 0 | 0 | if ($tag) { | ||||||||||
262 | 0 | 0 | 0 | 0 | if ($options->{interpret_markdown_on_attribute} and $opening_tag =~ s/$markdown_attr//i) { | |||||||||
263 | 0 | 0 | my $markdown = $2; | |||||||||||
264 | 0 | 0 | 0 | if ($markdown =~ /^(1|on|yes)$/) { | ||||||||||
265 | # interpret markdown and reconstruct $tag to include the interpreted $text_in_tag | |||||||||||||
266 | 0 | 0 | my $wrap_in_p_tags = $opening_tag =~ /^<(div|iframe)/; | |||||||||||
267 | 0 | 0 | $tag = $prefix . $opening_tag . "\n" | |||||||||||
268 | . $self->_RunBlockGamut($text_in_tag, {wrap_in_p_tags => $wrap_in_p_tags}) | |||||||||||||
269 | . "\n" . $closing_tag | |||||||||||||
270 | ; | |||||||||||||
271 | } else { | |||||||||||||
272 | # just remove the markdown="0" attribute | |||||||||||||
273 | 0 | 0 | $tag = $prefix . $opening_tag . $text_in_tag . $closing_tag; | |||||||||||
274 | } | |||||||||||||
275 | } | |||||||||||||
276 | 0 | 0 | my $key = _md5_utf8($tag); | |||||||||||
277 | 0 | 0 | $self->{_html_blocks}{$key} = $tag; | |||||||||||
278 | 0 | 0 | push @chunks, "\n\n" . $key . "\n\n"; | |||||||||||
279 | 0 | 0 | $text = $remainder; | |||||||||||
280 | } | |||||||||||||
281 | else { | |||||||||||||
282 | # No tag match, so toss $cur_line into @chunks | |||||||||||||
283 | 0 | 0 | push @chunks, $cur_line; | |||||||||||
284 | } | |||||||||||||
285 | } | |||||||||||||
286 | else { | |||||||||||||
287 | # current line could NOT be start of code block | |||||||||||||
288 | 12 | 78 | push @chunks, $cur_line; | |||||||||||
289 | } | |||||||||||||
290 | ||||||||||||||
291 | } | |||||||||||||
292 | 6 | 12 | push @chunks, $text; # whatever is left | |||||||||||
293 | ||||||||||||||
294 | 6 | 13 | $text = join '', @chunks; | |||||||||||
295 | ||||||||||||||
296 | 6 | 81 | return $text; | |||||||||||
297 | } | |||||||||||||
298 | ||||||||||||||
299 | sub _HashHR { | |||||||||||||
300 | 3 | 3 | 4 | my ($self, $text) = @_; | ||||||||||
301 | 3 | 9 | my $less_than_tab = $self->{tab_width} - 1; | |||||||||||
302 | ||||||||||||||
303 | 3 | 63 | $text =~ s{ | |||||||||||
304 | (?: | |||||||||||||
305 | (?<=\n\n) # Starting after a blank line | |||||||||||||
306 | | # or | |||||||||||||
307 | \A\n? # the beginning of the doc | |||||||||||||
308 | ) | |||||||||||||
309 | ( # save in $1 | |||||||||||||
310 | [ ]{0,$less_than_tab} | |||||||||||||
311 | <(hr) # start tag = $2 | |||||||||||||
312 | \b # word break | |||||||||||||
313 | ([^<>])*? # | |||||||||||||
314 | /?> # the matching end tag | |||||||||||||
315 | [ \t]* | |||||||||||||
316 | (?=\n{2,}|\Z) # followed by a blank line or end of document | |||||||||||||
317 | ) | |||||||||||||
318 | }{ | |||||||||||||
319 | 0 | 0 | my $key = _md5_utf8($1); | |||||||||||
320 | 0 | 0 | $self->{_html_blocks}{$key} = $1; | |||||||||||
321 | 0 | 0 | "\n\n" . $key . "\n\n"; | |||||||||||
322 | }egx; | |||||||||||||
323 | ||||||||||||||
324 | 3 | 9 | return $text; | |||||||||||
325 | } | |||||||||||||
326 | ||||||||||||||
327 | sub _HashHTMLComments { | |||||||||||||
328 | 3 | 3 | 6 | my ($self, $text) = @_; | ||||||||||
329 | 3 | 7 | my $less_than_tab = $self->{tab_width} - 1; | |||||||||||
330 | ||||||||||||||
331 | # Special case for standalone HTML comments: | |||||||||||||
332 | 3 | 52 | $text =~ s{ | |||||||||||
333 | (?: | |||||||||||||
334 | (?<=\n\n) # Starting after a blank line | |||||||||||||
335 | | # or | |||||||||||||
336 | \A\n? # the beginning of the doc | |||||||||||||
337 | ) | |||||||||||||
338 | ( # save in $1 | |||||||||||||
339 | [ ]{0,$less_than_tab} | |||||||||||||
340 | (?s: | |||||||||||||
341 | ||||||||||||||
342 | (--.*?--\s*)+ | |||||||||||||
343 | > | |||||||||||||
344 | ) | |||||||||||||
345 | [ \t]* | |||||||||||||
346 | (?=\n{2,}|\Z) # followed by a blank line or end of document | |||||||||||||
347 | ) | |||||||||||||
348 | }{ | |||||||||||||
349 | 0 | 0 | my $key = _md5_utf8($1); | |||||||||||
350 | 0 | 0 | $self->{_html_blocks}{$key} = $1; | |||||||||||
351 | 0 | 0 | "\n\n" . $key . "\n\n"; | |||||||||||
352 | }egx; | |||||||||||||
353 | ||||||||||||||
354 | 3 | 10 | return $text; | |||||||||||
355 | } | |||||||||||||
356 | ||||||||||||||
357 | sub _HashPHPASPBlocks { | |||||||||||||
358 | 3 | 3 | 5 | my ($self, $text) = @_; | ||||||||||
359 | 3 | 5 | my $less_than_tab = $self->{tab_width} - 1; | |||||||||||
360 | ||||||||||||||
361 | # PHP and ASP-style processor instructions ( and <%…%>) | |||||||||||||
362 | 3 | 62 | $text =~ s{ | |||||||||||
363 | (?: | |||||||||||||
364 | (?<=\n\n) # Starting after a blank line | |||||||||||||
365 | | # or | |||||||||||||
366 | \A\n? # the beginning of the doc | |||||||||||||
367 | ) | |||||||||||||
368 | ( # save in $1 | |||||||||||||
369 | [ ]{0,$less_than_tab} | |||||||||||||
370 | (?s: | |||||||||||||
371 | <([?%]) # $2 | |||||||||||||
372 | .*? | |||||||||||||
373 | \2> | |||||||||||||
374 | ) | |||||||||||||
375 | [ \t]* | |||||||||||||
376 | (?=\n{2,}|\Z) # followed by a blank line or end of document | |||||||||||||
377 | ) | |||||||||||||
378 | }{ | |||||||||||||
379 | 0 | 0 | my $key = _md5_utf8($1); | |||||||||||
380 | 0 | 0 | $self->{_html_blocks}{$key} = $1; | |||||||||||
381 | 0 | 0 | "\n\n" . $key . "\n\n"; | |||||||||||
382 | }egx; | |||||||||||||
383 | 3 | 10 | return $text; | |||||||||||
384 | } | |||||||||||||
385 | ||||||||||||||
386 | sub _RunBlockGamut { | |||||||||||||
387 | # | |||||||||||||
388 | # These are all the transformations that form block-level | |||||||||||||
389 | # tags like paragraphs, headers, and list items. | |||||||||||||
390 | # | |||||||||||||
391 | 3 | 3 | 7 | my ($self, $text, $options) = @_; | ||||||||||
392 | ||||||||||||||
393 | # Do headers first, as these populate cross-refs | |||||||||||||
394 | 3 | 10 | $text = $self->_DoHeaders($text); | |||||||||||
395 | ||||||||||||||
396 | # Do Horizontal Rules: | |||||||||||||
397 | 3 | 8 | my $less_than_tab = $self->{tab_width} - 1; | |||||||||||
398 | #$text =~ s{^[ ]{0,$less_than_tab}(\*[ ]?){3,}[ \t]*$}{\n {empty_element_suffix}\n}gmx; |
|||||||||||||
399 | #$text =~ s{^[ ]{0,$less_than_tab}(-[ ]?){3,}[ \t]*$}{\n {empty_element_suffix}\n}gmx; |
|||||||||||||
400 | #$text =~ s{^[ ]{0,$less_than_tab}(_[ ]?){3,}[ \t]*$}{\n {empty_element_suffix}\n}gmx; |
|||||||||||||
401 | 3 | 32 | $text =~ s{^[ ]{0,$less_than_tab}(\*[ ]?){3,}[ \t]*$}{"\n" . ("=" x 72) . "\n\n"}egmx; | |||||||||||
0 | 0 | |||||||||||||
402 | 3 | 28 | $text =~ s{^[ ]{0,$less_than_tab}(-[ ]?){3,}[ \t]*$}{"\n" . ("=" x 72) . "\n\n"}egmx; | |||||||||||
0 | 0 | |||||||||||||
403 | 3 | 28 | $text =~ s{^[ ]{0,$less_than_tab}(_[ ]?){3,}[ \t]*$}{"\n" . ("=" x 72) . "\n\n"}egmx; | |||||||||||
0 | 0 | |||||||||||||
404 | ||||||||||||||
405 | 3 | 12 | $text = $self->_DoLists($text); | |||||||||||
406 | ||||||||||||||
407 | 3 | 12 | $text = $self->_DoCodeBlocks($text); | |||||||||||
408 | ||||||||||||||
409 | 3 | 11 | $text = $self->_DoBlockQuotes($text); | |||||||||||
410 | ||||||||||||||
411 | # We already ran _HashHTMLBlocks() before, in Markdown(), but that | |||||||||||||
412 | # was to escape raw HTML in the original Markdown source. This time, | |||||||||||||
413 | # we're escaping the markup we've just created, so that we don't wrap | |||||||||||||
414 | # tags around block-level tags. |
|||||||||||||
415 | 3 | 9 | $text = $self->_HashHTMLBlocks($text); | |||||||||||
416 | ||||||||||||||
417 | # Special case just for . It was easier to make a special case than |
|||||||||||||
418 | # to make the other regex more complicated. | |||||||||||||
419 | 3 | 14 | $text = $self->_HashHR($text); | |||||||||||
420 | ||||||||||||||
421 | 3 | 10 | $text = $self->_HashHTMLComments($text); | |||||||||||
422 | ||||||||||||||
423 | 3 | 12 | $text = $self->_HashPHPASPBlocks($text); | |||||||||||
424 | ||||||||||||||
425 | 3 | 18 | $text = $self->_FormParagraphs($text, {wrap_in_p_tags => $options->{wrap_in_p_tags}}); | |||||||||||
426 | ||||||||||||||
427 | 3 | 11 | return $text; | |||||||||||
428 | } | |||||||||||||
429 | ||||||||||||||
430 | sub _RunSpanGamut { | |||||||||||||
431 | # | |||||||||||||
432 | # These are all the transformations that occur *within* block-level | |||||||||||||
433 | # tags like paragraphs, headers, and list items. | |||||||||||||
434 | # | |||||||||||||
435 | 3 | 3 | 6 | my ($self, $text) = @_; | ||||||||||
436 | ||||||||||||||
437 | 3 | 10 | $text = $self->_DoCodeSpans($text); | |||||||||||
438 | 3 | 9 | $text = $self->_EscapeSpecialCharsWithinTagAttributes($text); | |||||||||||
439 | 3 | 10 | $text = $self->_EscapeSpecialChars($text); | |||||||||||
440 | ||||||||||||||
441 | # Process anchor and image tags. Images must come first, | |||||||||||||
442 | # because ![foo][f] looks like an anchor. | |||||||||||||
443 | 3 | 11 | $text = $self->_DoImages($text); | |||||||||||
444 | 3 | 10 | $text = $self->_DoAnchors($text); | |||||||||||
445 | ||||||||||||||
446 | # Make links out of things like ` |
|||||||||||||
447 | # Must come after _DoAnchors(), because you can use < and > | |||||||||||||
448 | # delimiters in inline links like [this]( |
|||||||||||||
449 | 3 | 11 | $text = $self->_DoAutoLinks($text); | |||||||||||
450 | ||||||||||||||
451 | 3 | 8 | $text = $self->_EncodeAmpsAndAngles($text); | |||||||||||
452 | ||||||||||||||
453 | 3 | 10 | $text = $self->_DoItalicsAndBold($text); | |||||||||||
454 | ||||||||||||||
455 | # FIXME - Is hard coding space here sane, or does this want to be related to tab width? | |||||||||||||
456 | # Do hard breaks: | |||||||||||||
457 | 3 | 9 | $text =~ s/ {2,}\n/ {empty_element_suffix}\n/g; |
|||||||||||
458 | ||||||||||||||
459 | 3 | 11 | return $text; | |||||||||||
460 | } | |||||||||||||
461 | ||||||||||||||
462 | sub _EscapeSpecialChars { | |||||||||||||
463 | 3 | 3 | 5 | my ($self, $text) = @_; | ||||||||||
464 | 3 | 33 | 30 | my $tokens ||= $self->_TokenizeHTML($text); | ||||||||||
465 | ||||||||||||||
466 | 3 | 6 | $text = ''; # rebuild $text from the tokens | |||||||||||
467 | # my $in_pre = 0; # Keep track of when we're inside or |
|||||||||||||
468 | # my $tags_to_skip = qr!<(/?)(?:pre|code|kbd|script|math)[\s>]!; | |||||||||||||
469 | ||||||||||||||
470 | 3 | 7 | foreach my $cur_token (@$tokens) { | |||||||||||
471 | 7 | 100 | 17 | if ($cur_token->[0] eq "tag") { | ||||||||||
472 | # Within tags, encode * and _ so they don't conflict | |||||||||||||
473 | # with their use in Markdown for italics and strong. | |||||||||||||
474 | # We're replacing each such character with its | |||||||||||||
475 | # corresponding MD5 checksum value; this is likely | |||||||||||||
476 | # overkill, but it should prevent us from colliding | |||||||||||||
477 | # with the escape values by accident. | |||||||||||||
478 | 2 | 5 | $cur_token->[1] =~ s! \* !$g_escape_table{'*'}!ogx; | |||||||||||
479 | 2 | 3 | $cur_token->[1] =~ s! _ !$g_escape_table{'_'}!ogx; | |||||||||||
480 | 2 | 4 | $text .= $cur_token->[1]; | |||||||||||
481 | } else { | |||||||||||||
482 | 5 | 7 | my $t = $cur_token->[1]; | |||||||||||
483 | 5 | 13 | $t = $self->_EncodeBackslashEscapes($t); | |||||||||||
484 | 5 | 14 | $text .= $t; | |||||||||||
485 | } | |||||||||||||
486 | } | |||||||||||||
487 | 3 | 11 | return $text; | |||||||||||
488 | } | |||||||||||||
489 | ||||||||||||||
490 | sub _EscapeSpecialCharsWithinTagAttributes { | |||||||||||||
491 | # | |||||||||||||
492 | # Within tags -- meaning between < and > -- encode [\ ` * _] so they | |||||||||||||
493 | # don't conflict with their use in Markdown for code, italics and strong. | |||||||||||||
494 | # We're replacing each such character with its corresponding MD5 checksum | |||||||||||||
495 | # value; this is likely overkill, but it should prevent us from colliding | |||||||||||||
496 | # with the escape values by accident. | |||||||||||||
497 | # | |||||||||||||
498 | 3 | 3 | 6 | my ($self, $text) = @_; | ||||||||||
499 | 3 | 33 | 16 | my $tokens ||= $self->_TokenizeHTML($text); | ||||||||||
500 | 3 | 6 | $text = ''; # rebuild $text from the tokens | |||||||||||
501 | ||||||||||||||
502 | 3 | 8 | foreach my $cur_token (@$tokens) { | |||||||||||
503 | 7 | 100 | 21 | if ($cur_token->[0] eq "tag") { | ||||||||||
504 | 2 | 5 | $cur_token->[1] =~ s! \\ !$g_escape_table{'\\'}!gox; | |||||||||||
505 | 2 | 4 | $cur_token->[1] =~ s{ (?<=.)?code>(?=.) }{$g_escape_table{'`'}}gox; | |||||||||||
506 | 2 | 4 | $cur_token->[1] =~ s! \* !$g_escape_table{'*'}!gox; | |||||||||||
507 | 2 | 3 | $cur_token->[1] =~ s! _ !$g_escape_table{'_'}!gox; | |||||||||||
508 | } | |||||||||||||
509 | 7 | 17 | $text .= $cur_token->[1]; | |||||||||||
510 | } | |||||||||||||
511 | 3 | 11 | return $text; | |||||||||||
512 | } | |||||||||||||
513 | ||||||||||||||
514 | sub _DoAnchors { | |||||||||||||
515 | # | |||||||||||||
516 | # Turn Markdown link shortcuts into XHTML tags. | |||||||||||||
517 | # | |||||||||||||
518 | 3 | 3 | 4 | my ($self, $text) = @_; | ||||||||||
519 | ||||||||||||||
520 | # | |||||||||||||
521 | # First, handle reference-style links: [link text] [id] | |||||||||||||
522 | # | |||||||||||||
523 | 3 | 127 | $text =~ s{ | |||||||||||
524 | ( # wrap whole match in $1 | |||||||||||||
525 | \[ | |||||||||||||
526 | ($g_nested_brackets) # link text = $2 | |||||||||||||
527 | \] | |||||||||||||
528 | ||||||||||||||
529 | [ ]? # one optional space | |||||||||||||
530 | (?:\n[ ]*)? # one optional newline followed by spaces | |||||||||||||
531 | ||||||||||||||
532 | \[ | |||||||||||||
533 | (.*?) # id = $3 | |||||||||||||
534 | \] | |||||||||||||
535 | ) | |||||||||||||
536 | }{ | |||||||||||||
537 | 0 | 0 | my $whole_match = $1; | |||||||||||
538 | 0 | 0 | my $link_text = $2; | |||||||||||
539 | 0 | 0 | my $link_id = lc $3; | |||||||||||
540 | ||||||||||||||
541 | 0 | 0 | 0 | if ($link_id eq "") { | ||||||||||
542 | 0 | 0 | $link_id = lc $link_text; # for shortcut links like [this][]. | |||||||||||
543 | } | |||||||||||||
544 | ||||||||||||||
545 | 0 | 0 | $link_id =~ s{[ ]*\n}{ }g; # turn embedded newlines into spaces | |||||||||||
546 | ||||||||||||||
547 | 0 | 0 | $self->_GenerateAnchor($whole_match, $link_text, $link_id); | |||||||||||
548 | }xsge; | |||||||||||||
549 | ||||||||||||||
550 | # | |||||||||||||
551 | # Next, inline-style links: [link text](url "optional title") | |||||||||||||
552 | # | |||||||||||||
553 | 3 | 208 | $text =~ s{ | |||||||||||
554 | ( # wrap whole match in $1 | |||||||||||||
555 | \[ | |||||||||||||
556 | ($g_nested_brackets) # link text = $2 | |||||||||||||
557 | \] | |||||||||||||
558 | \( # literal paren | |||||||||||||
559 | [ \t]* | |||||||||||||
560 | ($g_nested_parens) # href = $3 | |||||||||||||
561 | [ \t]* | |||||||||||||
562 | ( # $4 | |||||||||||||
563 | (['"]) # quote char = $5 | |||||||||||||
564 | (.*?) # Title = $6 | |||||||||||||
565 | \5 # matching quote | |||||||||||||
566 | [ \t]* # ignore any spaces/tabs between closing quote and ) | |||||||||||||
567 | )? # title is optional | |||||||||||||
568 | \) | |||||||||||||
569 | ) | |||||||||||||
570 | }{ | |||||||||||||
571 | 0 | 0 | my $result; | |||||||||||
572 | 0 | 0 | my $whole_match = $1; | |||||||||||
573 | 0 | 0 | my $link_text = $2; | |||||||||||
574 | 0 | 0 | my $url = $3; | |||||||||||
575 | 0 | 0 | my $title = $6; | |||||||||||
576 | ||||||||||||||
577 | 0 | 0 | $self->_GenerateAnchor($whole_match, $link_text, undef, $url, $title); | |||||||||||
578 | }xsge; | |||||||||||||
579 | ||||||||||||||
580 | # | |||||||||||||
581 | # Last, handle reference-style shortcuts: [link text] | |||||||||||||
582 | # These must come last in case you've also got [link test][1] | |||||||||||||
583 | # or [link test](/foo) | |||||||||||||
584 | # | |||||||||||||
585 | 3 | 8 | $text =~ s{ | |||||||||||
586 | ( # wrap whole match in $1 | |||||||||||||
587 | \[ | |||||||||||||
588 | ([^\[\]]+) # link text = $2; can't contain '[' or ']' | |||||||||||||
589 | \] | |||||||||||||
590 | ) | |||||||||||||
591 | }{ | |||||||||||||
592 | 0 | 0 | my $result; | |||||||||||
593 | 0 | 0 | my $whole_match = $1; | |||||||||||
594 | 0 | 0 | my $link_text = $2; | |||||||||||
595 | 0 | 0 | (my $link_id = lc $2) =~ s{[ ]*\n}{ }g; # lower-case and turn embedded newlines into spaces | |||||||||||
596 | ||||||||||||||
597 | 0 | 0 | $self->_GenerateAnchor($whole_match, $link_text, $link_id); | |||||||||||
598 | }xsge; | |||||||||||||
599 | ||||||||||||||
600 | 3 | 9 | return $text; | |||||||||||
601 | } | |||||||||||||
602 | ||||||||||||||
603 | sub _GenerateAnchor { | |||||||||||||
604 | # FIXME - Fugly, change to named params? | |||||||||||||
605 | 0 | 0 | 0 | my ($self, $whole_match, $link_text, $link_id, $url, $title, $attributes) = @_; | ||||||||||
606 | ||||||||||||||
607 | 0 | 0 | my $result; | |||||||||||
608 | ||||||||||||||
609 | 0 | 0 | 0 | $attributes = '' unless defined $attributes; | ||||||||||
610 | ||||||||||||||
611 | 0 | 0 | 0 | 0 | if ( !defined $url && defined $self->{_urls}{$link_id}) { | |||||||||
612 | 0 | 0 | $url = $self->{_urls}{$link_id}; | |||||||||||
613 | } | |||||||||||||
614 | ||||||||||||||
615 | 0 | 0 | 0 | if (!defined $url) { | ||||||||||
616 | 0 | 0 | return $whole_match; | |||||||||||
617 | } | |||||||||||||
618 | ||||||||||||||
619 | 0 | 0 | $url =~ s! \* !$g_escape_table{'*'}!gox; # We've got to encode these to avoid | |||||||||||
620 | 0 | 0 | $url =~ s! _ !$g_escape_table{'_'}!gox; # conflicting with italics/bold. | |||||||||||
621 | 0 | 0 | $url =~ s{^<(.*)>$}{$1}; # Remove <>'s surrounding URL, if present | |||||||||||
622 | ||||||||||||||
623 | 0 | 0 | $result = qq{ | |||||||||||
624 | ||||||||||||||
625 | 0 | 0 | 0 | 0 | if ( !defined $title && defined $link_id && defined $self->{_titles}{$link_id} ) { | |||||||||
0 | ||||||||||||||
626 | 0 | 0 | $title = $self->{_titles}{$link_id}; | |||||||||||
627 | } | |||||||||||||
628 | ||||||||||||||
629 | 0 | 0 | 0 | if ( defined $title ) { | ||||||||||
630 | 0 | 0 | $title =~ s/"/"/g; | |||||||||||
631 | 0 | 0 | $title =~ s! \* !$g_escape_table{'*'}!gox; | |||||||||||
632 | 0 | 0 | $title =~ s! _ !$g_escape_table{'_'}!gox; | |||||||||||
633 | 0 | 0 | $result .= qq{ title="$title"}; | |||||||||||
634 | } | |||||||||||||
635 | ||||||||||||||
636 | #$result .= "$attributes>$link_text"; | |||||||||||||
637 | 0 | 0 | 0 | $result = __podfmt(L => ($url . ($title ? "|$title" : ""))); | ||||||||||
638 | ||||||||||||||
639 | 0 | 0 | return $result; | |||||||||||
640 | } | |||||||||||||
641 | ||||||||||||||
642 | sub _DoImages { | |||||||||||||
643 | # | |||||||||||||
644 | # Turn Markdown image shortcuts into tags. | |||||||||||||
645 | # | |||||||||||||
646 | 3 | 3 | 5 | my ($self, $text) = @_; | ||||||||||
647 | ||||||||||||||
648 | # | |||||||||||||
649 | # First, handle reference-style labeled images: ![alt text][id] | |||||||||||||
650 | # | |||||||||||||
651 | 3 | 6 | $text =~ s{ | |||||||||||
652 | ( # wrap whole match in $1 | |||||||||||||
653 | !\[ | |||||||||||||
654 | (.*?) # alt text = $2 | |||||||||||||
655 | \] | |||||||||||||
656 | ||||||||||||||
657 | [ ]? # one optional space | |||||||||||||
658 | (?:\n[ ]*)? # one optional newline followed by spaces | |||||||||||||
659 | ||||||||||||||
660 | \[ | |||||||||||||
661 | (.*?) # id = $3 | |||||||||||||
662 | \] | |||||||||||||
663 | ||||||||||||||
664 | ) | |||||||||||||
665 | }{ | |||||||||||||
666 | 0 | 0 | my $result; | |||||||||||
667 | 0 | 0 | my $whole_match = $1; | |||||||||||
668 | 0 | 0 | my $alt_text = $2; | |||||||||||
669 | 0 | 0 | my $link_id = lc $3; | |||||||||||
670 | ||||||||||||||
671 | 0 | 0 | 0 | if ($link_id eq '') { | ||||||||||
672 | 0 | 0 | $link_id = lc $alt_text; # for shortcut links like ![this][]. | |||||||||||
673 | } | |||||||||||||
674 | ||||||||||||||
675 | 0 | 0 | $self->_GenerateImage($whole_match, $alt_text, $link_id); | |||||||||||
676 | }xsge; | |||||||||||||
677 | ||||||||||||||
678 | # | |||||||||||||
679 | # Next, handle inline images: ![alt text](url "optional title") | |||||||||||||
680 | # Don't forget: encode * and _ | |||||||||||||
681 | ||||||||||||||
682 | 3 | 184 | $text =~ s{ | |||||||||||
683 | ( # wrap whole match in $1 | |||||||||||||
684 | !\[ | |||||||||||||
685 | (.*?) # alt text = $2 | |||||||||||||
686 | \] | |||||||||||||
687 | \( # literal paren | |||||||||||||
688 | [ \t]* | |||||||||||||
689 | ($g_nested_parens) # src url - href = $3 | |||||||||||||
690 | [ \t]* | |||||||||||||
691 | ( # $4 | |||||||||||||
692 | (['"]) # quote char = $5 | |||||||||||||
693 | (.*?) # title = $6 | |||||||||||||
694 | \5 # matching quote | |||||||||||||
695 | [ \t]* | |||||||||||||
696 | )? # title is optional | |||||||||||||
697 | \) | |||||||||||||
698 | ) | |||||||||||||
699 | }{ | |||||||||||||
700 | 0 | 0 | my $result; | |||||||||||
701 | 0 | 0 | my $whole_match = $1; | |||||||||||
702 | 0 | 0 | my $alt_text = $2; | |||||||||||
703 | 0 | 0 | my $url = $3; | |||||||||||
704 | 0 | 0 | my $title = ''; | |||||||||||
705 | 0 | 0 | 0 | if (defined($6)) { | ||||||||||
706 | 0 | 0 | $title = $6; | |||||||||||
707 | } | |||||||||||||
708 | ||||||||||||||
709 | 0 | 0 | $self->_GenerateImage($whole_match, $alt_text, undef, $url, $title); | |||||||||||
710 | }xsge; | |||||||||||||
711 | ||||||||||||||
712 | 3 | 10 | return $text; | |||||||||||
713 | } | |||||||||||||
714 | ||||||||||||||
715 | sub _GenerateImage { | |||||||||||||
716 | # FIXME - Fugly, change to named params? | |||||||||||||
717 | 0 | 0 | 0 | my ($self, $whole_match, $alt_text, $link_id, $url, $title, $attributes) = @_; | ||||||||||
718 | ||||||||||||||
719 | 0 | 0 | my $result; | |||||||||||
720 | ||||||||||||||
721 | 0 | 0 | 0 | $attributes = '' unless defined $attributes; | ||||||||||
722 | ||||||||||||||
723 | 0 | 0 | 0 | $alt_text ||= ''; | ||||||||||
724 | 0 | 0 | $alt_text =~ s/"/"/g; | |||||||||||
725 | # FIXME - how about > | |||||||||||||
726 | ||||||||||||||
727 | 0 | 0 | 0 | 0 | if ( !defined $url && defined $self->{_urls}{$link_id}) { | |||||||||
728 | 0 | 0 | $url = $self->{_urls}{$link_id}; | |||||||||||
729 | } | |||||||||||||
730 | ||||||||||||||
731 | # If there's no such link ID, leave intact: | |||||||||||||
732 | 0 | 0 | 0 | return $whole_match unless defined $url; | ||||||||||
733 | ||||||||||||||
734 | 0 | 0 | $url =~ s! \* !$g_escape_table{'*'}!ogx; # We've got to encode these to avoid | |||||||||||
735 | 0 | 0 | $url =~ s! _ !$g_escape_table{'_'}!ogx; # conflicting with italics/bold. | |||||||||||
736 | 0 | 0 | $url =~ s{^<(.*)>$}{$1}; # Remove <>'s surrounding URL, if present | |||||||||||
737 | ||||||||||||||
738 | 0 | 0 | 0 | 0 | if (!defined $title && length $link_id && defined $self->{_titles}{$link_id} && length $self->{_titles}{$link_id}) { | |||||||||
0 | ||||||||||||||
0 | ||||||||||||||
739 | 0 | 0 | $title = $self->{_titles}{$link_id}; | |||||||||||
740 | } | |||||||||||||
741 | ||||||||||||||
742 | 0 | 0 | $result = qq{ | |||||||||||
743 | 0 | 0 | 0 | 0 | if (defined $title && length $title) { | |||||||||
744 | 0 | 0 | $title =~ s! \* !$g_escape_table{'*'}!ogx; | |||||||||||
745 | 0 | 0 | $title =~ s! _ !$g_escape_table{'_'}!ogx; | |||||||||||
746 | 0 | 0 | $title =~ s/"/"/g; | |||||||||||
747 | 0 | 0 | $result .= qq{ title="$title"}; | |||||||||||
748 | } | |||||||||||||
749 | 0 | 0 | $result .= $attributes . $self->{empty_element_suffix}; | |||||||||||
750 | ||||||||||||||
751 | 0 | 0 | $result = "\n\n=begin HTML\n\n$result\n\n=end HTML\n\n"; | |||||||||||
752 | ||||||||||||||
753 | 0 | 0 | return $result; | |||||||||||
754 | } | |||||||||||||
755 | ||||||||||||||
756 | sub _DoHeaders { | |||||||||||||
757 | 3 | 3 | 4 | my ($self, $text) = @_; | ||||||||||
758 | ||||||||||||||
759 | # Setext-style headers: | |||||||||||||
760 | # Header 1 | |||||||||||||
761 | # ======== | |||||||||||||
762 | # | |||||||||||||
763 | # Header 2 | |||||||||||||
764 | # -------- | |||||||||||||
765 | # | |||||||||||||
766 | 3 | 7 | $text =~ s{ ^(.+)[ \t]*\n=+[ \t]*\n+ }{ | |||||||||||
767 | 0 | 0 | $self->_GenerateHeader('1', $1); | |||||||||||
768 | }egmx; | |||||||||||||
769 | ||||||||||||||
770 | 3 | 5 | $text =~ s{ ^(.+)[ \t]*\n-+[ \t]*\n+ }{ | |||||||||||
771 | 0 | 0 | $self->_GenerateHeader('2', $1); | |||||||||||
772 | }egmx; | |||||||||||||
773 | ||||||||||||||
774 | ||||||||||||||
775 | # atx-style headers: | |||||||||||||
776 | # # Header 1 | |||||||||||||
777 | # ## Header 2 | |||||||||||||
778 | # ## Header 2 with closing hashes ## | |||||||||||||
779 | # ... | |||||||||||||
780 | # ###### Header 6 | |||||||||||||
781 | # | |||||||||||||
782 | 3 | 4 | my $l; | |||||||||||
783 | 3 | 6 | $text =~ s{ | |||||||||||
784 | ^(\#{1,6}) # $1 = string of #'s | |||||||||||||
785 | [ \t]* | |||||||||||||
786 | (.+?) # $2 = Header text | |||||||||||||
787 | [ \t]* | |||||||||||||
788 | \#* # optional closing #'s (not counted) | |||||||||||||
789 | \n+ | |||||||||||||
790 | }{ | |||||||||||||
791 | 0 | 0 | my $h_level = length($1); | |||||||||||
792 | 0 | 0 | $self->_GenerateHeader($h_level, $2); | |||||||||||
793 | }egmx; | |||||||||||||
794 | ||||||||||||||
795 | 3 | 7 | return $text; | |||||||||||
796 | } | |||||||||||||
797 | ||||||||||||||
798 | sub _GenerateHeader { | |||||||||||||
799 | 0 | 0 | 0 | my ($self, $level, $id) = @_; | ||||||||||
800 | ||||||||||||||
801 | #return " |
|||||||||||||
802 | 0 | 0 | return "=head$level " . $self->_RunSpanGamut($id) . "\n\n"; | |||||||||||
803 | } | |||||||||||||
804 | ||||||||||||||
805 | sub _DoLists { | |||||||||||||
806 | # | |||||||||||||
807 | # Form HTML ordered (numbered) and unordered (bulleted) lists. | |||||||||||||
808 | # | |||||||||||||
809 | 3 | 3 | 6 | my ($self, $text) = @_; | ||||||||||
810 | 3 | 7 | my $less_than_tab = $self->{tab_width} - 1; | |||||||||||
811 | ||||||||||||||
812 | # Re-usable patterns to match list item bullets and number markers: | |||||||||||||
813 | 3 | 10 | my $marker_ul = qr/[*+-]/; | |||||||||||
814 | 3 | 9 | my $marker_ol = qr/\d+[.]/; | |||||||||||
815 | 3 | 34 | my $marker_any = qr/(?:$marker_ul|$marker_ol)/; | |||||||||||
816 | ||||||||||||||
817 | # Re-usable pattern to match any entirel ul or ol list: | |||||||||||||
818 | 3 | 76 | my $whole_list = qr{ | |||||||||||
819 | ( # $1 = whole list | |||||||||||||
820 | ( # $2 | |||||||||||||
821 | [ ]{0,$less_than_tab} | |||||||||||||
822 | (${marker_any}) # $3 = first list item marker | |||||||||||||
823 | [ \t]+ | |||||||||||||
824 | ) | |||||||||||||
825 | (?s:.+?) | |||||||||||||
826 | ( # $4 | |||||||||||||
827 | \z | |||||||||||||
828 | | | |||||||||||||
829 | \n{2,} | |||||||||||||
830 | (?=\S) | |||||||||||||
831 | (?! # Negative lookahead for another list item marker | |||||||||||||
832 | [ \t]* | |||||||||||||
833 | ${marker_any}[ \t]+ | |||||||||||||
834 | ) | |||||||||||||
835 | ) | |||||||||||||
836 | ) | |||||||||||||
837 | }mx; | |||||||||||||
838 | ||||||||||||||
839 | # We use a different prefix before nested lists than top-level lists. | |||||||||||||
840 | # See extended comment in _ProcessListItems(). | |||||||||||||
841 | # | |||||||||||||
842 | # Note: There's a bit of duplication here. My original implementation | |||||||||||||
843 | # created a scalar regex pattern as the conditional result of the test on | |||||||||||||
844 | # $self->{_list_level}, and then only ran the $text =~ s{...}{...}egmx | |||||||||||||
845 | # substitution once, using the scalar as the pattern. This worked, | |||||||||||||
846 | # everywhere except when running under MT on my hosting account at Pair | |||||||||||||
847 | # Networks. There, this caused all rebuilds to be killed by the reaper (or | |||||||||||||
848 | # perhaps they crashed, but that seems incredibly unlikely given that the | |||||||||||||
849 | # same script on the same server ran fine *except* under MT. I've spent | |||||||||||||
850 | # more time trying to figure out why this is happening than I'd like to | |||||||||||||
851 | # admit. My only guess, backed up by the fact that this workaround works, | |||||||||||||
852 | # is that Perl optimizes the substition when it can figure out that the | |||||||||||||
853 | # pattern will never change, and when this optimization isn't on, we run | |||||||||||||
854 | # afoul of the reaper. Thus, the slightly redundant code to that uses two | |||||||||||||
855 | # static s/// patterns rather than one conditional pattern. | |||||||||||||
856 | ||||||||||||||
857 | 3 | 50 | 13 | if ($self->{_list_level}) { | ||||||||||
858 | 0 | 0 | $text =~ s{ | |||||||||||
859 | ^ | |||||||||||||
860 | $whole_list | |||||||||||||
861 | }{ | |||||||||||||
862 | 0 | 0 | my $list = $1; | |||||||||||
863 | 0 | 0 | my $marker = $3; | |||||||||||
864 | 0 | 0 | 0 | my $list_type = ($marker =~ m/$marker_ul/) ? "ul" : "ol"; | ||||||||||
865 | # Turn double returns into triple returns, so that we can make a | |||||||||||||
866 | # paragraph for the last item in a list, if necessary: | |||||||||||||
867 | 0 | 0 | $list =~ s/\n{2,}/\n\n\n/g; | |||||||||||
868 | 0 | 0 | 0 | my $result = ( $list_type eq 'ul' ) ? | ||||||||||
869 | $self->_ProcessListItemsUL($list, $marker_ul) | |||||||||||||
870 | : $self->_ProcessListItemsOL($list, $marker_ol); | |||||||||||||
871 | ||||||||||||||
872 | 0 | 0 | $result = $self->_MakeList($list_type, $result, $marker); | |||||||||||
873 | 0 | 0 | $result; | |||||||||||
874 | }egmx; | |||||||||||||
875 | } | |||||||||||||
876 | else { | |||||||||||||
877 | 3 | 96 | $text =~ s{ | |||||||||||
878 | (?:(?<=\n\n)|\A\n?) | |||||||||||||
879 | $whole_list | |||||||||||||
880 | }{ | |||||||||||||
881 | 0 | 0 | my $list = $1; | |||||||||||
882 | 0 | 0 | my $marker = $3; | |||||||||||
883 | 0 | 0 | 0 | my $list_type = ($marker =~ m/$marker_ul/) ? "ul" : "ol"; | ||||||||||
884 | # Turn double returns into triple returns, so that we can make a | |||||||||||||
885 | # paragraph for the last item in a list, if necessary: | |||||||||||||
886 | 0 | 0 | $list =~ s/\n{2,}/\n\n\n/g; | |||||||||||
887 | 0 | 0 | 0 | my $result = ( $list_type eq 'ul' ) ? | ||||||||||
888 | $self->_ProcessListItemsUL($list, $marker_ul) | |||||||||||||
889 | : $self->_ProcessListItemsOL($list, $marker_ol); | |||||||||||||
890 | 0 | 0 | $result = $self->_MakeList($list_type, $result, $marker); | |||||||||||
891 | 0 | 0 | $result; | |||||||||||
892 | }egmx; | |||||||||||||
893 | } | |||||||||||||
894 | ||||||||||||||
895 | ||||||||||||||
896 | 3 | 16 | return $text; | |||||||||||
897 | } | |||||||||||||
898 | ||||||||||||||
899 | sub _MakeList { | |||||||||||||
900 | 0 | 0 | 0 | my ($self, $list_type, $content, $marker) = @_; | ||||||||||
901 | ||||||||||||||
902 | 0 | 0 | 0 | 0 | if ($list_type eq 'ol' and $self->{trust_list_start_value}) { | |||||||||
903 | 0 | 0 | my ($num) = $marker =~ /^(\d+)[.]/; | |||||||||||
904 | #return "
|
|||||||||||||
905 | 0 | 0 | return "=over\n\n" . $content . "=back\n\n"; | |||||||||||
906 | } | |||||||||||||
907 | ||||||||||||||
908 | #return "<$list_type>\n" . $content . "$list_type>\n"; | |||||||||||||
909 | 0 | 0 | return "=over\n\n" . $content . "=back\n\n"; | |||||||||||
910 | } | |||||||||||||
911 | ||||||||||||||
912 | sub _ProcessListItemsOL { | |||||||||||||
913 | # | |||||||||||||
914 | # Process the contents of a single ordered list, splitting it | |||||||||||||
915 | # into individual list items. | |||||||||||||
916 | # | |||||||||||||
917 | ||||||||||||||
918 | 0 | 0 | 0 | my ($self, $list_str, $marker_any) = @_; | ||||||||||
919 | ||||||||||||||
920 | ||||||||||||||
921 | # The $self->{_list_level} global keeps track of when we're inside a list. | |||||||||||||
922 | # Each time we enter a list, we increment it; when we leave a list, | |||||||||||||
923 | # we decrement. If it's zero, we're not in a list anymore. | |||||||||||||
924 | # | |||||||||||||
925 | # We do this because when we're not inside a list, we want to treat | |||||||||||||
926 | # something like this: | |||||||||||||
927 | # | |||||||||||||
928 | # I recommend upgrading to version | |||||||||||||
929 | # 8. Oops, now this line is treated | |||||||||||||
930 | # as a sub-list. | |||||||||||||
931 | # | |||||||||||||
932 | # As a single paragraph, despite the fact that the second line starts | |||||||||||||
933 | # with a digit-period-space sequence. | |||||||||||||
934 | # | |||||||||||||
935 | # Whereas when we're inside a list (or sub-list), that line will be | |||||||||||||
936 | # treated as the start of a sub-list. What a kludge, huh? This is | |||||||||||||
937 | # an aspect of Markdown's syntax that's hard to parse perfectly | |||||||||||||
938 | # without resorting to mind-reading. Perhaps the solution is to | |||||||||||||
939 | # change the syntax rules such that sub-lists must start with a | |||||||||||||
940 | # starting cardinal number; e.g. "1." or "a.". | |||||||||||||
941 | ||||||||||||||
942 | 0 | 0 | $self->{_list_level}++; | |||||||||||
943 | ||||||||||||||
944 | # trim trailing blank lines: | |||||||||||||
945 | 0 | 0 | $list_str =~ s/\n{2,}\z/\n/; | |||||||||||
946 | ||||||||||||||
947 | ||||||||||||||
948 | 0 | 0 | my $i = 0; | |||||||||||
949 | ||||||||||||||
950 | 0 | 0 | $list_str =~ s{ | |||||||||||
951 | (\n)? # leading line = $1 | |||||||||||||
952 | (^[ \t]*) # leading whitespace = $2 | |||||||||||||
953 | ($marker_any) [ \t]+ # list marker = $3 | |||||||||||||
954 | ((?s:.+?) # list item text = $4 | |||||||||||||
955 | (\n{1,2})) | |||||||||||||
956 | (?= \n* (\z | \2 ($marker_any) [ \t]+)) | |||||||||||||
957 | }{ | |||||||||||||
958 | 0 | 0 | my $item = $4; | |||||||||||
959 | 0 | 0 | my $leading_line = $1; | |||||||||||
960 | 0 | 0 | my $leading_space = $2; | |||||||||||
961 | ||||||||||||||
962 | 0 | 0 | 0 | 0 | if ($leading_line or ($item =~ m/\n{2,}/)) { | |||||||||
963 | 0 | 0 | $item = $self->_RunBlockGamut($self->_Outdent($item), {wrap_in_p_tags => 1}); | |||||||||||
964 | } | |||||||||||||
965 | else { | |||||||||||||
966 | # Recursion for sub-lists: | |||||||||||||
967 | 0 | 0 | $item = $self->_DoLists($self->_Outdent($item)); | |||||||||||
968 | 0 | 0 | chomp $item; | |||||||||||
969 | 0 | 0 | $item = $self->_RunSpanGamut($item); | |||||||||||
970 | } | |||||||||||||
971 | ||||||||||||||
972 | #" |
|||||||||||||
973 | 0 | 0 | $i++; "=item $i. " . $item . "\n\n"; | |||||||||||
0 | 0 | |||||||||||||
974 | }egmxo; | |||||||||||||
975 | ||||||||||||||
976 | 0 | 0 | $self->{_list_level}--; | |||||||||||
977 | 0 | 0 | return $list_str; | |||||||||||
978 | } | |||||||||||||
979 | ||||||||||||||
980 | sub _ProcessListItemsUL { | |||||||||||||
981 | # | |||||||||||||
982 | # Process the contents of a single unordered list, splitting it | |||||||||||||
983 | # into individual list items. | |||||||||||||
984 | # | |||||||||||||
985 | ||||||||||||||
986 | 0 | 0 | 0 | my ($self, $list_str, $marker_any) = @_; | ||||||||||
987 | ||||||||||||||
988 | ||||||||||||||
989 | # The $self->{_list_level} global keeps track of when we're inside a list. | |||||||||||||
990 | # Each time we enter a list, we increment it; when we leave a list, | |||||||||||||
991 | # we decrement. If it's zero, we're not in a list anymore. | |||||||||||||
992 | # | |||||||||||||
993 | # We do this because when we're not inside a list, we want to treat | |||||||||||||
994 | # something like this: | |||||||||||||
995 | # | |||||||||||||
996 | # I recommend upgrading to version | |||||||||||||
997 | # 8. Oops, now this line is treated | |||||||||||||
998 | # as a sub-list. | |||||||||||||
999 | # | |||||||||||||
1000 | # As a single paragraph, despite the fact that the second line starts | |||||||||||||
1001 | # with a digit-period-space sequence. | |||||||||||||
1002 | # | |||||||||||||
1003 | # Whereas when we're inside a list (or sub-list), that line will be | |||||||||||||
1004 | # treated as the start of a sub-list. What a kludge, huh? This is | |||||||||||||
1005 | # an aspect of Markdown's syntax that's hard to parse perfectly | |||||||||||||
1006 | # without resorting to mind-reading. Perhaps the solution is to | |||||||||||||
1007 | # change the syntax rules such that sub-lists must start with a | |||||||||||||
1008 | # starting cardinal number; e.g. "1." or "a.". | |||||||||||||
1009 | ||||||||||||||
1010 | 0 | 0 | $self->{_list_level}++; | |||||||||||
1011 | ||||||||||||||
1012 | # trim trailing blank lines: | |||||||||||||
1013 | 0 | 0 | $list_str =~ s/\n{2,}\z/\n/; | |||||||||||
1014 | ||||||||||||||
1015 | ||||||||||||||
1016 | 0 | 0 | $list_str =~ s{ | |||||||||||
1017 | (\n)? # leading line = $1 | |||||||||||||
1018 | (^[ \t]*) # leading whitespace = $2 | |||||||||||||
1019 | ($marker_any) [ \t]+ # list marker = $3 | |||||||||||||
1020 | ((?s:.+?) # list item text = $4 | |||||||||||||
1021 | (\n{1,2})) | |||||||||||||
1022 | (?= \n* (\z | \2 ($marker_any) [ \t]+)) | |||||||||||||
1023 | }{ | |||||||||||||
1024 | 0 | 0 | my $item = $4; | |||||||||||
1025 | 0 | 0 | my $leading_line = $1; | |||||||||||
1026 | 0 | 0 | my $leading_space = $2; | |||||||||||
1027 | ||||||||||||||
1028 | 0 | 0 | 0 | 0 | if ($leading_line or ($item =~ m/\n{2,}/)) { | |||||||||
1029 | 0 | 0 | $item = $self->_RunBlockGamut($self->_Outdent($item), {wrap_in_p_tags => 1}); | |||||||||||
1030 | } | |||||||||||||
1031 | else { | |||||||||||||
1032 | # Recursion for sub-lists: | |||||||||||||
1033 | 0 | 0 | $item = $self->_DoLists($self->_Outdent($item)); | |||||||||||
1034 | 0 | 0 | chomp $item; | |||||||||||
1035 | 0 | 0 | $item = $self->_RunSpanGamut($item); | |||||||||||
1036 | } | |||||||||||||
1037 | ||||||||||||||
1038 | #" |
|||||||||||||
1039 | 0 | 0 | "=item * " . $item . "\n\n"; | |||||||||||
1040 | }egmxo; | |||||||||||||
1041 | ||||||||||||||
1042 | 0 | 0 | $self->{_list_level}--; | |||||||||||
1043 | 0 | 0 | return $list_str; | |||||||||||
1044 | } | |||||||||||||
1045 | ||||||||||||||
1046 | sub _DoCodeBlocks { | |||||||||||||
1047 | # | |||||||||||||
1048 | # Process Markdown code blocks (indented with 4 spaces or 1 tab): | |||||||||||||
1049 | # * outdent the spaces/tab | |||||||||||||
1050 | # * encode <, >, & into HTML entities | |||||||||||||
1051 | # * escape Markdown special characters into MD5 hashes | |||||||||||||
1052 | # * trim leading and trailing newlines | |||||||||||||
1053 | # | |||||||||||||
1054 | ||||||||||||||
1055 | 3 | 3 | 7 | my ($self, $text) = @_; | ||||||||||
1056 | ||||||||||||||
1057 | 3 | 66 | $text =~ s{ | |||||||||||
1058 | (?:\n\n|\A) | |||||||||||||
1059 | ( # $1 = the code block -- one or more lines, starting with a space/tab | |||||||||||||
1060 | (?: | |||||||||||||
1061 | (?:[ ]{$self->{tab_width}} | \t) # Lines must start with a tab or a tab-width of spaces | |||||||||||||
1062 | .*\n+ | |||||||||||||
1063 | )+ | |||||||||||||
1064 | ) | |||||||||||||
1065 | ((?=^[ ]{0,$self->{tab_width}}\S)|\Z) # Lookahead for non-space at line-start, or end of doc | |||||||||||||
1066 | }{ | |||||||||||||
1067 | 0 | 0 | my $codeblock = $1; | |||||||||||
1068 | 0 | 0 | my $result; # return value | |||||||||||
1069 | ||||||||||||||
1070 | 0 | 0 | $codeblock = $self->_EncodeCode($self->_Outdent($codeblock), 0); | |||||||||||
1071 | 0 | 0 | $codeblock = $self->_Detab($codeblock); | |||||||||||
1072 | 0 | 0 | $codeblock =~ s/\A\n+//; # trim leading newlines | |||||||||||
1073 | 0 | 0 | $codeblock =~ s/\n+\z//; # trim trailing newlines | |||||||||||
1074 | ||||||||||||||
1075 | #$result = "\n\n \n\n"; |
|||||||||||||
1076 | 0 | 0 | $codeblock =~ s/^/ /mg; | |||||||||||
1077 | ||||||||||||||
1078 | 0 | 0 | $result = "\n\n" . $codeblock . "\n\n"; | |||||||||||
1079 | ||||||||||||||
1080 | 0 | 0 | $result; | |||||||||||
1081 | }egmx; | |||||||||||||
1082 | ||||||||||||||
1083 | 3 | 9 | return $text; | |||||||||||
1084 | } | |||||||||||||
1085 | ||||||||||||||
1086 | sub _DoCodeSpans { | |||||||||||||
1087 | # | |||||||||||||
1088 | # * Backtick quotes are used for spans. |
|||||||||||||
1089 | # | |||||||||||||
1090 | # * You can use multiple backticks as the delimiters if you want to | |||||||||||||
1091 | # include literal backticks in the code span. So, this input: | |||||||||||||
1092 | # | |||||||||||||
1093 | # Just type ``foo `bar` baz`` at the prompt. | |||||||||||||
1094 | # | |||||||||||||
1095 | # Will translate to: | |||||||||||||
1096 | # | |||||||||||||
1097 | # Just type |
|||||||||||||
1098 | # | |||||||||||||
1099 | # There's no arbitrary limit to the number of backticks you | |||||||||||||
1100 | # can use as delimters. If you need three consecutive backticks | |||||||||||||
1101 | # in your code, use four for delimiters, etc. | |||||||||||||
1102 | # | |||||||||||||
1103 | # * You can use spaces to get literal backticks at the edges: | |||||||||||||
1104 | # | |||||||||||||
1105 | # ... type `` `bar` `` ... | |||||||||||||
1106 | # | |||||||||||||
1107 | # Turns to: | |||||||||||||
1108 | # | |||||||||||||
1109 | # ... type `bar` ... |
|||||||||||||
1110 | # | |||||||||||||
1111 | ||||||||||||||
1112 | 3 | 3 | 5 | my ($self, $text) = @_; | ||||||||||
1113 | ||||||||||||||
1114 | 3 | 11 | $text =~ s@ | |||||||||||
1115 | (? | |||||||||||||
1116 | (`+) # $1 = Opening run of ` | |||||||||||||
1117 | (.+?) # $2 = The code block | |||||||||||||
1118 | (? | |||||||||||||
1119 | \1 # Matching closer | |||||||||||||
1120 | (?!`) | |||||||||||||
1121 | @ | |||||||||||||
1122 | 1 | 3 | my $c = "$2"; | |||||||||||
1123 | 1 | 12 | $c =~ s/^[ \t]*//g; # leading whitespace | |||||||||||
1124 | 1 | 6 | $c =~ s/[ \t]*$//g; # trailing whitespace | |||||||||||
1125 | 1 | 5 | $c = $self->_EncodeCode($c); | |||||||||||
1126 | #"$c "; |
|||||||||||||
1127 | 1 | 3 | __podfmt(C => $c); | |||||||||||
1128 | @egsx; | |||||||||||||
1129 | ||||||||||||||
1130 | 3 | 6 | return $text; | |||||||||||
1131 | } | |||||||||||||
1132 | ||||||||||||||
1133 | sub _EncodeCode { | |||||||||||||
1134 | # | |||||||||||||
1135 | # Encode/escape certain characters inside Markdown code runs. | |||||||||||||
1136 | # The point is that in code, these characters are literals, | |||||||||||||
1137 | # and lose their special Markdown meanings. | |||||||||||||
1138 | # | |||||||||||||
1139 | 1 | 1 | 3 | my $self = shift; | ||||||||||
1140 | 1 | 3 | local $_ = shift; | |||||||||||
1141 | 1 | 50 | 10 | my $do_angle_bracket = shift // 1; | ||||||||||
1142 | ||||||||||||||
1143 | # Encode all ampersands; HTML entities are not | |||||||||||||
1144 | # entities within a Markdown code span. | |||||||||||||
1145 | #s/&/&/g; | |||||||||||||
1146 | ||||||||||||||
1147 | # Encode $'s, but only if we're running under Blosxom. | |||||||||||||
1148 | # (Blosxom interpolates Perl variables in article bodies.) | |||||||||||||
1149 | { | |||||||||||||
1150 | 1 | 1 | 29 | no warnings 'once'; | ||||||||||
1 | 2 | |||||||||||||
1 | 2297 | |||||||||||||
1 | 2 | |||||||||||||
1151 | 1 | 50 | 4 | if (defined($blosxom::version)) { | ||||||||||
1152 | #s/\$/$/g; | |||||||||||||
1153 | } | |||||||||||||
1154 | } | |||||||||||||
1155 | ||||||||||||||
1156 | ||||||||||||||
1157 | # Do the angle bracket song and dance: | |||||||||||||
1158 | #s! < !<!gx; | |||||||||||||
1159 | #s! > !>!gx; | |||||||||||||
1160 | 1 | 100 | 7 | s! ([<>]) !$1 eq '<' ? 'E |
||||||||||
2 | 50 | 10 | ||||||||||||
1161 | ||||||||||||||
1162 | # Now, escape characters that are magic in Markdown: | |||||||||||||
1163 | 1 | 3 | s! \* !$g_escape_table{'*'}!ogx; | |||||||||||
1164 | 1 | 2 | s! _ !$g_escape_table{'_'}!ogx; | |||||||||||
1165 | 1 | 2 | s! { !$g_escape_table{'{'}!ogx; | |||||||||||
1166 | 1 | 2 | s! } !$g_escape_table{'}'}!ogx; | |||||||||||
1167 | 1 | 2 | s! \[ !$g_escape_table{'['}!ogx; | |||||||||||
1168 | 1 | 2 | s! \] !$g_escape_table{']'}!ogx; | |||||||||||
1169 | 1 | 2 | s! \\ !$g_escape_table{'\\'}!ogx; | |||||||||||
1170 | ||||||||||||||
1171 | 1 | 3 | return $_; | |||||||||||
1172 | } | |||||||||||||
1173 | ||||||||||||||
1174 | sub __podfmt { | |||||||||||||
1175 | 2 | 2 | 6 | my ($fmt, $content) = @_; | ||||||||||
1176 | 2 | 100 | 10 | if ($content =~ /[<>]/) { | ||||||||||
1177 | 1 | 5 | "$fmt<< $content >>"; | |||||||||||
1178 | } else { | |||||||||||||
1179 | 1 | 8 | "$fmt<$content>"; | |||||||||||
1180 | } | |||||||||||||
1181 | } | |||||||||||||
1182 | ||||||||||||||
1183 | sub _DoItalicsAndBold { | |||||||||||||
1184 | 3 | 3 | 5 | my ($self, $text) = @_; | ||||||||||
1185 | ||||||||||||||
1186 | # Handle at beginning of lines: | |||||||||||||
1187 | 3 | 8 | $text =~ s{ ^(\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1 } | |||||||||||
1188 | #{$2}gsx; | |||||||||||||
1189 | 0 | 0 | {__podfmt(B => $2)}gsex; | |||||||||||
1190 | ||||||||||||||
1191 | 3 | 7 | $text =~ s{ ^(\*|_) (?=\S) (.+?) (?<=\S) \1 } | |||||||||||
1192 | #{$2}gsx; | |||||||||||||
1193 | 0 | 0 | {__podfmt(I => $2)}gsex; | |||||||||||
1194 | ||||||||||||||
1195 | # must go first: | |||||||||||||
1196 | 3 | 13 | $text =~ s{ (?<=\W) (\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1 } | |||||||||||
1197 | #{$2}gsx; | |||||||||||||
1198 | 0 | 0 | {__podfmt(B => $2)}gsex; | |||||||||||
1199 | ||||||||||||||
1200 | 3 | 18 | $text =~ s{ (?<=\W) (\*|_) (?=\S) (.+?) (?<=\S) \1 } | |||||||||||
1201 | #{$2}gsx; | |||||||||||||
1202 | 1 | 5 | {__podfmt(I => $2)}gsex; | |||||||||||
1203 | ||||||||||||||
1204 | # And now, a second pass to catch nested strong and emphasis special cases | |||||||||||||
1205 | 3 | 12 | $text =~ s{ (?<=\W) (\*\*|__) (?=\S) (.+?[*_]*) (?<=\S) \1 } | |||||||||||
1206 | #{$2}gsx; | |||||||||||||
1207 | 0 | 0 | {__podfmt(B => $2)}gsex; | |||||||||||
1208 | ||||||||||||||
1209 | 3 | 12 | $text =~ s{ (?<=\W) (\*|_) (?=\S) (.+?) (?<=\S) \1 } | |||||||||||
1210 | #{$2}gsx; | |||||||||||||
1211 | 0 | 0 | {__podfmt(I => $2)}gsex; | |||||||||||
1212 | ||||||||||||||
1213 | 3 | 8 | return $text; | |||||||||||
1214 | } | |||||||||||||
1215 | ||||||||||||||
1216 | sub _DoBlockQuotes { | |||||||||||||
1217 | 3 | 3 | 7 | my ($self, $text) = @_; | ||||||||||
1218 | ||||||||||||||
1219 | 3 | 4 | $text =~ s{ | |||||||||||
1220 | ( # Wrap whole match in $1 | |||||||||||||
1221 | ( | |||||||||||||
1222 | ^[ \t]*>[ \t]? # '>' at the start of a line | |||||||||||||
1223 | .+\n # rest of the first line | |||||||||||||
1224 | (.+\n)* # subsequent consecutive lines | |||||||||||||
1225 | \n* # blanks | |||||||||||||
1226 | )+ | |||||||||||||
1227 | ) | |||||||||||||
1228 | }{ | |||||||||||||
1229 | 0 | 0 | my $bq = $1; | |||||||||||
1230 | 0 | 0 | $bq =~ s/^([ \t]*>)/ $1/gm; | |||||||||||
1231 | 0 | 0 | $bq; | |||||||||||
1232 | #$bq =~ s/^[ \t]*>[ \t]?//gm; # trim one level of quoting | |||||||||||||
1233 | #$bq =~ s/^[ \t]+$//mg; # trim whitespace-only lines | |||||||||||||
1234 | #$bq = $self->_RunBlockGamut($bq, {wrap_in_p_tags => 1}); # recurse | |||||||||||||
1235 | ||||||||||||||
1236 | #$bq =~ s/^/ /mg; | |||||||||||||
1237 | ## These leading spaces screw with content, so we need to fix that: |
|||||||||||||
1238 | #$bq =~ s{ | |||||||||||||
1239 | # (\s*.+?) |
|||||||||||||
1240 | # }{ | |||||||||||||
1241 | # my $pre = $1; | |||||||||||||
1242 | # #$pre =~ s/^ //mg; | |||||||||||||
1243 | # $pre; | |||||||||||||
1244 | # }egsx; | |||||||||||||
1245 | # | |||||||||||||
1246 | #"\n$bq\n\n\n"; |
|||||||||||||
1247 | }egmx; | |||||||||||||
1248 | ||||||||||||||
1249 | ||||||||||||||
1250 | 3 | 7 | return $text; | |||||||||||
1251 | } | |||||||||||||
1252 | ||||||||||||||
1253 | sub _FormParagraphs { | |||||||||||||
1254 | # | |||||||||||||
1255 | # Params: | |||||||||||||
1256 | # $text - string to process with html tags |
|||||||||||||
1257 | # | |||||||||||||
1258 | 3 | 3 | 6 | my ($self, $text, $options) = @_; | ||||||||||
1259 | ||||||||||||||
1260 | # Strip leading and trailing lines: | |||||||||||||
1261 | 3 | 5 | $text =~ s/\A\n+//; | |||||||||||
1262 | 3 | 11 | $text =~ s/\n+\z//; | |||||||||||
1263 | ||||||||||||||
1264 | 3 | 14 | my @grafs = split(/\n{2,}/, $text); | |||||||||||
1265 | ||||||||||||||
1266 | # | |||||||||||||
1267 | # Wrap tags. |
|||||||||||||
1268 | # | |||||||||||||
1269 | 3 | 8 | foreach (@grafs) { | |||||||||||
1270 | 3 | 50 | 11 | unless (defined( $self->{_html_blocks}{$_} )) { | ||||||||||
1271 | 3 | 10 | $_ = $self->_RunSpanGamut($_); | |||||||||||
1272 | #if ($options->{wrap_in_p_tags}) { | |||||||||||||
1273 | # s/^([ \t]*) //; |
|||||||||||||
1274 | # $_ .= ""; | |||||||||||||
1275 | #} | |||||||||||||
1276 | } | |||||||||||||
1277 | } | |||||||||||||
1278 | ||||||||||||||
1279 | # | |||||||||||||
1280 | # Unhashify HTML blocks | |||||||||||||
1281 | # | |||||||||||||
1282 | 3 | 9 | foreach (@grafs) { | |||||||||||
1283 | 3 | 50 | 16 | if (defined( $self->{_html_blocks}{$_} )) { | ||||||||||
1284 | 0 | 0 | $_ = $self->{_html_blocks}{$_}; | |||||||||||
1285 | } | |||||||||||||
1286 | } | |||||||||||||
1287 | ||||||||||||||
1288 | 3 | 14 | return join "\n\n", @grafs; | |||||||||||
1289 | } | |||||||||||||
1290 | ||||||||||||||
1291 | sub _EncodeAmpsAndAngles { | |||||||||||||
1292 | # Smart processing for ampersands and angle brackets that need to be encoded. | |||||||||||||
1293 | ||||||||||||||
1294 | 3 | 3 | 7 | my ($self, $text) = @_; | ||||||||||
1295 | 3 | 50 | 33 | 14 | return '' if (!defined $text or !length $text); | |||||||||
1296 | ||||||||||||||
1297 | 3 | 9 | return $text; | |||||||||||
1298 | ||||||||||||||
1299 | # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: | |||||||||||||
1300 | # http://bumppo.net/projects/amputator/ | |||||||||||||
1301 | 0 | 0 | $text =~ s/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/&/g; | |||||||||||
1302 | ||||||||||||||
1303 | # Encode naked <'s | |||||||||||||
1304 | 0 | 0 | $text =~ s{<(?![a-z/?\$!])}{<}gi; | |||||||||||
1305 | ||||||||||||||
1306 | # And >'s - added by Fletcher Penney | |||||||||||||
1307 | # $text =~ s{>(?![a-z/?\$!])}{>}gi; | |||||||||||||
1308 | # Causes problems... | |||||||||||||
1309 | ||||||||||||||
1310 | # Remove encoding inside comments | |||||||||||||
1311 | 0 | 0 | $text =~ s{ | |||||||||||
1312 | (?<=) # End comments | |||||||||||||
1315 | }{ | |||||||||||||
1316 | 0 | 0 | my $t = $1; | |||||||||||
1317 | 0 | 0 | $t =~ s/&/&/g; | |||||||||||
1318 | 0 | 0 | $t =~ s/</ | |||||||||||
1319 | 0 | 0 | $t; | |||||||||||
1320 | }egsx; | |||||||||||||
1321 | ||||||||||||||
1322 | 0 | 0 | return $text; | |||||||||||
1323 | } | |||||||||||||
1324 | ||||||||||||||
1325 | sub _EncodeBackslashEscapes { | |||||||||||||
1326 | # | |||||||||||||
1327 | # Parameter: String. | |||||||||||||
1328 | # Returns: The string, with after processing the following backslash | |||||||||||||
1329 | # escape sequences. | |||||||||||||
1330 | # | |||||||||||||
1331 | 5 | 5 | 6 | my $self = shift; | ||||||||||
1332 | 5 | 40 | local $_ = shift; | |||||||||||
1333 | ||||||||||||||
1334 | 5 | 9 | s! \\\\ !$g_escape_table{'\\'}!ogx; # Must process escaped backslashes first. | |||||||||||
1335 | 5 | 7 | s! \\` !$g_escape_table{'`'}!ogx; | |||||||||||
1336 | 5 | 7 | s! \\\* !$g_escape_table{'*'}!ogx; | |||||||||||
1337 | 5 | 5 | s! \\_ !$g_escape_table{'_'}!ogx; | |||||||||||
1338 | 5 | 8 | s! \\\{ !$g_escape_table{'{'}!ogx; | |||||||||||
1339 | 5 | 7 | s! \\\} !$g_escape_table{'}'}!ogx; | |||||||||||
1340 | 5 | 6 | s! \\\[ !$g_escape_table{'['}!ogx; | |||||||||||
1341 | 5 | 6 | s! \\\] !$g_escape_table{']'}!ogx; | |||||||||||
1342 | 5 | 13 | s! \\\( !$g_escape_table{'('}!ogx; | |||||||||||
1343 | 5 | 6 | s! \\\) !$g_escape_table{')'}!ogx; | |||||||||||
1344 | 5 | 6 | s! \\> !$g_escape_table{'>'}!ogx; | |||||||||||
1345 | 5 | 8 | s! \\\# !$g_escape_table{'#'}!ogx; | |||||||||||
1346 | 5 | 7 | s! \\\+ !$g_escape_table{'+'}!ogx; | |||||||||||
1347 | 5 | 6 | s! \\\- !$g_escape_table{'-'}!ogx; | |||||||||||
1348 | 5 | 6 | s! \\\. !$g_escape_table{'.'}!ogx; | |||||||||||
1349 | 5 | 6 | s{ \\! }{$g_escape_table{'!'}}ogx; | |||||||||||
1350 | ||||||||||||||
1351 | 5 | 13 | return $_; | |||||||||||
1352 | } | |||||||||||||
1353 | ||||||||||||||
1354 | sub _DoAutoLinks { | |||||||||||||
1355 | 3 | 3 | 5 | my ($self, $text) = @_; | ||||||||||
1356 | ||||||||||||||
1357 | #$text =~ s{<((https?|ftp):[^'">\s]+)>}{$1}gi; | |||||||||||||
1358 | #$text =~ s{<((https?|ftp):[^'">\s]+)>}{__podfmt(L => $1)}egi; | |||||||||||||
1359 | ||||||||||||||
1360 | # Email addresses: | |||||||||||||
1361 | 3 | 4 | $text =~ s{ | |||||||||||
1362 | < | |||||||||||||
1363 | (?:mailto:)? | |||||||||||||
1364 | ( | |||||||||||||
1365 | [-.\w\+]+ | |||||||||||||
1366 | \@ | |||||||||||||
1367 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ | |||||||||||||
1368 | ) | |||||||||||||
1369 | > | |||||||||||||
1370 | }{ | |||||||||||||
1371 | 0 | 0 | $self->_EncodeEmailAddress( $self->_UnescapeSpecialChars($1) ); | |||||||||||
1372 | }egix; | |||||||||||||
1373 | ||||||||||||||
1374 | 3 | 7 | return $text; | |||||||||||
1375 | } | |||||||||||||
1376 | ||||||||||||||
1377 | sub _EncodeEmailAddress { | |||||||||||||
1378 | # | |||||||||||||
1379 | # Input: an email address, e.g. "foo@example.com" | |||||||||||||
1380 | # | |||||||||||||
1381 | # Output: the email address as a mailto link, with each character | |||||||||||||
1382 | # of the address encoded as either a decimal or hex entity, in | |||||||||||||
1383 | # the hopes of foiling most address harvesting spam bots. E.g.: | |||||||||||||
1384 | # | |||||||||||||
1385 | # 1386 | # xample.com">foo | ||||||||||||
1387 | # @example.com | |||||||||||||
1388 | # | |||||||||||||
1389 | # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk | |||||||||||||
1390 | # mailing list: |
|||||||||||||
1391 | # | |||||||||||||
1392 | ||||||||||||||
1393 | 0 | 0 | 0 | my ($self, $addr) = @_; | ||||||||||
1394 | ||||||||||||||
1395 | my @encode = ( | |||||||||||||
1396 | 0 | 0 | 0 | sub { '' . ord(shift) . ';' }, | ||||||||||
1397 | 0 | 0 | 0 | sub { '' . sprintf( "%X", ord(shift) ) . ';' }, | ||||||||||
1398 | 0 | 0 | 0 | sub { shift }, | ||||||||||
1399 | 0 | 0 | ); | |||||||||||
1400 | ||||||||||||||
1401 | 0 | 0 | $addr = "mailto:" . $addr; | |||||||||||
1402 | ||||||||||||||
1403 | 0 | 0 | $addr =~ s{(.)}{ | |||||||||||
1404 | 0 | 0 | my $char = $1; | |||||||||||
1405 | 0 | 0 | 0 | if ( $char eq '@' ) { | ||||||||||
0 | ||||||||||||||
1406 | # this *must* be encoded. I insist. | |||||||||||||
1407 | 0 | 0 | $char = $encode[int rand 1]->($char); | |||||||||||
1408 | } | |||||||||||||
1409 | elsif ( $char ne ':' ) { | |||||||||||||
1410 | # leave ':' alone (to spot mailto: later) | |||||||||||||
1411 | 0 | 0 | my $r = rand; | |||||||||||
1412 | # roughly 10% raw, 45% hex, 45% dec | |||||||||||||
1413 | 0 | 0 | 0 | $char = ( | ||||||||||
0 | ||||||||||||||
1414 | $r > .9 ? $encode[2]->($char) : | |||||||||||||
1415 | $r < .45 ? $encode[1]->($char) : | |||||||||||||
1416 | $encode[0]->($char) | |||||||||||||
1417 | ); | |||||||||||||
1418 | } | |||||||||||||
1419 | 0 | 0 | $char; | |||||||||||
1420 | }gex; | |||||||||||||
1421 | ||||||||||||||
1422 | 0 | 0 | $addr = qq{$addr}; | |||||||||||
1423 | 0 | 0 | $addr =~ s{">.+?:}{">}; # strip the mailto: from the visible part | |||||||||||
1424 | ||||||||||||||
1425 | 0 | 0 | return $addr; | |||||||||||
1426 | } | |||||||||||||
1427 | ||||||||||||||
1428 | sub _UnescapeSpecialChars { | |||||||||||||
1429 | # | |||||||||||||
1430 | # Swap back in all the special characters we've hidden. | |||||||||||||
1431 | # | |||||||||||||
1432 | 3 | 3 | 7 | my ($self, $text) = @_; | ||||||||||
1433 | ||||||||||||||
1434 | 3 | 15 | while( my($char, $hash) = each(%g_escape_table) ) { | |||||||||||
1435 | 48 | 526 | $text =~ s/$hash/$char/g; | |||||||||||
1436 | } | |||||||||||||
1437 | 3 | 7 | return $text; | |||||||||||
1438 | } | |||||||||||||
1439 | ||||||||||||||
1440 | sub _TokenizeHTML { | |||||||||||||
1441 | # | |||||||||||||
1442 | # Parameter: String containing HTML markup. | |||||||||||||
1443 | # Returns: Reference to an array of the tokens comprising the input | |||||||||||||
1444 | # string. Each token is either a tag (possibly with nested, | |||||||||||||
1445 | # tags contained therein, such as , or a | |||||||||||||
1446 | # run of text between tags. Each element of the array is a | |||||||||||||
1447 | # two-element array; the first is either 'tag' or 'text'; | |||||||||||||
1448 | # the second is the actual value. | |||||||||||||
1449 | # | |||||||||||||
1450 | # | |||||||||||||
1451 | # Derived from the _tokenize() subroutine from Brad Choate's MTRegex plugin. | |||||||||||||
1452 | # |
|||||||||||||
1453 | # | |||||||||||||
1454 | ||||||||||||||
1455 | 6 | 6 | 8 | my ($self, $str) = @_; | ||||||||||
1456 | 6 | 7 | my $pos = 0; | |||||||||||
1457 | 6 | 8 | my $len = length $str; | |||||||||||
1458 | 6 | 6 | my @tokens; | |||||||||||
1459 | ||||||||||||||
1460 | 6 | 7 | my $depth = 6; | |||||||||||
1461 | 6 | 30 | my $nested_tags = join('|', ('(?:<[a-z/!$](?:[^<>]') x $depth) . (')*>)' x $depth); | |||||||||||
1462 | 6 | 196 | my $match = qr/(?s: ) | # comment | |||||||||||
1463 | (?s: <\? .*? \?> ) | # processing instruction | |||||||||||||
1464 | $nested_tags/iox; # nested tags | |||||||||||||
1465 | ||||||||||||||
1466 | 6 | 224 | while ($str =~ m/($match)/og) { | |||||||||||
1467 | 4 | 7 | my $whole_tag = $1; | |||||||||||
1468 | 4 | 7 | my $sec_start = pos $str; | |||||||||||
1469 | 4 | 5 | my $tag_start = $sec_start - length $whole_tag; | |||||||||||
1470 | 4 | 50 | 56 | if ($pos < $tag_start) { | ||||||||||
1471 | 4 | 15 | push @tokens, ['text', substr($str, $pos, $tag_start - $pos)]; | |||||||||||
1472 | } | |||||||||||||
1473 | 4 | 10 | push @tokens, ['tag', $whole_tag]; | |||||||||||
1474 | 4 | 21 | $pos = pos $str; | |||||||||||
1475 | } | |||||||||||||
1476 | 6 | 50 | 29 | push @tokens, ['text', substr($str, $pos, $len - $pos)] if $pos < $len; | ||||||||||
1477 | 6 | 38 | \@tokens; | |||||||||||
1478 | } | |||||||||||||
1479 | ||||||||||||||
1480 | sub _Outdent { | |||||||||||||
1481 | # | |||||||||||||
1482 | # Remove one level of line-leading tabs or spaces | |||||||||||||
1483 | # | |||||||||||||
1484 | 0 | 0 | 0 | my ($self, $text) = @_; | ||||||||||
1485 | ||||||||||||||
1486 | 0 | 0 | $text =~ s/^(\t|[ ]{1,$self->{tab_width}})//gm; | |||||||||||
1487 | 0 | 0 | return $text; | |||||||||||
1488 | } | |||||||||||||
1489 | ||||||||||||||
1490 | sub _Detab { | |||||||||||||
1491 | # | |||||||||||||
1492 | # Cribbed from a post by Bart Lateur: | |||||||||||||
1493 | # |
|||||||||||||
1494 | # | |||||||||||||
1495 | 3 | 3 | 6 | my ($self, $text) = @_; | ||||||||||
1496 | ||||||||||||||
1497 | # FIXME - Better anchor/regex would be quicker. | |||||||||||||
1498 | ||||||||||||||
1499 | # Original: | |||||||||||||
1500 | #$text =~ s{(.*?)\t}{$1.(' ' x ($self->{tab_width} - length($1) % $self->{tab_width}))}ge; | |||||||||||||
1501 | ||||||||||||||
1502 | # Much swifter, but pretty hateful: | |||||||||||||
1503 | 3 | 14 | do {} while ($text =~ s{^(.*?)\t}{$1.(' ' x ($self->{tab_width} - length($1) % $self->{tab_width}))}mge); | |||||||||||
0 | 0 | |||||||||||||
1504 | 3 | 8 | return $text; | |||||||||||
1505 | } | |||||||||||||
1506 | ||||||||||||||
1507 | sub _ConvertCopyright { | |||||||||||||
1508 | 3 | 3 | 5 | my ($self, $text) = @_; | ||||||||||
1509 | # Convert to an XML compatible form of copyright symbol | |||||||||||||
1510 | ||||||||||||||
1511 | 3 | 5 | $text =~ s/©/©/gi; | |||||||||||
1512 | ||||||||||||||
1513 | 3 | 6 | return $text; | |||||||||||
1514 | } | |||||||||||||
1515 | ||||||||||||||
1516 | 1; | |||||||||||||
1517 | ||||||||||||||
1518 | __END__ |