import * as React from 'react'; import './render-plugin-doc.scss'; import { PluginContentType } from '../../../src'; class PluginOption { name: string; description: string[]; type: string; required: boolean; default?: string | number | boolean; aliases?: string[]; suboptions?: PluginOption[]; } class PluginDoc { short_description: string; description: string[]; options?: PluginOption[]; requirements?: string[]; notes?: string[]; deprecated?: { removed_in?: string; alternative?: string; why?: string; }; } class ReturnedValue { name: string; description: string[]; returned: string; type: string; // if string: display the value, if object or list return JSON sample: any; contains: ReturnedValue[]; } // Documentation for module doc string spec // https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html interface IState { renderError: boolean; } interface IProps { plugin: PluginContentType; renderModuleLink: (moduleName: string) => React.ReactElement; renderDocLink: (name: string, href: string) => React.ReactElement; renderTableOfContentsLink: ( title: string, section: string, ) => React.ReactElement; renderWarning: (text: string) => React.ReactElement; } export class RenderPluginDoc extends React.Component { // checks if I(), B(), M(), U(), L(), or C() exists. Returns type (ex: B) // and value in parenthesis. Based off of formatters in ansible: // https://github.com/ansible/ansible/blob/devel/hacking/build_library/build_ansible/jinja2/filters.py#L26 CUSTOM_FORMATTERS = /([IBMULC])\(([^)]+)\)/gm; subOptionsMaxDepth: number; returnContainMaxDepth: number; constructor(props) { super(props); this.state = { renderError: false, }; } componentDidCatch(error, info) { console.log(error); this.setState({ renderError: true }); } render() { const { plugin } = this.props; if (!this.state.renderError) { // componentDidCatch doesn't seem to be able to catch errors that // are thrown outside of return(), so we'll wrap everything in a // try just in case let doc: PluginDoc; let example: string; let returnVals: ReturnedValue[]; let content: any; try { doc = this.parseDocString(plugin); example = this.parseExamples(plugin); returnVals = this.parseReturn(plugin); content = { synopsis: this.renderSynopsis(doc), parameters: this.renderParameters( doc.options, plugin.content_type, this.subOptionsMaxDepth, ), notes: this.renderNotes(doc), examples: this.renderExample(example), returnValues: this.renderReturnValues( returnVals, this.returnContainMaxDepth, ), shortDescription: this.renderShortDescription(doc), deprecated: this.renderDeprecated(doc, plugin.content_name), requirements: this.renderRequirements(doc), }; } catch (err) { console.log(err); return this.renderError(plugin); } return (

{plugin.content_type} > {plugin.content_name}


{content.shortDescription} {content.deprecated} {this.renderTableOfContents(content)} {content.synopsis} {content.requirements} {content.parameters} {content.notes} {content.examples} {content.returnValues}
); } else { return this.renderError(plugin); } } private renderError(plugin) { // There's a good chance that something about the plugin doc data will // be malformed since it isn't validated. When that hapens, show an // error instead of crashing the whole app return ( {this.props.renderWarning( 'Documentation Syntax Error: cannot parse plugin documention.', )}
{plugin.content_type && plugin.content_name ? (

{plugin.content_type} > {plugin.content_name}

) : null}

The documentation object for this plugin seems to contain invalid syntax that makes it impossible for Automation Hub to parse. You can still look at the unformatted documentation object bellow if you need to.

Unformatted Documentation

                        {JSON.stringify(plugin, null, 2)}
                    
); } private parseDocString(plugin: PluginContentType): PluginDoc { // TODO: if the parser can't figure out what to do with the object // passed to it, it should throw an error that can be displayed to the // user with the piece of the documention that's broken. // TODO: make the doc string match the desired output as closely as // possible if (!plugin.doc_strings) { return { description: [], short_description: '' } as PluginDoc; } const doc: PluginDoc = { ...plugin.doc_strings.doc }; let maxDepth = 0; const parseOptions = (options: PluginOption[], depth) => { if (depth > maxDepth) { maxDepth = depth; } for (let op of options) { // Description is expected to be an array of strings. If its not, // do what we can to make it one op.description = this.ensureListofStrings(op.description); if (typeof op.default === 'object') { op.default = JSON.stringify(op.default); } // recursively parse sub options if (op.suboptions) { parseOptions(op.suboptions, depth + 1); } } }; if (doc.options) { parseOptions(doc.options, 0); } doc.description = this.ensureListofStrings(doc.description); this.subOptionsMaxDepth = maxDepth; return doc; } private parseExamples(plugin: PluginContentType): string { if (!plugin.doc_strings) { return null; } if (typeof plugin.doc_strings.examples === 'string') { // the examples always seem to have an annoying new line at the top // so just replace it here if it exists. return plugin.doc_strings.examples.replace('\n', ''); } else { return null; } } private parseReturn(plugin: PluginContentType): ReturnedValue[] { // TODO: make the return string match the desired output as closely as // possible if (!plugin.doc_strings) { return null; } if (!plugin.doc_strings.return) { return null; } let maxDepth = 0; const parseReturnRecursive = (returnV: ReturnedValue[], depth) => { if (depth > maxDepth) { maxDepth = depth; } for (let ret of returnV) { // Description is expected to be an array of strings. If its not, // do what we can to make it one ret.description = this.ensureListofStrings(ret.description); // recursively parse sub options if (ret.contains) { parseReturnRecursive(ret.contains, depth + 1); } } }; const returnValues = [...plugin.doc_strings.return]; parseReturnRecursive(returnValues, 0); this.returnContainMaxDepth = maxDepth; return returnValues; } // This functions similar to how string.replace() works, except it returns // a react object instead of a string private reactReplace( text: string, reg: RegExp, replacement: (matches: string[]) => React.ReactNode, ): React.ReactNode { const fragments = []; let match: string[]; let prevIndex = 0; while ((match = reg.exec(text)) !== null) { fragments.push( text.substr( prevIndex, reg.lastIndex - prevIndex - match[0].length, ), ); fragments.push(replacement(match)); prevIndex = reg.lastIndex; } if (fragments.length === 0) { return {text}; } // append any text after the last match if (prevIndex != text.length - 1) { fragments.push(text.substring(prevIndex)); } return ( {fragments.map((x, i) => ( {x} ))} ); } private applyDocFormatters(text: string): React.ReactNode { const { renderModuleLink, renderDocLink } = this.props; const nstring = this.reactReplace( text, this.CUSTOM_FORMATTERS, match => { const fullMatch = match[0]; const type = match[1]; const textMatch = match[2]; switch (type) { case 'L': const url = textMatch.split(','); return renderDocLink(url[0], url[1]); case 'U': return ( {textMatch} ); case 'I': return {textMatch}; case 'C': return {textMatch}; case 'M': return renderModuleLink(textMatch); case 'B': return {textMatch}; default: return fullMatch; } }, ); return nstring; } private ensureListofStrings(v) { if (typeof v === 'string') { return [v]; } else if (!v) { return []; } else { return v; } } private renderDeprecated(doc: PluginDoc, pluginName: string) { const isDeprecated = doc.deprecated || pluginName.startsWith('_'); if (!isDeprecated) { return null; } const deprecated = doc.deprecated || {}; return (

DEPRECATED

{deprecated.removed_in ? (
Removed in version {doc.deprecated.removed_in}
) : null}
Why: {deprecated.why ? doc.deprecated.why : 'No reason specified.'}
Alternative: {deprecated.alternative ? doc.deprecated.alternative : 'No alternatives specified.'}
); } private renderTableOfContents(content: any) { // return this.props.renderTableOfContentsLink('Synopsis', 'synopsis'); return ( ); } private renderShortDescription(doc: PluginDoc) { return
{doc.short_description}
; } private renderSynopsis(doc: PluginDoc) { return (

Synopsis

); } private renderParameters( parameters: PluginOption[], content_type: string, maxDepth: number, ) { if (!parameters) { return null; } // render the entries first, const paramEntries = this.renderParameterEntries( parameters, content_type, 0, maxDepth, '', ); return (

Parameters

{content_type !== 'module' ? ( ) : null} { // TODO: add support for sub options. Example: //https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/network/fortios/fortios_dlp_fp_doc_source.py#L93} // TODO: do we need to display version added? } {paramEntries}
Parameter Choices/ Defaults ConfigurationComments
); } private renderParameterEntries( parameters: PluginOption[], content_type: string, depth: number, maxDepth: number, parent: string, ) { let output = []; parameters.forEach((option, i) => { const spacers = []; const key = `${parent}-${option.name}`; for (let x = 0; x < depth; x++) { spacers.push(); } output.push( { // PARAMETER -------------------------------- } {spacers} {option.name} {this.documentedType(option['type'])} {option['elements'] ? ( {' '} / elements = {this.documentedType(option['elements'])} ) : null} {option['required'] ? ( {' '} / required ) : null} { // CHOICES ------------------------------- } {this.renderChoices(option)} { // CONFIGURATION (non module only) ------- } {content_type !== 'module' ? ( {this.renderPluginConfiguration(option)} ) : null} { // COMMENTS ------------------------------ } {option.description.map((d, i) => (

{this.applyDocFormatters(d)}

))} {option['aliases'] ? ( aliases: {option['aliases'].join(', ')} ) : null} , ); // recursively render sub options if (option.suboptions) { output = output.concat( this.renderParameterEntries( option.suboptions, content_type, depth + 1, maxDepth, key, ), ); } }); return output; } private renderPluginConfiguration(option) { return ( {option['ini'] ? (
ini entries: {option['ini'].map((v, i) => (

[{v.section}]
{v.key} = {v.default ? v.default : 'VALUE'}

))}
) : null} {option['env'] ? (
{option['env'].map((v, i) => (
env: {v.name}
))}
) : null} {option['vars'] ? (
{option['vars'].map((v, i) => (
var: {v.name}
))}
) : null}
); } private renderChoices(option) { let choices, defaul; if (option['type'] === 'bool') { choices = ['no', 'yes']; if (option['default'] === true) { defaul = 'yes'; } else if (option['default'] === false) { defaul = 'no'; } } else { choices = option['choices'] || []; defaul = option['default']; } return ( {choices && Array.isArray(choices) && choices.length !== 0 ? (
Choices:
    {choices.map((c, i) => (
  • {c === defaul ? ( {c}  ← ) : ( c )}
  • ))}
) : null} {defaul && !choices.includes(defaul) ? ( Default: {defaul} ) : null}
); } private renderNotes(doc: PluginDoc) { if (!doc.notes) { return null; } return (

Notes

); } private renderRequirements(doc: PluginDoc) { if (!doc.requirements) { return null; } return (

Requirements

); } private renderExample(example: string) { if (!example) { return null; } return (

Examples

{example}
); } private renderReturnValues(returnV: ReturnedValue[], maxDepth: number) { if (!returnV) { return null; } return (

Return Values

{this.renderReturnValueEntries( returnV, 0, maxDepth, '', )}
Key Returned Description
); } private renderReturnValueEntries( returnValues: ReturnedValue[], depth: number, maxDepth: number, parent: string, ) { let entries = []; returnValues.forEach((option, i) => { const spacers = []; for (let x = 0; x < depth; x++) { spacers.push(); } const key = `${parent}-${option.name}`; entries.push( {spacers} {option.name}
({option.type}) {option.returned} {option.description.map((d, i) => (

{this.applyDocFormatters(d)}

))} {option.sample ? (

sample: {typeof option.sample === 'string' ? ( option.sample ) : (
                                        {JSON.stringify(option.sample, null, 2)}
                                    
)}
) : null} , ); if (option.contains) { entries = entries.concat( // recursively render values this.renderReturnValueEntries( option.contains, depth + 1, maxDepth, key, ), ); } }); return entries; } // https://github.com/ansible/ansible/blob/1b8aa798df6f6fa96ba5ea2a9dbf01b3f1de555c/hacking/build_library/build_ansible/jinja2/filters.py#L53 private documentedType(text) { switch (text) { case 'str': return 'string'; case 'bool': return 'boolean'; case 'int': return 'integer'; case 'dict': return 'dictionary'; case undefined: return '-'; default: return text; } } }