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
Online forums and learning are now in one easy-to-use experience.
By continuing, you accept the updated Community Terms of Use and acknowledge the Privacy Policy. Your public name, photo, and achievements may be publicly visible and available in search engines.
9 comments