move Horde_Text_Textile to git
authorChuck Hagenbuch <chuck@horde.org>
Sat, 15 Nov 2008 14:54:59 +0000 (09:54 -0500)
committerChuck Hagenbuch <chuck@horde.org>
Sat, 15 Nov 2008 14:54:59 +0000 (09:54 -0500)
43 files changed:
framework/Text_Textile/CVS/Entries [new file with mode: 0644]
framework/Text_Textile/CVS/Repository [new file with mode: 0644]
framework/Text_Textile/CVS/Root [new file with mode: 0644]
framework/Text_Textile/CVS/Template [new file with mode: 0644]
framework/Text_Textile/lib/CVS/Entries [new file with mode: 0644]
framework/Text_Textile/lib/CVS/Repository [new file with mode: 0644]
framework/Text_Textile/lib/CVS/Root [new file with mode: 0644]
framework/Text_Textile/lib/CVS/Template [new file with mode: 0644]
framework/Text_Textile/lib/Horde/CVS/Entries [new file with mode: 0644]
framework/Text_Textile/lib/Horde/CVS/Repository [new file with mode: 0644]
framework/Text_Textile/lib/Horde/CVS/Root [new file with mode: 0644]
framework/Text_Textile/lib/Horde/CVS/Template [new file with mode: 0644]
framework/Text_Textile/lib/Horde/Text/CVS/Entries [new file with mode: 0644]
framework/Text_Textile/lib/Horde/Text/CVS/Repository [new file with mode: 0644]
framework/Text_Textile/lib/Horde/Text/CVS/Root [new file with mode: 0644]
framework/Text_Textile/lib/Horde/Text/CVS/Template [new file with mode: 0644]
framework/Text_Textile/lib/Horde/Text/Textile.php [new file with mode: 0755]
framework/Text_Textile/package.xml [new file with mode: 0644]
framework/Text_Textile/test/CVS/Entries [new file with mode: 0644]
framework/Text_Textile/test/CVS/Repository [new file with mode: 0644]
framework/Text_Textile/test/CVS/Root [new file with mode: 0644]
framework/Text_Textile/test/CVS/Template [new file with mode: 0644]
framework/Text_Textile/test/Horde/CVS/Entries [new file with mode: 0644]
framework/Text_Textile/test/Horde/CVS/Repository [new file with mode: 0644]
framework/Text_Textile/test/Horde/CVS/Root [new file with mode: 0644]
framework/Text_Textile/test/Horde/CVS/Template [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/CVS/Entries [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/CVS/Repository [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/CVS/Root [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/CVS/Template [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/AllTests.php [new file with mode: 0755]
framework/Text_Textile/test/Horde/Text/Textile/AttributesTest.php [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/BlockModifiersTest.php [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/CVS/Entries [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/CVS/Repository [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/CVS/Root [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/CVS/Template [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/ExternalReferencesTest.php [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/ListsTest.php [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/PhraseModifiersTest.php [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/TablesTest.php [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/TestCase.php [new file with mode: 0644]
framework/Text_Textile/test/Horde/Text/Textile/WritingTest.php [new file with mode: 0755]

diff --git a/framework/Text_Textile/CVS/Entries b/framework/Text_Textile/CVS/Entries
new file mode 100644 (file)
index 0000000..a929968
--- /dev/null
@@ -0,0 +1,3 @@
+/package.xml/1.5/Thu May 29 22:28:36 2008//
+D/lib////
+D/test////
diff --git a/framework/Text_Textile/CVS/Repository b/framework/Text_Textile/CVS/Repository
new file mode 100644 (file)
index 0000000..58462a6
--- /dev/null
@@ -0,0 +1 @@
+framework/Text_Textile
diff --git a/framework/Text_Textile/CVS/Root b/framework/Text_Textile/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Text_Textile/CVS/Template b/framework/Text_Textile/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Text_Textile/lib/CVS/Entries b/framework/Text_Textile/lib/CVS/Entries
new file mode 100644 (file)
index 0000000..ae2779e
--- /dev/null
@@ -0,0 +1 @@
+D/Horde////
diff --git a/framework/Text_Textile/lib/CVS/Repository b/framework/Text_Textile/lib/CVS/Repository
new file mode 100644 (file)
index 0000000..2449409
--- /dev/null
@@ -0,0 +1 @@
+framework/Text_Textile/lib
diff --git a/framework/Text_Textile/lib/CVS/Root b/framework/Text_Textile/lib/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Text_Textile/lib/CVS/Template b/framework/Text_Textile/lib/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Text_Textile/lib/Horde/CVS/Entries b/framework/Text_Textile/lib/Horde/CVS/Entries
new file mode 100644 (file)
index 0000000..66e2d71
--- /dev/null
@@ -0,0 +1 @@
+D/Text////
diff --git a/framework/Text_Textile/lib/Horde/CVS/Repository b/framework/Text_Textile/lib/Horde/CVS/Repository
new file mode 100644 (file)
index 0000000..43b6924
--- /dev/null
@@ -0,0 +1 @@
+framework/Text_Textile/lib/Horde
diff --git a/framework/Text_Textile/lib/Horde/CVS/Root b/framework/Text_Textile/lib/Horde/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Text_Textile/lib/Horde/CVS/Template b/framework/Text_Textile/lib/Horde/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Text_Textile/lib/Horde/Text/CVS/Entries b/framework/Text_Textile/lib/Horde/Text/CVS/Entries
new file mode 100644 (file)
index 0000000..a82c128
--- /dev/null
@@ -0,0 +1,2 @@
+/Textile.php/1.2/Thu May 29 22:28:36 2008//
+D
diff --git a/framework/Text_Textile/lib/Horde/Text/CVS/Repository b/framework/Text_Textile/lib/Horde/Text/CVS/Repository
new file mode 100644 (file)
index 0000000..2baaf83
--- /dev/null
@@ -0,0 +1 @@
+framework/Text_Textile/lib/Horde/Text
diff --git a/framework/Text_Textile/lib/Horde/Text/CVS/Root b/framework/Text_Textile/lib/Horde/Text/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Text_Textile/lib/Horde/Text/CVS/Template b/framework/Text_Textile/lib/Horde/Text/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Text_Textile/lib/Horde/Text/Textile.php b/framework/Text_Textile/lib/Horde/Text/Textile.php
new file mode 100755 (executable)
index 0000000..df7072e
--- /dev/null
@@ -0,0 +1,839 @@
+<?php
+/**
+ * @category Horde
+ * @package  Horde_Text_Textile
+ */
+
+/**
+ * References:
+ *   http://www.textism.com/tools/textile/
+ *   http://en.wikipedia.org/wiki/Textile_(markup_language)
+ *   http://hobix.com/textile/
+ *   http://whytheluckystiff.net/ruby/redcloth/
+ *   http://redcloth.rubyforge.org/rdoc/
+ *   http://code.whytheluckystiff.net/redcloth/browser/trunk/test/textism.yml
+ *
+ * Example: get XHTML from a given Textile-markup string ($string)
+ *   $textile = new Horde_Text_Textile;
+ *   echo $textile->toHtml($string);
+ *
+ * @category Horde
+ * @package  Horde_Text_Textile
+ */
+class Horde_Text_Textile {
+
+    /**
+     * A_HLGN
+     */
+    const REGEX_A_HLGN = '(?:\<(?!>)|(?<!<)\>|\<\>|\=|[()]+(?! ))';
+
+    /**
+     * A_VLGN
+     */
+    const REGEX_A_VLGN = '[\-^~]';
+
+    /**
+     * '(?:' . A_HLGN . '|' . A_VLGN . ')*'
+     */
+    const REGEX_A = '(?:(?:\<(?!>)|(?<!<)\>|\<\>|\=|[()]+(?! ))|[\-^~])*';
+
+    /**
+     * '(?:' . S_CSPN . '|' . S_RSPN . ')*'
+     */
+    const REGEX_S = '(?:(?:\\\\\d+)|(?:\/\d+))*';
+
+    /**
+     * '(?:' . C_CLAS . '|' . C_STYL . '|' . C_LNGE . '|' . A_HLGN . ')*'
+     */
+    const REGEX_C = '(?:(?:\([^)]+\))|(?:\{[^}]+\})|(?:\[[^]]+\])|(?:\<(?!>)|(?<!<)\>|\<\>|\=|[()]+(?! )))*';
+
+    /**
+     * PUNCT
+     */
+    const REGEX_PUNCT = '\!"#\$%&\'\*\+,-\.\/\:;\=\?@\\\^_`\|~';
+
+    /**
+     * LINK_RE
+     */
+    const REGEX_URL = '[\w"$\-_.+!*\'(),";\/?:@=&%#{}|\\^~\[\]`]';
+
+    /**
+     * Block tags
+     */
+    const REGEX_BLOCK_TAGS = 'bq|bc|notextile|pre|h[1-6]|fn\d+|p';
+
+    /**
+     * Glyphs. Can be overridden if you want to substitute different
+     * entities.
+     */
+    public static $GLYPH_QUOTE_SINGLE_OPEN = '&#8216;';
+    public static $GLYPH_QUOTE_SINGLE_CLOSE = '&#8217;';
+    public static $GLYPH_QUOTE_DOUBLE_OPEN = '&#8220;';
+    public static $GLYPH_QUOTE_DOUBLE_CLOSE = '&#8221;';
+    public static $GLYPH_APOSTROPHE = '&#8217;';
+    public static $GLYPH_PRIME = '&#8242;';
+    public static $GLYPH_PRIME_DOUBLE = '&#8243;';
+    public static $GLYPH_ELLIPSIS = '&#8230;';
+    public static $GLYPH_EMDASH = '&#8212;';
+    public static $GLYPH_ENDASH = '&#8211;';
+    public static $GLYPH_DIMENSION = '&#215;';
+    public static $GLYPH_TRADEMARK = '&#8482;';
+    public static $GLYPH_REGISTERED = '&#174;';
+    public static $GLYPH_COPYRIGHT = '&#169;';
+    public static $GLYPH_RETURN_ARROW = '&#8617;';
+
+    /**
+     * Show images? On by default.
+     *
+     * @var boolean
+     */
+    public $images = true;
+
+    /**
+     * Rel attribute for links (ex: nofollow).
+     *
+     * @var string
+     */
+    public $rel = '';
+
+    /**
+     * Shelf of values being processed.
+     *
+     * @var array
+     */
+    protected $_shelf = array();
+
+    public function transform($text, $rel = '')
+    {
+        if ($rel) {
+            $this->rel = ' rel="' . $rel . '" ';
+        }
+
+        $text = $this->cleanWhiteSpace($text);
+        $text = $this->getRefs($text);
+        $text = $this->block($text);
+
+        return $this->retrieve($text);
+    }
+
+    /**
+     * parse block attributes
+     * @ignore
+     */
+    public function parseBlockAttributes($in, $element = '')
+    {
+        $style = '';
+        $class = '';
+        $lang = '';
+        $colspan = '';
+        $rowspan = '';
+        $id = '';
+        $atts = '';
+
+        if (!empty($in)) {
+            $matched = $in;
+            if ($element == 'td') {
+                if (preg_match("/\\\\(\d+)/", $matched, $csp)) $colspan = $csp[1];
+                if (preg_match("/\/(\d+)/", $matched, $rsp)) $rowspan = $rsp[1];
+            }
+
+            if ($element == 'td' || $element == 'tr') {
+                if (preg_match('/(' . self::REGEX_A_VLGN . ')/', $matched, $vert))
+                    $style[] = 'vertical-align:' . $this->vAlign($vert[1]) . ';';
+            }
+
+            if (preg_match('/\{([^}]*)\}/', $matched, $sty)) {
+                $style[] = rtrim($sty[1], ';') . ';';
+                $matched = str_replace($sty[0], '', $matched);
+            }
+
+            if (preg_match('/\[([^]]+)\]/U', $matched, $lng)) {
+                $lang = $lng[1];
+                $matched = str_replace($lng[0], '', $matched);
+            }
+
+            if (preg_match('/\(([^()]+)\)/U', $matched, $cls)) {
+                $class = $cls[1];
+                $matched = str_replace($cls[0], '', $matched);
+            }
+
+            if (preg_match('/([(]+)/', $matched, $pl)) {
+                $style[] = 'padding-left:' . strlen($pl[1]) . 'em;';
+                $matched = str_replace($pl[0], '', $matched);
+            }
+
+            if (preg_match('/([)]+)/', $matched, $pr)) {
+                $style[] = 'padding-right:' . strlen($pr[1]) . 'em;';
+                $matched = str_replace($pr[0], '', $matched);
+            }
+
+            if (preg_match('/(' . self::REGEX_A_HLGN . ')/', $matched, $horiz)) {
+                $style[] = 'text-align:' . $this->hAlign($horiz[1]) . ';';
+            }
+
+            if (preg_match('/^(.*)#(.*)$/', $class, $ids)) {
+                $id = $ids[2];
+                $class = $ids[1];
+            }
+
+            return
+                ($style     ? ' style="'   . implode('', $style) . '"' : '')
+                . ($class   ? ' class="'   . $class              . '"' : '')
+                . ($lang    ? ' lang="'    . $lang               . '"' : '')
+                . ($id      ? ' id="'      . $id                 . '"' : '')
+                . ($colspan ? ' colspan="' . $colspan            . '"' : '')
+                . ($rowspan ? ' rowspan="' . $rowspan            . '"' : '');
+        }
+
+        return '';
+    }
+
+    /**
+     * @ignore
+     */
+    public function hasRawText($text)
+    {
+        // Checks whether the text has text not already enclosed by a
+        // block tag.
+        $r = trim(preg_replace('@<(p|blockquote|div|form|table|ul|ol|pre|h\d)[^>]*?>.*</\1>@s', '', trim($text)));
+        $r = trim(preg_replace('@<\/?(p|blockquote|div|form|table|ul|ol|pre|h\d)[^>]*?\/?>@s', '', $r));
+        $r = trim(preg_replace('@<(hr|br)[^>]*?/>@', '', $r));
+        return '' != $r;
+    }
+
+    /**
+     * @ignore
+     */
+    public function table($text)
+    {
+        $text = $text . "\n\n";
+        return preg_replace_callback("/^(?:table(_?" . self::REGEX_S . self::REGEX_A . self::REGEX_C . ")\. ?\n)?^(" . self::REGEX_A . self::REGEX_C . "\.? ?\|.*\|)\n\n/smU",
+                                     array($this, 'fTable'), $text);
+    }
+
+    /**
+     * @ignore
+     */
+    public function fTable($matches)
+    {
+        $tatts = $this->parseBlockAttributes($matches[1], 'table');
+
+        foreach (preg_split("/\|$/m", $matches[2], -1, PREG_SPLIT_NO_EMPTY) as $row) {
+            if (preg_match("/^(" . self::REGEX_A . self::REGEX_C . "\. )(.*)/m", ltrim($row), $rmtch)) {
+                $ratts = $this->parseBlockAttributes($rmtch[1], 'tr');
+                $row = $rmtch[2];
+            } else {
+                $ratts = '';
+            }
+            $cells = array();
+            foreach (explode('|', $row) as $cell) {
+                $ctyp = 'd';
+                if (preg_match("/^_/", $cell)) {
+                    $ctyp = 'h';
+                }
+                if (preg_match("/^(_?" . self::REGEX_S . self::REGEX_A . self::REGEX_C . "\. )(.*)/", $cell, $cmtch)) {
+                    $catts = $this->parseBlockAttributes($cmtch[1], 'td');
+                    $cell = $cmtch[2];
+                } else {
+                    $catts = '';
+                }
+
+                $cell = $this->paragraph($this->span($cell));
+                if (trim($cell) != '') {
+                    $cells[] = "\t\t\t<t$ctyp$catts>$cell</t$ctyp>";
+                }
+            }
+            $rows[] = "\t\t<tr$ratts>\n" . implode("\n", $cells) . ($cells ? "\n" : '') . "\t\t</tr>";
+            unset($cells, $catts);
+        }
+        return "\t<table$tatts>\n" . implode("\n", $rows) . "\n\t</table>\n\n";
+    }
+
+    /**
+     * @ignore
+     */
+    public function lists($text)
+    {
+        return preg_replace_callback("/^([#*]+" . self::REGEX_C . ".*)$(?![^#*])/smU", array($this, 'fList'), $text);
+    }
+
+    /**
+     * @ignore
+     */
+    public function fList($m)
+    {
+        $out = array();
+        $lines = explode("\n", $m[0]);
+        for ($i = 0, $i_max = count($lines); $i < $i_max; $i++) {
+            $line = $lines[$i];
+            $nextline = isset($lines[$i + 1]) ? $lines[$i + 1] : false;
+
+            if (preg_match("/^([#*]+)(" . self::REGEX_A . self::REGEX_C . ") (.*)$/s", $line, $m)) {
+                list(, $tl, $atts, $content) = $m;
+                $nl = '';
+                if (preg_match("/^([#*]+)\s.*/", $nextline, $nm)) {
+                    $nl = $nm[1];
+                }
+                $level = strlen($tl);
+                if (!isset($lists[$tl])) {
+                    $lists[$tl] = true;
+                    $atts = $this->parseBlockAttributes($atts);
+                    $line = str_repeat("\t", $level) . '<' . $this->lT($tl) . "l$atts>\n" . str_repeat("\t", $level + 1) . '<li>' . $this->paragraph($content);
+                } else {
+                    $line = str_repeat("\t", $level + 1) . '<li>' . $this->paragraph($content);
+                }
+
+                if (strlen($nl) <= strlen($tl)) {
+                    $line .= '</li>';
+                }
+                foreach (array_reverse($lists) as $k => $v) {
+                    if (strlen($k) > strlen($nl)) {
+                        $line .= "\n" . str_repeat("\t", $level--) . '</' . $this->lT($k) . 'l>';
+                        if (strlen($k) > 1) {
+                            $line .= '</li>';
+                        }
+                        unset($lists[$k]);
+                    }
+                }
+            }
+
+            $out[] = $line;
+        }
+
+        return implode("\n", $out);
+    }
+
+    /**
+     * @ignore
+     */
+    public function lT($in)
+    {
+        return substr($in, 0, 1) == '#' ? 'o' : 'u';
+    }
+
+    /**
+     * @ignore
+     */
+    public function doPBr($in)
+    {
+        return preg_replace_callback('@<(p)([^>]*?)>(.*)(</\1>)@s', array($this, 'doBr'), $in);
+    }
+
+    /**
+     * @ignore
+     */
+    public function doBr($m)
+    {
+        $content = preg_replace("@(.+)(?<!<br>|<br />)\n(?![#*\s|])@", "\$1<br />\n", $m[3]);
+        return '<' . $m[1] . $m[2] . '>' . $content . $m[4];
+    }
+
+    /**
+     * @ignore
+     */
+    public function block($text)
+    {
+        $tag = 'p';
+        $atts = $cite = $graf = $ext = '';
+
+        $text = explode("\n\n", $text);
+        foreach ($text as $line) {
+            $anon = 0;
+            if (preg_match('/^(' . self::REGEX_BLOCK_TAGS . ')(' . self::REGEX_A . self::REGEX_C . ')\.(\.?)(?::(\S+))? (.*)$/s', $line, $m)) {
+                if ($ext) {
+                    // last block was extended, so close it
+                    $out[count($out) - 1] .= $c1;
+                }
+                // new block
+                list(, $tag, $atts, $ext, $cite, $graf) = $m;
+                list($o1, $o2, $content, $c2, $c1) = $this->fBlock(array(0, $tag, $atts, $ext, $cite, $graf));
+
+                // leave off c1 if this block is extended, we'll close
+                // it at the start of the next block
+                if ($ext) {
+                    $line = $o1 . $o2 . $content . $c2;
+                } else {
+                    $line = $o1 . $o2 . $content . $c2 . $c1;
+                }
+            } else {
+                // anonymous block
+                $anon = 1;
+                if ($ext || !preg_match('/^ /', $line)) {
+                    list($o1, $o2, $content, $c2, $c1) = $this->fBlock(array(0, $tag, $atts, $ext, $cite, $line));
+                    // skip $o1/$c1 because this is part of a
+                    // continuing extended block
+                    if ($tag == 'p' && !$this->hasRawText($content)) {
+                        $line = $content;
+                    } else {
+                        $line = $o2 . $content . $c2;
+                    }
+                } else {
+                   $line = $this->paragraph($line);
+                }
+            }
+
+            $line = preg_replace('/<br>/', '<br />', $this->doPBr($line));
+
+            if ($ext && $anon) {
+                $out[count($out) - 1] .= "\n" . $line;
+            } else {
+                $out[] = $line;
+            }
+
+            if (!$ext) {
+                $tag = 'p';
+                $atts = '';
+                $cite = '';
+                $graf = '';
+            }
+        }
+        if ($ext) {
+            $out[count($out) - 1] .= $c1;
+        }
+        return implode("\n\n", $out);
+    }
+
+    /**
+     * @ignore
+     */
+    public function fBlock($m)
+    {
+        list(, $tag, $atts, $ext, $cite, $content) = $m;
+        $atts = $this->parseBlockAttributes($atts);
+
+        $o1 = $o2 = $c2 = $c1 = '';
+
+        if (preg_match('/fn(\d+)/', $tag, $fns)) {
+            $tag = 'p';
+            $fnid = $fns[1];
+            $atts .= ' id="fn' . $fnid . '"';
+            $content = '<sup>' . $fns[1] . '</sup> ' . $content . ' <a href="#fnr' . $fnid . '">' . self::$GLYPH_RETURN_ARROW . '</a>';
+        }
+
+        if ($tag == 'bq') {
+            $cite = $this->checkRefs($cite);
+            $cite = ($cite != '') ? ' cite="' . $cite . '"' : '';
+            $o1 = '<blockquote' . $cite . $atts . ">\n";
+            $o2 = "<p$atts>";
+            $c2 = '</p>';
+            $c1 = "\n</blockquote>";
+        } elseif ($tag == 'bc') {
+            $o1 = "<pre$atts>";
+            $o2 = "<code$atts>";
+            $c2 = '</code>';
+            $c1 = '</pre>';
+            $content = $this->shelve($this->encodeHtml(rtrim($content, "\n") . "\n"));
+        } elseif ($tag == 'notextile') {
+            $content = $this->shelve($content);
+            $o1 = $o2 = '';
+            $c1 = $c2 = '';
+        } elseif ($tag == 'pre') {
+            $content = $this->shelve($this->encodeHtml(rtrim($content, "\n") . "\n"));
+            $o1 = "<pre$atts>";
+            $o2 = $c2 = '';
+            $c1 = '</pre>';
+        } else {
+            $o2 = "<$tag$atts>";
+            $c2 = "</$tag>";
+        }
+
+        return array($o1, $o2, $this->paragraph($content), $c2, $c1);
+    }
+
+    /**
+     * Handle normal paragraph text.
+     * @ignore
+     */
+    public function paragraph($text)
+    {
+        $text = $this->code($this->noTextile($text));
+        $text = $this->links($text);
+        if ($this->images) {
+            $text = $this->image($text);
+        }
+
+        $text = $this->table($this->lists($text));
+        $text = $this->glyphs($this->footnoteRef($this->span($text)));
+        return rtrim($text, "\n");
+    }
+
+    /**
+     * @ignore
+     */
+    public function span($text)
+    {
+        $qtags = array('\*\*', '\*', '\?\?', '-', '__', '_', '%', '\+', '~', '\^');
+        $pnct = ".,\"'?!;:";
+
+        foreach ($qtags as $f) {
+            $text = preg_replace_callback("/
+                (?:^|(?<=[\s>$pnct])|([{[]))
+                ($f)(?!$f)
+                (" . self::REGEX_C . ")
+                (?::(\S+))?
+                ([^\s$f]+|\S[^$f\n]*[^\s$f\n])
+                ([$pnct]*)
+                $f
+                (?:$|([\]}])|(?=[[:punct:]]{1,2}|\s))
+            /x", array($this, 'fSpan'), $text);
+        }
+        return $text;
+    }
+
+    /**
+     * @ignore
+     */
+    public function fSpan($m)
+    {
+        $qtags = array(
+            '*'  => 'strong',
+            '**' => 'b',
+            '??' => 'cite',
+            '_'  => 'em',
+            '__' => 'i',
+            '-'  => 'del',
+            '%'  => 'span',
+            '+'  => 'ins',
+            '~'  => 'sub',
+            '^'  => 'sup',
+        );
+
+        list(, , $tag, $atts, $cite, $content, $end) = $m;
+        $tag = $qtags[$tag];
+        $atts = $this->parseBlockAttributes($atts)
+            . ($cite ? 'cite="' . $cite . '"' : '');
+
+        return "<$tag$atts>$content$end</$tag>";
+    }
+
+    /**
+     * @ignore
+     */
+    public function links($text)
+    {
+        $punct = preg_quote('!"#$%&\'*+,-./:;=?@\\^_`|~', '/');
+        return preg_replace_callback('/
+            (^|(?<=[\s>.' . self::REGEX_PUNCT . '\(])|([{[]))  # $pre
+            "                                                  # $start
+            (' . self::REGEX_C . ')                            # $atts
+            ([^"]+)                                            # $text
+            \s?
+            (?:\(([^)]+)\)(?="))?                              # $title
+            ":
+            (' . self::REGEX_URL . '+)                         # $url
+            (\/)?                                              # $slash
+            ([^\w\/;]*)                                        # $post
+            (?:([\]}])|(?=\s|$|\)))
+        /Ux', array($this, 'fLink'), $text);
+    }
+
+    /**
+     * @ignore
+     */
+    public function fLink($m)
+    {
+        list(, $pre, $start, $atts, $text, $title, $url, $slash, $post) = $m;
+
+        $atts = $this->parseBlockAttributes($atts)
+            . ($title != '') ? ' title="' . $this->encodeHtml($title) . '"' : '';
+
+        if ($this->images) {
+            $text = $this->image($text);
+        }
+        $text = $this->glyphs($this->span($text));
+
+        $url = $this->checkRefs($url);
+
+        return $this->shelve('<a href="'
+                             . $this->encodeHtml($url . $slash)
+                             . '"' . $atts . ($this->rel ? ' rel="' . $this->rel . '" ' : '') . '>'
+                             . $text . '</a>' . $post);
+    }
+
+    /**
+     * @ignore
+     */
+    public function getRefs($text)
+    {
+        return preg_replace_callback("/(?<=^|\s)\[(.+)\]((?:http:\/\/|\/)\S+)(?=\s|$)/U",
+            array($this, 'refs'), $text);
+    }
+
+    /**
+     * @ignore
+     */
+    public function refs($m)
+    {
+        list(, $flag, $url) = $m;
+        $this->urlrefs[$flag] = $url;
+        return '';
+    }
+
+    /**
+     * @ignore
+     */
+    public function checkRefs($text)
+    {
+        return isset($this->urlrefs[$text]) ? $this->urlrefs[$text] : $text;
+    }
+
+    /**
+     * @ignore
+     */
+    public function image($text)
+    {
+        return preg_replace_callback("/
+            (?:[[{])?               # pre
+            \!                      # opening !
+            (\<|\=|\>)??            # optional alignment attributes
+            (" . self::REGEX_C . ") # optional style, class attributes
+            (?:\. )?                # optional dot-space
+            ([^\s(!]+)              # presume this is the src
+            \s?                     # optional space
+            (?:\(([^\)]+)\))?       # optional title
+            \!                      # closing
+            (?::(\S+))?             # optional href
+            (?:[\]}]|(?=\s|$))      # lookahead: space or end of string
+        /Ux", array($this, 'fImage'), $text);
+    }
+
+    /**
+     * @ignore
+     */
+    public function fImage($m)
+    {
+        list(, $algn, $atts, $url) = $m;
+        $title = isset($m[4]) ? $m[4] : '';
+        $atts = $this->parseBlockAttributes($atts)
+            . ($algn != '' ? ' align="' . $this->iAlign($algn) . '"' : '')
+            . ($title ? ' title="' . $title . '"' : '')
+            . ' alt="'   . $title . '"';
+
+        $href = isset($m[5]) ? $this->checkRefs($m[5]) : '';
+        $url = $this->checkRefs($url);
+
+        return ($href ? '<a href="' . $href . '">' : '')
+            . '<img src="' . $url . '"' . $atts . ' />'
+            . ($href ? '</a>' : '');
+    }
+
+    /**
+     * @ignore
+     */
+    public function code($text)
+    {
+        $text = $this->doSpecial($text, '<code>', '</code>', 'fCode');
+        $text = $this->doSpecial($text, '@', '@', 'fCode');
+        $text = $this->doSpecial($text, '<pre>', '</pre>', 'fPre');
+        return $text;
+    }
+
+    /**
+     * @ignore
+     */
+    public function fCode($m)
+    {
+        @list(, $before, $text, $after) = $m;
+        return $before . $this->shelve('<code>' . $this->encodeHtml($text, false) . '</code>') . $after;
+    }
+
+    /**
+     * @ignore
+     */
+    public function fPre($m)
+    {
+        @list(, $before, $text, $after) = $m;
+        return $before . '<pre>' . $this->shelve($this->encodeHtml($text, false)) . '</pre>' . $after;
+    }
+
+    /**
+     * @ignore
+     */
+    public function shelve($val)
+    {
+        $i = uniqid(mt_rand());
+        $this->_shelf[$i] = $val;
+        return $i;
+    }
+
+    /**
+     * @ignore
+     */
+    public function retrieve($text)
+    {
+        if (is_array($this->_shelf)) {
+            do {
+                $old = $text;
+                $text = strtr($text, $this->_shelf);
+            } while ($text != $old);
+        }
+        return $text;
+    }
+
+    /**
+     * @ignore
+     */
+    public function cleanWhiteSpace($text)
+    {
+        return preg_replace(array("/\r\n/", "/\n{3,}/", "/\n *\n/"),
+                            array("\n",     "\n\n",     "\n\n"),
+                            $text);
+    }
+
+    /**
+     * @ignore
+     */
+    public function doSpecial($text, $start, $end, $method = 'fSpecial')
+    {
+        return preg_replace_callback('/(^|\s|[[({>])' . preg_quote($start, '/') . '(.*?)' . preg_quote($end, '/') . '(\s|$|[\])}])?/ms',
+                                     array($this, $method), $text);
+    }
+
+    /**
+     * @ignore
+     */
+    public function fSpecial($m)
+    {
+        // A special block like notextile or code
+        @list(, $before, $text, $after) = $m;
+        return $before . $this->shelve($this->encodeHtml($text)) . $after;
+    }
+
+    /**
+     * @ignore
+     */
+    public function noTextile($text)
+    {
+         $text = $this->doSpecial($text, '<notextile>', '</notextile>', 'fTextile');
+         return $this->doSpecial($text, '==', '==', 'fTextile');
+    }
+
+    /**
+     * @ignore
+     */
+    public function fTextile($m)
+    {
+        @list(, $before, $notextile, $after) = $m;
+        return $before . $this->shelve($notextile) . $after;
+    }
+
+    /**
+     * @ignore
+     */
+    public function footnoteRef($text)
+    {
+        return preg_replace('/\b\[([0-9]+)\](\s)?/U',
+                            '<sup><a id="fnr$1" href="#fn$1">$1</a></sup>$2',
+                            $text);
+    }
+
+    /**
+     * @ignore
+     */
+    public function glyphs($text)
+    {
+        $glyph_search = array(
+            '/(\w)\'(\w)/',                                           // apostrophe's
+            '/(\s)\'(\d+\w?)\b(?!\')/',                               // back in '88
+            '/(\S)\'(?=\s|[[:punct:]]|<|$)/',                         // single closing
+            '/\'/',                                                   // single opening
+            '/(\S)\"(?=\s|[[:punct:]]|<|$)/',                         // double closing
+            '/"/',                                                    // double opening
+            '/\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/',             // 3+ uppercase acronym
+            '/\b([A-Z][A-Z\'\-]+[A-Z])(?=[\s.,\)>])/',                // 3+ uppercase
+            '/\b( )?\.{3}/',                                          // ellipsis
+            '/(\s?)--(\s?)/',                                         // em dash
+            '/\s-(?:\s|$)/',                                          // en dash
+            '/(\d+)( ?)x( ?)(?=\d+)/',                                // dimension sign
+            '/\b ?[([]TM[])]/i',                                      // trademark
+            '/\b ?[([]R[])]/i',                                       // registered
+            '/\b ?[([]C[])]/i',                                       // copyright
+        );
+
+        $glyph_replace = array(
+            '$1' . self::$GLYPH_APOSTROPHE . '$2',      // apostrophes
+            '$1' . self::$GLYPH_APOSTROPHE . '$2',      // back in '88
+            '$1' . self::$GLYPH_QUOTE_SINGLE_CLOSE,     // single closing
+            self::$GLYPH_QUOTE_SINGLE_OPEN,             // single opening
+            '$1' . self::$GLYPH_QUOTE_DOUBLE_CLOSE,     // double closing
+            self::$GLYPH_QUOTE_DOUBLE_OPEN,             // double opening
+            '<acronym title="$2">$1</acronym>',                       // 3+ uppercase acronym
+            '<span class="caps">$1</span>',                           // 3+ uppercase
+            '$1' . self::$GLYPH_ELLIPSIS,               // ellipsis
+            self::$GLYPH_EMDASH,                        // em dash
+            ' ' . self::$GLYPH_ENDASH . ' ',            // en dash
+            '$1' . self::$GLYPH_DIMENSION,              // dimension sign
+            self::$GLYPH_TRADEMARK,                     // trademark
+            self::$GLYPH_REGISTERED,                    // registered
+            self::$GLYPH_COPYRIGHT,                     // copyright
+        );
+
+        $text = preg_split('/(<.*>)/U', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+        foreach ($text as $line) {
+            if (!preg_match('/<.*>/', $line)) {
+                $line = preg_replace($glyph_search, $glyph_replace, $line);
+            }
+            $glyph_out[] = $line;
+        }
+
+        return implode('', $glyph_out);
+    }
+
+    /**
+     * @ignore
+     */
+    public function iAlign($in)
+    {
+        $vals = array(
+            '<' => 'left',
+            '=' => 'center',
+            '>' => 'right');
+        return isset($vals[$in]) ? $vals[$in] : '';
+    }
+
+    /**
+     * @ignore
+     */
+    public function hAlign($in)
+    {
+        $vals = array(
+            '<'  => 'left',
+            '='  => 'center',
+            '>'  => 'right',
+            '<>' => 'justify');
+        return isset($vals[$in]) ? $vals[$in] : '';
+    }
+
+    /**
+     * @ignore
+     */
+    public function vAlign($in)
+    {
+        $vals = array(
+            '^' => 'top',
+            '-' => 'middle',
+            '~' => 'bottom');
+        return isset($vals[$in]) ? $vals[$in] : '';
+    }
+
+    /**
+     * @ignore
+     */
+    public function encodeHtml($str, $quotes = true)
+    {
+        $a = array(
+            '&' => '&amp;',
+            '<' => '&lt;',
+            '>' => '&gt;',
+        );
+        if ($quotes) {
+            $a = $a + array(
+                "'" => '&#39;',
+                '"' => '&#34;',
+            );
+        }
+
+        return strtr($str, $a);
+    }
+
+}
diff --git a/framework/Text_Textile/package.xml b/framework/Text_Textile/package.xml
new file mode 100644 (file)
index 0000000..c158ebf
--- /dev/null
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Horde_Text_Textile</name>
+ <channel>pear.horde.org</channel>
+ <summary>Textile formatting library</summary>
+ <description>
+_____________
+T E X T I L E
+
+A Humane Web Text Generator
+
+Version 2.0
+
+Copyright (c) 2003-2004, Dean Allen &lt;dean@textism.com&gt;
+All rights reserved.
+
+Thanks to Carlo Zottmann &lt;carlo@g-blog.net&gt; for refactoring
+Textile's procedural code into a class framework
+
+Additions and fixes Copyright (c) 2006 Alex Shiels http://thresholdstate.com/
+
+_____________
+L I C E N S E
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+  this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name Textile nor the names of its contributors may be used to
+  endorse or promote products derived from this software without specific
+  prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2007-05-12</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://opensource.org/licenses/bsd-license.php">BSD</license>
+ <notes>
+* Initial release.
+* Add return links to footnotes
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Text">
+      <file name="Textile.php" role="php" />
+     </dir> <!-- /lib/Horde/Text -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.1.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Text/Textile.php" as="Horde/Text/Textile.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/Text_Textile/test/CVS/Entries b/framework/Text_Textile/test/CVS/Entries
new file mode 100644 (file)
index 0000000..ae2779e
--- /dev/null
@@ -0,0 +1 @@
+D/Horde////
diff --git a/framework/Text_Textile/test/CVS/Repository b/framework/Text_Textile/test/CVS/Repository
new file mode 100644 (file)
index 0000000..ba8a059
--- /dev/null
@@ -0,0 +1 @@
+framework/Text_Textile/test
diff --git a/framework/Text_Textile/test/CVS/Root b/framework/Text_Textile/test/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Text_Textile/test/CVS/Template b/framework/Text_Textile/test/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Text_Textile/test/Horde/CVS/Entries b/framework/Text_Textile/test/Horde/CVS/Entries
new file mode 100644 (file)
index 0000000..66e2d71
--- /dev/null
@@ -0,0 +1 @@
+D/Text////
diff --git a/framework/Text_Textile/test/Horde/CVS/Repository b/framework/Text_Textile/test/Horde/CVS/Repository
new file mode 100644 (file)
index 0000000..b68d0fa
--- /dev/null
@@ -0,0 +1 @@
+framework/Text_Textile/test/Horde
diff --git a/framework/Text_Textile/test/Horde/CVS/Root b/framework/Text_Textile/test/Horde/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Text_Textile/test/Horde/CVS/Template b/framework/Text_Textile/test/Horde/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Text_Textile/test/Horde/Text/CVS/Entries b/framework/Text_Textile/test/Horde/Text/CVS/Entries
new file mode 100644 (file)
index 0000000..0113b0c
--- /dev/null
@@ -0,0 +1 @@
+D/Textile////
diff --git a/framework/Text_Textile/test/Horde/Text/CVS/Repository b/framework/Text_Textile/test/Horde/Text/CVS/Repository
new file mode 100644 (file)
index 0000000..3e4e827
--- /dev/null
@@ -0,0 +1 @@
+framework/Text_Textile/test/Horde/Text
diff --git a/framework/Text_Textile/test/Horde/Text/CVS/Root b/framework/Text_Textile/test/Horde/Text/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Text_Textile/test/Horde/Text/CVS/Template b/framework/Text_Textile/test/Horde/Text/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/AllTests.php b/framework/Text_Textile/test/Horde/Text/Textile/AllTests.php
new file mode 100755 (executable)
index 0000000..9e41caf
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_Text_Textile_AllTests::main');
+}
+
+require_once 'PHPUnit/Framework/TestSuite.php';
+require_once 'PHPUnit/TextUI/TestRunner.php';
+
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_AllTests {
+
+    public static function main()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+
+    public static function suite()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Horde_Text_Textile');
+
+        $basedir = dirname(__FILE__);
+        $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/');
+
+        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) {
+            if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) {
+                $pathname = $file->getPathname();
+                require $pathname;
+
+                $class = str_replace(DIRECTORY_SEPARATOR, '_',
+                                     preg_replace("/^$baseregexp(.*)\.php/", '\\1', $pathname));
+                $suite->addTestSuite('Horde_Text_Textile_' . $class);
+            }
+        }
+
+        return $suite;
+    }
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_Text_Textile_AllTests::main') {
+    Horde_Text_Textile_AllTests::main();
+}
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/AttributesTest.php b/framework/Text_Textile/test/Horde/Text/Textile/AttributesTest.php
new file mode 100644 (file)
index 0000000..58e2770
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+/** Horde_Text_Textile_TestCase */
+require_once dirname(__FILE__) . '/TestCase.php';
+
+/**
+ * These tests correspond to "4. Attributes" from http://hobix.com/textile/.
+ *
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_AttributesTest extends Horde_Text_Textile_TestCase {
+
+    public function testBlockAttributes()
+    {
+        $this->assertTransforms('p(example1). An example',
+                                '<p class="example1">An example</p>');
+
+        $this->assertTransforms('p(#big-red). Red here',
+                                '<p id="big-red">Red here</p>');
+
+        $this->assertTransforms('p(example1#big-red2). Red here',
+                                '<p class="example1" id="big-red2">Red here</p>');
+
+        $this->assertTransforms('p{color:blue;margin:30px}. Spacey blue',
+                                '<p style="color:blue;margin:30px;">Spacey blue</p>');
+
+        $this->assertTransforms('p[fr]. rouge',
+                                '<p lang="fr">rouge</p>');
+    }
+
+    public function testPhraseAttributes()
+    {
+        $this->assertTransforms('I seriously *{color:red}blushed* when I _(big)sprouted_ that corn stalk from my %[es]cabeza%.',
+                                '<p>I seriously <strong style="color:red;">blushed</strong> when I <em class="big">sprouted</em> that corn stalk from my <span lang="es">cabeza</span>.</p>');
+    }
+
+    public function testBlockAlignments()
+    {
+        $this->assertTransforms('p<. align left',
+                                '<p style="text-align:left;">align left</p>');
+
+        $this->assertTransforms('p>. align right',
+                                '<p style="text-align:right;">align right</p>');
+
+        $this->assertTransforms('p=. centered',
+                                '<p style="text-align:center;">centered</p>');
+
+        $this->assertTransforms('p<>. justified',
+                                '<p style="text-align:justify;">justified</p>');
+
+        $this->assertTransforms('p(. left ident 1em',
+                                '<p style="padding-left:1em;">left ident 1em</p>');
+
+        $this->assertTransforms('p((. left ident 2em',
+                                '<p style="padding-left:2em;">left ident 2em</p>');
+
+        $this->assertTransforms('p))). right ident 3em',
+                                '<p style="padding-right:3em;">right ident 3em</p>');
+    }
+
+    public function testCombinedAlignments()
+    {
+        $this->assertTransforms('h2()>. Bingo.',
+                                '<h2 style="padding-left:1em;padding-right:1em;text-align:right;">Bingo.</h2>');
+
+        $this->assertTransforms('h3()>[no]{color:red}. Bingo',
+                                '<h3 style="color:red;padding-left:1em;padding-right:1em;text-align:right;" lang="no">Bingo</h3>');
+    }
+
+    public function testHtml()
+    {
+        $this->assertTransforms('<pre>
+<code>
+a.gsub!( /</, \'\' )
+</code>
+</pre>',
+                       '<pre>
+<code>
+a.gsub!( /&lt;/, \'\' )
+</code>
+</pre>');
+
+        $text = '<div style="float:right;">
+
+h3. Sidebar
+
+"Hobix":http://hobix.com/ "Ruby":http://ruby-lang.org/
+
+</div>
+
+The main text of the page goes here and will stay to the left of the sidebar.';
+
+        $html = '<div style="float:right;">
+
+<h3>Sidebar</h3>
+
+<p><a href="http://hobix.com/">Hobix</a> <a href="http://ruby-lang.org/">Ruby</a></p>
+
+</div>
+
+<p>The main text of the page goes here and will stay to the left of the sidebar.</p>';
+
+        $this->assertTransforms($text, $html);
+    }
+
+}
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/BlockModifiersTest.php b/framework/Text_Textile/test/Horde/Text/Textile/BlockModifiersTest.php
new file mode 100644 (file)
index 0000000..514bea1
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+/** Horde_Text_Textile_TestCase */
+require_once dirname(__FILE__) . '/TestCase.php';
+
+/**
+ * These tests correspond to "2. Quick Block Modifiers" from http://hobix.com/textile/.
+ *
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_BlockModifiersTest extends Horde_Text_Textile_TestCase {
+
+    public function testHeaders()
+    {
+        $this->assertTransforms('h1. Header 1', '<h1>Header 1</h1>', 'H1');
+        $this->assertTransforms('h2. Header 2', '<h2>Header 2</h2>', 'H2');
+        $this->assertTransforms('h3. Header 3', '<h3>Header 3</h3>', 'H3');
+    }
+
+    public function testBlockQuotes()
+    {
+        $text = "An old text
+
+bq. A block quotation.
+
+Any old text";
+        $html = "<p>An old text</p>
+
+<blockquote>
+<p>A block quotation.</p>
+</blockquote>
+
+<p>Any old text</p>";
+
+        $this->assertTransforms($text, $html);
+    }
+
+    public function testFootnotes()
+    {
+        $text = 'This is covered elsewhere[1].';
+        $html = '<p>This is covered elsewhere<sup><a id="fnr1" href="#fn1">1</a></sup>.</p>';
+        $this->assertTransforms($text, $html);
+
+        $text = 'fn1. Down here, in fact.';
+        $html = '<p id="fn1"><sup>1</sup> Down here, in fact. <a href="#fnr1">&#8617;</a></p>';
+        $this->assertTransforms($text, $html);
+    }
+
+}
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/CVS/Entries b/framework/Text_Textile/test/Horde/Text/Textile/CVS/Entries
new file mode 100644 (file)
index 0000000..b03e78a
--- /dev/null
@@ -0,0 +1,10 @@
+/AllTests.php/1.1/Wed Mar  5 20:37:52 2008//
+/AttributesTest.php/1.1/Wed Mar  5 20:37:52 2008//
+/BlockModifiersTest.php/1.2/Thu May 29 22:28:36 2008//
+/ExternalReferencesTest.php/1.1/Wed Mar  5 20:37:52 2008//
+/ListsTest.php/1.1/Wed Mar  5 20:37:52 2008//
+/PhraseModifiersTest.php/1.1/Wed Mar  5 20:37:52 2008//
+/TablesTest.php/1.1/Wed Mar  5 20:37:52 2008//
+/TestCase.php/1.1/Wed Mar  5 20:37:52 2008//
+/WritingTest.php/1.1/Wed Mar  5 20:37:52 2008//
+D
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/CVS/Repository b/framework/Text_Textile/test/Horde/Text/Textile/CVS/Repository
new file mode 100644 (file)
index 0000000..787ce38
--- /dev/null
@@ -0,0 +1 @@
+framework/Text_Textile/test/Horde/Text/Textile
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/CVS/Root b/framework/Text_Textile/test/Horde/Text/Textile/CVS/Root
new file mode 100644 (file)
index 0000000..5d63612
--- /dev/null
@@ -0,0 +1 @@
+chuck@cvs.horde.org:/repository
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/CVS/Template b/framework/Text_Textile/test/Horde/Text/Textile/CVS/Template
new file mode 100644 (file)
index 0000000..3971591
--- /dev/null
@@ -0,0 +1,8 @@
+
+Bug: 
+Submitted by: 
+Merge after:
+CVS: ----------------------------------------------------------------------
+CVS: Bug:           Fill this in if a listed bug is affected by the change.
+CVS: Submitted by:  Fill this in if someone else sent in the change.
+CVS: Merge after:   N [day[s]|week[s]|month[s]] (days assumed by default)
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/ExternalReferencesTest.php b/framework/Text_Textile/test/Horde/Text/Textile/ExternalReferencesTest.php
new file mode 100644 (file)
index 0000000..d34fc13
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+/** Horde_Text_Textile_TestCase */
+require_once dirname(__FILE__) . '/TestCase.php';
+
+/**
+ * These tests correspond to "6. External References" from http://hobix.com/textile/.
+ *
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_ExternalReferencesTest extends Horde_Text_Textile_TestCase {
+
+    public function testHypertextLinks()
+    {
+        $this->assertTransforms('I searched "Google":http://google.com.',
+                       '<p>I searched <a href="http://google.com">Google</a>.</p>');
+    }
+
+    public function testLinkAliases()
+    {
+        $this->assertTransforms('I am crazy about "Hobix":hobix and "it\'s":hobix "all":hobix I ever "link to":hobix!
+
+[hobix]http://hobix.com',
+                       '<p>I am crazy about <a href="http://hobix.com">Hobix</a> and <a href="http://hobix.com">it&#8217;s</a> <a href="http://hobix.com">all</a> I ever <a href="http://hobix.com">link to</a>!</p>
+
+');
+    }
+
+    public function testEmbeddedImages()
+    {
+        $this->assertTransforms('!http://hobix.com/sample.jpg!',
+                                '<p><img src="http://hobix.com/sample.jpg" alt="" /></p>');
+
+        $this->assertTransforms('!openwindow1.gif(Bunny.)!',
+                                '<p><img src="openwindow1.gif" title="Bunny." alt="Bunny." /></p>');
+
+        $this->assertTransforms('!openwindow1.gif!:http://hobix.com/',
+                                '<p><a href="http://hobix.com/"><img src="openwindow1.gif" alt="" /></a></p>');
+    }
+
+    public function testImageAlignments()
+    {
+        $this->assertTransforms('!>obake.gif!
+
+And others sat all round the small machine and paid it to sing to them.',
+                       '<p><img src="obake.gif" align="right" alt="" /></p>
+
+<p>And others sat all round the small machine and paid it to sing to them.</p>');
+    }
+
+    public function testAcronyms()
+    {
+        $this->assertTransforms('We use CSS(Cascading Style Sheets).',
+                                '<p>We use <acronym title="Cascading Style Sheets">CSS</acronym>.</p>');
+    }
+
+}
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/ListsTest.php b/framework/Text_Textile/test/Horde/Text/Textile/ListsTest.php
new file mode 100644 (file)
index 0000000..1c2e153
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+/** Horde_Text_Textile_TestCase */
+require_once dirname(__FILE__) . '/TestCase.php';
+
+/**
+ * These tests correspond to "5. Lists" from http://hobix.com/textile/.
+ *
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_ListsTest extends Horde_Text_Textile_TestCase {
+
+    public function testNumericList()
+    {
+        $this->assertTransforms('# A first item
+# A second item
+# A third',
+                       "\t<ol>
+\t\t<li>A first item</li>
+\t\t<li>A second item</li>
+\t\t<li>A third</li>
+\t</ol>");
+
+        $this->assertTransforms('# Fuel could be:
+## Coal
+## Gasoline
+## Electricity
+# Humans need only:
+## Water
+## Protein',
+                       "\t<ol>
+\t\t<li>Fuel could be:
+\t\t<ol>
+\t\t\t<li>Coal</li>
+\t\t\t<li>Gasoline</li>
+\t\t\t<li>Electricity</li>
+\t\t</ol></li>
+\t\t<li>Humans need only:
+\t\t<ol>
+\t\t\t<li>Water</li>
+\t\t\t<li>Protein</li>
+\t\t</ol></li>
+\t</ol>");
+    }
+
+    public function testBulletedLists()
+    {
+        $this->assertTransforms('* A first item
+* A second item
+* A third',
+                       "\t<ul>
+\t\t<li>A first item</li>
+\t\t<li>A second item</li>
+\t\t<li>A third</li>
+\t</ul>");
+
+        $this->assertTransforms('* Fuel could be:
+** Coal
+** Gasoline
+** Electricity
+* Humans need only:
+** Water
+** Protein',
+                       "\t<ul>
+\t\t<li>Fuel could be:
+\t\t<ul>
+\t\t\t<li>Coal</li>
+\t\t\t<li>Gasoline</li>
+\t\t\t<li>Electricity</li>
+\t\t</ul></li>
+\t\t<li>Humans need only:
+\t\t<ul>
+\t\t\t<li>Water</li>
+\t\t\t<li>Protein</li>
+\t\t</ul></li>
+\t</ul>");
+    }
+
+}
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/PhraseModifiersTest.php b/framework/Text_Textile/test/Horde/Text/Textile/PhraseModifiersTest.php
new file mode 100644 (file)
index 0000000..82e5db0
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+/** Horde_Text_Textile_TestCase */
+require_once dirname(__FILE__) . '/TestCase.php';
+
+/**
+ * These tests correspond to "3. Quick Phrase Modifiers" from http://hobix.com/textile/.
+ *
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_PhraseModifiersTest extends Horde_Text_Textile_TestCase {
+
+    public function testStructuralEmphasis()
+    {
+        $this->assertTransforms('I _believe_ every word.',
+                                '<p>I <em>believe</em> every word.</p>');
+
+        $this->assertTransforms('And then? She *fell*!',
+                                '<p>And then? She <strong>fell</strong>!</p>');
+
+        $this->assertTransforms("I __know__.
+I **really** __know__.",
+                       "<p>I <i>know</i>.<br />
+I <b>really</b> <i>know</i>.</p>");
+
+        $this->assertTransforms('??Cat\'s Cradle?? by Vonnegut',
+                                '<p><cite>Cat&#8217;s Cradle</cite> by Vonnegut</p>',
+                                'Citation');
+
+        $this->assertTransforms('Convert with @r.to_html@',
+                                '<p>Convert with <code>r.to_html</code></p>',
+                                'Code');
+
+        $this->assertTransforms('I\'m -sure- not sure.',
+                                '<p>I&#8217;m <del>sure</del> not sure.</p>',
+                                'Deleted Text');
+
+        $this->assertTransforms('You are a +pleasant+ child.',
+                                '<p>You are a <ins>pleasant</ins> child.</p>',
+                                'Inserted Text');
+
+        $this->assertTransforms('a ^2^ + b ^2^ = c ^2^',
+                                '<p>a <sup>2</sup> + b <sup>2</sup> = c <sup>2</sup></p>',
+                                'Superscript');
+
+        $this->assertTransforms('log ~2~ x',
+                                '<p>log <sub>2</sub> x</p>',
+                                'Subscript');
+    }
+
+    public function testHtmlAttributes()
+    {
+        $this->assertTransforms('I\'m %unaware% of most soft drinks.',
+                                '<p>I&#8217;m <span>unaware</span> of most soft drinks.</p>');
+
+        $this->assertTransforms('I\'m %{color:red}unaware% of most soft drinks.',
+                                '<p>I&#8217;m <span style="color:red;">unaware</span> of most soft drinks.</p>');
+    }
+
+}
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/TablesTest.php b/framework/Text_Textile/test/Horde/Text/Textile/TablesTest.php
new file mode 100644 (file)
index 0000000..3bc1603
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+/** Horde_Text_Textile_TestCase */
+require_once dirname(__FILE__) . '/TestCase.php';
+
+/**
+ * These tests correspond to "6. Tables" from http://hobix.com/textile/.
+ *
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_TablesTest extends Horde_Text_Textile_TestCase {
+
+    public function testSimpleTable()
+    {
+        $text = '| name | age | sex |
+| joan | 24 | f |
+| archie | 29 | m |
+| bella | 45 | f |';
+        $html = "\t<table>
+\t\t<tr>
+\t\t\t<td> name </td>
+\t\t\t<td> age </td>
+\t\t\t<td> sex </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> joan </td>
+\t\t\t<td> 24 </td>
+\t\t\t<td> f </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> archie </td>
+\t\t\t<td> 29 </td>
+\t\t\t<td> m </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> bella </td>
+\t\t\t<td> 45 </td>
+\t\t\t<td> f </td>
+\t\t</tr>
+\t</table>";
+        $this->assertTransforms($text, $html);
+    }
+
+    public function testSimpleTableHeaders()
+    {
+        $text = '|_. name |_. age |_. sex |
+| joan | 24 | f |
+| archie | 29 | m |
+| bella | 45 | f |';
+        $html = "\t<table>
+\t\t<tr>
+\t\t\t<th>name </th>
+\t\t\t<th>age </th>
+\t\t\t<th>sex </th>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> joan </td>
+\t\t\t<td> 24 </td>
+\t\t\t<td> f </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> archie </td>
+\t\t\t<td> 29 </td>
+\t\t\t<td> m </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> bella </td>
+\t\t\t<td> 45 </td>
+\t\t\t<td> f </td>
+\t\t</tr>
+\t</table>";
+        $this->assertTransforms($text, $html);
+    }
+
+    public function testCellAttributes()
+    {
+        $text = '|_. attribute list |
+|<. align left |
+|>. align right|
+|=. center |
+|<>. justify |
+|^. valign top |
+|~. bottom |';
+        $html = "\t<table>
+\t\t<tr>
+\t\t\t<th>attribute list </th>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td style=\"text-align:left;\">align left </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td style=\"text-align:right;\">align right</td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td style=\"text-align:center;\">center </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td style=\"text-align:justify;\">justify </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td style=\"vertical-align:top;\">valign top </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td style=\"vertical-align:bottom;\">bottom </td>
+\t\t</tr>
+\t</table>";
+        $this->assertTransforms($text, $html, 'Cell alignment');
+
+        $text = '|\2. spans two cols |
+| col 1 | col 2 |';
+        $html = "\t<table>
+\t\t<tr>
+\t\t\t<td colspan=\"2\">spans two cols </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> col 1 </td>
+\t\t\t<td> col 2 </td>
+\t\t</tr>
+\t</table>";
+        $this->assertTransforms($text, $html, 'Colspan');
+
+        $text = '|/3. spans 3 rows | a |
+| b |
+| c |';
+        $html = "\t<table>
+\t\t<tr>
+\t\t\t<td rowspan=\"3\">spans 3 rows </td>
+\t\t\t<td> a </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> b </td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td> c </td>
+\t\t</tr>
+\t</table>";
+        $this->assertTransforms($text, $html, 'Rowspan');
+
+        $text = '|{background:#ddd}. Grey cell|';
+        $html = "\t<table>
+\t\t<tr>
+\t\t\t<td style=\"background:#ddd;\">Grey cell</td>
+\t\t</tr>
+\t</table>";
+        $this->assertTransforms($text, $html, 'Block attributes on cells');
+    }
+
+    public function testTableRowAttributes()
+    {
+        $text = 'table{border:1px solid black}.
+|This|is|a|row|
+|This|is|a|row|';
+        $html = "\t<table style=\"border:1px solid black;\">
+\t\t<tr>
+\t\t\t<td>This</td>
+\t\t\t<td>is</td>
+\t\t\t<td>a</td>
+\t\t\t<td>row</td>
+\t\t</tr>
+\t\t<tr>
+\t\t\t<td>This</td>
+\t\t\t<td>is</td>
+\t\t\t<td>a</td>
+\t\t\t<td>row</td>
+\t\t</tr>
+\t</table>";
+        $this->assertTransforms($text, $html, 'Table-wide attributes');
+
+        $text = '|This|is|a|row|
+{background:#ddd}. |This|is|grey|row|';
+        $html = "\t<table>
+\t\t<tr>
+\t\t\t<td>This</td>
+\t\t\t<td>is</td>
+\t\t\t<td>a</td>
+\t\t\t<td>row</td>
+\t\t</tr>
+\t\t<tr style=\"background:#ddd;\">
+\t\t\t<td>This</td>
+\t\t\t<td>is</td>
+\t\t\t<td>grey</td>
+\t\t\t<td>row</td>
+\t\t</tr>
+\t</table>";
+        $this->assertTransforms($text, $html, 'Row attributes');
+    }
+
+}
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/TestCase.php b/framework/Text_Textile/test/Horde/Text/Textile/TestCase.php
new file mode 100644 (file)
index 0000000..15775b3
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+/** Horde_Text_Textile */
+require_once dirname(__FILE__) . '/../../../../lib/Horde/Text/Textile.php';
+
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_TestCase extends PHPUnit_Framework_TestCase {
+
+    public function setUp()
+    {
+        $this->textilizer = new Horde_Text_Textile;
+    }
+
+
+    public function assertTransforms($text, $html, $msg = '')
+    {
+        $this->assertEquals($html, $this->textilizer->transform($text), $msg);
+    }
+
+}
diff --git a/framework/Text_Textile/test/Horde/Text/Textile/WritingTest.php b/framework/Text_Textile/test/Horde/Text/Textile/WritingTest.php
new file mode 100755 (executable)
index 0000000..029bb0c
--- /dev/null
@@ -0,0 +1,68 @@
+<?php
+/**
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+
+/** Horde_Text_Textile_TestCase */
+require_once dirname(__FILE__) . '/TestCase.php';
+
+/**
+ * These tests correspond to "1. Writing in Textile" from http://hobix.com/textile/.
+ *
+ * @category   Horde
+ * @package    Horde_Text_Textile
+ * @subpackage UnitTests
+ */
+class Horde_Text_Textile_WritingTest extends Horde_Text_Textile_TestCase {
+
+    public function testParagraphs()
+    {
+        $text = "A single paragraph.
+
+Followed by another.";
+        $html = "<p>A single paragraph.</p>
+
+<p>Followed by another.</p>";
+
+        $this->assertTransforms($text, $html);
+    }
+
+    public function testHtml()
+    {
+        $text = "I am <b>very</b> serious.
+
+<pre>
+I am <b>very</b> serious.
+</pre>";
+        $html = "<p>I am <b>very</b> serious.</p>
+
+<pre>
+I am &lt;b&gt;very&lt;/b&gt; serious.
+</pre>";
+
+        $this->assertTransforms($text, $html);
+    }
+
+    public function testLinebreaks()
+    {
+        $text = "I spoke.
+And none replied.";
+        $html = "<p>I spoke.<br />
+And none replied.</p>";
+
+        $this->assertTransforms($text, $html);
+    }
+
+    public function testEntities()
+    {
+        $this->assertTransforms('"Observe!"', '<p>&#8220;Observe!&#8221;</p>', 'Curly Quotes');
+        $this->assertTransforms('Observe -- very nice!', '<p>Observe&#8212;very nice!</p>', 'Em Dash');
+        $this->assertTransforms('Observe - tiny and brief.', '<p>Observe &#8211; tiny and brief.</p>', 'En Dash');
+        $this->assertTransforms('Observe...', '<p>Observe&#8230;</p>', 'Ellipsis');
+        $this->assertTransforms('Observe: 2 x 2.', '<p>Observe: 2&#215;2.</p>', 'Dimension');
+        $this->assertTransforms('one(TM), two(R), three(C).', '<p>one&#8482;, two&#174;, three&#169;.</p>', 'Trademark, Registered, Copyright');
+    }
+
+}