Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
SMF/Elkarte BBC Parser

This is a very slow and thorough rewrite of the BBC parsing in Elkarte.

I have done this once or twice before with miserable results. Now, I am going to do it much thoroughly.

Each commit should pass all of the tests (as I write those tests). Not every commit will result in a faster parser, but the end result should be faster and better for resources. In the end, I hope to make it much more maintainable and more object oriented.

Long term, this should use preg_metch with offset capture and even an AST

preg_split() on any [$tag and [/$tag (itemcodes includes) and ]

in index.php, setting SAVE_TOP_RESULTS to true will result in it creating a csv file. To parse this CSV file, open TopResults.php

=== Changes

  • $no_autolink_tags no longer exists. It is now an attribute of the tag as "autolink"
  • you can get the bbc without loading parse_bbc(). Seperate loading from parsing
  • changed substr() == str to substr_compare(). Don't use strpos() when you want to do a substr_compare either
  • seperate construction of the parser from the execution more
  • replace substr() . substr() with substr_replace()
  • removed ftp:// autolinking and the [ftp] tag. Pointless to keep it
<?php
/**
* Microsoft uses their own character set Code Page 1252 (CP1252), which is a
* superset of ISO 8859-1, defining several characters between DEC 128 and 159
* that are not normally displayable. This converts the popular ones that
* appear from a cut and paste from windows.
*
* @param string|false $string
* @return string $string
*/
function sanitizeMSCutPaste($string)
{
if (empty($string))
return $string;
// UTF-8 occurrences of MS special characters
$findchars_utf8 = array(
"\xe2\x80\x9a", // single low-9 quotation mark
"\xe2\x80\x9e", // double low-9 quotation mark
"\xe2\x80\xa6", // horizontal ellipsis
"\xe2\x80\x98", // left single curly quote
"\xe2\x80\x99", // right single curly quote
"\xe2\x80\x9c", // left double curly quote
"\xe2\x80\x9d", // right double curly quote
"\xe2\x80\x93", // en dash
"\xe2\x80\x94", // em dash
);
// safe replacements
$replacechars = array(
',', // &sbquo;
',,', // &bdquo;
'...', // &hellip;
"'", // &lsquo;
"'", // &rsquo;
'"', // &ldquo;
'"', // &rdquo;
'-', // &ndash;
'--', // &mdash;
);
$string = str_replace($findchars_utf8, $replacechars, $string);
return $string;
}
/**
* Parse smileys in the passed message.
*
* What it does:
* - The smiley parsing function which makes pretty faces appear :).
* - If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
* - These are specifically not parsed in code tags [url=mailto:Dad@blah.com]
* - Caches the smileys from the database or array in memory.
* - Doesn't return anything, but rather modifies message directly.
*
* @param string $message
*/
function parsesmileys(&$message)
{
global $modSettings, $txt, $user_info;
static $smileyPregSearch = null, $smileyPregReplacements = array();
// No smiley set at all?!
if ($user_info['smiley_set'] == 'none' || trim($message) == '')
return;
// If smileyPregSearch hasn't been set, do it now.
if (empty($smileyPregSearch))
{
// Use the default smileys if it is disabled. (better for "portability" of smileys.)
if (empty($modSettings['smiley_enable']))
{
$smileysfrom = array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:)');
$smileysto = array('evil.gif', 'cheesy.gif', 'rolleyes.gif', 'angry.gif', 'laugh.gif', 'smiley.gif', 'wink.gif', 'grin.gif', 'sad.gif', 'shocked.gif', 'cool.gif', 'tongue.gif', 'huh.gif', 'embarrassed.gif', 'lipsrsealed.gif', 'kiss.gif', 'cry.gif', 'undecided.gif', 'azn.gif', 'afro.gif', 'police.gif', 'angel.gif');
$smileysdescs = array('', $txt['icon_cheesy'], $txt['icon_rolleyes'], $txt['icon_angry'], $txt['icon_laugh'], $txt['icon_smiley'], $txt['icon_wink'], $txt['icon_grin'], $txt['icon_sad'], $txt['icon_shocked'], $txt['icon_cool'], $txt['icon_tongue'], $txt['icon_huh'], $txt['icon_embarrassed'], $txt['icon_lips'], $txt['icon_kiss'], $txt['icon_cry'], $txt['icon_undecided'], '', '', '', $txt['icon_angel']);
}
else
{
// Load the smileys in reverse order by length so they don't get parsed wrong.
if (($temp = cache_get_data('parsing_smileys', 480)) == null)
{
$smileysfrom = array();
$smileysto = array();
$smileysdescs = array();
// @todo there is no reason $db should be used before this
$db = database();
$db->fetchQueryCallback('
SELECT code, filename, description
FROM {db_prefix}smileys
ORDER BY LENGTH(code) DESC',
array(
),
function($row) use (&$smileysfrom, &$smileysto, &$smileysdescs)
{
$smileysfrom[] = $row['code'];
$smileysto[] = htmlspecialchars($row['filename']);
$smileysdescs[] = $row['description'];
}
);
cache_put_data('parsing_smileys', array($smileysfrom, $smileysto, $smileysdescs), 480);
}
else
list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
}
// The non-breaking-space is a complex thing...
$non_breaking_space = '\x{A0}';
// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:David@bla.com] doesn't parse the :D smiley)
$smileyPregReplacements = array();
$searchParts = array();
$smileys_path = htmlspecialchars($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
{
$specialChars = htmlspecialchars($smileysfrom[$i], ENT_QUOTES);
$smileyCode = '<img src="' . $smileys_path . $smileysto[$i] . '" alt="' . strtr($specialChars, array(':' => '&#58;', '(' => '&#40;', ')' => '&#41;', '$' => '&#36;', '[' => '&#091;')). '" title="' . strtr(htmlspecialchars($smileysdescs[$i]), array(':' => '&#58;', '(' => '&#40;', ')' => '&#41;', '$' => '&#36;', '[' => '&#091;')) . '" class="smiley" />';
$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
$searchParts[] = preg_quote($smileysfrom[$i], '~');
if ($smileysfrom[$i] != $specialChars)
{
$smileyPregReplacements[$specialChars] = $smileyCode;
$searchParts[] = preg_quote($specialChars, '~');
}
}
$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|^)(' . implode('|', $searchParts) . ')(?=[^[:alpha:]0-9]|$)~';
//$smileyPregSearch = '~\n(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|^)(' . implode('|', $searchParts) . ')(?=[^[:alpha:]0-9]|$)\n~';
}
// Replace away!
$message = preg_replace_callback($smileyPregSearch, function ($matches) use ($smileyPregReplacements)
{
return $smileyPregReplacements[$matches[0]];
}, $message);
}
/**
* Calculates all the possible permutations (orders) of an array.
*
* What it does:
* - should not be called on arrays bigger than 10 elements as this function is memory hungry
* - returns an array containing each permutation.
* - e.g. (1,2,3) returns (1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), and (3,2,1)
* - really a combinations without repetition N! function so 3! = 6 and 10! = 4098 combinations
* - Used by parse_bbc to allow bbc tag parameters to be in any order and still be
* parsed properly
*
* @param mixed[] $array index array of values
* @return mixed[] array representing all permutations of the supplied array
*/
function permute($array)
{
$orders = array($array);
$n = count($array);
$p = range(0, $n);
for ($i = 1; $i < $n; null)
{
$p[$i]--;
$j = $i % 2 != 0 ? $p[$i] : 0;
$temp = $array[$i];
$array[$i] = $array[$j];
$array[$j] = $temp;
for ($i = 1; $p[$i] == 0; $i++)
$p[$i] = 1;
$orders[] = $array;
}
return $orders;
}
function pc_next_permutation($p, $size)
{
// If there is only 1, then there can only be 1 permutation... duh.
if ($size < 1)
{
return false;
}
// slide down the array looking for where we're smaller than the next guy
for ($i = $size - 1; isset($p[$i]) && $p[$i] >= $p[$i + 1]; --$i);
// if this doesn't occur, we've finished our permutations
// the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1)
if ($i < 0)
{
return false;
}
// slide down the array looking for a bigger number than what we found before
for ($j = $size; $p[$j] <= $p[$i]; --$j);
// swap them
$tmp = $p[$i];
$p[$i] = $p[$j];
$p[$j] = $tmp;
// now reverse the elements in between by swapping the ends
for (++$i, $j = $size; $i < $j; ++$i, --$j)
{
$tmp = $p[$i];
$p[$i] = $p[$j];
$p[$j] = $tmp;
}
return $p;
}
// This is just a mock so we don't break anything
function call_integration_hook($hook, $parameters = array())
{
return;
}
function cache_put_data($key, $value, $ttl = 120)
{
return;
}
function cache_get_data($key, $ttl = 120)
{
return;
}
<?php
$total_old_time = 0;
$total_new_time = 0;
$stack = array();
$stack_max_len = 5;
$stack_len = 0;
foreach ($results as $i => $result)
{
if (!is_array($result))
{
continue;
}
$total_old_time += $result['old']['total_time'];
$total_new_time += $result['new']['total_time'];
if (defined('SAVE_TOP_RESULTS') && SAVE_TOP_RESULTS)
{
if (count($stack) < $stack_max_len + 1)
{
$stack_len++;
$stack[$i] = $result['time_diff_perc'];
}
else
{
foreach ($stack as $k => $v)
{
if ($v < $result['time_diff_perc'])
{
unset($stack[$k]);
$stack[$i] = $result['time_diff_perc'];
asort($stack);
break;
}
}
}
}
}
if (defined('SAVE_TOP_RESULTS') && SAVE_TOP_RESULTS)
{
asort($stack);
file_put_contents('top_time_diff_perc.csv', implode(array_keys($stack), ',') . "\n", FILE_APPEND);
}
?>
<div>
Messages: <?= $results['num_messages'] ?><br>
Iterations: <?= $results['iterations'] ?><br>
Total Time In Tests: <?= round($total_old_time + $total_new_time, 2) ?><br>
Total Old Time: <?= round($total_old_time, 2) ?><br>
Total New Time: <?= round($total_new_time, 2) ?><br>
Diff Total Time: <?= round(max($total_old_time, $total_new_time) - min($total_old_time, $total_new_time), 2) ?><br>
Diff Total Time %: <?= round((max($total_old_time, $total_new_time) - min($total_old_time, $total_new_time) / max($total_old_time, $total_new_time)), 2) ?><br>
</div>
<table class="table table-striped table-bordered table-condensed" data-page-length="1000">
<!--<colgroup>
<col class="col-md-1">
<col class="col-md-3">
<col class="col-md-4">
<col class="col-md-4">
</colgroup>-->
<thead>
<tr>
<th>Test</th>
<th>Order</th>
<th>Pass</th>
<th>Old Time</th>
<th>New Time</th>
<th>Time Diff</th>
<th>Time Diff %</th>
<th>Old Mem</th>
<th>New Mem</th>
<th>Mem Diff</th>
<th>Old Peak Mem</th>
<th>New Peak Mem</th>
<th>Mem Peak Diff</th>
</tr>
</thead>
<tbody>
<?php
foreach ($results as $test => $result)
{
if (!is_array($result))
{
continue;
}
?>
<tr>
<td><?= $test ?></td>
<td><?= $result['order'] ?></td>
<?php
if (isset($result['pass']))
{
echo '<td class="', $result['pass'] ? 'success' : 'danger', '">', $result['pass'] ? 'pass' : 'fail', '</td>';
}
else
{
echo '<td></td>';
}
?>
<td class="<?= $result['time_winner'] === 'old' ? 'success' : ''?>">
<?= $result['old']['total_time'] ?>
</td>
<td class="<?= $result['time_winner'] === 'new' ? 'success' : ''?>">
<?= $result['new']['total_time'] ?>
</td>
<td><?= $result['time_diff'] ?></td>
<td><?= round(($result['time_diff'] / max($result['new']['total_time'], $result['old']['total_time'])) * 100, 2) ?></td>
<td class="<?= $result['mem_winner'] === 'old' ? 'success' : ''?>">
<?= $result['old']['memory_usage'] ?>
</td>
<td class="<?= $result['mem_winner'] === 'new' ? 'success' : ''?>">
<?= $result['new']['memory_usage'] ?>
</td>
<td><?= $result['mem_diff'] ?></td>
<td class="<?= $result['peak_mem_winner'] === 'old' ? 'success' : ''?>"><?= $result['old']['memory_peak_after'] ?></td>
<td class="<?= $result['peak_mem_winner'] === 'new' ? 'success' : ''?>"><?= $result['new']['memory_peak_after'] ?></td>
<td><?= $result['peak_mem_diff'] ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
<?php
namespace BBC;
// @todo add attribute for TEST_PARAM_STRING and TEST_CONTENT so people can test the content
// @todo change ATTR_TEST to be able to test the entire message with the current offset
class Codes
{
/** the tag's name - must be lowercase */
const ATTR_TAG = 1;
/** One of self::TYPE_* */
const ATTR_TYPE = 2;
/**
* An optional array of parameters, for the form
* [tag abc=123]content[/tag]. The array is an associative array
* where the keys are the parameter names, and the values are an
* array which *may* contain any of self::PARAM_ATTR_*
*/
const ATTR_PARAM = 3;
/**
* A regular expression to test immediately after the tag's
* '=', ' ' or ']'. Typically, should have a \] at the end.
* Optional.
*/
const ATTR_TEST = 4;
/**
* Only available for unparsed_content, closed, unparsed_commas_content, and unparsed_equals_content.
* $1 is replaced with the content of the tag.
* Parameters are replaced in the form {param}.
* For unparsed_commas_content, $2, $3, ..., $n are replaced.
*/
const ATTR_CONTENT = 5;
/**
* Only when content is not used, to go before any content.
* For unparsed_equals, $1 is replaced with the value.
* For unparsed_commas, $1, $2, ..., $n are replaced.
*/
const ATTR_BEFORE = 6;
/**
* Similar to before in every way, except that it is used when the tag is closed.
*/
const ATTR_AFTER = 7;
/**
* Used in place of content when the tag is disabled.
* For closed, default is '', otherwise it is '$1' if block_level is false, '<div>$1</div>' elsewise.
*/
const ATTR_DISABLED_CONTENT = 8;
/**
* Used in place of before when disabled.
* Defaults to '<div>' if block_level, '' if not.
*/
const ATTR_DISABLED_BEFORE = 9;
/**
* Used in place of after when disabled.
* Defaults to '</div>' if block_level, '' if not.
*/
const ATTR_DISABLED_AFTER = 10;
/**
* Set to true the tag is a "block level" tag, similar to HTML.
* Block level tags cannot be nested inside tags that are not block level, and will not be implicitly closed as easily.
* One break following a block level tag may also be removed.
*/
const ATTR_BLOCK_LEVEL = 11;
/**
* Trim the whitespace after the opening tag or the closing tag or both.
* One of self::TRIM_*
* Optional
*/
const ATTR_TRIM = 12;
/**
* Except when type is missing or 'closed', a callback to validate the data as $data.
* Depending on the tag's type, $data may be a string or an array of strings (corresponding to the replacement.)
*/
const ATTR_VALIDATE = 13;
/**
* When type is unparsed_equals or parsed_equals only, may be not set,
* 'optional', or 'required' corresponding to if the content may be quoted.
* This allows the parser to read [tag="abc]def[esdf]"] properly.
*/
const ATTR_QUOTED = 14;
/**
* An array of tag names, or not set.
* If set, the enclosing tag *must* be one of the listed tags, or parsing won't occur.
*/
const ATTR_REQUIRE_PARENTS = 15;
/**
* similar to require_parents, if set children won't be parsed if they are not in the list.
*/
const ATTR_REQUIRE_CHILDREN = 16;
/**
* Similar to, but very different from, require_parents.
* If it is set the listed tags will not be parsed inside the tag.
*/
const ATTR_DISALLOW_PARENTS = 17;
/**
* Similar to, but very different from, require_children.
* If it is set the listed tags will not be parsed inside the tag.
*/
const ATTR_DISALLOW_CHILDREN = 18;
/**
* When ATTR_DISALLOW_* is used, this gets put before the tag.
*/
const ATTR_DISALLOW_BEFORE = 19;
/**
* * When ATTR_DISALLOW_* is used, this gets put after the tag.
*/
const ATTR_DISALLOW_AFTER = 20;
/**
* an array restricting what BBC can be in the parsed_equals parameter, if desired.
*/
const ATTR_PARSED_TAGS_ALLOWED = 21;
/**
* (bool) Turn uris like http://www.google.com in to links
*/
const ATTR_AUTOLINK = 22;
/**
* The length of the tag
*/
const ATTR_LENGTH = 23;
/**
* Whether the tag is disabled
*/
const ATTR_DISABLED = 24;
/** [tag]parsed content[/tag] */
const TYPE_PARSED_CONTENT = 0;
/** [tag=xyz]parsed content[/tag] */
const TYPE_UNPARSED_EQUALS = 1;
/** [tag=parsed data]parsed content[/tag] */
const TYPE_PARSED_EQUALS = 2;
/** [tag]unparsed content[/tag] */
const TYPE_UNPARSED_CONTENT = 3;
/** [tag], [tag/], [tag /] */
const TYPE_CLOSED = 4;
/** [tag=1,2,3]parsed content[/tag] */
const TYPE_UNPARSED_COMMAS = 5;
/** [tag=1,2,3]unparsed content[/tag] */
const TYPE_UNPARSED_COMMAS_CONTENT = 6;
/** [tag=...]unparsed content[/tag] */
const TYPE_UNPARSED_EQUALS_CONTENT = 7;
/** [*] */
const TYPE_ITEMCODE = 8;
/** a regular expression to validate and match the value. */
const PARAM_ATTR_MATCH = 0;
/** true if the value should be quoted. */
const PARAM_ATTR_QUOTED = 1;
/** callback to evaluate on the data, which is $data. */
const PARAM_ATTR_VALIDATE = 2;
/** a string in which to replace $1 with the data. Either it or validate may be used, not both. */
const PARAM_ATTR_VALUE = 3;
/** true if the parameter is optional. */
const PARAM_ATTR_OPTIONAL = 4;
/** */
const TRIM_NONE = 0;
/** */
const TRIM_INSIDE = 1;
/** */
const TRIM_OUTSIDE = 2;
/** */
const TRIM_BOTH = 3;
const OPTIONAL = -1;
/**
* An array of self::ATTR_*
* ATTR_TAG and ATTR_TYPE are required for every tag.
* The rest of the attributes depend on the type and other options.
*/
protected $bbc;
protected $itemcodes;
protected $additional_bbc;
protected $disabled;
public function __construct(array $tags = array(), array $disabled = array())
{
$this->bbc = $this->getDefault();
$this->additional_bbc = $tags;
$this->disabled = $disabled;
foreach ($disabled as $tag)
{
$this->removeTag($tag);
}
foreach ($tags as $tag)
{
$this->addTag($tag);
}
}
public function addTag(array $tag)
{
$this->checkNewTag($tag);
}
protected function checkNewTag(array &$tag)
{
if (!isset($tag[self::ATTR_TAG]) || !is_string($tag))
{
throw new \InvalidArgumentException('BBC must have a tag name');
}
$tag[self::ATTR_TAG] = trim($tag[self::ATTR_TAG]);
if ($tag[self::ATTR_TAG] == '')
{
throw new \InvalidArgumentException('BBC must have a tag name');
}
$tag[self::ATTR_TYPE] = empty($tag[self::ATTR_TYPE]) ? self::UNPARSED_CONTENT : $tag[self::ATTR_TYPE];
if (!is_int($tag[self::ATTR_TYPE]) || $tag[self::ATTR_TYPE] > self::TYPE_PARSED_EQUALS_CONTENT || $tag[self::ATTR_TYPE] < self::UNPARSED_CONTENT)
{
throw new \InvalidArgumentException('Invalid type for tag: ' . $tag[self::ATTR_TYPE]);
}
if (isset($tag[self::ATTR_PARAM]))
{
foreach ($parameters as &$parameter)
{
$parameter[self::PARAM_ATTR_QUOTED] = !empty($parameter[self::PARAM_ATTR_QUOTED]);
$parameter[self::PARAM_ATTR_OPTIONAL] = !empty($parameter[self::PARAM_ATTR_OPTIONAL]);
if (isset($parameter[self::PARAM_ATTR_VALIDATE]) && isset($parameter[self::PARAM_ATTR_VALUE]))
{
throw new \InvalidArgumentException('Parameters may only use value or validate, not both');
}
}
}
if (!isset($tag[self::ATTR_LENGTH]))
{
$tag[self::ATTR_LENGTH] = strlen($tag[self::ATTR_TAG]);
}
$tag[self::ATTR_AUTOLINK] = !empty($tag[self::ATTR_AUTOLINK]);
$tag[self::ATTR_BLOCK_LEVEL] = !empty($tag[self::ATTR_BLOCK_LEVEL]);
}
public function removeTag($tag)
{
foreach ($this->bbc as $k => $v)
{
if ($tag === $v[self::ATTR_TAG])
{
unset($this->bbc[$k]);
}
}
/*
array_filter takex 50% more time
return;
$this->bbc = array_filter($this->bbc, function ($ele) use ($tag) {
return $ele[self::ATTR_TAG] !== $tag;
});*/
}
public function getDefault()
{
global $modSettings, $txt, $scripturl;
return array(
array(
self::ATTR_TAG => 'abbr',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
self::ATTR_BEFORE => '<abbr title="$1">',
self::ATTR_AFTER => '</abbr>',
self::ATTR_QUOTED => self::OPTIONAL,
self::ATTR_DISABLED_AFTER => ' ($1)',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'anchor',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
// USES CLOSING BRACKET
//self::ATTR_TEST => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
self::ATTR_TEST => '[#]?([A-Za-z][A-Za-z0-9_\-]*)',
self::ATTR_BEFORE => '<span id="post_$1">',
self::ATTR_AFTER => '</span>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 6,
),
array(
self::ATTR_TAG => 'b',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<strong class="bbc_strong">',
self::ATTR_AFTER => '</strong>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 1,
),
array(
self::ATTR_TAG => 'br',
self::ATTR_TYPE => self::TYPE_CLOSED,
self::ATTR_CONTENT => '<br />',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 2,
),
array(
self::ATTR_TAG => 'center',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<div class="centertext">',
self::ATTR_AFTER => '</div>',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 6,
),
array(
self::ATTR_TAG => 'code',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_CONTENT => '<div class="codeheader">' . $txt['code'] . ': <a href="javascript:void(0);" onclick="return elkSelectText(this);" class="codeoperation">' . $txt['code_select'] . '</a></div><pre class="bbc_code prettyprint">$1</pre>',
self::ATTR_VALIDATE => $this->isDisabled('code') ? null : function(&$tag, &$data, $disabled) {
$data = str_replace("\t", "<span class=\"tab\">\t</span>", $data);
},
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'code',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS_CONTENT,
self::ATTR_CONTENT => '<div class="codeheader">' . $txt['code'] . ': ($2) <a href="#" onclick="return elkSelectText(this);" class="codeoperation">' . $txt['code_select'] . '</a></div><pre class="bbc_code prettyprint">$1</pre>',
self::ATTR_VALIDATE => $this->isDisabled('code') ? null : function(&$tag, &$data, $disabled) {
$data[0] = str_replace("\t", "<span class=\"tab\">\t</span>", $data[0]);
},
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'color',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
// USES CLOSING BRACKET
//self::ATTR_TEST => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s?,\s?){2}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\))\]',
self::ATTR_TEST => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s?,\s?){2}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\))',
self::ATTR_BEFORE => '<span style="color: $1;" class="bbc_color">',
self::ATTR_AFTER => '</span>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'email',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_CONTENT => '<a href="mailto:$1" class="bbc_email">$1</a>',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
},
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'email',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
self::ATTR_BEFORE => '<a href="mailto:$1" class="bbc_email">',
self::ATTR_AFTER => '</a>',
//self::ATTR_DISALLOW_CHILDREN => array('email', 'ftp', 'url', 'iurl'),
self::ATTR_DISALLOW_CHILDREN => array('email' => 'email', 'url' => 'url', 'iurl' => 'iurl'),
self::ATTR_DISABLED_AFTER => ' ($1)',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'footnote',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<sup class="bbc_footnotes">%fn%',
self::ATTR_AFTER => '%fn%</sup>',
//self::ATTR_DISALLOW_PARENTS => array('footnote', 'code', 'anchor', 'url', 'iurl'),
self::ATTR_DISALLOW_PARENTS => array('footnote' => 'footnote', 'code' => 'code', 'anchor' => 'anchor', 'url' => 'url', 'iurl' => 'iurl'),
self::ATTR_DISALLOW_BEFORE => '',
self::ATTR_DISALLOW_AFTER => '',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 8,
),
array(
self::ATTR_TAG => 'font',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
// USES CLOSING BRACKET
//self::ATTR_TEST => '[A-Za-z0-9_,\-\s]+?\]',
self::ATTR_TEST => '[A-Za-z0-9_,\-\s]+?',
self::ATTR_BEFORE => '<span style="font-family: $1;" class="bbc_font">',
self::ATTR_AFTER => '</span>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 4,
),
/* array(
self::ATTR_TAG => 'ftp',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_CONTENT => '<a href="$1" class="bbc_ftp new_win" target="_blank">$1</a>',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'ftp://') !== 0 && strpos($data, 'ftps://') !== 0)
{
$data = 'ftp://' . $data;
}
},
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 3,
),
array(
self::ATTR_TAG => 'ftp',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
self::ATTR_BEFORE => '<a href="$1" class="bbc_ftp new_win" target="_blank">',
self::ATTR_AFTER => '</a>',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
if (strpos($data, 'ftp://') !== 0 && strpos($data, 'ftps://') !== 0)
{
$data = 'ftp://' . $data;
}
},
self::ATTR_DISALLOW_CHILDREN => array('email', 'ftp', 'url', 'iurl'),
self::ATTR_DISABLED_AFTER => ' ($1)',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 3,
),
*/ array(
self::ATTR_TAG => 'hr',
self::ATTR_TYPE => self::TYPE_CLOSED,
self::ATTR_CONTENT => '<hr />',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 2,
),
array(
self::ATTR_TAG => 'i',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<em>',
self::ATTR_AFTER => '</em>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 1,
),
array(
self::ATTR_TAG => 'img',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_PARAM => array(
'alt' => array(self::PARAM_ATTR_OPTIONAL => true),
'width' => array(
self::PARAM_ATTR_OPTIONAL => true,
self::PARAM_ATTR_VALUE => 'width:100%;max-width:$1px;',
self::PARAM_ATTR_MATCH => '(\d+)'
),
'height' => array(
self::PARAM_ATTR_OPTIONAL => true,
self::PARAM_ATTR_VALUE => 'max-height:$1px;',
self::PARAM_ATTR_MATCH => '(\d+)'
),
),
self::ATTR_CONTENT => '<img src="$1" alt="{alt}" style="{width}{height}" class="bbc_img resized" />',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
{
$data = 'http://' . $data;
}
},
self::ATTR_DISABLED_CONTENT => '($1)',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 3,
),
array(
self::ATTR_TAG => 'img',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_CONTENT => '<img src="$1" alt="" class="bbc_img" />',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
{
$data = 'http://' . $data;
}
},
self::ATTR_DISABLED_CONTENT => '($1)',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 3,
),
array(
self::ATTR_TAG => 'iurl',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_CONTENT => '<a href="$1" class="bbc_link">$1</a>',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
{
$data = 'http://' . $data;
}
},
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'iurl',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
self::ATTR_BEFORE => '<a href="$1" class="bbc_link">',
self::ATTR_AFTER => '</a>',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
if ($data[0] === '#')
{
$data = '#post_' . substr($data, 1);
}
elseif (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
{
$data = 'http://' . $data;
}
},
//self::ATTR_DISALLOW_CHILDREN => array('email', 'ftp', 'url', 'iurl'),
self::ATTR_DISALLOW_CHILDREN => array('email' => 'email', 'url' => 'url', 'iurl' => 'iurl'),
self::ATTR_DISABLED_AFTER => ' ($1)',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'left',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<div style="text-align: left;">',
self::ATTR_AFTER => '</div>',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'li',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<li>',
self::ATTR_AFTER => '</li>',
self::ATTR_TRIM => self::TRIM_OUTSIDE,
self::ATTR_REQUIRE_PARENTS => array('list'),
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_DISABLED_BEFORE => '',
self::ATTR_DISABLED_AFTER => '<br />',
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 2,
),
array(
self::ATTR_TAG => 'list',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<ul class="bbc_list">',
self::ATTR_AFTER => '</ul>',
self::ATTR_TRIM => self::TRIM_INSIDE,
self::ATTR_REQUIRE_CHILDREN => array('li', 'list'),
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'list',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_PARAM => array(
'type' => array(self::PARAM_ATTR_MATCH => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'),
),
self::ATTR_BEFORE => '<ul class="bbc_list" style="list-style-type: {type};">',
self::ATTR_AFTER => '</ul>',
self::ATTR_TRIM => self::TRIM_INSIDE,
self::ATTR_REQUIRE_CHILDREN => array('li'),
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'me',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
self::ATTR_BEFORE => '<div class="meaction">&nbsp;$1 ',
self::ATTR_AFTER => '</div>',
self::ATTR_QUOTED => 'optional',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_DISABLED_BEFORE => '/me ',
self::ATTR_DISABLED_AFTER => '<br />',
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 2,
),
array(
self::ATTR_TAG => 'member',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
// USES CLOSING BRACKET
self::ATTR_TEST => '[\d*]',
self::ATTR_BEFORE => '<span class="bbc_mention"><a href="' . $scripturl . '?action=profile;u=$1">@',
self::ATTR_AFTER => '</a></span>',
self::ATTR_DISABLED_BEFORE => '@',
self::ATTR_DISABLED_AFTER => '',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 6,
),
array(
self::ATTR_TAG => 'nobbc',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_CONTENT => '$1',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'pre',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<pre class="bbc_pre">',
self::ATTR_AFTER => '</pre>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 3,
),
array(
self::ATTR_TAG => 'quote',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<div class="quoteheader">' . $txt['quote'] . '</div><blockquote>',
self::ATTR_AFTER => '</blockquote>',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'quote',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_PARAM => array(
'author' => array(
self::PARAM_ATTR_MATCH => '(.{1,192}?)',
self::ATTR_QUOTED => true
),
),
self::ATTR_BEFORE => '<div class="quoteheader">' . $txt['quote_from'] . ': {author}</div><blockquote>',
self::ATTR_AFTER => '</blockquote>',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'quote',
self::ATTR_TYPE => self::TYPE_PARSED_EQUALS,
self::ATTR_BEFORE => '<div class="quoteheader">' . $txt['quote_from'] . ': $1</div><blockquote>',
self::ATTR_AFTER => '</blockquote>',
self::ATTR_QUOTED => 'optional',
// Don't allow everything to be embedded with the author name.
self::ATTR_PARSED_TAGS_ALLOWED => array('url', 'iurl', 'ftp'),
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'quote',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_PARAM => array(
'author' => array(self::PARAM_ATTR_MATCH => '([^<>]{1,192}?)'),
'link' => array(self::PARAM_ATTR_MATCH => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d{1,40}|action=profile;u=\d+)'),
'date' => array(self::PARAM_ATTR_MATCH => '(\d+)', self::ATTR_VALIDATE => 'htmlTime'),
),
self::ATTR_BEFORE => '<div class="quoteheader"><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . ($modSettings['todayMod'] == 3 ? ' - ' : $txt['search_on']) . ' {date}</a></div><blockquote>',
self::ATTR_AFTER => '</blockquote>',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'quote',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_PARAM => array(
'author' => array(self::PARAM_ATTR_MATCH => '(.{1,192}?)'),
),
self::ATTR_BEFORE => '<div class="quoteheader">' . $txt['quote_from'] . ': {author}</div><blockquote>',
self::ATTR_AFTER => '</blockquote>',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'right',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<div style="text-align: right;">',
self::ATTR_AFTER => '</div>',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 's',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<del>',
self::ATTR_AFTER => '</del>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 1,
),
array(
self::ATTR_TAG => 'size',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
// USES CLOSING BRACKET
//self::ATTR_TEST => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
self::ATTR_TEST => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)',
self::ATTR_BEFORE => '<span style="font-size: $1;" class="bbc_size">',
self::ATTR_AFTER => '</span>',
self::ATTR_DISALLOW_PARENTS => array('size' => 'size'),
self::ATTR_DISALLOW_BEFORE => '<span>',
self::ATTR_DISALLOW_AFTER => '</span>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'size',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
// USES CLOSING BRACKET
//self::ATTR_TEST => '[1-7]\]',
//self::ATTR_TEST => '[1-7]',
self::ATTR_TEST => '[1-7]{1}$',
self::ATTR_BEFORE => '<span style="font-size: $1;" class="bbc_size">',
self::ATTR_AFTER => '</span>',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
$data = $sizes[(int) $data] . 'em';
},
self::ATTR_DISALLOW_PARENTS => array('size' => 'size'),
self::ATTR_DISALLOW_BEFORE => '<span>',
self::ATTR_DISALLOW_AFTER => '</span>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 4,
),
array(
self::ATTR_TAG => 'spoiler',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<span class="spoilerheader">' . $txt['spoiler'] . '</span><div class="spoiler"><div class="bbc_spoiler" style="display: none;">',
self::ATTR_AFTER => '</div></div>',
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 7,
),
array(
self::ATTR_TAG => 'sub',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<sub>',
self::ATTR_AFTER => '</sub>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 3,
),
array(
self::ATTR_TAG => 'sup',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<sup>',
self::ATTR_AFTER => '</sup>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 3,
),
array(
self::ATTR_TAG => 'table',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<div class="bbc_table_container"><table class="bbc_table">',
self::ATTR_AFTER => '</table></div>',
self::ATTR_TRIM => self::TRIM_INSIDE,
self::ATTR_REQUIRE_CHILDREN => array('tr'),
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 5,
),
array(
self::ATTR_TAG => 'td',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<td>',
self::ATTR_AFTER => '</td>',
self::ATTR_REQUIRE_PARENTS => array('tr'),
self::ATTR_TRIM => self::TRIM_OUTSIDE,
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_DISABLED_BEFORE => '',
self::ATTR_DISABLED_AFTER => '',
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 2,
),
array(
self::ATTR_TAG => 'th',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<th>',
self::ATTR_AFTER => '</th>',
self::ATTR_REQUIRE_PARENTS => array('tr'),
self::ATTR_TRIM => self::TRIM_OUTSIDE,
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_DISABLED_BEFORE => '',
self::ATTR_DISABLED_AFTER => '',
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 2,
),
array(
self::ATTR_TAG => 'tr',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<tr>',
self::ATTR_AFTER => '</tr>',
self::ATTR_REQUIRE_PARENTS => array('table'),
self::ATTR_REQUIRE_CHILDREN => array('td', 'th'),
self::ATTR_TRIM => self::TRIM_BOTH,
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_DISABLED_BEFORE => '',
self::ATTR_DISABLED_AFTER => '',
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 2,
),
array(
self::ATTR_TAG => 'tt',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<span class="bbc_tt">',
self::ATTR_AFTER => '</span>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 2,
),
array(
self::ATTR_TAG => 'u',
self::ATTR_TYPE => self::TYPE_PARSED_CONTENT,
self::ATTR_BEFORE => '<span class="bbc_u">',
self::ATTR_AFTER => '</span>',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => true,
self::ATTR_LENGTH => 1,
),
array(
self::ATTR_TAG => 'url',
self::ATTR_TYPE => self::TYPE_UNPARSED_CONTENT,
self::ATTR_CONTENT => '<a href="$1" class="bbc_link" target="_blank">$1</a>',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
{
$data = 'http://' . $data;
}
},
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 3,
),
array(
self::ATTR_TAG => 'url',
self::ATTR_TYPE => self::TYPE_UNPARSED_EQUALS,
self::ATTR_BEFORE => '<a href="$1" class="bbc_link" target="_blank">',
self::ATTR_AFTER => '</a>',
self::ATTR_VALIDATE => function(&$tag, &$data, $disabled) {
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
{
$data = 'http://' . $data;
}
},
//self::ATTR_DISALLOW_CHILDREN => array('email', 'ftp', 'url', 'iurl'),
self::ATTR_DISALLOW_CHILDREN => array('email' => 'email', 'url' => 'url', 'iurl' => 'iurl'),
self::ATTR_DISABLED_AFTER => ' ($1)',
self::ATTR_BLOCK_LEVEL => false,
self::ATTR_AUTOLINK => false,
self::ATTR_LENGTH => 3,
),
);
}
public function getItemCodes()
{
$item_codes = array(
'*' => 'disc',
'@' => 'disc',
'+' => 'square',
'x' => 'square',
'#' => 'decimal',
'0' => 'decimal',
'o' => 'circle',
'O' => 'circle',
);
//call_integration_hook('integrate_item_codes', array(&$item_codes));
return $item_codes;
}
public function getCodes()
{
return $this->bbc;
}
public function getCodesGroupedByTag()
{
$bbc = array();
foreach ($this->bbc as $code)
{
if (!isset($bbc[$code[self::ATTR_TAG]]))
{
$bbc[$code[self::ATTR_TAG]] = array();
}
$bbc[$code[self::ATTR_TAG]][] = $code;
}
return $bbc;
}
public function getTags()
{
$tags = array();
foreach ($this->bbc as $tag)
{
$tags[$tag[self::ATTR_TAG]] = $tag[self::ATTR_TAG];
}
return $tags;
}
// @todo besides the itemcodes (just add a arg $with_itemcodes), this way should be standard and saved like that.
// Even, just remove the itemcodes when needed
public function getForParsing()
{
$bbc = $this->bbc;
//call_integration_hook('bbc_codes_parsing', array(&$bbc, &$itemcodes));
if (!$this->isDisabled('li') && !$this->isDisabled('list'))
{
foreach ($this->getItemCodes() as $c => $dummy)
{
// Skip anything "bad"
if (!is_string($c) || (is_string($c) && trim($c) === ''))
{
continue;
}
$bbc[$c] = $this->getItemCodeTag($c);
}
}
$return = array();
// Find the first letter of the tag faster
foreach ($bbc as $code)
{
$return[$code[self::ATTR_TAG][0]][] = $code;
}
return $return;
}
public function newGetBBC()
{
$bbc = array();
foreach ($this->bbc as $code)
{
$char = $code[0];
$tag = $code[self::ATTR_TAG];
if (!isset($return[$char]))
{
$bbc[$code[0]] = array();
}
if (!isset($return[$char][$tag]))
{
$bbc[$char][$tag] = array();
}
$bbc[$char][$tag][] = $code;
}
}
protected function getItemCodeTag($code)
{
return array(
self::ATTR_TAG => $code,
self::ATTR_TYPE => self::TYPE_ITEMCODE,
self::ATTR_BLOCK_LEVEL => true,
self::ATTR_LENGTH => 1,
'itemcode' => true,
);
}
public function setForPrinting()
{
// Colors can't well be displayed... supposed to be black and white.
$this->disable('color');
$this->disable('me');
// Links are useless on paper... just show the link.
$this->disable('url');
$this->disable('iurl');
$this->disable('email');
// @todo Change maybe?
if (!isset($_GET['images']))
{
$this->disable('img');
}
// @todo Interface/setting to add more?
// call_integration_hook();
return $this;
}
public function isDisabled($tag)
{
return isset($this->disabled[$tag]);
}
public function getDisabled()
{
return $this->disabled;
}
public function disable($tag, $disable = true)
{
// It was already disabled.
if (isset($this->disabled[$tag]))
{
return true;
}
elseif (!$disable)
{
unset($this->disabled[$tag]);
}
foreach ($this->bbc as &$bbc)
{
if ($bbc['tag'] === $tag)
{
$bbc[self::ATTR_DISABLED] = $disable;
}
}
$this->disabled[$tag] = $tag;
}
}
<?php
namespace BBC;
// Sanitize inputs
$type = isset($_GET['type']) ? $_GET['type'] : false;
if (isset($_GET['msg']))
{
if (is_array($_GET['msg']))
{
$msgs = array();
foreach ($_GET['msg'] as $msg)
{
$msgs[] = (int) $msg;
}
$msgs = array_unique($msgs);
}
else
{
$msgs = $_GET['msg'];
}
}
else
{
$msgs = null;
}
$input = array(
'type' => array(
'test' => $type === 'test' ? ' selected="selected"' : '',
'bench' => $type === 'bench' ? ' selected="selected"' : '',
'codes' => $type === 'codes' ? ' selected="selected"' : '',
),
'iterations' => isset($_GET['iterations']) ? min($_GET['iterations'], 10000) : 0,
'debug' => isset($_GET['debug']) && $_GET['debug'] ? 'checked="checked"' : '',
'fatal' => isset($_GET['fatal']) && $_GET['fatal'] ? 'checked="checked"' : '',
'msg' => $msgs,
);
// Setup those constants for the test file
define('ITERATIONS', $input['iterations']);
define('DEBUG', !empty($input['debug']));
define('FAILED_TEST_IS_FATAL', !empty($input['fatal']));
define('SAVE_TOP_RESULTS', true);
// Include the test file
require_once 'Tester.php';
// Run the test (based on type)
$test_types = array(
'test' => 'tests',
'bench' => 'benchmark',
'codes' => 'codes',
);
if (isset($test_types[$type]))
{
define('TEST_TYPE', $type);
$results = call_user_func('\BBC\\' . $test_types[$type], $input);
}
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BBC Parser Test</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="//cdn.datatables.net/1.10.8/js/jquery.dataTables.min.js"></script>
<!--
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/styles/default.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
-->
<style>
.code {
height: auto;
max-height: 10em;
overflow: auto !important;
word-break: normal !important;
word-wrap: normal !important;
width: 30em;
margin-bottom: .5em;
}
</style>
</head>
<body>
<div class="container-fluid">
<div id="top">
<button type="button" class="btn btn-primary btn-lg pull-right" data-toggle="modal" data-target="#controls">Controls</button>
<h1>BBC Parser Test</h1>
</div>
<?php
if (empty($results))
{
?><div>
No results to display. Click the "Controls" button to run tests.
<pre class="well"><?= htmlspecialchars('<html><body>something</body></html>'); ?></pre>
</div><?php
}
// RESULTS TO DISPLAY
else
{
if (isset($test_types[$type]))
{
require_once ucfirst($type) . 'Output.php';
}
} // RESULTS TO DISPLAY
?>
</div>
<div class="modal" id="controls" tabindex="-1" role="dialog" aria-labelledby="controlsLabel">
<form class="modal-dialog" role="document" method="get">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Controls</h4>
</div>
<div class="modal-body">
<div class="formgroup">
<label for="debug">Enable debug()?</label>
<input name="debug" type="checkbox" <?= $input['debug'] ?> class="form-control">
</div>
<div class="formgroup">
<label for="type">Type of test to run</label>
<select name="type" class="form-control">
<option value="test" <?= $input['type']['test'] ?>>Test</option>
<option value="bench" <?= $input['type']['bench'] ?>>Benchmark</option>
<option value="code" <?= $input['type']['codes'] ?>>Codes</option>
</select>
</div>
<div class="formgroup">
<label for="fatal">End tests if one fails</label>
<input name="fatal" type="checkbox" <?= $input['fatal'] ?> class="form-control">
</div>
<div class="formgroup">
<label for="iterations">Number of iterations</label>
<input name="iterations" type="text" value="<?= $input['iterations'] ?>" class="form-control">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</div><!-- /.modal-content -->
</form><!-- /.modal-dialog -->
</div>
<script>
$(document).ready(function(){
$('table').DataTable();
});</script>
</body>
</html>
<?php
/**
* The test messags.
* Generally, they go from less to more complex
*/
return array(
// Nothing. It should just return
'',
// It shouldn't treat these as a bool
'false',
'0',
'array()',
' ',
// Simple message, no BBC
'hello world',
'foo bar',
"Breaker\nbreaker\n1\n9",
// Simple BBC
'[b]Bold[/b]',
'[i]Italics[/i]',
'[u]Underline[/u]',
'[s]Strike through[/s]',
'[b][i][u]Bold, italics, underline[/u][/i][/b]',
'Super[sup]script[/sup]',
'Sub[sub]script[/sub]',
'[sup]Super[/sup]-[sub]sub[/sub]-script',
// A longer message but without bbc
'This is a div with multiple classes and no ID. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec volutpat tellus vulputate dui venenatis quis euismod turpis pellentesque. Suspendisse [sit] amet ipsum eu odio sagittis ultrices at non sapien. Quisque viverra feugiat purus, eu mollis felis condimentum id. In luctus faucibus felis eget viverra. Vivamus et velit orci. In in tellus mauris, at fermentum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed a magna nunc, vel tempor magna. Nam dictum, arcu in pretium varius, libero enim hendrerit nisl, et commodo enim sapien eu augue.
This is a div with multiple classes and no ID. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec volutpat tellus vulputate dui venenatis quis euismod turpis pellentesque. Suspendisse [sit] amet ipsum eu odio sagittis ultrices at non sapien. Quisque viverra feugiat purus, eu mollis felis condimentum id. In luctus faucibus felis eget viverra. Vivamus et velit orci. In in tellus mauris, at fermentum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed a magna nunc, vel tempor magna. Nam dictum, arcu in pretium varius, libero enim hendrerit nisl, et commodo enim sapien eu augue.
This is a div with multiple classes and no ID. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec volutpat tellus vulputate dui venenatis quis euismod turpis pellentesque. Suspendisse [sit] amet ipsum eu odio sagittis ultrices at non sapien. Quisque viverra feugiat purus, eu mollis felis condimentum id. In luctus faucibus felis eget viverra. Vivamus et velit orci. In in tellus mauris, at fermentum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed a magna nunc, vel tempor magna. Nam dictum, arcu in pretium varius, libero enim hendrerit nisl, et commodo enim sapien eu augue.
This is a div with multiple classes and no ID. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec volutpat tellus vulputate dui venenatis quis euismod turpis pellentesque. Suspendisse [sit] amet ipsum eu odio sagittis ultrices at non sapien. Quisque viverra feugiat purus, eu mollis felis condimentum id. In luctus faucibus felis eget viverra. Vivamus et velit orci. In in tellus mauris, at fermentum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed a magna nunc, vel tempor magna. Nam dictum, arcu in pretium varius, libero enim hendrerit nisl, et commodo enim sapien eu augue.
This is a div with multiple classes and no ID. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec volutpat tellus vulputate dui venenatis quis euismod turpis pellentesque. Suspendisse [sit] amet ipsum eu odio sagittis ultrices at non sapien. Quisque viverra feugiat purus, eu mollis felis condimentum id. In luctus faucibus felis eget viverra. Vivamus et velit orci. In in tellus mauris, at fermentum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed a magna nunc, vel tempor magna. Nam dictum, arcu in pretium varius, libero enim hendrerit nisl, et commodo enim sapien eu augue.',
// A message that might have bbc, but really doesn't
'This message doesn\'t actually have [ bbc',
'Neither does [] this one',
'Nor do[es] this one',
'Not ev[en] this on[/en] has bbc',
'This one is sneaky: [/] [ /] [ /] [ /]',
// Time for smilies
' :) ',
':)',
'Smile :)',
'Smil:)ey face',
'Now for all of the default: :) ;) :D ;D :( :( ::) >:( >:( :o 8) ??? :P :-[ :-X :-\ :-\ :-* :-* :\'( O:-) ',
'and the good old whatzup??? which should not show',
// Time to test BBC
'[b]This statement is bold[/b]',
'[url=http://www.google.com]Google[/url]. Basic unparsed equals',
'[nobbc][b]please do not parse this[/b][/nobbc]',
'[br][hr][br /][hr /]',
"[code][/code]\ne",
// Lists are probably the most complicated part of the parser
'[list][li]short list[/li][/list]',
'[list][li]short list[/li][li]to do[/li][li]growing[/li][/list]',
'[list][li]quick[li]no[li]time[li]for[li]closing[li]tags[/list]',
'[list][li]I[/li][li]feel[list][li]like[/list][li]Santa[/li][/list]',
'[list type=decimal][li]Lorem ipsum dolor sit amet, consectetuer adipiscing elit.[/li][li]Aliquam laoreet pulvinar sem. Aenean at odio.[/li][/list]',
// Tables
'[table][tr][td]remember[/td][td]frontpage?[/td][/tr][/table]',
'[table][tr][td]let me see[/td][td][table][tr][td]if[/td][td]I[/td][/tr][tr][td]can[/td][td]break[/td][/tr][tr][td]the[/td][td]internet[/td][/td][/tr][/table]',
'[table][tr][th][/th][/tr][tr][td][/td][/tr][tr][td][/td][/tr][/table]',
// Images
'[img width=500]http://www.google.com/intl/en_ALL/images/srpr/logo1w.png[/img]',
'[img height=50]http://www.google.com/intl/en_ALL/images/srpr/logo1w.png[/img]',
'[img width=43 alt="google" height=50]http://www.google.com/intl/en_ALL/images/srpr/logo1w.png[/img]',
// Quotes are actually a huge part of the parser
'[quote]If at first you do not succeed; call it version 1.0[/quote]',
'[quote=&quot;Edsger Dijkstra&quot;]If debugging is the process of removing software bugs, then programming must be the process of putting them in[/quote]',
'[quote author=Gates]Measuring programming progress by lines of code is like measuring aircraft building progress by weight.[/quote]',
'[quote]Some[quote]basic[/quote]nesting[/quote]',
'[quote][quote][quote][quote]Some[quote]basic[/quote]nesting[/quote]Still[/quote]not[/quote]deep[/quote]enough',
'[quote author=Mutt & Jeff link=topic=14764.msg87204#msg87204 date=1329175080]Lorem ipsum dolor sit amet, consectetur adipiscing elit.[/quote]',
'[quote link=topic=14764.msg87204#msg87204 author=Mutt & Jeff date=1329175080]I started a band called 999 Megabytes. We don&apos;t have a gig yet.[/quote]',
'[quote=Joe Doe joe@email.com]Here is what Joe said.[/quote]',
// Nested Quotes
'[quote]Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque non sapien a eros tincidunt accumsan. Ut nisl dui, dignissim at posuere quis, facilisis eget lectus. Morbi vitae massa eu metus pharetra rhoncus. Suspendisse potenti. Phasellus laoreet dapibus dapibus. Duis faucibus lacinia diam, nec pharetra est pharetra vitae. Etiam sodales, nulla et ullamcorper mattis, augue nunc sollicitudin risus, nec imperdiet est leo vitae est. Integer ultricies, metus at scelerisque interdum, sapien lorem mollis orci, vel mattis felis augue vitae nunc. Fusce eget sem sed orci interdum commodo sit amet et metus. In ultricies feugiat eleifend. Aliquam erat volutpat.
[quote author=ElkArte]Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque non sapien a eros tincidunt accumsan. Ut nisl dui, dignissim at posuere quis, facilisis eget lectus. Morbi vitae massa eu metus pharetra rhoncus. Suspendisse potenti. Phasellus laoreet dapibus dapibus. Duis faucibus lacinia diam, nec pharetra est pharetra vitae. Etiam sodales, nulla et ullamcorper mattis, augue nunc sollicitudin risus, nec imperdiet est leo vitae est. Integer ultricies, metus at scelerisque interdum, sapien lorem mollis orci, vel mattis felis augue vitae nunc. Fusce eget sem sed orci interdum commodo sit amet et metus. In ultricies feugiat eleifend. Aliquam erat volutpat.[/quote]
[quote link=topic=14764.msg87204#msg87204 date=1329175080 author=Mutt & Jeff]Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque non sapien a eros tincidunt accumsan. Ut nisl dui, dignissim at posuere quis, facilisis eget lectus. Morbi vitae massa eu metus pharetra rhoncus. Suspendisse potenti. Phasellus laoreet dapibus dapibus. Duis faucibus lacinia diam, nec pharetra est pharetra vitae. Etiam sodales, nulla et ullamcorper mattis, augue nunc sollicitudin risus, nec imperdiet est leo vitae est. Integer ultricies, metus at scelerisque interdum, sapien lorem mollis orci, vel mattis felis augue vitae nunc. Fusce eget sem sed orci interdum commodo sit amet et metus. In ultricies feugiat eleifend. Aliquam erat volutpat.
[quote author=Mutt & Jeff link=topic=14764.msg87204#msg87204 date=1329175080]Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque non sapien a eros tincidunt accumsan. Ut nisl dui, dignissim at posuere quis, facilisis eget lectus. Morbi vitae massa eu metus pharetra rhoncus. Suspendisse potenti. Phasellus laoreet dapibus dapibus. Duis faucibus lacinia diam, nec pharetra est pharetra vitae. Etiam sodales, nulla et ullamcorper mattis, augue nunc sollicitudin risus, nec imperdiet est leo vitae est. Integer ultricies, metus at scelerisque interdum, sapien lorem mollis orci, vel mattis felis augue vitae nunc. Fusce eget sem sed orci interdum commodo sit amet et metus. In ultricies feugiat eleifend. Aliquam erat volutpat.[/quote]
[/quote]
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
[/quote]',
// Item codes... suck.
"[*]one dot\n[*]two dots",
"[*]Ahoy!\n[*]Me[@]Matey\n[+]Shiver\n[x]Me\n[#]Timbers\n[!]\n[*]I[*]dunno[*]why",
// Autolinks (specifically avoiding FTP)
'http://www.google.com',
'https://google.com',
'http://google.de',
'www.google.com',
'me@email.com',
'http://www.cool.guy/linked?no&8)',
'http://www.facebook.com/profile.php?id=1439984468#!/group.php?gid=103300379708494&ref=ts',
'www.ñchan.org',
// FTP Autolinks
'[ftp]http://somewhere.com/[/ftp]',
// Autolinks inside of links:
'[url=http://www.google.com/]test www.elkarte.net test[/url]',
'[url=http://www.elkarte.org/community/index.php [^]]ask us for assistance[/url]',
// These shouldn't be autolinked.
'[url=https://www.google.com]http://www.google.com/404[/url]',
'[url=https://www.google.com]www.google.com[/url]',
'[url=https://www.google.com]you@mailed.it[/url]',
// URIs in no autolink areas
'[url=http://www.google.com]www.bing.com[/url]',
'[iurl=http://www.google.com]www.bing.com[/iurl]',
'[email=jack@theripper.com]www.bing.com[/email]',
'[url=http://www.google.com]iam@batman.net[/url]',
// Links inside links:
'[url=http://www.google.com/]this url has [email=someone@someplace.org]an email[/email][/url]',
'[url=http://www.yahoo.com]another URL[/url] in it![/url]',
// Colors
'[color=red]red[/color][color=green]green[/color][color=blue]blue[/color]',
'[color=red]Lorem ipsum dolor sit amet, consectetur adipiscing elit.[/color]',
'[color=blue]Volutpat tellus vulputate dui venenatis quis euismod turpis pellentesque.[/color]',
'[color=#f66]Suspendisse sit amet ipsum eu odio sagittis ultrices at non sapien.[/color]',
'[color=#ff0088]Quisque viverra feugiat purus, in luctus faucibus felis eget viverra.[/color]',
'[color=#cccccc]Suspendisse sit amet ipsum eu odio sagittis ultrices at non sapien.[/color]',
'[color=DarkSlateBlue]this is colored![/color]',
// Fonts
'[size=4]Font Family[/size]',
'[font=Arial]Lorem ipsum dolor sit amet, consectetur adipiscing elit.[/font]',
'[font=Tahoma]Suspendisse sit amet ipsum eu odio sagittis ultrices at non sapien.[/font]',
'[font=Monospace]Quisque viverra feugiat purus, in luctus faucibus felis eget viverra.[/font]',
'[font=Times]Suspendisse sit amet ipsum eu odio sagittis ultrices at non sapien.[/font]',
// Bad BBC
'[i]lets go for italics',
'[u][i]Why do you do this to yourself?[/u][/i]',
'[u][quote]should not get underlined[/quote][/u]',
'[img src=www.here.com/index.php?action=dlattach] this is actually a security issue',
'[quote this=should not=work but=maybe it=will]only a test will tell[/quote]',
// Footnotes
'Footnote[footnote]Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec volutpat tellus vulputate dui venenatis quis euismod turpis pellentesque. Suspendisse sit amet ipsum eu odio sagittis ultrices at non sapien. Quisque viverra feugiat purus, eu mollis felis condimentum id. In luctus faucibus felis eget viverra. Vivamus et velit orci. In in tellus mauris, at fermentum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed a magna nunc, vel tempor magna. Nam dictum, arcu in pretium varius, libero enim hendrerit nisl, et commodo enim sapien eu augue. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti. Proin tempor porta porttitor. Nullam a malesuada arcu.[/footnote]',
// Spoilers
'[spoiler]Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec volutpat tellus vulputate dui venenatis quis euismod turpis pellentesque. Suspendisse sit amet ipsum eu odio sagittis ultrices at non sapien. Quisque viverra feugiat purus, eu mollis felis condimentum id. In luctus faucibus felis eget viverra. Vivamus et velit orci. In in tellus mauris, at fermentum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed a magna nunc, vel tempor magna. Nam dictum, arcu in pretium varius, libero enim hendrerit nisl, et commodo enim sapien eu augue. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti. Proin tempor porta porttitor. Nullam a malesuada arcu.[/spoiler]',
// Align
'[center]Center Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aliquam laoreet pulvinar sem. Aenean at odio.[/center]',
'[tt]Teletype Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec elit. Fusce eget enim. Nullam tellus felis, sodales nec, sodales ac, commodo eu, ante.[/tt]',
'[right]Right Curabitur tincidunt, lacus eget iaculis tincidunt, elit libero iaculis arcu, eleifend condimentum sem est quis dolor. Curabitur sed tellus. Donec id dolor.[/right]',
'[left]Left Curabitur tincidunt, lacus eget iaculis tincidunt, elit libero iaculis arcu, eleifend condimentum sem est quis dolor. Curabitur sed tellus. Donec id dolor.[/left]',
'[pre]Pre .. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec elit. Fusce eget enim. Nullam tellus felis, sodales nec, sodales ac, commodo eu, ante.[/pre]',
// Code
'[code]bee boop bee booo[/code]',
'Everyone\n[code]\ngets a line\n[/code]\nbreak',
'You\n[code=me]\nget 1\n[/code]and [code]\nyou get one[/code]',
'[code]I [b]am[/b] a robot [quote]bee boo bee boop[/quote][/code]',
"[code] this has tabs\n\n\n tab\n tab\n[/code]\neven\tsome\toutside\t THE code",
'[code=php]
<?php
/**
* This controller is the most important and probably most accessed of all.
* It controls topic display, with all related.
*/
class Display_Controller
{
/**
* Default action handler for this controller
*/
public function action_index()
{
// what to do... display things!
$this->action_display();
}?>
[/code]',
'[code][b]Bold[/b]
Italics
Underline
Strike through[/code]',
'[code]email@domain.com
:] :/ >[ :p >_>
:happy: :aw: :cool: :kiss: :meh: :mmf: :heart:
[/code]',
// Just to test unparsed_commas, but you need to add the defintion to the parser
'[glow=red,2,50]glow[/glow]',
);
<?php
/**
* Parse bulletin board code in a string, as well as smileys optionally.
*
* What it does:
* - only parses bbc tags which are not disabled in disabledBBC.
* - handles basic HTML, if enablePostHTML is on.
* - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
* - only parses smileys if smileys is true.
* - does nothing if the enableBBC setting is off.
* - uses the cache_id as a unique identifier to facilitate any caching it may do.
* - returns the modified message.
*
* @param string|false $message if false return list of enabled bbc codes
* @param bool|string $smileys = true
* @param string $cache_id = ''
* @param string[]|null $parse_tags array of tags to parse, null for all
* @return string
*/
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
{
global $txt, $scripturl, $context, $modSettings, $user_info;
static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array();
static $disabled, $default_disabled, $parse_tag_cache;
// Don't waste cycles
if ($message === '')
return '';
// Clean up any cut/paste issues we may have
$message = sanitizeMSCutPaste($message);
// If the load average is too high, don't parse the BBC.
if (!empty($modSettings['bbc']) && $modSettings['current_load'] >= $modSettings['bbc'])
{
$context['disabled_parse_bbc'] = true;
return $message;
}
if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
$smileys = (bool) $smileys;
if (empty($modSettings['enableBBC']) && $message !== false)
{
if ($smileys === true)
parsesmileys($message);
return $message;
}
// Allow addons access before entering the main parse_bbc loop
call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
// Sift out the bbc for a performance improvement.
if (empty($bbc_codes) || $message === false)
{
if (!empty($modSettings['disabledBBC']))
{
$temp = explode(',', strtolower($modSettings['disabledBBC']));
foreach ($temp as $tag)
$disabled[trim($tag)] = true;
}
/* The following bbc are formatted as an array, with keys as follows:
tag: the tag's name - should be lowercase!
type: one of...
- (missing): [tag]parsed content[/tag]
- unparsed_equals: [tag=xyz]parsed content[/tag]
- parsed_equals: [tag=parsed data]parsed content[/tag]
- unparsed_content: [tag]unparsed content[/tag]
- closed: [tag], [tag/], [tag /]
- unparsed_commas: [tag=1,2,3]parsed content[/tag]
- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
- unparsed_equals_content: [tag=...]unparsed content[/tag]
parameters: an optional array of parameters, for the form
[tag abc=123]content[/tag]. The array is an associative array
where the keys are the parameter names, and the values are an
array which may contain the following:
- match: a regular expression to validate and match the value.
- quoted: true if the value should be quoted.
- validate: callback to evaluate on the data, which is $data.
- value: a string in which to replace $1 with the data.
either it or validate may be used, not both.
- optional: true if the parameter is optional.
test: a regular expression to test immediately after the tag's
'=', ' ' or ']'. Typically, should have a \] at the end.
Optional.
content: only available for unparsed_content, closed,
unparsed_commas_content, and unparsed_equals_content.
$1 is replaced with the content of the tag. Parameters
are replaced in the form {param}. For unparsed_commas_content,
$2, $3, ..., $n are replaced.
before: only when content is not used, to go before any
content. For unparsed_equals, $1 is replaced with the value.
For unparsed_commas, $1, $2, ..., $n are replaced.
after: similar to before in every way, except that it is used
when the tag is closed.
disabled_content: used in place of content when the tag is
disabled. For closed, default is '', otherwise it is '$1' if
block_level is false, '<div>$1</div>' elsewise.
disabled_before: used in place of before when disabled. Defaults
to '<div>' if block_level, '' if not.
disabled_after: used in place of after when disabled. Defaults
to '</div>' if block_level, '' if not.
block_level: set to true the tag is a "block level" tag, similar
to HTML. Block level tags cannot be nested inside tags that are
not block level, and will not be implicitly closed as easily.
One break following a block level tag may also be removed.
trim: if set, and 'inside' whitespace after the begin tag will be
removed. If set to 'outside', whitespace after the end tag will
meet the same fate.
validate: except when type is missing or 'closed', a callback to
validate the data as $data. Depending on the tag's type, $data
may be a string or an array of strings (corresponding to the
replacement.)
quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
may be not set, 'optional', or 'required' corresponding to if
the content may be quoted. This allows the parser to read
[tag="abc]def[esdf]"] properly.
require_parents: an array of tag names, or not set. If set, the
enclosing tag *must* be one of the listed tags, or parsing won't
occur.
require_children: similar to require_parents, if set children
won't be parsed if they are not in the list.
disallow_children: similar to, but very different from,
require_children, if it is set the listed tags will not be
parsed inside the tag.
disallow_parents: similar to, but very different from,
require_parents, if it is set the listed tags will not be
parsed inside the tag.
parsed_tags_allowed: an array restricting what BBC can be in the
parsed_equals parameter, if desired.
*/
$codes = array(
// @todo Just to test unparsed_commas, but remove this autacity !
array(
'tag' => 'glow',
'type' => 'unparsed_commas',
'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
'before' => '<table style="border: 0; border-spacing: 0; padding: 0; display: inline; vertical-align: middle; font: inherit;"><tr><td style="filter: Glow(color=$1, strength=$2); font: inherit;">',
'after' => '</td></tr></table> ',
),
array(
'tag' => 'abbr',
'type' => 'unparsed_equals',
'before' => '<abbr title="$1">',
'after' => '</abbr>',
'quoted' => 'optional',
'disabled_after' => ' ($1)',
),
array(
'tag' => 'anchor',
'type' => 'unparsed_equals',
'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
'before' => '<span id="post_$1">',
'after' => '</span>',
),
array(
'tag' => 'b',
'before' => '<strong class="bbc_strong">',
'after' => '</strong>',
),
array(
'tag' => 'br',
'type' => 'closed',
'content' => '<br />',
),
array(
'tag' => 'center',
'before' => '<div class="centertext">',
'after' => '</div>',
'block_level' => true,
),
array(
'tag' => 'code',
'type' => 'unparsed_content',
'content' => '<div class="codeheader">' . $txt['code'] . ': <a href="javascript:void(0);" onclick="return elkSelectText(this);" class="codeoperation">' . $txt['code_select'] . '</a></div><pre class="bbc_code prettyprint">$1</pre>',
'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) {
if (!isset($disabled['code']))
$data = str_replace("\t", "<span class=\"tab\">\t</span>", $data);
},
'block_level' => true,
),
array(
'tag' => 'code',
'type' => 'unparsed_equals_content',
'content' => '<div class="codeheader">' . $txt['code'] . ': ($2) <a href="#" onclick="return elkSelectText(this);" class="codeoperation">' . $txt['code_select'] . '</a></div><pre class="bbc_code prettyprint">$1</pre>',
'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) {
if (!isset($disabled['code']))
$data[0] = str_replace("\t", "<span class=\"tab\">\t</span>", $data[0]);
},
'block_level' => true,
),
array(
'tag' => 'color',
'type' => 'unparsed_equals',
'test' => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s?,\s?){2}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\))\]',
'before' => '<span style="color: $1;" class="bbc_color">',
'after' => '</span>',
),
array(
'tag' => 'email',
'type' => 'unparsed_content',
'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
'validate' => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
},
),
array(
'tag' => 'email',
'type' => 'unparsed_equals',
'before' => '<a href="mailto:$1" class="bbc_email">',
'after' => '</a>',
'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
'disabled_after' => ' ($1)',
),
array(
'tag' => 'footnote',
'before' => '<sup class="bbc_footnotes">%fn%',
'after' => '%fn%</sup>',
'disallow_parents' => array('footnote', 'code', 'anchor', 'url', 'iurl'),
'disallow_before' => '',
'disallow_after' => '',
'block_level' => true,
),
array(
'tag' => 'font',
'type' => 'unparsed_equals',
'test' => '[A-Za-z0-9_,\-\s]+?\]',
'before' => '<span style="font-family: $1;" class="bbc_font">',
'after' => '</span>',
),
array(
'tag' => 'ftp',
'type' => 'unparsed_content',
'content' => '<a href="$1" class="bbc_ftp new_win" target="_blank">$1</a>',
'validate' => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'ftp://') !== 0 && strpos($data, 'ftps://') !== 0)
$data = 'ftp://' . $data;
},
),
array(
'tag' => 'ftp',
'type' => 'unparsed_equals',
'before' => '<a href="$1" class="bbc_ftp new_win" target="_blank">',
'after' => '</a>',
'validate' => function(&$tag, &$data, $disabled) {
if (strpos($data, 'ftp://') !== 0 && strpos($data, 'ftps://') !== 0)
$data = 'ftp://' . $data;
},
'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
'disabled_after' => ' ($1)',
),
array(
'tag' => 'hr',
'type' => 'closed',
'content' => '<hr />',
'block_level' => true,
),
array(
'tag' => 'i',
'before' => '<em>',
'after' => '</em>',
),
array(
'tag' => 'img',
'type' => 'unparsed_content',
'parameters' => array(
'alt' => array('optional' => true),
'width' => array('optional' => true, 'value' => 'width:100%;max-width:$1px;', 'match' => '(\d+)'),
'height' => array('optional' => true, 'value' => 'max-height:$1px;', 'match' => '(\d+)'),
),
'content' => '<img src="$1" alt="{alt}" style="{width}{height}" class="bbc_img resized" />',
'validate' => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
$data = 'http://' . $data;
},
'disabled_content' => '($1)',
),
array(
'tag' => 'img',
'type' => 'unparsed_content',
'content' => '<img src="$1" alt="" class="bbc_img" />',
'validate' => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
$data = 'http://' . $data;
},
'disabled_content' => '($1)',
),
array(
'tag' => 'iurl',
'type' => 'unparsed_content',
'content' => '<a href="$1" class="bbc_link">$1</a>',
'validate' => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
$data = 'http://' . $data;
},
),
array(
'tag' => 'iurl',
'type' => 'unparsed_equals',
'before' => '<a href="$1" class="bbc_link">',
'after' => '</a>',
'validate' => function(&$tag, &$data, $disabled) {
if ($data[0] === '#')
$data = '#post_' . substr($data, 1);
elseif (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
$data = 'http://' . $data;
},
'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
'disabled_after' => ' ($1)',
),
array(
'tag' => 'left',
'before' => '<div style="text-align: left;">',
'after' => '</div>',
'block_level' => true,
),
array(
'tag' => 'li',
'before' => '<li>',
'after' => '</li>',
'trim' => 'outside',
'require_parents' => array('list'),
'block_level' => true,
'disabled_before' => '',
'disabled_after' => '<br />',
),
array(
'tag' => 'list',
'before' => '<ul class="bbc_list">',
'after' => '</ul>',
'trim' => 'inside',
'require_children' => array('li', 'list'),
'block_level' => true,
),
array(
'tag' => 'list',
'parameters' => array(
'type' => array('match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'),
),
'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
'after' => '</ul>',
'trim' => 'inside',
'require_children' => array('li'),
'block_level' => true,
),
array(
'tag' => 'me',
'type' => 'unparsed_equals',
'before' => '<div class="meaction">&nbsp;$1 ',
'after' => '</div>',
'quoted' => 'optional',
'block_level' => true,
'disabled_before' => '/me ',
'disabled_after' => '<br />',
),
array(
'tag' => 'member',
'type' => 'unparsed_equals',
'test' => '[\d*]',
'before' => '<span class="bbc_mention"><a href="' . $scripturl . '?action=profile;u=$1">@',
'after' => '</a></span>',
'disabled_before' => '@',
'disabled_after' => '',
),
array(
'tag' => 'nobbc',
'type' => 'unparsed_content',
'content' => '$1',
),
array(
'tag' => 'pre',
'before' => '<pre class="bbc_pre">',
'after' => '</pre>',
),
array(
'tag' => 'quote',
'before' => '<div class="quoteheader">' . $txt['quote'] . '</div><blockquote>',
'after' => '</blockquote>',
'block_level' => true,
),
array(
'tag' => 'quote',
'parameters' => array(
'author' => array('match' => '(.{1,192}?\]?)', 'quoted' => true),
),
'before' => '<div class="quoteheader">' . $txt['quote_from'] . ': {author}</div><blockquote>',
'after' => '</blockquote>',
'block_level' => true,
),
array(
'tag' => 'quote',
'type' => 'parsed_equals',
'before' => '<div class="quoteheader">' . $txt['quote_from'] . ': $1</div><blockquote>',
'after' => '</blockquote>',
'quoted' => 'optional',
// Don't allow everything to be embedded with the author name.
'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
'block_level' => true,
),
array(
'tag' => 'quote',
'parameters' => array(
'author' => array('match' => '([^<>]{1,192}?)'),
'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d{1,40}|action=profile;u=\d+)'),
'date' => array('match' => '(\d+)', 'validate' => 'htmlTime'),
),
'before' => '<div class="quoteheader"><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . ($modSettings['todayMod'] == 3 ? ' - ' : $txt['search_on']) . ' {date}</a></div><blockquote>',
'after' => '</blockquote>',
'block_level' => true,
),
array(
'tag' => 'quote',
'parameters' => array(
'author' => array('match' => '(.{1,192}?)'),
),
'before' => '<div class="quoteheader">' . $txt['quote_from'] . ': {author}</div><blockquote>',
'after' => '</blockquote>',
'block_level' => true,
),
array(
'tag' => 'right',
'before' => '<div style="text-align: right;">',
'after' => '</div>',
'block_level' => true,
),
array(
'tag' => 's',
'before' => '<del>',
'after' => '</del>',
),
array(
'tag' => 'size',
'type' => 'unparsed_equals',
'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
'before' => '<span style="font-size: $1;" class="bbc_size">',
'after' => '</span>',
'disallow_parents' => array('size'),
'disallow_before' => '<span>',
'disallow_after' => '</span>',
),
array(
'tag' => 'size',
'type' => 'unparsed_equals',
'test' => '[1-7]\]',
'before' => '<span style="font-size: $1;" class="bbc_size">',
'after' => '</span>',
'validate' => function(&$tag, &$data, $disabled) {
$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
$data = $sizes[$data] . 'em';
},
'disallow_parents' => array('size'),
'disallow_before' => '<span>',
'disallow_after' => '</span>',
),
array(
'tag' => 'spoiler',
'before' => '<span class="spoilerheader">' . $txt['spoiler'] . '</span><div class="spoiler"><div class="bbc_spoiler" style="display: none;">',
'after' => '</div></div>',
'block_level' => true,
),
array(
'tag' => 'sub',
'before' => '<sub>',
'after' => '</sub>',
),
array(
'tag' => 'sup',
'before' => '<sup>',
'after' => '</sup>',
),
array(
'tag' => 'table',
'before' => '<div class="bbc_table_container"><table class="bbc_table">',
'after' => '</table></div>',
'trim' => 'inside',
'require_children' => array('tr'),
'block_level' => true,
),
array(
'tag' => 'td',
'before' => '<td>',
'after' => '</td>',
'require_parents' => array('tr'),
'trim' => 'outside',
'block_level' => true,
'disabled_before' => '',
'disabled_after' => '',
),
array(
'tag' => 'th',
'before' => '<th>',
'after' => '</th>',
'require_parents' => array('tr'),
'trim' => 'outside',
'block_level' => true,
'disabled_before' => '',
'disabled_after' => '',
),
array(
'tag' => 'tr',
'before' => '<tr>',
'after' => '</tr>',
'require_parents' => array('table'),
'require_children' => array('td', 'th'),
'trim' => 'both',
'block_level' => true,
'disabled_before' => '',
'disabled_after' => '',
),
array(
'tag' => 'tt',
'before' => '<span class="bbc_tt">',
'after' => '</span>',
),
array(
'tag' => 'u',
'before' => '<span class="bbc_u">',
'after' => '</span>',
),
array(
'tag' => 'url',
'type' => 'unparsed_content',
'content' => '<a href="$1" class="bbc_link" target="_blank">$1</a>',
'validate' => function(&$tag, &$data, $disabled) {
$data = strtr($data, array('<br />' => ''));
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
$data = 'http://' . $data;
},
),
array(
'tag' => 'url',
'type' => 'unparsed_equals',
'before' => '<a href="$1" class="bbc_link" target="_blank">',
'after' => '</a>',
'validate' => function(&$tag, &$data, $disabled) {
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0)
$data = 'http://' . $data;
},
'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
'disabled_after' => ' ($1)',
),
);
// Inside these tags autolink is not recommendable.
$no_autolink_tags = array(
'url',
'iurl',
'ftp',
'email',
);
// So the parser won't skip them.
$itemcodes = array(
'*' => 'disc',
'@' => 'disc',
'+' => 'square',
'x' => 'square',
'#' => 'decimal',
'0' => 'decimal',
'o' => 'circle',
'O' => 'circle',
);
// Let addons add new BBC without hassle.
call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags, &$itemcodes));
// This is mainly for the bbc manager, so it's easy to add tags above. Custom BBC should be added above this line.
if ($message === false)
{
if (isset($temp_bbc))
$bbc_codes = $temp_bbc;
return $codes;
}
if (!isset($disabled['li']) && !isset($disabled['list']))
{
foreach ($itemcodes as $c => $dummy)
$bbc_codes[$c] = array();
}
foreach ($codes as $code)
$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
}
// If we are not doing every enabled tag then create a cache for this parsing group.
if ($parse_tags !== array() && is_array($parse_tags))
{
$temp_bbc = $bbc_codes;
$tags_cache_id = implode(',', $parse_tags);
if (!isset($default_disabled))
$default_disabled = isset($disabled) ? $disabled : array();
// Already cached, use it, otherwise create it
if (isset($parse_tag_cache[$tags_cache_id]))
list ($bbc_codes, $disabled) = $parse_tag_cache[$tags_cache_id];
else
{
foreach ($bbc_codes as $key_bbc => $bbc)
{
foreach ($bbc as $key_code => $code)
{
if (!in_array($code['tag'], $parse_tags))
{
$disabled[$code['tag']] = true;
unset($bbc_codes[$key_bbc][$key_code]);
}
}
}
$parse_tag_cache[$tags_cache_id] = array($bbc_codes, $disabled);
}
}
elseif (isset($default_disabled))
$disabled = $default_disabled;
// Shall we take the time to cache this?
if ($cache_id !== '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
{
// It's likely this will change if the message is modified.
$cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . serialize($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']);
if (($temp = cache_get_data($cache_key, 240)) !== null)
return $temp;
$cache_t = microtime(true);
}
if ($smileys === 'print')
{
// Colors can't well be displayed... supposed to be black and white.
$disabled['color'] = true;
$disabled['me'] = true;
// Links are useless on paper... just show the link.
$disabled['url'] = true;
$disabled['iurl'] = true;
$disabled['email'] = true;
// @todo Change maybe?
if (!isset($_GET['images']))
$disabled['img'] = true;
// @todo Interface/setting to add more?
}
$open_tags = array();
$message = strtr($message, array("\n" => '<br />'));
// The non-breaking-space looks a bit different each time.
$non_breaking_space = '\x{A0}';
$pos = -1;
while ($pos !== false)
{
$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
$pos = strpos($message, '[', $pos + 1);
// Failsafe.
if ($pos === false || $last_pos > $pos)
$pos = strlen($message) + 1;
// Can't have a one letter smiley, URL, or email! (sorry.)
if ($last_pos < $pos - 1)
{
// Make sure the $last_pos is not negative.
$last_pos = max($last_pos, 0);
// Pick a block of data to do some raw fixing on.
$data = substr($message, $last_pos, $pos - $last_pos);
// Take care of some HTML!
if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
{
$data = preg_replace('~&lt;a\s+href=((?:&quot;)?)((?:https?://|ftps?://|mailto:)\S+?)\\1&gt;~i', '[url=$2]', $data);
$data = preg_replace('~&lt;/a&gt;~i', '[/url]', $data);
// <br /> should be empty.
$empty_tags = array('br', 'hr');
foreach ($empty_tags as $tag)
$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '[' . $tag . ' /]', $data);
// b, u, i, s, pre... basic tags.
$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote');
foreach ($closable_tags as $tag)
{
$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
if ($diff > 0)
$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
}
// Do <img ... /> - with security... action= -> action-.
preg_match_all('~&lt;img\s+src=((?:&quot;)?)((?:https?://|ftps?://)\S+?)\\1(?:\s+alt=(&quot;.*?&quot;|\S*?))?(?:\s?/)?&gt;~i', $data, $matches, PREG_PATTERN_ORDER);
if (!empty($matches[0]))
{
$replaces = array();
foreach ($matches[2] as $match => $imgtag)
{
$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
// Remove action= from the URL - no funny business, now.
if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) !== 0)
$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
// Check if the image is larger than allowed.
// @todo - We should seriously look at deprecating some of this in favour of CSS resizing.
if (!empty($modSettings['max_image_width']) && !empty($modSettings['max_image_height']))
{
// For images, we'll want this.
require_once(SUBSDIR . '/Attachments.subs.php');
list ($width, $height) = url_image_size($imgtag);
if (!empty($modSettings['max_image_width']) && $width > $modSettings['max_image_width'])
{
$height = (int) (($modSettings['max_image_width'] * $height) / $width);
$width = $modSettings['max_image_width'];
}
if (!empty($modSettings['max_image_height']) && $height > $modSettings['max_image_height'])
{
$width = (int) (($modSettings['max_image_height'] * $width) / $height);
$height = $modSettings['max_image_height'];
}
// Set the new image tag.
$replaces[$matches[0][$match]] = '[img width=' . $width . ' height=' . $height . $alt . ']' . $imgtag . '[/img]';
}
else
$replaces[$matches[0][$match]] = '[img' . $alt . ']' . $imgtag . '[/img]';
}
$data = strtr($data, $replaces);
}
}
if (!empty($modSettings['autoLinkUrls']))
{
// Are we inside tags that should be auto linked?
$no_autolink_area = false;
if (!empty($open_tags))
{
foreach ($open_tags as $open_tag)
if (in_array($open_tag['tag'], $no_autolink_tags))
$no_autolink_area = true;
}
// Don't go backwards.
// @todo Don't think is the real solution....
$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
if ($pos < $lastAutoPos)
$no_autolink_area = true;
$lastAutoPos = $pos;
if (!$no_autolink_area)
{
// Parse any URLs.... have to get rid of the @ problems some things cause... stupid email addresses.
if (!isset($disabled['url']) && (strpos($data, '://') !== false || strpos($data, 'www.') !== false) && strpos($data, '[url') === false)
{
// Switch out quotes really quick because they can cause problems.
$data = strtr($data, array('&#039;' => '\'', '&nbsp;' => "\xC2\xA0", '&quot;' => '>">', '"' => '<"<', '&lt;' => '<lt<'));
// Only do this if the preg survives.
if (is_string($result = preg_replace(array(
'~(?<=[\s>\.(;\'"]|^)((?:http|https)://[\w\-_%@:|]+(?:\.[\w\-_%]+)*(?::\d+)?(?:/[\p{L}\p{N}\-_\~%\.@!,\?&;=#(){}+:\'\\\\]*)*[/\p{L}\p{N}\-_\~%@\?;=#}\\\\])~ui',
'~(?<=[\s>\.(;\'"]|^)((?:ftp|ftps)://[\w\-_%@:|]+(?:\.[\w\-_%]+)*(?::\d+)?(?:/[\w\-_\~%\.@,\?&;=#(){}+:\'\\\\]*)*[/\w\-_\~%@\?;=#}\\\\])~i',
'~(?<=[\s>(\'<]|^)(www(?:\.[\w\-_]+)+(?::\d+)?(?:/[\p{L}\p{N}\-_\~%\.@!,\?&;=#(){}+:\'\\\\]*)*[/\p{L}\p{N}\-_\~%@\?;=#}\\\\])~ui'
), array(
'[url]$1[/url]',
'[ftp]$1[/ftp]',
'[url=http://$1]$1[/url]'
), $data)))
$data = $result;
$data = strtr($data, array('\'' => '&#039;', "\xC2\xA0" => '&nbsp;', '>">' => '&quot;', '<"<' => '"', '<lt<' => '&lt;'));
}
// Next, emails...
if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false)
{
$data = preg_replace('~(?<=[\?\s' . $non_breaking_space . '\[\]()*\\\;>]|^)([\w\-\.]{1,80}@[\w\-]+\.[\w\-\.]+[\w\-])(?=[?,\s' . $non_breaking_space . '\[\]()*\\\]|$|<br />|&nbsp;|&gt;|&lt;|&quot;|&#039;|\.(?:\.|;|&nbsp;|\s|$|<br />))~u', '[email]$1[/email]', $data);
$data = preg_replace('~(?<=<br />)([\w\-\.]{1,80}@[\w\-]+\.[\w\-\.]+[\w\-])(?=[?\.,;\s' . $non_breaking_space . '\[\]()*\\\]|$|<br />|&nbsp;|&gt;|&lt;|&quot;|&#039;)~u', '[email]$1[/email]', $data);
}
}
}
$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
// If it wasn't changed, no copying or other boring stuff has to happen!
if ($data !== substr($message, $last_pos, $pos - $last_pos))
{
$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
// Since we changed it, look again in case we added or removed a tag. But we don't want to skip any.
$old_pos = strlen($data) + $last_pos;
$pos = strpos($message, '[', $last_pos);
$pos = $pos === false ? $old_pos : min($pos, $old_pos);
}
}
// Are we there yet? Are we there yet?
if ($pos >= strlen($message) - 1)
break;
$tags = strtolower($message[$pos + 1]);
if ($tags === '/' && !empty($open_tags))
{
$pos2 = strpos($message, ']', $pos + 1);
if ($pos2 === $pos + 2)
continue;
$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
$to_close = array();
$block_level = null;
do
{
$tag = array_pop($open_tags);
if (!$tag)
break;
if (!empty($tag['block_level']))
{
// Only find out if we need to.
if ($block_level === false)
{
array_push($open_tags, $tag);
break;
}
// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
{
foreach ($bbc_codes[$look_for[0]] as $temp)
if ($temp['tag'] === $look_for)
{
$block_level = !empty($temp['block_level']);
break;
}
}
if ($block_level !== true)
{
$block_level = false;
array_push($open_tags, $tag);
break;
}
}
$to_close[] = $tag;
}
while ($tag['tag'] !== $look_for);
// Did we just eat through everything and not find it?
if ((empty($open_tags) && (empty($tag) || $tag['tag'] !== $look_for)))
{
$open_tags = $to_close;
continue;
}
elseif (!empty($to_close) && $tag['tag'] !== $look_for)
{
if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
{
foreach ($bbc_codes[$look_for[0]] as $temp)
if ($temp['tag'] === $look_for)
{
$block_level = !empty($temp['block_level']);
break;
}
}
// We're not looking for a block level tag (or maybe even a tag that exists...)
if (!$block_level)
{
foreach ($to_close as $tag)
array_push($open_tags, $tag);
continue;
}
}
foreach ($to_close as $tag)
{
$message = substr_replace($message, "\n" . $tag['after'] . "\n", $pos, $pos2 + 1 - $pos);
$pos += strlen($tag['after']) + 2;
$pos2 = $pos - 1;
// See the comment at the end of the big loop - just eating whitespace ;).
if (!empty($tag['block_level']) && substr($message, $pos, 6) === '<br />')
$message = substr_replace($message, '', $pos, 6);
if (!empty($tag['trim']) && $tag['trim'] !== 'inside' && preg_match('~(<br />|&nbsp;|\s)*~', substr($message, $pos), $matches) !== 0)
$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
}
if (!empty($to_close))
{
$to_close = array();
$pos--;
}
continue;
}
// No tags for this character, so just keep going (fastest possible course.)
if (!isset($bbc_codes[$tags]))
{
continue;
}
$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
$tag = null;
foreach ($bbc_codes[$tags] as $possible)
{
$pt_strlen = strlen($possible['tag']);
// Not a match?
if (substr_compare($message, $possible['tag'], $pos + 1, $pt_strlen, true) !== 0)
continue;
$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
// A test validation?
if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
continue;
// Do we want parameters?
elseif (!empty($possible['parameters']))
{
if ($next_c !== ' ')
continue;
}
elseif (isset($possible['type']))
{
// Do we need an equal sign?
if ($next_c !== '=' && in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')))
continue;
// Maybe we just want a /...
if ($possible['type'] === 'closed' && $next_c !== ']' && substr($message, $pos + 1 + $pt_strlen, 2) !== '/]' && substr($message, $pos + 1 + $pt_strlen, 3) !== ' /]')
continue;
// An immediate ]?
if ($possible['type'] === 'unparsed_content' && $next_c !== ']')
continue;
}
// No type means 'parsed_content', which demands an immediate ] without parameters!
elseif ($next_c !== ']')
continue;
// Check allowed tree?
if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
continue;
elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
continue;
// If this is in the list of disallowed child tags, don't parse it.
elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
continue;
// Not allowed in this parent, replace the tags or show it like regular text
elseif (isset($possible['disallow_parents']) && ($inside !== null && in_array($inside['tag'], $possible['disallow_parents'])))
{
if (!isset($possible['disallow_before'], $possible['disallow_after']))
continue;
$possible['before'] = isset($possible['disallow_before']) ? $tag['disallow_before'] : $possible['before'];
$possible['after'] = isset($possible['disallow_after']) ? $tag['disallow_after'] : $possible['after'];
}
$pos1 = $pos + 1 + $pt_strlen + 1;
// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
if ($possible['tag'] === 'quote')
{
// Start with standard
$quote_alt = false;
foreach ($open_tags as $open_quote)
{
// Every parent quote this quote has flips the styling
if ($open_quote['tag'] === 'quote')
$quote_alt = !$quote_alt;
}
// Add a class to the quote to style alternating blockquotes
// @todo - Frankly it makes little sense to allow alternate blockquote
// styling without also catering for alternate quoteheader styling.
// I do remember coding that some time back, but it seems to have gotten
// lost somewhere in the Elk processes.
// Come to think of it, it may be better to append a second class rather
// than alter the standard one.
// - Example: class="bbc_quote" and class="bbc_quote alt_quote".
// This would mean simpler CSS for themes (like default) which do not use the alternate styling,
// but would still allow it for themes that want it.
$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
}
// This is long, but it makes things much easier and cleaner.
if (!empty($possible['parameters']))
{
$preg = array();
foreach ($possible['parameters'] as $p => $info)
$preg[] = '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . ')' . (empty($info['optional']) ? '' : '?');
// Okay, this may look ugly and it is, but it's not going to happen much and it is the best way
// of allowing any order of parameters but still parsing them right.
$param_size = count($preg) - 1;
$preg_keys = range(0, $param_size);
$message_stub = substr($message, $pos1 - 1);
// If an addon adds many parameters we can exceed max_execution time, lets prevent that
// 5040 = 7, 40,320 = 8, (N!) etc
$max_iterations = 5040;
// Step, one by one, through all possible permutations of the parameters until we have a match
do {
$match_preg = '~^';
foreach ($preg_keys as $key)
$match_preg .= $preg[$key];
$match_preg .= '\]~i';
// Check if this combination of parameters matches the user input
$match = preg_match($match_preg, $message_stub, $matches) !== 0;
} while (!$match && --$max_iterations && ($preg_keys = pc_next_permutation($preg_keys, $param_size)));
// Didn't match our parameter list, try the next possible.
if (!$match)
continue;
$params = array();
for ($i = 1, $n = count($matches); $i < $n; $i += 2)
{
$key = strtok(ltrim($matches[$i]), '=');
if (isset($possible['parameters'][$key]['value']))
$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
elseif (isset($possible['parameters'][$key]['validate']))
$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
else
$params['{' . $key . '}'] = $matches[$i + 1];
// Just to make sure: replace any $ or { so they can't interpolate wrongly.
$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
}
foreach ($possible['parameters'] as $p => $info)
{
if (!isset($params['{' . $p . '}']))
$params['{' . $p . '}'] = '';
}
$tag = $possible;
// Put the parameters into the string.
if (isset($tag['before']))
$tag['before'] = strtr($tag['before'], $params);
if (isset($tag['after']))
$tag['after'] = strtr($tag['after'], $params);
if (isset($tag['content']))
$tag['content'] = strtr($tag['content'], $params);
$pos1 += strlen($matches[0]) - 1;
}
else
$tag = $possible;
break;
}
// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
if ($smileys !== false && $tag === null && isset($message[$pos + 2]) && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] === ']' && !isset($disabled['list']) && !isset($disabled['li']))
{
if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
{
continue;
}
$tag = $itemcodes[$message[$pos + 1]];
// First let's set up the tree: it needs to be in a list, or after an li.
if ($inside === null || ($inside['tag'] !== 'list' && $inside['tag'] !== 'li'))
{
$open_tags[] = array(
'tag' => 'list',
'after' => '</ul>',
'block_level' => true,
'require_children' => array('li'),
'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
);
$code = '<ul' . ($tag === '' ? '' : ' style="list-style-type: ' . $tag . '"') . ' class="bbc_list">';
}
// We're in a list item already: another itemcode? Close it first.
elseif ($inside['tag'] === 'li')
{
array_pop($open_tags);
$code = '</li>';
}
else
$code = '';
// Now we open a new tag.
$open_tags[] = array(
'tag' => 'li',
'after' => '</li>',
'trim' => 'outside',
'block_level' => true,
'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
);
// First, open the tag...
$code .= '<li>';
$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
$pos += strlen($code) - 1 + 2;
// Next, find the next break (if any.) If there's more itemcode after it, keep it going - otherwise close!
$pos2 = strpos($message, '<br />', $pos);
$pos3 = strpos($message, '[/', $pos);
if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
{
preg_match('~^(<br />|&nbsp;|\s|\[)+~', substr($message, $pos2 + 6), $matches);
$message = substr_replace($message, (!empty($matches[0]) && substr($matches[0], -1) === '[' ? '[/li]' : '[/li][/list]'), $pos2, 0);
$open_tags[count($open_tags) - 2]['after'] = '</ul>';
}
// Tell the [list] that it needs to close specially.
else
{
// Move the li over, because we're not sure what we'll hit.
$open_tags[count($open_tags) - 1]['after'] = '';
$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
}
continue;
}
// Implicitly close lists and tables if something other than what's required is in them. This is needed for itemcode.
if ($tag === null && $inside !== null && !empty($inside['require_children']))
{
array_pop($open_tags);
$message = substr_replace($message, "\n" . $inside['after'] . "\n", $pos, 0);
$pos += strlen($inside['after']) - 1 + 2;
}
// No tag? Keep looking, then. Silly people using brackets without actual tags.
if ($tag === null)
continue;
// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
if (isset($inside['disallow_children']))
$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
// Is this tag disabled?
if (isset($disabled[$tag['tag']]))
{
if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
{
$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
$tag['content'] = isset($tag['type']) && $tag['type'] === 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
}
elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
{
$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
}
else
$tag['content'] = $tag['disabled_content'];
}
// We use this alot
$tag_strlen = strlen($tag['tag']);
// The only special case is 'html', which doesn't need to close things.
if (!empty($tag['block_level']) && $tag['tag'] !== 'html' && empty($inside['block_level']))
{
$n = count($open_tags) - 1;
while (empty($open_tags[$n]['block_level']) && $n >= 0)
$n--;
// Close all the non block level tags so this tag isn't surrounded by them.
for ($i = count($open_tags) - 1; $i > $n; $i--)
{
$message = substr_replace($message, "\n" . $open_tags[$i]['after'] . "\n", $pos, 0);
$ot_strlen = strlen($open_tags[$i]['after']);
$pos += $ot_strlen + 2;
$pos1 += $ot_strlen + 2;
// Trim or eat trailing stuff... see comment at the end of the big loop.
if (!empty($open_tags[$i]['block_level']) && substr_compare($message, '<br />', $pos, 6) === 0)
$message = substr_replace($message, '', $pos, 6);
if (!empty($open_tags[$i]['trim']) && $tag['trim'] !== 'inside' && preg_match('~(<br />|&nbsp;|\s)*~', substr($message, $pos), $matches) !== 0)
$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
array_pop($open_tags);
}
}
// No type means 'parsed_content'.
if (!isset($tag['type']))
{
// @todo Check for end tag first, so people can say "I like that [i] tag"?
$open_tags[] = $tag;
$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
$pos += strlen($tag['before']) - 1 + 2;
}
// Don't parse the content, just skip it.
elseif ($tag['type'] === 'unparsed_content')
{
$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
if ($pos2 === false)
continue;
$data = substr($message, $pos1, $pos2 - $pos1);
if (!empty($tag['block_level']) && substr($data, 0, 6) === '<br />')
{
$data = substr($data, 6);
}
if (isset($tag['validate']))
$tag['validate']($tag, $data, $disabled);