Skip to content

Instantly share code, notes, and snippets.

@rich-biker
Last active September 20, 2024 04:14
Show Gist options
  • Save rich-biker/9a3c86c5a576ce0d8639856f3ee81651 to your computer and use it in GitHub Desktop.
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.
/*
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");
@EnterTheQuirk
Copy link

Thanks @rich-biker. New to Archi, coming from Sparx Enterprise Architect.

Customising reports in Jaspersoft is woefully under-documented. This is enough to get me across the line - so I'm extremely grateful!

For those new to Archi, here's some more info for getting started:

A good overview of what the script above is all about: https://irp-cdn.multiscreensite.com/aee068df/files/uploaded/ArchiMate%20Documentation.pdf

To get started:

  1. Install the jArchi plugin to enable scripting within the Archi GUI. You can compile it yourself for free, or become a Patreon for a (very) small fee (as low as $1) to access the binary: https://www.archimatetool.com/plugins/
  2. Once it's installed, open Archi and go to the Script manager. Add a new script, copying the code above
  3. Create a new Archi view (what the guide calls a "driving" view. Viewpoint doesn't matter). Add Group(s) onto the view as per the guide linked above. Drag view(s) from your model tree onto the "driving" view. Add trigger flows between the groups.
  4. With the driving view selected in the model tree, go to the Scripts Manager, select your script and hit execute.
  5. Choose somewhere to save the generated doco
  6. You're done!

Also note:

@EnterTheQuirk
Copy link

@rich-biker
I've made my own changes. Mostly so I can configure what gets included in the documentation for each group.
I've created a secret gist because I don't have the tools currently to fork. Hope to get to it one day...
https://gist.github.com/EnterTheQuirk/6472cccac6cdc94ee133c78b8a73d591

I've setup some variables:

  • IncludeDiagram
  • IncludeViewElements
  • IncludeProperties

If you include those variable names as properties on a group in a driving view, setting the value to "true" or "false", then any nested views or groups will inherit those properties.

Found it useful so I can reuse the same views and show the diagram in one part of a document, but saving the catalogue for an Appendix. Under the top level "Appendix" group, I can turn off "IncludeDiagram", then drop in the same views that I used earlier, and all sub branches will have diagrams turned off.

@rich-biker
Copy link
Author

Thanks for the kind comments. I'll incorporate your ideas when I get chance. I have already got a version that toggles properties, but hadn't thought of doing it on the driving groups themselves. Nice.

I hadn't realised how useful this was going to be, but when I get the "can I just have a quick PDF of those views" requests, I'm can turn it round fast and make it look good.

@jbsarrodie
Copy link

Hi,

I don't know how this is at all possible, but I only noticed this script today.

@rich-biker thank you very much for sharing this script and same for @EnterTheQuirk and @steveblamey for you customized versions.

Some time ago I worked on a similar idea while wanted to experiment with what could be a better way to generate documentation in Archi. I think several of the ideas I had at that time could profit from your script, so I'm listing them together with some more generic remarks here to share (hopefully I'll come back later to say I've implemented them) :

  • I personally don't recommend using Grouping elements as this pollutes your model with groupings that have no real architecture meaning. I would suggest you to use Visual Groups and connections, which you can also query in jArchi but have no side effect on your model.
  • Some of the options (set as properties) that have been described could be renamed to match the ones that are already used by Archi itself when it generate jasper reports using the default template (e.g. a property Report:View:Detailed set to true will print the detailed list of elements...). The full list is included in Archi's user manual.
  • While this requires some changes to jArchi (but Phil or I could implement them), in an ideal world I'd use Canvas instead of an ArchiMate views to define reports. The big advantage of Canvas being that you can save Canvas templates and then create new Canvas through these templates. This would make it much more easier to share pre-defined report structure.
  • With only some small changes, your script could be used with RemarkJS. I've played a bit with Remark and discovered simple ways to define some "master slides" and reuse them all across the presentation. If you don't know it already I'd suggest also looking at this example of what could be done. BTW, RemakJS presentation can easily be saved as PDF from within a browser.
  • It should also be quite simple for me to use the same kind of view structure and "parsing rules" as an optional way to generate single page HTML exports.

So it seems to me that we could well end up with a set of conventions to describe a report table of content, that could be used by multiple scripts to generate documents, presentations, websites...

@bdendulk
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment