From 35fc896c53630488c6848bcfb3a9b6e864dc86ee Mon Sep 17 00:00:00 2001 From: Joseph Izang Date: Wed, 12 Apr 2023 14:16:04 +0100 Subject: [PATCH 1/3] extract key function from sol2uml --- .../src/app/plugins/solidity-umlgen.tsx | 205 +++++++++++++++++- 1 file changed, 203 insertions(+), 2 deletions(-) diff --git a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx index 5e955d42db..797c3f00bf 100644 --- a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx +++ b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx @@ -6,11 +6,12 @@ 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 { 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 || [] @@ -86,7 +87,9 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen { } const ast = result.length > 1 ? parser.parse(result) : parser.parse(source.sources[file].content) const umlClasses = convertAST2UmlClasses(ast, this.currentFile) - const umlDot = convertUmlClasses2Dot(umlClasses) + let umlDot = '' + const matchTheme = themeCollection.filter(theme => theme.themeName === currentTheme.name) + umlDot = convertUmlClasses2Dot(umlClasses, false, { backColor: matchTheme[0].backgroundColor, textColor: matchTheme[0].textColor, shapeColor: '#caf4e9', fillColor: '#fbe7f8' }) const payload = vizRenderStringSync(umlDot) this.updatedSvg = payload _paq.push(['trackEvent', 'solidityumlgen', 'umlgenerated']) @@ -201,3 +204,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 + ']' +} \ No newline at end of file From 57e7ef7e1fcc7f735af7ba42dfe4809e15e74c81 Mon Sep 17 00:00:00 2001 From: Joseph Izang Date: Wed, 12 Apr 2023 14:23:00 +0100 Subject: [PATCH 2/3] fix errors --- .../src/app/plugins/solidity-umlgen.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx index 797c3f00bf..b3aa81065d 100644 --- a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx +++ b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx @@ -26,16 +26,16 @@ 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: '--body-bg', actualHex: '#011628', textColor: '#babbcc'}, + { themeName: 'Cerulean', backgroundColor: '--body-bg', actualHex: '#fff', textColor: '#343a40'}, + { themeName: 'Cyborg', backgroundColor: '--body-bg', actualHex: '#060606', textColor: '#adafae'}, + { themeName: 'Dark', backgroundColor: '--body-bg', actualHex: '#222336', textColor: '#babbcc'}, + { themeName: 'Flatly', backgroundColor: '--body-bg', actualHex: '#fff', textColor: '#7b8a8b'}, + { themeName: 'Black', backgroundColor: '--body-bg', actualHex: '#1a1a1a', textColor: '#babbcc'}, + { themeName: 'Light', backgroundColor: '--body-bg', actualHex: '#eef1f6', textColor: '#3b445e'}, + { themeName: 'Midcentuary', backgroundColor: '--body-bg', actualHex: '#DBE2E0', textColor: '#11556c'}, + { themeName: 'Spacelab', backgroundColor: '--body-bg', actualHex: '#fff', textColor: '#343a40'}, + { themeName: 'Candy', backgroundColor: '--body-bg', actualHex: '#d5efff', textColor: '#11556c', }, ] /** @@ -104,7 +104,7 @@ export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen { const themeQuality: ThemeQualityType = await this.call('theme', 'currentTheme') themeCollection.forEach((theme) => { if (theme.themeName === themeQuality.name) { - this.themeDark = theme.dark + this.themeDark = theme.actualHex } }) this.renderComponent() From 58804ce43580fce0ea8dfb5ca6c1b5bc625591f3 Mon Sep 17 00:00:00 2001 From: Joseph Izang Date: Thu, 13 Apr 2023 12:10:39 +0100 Subject: [PATCH 3/3] style generated uml and respond to theme change --- .../src/app/plugins/solidity-umlgen.tsx | 67 +++++++++++++------ .../solidity-uml-gen/src/types/index.ts | 7 +- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx index b3aa81065d..24c886763b 100644 --- a/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx +++ b/apps/remix-ide/src/app/plugins/solidity-umlgen.tsx @@ -6,7 +6,6 @@ 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' @@ -26,16 +25,26 @@ const profile = { } const themeCollection = [ - { themeName: 'HackerOwl', backgroundColor: '--body-bg', actualHex: '#011628', textColor: '#babbcc'}, - { themeName: 'Cerulean', backgroundColor: '--body-bg', actualHex: '#fff', textColor: '#343a40'}, - { themeName: 'Cyborg', backgroundColor: '--body-bg', actualHex: '#060606', textColor: '#adafae'}, - { themeName: 'Dark', backgroundColor: '--body-bg', actualHex: '#222336', textColor: '#babbcc'}, - { themeName: 'Flatly', backgroundColor: '--body-bg', actualHex: '#fff', textColor: '#7b8a8b'}, - { themeName: 'Black', backgroundColor: '--body-bg', actualHex: '#1a1a1a', textColor: '#babbcc'}, - { themeName: 'Light', backgroundColor: '--body-bg', actualHex: '#eef1f6', textColor: '#3b445e'}, - { themeName: 'Midcentuary', backgroundColor: '--body-bg', actualHex: '#DBE2E0', textColor: '#11556c'}, - { themeName: 'Spacelab', backgroundColor: '--body-bg', actualHex: '#fff', textColor: '#343a40'}, - { themeName: 'Candy', backgroundColor: '--body-bg', actualHex: '#d5efff', textColor: '#11556c', }, + { 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' }, ] /** @@ -53,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 = () => {} @@ -65,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 @@ -86,10 +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) + this.umlClasses = convertAST2UmlClasses(ast, this.currentFile) let umlDot = '' - const matchTheme = themeCollection.filter(theme => theme.themeName === currentTheme.name) - umlDot = convertUmlClasses2Dot(umlClasses, false, { backColor: matchTheme[0].backgroundColor, textColor: matchTheme[0].textColor, shapeColor: '#caf4e9', fillColor: '#fbe7f8' }) + 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']) @@ -99,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.actualHex + 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') }) } @@ -118,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) @@ -187,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, }) } diff --git a/libs/remix-ui/solidity-uml-gen/src/types/index.ts b/libs/remix-ui/solidity-uml-gen/src/types/index.ts index 7f0dd41736..210b9c6066 100644 --- a/libs/remix-ui/solidity-uml-gen/src/types/index.ts +++ b/libs/remix-ui/solidity-uml-gen/src/types/index.ts @@ -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): void @@ -19,10 +21,11 @@ export interface ISolidityUmlGen extends ViewPlugin { flattenContract (source: any, filePath: string, data: any): Promise 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 } \ No newline at end of file +export type ThemeSummary = { themeName: string, backgroundColor: string, textColor?: string, +shapeColor?: string, fillColor?: string } \ No newline at end of file