Last active
July 11, 2025 12:46
-
-
Save smileham/578bbbb88dc0ed5a1403f3b98711ec25 to your computer and use it in GitHub Desktop.
Export an ArchiMate diagram to Markdown format. #jarchi
This file contains hidden or 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
/* | |
* 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("<", "<").replaceAll("\n>", "\n~QUOTE~").substring(0, 1) + str.substring(1).replaceAll(">", ">").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\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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for this example