@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.
@@ -41,9 +41,9 @@ export async function sendAIMessage(content, conversationId, options = {}) {
41
41
  // Prepare sender info
42
42
  const senderInfo = {
43
43
  id: getAIUserId(),
44
- username: 'AI Assistant',
44
+ username: 'Newton_AI',
45
45
  profile: {
46
- displayName: options.customSender?.displayName || `${options.subject || 'AI'} Assistant`,
46
+ displayName: "Newton AI",
47
47
  profilePicture: options.customSender?.profilePicture || null,
48
48
  },
49
49
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.1.15",
3
+ "version": "1.1.17",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -22,6 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@google-cloud/storage": "^7.16.0",
25
+ "@pdf-lib/fontkit": "^1.1.1",
25
26
  "@prisma/client": "^6.7.0",
26
27
  "@trpc/server": "^11.4.3",
27
28
  "bcryptjs": "^3.0.2",
@@ -189,6 +189,9 @@ model File {
189
189
  message Message? @relation("MessageAttachments", fields: [messageId], references: [id], onDelete: Cascade)
190
190
 
191
191
  schools School[]
192
+
193
+ schoolDevelopementProgram SchoolDevelopementProgram? @relation("SchoolDevelopementProgramSupportingDocumentation", fields: [schoolDevelopementProgramId], references: [id], onDelete: Cascade)
194
+ schoolDevelopementProgramId String?
192
195
  }
193
196
 
194
197
  model Assignment {
@@ -398,3 +401,30 @@ model Mention {
398
401
 
399
402
  @@unique([messageId, userId])
400
403
  }
404
+
405
+
406
+ model SchoolDevelopementProgram {
407
+ id String @id
408
+ name String
409
+ type String
410
+ address String
411
+ city String
412
+ country String
413
+ numberOfStudents Int
414
+ numberOfTeachers Int
415
+ website String?
416
+
417
+ contactName String?
418
+ contactRole String?
419
+ contactEmail String?
420
+ contactPhone String? @default("")
421
+ eligibilityInformation String?
422
+ whyHelp String?
423
+ additionalInformation String?
424
+ submittedAt DateTime? @default(now())
425
+ reviewedAt DateTime?
426
+
427
+ status String @default("PENDING") // PENDING, APPROVED, REJECTED, REFERRED
428
+
429
+ supportingDocumentation File[] @relation("SchoolDevelopementProgramSupportingDocumentation")
430
+ }
Binary file
Binary file
Binary file
@@ -1,20 +1,49 @@
1
1
  import { PDFDocument, PDFFont, RGB, StandardFonts, last, rgb } from 'pdf-lib'
2
+ import fontkit from '@pdf-lib/fontkit'
3
+ import { readFileSync } from 'fs'
4
+ import { join } from 'path'
2
5
  import { writeFile } from 'fs'
3
- import { DocumentBlock, FormatTypes, Fonts } from './jsonStyles'
6
+ import { DocumentBlock, FormatTypes, Fonts } from './jsonStyles.js'
4
7
 
5
8
  export async function createPdf(blocks: DocumentBlock[]) {
6
9
  console.log('createPdf: Starting PDF creation with', blocks.length, 'blocks');
7
10
  try {
8
11
  const pdfDoc = await PDFDocument.create()
9
12
  console.log('createPdf: PDFDocument created successfully');
10
- const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)
11
- const courierFont = await pdfDoc.embedFont(StandardFonts.Courier)
12
- const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica)
13
- const helveticaBoldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
14
- const helveticaItalicFont = await pdfDoc.embedFont(StandardFonts.HelveticaOblique)
15
- const helveticaBoldItalicFont = await pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique)
16
-
17
- const defaultFont = helveticaFont
13
+
14
+ // Register fontkit to enable custom font embedding
15
+ pdfDoc.registerFontkit(fontkit)
16
+
17
+ // Load Unicode-compatible fonts (Noto Sans)
18
+ const fontDir = join(process.cwd(), 'src', 'lib')
19
+
20
+ let notoSansRegular: PDFFont
21
+ let notoSansBold: PDFFont
22
+ let notoSansItalic: PDFFont
23
+ let courierFont: PDFFont
24
+
25
+ try {
26
+ // Try to load custom Unicode fonts
27
+ const regularFontBytes = readFileSync(join(fontDir, 'NotoSans-Regular.ttf'))
28
+ const boldFontBytes = readFileSync(join(fontDir, 'NotoSans-Bold.ttf'))
29
+ const italicFontBytes = readFileSync(join(fontDir, 'NotoSans-Italic.ttf'))
30
+
31
+ notoSansRegular = await pdfDoc.embedFont(regularFontBytes)
32
+ notoSansBold = await pdfDoc.embedFont(boldFontBytes)
33
+ notoSansItalic = await pdfDoc.embedFont(italicFontBytes)
34
+ courierFont = await pdfDoc.embedFont(StandardFonts.Courier) // Keep Courier for code blocks
35
+
36
+ console.log('createPdf: Unicode fonts loaded successfully');
37
+ } catch (fontError) {
38
+ console.warn('createPdf: Failed to load custom fonts, falling back to standard fonts:', fontError);
39
+ // Fallback to standard fonts if custom fonts fail
40
+ notoSansRegular = await pdfDoc.embedFont(StandardFonts.Helvetica)
41
+ notoSansBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
42
+ notoSansItalic = await pdfDoc.embedFont(StandardFonts.HelveticaOblique)
43
+ courierFont = await pdfDoc.embedFont(StandardFonts.Courier)
44
+ }
45
+
46
+ const defaultFont = notoSansRegular
18
47
  const defaultParagraphSpacing = 10;
19
48
  const defaultLineHeight = 1.3
20
49
  const defaultFontSize = 12
@@ -25,23 +54,23 @@ export async function createPdf(blocks: DocumentBlock[]) {
25
54
  const paragraphColor = rgb(0.15, 0.15, 0.15)
26
55
 
27
56
  const FONTS: Record<number, PDFFont> = {
28
- [Fonts.TIMES_ROMAN]: timesRomanFont,
57
+ [Fonts.TIMES_ROMAN]: notoSansRegular, // Use Noto Sans instead of Times
29
58
  [Fonts.COURIER]: courierFont,
30
- [Fonts.HELVETICA]: helveticaFont,
31
- [Fonts.HELVETICA_BOLD]: helveticaBoldFont,
32
- [Fonts.HELVETICA_ITALIC]: helveticaItalicFont,
33
- [Fonts.HELVETICA_BOLD_ITALIC]: helveticaBoldItalicFont,
59
+ [Fonts.HELVETICA]: notoSansRegular,
60
+ [Fonts.HELVETICA_BOLD]: notoSansBold,
61
+ [Fonts.HELVETICA_ITALIC]: notoSansItalic,
62
+ [Fonts.HELVETICA_BOLD_ITALIC]: notoSansBold, // Use bold for now, could add bold-italic later
34
63
  }
35
64
 
36
65
  const STYLE_PRESETS: Record<number,
37
66
  { fontSize: number; lineHeight: number; paragraphSpacing?: number; font?: PDFFont; color?: RGB; background?: RGB }> =
38
67
  {
39
- [FormatTypes.HEADER_1]: { fontSize: 28, lineHeight: 1.35, font: helveticaBoldFont, color: headingColor },
40
- [FormatTypes.HEADER_2]: { fontSize: 22, lineHeight: 1.35, font: helveticaBoldFont, color: headingColor },
41
- [FormatTypes.HEADER_3]: { fontSize: 18, lineHeight: 1.35, font: helveticaBoldFont, color: headingColor },
42
- [FormatTypes.HEADER_4]: { fontSize: 16, lineHeight: 1.3, font: helveticaBoldFont, color: headingColor },
43
- [FormatTypes.HEADER_5]: { fontSize: 14, lineHeight: 1.3, font: helveticaBoldFont, color: headingColor },
44
- [FormatTypes.HEADER_6]: { fontSize: 12, lineHeight: 1.3, font: helveticaBoldFont, color: headingColor },
68
+ [FormatTypes.HEADER_1]: { fontSize: 28, lineHeight: 1.35, font: notoSansBold, color: headingColor },
69
+ [FormatTypes.HEADER_2]: { fontSize: 22, lineHeight: 1.35, font: notoSansBold, color: headingColor },
70
+ [FormatTypes.HEADER_3]: { fontSize: 18, lineHeight: 1.35, font: notoSansBold, color: headingColor },
71
+ [FormatTypes.HEADER_4]: { fontSize: 16, lineHeight: 1.3, font: notoSansBold, color: headingColor },
72
+ [FormatTypes.HEADER_5]: { fontSize: 14, lineHeight: 1.3, font: notoSansBold, color: headingColor },
73
+ [FormatTypes.HEADER_6]: { fontSize: 12, lineHeight: 1.3, font: notoSansBold, color: headingColor },
45
74
  [FormatTypes.QUOTE]: { fontSize: 14, lineHeight: 1.5, color: rgb(0.35, 0.35, 0.35) },
46
75
  [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) },
47
76
  [FormatTypes.PARAGRAPH]: { fontSize: 12, lineHeight: 1.3, color: paragraphColor },
@@ -73,26 +102,173 @@ export async function createPdf(blocks: DocumentBlock[]) {
73
102
  }
74
103
  }
75
104
 
76
- // Function to replace Unicode characters that can't be encoded by WinAnsi
105
+ // Minimal sanitization - only remove truly problematic invisible characters
106
+ // With Unicode fonts, we can now keep most characters as-is
77
107
  const sanitizeText = (text: string): string => {
78
108
  return text
79
- .replace(/→/g, '->') // Right arrow
80
- .replace(/←/g, '<-') // Left arrow
81
- .replace(/↑/g, '^') // Up arrow
82
- .replace(/↓/g, 'v') // Down arrow
83
- .replace(/•/g, '*') // Bullet point
84
- .replace(/–/g, '-') // En dash
85
- .replace(/—/g, '--') // Em dash
86
- .replace(/'/g, "'") // Left single quote
87
- .replace(/'/g, "'") // Right single quote
88
- .replace(/"/g, '"') // Left double quote
89
- .replace(/"/g, '"') // Right double quote
90
- .replace(/…/g, '...') // Ellipsis
91
- .replace(/°/g, ' degrees') // Degree symbol
92
- .replace(/±/g, '+/-') // Plus-minus
93
- .replace(/×/g, 'x') // Multiplication sign
94
- .replace(/÷/g, '/') // Division sign
95
- // Add more replacements as needed
109
+ // Only remove invisible/control characters that break PDF generation
110
+ .replace(/\uFEFF/g, '') // Remove BOM (Byte Order Mark)
111
+ .replace(/\u200B/g, '') // Remove zero-width space
112
+ .replace(/\u200C/g, '') // Remove zero-width non-joiner
113
+ .replace(/\u200D/g, '') // Remove zero-width joiner
114
+ .replace(/\uFFFD/g, '?') // Replace replacement character with ?
115
+ // Keep ALL visible Unicode characters - Noto Sans supports them!
116
+ }
117
+
118
+ // Parse markdown and return styled text segments
119
+ interface TextSegment {
120
+ text: string;
121
+ font: PDFFont;
122
+ color: RGB;
123
+ }
124
+
125
+ const parseMarkdown = (text: string, baseFont: PDFFont, baseColor: RGB): TextSegment[] => {
126
+ const segments: TextSegment[] = [];
127
+ let currentIndex = 0;
128
+
129
+ // Regex patterns for markdown
130
+ const patterns = [
131
+ { regex: /\*\*(.*?)\*\*/g, font: notoSansBold }, // **bold**
132
+ { regex: /__(.*?)__/g, font: notoSansBold }, // __bold__
133
+ { regex: /\*(.*?)\*/g, font: notoSansItalic }, // *italic*
134
+ { regex: /_(.*?)_/g, font: notoSansItalic }, // _italic_
135
+ { regex: /`(.*?)`/g, font: courierFont, color: rgb(0.2, 0.2, 0.2) }, // `code`
136
+ ];
137
+
138
+ // Find all markdown matches
139
+ const matches: Array<{start: number, end: number, text: string, font: PDFFont, color?: RGB}> = [];
140
+
141
+ for (const pattern of patterns) {
142
+ let match;
143
+ pattern.regex.lastIndex = 0; // Reset regex
144
+ while ((match = pattern.regex.exec(text)) !== null) {
145
+ matches.push({
146
+ start: match.index,
147
+ end: match.index + match[0].length,
148
+ text: match[1], // Captured group (content without markdown syntax)
149
+ font: pattern.font,
150
+ color: pattern.color
151
+ });
152
+ }
153
+ }
154
+
155
+ // Sort matches by start position
156
+ matches.sort((a, b) => a.start - b.start);
157
+
158
+ // Remove overlapping matches (keep the first one)
159
+ const filteredMatches: Array<{start: number, end: number, text: string, font: PDFFont, color?: RGB}> = [];
160
+ for (const match of matches) {
161
+ const hasOverlap = filteredMatches.some(existing =>
162
+ (match.start < existing.end && match.end > existing.start)
163
+ );
164
+ if (!hasOverlap) {
165
+ filteredMatches.push(match);
166
+ }
167
+ }
168
+
169
+ // Build segments
170
+ for (const match of filteredMatches) {
171
+ // Add text before this match
172
+ if (match.start > currentIndex) {
173
+ const beforeText = text.substring(currentIndex, match.start);
174
+ if (beforeText) {
175
+ segments.push({
176
+ text: sanitizeText(beforeText),
177
+ font: baseFont,
178
+ color: baseColor
179
+ });
180
+ }
181
+ }
182
+
183
+ // Add the styled match
184
+ segments.push({
185
+ text: sanitizeText(match.text),
186
+ font: match.font,
187
+ color: match.color || baseColor
188
+ });
189
+
190
+ currentIndex = match.end;
191
+ }
192
+
193
+ // Add remaining text
194
+ if (currentIndex < text.length) {
195
+ const remainingText = text.substring(currentIndex);
196
+ if (remainingText) {
197
+ segments.push({
198
+ text: sanitizeText(remainingText),
199
+ font: baseFont,
200
+ color: baseColor
201
+ });
202
+ }
203
+ }
204
+
205
+ // If no markdown was found, return the whole text as one segment
206
+ if (segments.length === 0) {
207
+ segments.push({
208
+ text: sanitizeText(text),
209
+ font: baseFont,
210
+ color: baseColor
211
+ });
212
+ }
213
+
214
+ return segments;
215
+ }
216
+
217
+ // Enhanced text wrapping that handles styled segments
218
+ const wrapStyledText = (segments: TextSegment[], fontSize: number, maxWidth: number): Array<{segments: TextSegment[], width: number}> => {
219
+ const lines: Array<{segments: TextSegment[], width: number}> = [];
220
+ let currentLine: TextSegment[] = [];
221
+ let currentWidth = 0;
222
+
223
+ for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
224
+ const segment = segments[segmentIndex];
225
+ const words = segment.text.split(/\s+/).filter(word => word.length > 0);
226
+
227
+ for (let i = 0; i < words.length; i++) {
228
+ const word = words[i];
229
+ const wordWidth = segment.font.widthOfTextAtSize(word, fontSize);
230
+ const spaceWidth = segment.font.widthOfTextAtSize(' ', fontSize);
231
+
232
+ // Add space before word if:
233
+ // 1. Not the first word in the line AND
234
+ // 2. (Not the first word in the segment OR not the first segment)
235
+ const needSpace = currentLine.length > 0 && (i > 0 || segmentIndex > 0);
236
+ const totalWidth = wordWidth + (needSpace ? spaceWidth : 0);
237
+
238
+ // Check if we need to wrap to new line
239
+ if (currentWidth + totalWidth > maxWidth && currentLine.length > 0) {
240
+ // Finish current line
241
+ lines.push({ segments: [...currentLine], width: currentWidth });
242
+ currentLine = [];
243
+ currentWidth = 0;
244
+ }
245
+
246
+ // Add the word to current line
247
+ if (needSpace && currentLine.length > 0) {
248
+ // Try to merge with last segment if same font and color
249
+ const lastSegment = currentLine[currentLine.length - 1];
250
+ if (lastSegment.font === segment.font && lastSegment.color === segment.color) {
251
+ lastSegment.text += ' ' + word;
252
+ currentWidth += spaceWidth + wordWidth;
253
+ } else {
254
+ // Add space + word as new segment
255
+ currentLine.push({ text: ' ' + word, font: segment.font, color: segment.color });
256
+ currentWidth += spaceWidth + wordWidth;
257
+ }
258
+ } else {
259
+ // Add word without space (first word in line or first word overall)
260
+ currentLine.push({ text: word, font: segment.font, color: segment.color });
261
+ currentWidth += wordWidth;
262
+ }
263
+ }
264
+ }
265
+
266
+ // Add final line if it has content
267
+ if (currentLine.length > 0) {
268
+ lines.push({ segments: currentLine, width: currentWidth });
269
+ }
270
+
271
+ return lines.length > 0 ? lines : [{ segments: [{ text: '', font: defaultFont, color: rgb(0, 0, 0) }], width: 0 }];
96
272
  }
97
273
 
98
274
  let page = pdfDoc.addPage()
@@ -204,42 +380,57 @@ export async function createPdf(blocks: DocumentBlock[]) {
204
380
  }
205
381
 
206
382
  const drawParagraph = (text: string) => {
207
- const sanitizedText = sanitizeText(text)
208
- const lines = wrapText(sanitizedText, font, fontSize, maxTextWidth())
383
+ const segments = parseMarkdown(text, font, color);
384
+ const lines = wrapStyledText(segments, fontSize, maxTextWidth());
385
+
209
386
  for (const line of lines) {
210
- ensureSpace(lineHeight)
211
- page.drawText(line, {
212
- x: marginLeft,
213
- y: y,
214
- size: fontSize,
215
- font: font,
216
- color: color,
217
- })
218
- y -= lineHeight
387
+ ensureSpace(lineHeight);
388
+ let currentX = marginLeft;
389
+
390
+ for (const segment of line.segments) {
391
+ if (segment.text.trim()) {
392
+ page.drawText(segment.text, {
393
+ x: currentX,
394
+ y: y,
395
+ size: fontSize,
396
+ font: segment.font,
397
+ color: segment.color,
398
+ });
399
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
400
+ }
401
+ }
402
+ y -= lineHeight;
219
403
  }
220
404
  }
221
405
 
222
406
  const drawHeading = (text: string, align?: 'left' | 'center' | 'right') => {
223
- const sanitizedText = sanitizeText(text)
224
- const lines = wrapText(sanitizedText, font, fontSize, maxTextWidth())
407
+ const segments = parseMarkdown(text, font, color);
408
+ const lines = wrapStyledText(segments, fontSize, maxTextWidth());
409
+
225
410
  for (const line of lines) {
226
- ensureSpace(lineHeight)
227
- let x = marginLeft
411
+ ensureSpace(lineHeight);
412
+ let startX = marginLeft;
413
+
228
414
  if (align === 'center') {
229
- const tw = font.widthOfTextAtSize(line, fontSize)
230
- x = marginLeft + (maxTextWidth() - tw) / 2
415
+ startX = marginLeft + (maxTextWidth() - line.width) / 2;
231
416
  } else if (align === 'right') {
232
- const tw = font.widthOfTextAtSize(line, fontSize)
233
- x = marginLeft + maxTextWidth() - tw
417
+ startX = marginLeft + maxTextWidth() - line.width;
234
418
  }
235
- page.drawText(line, {
236
- x,
237
- y: y,
238
- size: fontSize,
239
- font: font,
240
- color: color,
241
- })
242
- y -= lineHeight
419
+
420
+ let currentX = startX;
421
+ for (const segment of line.segments) {
422
+ if (segment.text.trim()) {
423
+ page.drawText(segment.text, {
424
+ x: currentX,
425
+ y: y,
426
+ size: fontSize,
427
+ font: segment.font,
428
+ color: segment.color,
429
+ });
430
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
431
+ }
432
+ }
433
+ y -= lineHeight;
243
434
  }
244
435
  }
245
436
 
@@ -248,37 +439,41 @@ export async function createPdf(blocks: DocumentBlock[]) {
248
439
  const gap = 8
249
440
  const contentWidth = maxTextWidth() - (bulletIndent + gap)
250
441
  for (const item of items) {
251
- const sanitizedItem = sanitizeText(item)
252
- const lines = wrapText(sanitizedItem, font, fontSize, contentWidth)
253
- ensureSpace(lineHeight)
254
- // Bullet glyph (use ASCII bullet instead of Unicode)
255
- page.drawText('*', {
256
- x: marginLeft + gap,
257
- y: y,
258
- size: fontSize,
259
- font: font,
260
- color: color,
261
- })
262
- // First line
263
- page.drawText(lines[0], {
264
- x: marginLeft + bulletIndent + gap,
265
- y: y,
266
- size: fontSize,
267
- font: font,
268
- color: color,
269
- })
270
- y -= lineHeight
271
- // Continuation lines with hanging indent
272
- for (let i = 1; i < lines.length; i++) {
273
- ensureSpace(lineHeight)
274
- page.drawText(lines[i], {
275
- x: marginLeft + bulletIndent + gap,
276
- y: y,
277
- size: fontSize,
278
- font: font,
279
- color: color,
280
- })
281
- y -= lineHeight
442
+ // Clean up any bullet symbols that the AI might have added
443
+ const cleanItem = item.replace(/^\s*[•*-]\s*/, '').trim();
444
+ const segments = parseMarkdown(cleanItem, font, color);
445
+ const lines = wrapStyledText(segments, fontSize, contentWidth);
446
+
447
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
448
+ const line = lines[lineIndex];
449
+ ensureSpace(lineHeight);
450
+
451
+ // Draw bullet only on first line
452
+ if (lineIndex === 0) {
453
+ page.drawText('•', {
454
+ x: marginLeft + gap,
455
+ y: y,
456
+ size: fontSize,
457
+ font: font,
458
+ color: color,
459
+ });
460
+ }
461
+
462
+ // Draw styled text
463
+ let currentX = marginLeft + bulletIndent + gap;
464
+ for (const segment of line.segments) {
465
+ if (segment.text.trim()) {
466
+ page.drawText(segment.text, {
467
+ x: currentX,
468
+ y: y,
469
+ size: fontSize,
470
+ font: segment.font,
471
+ color: segment.color,
472
+ });
473
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
474
+ }
475
+ }
476
+ y -= lineHeight;
282
477
  }
283
478
  }
284
479
  }
@@ -289,38 +484,45 @@ export async function createPdf(blocks: DocumentBlock[]) {
289
484
  const contentWidth = maxTextWidth() - (numberIndent + gap)
290
485
  let index = 1
291
486
  for (const item of items) {
487
+ // Clean up any numbers that the AI might have added
488
+ const cleanItem = item.replace(/^\s*\d+\.\s*/, '').trim();
292
489
  const numLabel = `${index}.`
293
490
  const numWidth = font.widthOfTextAtSize(numLabel, fontSize)
294
- const sanitizedItem = sanitizeText(item)
295
- const lines = wrapText(sanitizedItem, font, fontSize, contentWidth)
296
- ensureSpace(lineHeight)
297
- page.drawText(numLabel, {
298
- x: marginLeft + gap,
299
- y: y,
300
- size: fontSize,
301
- font: font,
302
- color: color,
303
- })
304
- page.drawText(lines[0], {
305
- x: marginLeft + Math.max(numberIndent, numWidth + 6) + gap,
306
- y: y,
307
- size: fontSize,
308
- font: font,
309
- color: color,
310
- })
311
- y -= lineHeight
312
- for (let i = 1; i < lines.length; i++) {
313
- ensureSpace(lineHeight)
314
- page.drawText(lines[i], {
315
- x: marginLeft + Math.max(numberIndent, numWidth + 6) + gap,
316
- y: y,
317
- size: fontSize,
318
- font: font,
319
- color: color,
320
- })
321
- y -= lineHeight
491
+ const segments = parseMarkdown(cleanItem, font, color);
492
+ const lines = wrapStyledText(segments, fontSize, contentWidth);
493
+
494
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
495
+ const line = lines[lineIndex];
496
+ ensureSpace(lineHeight);
497
+
498
+ // Draw number only on first line
499
+ if (lineIndex === 0) {
500
+ page.drawText(numLabel, {
501
+ x: marginLeft + gap,
502
+ y: y,
503
+ size: fontSize,
504
+ font: font,
505
+ color: color,
506
+ });
507
+ }
508
+
509
+ // Draw styled text
510
+ let currentX = marginLeft + Math.max(numberIndent, numWidth + 6) + gap;
511
+ for (const segment of line.segments) {
512
+ if (segment.text.trim()) {
513
+ page.drawText(segment.text, {
514
+ x: currentX,
515
+ y: y,
516
+ size: fontSize,
517
+ font: segment.font,
518
+ color: segment.color,
519
+ });
520
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
521
+ }
522
+ }
523
+ y -= lineHeight;
322
524
  }
323
- index++
525
+ index++;
324
526
  }
325
527
  }
326
528
 
@@ -329,14 +531,15 @@ export async function createPdf(blocks: DocumentBlock[]) {
329
531
  const ruleGap = 8
330
532
  const contentX = marginLeft + ruleWidth + ruleGap
331
533
  const contentWidth = maxTextWidth() - (ruleWidth + ruleGap)
332
- const sanitizedText = sanitizeText(text)
333
- const lines = wrapText(sanitizedText, font, fontSize, contentWidth)
534
+ const segments = parseMarkdown(text, font, color);
535
+ const lines = wrapStyledText(segments, fontSize, contentWidth);
334
536
  const totalHeight = lines.length * lineHeight + fontSize
335
537
  var remainingHeight = totalHeight
538
+
336
539
  for (const line of lines) {
337
540
  let pageAdded = ensureSpace(lineHeight)
338
541
  if (pageAdded || remainingHeight == totalHeight) {
339
- let blockHeight = Math.floor(Math.min(remainingHeight, y - marginBottom) / lineHeight) * lineHeight // Get remaining height on page
542
+ let blockHeight = Math.floor(Math.min(remainingHeight, y - marginBottom) / lineHeight) * lineHeight
340
543
  page.drawRectangle({
341
544
  x: marginLeft,
342
545
  y: y + lineHeight,
@@ -346,14 +549,22 @@ export async function createPdf(blocks: DocumentBlock[]) {
346
549
  })
347
550
  remainingHeight -= blockHeight + lineHeight - fontSize
348
551
  }
349
- page.drawText(line, {
350
- x: contentX,
351
- y: y,
352
- size: fontSize,
353
- font: font,
354
- color: color,
355
- })
356
- y -= lineHeight
552
+
553
+ // Draw styled text
554
+ let currentX = contentX;
555
+ for (const segment of line.segments) {
556
+ if (segment.text.trim()) {
557
+ page.drawText(segment.text, {
558
+ x: currentX,
559
+ y: y,
560
+ size: fontSize,
561
+ font: segment.font,
562
+ color: segment.color,
563
+ });
564
+ currentX += segment.font.widthOfTextAtSize(segment.text, fontSize);
565
+ }
566
+ }
567
+ y -= lineHeight;
357
568
  }
358
569
  y -= lineHeight - fontSize
359
570
  }