Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: switch to react-component #166

Merged
merged 8 commits into from
May 6, 2021
Merged
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
node_modules
test/output
output
sample
coverage
11 changes: 11 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
*.swp
.github
.all-contributorsrc
.editorconfig
coverage
.nyc_output
scripts
sample
vscode
test
48 changes: 33 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,55 @@
# HTML template for the AsyncAPI Generator
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<h5 align="center">
<br>
<a href="https://www.asyncapi.org"><img src="https://github.com/asyncapi/parser-nodejs/raw/master/assets/logo.png" alt="AsyncAPI logo" width="200"></a>
<br>
HTML template for the AsyncAPI Generator
</h5>

HTML template for the [AsyncAPI Generator](https://github.com/asyncapi/generator) using an [AsyncAPI React Component](https://github.com/asyncapi/asyncapi-react) under the hood.

---

[![All Contributors][contributors]](#contributors-)
![npm](https://img.shields.io/npm/dm/@asyncapi/html-template?style=flat-square)

---

## Usage

```
```bash
ag asyncapi.yaml @asyncapi/html-template -o output
```

If you don't have the AsyncAPI Generator installed, you can install it like this:

```
```bash
npm install -g @asyncapi/generator
```

## Supported parameters

|Name|Description|Required|Default|Allowed values|Example|
| Name | Description | Required | Default | Allowed values | Example |
|---|---|---|---|---|---|
|sidebarOrganization|Defines how the sidebar should be organized. Set its value to `byTagsNoRoot` to categorize operations by operations tags. Set its value to `byTags` when you have tags on a root level. These tags are used to model tags navigation and need to have the same tags in operations.|No|undefined|`byTags`, `byTagsNoRoot`|`byTagsNoRoot`|
|baseHref|Sets the base URL for links and forms.|No|`/`|*Any*|`/docs`|
|version|Override the version of your application provided under `info.version` location in the specification file.|No|Version is taken from the spec file.|*Any* ([See Semver versionning](https://semver.org/))|`1.0.0`|
|singleFile|Set output into one html-file with styles and scripts inside|No|`false`|`true`,`false`|`true`|
|outFilename|The filename of the output file.|No|`index.html`|*Any*|`asyncapi.html`|
|pdf|Generates output HTML as PDF|No|`false`|`true,false`|`false`|
| sidebarOrganization | Defines how the sidebar should be organized. Set its value to `byTagsNoRoot` to categorize operations by operations tags. Set its value to `byTags` when you have tags on a root level. These tags are used to model tags navigation and need to have the same tags in operations. | No | undefined | `byTags`, `byTagsNoRoot` | `byTagsNoRoot` |
| baseHref | Sets the base URL for links and forms. | No | `/` | *Any* | `/docs` |
| version | Override the version of your application provided under `info.version` location in the specification file. | No | Version is taken from the spec file. | *Any* ([See Semver versionning](https://semver.org/)) | `1.0.0` |
| singleFile | Set output into one html-file with styles and scripts inside | No | `false` | `true`,`false` | `true` |
| outFilename | The filename of the output file. | No | `index.html` | *Any* | `asyncapi.html` |
| pdf | Generates output HTML as PDF | No | `false` | `true,false` | `false` |

If you only generate an html website, set the environment variable `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` to `true` and the generator will skip downloading chromium.
> **NOTE**: If you only generate an HTML website, set the environment variable `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD` to `true` and the generator will skip downloading chromium.

## Development

The HTML-Template is built with an [AsyncAPI React Component](https://github.com/asyncapi/asyncapi-react). For any changes regarding the styling of the page, rendering of the missing/existing elements, please contribute to the [AsyncAPI React Component](https://github.com/asyncapi/asyncapi-react) repository.

If you want make changes in template itself, please follow:

1. Make sure you have the latest generator installed: `npm install -g @asyncapi/generator`.
1. Modify the template or its helper functions.

>**NOTE:** If you have to modify the [`dummy.yml`](https://github.com/asyncapi/generator/blob/master/test/docs/dummy.yml) file to develop your features, open a PR with the changes in the [asyncapi/generator](https://github.com/asyncapi/generator) repository.

1. Adjust styling and generate `tailwind.min.css` with `npm run generate:assets`
1. Generate output with watcher enabled: `npm run develop`.

>**NOTE:** If your changes are not visible, this is maybe because the `ag` use the already installed `html-template` so you should use the `--install` option
Expand Down Expand Up @@ -76,4 +90,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

<!-- ALL-CONTRIBUTORS-LIST:END -->

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[contributors]: https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square 'All Contributors'
<!-- ALL-CONTRIBUTORS-BADGE:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
170 changes: 73 additions & 97 deletions filters/all.js
Original file line number Diff line number Diff line change
@@ -1,118 +1,94 @@
const filter = module.exports;
const fs = require('fs');
const path = require('path');
const React = require('react');
magicmatatjahu marked this conversation as resolved.
Show resolved Hide resolved
const ReactDOMServer = require('react-dom/server');
const AsyncApiUI = require('@asyncapi/react-component').default;

function isExpandable(obj) {
const fun = (obj) => typeof obj === "function";
if (
(fun(obj.type) && obj.type() === "object") ||
(fun(obj.type) && obj.type() === "array") ||
(fun(obj.oneOf) && obj.oneOf() && obj.oneOf().length) ||
(fun(obj.anyOf) && obj.anyOf() && obj.anyOf().length) ||
(fun(obj.allOf) && obj.allOf() && obj.allOf().length) ||
(fun(obj.items) && obj.items()) ||
(fun(obj.additionalItems) && obj.additionalItems()) ||
(fun(obj.properties) && obj.properties() && Object.keys(obj.properties()).length) ||
(fun(obj.additionalProperties) && obj.additionalProperties()) ||
(fun(obj.extensions) && obj.extensions() &&
Object.keys(obj.extensions()).filter(e => !e.startsWith("x-parser-")).length) ||
(fun(obj.patternProperties) && Object.keys(obj.patternProperties()).length)
) return true;
const filter = module.exports;

return false;
/**
* Prepares configuration for component.
*/
function prepareConfiguration(params = {}) {
const config = { show: { sidebar: true }, sidebar: { showOperations: 'byDefault' } };
if (params.sidebarOrganization === 'byTags') {
config.sidebar.showOperations = 'bySpecTags';
}
if (params.sidebarOrganization === 'byTagsNoRoot') {
config.sidebar.showOperations = 'byOperationsTags';
}
return config;
}
filter.isExpandable = isExpandable;

function nonParserExtensions(schema) {
if (!schema || !schema.extensions || typeof schema.extensions !== "function") return new Map();
const extensions = Object.entries(schema.extensions());
return new Map(extensions.filter(e => !e[0].startsWith("x-parser-")).filter(Boolean));
function replaceObject(val) {
return Object.entries(val).reduce((o, [propertyName, property]) => {
if (propertyName.startsWith('x-parser-')) {
o[propertyName] = property;
}
return o;
}, {});
}
filter.nonParserExtensions = nonParserExtensions;

/**
* Check if there is a channel which does not have one of the tags specified.
* Remove this function when it will be implemented https://github.com/asyncapi/parser-js/issues/266
*/
function containTags(object, tagsToCheck) {
if (!object) {
throw new Error("object for containsTag was not provided?");
}

if (!tagsToCheck) {
throw new Error("tagsToCheck for containsTag was not provided?");
}
function replaceCircular(val, cache) {
cache = cache || new WeakSet();

//Ensure if only 1 tag are provided it is converted to array.
if (tagsToCheck && !Array.isArray(tagsToCheck)) {
tagsToCheck = [tagsToCheck];
}
//Check if pubsub contain one of the tags to check.
let check = (tag) => {
let found = false;
for (let tagToCheckIndex in tagsToCheck) {
let tagToCheck = tagsToCheck[tagToCheckIndex]._json;
let tagName = tag.name;
if ((tagToCheck && tagToCheck.name === tagName) ||
tagsToCheck[tagToCheckIndex] === tagName) {
found = true;
if (val && typeof(val) == 'object') {
if (cache.has(val)) {
if (!Array.isArray(val)) {
return replaceObject(val);
}
return {};
}

cache.add(val);

const obj = (Array.isArray(val) ? [] : {});
for(var idx in val) {
obj[idx] = replaceCircular(val[idx], cache);
}
return found;
};

//Ensure tags are checked for the group tags
let containTags = object._json.tags ? object._json.tags.find(check) != null : false;
return containTags;
cache.delete(val);
return obj;
}
return val;
}
filter.containTags = containTags;

/**
* Check if there is a channel which does not have one of the tags specified.
* Stringifies the specification with escaping circular refs
* and annotates that specification is parsed.
*/
function containNoTag(channels, tagsToCheck) {
if (!channels) {
throw new Error("Channels for containNoTag was not provided?");
}
for (let channelIndex in channels) {
let channel = channels[channelIndex]._json;
//Check if the channel contains publish or subscribe which does not contain tags
if (channel.publish && (!channel.publish.tags || channel.publish.tags.length == 0) ||
channel.subscribe && (!channel.subscribe.tags || channel.subscribe.tags.length == 0)
) {
//one does not contain tags
return true;
}
function stringifySpec(asyncapi) {
asyncapi._json['x-parser-spec-parsed'] = true;
return JSON.stringify(replaceCircular(asyncapi.json()));
}
filter.stringifySpec = stringifySpec;

//Check if channel publish or subscribe does not contain one of the tags to check.
let check = (tag) => {
let found = false;
for (let tagToCheckIndex in tagsToCheck) {
let tagToCheck = tagsToCheck[tagToCheckIndex]._json;
let tagName = tag.name;
if ((typeof tagToCheck !== 'undefined' && tagToCheck.name === tagName) ||
tagsToCheck[tagToCheckIndex] === tagName) {
found = true;
}
}
return found;
};
/**
* More safe function to include content of given file than default Nunjuck's `include`.
* Attaches raw file's content instead of executing it - problem with some attached files in template.
*/
function includeFile(pathFile) {
const pathToFile = path.resolve(__dirname, '../', pathFile);
return fs.readFileSync(pathToFile);
}
filter.includeFile = includeFile;

//Ensure pubsub tags are checked for the group tags
let publishContainsNoTag = channel.publish && channel.publish.tags ? channel.publish.tags.find(check) == null : false;
if (publishContainsNoTag === true) return true;
let subscribeContainsNoTag = channel.subscribe && channel.subscribe.tags ? channel.subscribe.tags.find(check) == null : false;
if (subscribeContainsNoTag === true) return true;
}
return false;
/**
* Stringifies prepared configuration for component.
*/
function stringifyConfiguration(params) {
return JSON.stringify(prepareConfiguration(params));
}
filter.containNoTag = containNoTag;
filter.stringifyConfiguration = stringifyConfiguration;

function operationsTags(object) {
let tags = new Set();
const extractName = (tags, acc) => tags.forEach((tag) => acc.add(tag.name()));
object.channelNames().forEach(channelName => {
let channel = object.channel(channelName);
if (channel.hasPublish() && channel.publish().hasTags()) extractName(channel.publish().tags(), tags);
if (channel.hasSubscribe() && channel.subscribe().hasTags()) extractName(channel.subscribe().tags(), tags);
});
return Array.from(tags);
/**
* Renders AsyncApiUI component by given AsyncAPI spec and with corresponding template configuration.
*/
function renderSpec(asyncapi, params) {
const component = React.createElement(AsyncApiUI, { schema: asyncapi, config: prepareConfiguration(params) });
return ReactDOMServer.renderToString(component);
}
filter.operationsTags = operationsTags;
filter.renderSpec = renderSpec;
9 changes: 9 additions & 0 deletions hooks/00_changeSpecVersion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Changes the version of the specification by the `version` parameter.
*/
module.exports = {
'generate:before': ({ asyncapi, templateParams = {} }) => {
const version = templateParams.version || asyncapi.info().version();
asyncapi._json.info.version = version;
}
};
43 changes: 20 additions & 23 deletions hooks/01_removeNotRelevantParts.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
const fs = require('fs');
const path = require('path');
const rimraf = require('rimraf');

module.exports = {
'generate:after': generator => {

const params = generator.templateParams;
const singleFile = params.singleFile === 'true';

if (singleFile) {

const jsDir = path.resolve(generator.targetDir, 'js');
const cssDir = path.resolve(generator.targetDir, 'css');
const callback = (error) => {
if (error) {
throw error;
}
};

const callback = (error) => {
if (error) {
throw error;
}
};
/**
* Removes unnecessary files (css, js) if user pass `singleFile` parameter.
*/
module.exports = {
'generate:after': ({ templateParams = {}, targetDir }) => {
if (templateParams.singleFile === 'true') {
const jsDir = path.resolve(targetDir, 'js');
const cssDir = path.resolve(targetDir, 'css');

const opts = {
disableGlob: true,
maxBusyTries: 3
};
const opts = {
disableGlob: true,
maxBusyTries: 3
};

rimraf(jsDir, opts, callback);
rimraf(cssDir, opts, callback);
}
rimraf(jsDir, opts, callback);
rimraf(cssDir, opts, callback);
}
}
};
16 changes: 11 additions & 5 deletions hooks/02_renameOutFile.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
const fs = require('fs');

/**
* Renames out file's name to passed as `outFilename` parameter.
*/
module.exports = {
'generate:after': generator => {
if(generator.templateParams.outFilename !== 'index.html') {
fs.renameSync(`${generator.targetDir}/index.html`,
`${generator.targetDir}/${generator.templateParams.outFilename}`);
}
'generate:after': ({ templateParams = {}, targetDir }) => {
const outFilename = templateParams.outFilename;
if (outFilename && outFilename !== 'index.html') {
fs.renameSync(
`${targetDir}/index.html`,
`${targetDir}/${outFilename}`
);
}
}
}
Loading