change app folder name

pull/3542/head
Joseph Izang 2 years ago
parent 914292d930
commit 393a6e7ea9
  1. 1
      apps/ethdoc/src/react-app-env.d.ts
  2. 5
      apps/ethdoc/src/setupTests.ts
  3. 0
      apps/remixdocgen/.coveralls.yml
  4. 0
      apps/remixdocgen/.eslintcache
  5. 0
      apps/remixdocgen/docs/img/ethdoc.png
  6. 0
      apps/remixdocgen/docs/index.md
  7. 0
      apps/remixdocgen/mkdocs.yml
  8. 0
      apps/remixdocgen/public/favicon.ico
  9. 0
      apps/remixdocgen/public/index.html
  10. 0
      apps/remixdocgen/public/logo192.png
  11. 0
      apps/remixdocgen/public/logo512.png
  12. 0
      apps/remixdocgen/public/manifest.json
  13. 0
      apps/remixdocgen/public/robots.txt
  14. 0
      apps/remixdocgen/src/app/App.css
  15. 4
      apps/remixdocgen/src/app/App.tsx
  16. 2
      apps/remixdocgen/src/app/AppContext.tsx
  17. 22
      apps/remixdocgen/src/app/docgen/common/helpers.ts
  18. 138
      apps/remixdocgen/src/app/docgen/common/properties.ts
  19. 84
      apps/remixdocgen/src/app/docgen/config.ts
  20. 27
      apps/remixdocgen/src/app/docgen/doc-item.ts
  21. 87
      apps/remixdocgen/src/app/docgen/render.ts
  22. 138
      apps/remixdocgen/src/app/docgen/site.ts
  23. 95
      apps/remixdocgen/src/app/docgen/templates.ts
  24. 34
      apps/remixdocgen/src/app/docgen/themes/markdown/common.hbs
  25. 8
      apps/remixdocgen/src/app/docgen/themes/markdown/contract.hbs
  26. 9
      apps/remixdocgen/src/app/docgen/themes/markdown/enum.hbs
  27. 1
      apps/remixdocgen/src/app/docgen/themes/markdown/error.hbs
  28. 1
      apps/remixdocgen/src/app/docgen/themes/markdown/event.hbs
  29. 1
      apps/remixdocgen/src/app/docgen/themes/markdown/function.hbs
  30. 49
      apps/remixdocgen/src/app/docgen/themes/markdown/helpers.ts
  31. 1
      apps/remixdocgen/src/app/docgen/themes/markdown/modifier.hbs
  32. 8
      apps/remixdocgen/src/app/docgen/themes/markdown/page.hbs
  33. 9
      apps/remixdocgen/src/app/docgen/themes/markdown/struct.hbs
  34. 1
      apps/remixdocgen/src/app/docgen/themes/markdown/user-defined-value-type.hbs
  35. 1
      apps/remixdocgen/src/app/docgen/themes/markdown/variable.hbs
  36. 13
      apps/remixdocgen/src/app/docgen/utils/ItemError.ts
  37. 5
      apps/remixdocgen/src/app/docgen/utils/arrays-equal.ts
  38. 1
      apps/remixdocgen/src/app/docgen/utils/assert-equal-types.ts
  39. 6
      apps/remixdocgen/src/app/docgen/utils/clone.ts
  40. 12
      apps/remixdocgen/src/app/docgen/utils/ensure-array.ts
  41. 18
      apps/remixdocgen/src/app/docgen/utils/execall.ts
  42. 5
      apps/remixdocgen/src/app/docgen/utils/is-child.ts
  43. 7
      apps/remixdocgen/src/app/docgen/utils/item-type.ts
  44. 4
      apps/remixdocgen/src/app/docgen/utils/map-keys.ts
  45. 19
      apps/remixdocgen/src/app/docgen/utils/map-values.ts
  46. 23
      apps/remixdocgen/src/app/docgen/utils/memoized-getter.ts
  47. 145
      apps/remixdocgen/src/app/docgen/utils/natspec.ts
  48. 26
      apps/remixdocgen/src/app/docgen/utils/read-item-docs.ts
  49. 63
      apps/remixdocgen/src/app/docgen/utils/scope.ts
  50. 0
      apps/remixdocgen/src/app/hooks/useLocalStorage.tsx
  51. 0
      apps/remixdocgen/src/app/routes.tsx
  52. 0
      apps/remixdocgen/src/app/views/ErrorView.tsx
  53. 6
      apps/remixdocgen/src/app/views/HomeView.tsx
  54. 0
      apps/remixdocgen/src/app/views/index.ts
  55. 2
      apps/remixdocgen/src/index.tsx
  56. 0
      apps/remixdocgen/src/types.ts
  57. 0
      apps/remixdocgen/src/utils/faker.ts
  58. 0
      apps/remixdocgen/src/utils/index.ts
  59. 0
      apps/remixdocgen/src/utils/publisher.test.ts
  60. 0
      apps/remixdocgen/src/utils/publisher.ts
  61. 0
      apps/remixdocgen/src/utils/sample-data/file.json
  62. 0
      apps/remixdocgen/src/utils/sample-data/sample-artifact-with-comments.json
  63. 0
      apps/remixdocgen/src/utils/sample-data/sample-artifact.json
  64. 0
      apps/remixdocgen/src/utils/template.ts
  65. 0
      apps/remixdocgen/src/utils/types.ts
  66. 0
      apps/remixdocgen/src/utils/utils.test.ts
  67. 0
      apps/remixdocgen/src/utils/utils.ts
  68. 0
      apps/remixdocgen/tslint.json

@ -1 +0,0 @@
/// <reference types="react-scripts" />

@ -1,5 +0,0 @@
// 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';

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -11,10 +11,10 @@ import { Status } from "@remixproject/plugin-utils";
import { AppContext } from "./AppContext"; import { AppContext } from "./AppContext";
import { Routes } from "./routes"; import { Routes } from "./routes";
import { useLocalStorage } from "./hooks/useLocalStorage"; import { useLocalStorage } from "./hooks/useLocalStorage";
import { createDocumentation } from "./utils/utils"; import { createDocumentation } from "../utils/utils";
import "./App.css"; import "./App.css";
import { ContractName, Documentation } from "./types"; import { ContractName, Documentation } from "../types";
export const getNewContractNames = (compilationResult: CompilationResult) => { export const getNewContractNames = (compilationResult: CompilationResult) => {
const compiledContracts = compilationResult.contracts; const compiledContracts = compilationResult.contracts;

@ -3,7 +3,7 @@ import { PluginClient } from "@remixproject/plugin";
import { PluginApi, Api } from "@remixproject/plugin-utils"; import { PluginApi, Api } from "@remixproject/plugin-utils";
import { IRemixApi } from "@remixproject/plugin-api"; import { IRemixApi } from "@remixproject/plugin-api";
import { ContractName, Documentation, PublishedSite } from "./types"; import { ContractName, Documentation, PublishedSite } from "../types";
export const AppContext = React.createContext({ export const AppContext = React.createContext({
clientInstance: {} as PluginApi<Readonly<IRemixApi>> & clientInstance: {} as PluginApi<Readonly<IRemixApi>> &

@ -0,0 +1,22 @@
import { VariableDeclaration } from "solidity-ast";
export function trim(text: string) {
if (typeof text === 'string') {
return text.trim();
}
}
export function joinLines(text?: string) {
if (typeof text === 'string') {
return text.replace(/\n+/g, ' ');
}
}
/**
* Format a variable as its type followed by its name, if available.
*/
export function formatVariable(v: VariableDeclaration): string {
return [v.typeName?.typeDescriptions.typeString].concat(v.name || []).join(' ');
}
export const eq = (a: unknown, b: unknown) => a === b;

@ -0,0 +1,138 @@
import { EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, ModifierDefinition, ParameterList, StructDefinition, UserDefinedValueTypeDefinition, VariableDeclaration } from 'solidity-ast';
import { findAll, isNodeType } from 'solidity-ast/utils';
import { NatSpec, parseNatspec } from '../utils/natspec';
import { DocItemContext, DOC_ITEM_CONTEXT } from '../site';
import { mapValues } from '../utils/map-values';
import { DocItem, docItemTypes } from '../doc-item';
import { formatVariable } from './helpers';
import { PropertyGetter } from '../templates';
import { itemType } from '../utils/item-type';
type TypeDefinition = StructDefinition | EnumDefinition | UserDefinedValueTypeDefinition;
export function type({ item }: DocItemContext): string {
return itemType(item);
}
export function natspec({ item }: DocItemContext): NatSpec {
return parseNatspec(item);
}
export function name({ item }: DocItemContext, original?: unknown): string {
if (item.nodeType === 'FunctionDefinition') {
return item.kind === 'function' ? original as string : item.kind;
} else {
return original as string;
}
}
export function fullName({ item, contract }: DocItemContext): string {
if (contract) {
return `${contract.name}.${item.name}`;
} else {
return `${item.name}`;
}
}
export function signature({ item }: DocItemContext): string | undefined {
switch (item.nodeType) {
case 'ContractDefinition':
return undefined;
case 'FunctionDefinition': {
const { kind, name } = item;
const params = item.parameters.parameters;
const returns = item.returnParameters.parameters;
const head = (kind === 'function' || kind === 'freeFunction') ? [kind, name].join(' ') : kind;
const res = [
`${head}(${params.map(formatVariable).join(', ')})`,
item.visibility,
];
if (item.stateMutability !== 'nonpayable') {
res.push(item.stateMutability);
}
if (item.virtual) {
res.push('virtual');
}
if (returns.length > 0) {
res.push(`returns (${returns.map(formatVariable).join(', ')})`);
}
return res.join(' ');
}
case 'EventDefinition': {
const params = item.parameters.parameters;
return `event ${item.name}(${params.map(formatVariable).join(', ')})`;
}
case 'ErrorDefinition': {
const params = item.parameters.parameters;
return `error ${item.name}(${params.map(formatVariable).join(', ')})`;
}
case 'ModifierDefinition': {
const params = item.parameters.parameters;
return `modifier ${item.name}(${params.map(formatVariable).join(', ')})`;
}
case 'VariableDeclaration':
return formatVariable(item);
}
}
interface Param extends VariableDeclaration {
type: string;
natspec?: string;
};
function getParams(params: ParameterList, natspec: NatSpec['params'] | NatSpec['returns']): Param[] {
return params.parameters.map((p, i) => ({
...p,
type: p.typeDescriptions.typeString!,
natspec: natspec?.find((q, j) => q.name === undefined ? i === j : p.name === q.name)?.description,
}));
}
export function params({ item }: DocItemContext): Param[] | undefined {
if ('parameters' in item) {
return getParams(item.parameters, natspec(item[DOC_ITEM_CONTEXT]).params);
}
}
export function returns({ item }: DocItemContext): Param[] | undefined {
if ('returnParameters' in item) {
return getParams(item.returnParameters, natspec(item[DOC_ITEM_CONTEXT]).returns);
}
}
export function items({ item }: DocItemContext): DocItem[] | undefined {
return (item.nodeType === 'ContractDefinition')
? item.nodes.filter(isNodeType(docItemTypes)).filter(n => !('visibility' in n) || n.visibility !== 'private')
: undefined;
}
export function functions({ item }: DocItemContext): FunctionDefinition[] | undefined {
return [...findAll('FunctionDefinition', item)].filter(f => f.visibility !== 'private');
}
export function events({ item }: DocItemContext): EventDefinition[] | undefined {
return [...findAll('EventDefinition', item)];
}
export function modifiers({ item }: DocItemContext): ModifierDefinition[] | undefined {
return [...findAll('ModifierDefinition', item)];
}
export function errors({ item }: DocItemContext): ErrorDefinition[] | undefined {
return [...findAll('ErrorDefinition', item)];
}
export function variables({ item }: DocItemContext): VariableDeclaration[] | undefined {
return (item.nodeType === 'ContractDefinition')
? item.nodes.filter(isNodeType('VariableDeclaration')).filter(v => v.stateVariable && v.visibility !== 'private')
: undefined;
}
export function types({ item }: DocItemContext): TypeDefinition[] | undefined {
return [...findAll(['StructDefinition', 'EnumDefinition', 'UserDefinedValueTypeDefinition'], item)];
}

@ -0,0 +1,84 @@
import type { SourceUnit } from 'solidity-ast';
import type { DocItem } from './doc-item';
import type { PageAssigner, PageStructure } from './site';
export interface UserConfig {
/**
* The directory where rendered pages will be written.
* Defaults to 'docs'.
*/
outputDir?: string;
/**
* A directory of custom templates that should take precedence over the
* theme's templates.
*/
templates?: string;
/**
* The name of the built-in templates that will be used by default.
* Defaults to 'markdown'.
*/
theme?: string;
/**
* The way documentable items (contracts, functions, custom errors, etc.)
* will be organized in pages. Built in options are:
* - 'single': all items in one page
* - 'items': one page per item
* - 'files': one page per input Solidity file
* More customization is possible by defining a function that returns a page
* path given the AST node for the item and the source unit where it is
* defined.
* Defaults to 'single'.
*/
pages?: 'single' | 'items' | 'files' | PageAssigner;
/**
* An array of sources subdirectories that should be excluded from
* documentation, relative to the contract sources directory.
*/
exclude?: string[];
/**
* Clean up the output by collapsing 3 or more contiguous newlines into only 2.
* Enabled by default.
*/
collapseNewlines?: boolean;
/**
* The extension for generated pages.
* Defaults to '.md'.
*/
pageExtension?: string;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// Other config parameters that will be provided by the environment (e.g. Hardhat)
// rather than by the user manually, unless using the library directly.
export interface Config extends UserConfig {
/**
* The root directory relative to which 'outputDir', 'sourcesDir', and
* 'templates' are specified. Defaults to the working directory.
*/
root?: string;
/**
* The Solidity sources directory.
*/
sourcesDir?: string;
}
export type FullConfig = Required<Config>;
export const defaults: Omit<FullConfig, 'templates'> = {
root: process.cwd(),
sourcesDir: 'contracts',
outputDir: 'docs',
pages: 'single',
exclude: [],
theme: 'markdown',
collapseNewlines: true,
pageExtension: '.md',
};

@ -0,0 +1,27 @@
import { ContractDefinition, ImportDirective, PragmaDirective, SourceUnit, UsingForDirective } from "solidity-ast";
import { Node, NodeType, NodeTypeMap } from "solidity-ast/node";
import { AssertEqual } from "./utils/assert-equal-types";
export type DocItem = Exclude<
SourceUnit['nodes'][number] | ContractDefinition['nodes'][number],
ImportDirective | PragmaDirective | UsingForDirective
>;
export const docItemTypes = [
'ContractDefinition',
'EnumDefinition',
'ErrorDefinition',
'EventDefinition',
'FunctionDefinition',
'ModifierDefinition',
'StructDefinition',
'UserDefinedValueTypeDefinition',
'VariableDeclaration',
] as const;
// Make sure at compile time that docItemTypes contains exactly the node types of DocItem.
const _: AssertEqual<typeof docItemTypes[number], DocItem['nodeType']> = true;
export function isDocItem(node: Node): node is DocItem {
return (docItemTypes as readonly string[]).includes(node.nodeType);
}

@ -0,0 +1,87 @@
import Handlebars, { RuntimeOptions } from 'handlebars';
import { Site, Page, DocItemWithContext, DOC_ITEM_CONTEXT } from './site';
import { Templates } from './templates';
import { itemType } from './utils/item-type';
import fs from 'fs';
export interface RenderedPage {
id: string;
contents: string;
}
interface TemplateOptions {
data: {
site: Site;
};
}
export function render(site: Site, templates: Templates, collapseNewlines?: boolean): RenderedPage[] {
const renderPage = buildRenderer(templates);
const renderedPages: RenderedPage[] = [];
for (const page of site.pages) {
let contents = renderPage(page, { data: { site } });
if (collapseNewlines) {
contents = contents.replace(/\n{3,}/g, '\n\n');
}
renderedPages.push({
id: page.id,
contents,
});
}
return renderedPages;
}
export const itemPartialName = (item: DocItemWithContext) => itemType(item).replace(/ /g, '-').toLowerCase();
function itemPartial(item: DocItemWithContext, options?: RuntimeOptions) {
if (!item.__item_context) {
throw new Error(`Partial 'item' used in unsupported context (not a doc item)`);
}
const partial = options?.partials?.[itemPartialName(item)];
if (!partial) {
throw new Error(`Missing partial '${itemPartialName(item)}'`);
}
return partial(item, options);
}
function readmeHelper(H: typeof Handlebars, path: string, opts: RuntimeOptions) {
const items: DocItemWithContext[] = opts.data.root.items;
const renderedItems = Object.fromEntries(
items.map(item => [
item.name,
new H.SafeString(
H.compile('{{>item}}')(item, opts),
),
]),
);
return new H.SafeString(
H.compile(fs.readFileSync(path, 'utf8'))(renderedItems, opts),
);
}
function buildRenderer(templates: Templates): (page: Page, options: TemplateOptions) => string {
const pageTemplate = templates.partials?.page;
if (pageTemplate === undefined) {
throw new Error(`Missing 'page' template`);
}
const H = Handlebars.create();
for (const [name, getBody] of Object.entries(templates.partials ?? {})) {
let partial: HandlebarsTemplateDelegate | undefined;
H.registerPartial(name, (...args) => {
partial ??= H.compile(getBody());
return partial(...args);
});
}
H.registerHelper('readme', (path: string, opts: RuntimeOptions) => readmeHelper(H, path, opts));
for (const [name, fn] of Object.entries(templates.helpers ?? {})) {
H.registerHelper(name, fn);
}
H.registerPartial('item', itemPartial);
return H.compile('{{>page}}');
}

@ -0,0 +1,138 @@
import path from 'path';
import { ContractDefinition, SourceUnit } from 'solidity-ast';
import { SolcOutput, SolcInput } from 'solidity-ast/solc';
import { astDereferencer, ASTDereferencer, findAll, isNodeType, srcDecoder, SrcDecoder } from 'solidity-ast/utils';
import { FullConfig } from './config';
import { DocItem, docItemTypes, isDocItem } from './doc-item';
import { Properties } from './templates';
import { clone } from './utils/clone';
import { isChild } from './utils/is-child';
import { mapValues } from './utils/map-values';
import { defineGetterMemoized } from './utils/memoized-getter';
export interface Build {
input: SolcInput;
output: SolcOutput;
}
export interface BuildContext extends Build {
deref: ASTDereferencer;
decodeSrc: SrcDecoder;
}
export type SiteConfig = Pick<FullConfig, 'pages' | 'exclude' | 'sourcesDir' | 'pageExtension'>;
export type PageStructure = SiteConfig['pages'];
export type PageAssigner = ((item: DocItem, file: SourceUnit, config: SiteConfig) => string | undefined);
export const pageAssigner: Record<PageStructure & string, PageAssigner> = {
single: (_1, _2, { pageExtension: ext }) => 'index' + ext,
items: (item, _, { pageExtension: ext }) => item.name + ext,
files: (_, file, { pageExtension: ext, sourcesDir }) =>
path.relative(sourcesDir, file.absolutePath).replace('.sol', ext),
};
export interface Site {
items: DocItemWithContext[];
pages: Page[];
}
export interface Page {
id: string;
items: DocItemWithContext[];
}
export const DOC_ITEM_CONTEXT = '__item_context' as const;
export type DocItemWithContext = DocItem & { [DOC_ITEM_CONTEXT]: DocItemContext };
export interface DocItemContext {
page?: string;
item: DocItemWithContext;
contract?: ContractDefinition;
file: SourceUnit;
build: BuildContext;
}
export function buildSite(builds: Build[], siteConfig: SiteConfig, properties: Properties = {}): Site {
const assign = typeof siteConfig.pages === 'string' ? pageAssigner[siteConfig.pages] : siteConfig.pages;
const seen = new Set<string>();
const items: DocItemWithContext[] = [];
const pages: Record<string, DocItemWithContext[]> = {};
// eslint-disable-next-line prefer-const
for (let { input, output } of builds) {
// Clone because we will mutate in order to add item context.
output = { ...output, sources: clone(output.sources) };
const deref = astDereferencer(output);
const decodeSrc = srcDecoder(input, output);
const build = { input, output, deref, decodeSrc };
for (const { ast: file } of Object.values(output.sources)) {
const isNewFile = !seen.has(file.absolutePath);
seen.add(file.absolutePath);
for (const topLevelItem of file.nodes) {
if (!isDocItem(topLevelItem)) continue;
const page = assignIfIncludedSource(assign, topLevelItem, file, siteConfig);
const withContext = defineContext(topLevelItem, build, file, page);
defineProperties(withContext, properties);
if (isNewFile && page !== undefined) {
(pages[page] ??= []).push(withContext);
items.push(withContext);
}
if (!isNodeType('ContractDefinition', topLevelItem)) {
continue;
}
for (const item of topLevelItem.nodes) {
if (!isDocItem(item)) continue;
if (isNewFile && page !== undefined) items.push(item as DocItemWithContext);
const contract = topLevelItem.nodeType === 'ContractDefinition' ? topLevelItem : undefined;
const withContext = defineContext(item, build, file, page, contract);
defineProperties(withContext, properties);
}
}
}
}
return {
items,
pages: Object.entries(pages).map(([id, pageItems]) => ({ id, items: pageItems })),
};
}
function defineContext(item: DocItem, build: BuildContext, file: SourceUnit, page?: string, contract?: ContractDefinition): DocItemWithContext {
return Object.assign(item, {
[DOC_ITEM_CONTEXT]: { build, file, contract, page, item: item as DocItemWithContext },
});
}
function defineProperties(item: DocItemWithContext, properties: Properties) {
for (const [prop, fn] of Object.entries(properties)) {
const original: unknown = (item as any)[prop];
defineGetterMemoized(item as any, prop, () => fn(item.__item_context, original));
}
}
function assignIfIncludedSource(
assign: PageAssigner,
item: DocItem,
file: SourceUnit,
config: SiteConfig,
) {
return isFileIncluded(file.absolutePath, config)
? assign(item, file, config)
: undefined;
}
function isFileIncluded(file: string, config: SiteConfig) {
return (
isChild(file, config.sourcesDir) &&
config.exclude.every(e => !isChild(file, path.join(config.sourcesDir, e)))
);
}

@ -0,0 +1,95 @@
import { mapKeys } from './utils/map-keys';
import { DocItemContext } from './site';
import * as defaultProperties from './common/properties';
export type PropertyGetter = (ctx: DocItemContext, original?: unknown) => unknown;
export type Properties = Record<string, PropertyGetter>;
export interface Templates {
partials?: Record<string, () => string>;
helpers?: Record<string, (...args: unknown[]) => string>;
properties?: Record<string, PropertyGetter>;
}
/**
* Loads the templates that will be used for rendering a site based on a
* default theme and user templates.
*
* The result contains all partials, helpers, and property getters defined in
* the user templates and the default theme, where the user's take precedence
* if there is a clash. Additionally, all theme partials and helpers are
* included with the theme prefix, e.g. `markdown/contract` will be a partial.
*/
export async function loadTemplates(defaultTheme: string, root: string, userTemplatesPath?: string): Promise<Templates> {
const themes = await readThemes();
// Initialize templates with the default theme.
const templates: Required<Templates> = {
partials: { ...themes[defaultTheme]?.partials },
helpers: { ...themes[defaultTheme]?.helpers },
properties: { ...defaultProperties },
};
// Add partials and helpers from all themes, prefixed with the theme name.
for (const [themeName, theme] of Object.entries(themes)) {
const addPrefix = (k: string) => `${themeName}/${k}`;
Object.assign(templates.partials, mapKeys(theme.partials, addPrefix));
Object.assign(templates.helpers, mapKeys(theme.helpers, addPrefix));
}
return templates;
}
/**
* Read templates and helpers from a directory.
*/
export async function readTemplates(): Promise<Required<Templates>> {
return {
partials: await readPartials(),
helpers: await readHelpers('helpers'),
properties: await readHelpers('properties'),
};
}
async function readPartials() {
const partials: NonNullable<Templates['partials']> = {};
const partialNames = ["common", "contract", "enum", "error", "event", "function", "modifier", "page", "struct", "variable", "user-defined-value-type"]
for (const name of partialNames) {
const p = await import('raw-loader!./themes/markdown/' + name + '.hbs')
partials[name] = () => p.default
}
return partials;
}
async function readHelpers(name: string) {
let helpersPath;
const h = await import('./themes/markdown/helpers');
const helpers: Record<string, (...args: any[]) => any> = {};
for (const name in h) {
if (typeof h[name] === 'function') {
helpers[name] = h[name];
}
}
return helpers;
}
/**
* Reads all built-in themes into an object. Partials will always be found in
* src/themes, whereas helpers may instead be found in dist/themes if TypeScript
* can't be imported directly.
*/
async function readThemes(): Promise<Record<string, Required<Templates>>> {
const themes: Record<string, Required<Templates>> = {};
themes['markdown'] = await readTemplates();
return themes;
}

@ -0,0 +1,34 @@
{{h}} {{name}}
{{#if signature}}
```solidity
{{{signature}}}
```
{{/if}}
{{{natspec.notice}}}
{{#if natspec.dev}}
_{{{natspec.dev}}}_
{{/if}}
{{#if natspec.params}}
{{h 2}} Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
{{#each params}}
| {{name}} | {{type}} | {{{joinLines natspec}}} |
{{/each}}
{{/if}}
{{#if natspec.returns}}
{{h 2}} Return Values
| Name | Type | Description |
| ---- | ---- | ----------- |
{{#each returns}}
| {{#if name}}{{name}}{{else}}[{{@index}}]{{/if}} | {{type}} | {{{joinLines natspec}}} |
{{/each}}
{{/if}}

@ -0,0 +1,8 @@
{{>common}}
{{#each items}}
{{#hsection}}
{{>item}}
{{/hsection}}
{{/each}}

@ -0,0 +1,9 @@
{{>common}}
```solidity
enum {{name}} {
{{#each members}}
{{name}}{{#unless @last}},{{/unless}}
{{/each}}
}
```

@ -0,0 +1,49 @@
import { HelperOptions, Utils } from 'handlebars';
export * from '../../common/helpers';
/**
* Returns a Markdown heading marker. An optional number increases the heading level.
*
* Input Output
* {{h}} {{name}} # Name
* {{h 2}} {{name}} ## Name
*/
export function h(opts: HelperOptions): string;
export function h(hsublevel: number, opts: HelperOptions): string;
export function h(hsublevel: number | HelperOptions, opts?: HelperOptions) {
const { hlevel } = getHLevel(hsublevel, opts);
return new Array(hlevel).fill('#').join('');
};
/**
* Delineates a section where headings should be increased by 1 or a custom number.
*
* {{#hsection}}
* {{>partial-with-headings}}
* {{/hsection}}
*/
export function hsection(opts: HelperOptions): string;
export function hsection(hsublevel: number, opts: HelperOptions): string;
export function hsection(this: unknown, hsublevel: number | HelperOptions, opts?: HelperOptions) {
let hlevel;
({ hlevel, opts } = getHLevel(hsublevel, opts));
opts.data = Utils.createFrame(opts.data);
opts.data.hlevel = hlevel;
return opts.fn(this as unknown, opts);
}
/**
* Helper for dealing with the optional hsublevel argument.
*/
function getHLevel(hsublevel: number | HelperOptions, opts?: HelperOptions) {
if (typeof hsublevel === 'number') {
opts = opts!;
hsublevel = Math.max(1, hsublevel);
} else {
opts = hsublevel;
hsublevel = 1;
}
const contextHLevel: number = opts.data?.hlevel ?? 0;
return { opts, hlevel: contextHLevel + hsublevel };
}

@ -0,0 +1,8 @@
# Solidity API
{{#each items}}
{{#hsection}}
{{>item}}
{{/hsection}}
{{/each}}

@ -0,0 +1,9 @@
{{>common}}
```solidity
struct {{name}} {
{{#each members}}
{{{typeName.typeDescriptions.typeString}}} {{name}};
{{/each}}
}
```

@ -0,0 +1,13 @@
import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site';
export class ItemError extends Error {
constructor(msg: string, item: DocItemWithContext) {
const ctx = item[DOC_ITEM_CONTEXT];
const src = ctx && ctx.build.decodeSrc(item);
if (src) {
super(msg + ` (${src})`);
} else {
super(msg);
}
}
}

@ -0,0 +1,5 @@
export function arraysEqual<T>(a: T[], b: T[]): boolean;
export function arraysEqual<T, U>(a: T[], b: T[], mapFn: (x: T) => U): boolean;
export function arraysEqual<T>(a: T[], b: T[], mapFn = (x: T) => x): boolean {
return a.length === b.length && a.every((x, i) => mapFn(x) === mapFn(b[i]!));
}

@ -0,0 +1 @@
export type AssertEqual<T, U> = [T, U] extends [U, T] ? true : never;

@ -0,0 +1,6 @@
/**
* Deep cloning good enough for simple objects like solc output. Types are not
* sound because the function may lose information: non-enumerable properties,
* symbols, undefined values, prototypes, etc.
*/
export const clone = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));

@ -0,0 +1,12 @@
// The function below would not be correctly typed if the return type was T[]
// because T may itself be an array type and Array.isArray would not know the
// difference. Adding IfArray<T> makes sure the return type is always correct.
type IfArray<T> = T extends any[] ? T : never;
export function ensureArray<T>(x: T | T[]): T[] | IfArray<T> {
if (Array.isArray(x)) {
return x;
} else {
return [x];
}
}

@ -0,0 +1,18 @@
/**
* Iterates over all contiguous matches of the regular expression over the
* text. Stops as soon as the regular expression no longer matches at the
* current position.
*/
export function* execAll(re: RegExp, text: string) {
re = new RegExp(re, re.flags + (re.sticky ? '' : 'y'));
while (true) {
const match = re.exec(text);
// We break out of the loop if there is no match or if the empty string is
// matched because no progress will be made and it will loop indefinitely.
if (!match?.[0]) break;
yield match;
}
}

@ -0,0 +1,5 @@
import path from 'path';
export function isChild(file: string, parent: string) {
return path.normalize(file + path.sep).startsWith(path.normalize(parent + path.sep));
}

@ -0,0 +1,7 @@
import { DocItem } from '../doc-item';
export function itemType(item: DocItem): string {
return item.nodeType
.replace(/(Definition|Declaration)$/, '')
.replace(/(\w)([A-Z])/g, '$1 $2');
}

@ -0,0 +1,4 @@
export function mapKeys<T>(obj: Record<string, T>, fn: (key: string) => string): Record<string, T> {
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [fn(k), v]));
}

@ -0,0 +1,19 @@
export function mapValues<T, U>(obj: Record<string, T>, fn: (value: T) => U): Record<string, U> {
const res: Record<string, U> = {};
for (const [k, v] of Object.entries(obj)) {
res[k] = fn(v);
}
return res;
}
export function filterValues<T, U extends T>(obj: Record<string, T>, fn: (value: T) => value is U): Record<string, U>;
export function filterValues<T>(obj: Record<string, T>, fn: (value: T) => boolean): Record<string, T>;
export function filterValues<T>(obj: Record<string, T>, fn: (value: T) => boolean): Record<string, T> {
const res: Record<string, T> = {};
for (const [k, v] of Object.entries(obj)) {
if (fn(v)) {
res[k] = v;
}
}
return res;
}

@ -0,0 +1,23 @@
export function defineGetterMemoized<K extends keyof any, T, O extends { [k in K]?: T }>(obj: O, key: K, getter: () => T) {
let state: 'todo' | 'doing' | 'done' = 'todo';
let value: T;
Object.defineProperty(obj, key, {
enumerable: true,
get() {
switch (state) {
case 'done':
return value;
case 'doing':
throw new Error("Detected recursion");
case 'todo':
state = 'doing';
value = getter();
state = 'done';
return value;
}
}
});
}

@ -0,0 +1,145 @@
import { FunctionDefinition } from 'solidity-ast';
import { findAll } from 'solidity-ast/utils';
import { DocItemWithContext, DOC_ITEM_CONTEXT } from '../site';
import { arraysEqual } from './arrays-equal';
import { execAll } from './execall';
import { itemType } from './item-type';
import { ItemError } from './ItemError';
import { readItemDocs } from './read-item-docs';
import { getContractsInScope } from './scope';
export interface NatSpec {
title?: string;
notice?: string;
dev?: string;
params?: {
name: string;
description: string;
}[];
returns?: {
name?: string;
description: string;
}[];
custom?: {
[tag: string]: string;
};
}
export function parseNatspec(item: DocItemWithContext): NatSpec {
if (!item[DOC_ITEM_CONTEXT]) throw new Error(`Not an item or item is missing context`);
let res: NatSpec = {};
const docSource = readItemDocs(item);
const docString = docSource !== undefined
? cleanUpDocstringFromSource(docSource)
: 'documentation' in item && item.documentation
? typeof item.documentation === 'string'
? item.documentation
: cleanUpDocstringFromSolc(item.documentation.text)
: '';
const tagMatches = execAll(
/^(?:@(\w+|custom:[a-z][a-z-]*) )?((?:(?!^@(?:\w+|custom:[a-z][a-z-]*) )[^])*)/m,
docString,
);
let inheritFrom: FunctionDefinition | undefined;
for (const [, tag = 'notice', content] of tagMatches) {
if (content === undefined) throw new ItemError('Unexpected error', item);
if (tag === 'dev' || tag === 'notice') {
res[tag] ??= '';
res[tag] += content;
}
if (tag === 'title') {
res.title = content.trim();
}
if (tag === 'param') {
const paramMatches = content.match(/(\w+) ([^]*)/);
if (paramMatches) {
const [, name, description] = paramMatches as [string, string, string];
res.params ??= [];
res.params.push({ name, description: description.trim() });
}
}
if (tag === 'return') {
if (!('returnParameters' in item)) {
throw new ItemError(`Item does not contain return parameters`, item);
}
res.returns ??= [];
const i = res.returns.length;
const p = item.returnParameters.parameters[i];
if (p === undefined) {
throw new ItemError('Got more @return tags than expected', item);
}
if (!p.name) {
res.returns.push({ description: content.trim() });
} else {
const paramMatches = content.match(/(\w+)( ([^]*))?/);
if (!paramMatches || paramMatches[1] !== p.name) {
throw new ItemError(`Expected @return tag to start with name '${p.name}'`, item);
}
const [, name, description] = paramMatches as [string, string, string?];
res.returns.push({ name, description: description?.trim() ?? '' });
}
}
if (tag?.startsWith('custom:')) {
const key = tag.replace(/^custom:/, '');
res.custom ??= {};
res.custom[key] ??= '';
res.custom[key] += content;
}
if (tag === 'inheritdoc') {
if (!(item.nodeType === 'FunctionDefinition' || item.nodeType === 'VariableDeclaration')) {
throw new ItemError(`Expected function or variable but saw ${itemType(item)}`, item);
}
const parentContractName = content.trim();
const parentContract = getContractsInScope(item)[parentContractName];
if (!parentContract) {
throw new ItemError(`Parent contract '${parentContractName}' not found`, item);
}
inheritFrom = [...findAll('FunctionDefinition', parentContract)].find(f => item.baseFunctions?.includes(f.id));
}
}
if (docString.length === 0) {
if ('baseFunctions' in item && item.baseFunctions?.length === 1) {
const baseFn = item[DOC_ITEM_CONTEXT].build.deref('FunctionDefinition', item.baseFunctions[0]!);
const shouldInherit = item.nodeType === 'VariableDeclaration' || arraysEqual(item.parameters.parameters, baseFn.parameters.parameters, p => p.name);
if (shouldInherit) {
inheritFrom = baseFn;
}
}
}
if (res.dev) res.dev = res.dev.trim();
if (res.notice) res.notice = res.notice.trim();
if (inheritFrom) {
res = { ...parseNatspec(inheritFrom as DocItemWithContext), ...res };
}
return res;
}
// Fix solc buggy parsing of doc comments.
// Reverse engineered from solc behavior.
function cleanUpDocstringFromSolc(text: string) {
return text
.replace(/\n\n?^[ \t]*(?:\*|\/\/\/)/mg, '\n\n')
.replace(/^[ \t]?/mg, '');
}
function cleanUpDocstringFromSource(text: string) {
return text
.replace(/^\/\*\*(.*)\*\/$/s, '$1')
.trim()
.replace(/^[ \t]*(\*|\/\/\/)[ \t]?/mg, '');
}

@ -0,0 +1,26 @@
import { DocItemWithContext, DOC_ITEM_CONTEXT, Build } from '../site';
export function readItemDocs(item: DocItemWithContext): string | undefined {
const { build } = item[DOC_ITEM_CONTEXT];
// Note that Solidity 0.5 has item.documentation: string even though the
// types do not reflect that. This is why we check typeof === object.
if ('documentation' in item && item.documentation && typeof item.documentation === 'object') {
const { source, start, length } = decodeSrc(item.documentation.src, build);
const content = build.input.sources[source]?.content;
if (content !== undefined) {
return Buffer.from(content, 'utf8').slice(start, start + length).toString('utf8');
}
}
}
function decodeSrc(src: string, build: Build): { source: string; start: number; length: number } {
const [start, length, sourceId] = src.split(':').map(s => parseInt(s));
if (start === undefined || length === undefined || sourceId === undefined) {
throw new Error(`Bad source string ${src}`);
}
const source = Object.keys(build.output.sources).find(s => build.output.sources[s]?.id === sourceId);
if (source === undefined) {
throw new Error(`No source with id ${sourceId}`);
}
return { source, start, length };
}

@ -0,0 +1,63 @@
import { ContractDefinition, SourceUnit } from "solidity-ast";
import { findAll, isNodeType } from "solidity-ast/utils";
import { DocItemWithContext } from "../site";
import { filterValues, mapValues } from './map-values';
import { mapKeys } from './map-keys';
type Definition = SourceUnit['nodes'][number] & { name: string };
type Scope = { [name in string]: () => { namespace: Scope } | { definition: Definition } };
export function getContractsInScope(item: DocItemWithContext) {
const cache = new WeakMap<SourceUnit, Scope>();
return filterValues(
flattenScope(run(item.__item_context.file)),
isNodeType('ContractDefinition'),
);
function run(file: SourceUnit): Scope {
if (cache.has(file)) {
return cache.get(file)!;
}
const scope: Scope = {};
cache.set(file, scope);
for (const c of file.nodes) {
if ('name' in c) {
scope[c.name] = () => ({ definition: c });
}
}
for (const i of findAll('ImportDirective', file)) {
const importedFile = item.__item_context.build.deref('SourceUnit', i.sourceUnit);
const importedScope = run(importedFile);
if (i.unitAlias) {
scope[i.unitAlias] = () => ({ namespace: importedScope });
} else if (i.symbolAliases.length === 0) {
Object.assign(scope, importedScope);
} else {
for (const a of i.symbolAliases) {
// Delayed function call supports circular dependencies
scope[a.local ?? a.foreign.name] = importedScope[a.foreign.name] ?? (() => importedScope[a.foreign.name]!());
}
}
};
return scope;
}
}
function flattenScope(scope: Scope): Record<string, Definition> {
return Object.fromEntries(
Object.entries(scope).flatMap(([k, fn]) => {
const v = fn();
if ('definition' in v) {
return [[k, v.definition] as const];
} else {
return Object.entries(mapKeys(flattenScope(v.namespace), k2 => k + '.' + k2));
}
}),
);
}

@ -1,8 +1,8 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { AppContext } from "../AppContext"; import { AppContext } from "../AppContext";
import { ContractName, Documentation } from "../types"; import { ContractName, Documentation } from "../../types";
import { publish } from "../utils"; import { publish } from "../../utils";
import { htmlTemplate } from "../utils/template"; import { htmlTemplate } from "../../utils/template";
export const HomeView: React.FC = () => { export const HomeView: React.FC = () => {
const [activeItem, setActiveItem] = useState(""); const [activeItem, setActiveItem] = useState("");

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import App from "./App"; import App from "./app/App";
// import { Routes } from "./routes"; // import { Routes } from "./routes";
ReactDOM.render( ReactDOM.render(
Loading…
Cancel
Save