Expose code & docblocks to the api explorer
authorColeman Watts <coleman@civicrm.org>
Thu, 5 Feb 2015 15:49:55 +0000 (10:49 -0500)
committerColeman Watts <coleman@civicrm.org>
Thu, 5 Feb 2015 16:35:57 +0000 (11:35 -0500)
CRM/Admin/Page/APIExplorer.php
CRM/Core/xml/Menu/Misc.xml
templates/CRM/Admin/Page/APIExplorer.js
templates/CRM/Admin/Page/APIExplorer.tpl

index 79a7bacebea0347c4061837fd753ee91825388d1..b63f7dd2dca0926c877e7e10cd353c724eb441cd 100644 (file)
@@ -42,7 +42,6 @@ class CRM_Admin_Page_APIExplorer extends CRM_Core_Page {
    * @return string
    */
   public function run() {
-    CRM_Utils_System::setTitle('CiviCRM API');
     CRM_Core_Resources::singleton()
       ->addScriptFile('civicrm', 'templates/CRM/Admin/Page/APIExplorer.js')
       ->addScriptUrl('//cdnjs.cloudflare.com/ajax/libs/prettify/r298/prettify.min.js', 99)
@@ -98,6 +97,117 @@ class CRM_Admin_Page_APIExplorer extends CRM_Core_Page {
       }
       CRM_Utils_System::civiExit();
     }
+    CRM_Utils_System::permissionDenied();
+  }
+
+  /**
+   * Ajax callback to display code docs
+   */
+  public static function getDoc() {
+    if (!empty($_GET['entity']) && strpos($_GET['entity'], '.') === FALSE) {
+      $entity = _civicrm_api_get_camel_name($_GET['entity']);
+      $action = CRM_Utils_Array::value('action', $_GET);
+      $doc = self::getDocblock($entity, $action);
+      $result = array(
+        'doc' => $doc ? self::formatDocBlock($doc[0]) : 'Not found.',
+        'code' => $doc ? $doc[1] : NULL,
+      );
+      if (!$action) {
+        $actions = civicrm_api3($entity, 'getactions');
+        $result['actions'] = CRM_Utils_Array::makeNonAssociative(array_combine($actions['values'], $actions['values']));
+      }
+      CRM_Utils_JSON::output($result);
+    }
+    CRM_Utils_System::permissionDenied();
+  }
+
+  /**
+   * @param string $entity
+   * @param string|null $action
+   * @return array|bool
+   *   [docblock, code]
+   */
+  private static function getDocBlock($entity, $action) {
+    if (!$entity) {
+      return FALSE;
+    }
+    $contents = file_get_contents("api/v3/$entity.php", FILE_USE_INCLUDE_PATH);
+    if (!$contents) {
+      // Api does not exist
+      return FALSE;
+    }
+    $docblock = $code = array();
+    // Fetch docblock for the api file
+    if (!$action) {
+      if (preg_match('#/\*\*\n.*?\n \*/\n#s', $contents, $docblock)) {
+        return array($docblock[0], NULL);
+      }
+    }
+    // Fetch block for a specific action
+    else {
+      $action = strtolower($action);
+      $fnName = 'civicrm_api3_' . _civicrm_api_get_entity_name_from_camel($entity) . '_' . $action;
+      // Support the alternate "1 file per action" structure
+      $actionFile = file_get_contents("api/v3/$entity/" . ucfirst($action) . '.php', FILE_USE_INCLUDE_PATH);
+      if ($actionFile) {
+        $contents = $actionFile;
+      }
+      // If action isn't in this file, try generic
+      if (strpos($contents, $fnName) === FALSE) {
+        $fnName = "civicrm_api3_generic_$action";
+        $contents = file_get_contents("api/v3/Generic/" . ucfirst($action) . '.php', FILE_USE_INCLUDE_PATH);
+        if (!$contents) {
+          $contents = file_get_contents("api/v3/Generic.php", FILE_USE_INCLUDE_PATH);
+        }
+      }
+      if (preg_match('#(/\*\*(\n \*.*)*\n \*/\n)function[ ]+' . $fnName . '#i', $contents, $docblock)) {
+        // Fetch the code in a separate regex to preserve sanity
+        preg_match("#^function[ ]+$fnName.*?^}#ism", $contents, $code);
+        return array($docblock[1], $code[0]);
+      }
+    }
+  }
+
+  /**
+   * Format a docblock to be a bit more readable
+   * Not a proper doc parser... patches welcome :)
+   *
+   * @param string $text
+   * @return string
+   */
+  private static function formatDocBlock($text) {
+    // Get rid of comment stars
+    $text = str_replace(array("\n * ", "\n *\n", "\n */\n", "/**\n"), array("\n", "\n\n", '', ''), $text);
+
+    // Format for html
+    $text = htmlspecialchars($text);
+
+    // Extract code blocks - save for later to skip html conversion
+    $code = array();
+    preg_match_all('#@code(.*?)@endcode#is', $text, $code);
+    $text = preg_replace('#@code.*?@endcode#is', '<pre></pre>', $text);
+
+    // Convert @annotations to titles
+    $text = preg_replace_callback(
+      '#^[ ]*@(\w+)([ ]*)#m',
+      function($matches) {
+        return "<strong>" . ucfirst($matches[1]) . "</strong>" . (empty($matches[2]) ? '' : ': ');
+      },
+      $text);
+
+    // Preserve indentation
+    $text = str_replace("\n ", "\n&nbsp;&nbsp;&nbsp;&nbsp;", $text);
+
+    // Convert newlines
+    $text = nl2br($text);
+
+    // Add unformatted code blocks back in
+    if ($code && !empty($code[1])) {
+      foreach ($code[1] as $block) {
+        $text = preg_replace('#<pre></pre>#', "<pre class='prettyprint'>$block</pre>", $text, 1);
+      }
+    }
+    return $text;
   }
 
 }
index 2fab624cfa0d300543e9b5d0ee1c636e11eb6b8e..8de633f0808a93a3775001c2398a22a0bd648fbf 100644 (file)
      <page_callback>CRM_Admin_Page_APIExplorer::getExampleFile</page_callback>
      <access_arguments>access CiviCRM</access_arguments>
   </item>
+  <item>
+     <path>civicrm/ajax/apidoc</path>
+     <page_callback>CRM_Admin_Page_APIExplorer::getDoc</page_callback>
+     <access_arguments>access CiviCRM</access_arguments>
+  </item>
   <item>
      <path>civicrm/ajax/rest</path>
      <page_callback>CRM_Utils_REST::ajax</page_callback>
index e8c8119cf98548391bf2115475a827bbf83ab1f0..b071b54a261ef0c6a89445bb7467200616451336 100644 (file)
@@ -14,6 +14,7 @@
     optionsTpl = _.template($('#api-options-tpl').html()),
     returnTpl = _.template($('#api-return-tpl').html()),
     chainTpl = _.template($('#api-chain-tpl').html()),
+    docCodeTpl = _.template($('#doc-code-tpl').html()),
 
     // These types of entityRef don't require any input to open
     OPEN_IMMEDIATELY = ['RelationshipType', 'Event', 'Group', 'Tag'],
     }
   }
 
+  /**
+   * Fetch entity docs & actions
+   */
+  function getDocEntity() {
+    CRM.utils.setOptions($('#doc-action').prop('disabled', true).addClass('loading'), []);
+    $.getJSON(CRM.url('civicrm/ajax/apidoc', {entity: $(this).val()}))
+      .done(function(result) {
+        CRM.utils.setOptions($('#doc-action').prop('disabled', false).removeClass('loading'), result.actions);
+        $('#doc-result').html(result.doc);
+        prettyPrint();
+      });
+  }
+
+  /**
+   * Fetch entity+action docs & code
+   */
+  function getDocAction() {
+    var
+      entity = $('#doc-entity').val(),
+      action = $('#doc-action').val();
+    if (entity && action) {
+      $('#doc-result').block();
+      $.get(CRM.url('civicrm/ajax/apidoc', {entity: entity, action: action}))
+        .done(function(result) {
+          $('#doc-result').unblock().html(result.doc);
+          if (result.code) {
+            $('#doc-result').append(docCodeTpl(result));
+          }
+          prettyPrint();
+        });
+    } else {
+      $('#doc-result').html($('#doc-result').attr('title'));
+    }
+  }
+
   $(document).ready(function() {
     // Set up tabs - bind active tab to document hash because... it's cool?
     document.location.hash = document.location.hash || 'explorer';
           document.location.hash = ui.newPanel.attr('id').replace('-tab', '');
         }
       });
+    $(window).on('hashchange', function() {
+      $('#mainTabContainer').tabs('option', 'active', $(document.location.hash + '-tab').index() - 1);
+    });
 
     // Initialize widgets
-    $('#api-entity, #example-entity').crmSelect2({
+    $('#api-entity, #example-entity, #doc-entity').crmSelect2({
       // Add strikethough class to selection to indicate deprecated apis
       formatSelection: function(option) {
         return $(option.element).hasClass('strikethrough') ? '<span class="strikethrough">' + option.text + '</span>' : option.text;
       .on('change', 'select.api-chain-entity', getChainedAction);
     $('#example-entity').on('change', getExamples);
     $('#example-action').on('change', getExample);
+    $('#doc-entity').on('change', getDocEntity);
+    $('#doc-action').on('change', getDocAction);
     $('#api-params-add').on('click', function(e) {
       e.preventDefault();
       addField();
index e85dc00a4ad9bc170d836bc3719a342b54b85f87..4d4e43a62fc293157094989cd236aa2c3a653b03 100644 (file)
@@ -40,6 +40,7 @@
     margin-bottom: .6em;
   }
   pre#api-result,
+  div#doc-result,
   pre#example-result {
     padding:1em;
     max-height: 50em;
   pre ol.linenums li:hover {
     color: #9c9c9c;
   }
+  .api-doc-code {
+    margin-top: 1em;
+    border-top: 1px solid #d3d3d3;
+  }
+  .api-doc-code .collapsible-title {
+    font-weight: bold;
+    margin-top: .5em;
+  }
   {/literal}
 </style>
 
 <div id="mainTabContainer">
   <ul>
-    <li class="ui-corner-all"><a href="#explorer-tab">{ts}Explorer{/ts}</a></li>
-    <li class="ui-corner-all"><a href="#examples-tab">{ts}Examples{/ts}</a></li>
+    <li class="ui-corner-all" title="GUI to build and execute API calls">
+      <a href="#explorer-tab">{ts}Explorer{/ts}</a>
+    </li>
+    <li class="ui-corner-all" title="Auto-generated examples from the test suite">
+      <a href="#examples-tab">{ts}Examples{/ts}</a>
+    </li>
+    <li class="ui-corner-all" title="API source-code and code-level documentation">
+      <a href="#docs-tab">{ts}Code Docs{/ts}</a>
+    </li>
   </ul>
 
   <div id="explorer-tab">
 </pre>
     </form>
   </div>
+
+  <div id="docs-tab">
+    <form id="api-docs">
+      <label for="doc-entity">{ts}Entity{/ts}:</label>
+      <select class="crm-form-select big required" id="doc-entity" name="entity">
+        <option value="" selected="selected">{ts}Choose{/ts}...</option>
+        {foreach from=$entities.values item=entity}
+          <option value="{$entity}" {if !empty($entities.deprecated) && in_array($entity, $entities.deprecated)}class="strikethrough"{/if}>
+            {$entity}
+          </option>
+        {/foreach}
+      </select>
+      &nbsp;&nbsp;
+      <label for="doc-action">{ts}Action{/ts}:</label>
+      <select class="crm-form-select big crm-select2" id="doc-action" name="action">
+        <option value="" selected="selected">{ts}Choose{/ts}...</option>
+      </select>
+      <div id="doc-result" title="{ts escape='html'}Results are displayed here.{/ts}">
+        {ts}Results are displayed here.{/ts}
+      </div>
+    </form>
+  </div>
 </div>
 
 {strip}
     </td>
   </tr>
 </script>
+
+<script type="text/template" id="doc-code-tpl">
+  <div class="crm-collapsible collapsed api-doc-code">
+    <div class="collapsible-title">{ts}Source Code{/ts}</div>
+    <pre class="prettyprint lang-php linenums"><%- code %></pre>
+  </div>
+</script>
 {/strip}