Alpha Commit / Move primary workspace to Notebook

master
Harald Wolff 2023-08-12 12:35:42 +02:00
parent dfb3850f36
commit 5863289947
34 changed files with 1694 additions and 21 deletions

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions">
<enabled-global>
<option value="SCSS" />
</enabled-global>
</component>
</project>

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ln.http" Version="0.9.1-test01" />
<PackageReference Include="ln.http" Version="0.9.7-test0" />
<PackageReference Include="ln.templates" Version="0.4.0" />
</ItemGroup>

View File

@ -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/(?<templatePath>.*\\.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/(?<templatePath>.*\\.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/(?<templatePath>.*)$")]
[Map(HttpMethod.GET, "/render/(?<templatePath>.*)$")]
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/(?<templatePath>.*)$")]
[Map(HttpMethod.GET, "/pdf/(?<templatePath>.*)$")]
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();
}
}
}

View File

@ -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();
}
}

View File

@ -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()
);
}
}
}

View File

@ -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<string> 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<string> templateFileNames = new List<string>();
Queue<string> subdirs = new Queue<string>();
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
);
}
}
}

View File

@ -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/<template-path-and-filename>
Retrieve source of template identified by <template-path-and-filename> which must end in .html
### POST /templates/<template-path-and-filename>
Create or update template identified by <template-path-and-filename> 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/<template-path-and-filename>
Renders the template identified by <template-path-and-filename> which must end in .html to HTML
Optional JSON object may be posted to be used as globals for this rendering.
### GET, POST /pdf/<template-path-and-filename>
Renders the template identified by <template-path-and-filename> which must end in .html to PDF
Optional JSON object may be posted to be used as globals for this rendering.

View File

@ -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 <u>23.04.2023</u>\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle <em>Zeilenumbrüche</em> 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 <u>23.04.2023</u>\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle <em>Zeilenumbrüche</em> 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 <u>23.04.2023</u>\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle <em>Zeilenumbrüche</em> 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 <u>23.04.2023</u>\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle <em>Zeilenumbrüche</em> 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 <u>23.04.2023</u>\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle <em>Zeilenumbrüche</em> 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 <u>23.04.2023</u>\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle <em>Zeilenumbrüche</em> 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 <u>23.04.2023</u>\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle <em>Zeilenumbrüche</em> 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 <u>23.04.2023</u>\nEs wurden diverse Updates eingespielt und dieser Text wird hier unheimlich lang gemacht, damit wir auch potentielle <em>Zeilenumbrüche</em> 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"
}
},
"": ""
}

View File

@ -0,0 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFrameworks>net6.0;net5.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\ln.bootstrap\ln.bootstrap\ln.bootstrap.csproj" />
<ProjectReference Include="..\ln.templates\ln.templates.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ln.bootstrap" Version="1.4.0" />
<PackageReference Include="ln.http" Version="0.9.7-test0" />
<PackageReference Include="ln.json" Version="1.2.4" />
<PackageReference Include="PuppeteerSharp" Version="10.1.2" />
</ItemGroup>
<ItemGroup>
<None Update="templates\page.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="templates\css\default.css">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="templates\page.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="storage\templates\page.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="storage\templates\page.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="storage\resources\css\default.scss">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="storage\templates\layout\pagefooter.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="storage\templates\layout\pageheader.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="storage\templates\blocks\address.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="storage\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=storage_005Ctemplates/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=templates/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -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",
});
}
}
}

View File

@ -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<string>($@"()=>{{
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 = "<div></div>",
FooterTemplate = footerTemplate,
DisplayHeaderFooter = true,
}).Wait();
pdfStream = pdfTempStream;
return true;
}
}
}
}
static PuppeteerRenderer()
{
_browserFetcher = new BrowserFetcher();
_browserFetcher.DownloadAsync(BrowserFetcher.DefaultChromiumRevision).Wait();
}
}
}

View File

@ -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 */

View File

@ -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"}

View File

@ -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;
}

View File

@ -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 */

View File

@ -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"}

View File

@ -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;
}

View File

@ -0,0 +1,7 @@
<div class="address">
<div class="sender">{{ company.shortname }} &centerdot; {{ company.address.street }} &centerdot; {{ company.address.postcode }} {{ company.address.town }}</div>
<slot></slot>
<div class="street">{{ address.street }}</div>
<div class="postcode">{{ address.postcode }} {{ address.town }}</div>
<div class="country" v-if="address.country">{{ address.country }}</div>
</div>

View File

@ -0,0 +1,48 @@
<table class="orderlines">
<thead>
<tr>
<td>Pos.</td>
<td>Bezeichnung</td>
<td>Einzelpreis</td>
<td class="text-left">Menge</td>
<td class="text-left">Netto</td>
<td class="text-left">Steuern</td>
</tr>
</thead>
<tbody>
<template v-for:orderline="orderlines">
<tr :class="{ 'break-avoid-after': !!orderline.text }">
<td class="text-right" style="padding-right: 0.2cm;">{{ orderline.position }}</td>
<td>{{ orderline.name }}</td>
<td class="text-right">{{ orderline.item_price_net }} {{ document.currency }}</td>
<td class="text-right">{{ orderline.quantity }}</td>
<td class="text-right">{{ orderline.line_price_net }} {{ document.currency }}</td>
<td class="text-right">
<div v-for:tax="orderline.line_taxes" class="duo nowrap">
<div>{{ tax.key }}</div>
<div>{{ tax.value }} {{ document.currency }}</div>
</div>
</td>
</tr>
<tr v-if="orderline.text">
<td colspan="5" class="text" style="white-space: pre-wrap;">{{ orderline.text }}</td>
</tr>
</template>
<tr class="break-avoid-after">
<td></td>
<td colspan="4">Netto:</td>
<td class="text-right">{{ document.sum_net }}</td>
</tr>
<tr v-for:tax="document.taxes" class="break-avoid-after">
<td></td>
<td colspan="4">{{ tax.value.name }} ({{ tax.key }}):</td>
<td class="text-right">{{ tax.value.sum }}</td>
</tr>
<tr class="pt">
<td></td>
<td colspan="4">Rechnungsbetrag:</td>
<td class="text-right"><em>{{ document.sum_total }}</em></td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,47 @@
<footer>
<style>
* {
font-size: 0.2cm;
}
footer {
display: block;
width: 100%;
font-size: 0.25cm;
margin: 0cm 2cm 0cm 2.5cm;
border-top: 0.5mm solid black;
}
table {
width: 100%;
}
td {
vertical-align: top;
}
.duo > div {
width: 100%;
}
</style>
<table>
<tr>
<td>{{ company.name }}<br>
{{ company.address.street }}<br>
{{ company.address.postcode }} {{ company.address.town }}</td>
<td>buchhaltung@wk-pro.com<br>
http://wk-pro.com<br>
+49 7082 4252629<br>
</td>
<td>Bankverbindung:<br>
{{ company.bank.iban }}<br>
{{ company.bank.name }}
</td>
<td>
<div>Seite <span class="pageNumber"></span> von <span class="totalPages"></span><br>
<br>
USt.ID: DE9834251</div>
</td>
</tr>
</table>
</footer>

View File

@ -0,0 +1,40 @@
<header>
<img src="https://haag.wk-pro.com/image/kivitendo.png">
<div class="right">
<h1>{{ document.title || "" }}</h1>
<h2>{{ document.ref }}</h2>
<div class="duo">
<div>Datum:</div>
<div>{{ document.date.substring(6,8) }}.{{ document.date.substring(4,6) }}.{{ document.date.substring(0,4) }}</div>
</div>
<div class="duo">
<div>Kunden-Nr.:</div>
<div>{{ document.customer.id }}</div>
</div>
<div v-if="document.author" class="duo">
<div>Ansprechpartner:</div>
<div>{{ document.author.firstname }} {{ document.author.lastname }}</div>
</div>
<div v-if="document.author.phone" class="duo">
<div>Telefon:</div>
<div>{{ document.author.phone }}</div>
</div>
<div v-if="document.author.fax" class="duo">
<div>Telefax:</div>
<div>{{ document.author.fax }}</div>
</div>
<div v-if="document.author.email" class="duo mb">
<div>E-Mail:</div>
<div>{{ document.author.email }}</div>
</div>
<div class="duo" v-if="company.bank">
<div>GiroCode</div>
<div class="qrcode"><qrcode :value="'BCD\n002\n1\nSCT\n\n' + company.name + '\n' + company.bank.iban + '\nEUR0.01\n\n\n' + document.customer.id + ' ' + document.ref + '\n\n' "></qrcode></div>
</div>
</div>
<template v-include="blocks/address.html" v-set:address="document.customer.address"><div v-slot="">{{ document.customer.firstname }} {{ document.customer.lastname }}</div></template>
<div class="clear"></div>
</header>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{ document.title || "" }} {{ document.ref || "" }}</title>
<link rel="stylesheet" href="css/default.css">
</head>
<body>
<template v-include="layout/pageheader.html"></template>
<main>
<div class="text">{{ document.text }}</div>
</main>
<template v-include="blocks/orderlines.html" v-set:orderlines="document.lines"></template>
<div v-if="document.texts" class="mt pt">
<div v-for:text="document.texts" class="small text-justify">{{ text }}</div>
</div>
<template v-include="layout/pagefooter.html"></template>
</body>
</html>

View File

@ -0,0 +1,4 @@
{
"title": "A simple Page",
"author": "Harald Wolff-Thobaben <harald@l--n.de>"
}

View File

@ -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

View File

@ -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());
}
}

View File

@ -0,0 +1,14 @@
<html>
<body>
<h1>m</h1>
<template v-for:k="m">
{{ k }}
{{ k.Key }} => {{ k.Value }}
</template>
<h1>o</h1>
<template v-for:k="o">
{{ k }}
{{ k.Key }} => {{ k.Value }}
</template>
</body>
</html>

View File

@ -0,0 +1,5 @@
{
"a": "b",
"author": "Harald",
"title": "Some title..."
}

View File

@ -16,6 +16,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ln.templates.service\ln.templates.service.csproj" />
<ProjectReference Include="..\ln.templates\ln.templates.csproj" />
</ItemGroup>
@ -38,6 +39,12 @@
<None Update="tests\test_slots_recursive.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="__templates__\templates\test_meta.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="__templates__\templates\test_meta.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -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<KeyValuePair<string, object>> 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()
{
}
}

View File

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

View File

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

View File

@ -24,6 +24,7 @@ namespace ln.templates.html
Dictionary<string, NewExpression> expressions = new Dictionary<string, NewExpression>();
Dictionary<string, NewExpression> loops = new Dictionary<string, NewExpression>();
Dictionary<string, NewExpression> _vsets = new Dictionary<string, NewExpression>();
List<NewExpression> conditions = new List<NewExpression>();
private Dictionary<string, Element> _slots = new Dictionary<string, Element>();
@ -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<string, object> savesets = null;
if (checkConditions(renderContext))
{
if (_vsets.Count > 0)
{
savesets = new Dictionary<string, object>();
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<string> classes = new HashSet<string>(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<String, String> 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("</{0}>", 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":

View File

@ -17,6 +17,7 @@
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-preview-288" />
<PackageReference Include="Net.Codecrete.QrCodeGenerator" Version="2.0.3" />
</ItemGroup>
</Project>