├── version.md ├── .gitignore ├── LICENSE ├── README.md └── debugger.ftl /version.md: -------------------------------------------------------------------------------- 1 | 1.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .project 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Rather Blue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freemarker-debugger v1.0 2 | 3 | Debugging in FreeMarker made easy! Manually or dynamically traverse through local and namespaced FreeMarker variables or objects from the data model sent to the FreeMarker view. 4 | 5 | ## Usage 6 | 7 | ```ftl 8 | <#import "debugger.ftl" as debugger /> 9 | 10 | <#-- Basic usage. Creates a table of the top-level data model objects --> 11 | <@debugger.debug /> 12 | 13 | <#-- Adds links to traversable elements --> 14 | <@debugger.debugDynamic /> 15 | ``` 16 | 17 | ## Settings 18 | 19 | Basic settings are included to allow for configuration flexibility: 20 | ```ftl 21 | <#assign settings = { 22 | "styleClassPrefix": "freemarker-debug", 23 | "queryParamKey": "debugQuery", 24 | "includeStyles": true, 25 | "ignoredKeys": ["class"], 26 | "ignoredPatterns": ["org.springframework."] 27 | } /> 28 | ``` 29 | 30 | `styleClassPrefix` 31 | * Controls what all the CSS classes are prefixed with. 32 | * While it is unlikely you will have existing styles that conflict with `freemarker-debug`, this option is included in case customization is preferred. 33 | 34 | `queryParamKey` 35 | * Controls what query string dynamic links are built with. 36 | * It is recommended that this value be customized in order to obscure what parameter your project will use. (Projects should be configured to prevent the debugger to run in production.) 37 | 38 | `includeStyles` 39 | * Flag to determine whether or not the default CSS styles should be included. 40 | * The styles only affect the debug output and are added so that it is readable no matter what the design of the page is. 41 | 42 | `ignoredKeys` 43 | * Keys exactly matching any of these values will not be output. 44 | * Case-sensitive 45 | 46 | `ignoredPatterns` 47 | * Keys **starting** with any of these values will not be output. 48 | * Case sensitive. 49 | 50 | ### Example setting customization 51 | 52 | ```ftl 53 | <#import "debugger.ftl" as debugger /> 54 | 55 | <#-- This will change all css classes to be prefixed 56 | with "custom-prefix" and ignore any keys equal to "class" or "equals" --> 57 | <#assign customSettings = debugger.settings + { 58 | "styleClassPrefix": "custom-prefix", 59 | "ignoredKeys": ["class", "equals"] 60 | } /> 61 | <#assign settings = customSettings in debugger /> 62 | 63 | <@debugger.debug /> 64 | ``` 65 | ## License 66 | 67 | [MIT](http://opensource.org/licenses/MIT) 68 | -------------------------------------------------------------------------------- /debugger.ftl: -------------------------------------------------------------------------------- 1 | <#ftl strip_text=true /> 2 | 3 | <#--- 4 | @homepage https://github.com/ratherblue/freemarker-debugger/ 5 | @license MIT 6 | @version 1.0 7 | 8 | @name freemarker-debugger 9 | @description Macros and functions used to generate a tabular view of the .locals, .main, and .data model. 10 | 11 | @see http://freemarker.org/docs/ref_specvar.html 12 | 13 | @namespace debugger 14 | 15 | Default usage (expands the top-level properties from .data_model): 16 | 17 | <#import "debugger.ftl" as debugger /> 18 | 19 | <@debugger.debug /> 20 | 21 | More examples: 22 | 23 | Expands first and second-level properties for the .locals (local variables and macro parameters) 24 | <@debugger.debug debugObject=.locals depth=2 /> 25 | 26 | 27 | <@debugger.debugDynamic debugObject=.locals depth=2 /> 28 | 29 | 30 | --> 31 | 32 | 33 | <#--- 34 | Settings for the debugger 35 | --> 36 | <#assign settings = { 37 | "styleClassPrefix": "freemarker-debug", 38 | "queryParamKey": "debugQuery", 39 | "includeStyles": true, 40 | "ignoredKeys": ["class"], <#-- Ignore keys that exactly match these values. Case-sensitive --> 41 | "ignoredPatterns": ["org.springframework."] <#-- Ignore keys that start with these values. Case-sensitive --> 42 | } /> 43 | 44 | 45 | <#--- 46 | Value of parameter used when dynamically expanding objects through links 47 | --> 48 | <#assign debugQuery = (RequestParameters[settings.queryParamKey]!'')?trim /> 49 | 50 | 51 | <#--- 52 | @param debugObject Object to expand 53 | @param depth How deep to expand the object 54 | --> 55 | <#macro debug debugObject=.data_model depth=1> 56 | 57 | <#-- include optional table styling --> 58 | <#if settings.includeStyles> 59 | <@tableStyles /> 60 | 61 | 62 | <#local title = varClass(debugObject) /> 63 | 64 |
65 | <@debugTable 66 | debugObject=debugObject 67 | depth=depth 68 | title=title!'' /> 69 |
70 | 71 | 72 | 73 | <#--- 74 | Shortcut for enabling links on expandable objects 75 | 76 | @param depth 77 | --> 78 | <#macro debugDynamic depth=1> 79 | 80 | <#-- include optional table styling --> 81 | <#if settings.includeStyles> 82 | <@tableStyles /> 83 | 84 | 85 |
86 | <#if debugQuery?has_content> 87 | 88 | <#local root = getDebugRoot(debugQuery) /> 89 | <#local debugObject = debugObjectFromUrl(root.object, debugQuery) /> 90 | 91 | <#if debugObject?is_number && debugObject == -1> 92 |
93 | Error: Unable to parse ${debugQuery?xhtml} 94 |
95 | <#else> 96 | <@debugTable 97 | debugObject=debugObject 98 | depth=depth 99 | dynamic=true 100 | title=getTitleLink(debugQuery) /> 101 | 102 | <#else> 103 | <@debugTable 104 | debugObject=.data_model 105 | depth=depth 106 | dynamic=true 107 | title=[{"title": ".data_model", "url": ""}] 108 | queryParam=".data_model" /> 109 | 110 |
111 | 112 | 113 | 114 | <#--- 115 | Prints out expandable objects (hash_ex, sequences) in tabular form 116 | @param debugObject Object to expand 117 | @param depth How deep to expand the object 118 | @param dynamic Boolean to make expandable objects a link to quickly drill-down into data 119 | @param queryParam Parameter used in query string for direct debugging 120 | @param title Title of the section 121 | --> 122 | <#macro debugTable debugObject depth dynamic=false queryParam="" title=""> 123 | 124 | <#if isComplexObject(debugObject)> 125 | <@tableWrapper title=title titleUrl=getDebugUrl(queryParam)> 126 | <#if debugObject?is_hash_ex> 127 | <#list debugObject?keys?sort as key> 128 | <@properties 129 | key=key 130 | value=(debugObject[key])!"" 131 | depth=depth 132 | dynamic=dynamic 133 | queryParam=buildQueryParam(key, queryParam, dynamic) /> 134 | 135 | <#elseif debugObject?is_sequence> 136 | <#list debugObject as obj> 137 | <@properties 138 | key=obj_index 139 | value=obj!"" 140 | depth=depth 141 | dynamic=dynamic 142 | queryParam=buildQueryParam(obj_index, queryParam, dynamic) /> 143 | 144 | 145 | 146 | <#else> 147 | <@simpleValue value=debugObject /> 148 | 149 | 150 | 151 | 152 | 153 | <#--- 154 | Basic styles for the debug table 155 | @param classPrefix Customizable class prefix used to give basic styles to the debug table. 156 | --> 157 | <#macro tableStyles classPrefix=settings.styleClassPrefix> 158 | <#compress> 159 | 274 | 275 | 276 | 277 | 278 | <#--- 279 | Shortcut for table structure 280 | @param title 281 | @param titleUrl 282 | FIXME: this is ugly 283 | --> 284 | <#macro tableWrapper title="" titleUrl=""> 285 | <#compress> 286 | 287 | 288 | <#if title?has_content> 289 | 290 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | <#nested /> 319 | 320 |
291 | <#-- TODO: ugly logic, fix --> 292 | <#if title?is_sequence> 293 | 302 | <#elseif titleUrl?has_content> 303 | 304 | ${title?xhtml} 305 | 306 | <#else> 307 | ${title?xhtml} 308 | 309 |
KeyValue
321 | 322 | 323 | 324 | 325 | 326 | <#--- 327 | Determines if a value is a sequence or a hash_ex and can be expanded. Ignores objects that are both sequence+method. 328 | @param object Object to determine if it is a hash_ex or sequence 329 | @returns boolean 330 | --> 331 | <#function isComplexObject object> 332 | 333 | <#if (object?is_sequence && object?is_method)> 334 | <#return false /> 335 | <#elseif (object?is_hash_ex || object?is_sequence)> 336 | <#return true /> 337 | 338 | 339 | <#return false /> 340 | 341 | 342 | 343 | 344 | <#--- 345 | 346 | @param key 347 | @param value 348 | @param depth How deep to expand the object 349 | @param dynamic Boolean to make expandable objects a link to quickly drill-down into data 350 | @param queryParam Parameter used in query string for direct debugging 351 | --> 352 | <#macro properties key value depth dynamic queryParam=""> 353 | <#-- ignore spring framework properties --> 354 | <#if ignoreKey(key)> 355 | <#return /> 356 | 357 | 358 | <#local isComplex = isComplexObject(value) /> 359 | 360 | <#if isComplex> 361 | <#if ((depth > 1) || dynamic) && value?has_content> 362 | <#local expandedClass = " " + settings.styleClassPrefix + "-expanded" /> 363 | 364 | 365 | 366 | 367 | ${key?xhtml} 368 | 369 | <#if isComplex> 370 | <@complexValue 371 | key=key 372 | value=value 373 | depth=depth 374 | dynamic=dynamic 375 | queryParam=queryParam /> 376 | <#else> 377 | <@simpleValue value=value /> 378 | 379 | 380 | 381 | 382 | 383 | 384 | <#--- 385 | Determines if a key should be ignored according to the settings. 386 | @param key 387 | @returns boolean 388 | --> 389 | <#function ignoreKey key> 390 | 391 | <#if key?is_string> 392 | <#if settings.ignoredKeys?seq_contains(key)> 393 | <#return true /> 394 | 395 | 396 | <#list settings.ignoredPatterns as pattern> 397 | <#if key?starts_with(pattern)> 398 | <#return true /> 399 | 400 | 401 | 402 | 403 | <#return false /> 404 | 405 | 406 | 407 | <#--- 408 | Displays a simple value (not hash_ex or sequence) 409 | @param value 410 | --> 411 | <#macro simpleValue value> 412 | 413 | <#-- METHOD --> 414 | <#-- check first because ?has_content doesn't work on methods --> 415 | <#-- TODO: See what methods can be expanded --> 416 | <#if value?is_method> 417 | method() 418 | 419 | <#-- MACRO/FUNCTIONS --> 420 | <#-- ?has_content evaluates to false for these --> 421 | <#elseif value?is_macro> 422 | macro/function 423 | 424 | <#elseif value?has_content> 425 | 426 | <#-- NUMBER --> 427 | <#if value?is_number> 428 | ${value?c}<#-- prevent number formatting --> 429 | 430 | <#-- DATE --> 431 | <#-- TODO: format --> 432 | <#elseif value?is_date> 433 | date: ${value?date} 434 | 435 | <#-- BOOLEAN --> 436 | <#elseif value?is_boolean> 437 | ${value?string("TRUE","FALSE")} 438 | 439 | <#-- HASH --> 440 | <#-- TODO: Look into how to expand this --> 441 | <#elseif value?is_hash> 442 | hash 443 | 444 | <#-- STRING --> 445 | <#-- always check string last since some objects will evaluate 446 | to string as well as another type --> 447 | <#elseif value?is_string> 448 | <#-- show full value in source --> 449 | 450 | ${truncateString(value)?xhtml}<#-- prevent injection --> 451 | 452 | 453 | <#else> 454 | (empty) 455 | 456 | 457 | 458 | 459 | <#--- 460 | Function to determine what the debug url will be 461 | @param queryParam Parameter used in query string for direct debugging 462 | @returns string 463 | --> 464 | <#function getDebugUrl queryParam> 465 | 466 | <#local url = getBaseUrl() /> 467 | 468 | <#if debugQuery?has_content> 469 | <#local url = url + debugQuery + queryParam /> 470 | <#else> 471 | <#local url = url + queryParam /> 472 | 473 | 474 | <#return url /> 475 | 476 | 477 | 478 | 479 | <#--- 480 | Determine the base debug url 481 | @returns string 482 | --> 483 | <#function getBaseUrl> 484 | 485 | <#local url = "?" /> 486 | 487 | <#if RequestParameters?has_content> 488 | <#list RequestParameters?keys as paramKey> 489 | <#if paramKey != settings.queryParamKey> 490 | <#local url = url + paramKey + "=" + RequestParameters[paramKey] + "&" /> 491 | 492 | 493 | 494 | 495 | <#return (url + settings.queryParamKey + "=") /> 496 | 497 | 498 | 499 | <#--- 500 | Builds the parameter to use in the query string for debugging 501 | @param key 502 | @param debugQueryPrefix 503 | @param dynamic 504 | @returns string 505 | --> 506 | <#function buildQueryParam key debugQueryPrefix="" dynamic=false> 507 | 508 | <#-- only build if it is debugDynamic --> 509 | <#if !dynamic> 510 | <#return "" /> 511 | 512 | 513 | <#local urlParam = "[" + (key?is_number)?string(key, '"' + key + '"') + "]" /> 514 | 515 | <#return (debugQueryPrefix + urlParam) /> 516 | 517 | 518 | 519 | 520 | <#--- 521 | Handles the output of hash_ex and sequences 522 | @param key 523 | @param value 524 | @param depth How deep to expand the object 525 | @param dynamic Boolean to make expandable objects a link to quickly drill-down into data 526 | @param queryParam Parameter used in query string for direct debugging 527 | --> 528 | <#macro complexValue key value depth dynamic queryParam=""> 529 | 530 | <#-- store class name --> 531 | <#if value?is_string && value?is_hash_ex> 532 | <#local className = varClass(value) /> 533 | <#local fullValue = value /><#-- prevent injection --> 534 | <#local shortValue = truncateString(value) /><#-- prevent injection --> 535 | 536 | 537 | <#if (depth > 1) && (value?has_content)> 538 | <@debugTable 539 | debugObject=value 540 | depth=(depth - 1) 541 | dynamic=dynamic 542 | queryParam=queryParam 543 | title=className!'' /> 544 | <#else> 545 | 546 | <#local staticValue = (value?is_hash_ex)?string("hash_ex", "sequence") + "(" + value?size + ")" /> 547 | 548 | <#-- show class name for hash_ex --> 549 | <#if value?is_string && value?is_hash_ex> 550 | <#-- show full value in source --> 551 | 552 | <#local staticValue = staticValue + " " + (shortValue!'') /> 553 | 554 | 555 | <#if dynamic> 556 | ${staticValue} 557 | <#else> 558 | ${staticValue} 559 | 560 | 561 | 562 | 563 | 564 | 565 | <#--- 566 | Truncates large strings 567 | @param string 568 | @param maxLength The length to truncate the string to 569 | @returns string 570 | --> 571 | <#function truncateString string maxLength=100> 572 | 573 | <#if string?is_string && (string?length > maxLength)> 574 | <#return string?substring(0, 100) + "…" /> 575 | 576 | 577 | <#return string /> 578 | 579 | 580 | 581 | 582 | <#--- 583 | Helper macro that outputs a variable type 584 | @param var 585 | --> 586 | <#macro variableType var> 587 | ${var?is_string?string("is_string
", "")} 588 | ${var?is_number?string("is_number
", "")} 589 | ${var?is_boolean?string("is_date
", "")} 590 | ${var?is_date?string("is_date
", "")} 591 | ${var?is_method?string("is_method
", "")} 592 | ${var?is_transform?string("is_transform
", "")} 593 | ${var?is_macro?string("is_macro
", "")} 594 | ${var?is_hash?string("is_hash
", "")} 595 | ${var?is_hash_ex?string("is_hash_ex
", "")} 596 | ${var?is_sequence?string("is_sequence
", "")} 597 | ${var?is_collection?string("is_collection
", "")} 598 | ${var?is_enumerable?string("is_enumerable
", "")} 599 | ${var?is_indexable?string("is_indexable
", "")} 600 | ${var?is_directive?string("is_directive
", "")} 601 | ${var?is_node?string("is_node
", "")} 602 | ${var?has_content?string("has_content
", "")} 603 | 604 | 605 | 606 | <#--- 607 | Gets the class of a var if applicable 608 | @param value 609 | @returns string 610 | --> 611 | <#function varClass value> 612 | 613 | <#if value?is_hash_ex && ((value.class)??)> 614 | <#return value.class /> 615 | 616 | 617 | <#return "" /> 618 | 619 | 620 | 621 | 622 | <#--- 623 | Rebuild the debug object from the url parameter. 624 | @param root 625 | @param query 626 | --> 627 | <#function debugObjectFromUrl root query> 628 | 629 | <#local convertedArray = convertArray(query) /> 630 | 631 | <#return validObject(root, convertedArray) /> 632 | 633 | 634 | 635 | 636 | <#--- 637 | Determine the debug root information based on the query 638 | 639 | @param query 640 | @returns object 641 | --> 642 | <#function getDebugRoot query> 643 | 644 | <#-- default to data_model --> 645 | <#local object = .data_model /> 646 | <#local title = ".data_model" /> 647 | 648 | <#-- .locals --> 649 | <#if query?starts_with(".locals")> 650 | <#local object = .locals /> 651 | <#local title = ".locals" /> 652 | 653 | <#-- .main --> 654 | <#elseif query?starts_with(".main")> 655 | <#local object = .main /> 656 | <#local title = ".main" /> 657 | 658 | <#-- .namespace --> 659 | <#elseif query?starts_with(".namespace")> 660 | <#local object = .namespace /> 661 | <#local title = ".namespace" /> 662 | 663 | 664 | 665 | <#return { "object": object, "title": title } /> 666 | 667 | 668 | 669 | 670 | <#--- 671 | Converts the debug query into an array of strings and numbers 672 | 673 | @param query 674 | @returns array 675 | --> 676 | <#function convertArray query> 677 | 678 | <#if query?contains("[") && query?contains("]")> 679 | <#-- chop off front and back brackets, then split on '][' to form array --> 680 | <#local query = query?substring( 681 | query?index_of("[") + 1, 682 | query?last_index_of("]") 683 | ) /> 684 | 685 | <#if query?contains("][")> 686 | <#local queryArray = query?split("][") /> 687 | <#else> 688 | <#local queryArray = [query] /> 689 | 690 | 691 | <#-- remove quotes for strings, convert others to numbers --> 692 | <#local convertedArray = [] /> 693 | <#list queryArray as q> 694 | <#if q?starts_with('"')> 695 | <#local str = q?substring(1, q?length - 1) /> 696 | <#local convertedArray = convertedArray + [str?j_string] /> 697 | <#else> 698 | <#local convertedArray = convertedArray + [q?number] /> 699 | 700 | 701 | 702 | <#return convertedArray /> 703 | 704 | <#else> 705 | <#return [] /> 706 | 707 | 708 | 709 | 710 | 711 | <#--- 712 | Function to check if the value we're debugging exists 713 | @param debugParent 714 | @param array 715 | @returns boolean 716 | --> 717 | <#function validObject debugParent array> 718 | 719 | <#local obj = debugParent /> 720 | 721 | <#list array as x> 722 | <#if (obj[x]??)> 723 | <#local obj = obj[x] /> 724 | <#else> 725 | <#return -1 /> 726 | 727 | 728 | 729 | <#return obj /> 730 | 731 | 732 | 733 | 734 | <#--- 735 | Builds a top-level list of links for easier traversal 736 | @param query 737 | @returns array 738 | --> 739 | <#function getTitleLink query> 740 | 741 | <#local root = getDebugRoot(query) /> 742 | <#local convertedArray = convertArray(query) /> 743 | 744 | <#local url = getBaseUrl() + root.title /> 745 | 746 | <#local urlList = [{ 747 | "title": root.title, 748 | "url": url 749 | }] /> 750 | 751 | <#list convertedArray as k> 752 | <#if k?is_string> 753 | <#local title = '["' + k + '"]' /> 754 | <#else> 755 | <#local title = '[' + k?string + ']' /> 756 | 757 | 758 | <#local url = url + title /> 759 | 760 | <#local urlList = urlList + [{ 761 | "title": title, 762 | "url": url 763 | }] /> 764 | 765 | 766 | <#return urlList /> 767 | 768 | 769 | --------------------------------------------------------------------------------