@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
package/dist/utils/inference.js
CHANGED
|
@@ -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: '
|
|
44
|
+
username: 'Newton_AI',
|
|
45
45
|
profile: {
|
|
46
|
-
displayName:
|
|
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.
|
|
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",
|
package/prisma/schema.prisma
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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]:
|
|
57
|
+
[Fonts.TIMES_ROMAN]: notoSansRegular, // Use Noto Sans instead of Times
|
|
29
58
|
[Fonts.COURIER]: courierFont,
|
|
30
|
-
[Fonts.HELVETICA]:
|
|
31
|
-
[Fonts.HELVETICA_BOLD]:
|
|
32
|
-
[Fonts.HELVETICA_ITALIC]:
|
|
33
|
-
[Fonts.HELVETICA_BOLD_ITALIC]:
|
|
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:
|
|
40
|
-
[FormatTypes.HEADER_2]: { fontSize: 22, lineHeight: 1.35, font:
|
|
41
|
-
[FormatTypes.HEADER_3]: { fontSize: 18, lineHeight: 1.35, font:
|
|
42
|
-
[FormatTypes.HEADER_4]: { fontSize: 16, lineHeight: 1.3, font:
|
|
43
|
-
[FormatTypes.HEADER_5]: { fontSize: 14, lineHeight: 1.3, font:
|
|
44
|
-
[FormatTypes.HEADER_6]: { fontSize: 12, lineHeight: 1.3, font:
|
|
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
|
-
//
|
|
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
|
-
|
|
80
|
-
.replace(
|
|
81
|
-
.replace(
|
|
82
|
-
.replace(
|
|
83
|
-
.replace(
|
|
84
|
-
.replace(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
208
|
-
const lines =
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
224
|
-
const lines =
|
|
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
|
|
411
|
+
ensureSpace(lineHeight);
|
|
412
|
+
let startX = marginLeft;
|
|
413
|
+
|
|
228
414
|
if (align === 'center') {
|
|
229
|
-
|
|
230
|
-
x = marginLeft + (maxTextWidth() - tw) / 2
|
|
415
|
+
startX = marginLeft + (maxTextWidth() - line.width) / 2;
|
|
231
416
|
} else if (align === 'right') {
|
|
232
|
-
|
|
233
|
-
x = marginLeft + maxTextWidth() - tw
|
|
417
|
+
startX = marginLeft + maxTextWidth() - line.width;
|
|
234
418
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
295
|
-
const lines =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
333
|
-
const lines =
|
|
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
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
}
|