Alpha Commit / Move primary workspace to Notebook
parent
dfb3850f36
commit
5863289947
|
@ -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>
|
|
@ -10,7 +10,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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" />
|
<PackageReference Include="ln.templates" Version="0.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"": ""
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 */
|
|
@ -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"}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 */
|
|
@ -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"}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<div class="address">
|
||||||
|
<div class="sender">{{ company.shortname }} · {{ company.address.street }} · {{ 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"title": "A simple Page",
|
||||||
|
"author": "Harald Wolff-Thobaben <harald@l--n.de>"
|
||||||
|
}
|
|
@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.templates.test", "ln.tem
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.templates", "ln.http.templates\ln.http.templates.csproj", "{011E8B81-DF87-4AFB-BEA8-ADA6FD3F3665}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http.templates", "ln.http.templates\ln.http.templates.csproj", "{011E8B81-DF87-4AFB-BEA8-ADA6FD3F3665}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{011E8B81-DF87-4AFB-BEA8-ADA6FD3F3665}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
@ -11,6 +11,12 @@ using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
using ln.templates.html;
|
using ln.templates.html;
|
||||||
using System.IO;
|
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
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"a": "b",
|
||||||
|
"author": "Harald",
|
||||||
|
"title": "Some title..."
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ln.templates.service\ln.templates.service.csproj" />
|
||||||
<ProjectReference Include="..\ln.templates\ln.templates.csproj" />
|
<ProjectReference Include="..\ln.templates\ln.templates.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -38,6 +39,12 @@
|
||||||
<None Update="tests\test_slots_recursive.html">
|
<None Update="tests\test_slots_recursive.html">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<None Update="__templates__\templates\test_meta.html">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="__templates__\templates\test_meta.json">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Jint;
|
using Jint;
|
||||||
|
using Jint.Native;
|
||||||
|
using Jint.Native.Object;
|
||||||
using ln.templates.html;
|
using ln.templates.html;
|
||||||
|
|
||||||
namespace ln.templates;
|
namespace ln.templates;
|
||||||
|
|
||||||
|
|
||||||
public class Context
|
public class Context : IDisposable
|
||||||
{
|
{
|
||||||
public Context(ITemplateResolver resolver, IEnumerable<KeyValuePair<string, object>> scriptObjects, TextWriter targetWriter)
|
public Context(ITemplateResolver resolver, IEnumerable<KeyValuePair<string, object>> scriptObjects, TextWriter targetWriter)
|
||||||
{
|
{
|
||||||
|
@ -29,6 +32,9 @@ public class Context
|
||||||
TargetWriter = source.TargetWriter;
|
TargetWriter = source.TargetWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Context(ITemplateResolver resolver, TextWriter targetWriter)
|
||||||
|
: this(resolver, new Engine(), targetWriter){}
|
||||||
|
|
||||||
public Engine Engine { get; private set; }
|
public Engine Engine { get; private set; }
|
||||||
public ITemplateResolver Resolver { get; }
|
public ITemplateResolver Resolver { get; }
|
||||||
|
|
||||||
|
@ -49,5 +55,15 @@ public class Context
|
||||||
foreach (var pair in slots)
|
foreach (var pair in slots)
|
||||||
_slots.Add(pair.Key, pair.Value);
|
_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()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Jint;
|
||||||
|
using Jint.Native;
|
||||||
|
using Jint.Native.Json;
|
||||||
|
using Jint.Native.Object;
|
||||||
using ln.templates.html;
|
using ln.templates.html;
|
||||||
|
|
||||||
namespace ln.templates;
|
namespace ln.templates;
|
||||||
|
@ -26,13 +30,24 @@ public class Template
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void Load()
|
private void Load()
|
||||||
{
|
{
|
||||||
Document = TemplateReader.ReadTemplate(FileName);
|
Document = TemplateReader.ReadTemplate(FileName);
|
||||||
LastWriteTime = File.GetLastWriteTime(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 ITemplateResolver Resolver { get; set; }
|
||||||
public string FileName { get; }
|
public string FileName { get; }
|
||||||
public TemplateDocument Document { get; private set; }
|
public TemplateDocument Document { get; private set; }
|
||||||
|
@ -45,12 +60,25 @@ public class Template
|
||||||
Render(context);
|
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);
|
DateTime currentLastWriteTime = File.GetLastWriteTime(FileName);
|
||||||
if (currentLastWriteTime > LastWriteTime)
|
if (currentLastWriteTime > LastWriteTime)
|
||||||
Load();
|
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);
|
Document.RenderTemplate(context);
|
||||||
|
context.TargetWriter.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* JSON Metadata */
|
||||||
|
private string _jsonMeta;
|
||||||
|
public JsValue GetMetaObject(Engine engine) => new JsonParser(engine).Parse(_jsonMeta);
|
||||||
|
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ namespace ln.templates.html
|
||||||
|
|
||||||
Dictionary<string, NewExpression> expressions = new Dictionary<string, NewExpression>();
|
Dictionary<string, NewExpression> expressions = new Dictionary<string, NewExpression>();
|
||||||
Dictionary<string, NewExpression> loops = 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>();
|
List<NewExpression> conditions = new List<NewExpression>();
|
||||||
|
|
||||||
private Dictionary<string, Element> _slots = new Dictionary<string, Element>();
|
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));
|
loops.Add(attributeName.Substring(6), new NewExpression(attributeValue));
|
||||||
else if (attributeName.Equals("v-if", StringComparison.InvariantCultureIgnoreCase))
|
else if (attributeName.Equals("v-if", StringComparison.InvariantCultureIgnoreCase))
|
||||||
conditions.Add(new NewExpression(attributeValue));
|
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))
|
else if (attributeName.Equals(":class", StringComparison.InvariantCultureIgnoreCase))
|
||||||
classExpression = new NewExpression(attributeValue);
|
classExpression = new NewExpression(attributeValue);
|
||||||
else if (attributeName.StartsWith("::"))
|
else if (attributeName.StartsWith("::"))
|
||||||
|
@ -128,8 +131,21 @@ namespace ln.templates.html
|
||||||
*/
|
*/
|
||||||
public virtual void RenderElement(Context renderContext)
|
public virtual void RenderElement(Context renderContext)
|
||||||
{
|
{
|
||||||
|
Dictionary<string, object> savesets = null;
|
||||||
|
|
||||||
if (checkConditions(renderContext))
|
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))
|
if (TryGetAttribute(renderContext, "v-include", out string templatePath))
|
||||||
{
|
{
|
||||||
Template template = renderContext.Resolver.GetTemplateByPath(templatePath);
|
Template template = renderContext.Resolver.GetTemplateByPath(templatePath);
|
||||||
|
@ -145,30 +161,30 @@ namespace ln.templates.html
|
||||||
{
|
{
|
||||||
if (!hideTag)
|
if (!hideTag)
|
||||||
{
|
{
|
||||||
|
renderContext.TargetWriter.Write("<{0}", Name);
|
||||||
|
|
||||||
object classObject = null;
|
object classObject = null;
|
||||||
if (classExpression is not null)
|
if (classExpression is not null)
|
||||||
{
|
{
|
||||||
classObject = renderContext.Engine.Evaluate("(function(){ return " + classExpression
|
classObject = renderContext.Engine.Evaluate("(function(){ return " + classExpression
|
||||||
.ExpressionText + ";})()");
|
.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)
|
foreach (KeyValuePair<String, String> attributePair in Attributes)
|
||||||
{
|
{
|
||||||
if (attributePair.Key.Equals("class") && (classObject is ObjectInstance classObjectInstance))
|
if (!attributePair.Key.Equals("class") || (classExpression is null))
|
||||||
{
|
|
||||||
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
|
|
||||||
{
|
{
|
||||||
renderContext.TargetWriter.Write(" {0}=\"{1}\"", attributePair.Key,
|
renderContext.TargetWriter.Write(" {0}=\"{1}\"", attributePair.Key,
|
||||||
attributePair.Value);
|
attributePair.Value);
|
||||||
|
@ -195,6 +211,13 @@ namespace ln.templates.html
|
||||||
renderContext.TargetWriter.Write("</{0}>", Name);
|
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)
|
switch (tagName)
|
||||||
{
|
{
|
||||||
|
case "qrcode":
|
||||||
|
return new QrCodeElement(tagName);
|
||||||
case "slot":
|
case "slot":
|
||||||
return new SlotElement(tagName);
|
return new SlotElement(tagName);
|
||||||
case "template-script":
|
case "template-script":
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jint" Version="3.0.0-preview-288" />
|
<PackageReference Include="Jint" Version="3.0.0-preview-288" />
|
||||||
|
<PackageReference Include="Net.Codecrete.QrCodeGenerator" Version="2.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Loading…
Reference in New Issue