@studious-lms/server 1.1.15 → 1.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/jsonConversion.d.ts +1 -1
- package/dist/lib/jsonConversion.d.ts.map +1 -1
- package/dist/lib/jsonConversion.js +291 -125
- package/dist/routers/_app.d.ts +180 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +2 -0
- package/dist/routers/assignment.d.ts +15 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/class.d.ts +5 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +7 -0
- package/dist/routers/file.d.ts +2 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/labChat.js +4 -3
- package/dist/routers/marketing.d.ts +70 -0
- package/dist/routers/marketing.d.ts.map +1 -0
- package/dist/routers/marketing.js +65 -0
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +1459 -24
- package/dist/utils/inference.js +2 -2
- package/package.json +2 -1
- package/prisma/schema.prisma +30 -0
- package/src/lib/NotoSans-Bold.ttf +0 -0
- package/src/lib/NotoSans-Italic.ttf +0 -0
- package/src/lib/NotoSans-Regular.ttf +0 -0
- package/src/lib/jsonConversion.ts +347 -136
- package/src/routers/_app.ts +2 -0
- package/src/routers/class.ts +7 -0
- package/src/routers/labChat.ts +6 -3
- package/src/routers/marketing.ts +69 -0
- package/src/seedDatabase.ts +1512 -21
- package/src/utils/inference.ts +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jsonConversion.d.ts","sourceRoot":"","sources":["../../src/lib/jsonConversion.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"jsonConversion.d.ts","sourceRoot":"","sources":["../../src/lib/jsonConversion.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAsB,MAAM,iBAAiB,CAAA;AAEnE,wBAAsB,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,wCAouBtD"}
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
|
|
2
|
-
import
|
|
2
|
+
import fontkit from '@pdf-lib/fontkit';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { FormatTypes, Fonts } from './jsonStyles.js';
|
|
3
6
|
export async function createPdf(blocks) {
|
|
4
7
|
console.log('createPdf: Starting PDF creation with', blocks.length, 'blocks');
|
|
5
8
|
try {
|
|
6
9
|
const pdfDoc = await PDFDocument.create();
|
|
7
10
|
console.log('createPdf: PDFDocument created successfully');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Register fontkit to enable custom font embedding
|
|
12
|
+
pdfDoc.registerFontkit(fontkit);
|
|
13
|
+
// Load Unicode-compatible fonts (Noto Sans)
|
|
14
|
+
const fontDir = join(process.cwd(), 'src', 'lib');
|
|
15
|
+
let notoSansRegular;
|
|
16
|
+
let notoSansBold;
|
|
17
|
+
let notoSansItalic;
|
|
18
|
+
let courierFont;
|
|
19
|
+
try {
|
|
20
|
+
// Try to load custom Unicode fonts
|
|
21
|
+
const regularFontBytes = readFileSync(join(fontDir, 'NotoSans-Regular.ttf'));
|
|
22
|
+
const boldFontBytes = readFileSync(join(fontDir, 'NotoSans-Bold.ttf'));
|
|
23
|
+
const italicFontBytes = readFileSync(join(fontDir, 'NotoSans-Italic.ttf'));
|
|
24
|
+
notoSansRegular = await pdfDoc.embedFont(regularFontBytes);
|
|
25
|
+
notoSansBold = await pdfDoc.embedFont(boldFontBytes);
|
|
26
|
+
notoSansItalic = await pdfDoc.embedFont(italicFontBytes);
|
|
27
|
+
courierFont = await pdfDoc.embedFont(StandardFonts.Courier); // Keep Courier for code blocks
|
|
28
|
+
console.log('createPdf: Unicode fonts loaded successfully');
|
|
29
|
+
}
|
|
30
|
+
catch (fontError) {
|
|
31
|
+
console.warn('createPdf: Failed to load custom fonts, falling back to standard fonts:', fontError);
|
|
32
|
+
// Fallback to standard fonts if custom fonts fail
|
|
33
|
+
notoSansRegular = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
34
|
+
notoSansBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
35
|
+
notoSansItalic = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
|
|
36
|
+
courierFont = await pdfDoc.embedFont(StandardFonts.Courier);
|
|
37
|
+
}
|
|
38
|
+
const defaultFont = notoSansRegular;
|
|
15
39
|
const defaultParagraphSpacing = 10;
|
|
16
40
|
const defaultLineHeight = 1.3;
|
|
17
41
|
const defaultFontSize = 12;
|
|
@@ -20,20 +44,20 @@ export async function createPdf(blocks) {
|
|
|
20
44
|
const headingColor = rgb(0.1, 0.1, 0.1);
|
|
21
45
|
const paragraphColor = rgb(0.15, 0.15, 0.15);
|
|
22
46
|
const FONTS = {
|
|
23
|
-
[Fonts.TIMES_ROMAN]:
|
|
47
|
+
[Fonts.TIMES_ROMAN]: notoSansRegular, // Use Noto Sans instead of Times
|
|
24
48
|
[Fonts.COURIER]: courierFont,
|
|
25
|
-
[Fonts.HELVETICA]:
|
|
26
|
-
[Fonts.HELVETICA_BOLD]:
|
|
27
|
-
[Fonts.HELVETICA_ITALIC]:
|
|
28
|
-
[Fonts.HELVETICA_BOLD_ITALIC]:
|
|
49
|
+
[Fonts.HELVETICA]: notoSansRegular,
|
|
50
|
+
[Fonts.HELVETICA_BOLD]: notoSansBold,
|
|
51
|
+
[Fonts.HELVETICA_ITALIC]: notoSansItalic,
|
|
52
|
+
[Fonts.HELVETICA_BOLD_ITALIC]: notoSansBold, // Use bold for now, could add bold-italic later
|
|
29
53
|
};
|
|
30
54
|
const STYLE_PRESETS = {
|
|
31
|
-
[FormatTypes.HEADER_1]: { fontSize: 28, lineHeight: 1.35, font:
|
|
32
|
-
[FormatTypes.HEADER_2]: { fontSize: 22, lineHeight: 1.35, font:
|
|
33
|
-
[FormatTypes.HEADER_3]: { fontSize: 18, lineHeight: 1.35, font:
|
|
34
|
-
[FormatTypes.HEADER_4]: { fontSize: 16, lineHeight: 1.3, font:
|
|
35
|
-
[FormatTypes.HEADER_5]: { fontSize: 14, lineHeight: 1.3, font:
|
|
36
|
-
[FormatTypes.HEADER_6]: { fontSize: 12, lineHeight: 1.3, font:
|
|
55
|
+
[FormatTypes.HEADER_1]: { fontSize: 28, lineHeight: 1.35, font: notoSansBold, color: headingColor },
|
|
56
|
+
[FormatTypes.HEADER_2]: { fontSize: 22, lineHeight: 1.35, font: notoSansBold, color: headingColor },
|
|
57
|
+
[FormatTypes.HEADER_3]: { fontSize: 18, lineHeight: 1.35, font: notoSansBold, color: headingColor },
|
|
58
|
+
[FormatTypes.HEADER_4]: { fontSize: 16, lineHeight: 1.3, font: notoSansBold, color: headingColor },
|
|
59
|
+
[FormatTypes.HEADER_5]: { fontSize: 14, lineHeight: 1.3, font: notoSansBold, color: headingColor },
|
|
60
|
+
[FormatTypes.HEADER_6]: { fontSize: 12, lineHeight: 1.3, font: notoSansBold, color: headingColor },
|
|
37
61
|
[FormatTypes.QUOTE]: { fontSize: 14, lineHeight: 1.5, color: rgb(0.35, 0.35, 0.35) },
|
|
38
62
|
[FormatTypes.CODE_BLOCK]: { fontSize: 12, lineHeight: 1.6, font: courierFont, color: rgb(0.1, 0.1, 0.1), background: rgb(0.95, 0.95, 0.95) },
|
|
39
63
|
[FormatTypes.PARAGRAPH]: { fontSize: 12, lineHeight: 1.3, color: paragraphColor },
|
|
@@ -68,26 +92,146 @@ export async function createPdf(blocks) {
|
|
|
68
92
|
return color;
|
|
69
93
|
}
|
|
70
94
|
};
|
|
71
|
-
//
|
|
95
|
+
// Minimal sanitization - only remove truly problematic invisible characters
|
|
96
|
+
// With Unicode fonts, we can now keep most characters as-is
|
|
72
97
|
const sanitizeText = (text) => {
|
|
73
98
|
return text
|
|
74
|
-
|
|
75
|
-
.replace(
|
|
76
|
-
.replace(
|
|
77
|
-
.replace(
|
|
78
|
-
.replace(
|
|
79
|
-
.replace(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
// Only remove invisible/control characters that break PDF generation
|
|
100
|
+
.replace(/\uFEFF/g, '') // Remove BOM (Byte Order Mark)
|
|
101
|
+
.replace(/\u200B/g, '') // Remove zero-width space
|
|
102
|
+
.replace(/\u200C/g, '') // Remove zero-width non-joiner
|
|
103
|
+
.replace(/\u200D/g, '') // Remove zero-width joiner
|
|
104
|
+
.replace(/\uFFFD/g, '?'); // Replace replacement character with ?
|
|
105
|
+
// Keep ALL visible Unicode characters - Noto Sans supports them!
|
|
106
|
+
};
|
|
107
|
+
const parseMarkdown = (text, baseFont, baseColor) => {
|
|
108
|
+
const segments = [];
|
|
109
|
+
let currentIndex = 0;
|
|
110
|
+
// Regex patterns for markdown
|
|
111
|
+
const patterns = [
|
|
112
|
+
{ regex: /\*\*(.*?)\*\*/g, font: notoSansBold }, // **bold**
|
|
113
|
+
{ regex: /__(.*?)__/g, font: notoSansBold }, // __bold__
|
|
114
|
+
{ regex: /\*(.*?)\*/g, font: notoSansItalic }, // *italic*
|
|
115
|
+
{ regex: /_(.*?)_/g, font: notoSansItalic }, // _italic_
|
|
116
|
+
{ regex: /`(.*?)`/g, font: courierFont, color: rgb(0.2, 0.2, 0.2) }, // `code`
|
|
117
|
+
];
|
|
118
|
+
// Find all markdown matches
|
|
119
|
+
const matches = [];
|
|
120
|
+
for (const pattern of patterns) {
|
|
121
|
+
let match;
|
|
122
|
+
pattern.regex.lastIndex = 0; // Reset regex
|
|
123
|
+
while ((match = pattern.regex.exec(text)) !== null) {
|
|
124
|
+
matches.push({
|
|
125
|
+
start: match.index,
|
|
126
|
+
end: match.index + match[0].length,
|
|
127
|
+
text: match[1], // Captured group (content without markdown syntax)
|
|
128
|
+
font: pattern.font,
|
|
129
|
+
color: pattern.color
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Sort matches by start position
|
|
134
|
+
matches.sort((a, b) => a.start - b.start);
|
|
135
|
+
// Remove overlapping matches (keep the first one)
|
|
136
|
+
const filteredMatches = [];
|
|
137
|
+
for (const match of matches) {
|
|
138
|
+
const hasOverlap = filteredMatches.some(existing => (match.start < existing.end && match.end > existing.start));
|
|
139
|
+
if (!hasOverlap) {
|
|
140
|
+
filteredMatches.push(match);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Build segments
|
|
144
|
+
for (const match of filteredMatches) {
|
|
145
|
+
// Add text before this match
|
|
146
|
+
if (match.start > currentIndex) {
|
|
147
|
+
const beforeText = text.substring(currentIndex, match.start);
|
|
148
|
+
if (beforeText) {
|
|
149
|
+
segments.push({
|
|
150
|
+
text: sanitizeText(beforeText),
|
|
151
|
+
font: baseFont,
|
|
152
|
+
color: baseColor
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Add the styled match
|
|
157
|
+
segments.push({
|
|
158
|
+
text: sanitizeText(match.text),
|
|
159
|
+
font: match.font,
|
|
160
|
+
color: match.color || baseColor
|
|
161
|
+
});
|
|
162
|
+
currentIndex = match.end;
|
|
163
|
+
}
|
|
164
|
+
// Add remaining text
|
|
165
|
+
if (currentIndex < text.length) {
|
|
166
|
+
const remainingText = text.substring(currentIndex);
|
|
167
|
+
if (remainingText) {
|
|
168
|
+
segments.push({
|
|
169
|
+
text: sanitizeText(remainingText),
|
|
170
|
+
font: baseFont,
|
|
171
|
+
color: baseColor
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// If no markdown was found, return the whole text as one segment
|
|
176
|
+
if (segments.length === 0) {
|
|
177
|
+
segments.push({
|
|
178
|
+
text: sanitizeText(text),
|
|
179
|
+
font: baseFont,
|
|
180
|
+
color: baseColor
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return segments;
|
|
184
|
+
};
|
|
185
|
+
// Enhanced text wrapping that handles styled segments
|
|
186
|
+
const wrapStyledText = (segments, fontSize, maxWidth) => {
|
|
187
|
+
const lines = [];
|
|
188
|
+
let currentLine = [];
|
|
189
|
+
let currentWidth = 0;
|
|
190
|
+
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
|
|
191
|
+
const segment = segments[segmentIndex];
|
|
192
|
+
const words = segment.text.split(/\s+/).filter(word => word.length > 0);
|
|
193
|
+
for (let i = 0; i < words.length; i++) {
|
|
194
|
+
const word = words[i];
|
|
195
|
+
const wordWidth = segment.font.widthOfTextAtSize(word, fontSize);
|
|
196
|
+
const spaceWidth = segment.font.widthOfTextAtSize(' ', fontSize);
|
|
197
|
+
// Add space before word if:
|
|
198
|
+
// 1. Not the first word in the line AND
|
|
199
|
+
// 2. (Not the first word in the segment OR not the first segment)
|
|
200
|
+
const needSpace = currentLine.length > 0 && (i > 0 || segmentIndex > 0);
|
|
201
|
+
const totalWidth = wordWidth + (needSpace ? spaceWidth : 0);
|
|
202
|
+
// Check if we need to wrap to new line
|
|
203
|
+
if (currentWidth + totalWidth > maxWidth && currentLine.length > 0) {
|
|
204
|
+
// Finish current line
|
|
205
|
+
lines.push({ segments: [...currentLine], width: currentWidth });
|
|
206
|
+
currentLine = [];
|
|
207
|
+
currentWidth = 0;
|
|
208
|
+
}
|
|
209
|
+
// Add the word to current line
|
|
210
|
+
if (needSpace && currentLine.length > 0) {
|
|
211
|
+
// Try to merge with last segment if same font and color
|
|
212
|
+
const lastSegment = currentLine[currentLine.length - 1];
|
|
213
|
+
if (lastSegment.font === segment.font && lastSegment.color === segment.color) {
|
|
214
|
+
lastSegment.text += ' ' + word;
|
|
215
|
+
currentWidth += spaceWidth + wordWidth;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Add space + word as new segment
|
|
219
|
+
currentLine.push({ text: ' ' + word, font: segment.font, color: segment.color });
|
|
220
|
+
currentWidth += spaceWidth + wordWidth;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// Add word without space (first word in line or first word overall)
|
|
225
|
+
currentLine.push({ text: word, font: segment.font, color: segment.color });
|
|
226
|
+
currentWidth += wordWidth;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Add final line if it has content
|
|
231
|
+
if (currentLine.length > 0) {
|
|
232
|
+
lines.push({ segments: currentLine, width: currentWidth });
|
|
233
|
+
}
|
|
234
|
+
return lines.length > 0 ? lines : [{ segments: [{ text: '', font: defaultFont, color: rgb(0, 0, 0) }], width: 0 }];
|
|
91
235
|
};
|
|
92
236
|
let page = pdfDoc.addPage();
|
|
93
237
|
let { width, height } = page.getSize();
|
|
@@ -196,41 +340,51 @@ export async function createPdf(blocks) {
|
|
|
196
340
|
}
|
|
197
341
|
};
|
|
198
342
|
const drawParagraph = (text) => {
|
|
199
|
-
const
|
|
200
|
-
const lines =
|
|
343
|
+
const segments = parseMarkdown(text, font, color);
|
|
344
|
+
const lines = wrapStyledText(segments, fontSize, maxTextWidth());
|
|
201
345
|
for (const line of lines) {
|
|
202
346
|
ensureSpace(lineHeight);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
347
|
+
let currentX = marginLeft;
|
|
348
|
+
for (const segment of line.segments) {
|
|
349
|
+
if (segment.text.trim()) {
|
|
350
|
+
page.drawText(segment.text, {
|
|
351
|
+
x: currentX,
|
|
352
|
+
y: y,
|
|
353
|
+
size: fontSize,
|
|
354
|
+
font: segment.font,
|
|
355
|
+
color: segment.color,
|
|
356
|
+
});
|
|
357
|
+
currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
210
360
|
y -= lineHeight;
|
|
211
361
|
}
|
|
212
362
|
};
|
|
213
363
|
const drawHeading = (text, align) => {
|
|
214
|
-
const
|
|
215
|
-
const lines =
|
|
364
|
+
const segments = parseMarkdown(text, font, color);
|
|
365
|
+
const lines = wrapStyledText(segments, fontSize, maxTextWidth());
|
|
216
366
|
for (const line of lines) {
|
|
217
367
|
ensureSpace(lineHeight);
|
|
218
|
-
let
|
|
368
|
+
let startX = marginLeft;
|
|
219
369
|
if (align === 'center') {
|
|
220
|
-
|
|
221
|
-
x = marginLeft + (maxTextWidth() - tw) / 2;
|
|
370
|
+
startX = marginLeft + (maxTextWidth() - line.width) / 2;
|
|
222
371
|
}
|
|
223
372
|
else if (align === 'right') {
|
|
224
|
-
|
|
225
|
-
|
|
373
|
+
startX = marginLeft + maxTextWidth() - line.width;
|
|
374
|
+
}
|
|
375
|
+
let currentX = startX;
|
|
376
|
+
for (const segment of line.segments) {
|
|
377
|
+
if (segment.text.trim()) {
|
|
378
|
+
page.drawText(segment.text, {
|
|
379
|
+
x: currentX,
|
|
380
|
+
y: y,
|
|
381
|
+
size: fontSize,
|
|
382
|
+
font: segment.font,
|
|
383
|
+
color: segment.color,
|
|
384
|
+
});
|
|
385
|
+
currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
|
|
386
|
+
}
|
|
226
387
|
}
|
|
227
|
-
page.drawText(line, {
|
|
228
|
-
x,
|
|
229
|
-
y: y,
|
|
230
|
-
size: fontSize,
|
|
231
|
-
font: font,
|
|
232
|
-
color: color,
|
|
233
|
-
});
|
|
234
388
|
y -= lineHeight;
|
|
235
389
|
}
|
|
236
390
|
};
|
|
@@ -239,36 +393,37 @@ export async function createPdf(blocks) {
|
|
|
239
393
|
const gap = 8;
|
|
240
394
|
const contentWidth = maxTextWidth() - (bulletIndent + gap);
|
|
241
395
|
for (const item of items) {
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
y: y,
|
|
249
|
-
size: fontSize,
|
|
250
|
-
font: font,
|
|
251
|
-
color: color,
|
|
252
|
-
});
|
|
253
|
-
// First line
|
|
254
|
-
page.drawText(lines[0], {
|
|
255
|
-
x: marginLeft + bulletIndent + gap,
|
|
256
|
-
y: y,
|
|
257
|
-
size: fontSize,
|
|
258
|
-
font: font,
|
|
259
|
-
color: color,
|
|
260
|
-
});
|
|
261
|
-
y -= lineHeight;
|
|
262
|
-
// Continuation lines with hanging indent
|
|
263
|
-
for (let i = 1; i < lines.length; i++) {
|
|
396
|
+
// Clean up any bullet symbols that the AI might have added
|
|
397
|
+
const cleanItem = item.replace(/^\s*[•*-]\s*/, '').trim();
|
|
398
|
+
const segments = parseMarkdown(cleanItem, font, color);
|
|
399
|
+
const lines = wrapStyledText(segments, fontSize, contentWidth);
|
|
400
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
401
|
+
const line = lines[lineIndex];
|
|
264
402
|
ensureSpace(lineHeight);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
403
|
+
// Draw bullet only on first line
|
|
404
|
+
if (lineIndex === 0) {
|
|
405
|
+
page.drawText('•', {
|
|
406
|
+
x: marginLeft + gap,
|
|
407
|
+
y: y,
|
|
408
|
+
size: fontSize,
|
|
409
|
+
font: font,
|
|
410
|
+
color: color,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
// Draw styled text
|
|
414
|
+
let currentX = marginLeft + bulletIndent + gap;
|
|
415
|
+
for (const segment of line.segments) {
|
|
416
|
+
if (segment.text.trim()) {
|
|
417
|
+
page.drawText(segment.text, {
|
|
418
|
+
x: currentX,
|
|
419
|
+
y: y,
|
|
420
|
+
size: fontSize,
|
|
421
|
+
font: segment.font,
|
|
422
|
+
color: segment.color,
|
|
423
|
+
});
|
|
424
|
+
currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
272
427
|
y -= lineHeight;
|
|
273
428
|
}
|
|
274
429
|
}
|
|
@@ -279,35 +434,39 @@ export async function createPdf(blocks) {
|
|
|
279
434
|
const contentWidth = maxTextWidth() - (numberIndent + gap);
|
|
280
435
|
let index = 1;
|
|
281
436
|
for (const item of items) {
|
|
437
|
+
// Clean up any numbers that the AI might have added
|
|
438
|
+
const cleanItem = item.replace(/^\s*\d+\.\s*/, '').trim();
|
|
282
439
|
const numLabel = `${index}.`;
|
|
283
440
|
const numWidth = font.widthOfTextAtSize(numLabel, fontSize);
|
|
284
|
-
const
|
|
285
|
-
const lines =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
x: marginLeft + gap,
|
|
289
|
-
y: y,
|
|
290
|
-
size: fontSize,
|
|
291
|
-
font: font,
|
|
292
|
-
color: color,
|
|
293
|
-
});
|
|
294
|
-
page.drawText(lines[0], {
|
|
295
|
-
x: marginLeft + Math.max(numberIndent, numWidth + 6) + gap,
|
|
296
|
-
y: y,
|
|
297
|
-
size: fontSize,
|
|
298
|
-
font: font,
|
|
299
|
-
color: color,
|
|
300
|
-
});
|
|
301
|
-
y -= lineHeight;
|
|
302
|
-
for (let i = 1; i < lines.length; i++) {
|
|
441
|
+
const segments = parseMarkdown(cleanItem, font, color);
|
|
442
|
+
const lines = wrapStyledText(segments, fontSize, contentWidth);
|
|
443
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
444
|
+
const line = lines[lineIndex];
|
|
303
445
|
ensureSpace(lineHeight);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
446
|
+
// Draw number only on first line
|
|
447
|
+
if (lineIndex === 0) {
|
|
448
|
+
page.drawText(numLabel, {
|
|
449
|
+
x: marginLeft + gap,
|
|
450
|
+
y: y,
|
|
451
|
+
size: fontSize,
|
|
452
|
+
font: font,
|
|
453
|
+
color: color,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
// Draw styled text
|
|
457
|
+
let currentX = marginLeft + Math.max(numberIndent, numWidth + 6) + gap;
|
|
458
|
+
for (const segment of line.segments) {
|
|
459
|
+
if (segment.text.trim()) {
|
|
460
|
+
page.drawText(segment.text, {
|
|
461
|
+
x: currentX,
|
|
462
|
+
y: y,
|
|
463
|
+
size: fontSize,
|
|
464
|
+
font: segment.font,
|
|
465
|
+
color: segment.color,
|
|
466
|
+
});
|
|
467
|
+
currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
311
470
|
y -= lineHeight;
|
|
312
471
|
}
|
|
313
472
|
index++;
|
|
@@ -318,14 +477,14 @@ export async function createPdf(blocks) {
|
|
|
318
477
|
const ruleGap = 8;
|
|
319
478
|
const contentX = marginLeft + ruleWidth + ruleGap;
|
|
320
479
|
const contentWidth = maxTextWidth() - (ruleWidth + ruleGap);
|
|
321
|
-
const
|
|
322
|
-
const lines =
|
|
480
|
+
const segments = parseMarkdown(text, font, color);
|
|
481
|
+
const lines = wrapStyledText(segments, fontSize, contentWidth);
|
|
323
482
|
const totalHeight = lines.length * lineHeight + fontSize;
|
|
324
483
|
var remainingHeight = totalHeight;
|
|
325
484
|
for (const line of lines) {
|
|
326
485
|
let pageAdded = ensureSpace(lineHeight);
|
|
327
486
|
if (pageAdded || remainingHeight == totalHeight) {
|
|
328
|
-
let blockHeight = Math.floor(Math.min(remainingHeight, y - marginBottom) / lineHeight) * lineHeight;
|
|
487
|
+
let blockHeight = Math.floor(Math.min(remainingHeight, y - marginBottom) / lineHeight) * lineHeight;
|
|
329
488
|
page.drawRectangle({
|
|
330
489
|
x: marginLeft,
|
|
331
490
|
y: y + lineHeight,
|
|
@@ -335,13 +494,20 @@ export async function createPdf(blocks) {
|
|
|
335
494
|
});
|
|
336
495
|
remainingHeight -= blockHeight + lineHeight - fontSize;
|
|
337
496
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
497
|
+
// Draw styled text
|
|
498
|
+
let currentX = contentX;
|
|
499
|
+
for (const segment of line.segments) {
|
|
500
|
+
if (segment.text.trim()) {
|
|
501
|
+
page.drawText(segment.text, {
|
|
502
|
+
x: currentX,
|
|
503
|
+
y: y,
|
|
504
|
+
size: fontSize,
|
|
505
|
+
font: segment.font,
|
|
506
|
+
color: segment.color,
|
|
507
|
+
});
|
|
508
|
+
currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
345
511
|
y -= lineHeight;
|
|
346
512
|
}
|
|
347
513
|
y -= lineHeight - fontSize;
|