parent
5d341962bb
commit
d3489d7889
@ -0,0 +1,2 @@ |
|||||||
|
service_name: circleci |
||||||
|
repo_token: $COVERALLS_REPO_TOKEN |
@ -0,0 +1,119 @@ |
|||||||
|
import { |
||||||
|
CompilationResult, |
||||||
|
CompiledContract, |
||||||
|
DeveloperDocumentation, |
||||||
|
UserDocumentation, |
||||||
|
DevMethodDoc, |
||||||
|
FunctionDescription, |
||||||
|
UserMethodDoc, |
||||||
|
ABIParameter |
||||||
|
} from '@remixproject/plugin-iframe' |
||||||
|
|
||||||
|
type TemplateDoc<T> = { [key in keyof T]: (...params: any[]) => string } |
||||||
|
|
||||||
|
/** Create documentation for a compilation result */ |
||||||
|
export function createDoc(result: CompilationResult) { |
||||||
|
return Object.keys(result.contracts).reduce((acc, fileName) => { |
||||||
|
const contracts = result.contracts[fileName] |
||||||
|
Object.keys(contracts).forEach((name) => (acc[name] = getContractDoc(name, contracts[name]))) |
||||||
|
return acc |
||||||
|
}, {}) |
||||||
|
} |
||||||
|
|
||||||
|
//////////////
|
||||||
|
// CONTRACT //
|
||||||
|
//////////////
|
||||||
|
|
||||||
|
type ContractDoc = DeveloperDocumentation & UserDocumentation |
||||||
|
|
||||||
|
/** Map of the content to display for a contract */ |
||||||
|
const contractDocTemplate: TemplateDoc<ContractDoc> = { |
||||||
|
author: (author: string) => `> Created By ${author}\n`, |
||||||
|
details: (details: string) => `${details}`, |
||||||
|
title: (title: string) => `## ${title}`, |
||||||
|
notice: (notice: string) => `_${notice}_`, |
||||||
|
methods: () => '' // Methods is managed by getMethod()
|
||||||
|
} |
||||||
|
|
||||||
|
/** Create the documentation for a contract */ |
||||||
|
function getContractDoc(name: string, contract: CompiledContract) { |
||||||
|
const methods = { ...contract.userdoc.methods, ...contract.devdoc.methods } |
||||||
|
const contractDoc = { ...contract.userdoc, ...contract.devdoc, methods } |
||||||
|
|
||||||
|
const methodsDoc = contract.abi |
||||||
|
.map((def: FunctionDescription) => { |
||||||
|
if (def.type === 'constructor') { |
||||||
|
def.name = 'constructor' |
||||||
|
// because "constructor" is a string and not a { notice } object for userdoc we need to do that
|
||||||
|
const methodDoc = { |
||||||
|
...(contract.devdoc.methods.constructor || {}), |
||||||
|
notice: contract.userdoc.methods.constructor as string |
||||||
|
} |
||||||
|
return getMethodDoc(def, methodDoc) |
||||||
|
} else { |
||||||
|
if (def.type === 'fallback') def.name = 'fallback' |
||||||
|
const method = Object.keys(contractDoc.methods).find((key) => key.includes(def.name)) |
||||||
|
const methodDoc = contractDoc.methods[method] |
||||||
|
return getMethodDoc(def, methodDoc) |
||||||
|
} |
||||||
|
}) |
||||||
|
.join('\n') |
||||||
|
|
||||||
|
const doc = Object.keys(contractDoc) |
||||||
|
.filter((key) => key !== 'methods') |
||||||
|
.map((key) => contractDocTemplate[key](contractDoc[key])) |
||||||
|
.join('\n') |
||||||
|
|
||||||
|
return `# ${name}\n${doc}\n${methodsDoc}` |
||||||
|
} |
||||||
|
|
||||||
|
////////////
|
||||||
|
// METHOD //
|
||||||
|
////////////
|
||||||
|
type MethodDoc = DevMethodDoc & UserMethodDoc |
||||||
|
|
||||||
|
/** Map of the content to display for a method */ |
||||||
|
const devMethodDocTemplate: TemplateDoc<MethodDoc> = { |
||||||
|
author: (author: string) => `> Created By ${author}\n`, |
||||||
|
details: (details: string) => details, |
||||||
|
return: (value: string) => `Return : ${value}`, |
||||||
|
notice: (notice: string) => notice, |
||||||
|
returns: () => '', // Implemented by getParams()
|
||||||
|
params: () => '' // Implemented by getParams()
|
||||||
|
} |
||||||
|
|
||||||
|
/** Create a table of param */ |
||||||
|
const getParams = (params: string[]) => |
||||||
|
params.length === 0 |
||||||
|
? '_No parameters_' |
||||||
|
: `|name |type |description
|
||||||
|
|-----|-----|----------- |
||||||
|
${params.join('\n')}` |
||||||
|
|
||||||
|
/** Get the details of a method */ |
||||||
|
const getMethodDetails = (devMethod: Partial<MethodDoc>) => |
||||||
|
!devMethod |
||||||
|
? '**Add Documentation for the method here**' |
||||||
|
: Object.keys(devMethod) |
||||||
|
.filter((key) => key !== 'params') |
||||||
|
.map((key) => devMethodDocTemplate[key](devMethod[key])) |
||||||
|
.join('\n') |
||||||
|
|
||||||
|
function extractParams(params: ABIParameter[], devparams: any) { |
||||||
|
return params.map((input) => { |
||||||
|
const description = devparams[input.name] || '' |
||||||
|
return `|${input.name}|${input.type}|${description}` |
||||||
|
}) |
||||||
|
} |
||||||
|
/** Get the doc for a method */ |
||||||
|
function getMethodDoc(def: FunctionDescription, devdoc?: Partial<MethodDoc>) { |
||||||
|
const doc = devdoc || {} |
||||||
|
const devparams = doc.params || {} |
||||||
|
const params = extractParams(def.inputs || [], devparams) |
||||||
|
const returns = extractParams(def.outputs || [], devparams) |
||||||
|
return ` |
||||||
|
## ${def.name} - ${def.constant ? 'view' : 'read'} |
||||||
|
${getParams(params)} |
||||||
|
${getMethodDetails(devdoc)} |
||||||
|
${`Returns:\n${getParams(returns)}`}` |
||||||
|
} |
After Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,77 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
||||||
|
<meta name="theme-color" content="#000000" /> |
||||||
|
<meta |
||||||
|
name="description" |
||||||
|
content="Web site created using create-react-app" |
||||||
|
/> |
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> |
||||||
|
<!-- |
||||||
|
manifest.json provides metadata used when your web app is installed on a |
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ |
||||||
|
--> |
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
||||||
|
<!-- |
||||||
|
Notice the use of %PUBLIC_URL% in the tags above. |
||||||
|
It will be replaced with the URL of the `public` folder during the build. |
||||||
|
Only files inside the `public` folder can be referenced from the HTML. |
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
||||||
|
work correctly both with client-side routing and a non-root public URL. |
||||||
|
Learn how to configure a non-root public URL by running `npm run build`. |
||||||
|
--> |
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous"> |
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> |
||||||
|
<!-- Global site tag (gtag.js) - Google Analytics --> |
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-146337217-6"></script> |
||||||
|
<script> |
||||||
|
window.dataLayer = window.dataLayer || []; |
||||||
|
function gtag(){dataLayer.push(arguments);} |
||||||
|
gtag('js', new Date()); |
||||||
|
|
||||||
|
gtag('config', ''); |
||||||
|
</script> |
||||||
|
<style> |
||||||
|
*, |
||||||
|
*:after, |
||||||
|
*:before { |
||||||
|
-webkit-box-sizing: border-box; |
||||||
|
-moz-box-sizing: border-box; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
body, |
||||||
|
html { |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
body { |
||||||
|
padding: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
||||||
|
|
||||||
|
<title>Remix Plugin</title> |
||||||
|
|
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript> |
||||||
|
<div id="root"></div> |
||||||
|
<!-- |
||||||
|
This HTML file is a template. |
||||||
|
If you open it directly in the browser, you will see an empty page. |
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file. |
||||||
|
The build step will place the bundled scripts into the <body> tag. |
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`. |
||||||
|
To create a production bundle, use `npm run build` or `yarn build`. |
||||||
|
--> |
||||||
|
</body> |
||||||
|
</html> |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 9.4 KiB |
@ -0,0 +1,25 @@ |
|||||||
|
{ |
||||||
|
"short_name": "React App", |
||||||
|
"name": "Create React App Sample", |
||||||
|
"icons": [ |
||||||
|
{ |
||||||
|
"src": "favicon.ico", |
||||||
|
"sizes": "64x64 32x32 24x24 16x16", |
||||||
|
"type": "image/x-icon" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"src": "logo192.png", |
||||||
|
"type": "image/png", |
||||||
|
"sizes": "192x192" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"src": "logo512.png", |
||||||
|
"type": "image/png", |
||||||
|
"sizes": "512x512" |
||||||
|
} |
||||||
|
], |
||||||
|
"start_url": ".", |
||||||
|
"display": "standalone", |
||||||
|
"theme_color": "#000000", |
||||||
|
"background_color": "#ffffff" |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
# https://www.robotstxt.org/robotstxt.html |
||||||
|
User-agent: * |
||||||
|
Disallow: |
@ -0,0 +1,9 @@ |
|||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
#ethdoc .active { |
||||||
|
color: #212529; |
||||||
|
background-color: #e9ecef; |
||||||
|
border-color: #e9ecef; |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
import React, { useState, useEffect, useRef } from "react" |
||||||
|
|
||||||
|
import { |
||||||
|
createIframeClient, |
||||||
|
CompilationFileSources, |
||||||
|
CompilationResult, |
||||||
|
Status |
||||||
|
} from "@remixproject/plugin" |
||||||
|
|
||||||
|
import { AppContext } from "./AppContext" |
||||||
|
import { Routes } from "./routes" |
||||||
|
import { useLocalStorage } from "./hooks/useLocalStorage" |
||||||
|
import { createDocumentation } from "./utils/utils" |
||||||
|
|
||||||
|
import "./App.css" |
||||||
|
import { ContractName, Documentation } from "./types" |
||||||
|
|
||||||
|
const devMode = { port: 8080 } |
||||||
|
|
||||||
|
export const getNewContractNames = (compilationResult: CompilationResult) => { |
||||||
|
const compiledContracts = compilationResult.contracts |
||||||
|
let result: string[] = [] |
||||||
|
|
||||||
|
for (const file of Object.keys(compiledContracts)) { |
||||||
|
const newContractNames = Object.keys(compiledContracts[file]) |
||||||
|
result = [...result, ...newContractNames] |
||||||
|
} |
||||||
|
|
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
const sampleMap = new Map<ContractName, Documentation>() |
||||||
|
|
||||||
|
const App = () => { |
||||||
|
const [clientInstance, setClientInstance] = useState(undefined as any) |
||||||
|
const [contracts, setContracts] = useState(sampleMap) |
||||||
|
const [sites, setSites] = useLocalStorage('sites', []) |
||||||
|
const clientInstanceRef = useRef(clientInstance) |
||||||
|
clientInstanceRef.current = clientInstance |
||||||
|
const contractsRef = useRef(contracts) |
||||||
|
contractsRef.current = contracts |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
console.log("Remix EthDoc loading...") |
||||||
|
const client = createIframeClient({ devMode }) |
||||||
|
const loadClient = async () => { |
||||||
|
await client.onload() |
||||||
|
setClientInstance(client) |
||||||
|
console.log("Remix EthDoc Plugin has been loaded") |
||||||
|
|
||||||
|
client.solidity.on("compilationFinished", (fileName: string, source: CompilationFileSources, languageVersion: string, data: CompilationResult) => { |
||||||
|
console.log("New compilation received") |
||||||
|
|
||||||
|
const existingMap = contractsRef.current; |
||||||
|
const newContractsMapWithDocumentation = createDocumentation(fileName, data) |
||||||
|
const newMap = new Map([...existingMap, ...newContractsMapWithDocumentation]) |
||||||
|
|
||||||
|
console.log("New Map", newMap) |
||||||
|
|
||||||
|
const status: Status = { key: 'succeed', type: 'success', title: 'New documentation ready' } |
||||||
|
clientInstanceRef.current.emit('statusChanged', status) |
||||||
|
setContracts(newMap) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
loadClient() |
||||||
|
}, []) |
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<AppContext.Provider |
||||||
|
value={{ |
||||||
|
clientInstance, |
||||||
|
contracts, |
||||||
|
setContracts, |
||||||
|
sites, |
||||||
|
setSites |
||||||
|
}}> |
||||||
|
<Routes /> |
||||||
|
</AppContext.Provider> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default App |
@ -0,0 +1,17 @@ |
|||||||
|
import React from "react" |
||||||
|
import { PluginApi, IRemixApi, Api, PluginClient, CompilationResult } from "@remixproject/plugin" |
||||||
|
|
||||||
|
import { ContractName, Documentation, PublishedSite } from "./types" |
||||||
|
|
||||||
|
export const AppContext = React.createContext({ |
||||||
|
clientInstance: {} as PluginApi<Readonly<IRemixApi>> & |
||||||
|
PluginClient<Api, Readonly<IRemixApi>>, |
||||||
|
contracts: new Map<ContractName, Documentation>(), |
||||||
|
setContracts: (contracts: Map<ContractName, Documentation>) => { |
||||||
|
console.log("Calling Set Contract Names") |
||||||
|
}, |
||||||
|
sites: [], |
||||||
|
setSites: (sites: PublishedSite[]) => { |
||||||
|
console.log("Calling Set Sites") |
||||||
|
} |
||||||
|
}) |
@ -0,0 +1,37 @@ |
|||||||
|
import { useState } from "react" |
||||||
|
|
||||||
|
export function useLocalStorage(key: string, initialValue: any) { |
||||||
|
// State to store our value
|
||||||
|
// Pass initial state function to useState so logic is only executed once
|
||||||
|
const [storedValue, setStoredValue] = useState(() => { |
||||||
|
try { |
||||||
|
// Get from local storage by key
|
||||||
|
const item = window.localStorage.getItem(key) |
||||||
|
// Parse stored json or if none return initialValue
|
||||||
|
return item ? JSON.parse(item) : initialValue |
||||||
|
} catch (error) { |
||||||
|
// If error also return initialValue
|
||||||
|
console.log(error) |
||||||
|
return initialValue |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// Return a wrapped version of useState's setter function that ...
|
||||||
|
// ... persists the new value to localStorage.
|
||||||
|
const setValue = (value: any) => { |
||||||
|
try { |
||||||
|
// Allow value to be a function so we have same API as useState
|
||||||
|
const valueToStore = |
||||||
|
value instanceof Function ? value(storedValue) : value |
||||||
|
// Save state
|
||||||
|
setStoredValue(valueToStore) |
||||||
|
// Save to local storage
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(valueToStore)) |
||||||
|
} catch (error) { |
||||||
|
// A more advanced implementation would handle the error case
|
||||||
|
console.log(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return [storedValue, setValue] |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import ReactDOM from 'react-dom'; |
||||||
|
import App from './App'; |
||||||
|
import { Routes } from './routes' |
||||||
|
|
||||||
|
ReactDOM.render( |
||||||
|
<React.StrictMode> |
||||||
|
<App /> |
||||||
|
</React.StrictMode>, |
||||||
|
document.getElementById('root') |
||||||
|
); |
@ -0,0 +1 @@ |
|||||||
|
/// <reference types="react-scripts" />
|
@ -0,0 +1,36 @@ |
|||||||
|
import React from "react" |
||||||
|
import { |
||||||
|
BrowserRouter as Router, |
||||||
|
Switch, |
||||||
|
Route, |
||||||
|
RouteProps, |
||||||
|
} from "react-router-dom" |
||||||
|
|
||||||
|
import { ErrorView, HomeView } from "./views" |
||||||
|
|
||||||
|
interface Props extends RouteProps { |
||||||
|
component: any // TODO: new (props: any) => React.Component
|
||||||
|
from: string |
||||||
|
} |
||||||
|
|
||||||
|
const CustomRoute = ({ component: Component, ...rest }: Props) => { |
||||||
|
return ( |
||||||
|
<Route |
||||||
|
{...rest} |
||||||
|
render={(matchProps) => ( |
||||||
|
<Component {...matchProps} /> |
||||||
|
)} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export const Routes = () => ( |
||||||
|
<Router> |
||||||
|
<Switch> |
||||||
|
<CustomRoute exact path="/" component={HomeView} from="/" /> |
||||||
|
<Route path="/error"> |
||||||
|
<ErrorView /> |
||||||
|
</Route> |
||||||
|
</Switch> |
||||||
|
</Router> |
||||||
|
) |
@ -0,0 +1,5 @@ |
|||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom/extend-expect'; |
@ -0,0 +1,11 @@ |
|||||||
|
export type Documentation = string |
||||||
|
|
||||||
|
export interface EthDocumentation { |
||||||
|
[contractName: string]: Documentation |
||||||
|
} |
||||||
|
|
||||||
|
export type ContractName = string |
||||||
|
|
||||||
|
export type FileName = string |
||||||
|
|
||||||
|
export type PublishedSite = string |
@ -0,0 +1,30 @@ |
|||||||
|
import { CompiledContract, ABIParameter } from '@remixproject/plugin' |
||||||
|
|
||||||
|
import sampleData from './sample-data/sample-artifact.json' |
||||||
|
import sampleDataWithComments from './sample-data/sample-artifact-with-comments.json' |
||||||
|
|
||||||
|
export const buildFakeArtifact: () => CompiledContract = () => { |
||||||
|
const result = sampleData as never as CompiledContract |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
export const buildFakeArtifactWithComments: () => CompiledContract = () => { |
||||||
|
const result = sampleDataWithComments as never as CompiledContract |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
export const buildFakeABIParameter: () => ABIParameter = () => { |
||||||
|
return { |
||||||
|
internalType: "address", |
||||||
|
name: "allocator", |
||||||
|
type: "address" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const buildFakeABIParameterWithDocumentation: () => ABIParameter = () => { |
||||||
|
return { |
||||||
|
internalType: "address", |
||||||
|
name: "allocator", |
||||||
|
type: "address" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
export * from './utils' |
||||||
|
export * from './publisher' |
@ -0,0 +1,40 @@ |
|||||||
|
import { publish } from './publisher'; |
||||||
|
|
||||||
|
const open = require('open') |
||||||
|
|
||||||
|
jest.setTimeout(10000) |
||||||
|
|
||||||
|
describe('Publisher tests', () => { |
||||||
|
|
||||||
|
test('it can publish', async () => { |
||||||
|
const result = await publish("hello 123") |
||||||
|
|
||||||
|
expect(result).toBeDefined() |
||||||
|
}) |
||||||
|
|
||||||
|
test('it can publish html', async () => { |
||||||
|
const result = await publish(` |
||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<meta |
||||||
|
name="description" |
||||||
|
content="Web site created using create-react-app" |
||||||
|
/> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div>Content custom</div> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
`)
|
||||||
|
|
||||||
|
// Uncomment for testing
|
||||||
|
|
||||||
|
// const url = `https://ipfs.io/ipfs/${result}`;
|
||||||
|
|
||||||
|
// await open(url, { app: ['google chrome', '--incognito'] });
|
||||||
|
|
||||||
|
expect(result).toBeDefined() |
||||||
|
}) |
||||||
|
}) |
@ -0,0 +1,13 @@ |
|||||||
|
import { HTMLContent } from "./types"; |
||||||
|
|
||||||
|
const IpfsClient = require('ipfs-mini') |
||||||
|
|
||||||
|
export const publish = async (content: HTMLContent) => { |
||||||
|
const ipfs = new IpfsClient({ host: 'ipfs.infura.io', port: 5001, protocol: 'https' }); |
||||||
|
|
||||||
|
const documentHash = await ipfs.add(content) |
||||||
|
|
||||||
|
console.log("Document hash", documentHash) |
||||||
|
|
||||||
|
return documentHash |
||||||
|
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,122 @@ |
|||||||
|
import { FunctionDocumentation, TemplateDoc, MethodDoc, ContractDoc, ContractDocumentation, ParameterDocumentation } from "./types" |
||||||
|
type HTMLContent = string |
||||||
|
|
||||||
|
export const htmlTemplate = (content: HTMLContent) => ` |
||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8" /> |
||||||
|
<meta |
||||||
|
name="description" |
||||||
|
content="Web site created with EthDoc" |
||||||
|
/> |
||||||
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
${content} |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
` |
||||||
|
|
||||||
|
export const template = (name: string, contractDoc: ContractDocumentation, functions: FunctionDocumentation[]) => ` |
||||||
|
<style> |
||||||
|
#ethdoc-viewer{ |
||||||
|
font-size: 0.8em; |
||||||
|
padding: 1em; |
||||||
|
} |
||||||
|
#ethdoc-viewer .lead{ |
||||||
|
font-size: 1em; |
||||||
|
} |
||||||
|
#ethdoc-viewer table { |
||||||
|
width: 50%; |
||||||
|
} |
||||||
|
#ethdoc-viewer hr { |
||||||
|
margin: 0; |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
} |
||||||
|
#ethdoc-viewer p{ |
||||||
|
margin-bottom: 0.5rem; |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
<div id="ethdoc-viewer"> |
||||||
|
|
||||||
|
${functions.length == 0 ? "No contract to display" : renderHeader(name, contractDoc)} |
||||||
|
|
||||||
|
${functions.map((item) => ` |
||||||
|
<h6>${item.name} - ${item.type}</h6> |
||||||
|
<hr> |
||||||
|
${renderParameterDocumentation(item.inputs)} |
||||||
|
|
||||||
|
${getMethodDetails(item.devdoc)} |
||||||
|
|
||||||
|
<p>Returns:</p> |
||||||
|
|
||||||
|
${renderParameterDocumentation(item.outputs)} |
||||||
|
|
||||||
|
`).join('\n')}
|
||||||
|
|
||||||
|
</div> |
||||||
|
` |
||||||
|
|
||||||
|
// const contractDocTemplate: TemplateDoc<ContractDoc> = {
|
||||||
|
// author: (author: string) => '',//`Author: ${author}`,
|
||||||
|
// details: (details: string) => `<p class="lead text-muted">${details}</p>`,
|
||||||
|
// title: (title: string) => {
|
||||||
|
// return title ?
|
||||||
|
// `<small>${title}</small>`
|
||||||
|
// : ''
|
||||||
|
// },
|
||||||
|
// notice: (notice: string) => `<p class="lead text-muted">${notice}</p>`,
|
||||||
|
// methods: () => '' // Methods is managed by getMethod()
|
||||||
|
// }
|
||||||
|
const devMethodDocTemplate: Partial<TemplateDoc<MethodDoc>> = { |
||||||
|
author: (author: string) => `<p>Created By ${author}</p>`, |
||||||
|
details: (details: string) => `<p>${details}</p>`, |
||||||
|
return: (value: string) => `<p>Return : ${value}</p>`, |
||||||
|
notice: (notice: string) => `<p>${notice}</p>`, |
||||||
|
// returns: () => '', // Implemented by getParams()
|
||||||
|
params: () => '' // Implemented by getParams()
|
||||||
|
} |
||||||
|
|
||||||
|
export const renderHeader = (name: string, contractDoc: ContractDocumentation) => ` |
||||||
|
<h3>${name} ${contractDoc.title ? `<small>: ${contractDoc.title}</small>` : ''}</h3> |
||||||
|
|
||||||
|
${contractDoc.notice ? `<p class="lead">${contractDoc.notice}</p>` : ''} |
||||||
|
|
||||||
|
${contractDoc.author ? `<p>Author: ${contractDoc.author}</p>` : ''} |
||||||
|
|
||||||
|
<p><strong>Functions</strong></p> |
||||||
|
` |
||||||
|
|
||||||
|
export const renderParameterDocumentation = (parameters: ParameterDocumentation[]) => ` |
||||||
|
${parameters.length > 0 ? ` |
||||||
|
<table class="table table-sm table-bordered table-striped"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th>Name</th> |
||||||
|
<th>Type</th> |
||||||
|
<th>Description</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
${parameters.map((output) => (`<tr>
|
||||||
|
<td>${output.name}</td> |
||||||
|
<td>${output.type}</td> |
||||||
|
<td>${output.description}</td> |
||||||
|
</tr>`))}
|
||||||
|
</tbody> |
||||||
|
</table>` :
|
||||||
|
'<p>No parameters</p>'} |
||||||
|
` |
||||||
|
|
||||||
|
export const getMethodDetails = (devMethod?: Partial<MethodDoc>) => { |
||||||
|
return !devMethod |
||||||
|
? '<p><strong>**Add Documentation for the method here**</strong></p>' |
||||||
|
: Object.keys(devMethod) |
||||||
|
.filter((key) => key !== 'params') |
||||||
|
.map((key) => { |
||||||
|
(devMethodDocTemplate as any)[key]((devMethod as any)[key]) |
||||||
|
}) |
||||||
|
.join('\n') |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
import { UserMethodDoc, DevMethodDoc, DeveloperDocumentation, UserDocumentation } from "@remixproject/plugin"; |
||||||
|
|
||||||
|
export interface MethodsDocumentation { |
||||||
|
[x: string]: UserMethodDoc | DevMethodDoc |
||||||
|
} |
||||||
|
|
||||||
|
export interface ContractDocumentation { |
||||||
|
methods: MethodsDocumentation; |
||||||
|
author: string; |
||||||
|
title: string; |
||||||
|
details: string; |
||||||
|
notice: string; |
||||||
|
} |
||||||
|
|
||||||
|
export type MethodDoc = DevMethodDoc & UserMethodDoc |
||||||
|
|
||||||
|
export type TemplateDoc<T> = { [key in keyof T]: (...params: any[]) => string } |
||||||
|
|
||||||
|
// Contract
|
||||||
|
export type ContractDoc = DeveloperDocumentation & UserDocumentation |
||||||
|
|
||||||
|
export interface FunctionDocumentation { |
||||||
|
name: string |
||||||
|
type: string |
||||||
|
devdoc?: Partial<MethodDoc> |
||||||
|
inputs: ParameterDocumentation[] |
||||||
|
outputs: ParameterDocumentation[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface ParameterDocumentation { |
||||||
|
name: string |
||||||
|
type: string |
||||||
|
description: string |
||||||
|
} |
||||||
|
|
||||||
|
export type HTMLContent = string |
@ -0,0 +1,93 @@ |
|||||||
|
const open = require('open') |
||||||
|
|
||||||
|
import { getContractDoc, mergeParametersWithDevdoc, getFunctionDocumentation, getContractDocumentation } from './utils'; |
||||||
|
import { FunctionDescription } from '@remixproject/plugin'; |
||||||
|
import { buildFakeArtifactWithComments, buildFakeABIParameter } from './faker' |
||||||
|
|
||||||
|
jest.setTimeout(10000) |
||||||
|
|
||||||
|
describe('Publisher tests', () => { |
||||||
|
|
||||||
|
describe('getContractDocumentation', () => { |
||||||
|
test('getContractDocumentation', () => { |
||||||
|
const result = getContractDocumentation(buildFakeArtifactWithComments()) |
||||||
|
|
||||||
|
const result2 = { |
||||||
|
methods: |
||||||
|
{ |
||||||
|
'age(uint256)': |
||||||
|
{ |
||||||
|
author: 'Mary A. Botanist', |
||||||
|
details: |
||||||
|
'The Alexandr N. Tetearing algorithm could increase precision', |
||||||
|
params: [Object], |
||||||
|
return: 'age in years, rounded up for partial years' |
||||||
|
} |
||||||
|
}, |
||||||
|
notice: |
||||||
|
'You can use this contract for only the most basic simulation', |
||||||
|
author: 'Larry A. Gardner', |
||||||
|
details: |
||||||
|
'All function calls are currently implemented without side effects', |
||||||
|
title: 'A simulator for trees' |
||||||
|
} |
||||||
|
|
||||||
|
expect(result).toBeDefined() |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('getContractDoc', () => { |
||||||
|
test('getContractDoc', () => { |
||||||
|
|
||||||
|
const template = getContractDoc("Fallout", buildFakeArtifactWithComments()); |
||||||
|
|
||||||
|
console.log("Template", template) |
||||||
|
|
||||||
|
expect(template).toBeDefined() |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('getFunctionDocumentation', () => { |
||||||
|
test('getFunctionDocumentation', () => { |
||||||
|
const abiItem: FunctionDescription = { |
||||||
|
constant: false, |
||||||
|
inputs: [], |
||||||
|
name: "Fal1out", |
||||||
|
outputs: [], |
||||||
|
payable: true, |
||||||
|
stateMutability: "payable", |
||||||
|
type: "function" |
||||||
|
} |
||||||
|
|
||||||
|
const result = getFunctionDocumentation(abiItem, {}) |
||||||
|
|
||||||
|
expect(result).toBeDefined() |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('mergeParametersWithDevdoc', () => { |
||||||
|
test('mergeParametersWithDevdoc', () => { |
||||||
|
const abiParameters = [buildFakeABIParameter()] |
||||||
|
const devParams = {} |
||||||
|
const result = mergeParametersWithDevdoc(abiParameters, devParams) |
||||||
|
|
||||||
|
expect(result.length).toEqual(1) |
||||||
|
}) |
||||||
|
|
||||||
|
test('mergeParametersWithDevdoc with documentation', () => { |
||||||
|
const abiParameters = [buildFakeABIParameter()] |
||||||
|
const devParams = {} |
||||||
|
const result = mergeParametersWithDevdoc(abiParameters, devParams) |
||||||
|
|
||||||
|
expect(result.length).toEqual(1) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
|
||||||
|
test.skip('html generation', async () => { |
||||||
|
await open('https://ipfs.io/ipfs/QmPYQyWyTrUZt3tjiPsEnkRQxedChYUjgEk9zLQ36SfpyW', { app: ['google chrome', '--incognito'] }); |
||||||
|
// start server
|
||||||
|
// generate html
|
||||||
|
// server it
|
||||||
|
}) |
||||||
|
}) |
@ -0,0 +1,94 @@ |
|||||||
|
import { CompilationResult, CompiledContract, FunctionDescription, ABIDescription, DevMethodDoc, UserMethodDoc, ABIParameter, DeveloperDocumentation, UserDocumentation } from "@remixproject/plugin" |
||||||
|
|
||||||
|
import { EthDocumentation, FileName, Documentation, ContractName } from '../types' |
||||||
|
import { template } from './template' |
||||||
|
import { ContractDocumentation, MethodDoc, FunctionDocumentation, ParameterDocumentation, MethodsDocumentation } from './types' |
||||||
|
|
||||||
|
export const createDocumentation = (fileName: FileName, compilationResult: CompilationResult) => { |
||||||
|
console.log("Filename", fileName) |
||||||
|
const result = new Map<ContractName, Documentation>(); |
||||||
|
|
||||||
|
const contracts = compilationResult.contracts[fileName] |
||||||
|
console.log("Contracts", contracts) |
||||||
|
|
||||||
|
Object.keys(contracts).forEach((name) => { |
||||||
|
console.log("CompiledContract", JSON.stringify(contracts[name])) |
||||||
|
result.set(name, getContractDoc(name, contracts[name])) |
||||||
|
}) |
||||||
|
|
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
export const getContractDoc = (name: string, contract: CompiledContract) => { |
||||||
|
const contractDoc: ContractDocumentation = getContractDocumentation(contract) |
||||||
|
|
||||||
|
const functionsDocumentation = contract.abi |
||||||
|
.map((def: ABIDescription) => { |
||||||
|
if (def.type === 'constructor') { |
||||||
|
def.name = 'constructor' |
||||||
|
// because "constructor" is a string and not a { notice } object for userdoc we need to do that
|
||||||
|
const methodDoc = { |
||||||
|
...(contract.devdoc.methods.constructor || {}), |
||||||
|
notice: contract.userdoc.methods.constructor as string |
||||||
|
} |
||||||
|
return getFunctionDocumentation(def, methodDoc) |
||||||
|
} else { |
||||||
|
if (def.type === 'fallback') { |
||||||
|
def.name = 'fallback' |
||||||
|
} |
||||||
|
const method = Object.keys(contractDoc.methods).find((key) => key.includes(def.name as string)) as string |
||||||
|
const methodDoc = contractDoc.methods[method] |
||||||
|
return getFunctionDocumentation(def as FunctionDescription, methodDoc) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return template(name, contractDoc, functionsDocumentation) |
||||||
|
} |
||||||
|
|
||||||
|
export const getContractDocumentation = (contract: CompiledContract) => { |
||||||
|
let methods: MethodsDocumentation = {}; |
||||||
|
|
||||||
|
Object.keys(contract.userdoc.methods).map((item) => { |
||||||
|
if (contract.devdoc.methods[item]) { |
||||||
|
const finalResult = { |
||||||
|
...contract.userdoc.methods[item], |
||||||
|
...contract.devdoc.methods[item] |
||||||
|
} |
||||||
|
methods[item] = finalResult |
||||||
|
} else { |
||||||
|
methods[item] = contract.userdoc.methods[item] |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const contractDoc = { ...contract.userdoc, ...contract.devdoc, methods } |
||||||
|
|
||||||
|
return contractDoc |
||||||
|
} |
||||||
|
|
||||||
|
export const getFunctionDocumentation = (def: FunctionDescription, devdoc?: Partial<MethodDoc>) => { |
||||||
|
const doc = devdoc || {} |
||||||
|
const devparams = doc.params || {} |
||||||
|
const inputsWithDescription = mergeParametersWithDevdoc(def.inputs || [], devparams) |
||||||
|
const outputsWithDescription = mergeParametersWithDevdoc(def.outputs || [], devparams) |
||||||
|
const type = def.constant ? 'view' : 'read' |
||||||
|
|
||||||
|
return { |
||||||
|
name: def.name, |
||||||
|
type, |
||||||
|
devdoc: devdoc, |
||||||
|
inputs: inputsWithDescription, |
||||||
|
outputs: outputsWithDescription |
||||||
|
} as FunctionDocumentation |
||||||
|
} |
||||||
|
|
||||||
|
export const mergeParametersWithDevdoc = (params: ABIParameter[], devparams: any) => { |
||||||
|
return params.map((input) => { |
||||||
|
const description = devparams[input.name] || '' |
||||||
|
return { |
||||||
|
name: input.name, |
||||||
|
type: input.type, |
||||||
|
description |
||||||
|
} as ParameterDocumentation |
||||||
|
}) |
||||||
|
} |
||||||
|
|
@ -0,0 +1,31 @@ |
|||||||
|
import React from "react" |
||||||
|
|
||||||
|
export const ErrorView: React.FC = () => { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
style={{ |
||||||
|
width: "100%", |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
alignItems: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
<img |
||||||
|
style={{ paddingBottom: "2em" }} |
||||||
|
width="250" |
||||||
|
src="https://res.cloudinary.com/key-solutions/image/upload/v1580400635/solid/error-png.png" |
||||||
|
alt="Error page" |
||||||
|
></img> |
||||||
|
<h5>Sorry, something unexpected happened. </h5> |
||||||
|
<h5> |
||||||
|
Please raise an issue:{" "} |
||||||
|
<a |
||||||
|
style={{ color: "red" }} |
||||||
|
href="" |
||||||
|
> |
||||||
|
Here |
||||||
|
</a> |
||||||
|
</h5> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,128 @@ |
|||||||
|
import React, { useState, useEffect } from "react" |
||||||
|
import { AppContext } from "../AppContext" |
||||||
|
import { ContractName, Documentation } from "../types" |
||||||
|
import { publish } from '../utils' |
||||||
|
import { htmlTemplate } from "../utils/template" |
||||||
|
|
||||||
|
export const HomeView: React.FC = () => { |
||||||
|
const [activeItem, setActiveItem] = useState("") |
||||||
|
const [isPublishing, setIsPublishing] = useState(false) |
||||||
|
const [htmlDocumentation, setHtmlDocumentation] = useState("") |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
async function publishDocumentation() { |
||||||
|
try { |
||||||
|
|
||||||
|
const hash = await publish(htmlDocumentation) |
||||||
|
console.log("Hash", hash) |
||||||
|
setIsPublishing(false) |
||||||
|
|
||||||
|
const url = `https://ipfs.io/ipfs/${hash}`; |
||||||
|
|
||||||
|
window.open(url); |
||||||
|
|
||||||
|
} catch (error) { |
||||||
|
setIsPublishing(false) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (isPublishing) { |
||||||
|
publishDocumentation() |
||||||
|
} |
||||||
|
|
||||||
|
}, [isPublishing]) |
||||||
|
|
||||||
|
|
||||||
|
const displayDocumentation = (client: any, contractName: ContractName, documentation: Documentation) => { |
||||||
|
|
||||||
|
console.log("Display Documentation", contractName, documentation) |
||||||
|
|
||||||
|
client.emit('documentation-generated', documentation) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<AppContext.Consumer> |
||||||
|
{({ clientInstance, contracts, setContracts }) => |
||||||
|
<div id="ethdoc"> |
||||||
|
{[...contracts.keys()].length === 0 && |
||||||
|
<p>Compile a contract with Solidity Compiler</p> |
||||||
|
} |
||||||
|
|
||||||
|
{[...contracts.keys()].length > 0 && |
||||||
|
<div> |
||||||
|
<div className="list-group"> |
||||||
|
{[...contracts.keys()].map((item) => { |
||||||
|
const documentation = contracts.get(item) as string; |
||||||
|
return ( |
||||||
|
<button key={item} |
||||||
|
className={activeItem === item ? 'list-group-item list-group-item-action active' : 'list-group-item list-group-item-action'} |
||||||
|
aria-pressed="false" |
||||||
|
onClick={() => { |
||||||
|
setActiveItem(item) |
||||||
|
displayDocumentation(clientInstance, item, documentation) |
||||||
|
const documentationAsHtml = htmlTemplate(documentation) |
||||||
|
setHtmlDocumentation(documentationAsHtml) |
||||||
|
}}> |
||||||
|
{item} Documentation |
||||||
|
</button> |
||||||
|
) |
||||||
|
})} |
||||||
|
</div> |
||||||
|
<div style={{ float: "right" }}> |
||||||
|
<button type="button" className="btn btn-sm btn-link" onClick={() => { |
||||||
|
setContracts(new Map()) |
||||||
|
displayDocumentation(clientInstance, "", "") |
||||||
|
|
||||||
|
}}>Clear</button> |
||||||
|
</div> |
||||||
|
<div style={{ width: "15em" }}> |
||||||
|
{activeItem !== "" && |
||||||
|
<PublishButton isPublishing={isPublishing} onClick={() => { |
||||||
|
console.log("Is publishing") |
||||||
|
setIsPublishing(true); |
||||||
|
}} />} |
||||||
|
</div> |
||||||
|
</div>} |
||||||
|
</div> |
||||||
|
} |
||||||
|
</AppContext.Consumer> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
interface PublishButtonProps { |
||||||
|
isPublishing: boolean |
||||||
|
onClick: any |
||||||
|
} |
||||||
|
|
||||||
|
export const PublishButton: React.FC<PublishButtonProps> = ({ isPublishing, onClick }) => { |
||||||
|
return (<button |
||||||
|
style={{ marginTop: "1em" }} |
||||||
|
className="btn btn-secondary btn-sm btn-block" |
||||||
|
disabled={isPublishing} |
||||||
|
onClick={onClick}> |
||||||
|
{!isPublishing && "Publish"} |
||||||
|
|
||||||
|
{isPublishing && ( |
||||||
|
<div> |
||||||
|
<span |
||||||
|
className="spinner-border spinner-border-sm" |
||||||
|
role="status" |
||||||
|
aria-hidden="true" |
||||||
|
style={{ marginRight: "0.3em" }} |
||||||
|
/> |
||||||
|
Publishing...Please wait |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
</button>) |
||||||
|
} |
||||||
|
|
||||||
|
// <label class="btn btn-secondary active">
|
||||||
|
// <input type="radio" name="options" id="option1" checked> Active
|
||||||
|
// </label>
|
||||||
|
// <label class="btn btn-secondary">
|
||||||
|
// <input type="radio" name="options" id="option2"> Radio
|
||||||
|
// </label>
|
||||||
|
// <label class="btn btn-secondary">
|
||||||
|
// <input type="radio" name="options" id="option3"> Radio
|
||||||
|
// </label>
|
@ -0,0 +1,2 @@ |
|||||||
|
export { HomeView } from "./HomeView" |
||||||
|
export { ErrorView } from "./ErrorView" |
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"extends": [ |
||||||
|
"tslint:recommended", |
||||||
|
"tslint-react", |
||||||
|
"tslint-config-prettier" |
||||||
|
], |
||||||
|
"rulesDirectory": [ |
||||||
|
"tslint-plugin-prettier" |
||||||
|
], |
||||||
|
"rules": { |
||||||
|
"prettier": true, |
||||||
|
"interface-name": false, |
||||||
|
"no-console": false, |
||||||
|
"jsx-no-lambda": false |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue