<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>art/100cases.png</filename>
    </added>
    <added>
      <filename>tests/HTMLPurifier/AttrValidator_ErrorsTest.php</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -18,8 +18,8 @@ NEWS ( CHANGELOG and HISTORY )                                     HTMLPurifier
 ! Newlines normalized internally, and then converted back to the
   value of PHP_EOL. If this is not desired, set your newline format
   using %Output.Newline.
-! Beta error collection, messages are implemented for Lexer and
-  RemoveForeignElements
+! Beta error collection, messages are implemented for the most generic
+  cases involving Lexing or Strategies
 - Clean up special case code for &lt;script&gt; tags
 - Reorder includes for DefinitionCache decorators, fixes a possible
   missing class error</diff>
      <filename>NEWS</filename>
    </modified>
    <modified>
      <diff>@@ -1,12 +1,31 @@
 &lt;?php
 
+/**
+ * Validates the attributes of a token. Doesn't manage required attributes
+ * very well. The only reason we factored this out was because RemoveForeignElements
+ * also needed it besides ValidateAttributes.
+ */
 class HTMLPurifier_AttrValidator
 {
     
-    
-    function validateToken($token, &amp;$config, &amp;$context) {
+    /**
+     * Validates the attributes of a token, returning a modified token
+     * that has valid tokens
+     * @param $token Reference to token to validate. We require a reference
+     *     because the operation this class performs on the token are
+     *     not atomic, so the context CurrentToken to be updated
+     *     throughout
+     * @param $config Instance of HTMLPurifier_Config
+     * @param $context Instance of HTMLPurifier_Context
+     */
+    function validateToken(&amp;$token, &amp;$config, &amp;$context) {
             
         $definition = $config-&gt;getHTMLDefinition();
+        $e =&amp; $context-&gt;get('ErrorCollector', true);
+        
+        // initialize CurrentToken if necessary
+        $current_token =&amp; $context-&gt;get('CurrentToken', true);
+        if (!$current_token) $context-&gt;register('CurrentToken', $token);
         
         if ($token-&gt;type !== 'start' &amp;&amp; $token-&gt;type !== 'empty') return $token;
         
@@ -14,21 +33,21 @@ class HTMLPurifier_AttrValidator
         // DEFINITION CALL
         $d_defs = $definition-&gt;info_global_attr;
         
-        // copy out attributes for easy manipulation
-        $attr = $token-&gt;attr;
+        // reference attributes for easy manipulation
+        $attr =&amp; $token-&gt;attr;
         
         // do global transformations (pre)
         // nothing currently utilizes this
         foreach ($definition-&gt;info_attr_transform_pre as $transform) {
-            $attr = $transform-&gt;transform($attr, $config, $context);
+            $attr = $transform-&gt;transform($o = $attr, $config, $context);
+            if ($e &amp;&amp; ($attr != $o)) $e-&gt;send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
         }
         
         // do local transformations only applicable to this element (pre)
         // ex. &lt;p align=&quot;right&quot;&gt; to &lt;p style=&quot;text-align:right;&quot;&gt;
-        foreach ($definition-&gt;info[$token-&gt;name]-&gt;attr_transform_pre
-            as $transform
-        ) {
-            $attr = $transform-&gt;transform($attr, $config, $context);
+        foreach ($definition-&gt;info[$token-&gt;name]-&gt;attr_transform_pre as $transform) {
+            $attr = $transform-&gt;transform($o = $attr, $config, $context);
+            if ($e &amp;&amp; ($attr != $o)) $e-&gt;send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
         }
         
         // create alias to this element's attribute definition array, see
@@ -36,6 +55,9 @@ class HTMLPurifier_AttrValidator
         // DEFINITION CALL
         $defs = $definition-&gt;info[$token-&gt;name]-&gt;attr;
         
+        $attr_key = false;
+        $context-&gt;register('CurrentAttr', $attr_key);
+        
         // iterate through all the attribute keypairs
         // Watch out for name collisions: $key has previously been used
         foreach ($attr as $attr_key =&gt; $value) {
@@ -69,9 +91,17 @@ class HTMLPurifier_AttrValidator
             
             // put the results into effect
             if ($result === false || $result === null) {
+                // this is a generic error message that should replaced
+                // with more specific ones when possible
+                if ($e) $e-&gt;send(E_ERROR, 'AttrValidator: Attribute removed');
+                
                 // remove the attribute
                 unset($attr[$attr_key]);
             } elseif (is_string($result)) {
+                // generally, if a substitution is happening, there
+                // was some sort of implicit correction going on. We'll
+                // delegate it to the attribute classes to say exactly what.
+                
                 // simple substitution
                 $attr[$attr_key] = $result;
             }
@@ -83,21 +113,24 @@ class HTMLPurifier_AttrValidator
             // others would prepend themselves).
         }
         
+        $context-&gt;destroy('CurrentAttr');
+        
         // post transforms
         
-        // ex. &lt;x lang=&quot;fr&quot;&gt; to &lt;x lang=&quot;fr&quot; xml:lang=&quot;fr&quot;&gt;
+        // global (error reporting untested)
         foreach ($definition-&gt;info_attr_transform_post as $transform) {
-            $attr = $transform-&gt;transform($attr, $config, $context);
+            $attr = $transform-&gt;transform($o = $attr, $config, $context);
+            if ($e &amp;&amp; ($attr != $o)) $e-&gt;send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
         }
         
-        // ex. &lt;bdo&gt; to &lt;bdo dir=&quot;ltr&quot;&gt;
+        // local (error reporting untested)
         foreach ($definition-&gt;info[$token-&gt;name]-&gt;attr_transform_post as $transform) {
-            $attr = $transform-&gt;transform($attr, $config, $context);
+            $attr = $transform-&gt;transform($o = $attr, $config, $context);
+            if ($e &amp;&amp; ($attr != $o)) $e-&gt;send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr);
         }
         
-        // commit changes
-        $token-&gt;attr = $attr;
-        return $token;
+        // destroy CurrentToken if we made it ourselves
+        if (!$current_token) $context-&gt;destroy('CurrentToken');
         
     }
     </diff>
      <filename>library/HTMLPurifier/AttrValidator.php</filename>
    </modified>
    <modified>
      <diff>@@ -26,9 +26,10 @@ class HTMLPurifier_ErrorCollector
      * @param $severity int Error severity, PHP error style (don't use E_USER_)
      * @param $msg string Error message text
      */
-    function send($severity, $msg, $args = array()) {
+    function send($severity, $msg) {
         
-        if (!is_array($args)) {
+        $args = array();
+        if (func_num_args() &gt; 2) {
             $args = func_get_args();
             array_shift($args);
             unset($args[0]);
@@ -94,7 +95,7 @@ class HTMLPurifier_ErrorCollector
         foreach ($errors as $error) {
             list($line, $severity, $msg) = $error;
             $string = '';
-            $string .= $this-&gt;locale-&gt;getErrorName($severity) . ': ';
+            $string .= '&lt;strong&gt;' . $this-&gt;locale-&gt;getErrorName($severity) . '&lt;/strong&gt;: ';
             $string .= $this-&gt;generator-&gt;escape($msg); 
             if ($line) {
                 // have javascript link generation that causes </diff>
      <filename>library/HTMLPurifier/ErrorCollector.php</filename>
    </modified>
    <modified>
      <diff>@@ -79,6 +79,25 @@ class HTMLPurifier_Language
     }
     
     /**
+     * Converts an array list into a string readable representation
+     */
+    function listify($array) {
+        $sep      = $this-&gt;getMessage('Item separator');
+        $sep_last = $this-&gt;getMessage('Item separator last');
+        $ret = '';
+        for ($i = 0, $c = count($array); $i &lt; $c; $i++) {
+            if ($i == 0) {
+            } elseif ($i + 1 &lt; $c) {
+                $ret .= $sep;
+            } else {
+                $ret .= $sep_last;
+            }
+            $ret .= $array[$i];
+        }
+        return $ret;
+    }
+    
+    /**
      * Formats a localised message with passed parameters
      * @param $key string identifier of message
      * @param $args Parameters to substitute in
@@ -94,22 +113,35 @@ class HTMLPurifier_Language
         $generator = false;
         foreach ($args as $i =&gt; $value) {
             if (is_object($value)) {
-                // complicated stuff
-                if (!$generator) $generator = $this-&gt;context-&gt;get('Generator');
-                // assuming it's a token
-                if (isset($value-&gt;name)) $subst['$'.$i.'.Name'] = $value-&gt;name;
-                if (isset($value-&gt;data)) $subst['$'.$i.'.Data'] = $value-&gt;data;
-                $subst['$'.$i.'.Compact'] = 
-                $subst['$'.$i.'.Serialized'] = $generator-&gt;generateFromToken($value);
-                // a more complex algorithm for compact representation
-                // could be introduced for all types of tokens. This
-                // may need to be factored out into a dedicated class
-                if (!empty($value-&gt;attr)) {
-                    $stripped_token = $value-&gt;copy();
-                    $stripped_token-&gt;attr = array();
-                    $subst['$'.$i.'.Compact'] = $generator-&gt;generateFromToken($stripped_token);
+                if (is_a($value, 'HTMLPurifier_Token')) {
+                    // factor this out some time
+                    if (!$generator) $generator = $this-&gt;context-&gt;get('Generator');
+                    if (isset($value-&gt;name)) $subst['$'.$i.'.Name'] = $value-&gt;name;
+                    if (isset($value-&gt;data)) $subst['$'.$i.'.Data'] = $value-&gt;data;
+                    $subst['$'.$i.'.Compact'] = 
+                    $subst['$'.$i.'.Serialized'] = $generator-&gt;generateFromToken($value);
+                    // a more complex algorithm for compact representation
+                    // could be introduced for all types of tokens. This
+                    // may need to be factored out into a dedicated class
+                    if (!empty($value-&gt;attr)) {
+                        $stripped_token = $value-&gt;copy();
+                        $stripped_token-&gt;attr = array();
+                        $subst['$'.$i.'.Compact'] = $generator-&gt;generateFromToken($stripped_token);
+                    }
+                    $subst['$'.$i.'.Line'] = $value-&gt;line ? $value-&gt;line : 'unknown';
+                }
+                continue;
+            } elseif (is_array($value)) {
+                $keys = array_keys($value);
+                if (array_keys($keys) === $keys) {
+                    // list
+                    $subst['$'.$i] = $this-&gt;listify($value);
+                } else {
+                    // associative array
+                    // no $i implementation yet, sorry
+                    $subst['$'.$i.'.Keys'] = $this-&gt;listify($keys);
+                    $subst['$'.$i.'.Values'] = $this-&gt;listify(array_values($value));
                 }
-                $subst['$'.$i.'.Line'] = $value-&gt;line ? $value-&gt;line : 'unknown';
                 continue;
             }
             $subst['$' . $i] = $value;</diff>
      <filename>library/HTMLPurifier/Language.php</filename>
    </modified>
    <modified>
      <diff>@@ -5,7 +5,14 @@ $fallback = false;
 $messages = array(
 
 'HTMLPurifier' =&gt; 'HTML Purifier',
-'LanguageFactoryTest: Pizza' =&gt; 'Pizza', // for unit testing purposes
+
+// for unit testing purposes
+'LanguageFactoryTest: Pizza' =&gt; 'Pizza',
+'LanguageTest: List' =&gt; '$1',
+'LanguageTest: Hash' =&gt; '$1.Keys; $1.Values',
+
+'Item separator' =&gt; ', ',
+'Item separator last' =&gt; ' and ', // non-Harvard style
 
 'ErrorCollector: No errors' =&gt; 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.',
 'ErrorCollector: At line' =&gt; ' at line $line',
@@ -37,6 +44,9 @@ $messages = array(
 'Strategy_FixNesting: Node reorganized'      =&gt; 'Contents of $CurrentToken.Compact node reorganized to enforce its content model',
 'Strategy_FixNesting: Node contents removed' =&gt; 'Contents of $CurrentToken.Compact node removed',
 
+'AttrValidator: Attributes transformed' =&gt; 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys',
+'AttrValidator: Attribute removed' =&gt; '$CurrentAttr.Name attribute on $CurrentToken.Compact removed',
+
 );
 
 $errorNames = array(</diff>
      <filename>library/HTMLPurifier/Language/messages/en.php</filename>
    </modified>
    <modified>
      <diff>@@ -91,7 +91,7 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
                         $definition-&gt;info[$token-&gt;name]-&gt;required_attr &amp;&amp;
                         ($token-&gt;name != 'img' || $remove_invalid_img) // ensure config option still works
                     ) {
-                        $token = $attr_validator-&gt;validateToken($token, $config, $context);
+                        $attr_validator-&gt;validateToken($token, $config, $context);
                         $ok = true;
                         foreach ($definition-&gt;info[$token-&gt;name]-&gt;required_attr as $name) {
                             if (!isset($token-&gt;attr[$name])) {</diff>
      <filename>library/HTMLPurifier/Strategy/RemoveForeignElements.php</filename>
    </modified>
    <modified>
      <diff>@@ -27,6 +27,9 @@ class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
         // setup validator
         $validator = new HTMLPurifier_AttrValidator();
         
+        $token = false;
+        $context-&gt;register('CurrentToken', $token);
+        
         foreach ($tokens as $key =&gt; $token) {
             
             // only process tokens that have attributes,
@@ -36,7 +39,8 @@ class HTMLPurifier_Strategy_ValidateAttributes extends HTMLPurifier_Strategy
             // skip tokens that are armored
             if (!empty($token-&gt;armor['ValidateAttributes'])) continue;
             
-            $tokens[$key] = $validator-&gt;validateToken($token, $config, $context);
+            // note that we have no facilities here for removing tokens
+            $validator-&gt;validateToken($token, $config, $context);
         }
         
         $context-&gt;destroy('IDAccumulator');</diff>
      <filename>library/HTMLPurifier/Strategy/ValidateAttributes.php</filename>
    </modified>
    <modified>
      <diff>@@ -45,8 +45,8 @@ class HTMLPurifier_ErrorCollectorTest extends UnitTestCase
         $this-&gt;assertIdentical($collector-&gt;getRaw(), $result);
         
         $formatted_result = 
-            '&lt;ul&gt;&lt;li&gt;Warning: Message 2 at line 3&lt;/li&gt;'.
-            '&lt;li&gt;Error: Message 1 at line 23&lt;/li&gt;&lt;/ul&gt;';
+            '&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Warning&lt;/strong&gt;: Message 2 at line 3&lt;/li&gt;'.
+            '&lt;li&gt;&lt;strong&gt;Error&lt;/strong&gt;: Message 1 at line 23&lt;/li&gt;&lt;/ul&gt;';
         
         $config = HTMLPurifier_Config::create(array('Core.MaintainLineNumbers' =&gt; true));
         
@@ -91,8 +91,8 @@ class HTMLPurifier_ErrorCollectorTest extends UnitTestCase
         $this-&gt;assertIdentical($collector-&gt;getRaw(), $result);
         
         $formatted_result = 
-            '&lt;ul&gt;&lt;li&gt;Error: Message 1&lt;/li&gt;'.
-            '&lt;li&gt;Error: Message 2&lt;/li&gt;&lt;/ul&gt;';
+            '&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Error&lt;/strong&gt;: Message 1&lt;/li&gt;'.
+            '&lt;li&gt;&lt;strong&gt;Error&lt;/strong&gt;: Message 2&lt;/li&gt;&lt;/ul&gt;';
         $config = HTMLPurifier_Config::createDefault();
         $this-&gt;assertIdentical($collector-&gt;getHTMLFormatted($config), $formatted_result);
     }</diff>
      <filename>tests/HTMLPurifier/ErrorCollectorTest.php</filename>
    </modified>
    <modified>
      <diff>@@ -7,6 +7,13 @@ class HTMLPurifier_LanguageTest extends UnitTestCase
     
     var $lang;
     
+    function generateEnLanguage() {
+        $factory = HTMLPurifier_LanguageFactory::instance();
+        $config = HTMLPurifier_Config::create(array('Core.Language' =&gt; 'en'));
+        $context = new HTMLPurifier_Context();
+        return $factory-&gt;create($config, $context);
+    }
+    
     function test_getMessage() {
         $config = HTMLPurifier_Config::createDefault();
         $context = new HTMLPurifier_Context();
@@ -26,7 +33,7 @@ class HTMLPurifier_LanguageTest extends UnitTestCase
         $this-&gt;assertIdentical($lang-&gt;formatMessage('LanguageTest: Error', array(1=&gt;'fatal', 32)), 'Error is fatal on line 32');
     }
     
-    function test_formatMessage_complexParameter() {
+    function test_formatMessage_tokenParameter() {
         $config = HTMLPurifier_Config::createDefault();
         $context = new HTMLPurifier_Context();
         $generator = new HTMLPurifier_Generator(); // replace with mock if this gets icky
@@ -43,6 +50,29 @@ class HTMLPurifier_LanguageTest extends UnitTestCase
             'Data Token: data&gt;, data&amp;gt;, data&amp;gt;, 23');
     }
     
+    function test_listify() {
+        $lang = $this-&gt;generateEnLanguage();
+        $this-&gt;assertEqual($lang-&gt;listify(array('Item')), 'Item');
+        $this-&gt;assertEqual($lang-&gt;listify(array('Item', 'Item2')), 'Item and Item2');
+        $this-&gt;assertEqual($lang-&gt;listify(array('Item', 'Item2', 'Item3')), 'Item, Item2 and Item3');
+    }
+    
+    function test_formatMessage_arrayParameter() {
+        $lang = $this-&gt;generateEnLanguage();
+        
+        $array = array('Item1', 'Item2', 'Item3');
+        $this-&gt;assertIdentical(
+            $lang-&gt;formatMessage('LanguageTest: List', array(1=&gt;$array)),
+            'Item1, Item2 and Item3'
+        );
+        
+        $array = array('Key1' =&gt; 'Value1', 'Key2' =&gt; 'Value2');
+        $this-&gt;assertIdentical(
+            $lang-&gt;formatMessage('LanguageTest: Hash', array(1=&gt;$array)),
+            'Key1 and Key2; Value1 and Value2'
+        );
+    }
+    
 }
 
 ?&gt;
\ No newline at end of file</diff>
      <filename>tests/HTMLPurifier/LanguageTest.php</filename>
    </modified>
    <modified>
      <diff>@@ -52,6 +52,7 @@ $test_files[] = 'HTMLPurifier/AttrTransform/LangTest.php';
 $test_files[] = 'HTMLPurifier/AttrTransform/LengthTest.php';
 $test_files[] = 'HTMLPurifier/AttrTransform/NameTest.php';
 $test_files[] = 'HTMLPurifier/AttrTypesTest.php';
+$test_files[] = 'HTMLPurifier/AttrValidator_ErrorsTest.php';
 $test_files[] = 'HTMLPurifier/ChildDef/ChameleonTest.php';
 $test_files[] = 'HTMLPurifier/ChildDef/CustomTest.php';
 $test_files[] = 'HTMLPurifier/ChildDef/OptionalTest.php';</diff>
      <filename>tests/test_files.php</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>a005da8a4ca7a39028ee8286c7a76dc0bde5941d</id>
    </parent>
  </parents>
  <author>
    <name>Edward Z. Yang</name>
    <email>edwardzyang@thewritingpot.com</email>
  </author>
  <url>http://github.com/ezyang/htmlpurifier/commit/3a1d505b3dd639d453b7e8a5c4dd876d870609cf</url>
  <id>3a1d505b3dd639d453b7e8a5c4dd876d870609cf</id>
  <committed-date>2007-06-26T19:03:15-07:00</committed-date>
  <authored-date>2007-06-26T19:03:15-07:00</authored-date>
  <message>[2.0.1] Implement haphazard error collection for AttrValidator.
- Error collector / Language can take arrays and listify them
- AttrValidator takes token by reference
- Formatted errors now have their severity &lt;strong&gt;
- 100 test-cases! W00t!

git-svn-id: http://htmlpurifier.org/svnroot/htmlpurifier/trunk@1250 48356398-32a2-884e-a903-53898d9a118a</message>
  <tree>cc78176459ee56086c275ec87510fc1a29e3f067</tree>
  <committer>
    <name>Edward Z. Yang</name>
    <email>edwardzyang@thewritingpot.com</email>
  </committer>
</commit>
