diff --git a/assessml.d.ts b/assessml.d.ts index 8d3b8b0..4366f3f 100644 --- a/assessml.d.ts +++ b/assessml.d.ts @@ -3,8 +3,8 @@ export interface AST { readonly ast: ASTObject[]; } -export type ASTObject = Content | Variable | Input | Essay | Check | Radio | Drag | Drop | Image | Solution | Code | Graph | Shuffle; -export type ASTObjectType = 'VARIABLE' | 'INPUT' | 'ESSAY' | 'CONTENT' | 'CHECK' | 'RADIO' | 'DRAG' | 'DROP' | 'IMAGE' | 'SOLUTION' | 'CODE' | 'SHUFFLE' | 'GRAPH'; +export type ASTObject = Content | Variable | Input | Essay | Check | Radio | Drag | Drop | Image | Solution | Code | Graph | Shuffle | Markdown; +export type ASTObjectType = 'VARIABLE' | 'INPUT' | 'ESSAY' | 'CONTENT' | 'CHECK' | 'RADIO' | 'DRAG' | 'DROP' | 'IMAGE' | 'SOLUTION' | 'CODE' | 'SHUFFLE' | 'GRAPH' | 'MARKDOWN'; export interface Check { readonly type: 'CHECK'; @@ -82,6 +82,12 @@ export interface Shuffle { readonly shuffledIndeces: number[]; } +export interface Markdown { + readonly type: 'MARKDOWN'; + readonly varName: string; + readonly content: ASTObject[]; +} + export interface BuildASTResult { readonly ast: AST; readonly numInputs: number; @@ -93,4 +99,5 @@ export interface BuildASTResult { readonly numSolutions: number; readonly numCodes: number; readonly numShuffles: number; + readonly numMarkdowns: number; } diff --git a/assessml.ts b/assessml.ts index 9f63a00..e5dbd2c 100644 --- a/assessml.ts +++ b/assessml.ts @@ -15,6 +15,7 @@ import { Code, Graph, Shuffle, + Markdown, BuildASTResult } from './assessml.d'; @@ -129,6 +130,17 @@ export function compileToHTML(source: AST | string, generateVarValue: (varName: }; } + if (astObject.type === 'MARKDOWN') { + return { + htmlString: `${result.htmlString}
`, + radioGroupName: result.radioGroupName, + radioGroupNumber: result.radioGroupNumber + }; + } + if (astObject.type === 'GRAPH') { return { htmlString: `${result.htmlString}`, @@ -197,7 +209,8 @@ export function compileToAssessML(source: AST | string, generateVarValue: (varNa astObject.type === 'SHUFFLE' || astObject.type === 'SOLUTION' || astObject.type === 'DRAG' || - astObject.type === 'DROP' + astObject.type === 'DROP' || + astObject.type === 'MARKDOWN' ) { return `${result}[${astObject.varName}]${compileToAssessML({ type: 'AST', @@ -213,10 +226,10 @@ export function parse(source: string, generateVarValue: (varName: string) => num return buildAST(source, { type: 'AST', ast: [] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, 0, 0, 0, 0, 0, 0, 0, 0, 0).ast; + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0).ast; } -function buildAST(source: string, ast: AST, generateVarValue: (varName: string) => number | string, generateImageSrc: (varName: string) => string, generateGraphEquations: (varName: string) => string[], generateShuffledIndeces: (varName: string) => number[], numInputs: number, numEssays: number, numChecks: number, numRadios: number, numDrags: number, numDrops: number, numSolutions: number, numCodes: number, numShuffles: number): BuildASTResult { +function buildAST(source: string, ast: AST, generateVarValue: (varName: string) => number | string, generateImageSrc: (varName: string) => string, generateGraphEquations: (varName: string) => string[], generateShuffledIndeces: (varName: string) => number[], numInputs: number, numEssays: number, numChecks: number, numRadios: number, numDrags: number, numDrops: number, numSolutions: number, numCodes: number, numShuffles: number, numMarkdowns: number): BuildASTResult { const variableRegex: RegExp = /\[var((.|\n|\r)+?)\]/; const inputRegex: RegExp = /\[input((.|\n|\r)+?)\]/; const essayRegex: RegExp = /\[essay((.|\n|\r)+?)\]/; @@ -229,9 +242,11 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) const shuffleRegex: RegExp = /\[shuffle((.|\n|\r)+?)\]((.|\n|\r)*?)\[shuffle\1\]/; const solutionStartRegex: RegExp = /\[solution((.|\n|\r)+?)\]/; const solutionRegex: RegExp = /\[solution((.|\n|\r)+?)\]((.|\n|\r)*?)\[solution\1\]/; + const markdownStartRegex: RegExp = /\[markdown((.|\n|\r)+?)\]/; + const markdownRegex: RegExp = /\[markdown((.|\n|\r)+?)\]((.|\n|\r)*?)\[markdown\1\]/; const imageRegex: RegExp = /\[img((.|\n|\r)+?)\]/; const graphRegex: RegExp = /\[graph((.|\n|\r)+?)\]/; - const contentRegex: RegExp = new RegExp(`((.|\n|\r)+?)((${variableRegex.source}|${inputRegex.source}|${essayRegex.source}|${codeRegex.source}|${checkRegex.source}|${checkStartRegex.source}|${radioRegex.source}|${radioStartRegex.source}|${imageRegex.source}|${graphRegex.source}|${solutionRegex.source}|${solutionStartRegex.source}|${shuffleRegex.source}|${shuffleStartRegex.source})|$)`); + const contentRegex: RegExp = new RegExp(`((.|\n|\r)+?)((${variableRegex.source}|${inputRegex.source}|${essayRegex.source}|${codeRegex.source}|${checkRegex.source}|${checkStartRegex.source}|${radioRegex.source}|${radioStartRegex.source}|${imageRegex.source}|${graphRegex.source}|${solutionRegex.source}|${solutionStartRegex.source}|${markdownRegex.source}|${markdownStartRegex.source}|${shuffleRegex.source}|${shuffleStartRegex.source})|$)`); if (source.search(variableRegex) === 0) { const match = source.match(variableRegex) || []; @@ -252,7 +267,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, variable] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns); } if (source.search(inputRegex) === 0) { @@ -269,7 +284,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, input] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs + 1, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs + 1, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns); } if (source.search(essayRegex) === 0) { @@ -286,7 +301,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, essay] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays + 1, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays + 1, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns); } if (source.search(codeRegex) === 0) { @@ -303,7 +318,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, code] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes + 1, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes + 1, numShuffles, numMarkdowns); } if (source.search(checkRegex) === 0) { @@ -316,7 +331,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) const contentAST: BuildASTResult = buildAST(insideContent, { type: 'AST', ast: [] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks + 1, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks + 1, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns); const check: Check = { type: 'CHECK', varName, @@ -326,7 +341,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, check] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles, contentAST.numMarkdowns); } if (source.search(radioRegex) === 0) { @@ -339,7 +354,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) const contentAST: BuildASTResult = buildAST(insideContent, { type: 'AST', ast: [] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios + 1, numDrags, numDrops, numSolutions, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios + 1, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns); const radio: Radio = { type: 'RADIO', varName, @@ -349,7 +364,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, radio] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles, contentAST.numMarkdowns); } if (source.search(solutionRegex) === 0) { @@ -362,7 +377,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) const contentAST: BuildASTResult = buildAST(insideContent, { type: 'AST', ast: [] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions + 1, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions + 1, numCodes, numShuffles, numMarkdowns); const solution: Solution = { type: 'SOLUTION', varName, @@ -372,7 +387,30 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, solution] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles, contentAST.numMarkdowns); + } + + if (source.search(markdownRegex) === 0) { + const match = source.match(markdownRegex) || []; + const matchedContent = match[0]; + const variableSuffix = match[1]; + const insideContent = match[3]; + const varName = `markdown${variableSuffix}`; + + const contentAST: BuildASTResult = buildAST(insideContent, { + type: 'AST', + ast: [] + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns + 1); + const markdown: Markdown = { + type: 'MARKDOWN', + varName, + content: contentAST.ast.ast + }; + + return buildAST(source.replace(matchedContent, ''), { + ...ast, + ast: [...ast.ast, markdown] + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles, contentAST.numMarkdowns); } if (source.search(shuffleRegex) === 0) { @@ -385,7 +423,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) const contentAST: BuildASTResult = buildAST(insideContent, { type: 'AST', ast: [] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles + 1); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles + 1, numMarkdowns); const existingShuffledIndeces = generateShuffledIndeces(varName); const shuffle: Shuffle = { type: 'SHUFFLE', @@ -397,7 +435,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, shuffle] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, contentAST.numInputs, contentAST.numEssays, contentAST.numChecks, contentAST.numRadios, contentAST.numDrags, contentAST.numDrops, contentAST.numSolutions, contentAST.numCodes, contentAST.numShuffles, contentAST.numMarkdowns); } // if (source.search(dragRegex) === 0) { @@ -453,7 +491,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, image] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns); } if (source.search(graphRegex) === 0) { @@ -471,7 +509,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, graph] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns); } if (source.search(contentRegex) === 0) { @@ -487,7 +525,7 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) return buildAST(source.replace(matchedContent, ''), { ...ast, ast: [...ast.ast, content] - }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles); + }, generateVarValue, generateImageSrc, generateGraphEquations, generateShuffledIndeces, numInputs, numEssays, numChecks, numRadios, numDrags, numDrops, numSolutions, numCodes, numShuffles, numMarkdowns); } return { @@ -500,7 +538,8 @@ function buildAST(source: string, ast: AST, generateVarValue: (varName: string) numDrops, numSolutions, numCodes, - numShuffles + numShuffles, + numMarkdowns }; } @@ -515,7 +554,8 @@ export function getAstObjects(ast: AST, type: ASTObjectType, typesToExclude?: AS astObject.type === 'SOLUTION' || astObject.type === 'SHUFFLE' || astObject.type === 'DRAG' || - astObject.type === 'DROP' + astObject.type === 'DROP' || + astObject.type === 'MARKDOWN' ) && !shouldExcludeType ) { @@ -575,7 +615,8 @@ export function normalizeASTObjectPayloads(originalAST: AST, currentAST: AST): A astObject.type === 'SOLUTION' || astObject.type === 'SHUFFLE' || astObject.type === 'DRAG' || - astObject.type === 'DROP' + astObject.type === 'DROP' || + astObject.type === 'MARKDOWN' ) { return { ...astObject, diff --git a/package.json b/package.json index acf4236..b521211 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "assessml", - "version": "0.12.3", + "version": "0.13.0", "description": "AssessML (Assessment Markup Language) is a concise and flexible declarative language for educational assessments. This repository contains the language specification and JavaScript implementations of various language tools (AST generator, compilers, etc).", "scripts": { "test": "guesswork chromium firefox --entry test/index.js", diff --git a/test-utilities.ts b/test-utilities.ts index 867b0f0..709ba15 100644 --- a/test-utilities.ts +++ b/test-utilities.ts @@ -131,6 +131,22 @@ const arbSolution = jsverify.record({ }) }); +const arbMarkdown = jsverify.record({ + type: jsverify.constant('MARKDOWN'), + varName: jsverify.bless({ + generator: () => { + //TODO realistically the check prefix could have more characters than the UUID function allows. But we need to make sure they are unique + //TODO check var names being unique is a constraint that the user must follow. All nested tags must have unique variable names or the nesting will not work + return `markdown${createUUID()}`; + } + }), + content: jsverify.bless({ + generator: () => { + return jsverify.sampler(arbASTArray)(); + } + }) +}); + const arbShuffle = jsverify.record({ type: jsverify.constant('SHUFFLE'), varName: jsverify.bless({ @@ -147,7 +163,7 @@ const arbShuffle = jsverify.record({ }) }); -const arbASTArray = jsverify.array(jsverify.oneof([arbContent, arbVariable, arbInput, arbEssay, arbImage, arbCode, arbGraph, jsverify.oneof(arbContent, arbCheck), jsverify.oneof(arbContent, arbRadio), jsverify.oneof(arbContent, arbSolution), jsverify.oneof(arbContent, arbShuffle)])); +const arbASTArray = jsverify.array(jsverify.oneof([arbContent, arbVariable, arbInput, arbEssay, arbImage, arbCode, arbGraph, jsverify.oneof(arbContent, arbCheck), jsverify.oneof(arbContent, arbRadio), jsverify.oneof(arbContent, arbSolution), jsverify.oneof(arbContent, arbShuffle), jsverify.oneof(arbContent, arbMarkdown)])); export const arbAST = jsverify.record({ type: jsverify.constant('AST'), @@ -169,7 +185,8 @@ export function flattenContentObjects(ast: AST): AST { astObject.type === 'SOLUTION' || astObject.type === 'SHUFFLE' || astObject.type === 'DRAG' || - astObject.type === 'DROP' + astObject.type === 'DROP' || + astObject.type === 'MARKDOWN' ) { return [...result, { ...astObject, @@ -299,6 +316,22 @@ export function verifyHTML(ast: AST, htmlString: string) { } } + if (astObject.type === 'MARKDOWN') { + const markdownString = `
` + + if (result.indexOf(markdownString) === 0) { + return result.replace(markdownString, ''); + } + } + if (astObject.type === 'SHUFFLE') { //TODO Not sure this test is very useful. We are using the compileToHTML function in the implementation of the test for the compileToHTML function...albeit it is different because we're using it only on one piece of the AST instead of the entire AST //TODO this is not testing that the randomness works @@ -334,7 +367,8 @@ export function addShuffledIndeces(ast: AST): AST { astObject.type === 'SOLUTION' || astObject.type === 'SHUFFLE' || astObject.type === 'DRAG' || - astObject.type === 'DROP' + astObject.type === 'DROP' || + astObject.type === 'MARKDOWN' ) { if (astObject.type === 'SHUFFLE') { const flattenedContentAST = flattenContentObjects({