Tabulator-Support in Brieftexten + ausstehende Batches
Tabs (Eingabe + Rendering): - Tab-Taste fügt echten Tabulator ein (App-Konfigurator + Directus manueller Druck); \t in beiden Zeichen-Whitelists erlaubt. - Renderer: Zeilen mit führenden Tabs werden nicht mehr nach Wortbreite umgebrochen; Tab-Spruenge an den rechten Rand geclampt; bei Ueberlauf rechtsbuendig (Datum ganz rechts, ohne Absturz nach links). Tab-loser Text bleibt bit-identisch. Mailer: - loadSettings bricht nicht mehr ab, wenn team_email fehlt (Team- Empfaenger separat gelesen). - Eingebaute Fallback-Vorlagen fuer order_created + Status-Mails, damit Bestaetigung/Status auch ohne DB-Vorlage zugestellt werden. Weitere ausstehende Arbeit: - Abweichender Text je Schriftstueck (#5) in Generierung/Konfigurator. - Produktions-Board: Format-Sammelgruppen (Bulk) + Bootstrap-Feld. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -63,10 +63,12 @@ function buildDocuments(order, entries) {
|
||||
const nr = Number(entry.letter_number) || (i + 1);
|
||||
|
||||
if (!istDatei) {
|
||||
// Abweichender Text je Schriftstück (#5) hat Vorrang; sonst Standardtext.
|
||||
const briefText = (entry.text && String(entry.text).trim()) ? entry.text : (order.text_template || '');
|
||||
documents.push({
|
||||
index: nr,
|
||||
type: 'letter',
|
||||
text: order.text_template || '',
|
||||
text: briefText,
|
||||
format: letterFormat,
|
||||
font,
|
||||
placeholders,
|
||||
|
||||
@@ -78,8 +78,10 @@ const TAB_MARKER = '';
|
||||
/**
|
||||
* Breite einer Textzeile in Pixeln messen.
|
||||
* Unterstützt TAB_MARKER: springt zur nächsten Tabstop-Position (6 × fontSizePx).
|
||||
* maxWidthPx (optional) begrenzt den Tab-Sprung auf den rechten Rand, damit viele
|
||||
* Tabs nicht über die Seite hinauslaufen. Ohne Angabe (Infinity) unverändert.
|
||||
*/
|
||||
function measureTextWidthPx({ text, fontFileName, fontSizePx }) {
|
||||
function measureTextWidthPx({ text, fontFileName, fontSizePx, maxWidthPx = Infinity }) {
|
||||
const { unitsPerEm, glyphs } = parseSvgFont(fontFileName);
|
||||
const scale = fontSizePx / unitsPerEm;
|
||||
const tabStopPx = fontSizePx * 6;
|
||||
@@ -88,7 +90,7 @@ function measureTextWidthPx({ text, fontFileName, fontSizePx }) {
|
||||
|
||||
for (const ch of String(text || '')) {
|
||||
if (ch === TAB_MARKER) {
|
||||
x = Math.ceil((x + 1) / tabStopPx) * tabStopPx;
|
||||
x = Math.min(Math.ceil((x + 1) / tabStopPx) * tabStopPx, maxWidthPx);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -126,6 +128,19 @@ function wrapTextToLinesByWidth({
|
||||
measureTextWidthPx({ text: t, fontFileName, fontSizePx });
|
||||
|
||||
for (const para of paragraphs) {
|
||||
// Führende Tabs = bewusste Positionierung der Zeile (z.B. Datum nach rechts).
|
||||
// Solche Zeilen NICHT nach Wortbreite umbrechen, sonst würde der Text als
|
||||
// "zu breit" gewertet und die Wörter fielen nach links auf eine neue Zeile.
|
||||
// Der eigentliche Versatz (bzw. Rechtsbündigkeit bei Überlauf) passiert im Layout.
|
||||
const lead = para.match(new RegExp(`^${TAB_MARKER}+`));
|
||||
if (lead) {
|
||||
const rest = para.slice(lead[0].length);
|
||||
if (!rest.includes(TAB_MARKER) && measure(rest) <= maxWidthPx) {
|
||||
allLines.push(para);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Tokens: Wörter und TAB_MARKER getrennt ermitteln.
|
||||
// TAB_MARKER durch einen Platzhalter ersetzen, mit \s+ splitten, dann zurücktauschen.
|
||||
const TAB_PLACEHOLDER = '';
|
||||
@@ -208,6 +223,7 @@ function layoutTextToPaths({
|
||||
startY,
|
||||
lineHeightPx,
|
||||
realisticHandwriting = false,
|
||||
maxWidthPx = Infinity,
|
||||
}) {
|
||||
const { unitsPerEm, glyphs } = parseSvgFont(fontFileName);
|
||||
|
||||
@@ -223,13 +239,32 @@ function layoutTextToPaths({
|
||||
|
||||
const tabStopPx = fontSizePx * 6;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
// Führende Tabs vorab auflösen: normalerweise an die Tab-Position rücken;
|
||||
// würde der restliche Zeileninhalt dort über den rechten Rand laufen, die
|
||||
// Zeile stattdessen rechtsbündig an den Rand setzen (z.B. Datum ganz rechts,
|
||||
// ohne dass etwas daneben steht). Nur aktiv, wenn maxWidthPx bekannt ist.
|
||||
let startIdx = 0;
|
||||
if (line[0] === TAB_MARKER) {
|
||||
let leadCount = 0;
|
||||
while (line[leadCount] === TAB_MARKER) leadCount++;
|
||||
let tx = 0;
|
||||
for (let t = 0; t < leadCount; t++) {
|
||||
tx = Math.min(Math.ceil((tx + 1) / tabStopPx) * tabStopPx, maxWidthPx);
|
||||
}
|
||||
const rest = line.slice(leadCount);
|
||||
const restW = measureTextWidthPx({ text: rest, fontFileName, fontSizePx, maxWidthPx });
|
||||
if (tx + restW > maxWidthPx) tx = Math.max(0, maxWidthPx - restW);
|
||||
x = startX + tx;
|
||||
startIdx = leadCount;
|
||||
}
|
||||
|
||||
for (let i = startIdx; i < line.length; i++) {
|
||||
const ch = line[i];
|
||||
|
||||
// Tabulator-Marker → zur nächsten Tabstop-Position springen
|
||||
// Tabulator-Marker → zur nächsten Tabstop-Position springen (an den Rand geclampt)
|
||||
if (ch === TAB_MARKER) {
|
||||
const relX = x - startX;
|
||||
const nextTab = Math.ceil((relX + 1) / tabStopPx) * tabStopPx;
|
||||
const nextTab = Math.min(Math.ceil((relX + 1) / tabStopPx) * tabStopPx, maxWidthPx);
|
||||
x = startX + nextTab;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ function generateLetterSVG(text, format, options = {}) {
|
||||
startY: firstBaselineY,
|
||||
lineHeightPx,
|
||||
realisticHandwriting: options.realisticHandwriting ?? false,
|
||||
maxWidthPx,
|
||||
});
|
||||
|
||||
const pathElements = paths
|
||||
@@ -167,6 +168,7 @@ function generateLetterSVG(text, format, options = {}) {
|
||||
startY: currentY,
|
||||
lineHeightPx,
|
||||
realisticHandwriting: options.realisticHandwriting ?? false,
|
||||
maxWidthPx,
|
||||
});
|
||||
paths.forEach(p => allSvgElements.push(
|
||||
`<path d="${escapeXml(p.d)}" transform="${p.transform}"
|
||||
|
||||
Reference in New Issue
Block a user