<?php # $Id$ # serendipity_event_nl2br.php 2012-04-21 16:11:00 ian $ @serendipity_plugin_api::load_language(dirname(__FILE__)); class serendipity_event_nl2br extends serendipity_event { var $title = PLUGIN_EVENT_NL2BR_NAME; function introspect(&$propbag) { global $serendipity; $propbag->add('name', PLUGIN_EVENT_NL2BR_NAME); $propbag->add('description', PLUGIN_EVENT_NL2BR_DESC); $propbag->add('stackable', false); $propbag->add('author', 'Serendipity Team'); $propbag->add('version', '2.16'); $propbag->add('requirements', array( 'serendipity' => '0.8', 'smarty' => '2.6.7', 'php' => '4.1.0' )); $propbag->add('cachable_events', array('frontend_display' => true)); $propbag->add('event_hooks', array('frontend_display' => true, 'backend_configure' => true, 'css' => true )); $propbag->add('groups', array('MARKUP')); $this->markup_elements = array( array( 'name' => 'ENTRY_BODY', 'element' => 'body', ), array( 'name' => 'EXTENDED_BODY', 'element' => 'extended', ), array( 'name' => 'COMMENT', 'element' => 'comment', ), array( 'name' => 'HTML_NUGGET', 'element' => 'html_nugget', ) ); $conf_array = array('check_markup', 'isolate', 'p_tags', 'isobr', 'clean_tags'); foreach($this->markup_elements as $element) { $conf_array[] = $element['name']; } $propbag->add('configuration', $conf_array); } function cleanup() { global $serendipity; /* check possible config mismatch setting in combination with ISOBR */ if ( serendipity_db_bool($this->get_config('isobr')) === true ) { if( serendipity_db_bool($this->get_config('clean_tags')) === true ) { $this->set_config('clean_tags', false); echo '<div class="serendipityAdminMsgError msg_error"><img class="backend_attention" src="' . $serendipity['serendipityHTTPPath'] . 'templates/default/admin/img/admin_msg_note.png" alt="" />'; echo sprintf(PLUGIN_EVENT_NL2BR_CONFIG_ERROR, 'clean_tags', 'ISOBR') . '</div>'; return false; } if ( serendipity_db_bool($this->get_config('p_tags')) === true ) { $this->set_config('p_tags', false); echo '<div class="serendipityAdminMsgError msg_error"><img class="backend_attention" src="' . $serendipity['serendipityHTTPPath'] . 'templates/default/admin/img/admin_msg_note.png" alt="" />'; echo sprintf(PLUGIN_EVENT_NL2BR_CONFIG_ERROR, 'p_tags', 'ISOBR') . '</div>'; return false; } } /* check possible config mismatch setting in combination with P_TAGS */ if ( serendipity_db_bool($this->get_config('p_tags')) === true && serendipity_db_bool($this->get_config('clean_tags')) === true ) { $this->set_config('clean_tags', false); echo '<div class="serendipityAdminMsgError msg_error"><img class="backend_attention" src="' . $serendipity['serendipityHTTPPath'] . 'templates/default/admin/img/admin_msg_note.png" alt="" />'; echo sprintf(PLUGIN_EVENT_NL2BR_CONFIG_ERROR, 'clean_tags', 'P_TAGS') . '</div>'; return false; } return true; } function install() { serendipity_plugin_api::hook_event('backend_cache_entries', $this->title); } function uninstall() { serendipity_plugin_api::hook_event('backend_cache_purge', $this->title); serendipity_plugin_api::hook_event('backend_cache_entries', $this->title); } function generate_content(&$title) { $title = $this->title; } function introspect_config_item($name, &$propbag) { switch($name) { case 'check_markup': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_EVENT_NL2BR_CHECK_MARKUP); $propbag->add('description', PLUGIN_EVENT_NL2BR_CHECK_MARKUP_DESC); $propbag->add('default', 'true'); break; case 'isolate': $propbag->add('type', 'string'); $propbag->add('name', PLUGIN_EVENT_NL2BR_ISOLATE_TAGS); $propbag->add('description', PLUGIN_EVENT_NL2BR_ISOLATE_TAGS_DESC); $propbag->add('default', ''); break; case 'p_tags': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_EVENT_NL2BR_PTAGS); $propbag->add('description', PLUGIN_EVENT_NL2BR_PTAGS_DESC); $propbag->add('default', 'false'); break; case 'isobr': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_EVENT_NL2BR_ISOBR_TAG); $propbag->add('description', PLUGIN_EVENT_NL2BR_ISOBR_TAG_DESC); $propbag->add('default', 'true'); break; case 'clean_tags': $propbag->add('type', 'boolean'); $propbag->add('name', PLUGIN_EVENT_NL2BR_CLEANTAGS); $propbag->add('description', PLUGIN_EVENT_NL2BR_CLEANTAGS_DESC); $propbag->add('default', 'false'); break; default: $propbag->add('type', 'boolean'); $propbag->add('name', constant($name)); $propbag->add('description', sprintf(APPLY_MARKUP_TO, constant($name))); $propbag->add('default', 'true'); } return true; } function isolate($src, $regexp = NULL) { if($regexp) return preg_replace_callback($regexp, array($this, 'isolate'), $src); global $_buf; $_buf[] = $src[0]; return "\001" . (count($_buf) - 1); } function restore($text) { global $_buf; return preg_replace('~\001(\d+)~e', '$_buf[$1]', $text); } function event_hook($event, &$bag, &$eventData) { global $serendipity; static $markup = null; static $isolate = null; static $p_tags = null; static $isobr = null; static $clean_tags = null; global $_buf; $hooks = &$bag->get('event_hooks'); if ($markup === null) { $markup = serendipity_db_bool($this->get_config('check_markup')); } if ($p_tags === null) { $p_tags = serendipity_db_bool($this->get_config('p_tags')); } if ($isobr === null) { $isobr = serendipity_db_bool($this->get_config('isobr')); } if ($clean_tags === null) { $clean_tags = serendipity_db_bool($this->get_config('clean_tags')); } if (isset($hooks[$event])) { switch($event) { case 'frontend_display': // check single entry for temporary disabled markups if ( !$eventData['properties']['ep_disable_markup_' . $this->instance] && !in_array($this->instance, (array)$serendipity['POST']['properties']['disable_markups']) && !$eventData['properties']['ep_no_textile'] && !isset($serendipity['POST']['properties']['ep_no_textile']) && !$eventData['properties']['ep_no_markdown'] && !isset($serendipity['POST']['properties']['ep_no_markdown'])) { // yes, this markup shall be applied $serendipity['nl2br']['entry_disabled_markup'] = false; } else { // no, do not apply markup $serendipity['nl2br']['entry_disabled_markup'] = true; } // don't add additional br or p tags, if the wysiwyg-editor, the textile, or markdown plugin already took care about markup if($markup) { if ( ($serendipity['wysiwyg'] && serendipity_userLoggedIn()) || ($serendipity['nl2br']['entry_disabled_markup'] === false && (class_exists('serendipity_event_textile') || class_exists('serendipity_event_markdown'))) ) { return true; } } // check for users isolation tags if ($isolate === null) { $isolate = $this->get_config('isolate'); $tags = (array)explode(',', $isolate); $isolate = array(); foreach($tags AS $tag) { $tag = trim($tag); if (!empty($tag)) { $isolate[] = $tag; } } if (count($isolate) < 1) { $isolate = false; } } foreach ($this->markup_elements as $temp) { if (serendipity_db_bool($this->get_config($temp['name'], true)) && isset($eventData[$temp['element']]) && !$eventData['properties']['ep_disable_markup_' . $this->instance] && !in_array($this->instance, (array)$serendipity['POST']['properties']['disable_markups']) && !$eventData['properties']['ep_no_nl2br'] && !isset($serendipity['POST']['properties']['ep_no_nl2br'])) { $element = $temp['element']; if ($p_tags) { $eventData[$element] = $this->nl2p($eventData[$element]); } else if ($isolate) { $eventData[$element] = $this->isolate($eventData[$element], '~[<\[](' . implode('|', $isolate) . ').*?[>\]].*?[<\[]/\1[>\]]~si'); $eventData[$element] = nl2br($eventData[$element]); $eventData[$element] = $this->restore($eventData[$element]); } else { if($isobr) { $eventData[$element] = $this->isolate($eventData[$element], '~[<\[](nl).*?[>\]].*?[<\[]/\1[>\]]~si'); $eventData[$element] = nl2br($eventData[$element]); $eventData[$element] = $this->restore($eventData[$element]); // unset nl tagline, if is $eventData[$element] = str_replace(array("<nl>", "</nl><br />", "</nl><br/>", "</nl>"), "", $eventData[$element]); } else { $eventData[$element] = nl2br($eventData[$element]); } } /* this is an option if not using new isobr default config setting */ if (!$p_tags && $isobr === false && $clean_tags === true) { // convert line endings to Unix style, if not already done $eventData[$element] = str_replace(array("\r\n", "\r"), "\n", $eventData[$element]); // clean special tags from nl2br $eventData[$element] = $this->clean_nl2brtags($eventData[$element]); } } } return true; break; case 'backend_configure': // check single entry for temporary disabled markups if( $isobr ) { $serendipity['nl2br']['iso2br'] = true; // include to global as also used by staticpages now if (!is_object($serendipity['smarty'])) { serendipity_smarty_init(); // if not set to avoid member function assign() on a non-object error, start Smarty templating } // hook into default/admin/entries.tpl somehow via the Heart Of Gold = serendipity_printEntryForm() before! it is loaded $serendipity['smarty']->assign('iso2br', true); } return true; break; case 'css': ?> p.whiteline { margin-top: 0em; margin-bottom: 1em; } p.break { margin-top: 0em; margin-bottom: 0em; } <?php return true; break; default: return false; } } else { return false; } } /* clean nl2br from markup where it is invalid and/or breaks html output * @param string entrytext * @return string * */ function clean_nl2brtags(&$entry) { $allTags = explode('|', 'table|thead|tbody|tfoot|th|tr|td|caption|colgroup|col|ol|ul|li|dl|dt|dd'); $br2nl = array(); foreach($allTags as $tag){ /* for \\1 ( start with : < followed by any number of white spaces : \s* optionally a slash : /? and the tag itself ) * for \\2 ( anything with spaces and characters following until ) * for \\3 ( finally the > ) * for \\4 ( <br followed by any number of spaces, the optional slash and ending with > ) * regex modifier : i - using a case-insensitive match, as upper <TAGS> are valid in HTML * regex modifier : s - using the dot metacharacter in the pattern to match all characters, including newlines */ $br2nl[] = "%(<\s*/?$tag)(.*?)([^>]*>)(<br\s*/?>)%is"; } if(sizeof($br2nl)) $entry = preg_replace($br2nl, '\\1\\2\\3', $entry); return $entry; } /* Insert <p class="whiteline" at paragraphs ending with two newlines * Insert <p class="break" at paragraphs ending with one nl * @param string text * @param boolean complex operations (not necessary when text is flat) * @return string * */ function nl2p(&$text, $complex=true) { if (empty($text)) { return $text; } //Standardize line endings: //DOS to Unix and Mac to Unix $text = str_replace(array("\r\n", "\r"), "\n", $text); $text = str_split($text); $big_p = '<p class="whiteline">'; $small_p = '<p class="break">'; $insert = true; $i = count($text); $whiteline = false; if ($text[$i-1] == "\n") { //prevent unnexessary p-tag at the end unset($text[$i-1]); } //main operation: convert \n to big_p and small_p while ($i > 0) { if ($insert) { $i = $this->next_nl_block($i, $text); if ($i == 0) { //prevent replacing of first character break; } if ($whiteline == true) { $text[$i] = '</p>' . $big_p; } else { $text[$i] = '</p>' . $small_p; } $whiteline = false; $insert = false; } else { if ($text[$i-1] === "\n") { //newline is follower of a newline $whiteline = true; } $insert = true; } } if ($whiteline) { $start_tag = $big_p; } else { $start_tag = $small_p; } if ($complex) { $textstring = $this->tidy_block_elements($text); $textstring = $this->formate_block_elements($textstring); $textstring = $this->isolate_block_elements($textstring); $textstring = $start_tag . $textstring . '</p>'; return $this->clean_code($textstring); } return $start_tag . implode($text) . '</p>'; } /* * Remove unnecessary paragraphs * Unnecessary are those which start and end immediately. * They only get created by isolate_block_elements * @param mixed text * @return string * */ function clean_code ($text) { if (is_array($text)) { $text = implode($text); } return str_replace(array('<p class="whiteline"></p>','<p class="break"></p>', '<p></p>'),"", $text); } function purge_p($text) { $text = str_replace('</p>', "", $text); return str_replace(array('<p class="whiteline">','<p class="break">', '<p>', '</p>'),"\n", $text); } /* * Use nl2p on text within blockelements, useful e.g. with blockquotes * @param array text * @return string * */ function formate_block_elements($textstring) { $block_elements = array('<blockquote'); foreach ($block_elements as $start_tag) { $end_tag = $this->end_tags($start_tag); //first see if block-element really exists $start_tag_position = strpos($textstring, $start_tag); while ($start_tag_position !== false) { $start_tag_end = strpos($textstring, '>', $start_tag_position)+1; $blocktext = $this->get_string_till($textstring, $end_tag, $start_tag_end); $blocktext_length = strlen($blocktext); $formatted_blocktext = $this->nl2p($blocktext); //insert formatted_blocktext into old blockelement $textstring = substr_replace($textstring, $formatted_blocktext, $start_tag_end, $blocktext_length); //next blockelement $start_tag_position = strpos($textstring, $start_tag, $start_tag_end+strlen($formatted_blocktext)); } } return $textstring; } /** * Make sure none of these block_elements are within a <p> * @param string text * @return string * */ function isolate_block_elements($textstring) { $block_elements = array('<table','<ul','<ol','<pre', '<dir', '<dl', '<h1', '<h2', '<h3', '<h4', '<h5', '<h6', '<menu', '<blockquote'); $block_elements_amount = count($block_elements); for($i=0;$i<$block_elements_amount;$i++) { $start_tag = $block_elements[$i]; //first see if block-element really exists $tag_position = strpos($textstring, $start_tag); if ($tag_position === false) { continue; } else { $end_tag = $this->end_tags($start_tag); $textstring = str_replace("$start_tag", "</p>$start_tag", $textstring); $textstring = str_replace("$end_tag", "$end_tag<p>", $textstring); } } return $textstring; } /** * Remove all <p>-tags from block-elements * Note: Walking from left to right * @param array text * @return string * */ function tidy_block_elements($text) { $remove = false; $textstring = implode($text); $block_elements = array('<table','<ul','<ol','<pre', '<dir', '<dl', '<h1', '<h2', '<h3', '<h4', '<h5', '<h6', '<menu', '<blockquote'); foreach ($block_elements as $start_tag) { $end_tag = $this->end_tags($start_tag); //first see if block-element really exists $start_tag_position = strpos($textstring, $start_tag); while ($start_tag_position !== false) { $start_tag_end = strpos($textstring, '>', $start_tag_position)+1; $blocktext = $this->get_string_till($textstring, $end_tag, $start_tag_end); $blocktext_length = strlen($blocktext); $formatted_blocktext = $this->purge_p($blocktext); //insert formatted_blocktext into old blockelement $textstring = substr_replace($textstring, $formatted_blocktext, $start_tag_end, $blocktext_length); //next blockelement $start_tag_position = strpos($textstring, $start_tag, $start_tag_end+strlen($formatted_blocktext)); } } return $textstring; } function get_string_till($text, $end_tag, $offset=0){ if (strpos($text, $end_tag, $offset) === false) { return ""; } $len = strpos($text, $end_tag, $offset) - $offset; return substr($text, $offset, $len); } /* * Return corresponding end-tag: <p -> </p> */ function end_tags($start_tag) { return str_replace("<", "</", $start_tag).">"; } /** * Find next newline seperated by text from current position * @param int start * $param array text */ function next_nl_block($i, $text) { $skipped = false; for ($i--; $i>0; $i-- ) { if (!$skipped){ //see if you skipped over a non-newline (heading to the next block) if (strpos($text[$i], "\n") === false) { $skipped = true; } }else if (strpos($text[$i], "\n") !== false) { break; } } return $i; } } /* vim: set sts=4 ts=4 expandtab : */