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: ``
});
}
/**
* 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();
}
} : {})
});
}
}
}