|
/* |
|
* 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 |
|
* Version 6.1: Include "Used in Views" as option, Replaced Options Screen, Enable multiple exports. |
|
* Version 6.2: Fix for "Junction" components |
|
* Version 6.2.1: Include "UID" against components. |
|
* Version 6.2.2: Include "UID" for relationships. |
|
* Verison 6.3: Added option to make TOC optional. |
|
* |
|
* (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 listSelection(title, choices) { |
|
var ListSelectionDialog = Java.type("org.eclipse.ui.dialogs.ListSelectionDialog"); |
|
var LabelProvider = Java.type('org.eclipse.jface.viewers.LabelProvider'); |
|
var ArrayContentProvider = Java.type('org.eclipse.jface.viewers.ArrayContentProvider'); |
|
var dialog = new ListSelectionDialog(shell, choices, ArrayContentProvider.getInstance(), |
|
new LabelProvider(), title); |
|
|
|
dialog.open(); |
|
result = dialog.getResult(); |
|
return result ? new String(result) : null; |
|
} |
|
|
|
function executeMarkdownScript() { |
|
let theOptions =["Include TOC","Embed Image in Markdown", "Generate Layered Document", "Include Hidden Relationships", "Include Component View List", "Include UID for Components"]; |
|
|
|
const theViews = $(selection).filter("archimate-diagram-model"); |
|
|
|
|
|
if (!theViews || theViews.length==0) { |
|
console.log("> Please Select a View"); |
|
exit(); |
|
} |
|
|
|
const multiMode = theViews.length>1; |
|
if (multiMode) { |
|
theOptions.push("Generate a Single Document from the selected views"); |
|
} |
|
|
|
const options = listSelection("Options", theOptions); |
|
if (options == null) { |
|
console.log("Options dialog cancelled. Exiting."); |
|
exit(); |
|
} |
|
|
|
const embed = options.includes("Embed"); |
|
const layered = options.includes("Layered"); |
|
const includeHiddenRelationships = options.includes("Hidden"); |
|
const showViews = options.includes("View"); |
|
const singleDoc = options.includes("Single"); |
|
const inclObjUID = options.includes("UID for Components"); |
|
const inclTOC = options.includes("TOC"); |
|
let megaDoc = ""; |
|
|
|
const theIndexMap = new Map(); |
|
|
|
theViews.each(function(theView){ |
|
|
|
console.log("Exporting View:"+theView); |
|
|
|
theDocument = ""; |
|
|
|
let markdownContent = generateMarkdown(theView, includeHiddenRelationships, layered,embed, showViews, inclObjUID, inclTOC); |
|
if (!singleDoc) { |
|
let theFilename = saveMarkdownToFile(theView, markdownContent,embed); |
|
if (multiMode) { |
|
theIndexMap.set(theView.name,theFilename); |
|
} |
|
} |
|
else { |
|
megaDoc += `${markdownContent}\n—\n`; |
|
} |
|
|
|
}); |
|
|
|
if (multiMode) { |
|
if (singleDoc) { |
|
saveIndexToFile(megaDoc); |
|
} |
|
else { |
|
const theIndex = generateIndex(theIndexMap); |
|
saveIndexToFile(theIndex); |
|
} |
|
} |
|
} |
|
|
|
function _getPath(theComponent) { |
|
var theParent = $(theComponent).parent()[0]; |
|
if (theParent && theParent.name) { |
|
return `${_getPath(theParent)}\\${theComponent.name}`; |
|
} |
|
else { |
|
return theComponent.name; |
|
} |
|
} |
|
|
|
function _usedInViews(element) { |
|
let viewContent = "**Used In Views**\n\n" |
|
let theViews = $(element.concept).viewRefs(); |
|
|
|
theViews.each(function (theView) { |
|
viewContent += `* ${_getPath(theView)}/${theView.name}\n`; |
|
}); |
|
return viewContent; |
|
} |
|
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) { |
|
let theComponents = _sortComponents($(element).children().not("relationship")); |
|
theComponents.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, inclObjUID) { |
|
const props = element.prop(); |
|
const sortedProperties = […props].sort(); |
|
|
|
let header = "|", line = "|", body = "|"; |
|
|
|
if (inclObjUID) { |
|
header += "UID|"; |
|
line += "—|"; |
|
body += `${element.concept !=null? element.concept.id:element.id}|`; |
|
} |
|
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 _relationshipRow(element, includeHidden, viewRels, inclObjUID){ |
|
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.source.type)})|${convertToText(element.type)}`; |
|
if (inclObjUID) row = `|${element.concept !=null? element.concept.id:element.id}${row}`; |
|
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, inclObjUID) { |
|
let header = "|From|Relationship|To|Name/Label|Description|"; |
|
let line = "|—|—|—|—|—|"; |
|
|
|
|
|
if (inclObjUID) { |
|
header = `|UID${header}`; |
|
line += "—|"; |
|
} |
|
|
|
let table = `${header}\n${line}\n`; |
|
|
|
if (includeHidden) { |
|
viewRelationships = $(element).rels(); |
|
allRelationships = $(element.concept).rels(); |
|
} |
|
else { |
|
allRelationships = $(element).rels(); |
|
viewRelationships = allRelationships; |
|
} |
|
//console.log(allRelationships); |
|
allRelationships.each(function (r) { |
|
table+= _relationshipRow(r, includeHidden, viewRelationships, inclObjUID); |
|
}); |
|
|
|
|
|
return `**Relationships**\n\n${table}`; |
|
} |
|
|
|
function generateNestedDocumentation(element, depth, bodyMap, documentContentWrapper, includeHiddenRelationships, showViews, inclObjUID) { |
|
// console.log(`${element}`); |
|
let theNotes = _sortComponents($(element).children("diagram-model-note")); |
|
theNotes.each(function (e) { |
|
if ($(e).rels().length === 0) { |
|
documentContentWrapper.str += `\n> ${_escapeMD(e.text).replaceAll("\n", "\n> ")}\n`; |
|
} |
|
}); |
|
let theComponents = _sortComponents($(element).children().not("relationship")); |
|
theComponents.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.type=='junction') { |
|
documentContentWrapper.str += `\nType: ${e.concept.getJunctionType()}\n`; |
|
} |
|
|
|
if (e.prop().length > 0 || e.specialization || inclObjUID) { |
|
documentContentWrapper.str += `\n${_escapeMD(generatePropertiesTable(e, inclObjUID))}`; |
|
} |
|
if ($(e).outRels().length > 0 || ($(e.concept).rels().length > 0 && includeHiddenRelationships) ) { |
|
documentContentWrapper.str += `\n${_escapeMD(generateRelationshipsTable(e, includeHiddenRelationships, inclObjUID))}`; |
|
} |
|
|
|
$(e).rels().ends().each(function (r) { |
|
if (r.text) { |
|
documentContentWrapper.str += `\n> ${_escapeMD(r.text).replaceAll("\n", "\n> ")}\n`; |
|
} |
|
}); |
|
if (showViews) { |
|
documentContentWrapper.str += `\n${_usedInViews(e)}`; |
|
} |
|
|
|
if ($(e).children().length > 0) { |
|
generateNestedDocumentation(e, depth + 1, bodyMap, documentContentWrapper, includeHiddenRelationships,showViews, inclObjUID); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
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 && $(note).parent.type=="archimate-diagram-model"){ |
|
theIntroduction += `\n> ${_escapeMD(note.text).replaceAll("\n", "\n> ")}\n`; |
|
} |
|
}); |
|
|
|
return theIntroduction; |
|
} |
|
|
|
const generateMarkdown = (view, includeHiddenRelationships, layered, embed, showViews, inclObjUID, inclTOC) => { |
|
let bodyMap = {}; |
|
let documentContentWrapper = {str: `# ${view.name}[^1]\nFolder: ${_getPath(view)}\n`}; |
|
|
|
// Javascript will pass an object reference! |
|
let tocContentWrapper = {str:"* [Introduction](#introduction)"}; |
|
|
|
//toc(0,theView); |
|
|
|
let tocMap = {}; |
|
if (inclTOC) { |
|
if (!layered) { |
|
generateToc(view,0,tocMap,tocContentWrapper); |
|
} |
|
else { |
|
generateLayeredToc(layers, tocContentWrapper); |
|
} |
|
documentContentWrapper.str += `\n## Table of Contents\n${tocContentWrapper.str}\n\n## Introduction\n`; |
|
} |
|
else |
|
{ |
|
documentContentWrapper.str += `\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,showViews, inclObjUID); |
|
} |
|
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 = `${model.name}.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; |
Nice work. Thanks for sharing!
Phil
LikeLike
[…] a given view, exporting the standard attributes and custom properties as headers. After putting my Export to Markdown script together, I felt like I could try and take a crack at putting this together in JArchi, with […]
LikeLike
Hello, those scripts are amazing!! just a question, do you know how debug Jarchi scripts with VS code ?
LikeLike
[…] powered Enterprise Architecture journey. These scripts make use of a previous script I created to Export a Model to Markdown. That script, combined with the Google Gemini API and some (fairly basic at the moment) prompts […]
LikeLike