Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Style Generated UML in Sol2Uml Plugin #3609

Merged
merged 3 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 247 additions & 19 deletions apps/remix-ide/src/app/plugins/solidity-umlgen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { RemixUiSolidityUmlGen } from '@remix-ui/solidity-uml-gen'
import { ISolidityUmlGen, ThemeQualityType, ThemeSummary } from 'libs/remix-ui/solidity-uml-gen/src/types'
import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types'
import { normalizeContractPath } from 'libs/remix-ui/solidity-compiler/src/lib/logic/flattenerUtilities'
import { convertUmlClasses2Dot } from 'sol2uml/lib/converterClasses2Dot'
import { convertAST2UmlClasses } from 'sol2uml/lib/converterAST2Classes'
import vizRenderStringSync from '@aduh95/viz.js/sync'
import { PluginViewWrapper } from '@remix-ui/helper'
import { customAction } from '@remixproject/plugin-api'
import { ClassOptions } from 'sol2uml/lib/converterClass2Dot'
const parser = (window as any).SolidityParser

const _paq = window._paq = window._paq || []
Expand All @@ -25,16 +25,26 @@ const profile = {
}

const themeCollection = [
{ themeName: 'HackerOwl', backgroundColor: '--body-bg', actualHex: '#011628', dark: '#fff4fd'},
{ themeName: 'Cerulean', backgroundColor: '--body-bg', actualHex: '#fff', dark: '#343a40'},
{ themeName: 'Cyborg', backgroundColor: '--body-bg', actualHex: '#060606', dark: '#adafae'},
{ themeName: 'Dark', backgroundColor: '--body-bg', actualHex: '#222336', dark: '#222336'},
{ themeName: 'Flatly', backgroundColor: '--body-bg', actualHex: '#fff', dark: '#7b8a8b'},
{ themeName: 'Black', backgroundColor: '--body-bg', actualHex: '#1a1a1a', dark: '#1a1a1a'},
{ themeName: 'Light', backgroundColor: '--body-bg', actualHex: '#eef1f6', dark: '#f8fafe'},
{ themeName: 'Midcentuary', backgroundColor: '--body-bg', actualHex: '#DBE2E0', dark: '#01414E'},
{ themeName: 'Spacelab', backgroundColor: '--body-bg', actualHex: '#fff', dark: '#333'},
{ themeName: 'Candy', backgroundColor: '--body-bg', actualHex: '#d5efff', dark: '#645fb5'},
{ themeName: 'HackerOwl', backgroundColor: '#011628', textColor: '#babbcc',
shapeColor: '#8694a1',fillColor: '#011C32'},
{ themeName: 'Cerulean', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#343a40',fillColor: '#f8f9fa'},
{ themeName: 'Cyborg', backgroundColor: '#060606', textColor: '#adafae',
shapeColor: '#adafae', fillColor: '#222222'},
{ themeName: 'Dark', backgroundColor: '#222336', textColor: '#babbcc',
shapeColor: '#babbcc',fillColor: '#2a2c3f'},
{ themeName: 'Flatly', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#7b8a8b',fillColor: '#ffffff'},
{ themeName: 'Black', backgroundColor: '#1a1a1a', textColor: '#babbcc',
shapeColor: '#b5b4bc',fillColor: '#1f2020'},
{ themeName: 'Light', backgroundColor: '#eef1f6', textColor: '#3b445e',
shapeColor: '#343a40',fillColor: '#ffffff'},
{ themeName: 'Midcentury', backgroundColor: '#DBE2E0', textColor: '#11556c',
shapeColor: '#343a40',fillColor: '#eeede9'},
{ themeName: 'Spacelab', backgroundColor: '#ffffff', textColor: '#343a40',
shapeColor: '#333333', fillColor: '#eeeeee'},
{ themeName: 'Candy', backgroundColor: '#d5efff', textColor: '#11556c',
shapeColor: '#343a40',fillColor: '#fbe7f8' },
]

/**
Expand All @@ -52,7 +62,9 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
themeDark: string
loading: boolean
themeCollection: ThemeSummary[]
activeTheme: ThemeSummary
triggerGenerateUml: boolean
umlClasses: UmlClass[] = []

appManager: RemixAppManager
dispatch: React.Dispatch<any> = () => {}
Expand All @@ -64,13 +76,16 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
this.loading = false
this.currentlySelectedTheme = ''
this.themeName = ''

this.themeCollection = themeCollection
this.activeTheme = themeCollection.find(t => t.themeName === 'Dark')
this.appManager = appManager
this.element = document.createElement('div')
this.element.setAttribute('id', 'sol-uml-gen')
}

onActivation(): void {
this.handleThemeChange()
this.on('solidity', 'compilationFinished', async (file: string, source, languageVersion, data, input, version) => {
if(!this.triggerGenerateUml) return
this.triggerGenerateUml = false
Expand All @@ -85,8 +100,10 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
result = await this.flattenContract(source, file, data)
}
const ast = result.length > 1 ? parser.parse(result) : parser.parse(source.sources[file].content)
const umlClasses = convertAST2UmlClasses(ast, this.currentFile)
const umlDot = convertUmlClasses2Dot(umlClasses)
this.umlClasses = convertAST2UmlClasses(ast, this.currentFile)
let umlDot = ''
this.activeTheme = themeCollection.find(theme => theme.themeName === currentTheme.name)
umlDot = convertUmlClasses2Dot(this.umlClasses, false, { backColor: this.activeTheme.backgroundColor, textColor: this.activeTheme.textColor, shapeColor: this.activeTheme.shapeColor, fillColor: this.activeTheme.fillColor })
const payload = vizRenderStringSync(umlDot)
this.updatedSvg = payload
_paq.push(['trackEvent', 'solidityumlgen', 'umlgenerated'])
Expand All @@ -96,15 +113,27 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
console.log('error', error)
}
})
}

getThemeCssVariables(cssVars: string) {
return window.getComputedStyle(document.documentElement)
.getPropertyValue(cssVars)
}

private handleThemeChange() {
this.on('theme', 'themeChanged', async (theme) => {
this.currentlySelectedTheme = theme.quality
const themeQuality: ThemeQualityType = await this.call('theme', 'currentTheme')
const themeQuality: ThemeQualityType = await this.call('theme', 'currentTheme')
themeCollection.forEach((theme) => {
if (theme.themeName === themeQuality.name) {
this.themeDark = theme.dark
this.themeDark = theme.backgroundColor
this.activeTheme = theme
const umlDot = convertUmlClasses2Dot(this.umlClasses, false, { backColor: this.activeTheme.backgroundColor, textColor: this.activeTheme.textColor, shapeColor: this.activeTheme.shapeColor, fillColor: this.activeTheme.fillColor })
this.updatedSvg = vizRenderStringSync(umlDot)
this.renderComponent()
}
})
this.renderComponent()
await this.call('tabs', 'focus', 'solidityumlgen')
})
}

Expand All @@ -115,8 +144,8 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
const element = parsedDocument.getElementsByTagName('svg')
themeCollection.forEach((theme) => {
if (theme.themeName === themeQuality.name) {
parsedDocument.documentElement.setAttribute('style', `background-color: var(${themeQuality.name === theme.themeName ? theme.backgroundColor : '--body-bg'})`)
element[0].setAttribute('fill', theme.actualHex)
parsedDocument.documentElement.setAttribute('style', `background-color: var(${this.getThemeCssVariables('--body-bg')})`)
element[0].setAttribute('fill', theme.backgroundColor)
}
})
const stringifiedSvg = new XMLSerializer().serializeToString(parsedDocument)
Expand Down Expand Up @@ -184,7 +213,8 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
themeName: this.themeName,
themeDark: this.themeDark,
fileName: this.currentFile,
themeCollection: this.themeCollection
themeCollection: this.themeCollection,
activeTheme: this.activeTheme,
})
}

Expand All @@ -201,3 +231,201 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen {
}
}


interface Sol2umlClassOptions extends ClassOptions {
backColor?: string
shapeColor?: string
fillColor?: string
textColor?: string
}

import { dirname } from 'path'
import { convertClass2Dot } from 'sol2uml/lib/converterClass2Dot'
import {
Association,
ClassStereotype,
ReferenceType,
UmlClass,
} from 'sol2uml/lib/umlClass'
import { findAssociatedClass } from 'sol2uml/lib/associations'

// const debug = require('debug')('sol2uml')

/**
* Converts UML classes to Graphviz's DOT format.
* The DOT grammar defines Graphviz nodes, edges, graphs, subgraphs, and clusters http://www.graphviz.org/doc/info/lang.html
* @param umlClasses array of UML classes of type `UMLClass`
* @param clusterFolders flag if UML classes are to be clustered into folders their source code was in
* @param classOptions command line options for the `class` command
* @return dotString Graphviz's DOT format for defining nodes, edges and clusters.
*/
export function convertUmlClasses2Dot(
umlClasses: UmlClass[],
clusterFolders: boolean = false,
classOptions: Sol2umlClassOptions = {}
): string {
let dotString: string = `
digraph UmlClassDiagram {
rankdir=BT
arrowhead=open
bgcolor="${classOptions.backColor}"
edge [color="${classOptions.shapeColor}"]
node [shape=record, style=filled, color="${classOptions.shapeColor}", fillcolor="${classOptions.fillColor}", fontcolor="${classOptions.textColor}"]`

// Sort UML Classes by folder of source file
const umlClassesSortedByCodePath = sortUmlClassesByCodePath(umlClasses)

let currentCodeFolder = ''
for (const umlClass of umlClassesSortedByCodePath) {
const codeFolder = dirname(umlClass.relativePath)
if (currentCodeFolder !== codeFolder) {
// Need to close off the last subgraph if not the first
if (currentCodeFolder != '') {
dotString += '\n}'
}

dotString += `
subgraph ${getSubGraphName(clusterFolders)} {
label="${codeFolder}"`

currentCodeFolder = codeFolder
}
dotString += convertClass2Dot(umlClass, classOptions)
}

// Need to close off the last subgraph if not the first
if (currentCodeFolder != '') {
dotString += '\n}'
}

dotString += addAssociationsToDot(umlClasses, classOptions)

// Need to close off the last the digraph
dotString += '\n}'

// debug(dotString)

return dotString
}

let subGraphCount = 0
function getSubGraphName(clusterFolders: boolean = false) {
if (clusterFolders) {
return ` cluster_${subGraphCount++}`
}
return ` graph_${subGraphCount++}`
}

function sortUmlClassesByCodePath(umlClasses: UmlClass[]): UmlClass[] {
return umlClasses.sort((a, b) => {
if (a.relativePath < b.relativePath) {
return -1
}
if (a.relativePath > b.relativePath) {
return 1
}
return 0
})
}

export function addAssociationsToDot(
umlClasses: UmlClass[],
classOptions: ClassOptions = {}
): string {
let dotString: string = ''

// for each class
for (const sourceUmlClass of umlClasses) {
if (!classOptions.hideEnums) {
// for each enum in the class
sourceUmlClass.enums.forEach((enumId) => {
// Has the enum been filtered out? eg depth limited
const targetUmlClass = umlClasses.find((c) => c.id === enumId)
if (targetUmlClass) {
// Draw aggregated link from contract to contract level Enum
dotString += `\n${enumId} -> ${sourceUmlClass.id} [arrowhead=diamond, weight=2]`
}
})
}
if (!classOptions.hideStructs) {
// for each struct in the class
sourceUmlClass.structs.forEach((structId) => {
// Has the struct been filtered out? eg depth limited
const targetUmlClass = umlClasses.find((c) => c.id === structId)
if (targetUmlClass) {
// Draw aggregated link from contract to contract level Struct
dotString += `\n${structId} -> ${sourceUmlClass.id} [arrowhead=diamond, weight=2]`
}
})
}

// for each association in that class
for (const association of Object.values(sourceUmlClass.associations)) {
const targetUmlClass = findAssociatedClass(
association,
sourceUmlClass,
umlClasses
)
if (targetUmlClass) {
dotString += addAssociationToDot(
sourceUmlClass,
targetUmlClass,
association,
classOptions
)
}
}
}

return dotString
}

function addAssociationToDot(
sourceUmlClass: UmlClass,
targetUmlClass: UmlClass,
association: Association,
classOptions: ClassOptions = {}
): string {
// do not include library or interface associations if hidden
// Or associations to Structs, Enums or Constants if they are hidden
if (
(classOptions.hideLibraries &&
(sourceUmlClass.stereotype === ClassStereotype.Library ||
targetUmlClass.stereotype === ClassStereotype.Library)) ||
(classOptions.hideInterfaces &&
(targetUmlClass.stereotype === ClassStereotype.Interface ||
sourceUmlClass.stereotype === ClassStereotype.Interface)) ||
(classOptions.hideAbstracts &&
(targetUmlClass.stereotype === ClassStereotype.Abstract ||
sourceUmlClass.stereotype === ClassStereotype.Abstract)) ||
(classOptions.hideStructs &&
targetUmlClass.stereotype === ClassStereotype.Struct) ||
(classOptions.hideEnums &&
targetUmlClass.stereotype === ClassStereotype.Enum) ||
(classOptions.hideConstants &&
targetUmlClass.stereotype === ClassStereotype.Constant)
) {
return ''
}

let dotString = `\n${sourceUmlClass.id} -> ${targetUmlClass.id} [`

if (
association.referenceType == ReferenceType.Memory ||
(association.realization &&
targetUmlClass.stereotype === ClassStereotype.Interface)
) {
dotString += 'style=dashed, '
}

if (association.realization) {
dotString += 'arrowhead=empty, arrowsize=3, '
if (!targetUmlClass.stereotype) {
dotString += 'weight=4, '
} else {
dotString += 'weight=3, '
}
}

return dotString + ']'
}
7 changes: 5 additions & 2 deletions libs/remix-ui/solidity-uml-gen/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export interface ISolidityUmlGen extends ViewPlugin {
updatedSvg: string
currentlySelectedTheme: string
themeName: string
themeDark: string
loading: boolean
themeCollection: ThemeSummary[]
activeTheme: ThemeSummary
showUmlDiagram(path: string, svgPayload: string): void
updateComponent(state: any): JSX.Element
setDispatch(dispatch: React.Dispatch<any>): void
Expand All @@ -19,10 +21,11 @@ export interface ISolidityUmlGen extends ViewPlugin {
flattenContract (source: any, filePath: string, data: any): Promise<string>
hideSpinner(): void
renderComponent (): void

triggerGenerateUml: boolean
render(): JSX.Element
}

export type ThemeQualityType = { name: string, quality: 'light' | 'dark', url: string }

export type ThemeSummary = { themeName: string, backgroundColor: string, actualHex: string }
export type ThemeSummary = { themeName: string, backgroundColor: string, textColor?: string,
shapeColor?: string, fillColor?: string }