Merge branch 'master' of https://github.com/ethereum/remix-project into resolve_with_package
commit
c81c559310
@ -0,0 +1,49 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { Plugin } from '@remixproject/engine' |
||||||
|
import { customAction } from '@remixproject/plugin-api' |
||||||
|
import { concatSourceFiles, getDependencyGraph } from '@remix-ui/solidity-compiler' |
||||||
|
|
||||||
|
const profile = { |
||||||
|
name: 'contractflattener', |
||||||
|
displayName: 'Contract Flattener', |
||||||
|
description: 'Flatten solidity contracts', |
||||||
|
methods: ['flattenAContract'], |
||||||
|
events: [], |
||||||
|
} |
||||||
|
|
||||||
|
export class ContractFlattener extends Plugin { |
||||||
|
fileName: string |
||||||
|
constructor() { |
||||||
|
super(profile) |
||||||
|
this.fileName = '' |
||||||
|
} |
||||||
|
|
||||||
|
onActivation(): void { |
||||||
|
this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data, input, version) => { |
||||||
|
await this.flattenContract(source, this.fileName, data) |
||||||
|
})
|
||||||
|
} |
||||||
|
|
||||||
|
async flattenAContract(action: customAction) { |
||||||
|
this.fileName = action.path[0] |
||||||
|
this.call('solidity', 'compile', this.fileName) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Takes currently compiled contract that has a bunch of imports at the top |
||||||
|
* and flattens them ready for UML creation. Takes the flattened result |
||||||
|
* and assigns to a local property |
||||||
|
* @returns {Promise<string>} |
||||||
|
*/ |
||||||
|
async flattenContract (source: any, filePath: string, data: any) { |
||||||
|
const ast = data.sources |
||||||
|
const dependencyGraph = getDependencyGraph(ast, filePath) |
||||||
|
const sorted = dependencyGraph.isEmpty() |
||||||
|
? [filePath] |
||||||
|
: dependencyGraph.sort().reverse() |
||||||
|
const sources = source.sources |
||||||
|
const result = concatSourceFiles(sorted, sources) |
||||||
|
await this.call('fileManager', 'writeFile', `${filePath}_flattened.sol`, result) |
||||||
|
return result |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,159 @@ |
|||||||
|
/* eslint-disable @nrwl/nx/enforce-module-boundaries */ |
||||||
|
import { ViewPlugin } from '@remixproject/engine-web' |
||||||
|
import React from 'react' |
||||||
|
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
|
||||||
|
import { RemixUiSolidityUmlGen } from '@remix-ui/solidity-uml-gen'
|
||||||
|
import { ISolidityUmlGen } from 'libs/remix-ui/solidity-uml-gen/src/types' |
||||||
|
import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types' |
||||||
|
import { concatSourceFiles, getDependencyGraph } 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' |
||||||
|
const parser = (window as any).SolidityParser |
||||||
|
|
||||||
|
const profile = { |
||||||
|
name: 'solidityumlgen', |
||||||
|
displayName: 'Solidity UML Generator', |
||||||
|
description: 'Generate UML diagram in svg format from last compiled contract', |
||||||
|
location: 'mainPanel', |
||||||
|
methods: ['showUmlDiagram', 'generateUml', 'generateCustomAction'], |
||||||
|
events: [], |
||||||
|
} |
||||||
|
|
||||||
|
export class SolidityUmlGen extends ViewPlugin implements ISolidityUmlGen { |
||||||
|
element: HTMLDivElement |
||||||
|
currentFile: string |
||||||
|
svgPayload: string |
||||||
|
updatedSvg: string |
||||||
|
currentlySelectedTheme: string |
||||||
|
loading: boolean |
||||||
|
|
||||||
|
appManager: RemixAppManager |
||||||
|
dispatch: React.Dispatch<any> = () => {} |
||||||
|
constructor(appManager: RemixAppManager) { |
||||||
|
super(profile) |
||||||
|
this.currentFile = '' |
||||||
|
this.svgPayload = '' |
||||||
|
this.updatedSvg = '' |
||||||
|
this.loading = false |
||||||
|
this.currentlySelectedTheme = '' |
||||||
|
this.appManager = appManager |
||||||
|
this.element = document.createElement('div') |
||||||
|
this.element.setAttribute('id', 'sol-uml-gen') |
||||||
|
} |
||||||
|
|
||||||
|
onActivation(): void { |
||||||
|
if (this.currentFile.length < 1)
|
||||||
|
this.on('solidity', 'compilationFinished', async (file, source, languageVersion, data, input, version) => { |
||||||
|
let result = '' |
||||||
|
try { |
||||||
|
if (data.sources && Object.keys(data.sources).length > 1) { // we should flatten first as there are multiple asts
|
||||||
|
result = await this.flattenContract(source, this.currentFile, data) |
||||||
|
} |
||||||
|
const ast = result.length > 1 ? parser.parse(result) : parser.parse(source.sources[this.currentFile].content) |
||||||
|
const umlClasses = convertAST2UmlClasses(ast, this.currentFile) |
||||||
|
const umlDot = convertUmlClasses2Dot(umlClasses) |
||||||
|
const payload = vizRenderStringSync(umlDot) |
||||||
|
const currentTheme = await this.call('theme', 'currentTheme') |
||||||
|
this.currentlySelectedTheme = currentTheme.quality |
||||||
|
this.updatedSvg = payload |
||||||
|
this.renderComponent() |
||||||
|
} catch (error) { |
||||||
|
console.log({ error }) |
||||||
|
} |
||||||
|
}) |
||||||
|
this.on('theme', 'themeChanged', (theme) => { |
||||||
|
this.currentlySelectedTheme = theme.quality |
||||||
|
this.renderComponent() |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async mangleSvgPayload(svgPayload: string) : Promise<string> { |
||||||
|
const parser = new DOMParser() |
||||||
|
const themeQuality = await this.call('theme', 'currentTheme') |
||||||
|
const parsedDocument = parser.parseFromString(svgPayload, 'image/svg+xml') |
||||||
|
const res = parsedDocument.documentElement |
||||||
|
parsedDocument.bgColor = '#cccabc' |
||||||
|
res.style.filter = themeQuality.quality === 'dark' ? 'invert(1)' : 'invert(0)' |
||||||
|
const stringifiedSvg = new XMLSerializer().serializeToString(parsedDocument) |
||||||
|
console.log({ parsedDocument, themeQuality, stringifiedSvg }) |
||||||
|
return stringifiedSvg |
||||||
|
} |
||||||
|
|
||||||
|
onDeactivation(): void { |
||||||
|
this.off('solidity', 'compilationFinished') |
||||||
|
} |
||||||
|
|
||||||
|
generateCustomAction = async (action: customAction) => { |
||||||
|
this.currentFile = action.path[0] |
||||||
|
await this.generateUml(action.path[0]) |
||||||
|
} |
||||||
|
|
||||||
|
async generateUml(currentFile: string) { |
||||||
|
await this.call('solidity', 'compile', currentFile) |
||||||
|
await this.call('tabs', 'focus', 'solidityumlgen') |
||||||
|
this.loading = true |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Takes currently compiled contract that has a bunch of imports at the top |
||||||
|
* and flattens them ready for UML creation. Takes the flattened result |
||||||
|
* and assigns to a local property |
||||||
|
* @returns {Promise<string>} |
||||||
|
*/ |
||||||
|
async flattenContract (source: any, filePath: string, data: any) { |
||||||
|
const hold = { data, source, filePath } |
||||||
|
const ast = data.sources |
||||||
|
const dependencyGraph = getDependencyGraph(ast, filePath) |
||||||
|
const sorted = dependencyGraph.isEmpty() |
||||||
|
? [filePath] |
||||||
|
: dependencyGraph.sort().reverse() |
||||||
|
const sources = source.sources |
||||||
|
const result = concatSourceFiles(sorted, sources) |
||||||
|
await this.call('fileManager', 'writeFile', `${filePath}_flattened.sol`, result) |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
async showUmlDiagram(svgPayload: string) { |
||||||
|
this.updatedSvg = svgPayload |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
|
||||||
|
hideSpinner() { |
||||||
|
this.loading = false |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
|
||||||
|
setDispatch (dispatch: React.Dispatch<any>) { |
||||||
|
this.dispatch = dispatch |
||||||
|
this.renderComponent() |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
return <div id='sol-uml-gen'> |
||||||
|
<PluginViewWrapper plugin={this} /> |
||||||
|
</div> |
||||||
|
} |
||||||
|
|
||||||
|
renderComponent () { |
||||||
|
this.dispatch({ |
||||||
|
...this, |
||||||
|
updatedSvg: this.updatedSvg, |
||||||
|
loading: this.loading, |
||||||
|
themeSelected: this.currentlySelectedTheme |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
updateComponent(state: any) { |
||||||
|
return <RemixUiSolidityUmlGen |
||||||
|
plugin={state} |
||||||
|
updatedSvg={state.updatedSvg} |
||||||
|
loading={state.loading} |
||||||
|
themeSelected={state.currentlySelectedTheme} |
||||||
|
/> |
||||||
|
} |
||||||
|
} |
||||||
|
|
After Width: | Height: | Size: 6.6 KiB |
@ -1,2 +1,3 @@ |
|||||||
export * from './lib/solidity-compiler' |
export * from './lib/solidity-compiler' |
||||||
export * from './lib/logic' |
export * from './lib/logic' |
||||||
|
export * from './lib/logic/flattenerUtilities' |
||||||
|
@ -0,0 +1,169 @@ |
|||||||
|
const IMPORT_SOLIDITY_REGEX = /^\s*import(\s+).*$/gm; |
||||||
|
const SPDX_SOLIDITY_REGEX = /^\s*\/\/ SPDX-License-Identifier:.*$/gm; |
||||||
|
|
||||||
|
export function getDependencyGraph(ast, target) { |
||||||
|
const graph = tsort(); |
||||||
|
const visited = {}; |
||||||
|
visited[target] = 1; |
||||||
|
_traverse(graph, visited, ast, target); |
||||||
|
return graph; |
||||||
|
} |
||||||
|
|
||||||
|
export function concatSourceFiles(files, sources) { |
||||||
|
|
||||||
|
let concat = ''; |
||||||
|
for (const file of files) { |
||||||
|
const source = sources[file].content; |
||||||
|
const sourceWithoutImport = source.replace(IMPORT_SOLIDITY_REGEX, ''); |
||||||
|
const sourceWithoutSPDX = sourceWithoutImport.replace(SPDX_SOLIDITY_REGEX, ''); |
||||||
|
concat += `\n// File: ${file}\n\n`; |
||||||
|
concat += sourceWithoutSPDX; |
||||||
|
} |
||||||
|
return concat; |
||||||
|
} |
||||||
|
|
||||||
|
function _traverse(graph, visited, ast, name) { |
||||||
|
const currentAst = ast[name].ast; |
||||||
|
const dependencies = _getDependencies(currentAst); |
||||||
|
for (const dependency of dependencies) { |
||||||
|
const path = resolve(name, dependency); |
||||||
|
if (path in visited) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
visited[path] = 1; |
||||||
|
graph.add(name, path); |
||||||
|
_traverse(graph, visited, ast, path); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function _getDependencies(ast) { |
||||||
|
const dependencies = ast.nodes |
||||||
|
.filter(node => node.nodeType === 'ImportDirective') |
||||||
|
.map(node => node.file); |
||||||
|
return dependencies; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// TSORT
|
||||||
|
|
||||||
|
function tsort(initial?: any) { |
||||||
|
const graph = new Graph(); |
||||||
|
|
||||||
|
if (initial) { |
||||||
|
initial.forEach(function (entry) { |
||||||
|
Graph.prototype.add.apply(graph, entry); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
return graph; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
function Graph() { |
||||||
|
this.nodes = {}; |
||||||
|
} |
||||||
|
|
||||||
|
// Add sorted items to the graph
|
||||||
|
Graph.prototype.add = function () { |
||||||
|
const self = this; |
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
let items = [].slice.call(arguments); |
||||||
|
|
||||||
|
if (items.length === 1 && Array.isArray(items[0])) |
||||||
|
items = items[0]; |
||||||
|
|
||||||
|
items.forEach(function (item) { |
||||||
|
if (!self.nodes[item]) { |
||||||
|
self.nodes[item] = []; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
for (let i = 1; i < items.length; i++) { |
||||||
|
const from = items[i]; |
||||||
|
const to = items[i - 1]; |
||||||
|
|
||||||
|
self.nodes[from].push(to); |
||||||
|
} |
||||||
|
|
||||||
|
return self; |
||||||
|
}; |
||||||
|
|
||||||
|
// Depth first search
|
||||||
|
// As given in http://en.wikipedia.org/wiki/Topological_sorting
|
||||||
|
Graph.prototype.sort = function () { |
||||||
|
const self = this; |
||||||
|
const nodes = Object.keys(this.nodes); |
||||||
|
|
||||||
|
const sorted = []; |
||||||
|
const marks = {}; |
||||||
|
|
||||||
|
for (let i = 0; i < nodes.length; i++) { |
||||||
|
const node = nodes[i]; |
||||||
|
|
||||||
|
if (!marks[node]) { |
||||||
|
visit(node); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return sorted; |
||||||
|
|
||||||
|
function visit(node) { |
||||||
|
if (marks[node] === 'temp') |
||||||
|
throw new Error("There is a cycle in the graph. It is not possible to derive a topological sort."); |
||||||
|
else if (marks[node]) |
||||||
|
return; |
||||||
|
|
||||||
|
marks[node] = 'temp'; |
||||||
|
self.nodes[node].forEach(visit); |
||||||
|
marks[node] = 'perm'; |
||||||
|
|
||||||
|
sorted.push(node); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
Graph.prototype.isEmpty = function () { |
||||||
|
const nodes = Object.keys(this.nodes); |
||||||
|
return nodes.length === 0; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// PATH
|
||||||
|
|
||||||
|
function resolve(parentPath, childPath) { |
||||||
|
if (_isAbsolute(childPath)) { |
||||||
|
return childPath; |
||||||
|
} |
||||||
|
const path = parentPath + '/../' + childPath; |
||||||
|
const pathParts = path.split('/'); |
||||||
|
const resolvedParts = _resolvePathArray(pathParts); |
||||||
|
const resolvedPath = resolvedParts |
||||||
|
.join('/') |
||||||
|
.replace('http:/', 'http://') |
||||||
|
.replace('https:/', 'https://'); |
||||||
|
return resolvedPath; |
||||||
|
} |
||||||
|
|
||||||
|
function _isAbsolute(path) { |
||||||
|
return path[0] !== '.'; |
||||||
|
} |
||||||
|
|
||||||
|
function _resolvePathArray(parts) { |
||||||
|
const res = []; |
||||||
|
for (let i = 0; i < parts.length; i++) { |
||||||
|
const p = parts[i]; |
||||||
|
|
||||||
|
// ignore empty parts
|
||||||
|
if (!p || p === '.') |
||||||
|
continue; |
||||||
|
|
||||||
|
if (p === '..') { |
||||||
|
if (res.length && res[res.length - 1] !== '..') { |
||||||
|
res.pop(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
res.push(p); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return res; |
||||||
|
} |
@ -0,0 +1,294 @@ |
|||||||
|
/* eslint-disable prefer-const */ |
||||||
|
import domToImage from 'dom-to-image'; |
||||||
|
import { jsPDF } from 'jspdf'; |
||||||
|
|
||||||
|
const _cloneNode = (node, javascriptEnabled) => { |
||||||
|
let child = node.firstChild |
||||||
|
const clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false) |
||||||
|
while (child) { |
||||||
|
if (javascriptEnabled === true || child.nodeType !== 1 || child.nodeName !== 'SCRIPT') { |
||||||
|
clone.appendChild(_cloneNode(child, javascriptEnabled)) |
||||||
|
} |
||||||
|
child = child.nextSibling |
||||||
|
} |
||||||
|
if (node.nodeType === 1) { |
||||||
|
if (node.nodeName === 'CANVAS') { |
||||||
|
clone.width = node.width |
||||||
|
clone.height = node.height |
||||||
|
clone.getContext('2d').drawImage(node, 0, 0) |
||||||
|
} else if (node.nodeName === 'TEXTAREA' || node.nodeName === 'SELECT') { |
||||||
|
clone.value = node.value |
||||||
|
} |
||||||
|
clone.addEventListener('load', (() => { |
||||||
|
clone.scrollTop = node.scrollTop |
||||||
|
clone.scrollLeft = node.scrollLeft |
||||||
|
}), true) |
||||||
|
} |
||||||
|
return clone |
||||||
|
} |
||||||
|
|
||||||
|
const _createElement = (tagName, {className, innerHTML, style}) => { |
||||||
|
let i |
||||||
|
let scripts |
||||||
|
const el = document.createElement(tagName) |
||||||
|
if (className) { |
||||||
|
el.className = className |
||||||
|
} |
||||||
|
if (innerHTML) { |
||||||
|
el.innerHTML = innerHTML |
||||||
|
scripts = el.getElementsByTagName('script') |
||||||
|
i = scripts.length |
||||||
|
while (i-- > 0) { |
||||||
|
scripts[i].parentNode.removeChild(scripts[i]) |
||||||
|
} |
||||||
|
} |
||||||
|
for (const key in style) { |
||||||
|
el.style[key] = style[key]; |
||||||
|
} |
||||||
|
return el; |
||||||
|
}; |
||||||
|
|
||||||
|
const _isCanvasBlank = canvas => { |
||||||
|
const blank = document.createElement('canvas'); |
||||||
|
blank.width = canvas.width; |
||||||
|
blank.height = canvas.height; |
||||||
|
const ctx = blank.getContext('2d'); |
||||||
|
ctx.fillStyle = '#FFFFFF'; |
||||||
|
ctx.fillRect(0, 0, blank.width, blank.height); |
||||||
|
return canvas.toDataURL() === blank.toDataURL(); |
||||||
|
}; |
||||||
|
|
||||||
|
const downloadPdf = (dom, options, cb) => { |
||||||
|
const a4Height = 841.89; |
||||||
|
const a4Width = 595.28; |
||||||
|
let overrideWidth; |
||||||
|
let container; |
||||||
|
let containerCSS; |
||||||
|
let containerWidth; |
||||||
|
let elements; |
||||||
|
let excludeClassNames; |
||||||
|
let excludeTagNames; |
||||||
|
let filename; |
||||||
|
let filterFn; |
||||||
|
let innerRatio; |
||||||
|
let overlay; |
||||||
|
let overlayCSS; |
||||||
|
let pageHeightPx; |
||||||
|
let proxyUrl; |
||||||
|
let compression = 'NONE'; |
||||||
|
let scale; |
||||||
|
let opts; |
||||||
|
let offsetHeight; |
||||||
|
let offsetWidth; |
||||||
|
let scaleObj; |
||||||
|
let style; |
||||||
|
const transformOrigin = 'top left'; |
||||||
|
const pdfOptions: any = { |
||||||
|
orientation: 'l', |
||||||
|
unit: 'pt', |
||||||
|
format: 'a4' |
||||||
|
}; |
||||||
|
|
||||||
|
({filename, excludeClassNames = [], excludeTagNames = ['button', 'input', 'select'], overrideWidth, proxyUrl, compression, scale} = options); |
||||||
|
|
||||||
|
overlayCSS = { |
||||||
|
position: 'fixed', |
||||||
|
zIndex: 1000, |
||||||
|
opacity: 0, |
||||||
|
left: 0, |
||||||
|
right: 0, |
||||||
|
bottom: 0, |
||||||
|
top: 0, |
||||||
|
backgroundColor: 'rgba(0,0,0,0.8)' |
||||||
|
}; |
||||||
|
if (overrideWidth) { |
||||||
|
overlayCSS.width = `${overrideWidth}px`; |
||||||
|
} |
||||||
|
containerCSS = { |
||||||
|
position: 'absolute', |
||||||
|
left: 0, |
||||||
|
right: 0, |
||||||
|
top: 0, |
||||||
|
height: 'auto', |
||||||
|
margin: 'auto', |
||||||
|
overflow: 'auto', |
||||||
|
backgroundColor: 'white' |
||||||
|
}; |
||||||
|
overlay = _createElement('div', { |
||||||
|
style: overlayCSS, |
||||||
|
className: '', |
||||||
|
innerHTML: '' |
||||||
|
}); |
||||||
|
container = _createElement('div', { |
||||||
|
style: containerCSS, |
||||||
|
className: '', |
||||||
|
innerHTML: '' |
||||||
|
}); |
||||||
|
//@ts-ignore
|
||||||
|
container.appendChild(_cloneNode(dom)); |
||||||
|
overlay.appendChild(container); |
||||||
|
document.body.appendChild(overlay); |
||||||
|
innerRatio = a4Height / a4Width; |
||||||
|
containerWidth = overrideWidth || container.getBoundingClientRect().width; |
||||||
|
pageHeightPx = Math.floor(containerWidth * innerRatio); |
||||||
|
elements = container.querySelectorAll('*'); |
||||||
|
|
||||||
|
for (let i = 0, len = excludeClassNames.length; i < len; i++) { |
||||||
|
const clName = excludeClassNames[i]; |
||||||
|
container.querySelectorAll(`.${clName}`).forEach(function(a) { |
||||||
|
return a.remove(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
for (let j = 0, len1 = excludeTagNames.length; j < len1; j++) { |
||||||
|
const tName = excludeTagNames[j]; |
||||||
|
let els = container.getElementsByTagName(tName); |
||||||
|
|
||||||
|
for (let k = els.length - 1; k >= 0; k--) { |
||||||
|
if (!els[k]) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
els[k].parentNode.removeChild(els[k]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Array.prototype.forEach.call(elements, el => { |
||||||
|
let clientRect; |
||||||
|
let endPage; |
||||||
|
let nPages; |
||||||
|
let pad; |
||||||
|
let rules; |
||||||
|
let startPage; |
||||||
|
rules = { |
||||||
|
before: false, |
||||||
|
after: false, |
||||||
|
avoid: true |
||||||
|
}; |
||||||
|
clientRect = el.getBoundingClientRect(); |
||||||
|
if (rules.avoid && !rules.before) { |
||||||
|
startPage = Math.floor(clientRect.top / pageHeightPx); |
||||||
|
endPage = Math.floor(clientRect.bottom / pageHeightPx); |
||||||
|
nPages = Math.abs(clientRect.bottom - clientRect.top) / pageHeightPx; |
||||||
|
// Turn on rules.before if the el is broken and is at most one page long.
|
||||||
|
if (endPage !== startPage && nPages <= 1) { |
||||||
|
rules.before = true; |
||||||
|
} |
||||||
|
// Before: Create a padding div to push the element to the next page.
|
||||||
|
if (rules.before) { |
||||||
|
pad = _createElement('div', { |
||||||
|
className: '', |
||||||
|
innerHTML: '', |
||||||
|
style: { |
||||||
|
display: 'block', |
||||||
|
height: `${pageHeightPx - clientRect.top % pageHeightPx}px` |
||||||
|
} |
||||||
|
}); |
||||||
|
return el.parentNode.insertBefore(pad, el); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Remove unnecessary elements from result pdf
|
||||||
|
filterFn = ({classList, tagName}) => { |
||||||
|
let cName; |
||||||
|
let j; |
||||||
|
let len; |
||||||
|
let ref; |
||||||
|
if (classList) { |
||||||
|
for (j = 0, len = excludeClassNames.length; j < len; j++) { |
||||||
|
cName = excludeClassNames[j]; |
||||||
|
if (Array.prototype.indexOf.call(classList, cName) >= 0) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
ref = tagName != null ? tagName.toLowerCase() : undefined; |
||||||
|
return excludeTagNames.indexOf(ref) < 0; |
||||||
|
}; |
||||||
|
|
||||||
|
opts = { |
||||||
|
filter: filterFn, |
||||||
|
proxy: proxyUrl |
||||||
|
}; |
||||||
|
|
||||||
|
if (scale) { |
||||||
|
offsetWidth = container.offsetWidth; |
||||||
|
offsetHeight = container.offsetHeight; |
||||||
|
style = { |
||||||
|
transform: 'scale(' + scale + ')', |
||||||
|
transformOrigin: transformOrigin, |
||||||
|
width: offsetWidth + 'px', |
||||||
|
height: offsetHeight + 'px' |
||||||
|
}; |
||||||
|
scaleObj = { |
||||||
|
width: offsetWidth * scale, |
||||||
|
height: offsetHeight * scale, |
||||||
|
quality: 1, |
||||||
|
style: style |
||||||
|
}; |
||||||
|
opts = Object.assign(opts, scaleObj); |
||||||
|
} |
||||||
|
|
||||||
|
return domToImage.toCanvas(container, opts).then(canvas => { |
||||||
|
let h; |
||||||
|
let imgData; |
||||||
|
let nPages; |
||||||
|
let page; |
||||||
|
let pageCanvas; |
||||||
|
let pageCtx; |
||||||
|
let pageHeight; |
||||||
|
let pdf; |
||||||
|
let pxFullHeight; |
||||||
|
let w; |
||||||
|
// Remove overlay
|
||||||
|
document.body.removeChild(overlay); |
||||||
|
// Initialize the PDF.
|
||||||
|
pdf = new jsPDF(pdfOptions); |
||||||
|
// Calculate the number of pages.
|
||||||
|
pxFullHeight = canvas.height; |
||||||
|
nPages = Math.ceil(pxFullHeight / pageHeightPx); |
||||||
|
// Define pageHeight separately so it can be trimmed on the final page.
|
||||||
|
pageHeight = a4Height; |
||||||
|
pageCanvas = document.createElement('canvas'); |
||||||
|
pageCtx = pageCanvas.getContext('2d'); |
||||||
|
pageCanvas.width = canvas.width; |
||||||
|
pageCanvas.height = pageHeightPx; |
||||||
|
page = 0; |
||||||
|
while (page < nPages) { |
||||||
|
if (page === nPages - 1 && pxFullHeight % pageHeightPx !== 0) { |
||||||
|
pageCanvas.height = pxFullHeight % pageHeightPx; |
||||||
|
pageHeight = pageCanvas.height * a4Width / pageCanvas.width; |
||||||
|
} |
||||||
|
w = pageCanvas.width; |
||||||
|
h = pageCanvas.height; |
||||||
|
pageCtx.fillStyle = 'white'; |
||||||
|
pageCtx.fillRect(0, 0, w, h); |
||||||
|
pageCtx.drawImage(canvas, 0, page * pageHeightPx, w, h, 0, 0, w, h); |
||||||
|
// Don't create blank pages
|
||||||
|
if (_isCanvasBlank(pageCanvas)) { |
||||||
|
++page; |
||||||
|
continue; |
||||||
|
} |
||||||
|
// Add the page to the PDF.
|
||||||
|
if (page) { |
||||||
|
pdf.addPage(); |
||||||
|
} |
||||||
|
imgData = pageCanvas.toDataURL('image/PNG'); |
||||||
|
pdf.addImage(imgData, 'PNG', 0, 0, a4Width, pageHeight, undefined, compression); |
||||||
|
++page; |
||||||
|
} |
||||||
|
if (typeof cb === "function") { |
||||||
|
cb(pdf); |
||||||
|
} |
||||||
|
return pdf.save(filename); |
||||||
|
}).catch(error => { |
||||||
|
// Remove overlay
|
||||||
|
document.body.removeChild(overlay); |
||||||
|
if (typeof cb === "function") { |
||||||
|
cb(null); |
||||||
|
} |
||||||
|
return console.error(error); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports = downloadPdf; |
@ -0,0 +1 @@ |
|||||||
|
export * from './lib/solidity-uml-gen' |
@ -0,0 +1,3 @@ |
|||||||
|
.remixui_default-message { |
||||||
|
margin-top: 100px; |
||||||
|
} |
@ -0,0 +1,118 @@ |
|||||||
|
import { CustomTooltip } from '@remix-ui/helper' |
||||||
|
import React, { Fragment, useEffect, useState } from 'react' |
||||||
|
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch' |
||||||
|
import { ISolidityUmlGen } from '../types' |
||||||
|
import './css/solidity-uml-gen.css' |
||||||
|
export interface RemixUiSolidityUmlGenProps { |
||||||
|
plugin?: ISolidityUmlGen |
||||||
|
updatedSvg?: string |
||||||
|
loading?: boolean |
||||||
|
themeSelected?: string |
||||||
|
} |
||||||
|
|
||||||
|
type ButtonAction = { |
||||||
|
svgValid: () => boolean |
||||||
|
action: () => void |
||||||
|
buttonText: string |
||||||
|
icon?: string |
||||||
|
customcss?: string |
||||||
|
} |
||||||
|
|
||||||
|
interface ActionButtonsProps { |
||||||
|
buttons: ButtonAction[] |
||||||
|
} |
||||||
|
|
||||||
|
const ActionButtons = ({ buttons }: ActionButtonsProps) => ( |
||||||
|
<> |
||||||
|
{buttons.map(btn => ( |
||||||
|
<CustomTooltip |
||||||
|
key={btn.buttonText} |
||||||
|
placement="top" |
||||||
|
tooltipText={btn.buttonText} |
||||||
|
tooltipId={btn.buttonText} |
||||||
|
> |
||||||
|
<button |
||||||
|
key={btn.buttonText} |
||||||
|
className={`btn btn-primary btn-sm rounded-circle ${btn.customcss}`} |
||||||
|
disabled={!btn.svgValid} |
||||||
|
onClick={btn.action} |
||||||
|
> |
||||||
|
<i className={btn.icon}></i> |
||||||
|
</button> |
||||||
|
</CustomTooltip> |
||||||
|
))} |
||||||
|
</> |
||||||
|
) |
||||||
|
|
||||||
|
export function RemixUiSolidityUmlGen ({ plugin, updatedSvg, loading, themeSelected }: RemixUiSolidityUmlGenProps) { |
||||||
|
const [showViewer, setShowViewer] = useState(false) |
||||||
|
const [svgPayload, setSVGPayload] = useState<string>('') |
||||||
|
const [validSvg, setValidSvg] = useState(false) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setValidSvg (updatedSvg.startsWith('<?xml') && updatedSvg.includes('<svg'))
|
||||||
|
setShowViewer(updatedSvg.startsWith('<?xml') && updatedSvg.includes('<svg')) |
||||||
|
} |
||||||
|
, [updatedSvg]) |
||||||
|
|
||||||
|
const buttons: ButtonAction[] = [ |
||||||
|
{
|
||||||
|
buttonText: 'Download as PDF', |
||||||
|
svgValid: () => validSvg, |
||||||
|
action: () => console.log('generated!!'), |
||||||
|
icon: 'fa mr-1 pt-1 pb-1 fa-file' |
||||||
|
}, |
||||||
|
{
|
||||||
|
buttonText: 'Download as PNG', |
||||||
|
svgValid: () => validSvg, |
||||||
|
action: () => console.log('generated!!'), |
||||||
|
icon: 'fa fa-picture-o' |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
const DefaultInfo = () => ( |
||||||
|
<div className="d-flex flex-column justify-content-center align-items-center mt-5"> |
||||||
|
<h2 className="h2 align-self-start"><p>To view your contract as a Uml Diragram</p></h2> |
||||||
|
<h3 className="h4 align-self-start"><p>Right Click on your contract file (Usually ends with .sol)</p></h3> |
||||||
|
<h3 className="h4 align-self-start"><p>Click on Generate UML</p></h3> |
||||||
|
</div> |
||||||
|
) |
||||||
|
const Display = () => { |
||||||
|
const invert = themeSelected === 'dark' ? 'invert(0.8)' : 'invert(0)' |
||||||
|
return ( |
||||||
|
<div className="d-flex flex-column justify-content-center align-items-center"> |
||||||
|
<div id="umlImageHolder" className="w-100 px-2 py-2"> |
||||||
|
{ validSvg && showViewer ? ( |
||||||
|
<TransformWrapper |
||||||
|
initialScale={1} |
||||||
|
> |
||||||
|
{ |
||||||
|
({ zoomIn, zoomOut, resetTransform }) => ( |
||||||
|
<Fragment> |
||||||
|
<TransformComponent> |
||||||
|
<img
|
||||||
|
src={`data:image/svg+xml;base64,${btoa(plugin.updatedSvg ?? svgPayload)}`} |
||||||
|
width={'100%'} |
||||||
|
height={'auto'} |
||||||
|
style={{ filter: invert }} |
||||||
|
/> |
||||||
|
</TransformComponent> |
||||||
|
</Fragment> |
||||||
|
) |
||||||
|
} |
||||||
|
</TransformWrapper> |
||||||
|
) : loading ? <div className="justify-content-center align-items-center d-flex mx-auto my-auto"> |
||||||
|
<i className="fas fa-spinner fa-spin fa-4x"></i> |
||||||
|
</div> : <DefaultInfo />} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
return (<> |
||||||
|
{ <Display /> } |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default RemixUiSolidityUmlGen |
@ -0,0 +1,14 @@ |
|||||||
|
import { ViewPlugin } from '@remixproject/engine-web' |
||||||
|
import React from 'react' |
||||||
|
|
||||||
|
export interface ISolidityUmlGen extends ViewPlugin { |
||||||
|
element: HTMLDivElement |
||||||
|
currentFile: string |
||||||
|
svgPayload: string |
||||||
|
updatedSvg: string |
||||||
|
showUmlDiagram(path: string, svgPayload: string): void |
||||||
|
updateComponent(state: any): JSX.Element |
||||||
|
setDispatch(dispatch: React.Dispatch<any>): void |
||||||
|
|
||||||
|
render(): JSX.Element |
||||||
|
} |
Loading…
Reference in new issue