Page 1 of 2

template engine - nested blocks

Posted: Sat Nov 17, 2007 6:13 pm
by s.dot
I'm developing a template engine. To replace template blocks such as..

Code: Select all

<!-- Start Name //-->
<p>My name is {name} and I am {years} years old.</p>
<!-- End Name //-->
I can handle that, by doing the following, using just an example of 3 names

Code: Select all

$tpl->assignBlock(
    'Name',
    array(
        'name' => 'Scott',
        'years' => 22
    )
);

$tpl->assignBlock(
    'Name',
    array(
        'name' => 'Jim',
        'years' => 75
    )
);

$tpl->assignBlock(
    'Name',
    array(
        'name' => 'Ann',
        'years' => 40
    )
);
And then in the template object, I can handle that by doing

Code: Select all

public function assignBlock($blockName, $values)
{
    $this->_blocks[$blockName][] = $values;
}
Then, in my display() method, I can grab just the one block of HTML from the .tpl file, and turn it into however many items are in that array.. here's that snippet of code

Code: Select all

//replace blocks
foreach ($this->_blocks AS $block => $blockValues)
{	
	//get block
	$block = preg_match("/<!-- Start $block \/\/-->(.+?)<!-- End $block \/\/-->/ism", $tpl, $matches);
	$block = $matches[1];
	
	//new block for each iteration
	$newBlocks = array();
	foreach ($blockValues AS $blockValue)
	{
		$newBlock = $block;
		foreach ($blockValue AS $key => $value)
		{
			$newBlock = str_replace('{' . $key . '}', $value, $newBlock);
		}
		//save block
		$newBlocks[] = $newBlock;
	}
			
	//implode blocks back to string
	$newBlock = implode("\n", $newBlocks);
			
	//replace original block with replaced looped block
	$tpl = str_replace($block, $newBlock, $tpl);
}
So, from that piece of code, my original tpl turns into

Code: Select all

<p>My name is Scott and I am 22 years old.</p>
<p>My name is Jim and I am 75 years old.</p>
<p>My name is Ann and I am 40 years old.</p>


All of the above works fine, I just wanted to explain what I am doing.



My problem comes when I have nested "blocks" :( I just can't figure out the logic to it. I'm pretty sure it has to be a more than 2-demensional multi-demensional array.

Here's a sample of a nested .tpl block

Code: Select all

<table width="100%" cellspacing="2" cellpadding="2" border="0" style="border: solid 1px #000;">
	<!-- Start Category //-->
	<tr>
		<td colspan="4" class="cat_name">{cat_name}</td>
	</tr>
	<tr>
		<td width="60%" class="titletext">{L_FORUM}</td>
		<td width="10%" class="titletext">{L_TOPICS}</td>
		<td width="10%" class="titletext">{L_POSTS}</td>
		<td width="20%" class="titletext">{L_LAST_POST}</td>
	</tr>
	<!-- Start Forum Row //-->
	<tr>
		<td colspan="4" class="forum_row">{forum_name}</td>
	</tr>
	<!-- End Forum Row //-->
	<tr>
		<td colspan="4">&nbsp;</td>
	</tr>
	<!-- End Category //-->
</table>
Now, the categories come out just fine, but they have forum row blocks nested in between. But I can't figure out how to display the forum row blocks for the appropriate category. =[

Right now, all forum rows are showing in each category.

I hope I've explained my problem well. Any help?

Posted: Sun Nov 18, 2007 9:52 pm
by s.dot
no one, eh?

Posted: Mon Nov 19, 2007 12:22 am
by Christopher
Have you looked at how the various template libraries do nested blocks? As I recall you can either use fancy regex or split on all block tags and then walk through from both ends and match begin and end tags on each level.

Posted: Mon Nov 19, 2007 11:05 am
by Mordred
You would have to work recursively - greedily match a block start-body-end, and then call the same parser on the body part only. If you allow blocks with the same name, the "greedy" part is important, but iirc you were doing this in your code anyway.

Btw I now see that your code is horribly inefficient - it replaces blocks on the fly with regular expressions. Come on! Even smarty would do it better :)

Posted: Mon Nov 19, 2007 12:36 pm
by s.dot
Mordred wrote:You would have to work recursively - greedily match a block start-body-end, and then call the same parser on the body part only. If you allow blocks with the same name, the "greedy" part is important, but iirc you were doing this in your code anyway.

Btw I now see that your code is horribly inefficient - it replaces blocks on the fly with regular expressions. Come on! Even smarty would do it better :)
Thanks for your input! ;d
I'll get it working first (just to get my head around it) and then try to optimize it a bit!
Recursively, eh?

I was thinking something like the following would work

Code: Select all

$this->_blocks[$mainBlock][$subBlock][$anotherSubBlock][$xamountfollowing][] = $values;
Then loop through, check for x amount of sub blocks, maybe using count()? and looping through each sub block and replacing, then imploding back into a string.

I don't know yet =/ It's early

Posted: Mon Nov 19, 2007 1:35 pm
by Mordred
I'm afraid you're doing it wrong from the start, later optimizations would not replace the inadequacy of the design. But that's your death ;)

A better alternative to recursive application of regex would be to decide on a number of nested block levels you would accept at most, and build ONE regexp that would handle them. You could even generate it for a given max number of levels :)

Posted: Mon Nov 19, 2007 1:41 pm
by John Cartwright
Take a look at the preg_replace_callback() example "Example 1720. preg_replace_callback() using recursive structure to handle encapsulated BB code".. that should give you some ideas ;)

Posted: Mon Nov 19, 2007 2:18 pm
by s.dot
Mordred wrote:I'm afraid you're doing it wrong from the start, later optimizations would not replace the inadequacy of the design. But that's your death ;)

A better alternative to recursive application of regex would be to decide on a number of nested block levels you would accept at most, and build ONE regexp that would handle them. You could even generate it for a given max number of levels :)
I think I get it now. You're saying I should match all blocks and determine nesting at once. And then match data against my nesting levels.

Posted: Mon Nov 19, 2007 2:18 pm
by s.dot
Jcart wrote:Take a look at the preg_replace_callback() example "Example 1720. preg_replace_callback() using recursive structure to handle encapsulated BB code".. that should give you some ideas ;)
Thanks! That looks promising.

Posted: Mon Nov 19, 2007 3:11 pm
by Christopher
scottayy, if you are building something like this I would be interested in both helping and the code.

Posted: Mon Nov 19, 2007 4:08 pm
by s.dot
arborint wrote:scottayy, if you are building something like this I would be interested in both helping and the code.
It is a very small part of a larger software package I am building. I would've preferred to use smarty or template lite, but I wanted to do the whole project from scratch.

Posted: Mon Nov 19, 2007 7:12 pm
by alex.barylski
It is a very small part of a larger software package I am building. I would've preferred to use smarty or template lite, but I wanted to do the whole project from scratch.
Nothing wrong with learning about different systems by actually doing. There is little in terms of accepted best practices, etc so you can really tear apart existing template engines and take what you like and leave what you don't.

The most formal paper on the subject I have ever read was by the author of ANTRL:

http://www.stringtemplate.org/

Here is the paper:

http://www.cs.usfca.edu/~parrt/papers/mvc.templates.pdf

Interesting read

Posted: Tue Nov 20, 2007 11:06 am
by s.dot
I was speaking with feyd yesterday, and (i believe) he implies recursion is not necessary. He gave me a regex to try, and I've tried in on the following template code

Code: Select all

<table width="100%" cellspacing="2" cellpadding="2" border="0" style="border: solid 1px #000;">
	<!-- Start Category -->
	<tr>
		<td colspan="4" class="cat_name">{cat_name}</td>
	</tr>
	<tr>
		<td width="60%" class="titletext">{L_FORUM}</td>
		<td width="10%" class="titletext">{L_TOPICS}</td>
		<td width="10%" class="titletext">{L_POSTS}</td>
		<td width="20%" class="titletext">{L_LAST_POST}</td>
	</tr>
	<!-- Start Forum Row -->
	<tr>
		<td colspan="4" class="forum_row">{forum_name}</td>
	</tr>
	<!-- Start Devnet -->
	<p>I love devnet! {adjective}</p>
	<!-- End Devnet -->
	<!-- End Forum Row -->
	<tr>
		<td colspan="4">&nbsp;</td>
	</tr>
	<!-- End Category -->
</table>
<!-- Start Test -->
<p>{test}</p>
<!-- End Test -->

Code: Select all

//grab all blocks and position
preg_match_all("/<!-- (?:Start|End) .+? -->/im", $this->_code, $matches, PREG_OFFSET_CAPTURE);
Which gives me the output of:

Code: Select all

Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => <!-- Start Category -->
                    [1] => 98
                )

            [1] => Array
                (
                    [0] => <!-- Start Forum Row -->
                    [1] => 419
                )

            [2] => Array
                (
                    [0] => <!-- Start Devnet -->
                    [1] => 516
                )

            [3] => Array
                (
                    [0] => <!-- End Devnet -->
                    [1] => 576
                )

            [4] => Array
                (
                    [0] => <!-- End Forum Row -->
                    [1] => 598
                )

            [5] => Array
                (
                    [0] => <!-- End Category -->

                    [1] => 669
                )

            [6] => Array
                (
                    [0] => <!-- Start Test -->
                    [1] => 702
                )

            [7] => Array
                (
                    [0] => <!-- End Test -->
                    [1] => 738
                )

        )

)
:) Using this information, I could do some calculations with substr() and get each block of code, ready for replacing.

I'm unsure of where to go next. Do I need to build a stack, pushing and popping elements to get the correct nest level, or am I ready to loop and replace?

I'm completely lost on this. :cry:

Posted: Tue Nov 20, 2007 11:13 am
by Christopher
You probably want to loop through that array and match the begins with the ends -- that would use a stack.

Posted: Tue Nov 20, 2007 11:50 am
by s.dot
I got it! ;d Although I didn't need a stack.

Full array of matches

Code: Select all

//grab all blocks and position
preg_match_all("/<!-- (?:Start|End) .+? -->/im", $this->_code, $matches, PREG_OFFSET_CAPTURE);
Produces:

Code: Select all

<pre>Array
(
    [0] => Array
        (
            [0] => Array
                (
                    [0] => <!-- Start Category -->
                    [1] => 98
                )

            [1] => Array
                (
                    [0] => <!-- Start Forum Row -->
                    [1] => 419
                )

            [2] => Array
                (
                    [0] => <!-- Start Devnet -->
                    [1] => 516
                )

            [3] => Array
                (
                    [0] => <!-- End Devnet -->
                    [1] => 576
                )

            [4] => Array
                (
                    [0] => <!-- End Forum Row -->
                    [1] => 598
                )

            [5] => Array
                (
                    [0] => <!-- End Category -->

                    [1] => 669
                )

            [6] => Array
                (
                    [0] => <!-- Start Test -->
                    [1] => 702
                )

            [7] => Array
                (
                    [0] => <!-- End Test -->
                    [1] => 738
                )

        )

)
</pre>
Running the following code I can get matching start/end positions into an array

Code: Select all

//loop through and match start/end tags, calculating positions of blocks
$pos = array();
foreach ($matches[0] AS $match)
{
	if (strpos($match[0], 'Start'))
	{
		//starting position
		$block = str_replace(array('<!-- Start ', ' -->'), '', $match[0]);
		$pos[$block]['Start'] = $match[1] + strlen($match[0]);     //added strlen() to get rid of the tag
	} else
	{
		//ending position
		$block = str_replace(array('<!-- End ', ' -->'), '', $match[0]);
		$pos[$block]['End'] = $match[1];
	}
}

Code: Select all

Array
(
    [Category] => Array
        (
            [Start] => 121
            [End] => 669
        )

    [Forum Row] => Array
        (
            [Start] => 443
            [End] => 598
        )

    [Devnet] => Array
        (
            [Start] => 537
            [End] => 576
        )

    [Test] => Array
        (
            [Start] => 721
            [End] => 738
        )

)
And then, finally, I can get each block of code into an array:

Code: Select all

$blocks = array();
foreach ($pos AS $block => $startend)
{
	$blocks[$block] = substr($this->_code, $startend['Start'], $startend['End']-$startend['Start']);
}

Code: Select all

Array
(
    [Category] => 
	<tr>
		<td colspan="4" class="cat_name">{cat_name}</td>
	</tr>
	<tr>

		<td width="60%" class="titletext">{L_FORUM}</td>
		<td width="10%" class="titletext">{L_TOPICS}</td>
		<td width="10%" class="titletext">{L_POSTS}</td>
		<td width="20%" class="titletext">{L_LAST_POST}</td>
	</tr>
	<!-- Start Forum Row -->
	<tr>

		<td colspan="4" class="forum_row">{forum_name}</td>
	</tr>
	<!-- Start Devnet -->
	<p>I love devnet! {adjective}</p>
	<!-- End Devnet -->
	<!-- End Forum Row -->
	<tr>
		<td colspan="4">&nbsp;</td>

	</tr>
	
    [Forum Row] => 
	<tr>
		<td colspan="4" class="forum_row">{forum_name}</td>
	</tr>
	<!-- Start Devnet -->
	<p>I love devnet! {adjective}</p>
	<!-- End Devnet -->

	
    [Devnet] => 
	<p>I love devnet! {adjective}</p>
	
    [Test] => 
<p>{test}</p>

)
:):):)

Now that I've got each block captured, I believe I can move onto assigning, looping, replacing, and building a template based on that.

I'll start that headache in a few minutes =/