Page 1 of 1

Check for BBCodes Missing / Improperly closed tags Rate Topic: -----

#1 ahmad_511  Icon User is offline

  • MSX
  • member icon

Reputation: 131
  • View blog
  • Posts: 722
  • Joined: 28-April 07

Posted 19 July 2011 - 09:53 AM

Hi there,

in this tutorial we'll use a simple rule to check for missing opening/closing BBCodes or improperly closed ones in non recursive way, this rule can be used to check for similar kinds of errors and it says (Last Opened First Closed).
of course we're talking here about Bulletin Board codes but you can adjusted to work with any basic markup languages.

Intro:
the concept I'll be using here depends on a Regular Expression that looks for tags and not their contents, the most important thing about this RegExp is that it must be able to provide us with BBCode tags in supplied string in the same order they found regardless of the tag name or the if it's an opening or closing tag.

the work flow can be summarized in these steps:
  • build one RegExp that matches opening / closing tags and use the replace method to process matched strings.
  • check if the match is one of tag set we want to check for. otherwise return it intact.
  • when an opening tag is found, we surround it with a temporary marker tag of our choice which has a fixed pattern and can uniquely identify the tag (using incremental counter), push the tag name and its identifier to an array (bbc_openedArr).
  • when a closing tag is found we test it against last opening tag in the bbc_openedArr:
    • if the current closing tag matches the last opened one we then remove it from bbc_openedArr and continue with no error.
    • if the current closing tag doesn't matches the last opened tag in bbc_openedArr, we either:
      • didn't open this tag.
      • we are improperly closing it.

    this can be determined by searching bbc_openedArr for last tag that matches the current closing one, finding it means that the current tag is closed improperly, otherwise, the current tag wasn't opened at all.
    in both cases we'll return the tag surrounded by span for highlighting purposes.
  • having some items in bbc_openedArr after the RegExp replacing is done means there are some tags did not closed yet and we need to highlight them as well, this can be simply done by looping the bbc_openedArr and surrounding all opening tags with highlighting spans using the marker tags we added before and its identifier.
  • clean up unnecessary marker tags as we don't need them anymore.
  • convert spaces and line breaks to something readable.
  • return the new string.


Open up your favorite text editor, create a new file and save it as bbc_check.js for example.

Code walk through:

first, we create the main function BBC_Check that accepts 2 arguments:
str the string to be checked for errors.
bbc_tags the BBCodes set we want to check for, a string contains BBCodes separated by a pipe ( | ).

inside it we initiate some variables:
error_count: object holds the count of found errors in 3 properties, mo: Missing Opening, mc: Missing Closing, ic: Improperly Closed.

bbc_openedArr: Array to store opened tag name and its identification number.

bb_id: incremental number to uniquely identify an opening tag

re: the RegExp object to match opening / closing tags, you can use your own RegExp as long as it can capture the opening/closing tag name.

also we lowe-case the bbc_tags to make comparison easier.
function BBC_Check(str,bbc_tags){
	var error_count={"mo":0,"mc":0,"ic":0};
	var bbc_openedArr=[];
	var bb_id=0;
	bbc_tags=bbc_tags.toLowerCase();
	var re=new RegExp("(?:\\[(\\/?(?:"+bbc_tags+"))\\s*(?:\\s*=\\s*(?:.*?))*\\s*\\])","gi");

	// the rest of code goes here
}


you have to know that we chose the pipe character to separate bbc_tags codes for one reason which is the pipe ( | ) is the way to say ( OR ) when dealing with RegExp.
I'll quickly explain what this RegExp is trying to match:
  • any sub string starts with an opening square bracket.
  • optionally followed by a forward slash ( as in closing tag ).
  • must followed by any of passed tag names.
  • followed by zero or more white spaces.
  • optionally followed by an equal sign and a value of any characters (value and equal sign could be surrounded by zero or more white spaces).
  • ends with a closing square bracket.


Note:
braces start with ?: mean the pattern group inside will not be captured. for more information about RegExp you can refer to http://www.regular-expressions.info/
the second argument gi in the RegExp object re means the search will be Global (doesn't stop on first occurrence) and case Insensitive (it doesn't matter if you used a capital or small letters in the BBCodes)


now, it's important to replace the grater than > and less than < symbols with their html entities for 3 reasons:
  • as everyone knows those symbols are used to construct html tags and we simply don't want the browser to render them when appeared within user input.
  • this will prevent executing scripts user may add to the content.
  • we'll use those symbols in the marker tags for potential error in opening tags, so it doesn't conflict with users input even if he used the same pattern we use (since user input we'll be using the entity equivalents of <,>).


so, let's deactivate some symbols.
str=str.replace(/</g,"&lt;").replace(/>/g,"&gt;");



we'll use the RegExp in cooperation with the replace method, so we can surround problematic BBCode tags with highlighting html spans.
and an anonymous function will be used to process matching strings, and since we have one group only to capture in the RegExp string, this function will accept 2 arguments:
a: the matched string (opening tag with its optional value or closing tag, including surrounding square brackets).
b: the captured pattern group (opening or closing tag name).

this anonymous function checks for argument (b) and return argument (a) surrounded with some tags.

first we lower-case the tag name to make it easier to check if it's one of passed BBCodes tag names.

after that we need to know (due to the RegExp I'm using) if found tag is representing an opening or closing tag, closing tag here starts with a forward slash ( / ) in this case we remove it to get the tag name only,

	str=str.replace(re,function(a,b ){
		b=b.toLowerCase();
		var isClosingTag=(b.substr(0,1)=="/");
		if(isClosingTag)b=b.substr(1);

		// to be continue



we then check if captured tag name b belongs to the passed BBCodes tag names, if not, we don't have to do anything with it and we return matched string (a) as is (everything inside and including the square brackets).
and here is a simple way to do that.
		if(("|"+bbc_tags+"|").indexOf("|"+b+"|")==-1) return a;



the script will continue if tag name belongs to the BBCode tag names we passed to the main function.
and here we're facing 2 possibilities:
1- b represents an opening tag, so we push it with the current bb_id into the temporary array bbc_openedArr, increment the bb_id and return matching string surrounded by our special marker tag in order to be able to refer to it later using the bb_id in the bbc_openedArr.
		if(!isClosingTag){
			bbc_openedArr.push([b,bb_id]);
			var id=bb_id++;
			return "<BB_OPEN_ID="+id+">"+a+"</BB_OPEN_ID="+id+">";
		}

		// to be continue



2- b represents a closing tag, and here we need to check for 3 things:
- the tag is closed properly.
- the tag is improperly closed
- the opening tag is missing

so how to do that?
it's time to use the bbc_openedArr where all opening tags are stored in so far.
considering the simple rule mentioned at the beginning of this tutorial "Last Opened tag must be First Closed", we'll take a look at last element in bbc_openedArr which tells us what is the last opened tag and then we try to see:

if the name of the current closing tag matches the last element in the array, that means the tag is closed properly, so we remove the opening tag from the array and return the matching string as is.

		var lastOpened=bbc_openedArr[bbc_openedArr.length-1]||"";
		if(lastOpened[0]==B)/>{
			bbc_openedArr.pop()
			return a;
		}

		// to be continue



if there is no match, we are in troubles right? well, there are two possibilities here:
- we forgot to close a child BBCode tag, so last opening element in the array still waiting to be closed.
- or we forgot to add the opening tag we're trying close and it's not in the bbc_openedArr at all.

to check which case we're encountering here we'll need to search the bbc_openedArr for an opening tag has the same name of the current closing one, if we were able to find it we store its index in a temporary variable ndx.

Note:
we search the array looking for the last matching opening tag so will do it in revers order.
you may want to change the code a little bit and use the lastIndexOf method and get ride of the for-loop I'm using here, but you have to consider that this method is not supported by all browser (or at least IE).
anyways, this loop shouldn't be very long unless you have a lot of nested opening tags (or you keep forgetting to close them).

		var ndx=-1;
		for(i=bbc_openedArr.length-1;i>=0;i--){
			if(bbc_openedArr[i][0]==B)/>{
				ndx=i;
				break;
			}
		}

		// to be continue



finding the opening tag inside above loop means we certainly messed up codes nesting and we improperly closed this tag.
so, we highlight the current closing tag as "Improperly Closed" by surrounding matching string with a span and return it, we then remove opening tag we found from bbc_openedArr since this problem is settled now.

if we couldn't find the opening tag we highlight the current closing tag as it misses an opening tag, surround it with corresponding span and return it.

in both cases we increase corresponding error counter.

		if(ndx>-1){
			error_count["ic"]++;
			bbc_openedArr.splice(ndx,1);
			return " <span class='bb_err_ic' title='Improperly closed'>"+a+"</span> ";
		}else{
			error_count["mo"]++;
			return " <span class='bb_err_mo' title='Missing Opening tag'>"+a+"</span> ";
		}
	});



now we have our replace method process done, but that's doesn't mean we highlighted all of the errors, we still need to find opened and never closed tags.

once again we'll use bbc_openedArr since it stores all opened tags, and because we were popping up all tags reference each time we found its closing tag, we can say that all remained items in this array (if any) have not been closed.

now, how can we refer and highlight those tags?
as you may still remember the bbc_openedArr stores the tag name in addition to its identification number, and we have been surrounding all opening tags with a marker tag of our own choice and this tag uses the same identification number in the bbc_openedArr.

so, we know the tag pattern, we know its ID, it will be easy for us now to replace those marker tags (surrounding opening tags) with necessary highlighting spans, also we'll increase the error counter.

	var ot_length=bbc_openedArr.length;
	for(i=0;i<ot_length;i++){
		error_count["mc"]++;
		var ot=bbc_openedArr.pop();
		str=str.replace("<BB_OPEN_ID="+ot[1]+">"," <span class='bb_err_mc' title='Missing Closing tag'>").replace("</BB_OPEN_ID="+ot[1]+">","</span> ");
	}



are we ready yet, not exactly, we still need to do some clean up by removing unused marker tags that still surrounding opening tags that do not have any problems.

	str=str.replace(/<BB_OPEN_ID=.*?>/gi,"").replace(/<\/BB_OPEN_ID=.*?>/gi,"");



last step is to display error summary, make the code more readable by replacing spaces and line breaks with HTML equivalent codes and finally return the modified input string.
we are ready now to pinpoint errors we found to the user.

	var error_total=error_count["mo"]+error_count["mc"]+error_count["ic"];
	if(error_total>0)str="<p class='bb_error_summary'>"+error_total+" Error"+(error_total>1?"s were":" was")+" detected <span class='bb_err_mo' title='Missing Opening tag'>"+error_count["mo"]+"</span> <span class='bb_err_mc' title='Missing Closing tag'>"+error_count["mc"]+"</span> <span class='bb_err_ic' title='Improperly closed'>"+error_count["ic"]+"</span></p>"+str;

	str=str.replace(/\r\n|\r|\n/g,"<br />").replace(/  /g,"&nbsp;&nbsp;");

	return str;
}







Usage:
as you may notice we added some class attributes to spans surrounding found errors, so we need to add some styling properties to these spans in order to see the result.
it's up to you whether to put the styling properties inside your html document or use a separate file styles.css for example, here I'm using the second option which requires us to link it to our document.
inside the document header section add this:
<link rel="StyleSheet" href="styles.css" />



and here is the styles.css contents: (modify them as needed)
.bb_error_summary{
	padding:10px;
	margin:0px;
	color:#FF0000;
	font-size:18px;
	font-family:monospace;
	font-weight:bold;
	font-style:normal;
	text-align:center;
}

.bb_err_mo{
	padding:0px 4px;
	letter-spacing:2px;
	color:#990000;
	background-color:#F4CCCC;
	border:1px dashed #990000;
	border-left:2px solid #990000;
}

.bb_err_mc{
	padding:0px 4px;
	letter-spacing:2px;
	color:#38761D;
	background-color:#D9EAD3;
	border:1px dashed #38761D;
	border-left:2px solid #38761D;
}

.bb_err_ic{
	padding:0px 4px;
	letter-spacing:2px;
	color:#0B5394;
	background-color:#CFE2F3;
	border:1px dashed #0B5394;
	border-left:2px solid #0B5394;
}



now load the bbc_check.js code file.
<script type="text/javascript" src="bbc_check.js"></script>



Now, let's create a function check that calls BBC_Check function:

<script type="text/javascript">
function check(){
	var bbc_tags="b|i|u|s|left|center|right|sup|sub|quote|list|li|table|tr|th|td|url|email|rtl|ltr|color|background|size|font";

	var result=BBC_Check(document.getElementById("tar1").value,bbc_tags);

	document.getElementById("checkResult").innerHTML=result;
}
</script>



in the document's body I'll add an empty text area tar1 for user input, a button to start the checking process by calling the check function and an empty div checkResult to display the result.

<textarea id="tar1" cols="50" rows="15"></textarea><br />
<input type="button" onclick="check()" value="Check" /><br />
<div id="checkResult"></div>



Ok, maybe it's unusual to hear this but try to type in some messy codes please :)

Hope it helps.

all files can be downloaded here Attached File  bbc_check.zip (2.55K)
Number of downloads: 214
a live test can be found here BBCodes Tags Check

Is This A Good Question/Topic? 1
  • +

Replies To: Check for BBCodes Missing / Improperly closed tags

#2 DimitriV  Icon User is offline

  • They don't think it be like it is, but it do
  • member icon

Reputation: 584
  • View blog
  • Posts: 2,738
  • Joined: 24-July 11

Posted 17 April 2012 - 08:50 PM

I tried your live demo and I must say, that is pretty cool.
Was This Post Helpful? 0
  • +
  • -

#3 ahmad_511  Icon User is offline

  • MSX
  • member icon

Reputation: 131
  • View blog
  • Posts: 722
  • Joined: 28-April 07

Posted 18 April 2012 - 10:37 AM

Thank you DimitriV for the comment
and thank you e_i_pi for the [+]

I really feel happy to share some codes but I always lack of time

Regards
Was This Post Helpful? 0
  • +
  • -

Page 1 of 1