This is a result of several hours' hard-wording, so please keep my name there if possible when you are using it.

Test case:

 echo htmlEscape(closeHtmlTags("<a>a")).'<br />'; // should be closed.
 echo htmlEscape
(closeHtmlTags("<_a>a")).'<br />'; // _a is not a valid html tag.
 echo htmlEscape
(closeHtmlTags("<a_>a")).'<br />'; // a_ is a possible html tag.
 echo htmlEscape
(closeHtmlTags("a")).'<br />';
 echo htmlEscape
(closeHtmlTags("<a>a<")).'<br />'; // Recognize last '<' as close tag.
 echo htmlEscape
(closeHtmlTags("<a>a</")).'<br />';
 echo htmlEscape
(closeHtmlTags("<a>a</a")).'<br />';
 echo htmlEscape
(closeHtmlTags("<a>a</a>")).'<br />';
 echo htmlEscape
(closeHtmlTags("<")).'<br />';
 echo htmlEscape
(closeHtmlTags("<a><img href=\"<aaa c='s>.jpg\"><img><br ><br><br/><br//>a")).'<br />';
 echo htmlEscape
(closeHtmlTags('<p href=">"><div><p><div><p>aa</p r="2>"><div>arr')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><p2><x><p3><p4>m</x>aa')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><p2><x><p3><p4>m<')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><p2><x><p3><p4>m</')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><p2><x><p3><p4>m</p')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><p2><x><p3><p4>m</p4 rel=">\'>')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><p2><x><p3><p4>m</x')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><p2><xx><p3><p4>m</x')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><script><xx>r')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><script><xx>r<')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><script><xx>r</')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><script><xx>r</scr')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><script><xx>r</script m="5>"')).'<br />';
 echo htmlEscape
(closeHtmlTags('<p1><script><xx>r</script m="5>">')).'<br />';
In the above test case, the htmlEscape is a function to escape "<" to "&lt;" etc.

// By David <david@24k.com.sg>
function htmlEscape($src) {
 $htmlReplaceTable
= array("\n"=>"<br/>","&"=>"&amp;","<"=>"&lt;",">"=>"&gt;","\r"=>"", "\""=>"&quot;");
 $src
= "$src";
 $dst
= "";
 
for ($index = 0, $indexMax = strlen($src); $index < $indexMax; $index++) {
  $char
= $src[$index];
 
if (isset($htmlReplaceTable[$char])) {
   $dst
= $dst . $htmlReplaceTable[$char];
 
} else {
   $dst
= $dst . $char;
 
}
 
}
 
return $dst;
}

 

The function:

  1. <?php
  2. // By David <david@24k.com.sg>
  3. function closeHtmlTags($html) {
  4.   $arr_single_tags = array('meta', 'img', 'br', 'link', 'area', 'hr', 'input', '!');
  5.   $at = 0;
  6.   $end = strlen($html);
  7.   $isInQuote1 = false;
  8.   $isInQuote2 = false;
  9.   $isInTag = false;
  10.   $isInOpeningTag = false;
  11.   $isReadingTag = false;
  12.   $tagClosing = array();
  13.   $tagClosingCount = 0;
  14.   while ($at < $end) {
  15.     $char = $html{$at};
  16.     if ($char == '<') {
  17.       if ($isInQuote1) {
  18.         // Pass
  19.       } else if ($isInQuote2) {
  20.         // Pass
  21.       } else if ($isInTag) {
  22.         // Pass
  23.       } else {
  24.         if ($at == $end - 1) {
  25.           if ($tagClosingCount) {
  26.             $html .= "/";
  27.             $isInTag = true;
  28.             $isInOpeningTag = false;
  29.             $isReadingTag = true;
  30.             $tagCurr = '';
  31.           } else {
  32.             $html .= " />";
  33.           }
  34.           break;
  35.         } else {
  36.           $charNext = $html{++$at};
  37.           if (($charNext >= 'a' && $charNext <= 'z') || ($charNext >= 'A' && $charNext <= 'Z') || ($charNext == '!')) {
  38.             $isInTag = true;
  39.             $isInOpeningTag = true;
  40.             $isReadingTag = $charNext != '!';
  41.             $tagCurr = $charNext;
  42.           } else if ($charNext == '/') {
  43.             if ($at == $end - 1) {
  44.               $isInTag = true;
  45.               $isInOpeningTag = false;
  46.               $isReadingTag = true;
  47.               $tagCurr = '';
  48.               break;
  49.             } else {
  50.               $charNext = $html{++$at};
  51.               if (($charNext >= 'a' && $charNext <= 'z') || ($charNext >= 'A' && $charNext <= 'Z')) {
  52.                 $isInTag = true;
  53.                 $isInOpeningTag = false;
  54.                 $isReadingTag = true;
  55.                 $tagCurr = $charNext;
  56.               } else {
  57.                 // Pass
  58.               }
  59.             }
  60.           } else {
  61.             // Pass
  62.           }
  63.         }
  64.       }
  65.     } else if ($char == '>') {
  66.       if ($isInQuote1) {
  67.         // Pass
  68.       } else if ($isInQuote2) {
  69.         // Pass
  70.       } else if (!$isInTag) {
  71.         // Pass
  72.       } else {
  73.         $isInTag = false;
  74.         $isReadingTag = false;
  75.         $tagCurr = strtolower($tagCurr);
  76.         if ($isInOpeningTag) {
  77.           if ($tagCurr === "script") {
  78.             $pos = stripos($html, "</script", $at);
  79.             if ($pos === false) {
  80.               $len = strlen($html);
  81.               if (!strcmp(strtolower(substr($html, $len - 1)), "<")) {
  82.                 $html .= "/script>";
  83.               } else if (!strcmp(strtolower(substr($html, $len - 2)), "</")) {
  84.                 $html .= "script>";
  85.               } else if (!strcmp(strtolower(substr($html, $len - 3)), "</s")) {
  86.                 $html .= "cript>";
  87.               } else if (!strcmp(strtolower(substr($html, $len - 4)), "</sc")) {
  88.                 $html .= "ript>";
  89.               } else if (!strcmp(strtolower(substr($html, $len - 5)), "</scr")) {
  90.                 $html .= "ipt>";
  91.               } else if (!strcmp(strtolower(substr($html, $len - 6)), "</scri")) {
  92.                 $html .= "pt>";
  93.               } else if (!strcmp(strtolower(substr($html, $len - 7)), "</scrip")) {
  94.                 $html .= "t>";
  95.               } else if (!strcmp(strtolower(substr($html, $len - 8)), "</script")) {
  96.                 $html .= ">";
  97.               } else {
  98.                 $html .= "</script>";
  99.               }
  100.               break;
  101.             } else {
  102.               $at = $pos + 8;
  103.               array_push($tagClosing, "script");
  104.               $tagClosingCount++;
  105.               $isInTag = true;
  106.               $isInOpeningTag = false;
  107.               $isReadingTag = false;
  108.               $tagCurr = "script";
  109.             }
  110.           } else if (in_array($tagCurr, $arr_single_tags, true)) {
  111.             // Pass
  112.           } else {
  113.             array_push($tagClosing, $tagCurr);
  114.             $tagClosingCount++;
  115.           }
  116.         } else {
  117.           if ($tagClosingCount && $tagClosing[$tagClosingCount - 1] === $tagCurr) {
  118.             array_pop($tagClosing);
  119.             $tagClosingCount--;
  120.           } else {
  121.             $tagAt = $tagClosingCount - 2;
  122.             while ($tagAt >= 0) {
  123.               if ($tagClosing[$tagAt] === $tagCurr) {
  124.                 break;
  125.               }
  126.               $tagAt--;
  127.             }
  128.             if ($tagAt >= 0) {
  129.               $tagClosingCount--;
  130.               while ($tagAt < $tagClosingCount) {
  131.                 $tagAt2 = $tagAt + 1;
  132.                 $tagClosing[$tagAt] = $tagClosing[$tagAt2];
  133.                 $tagAt = $tagAt2;
  134.               }
  135.               array_pop($tagClosing);
  136.             } else {
  137.               // Pass
  138.             }
  139.           }
  140.         }
  141.       }
  142.     } else if ($char == '"') {
  143.       if ($isInQuote1) {
  144.         $isInQuote1 = false;
  145.       } else if ($isInQuote2) {
  146.         // Pass
  147.       } else if ($isInTag) {
  148.         $isReadingTag = false;
  149.         $isInQuote1 = true;
  150.       } else {
  151.         // Pass
  152.       }
  153.     } else if ($char == "'") {
  154.       if ($isInQuote1) {
  155.         // Pass
  156.       } else if ($isInQuote2) {
  157.         $isInQuote2 = false;
  158.       } else if ($isInTag) {
  159.         $isReadingTag = false;
  160.         $isInQuote2 = true;
  161.       } else {
  162.         // Pass
  163.       }
  164.     } else if (($char >= 'a' && $char <= 'z') || ($char >= 'A' && $char <= 'Z') || ($char == "_") || ($char >= '0' && $char <= '9')) {
  165.       if ($isInQuote1) {
  166.         // Pass
  167.       } else if ($isInQuote2) {
  168.         // Pass
  169.       } else if ($isInTag) {
  170.         if ($isReadingTag) {
  171.           $tagCurr .= $char;
  172.         } else {
  173.           // Pass
  174.         }
  175.       } else {
  176.         // Pass
  177.       }
  178.     } else {
  179.       if ($isInQuote1) {
  180.         // Pass
  181.       } else if ($isInQuote2) {
  182.         // Pass
  183.       } else if ($isInTag) {
  184.         $isReadingTag = false;
  185.       } else {
  186.         // Pass
  187.       }
  188.     }
  189.     $at++;
  190.   }
  191.   if ($isInQuote1) {
  192.     $html .= '"';
  193.   }
  194.   if ($isInQuote2) {
  195.     $html .= "'";
  196.   }
  197.   if ($isInTag) {
  198.     if ($isInOpeningTag) {
  199.       $html .= "/>";
  200.     } else {
  201.       $tagCurr = strtolower($tagCurr);
  202.       $tagCurrLen = strlen($tagCurr);
  203.       if ($tagClosingCount && !strncmp($tagClosing[$tagClosingCount - 1], $tagCurr, $tagCurrLen)) {
  204.         if (strlen($tagClosing[$tagClosingCount - 1]) != $tagCurrLen) {
  205.           $html .= substr($tagClosing[$tagClosingCount - 1], $tagCurrLen);
  206.         }
  207.         $html .= ">";
  208.         array_pop($tagClosing);
  209.         $tagClosingCount--;
  210.       } else {
  211.         $tagAt = $tagClosingCount - 2;
  212.         while ($tagAt >= 0) {
  213.           if (!strncmp($tagClosing[$tagAt], $tagCurr, $tagCurrLen)) {
  214.             break;
  215.           }
  216.           $tagAt--;
  217.         }
  218.         if ($tagAt >= 0) {
  219.           if (strlen($tagClosing[$tagAt]) != $tagCurrLen) {
  220.             $html .= substr($tagClosing[$tagAt], $tagCurrLen);
  221.           }
  222.           $html .= ">";
  223.           $tagClosingCount--;
  224.           while ($tagAt < $tagClosingCount) {
  225.             $tagAt2 = $tagAt + 1;
  226.             $tagClosing[$tagAt] = $tagClosing[$tagAt2];
  227.             $tagAt = $tagAt2;
  228.           }
  229.           array_pop($tagClosing);
  230.         } else {
  231.           // Pass
  232.         }
  233.       }
  234.     }
  235.   }
  236.   while (--$tagClosingCount >= 0) {
  237.     $html .= "</{$tagClosing[$tagClosingCount]}>";
  238.   }
  239.   return $html;
  240. }
  241. ?>