Page MenuHomePhorge

Add remarkup syntax help for custom remarkup rule tokens
Closed, ResolvedPublic

Asked by mturdus on Jun 13 2024, 19:15.

Details

I'm working on a custom application extension which integrates a custom Remarkup rule (PhabricatorObjectRemarkupRule).
As this token has some parameters, I wanted to document these in the Remarkup syntax help page (located at /book/phorge/article/remarkup).

My idea was to implement a new interface:

<?php
interface RemarkupSyntaxDocumentationProvider {
    public function getDocumentation();
}

which can then be used like this:

<?php

final class DummyApplication extends PhabricatorApplication {
  public function getName() {
    return pht('Dummy');
  }

  public function getRemarkupRules() {
    return array(
      new DummyRemarkup(),
    );
  }

  public function getRoutes() {
    return [];
  }
}

final class DummyObject
{
  private $id;

  public function __construct($id) {
    $this->id = $id;
  }

  public function getID() {
    return $this->id;
  }

  public function getPHID() {
    return 'PHID-DUMM-0000000000000' . $this->id;
  }

}

final class DummyRemarkup extends PhabricatorObjectRemarkupRule implements RemarkupSyntaxDocumentationProvider {
  
    protected function getObjectNamePrefix() {
      return "DUMMY";
    }
  
    protected function loadHandles(array $objects) {
      $phids = mpull($objects, 'getPHID');
  
      $result = array();
      foreach ($objects as $id => $object) {
        $handle = new PhabricatorObjectHandle();
        $handle->setPHID($object->getPHID());
        $result[$id] = $handle;

      }
      return $result;
    }
  
    protected function loadObjects(array $ids) {
      return [
        new DummyObject(0),
        new DummyObject(1),
        new DummyObject(2),
        new DummyObject(3),
        new DummyObject(4),
        new DummyObject(5)
      ];
    }
      
    public function getDocumentation() {
      return '= Dummy remarkup explanation =
Here you find some help about the DUMMY token...
';
    }

    protected function renderObjectEmbed(
      $dummyObject,
      PhabricatorObjectHandle $handle,
      $options) {
  

      return phutil_safe_html('<strong>dummy ' . $dummyObject->getId() . '</strong>');
    }
  }

The getDocumentation function returns the help text in Remarkup format that should be appended at the bottom of the Remarkup syntax help page.
The table of content of this page will also be updated.

Currently, the Remarkup syntax help page points to the https://we.phorge.it page, even if you have the diviner pages locally installed.
This means of course that you cannot change the content of this page.
I added some extra code in PhabricatorEnv which checks if the diviner pages are locally installed. If yes, then the local pages will be used instead of the ones from https://we.phorge.it. Because this check requires a database connection and thus a $viewer, I had to add a static $viewer property to PhabricatorEnv (as all its functions are static functions). I'm not sure if this is completely kosher.

These are my changes:

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 9287085d03..8bf1369821 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -5878,6 +5878,7 @@ phutil_register_library_map(array(
     'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php',
     'QueryFuture' => 'infrastructure/storage/future/QueryFuture.php',
     'RemarkupProcessConduitAPIMethod' => 'applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php',
+    'RemarkupSyntaxDocumentationProvider' => 'applications/diviner/interface/RemarkupSyntaxDocumentationProvider.php',
     'RemarkupValue' => 'applications/remarkup/RemarkupValue.php',
     'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php',
     'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php',
diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php
index 3198bb9fd4..70511dbdbc 100644
--- a/src/aphront/configuration/AphrontApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontApplicationConfiguration.php
@@ -270,6 +270,9 @@ final class AphrontApplicationConfiguration
     try {
       $response = $controller->willBeginExecution();
 
+      // store viewer in static variable, so it can be used in PhabricatorEnv's static functions
+      PhabricatorEnv::setViewer($request->getViewer());
+
       if ($request->getUser() && $request->getUser()->getPHID()) {
         $access_log->setData(
           array(
diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php
index cf29389da9..5bc320a2dd 100644
--- a/src/applications/diviner/controller/DivinerAtomController.php
+++ b/src/applications/diviner/controller/DivinerAtomController.php
@@ -106,8 +106,24 @@ final class DivinerAtomController extends DivinerController {
     }
     $engine->process();
 
+    $extraContent = '';
+    $extraTOC = [];
+
+    // if Remarkup Syntax Help page is shown, search for RemarkupSyntaxDocumentationProviders in extensions
+    if ($book_name == 'phorge' && $atom_type == 'article' && $atom_name == 'remarkup') {
+      $remarkupSyntaxDocumentationProviders = id(new PhutilClassMapQuery())
+        ->setAncestorClass('RemarkupSyntaxDocumentationProvider')
+        ->execute();
+
+      // add custom Remarkup help
+      foreach ($remarkupSyntaxDocumentationProviders as $remarkupSyntaxDocumentationProvider) {
+        $extraContent .= $remarkupSyntaxDocumentationProvider->getDocumentation(). "\r\n\r\n";
+      }
+    }
+
     if ($atom) {
-      $content = $this->renderDocumentationText($symbol, $engine);
+      // convert Remarkup to HTML and parse Table of Content items out
+      $content = $this->renderDocumentationText($symbol, $engine, $viewer, $extraContent, $extraTOC);
       $document->appendChild($content);
     }
 
@@ -117,6 +133,9 @@ final class DivinerAtomController extends DivinerController {
       PhutilRemarkupHeaderBlockRule::KEY_HEADER_TOC,
       array());
 
+    // merge default and custom table of contents
+    $toc = array_merge($toc, $extraTOC);
+
     if (!$atom) {
       $document->appendChild(
         id(new PHUIInfoView())
@@ -617,11 +636,44 @@ final class DivinerAtomController extends DivinerController {
 
   private function renderDocumentationText(
     DivinerLiveSymbol $symbol,
-    PhabricatorMarkupEngine $engine) {
+    PhabricatorMarkupEngine $engine,
+    $viewer,
+    $extraContent,
+    array &$extraTOCItems) {
 
     $field = 'default';
     $content = $engine->getOutput($symbol, $field);
 
+    if ($extraContent != '') {
+      $remarkupEngine = PhabricatorMarkupEngine::newMarkupEngine(array())
+        ->setConfig('header.generate-toc', true)
+        ->setConfig('viewer', $viewer)
+        ->setMode(PhutilRemarkupEngine::MODE_HTML_MAIL);
+
+      try {
+        $extraContent = $remarkupEngine->markupText($extraContent);
+
+        $dom = new DOMDocument();
+        @$dom->loadHTML($extraContent);
+        $xpath = new DOMXPath($dom);
+        $headers = $xpath->query('//h1 | //h2 | //h3 | //h4 | //h5 | //h6');
+
+        foreach ($headers as $header) {
+          $key = $header->getElementsByTagName('a')->item(0)->getAttribute('name');
+          $headerDepth = (int)(substr($header->tagName, 1,1));
+          $textContent = trim($header->nodeValue);
+
+          $extraTOCItems[$key] = array(
+            $headerDepth,
+            new PhutilSafeHTML($textContent)
+          );
+        }
+      } catch (Exception $ex) {
+      }
+    }
+
+    $content->appendHTML($extraContent);
+
     if (strlen(trim($symbol->getMarkupText($field)))) {
       $content = phutil_tag(
         'div',
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index a471874e43..18a52ac756 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -58,6 +58,7 @@ final class PhabricatorEnv extends Phobject {
   private static $localeCode;
   private static $readOnly;
   private static $readOnlyReason;
+  private static $viewer;
 
   const READONLY_CONFIG = 'config';
   const READONLY_UNREACHABLE = 'unreachable';
@@ -494,9 +495,26 @@ final class PhabricatorEnv extends Phobject {
       'jump' => true,
     );
 
-    $uri = new PhutilURI(
-      'https://we.phorge.it/diviner/find/',
-      $params);
+    $useRemoteDocumentation = true;
+    if (PhabricatorEnv::$viewer) {
+      $books = id(new DivinerBookQuery())
+      ->setViewer(PhabricatorEnv::$viewer)
+      ->execute();
+
+      if (!!$books) {
+        $useRemoteDocumentation = false;
+      }
+    }
+
+    if ($useRemoteDocumentation) {
+      $uri = new PhutilURI(
+        'https://we.phorge.it/diviner/find/',
+        $params);
+    } else {
+      $uri = new PhutilURI(
+        '/diviner/find/',
+        $params);
+    }
 
     return phutil_string_cast($uri);
   }
@@ -986,5 +1004,8 @@ final class PhabricatorEnv extends Phobject {
     return $root.'/support/empty/';
   }
 
-
+  public static function setViewer($viewer)
+  {
+    PhabricatorEnv::$viewer = $viewer;
+  }
 }

Do you think this is something that can be integrated in Phorge?
Or is this something that will hardly be used.

Answers

avivey
Updated 83 Days Ago

This sounds like T15401 - essentially, create a new core application that will replace the existing page, will be served locally for each install, and updated to the rules available in that install.
The existing page (hard-coded to point to this install) will be removed.

mturdus
Updated 83 Days Ago

That seems indeed a better idea

New Answer

Answer

This question has been marked as closed, but you can still leave a new answer.