Last active
September 20, 2024 04:14
-
-
Save rich-biker/9a3c86c5a576ce0d8639856f3ee81651 to your computer and use it in GitHub Desktop.
Generate Markdown documentation from a driving view in Archi using jArchi scripting.
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) | |
28-Jan-2020 - Outputs the linked views in the visual left-right top-bottom order they are drawn in a driving group. This was tricky and probably messy code, but it works. | |
29-Jan-2020 - includes a hardNewpage variable to drop the text NEWPAGE into the output if your target is MS Word. This allows for post processing find-replace to swap it for proper new pages. It does this for all header levels in the listofNewpageheaders variable. | |
*/ | |
// 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; // false will store the images outside of the generated markdown; true is embedded inside | |
var hardNewpage = false; // if true, the text NEWPAGE will be put in the output for post processing in MS Word to find and replace for a proper new page. | |
var listofNewpageheaders = [1, 2, 3]; // these will generate the hardNewpage (if true) for these header levels | |
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 | |
"IncludeDocumentation": true, // if true, will include the view's documentation text (which itself can have markdown, by the way) | |
"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 thisPath = ""; | |
var imageURL = ""; | |
var viewList = []; | |
if (!Levelobj) { | |
return null; | |
} else { | |
// Find the view references composed within this group and put them into an array so we can sort them. | |
$(Levelobj).children().each(function (viewRefs) { | |
if ((viewRefs) && ((viewRefs.type == 'archimate-diagram-model') || (viewRefs.type == 'sketch-model'))) { | |
// Find the actual linked views | |
var viewsCollection = $('archimate-diagram-model'); | |
viewsCollection.add($('sketch-model')); | |
viewsCollection.each(function (linkedView) { | |
// this doesn't cater for duplicate view names, sorry | |
if (linkedView.name === viewRefs.name) { | |
viewList.push([viewRefs, linkedView]); | |
} | |
}); | |
} | |
}); | |
// sort viewList by x then y bounds. Effectively allows views to go top left to bottom right in order. | |
// this is complicated by the need to refer to the viewRefs part of the pair. | |
viewList.sort(function (left, right) { | |
return left[0].bounds.x - right[0].bounds.x; | |
}); | |
viewList.sort(function (top, bottom) { | |
return top[0].bounds.y - bottom[0].bounds.y; | |
}); | |
// now go through the sorted viewList | |
for (var k = 0; k < viewList.length; k++) { | |
var myView = viewList[k][0]; | |
var myRef = viewList[k][1]; | |
outputHdr(Level + 1, myView.name, true); | |
// Include the view's diagram (if desired) | |
if (settings.IncludeDiagram === true) { | |
var bytes = $.model.renderViewAsBase64(myRef, "PNG", { | |
scale: 1, | |
margin: 10 | |
}); | |
if (embed) { | |
outInfo += "\n![" + myView.name + "](data:image/png;base64," + bytes + ")\n"; | |
} else { | |
thisPath = path + myView.name; | |
$.fs.writeFile(thisPath + ".png", bytes, "BASE64"); | |
imageURL = thisPath.replaceAll(" ", "%20"); | |
outInfo += "\n![Diagram: " + myView.name + "][" + myView.name + "]\n"; | |
outInfo += "\n[" + myView.name + "]: " + imageURL + ".png\n"; | |
} | |
} | |
if (settings.IncludeDocumentation === true) { | |
myView.documentation != "" ? outInfo += "\n" + myView.documentation + "\n" : true; | |
} | |
// Now document the view details (if desired) | |
if (settings.IncludeViewElements === true) { | |
printViewElements(myRef, 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"; | |
} | |
if (Name != "Element Catalogue") { | |
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 | |
} | |
// put a fudge post processing to insert 'NEWPAGE' in for header levels listed in listofNewpageheaders | |
if (hardNewpage) { | |
if (listofNewpageheaders.indexOf(Level) != -1) { | |
outInfo += '\nNEWPAGE\n'; | |
} | |
} | |
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() { | |
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 IncludeDiagram setting: " + inclusionSettings["IncludeDiagram"]); | |
console.log("Default IncludeDocumentation setting: " + inclusionSettings["IncludeDocumentation"]); | |
console.log("Default IncludeVIewElements setting: " + inclusionSettings["IncludeViewElements"]); | |
console.log("Default 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["IncludeDocumentation"] === 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 checkIncludeDocumentation = settingsElement.prop("IncludeDocumentation"); | |
var checkIncludeElements = settingsElement.prop("IncludeViewElements"); | |
var checkIncludeProperties = settingsElement.prop("IncludeProperties"); | |
if (checkIncludeDiagram !== null) { | |
settings["IncludeDiagram"] = checkIncludeDiagram === "true" ? true : false; | |
} | |
if (checkIncludeDocumentation !== null) { | |
settings["IncludeDocumentation"] = checkIncludeDocumentation === "true" ? true : false; | |
} | |
if (checkIncludeElements !== null) { | |
settings["IncludeViewElements"] = checkIncludeElements === "true" ? true : false; | |
} | |
if (checkIncludeProperties !== null) { | |
settings["IncludeProperties"] = checkIncludeProperties === "true" ? true : false; | |
} | |
return settings; | |
} | |
// Main Code | |
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
Hello, I was just today made aware of this script. The possibility to only export parts of my model is a useful feature. However, when running the script, there is no image of the view in the exported markdown file although the corresponding settings are set to true (I just copied the script and added it whithin the jScript editor of Archi). Please advice.