<?php

/*
 * Simple (X)HTML Prototype
 *
 * (c) Copyright Thiemo Mättig (http://maettig.com/)
 * Any commercial use is stricly forbidden!
 * Contact me first: http://maettig.com/email
 *
 * HISTORY
 * - 2003-10-11
 *   - Anchor/link detection does not match ", ], < etc. any more (in
 *     JavaScript's HTML and in SimpleHTML::_callback).
 *   - Minimum auto-detected URL length is "www.12.34" or "http://12.34".
 *   - Regex are using <[^>]*> (prevents backtracking) instead of <[^>]*.
 *   - HTML() does nothing if "Cancel" in one of the prompt()'s was clicked.
 * - 2003-09-29
 *   - Improved the anchor/link detection a lot.
 *   - onload="focus()" added.
 * - 2003-09-18
 *   - Several changes in JavsScript function HTML().
 * - 2003-09-10
 *   - <tt> is an alias for <code> (similar to <b> and <i>).
 *   - Form uses POST now.
 *   - Removed $_SERVER for compatibility to PHP <4.1.0.
 *   - Fixed: Indention in first line was cropped by one space.
 * - 2003-09-09
 *   - Initial version.
 *
 * KNOWN BUGS/TODO
 * - <h1>, <h2> etc. is missing. Needed?
 * - Special characters are shown as "?" or as HTML entities, e.g. &#1234;.
 *   Maybe use Unicode? UTF-8?
 * - "<em>a\n\na</em>" becomes "<p><em>a</p>\n\n<p>a</em></p>" - wrong nested
 *   tags! Possible solution: Don't use <p>, use <br /> only. Or use a
 *   conditional executive Regex (e.g. <(.*)>.*</\1>|<br>{2,}).
 * - Wrong nested tags should cause an error, but they do not. Example:
 *   "<em>a <strong>a</em> a</strong>".
 */

// Show source if requested.
if (isset($_GET['source']) || isset($source))
{
    
highlight_file(__FILE__);
    die();
}

//-----------------------------------------------------------------------------

/**
 * Simple (X)HTML document.
 *
 * @author Thiemo Mättig (http://maettig.com/)
 */
class SimpleHTML
{
    
/**
     * @var string
     */
    
var $text "";

    
/**
     * @var array
     */
    
var $allowedTags = array(
        
'(a)(\s+href\s*=.*?)',
        
'(em)()',
        
'(strong)()',
        
'(code)()',
        
'(i)()',
        
'(b)()',
        
'(tt)()'
    
);

    
/**
     * @param string $text Simple (X)HTML source.
     */
    
function SimpleHTML($text "")
    {
        
$this->text $text;
    }

    
/**
     * @param string $text Simple (X)HTML source.
     */
    
function display($text "")
    {
        echo 
$this->toHTML($text);
    }

    
/**
     * @param string $text Simple (X)HTML source.
     */
    
function toHTML($text "")
    {
        if (! 
$text$text $this->text;

        
// Transform Windows/Mac/whatever line breaks to simple "\n".
        
$text preg_replace('{\s*?\n\r*|\s*?\r}'"\n"$text);

        
// Crop loooooooooooooooooooooooooooooooooooooooooooooooooooooong crap.
        
$text preg_replace('{(\S)(\1{3})\1*}''\2'$text);

        
// Detect links/anchors even if they are not in <a href>.
        
$text preg_replace('{
            (?:<|&(?:amp;)*lt;)a\s+href\s*=.*?(?:>|&(?:amp;)*gt;).*?(?:<|&(?:amp;)*lt;)/a(?:>|&(?:amp;)*gt;)
            |
            \b(http://|(www\.))([^\s"\'<>\[\]\\\]{4,}[\w/=])
            }eisx'
,
            
"str_replace('\\\"', '\"', '\\1' ? '<a href=\"http://\\2\\3\">\\0</a>' : '\\0')",
            
$text);

        
// Replace any special character with its HTML entity.
        //- $text = htmlspecialchars($text);
        
$text str_replace('&''&amp;'$text);
        
$text str_replace('<''&lt;'$text);
        
$text str_replace('>''&gt;'$text);

        
// Process a Regex and a callback function for any tag allowed.
        
foreach ($this->allowedTags as $allowedTag)
        {
            
$text preg_replace_callback('{
                (&(?:amp;)*lt;)
                ' 
$allowedTag '
                (&(?:amp;)*gt;)
                \s*(.*?)\s*
                (&(?:amp;)*lt;)
                /\2
                (&(?:amp;)*gt;)
                }isx'
,
                array(
'SimpleHTML''_callback'),
                
$text);
        }

        
// Replace double quotes with &raquo;...&laquo; (German style).
        // See http://maettig.com/1058945580
        
$text preg_replace(
            
'{<(code|head|pre|script)\b.+?</\1>|<[^>]*>|(?<!\w)"(([^"<]|<[^>]*>)+)"(?!\w)}esS',
            
"str_replace('\\\"', '\"', '\\2' ? '&raquo;\\2&laquo;' : '\\0')",
            
$text);

        
// Replace remaining double quotes with their HTML entities.
        //- $text = str_replace('"', '&quot;', $text);

        // Replace " - " and " -- " with the n-dash (German style).
        // See http://maettig.com/1058945580
        
$text preg_replace(
            
'!<(code|head|pre|script)\b.+?</\1>|<[^>]*>|(?<= )(-{1,2})(?= )!esS',
            
"str_replace('\\\"', '\"', '\\2' ? '&ndash;' : '\\0')",
            
$text);

        
// Preserve multiple vertical white spaces.
        
$text preg_replace('/(^|\s)[ \t]/mS''\1&nbsp;'$text);

        
// Replace any "\n" with "<br />\n".
        
$text nl2br($text);
        
// Transform multiple line breaks to paragraphs.
        
$text preg_replace('{(<br\s*/?>\n){2,}}is'"</p>\n\n<p>"$text);
        
$text "<p>" $text "</p>";

        
// HTML source beautification (not needed).
        
$text wordwrap($text);

        return 
$text;
    }

    
/**
     * @param array $matches Regex matches.
     * @access private
     */
    
function _callback($match)
    {
        
// Get inverted htmlspecialchars() table.
        
$trans array_flip(get_html_translation_table(HTML_SPECIALCHARS));
        
$isEscaped false;
        foreach (array(
1467) as $i)
        {
            
// Un-escape double-escaped entities only in allowed tags.
            
if (strlen($match[$i]) > 4)
            {
                
$isEscaped true;
                
$match[$i] = strtr($match[$i], $trans);
            }
        }
        if (
$isEscaped)
        {
            
// Return single-escaped value (similar to htmlspecialchars()).
            
return $match[1] . $match[2] . $match[3] . $match[4] . $match[5] . $match[6] . "/" $match[2] . $match[7];
        }
        else
        {
            
// Un-escape anything in single-escaped tags.
            
$match[3] = strtr($match[3], $trans);
            
// Normalize <a href='a'>, <a href=a> and <a href="a" invalid="a">.
            
$match[3] = preg_replace('{
                (\b\w+)\s*=\s*
                (
                "([^"]*).*
                |
                \'([^\']*).*
                |
                (\S*).*
                )
                }sx'
,
                
'\1="\3\4\5"',
                
$match[3]);
            
// <b> and <i> are deprecated and will be replaced.
            
if ($match[2] == "b"$match[2] = "strong";
            if (
$match[2] == "i"$match[2] = "em";
            if (
$match[2] == "tt"$match[2] = "code";
            return 
"<" $match[2] . $match[3] . ">" $match[5] . "</" $match[2] . ">";
        }
    }
}

//-----------------------------------------------------------------------------

// Fetch submitted text if the form was submitted before.
if (isset($_REQUEST['text'])) $text $_REQUEST['text'];
if (isset(
$text)) $text stripslashes($text);

// Default example text.
if (empty($text)) $text 'Links: <a href="http://maettig.com">valid</a>, <a href=http://maettig.com>short</a>, <a href="http://maettig.com" target="_blank">attributes are croped</a>, <a onclick="alert(\'!\')">invalid</a>. Auto-detected links: http://maettig.com, www.maettig.com.

Allowed inline markup: <em>emphatic</em>, <strong>strong</strong>, <code>code</code>. For compatibility reasons: <i>i</i>=em, <b>b</b>=strong, <tt>tt</tt>=code. Not allowed: <small>a</small>, <em style="color:red">a</em> and so on.

<code>
function noNeedToUsePre() {
    while ($i--)
        echo "<td>&nbsp;</td>";
}
</code>

     Indention is        p o  s   s    i     b      l       e.
Flooooooooooooooooooooooooooooooooooooooding is not.

&lt;em>Allowed but escaped</em>, <em>same</em&gt;, &amp;lt;em&gt;double-escaped&lt;/em&gt;.
Same goes for &lt;a href=...>a</a> but not for &lt;small>invalid tags</small> and not for &lt; entities.

Nice "quotes" (German newspaper style) and - as seen here - nice n-dashes (German style, "-" vs. " - ").'
;

// Create new document from text.
$document = new SimpleHTML($text);

//-----------------------------------------------------------------------------

if (isset($_SERVER['PHP_SELF'])) $PHP_SELF $_SERVER['PHP_SELF'];

?><html>
<head>
    <title>Simple (X)HTML Prototype |Thiemo Mättig</title>
    <style type="text/css">
        body{background-color:#F7F7F7;}
        body,input{font-family:Verdana,sans-serif;}
        code,textarea{font-family:monospace;}
        textarea{width:100%;}
        .subhead{font-size:x-small;}
        .info{
            background:#E7E7E7;
            border:1px solid #CCCCCC;
            color:#666666;
            margin:2px 0;
            padding:2px;
        }
        .comment{
            background:#F7F7F7;
            border:1px solid #CCCCCC;
            color:black;
        }
        .comment p{margin:0.6em 2px;}
        .highlight a{background:#CCCCFF;}
        .highlight code{background:#CCFFCC;}
        .highlight em{background:#FFFFCC;}
        .highlight strong{background:#FFCCCC;}
    </style>
    <script type="text/javascript">
        /**
         * Simple (X)HTML textarea helper.
         * Highlights adresses and/or inserts (X)HTML codes.
         */
        function html(c)
        {
            var e = document.forms[0].elements['text'];
            e.focus();
            var n = e.value;
            if (! c)
            {
                var regex = /(^|[^\w\/="'>])(http:\/\/|(www\.))([^\s"'<>\[\]]{4,}[\w\/=])/;
                while ((p = n.search(regex)) >= 0)
                {
                    var m = n.match(regex);
                    var text = prompt('Text (<a href="http://' + m[3] + m[4] + '">...):', m[3] + m[4]);
                    if (! text) return false;
                    var r = m[1] + '<a href="http://' + m[3] + m[4] + '">' + text + "</a>";
                    n = n.substr(0, p) + r + n.substr(p + m[0].length);
                }
                c = ' <a href=""></a>';
            }
            if (n != e.value) e.value = n;
            else
            {
                if ((p = c.indexOf('=') + 2) > 1)
                {
                    var url = prompt('URL (<a href="...):', "http://");
                    if (! url) return false;
                    c = c.substr(0, p) + url + c.substr(p);
                }
                if (document.selection)
                {
                    if (c.charAt(0) == " ") c = c.substring(1, c.length);
                    var selected = document.selection.createRange().text;
                    while (selected.charAt(selected.length - 1) == " ")
                    {
                        selected = selected.substr(0, selected.length - 1);
                        c += " ";
                    }
                    var p = c.indexOf(">") + 1;
                    document.selection.createRange().text = c.substr(0, p) + selected + c.substr(p);
                    e.focus();
                }
                else if (e.selectionStart || e.selectionStart == "0")
                {
                    if (c.charAt(0) == " ") c = c.substring(1, c.length);
                    var selectionStart = e.selectionStart;
                    var selectionEnd = e.selectionEnd;
                    var scrollTop = e.scrollTop;
                    var selected = n.substring(selectionStart, selectionEnd);
                    while (selected.charAt(selected.length - 1) == " ")
                    {
                        selected = selected.substr(0, selected.length - 1);
                        c += " ";
                    }
                    var p = c.indexOf(">") + 1;
                    selected = c.substr(0, p) + selected + c.substr(p);
                    e.value = n.substr(0, selectionStart) + selected + n.substr(selectionEnd);
                    e.selectionStart = selectionStart;
                    e.selectionEnd = selectionStart + selected.length;
                    e.scrollTop = scrollTop;
                }
                else if (n.indexOf(c) < 0 || n.lastIndexOf(c) < n.length - c.length || c.match(/^<[^<]*$/))
                    e.value += c;
            }
            return false;
        }
    </script>
</head>
<body onload="document.forms[0].elements['text'].focus()">

    <h1>Simple (X)HTML Prototype<br>
        <small class="subhead">
            UBBCode, Wiki Markup, Structured Text etc. are superfluous
        </small>
    </h1>
    
    <form action="<?=$PHP_SELF?>" id="form" method="post">
        <textarea cols="80" name="text" rows="12" wrap="virtual"><?=htmlspecialchars($text)?></textarea>
        <div class="info">
            Simple HTML is allowed:
            &lt;<a href="#form" onclick="return html('')" title="Insert HTML code">a href</a>&gt;,
            &lt;<a href="#form" onclick="return html(' <em></em>')" title="Insert HTML code"><em>em</em></a>&gt;,
            &lt;<a href="#form" onclick="return html(' <strong></strong>')" title="Insert HTML code"><strong>strong</strong></a>&gt;,
            &lt;<a href="#form" onclick="return html(' <code></code>')" title="Insert HTML code"><code>code</code></a>&gt;.
        </div>
        <input accesskey="s" type="submit" value="Show Preview">
    </form>
    
    <div class="highlight">
        <div class="info">Preview:
            <div class="comment">
                <?=$document->display()?>
            </div>
        </div>
        
        <p class="info">
            Detected tags are:
            <a href="#" onclick="return false">&lt;a href&gt;</a> (anchor),
            <em>&lt;em&gt;</em> (emphatic),
            <strong>&lt;strong&gt;</strong> and
            <code>&lt;code&gt;</code>.
        </p>
    </div>
    
    <p>
        <a href="<?=$PHP_SELF?>?source=1">Show Source</a><br>
        <a href="http://maettig.com/?page=E-Mail">Report Bugs/Suggestions</a>
    </p>

</body>
</html>