UNPKG

mermaid

Version:

Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.

4 lines 89.2 kB
{ "version": 3, "sources": ["../../../src/diagrams/architecture/architectureParser.ts", "../../../src/diagrams/architecture/architectureTypes.ts", "../../../src/diagrams/architecture/architectureDb.ts", "../../../src/diagrams/architecture/architectureStyles.ts", "../../../src/diagrams/architecture/architectureRenderer.ts", "../../../src/diagrams/architecture/architectureIcons.ts", "../../../src/diagrams/architecture/svgDraw.ts", "../../../src/diagrams/architecture/architectureDiagram.ts"], "sourcesContent": ["import type { Architecture } from '@mermaid-js/parser';\nimport { parse } from '@mermaid-js/parser';\nimport { log } from '../../logger.js';\nimport type { ParserDefinition } from '../../diagram-api/types.js';\nimport { populateCommonDb } from '../common/populateCommonDb.js';\nimport type { ArchitectureDB } from './architectureTypes.js';\nimport { db } from './architectureDb.js';\n\nconst populateDb = (ast: Architecture, db: ArchitectureDB) => {\n populateCommonDb(ast, db);\n ast.groups.map(db.addGroup);\n ast.services.map((service) => db.addService({ ...service, type: 'service' }));\n ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' }));\n // @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type?\n ast.edges.map(db.addEdge);\n};\n\nexport const parser: ParserDefinition = {\n parse: async (input: string): Promise<void> => {\n const ast: Architecture = await parse('architecture', input);\n log.debug(ast);\n populateDb(ast, db);\n },\n};\n", "import type { DiagramDBBase } from '../../diagram-api/types.js';\nimport type { ArchitectureDiagramConfig } from '../../config.type.js';\nimport type { D3Element } from '../../types.js';\nimport type cytoscape from 'cytoscape';\n\n/*=======================================*\\\n| Architecture Diagram Types |\n\\*=======================================*/\n\nexport type ArchitectureAlignment = 'vertical' | 'horizontal' | 'bend';\n\nexport type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';\nexport type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;\nexport type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;\n\n/**\n * Contains LL, RR, TT, BB which are impossible connections\n */\nexport type InvalidArchitectureDirectionPair = `${ArchitectureDirection}${ArchitectureDirection}`;\nexport type ArchitectureDirectionPair = Exclude<\n InvalidArchitectureDirectionPair,\n 'LL' | 'RR' | 'TT' | 'BB'\n>;\nexport type ArchitectureDirectionPairXY = Exclude<\n InvalidArchitectureDirectionPair,\n 'LL' | 'RR' | 'TT' | 'BB' | 'LR' | 'RL' | 'TB' | 'BT'\n>;\n\nexport const ArchitectureDirectionName = {\n L: 'left',\n R: 'right',\n T: 'top',\n B: 'bottom',\n} as const;\n\nexport const ArchitectureDirectionArrow = {\n L: (scale: number) => `${scale},${scale / 2} 0,${scale} 0,0`,\n R: (scale: number) => `0,${scale / 2} ${scale},0 ${scale},${scale}`,\n T: (scale: number) => `0,0 ${scale},0 ${scale / 2},${scale}`,\n B: (scale: number) => `${scale / 2},0 ${scale},${scale} 0,${scale}`,\n} as const;\n\nexport const ArchitectureDirectionArrowShift = {\n L: (orig: number, arrowSize: number) => orig - arrowSize + 2,\n R: (orig: number, _arrowSize: number) => orig - 2,\n T: (orig: number, arrowSize: number) => orig - arrowSize + 2,\n B: (orig: number, _arrowSize: number) => orig - 2,\n} as const;\n\nexport const getOppositeArchitectureDirection = function (\n x: ArchitectureDirection\n): ArchitectureDirection {\n if (isArchitectureDirectionX(x)) {\n return x === 'L' ? 'R' : 'L';\n } else {\n return x === 'T' ? 'B' : 'T';\n }\n};\n\nexport const isArchitectureDirection = function (x: unknown): x is ArchitectureDirection {\n const temp = x as ArchitectureDirection;\n return temp === 'L' || temp === 'R' || temp === 'T' || temp === 'B';\n};\n\nexport const isArchitectureDirectionX = function (\n x: ArchitectureDirection\n): x is ArchitectureDirectionX {\n const temp = x as ArchitectureDirectionX;\n return temp === 'L' || temp === 'R';\n};\n\nexport const isArchitectureDirectionY = function (\n x: ArchitectureDirection\n): x is ArchitectureDirectionY {\n const temp = x as ArchitectureDirectionY;\n return temp === 'T' || temp === 'B';\n};\n\nexport const isArchitectureDirectionXY = function (\n a: ArchitectureDirection,\n b: ArchitectureDirection\n) {\n const aX_bY = isArchitectureDirectionX(a) && isArchitectureDirectionY(b);\n const aY_bX = isArchitectureDirectionY(a) && isArchitectureDirectionX(b);\n return aX_bY || aY_bX;\n};\n\nexport const isArchitecturePairXY = function (\n pair: ArchitectureDirectionPair\n): pair is ArchitectureDirectionPairXY {\n const lhs = pair[0] as ArchitectureDirection;\n const rhs = pair[1] as ArchitectureDirection;\n const aX_bY = isArchitectureDirectionX(lhs) && isArchitectureDirectionY(rhs);\n const aY_bX = isArchitectureDirectionY(lhs) && isArchitectureDirectionX(rhs);\n return aX_bY || aY_bX;\n};\n\n/**\n * Verifies that the architecture direction pair does not contain an invalid match (LL, RR, TT, BB)\n * @param x - architecture direction pair which could potentially be invalid\n * @returns true if the pair is not LL, RR, TT, or BB\n */\nexport const isValidArchitectureDirectionPair = function (\n x: InvalidArchitectureDirectionPair\n): x is ArchitectureDirectionPair {\n return x !== 'LL' && x !== 'RR' && x !== 'TT' && x !== 'BB';\n};\n\nexport type ArchitectureDirectionPairMap = Partial<Record<ArchitectureDirectionPair, string>>;\n\n/**\n * Creates a pair of the directions of each side of an edge. This function should be used instead of manually creating it to ensure that the source is always the first character.\n *\n * Note: Undefined is returned when sourceDir and targetDir are the same. In theory this should never happen since the diagram parser throws an error if a user defines it as such.\n * @param sourceDir - source direction\n * @param targetDir - target direction\n * @returns\n */\nexport const getArchitectureDirectionPair = function (\n sourceDir: ArchitectureDirection,\n targetDir: ArchitectureDirection\n): ArchitectureDirectionPair | undefined {\n const pair: `${ArchitectureDirection}${ArchitectureDirection}` = `${sourceDir}${targetDir}`;\n return isValidArchitectureDirectionPair(pair) ? pair : undefined;\n};\n\n/**\n * Given an x,y position for an arrow and the direction of the edge it belongs to, return a factor for slightly shifting the edge\n * @param param0 - [x, y] coordinate pair\n * @param pair - architecture direction pair\n * @returns a new [x, y] coordinate pair\n */\nexport const shiftPositionByArchitectureDirectionPair = function (\n [x, y]: number[],\n pair: ArchitectureDirectionPair\n): number[] {\n const lhs = pair[0] as ArchitectureDirection;\n const rhs = pair[1] as ArchitectureDirection;\n if (isArchitectureDirectionX(lhs)) {\n if (isArchitectureDirectionY(rhs)) {\n return [x + (lhs === 'L' ? -1 : 1), y + (rhs === 'T' ? 1 : -1)];\n } else {\n return [x + (lhs === 'L' ? -1 : 1), y];\n }\n } else {\n if (isArchitectureDirectionX(rhs)) {\n return [x + (rhs === 'L' ? 1 : -1), y + (lhs === 'T' ? 1 : -1)];\n } else {\n return [x, y + (lhs === 'T' ? 1 : -1)];\n }\n }\n};\n\n/**\n * Given the directional pair of an XY edge, get the scale factors necessary to shift the coordinates inwards towards the edge\n * @param pair - XY pair of an edge\n * @returns - number[] containing [+/- 1, +/- 1]\n */\nexport const getArchitectureDirectionXYFactors = function (\n pair: ArchitectureDirectionPairXY\n): number[] {\n if (pair === 'LT' || pair === 'TL') {\n return [1, 1];\n } else if (pair === 'BL' || pair === 'LB') {\n return [1, -1];\n } else if (pair === 'BR' || pair === 'RB') {\n return [-1, -1];\n } else {\n return [-1, 1];\n }\n};\n\nexport const getArchitectureDirectionAlignment = function (\n a: ArchitectureDirection,\n b: ArchitectureDirection\n): ArchitectureAlignment {\n if (isArchitectureDirectionXY(a, b)) {\n return 'bend';\n } else if (isArchitectureDirectionX(a)) {\n return 'horizontal';\n }\n return 'vertical';\n};\n\nexport interface ArchitectureStyleOptions {\n archEdgeColor: string;\n archEdgeArrowColor: string;\n archEdgeWidth: string;\n archGroupBorderColor: string;\n archGroupBorderWidth: string;\n}\n\nexport interface ArchitectureService {\n id: string;\n type: 'service';\n edges: ArchitectureEdge[];\n icon?: string;\n iconText?: string;\n title?: string;\n in?: string;\n width?: number;\n height?: number;\n}\n\nexport interface ArchitectureJunction {\n id: string;\n type: 'junction';\n edges: ArchitectureEdge[];\n in?: string;\n width?: number;\n height?: number;\n}\n\nexport type ArchitectureNode = ArchitectureService | ArchitectureJunction;\n\nexport const isArchitectureService = function (x: ArchitectureNode): x is ArchitectureService {\n const temp = x as ArchitectureService;\n return temp.type === 'service';\n};\n\nexport const isArchitectureJunction = function (x: ArchitectureNode): x is ArchitectureJunction {\n const temp = x as ArchitectureJunction;\n return temp.type === 'junction';\n};\n\nexport interface ArchitectureGroup {\n id: string;\n icon?: string;\n title?: string;\n in?: string;\n}\n\nexport interface ArchitectureEdge<DT = ArchitectureDirection> {\n lhsId: string;\n lhsDir: DT;\n lhsInto?: boolean;\n lhsGroup?: boolean;\n rhsId: string;\n rhsDir: DT;\n rhsInto?: boolean;\n rhsGroup?: boolean;\n title?: string;\n}\n\nexport interface ArchitectureDB extends DiagramDBBase<ArchitectureDiagramConfig> {\n clear: () => void;\n addService: (service: Omit<ArchitectureService, 'edges'>) => void;\n getServices: () => ArchitectureService[];\n addJunction: (service: Omit<ArchitectureJunction, 'edges'>) => void;\n getJunctions: () => ArchitectureJunction[];\n getNodes: () => ArchitectureNode[];\n getNode: (id: string) => ArchitectureNode | null;\n addGroup: (group: ArchitectureGroup) => void;\n getGroups: () => ArchitectureGroup[];\n addEdge: (edge: ArchitectureEdge) => void;\n getEdges: () => ArchitectureEdge[];\n setElementForId: (id: string, element: D3Element) => void;\n getElementById: (id: string) => D3Element;\n getDataStructures: () => ArchitectureDataStructures;\n}\n\nexport type ArchitectureAdjacencyList = Record<string, ArchitectureDirectionPairMap>;\nexport type ArchitectureSpatialMap = Record<string, number[]>;\n\n/**\n * Maps the direction that groups connect from.\n *\n * **Outer key**: ID of group A\n *\n * **Inner key**: ID of group B\n *\n * **Value**: 'vertical' or 'horizontal'\n *\n * Note: tmp[groupA][groupB] == tmp[groupB][groupA]\n */\nexport type ArchitectureGroupAlignments = Record<\n string,\n Record<string, Exclude<ArchitectureAlignment, 'bend'>>\n>;\n\nexport interface ArchitectureDataStructures {\n adjList: ArchitectureAdjacencyList;\n spatialMaps: ArchitectureSpatialMap[];\n groupAlignments: ArchitectureGroupAlignments;\n}\n\nexport interface ArchitectureState extends Record<string, unknown> {\n nodes: Record<string, ArchitectureNode>;\n groups: Record<string, ArchitectureGroup>;\n edges: ArchitectureEdge[];\n registeredIds: Record<string, 'node' | 'group'>;\n dataStructures?: ArchitectureDataStructures;\n elements: Record<string, D3Element>;\n config: ArchitectureDiagramConfig;\n}\n\n/*=======================================*\\\n| Cytoscape Override Types |\n\\*=======================================*/\n\nexport interface EdgeSingularData {\n id: string;\n label?: string;\n source: string;\n sourceDir: ArchitectureDirection;\n sourceArrow?: boolean;\n sourceGroup?: boolean;\n target: string;\n targetDir: ArchitectureDirection;\n targetArrow?: boolean;\n targetGroup?: boolean;\n [key: string]: any;\n}\n\nexport const edgeData = (edge: cytoscape.EdgeSingular) => {\n return edge.data() as EdgeSingularData;\n};\n\nexport interface EdgeSingular extends cytoscape.EdgeSingular {\n _private: {\n bodyBounds: unknown;\n rscratch: {\n startX: number;\n startY: number;\n midX: number;\n midY: number;\n endX: number;\n endY: number;\n };\n };\n data(): EdgeSingularData;\n data<T extends keyof EdgeSingularData>(key: T): EdgeSingularData[T];\n}\n\nexport type NodeSingularData =\n | {\n type: 'service';\n id: string;\n icon?: string;\n label?: string;\n parent?: string;\n width: number;\n height: number;\n [key: string]: any;\n }\n | {\n type: 'junction';\n id: string;\n parent?: string;\n width: number;\n height: number;\n [key: string]: any;\n }\n | {\n type: 'group';\n id: string;\n icon?: string;\n label?: string;\n parent?: string;\n [key: string]: any;\n };\n\nexport const nodeData = (node: cytoscape.NodeSingular) => {\n return node.data() as NodeSingularData;\n};\n\nexport interface NodeSingular extends cytoscape.NodeSingular {\n _private: {\n bodyBounds: {\n h: number;\n w: number;\n x1: number;\n x2: number;\n y1: number;\n y2: number;\n };\n children: cytoscape.NodeSingular[];\n };\n data(): NodeSingularData;\n data<T extends keyof NodeSingularData>(key: T): NodeSingularData[T];\n}\n", "import type { ArchitectureDiagramConfig } from '../../config.type.js';\nimport DEFAULT_CONFIG from '../../defaultConfig.js';\nimport { getConfig as commonGetConfig } from '../../config.js';\nimport type { D3Element } from '../../types.js';\nimport { ImperativeState } from '../../utils/imperativeState.js';\nimport {\n clear as commonClear,\n getAccDescription,\n getAccTitle,\n getDiagramTitle,\n setAccDescription,\n setAccTitle,\n setDiagramTitle,\n} from '../common/commonDb.js';\nimport type {\n ArchitectureAlignment,\n ArchitectureDB,\n ArchitectureDirectionPair,\n ArchitectureDirectionPairMap,\n ArchitectureEdge,\n ArchitectureGroup,\n ArchitectureJunction,\n ArchitectureNode,\n ArchitectureService,\n ArchitectureSpatialMap,\n ArchitectureState,\n} from './architectureTypes.js';\nimport {\n getArchitectureDirectionAlignment,\n getArchitectureDirectionPair,\n isArchitectureDirection,\n isArchitectureJunction,\n isArchitectureService,\n shiftPositionByArchitectureDirectionPair,\n} from './architectureTypes.js';\nimport { cleanAndMerge } from '../../utils.js';\n\nconst DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =\n DEFAULT_CONFIG.architecture;\n\nconst state = new ImperativeState<ArchitectureState>(() => ({\n nodes: {},\n groups: {},\n edges: [],\n registeredIds: {},\n config: DEFAULT_ARCHITECTURE_CONFIG,\n dataStructures: undefined,\n elements: {},\n}));\n\nconst clear = (): void => {\n state.reset();\n commonClear();\n};\n\nconst addService = function ({\n id,\n icon,\n in: parent,\n title,\n iconText,\n}: Omit<ArchitectureService, 'edges'>) {\n if (state.records.registeredIds[id] !== undefined) {\n throw new Error(\n `The service id [${id}] is already in use by another ${state.records.registeredIds[id]}`\n );\n }\n if (parent !== undefined) {\n if (id === parent) {\n throw new Error(`The service [${id}] cannot be placed within itself`);\n }\n if (state.records.registeredIds[parent] === undefined) {\n throw new Error(\n `The service [${id}]'s parent does not exist. Please make sure the parent is created before this service`\n );\n }\n if (state.records.registeredIds[parent] === 'node') {\n throw new Error(`The service [${id}]'s parent is not a group`);\n }\n }\n\n state.records.registeredIds[id] = 'node';\n\n state.records.nodes[id] = {\n id,\n type: 'service',\n icon,\n iconText,\n title,\n edges: [],\n in: parent,\n };\n};\n\nconst getServices = (): ArchitectureService[] =>\n Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService);\n\nconst addJunction = function ({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>) {\n state.records.registeredIds[id] = 'node';\n\n state.records.nodes[id] = {\n id,\n type: 'junction',\n edges: [],\n in: parent,\n };\n};\n\nconst getJunctions = (): ArchitectureJunction[] =>\n Object.values(state.records.nodes).filter<ArchitectureJunction>(isArchitectureJunction);\n\nconst getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes);\n\nconst getNode = (id: string): ArchitectureNode | null => state.records.nodes[id];\n\nconst addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) {\n if (state.records.registeredIds[id] !== undefined) {\n throw new Error(\n `The group id [${id}] is already in use by another ${state.records.registeredIds[id]}`\n );\n }\n if (parent !== undefined) {\n if (id === parent) {\n throw new Error(`The group [${id}] cannot be placed within itself`);\n }\n if (state.records.registeredIds[parent] === undefined) {\n throw new Error(\n `The group [${id}]'s parent does not exist. Please make sure the parent is created before this group`\n );\n }\n if (state.records.registeredIds[parent] === 'node') {\n throw new Error(`The group [${id}]'s parent is not a group`);\n }\n }\n\n state.records.registeredIds[id] = 'group';\n\n state.records.groups[id] = {\n id,\n icon,\n title,\n in: parent,\n };\n};\nconst getGroups = (): ArchitectureGroup[] => {\n return Object.values(state.records.groups);\n};\n\nconst addEdge = function ({\n lhsId,\n rhsId,\n lhsDir,\n rhsDir,\n lhsInto,\n rhsInto,\n lhsGroup,\n rhsGroup,\n title,\n}: ArchitectureEdge<string>) {\n if (!isArchitectureDirection(lhsDir)) {\n throw new Error(\n `Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}`\n );\n }\n if (!isArchitectureDirection(rhsDir)) {\n throw new Error(\n `Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}`\n );\n }\n\n if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) {\n throw new Error(\n `The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`\n );\n }\n if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) {\n throw new Error(\n `The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`\n );\n }\n\n const lhsGroupId = state.records.nodes[lhsId].in;\n const rhsGroupId = state.records.nodes[rhsId].in;\n if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {\n throw new Error(\n `The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`\n );\n }\n if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) {\n throw new Error(\n `The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.`\n );\n }\n\n const edge = {\n lhsId,\n lhsDir,\n lhsInto,\n lhsGroup,\n rhsId,\n rhsDir,\n rhsInto,\n rhsGroup,\n title,\n };\n\n state.records.edges.push(edge);\n if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) {\n state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]);\n state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]);\n }\n};\n\nconst getEdges = (): ArchitectureEdge[] => state.records.edges;\n\n/**\n * Returns the current diagram's adjacency list, spatial map, & group alignments.\n * If they have not been created, run the algorithms to generate them.\n * @returns\n */\nconst getDataStructures = () => {\n if (state.records.dataStructures === undefined) {\n // Tracks how groups are aligned with one another. Generated while creating the adj list\n const groupAlignments: Record<\n string,\n Record<string, Exclude<ArchitectureAlignment, 'bend'>>\n > = {};\n\n // Create an adjacency list of the diagram to perform BFS on\n // Outer reduce applied on all services\n // Inner reduce applied on the edges for a service\n const adjList = Object.entries(state.records.nodes).reduce<\n Record<string, ArchitectureDirectionPairMap>\n >((prevOuter, [id, service]) => {\n prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {\n // track the direction groups connect to one another\n const lhsGroupId = getNode(edge.lhsId)?.in;\n const rhsGroupId = getNode(edge.rhsId)?.in;\n if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {\n const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);\n if (alignment !== 'bend') {\n groupAlignments[lhsGroupId] ??= {};\n groupAlignments[lhsGroupId][rhsGroupId] = alignment;\n groupAlignments[rhsGroupId] ??= {};\n groupAlignments[rhsGroupId][lhsGroupId] = alignment;\n }\n }\n\n if (edge.lhsId === id) {\n // source is LHS\n const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);\n if (pair) {\n prevInner[pair] = edge.rhsId;\n }\n } else {\n // source is RHS\n const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);\n if (pair) {\n prevInner[pair] = edge.lhsId;\n }\n }\n return prevInner;\n }, {});\n return prevOuter;\n }, {});\n\n // Configuration for the initial pass of BFS\n const firstId = Object.keys(adjList)[0];\n const visited = { [firstId]: 1 };\n // If a key is present in this object, it has not been visited\n const notVisited = Object.keys(adjList).reduce(\n (prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),\n {} as Record<string, number>\n );\n\n // Perform BFS on the adjacency list\n const BFS = (startingId: string): ArchitectureSpatialMap => {\n const spatialMap = { [startingId]: [0, 0] };\n const queue = [startingId];\n while (queue.length > 0) {\n const id = queue.shift();\n if (id) {\n visited[id] = 1;\n delete notVisited[id];\n const adj = adjList[id];\n const [posX, posY] = spatialMap[id];\n Object.entries(adj).forEach(([dir, rhsId]) => {\n if (!visited[rhsId]) {\n spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair(\n [posX, posY],\n dir as ArchitectureDirectionPair\n );\n queue.push(rhsId);\n }\n });\n }\n }\n return spatialMap;\n };\n const spatialMaps = [BFS(firstId)];\n\n // If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found\n while (Object.keys(notVisited).length > 0) {\n spatialMaps.push(BFS(Object.keys(notVisited)[0]));\n }\n state.records.dataStructures = {\n adjList,\n spatialMaps,\n groupAlignments,\n };\n }\n return state.records.dataStructures;\n};\n\nconst setElementForId = (id: string, element: D3Element) => {\n state.records.elements[id] = element;\n};\nconst getElementById = (id: string) => state.records.elements[id];\n\nconst getConfig = (): Required<ArchitectureDiagramConfig> => {\n const config = cleanAndMerge({\n ...DEFAULT_ARCHITECTURE_CONFIG,\n ...commonGetConfig().architecture,\n });\n return config;\n};\n\nexport const db: ArchitectureDB = {\n clear,\n setDiagramTitle,\n getDiagramTitle,\n setAccTitle,\n getAccTitle,\n setAccDescription,\n getAccDescription,\n getConfig,\n\n addService,\n getServices,\n addJunction,\n getJunctions,\n getNodes,\n getNode,\n addGroup,\n getGroups,\n addEdge,\n getEdges,\n setElementForId,\n getElementById,\n getDataStructures,\n};\n\n/**\n * Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined\n * @param field - the config field to access\n * @returns\n */\nexport function getConfigField<T extends keyof ArchitectureDiagramConfig>(\n field: T\n): Required<ArchitectureDiagramConfig>[T] {\n return getConfig()[field];\n}\n", "import type { DiagramStylesProvider } from '../../diagram-api/types.js';\nimport type { ArchitectureStyleOptions } from './architectureTypes.js';\n\nconst getStyles: DiagramStylesProvider = (options: ArchitectureStyleOptions) =>\n `\n .edge {\n stroke-width: ${options.archEdgeWidth};\n stroke: ${options.archEdgeColor};\n fill: none;\n }\n\n .arrow {\n fill: ${options.archEdgeArrowColor};\n }\n\n .node-bkg {\n fill: none;\n stroke: ${options.archGroupBorderColor};\n stroke-width: ${options.archGroupBorderWidth};\n stroke-dasharray: 8;\n }\n .node-icon-text {\n display: flex; \n align-items: center;\n }\n \n .node-icon-text > div {\n color: #fff;\n margin: 1px;\n height: fit-content;\n text-align: center;\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n }\n`;\n\nexport default getStyles;\n", "import { registerIconPacks } from '../../rendering-util/icons.js';\nimport type { Position } from 'cytoscape';\nimport cytoscape from 'cytoscape';\nimport type { FcoseLayoutOptions } from 'cytoscape-fcose';\nimport fcose from 'cytoscape-fcose';\nimport { select } from 'd3';\nimport type { DrawDefinition, SVG } from '../../diagram-api/types.js';\nimport type { Diagram } from '../../Diagram.js';\nimport { log } from '../../logger.js';\nimport { selectSvgElement } from '../../rendering-util/selectSvgElement.js';\nimport { setupGraphViewbox } from '../../setupGraphViewbox.js';\nimport { getConfigField } from './architectureDb.js';\nimport { architectureIcons } from './architectureIcons.js';\nimport type {\n ArchitectureAlignment,\n ArchitectureDataStructures,\n ArchitectureGroupAlignments,\n ArchitectureJunction,\n ArchitectureSpatialMap,\n EdgeSingular,\n EdgeSingularData,\n NodeSingularData,\n} from './architectureTypes.js';\nimport {\n type ArchitectureDB,\n type ArchitectureDirection,\n type ArchitectureEdge,\n type ArchitectureGroup,\n type ArchitectureService,\n ArchitectureDirectionName,\n edgeData,\n getOppositeArchitectureDirection,\n isArchitectureDirectionXY,\n isArchitectureDirectionY,\n nodeData,\n} from './architectureTypes.js';\nimport { drawEdges, drawGroups, drawJunctions, drawServices } from './svgDraw.js';\n\nregisterIconPacks([\n {\n name: architectureIcons.prefix,\n icons: architectureIcons,\n },\n]);\ncytoscape.use(fcose);\n\nfunction addServices(services: ArchitectureService[], cy: cytoscape.Core) {\n services.forEach((service) => {\n cy.add({\n group: 'nodes',\n data: {\n type: 'service',\n id: service.id,\n icon: service.icon,\n label: service.title,\n parent: service.in,\n width: getConfigField('iconSize'),\n height: getConfigField('iconSize'),\n } as NodeSingularData,\n classes: 'node-service',\n });\n });\n}\n\nfunction addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {\n junctions.forEach((junction) => {\n cy.add({\n group: 'nodes',\n data: {\n type: 'junction',\n id: junction.id,\n parent: junction.in,\n width: getConfigField('iconSize'),\n height: getConfigField('iconSize'),\n } as NodeSingularData,\n classes: 'node-junction',\n });\n });\n}\n\nfunction positionNodes(db: ArchitectureDB, cy: cytoscape.Core) {\n cy.nodes().map((node) => {\n const data = nodeData(node);\n if (data.type === 'group') {\n return;\n }\n data.x = node.position().x;\n data.y = node.position().y;\n\n const nodeElem = db.getElementById(data.id);\n nodeElem.attr('transform', 'translate(' + (data.x || 0) + ',' + (data.y || 0) + ')');\n });\n}\n\nfunction addGroups(groups: ArchitectureGroup[], cy: cytoscape.Core) {\n groups.forEach((group) => {\n cy.add({\n group: 'nodes',\n data: {\n type: 'group',\n id: group.id,\n icon: group.icon,\n label: group.title,\n parent: group.in,\n } as NodeSingularData,\n classes: 'node-group',\n });\n });\n}\n\nfunction addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) {\n edges.forEach((parsedEdge) => {\n const { lhsId, rhsId, lhsInto, lhsGroup, rhsInto, lhsDir, rhsDir, rhsGroup, title } =\n parsedEdge;\n const edgeType = isArchitectureDirectionXY(parsedEdge.lhsDir, parsedEdge.rhsDir)\n ? 'segments'\n : 'straight';\n const edge: EdgeSingularData = {\n id: `${lhsId}-${rhsId}`,\n label: title,\n source: lhsId,\n sourceDir: lhsDir,\n sourceArrow: lhsInto,\n sourceGroup: lhsGroup,\n sourceEndpoint:\n lhsDir === 'L'\n ? '0 50%'\n : lhsDir === 'R'\n ? '100% 50%'\n : lhsDir === 'T'\n ? '50% 0'\n : '50% 100%',\n target: rhsId,\n targetDir: rhsDir,\n targetArrow: rhsInto,\n targetGroup: rhsGroup,\n targetEndpoint:\n rhsDir === 'L'\n ? '0 50%'\n : rhsDir === 'R'\n ? '100% 50%'\n : rhsDir === 'T'\n ? '50% 0'\n : '50% 100%',\n };\n cy.add({\n group: 'edges',\n data: edge,\n classes: edgeType,\n });\n });\n}\n\nfunction getAlignments(\n db: ArchitectureDB,\n spatialMaps: ArchitectureSpatialMap[],\n groupAlignments: ArchitectureGroupAlignments\n): fcose.FcoseAlignmentConstraint {\n /**\n * Flattens the alignment object so nodes in different groups will be in the same alignment array IFF their groups don't connect in a conflicting alignment\n *\n * i.e., two groups which connect horizontally should not have nodes with vertical alignments to one another\n *\n * See: #5952\n *\n * @param alignmentObj - alignment object with the outer key being the row/col # and the inner key being the group name mapped to the nodes on that axis in the group\n * @param alignmentDir - alignment direction\n * @returns flattened alignment object with an arbitrary key mapping to nodes in the same row/col\n */\n const flattenAlignments = (\n alignmentObj: Record<number, Record<string, string[]>>,\n alignmentDir: ArchitectureAlignment\n ): Record<string, string[]> => {\n return Object.entries(alignmentObj).reduce(\n (prev, [dir, alignments]) => {\n // prev is the mapping of x/y coordinate to an array of the nodes in that row/column\n let cnt = 0;\n const arr = Object.entries(alignments); // [group name, array of nodes within the group on axis dir]\n if (arr.length === 1) {\n // If only one group exists in the row/column, we don't need to do anything else\n prev[dir] = arr[0][1];\n return prev;\n }\n for (let i = 0; i < arr.length - 1; i++) {\n for (let j = i + 1; j < arr.length; j++) {\n const [aGroupId, aNodeIds] = arr[i];\n const [bGroupId, bNodeIds] = arr[j];\n const alignment = groupAlignments[aGroupId]?.[bGroupId]; // Get how the two groups are intended to align (undefined if they aren't)\n\n if (alignment === alignmentDir) {\n // If the intended alignment between the two groups is the same as the alignment we are parsing\n prev[dir] ??= [];\n prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; // add the node ids of both groups to the axis array in prev\n } else if (aGroupId === 'default' || bGroupId === 'default') {\n // If either of the groups are in the default space (not in a group), use the same behavior as above\n prev[dir] ??= [];\n prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds];\n } else {\n // Otherwise, the nodes in the two groups are not intended to align\n const keyA = `${dir}-${cnt++}`;\n prev[keyA] = aNodeIds;\n const keyB = `${dir}-${cnt++}`;\n prev[keyB] = bNodeIds;\n }\n }\n }\n\n return prev;\n },\n {} as Record<string, string[]>\n );\n };\n\n const alignments = spatialMaps.map((spatialMap) => {\n const horizontalAlignments: Record<number, Record<string, string[]>> = {};\n const verticalAlignments: Record<number, Record<string, string[]>> = {};\n\n // Group service ids in an object with their x and y coordinate as the key\n Object.entries(spatialMap).forEach(([id, [x, y]]) => {\n const nodeGroup = db.getNode(id)?.in ?? 'default';\n\n horizontalAlignments[y] ??= {};\n horizontalAlignments[y][nodeGroup] ??= [];\n horizontalAlignments[y][nodeGroup].push(id);\n\n verticalAlignments[x] ??= {};\n verticalAlignments[x][nodeGroup] ??= [];\n verticalAlignments[x][nodeGroup].push(id);\n });\n\n // Merge the values of each object into a list if the inner list has at least 2 elements\n return {\n horiz: Object.values(flattenAlignments(horizontalAlignments, 'horizontal')).filter(\n (arr) => arr.length > 1\n ),\n vert: Object.values(flattenAlignments(verticalAlignments, 'vertical')).filter(\n (arr) => arr.length > 1\n ),\n };\n });\n\n // Merge the alignment lists for each spatial map into one 2d array per axis\n const [horizontal, vertical] = alignments.reduce(\n ([prevHoriz, prevVert], { horiz, vert }) => {\n return [\n [...prevHoriz, ...horiz],\n [...prevVert, ...vert],\n ];\n },\n [[] as string[][], [] as string[][]]\n );\n\n return {\n horizontal,\n vertical,\n };\n}\n\nfunction getRelativeConstraints(\n spatialMaps: ArchitectureSpatialMap[]\n): fcose.FcoseRelativePlacementConstraint[] {\n const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [];\n const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;\n const strToPos = (pos: string) => pos.split(',').map((p) => parseInt(p));\n\n spatialMaps.forEach((spatialMap) => {\n const invSpatialMap = Object.fromEntries(\n Object.entries(spatialMap).map(([id, pos]) => [posToStr(pos), id])\n );\n\n // perform BFS\n const queue = [posToStr([0, 0])];\n const visited: Record<string, number> = {};\n const directions: Record<ArchitectureDirection, number[]> = {\n L: [-1, 0],\n R: [1, 0],\n T: [0, 1],\n B: [0, -1],\n };\n while (queue.length > 0) {\n const curr = queue.shift();\n if (curr) {\n visited[curr] = 1;\n const currId = invSpatialMap[curr];\n if (currId) {\n const currPos = strToPos(curr);\n Object.entries(directions).forEach(([dir, shift]) => {\n const newPos = posToStr([currPos[0] + shift[0], currPos[1] + shift[1]]);\n const newId = invSpatialMap[newPos];\n // If there is an adjacent service to the current one and it has not yet been visited\n if (newId && !visited[newPos]) {\n queue.push(newPos);\n // @ts-ignore cannot determine if left/right or top/bottom are paired together\n relativeConstraints.push({\n [ArchitectureDirectionName[dir as ArchitectureDirection]]: newId,\n [ArchitectureDirectionName[\n getOppositeArchitectureDirection(dir as ArchitectureDirection)\n ]]: currId,\n gap: 1.5 * getConfigField('iconSize'),\n });\n }\n });\n }\n }\n }\n });\n return relativeConstraints;\n}\n\nfunction layoutArchitecture(\n services: ArchitectureService[],\n junctions: ArchitectureJunction[],\n groups: ArchitectureGroup[],\n edges: ArchitectureEdge[],\n db: ArchitectureDB,\n { spatialMaps, groupAlignments }: ArchitectureDataStructures\n): Promise<cytoscape.Core> {\n return new Promise((resolve) => {\n const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');\n const cy = cytoscape({\n container: document.getElementById('cy'),\n style: [\n {\n selector: 'edge',\n style: {\n 'curve-style': 'straight',\n label: 'data(label)',\n 'source-endpoint': 'data(sourceEndpoint)',\n 'target-endpoint': 'data(targetEndpoint)',\n },\n },\n {\n selector: 'edge.segments',\n style: {\n 'curve-style': 'segments',\n 'segment-weights': '0',\n 'segment-distances': [0.5],\n // @ts-ignore Incorrect library types\n 'edge-distances': 'endpoints',\n 'source-endpoint': 'data(sourceEndpoint)',\n 'target-endpoint': 'data(targetEndpoint)',\n },\n },\n {\n selector: 'node',\n style: {\n // @ts-ignore Incorrect library types\n 'compound-sizing-wrt-labels': 'include',\n },\n },\n {\n selector: 'node[label]',\n style: {\n 'text-valign': 'bottom',\n 'text-halign': 'center',\n 'font-size': `${getConfigField('fontSize')}px`,\n },\n },\n {\n selector: '.node-service',\n style: {\n label: 'data(label)',\n width: 'data(width)',\n height: 'data(height)',\n },\n },\n {\n selector: '.node-junction',\n style: {\n width: 'data(width)',\n height: 'data(height)',\n },\n },\n {\n selector: '.node-group',\n style: {\n // @ts-ignore Incorrect library types\n padding: `${getConfigField('padding')}px`,\n },\n },\n ],\n });\n // Remove element after layout\n renderEl.remove();\n\n addGroups(groups, cy);\n addServices(services, cy);\n addJunctions(junctions, cy);\n addEdges(edges, cy);\n // Use the spatial map to create alignment arrays for fcose\n const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments);\n\n // Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it\n const relativePlacementConstraint = getRelativeConstraints(spatialMaps);\n\n const layout = cy.layout({\n name: 'fcose',\n quality: 'proof',\n styleEnabled: false,\n animate: false,\n nodeDimensionsIncludeLabels: false,\n // Adjust the edge parameters if it passes through the border of a group\n // Hacky fix for: https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/issues/67\n idealEdgeLength(edge: EdgeSingular) {\n const [nodeA, nodeB] = edge.connectedNodes();\n const { parent: parentA } = nodeData(nodeA);\n const { parent: parentB } = nodeData(nodeB);\n const elasticity =\n parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize');\n return elasticity;\n },\n edgeElasticity(edge: EdgeSingular) {\n const [nodeA, nodeB] = edge.connectedNodes();\n const { parent: parentA } = nodeData(nodeA);\n const { parent: parentB } = nodeData(nodeB);\n const elasticity = parentA === parentB ? 0.45 : 0.001;\n return elasticity;\n },\n alignmentConstraint,\n relativePlacementConstraint,\n } as FcoseLayoutOptions);\n\n // Once the diagram has been generated and the service's position cords are set, adjust the XY edges to have a 90deg bend\n layout.one('layoutstop', () => {\n function getSegmentWeights(\n source: Position,\n target: Position,\n pointX: number,\n pointY: number\n ) {\n let W, D;\n const { x: sX, y: sY } = source;\n const { x: tX, y: tY } = target;\n\n D =\n (pointY - sY + ((sX - pointX) * (sY - tY)) / (sX - tX)) /\n Math.sqrt(1 + Math.pow((sY - tY) / (sX - tX), 2));\n W = Math.sqrt(Math.pow(pointY - sY, 2) + Math.pow(pointX - sX, 2) - Math.pow(D, 2));\n\n const distAB = Math.sqrt(Math.pow(tX - sX, 2) + Math.pow(tY - sY, 2));\n W = W / distAB;\n\n //check whether the point (pointX, pointY) is on right or left of the line src to tgt. for instance : a point C(X, Y) and line (AB). d=(xB-xA)(yC-yA)-(yB-yA)(xC-xA). if d>0, then C is on left of the line. if d<0, it is on right. if d=0, it is on the line.\n let delta1 = (tX - sX) * (pointY - sY) - (tY - sY) * (pointX - sX);\n switch (true) {\n case delta1 >= 0:\n delta1 = 1;\n break;\n case delta1 < 0:\n delta1 = -1;\n break;\n }\n //check whether the point (pointX, pointY) is \"behind\" the line src to tgt\n let delta2 = (tX - sX) * (pointX - sX) + (tY - sY) * (pointY - sY);\n switch (true) {\n case delta2 >= 0:\n delta2 = 1;\n break;\n case delta2 < 0:\n delta2 = -1;\n break;\n }\n\n D = Math.abs(D) * delta1; //ensure that sign of D is same as sign of delta1. Hence we need to take absolute value of D and multiply by delta1\n W = W * delta2;\n\n return {\n distances: D,\n weights: W,\n };\n }\n cy.startBatch();\n for (const edge of Object.values(cy.edges())) {\n if (edge.data?.()) {\n const { x: sX, y: sY } = edge.source().position();\n const { x: tX, y: tY } = edge.target().position();\n if (sX !== tX && sY !== tY) {\n const sEP = edge.sourceEndpoint();\n const tEP = edge.targetEndpoint();\n const { sourceDir } = edgeData(edge);\n const [pointX, pointY] = isArchitectureDirectionY(sourceDir)\n ? [sEP.x, tEP.y]\n : [tEP.x, sEP.y];\n const { weights, distances } = getSegmentWeights(sEP, tEP, pointX, pointY);\n edge.style('segment-distances', distances);\n edge.style('segment-weights', weights);\n }\n }\n }\n cy.endBatch();\n layout.run();\n });\n layout.run();\n\n cy.ready((e) => {\n log.info('Ready', e);\n resolve(cy);\n });\n });\n}\n\nexport const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) => {\n // TODO: Add title support for architecture diagrams\n\n const db = diagObj.db as ArchitectureDB;\n\n const services = db.getServices();\n const junctions = db.getJunctions();\n const groups = db.getGroups();\n const edges = db.getEdges();\n const ds = db.getDataStructures();\n\n const svg: SVG = selectSvgElement(id);\n\n const edgesElem = svg.append('g');\n edgesElem.attr('class', 'architecture-edges');\n\n const servicesElem = svg.append('g');\n servicesElem.attr('class', 'architecture-services');\n\n const groupElem = svg.append('g');\n groupElem.attr('class', 'architecture-groups');\n\n await drawServices(db, servicesElem, services);\n drawJunctions(db, servicesElem, junctions);\n\n const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);\n\n await drawEdges(edgesElem, cy);\n await drawGroups(groupElem, cy);\n positionNodes(db, cy);\n\n setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth'));\n};\n\nexport const renderer = { draw };\n", "import { unknownIcon } from '../../rendering-util/icons.js';\nimport type { IconifyJSON } from '@iconify/types';\n\nconst wrapIcon = (icon: string) => {\n return `<g><rect width=\"80\" height=\"80\" style=\"fill: #087ebf; stroke-width: 0px;\"/>${icon}</g>`;\n};\n\nexport const architectureIcons: IconifyJSON = {\n prefix: 'mermaid-architecture',\n height: 80,\n width: 80,\n icons: {\n database: {\n body: wrapIcon(\n '<path id=\"b\" data-name=\"4\" d=\"m20,57.86c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><path id=\"c\" data-name=\"3\" d=\"m20,45.95c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><path id=\"d\" data-name=\"2\" d=\"m20,34.05c0,3.94,8.95,7.14,20,7.14s20-3.2,20-7.14\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><ellipse id=\"e\" data-name=\"1\" cx=\"40\" cy=\"22.14\" rx=\"20\" ry=\"7.14\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><line x1=\"20\" y1=\"57.86\" x2=\"20\" y2=\"22.14\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><line x1=\"60\" y1=\"57.86\" x2=\"60\" y2=\"22.14\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/>'\n ),\n },\n server: {\n body: wrapIcon(\n '<rect x=\"17.5\" y=\"17.5\" width=\"45\" height=\"45\" rx=\"2\" ry=\"2\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><line x1=\"17.5\" y1=\"32.5\" x2=\"62.5\" y2=\"32.5\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><line x1=\"17.5\" y1=\"47.5\" x2=\"62.5\" y2=\"47.5\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><g><path d=\"m56.25,25c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z\" style=\"fill: #fff; stroke-width: 0px;\"/><path d=\"m56.25,25c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10;\"/></g><g><path d=\"m56.25,40c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z\" style=\"fill: #fff; stroke-width: 0px;\"/><path d=\"m56.25,40c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10;\"/></g><g><path d=\"m56.25,55c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z\" style=\"fill: #fff; stroke-width: 0px;\"/><path d=\"m56.25,55c0,.27-.45.5-1,.5h-10.5c-.55,0-1-.23-1-.5s.45-.5,1-.5h10.5c.55,0,1,.23,1,.5Z\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10;\"/></g><g><circle cx=\"32.5\" cy=\"25\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/><circle cx=\"27.5\" cy=\"25\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/><circle cx=\"22.5\" cy=\"25\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/></g><g><circle cx=\"32.5\" cy=\"40\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/><circle cx=\"27.5\" cy=\"40\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/><circle cx=\"22.5\" cy=\"40\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/></g><g><circle cx=\"32.5\" cy=\"55\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/><circle cx=\"27.5\" cy=\"55\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/><circle cx=\"22.5\" cy=\"55\" r=\".75\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10;\"/></g>'\n ),\n },\n disk: {\n body: wrapIcon(\n '<rect x=\"20\" y=\"15\" width=\"40\" height=\"50\" rx=\"1\" ry=\"1\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><ellipse cx=\"24\" cy=\"19.17\" rx=\".8\" ry=\".83\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><ellipse cx=\"56\" cy=\"19.17\" rx=\".8\" ry=\".83\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><ellipse cx=\"24\" cy=\"60.83\" rx=\".8\" ry=\".83\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><ellipse cx=\"56\" cy=\"60.83\" rx=\".8\" ry=\".83\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><ellipse cx=\"40\" cy=\"33.75\" rx=\"14\" ry=\"14.58\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><ellipse cx=\"40\" cy=\"33.75\" rx=\"4\" ry=\"4.17\" style=\"fill: #fff; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><path d=\"m37.51,42.52l-4.83,13.22c-.26.71-1.1,1.02-1.76.64l-4.18-2.42c-.66-.38-.81-1.26-.33-1.84l9.01-10.8c.88-1.05,2.56-.08,2.09,1.2Z\" style=\"fill: #fff; stroke-width: 0px;\"/>'\n ),\n },\n internet: {\n body: wrapIcon(\n '<circle cx=\"40\" cy=\"40\" r=\"22.5\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><line x1=\"40\" y1=\"17.5\" x2=\"40\" y2=\"62.5\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><line x1=\"17.5\" y1=\"40\" x2=\"62.5\" y2=\"40\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><path d=\"m39.99,17.51c-15.28,11.1-15.28,33.88,0,44.98\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><path d=\"m40.01,17.51c15.28,11.1,15.28,33.88,0,44.98\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><line x1=\"19.75\" y1=\"30.1\" x2=\"60.25\" y2=\"30.1\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/><line x1=\"19.75\" y1=\"49.9\" x2=\"60.25\" y2=\"49.9\" style=\"fill: none; stroke: #fff; stroke-miterlimit: 10; stroke-width: 2px;\"/>'\n ),\n },\n cloud: {\n body: wrapIcon(\n '<path d=\"m65,47.5c0,2.76-2.24,5-5,5H20c-2.76,0-5-2.24-5-5,0-1.87,1.03-3.51,2.56-4.36-.04-.21-.06-.42-.06-.64,0-2.6,2.48-4.74,5.65-4.97,1.65-4