diff --git a/apps/ethdoc/src/react-app-env.d.ts b/apps/ethdoc/src/react-app-env.d.ts
deleted file mode 100644
index 6431bc5fc6..0000000000
--- a/apps/ethdoc/src/react-app-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/apps/ethdoc/src/setupTests.ts b/apps/ethdoc/src/setupTests.ts
deleted file mode 100644
index 74b1a275a0..0000000000
--- a/apps/ethdoc/src/setupTests.ts
+++ /dev/null
@@ -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';
diff --git a/apps/ethdoc/.coveralls.yml b/apps/remixdocgen/.coveralls.yml
similarity index 100%
rename from apps/ethdoc/.coveralls.yml
rename to apps/remixdocgen/.coveralls.yml
diff --git a/apps/ethdoc/.eslintcache b/apps/remixdocgen/.eslintcache
similarity index 100%
rename from apps/ethdoc/.eslintcache
rename to apps/remixdocgen/.eslintcache
diff --git a/apps/ethdoc/docs/img/ethdoc.png b/apps/remixdocgen/docs/img/ethdoc.png
similarity index 100%
rename from apps/ethdoc/docs/img/ethdoc.png
rename to apps/remixdocgen/docs/img/ethdoc.png
diff --git a/apps/ethdoc/docs/index.md b/apps/remixdocgen/docs/index.md
similarity index 100%
rename from apps/ethdoc/docs/index.md
rename to apps/remixdocgen/docs/index.md
diff --git a/apps/ethdoc/mkdocs.yml b/apps/remixdocgen/mkdocs.yml
similarity index 100%
rename from apps/ethdoc/mkdocs.yml
rename to apps/remixdocgen/mkdocs.yml
diff --git a/apps/ethdoc/public/favicon.ico b/apps/remixdocgen/public/favicon.ico
similarity index 100%
rename from apps/ethdoc/public/favicon.ico
rename to apps/remixdocgen/public/favicon.ico
diff --git a/apps/ethdoc/public/index.html b/apps/remixdocgen/public/index.html
similarity index 100%
rename from apps/ethdoc/public/index.html
rename to apps/remixdocgen/public/index.html
diff --git a/apps/ethdoc/public/logo192.png b/apps/remixdocgen/public/logo192.png
similarity index 100%
rename from apps/ethdoc/public/logo192.png
rename to apps/remixdocgen/public/logo192.png
diff --git a/apps/ethdoc/public/logo512.png b/apps/remixdocgen/public/logo512.png
similarity index 100%
rename from apps/ethdoc/public/logo512.png
rename to apps/remixdocgen/public/logo512.png
diff --git a/apps/ethdoc/public/manifest.json b/apps/remixdocgen/public/manifest.json
similarity index 100%
rename from apps/ethdoc/public/manifest.json
rename to apps/remixdocgen/public/manifest.json
diff --git a/apps/ethdoc/public/robots.txt b/apps/remixdocgen/public/robots.txt
similarity index 100%
rename from apps/ethdoc/public/robots.txt
rename to apps/remixdocgen/public/robots.txt
diff --git a/apps/ethdoc/src/App.css b/apps/remixdocgen/src/app/App.css
similarity index 100%
rename from apps/ethdoc/src/App.css
rename to apps/remixdocgen/src/app/App.css
diff --git a/apps/ethdoc/src/App.tsx b/apps/remixdocgen/src/app/App.tsx
similarity index 96%
rename from apps/ethdoc/src/App.tsx
rename to apps/remixdocgen/src/app/App.tsx
index c8d0ac5cc4..3bb94bdb99 100644
--- a/apps/ethdoc/src/App.tsx
+++ b/apps/remixdocgen/src/app/App.tsx
@@ -11,10 +11,10 @@ import { Status } from "@remixproject/plugin-utils";
import { AppContext } from "./AppContext";
import { Routes } from "./routes";
import { useLocalStorage } from "./hooks/useLocalStorage";
-import { createDocumentation } from "./utils/utils";
+import { createDocumentation } from "../utils/utils";
import "./App.css";
-import { ContractName, Documentation } from "./types";
+import { ContractName, Documentation } from "../types";
export const getNewContractNames = (compilationResult: CompilationResult) => {
const compiledContracts = compilationResult.contracts;
diff --git a/apps/ethdoc/src/AppContext.tsx b/apps/remixdocgen/src/app/AppContext.tsx
similarity index 90%
rename from apps/ethdoc/src/AppContext.tsx
rename to apps/remixdocgen/src/app/AppContext.tsx
index b7acff6ad0..612ac7af9a 100644
--- a/apps/ethdoc/src/AppContext.tsx
+++ b/apps/remixdocgen/src/app/AppContext.tsx
@@ -3,7 +3,7 @@ import { PluginClient } from "@remixproject/plugin";
import { PluginApi, Api } from "@remixproject/plugin-utils";
import { IRemixApi } from "@remixproject/plugin-api";
-import { ContractName, Documentation, PublishedSite } from "./types";
+import { ContractName, Documentation, PublishedSite } from "../types";
export const AppContext = React.createContext({
clientInstance: {} as PluginApi> &
diff --git a/apps/remixdocgen/src/app/docgen/common/helpers.ts b/apps/remixdocgen/src/app/docgen/common/helpers.ts
new file mode 100644
index 0000000000..926e6df9da
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/common/helpers.ts
@@ -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;
diff --git a/apps/remixdocgen/src/app/docgen/common/properties.ts b/apps/remixdocgen/src/app/docgen/common/properties.ts
new file mode 100644
index 0000000000..3eb9919cd3
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/common/properties.ts
@@ -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)];
+}
diff --git a/apps/remixdocgen/src/app/docgen/config.ts b/apps/remixdocgen/src/app/docgen/config.ts
new file mode 100644
index 0000000000..2a3f9fcc6e
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/config.ts
@@ -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;
+
+export const defaults: Omit = {
+ root: process.cwd(),
+ sourcesDir: 'contracts',
+ outputDir: 'docs',
+ pages: 'single',
+ exclude: [],
+ theme: 'markdown',
+ collapseNewlines: true,
+ pageExtension: '.md',
+};
diff --git a/apps/remixdocgen/src/app/docgen/doc-item.ts b/apps/remixdocgen/src/app/docgen/doc-item.ts
new file mode 100644
index 0000000000..eb03223027
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/doc-item.ts
@@ -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 = true;
+
+export function isDocItem(node: Node): node is DocItem {
+ return (docItemTypes as readonly string[]).includes(node.nodeType);
+}
diff --git a/apps/remixdocgen/src/app/docgen/render.ts b/apps/remixdocgen/src/app/docgen/render.ts
new file mode 100644
index 0000000000..5c206f92b6
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/render.ts
@@ -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}}');
+}
diff --git a/apps/remixdocgen/src/app/docgen/site.ts b/apps/remixdocgen/src/app/docgen/site.ts
new file mode 100644
index 0000000000..90d63647a4
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/site.ts
@@ -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;
+export type PageStructure = SiteConfig['pages'];
+export type PageAssigner = ((item: DocItem, file: SourceUnit, config: SiteConfig) => string | undefined);
+
+export const pageAssigner: Record = {
+ 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();
+ const items: DocItemWithContext[] = [];
+ const pages: Record = {};
+
+ // 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)))
+ );
+}
diff --git a/apps/remixdocgen/src/app/docgen/templates.ts b/apps/remixdocgen/src/app/docgen/templates.ts
new file mode 100644
index 0000000000..7f6a43b308
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/templates.ts
@@ -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;
+
+export interface Templates {
+ partials?: Record string>;
+ helpers?: Record string>;
+ properties?: Record;
+}
+
+/**
+ * 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 {
+ const themes = await readThemes();
+
+ // Initialize templates with the default theme.
+ const templates: Required = {
+ 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> {
+ return {
+ partials: await readPartials(),
+ helpers: await readHelpers('helpers'),
+ properties: await readHelpers('properties'),
+ };
+}
+
+async function readPartials() {
+ const partials: NonNullable = {};
+ 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 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>> {
+ const themes: Record> = {};
+
+
+
+ themes['markdown'] = await readTemplates();
+
+
+ return themes;
+}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/common.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/common.hbs
new file mode 100644
index 0000000000..b2a3b2a5a2
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/common.hbs
@@ -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}}
+
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/contract.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/contract.hbs
new file mode 100644
index 0000000000..e4ed15831c
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/contract.hbs
@@ -0,0 +1,8 @@
+{{>common}}
+
+{{#each items}}
+{{#hsection}}
+{{>item}}
+{{/hsection}}
+
+{{/each}}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/enum.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/enum.hbs
new file mode 100644
index 0000000000..677406db9c
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/enum.hbs
@@ -0,0 +1,9 @@
+{{>common}}
+
+```solidity
+enum {{name}} {
+{{#each members}}
+ {{name}}{{#unless @last}},{{/unless}}
+{{/each}}
+}
+```
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/error.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/error.hbs
new file mode 100644
index 0000000000..5373296cbc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/error.hbs
@@ -0,0 +1 @@
+{{>common}}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/event.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/event.hbs
new file mode 100644
index 0000000000..5373296cbc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/event.hbs
@@ -0,0 +1 @@
+{{>common}}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/function.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/function.hbs
new file mode 100644
index 0000000000..5373296cbc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/function.hbs
@@ -0,0 +1 @@
+{{>common}}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/helpers.ts b/apps/remixdocgen/src/app/docgen/themes/markdown/helpers.ts
new file mode 100644
index 0000000000..bfd5cfbdd7
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/helpers.ts
@@ -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 };
+}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/modifier.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/modifier.hbs
new file mode 100644
index 0000000000..5373296cbc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/modifier.hbs
@@ -0,0 +1 @@
+{{>common}}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/page.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/page.hbs
new file mode 100644
index 0000000000..6597f0b5c7
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/page.hbs
@@ -0,0 +1,8 @@
+# Solidity API
+
+{{#each items}}
+{{#hsection}}
+{{>item}}
+{{/hsection}}
+
+{{/each}}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/struct.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/struct.hbs
new file mode 100644
index 0000000000..867069e2bc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/struct.hbs
@@ -0,0 +1,9 @@
+{{>common}}
+
+```solidity
+struct {{name}} {
+{{#each members}}
+ {{{typeName.typeDescriptions.typeString}}} {{name}};
+{{/each}}
+}
+```
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/user-defined-value-type.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/user-defined-value-type.hbs
new file mode 100644
index 0000000000..5373296cbc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/user-defined-value-type.hbs
@@ -0,0 +1 @@
+{{>common}}
diff --git a/apps/remixdocgen/src/app/docgen/themes/markdown/variable.hbs b/apps/remixdocgen/src/app/docgen/themes/markdown/variable.hbs
new file mode 100644
index 0000000000..5373296cbc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/themes/markdown/variable.hbs
@@ -0,0 +1 @@
+{{>common}}
diff --git a/apps/remixdocgen/src/app/docgen/utils/ItemError.ts b/apps/remixdocgen/src/app/docgen/utils/ItemError.ts
new file mode 100644
index 0000000000..3791f71443
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/ItemError.ts
@@ -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);
+ }
+ }
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/arrays-equal.ts b/apps/remixdocgen/src/app/docgen/utils/arrays-equal.ts
new file mode 100644
index 0000000000..fe08726e95
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/arrays-equal.ts
@@ -0,0 +1,5 @@
+export function arraysEqual(a: T[], b: T[]): boolean;
+export function arraysEqual(a: T[], b: T[], mapFn: (x: T) => U): boolean;
+export function arraysEqual(a: T[], b: T[], mapFn = (x: T) => x): boolean {
+ return a.length === b.length && a.every((x, i) => mapFn(x) === mapFn(b[i]!));
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/assert-equal-types.ts b/apps/remixdocgen/src/app/docgen/utils/assert-equal-types.ts
new file mode 100644
index 0000000000..0171d13a36
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/assert-equal-types.ts
@@ -0,0 +1 @@
+export type AssertEqual = [T, U] extends [U, T] ? true : never;
diff --git a/apps/remixdocgen/src/app/docgen/utils/clone.ts b/apps/remixdocgen/src/app/docgen/utils/clone.ts
new file mode 100644
index 0000000000..f92fa70189
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/clone.ts
@@ -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 = (obj: T): T => JSON.parse(JSON.stringify(obj));
diff --git a/apps/remixdocgen/src/app/docgen/utils/ensure-array.ts b/apps/remixdocgen/src/app/docgen/utils/ensure-array.ts
new file mode 100644
index 0000000000..cd0f8ea570
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/ensure-array.ts
@@ -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 makes sure the return type is always correct.
+type IfArray = T extends any[] ? T : never;
+
+export function ensureArray(x: T | T[]): T[] | IfArray {
+ if (Array.isArray(x)) {
+ return x;
+ } else {
+ return [x];
+ }
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/execall.ts b/apps/remixdocgen/src/app/docgen/utils/execall.ts
new file mode 100644
index 0000000000..2aca0ad77c
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/execall.ts
@@ -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;
+ }
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/is-child.ts b/apps/remixdocgen/src/app/docgen/utils/is-child.ts
new file mode 100644
index 0000000000..cb07cc7136
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/is-child.ts
@@ -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));
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/item-type.ts b/apps/remixdocgen/src/app/docgen/utils/item-type.ts
new file mode 100644
index 0000000000..8873ece1fc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/item-type.ts
@@ -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');
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/map-keys.ts b/apps/remixdocgen/src/app/docgen/utils/map-keys.ts
new file mode 100644
index 0000000000..7852cd85dc
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/map-keys.ts
@@ -0,0 +1,4 @@
+export function mapKeys(obj: Record, fn: (key: string) => string): Record {
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [fn(k), v]));
+}
+
diff --git a/apps/remixdocgen/src/app/docgen/utils/map-values.ts b/apps/remixdocgen/src/app/docgen/utils/map-values.ts
new file mode 100644
index 0000000000..c151d7d552
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/map-values.ts
@@ -0,0 +1,19 @@
+export function mapValues(obj: Record, fn: (value: T) => U): Record {
+ const res: Record = {};
+ for (const [k, v] of Object.entries(obj)) {
+ res[k] = fn(v);
+ }
+ return res;
+}
+
+export function filterValues(obj: Record, fn: (value: T) => value is U): Record;
+export function filterValues(obj: Record, fn: (value: T) => boolean): Record;
+export function filterValues(obj: Record, fn: (value: T) => boolean): Record {
+ const res: Record = {};
+ for (const [k, v] of Object.entries(obj)) {
+ if (fn(v)) {
+ res[k] = v;
+ }
+ }
+ return res;
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/memoized-getter.ts b/apps/remixdocgen/src/app/docgen/utils/memoized-getter.ts
new file mode 100644
index 0000000000..1a2fef4ae5
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/memoized-getter.ts
@@ -0,0 +1,23 @@
+export function defineGetterMemoized(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;
+ }
+ }
+ });
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/natspec.ts b/apps/remixdocgen/src/app/docgen/utils/natspec.ts
new file mode 100644
index 0000000000..40f752aaa8
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/natspec.ts
@@ -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, '');
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/read-item-docs.ts b/apps/remixdocgen/src/app/docgen/utils/read-item-docs.ts
new file mode 100644
index 0000000000..36ee13aa7f
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/read-item-docs.ts
@@ -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 };
+}
diff --git a/apps/remixdocgen/src/app/docgen/utils/scope.ts b/apps/remixdocgen/src/app/docgen/utils/scope.ts
new file mode 100644
index 0000000000..dabe990137
--- /dev/null
+++ b/apps/remixdocgen/src/app/docgen/utils/scope.ts
@@ -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();
+
+ 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 {
+ 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));
+ }
+ }),
+ );
+}
diff --git a/apps/ethdoc/src/hooks/useLocalStorage.tsx b/apps/remixdocgen/src/app/hooks/useLocalStorage.tsx
similarity index 100%
rename from apps/ethdoc/src/hooks/useLocalStorage.tsx
rename to apps/remixdocgen/src/app/hooks/useLocalStorage.tsx
diff --git a/apps/ethdoc/src/routes.tsx b/apps/remixdocgen/src/app/routes.tsx
similarity index 100%
rename from apps/ethdoc/src/routes.tsx
rename to apps/remixdocgen/src/app/routes.tsx
diff --git a/apps/ethdoc/src/views/ErrorView.tsx b/apps/remixdocgen/src/app/views/ErrorView.tsx
similarity index 100%
rename from apps/ethdoc/src/views/ErrorView.tsx
rename to apps/remixdocgen/src/app/views/ErrorView.tsx
diff --git a/apps/ethdoc/src/views/HomeView.tsx b/apps/remixdocgen/src/app/views/HomeView.tsx
similarity index 97%
rename from apps/ethdoc/src/views/HomeView.tsx
rename to apps/remixdocgen/src/app/views/HomeView.tsx
index 0845d99b39..11feae8bf6 100644
--- a/apps/ethdoc/src/views/HomeView.tsx
+++ b/apps/remixdocgen/src/app/views/HomeView.tsx
@@ -1,8 +1,8 @@
import React, { useState, useEffect, useRef } from "react";
import { AppContext } from "../AppContext";
-import { ContractName, Documentation } from "../types";
-import { publish } from "../utils";
-import { htmlTemplate } from "../utils/template";
+import { ContractName, Documentation } from "../../types";
+import { publish } from "../../utils";
+import { htmlTemplate } from "../../utils/template";
export const HomeView: React.FC = () => {
const [activeItem, setActiveItem] = useState("");
diff --git a/apps/ethdoc/src/views/index.ts b/apps/remixdocgen/src/app/views/index.ts
similarity index 100%
rename from apps/ethdoc/src/views/index.ts
rename to apps/remixdocgen/src/app/views/index.ts
diff --git a/apps/ethdoc/src/index.tsx b/apps/remixdocgen/src/index.tsx
similarity index 87%
rename from apps/ethdoc/src/index.tsx
rename to apps/remixdocgen/src/index.tsx
index 3779b3d173..a6dc3b3a47 100644
--- a/apps/ethdoc/src/index.tsx
+++ b/apps/remixdocgen/src/index.tsx
@@ -1,6 +1,6 @@
import React from "react";
import ReactDOM from "react-dom";
-import App from "./App";
+import App from "./app/App";
// import { Routes } from "./routes";
ReactDOM.render(
diff --git a/apps/ethdoc/src/types.ts b/apps/remixdocgen/src/types.ts
similarity index 100%
rename from apps/ethdoc/src/types.ts
rename to apps/remixdocgen/src/types.ts
diff --git a/apps/ethdoc/src/utils/faker.ts b/apps/remixdocgen/src/utils/faker.ts
similarity index 100%
rename from apps/ethdoc/src/utils/faker.ts
rename to apps/remixdocgen/src/utils/faker.ts
diff --git a/apps/ethdoc/src/utils/index.ts b/apps/remixdocgen/src/utils/index.ts
similarity index 100%
rename from apps/ethdoc/src/utils/index.ts
rename to apps/remixdocgen/src/utils/index.ts
diff --git a/apps/ethdoc/src/utils/publisher.test.ts b/apps/remixdocgen/src/utils/publisher.test.ts
similarity index 100%
rename from apps/ethdoc/src/utils/publisher.test.ts
rename to apps/remixdocgen/src/utils/publisher.test.ts
diff --git a/apps/ethdoc/src/utils/publisher.ts b/apps/remixdocgen/src/utils/publisher.ts
similarity index 100%
rename from apps/ethdoc/src/utils/publisher.ts
rename to apps/remixdocgen/src/utils/publisher.ts
diff --git a/apps/ethdoc/src/utils/sample-data/file.json b/apps/remixdocgen/src/utils/sample-data/file.json
similarity index 100%
rename from apps/ethdoc/src/utils/sample-data/file.json
rename to apps/remixdocgen/src/utils/sample-data/file.json
diff --git a/apps/ethdoc/src/utils/sample-data/sample-artifact-with-comments.json b/apps/remixdocgen/src/utils/sample-data/sample-artifact-with-comments.json
similarity index 100%
rename from apps/ethdoc/src/utils/sample-data/sample-artifact-with-comments.json
rename to apps/remixdocgen/src/utils/sample-data/sample-artifact-with-comments.json
diff --git a/apps/ethdoc/src/utils/sample-data/sample-artifact.json b/apps/remixdocgen/src/utils/sample-data/sample-artifact.json
similarity index 100%
rename from apps/ethdoc/src/utils/sample-data/sample-artifact.json
rename to apps/remixdocgen/src/utils/sample-data/sample-artifact.json
diff --git a/apps/ethdoc/src/utils/template.ts b/apps/remixdocgen/src/utils/template.ts
similarity index 100%
rename from apps/ethdoc/src/utils/template.ts
rename to apps/remixdocgen/src/utils/template.ts
diff --git a/apps/ethdoc/src/utils/types.ts b/apps/remixdocgen/src/utils/types.ts
similarity index 100%
rename from apps/ethdoc/src/utils/types.ts
rename to apps/remixdocgen/src/utils/types.ts
diff --git a/apps/ethdoc/src/utils/utils.test.ts b/apps/remixdocgen/src/utils/utils.test.ts
similarity index 100%
rename from apps/ethdoc/src/utils/utils.test.ts
rename to apps/remixdocgen/src/utils/utils.test.ts
diff --git a/apps/ethdoc/src/utils/utils.ts b/apps/remixdocgen/src/utils/utils.ts
similarity index 100%
rename from apps/ethdoc/src/utils/utils.ts
rename to apps/remixdocgen/src/utils/utils.ts
diff --git a/apps/ethdoc/tslint.json b/apps/remixdocgen/tslint.json
similarity index 100%
rename from apps/ethdoc/tslint.json
rename to apps/remixdocgen/tslint.json