├── 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 | #if>
61 |
62 | <#local title = varClass(debugObject) />
63 |
64 |
65 | <@debugTable
66 | debugObject=debugObject
67 | depth=depth
68 | title=title!'' />
69 |
70 | #macro>
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 | #if>
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 | #if>
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 | #if>
110 |
111 | #macro>
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 | #list>
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 | #list>
144 | #if>
145 | @tableWrapper>
146 | <#else>
147 | <@simpleValue value=debugObject />
148 | #if>
149 |
150 | #macro>
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 | #compress>
275 | #macro>
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 |
291 | <#-- TODO: ugly logic, fix -->
292 | <#if title?is_sequence>
293 |
294 | <#list title as x>
295 | <#if x_has_next>
296 | ${x.title?xhtml}<#t/>
297 | <#else>
298 | ${x.title?xhtml}<#t/>
299 | #if>
300 | #list>
301 |
302 | <#elseif titleUrl?has_content>
303 |
304 | ${title?xhtml}
305 |
306 | <#else>
307 | ${title?xhtml}
308 | #if>
309 | |
310 |
311 | #if>
312 |
313 | | Key |
314 | Value |
315 |
316 |
317 |
318 | <#nested />
319 |
320 |
321 | #compress>
322 | #macro>
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 | #if>
338 |
339 | <#return false />
340 |
341 | #function>
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 | #if>
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 | #if>
364 | #if>
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 | #if>
379 | |
380 |
381 | #macro>
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 | #if>
395 |
396 | <#list settings.ignoredPatterns as pattern>
397 | <#if key?starts_with(pattern)>
398 | <#return true />
399 | #if>
400 | #list>
401 | #if>
402 |
403 | <#return false />
404 |
405 | #function>
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 | #if>
452 |
453 | <#else>
454 | (empty)
455 | #if>
456 | #macro>
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 | #if>
473 |
474 | <#return url />
475 |
476 | #function>
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 | #if>
492 | #list>
493 | #if>
494 |
495 | <#return (url + settings.queryParamKey + "=") />
496 |
497 | #function>
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 | #if>
512 |
513 | <#local urlParam = "[" + (key?is_number)?string(key, '"' + key + '"') + "]" />
514 |
515 | <#return (debugQueryPrefix + urlParam) />
516 |
517 | #function>
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 | #if>
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 | #if>
554 |
555 | <#if dynamic>
556 | ${staticValue}
557 | <#else>
558 | ${staticValue}
559 | #if>
560 | #if>
561 |
562 | #macro>
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 | #if>
576 |
577 | <#return string />
578 |
579 | #function>
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 | #macro>
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 | #if>
616 |
617 | <#return "" />
618 |
619 | #function>
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 | #function>
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 | #if>
664 |
665 | <#return { "object": object, "title": title } />
666 |
667 | #function>
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 | #if>
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 | #if>
700 | #list>
701 |
702 | <#return convertedArray />
703 |
704 | <#else>
705 | <#return [] />
706 | #if>
707 |
708 | #function>
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 | #if>
727 | #list>
728 |
729 | <#return obj />
730 |
731 | #function>
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 | #if>
757 |
758 | <#local url = url + title />
759 |
760 | <#local urlList = urlList + [{
761 | "title": title,
762 | "url": url
763 | }] />
764 | #list>
765 |
766 | <#return urlList />
767 |
768 | #function>
769 |
--------------------------------------------------------------------------------