Jump to content
Edwin Yip

Sempare Template - How do you do recursive code generation?

Recommended Posts

With Sempare Template, how can you achieve recursive code generation?

 

For example, in case of html page menu, the level is not fixed, how do you recursively generate the nested `<ul><li>` items?

 

Thanks

Share this post


Link to post

The template engine is primarily focused on rendering output where data is provided by variables passed in. It does support calling functions written in Delphi. 

 

I think you could do two ways:

  1. have a function that does the recursion and returns the relevant html (so complexity is in the function in delphi). 
    • pro: template will appear simpler
    • con: some presentation is offloaded to the function
  2. you can pass the menu structure to the template. you could use a stack in the template, which would allow you to process the structure in a recursive way using a stack, rather than relying on function recursion with a function stack.
    • pro: all presentation is in the template
    • con: the template may look a bit more complex

I think I prefer option 2.

 

As the template engine will not know offhand how to create a stack, the  easiest is to have a structure containing menu and stack which is passed to the template. 

  • Like 1

Share this post


Link to post

Thanks for the help. I'm trying to use option 2, by passing multi-level nested objects that represents the menu structure and in the template code use nested `for` loops to generate the `ul/li` items.

Share this post


Link to post
Guest

When i worked with /another/ "engine" and had similar problems, i ran the payload through the engine in passes, changing the "template code" at each pass, %>, %%>, %%%>. A bit quirky and perhaps not the best approach for a server-side menu. Thought i'd post the idea anyway. Maybe this correlates to 2.

Edited by Guest

Share this post


Link to post

@Dany Marmur, Thanks for sharing.

 

Without sample code actually I'm not sure what does "use a stack in the template" as described by @darnocian actually mean. I use nested `for` loops to access the tree structure passed to the template engine.

Share this post


Link to post
22 hours ago, Edwin Yip said:

Without sample code actually I'm not sure what does "use a stack in the template" as described by @darnocian actually mean. I use nested `for` loops to access the tree structure passed to the template engine.

recursive functions call themselves and helper functions utilising the normal stack by managing CPU stack registers. It is often easier to do things this way as well, but there is also a scenario where you can have a stack overflow, depending on how much memory is accessible by the stack. From an algorithmic perspective, it is possible to transform functionally recursive functions to utilise a developer managed stacks, which are used to backtrack appropriately. 

 

last night, I wrote a quick recursive one that I can share. I'm a bit busy now, so might only get to the 'dev stack' bound one over the weekend or next week. Let me know if you want me to share stuff earlier.

Share this post


Link to post

Thanks, no hurry, because I've solved the issue with nested for loops in the template code.

The recursion must be represented in the template code in order to be helpful in my case. 

Share this post


Link to post

Let say there is some type stuff looking like follows:

type
  TMenuItem = class;

  TMenuItemCollection = class
  private
    FChildren: TObjectList<TMenuItem>;
  public
    constructor Create();
    destructor Destroy; override;
    function AddChild(const AName: string): TMenuItem;
    property Children: TObjectList<TMenuItem> read FChildren;
  end;

  TMenuItem = class(TMenuItemCollection)
  private
    FName: string;
    FLast: boolean;
  public
    constructor Create(const AName: string = '');
    property Name: string read FName;
    property Last: boolean read FLast write FLast;
  end;

  TMenu = TMenuItem;

With traditional recursion, you can do the following:

procedure GetHtmlRecursive(const AMenu: TMenu; const ASB: TStringBuilder); forward; overload;

procedure GetItemHtmlRecursive(const AMenuItem: TMenuItem; const ASB: TStringBuilder);
var
  LItem: TMenuItem;
begin
  ASB.Append('<li>');
  ASB.Append(AMenuItem.Name);
  if AMenuItem.Children.Count > 0 then
  begin
    GetHtmlRecursive(AMenuItem, ASB);
  end;
  ASB.Append('</li>');
end;

procedure GetHtmlRecursive(const AMenu: TMenu; const ASB: TStringBuilder); overload;
var
  LItem: TMenuItem;
begin
  if AMenu.Children.Count = 0 then
    exit;
  ASB.Append('<ol>');
  for LItem in AMenu.Children do
  begin
    GetItemHtmlRecursive(LItem, ASB);
  end;
  ASB.Append('</ol>');
end;

function GetHtmlRecursive(const AMenu: TMenu): string; overload;
var
  LSB: TStringBuilder;
  LItem: TMenuItem;
begin
  if AMenu.Children.Count = 0 then
    exit('');
  LSB := TStringBuilder.Create;
  try
    GetHtmlRecursive(AMenu, LSB);
    exit(LSB.ToString);
  finally
    LSB.free;
  end;
end;

To test, we can try something like:

var
  lmenu: TMenu;
  lchild: TMenuItem;
  lchild2: TMenuItem;
  lchild3: TMenuItem;
begin
  lmenu := TMenu.Create;
  try
    lchild := lmenu.AddChild('a');
    lchild2 := lchild.AddChild('a1');
    lchild3 := lchild2.AddChild('a1a');
    lchild3 := lchild2.AddChild('a1b');
    lchild2 := lchild.AddChild('a2');
    lchild2 := lchild.AddChild('a3');
    lchild := lmenu.AddChild('b');
    lchild2 := lchild.AddChild('b1');
    lchild2 := lchild.AddChild('b2');
    lchild3 := lchild2.AddChild('b2a');
    lchild3 := lchild2.AddChild('b2b');
    lchild2 := lchild.AddChild('b3');
    var
    lstr := GetHtmlRecursive(lmenu);
    writeln(lstr);
  finally
    lmenu.free;
  end;
end;

So the above just demonstrates how a menu could be done via delphi code...

 

So next, we want to try test this from the template... I havn't got to test this for myself with the above code yet, but you should be able to do the following:

type
	TMyTemplateData = record
    	Menu : TMenu;
        Data : TData; // whatever
    end;
    
   TTemplateUtils = class
	public
		class function RenderMenu(const AMenu: TMenu) : string; static; // call the function above, or just move that in here
	end;    
    
    
var
	LMyData:TMyTemplateData;
    LCtx : TTemplateContext;
 begin
    LMyData.Menu := ... some setup ..;
    LMyData.Data := ... some setup ...;
    LCtx := TTemplateContext.Create;
    LCtx.Functions.addfunctions(TTemplateUtils);
    writeln(Template.Eval(LCtx,'Here is my menu:<br><% RenderMenu(Menu) %>', LMyData);
 end;

 

Share this post


Link to post

As I understand it, I think you are actually demonstrating option 1?

However, it's not suitable for me since I need the logic to be represented in the template code.

But thanks all the same.

The nested for loops does the work - all after all, the menu always have a limited nesting level,  on contrary to my original statement...

Edited by Edwin Yip

Share this post


Link to post

Yes. I've illustrated option1 to start... as mentioned, I'll follow up on option 2...

 

personally, I don't think there has to be an absolute rule about where presentation logic is... ideally we want it constrained to the presentation layer where possible... going the option2 route can make the template less pleasant to read, and that is sort of what the helper functions are there to do, besides providing additional bridging.... Also note - the option1 route may be faster from a CPU perspective (just mentioning)...

 

anyways, I'll present that example in a few days when I get some time too look at it again.

Share this post


Link to post

After more thoughts I realized that you can customize the starting/closing tags through function parameters, so that to be more practical.

Edited by Edwin Yip

Share this post


Link to post
3 hours ago, Edwin Yip said:

After more thoughts I realized that you can customize the starting/closing tags through function parameters, so that to be more practical.

Yes, you can do something like the following:

begin
  var ctx := Template.Context;
  ctx.StartToken := '{{';
  ctx.EndToken := '}}';
  Assert.IsEqual('hello', Template.Eval(ctx, '{{ if true }}hello{{else}}bye{{end}}'));
end;

 

Share this post


Link to post
16 minutes ago, darnocian said:

Yes, you can do something like the following:


begin
  var ctx := Template.Context;
  ctx.StartToken := '{{';
  ctx.EndToken := '}}';
  Assert.IsEqual('hello', Template.Eval(ctx, '{{ if true }}hello{{else}}bye{{end}}'));
end;

 

Oh, I think you misunderstood - what I mean is that you can customize the html code generated by `RenderMenu` by allowing the passing to it the `<li>` tags to use, for example, use `<li class="levelItem">`

Share this post


Link to post

I did a quick stab at option 2 approach (delphi first)... with this working, could now apply the logic to the template engine (another post may follow):

type
  TMenuPos = (mpFirst, mpLast);
  TMenuPosInfo = set of TMenuPos;

  TMenuItem = class(TMenuItemCollection)
  private
    FName: string;
    FPosition: TMenuPosInfo;

  public
    constructor Create(const AName: string = '');
    property Name: string read FName;
    property Position: TMenuPosInfo read FPosition write FPosition;
  end;

  TMenu = TMenuItem;

function GetHtmlStack(const AMenu: TMenu): string;
var
  lStack: TStack<TMenuItem>;
  lParentStack: TStack<TMenuItem>;
  LItem: TMenuItem;
  LChildItem: TMenuItem;
  LSB: TStringBuilder;
  i: integer;
begin
  if AMenu.Children.Count = 0 then
    exit('');
  lStack := TStack<TMenuItem>.Create;
  lParentStack := TStack<TMenuItem>.Create;

  try
    LSB := TStringBuilder.Create;

    try
      lStack.Push(AMenu);

      while (lStack.Count > 0) do
      begin

        LItem := lStack.Pop;

        if AMenu <> LItem then
        begin
          if mpFirst in LItem.Position then
            LSB.Append('<ol>');

          LSB.Append('<li>');
          LSB.Append(LItem.Name);
        end;

        if LItem.Children.Count > 0 then
        begin
          lParentStack.Push(LItem);
          for i := LItem.Children.Count - 1 downto 0 do
          begin
            lStack.Push(LItem.Children[i]);
          end;
          continue;
        end;

        LSB.Append('</li>');

        if mpLast in LItem.Position then
        begin
          LSB.Append('</ol>').Append('</li>');
          lParentStack.Pop;
        end;
      end;

      while lParentStack.Count > 0 do
      begin
        LItem := lParentStack.Pop;
        lsb.Append('</ol>');
        if lParentStack.Count > 0 then
          LSB.Append('</li>');
      end;

      exit(LSB.ToString);
    finally
      LSB.free;
    end;
  finally
    lParentStack.free;
    lStack.free;
  end;
end;

this seems to produce the same as the functionally reclusive function.

Share this post


Link to post
Guest
8 hours ago, Edwin Yip said:

After more thoughts I realized that you can customize the starting/closing tags

Ah! So it did help 🙂

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×