From ee46374edfb377f57169f2b1de9afd19bf052da2 Mon Sep 17 00:00:00 2001 From: fukachan Date: Thu, 18 Oct 2001 08:32:16 +0000 Subject: Initial revision --- cpan/lib/HTML/FromText.pm | 736 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 736 insertions(+) create mode 100644 cpan/lib/HTML/FromText.pm (limited to 'cpan/lib/HTML') diff --git a/cpan/lib/HTML/FromText.pm b/cpan/lib/HTML/FromText.pm new file mode 100644 index 00000000..2b7f1753 --- /dev/null +++ b/cpan/lib/HTML/FromText.pm @@ -0,0 +1,736 @@ +require 5.004; +use strict; + +package HTML::FromText; +use Carp; +use Exporter; +use Text::Tabs 'expand'; +use vars qw($RCSID $VERSION $QUIET @EXPORT @ISA); + +@ISA = qw(Exporter); +@EXPORT = qw(text2html); +$RCSID = q$Id: FromText.pm,v 1.14 1999/10/06 10:53:37 garethr Exp $; +$VERSION = '1.005'; +$QUIET = 0; + +# This list of protocols is taken from RFC 1630: "Universal Resource +# Identifiers in WWW". The protocol "file" is omitted because +# experience suggests that it results in many false positives; "https" +# postdates RFC 1630. The protocol "mailto" is handled separately, by +# the email address matching code. + +my $protocol = join '|', + qw(afs cid ftp gopher http https mid news nntp prospero telnet wais); + +# The regular expressions matching email addresses use the following +# syntax elements from RFC 822. I can't use the full details of +# structured field bodies, because that would give too many false +# positives. (See Tom Christiansen's ckaddr.gz for a full +# implementation of the RFC 822.) +# +# addr-spec = local-part "@" domain +# local-part = word *("." word) +# word = atom +# domain = sub-domain *("." sub-domain) +# sub-domain = domain-ref +# domain-ref = atom +# atom = 1* +# specials = "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" +# / <"> / "." / "[" / "]" +# +# I have ignored quoting, domain literals and comments. +# +# Note that '&' can legally appear in email addresses (for example, +# 'fred&barney@stonehenge.com'). If the 'metachars' option is passed to +# text2html then I must use '&' to recognize '&'. Thus the regular +# expression $atom[0] recognizes an atom in the case where the option +# 'metachars' is false; $atom[1] recognizes an atom in the case where +# 'metachars' is true. Similarly for the regular expressions $email[0] +# and $email[1], which recognize email addresses. + +my @atom = + ( '[!#$%&\'*+\\-/0123456789=?ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}~]+', + '(?:&|[!#$%\'*+\\-/0123456789=?ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}~])+' ); + +my @email = ( "$atom[0](\\.$atom[0])*\@$atom[0](\\.$atom[0])*", + "$atom[1](\\.$atom[1])*\@$atom[1](\\.$atom[1])*" ); + +my @alignments = ( '', '', ' ALIGN="RIGHT"', ' ALIGN="CENTER"' ); + +sub string2html ($$) { + my $options = $_[1]; + for ($_[0]) { # Modify in-place. + + # METACHARS: mark up HTML metacharacters as corresponding entities. + if ($options->{metachars}) { + s/&/&/g; + s//>/g; + s/\"/"/g; + } + + # EMAIL, URLS: spot electronic mail addresses and turn them into + # links. Note (1) if `urls' is set but not `email', then only + # addresses prefixed by `mailto:' will be marked up; (2) that we leave + # the `mailto:' prefix in the anchor text. + if ($options->{email} or $options->{urls}) { + s|((?:mailto:)?)($email[$options->{metachars}?1:0])| + ($options->{email} or $1) + ? "$1$2" : $2|gex; + } + + # URLS: mark up URLs as links (note that `mailto' links are handled + # above). + if ($options->{urls}) { + s|\b((?:$protocol):\S+[\w/])|$1|g; + } + + # BOLD: mark up words in *asterisks* as bold. + if ($options->{bold}) { + s#(^|\s)\*([^*]+)\*(?=\s|$)#$1$2#g; + } + + # UNDERLINE: mark up words in _underscores_ as underlined. + if ($options->{underline}) { + s#(^|\s)_([^_]+?)_(?=\s|$)#$1$2#g; + } + } + + return $_[0]; +} + +sub text2html { + local $_ = shift; # Take a copy; don't modify in-place. + return $_ unless $_; + + my %options = ( metachars => 1, @_ ); + + # Check options for sanity. + unless ($QUIET) { + carp "text2html: `spaces' will be ignored since `lines' is not specified" + if $options{spaces} and not $options{lines}; + if ($options{paras}) { + if ($options{blockparas}) { + foreach my $o (qw(blockquotes blockcode)) { + carp "text2html: `$o' will be ignored since `blockparas' is specified" if $options{$o}; + } + } elsif ($options{blockcode} and $options{blockquotes}) { + carp "text2html: `blockquotes' will be ignored since `blockcode' is specified"; + } + } else { + foreach my $o (qw(bullets numbers blockquotes blockparas blockcode + title headings tables)) { + carp "text2html: `$o' will be ignored since `paras' is not specified" + if $options{$o}; + } + } + } + + # Expand tabs. + $_ = join "\n", expand(split /\r?\n/); + + # PRE: put text in
 element.
+  if ($options{pre}) {
+    string2html($_, \%options);
+    s|^|
|;
+    s|$|
|; + } + + # LINES: preserve line breaks from original text. + elsif ($options{lines}) { + string2html($_, \%options); + s/\n/
\n/gm; + + # SPACES: preserve spaces from original text. + s/ / /g if $options{spaces}; + } + + # PARAS: treat text as sequence of paragraphs. + elsif ($options{paras}) { + my @paras; + + # Remove initial and final blank lines. + s/^(?:\s*?\n)+//; + s/(?:\n\s*?)+$//; + + # Split on a different regexp depending on what kinds of paragraphs + # will be recognised later. The idea is that bulleted lists like + # this: + # + # * item 1 + # * item 2 + # + # will be recognised as multiple paragraphs if the 'bullets' option + # is supplied, but as a single paragraph otherwise. (Similarly for + # numbered lists). + if ($options{bullets} and $options{numbers}) { + @paras = split + /(?:\s*\n)+ # (0 or more blank lines, followed by LF) + (?:\s*\n # Either 1 or more blank lines, or + |(?=\s*[*-]\s+ # bulleted item follows, or + |\s*(?:\d+)[.\)\]]?\s+)) # numbered item follows + /x; + } elsif ($options{bullets}) { + @paras = split + /(?:\s*\n)+ # (0 or more blank lines, followed by LF) + (?:\s*\n # Either 1 or more blank lines, or + |(?=\s*[*-]\s+)) # bulleted item follows. + /x; + } elsif ($options{numbers}) { + @paras = split + /(?:\s*\n)+ # (0 or more blank lines, followed by LF) + (?:\s*\n # Either 1 or more blank lines, or + |(?=\s*(?:\d+)[.\)\]]?\s+)) # numbered item follows. + /x; + } else { + @paras = split + /\s*\n(?:\s*\n)+ # 1 or more blank lines. + /x; + } + + my $last = ''; # List type (OL/UL) of last paragraph + my $this; # List type (OL/UL) of this paragraph + my $first = 1; # True if this is first paragraph + + foreach (@paras) { + my (@rows,@starts,@ends); + $this = ''; + + # TITLE: mark up first paragraph as level-1 heading. + if ($options{title} and $first) { + string2html($_,\%options); + s|^|

|; + s|$|

|; + } + + # HEADINGS: mark up paragraphs with numbers at the start of the + # first line as headings. + elsif ($options{headings} and /^(\d+(\.\d+)*)\.?\s/) { + my $number = $1; + my $level = 1 + ($number =~ tr/././); + $level = 6 if $level > 6; + string2html($_,\%options); + s|^||; + s|$||; + } + + # BULLETS: mark up paragraphs starting with bullets as items in an + # unnumbered list. + elsif ($options{bullets} and /^\s*[*-]\s+/) { + string2html($_,\%options); + s/^\s*[*-]\s+/
  • /; + s|$|

    |; + $this = 'UL'; + } + + # NUMBERS: mark up paragraphs starting with numbers as items in a + # numbered list. + elsif ($options{numbers} and /^\s*(\d+)[.\)\]]?\s+/) { + string2html($_,\%options); + s/^\s*(\d+)[.\)\]]?\s+/
  • /; + s|$|

    |; + $this = 'OL'; + } + + # TABLES: spot and mark up tables. We combine the lines of the + # paragraph using the string bitwise or (|) operator, the result + # being in $spaces. A character in $spaces is a space only if + # there was a space at that position in every line of the + # paragraph. $space can be used to search for contiguous spaces + # that occur on all lines of the paragraph. If this results in at + # least two columns, the paragraph is identified as a table. + # + # Note that this option appears before the various 'blockquotes' + # options because a table may well have whitespace to the left, in + # which case it must not be incorrectly recognised as a + # blockquote. + elsif ($options{tables} and do { + @rows = split /\n/, $_; + my $spaces; + my $max = 0; + my $min = length; + foreach my $row (@rows) { + ($spaces |= $row) =~ tr/ /\xff/c; + $min = length $row if length $row < $min; + $max = length $row if $max < length $row; + } + $spaces = substr $spaces, 0, $min; + push(@starts, 0) unless $spaces =~ /^ /; + while ($spaces =~ /((?:^| ) +)(?=[^ ])/g) { + push @ends, pos($spaces) - length $1; + push @starts, pos($spaces); + } + shift(@ends) if $spaces =~ /^ /; + push(@ends, $max); + + # Two or more rows and two or more columns indicate a table. + 2 <= @rows and 2 <= @starts + }) { + # For each column, guess whether it should be left, centre or + # right aligned by examining all cells in that column for space + # to the left or the right. A simple majority among those cells + # that actually have space to one side or another decides (if no + # alignment gets a majority, left alignment wins by default). + + my @align; + foreach my $col (0 .. $#starts) { + my @count = (0, 0, 0, 0); + foreach my $row (@rows) { + my $width = $ends[$col] - $starts[$col]; + my $cell = substr $row, $starts[$col], $width; + ++ $count[($cell =~ /^ / ? 2 : 0) + + ($cell =~ / $/ || length($cell) < $width ? 1 : 0)]; + } + $align[$col] = 0; + my $population = $count[1] + $count[2] + $count[3]; + foreach (1 .. 3) { + if ($count[$_] * 2 > $population) { + $align[$col] = $_; + last; + } + } + } + + foreach my $row (@rows) { + $row = join '', '', (map { + my $cell = substr $row, $starts[$_], $ends[$_] - $starts[$_]; + $cell =~ s/^ +//; + $cell =~ s/ +$//; + string2html($cell,\%options); + ('', $cell, '') + } 0 .. $#starts), ''; + } + my $tag = $starts[0] == 0 ? 'P' : 'BLOCKQUOTE'; + $_ = join "\n", "<$tag>", @rows, "
    "; + } + + # BLOCKPARAS, BLOCKCODE, BLOCKQUOTES: mark up indented paragraphs + # as block quotes of various kinds. + elsif (($options{blockparas} or $options{blockquotes} + or $options{blockcode}) and /^(\s+).*(?:\n\1.*)*$/) { + string2html($_,\%options); + + # Every line in the paragraph starts with at white space, the common + # whitespace being in $1. Remove the common initial whitespace, + s/^$1//gm; + + # BLOCKPARAS: treat as a paragraph. + if ($options{blockparas}) { + s|^|

    |; + s|$|

    |; + } + + # BLOCKCODE, BLOCKQUOTES: preserve line breaks. + else { + s/\n/
    \n/gm; + + # BLOCKCODE: preserve spaces, use fixed-width font. + if ($options{blockcode}) { + s| | |g; + s|^||; + s|$||; + } + } + s|^|
    |; + s|$|
    |; + } + + # Didn't match any of the above, so just an ordinary paragraph. + else { + string2html($_,\%options); + s|^|

    |; + s|$|

    |; + } + + # Insert
      ,
    ,
      or
    if this paragraph belongs to a + # different list type than the previous one. + if ($this ne $last) { + s|^|<$this>| if ($this ne ''); + s|^|| if ($last ne ''); + } + $last = $this; + $first = 0; + } + if ($this ne '') { + push @paras, ""; + } + $_ = join "\n", @paras; + } + + # None of PRE, LINES, PARAS specified: apply basic transformations. + else { + string2html($_,\%options); + } + return $_; +} + +1; + +__END__ + +=head1 NAME + +HTML::FromText - mark up text as HTML + +=head1 SYNOPSIS + + use HTML::FromText; + print text2html($text, urls => 1, paras => 1, headings => 1); + +=head1 DESCRIPTION + +The C function marks up plain text as HTML. By default it +expands tabs and converts HTML metacharacters into the corresponding +entities. More complicated transformations, such as splitting the text +into paragraphs or marking up bulleted lists, can be carried out by +setting the appropriate options. + +=head1 SUMMARY OF OPTIONS + +These options always apply: + + metachars Convert HTML metacharacters to entity references + urls Convert URLs to links + email Convert email addresses to links + bold Mark up words with *asterisks* in bold + underline Mark up words with _underscores_ as underlined + +You can then choose to treat the text according to one of these options: + + pre Treat text as preformatted + lines Treat text as line-oriented + paras Treat text as paragraph-oriented + +(If more than one of these is specified, C
     takes precedence over
    +C which takes precedence over C.)  The following option
    +applies when the C option is specified:
    +
    +    spaces       Preserve spaces from the original text
    +
    +The following options apply when the C option is specified:
    +
    +    blockparas   Mark up indented paragraphs as block quote
    +    blockquotes  Ditto, also preserve lines from original
    +    blockcode    Ditto, also preserve spaces from original
    +    bullets      Mark up bulleted paragraphs as unordered list
    +    headings     Mark up headings
    +    numbers      Mark up numbered paragraphs as ordered list
    +    tables       Mark up tables
    +    title        Mark up first paragraph as level 1 heading
    +
    +C will issue a warning if it is passed nonsensical options,
    +for example C but not C.  These warnings can be
    +supressed by setting $HTML::FromText::QUIET to true.
    +
    +=head1 OPTIONS
    +
    +=over 4
    +
    +=item blockparas
    +
    +=item blockquotes
    +
    +=item blockcode
    +
    +These options cause to C to spot paragraphs where every line
    +begins with whitespace, and mark them up as block quotes.  If more than
    +one of these options is specified, C takes precedence over
    +C, which takes precedence over C.  All three
    +options are ignored unless the C option is also set.
    +
    +The C option marks up the paragraph as a block quote with no
    +other changes.  For example,
    +
    +    Turing wrote,
    +
    +        I propose to consider the question,
    +        "Can machines think?"
    +
    +becomes
    +
    +    

    Turing wrote,

    +
    I propose to consider the question, + "Can machines think?"
    + +The C option preserves line breaks in the original text. +For example, + + From "The Waste Land": + + Phlebas the Phoenecian, a fortnight dead, + Forgot the cry of gulls, and the deep sea swell + +becomes + +

    From "The Waste Land":

    +
    Phlebas the Phoenecian, a fortnight dead,
    + Forgot the cry of gulls, and the deep sea swell
    + +The C option preserves line breaks and spaces in the original +text and renders the paragraph in a fixed-width font. For example: + + Here's how to output numbers with commas: + + sub commify { + local $_ = shift; + 1 while s/^(-?\d+)(\d{3})/$1,$2/; + $_; + } + +becomes + +

    Here's how to output numbers with commas:

    +
    sub commify {
    +   local $_ = shift;
    +   1 while s/^(-?\d+)(\d{3})/$1,$2/;
    +   $_;
    + }
    + +=item bold + +Words surrounded with asterisks are marked up in bold, so C<*abc*> +becomes CBEabcE/BE>. + +=item bullets + +Spots bulleted paragraphs (beginning with optional whitespace, an +asterisk or hyphen, and whitespace) and marks them up as an unordered +list. Bulleted paragraphs don't have to be separated by blank lines. +For example, + + Shopping list: + + * apples + * pears + +becomes + +

    Shopping list:

    +
    • apples

      +
    • pears

      +
    + +This option is ignored unless the C option is set. + +=item email + +Spots email addresses in the text and converts them to links. For example + + Mail me at web@perl.com. + +becomes + + Mail me at web@perl.com. + +=item headings + +Spots headings (paragraphs starting with numbers) and marks them up as +headings of the appropriate level. For example, + + 1. Introduction + + 1.1 Background + + 1.1.1 Previous work + + 2. Conclusion + +becomes + +

    1. Introduction

    +

    1.1 Background

    +

    1.1.1 Previous work

    +

    2. Conclusion

    + +This option is ignored unless the C option is set. + +=item lines + +Formats the text so as to preserve line breaks. For example, + + Line 1 + Line 2 + +becomes + + Line 1
    + Line 2 + +If two or more of the options C
    , C and C are set,
    +then C
     takes precedence over C, which takes precedence over
    +C.
    +
    +=item metachars
    +
    +Converts HTML metacharacters into their corresponding entity references.
    +Ampersand (C>) becomes Camp;>, less than (C>)
    +becomes Clt;>, greater than (C>) becomes Cgt;>, and
    +quote (") becomes Cquot;>.  This option is 1 by default.
    +
    +=item numbers
    +
    +Spots numbered paragraphs (beginning with whitespace, digits, an
    +optional period/parenthesis/bracket, and whitespace) and marks them up
    +as an ordered list.  Numbered paragraphs don't have to be separated by
    +blank lines.  For example,
    +
    +    To do:
    +
    +       1. Write thesis
    +       2. Submit it
    +       3. Celebrate
    +
    +becomes
    +
    +    

    To do:

    +
    1. Write thesis

      +
    2. Submit it

      +
    3. Celebrate

      +
    + +This option is ignored unless the C option is set. + +=item paras + +Format the text into paragraphs. Paragraphs are separated by one or +more blank lines. For example, + + Paragraph 1 + + Paragraph 2 + +becomes + +

    Paragraph 1

    +

    Paragraph 2

    + +If two or more of the options C
    , C and C are set,
    +then C
     takes precedence over C, which takes precedence over
    +C.
    +
    +=item pre
    +
    +Wrap the whole input in a CPREE> element.  For example,
    +
    +    preformatted
    +    text
    +
    +becomes
    +
    +    
    preformatted
    +    text
    + +If two or more of the options C
    , C and C are set,
    +then C
     takes precedence over C, which takes precedence over
    +C.
    +
    +=item spaces
    +
    +Preserves spaces throughout the text.  For example,
    +
    +    Line 1
    +     Line  2
    +      Line   3
    +
    +becomes
    +
    +    Line 1
    +  Line  2
    +   Line   3 + +This option is ignored unless the C option is set. + +=item tables + +Spots tables and marks them up appropriately. Columns must be separated +by two or more spaces (this prevents accidental incorrect recognition of +a paragraph where interword spaces happen to line up). If there are two +or more rows in a paragraph and all rows share the same set of (two or +more) columns, the paragraph is assumed to be a table. For example + + -e File exists. + -z File has zero size. + -s File has nonzero size (returns size). + +becomes + +

    + + + +
    -eFile exists.
    -zFile has zero size.
    -sFile has nonzero size (returns size).

    + +C guesses for each column whether it is intended to be left, +centre or right aligned. + +This option is ignored unless the C option is set. + +=item title + +Formats the first paragraph of the text as a first-level heading. +For example, + + Paragraph 1 + + Paragraph 2 + +becomes + +

    Paragraph 1

    +

    Paragraph 2

    + +This option is ignored unless the C option is set. + +=item underline + +Words surrounded with underscores are marked up with underline, so C<_abc_> +becomes CUEabcE/UE>. + +=item urls + +Spots Uniform Resource Locators (URLs) in the text and converts them +to links. For example + + See https://perl.com/. + +becomes + + See https://perl.com/. + +=back + +=head1 SEE ALSO + +The C module (part of the LWP package) provides +functions for encoding and decoding HTML entities. + +Tom Christiansen has a complete implementation of RFC 822 structured +field bodies. See +C. + +Seth Golub's C utility does everything that C +does, and a few things that it would like to do. See +C. + +RFC 822: "Standard for the Format of ARPA Internet Text Messages" +describes the syntax of email addresses (the more esoteric features of +structured field bodies, in particular quoted-strings, domain literals +and comments, are not recognized by C). See +C. + +RFC 1630: "Universal Resource Identifiers in WWW" lists the protocols +that may appear in URLs. C also recognizes "https:", +but ignores "file:" because experience suggests that it results in too +many false positives. See C. + +=head1 AUTHOR + +Gareth Rees Cgarethr@cre.canon.co.ukE>. + +=head1 COPYRIGHT + +Copyright (c) 1999 Canon Research Centre Europe. All rights reserved. +This module is free software; you can redistribute it and/or modify it +under the same terms as Perl itself. + +=cut -- cgit v1.2.1