Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot make TSynMustache.Render emit unescaped html or Partials #221

Closed
fastbike opened this issue May 27, 2019 · 7 comments
Closed

Cannot make TSynMustache.Render emit unescaped html or Partials #221

fastbike opened this issue May 27, 2019 · 7 comments
Assignees
Labels
accepted Issue has been accepted and inserted in a future milestone enhancement
Milestone

Comments

@fastbike
Copy link
Contributor

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 ?

@fastbike
Copy link
Contributor Author

fastbike commented May 27, 2019

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 ?

@fastbike
Copy link
Contributor Author

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

@danieleteti
Copy link
Owner

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

1 similar comment
@danieleteti
Copy link
Owner

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

@danieleteti danieleteti reopened this May 27, 2019
@fastbike
Copy link
Contributor Author

fastbike commented May 28, 2019

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));

@fastbike
Copy link
Contributor Author

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.

@fastbike fastbike changed the title Cannot make TSynMustache.Render emit unescaped html Cannot make TSynMustache.Render emit unescaped html or Partials May 28, 2019
@danieleteti
Copy link
Owner

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

@danieleteti danieleteti added accepted Issue has been accepted and inserted in a future milestone enhancement labels Nov 5, 2020
@danieleteti danieleteti added this to the 3.2.1-carbon milestone Nov 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted Issue has been accepted and inserted in a future milestone enhancement
Projects
None yet
Development

No branches or pull requests

2 participants