Skip to content

Instantly share code, notes, and snippets.

@EnterTheQuirk
Created January 24, 2020 08:23
Show Gist options
  • Save EnterTheQuirk/6472cccac6cdc94ee133c78b8a73d591 to your computer and use it in GitHub Desktop.
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
/*
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