|
|
|
@ -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 + ']' |
|
|
|
|
} |