import { RenderConfig, Data, SupportedMode, TableOfContentEntry, PageConfig, Watch, PageTalkingToCollector } from '../types'; import { toHtml, dedent, highlightCodeWithMode, MarkDownStyles } from './markdown'; import { bundle } from './bundler/bundler'; import * as fse from 'fs-extra'; import { getDemoCodes, getMds } from './tsMagic/argumentCollector'; import * as types from '../types'; import { mainIndex } from "../app/mainIndex"; export const storyAndAppIndexTemplate = ( { index, jsFileName } : { index: number, jsFileName: string } ) => ` Demo: ${index}
`; /** * Collects a document */ export class Page { constructor(private config: RenderConfig & PageConfig & Watch & PageTalkingToCollector) { if (config.repoUrl) { /** http://tholman.com/github-corners/ */ this.html(``) } /** Add to TOC */ this._data.tableOfContents.push({ type: 'pageRoot', heading: config.heading, pageSubDirName: config.subDirName }); /** Add to contents */ this._data.contents.push({ type: 'html', pageSubDirName: config.subDirName, html: `

${config.heading}

` }); } /** * We collect the rendered contents here */ _data: Data = { tableOfContents: [], contents: [], } /** We collect all bundle things in here */ _bundleCollector: { [jsFileNameNoExt: string]: /** Entry path */ string } = {}; html(html: string) { this._data.contents.push({ pageSubDirName: this.config.subDirName, type: 'html', html }); return this; } githubStars({ user, repo }: { user: string, repo: string }) { this._data.contents.push({ type: 'html', pageSubDirName: this.config.subDirName, html: `` }); return this; } md(markdown: string) { /** render the markdown */ const { html, headings } = toHtml(dedent(markdown)); /** Store the html */ this._data.contents.push({ type: 'html', pageSubDirName: this.config.subDirName, html }); /** Ammend TOC */ this.ammendTocWithHeadings(headings); return this; } private ammendTocWithHeadings = (headings: types.Heading[], iframeId?: string) => { /** * Collect headings in table of contents **/ const tableOfContents = this._data.tableOfContents; headings.forEach(heading => { tableOfContents.push({ type: 'pageSub', id: heading.id, iframeId: iframeId, level: heading.level, pageSubDirName: this.config.subDirName, text: heading.text }) }); } code({ mode, code, collapsed }: { mode: SupportedMode, code: string, collapsed?: boolean }) { this._data.contents.push({ type: 'code', html: `
${highlightCodeWithMode({ mode, code: code.trim() })}
`, collapsed: !!collapsed, pageSubDirName: this.config.subDirName }); return this; } /** Each demo and story gets its index. This drives the JS / HTML files names */ private entryPointIndex = 0; story({ entryPointPath, }: { entryPointPath: string, }) { this.entryPointIndex++; const index = this.entryPointIndex; const jsFileName = `story-${this.entryPointIndex}.js`; const jsFileNameNoExt = `story-${this.entryPointIndex}`; const htmlFileName = `story-${this.entryPointIndex}.html`; /** Collect */ const code = fse.readFileSync(entryPointPath).toString(); const content: types.StoryContent = { type: 'story', index: this.entryPointIndex, htmlFileName, code: code, demoCodes: getDemoCodes(code).map( /** * Don't remove this lambda. * Otherwise `dedent` uses the `index` argument adding the number to output */ (c, _mustNotBePassedThrough) => dedent(c) ), pageSubDirName: this.config.subDirName, entryPointPath, }; this._data.contents.push(content); /** Ammend TOC */ const { headings } = toHtml(getMds(code).map(md => dedent(md)).join('\n'), types.makeIframeId(this.entryPointIndex)); this.ammendTocWithHeadings(headings, types.makeIframeId(this.entryPointIndex)); /** Write out the data */ fse.outputFileSync(`${this.config.outputDir}/${this.config.subDirName}/data-${this.entryPointIndex}.js`, `var data = ${JSON.stringify(content)}`); /** Write the html */ fse.outputFileSync( `${this.config.outputDir}/${this.config.subDirName}/${htmlFileName}`, storyAndAppIndexTemplate({ index, jsFileName }) ); /** Bundle */ this._bundleCollector[jsFileNameNoExt] = entryPointPath; return this; } /** Adds a raw application demo */ app({ entryPointPath, sourceUrl, height, width, }: { entryPointPath: string, sourceUrl?: string, /** * Use this as the height for the demo embedding * If not specified we default to the iframe content scroll height **/ height?: string /** * If specified * - becomes the width of the demo * - disables responsive controls on the demo */ width?: string }) { this.entryPointIndex++; const index = this.entryPointIndex; const jsFileName = `app-${this.entryPointIndex}.js`; const jsFileNameNoExt = `app-${this.entryPointIndex}`; const htmlFileName = `app-${this.entryPointIndex}.html`; const content: types.AppContent = { type: 'app', index: this.entryPointIndex, htmlFileName, sources: [ { mode: 'js', code: fse.readFileSync(entryPointPath).toString() } ], sourceUrl, height: height, width: width, pageSubDirName: this.config.subDirName }; /** Write out the data */ const data = JSON.stringify(content); fse.outputFileSync(`${this.config.outputDir}/${this.config.subDirName}/data-${this.entryPointIndex}.js`, `var data = ${JSON.stringify(content)}`); /** Collect */ this._data.contents.push(content); /** Write the html */ fse.outputFileSync( `${this.config.outputDir}/${this.config.subDirName}/${htmlFileName}`, storyAndAppIndexTemplate({ index, jsFileName }) ); /** Bundle */ this._bundleCollector[jsFileNameNoExt] = entryPointPath; return this; } /** The end */ async _done() { /** * DESIGN Notes: * We write out an * - a data.js that contains our data object * - index.html file * - an application `app.js` that loads uses data.js to render the application */ /** Write the app html + js */ fse.outputFileSync( `${this.config.outputDir}/${this.config.subDirName}/index.html`, mainIndex({ title: this.config.title || "Docs" }) ); fse.writeFileSync( `${this.config.outputDir}/${this.config.subDirName}/app.js`, fse.readFileSync(__dirname + '/../../lib/app.js') ); /** * Await all builds only if there are any builds * e.g. the user might not have made any calls to `.app` | `.story` **/ if (Object.keys(this._bundleCollector).length) { await bundle({ entryMap: this._bundleCollector, outputDirName: this.config.outputDir + '/' + this.config.subDirName, ...(this.config.watch ? { watch: () => { // re-collect and write the `data` files for all stories // to ensure they get the latest code this._data.contents.forEach(d => { if (d.type !== 'story') return; const code = fse.readFileSync(d.entryPointPath).toString(); d.demoCodes = getDemoCodes(code).map( /** * Don't remove this lambda. * Otherwise `dedent` uses the `index` argument adding the number to output */ (c, _mustNotBePassedThrough) => dedent(c) ); /** Write out the data */ fse.outputFileSync(`${this.config.outputDir}/${this.config.subDirName}/data-${this.entryPointIndex}.js`, `var data = ${JSON.stringify(d)}`); }); // also call the parent this.config.watch(); } } : {}) }); } } }