Skip to content

Instantly share code, notes, and snippets.

@smileham
Last active July 11, 2025 12:46
Show Gist options
  • Save smileham/578bbbb88dc0ed5a1403f3b98711ec25 to your computer and use it in GitHub Desktop.
Save smileham/578bbbb88dc0ed5a1403f3b98711ec25 to your computer and use it in GitHub Desktop.
Export an ArchiMate diagram to Markdown format. #jarchi
/*
* Export View to Markdown
*
* Requires jArchi - https://www.archimatetool.com/blog/2018/07/02/jarchi/
*
* Markdown - https://www.markdownguide.org/
*
* Version 2: Updated to support Diagram Groups
* Version 2.1: Add check for Selected View
* Version 2.2: Change to regex, added date of export
* Version 2.3: Include notes in documentation
* Version 3: Updated to include Relationships
* Version 3.1: Include name and description
* Version 3.2: Support repeated elements
* Version 3.3: Fix for relationships table
* Version 3.4: Fix for connected notes,
* Quotes in documenation,
* Embed view (experimental)
* Version 3.5: Added support for jArchi 4.4 (additional attributes)
* Version 3.6: Added support for Label Values and fixed issue with CR/LF in tables.
* Version 3.7: Added support for multiple views to be exported & fix for comments
* Version 3.8: Added support for specializations, refactored to improve code.
* Version 3.9: Added support for Index and ViewTypes
* Version 4: Added support to include "hidden" relationships
* Version 5: Added support for "Layering"
* Version 6: Added support for external Gemini AI script - https://gist.github.com/smileham/8cbb3116db7f0ee80bcab4f1a57d14a8
*
* (c) 2025 Steven Mileham
*
*/
console.show();
console.clear();
console.log("Export to Markdown");
const strategyLayer = {"label":"Strategy", "components":["resource","capability","course-of-action","value-stream"]};
const motivationLayer = {"label":"Motivation","components":["stakeholder","driver","assessment","goal","outcome","principle","requirement","constraint","meaning","value"]};
const migrationLayer = {"label":"Implementation and Migration", "components":["work-package","deliverable","implementation-event","plateau","gap"]};
const businessLayer ={"label":"Business", "components":["business-actor", "business-role","business-collaboration","business-interface","business-process","business-function","business-interaction", "business-event", "business-service", "contract", "product", "representation"]};
const dataLayer = {"label":"Data","components":["business-object", "data-object", "artifact"]};
const applicationLayer = {"label":"Application", "components":["application-component","application-collaboration","application-interface","application-function","application-process","application-interaction", "application-event", "application-service"]};
const technologyLayer = {"label":"Technology", "components":["node","device", "system-software", "technology-collaboration", "technology-interface","path", "communication-network", "technology-function", "technology-process", "technology-interaction", "technology-event", "technology-service", "equipment", "facility","location", "distribution-network", "material"]};
const layers = [migrationLayer,strategyLayer,motivationLayer,businessLayer,dataLayer, applicationLayer, technologyLayer];
function executeMarkdownScript() {
const embed = window.confirm("Do you want to embed the view image directly in the Markdown file?\n\n(Click 'OK' for Yes, 'Cancel' for No to link it as a separate file)");
const includeHiddenRelationships = window.confirm("Do you want to include relationships that are hidden on the diagram?\n\n(Click 'OK' for Yes, 'Cancel' for No)");
const layered = window.confirm("Do you want to generate a Layered Architecture Document?\n\n(Click 'OK' for Yes, 'Cancel' for No)");
const theViews = $(selection).filter("archimate-diagram-model");
if (!theViews || theViews.length==0) {
console.log("> Please Select a View");
}
const multiMode = theViews.length>1;
const theIndexMap = new Map();
theViews.each(function(theView){
console.log("Exporting View:"+theView);
theDocument = "";
let markdownContent = generateMarkdown(theView, includeHiddenRelationships, layered,embed);
let theFilename = saveMarkdownToFile(theView, markdownContent,embed);
if (multiMode) {
theIndexMap.set(theView.name,theFilename);
}
});
if (multiMode) {
const theIndex = generateIndex(theIndexMap);
saveIndexToFile(theIndex);
}
}
function generateIndex(theIndexMap) {
theIndexMarkdown = `# ${model.name} Export[^1]\n`;
theIndexMap.forEach(function (value, key, map) {
if (value!=null) {
theIndexMarkdown += `* [${_escapeMD(key)}](${generateIndexLink(value)})\n`;
}
})
theIndexMarkdown+=`\n[^1]: Generated: ${new Date().toLocaleString()}\n`;
return theIndexMarkdown;
}
function convertToText(type) {
return type.replaceAll("-", " ").split(" ").map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ").trim();
}
function _escapeMD(str) {
return str.replaceAll("<", "&lt;").replaceAll("\n>", "\n~QUOTE~").substring(0, 1) + str.substring(1).replaceAll(">", "&gt;").replaceAll("~QUOTE~", ">");
}
function generateLink(str) {
return `#${str.toLowerCase().replace(/[\[\]\(\)\#\\\/\"]/gi, "").replaceAll(" ", "-").replaceAll("\<", "lt").replaceAll("\>", "gt")}`;
}
function _escapeCR(str) {
return str.replaceAll("\r\n", "<br>").replaceAll("\n", "<br>").replaceAll("\r", "<br>");
}
function generateIndexLink(str) {
return str.replaceAll(" ","%20");
}
function generateToc(element, depth, tocMap, tocContentWrapper) {
$(element).children().not("relationship").each(function (e) {
if (e.name) {
let headerDepth = " ".repeat(depth);
const conceptText = convertToText(`${e.type}`);
let theHash = generateLink(`${e.name} (${conceptText})`);
tocMap[theHash] = (tocMap[theHash] || 0) + 1;
const linkNum = tocMap[theHash] > 1 ? `-${tocMap[theHash]}` : "";
tocContentWrapper.str += `\n${headerDepth}* [${_escapeMD(e.name)} (${conceptText})${linkNum.replace("\-", " ")}](${theHash}${linkNum})`;
if ($(e).children().not("relationship").length > 0) {
generateToc(e, depth + 1, tocMap, tocContentWrapper);
}
}
});
}
function generateLayeredToc (layers, tocContentWrapper) {
layers.forEach(function (layer){
tocContentWrapper.str += `\n* [${layer.label} Architecture](#${layer.label}%20Architecture)`;
});
}
function generatePropertiesTable(element) {
const props = element.prop();
const sortedProperties = [...props].sort();
let header = "|", line = "|", body = "|";
if (element.specialization) {
header += "Specialization|";
line += "---|";
body += `${element.specialization}|`;
}
for (const prop of sortedProperties) {
header += `${prop}|`;
line += "---|";
body += `${element.prop(prop)}|`;
}
return `**Properties**\n\n${header}\n${line}\n${body}\n`;
}
function _contains(element, collection) {
let response = false;
collection.each(function (e) {
if (e.type !== "diagram-model-connection" && e.concept.id == element.concept.id) {
response = true;
}
});
return response
}
function _relationshipTable(element, includeHidden, viewRels){
let table ="";
if (includeHidden) {
hiddenElement = !_contains(element,viewRels);
}
else {
hiddenElement = false;
}
if ((includeHidden && hiddenElement) || !hiddenElement) {
if (element.type !== "diagram-model-connection") {
let row = `|${element.source.name}|${convertToText(element.type)}`;
if (element.concept.accessType) row += ` (${element.concept.accessType})`;
if (element.concept.influenceStrength) row += ` (${element.concept.influenceStrength})`;
if (element.concept.specialization) row += ` (${element.concept.specialization})`;
row += `${hiddenElement?" (Hidden)":""}|[${_escapeMD(element.target.name)} (${convertToText(element.target.type)})](${generateLink(`${element.target.name} (${convertToText(element.target.type)})`)})|${element.labelValue ? element.labelValue.replaceAll("\\n", " ").replaceAll("\\r", " ") : element.name}|${element.documentation.replaceAll("\n", " ").replaceAll("\r", " ")}|\n`;
table += row;
}
}
return table
}
function generateRelationshipsTable(element, includeHidden) {
let table = "|From|Relationship|To|Name/Label|Description|\n|---|---|---|---|---|\n";
if (includeHidden) {
viewRelationships = $(element).rels();
allRelationships = $(element.concept).rels();
}
else {
allRelationships = $(element).rels();
viewRelationships = allRelationships;
}
allRelationships.each(function (r) {
table+= _relationshipTable(r, includeHidden, viewRelationships);
});
return `**Relationships**\n\n${table}`;
}
function generateNestedDocumentation(element, depth, bodyMap, documentContentWrapper, includeHiddenRelationships) {
// console.log(`${element}`);
$(element).children("diagram-model-note").each(function (e) {
if ($(e).rels().length === 0) {
documentContentWrapper.str += `\n> ${_escapeMD(e.text).replaceAll("\n", "\n> ")}\n`;
}
});
$(element).children().not("relationship").each(function (e) {
// console.log(`CHILD: ${e}`);
if (e.name) {
const headerDepth = "#".repeat(depth + 2);
const conceptText = convertToText(`${e.type}`);
let theHash = generateLink(`${e.name} (${conceptText})`);
bodyMap[theHash] = (bodyMap[theHash] || 0) + 1;
const linkNum = bodyMap[theHash] > 1 ? ` ${bodyMap[theHash]}` : "";
documentContentWrapper.str += `\n${headerDepth} ${_escapeMD(e.name)} (${conceptText})${linkNum}\n`;
if (e.documentation) {
documentContentWrapper.str += `\n${_escapeMD(e.documentation)}\n`;
}
if (e.prop().length > 0 || e.specialization) {
documentContentWrapper.str += `\n${_escapeMD(generatePropertiesTable(e))}`;
}
if ($(e).outRels().length > 0) {
documentContentWrapper.str += `\n${_escapeMD(generateRelationshipsTable(e, includeHiddenRelationships))}`;
}
$(e).rels().ends().each(function (r) {
if (r.text) {
documentContentWrapper.str += `\n> ${_escapeMD(r.text).replaceAll("\n", "\n> ")}\n`;
}
});
if ($(e).children().length > 0) {
generateNestedDocumentation(e, depth + 1, bodyMap, documentContentWrapper, includeHiddenRelationships);
}
}
});
}
function generateIntroduction(view) {
let theIntroduction = "";
// Notes with no relationships
$(view).find("diagram-model-note").each(function (note){
if ($(note).rels().length==0 && note.text.length()>3){
theIntroduction += `\n> ${_escapeMD(note.text).replaceAll("\n", "\n> ")}\n`;
}
});
return theIntroduction;
}
const generateMarkdown = (view, includeHiddenRelationships, layered, embed) => {
let bodyMap = {};
let documentContentWrapper = {str: `# ${view.name}[^1]\n`};
// Javascript will pass an object reference!
let tocContentWrapper = {str:"* [Introduction](#introduction)"};
//toc(0,theView);
let tocMap = {};
if (!layered) {
generateToc(view,0,tocMap,tocContentWrapper);
}
else {
generateLayeredToc(layers, tocContentWrapper);
}
documentContentWrapper.str += `\n## Table of Contents\n${tocContentWrapper.str}\n\n## Introduction\n`;
if (embed) {
const bytes = $.model.renderViewAsBase64(view, "PNG", { scale: 2, margin: 10 });
documentContentWrapper.str += `\n![${view.name}](data:image/png;base64,${bytes})\n`;
} else {
documentContentWrapper.str += `\n![${view.name}][embedView]\n`;
}
if (view.documentation) {
documentContentWrapper.str += `\n${_escapeMD(view.documentation)}\n`;
}
if (view.viewpoint && view.viewpoint.name!="None") {
documentContentWrapper.str+= `Viewpoint: ${view.viewpoint.name}\n`;
}
documentContentWrapper.str+= generateIntroduction(view);
if (!layered) {
generateNestedDocumentation(view, 0, bodyMap, documentContentWrapper, includeHiddenRelationships);
}
else {
generateLayeredDocumentation(view, documentContentWrapper);
}
documentContentWrapper.str+=`\n[^1]: Generated: ${new Date().toLocaleString()}\n`;
return documentContentWrapper.str;
}
function generateLayeredDocumentation(view, documentContentWrapper) {
let theLayer="";
layers.forEach(function (layer){
theTable= generateComponentTable(layer.components, view);
if (theTable!="") {
theLayer +=`\n## ${layer.label} Architecture\n\n${theTable}`;
}
});
documentContentWrapper.str+=theLayer;
//console.log(`${documentContentWrapper.str}`);
}
function _sortComponents(collection) {
collection.sort(function(a, b) {
// Access the 'name' property of each element and use localeCompare for string sorting
return a.name.localeCompare(b.name);
});
return collection;
}
function _uniqueComponents(collection) {
let uniqueCollection = new Map();
collection.forEach(function(component) {
if (uniqueCollection.get(`${component.name}:${component.type}`)==null){
uniqueCollection.set(`${component.name}:${component.type}`,component);
}
});
return uniqueCollection;
}
function generateComponentTable(layer, view) {
const theHeader ="|Component|Type|Description|\n|---|---|---|\n";
const theHeaderWithNotes ="|Component|Type|Description|Notes|\n|---|---|---|---|\n";
let theTable ="";
let includesNotes = false;
layer.forEach(function(component) {
components = $(view).find(component);
let sortedComponents = _sortComponents(components);
let uniqueComponents = _uniqueComponents(sortedComponents);
uniqueComponents.forEach(function (e) {
let notes = "";
$(e).rels().ends().filter("diagram-model-note").each(function (note){
notes+=note.text+"\n";
includesNotes=true;
})
theTable += `|${_escapeMD(e.name)}|${convertToText(e.type)}${e.specialization!=null?" ("+e.specialization+")":""}|${_escapeCR(_escapeMD(e.documentation))}|${notes!=""?_escapeCR(_escapeMD(notes))+"|":""}\n`;
});
});
if (theTable!="") {
if (includesNotes){
return theHeaderWithNotes+theTable;
}
else {
return theHeader+theTable;
}
}
else {
return "";
}
}
function saveMarkdownToFile(view, markdownContent,embed) {
const defaultFileName = view.name ? `${model.name}-${view.name}.md` : "Exported View.md";
const exportFile = window.promptSaveFile({ title: "Export to Markdown", filterExtensions: ["*.md"], fileName: defaultFileName });
if(exportFile) {
if (!embed) {
const imageURL = exportFile.substring(0,exportFile.length-3).replaceAll(" ","%20")+".png";
const relativeURL = imageURL.split("\\");
var bytes = $.model.renderViewAsBase64(view, "PNG", {scale: 2, margin: 10});
$.fs.writeFile(exportFile.substring(0,exportFile.length-3) +".png", bytes, "BASE64");
markdownContent+=`\n[embedView]: ${relativeURL[relativeURL.length-1]}`;
}
$.fs.writeFile(exportFile, markdownContent);
console.log("> Export done");
return exportFile;
}
else {
console.log("> Export cancelled");
}
}
function saveIndexToFile(markdownContent) {
const defaultFileName = "index.md";
const exportFile = window.promptSaveFile({ title: "Export to Markdown", filterExtensions: ["*.md"], fileName: defaultFileName });
if(exportFile) {
$.fs.writeFile(exportFile, markdownContent);
console.log("> Export done");
return exportFile;
}
else {
console.log("> Export cancelled");
}
}
try {
if (typeof library===undefined || !library) {
}
}
catch (e){
executeMarkdownScript();
};
module.exports=generateMarkdown;
@maxim-ge
Copy link

Thanks for this example

@onderwijsarchitectuur
Copy link

Very useful. Thanks.

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