@@ -27,7 +27,7 @@ export default class HParsons extends RunestoneBase {
2727 this . isBlockGrading = this . parseBooleanAttribute ( orig , "data-blockanswer" ) ;
2828 this . language = orig . getAttribute ( "data-language" ) ;
2929 // Detect math mode
30- if ( this . language === undefined && orig . textContent . includes ( 'span class="process-math"' ) ) {
30+ if ( ( this . language == null ) && orig . textContent . includes ( 'span class="process-math"' ) ) {
3131 this . language = "math" ;
3232 }
3333 if ( this . isBlockGrading ) {
@@ -54,9 +54,6 @@ export default class HParsons extends RunestoneBase {
5454 this . controlDiv = null ;
5555 this . processContent ( this . code ) ;
5656
57- this . microParsonToRaw = new Map ( ) ;
58- this . simulatedSolution = [ ] ;
59-
6057 // Change to factory when more execution based feedback is included
6158 if ( this . isBlockGrading ) {
6259 this . feedbackController = new BlockFeedback ( this ) ;
@@ -131,7 +128,7 @@ export default class HParsons extends RunestoneBase {
131128 this . hparsonsInput = this . outerDiv . querySelector ( "micro-parsons" ) ;
132129 this . renderMathInBlocks ( ) ;
133130 // Change "code" to "answer" in parsons direction for non-code languages
134- if ( this . language === undefined || this . language === "math" ) {
131+ if ( this . language == null || this . language === "math" ) {
135132 this . outerDiv . querySelectorAll ( ".hparsons-tip" ) . forEach ( el => {
136133 if ( el . textContent . includes ( "our code" ) ) {
137134 el . textContent = el . textContent . replace ( "our code" , "our answer" ) ;
@@ -195,53 +192,11 @@ export default class HParsons extends RunestoneBase {
195192 } ) ;
196193
197194 if ( window . MathJax && MathJax . typesetPromise ) {
198- MathJax . typesetPromise ( ) . then ( ( ) => this . simulateSolution ( ) ) ;
195+ MathJax . typesetPromise ( ) ;
199196 }
200197 } , 0 ) ;
201198 }
202199
203- /*
204- This function performs a simulated "correct answer" ordering using the
205- correct block indices specified in `this.blockAnswer`. It looks ahead
206- at the rendered content from the MicroParsons widget to build:
207- - this.simulatedSolution: an array of correctly ordered rendered strings
208- - this.microParsonToRaw: a Map that links rendered HTML (from MicroParsons)
209- to their original raw `<m>` source strings from PreTeXt
210-
211- This is called after MathJax renders the math blocks to ensure the mapping
212- is built from the final, visible DOM state. It is needed for grading
213- math-mode Parsons problems, where rendered symbols (e.g., “\(\alpha\)”) must
214- be matched against author-defined symbolic content.
215- */
216- simulateSolution ( ) {
217- if (
218- this . simulatedSolution . length > 0 &&
219- this . microParsonToRaw instanceof Map &&
220- this . microParsonToRaw . size > 0
221- ) { // Already initialized from local storage
222- this . feedbackController . solution = this . simulatedSolution ;
223- this . feedbackController . grader . solution = this . simulatedSolution ;
224- return ;
225- }
226-
227- this . microParsonToRaw = new Map ( ) ;
228-
229- const allBlocks = Array . from (
230- this . outerDiv . querySelectorAll ( "micro-parsons .parsons-block" )
231- ) ;
232- if ( ! this . blockAnswer || allBlocks . length === 0 ) return ;
233-
234- const rendered = this . hparsonsInput . getParsonsTextArray ( ) ;
235- const raw = this . originalBlocks ;
236- const correctOrder = this . blockAnswer . map ( Number ) ;
237-
238- this . simulatedSolution = correctOrder . map ( i => rendered [ i ] ) ;
239- rendered . forEach ( ( r , i ) => this . microParsonToRaw . set ( r , raw [ i ] . trim ( ) ) ) ;
240-
241- this . feedbackController . solution = this . simulatedSolution ;
242- this . feedbackController . grader . solution = this . simulatedSolution ;
243- }
244-
245200 // Return previous answers in local storage
246201 //
247202 localData ( ) {
@@ -268,59 +223,95 @@ export default class HParsons extends RunestoneBase {
268223 }
269224 */
270225 if ( serverData . answer ) {
271- this . hparsonsInput . restoreAnswer ( serverData . answer . blocks ) ;
226+ const blocks = serverData . answer . blocks ?? serverData . answer ;
227+
228+ if ( Array . isArray ( blocks ) && blocks . length > 0 ) {
229+ const first = blocks [ 0 ] ;
230+
231+ // Prefer indices (numbers or numeric strings)
232+ const looksNumeric = ( typeof first === "number" ) || ( / ^ \d + $ / . test ( String ( first ) ) ) ;
233+ if ( looksNumeric && this . hparsonsInput . restoreAnswerByIndices ) {
234+ this . hparsonsInput . restoreAnswerByIndices ( blocks . map ( Number ) ) ;
235+ } else {
236+ this . hparsonsInput . restoreAnswer ( blocks ) ;
237+ }
238+ }
272239 }
273240 if ( serverData . count ) {
274241 this . feedbackController . checkCount = serverData . count ;
275242 }
276243 }
244+
245+
246+
247+
248+
249+
250+
251+
252+
253+
254+
255+
256+
257+
258+
259+
260+
261+
262+
263+
264+
265+
266+
267+
268+
269+
270+
271+
272+
273+
274+
275+
276+
277277 // RunestoneBase: Load what is in local storage
278278 checkLocalStorage ( ) {
279279 if ( this . graderactive ) {
280280 // Zihan: I think this means the component is still loading?
281281 return ;
282282 }
283283 let localData = this . localData ( ) ;
284- if ( localData . answer ) {
284+ if ( localData . answerIndices && this . hparsonsInput . restoreAnswerByIndices ) {
285+ this . hparsonsInput . restoreAnswerByIndices ( localData . answerIndices . map ( Number ) ) ;
286+ } else if ( localData . answer ) {
287+ // Legacy restore (string-based)
285288 this . hparsonsInput . restoreAnswer ( localData . answer ) ;
289+
290+ // Best-effort migration: persist indices after restoring
291+ if ( this . isBlockGrading ) {
292+ const migrated = this . hparsonsInput . getBlockIndices ( ) ;
293+ localData . answerIndices = migrated ;
294+ localStorage . setItem ( this . storageId , JSON . stringify ( localData ) ) ;
295+ }
286296 }
287297 if ( localData . count ) {
288298 this . feedbackController . checkCount = localData . count ;
289299 }
290- if ( localData . simulatedSolution ) {
291- this . simulatedSolution = localData . simulatedSolution ;
292- }
293- if ( localData . microParsonToRaw ) {
294- this . microParsonToRaw = new Map ( Object . entries ( localData . microParsonToRaw ) ) ;
295- } else {
296- this . microParsonToRaw = new Map ( ) ;
297- }
298300 }
299301 // RunestoneBase: Set the state of the problem in local storage
300302 setLocalStorage ( data ) {
301303 let currentState = { } ;
302304 if ( data == undefined ) {
303- let userAnswer = this . hparsonsInput . getParsonsTextArray ( ) ;
304-
305- // In math mode, convert microParsons to raw before caching
306- // Additionally, save the solution and microParson ➜ Raw map.
307- if ( this . language === "math" ) {
308- userAnswer = userAnswer . map ( sym => this . microParsonToRaw . get ( sym ) ) ;
309- currentState = {
310- answer : userAnswer ,
311- simulatedSolution : this . simulatedSolution ,
312- microParsonToRaw : Object . fromEntries ( this . microParsonToRaw ) ,
313- } ;
314- } else {
315- currentState = {
316- answer : userAnswer ,
317- } ;
318- }
319-
305+
320306 if ( this . isBlockGrading ) {
321- // if this is block grading, add number of previous attempts too
307+ const answerIndices = this . hparsonsInput . getBlockIndices ( ) ;
308+ currentState = { answerIndices : answerIndices } ;
322309 currentState . count = this . feedbackController . checkCount ;
310+ } else {
311+ const userAnswer = this . hparsonsInput . getParsonsTextArray ( ) ;
312+ currentState = { answer : userAnswer } ;
323313 }
314+
324315 } else {
325316 currentState = data ;
326317 }
0 commit comments