Merge pull request #3609 from ethereum/sol2uml-styling

Style Generated UML in Sol2Uml Plugin
pull/5370/head
Joseph Izang 2 years ago committed by GitHub
commit c9525375cf
  1. 266
      apps/remix-ide/src/app/plugins/solidity-umlgen.tsx
  2. 7
      libs/remix-ui/solidity-uml-gen/src/types/index.ts

@ -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 || []
@ -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' },
]
/**
@ -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> = () => {}
@ -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
@ -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'])
@ -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')
})
}
@ -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)
@ -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,
})
}
@ -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 + ']'
}

@ -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
@ -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 }
Loading…
Cancel
Save