From 58632899475e7e0b727a6a637fb931175bb4119b Mon Sep 17 00:00:00 2001 From: Harald Wolff-Thobaben Date: Sat, 12 Aug 2023 12:35:42 +0200 Subject: [PATCH] Alpha Commit / Move primary workspace to Notebook --- .../.idea.ln.templates/.idea/watcherTasks.xml | 8 + ln.http.templates/ln.http.templates.csproj | 2 +- ln.templates.service/HttpTemplateEndpoints.cs | 83 ++++++ ln.templates.service/Program.cs | 40 +++ ln.templates.service/TempFileStream.cs | 47 ++++ ln.templates.service/TemplateService.cs | 170 +++++++++++++ ln.templates.service/api.REST.md | 24 ++ ln.templates.service/demo/order.json | 182 +++++++++++++ .../ln.templates.service.csproj | 56 ++++ .../ln.templates.service.csproj.DotSettings | 3 + ln.templates.service/render/HtmlToPDF.cs | 122 +++++++++ .../render/PuppeteerRenderer.cs | 65 +++++ .../storage/resources/css/default.css | 240 ++++++++++++++++++ .../storage/resources/css/default.css.map | 1 + .../storage/resources/css/default.scss | 223 ++++++++++++++++ .../storage/resources/css/reset.css | 40 +++ .../storage/resources/css/reset.css.map | 1 + .../storage/resources/css/reset.scss | 38 +++ .../storage/templates/blocks/address.html | 7 + .../storage/templates/blocks/orderlines.html | 48 ++++ .../storage/templates/layout/pagefooter.html | 47 ++++ .../storage/templates/layout/pageheader.html | 40 +++ .../storage/templates/page.html | 22 ++ .../storage/templates/page.json | 4 + ln.templates.sln | 28 ++ ln.templates.test/HtmlTests.cs | 15 ++ .../__templates__/templates/test_meta.html | 14 + .../__templates__/templates/test_meta.json | 5 + ln.templates.test/ln.templates.test.csproj | 7 + ln.templates/Context.cs | 18 +- ln.templates/Template.cs | 34 ++- ln.templates/html/QrCodeElement.cs | 23 ++ ln.templates/html/TemplateElement.cs | 57 +++-- ln.templates/ln.templates.csproj | 1 + 34 files changed, 1694 insertions(+), 21 deletions(-) create mode 100644 .idea/.idea.ln.templates/.idea/watcherTasks.xml create mode 100644 ln.templates.service/HttpTemplateEndpoints.cs create mode 100644 ln.templates.service/Program.cs create mode 100644 ln.templates.service/TempFileStream.cs create mode 100644 ln.templates.service/TemplateService.cs create mode 100644 ln.templates.service/api.REST.md create mode 100644 ln.templates.service/demo/order.json create mode 100644 ln.templates.service/ln.templates.service.csproj create mode 100644 ln.templates.service/ln.templates.service.csproj.DotSettings create mode 100644 ln.templates.service/render/HtmlToPDF.cs create mode 100644 ln.templates.service/render/PuppeteerRenderer.cs create mode 100644 ln.templates.service/storage/resources/css/default.css create mode 100644 ln.templates.service/storage/resources/css/default.css.map create mode 100644 ln.templates.service/storage/resources/css/default.scss create mode 100644 ln.templates.service/storage/resources/css/reset.css create mode 100644 ln.templates.service/storage/resources/css/reset.css.map create mode 100644 ln.templates.service/storage/resources/css/reset.scss create mode 100644 ln.templates.service/storage/templates/blocks/address.html create mode 100644 ln.templates.service/storage/templates/blocks/orderlines.html create mode 100644 ln.templates.service/storage/templates/layout/pagefooter.html create mode 100644 ln.templates.service/storage/templates/layout/pageheader.html create mode 100644 ln.templates.service/storage/templates/page.html create mode 100644 ln.templates.service/storage/templates/page.json create mode 100644 ln.templates.test/__templates__/templates/test_meta.html create mode 100644 ln.templates.test/__templates__/templates/test_meta.json create mode 100644 ln.templates/html/QrCodeElement.cs diff --git a/.idea/.idea.ln.templates/.idea/watcherTasks.xml b/.idea/.idea.ln.templates/.idea/watcherTasks.xml new file mode 100644 index 0000000..d5c17ba --- /dev/null +++ b/.idea/.idea.ln.templates/.idea/watcherTasks.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/ln.http.templates/ln.http.templates.csproj b/ln.http.templates/ln.http.templates.csproj index 6bd7ddd..aa4c7b3 100644 --- a/ln.http.templates/ln.http.templates.csproj +++ b/ln.http.templates/ln.http.templates.csproj @@ -10,7 +10,7 @@ - + diff --git a/ln.templates.service/HttpTemplateEndpoints.cs b/ln.templates.service/HttpTemplateEndpoints.cs new file mode 100644 index 0000000..7d5890e --- /dev/null +++ b/ln.templates.service/HttpTemplateEndpoints.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using Jint; +using Jint.Native; +using Jint.Native.Json; +using Jint.Native.Object; +using ln.http; +using ln.json; +using ln.json.mapping; + +namespace ln.templates.service +{ + public class HttpTemplateEndpoints : HttpEndpointController + { + private TemplateService _templateService; + + public HttpTemplateEndpoints(TemplateService templateService) + { + _templateService = templateService; + } + + [Map(HttpMethod.GET, "/templates$")] + public HttpResponse ListTemplates() + { + JSONArray templates = JSONMapper.DefaultMapper.ToJson(_templateService.ListTemplates()) as JSONArray; + return HttpResponse.OK().Content(templates); + } + + [Map(HttpMethod.GET, "/templates/(?.*\\.html)$")] + public HttpResponse GetTemplate(string templatePath) + { + if (_templateService.GetTemplateSource(templatePath, out Stream sourceStream)) + return HttpResponse.OK().Content(sourceStream); + return HttpResponse.NotFound(); + } + + [Map(HttpMethod.POST, "/templates/(?.*\\.html)$")] + public HttpResponse PostTemplate( + string templatePath, + [HttpArgumentSource(HttpArgumentSource.HEADER,ArgumentName = "content-type")]string contentType, + [HttpArgumentSource(HttpArgumentSource.CONTENT)]Stream sourceStream) + { + if (contentType.Equals("text/html")) + { + if (_templateService.StoreTemplate(templatePath, sourceStream)) + return HttpResponse.NoContent(); + } else if (contentType.Equals("application/json")) + { + if (_templateService.StoreTemplateMetadata(templatePath, sourceStream)) + return HttpResponse.NoContent(); + } + + return HttpResponse.BadRequest(); + } + + [Map(HttpMethod.POST, "/render/(?.*)$")] + [Map(HttpMethod.GET, "/render/(?.*)$")] + public HttpResponse RenderTemplate( + [HttpArgumentSource(HttpArgumentSource.PARAMETER)]string templatePath, + [HttpArgumentSource(HttpArgumentSource.CONTENT)]string postContent = "{}" + ) + { + ObjectInstance jsonObject = new JsonParser(new Engine()).Parse(postContent) as ObjectInstance; + if (_templateService.RenderTemplate(templatePath, jsonObject, out Stream templateStream)) + return HttpResponse.OK().ContentType("text/html").Content(templateStream); + return HttpResponse.InternalServerError(); + } + + [Map(HttpMethod.POST, "/pdf/(?.*)$")] + [Map(HttpMethod.GET, "/pdf/(?.*)$")] + public HttpResponse RenderTemplatePDF( + [HttpArgumentSource(HttpArgumentSource.PARAMETER)]string templatePath, + [HttpArgumentSource(HttpArgumentSource.CONTENT)]string postContent = "{}" + ) + { + ObjectInstance jsonObject = new JsonParser(new Engine()).Parse(postContent) as ObjectInstance; + if (_templateService.RenderPDF(templatePath, jsonObject, out Stream templateStream)) + return HttpResponse.OK().ContentType("application/pdf").Content(templateStream); + return HttpResponse.InternalServerError(); + } + + } +} \ No newline at end of file diff --git a/ln.templates.service/Program.cs b/ln.templates.service/Program.cs new file mode 100644 index 0000000..5b04566 --- /dev/null +++ b/ln.templates.service/Program.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using ln.bootstrap; +using ln.http; +using ln.templates.service; + +public class TemplateServiceApplication +{ + public static void Main(String[] arguments) + { + var app = new TemplateServiceApplication(arguments); + app.Run(); + } + + public string StoragePath { get; private set; } = Path.GetFullPath("storage"); + public short ServicePort { get; private set; } = 8890; + + public TemplateServiceApplication(string[] arguments) + { + string[] unused = new Options( + new Option('s',"storage", (v)=>StoragePath = v), + new Option('p',"port", (v)=>ServicePort = short.Parse(v)) + ) + .Parse(arguments); + Console.WriteLine("Unused Arguments: ", String.Join(" , ", unused)); + } + + public void Run() + { + TemplateService templateService = new TemplateService(StoragePath); + HttpTemplateEndpoints httpTemplateEndpoints = new HttpTemplateEndpoints(templateService); + + Listener httpListener = new Listener(httpTemplateEndpoints, ServicePort); + + Console.ReadKey(); + } + + +} + diff --git a/ln.templates.service/TempFileStream.cs b/ln.templates.service/TempFileStream.cs new file mode 100644 index 0000000..459ecdd --- /dev/null +++ b/ln.templates.service/TempFileStream.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using ln.type; +using Microsoft.Win32.SafeHandles; + +namespace ln.templates.service +{ + public class TempFileStream : FileStream, IDisposable + { + public new void Dispose() + { + try + { + base.Dispose(); + } + finally + { + if (File.Exists(Name)) + File.Delete(Name); + } + } + + public TempFileStream() + :base($"{GetUniqueFileName(Path.GetTempPath())}", FileMode.Create) + { + + } + + public TempFileStream(string extension) + :base($"{GetUniqueFileName(Path.GetTempPath(), extension)}", FileMode.Create) + { + } + + + public static string GetUniqueFileName(string path) => GetUniqueFileName(path, ""); + public static string GetUniqueFileName(string path, string extension) + { + return String.Format("{1}/tmp.{2}{0}", + extension, + path, + BitConverter.GetBytes(DateTime.Now.ToUnixTimeMilliseconds()).ToHexString() + ); + } + + } + +} \ No newline at end of file diff --git a/ln.templates.service/TemplateService.cs b/ln.templates.service/TemplateService.cs new file mode 100644 index 0000000..65809a6 --- /dev/null +++ b/ln.templates.service/TemplateService.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Jint.Native; +using Jint.Native.Object; +using ln.templates.service.render; + +namespace ln.templates.service +{ + + public class TemplateService + { + public string StoragePath { get; } + public RecursiveResolver Resolver { get; } + + public TemplateService(string storagePath) + { + StoragePath = Path.GetFullPath(storagePath); + Initialize(); + + Resolver = new RecursiveResolver(Path.Combine(StoragePath, "templates")); + } + + private void Initialize() + { + EnsureDirectory(""); + EnsureDirectory("templates"); + EnsureDirectory("resources"); + EnsureDirectory("spool"); + EnsureDirectory("out"); + } + + private void EnsureDirectory(string directoryName) + { + string finalPath = Path.Combine(StoragePath, directoryName); + if (!Directory.Exists(finalPath)) + Directory.CreateDirectory(finalPath); + } + + public bool StoreTemplate(string templatePath, Stream sourceStream) + { + string finalPath = Path.GetFullPath(Path.Combine(StoragePath, "templates", templatePath)); + if (finalPath.StartsWith(StoragePath)) + { + string finalDirectory = Path.GetDirectoryName(finalPath); + if (!Directory.Exists(finalDirectory)) + { + Span finalDirectoryParts = Path.GetRelativePath(StoragePath,finalDirectory).Split(Path.DirectorySeparatorChar); + string currentPath = StoragePath; + for (int n = 0; n < finalDirectoryParts.Length; n++) + { + currentPath = Path.Combine(currentPath, finalDirectoryParts[n]); + if (!Directory.Exists(currentPath)) + Directory.CreateDirectory(currentPath); + } + } + + using (FileStream fs = new FileStream(finalPath, FileMode.Create)) + sourceStream.CopyTo(fs); + + return true; + } + + return false; + } + + public bool StoreTemplateMetadata(string templatePath, Stream sourceStream) + { + string finalPath = Path.GetFullPath(Path.Combine(StoragePath, "templates", templatePath)); + if (finalPath.StartsWith(StoragePath)) + { + if (File.Exists(finalPath)) + { + string metaFileName = Path.Combine(Path.GetDirectoryName(finalPath), String.Format("{0}.json",Path.GetFileNameWithoutExtension(finalPath))); + using (FileStream fs = new FileStream(metaFileName, FileMode.Create)) + sourceStream.CopyTo(fs); + + return true; + } + } + return false; + } + + public bool GetTemplateSource(string templatePath, out Stream sourceStream) + { + string finalPath = Path.GetFullPath(Path.Combine(StoragePath, "templates", templatePath)); + if (finalPath.StartsWith(StoragePath)) + { + sourceStream = new FileStream(finalPath, FileMode.Open, FileAccess.Read); + return true; + } + + sourceStream = null; + return false; + } + + + public String[] ListTemplates() + { + List templateFileNames = new List(); + Queue subdirs = new Queue(); + string templatesPath = Path.Combine(StoragePath, "templates"); + + subdirs.Enqueue(templatesPath); + while (subdirs.Count > 0) + { + string currentSubDir = subdirs.Dequeue(); + + foreach (var file in Directory.GetFiles(currentSubDir, "*.html")) + { + templateFileNames.Add(Path.GetRelativePath(templatesPath, file)); + } + + foreach (var directory in Directory.GetDirectories(currentSubDir)) + { + subdirs.Enqueue(directory); + } + } + + return templateFileNames.ToArray(); + } + + public bool RenderTemplate(string templatePath, ObjectInstance o, out Stream templateStream) + { + if (templatePath is null) + throw new NullReferenceException(); + + string directoryPath = Path.GetDirectoryName(templatePath); + string templateFilename = Path.GetFileName(templatePath); + + TempFileStream tempFileStream = new TempFileStream(".html"); + + Template template = Resolver + .FindResolver(directoryPath) + .GetTemplate(templateFilename); + + Context context = new Context(template.Resolver, new StreamWriter(tempFileStream)); + context.Engine.Realm.GlobalEnv.SetMutableBinding("$baseurl", String.Format("file://{0}",Path.Combine(StoragePath, "resources/")), false); + template.Render(context, o); + + tempFileStream.Position = 0; + templateStream = tempFileStream; + + return true; + } + + public bool RenderPDF(string templatePath, ObjectInstance o, out Stream targetStream) + { + if (templatePath is null) + throw new NullReferenceException(); + + string directoryPath = Path.GetDirectoryName(templatePath); + string templateFilename = Path.GetFileName(templatePath); + + TempFileStream tempFileStream = new TempFileStream(".html"); + + Template template = Resolver + .FindResolver(directoryPath) + .GetTemplate(templateFilename); + + return PuppeteerRenderer.RenderTemplate( + template, + String.Format("file://{0}", Path.Combine(StoragePath, "resources/")), + o, + out targetStream + ); + } + + } +} diff --git a/ln.templates.service/api.REST.md b/ln.templates.service/api.REST.md new file mode 100644 index 0000000..7d851b3 --- /dev/null +++ b/ln.templates.service/api.REST.md @@ -0,0 +1,24 @@ +# REST Endpoints + +The TemplateService binds by default to localhost:9980 and uses HTTP + +### GET /templates +Retrieve JSON list of all templates available + +### GET /templates/ +Retrieve source of template identified by which must end in .html + +### POST /templates/ +Create or update template identified by which must end in .html +POST content-type must be either text/html or application/json. +If text/html is posted, the template source is saved. +If application/json is posted, the template meta data object is to be saved. + +### GET, POST /render/ +Renders the template identified by which must end in .html to HTML +Optional JSON object may be posted to be used as globals for this rendering. + +### GET, POST /pdf/ +Renders the template identified by which must end in .html to PDF +Optional JSON object may be posted to be used as globals for this rendering. + diff --git a/ln.templates.service/demo/order.json b/ln.templates.service/demo/order.json new file mode 100644 index 0000000..44a8a9f --- /dev/null +++ b/ln.templates.service/demo/order.json @@ -0,0 +1,182 @@ +{ + "document": { + "title": "Rechnung", + "date": "20230803", + "ref": "RG-2308-8453", + "author": { + "title": "", + "lastname": "Wolff-Thobaben", + "firstname": "Harald", + "middlenames": "Christian Joachim", + "phone": "+49 7082 4252629", + "email": "info@l--n.de", + "fax": null, + "": "" + }, + "customer": { + "id": "K34-823", + "title": "", + "lastname": "Wolff-Thobaben", + "firstname": "Harald", + "middlenames": "Christian Joachim", + "phone": "+49 7082 4252629", + "email": "info@l--n.de", + "fax": null, + "address": { + "street": "Gruppenstrasse 27", + "postcode": "75334", + "town": "Straubenhardt", + "country": null + } + }, + "text": "Sehr geehrte Damen und Herren,\n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", + "lines": [ + { + "position": "1", + "name": "Mainboard ASUS XF560-XL", + "quantity": "1.00", + "item_price_net": "34,56", + "item_taxes": "6,57", + "line_price_net": "34,56", + "line_taxes": { + "A": "6,57" + }, + "text": "" + }, + { + "position": "2", + "name": "Arbeitsstunde Techniker", + "quantity": "9,50", + "item_price_net": "89,00", + "item_taxes": "16,91", + "line_price_net": "845,50", + "line_taxes": { + "A": "160,65" + }, + "text": "Arbeiten an Ihrem System am 23.04.2023\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle Zeilenumbrüche testen können." + }, + { + "position": "3", + "name": "Arbeitsstunde Techniker", + "quantity": "9,50", + "item_price_net": "89,00", + "item_taxes": "16,91", + "line_price_net": "845,50", + "line_taxes": { + "A": "160,65" + }, + "text": "Arbeiten an Ihrem System am 23.04.2023\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle Zeilenumbrüche testen können." + }, + { + "position": "4", + "name": "Arbeitsstunde Techniker", + "quantity": "9,50", + "item_price_net": "89,00", + "item_taxes": "16,91", + "line_price_net": "845,50", + "line_taxes": { + "A": "160,65" + }, + + "text": "Arbeiten an Ihrem System am 23.04.2023\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle Zeilenumbrüche testen können." + }, + { + "position": "5", + "name": "Arbeitsstunde Techniker", + "quantity": "9,50", + "item_price_net": "89,00", + "item_taxes": "16,91", + "line_price_net": "845,50", + "line_taxes": { + "A": "160,65" + }, + + "text": "Arbeiten an Ihrem System am 23.04.2023\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle Zeilenumbrüche testen können." + }, + { + "position": "6", + "name": "Arbeitsstunde Techniker", + "quantity": "9,50", + "item_price_net": "89,00", + "item_taxes": "16,91", + "line_price_net": "845,50", + "line_taxes": { + "A": "160,65" + }, + + "text": "Arbeiten an Ihrem System am 23.04.2023\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle Zeilenumbrüche testen können." + }, + { + "position": "7", + "name": "Arbeitsstunde Techniker", + "quantity": "9,50", + "item_price_net": "89,00", + "item_taxes": "16,91", + "line_price_net": "845,50", + "line_taxes": { + "A": "160,65" + }, + + "text": "Arbeiten an Ihrem System am 23.04.2023\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle Zeilenumbrüche testen können." + }, + { + "position": "8", + "name": "Arbeitsstunde Techniker", + "quantity": "9,50", + "item_price_net": "89,00", + "item_taxes": "16,91", + "line_price_net": "845,50", + "line_taxes": { + "B": "59,18" + }, + + "text": "Arbeiten an Ihrem System am 23.04.2023\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle Zeilenumbrüche testen können." + }, + { + "position": "9", + "name": "Arbeitsstunde Techniker", + "quantity": "9,50", + "item_price_net": "89,00", + "item_taxes": "16,91", + "line_price_net": "845,50", + "line_taxes": { + "A": "160,65" + }, + + "text": "Arbeiten an Ihrem System am 23.04.2023\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle Zeilenumbrüche testen können." + } + ], + "taxes": { + "A": { + "name": "Umsatzsteuer 19%", + "sum": "1291,72" + }, + "B": { + "name": "Umsatzsteuer 7%", + "sum": "59,18" + } + }, + "texts": [ + "Soweit nicht anders angegeben, gilt das Rechnungsdatum als Liefer- und Leistungsdatum.", + "Alle gelieferten Gegenstände bleiben bis zur vollständigen Bezahlung unser Eigentum." + ], + "currency": "EUR", + "sum_net": "6798,56", + "sum_total": "8090,28" + }, + "company": { + "shortname": "WK-PRO", + "name": "WK-PRO Inh. Andreas und Harald", + "address": { + "street": "Gruppenstrasse 27", + "postcode": "75334", + "town": "Straubenhardt", + "country": null + }, + "bank": { + "name": "Volksbank PUR eG", + "iban": "DE11 6619 0000 0029 7275 97" + } + }, + "": "" +} \ No newline at end of file diff --git a/ln.templates.service/ln.templates.service.csproj b/ln.templates.service/ln.templates.service.csproj new file mode 100644 index 0000000..523a859 --- /dev/null +++ b/ln.templates.service/ln.templates.service.csproj @@ -0,0 +1,56 @@ + + + + Exe + disable + enable + net6.0;net5.0 + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + diff --git a/ln.templates.service/ln.templates.service.csproj.DotSettings b/ln.templates.service/ln.templates.service.csproj.DotSettings new file mode 100644 index 0000000..da86b28 --- /dev/null +++ b/ln.templates.service/ln.templates.service.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/ln.templates.service/render/HtmlToPDF.cs b/ln.templates.service/render/HtmlToPDF.cs new file mode 100644 index 0000000..eba6929 --- /dev/null +++ b/ln.templates.service/render/HtmlToPDF.cs @@ -0,0 +1,122 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace ln.templates.service.render +{ + + public class HtmlToPDF + { + public static string WkExecutablePath { get; set; } + public static string ChromiumExecutablePath { get; set; } + + public static bool RenderPDF(Stream htmlStream, string resourcePath, out Stream pdfStream) + { + if (htmlStream is TempFileStream tempFileStream) + { + return RenderPDF(tempFileStream.Name, resourcePath, out pdfStream); + } + else + { + using (TempFileStream htmlTempStream = new TempFileStream(".html")) + { + htmlStream.CopyTo(htmlTempStream); + return RenderPDF(htmlTempStream.Name, resourcePath, out pdfStream); + } + } + } + + public static bool RenderPDF(string htmlFileName, string resourcePath, out Stream pdfStream) + { + if (ChromiumExecutablePath is string) + return RenderPDFChromium(htmlFileName, resourcePath, out pdfStream); + else + return RenderPDFWk(htmlFileName, resourcePath, out pdfStream); + } + + public static bool RenderPDFWk(string htmlFileName, string resourcePath, out Stream pdfStream) + { + if (!File.Exists(htmlFileName)) + throw new FileNotFoundException(htmlFileName); + if ((resourcePath is String) && !Directory.Exists(resourcePath)) + throw new DirectoryNotFoundException(resourcePath); + + var pdfTempStream = new TempFileStream(".pdf"); + + StringBuilder argumentsBuilder = new StringBuilder(); + if (resourcePath is String) + argumentsBuilder.AppendFormat("--allow {0} ", resourcePath); + argumentsBuilder.Append("--disable-javascript --enable-local-file-access -B 0 -L 0 -R 0 -T 0 --print-media-type"); + + argumentsBuilder.AppendFormat(" {0}", htmlFileName); + argumentsBuilder.AppendFormat(" {0}", pdfTempStream.Name); + + Console.WriteLine("Starting WkHtmlToPdf: {0} {1}", WkExecutablePath, argumentsBuilder.ToString()); + var p = Process.Start(WkExecutablePath, argumentsBuilder.ToString()); + p.WaitForExit(); + if (p.ExitCode != 0) + { + pdfTempStream.Dispose(); + pdfStream = null; + return false; + } + + pdfStream = pdfTempStream; + return true; + } + + public static bool RenderPDFChromium(string htmlFileName, string resourcePath, out Stream pdfStream) + { + var pdfTempStream = new TempFileStream(".pdf"); + + string command = + String.Format( + "--headless=old --disable-gpu --print-to-pdf={1} --no-pdf-header-footer --no-margins {0}", + htmlFileName, + pdfTempStream.Name + ); + + Console.WriteLine("Starting chromium: {0} {1}", ChromiumExecutablePath, command); + var p = Process.Start(ChromiumExecutablePath, command); + p.WaitForExit(); + if (p.ExitCode != 0) + { + pdfTempStream.Dispose(); + pdfStream = null; + return false; + } + + pdfStream = pdfTempStream; + return true; + } + + static string LookupExecutable(String[] searchPaths) + { + foreach (var tryExe in searchPaths) + { + if (File.Exists(tryExe)) + return tryExe; + } + + return null; + } + + static HtmlToPDF() + { + WkExecutablePath = LookupExecutable(new String[] + { + "/usr/bin/wkhtmltopdf", + "/usr/local/bin/wkhtmltopdf", + }); + ChromiumExecutablePath = LookupExecutable(new String[] + { + "/usr/bin/chromium", + "/usr/local/bin/chromium", + "/usr/bin/chrome", + "/usr/local/bin/chrome", + }); + } + } + +} \ No newline at end of file diff --git a/ln.templates.service/render/PuppeteerRenderer.cs b/ln.templates.service/render/PuppeteerRenderer.cs new file mode 100644 index 0000000..0fc79ef --- /dev/null +++ b/ln.templates.service/render/PuppeteerRenderer.cs @@ -0,0 +1,65 @@ +using System.IO; +using Jint.Native.Object; +using PuppeteerSharp; + +namespace ln.templates.service.render +{ + public static class PuppeteerRenderer + { + private static BrowserFetcher _browserFetcher; + + public static bool RenderTemplate(Template template, string resourcePath, ObjectInstance globals, + out Stream pdfStream) + { + using (StringWriter htmlWriter = new StringWriter()) + { + Context context = new Context(template.Resolver, htmlWriter); + template.Render(context, globals); + + using (var browser = Puppeteer.LaunchAsync(new LaunchOptions + { + Headless = true, + }).Result) + { + using (var page = browser.NewPageAsync().Result) + { + page.GoToAsync(resourcePath).Wait(); + page.SetContentAsync(htmlWriter.ToString(),new NavigationOptions() + { + }).Wait(); + + var footerTemplate = page.EvaluateFunctionAsync($@"()=>{{ +let footers = document.getElementsByTagName('footer'); +if (footers.length){{ + let footerHtml = footers[0].outerHTML; + footers[0].remove(); + return footerHtml; +}} +return null; + }}").Result; + + page.SetContentAsync(page.GetContentAsync().Result).Wait(); + + TempFileStream pdfTempStream = new TempFileStream(".pdf"); + page.PdfAsync(pdfTempStream.Name, new PdfOptions() + { + HeaderTemplate = "
", + FooterTemplate = footerTemplate, + DisplayHeaderFooter = true, + }).Wait(); + + pdfStream = pdfTempStream; + return true; + } + } + } + } + + static PuppeteerRenderer() + { + _browserFetcher = new BrowserFetcher(); + _browserFetcher.DownloadAsync(BrowserFetcher.DefaultChromiumRevision).Wait(); + } + + } +} \ No newline at end of file diff --git a/ln.templates.service/storage/resources/css/default.css b/ln.templates.service/storage/resources/css/default.css new file mode 100644 index 0000000..f9d47ef --- /dev/null +++ b/ln.templates.service/storage/resources/css/default.css @@ -0,0 +1,240 @@ +*, *::after, *::before { + box-sizing: border-box; + padding: 0px; + font: inherit; +} + +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +html, body { + height: 100%; +} + +nav, a { + display: inline-block; +} + +a:visited { + color: blue; +} + +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +html { + overflow: hidden; +} + +body { + position: relative; + font-family: "DejaVu Sans", "Helvetica", "Nimbus Sans", "Open Sans", "Arial"; + font-size: var(--lns-base-font-size); + line-height: var(--lns-base-line-height); + overflow-scrolling: auto; +} + +@page { + size: A4; + margin: 1.7cm 2cm 1.7cm 2.5cm; +} +body { + padding: 0; + margin: 0; + font-size: 0.4cm; + font-family: "Source Code Pro"; +} + +.small { + font-size: 0.3cm; +} + +table { + width: 100%; + page-break-inside: auto; +} + +tr { + page-break-inside: avoid; +} + +td { + vertical-align: top; +} + +.clear { + clear: both; +} + +header { + position: relative; + width: 100%; + padding-bottom: 1cm; +} +header img { + position: absolute; + right: 0cm; + width: 7cm; +} +header .right { + float: right; + padding-top: 2cm; + width: 7cm; + font-size: 0.4cm; +} +header .right h2 { + width: 100%; + text-align: right; + margin-bottom: 0.2cm; +} +header .address { + position: absolute; + left: 0.5cm; + top: 3.8cm; + width: 8cm; +} +header .address .sender { + font-size: 0.25cm; + text-decoration: underline; + margin-bottom: 0.1cm; +} + +footer { + display: block; + width: 100%; + font-size: 0.25cm; +} +footer .duo div { + width: 100%; +} + +main { + display: inline-block; +} + +.duo { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.1cm; + width: 100%; +} +.duo :first-child { + flex-grow: 0; + flex-shrink: 1; +} +.duo :last-child { + position: relative; + flex-grow: 1; + flex-shrink: 0; + text-align: right; +} +.duo.nowrap { + flex-wrap: nowrap; +} + +.orderlines { + font-size: 0.35cm; +} +.orderlines thead { + font-weight: bold; + text-align: left; +} +.orderlines td { + padding-right: 0.3cm; + white-space: nowrap; + vertical-align: top; +} +.orderlines td:nth-of-type(2) { + width: 99%; +} +.orderlines tbody tr:not(.break-avoid-after) > td { + padding-bottom: 0.3cm; +} +.orderlines tbody .text { + padding-left: 1.5cm; + padding-right: 1cm; + text-align: justify; +} + +.break-avoid-after { + page-break-after: avoid; + page-break-inside: avoid; +} + +.qrcode > svg { + display: inline-block; + max-width: 3cm; + max-height: 3cm; +} + +.mb { + margin-bottom: 0.2cm; +} +.mb td { + margin-bottom: 0.2cm; +} + +.mt { + margin-top: 0.2cm; +} +.mt td { + margin-top: 0.2cm; +} + +.pb { + padding-bottom: 0.2cm; +} +.pb td { + padding-bottom: 0.2cm; +} + +.pt { + padding-top: 0.2cm; +} +.pt td { + padding-top: 0.2cm; +} + +.text { + margin-bottom: 1cm; + white-space: pre-wrap; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +.text-justify { + text-align: justify; +} + +h1, h2, h3, h4 { + padding: 0cm; + margin: 0cm; +} + +h1 { + font-size: 0.8cm; +} + +h2 { + font-size: 0.5cm; +} + +b, em { + font-weight: bold; +} + +/*# sourceMappingURL=default.css.map */ diff --git a/ln.templates.service/storage/resources/css/default.css.map b/ln.templates.service/storage/resources/css/default.css.map new file mode 100644 index 0000000..49d4b3a --- /dev/null +++ b/ln.templates.service/storage/resources/css/default.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["reset.scss","default.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EAAa;;;AAEb;EACE;;;AAGF;EACE;;;AAIF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;ACjCF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAIF;EACE;;;AAGF;EACE;;;AAIF;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EAEA;;AAEA;EACE;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAKN;EACE;EACA;EAGA;;AAGE;EACE;;;AAKN;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAIA;EACE;;AAGF;EACE;EACA;EACA;;;AAKN;EACE;EACA;;;AAIA;EACE;EACA;EACA;;;AAIJ;EACE;;AAEA;EACE;;;AAGJ;EACE;;AACA;EACE;;;AAIJ;EACE;;AACA;EACE;;;AAGJ;EACE;;AACA;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE","file":"default.css"} \ No newline at end of file diff --git a/ln.templates.service/storage/resources/css/default.scss b/ln.templates.service/storage/resources/css/default.scss new file mode 100644 index 0000000..1ba96f7 --- /dev/null +++ b/ln.templates.service/storage/resources/css/default.scss @@ -0,0 +1,223 @@ +@import "reset"; + +@page { + size: A4; + margin: 1.7cm 2cm 1.7cm 2.5cm; +} + +body { + padding: 0; + margin: 0; + font-size: 0.4cm; + font-family: "Source Code Pro"; +} + +.small { + font-size: 0.3cm; +} + +table { + width: 100%; + page-break-inside: auto; + //border: 1px dotted black; +} + +tr { + page-break-inside: avoid; +} + +td { + vertical-align: top; + //border: 1px dotted black; +} + +.clear { + clear: both; +} + +header { + position: relative; + width: 100%; + padding-bottom: 1cm; + + img { + position: absolute; + right: 0cm; + width: 7cm; + } + + .right { + float: right; + padding-top: 2cm; + width: 7cm; + + font-size: 0.4cm; + + h2 { + width: 100%; + text-align: right; + margin-bottom: 0.2cm; + } + } + + .address { + position: absolute; + left: 0.5cm; + top: 3.8cm; + width: 8cm; + + .sender { + font-size: 0.25cm; + text-decoration: underline; + margin-bottom: 0.1cm; + } + } +} + +footer { + display: block; + width: 100%; + //background-color: aqua; + + font-size: 0.25cm; + + .duo { + div { + width: 100%; + } + } +} + +main { + display: inline-block; +} + +.duo { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 0.1cm; + width: 100%; + + :first-child { + flex-grow: 0; + flex-shrink: 1; + } + + :last-child { + position: relative; + flex-grow: 1; + flex-shrink: 0; + text-align: right; + } + + &.nowrap { + flex-wrap: nowrap; + } +} + +.orderlines { + font-size: 0.35cm; + + thead { + font-weight: bold; + text-align: left; + } + + td { + padding-right: 0.3cm; + white-space: nowrap; + vertical-align: top; + } + + td:nth-of-type(2) { + width: 99%; + } + + tbody { + tr:not(.break-avoid-after) > td { + padding-bottom: 0.3cm; + } + + .text { + padding-left: 1.5cm; + padding-right: 1cm; + text-align: justify; + } + } +} + +.break-avoid-after { + page-break-after: avoid; + page-break-inside: avoid; +} + +.qrcode { + & > svg { + display: inline-block; + max-width: 3cm; + max-height: 3cm; + } +} + +.mb { + margin-bottom: 0.2cm; + + td { + margin-bottom: 0.2cm; + } +} +.mt { + margin-top: 0.2cm; + td { + margin-top: 0.2cm; + } +} + +.pb { + padding-bottom: 0.2cm; + td { + padding-bottom: 0.2cm; + } +} +.pt { + padding-top: 0.2cm; + td { + padding-top: 0.2cm; + } +} + +.text { + margin-bottom: 1cm; + white-space: pre-wrap; +} + +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} + +h1, h2, h3, h4 { + padding: 0cm; + margin: 0cm; +} + +h1 { + font-size: 0.8cm; +} + +h2 { + font-size: 0.5cm; +} + +b, em { + font-weight: bold; +} \ No newline at end of file diff --git a/ln.templates.service/storage/resources/css/reset.css b/ln.templates.service/storage/resources/css/reset.css new file mode 100644 index 0000000..74eefd2 --- /dev/null +++ b/ln.templates.service/storage/resources/css/reset.css @@ -0,0 +1,40 @@ +*, *::after, *::before { + box-sizing: border-box; + padding: 0px; + font: inherit; +} + +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +html, body { + height: 100%; +} + +nav, a { + display: inline-block; +} + +a:visited { + color: blue; +} + +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +html { + overflow: hidden; +} + +body { + position: relative; + font-family: "DejaVu Sans", "Helvetica", "Nimbus Sans", "Open Sans", "Arial"; + font-size: var(--lns-base-font-size); + line-height: var(--lns-base-line-height); + overflow-scrolling: auto; +} + +/*# sourceMappingURL=reset.css.map */ diff --git a/ln.templates.service/storage/resources/css/reset.css.map b/ln.templates.service/storage/resources/css/reset.css.map new file mode 100644 index 0000000..9f70235 --- /dev/null +++ b/ln.templates.service/storage/resources/css/reset.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["reset.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EAAa;;;AAEb;EACE;;;AAGF;EACE;;;AAIF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA","file":"reset.css"} \ No newline at end of file diff --git a/ln.templates.service/storage/resources/css/reset.scss b/ln.templates.service/storage/resources/css/reset.scss new file mode 100644 index 0000000..3784a26 --- /dev/null +++ b/ln.templates.service/storage/resources/css/reset.scss @@ -0,0 +1,38 @@ +*, *::after, *::before { + box-sizing: border-box; + padding: 0px; + font: inherit; +} + +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +html, body { height: 100%; } + +nav, a { + display: inline-block; +} + +a:visited { + color: blue; +} + + +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +html { + overflow: hidden; +} + +body { + position: relative; + font-family: "DejaVu Sans", "Helvetica", "Nimbus Sans", "Open Sans", "Arial"; + font-size: var(--lns-base-font-size); + line-height: var(--lns-base-line-height); + overflow-scrolling: auto; +} + diff --git a/ln.templates.service/storage/templates/blocks/address.html b/ln.templates.service/storage/templates/blocks/address.html new file mode 100644 index 0000000..1017817 --- /dev/null +++ b/ln.templates.service/storage/templates/blocks/address.html @@ -0,0 +1,7 @@ +
+
{{ company.shortname }} · {{ company.address.street }} · {{ company.address.postcode }} {{ company.address.town }}
+ +
{{ address.street }}
+
{{ address.postcode }} {{ address.town }}
+
{{ address.country }}
+
\ No newline at end of file diff --git a/ln.templates.service/storage/templates/blocks/orderlines.html b/ln.templates.service/storage/templates/blocks/orderlines.html new file mode 100644 index 0000000..8350f39 --- /dev/null +++ b/ln.templates.service/storage/templates/blocks/orderlines.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Pos.BezeichnungEinzelpreisMengeNettoSteuern
Netto:{{ document.sum_net }}
{{ tax.value.name }} ({{ tax.key }}):{{ tax.value.sum }}
Rechnungsbetrag:{{ document.sum_total }}
\ No newline at end of file diff --git a/ln.templates.service/storage/templates/layout/pagefooter.html b/ln.templates.service/storage/templates/layout/pagefooter.html new file mode 100644 index 0000000..436fb17 --- /dev/null +++ b/ln.templates.service/storage/templates/layout/pagefooter.html @@ -0,0 +1,47 @@ +
+ + + + + + + + + + +
{{ company.name }}
+ {{ company.address.street }}
+ {{ company.address.postcode }} {{ company.address.town }}
buchhaltung@wk-pro.com
+ http://wk-pro.com
+ +49 7082 4252629
+
Bankverbindung:
+ {{ company.bank.iban }}
+ {{ company.bank.name }} +
+
Seite von
+
+ USt.ID: DE9834251
+
+
\ No newline at end of file diff --git a/ln.templates.service/storage/templates/layout/pageheader.html b/ln.templates.service/storage/templates/layout/pageheader.html new file mode 100644 index 0000000..9a601f8 --- /dev/null +++ b/ln.templates.service/storage/templates/layout/pageheader.html @@ -0,0 +1,40 @@ +
+ +
+

{{ document.title || "" }}

+

{{ document.ref }}

+
+
Datum:
+
{{ document.date.substring(6,8) }}.{{ document.date.substring(4,6) }}.{{ document.date.substring(0,4) }}
+
+ +
+
Kunden-Nr.:
+
{{ document.customer.id }}
+
+
+
Ansprechpartner:
+
{{ document.author.firstname }} {{ document.author.lastname }}
+
+
+
Telefon:
+
{{ document.author.phone }}
+
+
+
Telefax:
+
{{ document.author.fax }}
+
+
+
E-Mail:
+
{{ document.author.email }}
+
+
+
GiroCode
+
+
+
+ + + +
+
\ No newline at end of file diff --git a/ln.templates.service/storage/templates/page.html b/ln.templates.service/storage/templates/page.html new file mode 100644 index 0000000..2c2637e --- /dev/null +++ b/ln.templates.service/storage/templates/page.html @@ -0,0 +1,22 @@ + + + + + {{ document.title || "" }} {{ document.ref || "" }} + + + + + +
+
{{ document.text }}
+
+ + + +
+
{{ text }}
+
+ + + \ No newline at end of file diff --git a/ln.templates.service/storage/templates/page.json b/ln.templates.service/storage/templates/page.json new file mode 100644 index 0000000..c73d307 --- /dev/null +++ b/ln.templates.service/storage/templates/page.json @@ -0,0 +1,4 @@ +{ + "title": "A simple Page", + "author": "Harald Wolff-Thobaben " +} \ No newline at end of file diff --git a/ln.templates.sln b/ln.templates.sln index 82acb01..164fbda 100644 --- a/ln.templates.sln +++ b/ln.templates.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.templates.test", "ln.tem EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.templates", "ln.http.templates\ln.http.templates.csproj", "{011E8B81-DF87-4AFB-BEA8-ADA6FD3F3665}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.templates.service", "ln.templates.service\ln.templates.service.csproj", "{D5D381D5-9EF4-4B27-A983-FD6134FA9E08}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.bootstrap", "..\ln.bootstrap\ln.bootstrap\ln.bootstrap.csproj", "{9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,5 +62,29 @@ Global {011E8B81-DF87-4AFB-BEA8-ADA6FD3F3665}.Release|x64.Build.0 = Release|Any CPU {011E8B81-DF87-4AFB-BEA8-ADA6FD3F3665}.Release|x86.ActiveCfg = Release|Any CPU {011E8B81-DF87-4AFB-BEA8-ADA6FD3F3665}.Release|x86.Build.0 = Release|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Debug|x64.ActiveCfg = Debug|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Debug|x64.Build.0 = Debug|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Debug|x86.ActiveCfg = Debug|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Debug|x86.Build.0 = Debug|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Release|Any CPU.Build.0 = Release|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Release|x64.ActiveCfg = Release|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Release|x64.Build.0 = Release|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Release|x86.ActiveCfg = Release|Any CPU + {D5D381D5-9EF4-4B27-A983-FD6134FA9E08}.Release|x86.Build.0 = Release|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Debug|x64.ActiveCfg = Debug|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Debug|x64.Build.0 = Debug|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Debug|x86.ActiveCfg = Debug|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Debug|x86.Build.0 = Debug|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Release|Any CPU.Build.0 = Release|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Release|x64.ActiveCfg = Release|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Release|x64.Build.0 = Release|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Release|x86.ActiveCfg = Release|Any CPU + {9A2C87C9-3180-4E80-8E0A-581CF2F6ABB1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ln.templates.test/HtmlTests.cs b/ln.templates.test/HtmlTests.cs index 47de7bc..8ee27df 100644 --- a/ln.templates.test/HtmlTests.cs +++ b/ln.templates.test/HtmlTests.cs @@ -11,6 +11,12 @@ using NUnit.Framework; using System; using ln.templates.html; using System.IO; +using System.Text; +using Jint; +using Jint.Native; +using Jint.Native.Json; +using Jint.Native.Object; +using ln.templates.service; namespace ln.templates.test { @@ -65,6 +71,15 @@ namespace ln.templates.test } } + [TestCase()] + public void Test_MetaData() + { + TemplateService templateService = new TemplateService("__templates__"); + JsValue o = new JsonParser(new Engine()).Parse("{ \"title\": \"Some Title\", \"some_number\": 5342.23 }"); + templateService.RenderTemplate("test_meta.html", o as ObjectInstance, out Stream memoryStream); + Console.WriteLine("META TEST:\n{0}", new StreamReader(memoryStream).ReadToEnd()); + } + } diff --git a/ln.templates.test/__templates__/templates/test_meta.html b/ln.templates.test/__templates__/templates/test_meta.html new file mode 100644 index 0000000..8dff05c --- /dev/null +++ b/ln.templates.test/__templates__/templates/test_meta.html @@ -0,0 +1,14 @@ + + +

m

+ +

o

+ + + diff --git a/ln.templates.test/__templates__/templates/test_meta.json b/ln.templates.test/__templates__/templates/test_meta.json new file mode 100644 index 0000000..83c3231 --- /dev/null +++ b/ln.templates.test/__templates__/templates/test_meta.json @@ -0,0 +1,5 @@ +{ + "a": "b", + "author": "Harald", + "title": "Some title..." +} \ No newline at end of file diff --git a/ln.templates.test/ln.templates.test.csproj b/ln.templates.test/ln.templates.test.csproj index 34740a3..0c28a43 100644 --- a/ln.templates.test/ln.templates.test.csproj +++ b/ln.templates.test/ln.templates.test.csproj @@ -16,6 +16,7 @@ + @@ -38,6 +39,12 @@ Always + + Always + + + Always + diff --git a/ln.templates/Context.cs b/ln.templates/Context.cs index 407988c..d5cf8c5 100644 --- a/ln.templates/Context.cs +++ b/ln.templates/Context.cs @@ -1,12 +1,15 @@ +using System; using System.Collections.Generic; using System.IO; using Jint; +using Jint.Native; +using Jint.Native.Object; using ln.templates.html; namespace ln.templates; -public class Context +public class Context : IDisposable { public Context(ITemplateResolver resolver, IEnumerable> scriptObjects, TextWriter targetWriter) { @@ -29,6 +32,9 @@ public class Context TargetWriter = source.TargetWriter; } + public Context(ITemplateResolver resolver, TextWriter targetWriter) + : this(resolver, new Engine(), targetWriter){} + public Engine Engine { get; private set; } public ITemplateResolver Resolver { get; } @@ -49,5 +55,15 @@ public class Context foreach (var pair in slots) _slots.Add(pair.Key, pair.Value); } + + public void AssignGlobals(ObjectInstance o) + { + foreach (var key in o.GetOwnPropertyKeys()) + Engine.Realm.GlobalObject.Set(key, o.Get(key)); + } + + public void Dispose() + { + } } diff --git a/ln.templates/Template.cs b/ln.templates/Template.cs index 04c40ef..98811cc 100644 --- a/ln.templates/Template.cs +++ b/ln.templates/Template.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; using System.IO; +using Jint; +using Jint.Native; +using Jint.Native.Json; +using Jint.Native.Object; using ln.templates.html; namespace ln.templates; @@ -26,13 +30,24 @@ public class Template } } + private void Load() { Document = TemplateReader.ReadTemplate(FileName); LastWriteTime = File.GetLastWriteTime(FileName); + + string metaFileName = Path.Combine(Path.GetDirectoryName(FileName), String.Format("{0}.json",Path.GetFileNameWithoutExtension(FileName))); + if (File.Exists(metaFileName)) + { + using (StreamReader sr = new StreamReader(metaFileName)) + _jsonMeta = sr.ReadToEnd(); + } + else + { + _jsonMeta = "{}"; + } } - - + public ITemplateResolver Resolver { get; set; } public string FileName { get; } public TemplateDocument Document { get; private set; } @@ -45,12 +60,25 @@ public class Template Render(context); } - public void Render(Context context) + public void Render(Context context) => Render(context, null); + public void Render(Context context, ObjectInstance globals) { DateTime currentLastWriteTime = File.GetLastWriteTime(FileName); if (currentLastWriteTime > LastWriteTime) Load(); + + + if (_jsonMeta is not null) + context.AssignGlobals(new JsonParser(context.Engine).Parse(_jsonMeta) as ObjectInstance); + if (globals is not null) + context.AssignGlobals(globals); Document.RenderTemplate(context); + context.TargetWriter.Flush(); } + + /* JSON Metadata */ + private string _jsonMeta; + public JsValue GetMetaObject(Engine engine) => new JsonParser(engine).Parse(_jsonMeta); + } \ No newline at end of file diff --git a/ln.templates/html/QrCodeElement.cs b/ln.templates/html/QrCodeElement.cs new file mode 100644 index 0000000..a40ff83 --- /dev/null +++ b/ln.templates/html/QrCodeElement.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; +using Net.Codecrete.QrCodeGenerator; + +namespace ln.templates.html; + +public class QrCodeElement : TemplateElement +{ + public QrCodeElement(string tagName) : base(tagName) + { + } + + public override void RenderElement(Context renderContext) + { + string[] lines = QrCode.EncodeText(GetAttribute(renderContext, "value"), QrCode.Ecc.Medium) + .ToSvgString(0) + .Split(Environment.NewLine.ToCharArray()) + .Skip(2) + .ToArray(); + string svgSource = String.Join(Environment.NewLine, lines); + renderContext.TargetWriter.Write(svgSource); + } +} \ No newline at end of file diff --git a/ln.templates/html/TemplateElement.cs b/ln.templates/html/TemplateElement.cs index 733e211..37c744e 100644 --- a/ln.templates/html/TemplateElement.cs +++ b/ln.templates/html/TemplateElement.cs @@ -24,6 +24,7 @@ namespace ln.templates.html Dictionary expressions = new Dictionary(); Dictionary loops = new Dictionary(); + Dictionary _vsets = new Dictionary(); List conditions = new List(); private Dictionary _slots = new Dictionary(); @@ -44,6 +45,8 @@ namespace ln.templates.html loops.Add(attributeName.Substring(6), new NewExpression(attributeValue)); else if (attributeName.Equals("v-if", StringComparison.InvariantCultureIgnoreCase)) conditions.Add(new NewExpression(attributeValue)); + else if (attributeName.StartsWith("v-set:", StringComparison.InvariantCultureIgnoreCase)) + _vsets.Add(attributeName.Substring(6), new NewExpression(attributeValue)); else if (attributeName.Equals(":class", StringComparison.InvariantCultureIgnoreCase)) classExpression = new NewExpression(attributeValue); else if (attributeName.StartsWith("::")) @@ -128,8 +131,21 @@ namespace ln.templates.html */ public virtual void RenderElement(Context renderContext) { + Dictionary savesets = null; + if (checkConditions(renderContext)) { + if (_vsets.Count > 0) + { + savesets = new Dictionary(); + foreach (var vset in _vsets) + { + if (renderContext.Engine.Realm.GlobalEnv.HasBinding(vset.Key)) + savesets.Add(vset.Key, renderContext.Engine.Realm.GlobalEnv.GetBindingValue(vset.Key, false)); + renderContext.Engine.SetValue(vset.Key, vset.Value.Resolve(renderContext.Engine)); + } + } + if (TryGetAttribute(renderContext, "v-include", out string templatePath)) { Template template = renderContext.Resolver.GetTemplateByPath(templatePath); @@ -145,30 +161,30 @@ namespace ln.templates.html { if (!hideTag) { + renderContext.TargetWriter.Write("<{0}", Name); + object classObject = null; if (classExpression is not null) { classObject = renderContext.Engine.Evaluate("(function(){ return " + classExpression .ExpressionText + ";})()"); + + HashSet classes = new HashSet(GetAttribute("class", "").Split(' ', StringSplitOptions.RemoveEmptyEntries)); + var classObjectInstance = classObject as ObjectInstance; + + foreach (var property in classObjectInstance.GetOwnProperties()) + { + if ((property.Value.Value is JsBoolean jsBooleanValue) && + jsBooleanValue.Equals(JsBoolean.True)) + classes.Add(property.Key.ToString()); + } + + renderContext.TargetWriter.Write(" class=\"{0}\"", string.Join(" ", classes)); } - - renderContext.TargetWriter.Write("<{0}", Name); + foreach (KeyValuePair attributePair in Attributes) { - if (attributePair.Key.Equals("class") && (classObject is ObjectInstance classObjectInstance)) - { - StringBuilder valueBuilder = new StringBuilder(attributePair.Value); - foreach (var property in classObjectInstance.GetOwnProperties()) - { - if ((property.Value.Value is JsBoolean jsBooleanValue) && - jsBooleanValue.Equals(JsBoolean.True)) - valueBuilder.AppendFormat(" {0}", property.Key.ToString()); - } - - renderContext.TargetWriter.Write(" {0}=\"{1}\"", attributePair.Key, - valueBuilder.ToString()); - } - else + if (!attributePair.Key.Equals("class") || (classExpression is null)) { renderContext.TargetWriter.Write(" {0}=\"{1}\"", attributePair.Key, attributePair.Value); @@ -195,6 +211,13 @@ namespace ln.templates.html renderContext.TargetWriter.Write("", Name); } } + + if (savesets?.Count > 0) + { + foreach (var saveset in savesets) + renderContext.Engine.SetValue(saveset.Key, saveset.Value); + } + } } @@ -275,6 +298,8 @@ namespace ln.templates.html { switch (tagName) { + case "qrcode": + return new QrCodeElement(tagName); case "slot": return new SlotElement(tagName); case "template-script": diff --git a/ln.templates/ln.templates.csproj b/ln.templates/ln.templates.csproj index d628acd..4491187 100644 --- a/ln.templates/ln.templates.csproj +++ b/ln.templates/ln.templates.csproj @@ -17,6 +17,7 @@ +