Cannot make TSynMustache.Render emit unescaped html or Partials
fastbike opened this issue · 7 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.