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