Edwin Yip 154 Posted September 28, 2021 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
darnocian 84 Posted September 28, 2021 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: 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 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. 1 Share this post Link to post
Edwin Yip 154 Posted September 29, 2021 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 Posted September 29, 2021 (edited) 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 September 29, 2021 by Guest Share this post Link to post
Edwin Yip 154 Posted September 29, 2021 @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
darnocian 84 Posted September 30, 2021 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
Edwin Yip 154 Posted September 30, 2021 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
darnocian 84 Posted September 30, 2021 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
Edwin Yip 154 Posted September 30, 2021 (edited) 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 September 30, 2021 by Edwin Yip Share this post Link to post
darnocian 84 Posted September 30, 2021 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
Edwin Yip 154 Posted September 30, 2021 (edited) After more thoughts I realized that you can customize the starting/closing tags through function parameters, so that to be more practical. Edited September 30, 2021 by Edwin Yip Share this post Link to post
darnocian 84 Posted September 30, 2021 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
Edwin Yip 154 Posted September 30, 2021 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
darnocian 84 Posted September 30, 2021 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 Posted September 30, 2021 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