Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:

strategy:
matrix:
node-version: [12.18.2, 14.x]
node-version: [16.18, 18.11]

steps:
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.cache/
.vscode/
coverage/
dist/
Expand Down
9 changes: 9 additions & 0 deletions doc-gen/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import process from 'process'

import { build } from './index.js'

(async function main() {
const [ directory ] = process.argv.slice(2)

await build(directory)
})()
146 changes: 146 additions & 0 deletions doc-gen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { compile } from '@mdx-js/mdx'
import fs from 'fs'
import matter from 'gray-matter'
import { basename, join } from 'path'
import process from 'process'
import { createElement } from 'react'
import * as ReactDOMServer from 'react-dom/server'
import rehypeHighlight from 'rehype-highlight'
import remarkFrontmatter from 'remark-frontmatter'
import rimraf from 'rimraf'
import { ServerStyleSheet } from 'styled-components'

import { PageLayout } from './site/index.js'
import { Page } from './site/page.js'
import { loadExamples } from '../example-lib/index.js'

export async function build(inputPath: string): Promise<void> {
const basePath = join(process.cwd(), inputPath)
const cachePath = join(process.cwd(), '.cache')

const pages = await loadPages(join(basePath, 'pages'))

const examples = await loadExamples(join(basePath, 'examples'))

const outputPath = join(basePath, 'dist')

// if (fs.existsSync(outputPath)){
// rimraf.sync(outputPath)
// }

if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath)
}

if (!fs.existsSync(join(outputPath, 'public'))) {
fs.mkdirSync(join(outputPath, 'public'))
}

if (!fs.existsSync(cachePath)){
fs.mkdirSync(cachePath)
}

const highlightStyles = await fs.promises.readFile(join(basePath, '..', 'node_modules', 'highlight.js', 'styles', 'github-dark.css'), 'utf-8')

for (const page of pages) {
const slug = basename(page.filename, '.mdx')
const path = join(outputPath, `${ slug }.html`)

console.log(`Creating page: ${ slug }`)

if (!page.title) {
console.log('Missing Title.')

return
}

const content = await fs.promises.readFile(page.path, 'utf-8')

const js = await compile(content, {
remarkPlugins: [
remarkFrontmatter
],
rehypePlugins: [
// rehypeHighlight
]
})

const jsPath = join(cachePath, `${ slug }.js`)

await fs.promises.writeFile(jsPath, js.value, 'utf-8')

// @ts-ignore
const { default: mdxComponent } = await import(`../.cache/${ slug }.js`)

const contentElement = createElement(mdxComponent, {})

const sheet = new ServerStyleSheet()

const pageElement = createElement(Page, {
children: contentElement,
slug,
title: page.title
})

const layoutElement = createElement(PageLayout, {
children: pageElement,
examples,
extraStyles: [
highlightStyles
],
title: `${ page.title } - Esix`
})

let markup = ReactDOMServer.renderToStaticMarkup(sheet.collectStyles(layoutElement))

const styleTags = sheet.getStyleTags()

markup = markup.replace('<style></style>', styleTags)

markup = '<!DOCTYPE html>\n' + markup

await fs.promises.writeFile(path, markup, 'utf-8')
}

}

type PageInfo = {
filename: string
path: string

title?: string
description?: string
}

async function loadPages(pagesPath: string): Promise<PageInfo[]> {
const filenames = await fs.promises.readdir(pagesPath)

const pageInfoList = await Promise.all(filenames.map(filename => {
return loadPageInfo(
filename,
join(pagesPath, filename)
)
}))

return pageInfoList.filter((pageInfo): pageInfo is PageInfo => pageInfo !== undefined)
}

async function loadPageInfo(filename: string, path: string): Promise<PageInfo | undefined> {
try {
const content = await fs.promises.readFile(path, 'utf-8')

const document = matter(content)

return {
description: document.data.description,
filename,
path,
title: document.data.title
}
} catch (error) {
console.error('Failed to load Page Info.', path)
console.error(error)
}

return undefined
}
45 changes: 45 additions & 0 deletions doc-gen/site/code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import hljs from 'highlight.js'
import lodash from 'lodash'
import React, { ReactElement, isValidElement, useMemo } from 'react'

type CodeProps = {
children?: string | string[]
code?: string
lang?: string
}

export function Code({ children, code, lang }: CodeProps): ReactElement {
const rawCode = useMemo(() => {
if (code) {
return code
}

const childTexts = childrenToText(children)

return lodash.isArray(childTexts) ? childTexts.join('\n') : childTexts
}, [ children, code ])

console.log({ lang, rawCode })

//@ts-ignore
const html = hljs.highlight(rawCode, { language: lang ?? 'typescript' }).value + '\n'

return (
<pre>
<code
className='hljs language-ts'
dangerouslySetInnerHTML={{ __html: html }}
/>
</pre>
)
}

const hasChildren = (element: any) => isValidElement(element) && Boolean((element?.props as any).children as any)

const childrenToText = (children: any): string => {
if (hasChildren(children)) {
return childrenToText(children.props.children)
}

return children
}
28 changes: 28 additions & 0 deletions doc-gen/site/example-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { ReactElement, createContext } from 'react'

import { Example } from '../../example-lib/index.js'

type ExampleContextProps = {
getExample: (name: string) => Example | undefined
}

export const ExampleContext = createContext<ExampleContextProps>({} as ExampleContextProps)

type ExampleProviderProps = {
children: ReactElement
examples: Example[]
}

export function ExampleProvider({ children, examples }: ExampleProviderProps): ReactElement {
const getExample = (name: string): Example | undefined => examples.find(example => example.name === name)

const context = {
getExample
}

return (
<ExampleContext.Provider value={ context }>
{ children }
</ExampleContext.Provider>
)
}
78 changes: 78 additions & 0 deletions doc-gen/site/example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { ReactElement, useContext } from 'react'
import { Code } from './code.js'

import { ExampleContext } from './example-provider.js'
import { Table } from './table.js'

interface ExampleProps {
name: string
}

export function ExampleComponent({ name }: ExampleProps): ReactElement | null {
const { getExample } = useContext(ExampleContext)

const example = getExample(name)

if (!example) {
return null
}

const isNumber = (a: any): a is number => !isNaN(a)
const isObject = (a: any): a is object => (typeof a === 'object' || typeof a === 'function') && (a !== null)
const isString = (a: any): a is string => typeof a === 'string'

const text = example.text || ''

if (isNumber(example.output) || isString(example.output)) {
return (
<Code>
{ text + '\n' }

{ `// => ${ example.output }` }
</Code>
)
}

// The Array check has to come before the Object one as an Array is also an Object.
if (Array.isArray(example.output)) {
if (!isObject(example.output[0])) {
return (
<Code>
{ text + '\n' }

{ `// => [ ${ example.output.map(v => `'${ v }'`).join(', ') } ]` }
</Code>
)
}

return (
<>
<Code>
{ text }
</Code>
<Table rows={ example.output } type="array" />
</>
)
}

if (isObject(example.output)) {
return (
<>
<Code code={ text } />
<Table rows={ [ example.output ] } type="object" />
</>
)
}

return (
<>
<Code>
{ text }
</Code>

<Code>
{ JSON.stringify(example.output, null, 2) }
</Code>
</>
)
}
Loading