danieleteti / delphimvcframework

DMVCFramework (for short) is a popular and powerful framework for WEB API in Delphi. Supports RESTful and JSON-RPC WEB APIs development.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cannot make TSynMustache.Render emit unescaped html or Partials

fastbike opened this issue · comments

The various Render methods on the TSynMustache class contain an EscapeInvert parameter to turn off html escaping.
However this is not used by the default TMVCMustacheViewEngine class, so all html is escaped
e.g. "
" becomes "<br>" and so is thus rendered as "
" in the browser rather than as a line break.

I have not yet come up with a fix, as the TMVCMustacheViewEngine descends from TMVCBaseViewEngine and there does not appear to be an easy way to add the extra "EScapeInvert" param to the Execute method.
And as the TMVCController.GetRenderedView method that calls the Execute method, also creates the instance of the ViewEngine via the ViewEngineClass type, the constructor cannot be altered to add an "EscapeInvert" parameter.

Am I missing something ?

I've come up with a work around (rather than a solution)

I subclassed the TMVCMustacheViewEngine class and changed the Execute function to look for a value set in the ViewModel.

procedure TMVCMustacheViewEngine2.Execute(const ViewName: string; const OutputStream: TStream);
var
  Data: TObject;
  EscapeInvert: Boolean;
  lViewFileName: string;
  lViewTemplate: RawUTF8;
  lViewEngine: TSynMustache;
  lSW: TStreamWriter;
begin
   EscapeInvert := False;
   if ViewModel.TryGetValue('escapehtml', Data) then
     EscapeInvert :=Integer(Data) = 1;

  PrepareModels;
  lViewFileName := GetRealFileName(ViewName);
  if not FileExists(lViewFileName) then
    raise EMVCFrameworkViewException.CreateFmt('View [%s] not found', [ViewName]);
  lViewTemplate := StringToUTF8(TFile.ReadAllText(lViewFileName, TEncoding.UTF8));
  lViewEngine := TSynMustache.Parse(lViewTemplate);
  lSW := TStreamWriter.Create(OutputStream);
  try
    lSW.Write(UTF8Tostring(lViewEngine.RenderJSON(FJSONModel, nil, nil, nil, EscapeInvert)));
  finally
    lSW.Free;
  end;
end;

My controller class can add this using this code

     ViewData['escapehtml'] := TObject(false);

or

     ViewData['escapehtml'] := TObject(true);

Maybe the View Engines could allow this property to be exposed directly ?

Closed because there is a work around, would be useful to add to the tutorials though.

Thank you for the workaround. We'll use your findings to make a built-in solution.

Thank you for the workaround. We'll use your findings to make a built-in solution.

Hmmm, my work around blew up tonight, in the Duck Typing part of PrepareModels.

My "new" work around is like this

...
   if ViewModel.TryGetValue('page', Data) then
      TJsonObject(Data).TryGetValue<Boolean>('escapehtml', EscapeInvert);
...

and the setting of this value, given a variable on the controller

var PageData:= TJSONobject.Create;
...
  ViewData['page'] := PageData;
  PageData.AddPair('escapehtml', TJSONBool.Create(True));

Also noticed that Partials are not working, here is the unit I have written to get Escaped html and Mustache Partials working.

unit uViewEngineMustache;

(*
  1. overrides the default Mustache rendering engine to allow unescaped html to be emitted
  Assuming you have a ViewData['page'] JSONObject on your controller
  e.g. PageData := TJSONObject.Create;  ViewData['page'] := PageData;
  Add the following line to your controller method to enable html un-escaping
      PageData.AddPair('escapehtml', TJSONBool.Create(True));
  2. Parses templates for Mustache Partials and includes them
*)

interface

uses
  MVCFramework, System.Generics.Collections, System.SysUtils,
  MVCFramework.Commons, System.IOUtils, System.Classes, MVCFramework.View.Renderers.Mustache;

type

  { This class implements the mustache view engine for server side views }
  TMVCMustacheViewEngine2 = class(TMVCMustacheViewEngine)
  strict private
    procedure PrepareModels;
  private
    FJSONModel: string;
  public
    procedure Execute(const ViewName: string; const OutputStream: TStream); override;
  end;

implementation

uses
  SynCommons,
  MVCFramework.Serializer.Defaults,
  MVCFramework.Serializer.Intf,
  MVCFramework.DuckTyping, System.JSON,
  SynMustache;

type
  TSynMustacheAccess = class(TSynMustache)
  end;

{$WARNINGS OFF}

procedure TMVCMustacheViewEngine2.Execute(const ViewName: string; const OutputStream: TStream);
var
  I: Integer;
  lPartialName: string;
  lData: TObject;
  lUnEscapeHTML: Boolean;
  lViewFileName: string;
  lViewTemplate: RawUTF8;
  lViewEngine: TSynMustache;
  lSW: TStreamWriter;
  lPartials: TSynMustachePartials;
begin
  lUnEscapeHTML := False;
  if ViewModel.TryGetValue('page', lData) then
    TJsonObject(lData).TryGetValue<Boolean>('escapehtml', lUnEscapeHTML);

  PrepareModels;
  lViewFileName := GetRealFileName(ViewName);
  if not FileExists(lViewFileName) then
    raise EMVCFrameworkViewException.CreateFmt('View [%s] not found', [ViewName]);
  lViewTemplate := StringToUTF8(TFile.ReadAllText(lViewFileName, TEncoding.UTF8));
  lViewEngine := TSynMustache.Parse(lViewTemplate);
  lSW := TStreamWriter.Create(OutputStream);
  lPartials := TSynMustachePartials.Create;
  try
    for I := 0 to Length(TSynMustacheAccess(lViewEngine).fTags) - 1 do
    begin
      if TSynMustacheAccess(lViewEngine).fTags[I].Kind = mtPartial then
      begin
        lPartialName := TSynMustacheAccess(lViewEngine).fTags[I].Value;
        lViewFileName := GetRealFileName(lPartialName);
        if not FileExists(lViewFileName) then
          raise EMVCFrameworkViewException.CreateFmt('Partial View [%s] not found', [lPartialName]);
        lViewTemplate := StringToUTF8(TFile.ReadAllText(lViewFileName, TEncoding.UTF8));
        lPartials.Add(lPartialName, lViewTemplate);
      end;
    end;
    lSW.Write(UTF8Tostring(lViewEngine.RenderJSON(FJSONModel, lPartials, nil, nil, lUnEscapeHTML)));
  finally
    lSW.Free;
    lPartials.Free;
  end;
end;

{$WARNINGS ON}

procedure TMVCMustacheViewEngine2.PrepareModels;
var
  lFirst: Boolean;
  lList: IMVCList;
  DataObj: TPair<string, TObject>;
  lSJSON: string;
  lJSON: string;
  lSer: IMVCSerializer;
begin
  if (FJSONModel <> '{}') and (not FJSONModel.IsEmpty) then
    Exit;
  FJSONModel := '{}';
  if Assigned(ViewModel) then
  begin
    lSer := GetDefaultSerializer;
    lSJSON := '{';
    lFirst := True;
    for DataObj in ViewModel do
    begin
      lList := TDuckTypedList.Wrap(DataObj.Value);
      if lList <> nil then
        lJSON := lSer.SerializeCollection(DataObj.Value)
      else
        lJSON := lSer.SerializeObject(DataObj.Value);
      if not lFirst then
        lSJSON := lSJSON + ',';
      lSJSON := lSJSON + '"' + DataObj.Key + '":' + lJSON;
      lFirst := False;
    end;
    lSJSON := lSJSON + '}';
    FJSONModel := lSJSON;
  end;
end;

end.

You changes about partials have been merged. However to avoid escaping in a mustache template you can just use 3 curly braces to open a tag instead of 2. I mean {{{myprop}} instead of {{myprop}}. To show even more mustache features I added a new "page" in the serversideviews_mustache sample. Look for a link "showcase" in the lower right corner of the first page. Here's a screenshot of that page.

image