@@ -8,8 +8,6 @@ use handlebars::{
88} ;
99use serde_json:: Value as JsonValue ;
1010
11- /// Simple static json helper
12- type H0 = fn ( ) -> JsonValue ;
1311/// Simple json to json helper
1412type H = fn ( & JsonValue ) -> JsonValue ;
1513/// Simple json to json helper with error handling
@@ -103,23 +101,30 @@ fn default_helper(v: &JsonValue, default: &JsonValue) -> JsonValue {
103101}
104102
105103fn plus_helper ( a : & JsonValue , b : & JsonValue ) -> JsonValue {
106- if let ( Some ( a) , Some ( b) ) = ( a. as_i64 ( ) , b. as_i64 ( ) ) {
107- ( a + b) . into ( )
108- } else if let ( Some ( a) , Some ( b) ) = ( a. as_f64 ( ) , b. as_f64 ( ) ) {
109- ( a + b) . into ( )
110- } else {
111- JsonValue :: Null
112- }
104+ arithmetic_helper ( a, b, i64:: checked_add, |a, b| a + b)
113105}
114106
115107fn minus_helper ( a : & JsonValue , b : & JsonValue ) -> JsonValue {
116- if let ( Some ( a) , Some ( b) ) = ( a. as_i64 ( ) , b. as_i64 ( ) ) {
117- ( a - b) . into ( )
118- } else if let ( Some ( a) , Some ( b) ) = ( a. as_f64 ( ) , b. as_f64 ( ) ) {
119- ( a - b) . into ( )
120- } else {
121- JsonValue :: Null
108+ arithmetic_helper ( a, b, i64:: checked_sub, |a, b| a - b)
109+ }
110+
111+ /// Integer inputs are kept exact where they fit in an `i64`, and fall back to
112+ /// floating point rather than overflowing.
113+ fn arithmetic_helper (
114+ a : & JsonValue ,
115+ b : & JsonValue ,
116+ exact : fn ( i64 , i64 ) -> Option < i64 > ,
117+ approximate : fn ( f64 , f64 ) -> f64 ,
118+ ) -> JsonValue {
119+ if let ( Some ( a) , Some ( b) ) = ( a. as_i64 ( ) , b. as_i64 ( ) )
120+ && let Some ( result) = exact ( a, b)
121+ {
122+ return result. into ( ) ;
123+ }
124+ if let ( Some ( a) , Some ( b) ) = ( a. as_f64 ( ) , b. as_f64 ( ) ) {
125+ return approximate ( a, b) . into ( ) ;
122126 }
127+ JsonValue :: Null
123128}
124129
125130fn starts_with_helper ( a : & JsonValue , b : & JsonValue ) -> JsonValue {
@@ -520,15 +525,6 @@ trait CanHelp: Send + Sync + 'static {
520525 fn call ( & self , v : & [ PathAndJson < ' _ > ] ) -> Result < JsonValue , String > ;
521526}
522527
523- impl CanHelp for H0 {
524- fn call ( & self , args : & [ PathAndJson < ' _ > ] ) -> Result < JsonValue , String > {
525- match args {
526- [ ] => Ok ( self ( ) ) ,
527- _ => Err ( "expected no arguments" . to_string ( ) ) ,
528- }
529- }
530- }
531-
532528impl CanHelp for H {
533529 fn call ( & self , args : & [ PathAndJson < ' _ > ] ) -> Result < JsonValue , String > {
534530 match args {
@@ -744,4 +740,153 @@ mod tests {
744740 fn as_json_context < ' a > ( path : & ' a str , value : & ' a Value ) -> ScopedJson < ' a > {
745741 ScopedJson :: Context ( value, vec ! [ path. to_string( ) ] )
746742 }
743+
744+ mod documented_helpers {
745+ use crate :: app_config:: tests:: test_config;
746+ use crate :: template_helpers:: {
747+ csv_escape_helper, entries_helper, loose_eq_helper, minus_helper, plus_helper,
748+ register_all_helpers, starts_with_helper, to_array_helper, url_encode_helper,
749+ } ;
750+ use handlebars:: Handlebars ;
751+ use serde_json:: json;
752+
753+ fn render ( template : & str ) -> String {
754+ let mut registry = Handlebars :: new ( ) ;
755+ register_all_helpers ( & mut registry, & test_config ( ) ) ;
756+ registry. render_template ( template, & json ! ( { } ) ) . unwrap ( )
757+ }
758+
759+ #[ test]
760+ fn sum_adds_every_argument ( ) {
761+ assert_eq ! ( render( "{{sum 1 2 3}}" ) , "6" ) ;
762+ assert_eq ! ( render( "{{sum 1.5 2.25}}" ) , "3.75" ) ;
763+ }
764+
765+ #[ test]
766+ fn all_and_any_short_circuit_on_truthiness ( ) {
767+ assert_eq ! ( render( "{{all 1 2 3}}" ) , "3" ) ;
768+ assert_eq ! ( render( "{{all 1 0 3}}" ) , "0" ) ;
769+ assert_eq ! ( render( "{{any 0 false 7}}" ) , "7" ) ;
770+ assert_eq ! ( render( "{{any 0 false}}" ) , "false" ) ;
771+ }
772+
773+ #[ test]
774+ fn starts_with_distinguishes_internal_columns ( ) {
775+ assert_eq ! (
776+ starts_with_helper( & json!( "_sqlpage_id" ) , & json!( "_sqlpage_" ) ) ,
777+ json!( true )
778+ ) ;
779+ assert_eq ! (
780+ starts_with_helper( & json!( "price" ) , & json!( "_sqlpage_" ) ) ,
781+ json!( false )
782+ ) ;
783+ assert_eq ! (
784+ starts_with_helper( & json!( [ 1 , 2 , 3 ] ) , & json!( [ 1 , 2 ] ) ) ,
785+ json!( true )
786+ ) ;
787+ }
788+
789+ #[ test]
790+ fn plus_and_minus_operate_on_numbers_only ( ) {
791+ assert_eq ! ( plus_helper( & json!( 2 ) , & json!( 3 ) ) , json!( 5 ) ) ;
792+ assert_eq ! ( minus_helper( & json!( 7 ) , & json!( 2 ) ) , json!( 5 ) ) ;
793+ assert_eq ! ( plus_helper( & json!( 2.5 ) , & json!( 0.25 ) ) , json!( 2.75 ) ) ;
794+ assert_eq ! ( minus_helper( & json!( 2.5 ) , & json!( 0.25 ) ) , json!( 2.25 ) ) ;
795+ assert_eq ! ( plus_helper( & json!( "a" ) , & json!( 1 ) ) , json!( null) ) ;
796+ assert_eq ! ( minus_helper( & json!( "a" ) , & json!( 1 ) ) , json!( null) ) ;
797+ }
798+
799+ #[ test]
800+ fn integer_arithmetic_falls_back_to_floats_instead_of_overflowing ( ) {
801+ let overflowed = plus_helper ( & json ! ( i64 :: MAX ) , & json ! ( 1 ) ) ;
802+ assert ! (
803+ overflowed. as_f64( ) . is_some_and( |v| v > 9.2e18 ) ,
804+ "{overflowed}"
805+ ) ;
806+
807+ let underflowed = minus_helper ( & json ! ( i64 :: MIN ) , & json ! ( 1 ) ) ;
808+ assert ! (
809+ underflowed. as_f64( ) . is_some_and( |v| v < -9.2e18 ) ,
810+ "{underflowed}"
811+ ) ;
812+ }
813+
814+ #[ test]
815+ fn csv_fields_are_quoted_whenever_they_need_to_be ( ) {
816+ for needs_quoting in [ "a,b" , "a\" b" , "a\n b" ] {
817+ let escaped = csv_escape_helper ( & json ! ( needs_quoting) , & json ! ( "," ) ) ;
818+ assert ! (
819+ escaped. as_str( ) . unwrap( ) . starts_with( '"' ) ,
820+ "{needs_quoting:?} was left unquoted as {escaped}"
821+ ) ;
822+ }
823+ assert_eq ! (
824+ csv_escape_helper( & json!( "a\" b" ) , & json!( "," ) ) ,
825+ json!( "\" a\" \" b\" " )
826+ ) ;
827+ assert_eq ! (
828+ csv_escape_helper( & json!( "plain" ) , & json!( "," ) ) ,
829+ json!( "plain" )
830+ ) ;
831+ assert_eq ! (
832+ csv_escape_helper( & json!( "a;b" ) , & json!( ";" ) ) ,
833+ json!( "\" a;b\" " )
834+ ) ;
835+ }
836+
837+ #[ test]
838+ fn url_encode_percent_encodes_reserved_characters ( ) {
839+ assert_eq ! (
840+ url_encode_helper( & json!( "hello world" ) ) ,
841+ json!( "hello%20world" )
842+ ) ;
843+ }
844+
845+ #[ test]
846+ fn entries_exposes_objects_and_arrays_as_key_value_pairs ( ) {
847+ assert_eq ! (
848+ entries_helper( & json!( { "a" : 1 } ) ) ,
849+ json!( [ { "key" : "a" , "value" : 1 } ] )
850+ ) ;
851+ assert_eq ! (
852+ entries_helper( & json!( [ 10 , 20 ] ) ) ,
853+ json!( [ { "key" : 0 , "value" : 10 } , { "key" : 1 , "value" : 20 } ] )
854+ ) ;
855+ assert_eq ! ( entries_helper( & json!( "scalar" ) ) , json!( [ ] ) ) ;
856+ }
857+
858+ #[ test]
859+ fn to_array_parses_json_list_strings ( ) {
860+ assert_eq ! ( to_array_helper( & json!( "[1,2]" ) ) , json!( [ 1 , 2 ] ) ) ;
861+ assert_eq ! ( to_array_helper( & json!( "nope" ) ) , json!( [ "nope" ] ) ) ;
862+ assert_eq ! ( to_array_helper( & json!( [ 1 , 2 ] ) ) , json!( [ 1 , 2 ] ) ) ;
863+ assert_eq ! ( to_array_helper( & json!( null) ) , json!( [ ] ) ) ;
864+ }
865+
866+ #[ test]
867+ fn loose_eq_compares_values_as_strings ( ) {
868+ assert_eq ! ( loose_eq_helper( & json!( 42 ) , & json!( "42" ) ) , json!( true ) ) ;
869+ assert_eq ! ( loose_eq_helper( & json!( "42" ) , & json!( 42 ) ) , json!( true ) ) ;
870+ assert_eq ! ( loose_eq_helper( & json!( "a" ) , & json!( "a" ) ) , json!( true ) ) ;
871+ assert_eq ! ( loose_eq_helper( & json!( 1 ) , & json!( 2 ) ) , json!( false ) ) ;
872+ }
873+ }
874+
875+ mod app_config_markdown_defaults {
876+ use crate :: app_config:: tests:: test_config;
877+ use crate :: template_helpers:: render_markdown_to_html;
878+
879+ #[ test]
880+ fn raw_html_is_escaped ( ) {
881+ let html = render_markdown_to_html ( & test_config ( ) , "<table><tr><td>" ) . unwrap ( ) ;
882+ assert_eq ! ( html, "<table><tr><td>" ) ;
883+ }
884+
885+ #[ test]
886+ fn dangerous_protocols_are_stripped ( ) {
887+ let html =
888+ render_markdown_to_html ( & test_config ( ) , "[click](javascript:alert(1))" ) . unwrap ( ) ;
889+ assert_eq ! ( html, "<p><a href=\" \" >click</a></p>" ) ;
890+ }
891+ }
747892}
0 commit comments