template engine - nested blocks

PHP programming forum. Ask questions or help people concerning PHP code. Don't understand a function? Need help implementing a class? Don't understand a class? Here is where to ask. Remember to do your homework!

Moderator: General Moderators

User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

template engine - nested blocks

Post 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?
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post by s.dot »

no one, eh?
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post 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.
(#10850)
User avatar
Mordred
DevNet Resident
Posts: 1579
Joined: Sun Sep 03, 2006 5:19 am
Location: Sofia, Bulgaria

Post 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 :)
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
Mordred
DevNet Resident
Posts: 1579
Joined: Sun Sep 03, 2006 5:19 am
Location: Sofia, Bulgaria

Post 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 :)
User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Post 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 ;)
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

scottayy, if you are building something like this I would be interested in both helping and the code.
(#10850)
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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.
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
alex.barylski
DevNet Evangelist
Posts: 6267
Joined: Tue Dec 21, 2004 5:00 pm
Location: Winnipeg

Post 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
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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:
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

You probably want to loop through that array and match the begins with the ends -- that would use a stack.
(#10850)
User avatar
s.dot
Tranquility In Moderation
Posts: 5001
Joined: Sun Feb 06, 2005 7:18 pm
Location: Indiana

Post 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 =/
Set Search Time - A google chrome extension. When you search only results from the past year (or set time period) are displayed. Helps tremendously when using new technologies to avoid outdated results.
Post Reply