Uma tarefa importante e frequente na publicação na Internet é a conversão de textos para HTML.
Não esperamos que o usuário tenha algum conhecimento do HTML. Além disto, é um trabalho muito massante escrever os textos em meio à uma profusão de marcações – que atrapalham inclusive alguma alteração posterior.
Ao bom estilo “wiki”, vamos criar uma função que reconhece os parágrafos, inserindo automaticamente as marcações para os títulos, listas e alguma coisa mais.
Para “potencializar” ainda mais o nosso formatador, vamos usar a função para filtrar o HTML que criamos anteriormente – filter_html().
Pode acontecer que, ao submeter um texto em um formulário, as linhas sejam quebradas em lugares indesejados. Para evitar confusão, vamos considerar como separação de parágrafo, uma linha em branco.
Nossa função irá pegar o primeiro caractére do parágrafo e fazer uma análize. Porém, algumas marcações poderão abranger múltiplos parágrafos, por isto, nosso método de percorrer os parágrafos não é o mais simples: ao invés de usarmos um simples explode(), que poderia nos entregar os parágrafos em um array, utilizaremos um ponteiro para procurar os parágrafos e os recortaremos um a um.
Teremos um conjunto de marcações padrão para serem utilizadas durante a formatação, mas aceitaremos como parâmetro marcações alternativas que, se forem informadas, serão utilizadas no lugar das marcações padrão.
Vamos à função :
<?php function text2html ($string, $filter_html_mode=1, $alternative_marks=false) { // text2html // As marcações utilizadas por padrão static $default_marks = array ( 'p_open'=>'<p>', 'p_close'=>'</p>', 'h1_open'=>'<h1>', 'h1_close'=>'</h1>', 'h2_open'=>'<h2>', 'h2_close'=>'</h2>', 'h3_open'=>'<h3>', 'h3_close'=>'</h3>', 'h4_open'=>'<h4>', 'h4_close'=>'</h4>', 'h5_open'=>'<h5>', 'h5_close'=>'</h5>', 'h6_open'=>'<h6>', 'h6_close'=>'</h6>', 'ul_open'=>'<ul>', 'ul_close'=>'</ul>', 'ol_open'=>'<ol>', 'ol_close'=>'</ol>', 'al_open'=>'<ol style="list-style-type:lower-latin">', 'al_close'=>'</ol>', 'li_open'=>'<li>', 'li_close'=>'</li>', 'pre_open'=>'<pre>', 'pre_close'=>'</pre>', 'code_open'=>'<code><pre>', 'code_close'=>'</pre></code>', 'style_open'=>'<style type="text/css">', 'style_close'=>'</style>', 'script_open'=>'<script type="text/javascript">', 'script_close'=>'</script>', 'hr'=>'<hr>' ); // Permite marcações alternativas is_array ($alternative_marks)? $marks = array_merge ($default_marks, $alternative_marks) : $marks = $default_marks; // Converte quebras de linhas Windows para Linux $string = str_replace ("\r\n", "\n", $string); // o cumprimento total da string $strlen = strlen ($string); // Os ponteiros indicando o início e o fim de um parágrafo $paragraph_start = 0; $paragraph_end = 0; // O resultado final $result = ''; // enquanto o ponteiro não alcançar o final da string while ($paragraph_start < $strlen) { // move pointer // um parágrafo é delimitado por duas quebras de linha consecutivas: $paragraph_end = strpos ($string, "\n\n", $paragraph_start); if ($paragraph_end === false) $paragraph_end = $strlen; else $paragraph_end += 2; // pega o primeiro caractére do parágrafo $char = $string[$paragraph_start]; switch ($char) { // switch char // Ignora os caractéres case "\r": case "\n": case "\t": $paragraph_end = $paragraph_start + 1; break; // Listas case '*': case '#': case '@': $paragraph = substr ($string, $paragraph_start, $paragraph_end - $paragraph_start); // Separa as linhas do parágrafo $lines = explode ("\n", $paragraph . "\n"); $last = ''; $last_level = 0; // Percorre as linhas deste parágrafo foreach ($lines as $line) { // each line // Quantos símbolos existem no início desta linha $current_level = strspn ($line, '*#@'); // Recorta os símbolos encontrados para análize posterior $current = substr ($line, 0, $current_level); // Retira os símbolos do início da linha $line = substr ($line, $current_level); $line = trim ($line); $line = filter_html ($line, $filter_html_mode); // Se preferir: $line = filter_html (trim (substr ($line, $current_level)), $filter_html_mode); // Se não houverem símbolos no início desta linha, trata-se de uma // quebra acidental, o que nos leva a emendar esta linha com a linha anterior if (!$current_level and strlen ($line)) $result .= ' ' . $line; else { // valid line // Vamos comparar os símbolos da linha atual com a linha anterior // para determinar até que ponto são iguais for ($equal = 0; $equal < $last_level and $equal < $current_level and $current[$equal] == $last[$equal]; $equal ++) { } // compare levels // Se o resultado indica que precisamos descer algum nível da lista for ($level = $last_level; $level > $equal; $level --) { // loop level down $char = $last[$level - 1]; if ($char == '*') $result .= $marks['li_close'] . "\n" . $marks['ul_close'] . "\n"; elseif ($char == '#') $result .= $marks['li_close'] . "\n" . $marks['ol_close'] . "\n"; else $result .= $marks['li_close'] . "\n" . $marks['al_close'] . "\n"; } // loop level down // Se fechamos uma lista, a linha atual deverá ser criada como um novo item if ($last_level > $equal and $equal) { // new item after close a list $result .= $marks['li_close'] . "\n" . $marks['li_open']; } // new item after close a list // Se o prefixo da lista anterior for idêntico ao desta, // simplesmente criamos um novo item if ($last_level == $equal and $current_level == $equal and $equal) { // item in same level $result .= $marks['li_close'] . "\n" . $marks['li_open']; } // item in same level // Se for preciso criar sublistas for ($level = $equal; $level < $current_level; $level ++) { // level up if ($level) $result .= "\n"; $char = $current[$level]; if ($char == '*') $result .= $marks['ul_open'] . "\n" . $marks['li_open']; elseif ($char == '#') $result .= $marks['ol_open'] . "\n" . $marks['li_open']; else $result .= $marks['al_open'] . "\n" . $marks['li_open']; } // level up // Finalmente, colocamos o conteúdo desta linha $result .= $line; // Preparamos a próxima iteração anotando os itens desta linha como // sendo os símbolos da linha anterior $last = $current; $last_level = $current_level; } // valid line } // each line break; // Cabeçálhos - ou Títulos case '=': $paragraph = substr ($string, $paragraph_start, $paragraph_end - $paragraph_start); // Vamos ver quantos símbolos = tem no início do parágrafo $level = strspn ($paragraph, '='); // Se forem de 1 a 6 if ($level <= 6) { // valid header level // Vamos filtrar o parágrafo, removendo os sinais de = no início e no final $paragraph = trim ($paragraph, "= \r\n\t"); $paragraph = filter_html ($paragraph, $filter_html_mode); $result .= $marks['h' . $level . '_open'] . $paragraph . $marks['h' . $level . '_close'] . "\n"; } // valid header level // Se tiverem mais de 6 sinais de =, será tratado como um parágrafo normal else $result .= $marks['p_open'] . filter_html (trim ($paragraph), $filter_html_mode) . $marks['p_close'] . "\n"; break; // Um separador horizontal case '-': $paragraph = substr ($string, $paragraph_start, $paragraph_end - $paragraph_start); // Quantos hifens ("-") tem no parágrafo $level = strspn ($paragraph, '-'); // Vamos limpar o parágrafo $clean = trim ($paragraph, "- \r\n\t"); // Somente se houverem mais de 4 hifens e se não houver restado nada após a limpeza if ($level > 4 and !strlen ($clean)) $result .= $marks['hr'] . "\n"; // Caso contrário, este será mais um parágrafo comum else $result .= $marks['p_open'] . filter_html (trim ($paragraph), $filter_html_mode) . $marks['p_close'] . "\n"; break; // Linhas pré-formatadas terão o HTML escapado case ' ': $paragraph = substr ($string, $paragraph_start, $paragraph_end - $paragraph_start); $result .= $marks['pre_open'] . "\n" . str_replace ('<', '<', trim ($paragraph, "\r\n")) . "\n" . $marks['pre_close'] . "\n"; break; // procura por marcações que alteram o método de formatação case '[': $tag_start = $paragraph_start + 1; // Procura por ] $tag_end = strpos ($string, ']', $tag_start); // Se for encontrado if (is_int ($tag_end)) { // end of tag found // Recortamos a tag $tag = substr ($string, $tag_start, $tag_end - $tag_start); list ($tag_type) = explode (':', $tag); $tag_original = $tag_type; // Vamos analizar seu conteúdo switch ($tag_type) { // switch tag case 'style': case 'script': // Se não for permitido script ou style, as marcas serão convertidas if ($filter_html_mode != 2) $tag_type = 'pre'; case 'code': case 'pre': // Vamos procurar pela marca de fechamento $close_tag = '[/' . $tag_original . ']'; $close_tag_length = strlen ($close_tag); $close_tag_pos = strpos ($string, $close_tag, $tag_end); // Se a encontrarmos if ($close_tag_pos) { // close tag found $paragraph_end = $close_tag_pos + $close_tag_length; $tag_end ++; $paragraph = substr ($string, $tag_end, $close_tag_pos - $tag_end); // Se o conteudo for do tipo code ou pre, escapamos o HTML if ($tag_type == 'code' or $tag_type == 'pre') $paragraph = str_replace ('<', '<', $paragraph); $result .= $marks[$tag_type . '_open'] . $paragraph . $marks[$tag_type . '_close'] . "\n"; break 2; } // close tag found // outras marcações serão consideradas como um parágrafo normal // simplesmente deixando o fluxo do programa prosseguir } // switch tag } // end of tag found // Um parágrafo normal default: $result .= $marks['p_open'] . filter_html (trim (substr ($string, $paragraph_start, $paragraph_end - $paragraph_start)), $filter_html_mode) . $marks['p_close'] . "\n"; } // switch char // Movemos o ponteiro adiante $paragraph_start = $paragraph_end; } // move pointer // converte quebras de linhas Linux para Windows $result = str_replace ("\n", "\r\n", $result); return $result; } // text2html ?>