User macros are pretty great. There are so many awesome things that you can accomplish with them. This is a user macro I wrote a few years ago to take a CSV file attached to a page and render it out as a table.
If you would like to know more about how this user macro works check out part 2 of this article. I had to split the article into two parts because there is a 20,000 character limit. Specifically, in part two I go into a bit of detail how I got a file attachment drop down in a user macro. "Say what! That's not possible!"
Oh, but it is. :)
Update:
01/09/2020:Updated to make the table sortable.
Macro Name: csv_table
Macro Title: CSV Table
Description: This will take an attached CSV file and render it as a table.
Categories: Confluence Content, Formatting
Macro Body Processing: No macro body
Template:
## Developed by: Davin Studer
## Date created: 05/07/2014
## @param CSVName:title=CSV File|type=string|required=false|desc=The CSV file
## @param TopHeader:title=Top Header Row|type=boolean|required=false|default=false|desc=Check if the top row should be a header row?
## @param Page:title=Page Title|type=confluence-content|required=false|desc=If not specified, the current page is used.
###########################################################################
## These are used for getting around velocity issues when writing jQuery ##
###########################################################################
#set( $d = '$' )
#set( $p = '#' )
##############################
## Create a unique id value ##
##############################
#set( $id=$action.dateFormatter.calendar.timeInMillis )
#if( $paramPage && $paramPage != "" )
##########################
## Get the page manager ##
##########################
#set( $containerManagerClass=$content.class.forName('com.atlassian.spring.container.ContainerManager') )
#set( $getInstanceMethod=$containerManagerClass.getDeclaredMethod('getInstance',null) )
#set( $containerManager=$getInstanceMethod.invoke(null,null) )
#set( $containerContext=$containerManager.containerContext )
#set( $pageManager=$containerContext.getComponent('pageManager') )
##########################
## Find the page ##
##########################
#set( $parts = $paramPage.split(":") )
#set( $i = 0 )
#set( $len = 0 )
#set( $key = "" )
#set( $name = "" )
##Having trouble finding out the length/size of $parts ... brute force it
#foreach( $part in $parts )
#set( $len = $len + 1 )
#end
#if( $len == 1 )
#set( $key = $content.spaceKey )
#set( $name = $paramPage )
#else
#foreach( $part in $parts )
#if( $i == 0 )
#set( $key = $part )
#set( $i = $i + 1 )
#else
#set( $name = $part )
#end
#end
#end
#set( $tempcontent = "" )
#if( $pageManager )
#set( $tempcontent = $pageManager.getPage($key, $name) )
#end
#if( $tempcontent && $tempcontent != "" )
#set( $content = $tempcontent )
#else
The page "$paramPage" was not found in this space.
#set( $content = "" )
#end
#end
#set( $attachments = $attachmentManager.getLatestVersionsOfAttachments($content) )
############################################################################
## Could not find a method to sort the $attachments in Alphabetical order ##
## Must sort them myself ... bubble sort ##
############################################################################
#set( $size = $attachments.size() )
#foreach( $junk in $attachments )
#set( $count = -1 )
#foreach( $line in $attachments )
#set( $count = $count + 1 )
#if( $velocityCount < $size )
#if( $line.getTitle().compareToIgnoreCase($attachments.get($velocityCount).getTitle()) > 0 )
#set( $tmp = $attachments.get($velocityCount) )
#set( $junk = $attachments.set($velocityCount, $line) )
#set( $junk = $attachments.set($count, $tmp) )
#end
#end
#end
#end
#set( $baseUrl = $action.getGlobalSettings().getBaseUrl() )
#foreach( $attachment in $attachments )
#if( $attachment.getTitle().toLowerCase() == $paramCSVName.toLowerCase() )
#set( $url = $attachment.getDownloadPathWithoutVersion() )
#end
#end
#########################################################################
## This will only run on the macro properties dialog. It will add a ##
## dropdown to select a CSV file. ##
#########################################################################
<script type="text/javascript">
AJS.toInit(function(){
/////////////////////////////////////////////////
// Are we on the macro properties edit screen? //
/////////////////////////////////////////////////
if(self != top && window.parent.AJS.${d}('${p}macro-browser-preview').length > 0) {
var options = '<option value="">Select a file</option>';
var csvInput = window.parent.AJS.${d}('${p}macro-param-CSVName');
var pageInput = window.parent.AJS.${d}('${p}macro-param-Page');
var refresh = window.parent.AJS.${d}('${p}macro-browser-preview-link');
var secondBlur = false;
//Hide cvsInput ... this will be updated programatically by our new dropdown
csvInput.css('display','none');
//Remove copy input if it exists ... macro preview refresh
window.parent.AJS.${d}('${p}macro-param-CSVName-copy').remove();
//Add copy field
csvInput.before('<select class="select" id="macro-param-CSVName-copy" name="macro-param-CSVName-copy"></select>');
//Reference to new field
var csvInputCopy = window.parent.AJS.${d}('${p}macro-param-CSVName-copy');
//Change original csvInput field when dropdown copy is changed
csvInputCopy.change(function(){
csvInput.val(csvInputCopy.val());
});
//Register change event for page input to populate the attachments for that page
//This focus/blur workaround is because if you select a page from the page hint
//popup the blur and change events won't have the full value right away. They will
//only see what the user typed which may not be the complete page title. So, after 500
//milliseconds we will focus it and then blur it to get the real value. The secondBlur boolean
//is to make sure we only run the blur event on the second time around.
pageInput.change(function(){
secondBlur = false;
setTimeout(function(){
secondBlur = true;
window.parent.AJS.${d}('${p}macro-param-Page').focus();
window.parent.AJS.${d}('${p}macro-param-Page').blur();
}, 500);
});
pageInput.blur(function(){
if(secondBlur) {
secondBlur = false;
//Clear csvInput since we are doing a preview refresh to get new dropdown values
csvInput.val('');
//Refresh the preview window which will trigger a fetch of the attachments from the specified page.
refresh.click();
}
});
csvInputCopy.html('');
#foreach( $attachment in $attachments )
#if($attachment.getFileExtension() == "csv")
#if($attachment.getTitle().toLowerCase() == $paramCSVName.toLowerCase())
#set( $selected = ' selected="selected"' )
#else
#set( $selected = '' )
#end
options += '<option value="$attachment.getTitle()"$selected>$attachment.getTitle()</option>';
#end
#end
if(options == '<option value="">Select a file</option>') {
csvInput.val('');
}
csvInputCopy.html(options);
}
});
</script>
#########################################################################
## End ##
#########################################################################
##########################
## Render the CSV table ##
##########################
#if(${url} && ${url} != "")
<script type="text/javascript">
//CSV parser from https://github.com/evanplaice/jquery-csv
RegExp.escape=function(e){return e.replace(/[-\/\\^${d}*+?.()|[\]{}]/g,"\\${d}&")};AJS.${d}.csv={defaults:{separator:",",delimiter:'"',headers:true},hooks:{castToScalar:function(e,t){var n=/\./;if(isNaN(e)){return e}else{if(n.test(e)){return parseFloat(e)}else{var r=parseInt(e);if(isNaN(r)){return null}else{return r}}}}},parsers:{parse:function(e,t){function f(){o=0;u="";if(t.start&&t.state.rowNum<t.start){s=[];t.state.rowNum++;t.state.colNum=1;return}if(t.onParseEntry===undefined){i.push(s)}else{var e=t.onParseEntry(s,t.state);if(e!==false){i.push(e)}}s=[];if(t.end&&t.state.rowNum>=t.end){a=true}t.state.rowNum++;t.state.colNum=1}function l(){if(t.onParseValue===undefined){s.push(u)}else{var e=t.onParseValue(u,t.state);if(e!==false){s.push(e)}}u="";o=0;t.state.colNum++}var n=t.separator;var r=t.delimiter;if(!t.state.rowNum){t.state.rowNum=1}if(!t.state.colNum){t.state.colNum=1}var i=[];var s=[];var o=0;var u="";var a=false;var c=RegExp.escape(n);var h=RegExp.escape(r);var p=/(D|S|\r\n|\n|\r|[^DS\r\n]+)/;var d=p.source;d=d.replace(/S/g,c);d=d.replace(/D/g,h);p=RegExp(d,"gm");e.replace(p,function(e){if(a){return}switch(o){case 0:if(e===n){u+="";l();break}if(e===r){o=1;break}if(/^(\r\n|\n|\r)${d}/.test(e)){l();f();break}u+=e;o=3;break;case 1:if(e===r){o=2;break}u+=e;o=1;break;case 2:if(e===r){u+=e;o=1;break}if(e===n){l();break}if(/^(\r\n|\n|\r)${d}/.test(e)){l();f();break}throw new Error("CSVDataError: Illegal State [Row:"+t.state.rowNum+"][Col:"+t.state.colNum+"]");case 3:if(e===n){l();break}if(/^(\r\n|\n|\r)${d}/.test(e)){l();f();break}if(e===r){throw new Error("CSVDataError: Illegal Quote [Row:"+t.state.rowNum+"][Col:"+t.state.colNum+"]")}throw new Error("CSVDataError: Illegal Data [Row:"+t.state.rowNum+"][Col:"+t.state.colNum+"]");default:throw new Error("CSVDataError: Unknown State [Row:"+t.state.rowNum+"][Col:"+t.state.colNum+"]")}});if(s.length!==0){l();f()}return i},splitLines:function(e,t){function a(){s=0;if(t.start&&t.state.rowNum<t.start){o="";t.state.rowNum++;return}if(t.onParseEntry===undefined){i.push(o)}else{var e=t.onParseEntry(o,t.state);if(e!==false){i.push(e)}}o="";if(t.end&&t.state.rowNum>=t.end){u=true}t.state.rowNum++}var n=t.separator;var r=t.delimiter;if(!t.state.rowNum){t.state.rowNum=1}var i=[];var s=0;var o="";var u=false;var f=RegExp.escape(n);var l=RegExp.escape(r);var c=/(D|S|\n|\r|[^DS\r\n]+)/;var h=c.source;h=h.replace(/S/g,f);h=h.replace(/D/g,l);c=RegExp(h,"gm");e.replace(c,function(e){if(u){return}switch(s){case 0:if(e===n){o+=e;s=0;break}if(e===r){o+=e;s=1;break}if(e==="\n"){a();break}if(/^\r${d}/.test(e)){break}o+=e;s=3;break;case 1:if(e===r){o+=e;s=2;break}o+=e;s=1;break;case 2:var i=o.substr(o.length-1);if(e===r&&i===r){o+=e;s=1;break}if(e===n){o+=e;s=0;break}if(e==="\n"){a();break}if(e==="\r"){break}throw new Error("CSVDataError: Illegal state [Row:"+t.state.rowNum+"]");case 3:if(e===n){o+=e;s=0;break}if(e==="\n"){a();break}if(e==="\r"){break}if(e===r){throw new Error("CSVDataError: Illegal quote [Row:"+t.state.rowNum+"]")}throw new Error("CSVDataError: Illegal state [Row:"+t.state.rowNum+"]");default:throw new Error("CSVDataError: Unknown state [Row:"+t.state.rowNum+"]")}});if(o!==""){a()}return i},parseEntry:function(e,t){function u(){if(t.onParseValue===undefined){i.push(o)}else{var e=t.onParseValue(o,t.state);if(e!==false){i.push(e)}}o="";s=0;t.state.colNum++}var n=t.separator;var r=t.delimiter;if(!t.state.rowNum){t.state.rowNum=1}if(!t.state.colNum){t.state.colNum=1}var i=[];var s=0;var o="";if(!t.match){var a=RegExp.escape(n);var f=RegExp.escape(r);var l=/(D|S|\n|\r|[^DS\r\n]+)/;var c=l.source;c=c.replace(/S/g,a);c=c.replace(/D/g,f);t.match=RegExp(c,"gm")}e.replace(t.match,function(e){switch(s){case 0:if(e===n){o+="";u();break}if(e===r){s=1;break}if(e==="\n"||e==="\r"){break}o+=e;s=3;break;case 1:if(e===r){s=2;break}o+=e;s=1;break;case 2:if(e===r){o+=e;s=1;break}if(e===n){u();break}if(e==="\n"||e==="\r"){break}throw new Error("CSVDataError: Illegal State [Row:"+t.state.rowNum+"][Col:"+t.state.colNum+"]");case 3:if(e===n){u();break}if(e==="\n"||e==="\r"){break}if(e===r){throw new Error("CSVDataError: Illegal Quote [Row:"+t.state.rowNum+"][Col:"+t.state.colNum+"]")}throw new Error("CSVDataError: Illegal Data [Row:"+t.state.rowNum+"][Col:"+t.state.colNum+"]");default:throw new Error("CSVDataError: Unknown State [Row:"+t.state.rowNum+"][Col:"+t.state.colNum+"]")}});u();return i}},helpers:{collectPropertyNames:function(e){var t,n,r=[];for(t in e){for(n in e[t]){if(e[t].hasOwnProperty(n)&&r.indexOf(n)<0&&typeof e[t][n]!=="function"){r.push(n)}}}return r}},toArray:function(e,t,n){var t=t!==undefined?t:{};var r={};r.callback=n!==undefined&&typeof n==="function"?n:false;r.separator="separator"in t?t.separator:AJS.${d}.csv.defaults.separator;r.delimiter="delimiter"in t?t.delimiter:AJS.${d}.csv.defaults.delimiter;var i=t.state!==undefined?t.state:{};var t={delimiter:r.delimiter,separator:r.separator,onParseEntry:t.onParseEntry,onParseValue:t.onParseValue,state:i};var s=AJS.${d}.csv.parsers.parseEntry(e,t);if(!r.callback){return s}else{r.callback("",s)}},toArrays:function(e,t,n){var t=t!==undefined?t:{};var r={};r.callback=n!==undefined&&typeof n==="function"?n:false;r.separator="separator"in t?t.separator:AJS.${d}.csv.defaults.separator;r.delimiter="delimiter"in t?t.delimiter:AJS.${d}.csv.defaults.delimiter;var i=[];var t={delimiter:r.delimiter,separator:r.separator,onPreParse:t.onPreParse,onParseEntry:t.onParseEntry,onParseValue:t.onParseValue,onPostParse:t.onPostParse,start:t.start,end:t.end,state:{rowNum:1,colNum:1}};if(t.onPreParse!==undefined){t.onPreParse(e,t.state)}i=AJS.${d}.csv.parsers.parse(e,t);if(t.onPostParse!==undefined){t.onPostParse(i,t.state)}if(!r.callback){return i}else{r.callback("",i)}},toObjects:function(e,t,n){var t=t!==undefined?t:{};var r={};r.callback=n!==undefined&&typeof n==="function"?n:false;r.separator="separator"in t?t.separator:AJS.${d}.csv.defaults.separator;r.delimiter="delimiter"in t?t.delimiter:AJS.${d}.csv.defaults.delimiter;r.headers="headers"in t?t.headers:AJS.${d}.csv.defaults.headers;t.start="start"in t?t.start:1;if(r.headers){t.start++}if(t.end&&r.headers){t.end++}var i=[];var s=[];var t={delimiter:r.delimiter,separator:r.separator,onPreParse:t.onPreParse,onParseEntry:t.onParseEntry,onParseValue:t.onParseValue,onPostParse:t.onPostParse,start:t.start,end:t.end,state:{rowNum:1,colNum:1},match:false,transform:t.transform};var o={delimiter:r.delimiter,separator:r.separator,start:1,end:1,state:{rowNum:1,colNum:1}};if(t.onPreParse!==undefined){t.onPreParse(e,t.state)}var u=AJS.${d}.csv.parsers.splitLines(e,o);var a=AJS.${d}.csv.toArray(u[0],t);var i=AJS.${d}.csv.parsers.splitLines(e,t);t.state.colNum=1;if(a){t.state.rowNum=2}else{t.state.rowNum=1}for(var f=0,l=i.length;f<l;f++){var c=AJS.${d}.csv.toArray(i[f],t);var h={};for(var p in a){h[a[p]]=c[p]}if(t.transform!==undefined){s.push(t.transform.call(undefined,h))}else{s.push(h)}t.state.rowNum++}if(t.onPostParse!==undefined){t.onPostParse(s,t.state)}if(!r.callback){return s}else{r.callback("",s)}},fromArrays:function(e,t,n){var t=t!==undefined?t:{};var r={};r.callback=n!==undefined&&typeof n==="function"?n:false;r.separator="separator"in t?t.separator:AJS.${d}.csv.defaults.separator;r.delimiter="delimiter"in t?t.delimiter:AJS.${d}.csv.defaults.delimiter;var i="",s,o,u,a;for(u=0;u<e.length;u++){s=e[u];o=[];for(a=0;a<s.length;a++){var f=s[a]===undefined||s[a]===null?"":s[a].toString();if(f.indexOf(r.delimiter)>-1){f=f.replace(r.delimiter,r.delimiter+r.delimiter)}var l="\n|\r|S|D";l=l.replace("S",r.separator);l=l.replace("D",r.delimiter);if(f.search(l)>-1){f=r.delimiter+f+r.delimiter}o.push(f)}i+=o.join(r.separator)+"\r\n"}if(!r.callback){return i}else{r.callback("",i)}},fromObjects:function(e,t,n){var t=t!==undefined?t:{};var r={};r.callback=n!==undefined&&typeof n==="function"?n:false;r.separator="separator"in t?t.separator:AJS.${d}.csv.defaults.separator;r.delimiter="delimiter"in t?t.delimiter:AJS.${d}.csv.defaults.delimiter;r.headers="headers"in t?t.headers:AJS.${d}.csv.defaults.headers;r.sortOrder="sortOrder"in t?t.sortOrder:"declare";r.manualOrder="manualOrder"in t?t.manualOrder:[];r.transform=t.transform;if(typeof r.manualOrder==="string"){r.manualOrder=AJS.${d}.csv.toArray(r.manualOrder,r)}if(r.transform!==undefined){var i=e;e=[];var s;for(s=0;s<i.length;s++){e.push(r.transform.call(undefined,i[s]))}}var o=AJS.${d}.csv.helpers.collectPropertyNames(e);if(r.sortOrder==="alpha"){o.sort()}if(r.manualOrder.length>0){var u=[].concat(r.manualOrder);var a;for(a=0;a<o.length;a++){if(u.indexOf(o[a])<0){u.push(o[a])}}o=u}var f,a,l,c=[],h;if(r.headers){c.push(o)}for(f=0;f<e.length;f++){l=[];for(a=0;a<o.length;a++){h=o[a];if(h in e[f]&&typeof e[f][h]!=="function"){l.push(e[f][h])}else{l.push("")}}c.push(l)}return AJS.${d}.csv.fromArrays(c,t,r.callback)}}
AJS.toInit(function(){
AJS.${d}.get("${baseUrl}${url}", function( data ) {
var result = AJS.${d}.csv.toArrays(data);
var html = '';
var count = 0;
var topHeader = $paramTopHeader;
for(var row in result) {
if(count == 0) {
if(topHeader) {
html += '<thead>\r\n';
html += '<tr>\r\n';
for(var item in result[row]) {
html += '<th class="confluenceTh">' + result[row][item] + '</th>\r\n';
}
html += '</tr>\r\n';
html += '</thead>\r\n';
html += '<tbody>\r\n';
} else {
html += '<tbody>\r\n';
html += '<tr>\r\n';
for(var item in result[row]) {
html += '<td class="confluenceTd">' + result[row][item] + '</td>\r\n';
}
html += '</tr>\r\n';
}
} else {
html += '<tr>\r\n';
for(var item in result[row]) {
html += '<td class="confluenceTd">' + result[row][item] + '</td>\r\n';
}
html += '</tr>\r\n';
}
count++
}
html += '</tbody>\r\n';
AJS.$('#csv-table-$id').html(html);
if(topHeader) {
AJS.tablessortable.setTableSortable(AJS.$('#csv-table-$id'));
}
});
});
</script>
#end
<table id="csv-table-$id" class="confluenceTable tablesorter">
</table>
Davin Studer
Business Intelligence Engineer
Vancouver, WA
480 accepted answers
9 comments