-
-
Save EnterTheQuirk/6472cccac6cdc94ee133c78b8a73d591 to your computer and use it in GitHub Desktop.
Generate Markdown documentation from a driving view in Archi using jArchi scripting. Based on rich-biker's work ... fork to come
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Sourced: https://gist.github.com/rich-biker/9a3c86c5a576ce0d8639856f3ee81651 | |
Script: Documentation Generation | |
Purpose: To generate output based on a driving view | |
Author: Richard Heward - Tame Blue Lion Ltd | |
This generates a markdown file with the embedded images and text based upon a driving view in Archi of groups that trigger each other and embedded views. | |
See my blog for a more detailed explaination. https://www.tamebluelion.co.uk/blog/archi-documentation | |
Setting embed to false will have the images saved to file and references placed in the document. It's then up to your markdown engine. This isn't that well tested. | |
Setting | |
Note - markdown can be converted to PDF / Word Docs or anything. I've used pandoc command line to do this. | |
Created: 8-Oct-2019 | |
11-Oct-2019 - Included sketch views in the driving view. | |
27-Nov-2019 - Added improved object layout as tables, plus their properties | |
29-Nov-2019 - Now includes all view objects via a .find instead of a .children. | |
12-Dec-2019 - Sorts the properties so they are grouped by element type | |
24-Jan-2020 - Allowed inclusion settings to be set by group. Inherited by nested groups. Made catalogue columns more dynamic (configurable in future?). Refactored recursive functions. (Jared Pritchard) | |
*/ | |
// Get current date | |
var currentDate = new Date().toLocaleString("en-US", {day: 'numeric', month: 'short', year: 'numeric'}); | |
console.show(); | |
console.clear(); | |
console.log("Documentation Generation @", currentDate); | |
var Verbose = false; | |
var Sections = []; | |
var Errors = []; | |
var nextOne = null; | |
var outInfo = ""; | |
var theToc = ""; | |
var path = ""; | |
var fileName = ""; | |
var embed = false; | |
var drivingView = null; // will be set to the selected view that has all the groups and view references on. | |
// Below is a hashtable of settings which define what to include in each section of the document. If not overridden by a group, these settings will apply to the entire document generated from a driving view. | |
// A property of the same name of the settings below, with a value of true/false, on the driving view or a group, will override this value for anything nested under that section of the document, unless overridden again. | |
var DefaultInclusionSettings = { | |
"IncludeDiagram": true, // if true, will include the view's diagram | |
"IncludeViewElements": true, // if true, will include a catalogue of the view's elements | |
"IncludeProperties": true, // if true, will include the "properties" field in a catalogue of elements from a view | |
//TODO: "ElementColumns": [{name: "Name", field: "name"}], // overrides the list of columns to include in the element catalogue (need to find a structure we can easily set in a property that we hopefully don't have to parse) | |
}; | |
// Shallow clones attributes of a basic object | |
function shallowClone(obj) { | |
// If the object provided is not actually an object, return null so we don't accidentally clobber some other reference | |
if (null === obj || "object" !== typeof obj) { | |
return null; | |
} | |
// Create a new, blank, object, then copy over the attributes | |
var copy = {}; | |
for (var attr in obj) { | |
copy[attr] = obj[attr]; | |
} | |
return copy; | |
} | |
function generateLink(theString) { | |
// this function thanks to Steven Mileham | |
var regex = /[\[\]\(\)\#\\\/\"]/gi; | |
return "#"+theString.toLowerCase().replace(regex, "") | |
.replaceAll(" [ -]*", "-") // originally .replaceAll(" ", "-"), but caused issues where name had a dash, eg. "MyProject - Some Viewpoint - Domain" | |
.replaceAll("\<", "lt") | |
.replaceAll("\>", "gt"); | |
} | |
function replaceNL(strIn) { | |
if (null === strIn || "string" !== typeof strIn ) return ""; | |
var newStr = strIn.replace( /[\r\n]+/gm, "<br>"); | |
return newStr; | |
} | |
function addPropsAsItalic(thisObj) { | |
// Bold keys take up even less space | |
var theProperties = thisObj.prop(); | |
if (theProperties) { | |
for (key in theProperties) { | |
if ((theProperties[key] != 'label') && (theProperties[key] != "drill down")) { | |
outInfo += "*" +theProperties[key] +":* " +thisObj.prop(theProperties[key]) +"<br>"; | |
} | |
} | |
} | |
} | |
// Prints (documents) the elements and maybe properties (not relations) on the view | |
function printViewElements(view, level, settings) { | |
var objList = []; | |
$(view).find("element").each(function(viewObj) { | |
objList.push(viewObj); | |
}); | |
objList.sort(); // ensures the output groups by object type | |
// Print heading | |
outputHdr (level+1, "Element Catalogue", false); | |
outInfo += "\n"; | |
// Define which columns should be included in the catalogue | |
var columns = []; | |
columns.push({"name": "Name", "field": "name"}); | |
columns.push({"name": "Type", "field": "type"}); | |
columns.push({"name": "Description", "field": "documentation"}); | |
if (settings.IncludeProperties === true) { | |
columns.push({"name": "Properties", "special": "properties"}); | |
} | |
// Print column headers | |
var columnString = ""; | |
var columnBorder = ""; | |
for (var c = 0; c < columns.length; c++) | |
{ | |
columnString += "| " + columns[c].name; | |
columnBorder += "|:--------"; | |
} | |
outInfo += columnString += "\n"; | |
outInfo += columnBorder += "|\n"; | |
// For each row (element) | |
var i; | |
for (i in objList) | |
{ | |
// Print each desired field under the table column for the row | |
for (var j = 0; j < columns.length; j++) | |
{ | |
// Start the cell | |
outInfo += "|"; | |
// Check columns for special cases | |
if (columns[j].special != null ) | |
{ | |
// If the special case is element properties, and we want to print properties... | |
if (columns[j].special === "properties" && settings.IncludeProperties === true) | |
{ | |
addPropsAsItalic(objList[i]); | |
} | |
} | |
else | |
{ | |
// Default printing of a cell's data. If it's the first column though, bold it. | |
outInfo += (j === 0) ? "**" : ""; | |
outInfo += replaceNL(objList[i][columns[j].field]); | |
outInfo += (j === 0) ? "**" : ""; | |
} | |
} | |
// Complete the last cell of the row | |
outInfo += "|\n"; | |
}; | |
} | |
function getViews(Level, Levelobj, settings) { | |
var thisView = null; | |
var thisPath = ""; | |
var imageURL = ""; | |
if (!Levelobj) { | |
return null; | |
} | |
else { | |
// Find the view references composed within this group | |
$(Levelobj).children().each(function(viewRefs) { | |
if ((viewRefs) && ((viewRefs.type == 'archimate-diagram-model' ) || (viewRefs.type == 'sketch-model' )) ) { | |
outputHdr (Level+1, viewRefs.name, true); | |
// Find the actual linked views | |
var viewsCollection = $('archimate-diagram-model'); | |
viewsCollection.add($('sketch-model')); | |
viewsCollection.each(function(linkedView) { | |
if (linkedView.name == viewRefs.name) { | |
//console.log(" Linked Item: ", linkedView.name, " -> ", linkedView.id); | |
// Include the view's diagram (if desired) | |
if (settings.IncludeDiagram === true) | |
{ | |
var bytes = $.model.renderViewAsBase64(linkedView, "PNG", {scale: 1, margin: 10}); | |
if (embed) { | |
outInfo+="\n!["+linkedView.name+"](data:image/png;base64,"+bytes+")\n"; | |
} | |
else { | |
thisPath = path +linkedView.name; | |
$.fs.writeFile(thisPath +".png", bytes, "BASE64"); | |
imageURL = thisPath.replaceAll(" ","%20"); | |
outInfo+="\n![Diagram: "+linkedView.name+"][" +linkedView.name +"]\n"; | |
outInfo += "\n[" +linkedView.name +"]: " +imageURL +".png\n"; | |
} | |
} | |
linkedView.documentation!=""?outInfo+="\n"+linkedView.documentation+"\n":true; | |
// Now document the view details (if desired) | |
if (settings.IncludeViewElements === true) { | |
printViewElements(linkedView, Level+1, settings); | |
} | |
} | |
}); | |
} | |
}); | |
} | |
} | |
function addSpace(numSpaces) { | |
var i; | |
var rtnText = " "; | |
for (i = 0; i < numSpaces; i++) { | |
rtnText += " "; | |
} | |
return rtnText; | |
} | |
function outputHdr (Level, Name, AddLink, Doc) { | |
var indent = ""; | |
var tocIndent = ""; | |
for (var i = 0; i < Level; i++) { | |
indent = indent +"#"; | |
} | |
for (var j = 0; j < Level-1; j++) { // ToC needs one less indent tab. | |
tocIndent = tocIndent +"\t"; | |
} | |
//console.log("Level ", Level, " (", Name, ")"); | |
console.log(addSpace(Level-1), Name); | |
var outHdr = indent +" " +Name; | |
if (Level == 1) { | |
outInfo += '<div style="page-break-before: always;"></div>'; | |
outInfo += "\n ___ \n"; // horiz line before level 1's | |
} | |
outInfo += "\n" +outHdr; | |
// Add a link to table of contents (TOC), if requested | |
if (AddLink) | |
{ | |
var thisLink = generateLink(Name); | |
outInfo += "\n" +"[](" +thisLink +")"; | |
theToc += tocIndent +"* [" +Name +"](" +thisLink +")\n"; | |
} | |
if (Doc) { | |
outInfo += "\n" +Doc; | |
} | |
} | |
// Finds the group's sub-group, if any | |
// Returns true if no errors were encountered | |
function getSubGroups(group, nextLevel, parentInclusionSettings) { | |
var outcome = true; | |
$(group).outRels("composition-relationship").each(function(subGroup) { | |
var incomingRels2 = $(subGroup.target).inRels("triggering-relationship").size(); | |
// If it's the first child in the sub group | |
if (incomingRels2 == 0) { | |
// add the sub group onto the array | |
outcome = outcome && addGroup(subGroup.target, nextLevel, parentInclusionSettings); | |
} | |
else { | |
// just ignore the rest, getSubGroups / getNextGroup will take care of them. | |
} | |
}); | |
return outcome; | |
} | |
// Finds the next sibling group in the series | |
// Returns true if no errors were encountered | |
function getNextGroup(group, level, parentInclusionSettings) { | |
var nextGroup = null; | |
var outgoingRels = $(group).outRels("triggering-relationship").size(); | |
if (outgoingRels == 1) { | |
// There's a triggering out so find the next.. | |
nextGroup = $(group).outRels("triggering-relationship").first(); | |
if (nextGroup) { | |
// add the next one onto the array | |
return addGroup(nextGroup.target, level, parentInclusionSettings); | |
} | |
else { | |
window.alert("The groups should all use triggering relationships"); | |
return false; | |
} | |
} | |
return true; | |
} | |
// Adds a group to the list of sections to output in the document, and orchestrates a recursive grab of the next sub group & sibling group (if existing) | |
// Returns true if no errors were encountered | |
function addGroup(group, level, parentInclusionSettings) { | |
// Get this group's updated inclusion settings | |
var settings = getGroupInclusionSettings(group, parentInclusionSettings); | |
if (settings === null) | |
{ | |
Errors.push( { message: "Group is missing settings", object: group} ); | |
return false; | |
} | |
// Add the group to the list of sections | |
Sections.push({"group": group, "level": level, "settings": settings}); | |
// Look for more sub groups under this one | |
getSubGroups(group, level + 1, settings) | |
// Look for sibling groups | |
getNextGroup(group, level, parentInclusionSettings) | |
return true; | |
} | |
function useDrivingView() { | |
var nextGroup = null; | |
drivingView = selection.filter("archimate-diagram-model").first(); | |
if (!drivingView) { | |
window.alert("Please open and select a Driving View for the documentation"); | |
} | |
else | |
{ | |
console.log("Driving view is: " + drivingView.name); | |
var inclusionSettings = getGroupInclusionSettings(drivingView, DefaultInclusionSettings); | |
console.log("Default driving view IncludeDiagram setting: " + inclusionSettings["IncludeDiagram"]); | |
console.log("Default driving view IncludeVIewElements setting: " + inclusionSettings["IncludeViewElements"]); | |
console.log("Default driving view IncludeProperties setting: " + inclusionSettings["IncludeProperties"]); | |
// Go through each immediate child group in the view, find the first group(s) in a series | |
var outcome = true; | |
$(drivingView).children("grouping").each(function(thisGroup) { | |
if (thisGroup) { | |
var incomingRels = $(thisGroup).inRels("triggering-relationship").size(); | |
if (incomingRels == 0) { | |
// It's a top-level section, put it in the array. | |
outcome = outcome && addGroup(thisGroup, 1, inclusionSettings); | |
} | |
else { | |
// Ignore if if there's an incoming triggering relationship ... our recursive getNextGroup function will find it. | |
} | |
} | |
}); | |
if (!outcome) { | |
window.alert("Error when extracting a group"); | |
console.log("Error stack:"); | |
for (var i = 0; i < Errors.length; i++) | |
{ | |
console.log("- " + Errors[i].message); | |
if (Verbose) | |
{ | |
console.log(" " + Errors[i].object); | |
} | |
} | |
} | |
} | |
return(true); | |
} // end of useDrivingView | |
// Get the settings for what to include in this branch of a document hierarchy | |
// settingsElement: reference to the driving view or a group which may have overriding settings | |
// defaultSettings: settings object to use as default (required) | |
function getGroupInclusionSettings(settingsElement, defaultSettings) { | |
// Check default settings | |
if ( defaultSettings === null | |
|| typeof defaultSettings !== "object" | |
|| defaultSettings["IncludeDiagram"] === null | |
|| defaultSettings["IncludeViewElements"] === null | |
|| defaultSettings["IncludeProperties"] === null | |
) { | |
window.alert("Default settings were not correctly passed to a child node"); | |
return(null); | |
} | |
var settings = shallowClone(defaultSettings); | |
// Check for overrides | |
var checkIncludeDiagram = settingsElement.prop("IncludeDiagram"); | |
var checkIncludeElements = settingsElement.prop("IncludeViewElements"); | |
var checkIncludeProperties = settingsElement.prop("IncludeProperties"); | |
if (checkIncludeDiagram !== null) | |
{ | |
settings["IncludeDiagram"] = checkIncludeDiagram === "true" ? true : false; | |
} | |
if (checkIncludeElements !== null) | |
{ | |
settings["IncludeViewElements"] = checkIncludeElements === "true" ? true : false; | |
} | |
if (checkIncludeProperties !== null) | |
{ | |
settings["IncludeProperties"] = checkIncludeProperties === "true" ? true : false; | |
} | |
return settings; | |
} | |
var docGen = ""; | |
if (useDrivingView()) | |
{ | |
var exportFile = window.promptSaveFile({ title: "Export to File", filterExtensions: [ "*.md" ], fileName: drivingView.name+".md" } ); | |
// where's the path. Find where the last slash delimiter is | |
var lastSlash = ""; | |
if (exportFile) { | |
if (exportFile.indexOf("/") == -1) { | |
lastSlash = exportFile.lastIndexOf("\\"); // Windows | |
} else { | |
lastSlash = exportFile.lastIndexOf("/"); // Mac or Linux | |
} | |
path = exportFile.substring(0,lastSlash+1); | |
fileName = exportFile.substring(lastSlash+1, exportFile.length); | |
console.log("path: ", exportFile.substring(0,lastSlash+1)); | |
console.log("fileName: ", exportFile.substring(lastSlash+1,exportFile.length)); | |
// go through the array, and output. | |
for (var i = 0; i < Sections.length; i++) { | |
outputHdr (Sections[i].level, Sections[i].group.name, true, Sections[i].group.documentation); | |
getViews (Sections[i].level, Sections[i].group, Sections[i].settings); | |
} | |
docGen = "# "+drivingView.name +"\n" | |
docGen += "\n ___ \n"; | |
docGen += theToc +"\n"; | |
docGen += outInfo; | |
docGen += "\n\n``Generated on: " +currentDate +"``"; | |
$.fs.writeFile(exportFile, docGen); | |
} | |
} | |
// end of script | |
console.log("Done"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment