projectdoc Toolbox

Renders a menu with tools to inspect information from a projectdoc document, shown in the browser.

Tags
Identifier
de.smartics.userscripts.confluence.projectdoc-inspect-menu
Type
Repository
Since
1.0

The userscript renders a menu on a Confluence page with tools to inspect the projectdoc document shown in the browser.

It implements the following actions:

  1. Display Document Properties
  2. Display Space Properties
  3. Display Transcluding Documents

Code

The code of the script for reference.

projectdoc-inspect-menu.js
/*
 * Copyright 2019-2024 Kronseder & Reiner GmbH, smartics
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
"use strict";

AJS.toInit(function () {
  const logToConsole = USERSCRIPT4C.isVerboseLoggingRequestedFor('projectdoc-inspect-menu');

  const $propertiesMarker = AJS.$(".projectdoc-document-element.properties");
  if (!$propertiesMarker.length) {
    if (logToConsole) AJS.log("[projectdoc-inspect-menu] Not a projectdoc document. Quitting.");
    return;
  }

  const appendHead = function (htmlTitle, $html) {
    const $head = AJS.$('<head></head>').append(AJS.$('<title></title>', {'text': htmlTitle}))
      .append(AJS.$('<style></style>', {
        'text': 'body {margin: 1rem !important;}' +
          ' .table-sm td, .table-sm th {padding: .1rem !important;}' +
          ' .table td, .table th { font-size: .8rem !important;}'
      }))
      .append(AJS.$('<link/>', {
        'rel': 'stylesheet',
        'href': 'https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css',
        'integrity': 'sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T',
        'crossorigin': 'anonymous'
      }));
    $head.appendTo($html);
    return $head
  };

  const createStandardTable = function ($rootElement) {
    return AJS.$('<table></table>').addClass('table table-sm table-bordered table-striped').appendTo($rootElement);
  };

  const appendRow = function ($table, url, name, shortDescription) {
    const $tr = AJS.$('<tr></tr>').appendTo($table);
    AJS.$('<th></th>').append(AJS.$('<a></a>', {
      'href': url,
      'text': name
    })).appendTo($tr);
    AJS.$('<td></td>').text(shortDescription).appendTo($tr);
  };

  const createHitCountIntroText = function (intro, hitCount) {
    return intro + (hitCount === 0 ? 'no document.' : (hitCount === 1 ? 'one document.' : hitCount + ' documents.'));
  };

  const showDialog = function ($html) {
    const showDialog = window.open('', '', 'width=750,height=800,location=no,toolbar=0');
    showDialog.document.body.innerHTML = $html.prop('outerHTML');
  };

  const showDocumentProperties = function () {
    if (logToConsole) AJS.log("[projectdoc-inspect-menu] Fetching document properties ...");
    const pageId = AJS.Meta.get('page-id');
    const locale = AJS.Meta.get('user-locale');
    const baseURL = AJS.Meta.get('base-url');

    const htmlTitle = "Page Properties";
    const $html = AJS.$("<html></html>").attr("lang", locale);
    appendHead(htmlTitle, $html);
    const $body = AJS.$('<body></body>');
    AJS.$('<h6></h6>').text(htmlTitle).appendTo($body);
    $body.appendTo($html);

    if (logToConsole) AJS.log("[projectdoc-inspect-menu] Making call ...");
    AJS.$.ajax({
      url: baseURL + "/rest/projectdoc/1/document/" + pageId + ".json?expand=property&resource-mode=html",
      async: true,
      contentType: 'application/json'
    }).success(function (data) {
      if (logToConsole) AJS.log("[projectdoc-inspect-menu] Document Properties Data: " + JSON.stringify(data));

      const $table = createStandardTable($body);
      AJS.$.each(data["property"],
        function (index, obj) {
          const $tr = AJS.$('<tr></tr>').appendTo($table);
          AJS.$('<th></th>').text(obj.name).appendTo($tr);
          // Render the content as HTML fragment
          AJS.$('<td></td>').append(obj.value).appendTo($tr);
        }
      );

      showDialog($html);
    }).error(function (jqXHR, textStatus) {
      AJS.log("[projectdoc-inspect-menu] Error fetching document properties: " + jqXHR.status + " (" + textStatus + ")");
      alert("Failed to fetch document properties: " + jqXHR.status + " (" + textStatus + ")");
    });
  };

  const showSpaceProperties = function () {
    const spaceKey = AJS.Meta.get('space-key');
    const locale = AJS.Meta.get('user-locale');
    const baseUrl = AJS.Meta.get('base-url');

    const htmlTitle = "Space Properties for " + spaceKey;
    const $html = AJS.$("<html></html>").attr("lang", locale);
    appendHead(htmlTitle, $html);

    const $body = AJS.$('<body></body>');
    AJS.$('<h6></h6>').text(htmlTitle).appendTo($body);
    $body.appendTo($html);

    if (logToConsole) AJS.log("[projectdoc-inspect-menu] Querying Properties Data for Space '" + spaceKey + "' ...");
    AJS.$.ajax({
      url: baseUrl + "/rest/projectdoc/1/space/" + spaceKey,
      async: true,
      dataType: 'json'
      //contentType: 'application/json'
    }).success(function (data) {
      if (logToConsole) AJS.log("[projectdoc-inspect-menu] Space Properties Data: " + JSON.stringify(data));

      const $table = createStandardTable($body);
      AJS.$.each(data["property"],
        function (index, obj) {
          const $tr = AJS.$("<tr></tr>").appendTo($table);
          AJS.$("<td></td>").text(obj.source).appendTo($tr);
          AJS.$("<th></th>").text(obj.name).appendTo($tr);
          // Render the content as HTML fragment
          AJS.$("<td></td>").append(obj.value).appendTo($tr);
        }
      );

      showDialog($html);
    }).error(function (jqXHR, textStatus) {
      AJS.log("[projectdoc-inspect-menu] Error fetching space properties: " + jqXHR.status + " (" + textStatus + ")");
      alert("Failed to fetch space properties: " + jqXHR.status + " (" + textStatus + ")");
    });
  };

  function renderDocumentTable(hitCount, $body, documents, i18n, tinyUrlNamePlain) {
    if (hitCount > 0) {
      const $tableTransclusions = createStandardTable($body);
      AJS.$.each(documents.document, function (_index, doc) {
        const current = {};
        AJS.$.each(doc.property, function (i, property) {
          current[property.name] = property.value;
        });
        const name = current[i18n.name];
        const shortDescription = current[i18n.shortDescription];
        const url = current[tinyUrlNamePlain];
        appendRow($tableTransclusions, url, name, shortDescription);
      });
    }
  }

  function renderTableWithAnchorLinks(currentDocument, $body, hitCount, documents, i18n, tinyUrlNamePlain) {
    const extractAnchorId = function (anchorLink, dynamicLinkPrefix, staticLinkPrefix) {
      const length = anchorLink.length;
      if (anchorLink.startsWith(dynamicLinkPrefix)) {
        const i = dynamicLinkPrefix.length + 1;
        if (length > i) {
          return anchorLink.substring(i);
        }
      } else if (anchorLink.startsWith(staticLinkPrefix)) {
        const i = staticLinkPrefix.length + 1;
        if (length > i) {
          return anchorLink.substring(i);
        }
      }
      return null;
    };
    const anchorLinksName = i18n["projectdoc.doctype.common.metadata.anchorLinks"];
    if (hitCount > 0) {
      const documentUrl = currentDocument[tinyUrlNamePlain];
      const documentTitle = currentDocument[i18n.title];
      const documentSpaceKey = currentDocument[i18n.spaceKey];
      const dynamicLinkPrefix = "*." + documentTitle;
      const staticLinkPrefix = documentSpaceKey + "." + documentTitle;

      const $table = createStandardTable($body);
      AJS.$.each(documents.document, function (_index, doc) {
        const current = {};
        AJS.$.each(doc.property, function (i, property) {
          current[property.name] = property.value;
        });
        const name = current[i18n.name];
        const anchorLinks = current[anchorLinksName];
        AJS.log("Value for property '" + anchorLinksName + "' of document '" + name + "' is: " + anchorLinks);
        if (anchorLinks) {
          const shortDescription = current[i18n.shortDescription];
          const url = current[tinyUrlNamePlain];

          const $tr = AJS.$('<tr></tr>').appendTo($table);
          AJS.$('<th></th>').append(AJS.$('<a></a>', {
            'href': url,
            'text': name
          })).appendTo($tr);
          AJS.$('<td></td>').text(shortDescription).appendTo($tr);

          const $list = AJS.$('<ul style="margin-bottom: 0;"></ul>');
          AJS.$.each(anchorLinks.split(/,\s*/), function (_index, anchorLink) {
            const anchorId = extractAnchorId(anchorLink, dynamicLinkPrefix, staticLinkPrefix);
            if (anchorId != null) {
              AJS.$('<li></li>').append(AJS.$('<a></a>', {
                'href': documentUrl + "#" + encodeURIComponent(anchorId),
                'text': anchorId
              })).appendTo($list);
            }
          });
          AJS.$('<td></td>').append($list).appendTo($tr);
        }
      });
    }
  }

  const listTranscludingDocument = function () {
    const pageId = AJS.Meta.get('page-id');
    const locale = AJS.Meta.get('user-locale');
    const baseUrl = AJS.Meta.get('base-url');

    function createPage(i18n, currentDocument, transclusionDocuments, delegateDocuments, dynamicLinkTitlesDocuments, doctypeNameReferencesDocuments, anchorLinkDocuments) {
      const documentName = currentDocument[i18n.name].trim();
      const htmlTitle = "Transcluding Documents for " + documentName;
      const $html = AJS.$("<html></html>").attr("lang", locale);
      appendHead(htmlTitle, $html);

      const transclusionHitCount = transclusionDocuments.document.length;
      const tinyUrlNamePlain = i18n["tinyUrl"] + '\u00a7';
      const $body = AJS.$('<body></body>').appendTo($html);
      AJS.$('<h3></h3>')
        .text(htmlTitle).appendTo($body);
      AJS.$("<p></p>")
        .append(document.createTextNode('Content of document '))
        .append(AJS.$('<a/>', {
          'href': currentDocument[tinyUrlNamePlain],
          'text': documentName
        }))
        .append(document.createTextNode(createHitCountIntroText(' is transcluded by ', transclusionHitCount))).appendTo($body);
      renderDocumentTable(transclusionHitCount, $body, transclusionDocuments, i18n, tinyUrlNamePlain);

      const delegateHitCount = delegateDocuments.document.length;
      AJS.$('<p></p>').text(createHitCountIntroText('The document is delegate of ', delegateHitCount)).appendTo($body);
      renderDocumentTable(delegateHitCount, $body, delegateDocuments, i18n, tinyUrlNamePlain);

      const dynamicLinksHitCount = dynamicLinkTitlesDocuments.document.length;
      AJS.$('<p></p>').text(createHitCountIntroText('The document is a possible target for dynamic links in ', dynamicLinksHitCount)).appendTo($body);
      renderDocumentTable(dynamicLinksHitCount, $body, dynamicLinkTitlesDocuments, i18n, tinyUrlNamePlain);

      const doctypeNameReferencesHitCount = doctypeNameReferencesDocuments.document.length;
      AJS.$('<p></p>').text(createHitCountIntroText('The document is a possible target for Doctype/Name reference in ', doctypeNameReferencesHitCount)).appendTo($body);
      renderDocumentTable(doctypeNameReferencesHitCount, $body, doctypeNameReferencesDocuments, i18n, tinyUrlNamePlain);


      const anchorLinkDocumentsHitCount = anchorLinkDocuments.document.length;
      AJS.$('<p></p>').text(createHitCountIntroText('The document is a possible target for anchor links in ', anchorLinkDocumentsHitCount)).appendTo($body);
      renderTableWithAnchorLinks(currentDocument, $body, anchorLinkDocumentsHitCount, anchorLinkDocuments, i18n, tinyUrlNamePlain);

      return $html;
    }

    if (PDBMLS) {
      const i18n = PDBMLS.fetchI18n(baseUrl, ["title", "spaceKey", "doctype", "name", "shortDescription", "tinyUrl", "projectdoc.doctype.common.delegateDocument.pageRef", "projectdoc.doctype.common.metadata.dynamicLinkTitles", "projectdoc.doctype.common.metadata.documentDoctypeNameReferences", "projectdoc.doctype.common.metadata.anchorLinkDocuments", "projectdoc.doctype.common.metadata.anchorLinks"]);
      const tinyUrlNamePlain = i18n["tinyUrl"] + '\u00a7';

      const currentDocument = PDBMLS.fetchDocument(baseUrl, pageId, [i18n.spaceKey, i18n.title, i18n.doctype, i18n.name, tinyUrlNamePlain]);

      if (currentDocument) {
        const spaceKey = currentDocument[i18n.spaceKey];
        const title = currentDocument[i18n.title];
        const pageReference = spaceKey + "." + title;
        const whereTransclusion = "$<TranscludedDocumentTitles>=[" + pageReference + "]";

        const tableDataPropertyNames = [i18n.name, i18n.shortDescription, tinyUrlNamePlain];
        const transcludingDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataPropertyNames, whereTransclusion);

        const delegatePageRefName = i18n["projectdoc.doctype.common.delegateDocument.pageRef"];
        const whereDelegate = "$<" + delegatePageRefName + ">=[" + pageReference + "]";
        const delegateDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataPropertyNames, whereDelegate);

        const dynamicLinkTitles = i18n["projectdoc.doctype.common.metadata.dynamicLinkTitles"];
        const whereDynamicLinkTitles = "$<" + dynamicLinkTitles + ">=[" + title + "]";
        // AJS.log("Where (Dynamic Link Title): " + whereDynamicLinkTitles);
        const dynamicLinkTitlesDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataPropertyNames, whereDynamicLinkTitles);

        let doctypeNameReferencesDocuments;
        const doctypeNameReferencesName = i18n["projectdoc.doctype.common.metadata.documentDoctypeNameReferences"];
        if (doctypeNameReferencesName) {
          const doctype = currentDocument[i18n.doctype];
          const name = currentDocument[i18n.name].trim();
          const documentReference = doctype + ":" + name;
          const whereDoctypeNameReferences = "$<" + doctypeNameReferencesName + ">=[" + documentReference + "]";
          // AJS.log("[projectdoc-inspect-menu] Where: " + whereDoctypeNameReferences);
          doctypeNameReferencesDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataPropertyNames, whereDoctypeNameReferences);
        }

        const anchorLinkDocumentsName = i18n["projectdoc.doctype.common.metadata.anchorLinkDocuments"];
        const whereAnchorLinkDocuments = "$<" + anchorLinkDocumentsName + ">~(" + spaceKey + "." + title + ", *." + title + "]";
        const anchorLinksName = i18n["projectdoc.doctype.common.metadata.anchorLinks"];
        const tableDataAnchorLinkPropertyNames = [i18n.name, i18n.shortDescription, tinyUrlNamePlain, anchorLinksName];
        const anchorLinkDocuments = PDBMLS.fetchDocuments(baseUrl, tableDataAnchorLinkPropertyNames, whereAnchorLinkDocuments);

        const $html = createPage(i18n, currentDocument, transcludingDocuments, delegateDocuments, dynamicLinkTitlesDocuments, doctypeNameReferencesDocuments, anchorLinkDocuments);
        showDialog($html);
      }
    } else {
      AJS.log("[projectdoc-inspect-menu] Error transcluding documents. PDBMLS service of Bookmarklets Extension not found.");
      alert("Failed to transcluding documents: PDBMLS service of Bookmarklets Extension not found.");
    }
  };

  const createMenu = function () {
    const menuId = "inspect";
    const propertiesSectionId = "projectdoc-inspect-menu-properties";

    const $mainMenu = USERSCRIPT4C_MENU.createMenu(menuId, "Inspect");
    USERSCRIPT4C_MENU.registerMenu("view.menu", $mainMenu);
    USERSCRIPT4C_MENU.addSection(menuId, {
      id: propertiesSectionId,
      label: "Properties",
      weight: 10
    });

    USERSCRIPT4C_MENU.addMenuItem(propertiesSectionId, {
      id: "projectdoc-menu-inspect-item-document-properties",
      label: "Show document properties",
      weight: "100"
    }, showDocumentProperties);
    USERSCRIPT4C_MENU.addMenuItem(propertiesSectionId, {
      id: "projectdoc-menu-inspect-item-space-properties",
      label: "Show space properties",
      weight: "200"
    }, showSpaceProperties);
    USERSCRIPT4C_MENU.addMenuItem(propertiesSectionId, {
      id: "projectdoc-menu-inspect-item-transclusions",
      label: "Show transclusions",
      weight: "300"
    }, listTranscludingDocument);

    return createMenu;
  }

  if (logToConsole) AJS.log("[projectdoc-inspect-menu] Adding menu ...");
  createMenu();
});

Details

More information on using this userscript.

Rendering

The inspect menu is rendered next to the create button in the Confluence toolbar.

Screenshot showing the menu with its menu items.

Requirements

The script requires the following apps to be installed on Confluence.

The projectdoc Toolbox for Atlassian Confluence
The projectdoc Toolbox supports agile teams in writing project documentation collaboratively. This is an introduction to use cases for and features of the projectdoc Toolbox.
Web API Extension
Add-on to extend projectdoc with an API to access on the web.
Bookmarklets Extension
Add-on to extend the Toolkit with Bookmarklets. Allows to execute tools via the browser.

Transcluding Documents Information

This version lists documents that

  1. transclude from the current document
  2. delegate to the current document

Only Static Transclusions

 

Please note that only static transclusions are listed in this report. A static transclusion is content from a referenced document. The Transclusion Macro uses static transclusion.

Dynamic transclusions are based on document queries. These transclusions are not listed in the report. The Transclude Documents Macro uses dynamic transclusion.

Related Scripts

NameShort Description
Hide projectdoc Tools
Removes projectdoc tools (blueprints and macros) from the current page.
projectdoc Search Tool
Provides an interface to specify and launch queries for projectdoc documents.
Refactor projectdoc Document
Adds a refactor menu and checks the current document for property issues.

Resources

More information on this topic is available by the following resources.

Display Document Properties
Displays the document properties of the projectdoc document currently shown in the browser.
Display Space Properties
Displays the space properties of the projectdoc document's space currently shown in the browser.
List Transcluding Documents
Shows the list of documents that transclude content from the current document.
projectdoc Dynamic Link Titles
Lists the titles of all pages targeted by dynamic links.