feat(): Compile markdown using dgeni

This commit is contained in:
Livio
2019-07-31 22:04:55 +02:00
parent 859af059e7
commit b8a8f176e0
48 changed files with 2439 additions and 424 deletions

1
.gitignore vendored
View File

@@ -125,3 +125,4 @@ src/app/homepage/pages/controllers/controllers.component.html
src/app/homepage/pages/pipes/pipes.component.html
src/app/homepage/pages/recipes/crud/crud.component.html
src/app/homepage/pages/recipes/serve-static/serve-static.component.html
src/app/homepage/pages/discover/who-uses/who-uses.component.html

View File

@@ -1,172 +0,0 @@
import * as chokidar from 'chokidar';
import * as fg from 'fast-glob';
import * as fs from 'fs';
import * as marked from 'marked';
import { join } from 'path';
import {
appendEmptyLine,
escapeBrackets,
insertText,
parseSwitcher,
replaceFilename,
} from './markdown-utils';
const renderer: any = new marked.Renderer();
const originalTableRenderer = renderer.table;
renderer.table = (header: string, body: string) =>
header.includes('<th></th>')
? originalTableRenderer.call(renderer, '', body)
: originalTableRenderer.call(renderer, header, body);
const originalCodeRenderer = renderer.code;
renderer.code = function(
code: string,
language: string,
isEscaped: boolean,
switcherKey?: string,
) {
const filenameKey = '@@filename';
const filenameIndex = code.indexOf(filenameKey);
if (filenameIndex >= 0) {
return replaceFilename(
(text, directiveRef) =>
renderer.code(text, language, isEscaped, directiveRef),
code,
filenameKey,
filenameIndex,
);
}
const switchKey = '@@switch';
const switchIndex = code.indexOf(switchKey);
if (switchIndex >= 0) {
const result = parseSwitcher(
(text, lang) => renderer.code(text, lang, isEscaped),
code,
switchKey,
switchIndex,
switcherKey,
);
return escapeBrackets(result);
}
let output: string = originalCodeRenderer.call(
renderer,
code,
language,
isEscaped,
);
output = switcherKey ? output : appendEmptyLine(output);
return escapeBrackets(output);
};
const originalLinkRenderer = renderer.link;
renderer.link = (href: string, title: string, text: string) => {
if (!href.includes('http') && !href.includes('mailto')) {
return (originalLinkRenderer.call(
renderer,
href,
title,
text,
) as string).replace('href', 'routerLink');
}
if (href.includes('http') && !href.includes('mailto')) {
let baseLink = (originalLinkRenderer.call(
renderer,
href,
title,
text
) as string);
baseLink = `${baseLink.substr(0, 2)} target="_blank"${baseLink.substr(2)}`;
return baseLink;
}
return originalLinkRenderer.call(renderer, href, title, text);
};
const originalHeadingRenderer = renderer.heading.bind(renderer);
renderer.heading = (...args: string[]) => {
let text = originalHeadingRenderer(...args);
if (!text.includes('h4')) {
return text;
}
const startIndex = text.indexOf('<h') + 3;
text = insertText(text, startIndex, ` appAnchor`);
text = insertText(text, text.indexOf('">') + 2, '<span>');
return insertText(text, text.length - 6, '</span>');
};
const originalBlockquoteRenderer = renderer.blockquote.bind(renderer);
renderer.blockquote = (quote: string) => {
let text: string = originalBlockquoteRenderer(quote);
text = text.replace('<p>', '');
text = text.replace('</p>', '');
const blockquoteTag = '<blockquote>';
text = text.replace('<blockquote>', '<blockquote');
text = insertText(text, blockquoteTag.length - 1, ` class="`);
text = insertText(text, text.indexOf('<strong>'), '">');
return text;
};
const rootDir = 'content';
function readAndCompile(path: string, done: (filename?: string) => void) {
fs.readFile(path, 'utf-8', (err, data) => {
if (err) {
console.error(err);
return done();
}
const html = `<div class="content" #contentReference>
<div class="github-links">
<a href="https://github.com/nestjs/docs.nestjs.com/edit/master/${path}" aria-label="Suggest Edits" title="Suggest Edits">
<i class="material-icons" aria-hidden="true" role="img">mode_edit</i>
</a>
</div>
${marked(data.toString(), { renderer })}
</div>`;
const distPath = path.slice(
path.indexOf(rootDir) + rootDir.length,
path.length - 3, // strip extension
);
const pathSegments = distPath.split('/');
const distFilename =
pathSegments[pathSegments.length - 1] + '.component.html';
fs.writeFileSync(
join(process.cwd(), `src/app/homepage/pages${distPath}/${distFilename}`),
html + '\n',
);
return done(distFilename);
});
}
const argv = require('yargs').argv;
if (argv.watch) {
const watcher = chokidar.watch(join(process.cwd(), rootDir), {
ignored: /(^|[\/\\])\../,
persistent: true,
});
console.log('# Markdown compiler is watching files.. (content dir)');
watcher.on('change', path => {
readAndCompile(path, (filename?: string) => {
filename &&
console.log(
`[${filename}] has been saved. (${new Date().toLocaleTimeString()})`,
);
});
});
} else {
fg(['content/**/*.md']).then(entries => {
entries.forEach(path =>
readAndCompile(path as string, (filename?: string) => {
filename &&
console.log(
`[${filename}] has been saved. (${new Date().toLocaleTimeString()})`,
);
}),
);
});
}

View File

@@ -0,0 +1,141 @@
{
"Header": [
{
"logo": "https://nestjs.com/img/valor-software.png",
"url": "https://valor-software.com/"
},
{
"logo": "/assets/logo/rewe.svg",
"url": "https://rewe-digital.com/",
"width": "180px"
},
{
"logo": "/assets/logo/roche-logo.png",
"url": "https://roche.com",
"width": "90px"
},
{
"logo": "/assets/logo/adidas.svg",
"url": "https://adidas.com",
"width": "80px"
},
{
"logo": "/assets/logo/capgemini.svg",
"url": "https://capgemini.com"
},
{
"logo": "https://nestjs.com/img/gojob-logo.png",
"url": "https://gojob.com/",
"width": "100px"
},
{
"logo": "https://nestjs.com/img/neoteric.png",
"url": "https://neoteric.eu/"
},
{
"logo": "/assets/logo/trellis.png",
"url": "https://trellis.org/",
"width": "70px"
},
{
"logo": "https://nestjs.com/img/scalio-logo.svg",
"url": "https://scal.io",
"width": "120px"
},
{
"logo": "https://nestjs.com/img/swingdev-logo.svg",
"url": "https://swingdev.io/"
},
{
"logo": "/assets/logo/cultura-colectiva.png",
"url": "https://culturacolectiva.com",
"width": "90px"
},
{
"logo": "/assets/logo/architectnow.png",
"url": "http://architectnow.net",
"width": "150px"
},
{
"logo": "/assets/logo/iflix.svg",
"url": "https://www.iflix.com",
"width": "70px"
},
{
"logo": "https://nestjs.com/img/genuinebee.svg",
"url": "https://genuinebee.com/"
},
{
"logo": "/assets/logo/run-players-league.png",
"url": "https://runplayersleague.com",
"width": "80px"
},
{
"logo": "/assets/logo/yanrongyun.svg",
"url": "http://www.yanrongyun.com",
"width": "120px"
},
{
"logo": "/assets/logo/sclable.svg",
"url": "https://sclable.com",
"width": "120px"
},
{
"logo": "/assets/logo/dozto.png",
"url": "https://www.dozto.com",
"width": "130px"
},
{
"logo": "/assets/logo/qingtui.png",
"url": "https://www.qingtui.cn/",
"width": "90px"
},
{
"logo": "/assets/logo/crowdlinker.svg",
"url": "https://crowdlinker.com/",
"width": "110px"
},
{
"logo": "/assets/logo/nothing.svg",
"url": "https://nothing.ch/",
"width": "110px"
}
],
"Body": [
"https://gorrion.io/",
"http://balticdatascience.com/",
"https://prohabits.com/",
"https://komed-health.com/",
"https://kerberus.com.co/",
"http://xtremis.com/",
"https://notadd.com/",
"http://jsdaddy.io/",
"https://yumpingo.com/",
"https://analytics-importer.cz/",
"https://dayzim.com/",
"https://wizkids.co.uk/",
"https://pilvia.com/",
"https://wi-q.com/",
"http://agrofel.com.br",
"https://societegenerale.com/",
"https://trashpanda.hulan.nl/",
"https://bytedance.com/",
"https://votercircle.com",
"https://erento.com",
"https://ideas.manticore-labs.com/",
"https://smartexlab.com/",
"https://automama.ru/",
"https://iflix.com/",
"https://framework.watch",
"https://mobilejazz.com/",
"https://cgiandi.com/",
"https://www.titlex.com.au/",
"https://codengage.com/",
"https://budacode.com/",
"https://blueanchor.io/",
"https://www.easymetrics.com/",
"https://getapollo.io/",
"https://big-bench.com/",
"https://www.qingtui.cn/"
]
}

View File

@@ -0,0 +1,8 @@
### Who is using Nest?
We are proudly helping various companies building their products at scale.
If you are using Nest and would you like to be listed here, see this [thread](https://github.com/nestjs/nest/issues/1006).
We are willing to put your logo here!
#### Companies
According to our knowledge, all the following companies have built awesome projects on top of our framework:

1728
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,17 +4,17 @@
"license": "MIT",
"scripts": {
"ng": "ng",
"prestart": "npm run markdown",
"prestart": "npm run docs",
"start": "ng serve --source-map=false",
"prebuild": "npm run markdown",
"prebuild": "npm run docs",
"build": "ng build --deleteOutputPath=false",
"prebuild:prod": "npm run markdown",
"prebuild:prod": "npm run docs",
"build:prod": "ng build --prod --aot --deleteOutputPath=false",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e",
"markdown": "ts-node -O \"{\\\"module\\\": \\\"commonjs\\\"}\" compiler/markdown-compiler.ts",
"markdown:watch": "ts-node -O \"{\\\"module\\\": \\\"commonjs\\\"}\" compiler/markdown-compiler.ts --watch"
"docs": "npm run docs-only",
"docs-only": "ts-node -P tools/tsconfig.json tools/dgeni-cli.ts tools/transforms/nestjs-package/index"
},
"private": true,
"dependencies": {
@@ -37,6 +37,8 @@
"@ultimate/ngxerrors": "1.4.0",
"angular2-highlight-js": "6.0.1-alpha",
"core-js": "2.6.9",
"dgeni": "0.4.12",
"dgeni-packages": "0.28.1",
"fast-glob": "3.0.4",
"hammerjs": "2.0.8",
"lodash": "4.17.15",
@@ -58,7 +60,9 @@
"@types/jasmine": "3.3.16",
"@types/jasminewd2": "2.0.6",
"@types/lodash": "4.14.136",
"@types/node": "12.6.8",
"@types/marked": "^0.6.5",
"@types/node": "^12.6.8",
"@types/rimraf": "^2.0.2",
"chokidar": "3.0.2",
"codelyzer": "5.1.0",
"concurrently": "4.1.1",
@@ -72,6 +76,7 @@
"karma-jasmine-html-reporter": "1.4.2",
"marked": "0.7.0",
"protractor": "6.0.0",
"rimraf": "^2.6.3",
"ts-node": "8.3.0",
"tslint": "5.18.0",
"typescript": "3.4.1",

View File

@@ -1,26 +0,0 @@
<div class="content">
<h3>Who is using Nest?</h3>
<p>
We are proudly helping various companies building their products at scale.
If you are using Nest and would you like to be listed here, see this <a href="https://github.com/nestjs/nest/issues/1006"
target="blank">thread</a>. We are willing to put your logo here!
</p>
<h4>Companies</h4>
<p>
According to our knowledge, all the following companies have built awesome projects on top of our framework:
</p>
<div class="companies-wrapper">
<a class="company-logo" *ngFor="let item of companies" [href]="item.url" target="blank"><img [src]="item.logo"
[style.width]="item.width" /></a>
</div>
<table class="companies-list">
<tr *ngFor="let item of companiesUrls">
<td><a [href]="item" target="blank">{{ item }}</a></td>
</tr>
</table>
<p>
and a lot more but we don't have enough time to update above list. Feel free to create a <a href="https://github.com/nestjs/docs.nestjs.com/pulls"
target="blank">pull
request</a> though!
</p>
</div>

19
tools/README.md Normal file
View File

@@ -0,0 +1,19 @@
# docs.nestjs.com project tooling
This document gives an overview of the tools we use to generate the content for the
docs.nestjs.com website.
# transforms
All the content that is rendered by the docs.nestjs.com application, and some of its
configuration files, are generated from source files by [Dgeni](https://github.com/angular/dgeni).
Dgeni is a general purpose documentation generation tool.
Markdown files in `content` are processed and transformed
into files that are consumed by the `docs.nestjs.com` web frontend.
# dgeni-cli
The dgeni CLI `tools/dgeni-cli.ts` is wrapper to start a Dgeni package from the command line.
We do not take use of the CLI interface provided by the Dgeni package itself, mainly because
it does not support TypeScript compilation on the fly.

23
tools/dgeni-cli.ts Normal file
View File

@@ -0,0 +1,23 @@
import { Dgeni } from 'dgeni';
import { resolve } from 'path';
const argv = require('yargs').argv;
const packagePaths = argv._;
const packages = packagePaths.map(packagePath => {
if (packagePath.indexOf('.') === 0) {
packagePath = resolve(packagePath);
}
return require('../' + packagePath).default;
});
new Dgeni(packages)
.generate()
.then(docs => {
console.log(`${docs.length} docs generated.`);
})
.catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,39 @@
# Transforms Overview
All the content that is rendered by the docs.nestjs.com application, and some of its
configuration files, are generated from source files by [Dgeni](https://github.com/angular/dgeni).
Dgeni is a general purpose documentation generation tool.
Markdown files in `content` are processed and transformed
into files that are consumed by the `docs.nestjs.com` web frontend.
## Packages
The documentation tool of NestJS is split into multiple Dgeni packages.
**nestjs-package**
The main package. Orchestrates all the following packages and sets
final configuration. It is responsible for cleaning up the file system.
**nestjs-base-package**
The base package for common configurations, services and processors for
each package. It handles the general input / output / template path resolution.
**nestjs-content-package**
Orchestrates all hand-written contents for the NestJS documentation.
It makes use of the `content`-folders markdown. On top of that
it takes care of the `content/**/*.json` files such as `content/discover/who-uses.json`.
**content-package**
A package to handle the markdown content files. It creates a new DocType `content`
which include a `content` and `title` of each markdown file.
The **nestjs-content-package** manages this content further.
## Templates
All the templates for the docs.nestjs.com dgeni transformations are stoted in the `tools/transforms/templates`
folder. See the [README](./templates/README.md).

View File

@@ -0,0 +1,10 @@
import { resolve } from 'path';
export const PROJECT_ROOT = resolve(__dirname, '../../');
export const CONTENTS_PATH = resolve(PROJECT_ROOT, 'content');
export const SRC_PATH = resolve(PROJECT_ROOT, 'src');
export const OUTPUT_PATH = resolve(SRC_PATH, 'app/homepage/pages');
export const DOCS_OUTPUT_PATH = resolve(OUTPUT_PATH, 'docs');
export const TEMPLATES_PATH = resolve(__dirname, './templates');

View File

@@ -0,0 +1,57 @@
import { Package } from 'dgeni';
import { ContentFileReader, contentFileReader } from './readers';
import {
extractContentTitleProcessor,
computeOutputPathProcessor,
computeWhoUsesProcessor
} from './processors';
import { renderNestJSMarkdown } from './services';
import { nestjsMarkedNunjucksTag } from './rendering/tags/nestjsmarked';
import { nestjsMarkedNunjucksFilter } from './rendering/filters/nestjsmarked';
import { Document } from '../shared';
export default new Package('content', [])
.factory(contentFileReader)
.factory(renderNestJSMarkdown)
.factory(nestjsMarkedNunjucksTag)
.factory(nestjsMarkedNunjucksFilter)
.processor(extractContentTitleProcessor)
.processor(computeOutputPathProcessor)
.processor(computeWhoUsesProcessor)
.config((readFilesProcessor: any, contentFileReader: ContentFileReader) => {
readFilesProcessor.fileReaders.push(contentFileReader);
})
.config(
(
templateEngine: any,
nestjsMarkedNunjucksTag: any,
nestjsMarkedNunjucksFilter: any
) => {
templateEngine.tags.push(nestjsMarkedNunjucksTag);
templateEngine.filters.push(nestjsMarkedNunjucksFilter);
}
)
.config(computeIdsProcessor => {
computeIdsProcessor.idTemplates.push({
docTypes: ['content', 'who-uses'],
getId: (doc: Document) => {
return (
doc.fileInfo.relativePath
// path should be relative to `modules` folder
.replace(/.*\/?modules\//, '')
// path should not include `/docs/`
.replace(/\/docs\//, '/')
// path should not have a suffix
.replace(/\.\w*$/, '')
);
},
getAliases: (doc: Document) => [doc.id]
});
});
export * from './readers';
export * from './processors';

View File

@@ -0,0 +1,35 @@
import { Processor } from 'dgeni';
import { join } from 'path';
import { Document } from '../../shared';
import { SRC_PATH } from '../../config';
export class ComputeOutputPathProcessor implements Processor {
$runBefore = ['writeFilesProcessor'];
$process(docs: Document[]) {
docs.forEach(doc => {
if (doc.docType === 'content' || doc.docType === 'who-uses') {
const filePath = doc.fileInfo.projectRelativePath;
const distPath = filePath.slice(
'content'.length + 1,
filePath.length - 3 // strip extension
);
const pathSegments = distPath.split('/');
const distFilename =
pathSegments[pathSegments.length - 1] + '.component.html';
doc.outputPath = join(
SRC_PATH,
'/app/homepage/pages',
distPath,
distFilename
);
}
});
}
}
export function computeOutputPathProcessor() {
return new ComputeOutputPathProcessor();
}

View File

@@ -0,0 +1,26 @@
import { Processor } from 'dgeni';
import { Document } from '../../shared';
export class ComputeWhoUsesProcessor implements Processor {
$runBefore = ['computePathsProcessor'];
$runAfter = ['readFilesProcessor'];
$process(docs: Document[]) {
let whoUsesJson;
docs.forEach((doc, index) => {
if (doc.docType === 'who-uses-json') {
whoUsesJson = doc.data;
docs.splice(index, 1);
}
});
docs.forEach(doc => {
if (doc.docType === 'who-uses') {
doc.id = 'who-uses';
doc.whoUses = whoUsesJson;
}
});
}
}
export function computeWhoUsesProcessor() {
return new ComputeWhoUsesProcessor();
}

View File

@@ -0,0 +1,30 @@
import { Processor, DocCollection } from 'dgeni';
/**
* Extracts the title of a content file.
* This processor assumes that the first line of the
* content includes the title.
*/
export class ExtractContentTitleProcessor implements Processor {
$runAfter = ['renderDocsProcessor'];
$runBefore = ['convertToJsonProcessor'];
$process(docs: DocCollection) {
docs.forEach(doc => {
if (doc.docType === 'content') {
try {
const firstLine: string = doc.content.split('\n')[0];
const title = firstLine.replace(/#/g, '').trim();
doc.title = title;
} catch (ex) {
// We do not care if the title does not exist here
// convertToJson will complain later in case the title
// does not exist
}
}
});
}
}
export function extractContentTitleProcessor() {
return new ExtractContentTitleProcessor();
}

View File

@@ -0,0 +1,3 @@
export * from './extractContentTitle';
export * from './computeOutputPath';
export * from './computeWhoUses';

View File

@@ -0,0 +1,23 @@
import { FileReader, FileInfo } from '../../shared';
/**
* Reads markdown content files
*/
export class ContentFileReader implements FileReader {
name = 'contentFileReader';
defaultPattern = /\.md$/;
getDocs(fileInfo: FileInfo) {
return [
{
docType: fileInfo.baseName === 'who-uses'
? 'who-uses'
: 'content',
content: fileInfo.content
}
];
}
}
export function contentFileReader() {
return new ContentFileReader();
}

View File

@@ -0,0 +1 @@
export * from './content';

View File

@@ -0,0 +1,11 @@
import { RenderNestJSMarkdown } from '../../services';
export function nestjsMarkedNunjucksFilter(renderNestJSMarkdown: RenderNestJSMarkdown) {
return {
name: 'nestjsmarked',
process(str: string) {
const output = str && renderNestJSMarkdown(str);
return output;
}
};
};

View File

@@ -0,0 +1,26 @@
import { RenderNestJSMarkdown } from '../../services';
export function nestjsMarkedNunjucksTag(renderNestJSMarkdown: RenderNestJSMarkdown) {
return {
tags: ['nestjsmarked'],
/** Disable autoescape for this tag because the markdown tag renders HTML that shouldn't be escaped. */
autoescape: false,
parse: function(parser: any, nodes: any) {
parser.advanceAfterBlockEnd();
var content = parser.parseUntilBlocks('endmarked');
var tag = new nodes.CallExtension(this, 'process', null, [content]);
parser.advanceAfterBlockEnd();
return tag;
},
process(_: any, content: () => string) {
const contentString = content();
const markedString = renderNestJSMarkdown(contentString);
return markedString;
}
};
}

View File

@@ -0,0 +1 @@
export * from './renderNestJSMarkdown';

View File

@@ -0,0 +1,23 @@
import * as marked from 'marked';
import {
applyTableRenderer,
applyCodeRenderer,
applyBlockQuoteRenderer,
applyHeadingRenderer,
applyLinkRenderer
} from './renderer';
export type RenderNestJSMarkdown = (content: string) => string;
export function renderNestJSMarkdown() {
const renderer = new marked.Renderer();
applyTableRenderer(renderer);
applyCodeRenderer(renderer);
applyLinkRenderer(renderer);
applyHeadingRenderer(renderer);
applyBlockQuoteRenderer(renderer);
// @ts-ignore
return (content: string) => marked(content, { renderer });
}

View File

@@ -0,0 +1,20 @@
import { Renderer } from 'marked';
import { insertText } from '../../../shared';
export function applyBlockQuoteRenderer(renderer: Renderer) {
const originalBlockquoteRenderer = renderer.blockquote.bind(renderer);
const blockquote = (quote: string) => {
let text: string = originalBlockquoteRenderer(quote);
text = text.replace('<p>', '');
text = text.replace('</p>', '');
const blockquoteTag = '<blockquote>';
text = text.replace('<blockquote>', '<blockquote');
text = insertText(text, blockquoteTag.length - 1, ` class="`);
text = insertText(text, text.indexOf('<strong>'), '">');
return text;
};
renderer.blockquote = blockquote;
}

View File

@@ -0,0 +1,51 @@
import { Renderer } from 'marked';
import {
replaceFilename,
parseSwitcher,
escapeBrackets,
appendEmptyLine
} from '../../../shared';
export function applyCodeRenderer(renderer: Renderer) {
const originalCodeRenderer = renderer.code;
renderer.code = function(
code: string,
language: string,
isEscaped: boolean,
switcherKey?: string
) {
const filenameKey = '@@filename';
const filenameIndex = code.indexOf(filenameKey);
if (filenameIndex >= 0) {
return replaceFilename(
(text, directiveRef) =>
// @ts-ignore
renderer.code(text, language, isEscaped, directiveRef),
code,
filenameKey,
filenameIndex
);
}
const switchKey = '@@switch';
const switchIndex = code.indexOf(switchKey);
if (switchIndex >= 0) {
const result = parseSwitcher(
(text, lang) => renderer.code(text, lang, isEscaped),
code,
switchKey,
switchIndex,
switcherKey
);
return escapeBrackets(result);
}
let output: string = originalCodeRenderer.call(
renderer,
code,
language,
isEscaped
);
output = switcherKey ? output : appendEmptyLine(output);
return escapeBrackets(output);
};
}

View File

@@ -0,0 +1,19 @@
import { Renderer } from 'marked';
import { insertText } from '../../../shared';
export function applyHeadingRenderer(renderer: Renderer) {
const originalHeadingRenderer = renderer.heading.bind(renderer);
const heading = (...args: any[]) => {
let text = originalHeadingRenderer(...args);
if (!text.includes('h4')) {
return text;
}
const startIndex = text.indexOf('<h') + 3;
text = insertText(text, startIndex, ` appAnchor`);
text = insertText(text, text.indexOf('">') + 2, '<span>');
return insertText(text, text.length - 6, '</span>');
};
renderer.heading = heading;
}

View File

@@ -0,0 +1,5 @@
export * from './code.renderer';
export * from './blockquote.renderer';
export * from './heading.renderer';
export * from './link.renderer';
export * from './table.renderer';

View File

@@ -0,0 +1,33 @@
import { Renderer } from 'marked';
export function applyLinkRenderer(renderer: Renderer) {
const originalLinkRenderer = renderer.link;
const link = (href: string, title: string, text: string) => {
if (!href.includes('http') && !href.includes('mailto')) {
return (originalLinkRenderer.call(
renderer,
href,
title,
text
) as string).replace('href', 'routerLink');
}
if (href.includes('http') && !href.includes('mailto')) {
let baseLink = originalLinkRenderer.call(
renderer,
href,
title,
text
) as string;
baseLink = `${baseLink.substr(0, 2)} target='_blank'${baseLink.substr(
2
)}`;
return baseLink;
}
return originalLinkRenderer.call(renderer, href, title, text);
};
renderer.link = link;
}

View File

@@ -0,0 +1,12 @@
import { Renderer } from 'marked';
export function applyTableRenderer(renderer: Renderer) {
const originalTableRenderer = renderer.table;
const table = (header: string, body: string) =>
header.includes('<th></th>')
? originalTableRenderer.call(renderer, '', body)
: originalTableRenderer.call(renderer, header, body);
renderer.table = table;
}

View File

@@ -0,0 +1,56 @@
import { Package } from 'dgeni';
import { relative } from 'path';
import * as nunjucksPackage from 'dgeni-packages/nunjucks';
import * as jsdocPackage from 'dgeni-packages/jsdoc';
import * as postProcessPackage from 'dgeni-packages/post-process-html';
import { PROJECT_ROOT, TEMPLATES_PATH, DOCS_OUTPUT_PATH } from '../config';
import { jsonFileReader, JsonFileReader } from './readers';
import { convertToJsonProcessor } from './processors';
export default new Package('nestjs-base', [
jsdocPackage,
nunjucksPackage,
postProcessPackage
])
.processor(convertToJsonProcessor)
.factory(jsonFileReader)
.config((readFilesProcessor: any, jsonFileReader: JsonFileReader) => {
readFilesProcessor.fileReaders.push(jsonFileReader);
readFilesProcessor.basePath = PROJECT_ROOT;
readFilesProcessor.sourceFiles = [];
})
.config(writeFilesProcessor => {
writeFilesProcessor.outputFolder = DOCS_OUTPUT_PATH;
})
.config(
(renderDocsProcessor: any, templateFinder: any, templateEngine: any) => {
// Where to find the templates for the doc rendering
templateFinder.templateFolders = [TEMPLATES_PATH];
// Standard patterns for matching docs to templates
templateFinder.templatePatterns = [
'${ doc.template }',
'${ doc.id }.${ doc.docType }.template.html',
'${ doc.id }.template.html',
'${ doc.docType }.template.html',
'${ doc.id }.${ doc.docType }.template.js',
'${ doc.id }.template.js',
'${ doc.docType }.template.js',
'${ doc.id }.${ doc.docType }.template.json',
'${ doc.id }.template.json',
'${ doc.docType }.template.json',
'common.template.html'
];
templateEngine.config.tags = { variableStart: '{$', variableEnd: '$}' };
// helpers are made available to the nunjucks templates
renderDocsProcessor.helpers.relativePath = (from, to) =>
relative(from, to);
}
);
export * from './processors';
export * from './readers';

View File

@@ -0,0 +1,44 @@
import { Processor, DocCollection } from 'dgeni';
import { CreateDocMessage, DgeniLogger } from '../../shared';
/**
* Converts doc types set by the `docTypes` property to JSON documents.
* A document requires `doc.renderContent` and `doc.title` or `doc.name`
* property.
*/
export class ConvertToJsonProcessor implements Processor {
constructor(private log: DgeniLogger, private createDocMessage: CreateDocMessage) {}
$runAfter = ['postProcessHtml'];
$runBefore = ['writeFilesProcessor'];
docTypes = [];
$process(docs: DocCollection) {
const docTypes = this.docTypes;
docs.forEach((doc: any) => {
if (docTypes.includes(doc.docType)) {
let contents = doc.renderedContent || '';
let title = doc.title;
if (title === undefined) {
title = doc.name;
}
// If there is still no title then log a warning
if (title === undefined) {
title = '';
this.log.warn(this.createDocMessage('Title property expected', doc));
}
doc.renderedContent = JSON.stringify(
{ id: doc.path, title, contents },
null,
2
);
}
});
}
}
export function convertToJsonProcessor(log: any, createDocMessage: any) {
return new ConvertToJsonProcessor(log, createDocMessage);
}

View File

@@ -0,0 +1 @@
export * from './convertToJson';

View File

@@ -0,0 +1 @@
export * from './json';

View File

@@ -0,0 +1,20 @@
import { FileReader, FileInfo } from '../../shared';
export class JsonFileReader implements FileReader {
name = 'jsonFileReader';
getDocs(fileInfo: FileInfo) {
return [
{
docType: fileInfo.baseName + '-json',
data: JSON.parse(fileInfo.content),
template: 'json-doc.template.json',
id: fileInfo.baseName,
aliases: [fileInfo.baseName, fileInfo.relativePath]
}
];
}
}
export function jsonFileReader() {
return new JsonFileReader();
}

View File

@@ -0,0 +1,53 @@
import { Package } from 'dgeni';
import { CONTENTS_PATH } from '../config';
import nestjsBasePackage, {
ConvertToJsonProcessor
} from '../nestjs-base-package';
import contentPackage, { contentFileReader } from '../content-package';
export default new Package('nestjs-content', [
nestjsBasePackage,
contentPackage
])
.config((readFilesProcessor: any) => {
readFilesProcessor.sourceFiles = readFilesProcessor.sourceFiles.concat([
{
basePath: CONTENTS_PATH,
include: CONTENTS_PATH + '/**/*.md',
fileReader: contentFileReader.name
},
{
basePath: CONTENTS_PATH,
include: CONTENTS_PATH + '/navigation.json',
fileReader: 'jsonFileReader'
},
{
basePath: CONTENTS_PATH,
include: CONTENTS_PATH + '/discover/who-uses.json',
fileReader: 'jsonFileReader'
}
]);
})
.config((computePathsProcessor: any) => {
// Replace any path templates inherited from other packages
// (we want full and transparent control)
computePathsProcessor.pathTemplates = computePathsProcessor.pathTemplates.concat(
[
{
docTypes: ['content', 'who-uses'],
getPath: doc => `${doc.id.replace(/\/index$/, '')}`,
outputPathTemplate: '${path}.json'
},
{
docTypes: ['who-uses-json'],
pathTemplate: '${id}',
outputPathTemplate: '../${id}.json'
}
]
);
})
// .config((convertToJsonProcessor: ConvertToJsonProcessor) => {
// convertToJsonProcessor.docTypes.push('content');
// });

View File

@@ -0,0 +1,7 @@
import { Package } from 'dgeni';
import nestjsContentPackage from '../nestjs-content-package';
import nestjsBasePackage from '../nestjs-base-package';
import { cleanGeneratedFiles } from './processors';
export default new Package('nestjs', [nestjsContentPackage, nestjsBasePackage])
.processor(cleanGeneratedFiles)

View File

@@ -0,0 +1,18 @@
import * as rimraf from 'rimraf';
import { Processor } from 'dgeni';
import { OUTPUT_PATH } from '../../config';
/**
* Removes the generated assets
*/
export class CleanGeneratedFiles implements Processor {
$runAfter = ['writing-files'];
$runBefore = ['writeFilesProcessor'];
$process() {
rimraf.sync(`${OUTPUT_PATH}/{docs,*.json}`);
}
}
export function cleanGeneratedFiles() {
return new CleanGeneratedFiles();
}

View File

@@ -0,0 +1 @@
export * from './cleanGeneratedFiles';

View File

@@ -0,0 +1,2 @@
export * from './interfaces';
export * from './utils';

View File

@@ -0,0 +1,31 @@
export interface Document {
docType: string;
fileInfo?: FileInfo;
outputPath?: string;
[any: string]: any;
}
export interface FileInfo {
basePath: string;
baseName: string;
extension: string;
filePath: string;
fileReader: string;
projectRelativePath: string;
content: string;
relativePath: string;
}
export interface FileReader {
name: string;
defaultPattern?: any;
getDocs: (fileInfo: FileInfo) => Document[];
}
export type CreateDocMessage = (message: string, document: Document) => string;
export interface DgeniLogger {
warn(message: string): void;
log(message: string): void;
error(message: string): void;
}

View File

@@ -0,0 +1 @@
export * from './dgeni-helpers';

View File

@@ -0,0 +1 @@
export * from './markdown-utils';

View File

@@ -0,0 +1,6 @@
# Dgeni Templates
This folder contains the dgeni templates that are used to generate the API and content docs
Generally there is a template for each docType. Templates can extend and/or include
other templates. Templates can also import macros from other template files.

View File

@@ -0,0 +1,14 @@
{% block content %}
<div class="content" #contentReference>
<div class="github-links">
<a
href="https://github.com/nestjs/docs.nestjs.com/edit/master/{$ doc.fileInfo.projectRelativePath $}"
aria-label="Suggest Edits"
title="Suggest Edits"
>
<i class="material-icons" aria-hidden="true" role="img">mode_edit</i>
</a>
</div>
{$ doc.description | nestjsmarked $}
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
{$ doc.data | json $}

View File

@@ -0,0 +1,25 @@
{% block content %}
<div class="content">
{$ doc.description | nestjsmarked $}
</div>
<div class="companies-wrapper">
{% for company in doc.whoUses.Header %}
<a class="company-logo" href="{$ company.url $}" target="blank"><img src="{$ company.logo $}"
{% if company.width %} style="width: {$ company.width $}" {% endif %} /></a>
{% endfor %}
</div>
<table class="companies-list">
{% for company in doc.whoUses.Body %}
<tr>
<td><a href="{$ company $}" target="blank">{$ company $}</a></td>
</tr>
{% endfor %}
</table>
<p>
and a lot more but we don't have enough time to update above list. Feel free to create a <a href="https://github.com/nestjs/docs.nestjs.com/pulls"
target="blank">pull
request</a> though!
</p>
{% endblock %}

16
tools/tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compileOnSave": false,
"compilerOptions": {
"declaration": false,
"allowUnusedLabels": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"target": "es5",
"lib": ["esnext"],
"baseUrl": "./"
}
}

View File

@@ -2,6 +2,11 @@
"rulesDirectory": [
"node_modules/codelyzer"
],
"linterOptions": {
"exclude": [
"tools/transforms/**/*.ts"
]
},
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,