Skip to content

Commit c00c9fb

Browse files
Merge pull request #127 from Watts-Lab/fall-markdown
Fall markdown
2 parents 32f880b + 7d4a972 commit c00c9fb

16 files changed

Lines changed: 513 additions & 218 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
"scripts": {
9595
"build:css": "npx windicss deliberation-empirica/client/src/baseStyles.css -o src/views/styles.css --config deliberation-empirica/client/windi.config.cjs && npx tailwindcss -i src/views/index.css -o src/views/playerStyles.css --config src/views/tailwind.config.js && npx tailwindcss -i src/views/globals.css -o src/views/layout.css --config src/views/tailwind.config.js",
9696
"prepare-submodule": "git submodule update --init --recursive",
97-
"copy-validators": "cp ./deliberation-empirica/server/src/preFlight/validateTreatmentFile.ts ./src/zod-validators && cp ./deliberation-empirica/server/src/preFlight/validatePromptFile.ts ./src/zod-validators && cp ./deliberation-empirica/server/src/preFlight/validateDlConfig.ts ./src/zod-validators && cp ./deliberation-empirica/server/src/preFlight/validateBatchConfig.ts ./src/zod-validators",
97+
"copy-validators": "cp ./deliberation-empirica/server/src/preFlight/validateTreatmentFile.ts ./src/zod-validators && cp ./deliberation-empirica/server/src/preFlight/validatePromptFile.ts ./src/zod-validators && cp ./deliberation-empirica/server/src/preFlight/validateDlConfig.ts ./src/zod-validators && cp ./deliberation-empirica/server/src/preFlight/validateBatchConfig.ts ./src/zod-validators && cp ./deliberation-empirica/server/src/preFlight/fillTemplates.ts ./src && sed -i 's|from \"./validateTreatmentFile\"|from \"./zod-validators/validateTreatmentFile\"|' ./src/fillTemplates.ts",
9898
"prepare-web": "npm run prepare-submodule && npm run copy-validators && npm run build:css",
9999
"compile-web": "npm run prepare-web && tsc -p tsconfig.json && npm run lint && node esbuild.js",
100100
"package-web": "npm run prepare-web && npm run check-types && npm run lint && node esbuild.js --production",

src/fillTemplates.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,4 @@ function applyTruncation(s: string, maxLines: number): { text: string; truncated
346346
slice.push("# … truncated …");
347347
return { text: slice.join("\n"), truncated: true };
348348
}
349+
//s

src/parsers/parseMarkdown.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,20 @@ export function parseMarkdown(document: vscode.TextDocument) {
156156
case "multipleChoice": {
157157
let { text, index } = getIndex(document, 3);
158158
const lineNum = (document.positionAt(index).line) + 1;
159+
if (!response || /^\s*$/.test(response)) {
160+
const diagnosticRange = new vscode.Range(
161+
document.positionAt(index),
162+
document.positionAt(text.length - 1)
163+
);
164+
const issue = "Response should contain at least one choice for type multiple choice";
165+
diagnostics.push(
166+
new vscode.Diagnostic(
167+
diagnosticRange,
168+
issue,
169+
vscode.DiagnosticSeverity.Warning
170+
)
171+
);
172+
}
159173
for (let i = lineNum; i < document.lineCount; i++) {
160174
const str = document.lineAt(i).text;
161175
// console.log(str);
@@ -181,6 +195,20 @@ export function parseMarkdown(document: vscode.TextDocument) {
181195
case "openResponse": {
182196
let { text, index } = getIndex(document, 3);
183197
const lineNum = (document.positionAt(index).line) + 1;
198+
if (!response || /^\s*$/.test(response)) {
199+
const diagnosticRange = new vscode.Range(
200+
document.positionAt(index),
201+
document.positionAt(text.length - 1)
202+
);
203+
const issue = "Response should contain text for type open response";
204+
diagnostics.push(
205+
new vscode.Diagnostic(
206+
diagnosticRange,
207+
issue,
208+
vscode.DiagnosticSeverity.Warning
209+
)
210+
);
211+
}
184212
for (let i = lineNum; i < document.lineCount; i++) {
185213
const str = document.lineAt(i).text;
186214
if (!(/^\s*$/.test(str)) && str.substring(0, 2) !== "> ") {
@@ -200,6 +228,42 @@ export function parseMarkdown(document: vscode.TextDocument) {
200228
}
201229
break;
202230
}
231+
case "listSorter": {
232+
let { text, index } = getIndex(document, 3);
233+
const lineNum = (document.positionAt(index).line) + 1;
234+
if (!response || /^\s*$/.test(response)) {
235+
const diagnosticRange = new vscode.Range(
236+
document.positionAt(index),
237+
document.positionAt(text.length - 1)
238+
);
239+
const issue = "Response should contain sortable choices for type list sorter";
240+
diagnostics.push(
241+
new vscode.Diagnostic(
242+
diagnosticRange,
243+
issue,
244+
vscode.DiagnosticSeverity.Warning
245+
)
246+
);
247+
}
248+
for (let i = lineNum; i < document.lineCount; i++) {
249+
const str = document.lineAt(i).text;
250+
if (!(/^\s*$/.test(str)) && str.substring(0, 2) !== "> ") {
251+
const diagnosticRange = new vscode.Range(
252+
new vscode.Position(i, 0),
253+
new vscode.Position(i, str.length)
254+
);
255+
const issue = `Response at line ${i + 1} should start with "> " (for list sorter)`;
256+
diagnostics.push(
257+
new vscode.Diagnostic(
258+
diagnosticRange,
259+
issue,
260+
vscode.DiagnosticSeverity.Warning
261+
)
262+
);
263+
}
264+
}
265+
break;
266+
}
203267
default: {
204268
// console.log("Type: " + type);
205269
break;

src/parsers/parseYaml.ts

Lines changed: 120 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
treatmentFileSchema,
88
TreatmentFileType,
99
} from "../zod-validators/validateTreatmentFile";
10+
import { detectPromptMarkdown } from '../detectFile';
1011
import { handleError, offsetToPosition, findPositionFromPath } from "../errorPosition";
1112
import { parse } from 'path';
1213
import { off } from 'process';
@@ -668,6 +669,124 @@ export async function parseYaml(document: vscode.TextDocument) {
668669

669670
runReferenceStageOrderChecks(document, parsedData, parsedData.toJS(), diagnostics);
670671

672+
async function validateReferencedPromptFiles(
673+
document: vscode.TextDocument,
674+
parsedDoc: YAML.Document.Parsed,
675+
root: any,
676+
diagnostics: vscode.Diagnostic[],
677+
fileExistsInWorkspace: (relativePath: string) => Promise<{ uri: vscode.Uri; exists: boolean }>
678+
) {
679+
try {
680+
// Collect all prompt file references from the YAML
681+
const promptFileRefs: Array<{
682+
path: (string | number)[];
683+
file: string;
684+
}> = [];
685+
686+
function walkYaml(node: any, path: (string | number)[] = []) {
687+
if (!node) return;
688+
if (Array.isArray(node)) {
689+
node.forEach((item, idx) => walkYaml(item, [...path, idx]));
690+
} else if (typeof node === 'object') {
691+
// Check if this is a prompt element with a file reference
692+
if (node.type === 'prompt' && typeof node.file === 'string') {
693+
promptFileRefs.push({
694+
path: [...path, 'file'],
695+
file: node.file
696+
});
697+
}
698+
Object.entries(node)?.forEach(([key, value]) => {
699+
walkYaml(value, [...path, key]);
700+
});
701+
}
702+
}
703+
704+
walkYaml(root);
705+
706+
// Validate each referenced prompt file
707+
for (const ref of promptFileRefs) {
708+
try {
709+
const fileData = await fileExistsInWorkspace(ref.file);
710+
711+
if (!fileData.exists) {
712+
// File doesn't exist - already handled by existing validation
713+
continue;
714+
}
715+
716+
// Open the document to get its URI
717+
let promptDoc: vscode.TextDocument;
718+
try {
719+
promptDoc = await vscode.workspace.openTextDocument(fileData.uri);
720+
} catch (err) {
721+
console.error(`Failed to open prompt file: ${ref.file}`, err);
722+
continue;
723+
}
724+
725+
const isPromptMarkdown = detectPromptMarkdown(promptDoc);
726+
const pos = findPositionFromPath(ref.path, parsedDoc, document);
727+
728+
if (!isPromptMarkdown) {
729+
if (pos) {
730+
diagnostics.push(new vscode.Diagnostic(
731+
new vscode.Range(pos.start, pos.end ?? pos.start),
732+
`File "${ref.file}" is not a valid prompt markdown file. Prompt markdown files must have YAML metadata with 'type' and 'name' fields between '---' separators.`,
733+
vscode.DiagnosticSeverity.Warning
734+
));
735+
}
736+
continue;
737+
}
738+
739+
// Check if the prompt file has any diagnostics
740+
const promptDiagnostics = vscode.languages.getDiagnostics(promptDoc.uri);
741+
742+
if (promptDiagnostics && promptDiagnostics.length > 0) {
743+
// Find the position of the 'file:' line in the treatments YAML
744+
const pos = findPositionFromPath(ref.path, parsedDoc, document);
745+
746+
if (pos) {
747+
// Count errors vs warnings
748+
const errorCount = promptDiagnostics.filter(
749+
d => d.severity === vscode.DiagnosticSeverity.Error
750+
).length;
751+
const warningCount = promptDiagnostics.filter(
752+
d => d.severity === vscode.DiagnosticSeverity.Warning
753+
).length;
754+
755+
let message = `Referenced prompt file "${ref.file}" has validation issues: `;
756+
const issues: string[] = [];
757+
if (errorCount > 0) issues.push(`${errorCount} error(s)`);
758+
if (warningCount > 0) issues.push(`${warningCount} warning(s)`);
759+
message += issues.join(', ');
760+
761+
// Use Error severity if the prompt has errors, Warning otherwise
762+
const severity = errorCount > 0
763+
? vscode.DiagnosticSeverity.Error
764+
: vscode.DiagnosticSeverity.Warning;
765+
766+
diagnostics.push(new vscode.Diagnostic(
767+
new vscode.Range(pos.start, pos.end ?? pos.start),
768+
message,
769+
severity
770+
));
771+
}
772+
}
773+
} catch (err) {
774+
console.error(`Error validating prompt file ${ref.file}:`, err);
775+
}
776+
}
777+
} catch (err) {
778+
console.error('Error in validateReferencedPromptFiles:', err);
779+
}
780+
}
781+
782+
await validateReferencedPromptFiles(
783+
document,
784+
parsedData,
785+
parsedData.toJS(),
786+
diagnostics,
787+
fileExistsInWorkspace
788+
);
789+
671790
const missingFiles = asyncValidateFilesToIssues(
672791
parsedData.toJS() as TreatmentFileType
673792
).then((issues: ZodIssue[]) => {
@@ -708,47 +827,13 @@ export async function parseYaml(document: vscode.TextDocument) {
708827
if (Array.isArray(node)) {
709828
node.forEach((item, idx) => walkYaml(item, [...path, idx]));
710829
} else if (typeof node === 'object') {
711-
// Only track elements of type "survey" for now
712-
// if (node.type === 'survey' && typeof node.name === 'string') {
713-
// // Find the path to the 'name' property
714-
// const namePath = [...path, 'name'];
715-
// const range = findPositionFromPath(namePath, parsedData, document);
716-
// // Default to line 1 if range is not found
717-
// const line = range ? range.start.line + 1 : 1;
718-
// if (!referenceTypeMap['survey']) {
719-
// referenceTypeMap['survey'] = [];
720-
// }
721-
// referenceTypeMap['survey'].push({ name: node.name, line });
722-
// }
723-
// if (node.type === 'prompt' && typeof node.name === 'string') {
724-
// // Find the path to the 'name' property
725-
// const namePath = [...path, 'name'];
726-
// const range = findPositionFromPath(namePath, parsedData, document);
727-
// // Default to line 1 if range is not found
728-
// const line = range ? range.start.line + 1 : 1;
729-
// if (!referenceTypeMap['prompt']) {
730-
// referenceTypeMap['prompt'] = [];
731-
// }
732-
// referenceTypeMap['prompt'].push({ name: node.name, line });
733-
// }
734-
// if (node.type === 'submitButton' && typeof node.name === 'string') {
735-
// // Find the path to the 'name' property
736-
// const namePath = [...path, 'name'];
737-
// const range = findPositionFromPath(namePath, parsedData, document);
738-
// // Default to line 1 if range is not found
739-
// const line = range ? range.start.line + 1 : 1;
740-
// if (!referenceTypeMap['submitButton']) {
741-
// referenceTypeMap['submitButton'] = [];
742-
// }
743-
// referenceTypeMap['submitButton'].push({ name: node.name, line });
744-
// }
745830
// Track references for any type (future extensibility)
746831
if (typeof node.reference === 'string' && node.reference.includes('.')) {
747832
const refPath = [...path, 'reference'];
748833
const range = findPositionFromPath(refPath, parsedData, document);
749834
const line = range ? range.start.line + 1 : 1;
750835
const [type] = node.reference.split('.', 1);
751-
referenceChecks.push({ type, line, fullRef: node.reference });
836+
referenceChecks.push({ type, line, fullRef: node.reference});
752837
}
753838
Object.entries(node)?.forEach(([key, value]) => {
754839
walkYaml(value, [...path, key]);
@@ -761,22 +846,6 @@ export async function parseYaml(document: vscode.TextDocument) {
761846

762847
// Now check references for each type
763848
referenceChecks.forEach(({ type, line, fullRef }) => {
764-
// if (type === 'survey') {
765-
// // Only 'survey' type is currently supported
766-
// const name = fullRef.split('.', 2)[1];
767-
// if (!(referenceTypeMap[type]?.some(entry => entry.name === name && entry.line < line))) {
768-
// diagnostics.push(
769-
// new vscode.Diagnostic(
770-
// new vscode.Range(
771-
// new vscode.Position(line, 0),
772-
// new vscode.Position(line, 100)
773-
// ),
774-
// `Reference "${fullRef}" does not match any previously defined ${type} element name.`,
775-
// vscode.DiagnosticSeverity.Warning
776-
// )
777-
// );
778-
// }
779-
// }
780849
if (type === 'discussion') {
781850
// Only 'discussion' type is currently supported
782851
const name = fullRef.split('.', 2)[1];
@@ -812,39 +881,6 @@ export async function parseYaml(document: vscode.TextDocument) {
812881
);
813882
}
814883
}
815-
// if (type === 'prompt') {
816-
// // Only 'prompt' type is currently supported
817-
// const name = fullRef.split('.', 2)[1];
818-
// if (!(referenceTypeMap[type]?.some(entry => entry.name === name && entry.line < line))) {
819-
// diagnostics.push(
820-
// new vscode.Diagnostic(
821-
// new vscode.Range(
822-
// new vscode.Position(line, 0),
823-
// new vscode.Position(line, 100)
824-
// ),
825-
// `Reference "${fullRef}" does not match any previously defined ${type} element name.`,
826-
// vscode.DiagnosticSeverity.Warning
827-
// )
828-
// );
829-
// }
830-
// }
831-
// if (type === 'submitButton') {
832-
// // Only 'submitButton' type is currently supported
833-
// const name = fullRef.split('.', 2)[1];
834-
// if (!(referenceTypeMap[type]?.some(entry => entry.name === name && entry.line < line))) {
835-
// diagnostics.push(
836-
// new vscode.Diagnostic(
837-
// new vscode.Range(
838-
// new vscode.Position(line, 0),
839-
// new vscode
840-
// .Position(line, 100)
841-
// ),
842-
// `Reference "${fullRef}" does not match any previously defined ${type} element name.`,
843-
// vscode.DiagnosticSeverity.Warning
844-
// )
845-
// );
846-
// }
847-
// }
848884
});
849885

850886

0 commit comments

Comments
 (0)