Use markdown in php docblocks & display in APIv4 Explorer
authorColeman Watts <coleman@civicrm.org>
Thu, 30 Jan 2020 01:34:49 +0000 (20:34 -0500)
committerColeman Watts <coleman@civicrm.org>
Thu, 30 Jan 2020 22:56:47 +0000 (17:56 -0500)
Reformats some docblocks to use markdown, and uses the marked.js library
to display them in the APIv4 Explorer.

14 files changed:
CRM/Api4/Page/Api4Explorer.php
Civi/Api4/ACL.php
Civi/Api4/Action/Setting/AbstractSettingAction.php
Civi/Api4/CustomValue.php
Civi/Api4/Generic/AbstractAction.php
Civi/Api4/Generic/AbstractGetAction.php
Civi/Api4/Generic/DAOGetAction.php
Civi/Api4/Utils/ReflectionUtils.php
ang/api4Explorer/Explorer.html
ang/api4Explorer/Explorer.js
api/api.php
composer.json
composer.lock
tests/phpunit/api/v4/Utils/ReflectionUtilsTest.php

index f1fb6f734d67c5757c7c30f22501551f492ce718..adc1d74326dd8727a288a7d3ebd8b359045632ff 100644 (file)
@@ -33,6 +33,7 @@ class CRM_Api4_Page_Api4Explorer extends CRM_Core_Page {
       ->addPermissions(['access debug output'])
       ->addScriptFile('civicrm', 'js/load-bootstrap.js')
       ->addScriptFile('civicrm', 'bower_components/js-yaml/dist/js-yaml.min.js')
+      ->addScriptFile('civicrm', 'bower_components/marked/marked.min.js')
       ->addScriptFile('civicrm', 'bower_components/google-code-prettify/bin/prettify.min.js')
       ->addStyleFile('civicrm', 'bower_components/google-code-prettify/bin/prettify.min.css');
 
index 5ac059f6131925e1472f1a3178aa2e1ca6c3d818..8501391d8e0cdc3b73072565f81b7a527240467d 100644 (file)
@@ -26,11 +26,11 @@ namespace Civi\Api4;
  *
  * An ACL record consists of:
  *
- *   - an Operation (e.g. 'View' or 'Edit')
- *   - a set of Data that the operation can be performed on (e.g. a group of contacts)
- *   - and a Role that has permission to do this operation.
+ *   1. An Operation (e.g. 'View' or 'Edit').
+ *   2. A set of Data that the operation can be performed on (e.g. a group of contacts).
+ *   3. A Role that has permission to do this operation.
  *
- * Creating a new ACL requires at minimum a entity table, entity ID and object_table.
+ * Creating a new ACL requires at minimum an entity table, entity ID and object_table.
  *
  * @see https://docs.civicrm.org/user/en/latest/initial-set-up/permissions-and-access-control
  * @package Civi\Api4
index 53b9cdd891c57c2681bc85fecc868f5c17f7cbc1..d4a7a7dbe5a96add9a8f7a6b1e1256bebbec4fb7 100644 (file)
@@ -58,7 +58,7 @@ abstract class AbstractSettingAction extends \Civi\Api4\Generic\AbstractAction {
   }
 
   /**
-   * Checks that really ought to be taken care of by Civi::settings
+   * Checks that really ought to be taken care of by `Civi::settings`.
    *
    * @param int $domain
    * @return array
index 3ed03a8d61a0e263d9d25cca8e86ea5798952865..60c6d6c8707400f88cd7eeea83a10d9b42f30f71 100644 (file)
@@ -29,8 +29,8 @@ namespace Civi\Api4;
  *
  * Each action takes the name of the custom group as a parameter, or in traditional syntax the entity is prefixed with 'Custom_'
  *
- * Ex. OOP: \Civi\Api4\CustomValue::get('MyStuff')->addWhere('id', '=', 123)
- * Non-OOP: civicrm_api4('Custom_MyStuff', 'get', ['where' => [['id', '=', 123]]]);
+ * **Ex. OOP:** `\Civi\Api4\CustomValue::get('MyStuff')->addWhere('id', '=', 123);`
+ * **Non-OOP:** `civicrm_api4('Custom_MyStuff', 'get', ['where' => [['id', '=', 123]]]);`
  *
  * Note: This class does NOT extend AbstractEntity so it doesn't get mistaken for a "real" entity.
  * @package Civi\Api4
index 310df486e5c4ca14b8573e2d0f0918677167672b..d4a0ed93711dc61ef9b4239d33398a83ccfc15d8 100644 (file)
@@ -56,16 +56,21 @@ abstract class AbstractAction implements \ArrayAccess {
    *
    * Keys can be any string - this will be the name given to the output.
    *
-   * You can reference other values in the api results in this call by prefixing them with $
+   * You can reference other values in the api results in this call by prefixing them with `$`.
    *
    * For example, you could create a contact and place them in a group by chaining the
-   * GroupContact api to the Contact api:
+   * `GroupContact` api to the `Contact` api:
    *
+   * ```php
    * Contact::create()
    *   ->setValue('first_name', 'Hello')
-   *   ->addChain('add_to_a_group', GroupContact::create()->setValue('contact_id', '$id')->setValue('group_id', 123))
+   *   ->addChain('add_a_group', GroupContact::create()
+   *     ->setValue('contact_id', '$id')
+   *     ->setValue('group_id', 123)
+   *   )
+   * ```
    *
-   * This will substitute the id of the newly created contact with $id.
+   * This will substitute the id of the newly created contact with `$id`.
    *
    * @var array
    */
@@ -84,10 +89,10 @@ abstract class AbstractAction implements \ArrayAccess {
   /**
    * Add debugging info to the api result.
    *
-   * When enabled, the $result->debug will be populated with information about the api call,
+   * When enabled, `$result->debug` will be populated with information about the api call,
    * including sql queries executed.
    *
-   * Note: with checkPermissions enabled, debug info will only be returned if the user has "view debug output" permission.
+   * **Note:** with checkPermissions enabled, debug info will only be returned if the user has "view debug output" permission.
    *
    * @var bool
    */
@@ -175,9 +180,8 @@ abstract class AbstractAction implements \ArrayAccess {
    * @param string $name
    *   Unique name for this chained request
    * @param \Civi\Api4\Generic\AbstractAction $apiRequest
-   * @param string|int $index
-   *   Either a string for how the results should be indexed e.g. 'name'
-   *   or the index of a single result to return e.g. 0 for the first result.
+   * @param string|int|array $index
+   *   See `civicrm_api4()` for documentation of `$index` param
    * @return $this
    */
   public function addChain($name, AbstractAction $apiRequest, $index = NULL) {
index 06d3f6c2f800d57c9ec1ba8901dc99d138c9137a..7a32a54da68d081091e85f744b061504d3d42c6d 100644 (file)
@@ -34,12 +34,12 @@ use Civi\Api4\Utils\SelectUtil;
 abstract class AbstractGetAction extends AbstractQueryAction {
 
   /**
-   * Fields to return. Defaults to all fields ["*"].
+   * Fields to return. Defaults to all fields `["*"]`.
    *
    * Use the * wildcard by itself to select all available fields, or use it to match similarly-named fields.
-   * E.g. "is_*" will match fields named is_primary, is_active, etc.
+   * E.g. `is_*` will match fields named is_primary, is_active, etc.
    *
-   * Set to ["row_count"] to return only the number of items found.
+   * Set to `["row_count"]` to return only the number of items found.
    *
    * @var array
    */
@@ -96,7 +96,7 @@ abstract class AbstractGetAction extends AbstractQueryAction {
    *
    * Ex: If getRecords fetches a long list of items each with a unique name,
    * but the user has specified a single record to retrieve, you can optimize the call
-   * by checking $this->_itemsToGet('name') and only fetching the item(s) with that name.
+   * by checking `$this->_itemsToGet('name')` and only fetching the item(s) with that name.
    *
    * @param string $field
    * @return array|null
index a85682e480c0d5febe3d22a2215eef618f730714..a6968a147f5eedd3b058f9b787cb19e3a68f008d 100644 (file)
@@ -32,9 +32,9 @@ class DAOGetAction extends AbstractGetAction {
   use Traits\DAOActionTrait;
 
   /**
-   * Fields to return. Defaults to all non-custom fields ["*"].
+   * Fields to return. Defaults to all non-custom fields `["*"]`.
    *
-   * Use the dot notation to perform joins in the select clause, e.g. selecting ["*", "contact.*"] from Email.get
+   * Use the dot notation to perform joins in the select clause, e.g. selecting `["*", "contact.*"]` from `Email::get()`
    * will select all fields for the email + all fields for the related contact.
    *
    * @var array
index a0d4b799f3c07a05922d1a6b517d7df3fb0fc461..9cb4f6fb5dd91c90ff534060c182a99753ddc682 100644 (file)
@@ -68,10 +68,13 @@ class ReflectionUtils {
       if (!$num || strpos($line, '*/') !== FALSE) {
         continue;
       }
-      $line = preg_replace('/[ ]+/', ' ', ltrim(trim($line), '* '));
-      if (strpos($line, '@') === 0) {
-        $words = explode(' ', $line);
-        $key = substr(array_shift($words), 1);
+      $line = ltrim(trim($line), '*');
+      if (strlen($line) && $line[0] === ' ') {
+        $line = substr($line, 1);
+      }
+      if (strpos(ltrim($line), '@') === 0) {
+        $words = explode(' ', ltrim($line, ' @'));
+        $key = array_shift($words);
         $param = NULL;
         if ($key == 'var') {
           $info['type'] = explode('|', $words[0]);
index bfa66e27507c895deb842369beff7f2946a426f9..e9246257462a3392a92a211811432a0701a49b59 100644 (file)
           <h3 class="panel-title" crm-icon="fa-info-circle">{{ helpTitle }}</h3>
         </div>
         <div class="panel-body">
-          <h4>{{ helpContent.description }}</h4>
-          <div ng-if="helpContent.comment">
-            <div ng-repeat='text in helpContent.comment.split("\n\n")'>
-              <p ng-if="text[0] !== '-' && text[0] !== '*'">{{ text }}</p>
-              <ul ng-if="text[0] === '-' || text[0] === '*'">
-                <li ng-repeat='item in text.split("\n")'>{{ item.substr(1) }}</li>
-              </ul>
-            </div>
-          </div>
+          <h4 ng-bind-html="helpContent.description"></h4>
+          <div ng-bind-html="helpContent.comment"></div>
           <p ng-repeat="(key, item) in helpContent" ng-if="key !== 'description' && key !== 'comment' && key !== 'see'">
             <strong>{{ key }}:</strong> {{ item }}
           </p>
index 542b84edae1f1a8994a02a2359d8c5d276aff6f7..a34f66460a62ab0bf4e366bbb417f7db8130912d 100644 (file)
@@ -34,6 +34,7 @@
     $scope.perm = {
       accessDebugOutput: CRM.checkPerm('access debug output')
     };
+    marked.setOptions({highlight: prettyPrintOne});
     var getMetaParams = {},
       objectParams = {orderBy: 'ASC', values: '', chain: ['Entity', '', '{}']},
       docs = CRM.vars.api4.docs,
       return fields;
     }
 
-    $scope.help = function(title, param) {
-      if (!param) {
+    $scope.help = function(title, content) {
+      if (!content) {
         $scope.helpTitle = helpTitle;
         $scope.helpContent = helpContent;
       } else {
         $scope.helpTitle = title;
-        $scope.helpContent = param;
+        $scope.helpContent = convertMarkdown(content);
       }
     };
 
+    // Sets the static help text (which gets overridden by mousing over other elements)
+    function setHelp(title, content) {
+      $scope.helpTitle = helpTitle = title;
+      $scope.helpContent = helpContent = convertMarkdown(content);
+    }
+
+    function convertMarkdown(rawContent) {
+      var formatted = _.cloneDeep(rawContent);
+      if (formatted.description) {
+        formatted.description = marked(formatted.description);
+      }
+      if (formatted.comment) {
+        formatted.comment = marked(formatted.comment);
+      }
+      return formatted;
+    }
+
     // Format the href for a @see help annotation
     $scope.formatRef = function(see) {
       var match = see.match(/^\\Civi\\Api4\\([a-zA-Z]+)$/);
     // Help for an entity with no action selected
     function showEntityHelp(entityName) {
       var entityInfo = getEntity(entityName);
-      $scope.helpTitle = helpTitle = $scope.entity;
-      $scope.helpContent = helpContent = {
+      setHelp($scope.entity, {
         description: entityInfo.description,
         comment: entityInfo.comment,
         see: entityInfo.see
-      };
+      });
     }
 
     if (!$scope.entity) {
-      $scope.helpTitle = helpTitle = ts('APIv4 Explorer');
-      $scope.helpContent = helpContent = {description: docs.description, comment: docs.comment, see: docs.see};
+      setHelp(ts('APIv4 Explorer'), {description: docs.description, comment: docs.comment, see: docs.see});
     } else if (!actions.length && !getEntity().actions) {
       getMetaParams.actions = [$scope.entity, 'getActions', {chain: {fields: [$scope.entity, 'getFields', {action: '$name'}]}}];
       fetchMeta();
       if ($scope.entity && $routeParams.api4action !== newVal && !_.isUndefined(newVal)) {
         $location.url('/explorer/' + $scope.entity + '/' + newVal);
       } else if (newVal) {
-        $scope.helpTitle = helpTitle = $scope.entity + '::' + newVal;
-        $scope.helpContent = helpContent = _.pick(_.findWhere(getEntity().actions, {name: newVal}), ['description', 'comment', 'see']);
+        setHelp($scope.entity + '::' + newVal, _.pick(_.findWhere(getEntity().actions, {name: newVal}), ['description', 'comment', 'see']));
       }
     });
 
index 224846279f99f02f2c089255cdbf9966b95b508e..1a70bc3921cb83182598e84da4c5e23a4d0702b8 100644 (file)
@@ -33,12 +33,12 @@ function civicrm_api(string $entity = NULL, string $action, array $params, $extr
  * @see https://docs.civicrm.org/dev/en/latest/api/v4/usage/
  *
  * @param string $entity Name of the CiviCRM entity to access.
- *   All entity names are capitalized CamelCase, e.g. "ContributionPage".
- *   Most entities correspond to a database table (e.g. "Contact" is the table "civicrm_contact").
- *   For a complete list of available entities, call civicrm_api4('Entity', 'get');
+ *   All entity names are capitalized CamelCase, e.g. `ContributionPage`.
+ *   Most entities correspond to a database table (e.g. `Contact` is the table `civicrm_contact`).
+ *   For a complete list of available entities, call `civicrm_api4('Entity', 'get');`
  *
  * @param string $action The "verb" of the api call.
- *   For a complete list of actions for a given entity (e.g. Contact), call civicrm_api4('Contact', 'getActions');
+ *   For a complete list of actions for a given entity (e.g. `Contact`), call `civicrm_api4('Contact', 'getActions');`
  *
  * @param array $params An array of API input keyed by parameter name.
  *   The easiest way to discover all available parameters is to visit the API Explorer on your CiviCRM site.
@@ -47,11 +47,14 @@ function civicrm_api(string $entity = NULL, string $action, array $params, $extr
  * @param string|int|array $index Controls the Result array format.
  *   By default the api Result contains a non-associative array of data. Passing an $index tells the api to
  *   automatically reformat the array, depending on the variable type passed:
- *
- *     - Integer: return a single result array; e.g. index = 0 will return the first result, 1 will return the second, and -1 will return the last.
- *     - String: index the results by a field value; e.g. index = "name" will return an associative array with the field 'name' as keys.
- *     - Non-associative array: return a single value from each result; e.g. index = ['title'] will return a non-associative array of strings - the 'title' field from each result.
- *     - Associative array: a combination of the previous two modes; e.g. index = ['name' => 'title'] will return an array of strings - the 'title' field keyed by the 'name' field.
+ *   - **Integer:** return a single result array;
+ *     e.g. `$index = 0` will return the first result, 1 will return the second, and -1 will return the last.
+ *   - **String:** index the results by a field value;
+ *     e.g. `$index = "name"` will return an associative array with the field 'name' as keys.
+ *   - **Non-associative array:** return a single value from each result;
+ *     e.g. `$index = ['title']` will return a non-associative array of strings - the 'title' field from each result.
+ *   - **Associative array:** a combination of the previous two modes;
+ *     e.g. `$index = ['name' => 'title']` will return an array of strings - the 'title' field keyed by the 'name' field.
  *
  * @return \Civi\Api4\Generic\Result
  * @throws \API_Exception
index 62faa8f79a5c6709e4f2d9a3000b7fcef51fbce3..b49a224770f769b3663a4e422e27dec950c45914 100644 (file)
         "url": "https://github.com/FortAwesome/Font-Awesome/archive/v4.7.0.zip",
         "ignore": ["*/.*", "*.json", "src", "*.yml", "Gemfile", "Gemfile.lock", "*.md"]
       },
+      "marked": {
+        "url": "https://github.com/markedjs/marked/archive/v0.8.0.zip",
+        "ignore": [".*", "*.json", "*.md", "Makefile", "*/*"]
+      },
       "google-code-prettify": {
         "url": "https://github.com/tcollard/google-code-prettify/archive/v1.0.5.zip",
         "ignore": ["closure-compiler", "js-modules", "tests", "yui-compressor", "Makefile"]
index cd6eedbff3c5a118aa96546abf1a3796e3c1e472..1265218ed338e27bae1bc73cb168d4425b70ecc6 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b3fb18d6fdb3244cc94be8eaa883b7ae",
+    "content-hash": "36eeb97f14e2af530920b89c59124de3",
     "packages": [
         {
             "name": "civicrm/civicrm-cxn-rpc",
index 8e02868a532d6d76b07bd1e0b6bb9608b02eaa9b..494e4d59931851ab3dc915e212132999e6ba4307 100644 (file)
@@ -86,7 +86,7 @@ This is the base class.';
             '$foo' => [
               'type' => ['int', 'string'],
               'description' => '',
-              'comment' => "Nothing interesting.\n",
+              'comment' => "  Nothing interesting.\n",
             ],
             '$bar' => [
               'type' => NULL,