h17-sspdf 0.1.0
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/LICENSE +201 -0
- package/README.md +409 -0
- package/cli.js +135 -0
- package/core/font-registry.js +47 -0
- package/core/pdf-core.js +853 -0
- package/core/plugin-chart.js +109 -0
- package/core/plugin-registry.js +46 -0
- package/core/render-document.js +931 -0
- package/core/shapes.js +354 -0
- package/core/units.js +39 -0
- package/core/validate.js +151 -0
- package/fonts/crimson-text.js +13 -0
- package/fonts/fira-code.js +9 -0
- package/fonts/ibm-plex-sans.js +13 -0
- package/fonts/inter.js +9 -0
- package/fonts/jetbrains-mono.js +13 -0
- package/fonts/lato.js +13 -0
- package/fonts/libre-baskerville.js +11 -0
- package/fonts/lora.js +13 -0
- package/fonts/merriweather.js +13 -0
- package/fonts/montserrat.js +13 -0
- package/fonts/nunito.js +13 -0
- package/fonts/open-sans.js +13 -0
- package/fonts/oswald.js +9 -0
- package/fonts/playfair-display.js +13 -0
- package/fonts/pt-sans.js +13 -0
- package/fonts/raleway.js +13 -0
- package/fonts/roboto.js +13 -0
- package/fonts/source-code-pro.js +13 -0
- package/fonts/source-serif-4.js +13 -0
- package/fonts/work-sans.js +13 -0
- package/index.js +24 -0
- package/package.json +62 -0
package/core/shapes.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* shapes.js
|
|
3
|
+
* ATS-safe visual decorators rendered as vector shapes.
|
|
4
|
+
*
|
|
5
|
+
* All functions draw shapes that are INVISIBLE to text extraction.
|
|
6
|
+
* ATS sees nothing. Humans see the decoration.
|
|
7
|
+
*
|
|
8
|
+
* Standard signature: (doc, x, y, color, size, pt) => void
|
|
9
|
+
* - doc: jsPDF instance
|
|
10
|
+
* - x, y: position (y is baseline of adjacent text)
|
|
11
|
+
* - color: [r, g, b] array
|
|
12
|
+
* - size: scale factor (default 1.0)
|
|
13
|
+
* - pt: font size in points — shape centers at half text height above baseline
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const PT_TO_MM = 0.3528;
|
|
19
|
+
|
|
20
|
+
// Vertical center: when pt is given, use half the font height; otherwise fall back to 1*s
|
|
21
|
+
function mid(y, s, pt) {
|
|
22
|
+
return pt ? y - (pt * PT_TO_MM) / 2 : y - 1 * s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================
|
|
26
|
+
// BULLET SHAPES
|
|
27
|
+
// ============================================
|
|
28
|
+
|
|
29
|
+
function arrow(doc, x, y, color, size = 1, pt = 0) {
|
|
30
|
+
const s = size;
|
|
31
|
+
const cy = mid(y, s, pt);
|
|
32
|
+
doc.setFillColor(...color);
|
|
33
|
+
doc.setDrawColor(...color);
|
|
34
|
+
doc.setLineWidth(0.3 * s);
|
|
35
|
+
doc.line(x, cy, x + 2.5 * s, cy);
|
|
36
|
+
doc.triangle(
|
|
37
|
+
x + 2.2 * s, cy - 0.8 * s,
|
|
38
|
+
x + 2.2 * s, cy + 0.8 * s,
|
|
39
|
+
x + 3.5 * s, cy,
|
|
40
|
+
'F'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function circle(doc, x, y, color, size = 1, pt = 0) {
|
|
45
|
+
const s = size;
|
|
46
|
+
const cy = mid(y, s, pt);
|
|
47
|
+
const radius = 0.6 * s;
|
|
48
|
+
doc.setFillColor(...color);
|
|
49
|
+
doc.circle(x + radius, cy, radius, 'F');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function square(doc, x, y, color, size = 1, pt = 0) {
|
|
53
|
+
const s = size;
|
|
54
|
+
const cy = mid(y, s, pt);
|
|
55
|
+
const side = 1.2 * s;
|
|
56
|
+
doc.setFillColor(...color);
|
|
57
|
+
doc.rect(x, cy - side / 2, side, side, 'F');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function diamond(doc, x, y, color, size = 1, pt = 0) {
|
|
61
|
+
const s = size;
|
|
62
|
+
const cx = x + 1.5 * s;
|
|
63
|
+
const cy = mid(y, s, pt);
|
|
64
|
+
const r = 0.9 * s;
|
|
65
|
+
doc.setFillColor(...color);
|
|
66
|
+
|
|
67
|
+
const topPoints = [
|
|
68
|
+
[cx, cy - r], [cx - r * 0.3, cy - r * 0.3],
|
|
69
|
+
[cx, cy], [cx + r * 0.3, cy - r * 0.3],
|
|
70
|
+
];
|
|
71
|
+
const rightPoints = [
|
|
72
|
+
[cx + r, cy], [cx + r * 0.3, cy - r * 0.3],
|
|
73
|
+
[cx, cy], [cx + r * 0.3, cy + r * 0.3],
|
|
74
|
+
];
|
|
75
|
+
const bottomPoints = [
|
|
76
|
+
[cx, cy + r], [cx + r * 0.3, cy + r * 0.3],
|
|
77
|
+
[cx, cy], [cx - r * 0.3, cy + r * 0.3],
|
|
78
|
+
];
|
|
79
|
+
const leftPoints = [
|
|
80
|
+
[cx - r, cy], [cx - r * 0.3, cy + r * 0.3],
|
|
81
|
+
[cx, cy], [cx - r * 0.3, cy - r * 0.3],
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
[topPoints, rightPoints, bottomPoints, leftPoints].forEach(petal => {
|
|
85
|
+
doc.triangle(petal[0][0], petal[0][1], petal[1][0], petal[1][1], petal[2][0], petal[2][1], 'F');
|
|
86
|
+
doc.triangle(petal[0][0], petal[0][1], petal[3][0], petal[3][1], petal[2][0], petal[2][1], 'F');
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function triangle(doc, x, y, color, size = 1, pt = 0) {
|
|
91
|
+
const s = size;
|
|
92
|
+
const cy = mid(y, s, pt);
|
|
93
|
+
doc.setFillColor(...color);
|
|
94
|
+
doc.triangle(
|
|
95
|
+
x, cy - 0.8 * s,
|
|
96
|
+
x, cy + 0.8 * s,
|
|
97
|
+
x + 1.5 * s, cy,
|
|
98
|
+
'F'
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function dash(doc, x, y, color, size = 1, pt = 0) {
|
|
103
|
+
const s = size;
|
|
104
|
+
const cy = mid(y, s, pt);
|
|
105
|
+
doc.setDrawColor(...color);
|
|
106
|
+
doc.setLineWidth(0.5 * s);
|
|
107
|
+
doc.line(x, cy, x + 2.5 * s, cy);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function chevron(doc, x, y, color, size = 1, pt = 0) {
|
|
111
|
+
const s = size;
|
|
112
|
+
const cy = mid(y, s, pt);
|
|
113
|
+
doc.setDrawColor(...color);
|
|
114
|
+
doc.setLineWidth(0.4 * s);
|
|
115
|
+
doc.setLineCap('round');
|
|
116
|
+
doc.line(x, cy - 0.8 * s, x + 1.2 * s, cy);
|
|
117
|
+
doc.line(x + 1.2 * s, cy, x, cy + 0.8 * s);
|
|
118
|
+
doc.setLineCap('butt');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================
|
|
122
|
+
// PREFIX DECORATORS
|
|
123
|
+
// ============================================
|
|
124
|
+
|
|
125
|
+
function doubleColon(doc, x, y, color, size = 1, pt = 0) {
|
|
126
|
+
const s = size;
|
|
127
|
+
const cy = mid(y, s, pt);
|
|
128
|
+
const dotR = 0.35 * s;
|
|
129
|
+
const gap = 1.2 * s;
|
|
130
|
+
const spread = 0.75 * s;
|
|
131
|
+
doc.setFillColor(...color);
|
|
132
|
+
doc.circle(x + dotR, cy - spread, dotR, 'F');
|
|
133
|
+
doc.circle(x + dotR, cy + spread, dotR, 'F');
|
|
134
|
+
doc.circle(x + dotR + gap, cy - spread, dotR, 'F');
|
|
135
|
+
doc.circle(x + dotR + gap, cy + spread, dotR, 'F');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function commentSlash(doc, x, y, color, size = 1, pt = 0) {
|
|
139
|
+
const s = size;
|
|
140
|
+
const cy = mid(y, s, pt);
|
|
141
|
+
const h = 1.1 * s;
|
|
142
|
+
doc.setDrawColor(...color);
|
|
143
|
+
doc.setLineWidth(0.35 * s);
|
|
144
|
+
doc.line(x + 1 * s, cy - h, x, cy + h);
|
|
145
|
+
doc.line(x + 2.2 * s, cy - h, x + 1.2 * s, cy + h);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function hashComment(doc, x, y, color, size = 1, pt = 0) {
|
|
149
|
+
const s = size;
|
|
150
|
+
const cy = mid(y, s, pt);
|
|
151
|
+
const h = 1.1 * s;
|
|
152
|
+
doc.setDrawColor(...color);
|
|
153
|
+
doc.setLineWidth(0.35 * s);
|
|
154
|
+
doc.line(x, cy - 0.45 * s, x + 2 * s, cy - 0.45 * s);
|
|
155
|
+
doc.line(x, cy + 0.45 * s, x + 2 * s, cy + 0.45 * s);
|
|
156
|
+
doc.line(x + 0.5 * s, cy - h, x + 0.3 * s, cy + h);
|
|
157
|
+
doc.line(x + 1.5 * s, cy - h, x + 1.3 * s, cy + h);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function bracketChevron(doc, x, y, color, size = 1, pt = 0) {
|
|
161
|
+
const s = size;
|
|
162
|
+
const cy = mid(y, s, pt);
|
|
163
|
+
const h = 1.1 * s;
|
|
164
|
+
doc.setDrawColor(...color);
|
|
165
|
+
doc.setLineWidth(0.35 * s);
|
|
166
|
+
doc.line(x + 0.3 * s, cy - h, x, cy - h);
|
|
167
|
+
doc.line(x, cy - h, x, cy + h);
|
|
168
|
+
doc.line(x, cy + h, x + 0.3 * s, cy + h);
|
|
169
|
+
doc.line(x + 1 * s, cy - 0.9 * s, x + 2 * s, cy);
|
|
170
|
+
doc.line(x + 2 * s, cy, x + 1 * s, cy + 0.9 * s);
|
|
171
|
+
doc.line(x + 2.7 * s, cy - h, x + 3 * s, cy - h);
|
|
172
|
+
doc.line(x + 3 * s, cy - h, x + 3 * s, cy + h);
|
|
173
|
+
doc.line(x + 3 * s, cy + h, x + 2.7 * s, cy + h);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function treeBranch(doc, x, y, color, size = 1, pt = 0) {
|
|
177
|
+
const s = size;
|
|
178
|
+
const cy = mid(y, s, pt);
|
|
179
|
+
doc.setDrawColor(...color);
|
|
180
|
+
doc.setLineWidth(0.35 * s);
|
|
181
|
+
doc.line(x + 0.3 * s, cy - 1.5 * s, x + 0.3 * s, cy + 1.5 * s);
|
|
182
|
+
doc.line(x + 0.3 * s, cy, x + 2.5 * s, cy);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function terminalPrompt(doc, x, y, color, size = 1, pt = 0) {
|
|
186
|
+
const s = size;
|
|
187
|
+
const cx = x + 0.8 * s;
|
|
188
|
+
const cy = mid(y, s, pt);
|
|
189
|
+
const w = 0.5 * s;
|
|
190
|
+
const h = 0.55 * s;
|
|
191
|
+
doc.setDrawColor(...color);
|
|
192
|
+
doc.setLineWidth(0.25 * s);
|
|
193
|
+
doc.setLineCap('round');
|
|
194
|
+
|
|
195
|
+
// $ vertical stroke
|
|
196
|
+
doc.line(cx, cy - 1.1 * s, cx, cy + 1.1 * s);
|
|
197
|
+
|
|
198
|
+
// Angular S: top-right → left → down → right → down → left
|
|
199
|
+
doc.line(cx + w, cy - h, cx - w, cy - h);
|
|
200
|
+
doc.line(cx - w, cy - h, cx - w, cy);
|
|
201
|
+
doc.line(cx - w, cy, cx + w, cy);
|
|
202
|
+
doc.line(cx + w, cy, cx + w, cy + h);
|
|
203
|
+
doc.line(cx + w, cy + h, cx - w, cy + h);
|
|
204
|
+
|
|
205
|
+
doc.setLineCap('butt');
|
|
206
|
+
|
|
207
|
+
// Chevron >
|
|
208
|
+
const ax = x + 2.5 * s;
|
|
209
|
+
doc.setLineWidth(0.4 * s);
|
|
210
|
+
doc.line(ax, cy - 0.8 * s, ax + 1 * s, cy);
|
|
211
|
+
doc.line(ax + 1 * s, cy, ax, cy + 0.8 * s);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================
|
|
215
|
+
// SYMBOL SHAPES
|
|
216
|
+
// ============================================
|
|
217
|
+
|
|
218
|
+
function checkmark(doc, x, y, color, size = 1, pt = 0) {
|
|
219
|
+
const s = size;
|
|
220
|
+
const cy = mid(y, s, pt);
|
|
221
|
+
doc.setDrawColor(...color);
|
|
222
|
+
doc.setLineWidth(0.45 * s);
|
|
223
|
+
doc.setLineCap('round');
|
|
224
|
+
doc.line(x, cy + 0.2 * s, x + 0.7 * s, cy + 1 * s);
|
|
225
|
+
doc.line(x + 0.7 * s, cy + 1 * s, x + 2 * s, cy - 1 * s);
|
|
226
|
+
doc.setLineCap('butt');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function cross(doc, x, y, color, size = 1, pt = 0) {
|
|
230
|
+
const s = size;
|
|
231
|
+
const cy = mid(y, s, pt);
|
|
232
|
+
doc.setDrawColor(...color);
|
|
233
|
+
doc.setLineWidth(0.45 * s);
|
|
234
|
+
doc.setLineCap('round');
|
|
235
|
+
doc.line(x, cy - 1 * s, x + 1.8 * s, cy + 1 * s);
|
|
236
|
+
doc.line(x + 1.8 * s, cy - 1 * s, x, cy + 1 * s);
|
|
237
|
+
doc.setLineCap('butt');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function star(doc, x, y, color, size = 1, pt = 0) {
|
|
241
|
+
const s = size;
|
|
242
|
+
const cx = x + 1.2 * s;
|
|
243
|
+
const cy = mid(y, s, pt);
|
|
244
|
+
const outer = 1.2 * s;
|
|
245
|
+
const inner = 0.5 * s;
|
|
246
|
+
doc.setFillColor(...color);
|
|
247
|
+
|
|
248
|
+
const points = [];
|
|
249
|
+
for (let i = 0; i < 5; i++) {
|
|
250
|
+
const aOuter = (i * 72 - 90) * Math.PI / 180;
|
|
251
|
+
const aInner = ((i * 72) + 36 - 90) * Math.PI / 180;
|
|
252
|
+
points.push([cx + Math.cos(aOuter) * outer, cy + Math.sin(aOuter) * outer]);
|
|
253
|
+
points.push([cx + Math.cos(aInner) * inner, cy + Math.sin(aInner) * inner]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < points.length; i++) {
|
|
257
|
+
const a = points[i];
|
|
258
|
+
const b = points[(i + 1) % points.length];
|
|
259
|
+
doc.triangle(cx, cy, a[0], a[1], b[0], b[1], 'F');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function plus(doc, x, y, color, size = 1, pt = 0) {
|
|
264
|
+
const s = size;
|
|
265
|
+
const cy = mid(y, s, pt);
|
|
266
|
+
doc.setDrawColor(...color);
|
|
267
|
+
doc.setLineWidth(0.45 * s);
|
|
268
|
+
doc.line(x + 0.9 * s, cy - 1 * s, x + 0.9 * s, cy + 1 * s);
|
|
269
|
+
doc.line(x, cy, x + 1.8 * s, cy);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function minus(doc, x, y, color, size = 1, pt = 0) {
|
|
273
|
+
const s = size;
|
|
274
|
+
const cy = mid(y, s, pt);
|
|
275
|
+
doc.setDrawColor(...color);
|
|
276
|
+
doc.setLineWidth(0.45 * s);
|
|
277
|
+
doc.line(x, cy, x + 1.8 * s, cy);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function warning(doc, x, y, color, size = 1, pt = 0) {
|
|
281
|
+
const s = size;
|
|
282
|
+
const cy = mid(y, s, pt);
|
|
283
|
+
doc.setFillColor(...color);
|
|
284
|
+
doc.triangle(
|
|
285
|
+
x + 1.2 * s, cy - 1.15 * s,
|
|
286
|
+
x, cy + 1.15 * s,
|
|
287
|
+
x + 2.4 * s, cy + 1.15 * s,
|
|
288
|
+
'F'
|
|
289
|
+
);
|
|
290
|
+
doc.setFillColor(255, 255, 255);
|
|
291
|
+
doc.rect(x + 1 * s, cy - 0.45 * s, 0.4 * s, 0.8 * s, 'F');
|
|
292
|
+
doc.circle(x + 1.2 * s, cy + 0.75 * s, 0.2 * s, 'F');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function infoCircle(doc, x, y, color, size = 1, pt = 0) {
|
|
296
|
+
const s = size;
|
|
297
|
+
const cx = x + 1.1 * s;
|
|
298
|
+
const cy = mid(y, s, pt);
|
|
299
|
+
doc.setFillColor(...color);
|
|
300
|
+
doc.circle(cx, cy, 1.1 * s, 'F');
|
|
301
|
+
doc.setFillColor(255, 255, 255);
|
|
302
|
+
doc.circle(cx, cy - 0.55 * s, 0.18 * s, 'F');
|
|
303
|
+
doc.rect(cx - 0.12 * s, cy - 0.25 * s, 0.24 * s, 0.7 * s, 'F');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ============================================
|
|
307
|
+
// SHAPE WIDTHS (mm) for text offset
|
|
308
|
+
// ============================================
|
|
309
|
+
|
|
310
|
+
const shapeWidths = {
|
|
311
|
+
arrow: 4,
|
|
312
|
+
circle: 2,
|
|
313
|
+
square: 2,
|
|
314
|
+
diamond: 3.5,
|
|
315
|
+
triangle: 2.5,
|
|
316
|
+
dash: 3.5,
|
|
317
|
+
chevron: 2,
|
|
318
|
+
doubleColon: 3.5,
|
|
319
|
+
commentSlash: 3,
|
|
320
|
+
hashComment: 2.5,
|
|
321
|
+
bracketChevron: 4.5,
|
|
322
|
+
treeBranch: 3.5,
|
|
323
|
+
terminalPrompt: 5,
|
|
324
|
+
checkmark: 2.5,
|
|
325
|
+
cross: 2.5,
|
|
326
|
+
star: 3,
|
|
327
|
+
plus: 2.5,
|
|
328
|
+
minus: 2.5,
|
|
329
|
+
warning: 3,
|
|
330
|
+
infoCircle: 2.8,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
function getShapeWidth(shapeName, size = 1) {
|
|
334
|
+
return (shapeWidths[shapeName] || 3) * size;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============================================
|
|
338
|
+
// REGISTRY
|
|
339
|
+
// ============================================
|
|
340
|
+
|
|
341
|
+
const shapes = {
|
|
342
|
+
arrow, circle, square, diamond, triangle, dash, chevron,
|
|
343
|
+
doubleColon, commentSlash, hashComment, bracketChevron, treeBranch, terminalPrompt,
|
|
344
|
+
checkmark, cross, star, plus, minus, warning, infoCircle,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
function renderShape(name, doc, x, y, color, size = 1, pt = 0) {
|
|
348
|
+
const shapeFn = shapes[name];
|
|
349
|
+
if (shapeFn) {
|
|
350
|
+
shapeFn(doc, x, y, color, size, pt);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
module.exports = { shapes, shapeWidths, getShapeWidth, renderShape, PT_TO_MM };
|
package/core/units.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const PT_TO_MM = 25.4 / 72;
|
|
2
|
+
const PX_TO_MM = 25.4 / 96;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert CSS px units to millimeters.
|
|
6
|
+
* @param {number} value
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
function pxToMm(value) {
|
|
10
|
+
return (Number(value) || 0) * PX_TO_MM;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert typographic pt units to millimeters.
|
|
15
|
+
* @param {number} value
|
|
16
|
+
* @returns {number}
|
|
17
|
+
*/
|
|
18
|
+
function ptToMm(value) {
|
|
19
|
+
return (Number(value) || 0) * PT_TO_MM;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert font size + line-height multiplier to line height in millimeters.
|
|
24
|
+
* @param {number} fontSizePt
|
|
25
|
+
* @param {number} lineHeightMultiplier
|
|
26
|
+
* @returns {number}
|
|
27
|
+
*/
|
|
28
|
+
function resolveLineHeightMm(fontSizePt, lineHeightMultiplier) {
|
|
29
|
+
const multiplier = Number(lineHeightMultiplier) || 1.2;
|
|
30
|
+
return ptToMm(Number(fontSizePt) || 10) * multiplier;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
PT_TO_MM,
|
|
35
|
+
PX_TO_MM,
|
|
36
|
+
pxToMm,
|
|
37
|
+
ptToMm,
|
|
38
|
+
resolveLineHeightMm,
|
|
39
|
+
};
|
package/core/validate.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const { hasPlugin } = require("./plugin-registry");
|
|
2
|
+
|
|
3
|
+
const OPERATION_TYPES = new Set([
|
|
4
|
+
"text", "row", "bullet", "divider", "spacer", "hiddenText",
|
|
5
|
+
"block", "group", "section", "quote",
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function validateSource(source) {
|
|
9
|
+
if (Array.isArray(source)) {
|
|
10
|
+
source.forEach((op, i) => validateOperation(op, `source[${i}]`));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (!source || typeof source !== "object") {
|
|
14
|
+
throw new Error("Source must be an object or array");
|
|
15
|
+
}
|
|
16
|
+
const ops = source.operations || source.content || source.items || source.sections || source.children;
|
|
17
|
+
if (Array.isArray(ops)) {
|
|
18
|
+
ops.forEach((op, i) => validateOperation(op, `source[${i}]`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw new Error("Source must contain operations, content, items, sections, or children array");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateOperation(op, path) {
|
|
25
|
+
if (!op || typeof op !== "object") {
|
|
26
|
+
throw new Error(`${path}: operation must be an object`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Sugar: { label, text/value }
|
|
30
|
+
if (!op.type && op.label && (op.text !== undefined || op.value !== undefined)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!op.type) {
|
|
35
|
+
throw new Error(`${path}: missing type`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const type = op.type;
|
|
39
|
+
|
|
40
|
+
if (type === "text") {
|
|
41
|
+
if (!op.label) throw new Error(`${path}: text requires label`);
|
|
42
|
+
if (op.text === undefined) throw new Error(`${path}: text requires text`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (type === "row") {
|
|
47
|
+
if (!op.leftLabel) throw new Error(`${path}: row requires leftLabel`);
|
|
48
|
+
if (!op.rightLabel) throw new Error(`${path}: row requires rightLabel`);
|
|
49
|
+
if (op.leftText === undefined) throw new Error(`${path}: row requires leftText`);
|
|
50
|
+
if (op.rightText === undefined) throw new Error(`${path}: row requires rightText`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (type === "bullet") {
|
|
55
|
+
if (!op.label) throw new Error(`${path}: bullet requires label`);
|
|
56
|
+
if (op.text === undefined && !op.items && !op.bullets) {
|
|
57
|
+
throw new Error(`${path}: bullet requires text, items, or bullets`);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (type === "divider") {
|
|
63
|
+
if (!op.label) throw new Error(`${path}: divider requires label`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (type === "spacer") {
|
|
68
|
+
if (op.mm === undefined && op.px === undefined && !op.label) {
|
|
69
|
+
throw new Error(`${path}: spacer requires mm, px, or label`);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (type === "hiddenText") {
|
|
75
|
+
if (!op.label) throw new Error(`${path}: hiddenText requires label`);
|
|
76
|
+
if (op.text === undefined) throw new Error(`${path}: hiddenText requires text`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (type === "quote") {
|
|
81
|
+
if (!op.label) throw new Error(`${path}: quote requires label`);
|
|
82
|
+
if (op.text === undefined) throw new Error(`${path}: quote requires text`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (type === "block" || type === "group" || type === "section") {
|
|
87
|
+
const children = op.children || op.content || op.items || op.sections;
|
|
88
|
+
if (!Array.isArray(children)) {
|
|
89
|
+
throw new Error(`${path}: ${type} requires children array`);
|
|
90
|
+
}
|
|
91
|
+
children.forEach((child, i) => validateOperation(child, `${path}.children[${i}]`));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (hasPlugin(type)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!OPERATION_TYPES.has(type)) {
|
|
100
|
+
throw new Error(`${path}: unknown operation type "${type}"`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateTheme(theme) {
|
|
105
|
+
if (!theme || typeof theme !== "object") {
|
|
106
|
+
throw new Error("Theme must be an object");
|
|
107
|
+
}
|
|
108
|
+
if (!theme.labels || typeof theme.labels !== "object") {
|
|
109
|
+
throw new Error("Theme must have a labels object");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateSourceAgainstTheme(source, theme) {
|
|
114
|
+
validateTheme(theme);
|
|
115
|
+
const labels = new Set(Object.keys(theme.labels));
|
|
116
|
+
const missing = [];
|
|
117
|
+
collectLabels(source, missing, labels);
|
|
118
|
+
if (missing.length > 0) {
|
|
119
|
+
throw new Error(`Missing theme labels: ${[...new Set(missing)].join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function collectLabels(node, missing, themeLabels) {
|
|
124
|
+
if (Array.isArray(node)) {
|
|
125
|
+
node.forEach((n) => collectLabels(n, missing, themeLabels));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!node || typeof node !== "object") return;
|
|
129
|
+
|
|
130
|
+
const ops = node.operations || node.content || node.items || node.sections || node.children;
|
|
131
|
+
if (Array.isArray(ops)) {
|
|
132
|
+
ops.forEach((n) => collectLabels(n, missing, themeLabels));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (node.label && !themeLabels.has(node.label)) missing.push(node.label);
|
|
136
|
+
if (node.leftLabel && !themeLabels.has(node.leftLabel)) missing.push(node.leftLabel);
|
|
137
|
+
if (node.rightLabel && !themeLabels.has(node.rightLabel)) missing.push(node.rightLabel);
|
|
138
|
+
if (node.markerLabel && !themeLabels.has(node.markerLabel)) missing.push(node.markerLabel);
|
|
139
|
+
if (node.spaceAfterLabel && !themeLabels.has(node.spaceAfterLabel)) missing.push(node.spaceAfterLabel);
|
|
140
|
+
|
|
141
|
+
if (node.type === "quote" && node.attribution) {
|
|
142
|
+
const attrLabel = node.attributionLabel || (node.label + ".attribution");
|
|
143
|
+
if (!themeLabels.has(attrLabel)) missing.push(attrLabel);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
validateSource,
|
|
149
|
+
validateTheme,
|
|
150
|
+
validateSourceAgainstTheme,
|
|
151
|
+
};
|