yavsc/src/Yavsc.Api/Helpers/TeXHelpers.cs

248 lines
10 KiB
C#
Raw Normal View History

2016-11-30 11:01:22 +01:00
using System.Diagnostics;
2023-03-19 17:57:55 +00:00
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
2016-11-06 02:03:36 +01:00
namespace Yavsc.Helpers
{
using ViewModels.Gen;
public class TeXString : HtmlString
2016-11-30 11:01:22 +01:00
{
2016-11-10 01:03:49 +01:00
public TeXString(TeXString teXString): base(teXString.ToString())
{
}
public TeXString(string str) : base(str)
{
2017-05-27 16:22:58 +02:00
}
2017-05-27 16:22:58 +02:00
2017-06-08 00:26:29 +02:00
public static TeXString operator+ (TeXString a, TeXString b) {
return new TeXString(a.ToString()+b.ToString());
}
}
public class Replacement
2016-11-30 11:01:22 +01:00
{
2016-11-10 01:03:49 +01:00
string target;
string replacement;
2016-11-30 11:01:22 +01:00
public Replacement(string target, string replacement)
{
this.target = target;
this.replacement = replacement;
2016-11-10 01:03:49 +01:00
}
public string Execute(string source)
{
2017-06-08 11:06:31 +02:00
return source?.Replace(target, replacement) ?? null;
2016-11-10 01:03:49 +01:00
}
}
public static class TeXHelpers
{
2017-05-27 16:22:58 +02:00
public static readonly Replacement[] SpecialCharsDefaultRendering =
2016-11-10 01:03:49 +01:00
{
new Replacement("<","\\textless"),
new Replacement(">","\\textgreater"),
new Replacement("©","\\copyright"),
new Replacement("®","\\textregistered"),
new Replacement("\\","\\textbackslash"),
new Replacement("™","\\texttrademark"),
new Replacement("¶","\\P"),
new Replacement("|","\\textbar"),
new Replacement("%","\\%"),
new Replacement("{","\\{"),
new Replacement("}","\\}"),
new Replacement("_","\\_"),
new Replacement("#","\\#"),
new Replacement("$","\\$"),
new Replacement("_","\\_"),
new Replacement("¿","\\textquestiondown"),
new Replacement("§","\\S"),
new Replacement("£","\\pounds"),
new Replacement("&","\\&"),
new Replacement("¡","\\textexclamdown"),
new Replacement("†","\\dag"),
new Replacement("–","\\textendash"),
new Replacement("°","\\textdegree")
2016-11-10 01:03:49 +01:00
};
2017-06-08 17:09:06 +02:00
public static TeXString ToTeX(this string source, string defaultValue="\\textit{néant}")
2016-11-30 11:01:22 +01:00
{
2017-06-08 17:09:06 +02:00
if (source==null) return new TeXString(defaultValue);
string result=source;
2017-05-27 16:22:58 +02:00
foreach (var r in SpecialCharsDefaultRendering)
2016-11-30 11:01:22 +01:00
{
result = r.Execute(result);
2016-11-10 01:03:49 +01:00
}
return new TeXString(result);
2016-11-10 01:03:49 +01:00
}
2017-06-08 11:06:31 +02:00
2017-06-08 17:09:06 +02:00
public static TeXString ToTeXCell(this string source, string defaultValue="\\textit{néant}")
2017-06-08 11:06:31 +02:00
{
2017-06-08 17:09:06 +02:00
if (source==null) return new TeXString(defaultValue);
2017-06-08 11:06:31 +02:00
string result=source;
foreach (var r in SpecialCharsDefaultRendering)
{
result = r.Execute(result);
}
result = result.Replace("\n","\\tabularnewline ");
return new TeXString(result);
}
2026-09-17 22:22:18 +01:00
2017-06-08 11:06:31 +02:00
2016-11-06 02:03:36 +01:00
public static string NewLinesWith(this string target, string separator)
2016-11-30 11:01:22 +01:00
{
var items = target.Split(new char[] { '\n' }).Where(
s => !string.IsNullOrWhiteSpace(s));
2016-11-06 02:03:36 +01:00
return string.Join(separator, items);
}
2016-11-10 01:03:49 +01:00
2017-06-08 17:09:06 +02:00
public static TeXString ToTeXLines(this string source, string defaultValue, string lineSeparator = "\n\\\\")
2016-11-10 01:03:49 +01:00
{
2017-06-08 17:09:06 +02:00
if (source == null) return new TeXString(defaultValue);
return new TeXString( source.ToTeX().ToString().NewLinesWith(lineSeparator) );
2016-11-10 01:03:49 +01:00
}
2017-06-08 17:09:06 +02:00
public static TeXString SplitAddressToTeX (this string source, string lineSeparator = "\n\\\\", string defaultValue = "\\textit{pas d'adresse postale}")
{
2017-06-08 17:09:06 +02:00
if (string.IsNullOrWhiteSpace(source)) return new TeXString(defaultValue);
var alines = source.Split(',');
var texlines = alines.Select(l=>l.ToTeX().ToString());
2017-06-08 17:09:06 +02:00
return new TeXString(string.Join(lineSeparator,texlines));
}
2026-09-17 22:22:18 +01:00
public static bool GenerateEstimatePdf(this SiteSettings settings, PdfGenerationViewModel Model)
2016-11-30 11:01:22 +01:00
{
string errorMsg = null;
// Resolve to an absolute path: SiteSettings.Bills may be a
// relative path (e.g. "bills"), and lualatex resolves
// -output-directory against its working directory — passing a
// relative path to both would nest them (<cwd>/bills/bills) and
// the log/pdf write would fail.
var billdir = new DirectoryInfo(Model.DestDir).FullName;
2016-11-30 11:01:22 +01:00
string name = Model.BaseFileName;
FileInfo fo = new FileInfo(System.IO.Path.Combine(billdir, name + ".pdf"));
System.IO.Directory.CreateDirectory(billdir);
// Compile the LaTeX source with the system lualatex, feeding it
// the TeX on standard input (no intermediate .tex file) and
// directing the resulting <name>.pdf straight into the bills
// directory. lualatex is a file-based engine — it cannot emit
// the PDF on stdout — so the pdf lands at its final destination
// and we only clean the transient aux/log artifacts beside it.
//
// We capture stdout/stderr so a LaTeX error (missing .sty,
// missing \includegraphics target, …) is surfaced in
// GenerationErrorMessage instead of being buried in the .log
// we then delete.
int exitCode;
string stdout = null, stderr = null;
try
2016-11-30 11:01:22 +01:00
{
using (Process p = new Process())
{
2021-03-10 01:41:31 +00:00
p.StartInfo = new ProcessStartInfo
{
UseShellExecute = false,
WorkingDirectory = billdir,
FileName = "lualatex",
Arguments = $"-interaction=nonstopmode -halt-on-error"
+ $" -jobname=\"{name}\" -output-directory=\"{billdir}\"",
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
2021-03-10 01:41:31 +00:00
};
2016-11-30 11:01:22 +01:00
p.Start();
// Write the TeX source, then drain stdout/stderr. Read
// them after WaitForExit — lualatex's output is small
// (a few KB), well under the pipe buffer, so no deadlock.
using (var stdin = p.StandardInput)
2016-11-30 11:01:22 +01:00
{
stdin.Write(Model.TeXSource);
}
p.WaitForExit();
exitCode = p.ExitCode;
stdout = p.StandardOutput.ReadToEnd();
stderr = p.StandardError.ReadToEnd();
2016-11-30 11:01:22 +01:00
}
}
catch (Exception ex)
{
errorMsg = "lualatex invocation failed: " + ex.Message;
exitCode = -1;
}
if (exitCode != 0 && errorMsg == null)
{
// Surface the engine's own diagnostics (stderr carries the
// "! Emergency stop" / "! LaTeX Error: …" lines; stdout has
// the transcript banner) so the caller can see why it failed
// without having to find the (soon-deleted) .log file.
var diag = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr;
errorMsg = $"Pdf generation failed with exit code {exitCode}: {diag}";
}
else if (!fo.Exists && errorMsg == null)
errorMsg = "Pdf generation produced no output";
// Remove the LaTeX build artifacts left beside the pdf so only
// <name>.pdf persists in the bills directory. Keep the .log on
// failure so it can be inspected.
foreach (var ext in new[] { ".aux", ".log", ".out", ".fls",
".fdb_latexmk", ".synctex.gz", ".toc" })
{
if (ext == ".log" && !fo.Exists) continue;
var f = new FileInfo(System.IO.Path.Combine(billdir, name + ext));
if (f.Exists) { try { f.Delete(); } catch { } }
}
2016-11-30 11:01:22 +01:00
Model.Generated = fo.Exists;
Model.GenerationErrorMessage = new HtmlString(errorMsg);
return fo.Exists;
}
2016-11-10 01:03:49 +01:00
2016-11-09 14:03:57 +01:00
public static string RenderViewToString(
this Controller controller, IViewEngine engine,
string viewName, object model, bool isMainPage = true)
2016-11-30 11:01:22 +01:00
{
if (engine == null)
throw new InvalidOperationException("no engine");
// Build an ActionContext from the controller's own context —
// .NET 10 deprecated IActionContextAccessor, and the controller
// already holds the live request context on its ControllerContext.
var controllerContext = controller.ControllerContext;
var actionContext = new ActionContext(
controllerContext.HttpContext,
controllerContext.RouteData,
controllerContext.ActionDescriptor);
ViewEngineResult viewResult = engine.FindView(actionContext, viewName, isMainPage);
if (!viewResult.Success)
throw new InvalidOperationException(
$"View '{viewName}' not found. Searched locations: "
+ string.Join(", ", viewResult.SearchedLocations));
// Hand the model to the view through ViewData so the template's
// @model directive resolves it (the previous implementation never
// set ViewData.Model, leaving Model null inside the template).
controller.ViewData.Model = model;
2016-11-30 11:01:22 +01:00
using (var sw = new StringWriter())
{
var viewContext = new ViewContext(
actionContext,
viewResult.View,
controller.ViewData,
controller.TempData,
sw,
new HtmlHelperOptions());
viewResult.View.RenderAsync(viewContext).GetAwaiter().GetResult();
2016-11-30 11:01:22 +01:00
return sw.GetStringBuilder().ToString();
}
}
2016-11-06 02:03:36 +01:00
}
2017-05-27 16:22:58 +02:00
}