theforeman/foreman

View on GitHub
webpack/assets/javascripts/react_app/components/Editor/EditorHelpers.js

Summary

Maintainability
B
6 hrs
Test Coverage
import {
  isEmpty,
  filter,
  find,
  replace,
  capitalize,
  forEach,
  concat,
  sortBy,
} from 'lodash';

import 'ace-builds/src-noconflict/ace';
import { setCompleters } from 'ace-builds/src-noconflict/ext-language_tools';

const resolveDefault = ({ default: defValue }) => {
  switch (typeof defValue) {
    case 'object':
      if (defValue === null) return 'nil';

      return '{}';
    case 'string':
      if (defValue === '') return '""';

      return `"${defValue}"`;
    default:
      return defValue;
  }
};

const methodReturns = ({ returns: { object: retObject } }) => {
  // TODO: add support for Hash return object with nested description
  // can be skipped for now since we don't use it yet and it wasn't tested properly
  if (retObject.class === 'Hash' && retObject.data.constructor === Array)
    return 'Object';
  const retMeta = capitalize(replace(retObject.meta, '_', ' '));
  const retData = replace(JSON.stringify(retObject.data), 'null', 'nil');
  const returns = retObject.data === null ? retObject.class : retData;
  return `${retMeta}: ${returns}`;
};

const methodTooltip = (method, isMacro) => {
  const usagePrefix = isMacro ? '' : 'obj.';
  const usage = `Usage:\n\t${usagePrefix}${methodSignature(method)}`;
  const returns = `Returns:\n\t${methodReturns(method)}`;
  if (isEmpty(method.examples))
    return [method.short_description, usage, returns].join('\n\n');

  let examples = method.examples
    .map(e => {
      const desc = e.desc ? `${e.desc}\n\n` : '';
      return `${desc}${e.example}`;
    })
    .join('\n\n');
  examples = `Examples:\n\n${examples}`;
  return [method.short_description, usage, returns, examples].join('\n\n');
};

const methodSignature = (method, isSnippet) => {
  if (isEmpty(method.params)) return method.name;

  let currParam = 1;
  const params = method.params.map(p => {
    switch (p.type) {
      case 'required':
        return isSnippet ? `\${${currParam++}:${p.name}}` : p.name;
      case 'optional': {
        if (p.expected_type === 'list') {
          return isSnippet
            ? `\${${currParam++}:first},\${${currParam++}:second}`
            : `*${p.name}`;
        }
        const defaultVal = resolveDefault(p);
        return isSnippet
          ? `\${${currParam++}:${defaultVal}}`
          : `${p.name} = ${defaultVal}`;
      }
      case 'keyword':
        return isSnippet
          ? `${p.name}: \${${currParam++}:${resolveDefault(p)}}`
          : `${p.name}: ${resolveDefault(p)}`;
      default:
        return null;
    }
  });
  const filtered = filter(params, p => p !== null).join(', ');
  const blockParam = find(method.params, p => p.type === 'block');
  const parts = [method.name];
  if (!isEmpty(method.params)) parts.push(`(${filtered})`);
  if (blockParam) parts.push(` ${blockParam.schema}`);
  return parts.join('');
};

export const parseDocs = cache => {
  // Do nothing if the editor is not being used for templates
  if (!cache) return;
  // Custom basic snippets
  /* eslint-disable */
  let completions = [
    {
      caption: '<%',
      snippet: '<% ${1:code} %>',
      tooltip: 'Executes code, but does not insert a value\n\n<% code %>',
      meta: 'erb',
    },
    {
      caption: '<%=',
      snippet: '<%= ${1:expression} %>',
      tooltip: 'Inserts the value of an expression\n\n<%= expression %>',
      meta: 'erb',
    },
    {
      caption: '<%-',
      snippet: '<% ${1:code} -%>',
      tooltip:
        'Executes code, but does not insert a value, trims the following line break\n\n<% code -%>',
      meta: 'erb',
    },
    {
      caption: '<%=-',
      snippet: '<%= ${1:expression} -%>',
      tooltip:
        'Inserts the value of an expression, trims the following line break\n\n<%= expression -%>',
      meta: 'erb',
    },
    {
      caption: '<%#',
      snippet: '<%# ${1:comment} -%>',
      tooltip:
        'Comment, removed from the final output, trims the following line break\n\n<%# comment -%>',
      meta: 'erb',
    },
  ];
  /* eslint-enable */
  // Parse JSON with DSL documentation
  forEach(JSON.parse(cache).docs.classes, cls => {
    const macros = cls.methods.map(method => ({
      caption: method.name,
      snippet: methodSignature(method, true),
      tooltip: methodTooltip(method, true),
      meta: 'macro',
    }));
    const props = cls.properties.map(prop => ({
      caption: `${cls.name}#${prop.name}`,
      snippet: methodSignature(prop, true),
      tooltip: methodTooltip(prop),
      meta: 'property',
    }));
    completions = concat(completions, macros, props);
  });
  const completer = {
    getDocTooltip: selected => selected.tooltip,
    getCompletions: (editor, session, pos, prefix, callback) =>
      callback(null, sortBy(completions, ['meta', 'caption'])),
  };

  setCompleters([completer]);
};