pi-interview 0.6.1 → 0.8.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/README.md +42 -25
- package/form/script.js +1417 -387
- package/form/styles.css +420 -17
- package/index.ts +412 -65
- package/package.json +1 -1
- package/schema.ts +81 -20
- package/server.ts +766 -90
package/schema.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
1
|
+
export interface BaseContentBlock {
|
|
2
|
+
source: string;
|
|
3
3
|
lang?: string;
|
|
4
4
|
file?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface MarkdownContentBlock extends BaseContentBlock {
|
|
9
|
+
lang: "md" | "markdown";
|
|
10
|
+
showSource?: boolean;
|
|
11
|
+
lines?: never;
|
|
12
|
+
highlights?: never;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CodeContentBlock extends BaseContentBlock {
|
|
5
16
|
lines?: string;
|
|
6
17
|
highlights?: number[];
|
|
7
|
-
|
|
18
|
+
showSource?: never;
|
|
8
19
|
}
|
|
9
20
|
|
|
21
|
+
export type ContentBlock = MarkdownContentBlock | CodeContentBlock;
|
|
22
|
+
|
|
10
23
|
export interface RichOption {
|
|
11
24
|
label: string;
|
|
12
|
-
|
|
25
|
+
content?: ContentBlock;
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
export type OptionValue = string | RichOption;
|
|
@@ -44,7 +57,7 @@ export interface Question {
|
|
|
44
57
|
conviction?: "strong" | "slight";
|
|
45
58
|
weight?: "critical" | "minor";
|
|
46
59
|
context?: string;
|
|
47
|
-
|
|
60
|
+
content?: ContentBlock;
|
|
48
61
|
media?: MediaBlock | MediaBlock[];
|
|
49
62
|
}
|
|
50
63
|
|
|
@@ -127,34 +140,71 @@ const SCHEMA_EXAMPLE = `Expected format:
|
|
|
127
140
|
]
|
|
128
141
|
}
|
|
129
142
|
Valid types: single, multi, text, image, info
|
|
130
|
-
Options: array of strings or objects with { label,
|
|
143
|
+
Options: array of strings or objects with { label, content? }`;
|
|
144
|
+
|
|
145
|
+
function isMarkdownLang(lang: unknown): lang is "md" | "markdown" {
|
|
146
|
+
if (typeof lang !== "string") return false;
|
|
147
|
+
const normalized = lang.trim().toLowerCase();
|
|
148
|
+
return normalized === "md" || normalized === "markdown";
|
|
149
|
+
}
|
|
131
150
|
|
|
132
|
-
function
|
|
151
|
+
function validateContentBlock(block: unknown, context: string): ContentBlock {
|
|
133
152
|
if (!block || typeof block !== "object") {
|
|
134
|
-
throw new Error(`${context}:
|
|
153
|
+
throw new Error(`${context}: content must be an object`);
|
|
135
154
|
}
|
|
136
155
|
const b = block as Record<string, unknown>;
|
|
137
|
-
if (typeof b.
|
|
138
|
-
throw new Error(`${context}:
|
|
156
|
+
if (typeof b.source !== "string") {
|
|
157
|
+
throw new Error(`${context}: content.source must be a string`);
|
|
139
158
|
}
|
|
140
159
|
if (b.lang !== undefined && typeof b.lang !== "string") {
|
|
141
|
-
throw new Error(`${context}:
|
|
160
|
+
throw new Error(`${context}: content.lang must be a string`);
|
|
142
161
|
}
|
|
143
162
|
if (b.file !== undefined && typeof b.file !== "string") {
|
|
144
|
-
throw new Error(`${context}:
|
|
163
|
+
throw new Error(`${context}: content.file must be a string`);
|
|
145
164
|
}
|
|
146
165
|
if (b.lines !== undefined && typeof b.lines !== "string") {
|
|
147
|
-
throw new Error(`${context}:
|
|
166
|
+
throw new Error(`${context}: content.lines must be a string`);
|
|
148
167
|
}
|
|
149
168
|
if (b.title !== undefined && typeof b.title !== "string") {
|
|
150
|
-
throw new Error(`${context}:
|
|
169
|
+
throw new Error(`${context}: content.title must be a string`);
|
|
170
|
+
}
|
|
171
|
+
if (b.showSource !== undefined && typeof b.showSource !== "boolean") {
|
|
172
|
+
throw new Error(`${context}: content.showSource must be a boolean`);
|
|
151
173
|
}
|
|
152
174
|
if (b.highlights !== undefined) {
|
|
153
175
|
if (!Array.isArray(b.highlights) || b.highlights.some((h) => typeof h !== "number")) {
|
|
154
|
-
throw new Error(`${context}:
|
|
176
|
+
throw new Error(`${context}: content.highlights must be an array of numbers`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isMarkdownLang(b.lang)) {
|
|
181
|
+
if (b.lines !== undefined) {
|
|
182
|
+
throw new Error(`${context}: content.lines is not allowed for markdown content`);
|
|
183
|
+
}
|
|
184
|
+
if (b.highlights !== undefined) {
|
|
185
|
+
throw new Error(`${context}: content.highlights is not allowed for markdown content`);
|
|
155
186
|
}
|
|
187
|
+
return {
|
|
188
|
+
source: b.source,
|
|
189
|
+
lang: b.lang.trim().toLowerCase() as "md" | "markdown",
|
|
190
|
+
file: b.file,
|
|
191
|
+
title: b.title,
|
|
192
|
+
showSource: b.showSource,
|
|
193
|
+
};
|
|
156
194
|
}
|
|
157
|
-
|
|
195
|
+
|
|
196
|
+
if (b.showSource !== undefined) {
|
|
197
|
+
throw new Error(`${context}: content.showSource is only valid when content.lang is "md" or "markdown"`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
source: b.source,
|
|
202
|
+
lang: b.lang,
|
|
203
|
+
file: b.file,
|
|
204
|
+
title: b.title,
|
|
205
|
+
lines: b.lines,
|
|
206
|
+
highlights: b.highlights as number[] | undefined,
|
|
207
|
+
};
|
|
158
208
|
}
|
|
159
209
|
|
|
160
210
|
function validateOption(option: unknown, questionId: string, index: number): OptionValue {
|
|
@@ -169,9 +219,17 @@ function validateOption(option: unknown, questionId: string, index: number): Opt
|
|
|
169
219
|
);
|
|
170
220
|
}
|
|
171
221
|
if (o.code !== undefined) {
|
|
172
|
-
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Question "${questionId}" option "${o.label}": legacy "code" is no longer supported; use "content"`
|
|
224
|
+
);
|
|
173
225
|
}
|
|
174
|
-
|
|
226
|
+
if (o.content !== undefined) {
|
|
227
|
+
return {
|
|
228
|
+
label: o.label,
|
|
229
|
+
content: validateContentBlock(o.content, `Question "${questionId}" option "${o.label}"`),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return { label: o.label };
|
|
175
233
|
}
|
|
176
234
|
throw new Error(
|
|
177
235
|
`Question "${questionId}": option at index ${index} must be a string or object with label`
|
|
@@ -240,7 +298,7 @@ function validateBasicStructure(data: unknown): QuestionsFile {
|
|
|
240
298
|
throw new Error(`Question "${q.id}": options must be a non-empty array`);
|
|
241
299
|
}
|
|
242
300
|
for (let j = 0; j < q.options.length; j++) {
|
|
243
|
-
validateOption(q.options[j], q.id as string, j);
|
|
301
|
+
q.options[j] = validateOption(q.options[j], q.id as string, j);
|
|
244
302
|
}
|
|
245
303
|
}
|
|
246
304
|
|
|
@@ -249,7 +307,10 @@ function validateBasicStructure(data: unknown): QuestionsFile {
|
|
|
249
307
|
}
|
|
250
308
|
|
|
251
309
|
if (q.codeBlock !== undefined) {
|
|
252
|
-
|
|
310
|
+
throw new Error(`Question "${q.id}": legacy "codeBlock" is no longer supported; use "content"`);
|
|
311
|
+
}
|
|
312
|
+
if (q.content !== undefined) {
|
|
313
|
+
q.content = validateContentBlock(q.content, `Question "${q.id}"`);
|
|
253
314
|
}
|
|
254
315
|
|
|
255
316
|
if (q.conviction !== undefined) {
|