Text to HTML

Revision as of 20:10, 14 March 2020 by Thundergnat (talk | contribs) (Rename Perl 6 -> Raku, alphabetize, minor clean-up)

When developing a Website it is occasionally necessary to handle text that is received without formatting, and present it in a pleasing manner. to achieve this the text needs to be converted to HTML.

Text to HTML is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

Write a converter from plain text to HTML.

The plain text has no formatting information.

It may have centered headlines, numbered sections, paragraphs, lists, and URIs. It could even have tables.

Simple converters restrict themselves at identifying paragraphs, but i believe more can be done if the text is analyzed.

You are not requested to copy the algorithm from the existing solutions but use whatever faculties available in your language to best solve the problem.

The only requirement is to ensure that the result is valid xhtml.

Go

This isn't very sophisticated but does a few things in a simple-minded way. <lang go>package main

import (

   "fmt"
   "html"
   "regexp"
   "strings"

)

var t = ` Sample Text

This is an example of converting plain text to HTML which demonstrates extracting a title and escaping certain characters within bulleted and numbered lists.

  • This is a bulleted list with a less than sign (<)
  • And this is its second line with a greater than sign (>)

A 'normal' paragraph between the lists.

1. This is a numbered list with an ampersand (&)

2. "Second line" in double quotes

3. 'Third line' in single quotes

That's all folks.`

func main() {

   p := regexp.MustCompile(`\n\s*(\n\s*)+`)
   ul := regexp.MustCompile(`^\*`)
   ol := regexp.MustCompile(`^\d\.`)
   t = html.EscapeString(t) // escape <, >, &, " and '
   paras := p.Split(t, -1)
   // Assume if first character of first paragraph is white-space
   // then it's probably a document title.
   firstChar := paras[0][0]
   title := "Untitled"
   k := 0
   if firstChar == ' ' || firstChar == '\t' {
       title = strings.TrimSpace(paras[0])
       k = 1
   }
   fmt.Println("<html>")
   fmt.Printf("<head><title>%s</title></head>\n", title)
   fmt.Println("<body>")
   blist := false
   nlist := false
   for _, para := range paras[k:] {
       para2 := strings.TrimSpace(para)
       if ul.MatchString(para2) {
           if !blist {
               blist = true

fmt.Println("

    ") } para2 = strings.TrimSpace(para2[1:]) fmt.Printf("
  • %s
  • \n", para2) continue } else if blist { blist = false fmt.Println("

")

       }
       if ol.MatchString(para2) {
           if !nlist {
               nlist = true

fmt.Println("

    ") } para2 = strings.TrimSpace(para2[2:]) fmt.Printf("
  1. %s
  2. \n", para2) continue } else if nlist { nlist = false fmt.Println("

")

       }
       if !blist && !nlist {

fmt.Printf("

%s

\n", para2)

       }
   }
   if blist {

fmt.Println("")

   }
   if nlist {

fmt.Println("")

   }
   fmt.Println("</body>")
   fmt.Println("</html>")

}</lang>

Output:

<lang html><html> <head><title>Sample Text</title></head> <body>

This is an example of converting plain text to HTML which demonstrates extracting a title and escaping certain characters within bulleted and numbered lists.

  • This is a bulleted list with a less than sign (<)
  • And this is its second line with a greater than sign (>)

A 'normal' paragraph between the lists.

  1. This is a numbered list with an ampersand (&)
  2. "Second line" in double quotes
  3. 'Third line' in single quotes

That's all folks.

</body> </html></lang>

Phix

The best thing to do here is to keep it utterly trivial. <lang Phix>constant {hchars,hsubs} = columnize({{"&","&"},

                                    {"<","<"},
                                    {">",">"},
                                    {"\"","""},
                                    {"\'","'"}})

constant fmt = """ <html> <head><title>%s</title></head> <body>

%s

</body> </html> """

function text_to_html_page(string title, text)

   title = substitute_all(title,hchars,hsubs)
   text = substitute_all(text,hchars,hsubs)
   return sprintf(fmt,{title,text})

-- return substitute_all(sprintf(fmt,{title,text}),hchars,hsubs) end function

constant text = """

 This is
 a paragraph

     a block of
     code

 * A one-bullet list
   > With quoted text
   >
   >     and code

"""

puts(1,text_to_html_page("my title",text))</lang>

Output:

The last line of text_to_html() (as commented out) was used to generate the sanitised version of the output, as needed for inclusion on this page.

<html>
<head><title>my title</title></head>
<body>
<pre>
  This is
  a paragraph

      a block of
      code

  * A one-bullet list
    &gt; With quoted text
    &gt;
    &gt;     and code

</pre>
</body>
</html>

Pike

algorithm:

  • split by line
  • find average line length to identify centered lines
  • find isolated lines to identify section headings
  • find URIs
  • identify section numbering
  • identify bullet and numbered lists
  • identify paragraphs
  • identify indented lines
  • if possible identify tables

to ensure valid xhtml create a nested structure:

  • create an xml node
  • add elements to node
  • add lines to element if appropriate

this implementation is still incomplete. <lang Pike>// function to calculate the average line length (not used yet below) int linelength(array lines) {

   array sizes = sizeof(lines[*])-({0}); 
   sizes = sort(sizes); 
   // only consider the larger half of lines minus the top 5%
   array larger = sizes[sizeof(sizes)/2..sizeof(sizes)-sizeof(sizes)/20];
   int averagelarger = `+(@larger)/sizeof(larger);
   return averagelarger; 

}

array mark_up(array lines) {

   array markup = ({});
   // find special lines
   foreach(lines; int index; string line)
   {
       string strippedline = String.trim_whites(line);
       if (sizeof(strippedline))
       {
           string firstchar = strippedline[0..0];
           int pos = search(line, firstchar);
           if (lines[index-1]-" "-"\t" =="" && lines[index+1]-" "-"\t" =="")
               markup +=({ ({ "heading", strippedline, pos }) });
           else if (firstchar == "*")
               markup += ({ ({ "bullet", strippedline, pos }) });
           else if ( (<"0","1","2","3","4","5","6","7","8","9">)[firstchar] )
               markup += ({ ({ "number", strippedline, pos }) });
           else if (pos > 0)
               markup += ({ ({ "indent", strippedline, pos }) });
           else            
               markup += ({ ({ "regular", strippedline, pos }) });
       }
       else markup += ({ ({ "empty" }) });
   }
   foreach(markup; int index; array line)
   {
       if (index > 0 && index < sizeof(markup)-1 )
       {
           if (line[0] == "regular" && markup[index-1][0] != "regular" && markup[index+1][0] != "regular")
               line[0] = "heading";
       }
   }
   //find paragraphs
   foreach(markup; int index; array line)
   {
       if (index > 0 && index < sizeof(markup)-1 )
       {
           if (line[0] == "empty" && markup[index-1][0] == "regular" && markup[index+1][0] == "regular")
               line[0] = "new paragraph";
           else if (line[0] == "empty" && markup[index-1][0] == "regular" && markup[index+1][0] != "regular")
               line[0] = "end paragraph";
           else if (line[0] == "empty" && markup[index-1][0] != "regular" && markup[index+1][0] == "regular")
               line[0] = "begin paragraph";
       }
   }
   return markup;

}

object make_tree(array markup) {

   object root = Parser.XML.Tree.SimpleRootNode(); 
   object newline = Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_TEXT, "", ([]), "\n");
   array current = ({ Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_ELEMENT, "div", ([]), "") });
   root->add_child(current[-1]);
   foreach (markup; int index; array line)
   {
       switch(line[0])
       {
           case "heading": 
                     current[-1]->add_child(newline);
                     object h = Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_ELEMENT, "h3", ([]), "");
                     h->add_child(Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_TEXT, "", ([]), line[1]));
                     current[-1]->add_child(h);
                     current[-1]->add_child(newline);
                 break;
           case "bullet":
           case "number":
                     if (current[-1]->get_tag_name() == "li")
                         current = Array.pop(current)[1];
                     current[-1]->add_child(newline);
                     object li = Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_ELEMENT, "li", ([]), "");
                     li->add_child(Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_TEXT, "", ([]), line[1]));
                     current[-1]->add_child(li);
                     current = Array.push(current, li);
                 break;
           case "indent":
                     if (markup[index-1][0] != "bullet" && markup[index-1][0] != "number")
                         current = Array.pop(current)[1];
                     current[-1]->add_child(Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_TEXT, "", ([]), line[1]));
                 break;
           case "new paragraph":
                     current = Array.pop(current)[1];
                     current[-1]->add_child(newline);
           case "begin paragraph":
                     object p = Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_ELEMENT, "p", ([]), "");
                     current[-1]->add_child(p); 
                     current = Array.push(current, p);
                break;
           case "end paragraph":
                     current = Array.pop(current)[1];
                     current[-1]->add_child(newline);
                break;
           case "regular":           
                     current[-1]->add_child(Parser.XML.Tree.SimpleNode(Parser.XML.Tree.XML_TEXT, "", ([]), line[1]));
           case "empty": 
                 break;
       } 
   }   
   return root;

}</lang>

Racket

This task seems like it's very under-defined, but the discussion seems to be headed towards a simple markdown specification. I therefore do this with a small interface to cmark to render commonmark text.

(Note that this is not some cooked code, it's coming from code that I'm using to render class notes, and hopefully it will be useful to have such an example here. It certainly seems to me as a useful thing compared to some half-baked not-really-markdown-or-anything implementation.)

<lang racket>

  1. lang at-exp racket

(require ffi/unsafe ffi/unsafe/define)

(define-ffi-definer defcmark (ffi-lib "libcmark"))

(define _cmark_opts

 (_bitmask '(sourcepos = 1 hardbreaks = 2 normalize = 4 smart = 8)))

(define-cpointer-type _node) (defcmark cmark_markdown_to_html

 (_fun [bs : _bytes] [_int = (bytes-length bs)] _cmark_opts
       -> [r : _bytes] -> (begin0 (bytes->string/utf-8 r) (free r))))

(define (cmark-markdown-to-html #:options [opts '(normalize smart)] . text)

   (cmark_markdown_to_html (string->bytes/utf-8 (string-append* text)) opts))

(display @cmark-markdown-to-html{

 This is
 a paragraph
     a block of
     code
 * A one-bullet list
   > With quoted text
   >
   >     and code

}) </lang>

Output:
<p>This is
a paragraph</p>
<pre><code>a block of
code
</code></pre>
<ul>
<li>A one-bullet list
<blockquote>
<p>With quoted text</p>
<pre><code>and code
</code></pre>
</blockquote>
</li>
</ul>

Raku

(formerly Perl 6)

Works with: Rakudo version 2019.11

The task specs are essentially non-existent. "Make a best guess at how to render mark-up free text"? Anything that could be trusted at all would either be extremely trivial or insanely complex. And it shows by the the task example writers staying away in droves. Five examples after seven years!?

Rather than waste time on that noise, I'll demonstrate POD6 to HTML conversion. POD6 is a simple, text-only mark-up used for Perl 6 documentation. (It's Plain Old Documentation for perl 6) It uses pretty simple textual markup and has multiple tools to convert the POD6 document in to many other formats, HTML among them.

It is not markup free, but it is actually usable in production. <lang perl6>use Pod::To::HTML; use HTML::Escape;

my $pod6 = q:to/POD6/; =begin pod

A very simple Pod6 document.

This is a very high-level, hand-wavey overview. There are I<lots> of other options available.

=head1 Section headings

=head1 A top level heading

=head2 A second level heading

=head3 A third level heading

=head4 A fourth level heading

=head1 Text

Ordinary paragraphs do not require an explicit marker or delimiters.

Alternatively, there is also an explicit =para marker that can be used to explicitly mark a paragraph.

=para This is an ordinary paragraph. Its text will be squeezed and short lines filled.

=head1 Code

Enclose code in a =code block (or V<C< >> markup for short, inline samples)

=begin code

   my $name = 'Rakudo';
   say $name;

=end code

=head1 Lists

=head3 Unordered lists

=item Grumpy =item Dopey =item Doc =item Happy =item Bashful =item Sneezy =item Sleepy

=head3 Multi-level lists

=item1 Animal =item2 Vertebrate =item2 Invertebrate

=item1 Phase =item2 Solid =item2 Liquid =item2 Gas

=head1 Formatting codes

Formatting codes provide a way to add inline mark-up to a piece of text.

All Pod6 formatting codes consist of a single capital letter followed immediately by a set of single or double angle brackets; Unicode double angle brackets may be used.

Formatting codes may nest other formatting codes.

There are many formatting codes available, some of the more common ones:

=item1 V<B< >> Bold =item1 V<I< >> Italic =item1 V<U< >> Underline =item1 V<C< >> Code =item1 V<L< >> Hyperlink =item1 V<V< >> Verbatim (Don't interpret anything inside as POD markup)

=head1 Tables

There is quite extensive markup to allow rendering tables.

A simple example:

=begin table :caption<Mystery Men>

       The Shoveller   Eddie Stevens     King Arthur's singing shovel
       Blue Raja       Geoffrey Smith    Master of cutlery
       Mr Furious      Roy Orson         Ticking time bomb of fury
       The Bowler      Carol Pinnsler    Haunted bowling ball

=end table

=end pod POD6

  1. for display on Rosettacode

say escape-html render($pod6);

  1. normally
  2. say render($pod6);</lang>
Returns something like:
<!doctype html>
<html lang="en">
    <head>
        <title></title>
        <meta charset="UTF-8" />
        <style>
        kbd { font-family: "Droid Sans Mono", "Luxi Mono", "Inconsolata", monospace }
        samp { font-family: "Terminus", "Courier", "Lucida Console", monospace }
        u { text-decoration: none }
        .nested {
            margin-left: 3em;
        }
        aside, u { opacity: 0.7 }
        a[id^="fn-"]:target { background: #ff0 }
        </style>
        <link rel="stylesheet" href="//design.perl6.org/perl.css">

    </head>
    <body class="pod">
    <div id="___top"></div>

    <nav class="indexgroup">
<table id="TOC">
<caption><h2 id="TOC_Title">Table of Contents</h2></caption>
    <tr class="toc-level-1"><td class="toc-number">1</td><td class="toc-text"><a href="#Section_headings">Section headings</a></td></tr>
 <tr class="toc-level-1"><td class="toc-number">2</td><td class="toc-text"><a href="#A_top_level_heading">A top level heading</a></td></tr>
 <tr class="toc-level-2"><td class="toc-number">2.1</td><td class="toc-text"><a href="#A_second_level_heading">A second level heading</a></td></tr>
 <tr class="toc-level-3"><td class="toc-number">2.1.1</td><td class="toc-text"><a href="#A_third_level_heading">A third level heading</a></td></tr>
 <tr class="toc-level-4"><td class="toc-number">2.1.1.1</td><td class="toc-text"><a href="#A_fourth_level_heading">A fourth level heading</a></td></tr>
 <tr class="toc-level-1"><td class="toc-number">3</td><td class="toc-text"><a href="#Text">Text</a></td></tr>
    <tr class="toc-level-1"><td class="toc-number">4</td><td class="toc-text"><a href="#Code">Code</a></td></tr>
      <tr class="toc-level-1"><td class="toc-number">5</td><td class="toc-text"><a href="#Lists">Lists</a></td></tr>
 <tr class="toc-level-3"><td class="toc-number">5.0.1</td><td class="toc-text"><a href="#Unordered_lists">Unordered lists</a></td></tr>
        <tr class="toc-level-3"><td class="toc-number">5.0.2</td><td class="toc-text"><a href="#Multi-level_lists">Multi-level lists</a></td></tr>
        <tr class="toc-level-1"><td class="toc-number">6</td><td class="toc-text"><a href="#Formatting_codes">Formatting codes</a></td></tr>
           <tr class="toc-level-1"><td class="toc-number">7</td><td class="toc-text"><a href="#Tables">Tables</a></td></tr>
              
</table>
</nav>

    <div class="pod-body">
    <p>A very simple Pod6 document.</p>
<p>This is a very high-level, hand-wavey overview. There are <em>lots</em> of other options available.</p>
<h1 id="Section_headings"><a class="u" href="#___top" title="go to top of document">Section headings</a></h1>
<h1 id="A_top_level_heading"><a class="u" href="#___top" title="go to top of document">A top level heading</a></h1>
<h2 id="A_second_level_heading"><a class="u" href="#___top" title="go to top of document">A second level heading</a></h2>
<h3 id="A_third_level_heading"><a class="u" href="#___top" title="go to top of document">A third level heading</a></h3>
<h4 id="A_fourth_level_heading"><a class="u" href="#___top" title="go to top of document">A fourth level heading</a></h4>
<h1 id="Text"><a class="u" href="#___top" title="go to top of document">Text</a></h1>
<p>Ordinary paragraphs do not require an explicit marker or delimiters.</p>
<p>Alternatively, there is also an explicit =para marker that can be used to explicitly mark a paragraph.</p>
<p>This is an ordinary paragraph. Its text will be squeezed and short lines filled.</p>
<h1 id="Code"><a class="u" href="#___top" title="go to top of document">Code</a></h1>
<p>Enclose code in a =code block (or C&lt; &gt; markup for short, inline samples)</p>
<pre class="pod-block-code">    my $name = &#39;Rakudo&#39;;
    say $name;
</pre>
<h1 id="Lists"><a class="u" href="#___top" title="go to top of document">Lists</a></h1>
<h3 id="Unordered_lists"><a class="u" href="#___top" title="go to top of document">Unordered lists</a></h3>
<ul><li><p>Grumpy</p>
</li>
<li><p>Dopey</p>
</li>
<li><p>Doc</p>
</li>
<li><p>Happy</p>
</li>
<li><p>Bashful</p>
</li>
<li><p>Sneezy</p>
</li>
<li><p>Sleepy</p>
</li>
</ul>
<h3 id="Multi-level_lists"><a class="u" href="#___top" title="go to top of document">Multi-level lists</a></h3>
<ul><li><p>Animal</p>
</li>
<ul><li><p>Vertebrate</p>
</li>
<li><p>Invertebrate</p>
</li>
</ul>
<li><p>Phase</p>
</li>
<ul><li><p>Solid</p>
</li>
<li><p>Liquid</p>
</li>
<li><p>Gas</p>
</li>
</ul>
</ul>
<h1 id="Formatting_codes"><a class="u" href="#___top" title="go to top of document">Formatting codes</a></h1>
<p>Formatting codes provide a way to add inline mark-up to a piece of text.</p>
<p>All Pod6 formatting codes consist of a single capital letter followed immediately by a set of single or double angle brackets; Unicode double angle brackets may be used.</p>
<p>Formatting codes may nest other formatting codes.</p>
<p>There are many formatting codes available, some of the more common ones:</p>
<ul><li><p>B&lt; &gt; Bold</p>
</li>
<li><p>I&lt; &gt; Italic</p>
</li>
<li><p>U&lt; &gt; Underline</p>
</li>
<li><p>C&lt; &gt; Code</p>
</li>
<li><p>L&lt; &gt; Hyperlink</p>
</li>
<li><p>V&lt; &gt; Verbatim (Don&#39;t interpret anything inside as POD markup)</p>
</li>
</ul>
<h1 id="Tables"><a class="u" href="#___top" title="go to top of document">Tables</a></h1>
<p>There is quite extensive markup to allow rendering tables.</p>
<p>A simple example:</p>
<table class="pod-table">
<caption>Mystery Men</caption>
<tbody>
<tr> <td>The Shoveller</td> <td>Eddie Stevens</td> <td>King Arthur&#39;s singing shovel</td> </tr> <tr> <td>Blue Raja</td> <td>Geoffrey Smith</td> <td>Master of cutlery</td> </tr> <tr> <td>Mr Furious</td> <td>Roy Orson</td> <td>Ticking time bomb of fury</td> </tr> <tr> <td>The Bowler</td> <td>Carol Pinnsler</td> <td>Haunted bowling ball</td> </tr>
</tbody>
</table>
    </div>

    </body>
</html>

Tcl

This renderer doesn't do all that much. Indeed, it deliberately avoids doing all the complexity that is possible; instead it seeks to just provide the minimum that could possibly be useful to someone who is doing very simple text pages. <lang tcl>package require Tcl 8.5

proc splitParagraphs {text} {

   split [regsub -all {\n\s*(\n\s*)+} [string trim $text] \u0000] "\u0000"

} proc determineParagraph {para} {

   set para [regsub -all {\s*\n\s*} $para " "]
   switch -regexp -- $para {

{^\s*\*+\s} { return [list ul [string trimleft $para " \t*"]] } {^\s*\d+\.\s} { set para [string trimleft $para " \t\n0123456789"] set para [string range $para 1 end] return [list ol [string trimleft $para " \t"]] } {^#+\s} { return [list heading [string trimleft $para " \t#"]] }

   }
   return [list normal $para]

} proc markupParagraphContent {para} {

   set para [string map {& & < < > >} $para]
   regsub -all {_([\w&;]+)_} $para {\1} para
   regsub -all {\*([\w&;]+)\*} $para {\1} para
   regsub -all {`([\w&;]+)`} $para {\1} para
   return $para

}

proc markupText {title text} {

   set title [string map {& & < < > >} $title]
   set result "<html>"
   append result "<head><title>" $title "</title>\n</head>"

append result "<body>" "

$title

\n"

   set state normal
   foreach para [splitParagraphs $text] {

lassign [determineParagraph $para] type para set para [markupParagraphContent $para] switch $state,$type {

normal,normal {append result "

" $para "

\n"}

normal,heading {

append result "

" $para "

\n"

set type normal }

normal,ol {append result "

    " "
  1. " $para "
  2. \n"} normal,ul {append result "
    " "
  • " $para "
  • \n"} ul,normal {append result "
" "

" $para "

\n"}

ul,heading {

append result "" "

" $para "

\n"

set type normal }

ul,ol {append result "" "
    " "
  1. " $para "
  2. \n"} ul,ul {append result "
  3. " $para "
  4. \n"} ol,normal {append result "
" "

" $para "

\n"}

ol,heading {

append result "

" "

" $para "

\n"

set type normal }

ol,ol {append result "

  • " $para "
  • \n"} ol,ul {append result "" "

      " "
    • " $para "
    • \n"}

      } set state $type

       }
       if {$state ne "normal"} {
    

    append result "</$state>"

       }
       return [append result "</body></html>"]
    

    }</lang> Here's an example of how it would be used. <lang tcl>set sample " This is an example of how a pseudo-markdown-ish formatting scheme could work. It's really much simpler than markdown, but does support a few things.

    1. Block paragraph types
    • This is a bulleted list
    • And this is the second item in it

    1. Here's a numbered list

    2. Second item

    3. Third item

    1. Inline formatting types

    The formatter can render text with _italics_, *bold* and in a `typewriter` font. It also does the right thing with <angle brackets> and &ersands, but relies on the encoding of the characters to be conveyed separately."

    puts [markupText "Sample" $sample]</lang>

    Output:

    <lang html><html><head><title>Sample</title>

    </head><body>

    Sample

    This is an example of how a pseudo-markdown-ish formatting scheme could work. It's really much simpler than markdown, but does support a few things.

    Block paragraph types

    • This is a bulleted list
    • And this is the second item in it
    1. Here's a numbered list
    2. Second item
    3. Third item

    Inline formatting types

    The formatter can render text with italics, bold and in a typewriter font. It also does the right thing with <angle brackets> and &amp;ersands, but relies on the encoding of the characters to be conveyed separately.

    </body></html></lang>