@@ -38,16 +38,26 @@ impl OutputCache {
3838/// * transformed document content (post-transform pipeline)
3939/// * output type string (e.g. `"html"`, `"pdf"`, `"docx"`)
4040/// * optional template name (empty string when absent)
41+ /// * optional template file content (empty string when absent or unreadable)
4142///
4243/// A change to any of these fields produces a different hash, causing a cache
43- /// miss and triggering a fresh pandoc run.
44- pub fn compute_output_hash ( transformed_content : & str , output_type : & str , template : Option < & str > ) -> String {
44+ /// miss and triggering a fresh pandoc run. Including the template file content
45+ /// (not just its name) means that editing a template file invalidates cached
46+ /// render outputs even when the template path has not changed.
47+ pub fn compute_output_hash (
48+ transformed_content : & str ,
49+ output_type : & str ,
50+ template : Option < & str > ,
51+ template_content : Option < & str > ,
52+ ) -> String {
4553 let mut hasher = Sha256 :: new ( ) ;
4654 hasher. update ( transformed_content. as_bytes ( ) ) ;
4755 hasher. update ( b"\x00 output-type\x00 " ) ;
4856 hasher. update ( output_type. as_bytes ( ) ) ;
4957 hasher. update ( b"\x00 template\x00 " ) ;
5058 hasher. update ( template. unwrap_or ( "" ) . as_bytes ( ) ) ;
59+ hasher. update ( b"\x00 template-content\x00 " ) ;
60+ hasher. update ( template_content. unwrap_or ( "" ) . as_bytes ( ) ) ;
5161 format ! ( "{:x}" , hasher. finalize( ) )
5262}
5363
@@ -116,13 +126,22 @@ impl TransformCache {
116126///
117127/// The hash covers:
118128/// * normalized input file content
129+ /// * config file content (raw YAML bytes)
119130/// * variables (sorted for determinism, regardless of map insertion order)
120131///
121132/// This hash uniquely identifies a combination of inputs so that a cache hit
122- /// guarantees the transform pipeline would produce the same output.
123- pub fn compute_input_hash ( content : & str , variables : & HashMap < String , String > ) -> String {
133+ /// guarantees the transform pipeline would produce the same output. Including
134+ /// the raw config file content means that any change to the config (not only
135+ /// to `variables`) invalidates the transform cache.
136+ pub fn compute_input_hash (
137+ content : & str ,
138+ config_content : & str ,
139+ variables : & HashMap < String , String > ,
140+ ) -> String {
124141 let mut hasher = Sha256 :: new ( ) ;
125142 hasher. update ( content. as_bytes ( ) ) ;
143+ hasher. update ( b"\x00 config\x00 " ) ;
144+ hasher. update ( config_content. as_bytes ( ) ) ;
126145
127146 // Sort entries so the hash is stable regardless of HashMap iteration order.
128147 let mut sorted: Vec < ( & String , & String ) > = variables. iter ( ) . collect ( ) ;
@@ -324,46 +343,60 @@ mod tests {
324343
325344 #[ test]
326345 fn test_same_inputs_produce_same_hash ( ) {
327- let h1 = compute_input_hash ( "hello world" , & vars ( & [ ( "key" , "val" ) ] ) ) ;
328- let h2 = compute_input_hash ( "hello world" , & vars ( & [ ( "key" , "val" ) ] ) ) ;
346+ let h1 = compute_input_hash ( "hello world" , "" , & vars ( & [ ( "key" , "val" ) ] ) ) ;
347+ let h2 = compute_input_hash ( "hello world" , "" , & vars ( & [ ( "key" , "val" ) ] ) ) ;
329348 assert_eq ! ( h1, h2) ;
330349 }
331350
332351 #[ test]
333352 fn test_different_content_produces_different_hash ( ) {
334- let h1 = compute_input_hash ( "content A" , & vars ( & [ ] ) ) ;
335- let h2 = compute_input_hash ( "content B" , & vars ( & [ ] ) ) ;
353+ let h1 = compute_input_hash ( "content A" , "" , & vars ( & [ ] ) ) ;
354+ let h2 = compute_input_hash ( "content B" , "" , & vars ( & [ ] ) ) ;
336355 assert_ne ! ( h1, h2) ;
337356 }
338357
339358 #[ test]
340359 fn test_different_variables_produce_different_hash ( ) {
341- let h1 = compute_input_hash ( "same content" , & vars ( & [ ( "k" , "v1" ) ] ) ) ;
342- let h2 = compute_input_hash ( "same content" , & vars ( & [ ( "k" , "v2" ) ] ) ) ;
360+ let h1 = compute_input_hash ( "same content" , "" , & vars ( & [ ( "k" , "v1" ) ] ) ) ;
361+ let h2 = compute_input_hash ( "same content" , "" , & vars ( & [ ( "k" , "v2" ) ] ) ) ;
343362 assert_ne ! ( h1, h2) ;
344363 }
345364
346365 #[ test]
347366 fn test_variable_order_does_not_affect_hash ( ) {
348- let h1 = compute_input_hash ( "content" , & vars ( & [ ( "a" , "1" ) , ( "b" , "2" ) ] ) ) ;
349- let h2 = compute_input_hash ( "content" , & vars ( & [ ( "b" , "2" ) , ( "a" , "1" ) ] ) ) ;
367+ let h1 = compute_input_hash ( "content" , "" , & vars ( & [ ( "a" , "1" ) , ( "b" , "2" ) ] ) ) ;
368+ let h2 = compute_input_hash ( "content" , "" , & vars ( & [ ( "b" , "2" ) , ( "a" , "1" ) ] ) ) ;
350369 assert_eq ! ( h1, h2) ;
351370 }
352371
353372 #[ test]
354373 fn test_empty_variables_produces_stable_hash ( ) {
355- let h1 = compute_input_hash ( "content" , & vars ( & [ ] ) ) ;
356- let h2 = compute_input_hash ( "content" , & vars ( & [ ] ) ) ;
374+ let h1 = compute_input_hash ( "content" , "" , & vars ( & [ ] ) ) ;
375+ let h2 = compute_input_hash ( "content" , "" , & vars ( & [ ] ) ) ;
357376 assert_eq ! ( h1, h2) ;
358377 }
359378
360379 #[ test]
361380 fn test_hash_is_hex_string ( ) {
362- let h = compute_input_hash ( "test" , & vars ( & [ ] ) ) ;
381+ let h = compute_input_hash ( "test" , "" , & vars ( & [ ] ) ) ;
363382 assert ! ( h. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) , "hash must be hex: {h}" ) ;
364383 assert_eq ! ( h. len( ) , 64 , "SHA-256 hex must be 64 chars" ) ;
365384 }
366385
386+ #[ test]
387+ fn test_different_config_content_produces_different_hash ( ) {
388+ let h1 = compute_input_hash ( "same content" , "config: a" , & vars ( & [ ] ) ) ;
389+ let h2 = compute_input_hash ( "same content" , "config: b" , & vars ( & [ ] ) ) ;
390+ assert_ne ! ( h1, h2) ;
391+ }
392+
393+ #[ test]
394+ fn test_empty_config_content_differs_from_nonempty ( ) {
395+ let h1 = compute_input_hash ( "content" , "" , & vars ( & [ ] ) ) ;
396+ let h2 = compute_input_hash ( "content" , "outputs:\n - type: html\n " , & vars ( & [ ] ) ) ;
397+ assert_ne ! ( h1, h2) ;
398+ }
399+
367400 // ── TransformCache ───────────────────────────────────────────────────────
368401
369402 #[ test]
@@ -438,46 +471,60 @@ mod tests {
438471
439472 #[ test]
440473 fn test_output_hash_same_inputs_stable ( ) {
441- let h1 = compute_output_hash ( "content" , "html" , None ) ;
442- let h2 = compute_output_hash ( "content" , "html" , None ) ;
474+ let h1 = compute_output_hash ( "content" , "html" , None , None ) ;
475+ let h2 = compute_output_hash ( "content" , "html" , None , None ) ;
443476 assert_eq ! ( h1, h2) ;
444477 }
445478
446479 #[ test]
447480 fn test_output_hash_different_output_type_differs ( ) {
448- let h1 = compute_output_hash ( "content" , "html" , None ) ;
449- let h2 = compute_output_hash ( "content" , "pdf" , None ) ;
481+ let h1 = compute_output_hash ( "content" , "html" , None , None ) ;
482+ let h2 = compute_output_hash ( "content" , "pdf" , None , None ) ;
450483 assert_ne ! ( h1, h2) ;
451484 }
452485
453486 #[ test]
454487 fn test_output_hash_different_template_differs ( ) {
455- let h1 = compute_output_hash ( "content" , "html" , Some ( "a.html" ) ) ;
456- let h2 = compute_output_hash ( "content" , "html" , Some ( "b.html" ) ) ;
488+ let h1 = compute_output_hash ( "content" , "html" , Some ( "a.html" ) , None ) ;
489+ let h2 = compute_output_hash ( "content" , "html" , Some ( "b.html" ) , None ) ;
457490 assert_ne ! ( h1, h2) ;
458491 }
459492
460493 #[ test]
461494 fn test_output_hash_no_template_differs_from_with_template ( ) {
462- let h1 = compute_output_hash ( "content" , "html" , None ) ;
463- let h2 = compute_output_hash ( "content" , "html" , Some ( "tmpl.html" ) ) ;
495+ let h1 = compute_output_hash ( "content" , "html" , None , None ) ;
496+ let h2 = compute_output_hash ( "content" , "html" , Some ( "tmpl.html" ) , None ) ;
464497 assert_ne ! ( h1, h2) ;
465498 }
466499
467500 #[ test]
468501 fn test_output_hash_different_content_differs ( ) {
469- let h1 = compute_output_hash ( "content A" , "html" , None ) ;
470- let h2 = compute_output_hash ( "content B" , "html" , None ) ;
502+ let h1 = compute_output_hash ( "content A" , "html" , None , None ) ;
503+ let h2 = compute_output_hash ( "content B" , "html" , None , None ) ;
471504 assert_ne ! ( h1, h2) ;
472505 }
473506
474507 #[ test]
475508 fn test_output_hash_is_hex_string ( ) {
476- let h = compute_output_hash ( "content" , "html" , None ) ;
509+ let h = compute_output_hash ( "content" , "html" , None , None ) ;
477510 assert ! ( h. chars( ) . all( |c| c. is_ascii_hexdigit( ) ) , "hash must be hex: {h}" ) ;
478511 assert_eq ! ( h. len( ) , 64 , "SHA-256 hex must be 64 chars" ) ;
479512 }
480513
514+ #[ test]
515+ fn test_output_hash_different_template_content_differs ( ) {
516+ let h1 = compute_output_hash ( "content" , "html" , Some ( "t.html" ) , Some ( "<html>v1</html>" ) ) ;
517+ let h2 = compute_output_hash ( "content" , "html" , Some ( "t.html" ) , Some ( "<html>v2</html>" ) ) ;
518+ assert_ne ! ( h1, h2) ;
519+ }
520+
521+ #[ test]
522+ fn test_output_hash_no_template_content_differs_from_some ( ) {
523+ let h1 = compute_output_hash ( "content" , "html" , Some ( "t.html" ) , None ) ;
524+ let h2 = compute_output_hash ( "content" , "html" , Some ( "t.html" ) , Some ( "<html></html>" ) ) ;
525+ assert_ne ! ( h1, h2) ;
526+ }
527+
481528 // ── OutputCache ──────────────────────────────────────────────────────────
482529
483530 #[ test]
0 commit comments