Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docs): add doctum shared workflow for reference docs generation #5455

Merged
merged 14 commits into from
Aug 27, 2024
353 changes: 353 additions & 0 deletions .github/doctum/apiclient-services/doctum.js.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
var Doctum = {
bshaffer marked this conversation as resolved.
Show resolved Hide resolved
treeJson: {{ tree|raw }},
{% verbatim -%}
/** @var boolean */
treeLoaded: false,
/** @var boolean */
listenersRegistered: false,
autoCompleteData: null,
/** @var boolean */
autoCompleteLoading: false,
/** @var boolean */
autoCompleteLoaded: false,
/** @var string|null */
rootPath: null,
/** @var string|null */
autoCompleteDataUrl: null,
/** @var HTMLElement|null */
doctumSearchAutoComplete: null,
/** @var HTMLElement|null */
doctumSearchAutoCompleteProgressBarContainer: null,
/** @var HTMLElement|null */
doctumSearchAutoCompleteProgressBar: null,
/** @var number */
doctumSearchAutoCompleteProgressBarPercent: 0,
/** @var autoComplete|null */
autoCompleteJS: null,
querySearchSecurityRegex: /([^0-9a-zA-Z:\\\\_\s])/gi,
buildTreeNode: function (treeNode, htmlNode, treeOpenLevel) {
var ulNode = document.createElement('ul');
for (var childKey in treeNode.c) {
Hectorhammett marked this conversation as resolved.
Show resolved Hide resolved
Hectorhammett marked this conversation as resolved.
Show resolved Hide resolved
var child = treeNode.c[childKey];
var liClass = document.createElement('li');
var hasChildren = child.hasOwnProperty('c');
var nodeSpecialName = (hasChildren ? 'namespace:' : 'class:') + child.p.replace(/\//g, '_');
liClass.setAttribute('data-name', nodeSpecialName);

// Create the node that will have the text
var divHd = document.createElement('div');
var levelCss = child.l - 1;
divHd.className = hasChildren ? 'hd' : 'hd leaf';
divHd.style.paddingLeft = (hasChildren ? (levelCss * 18) : (8 + (levelCss * 18))) + 'px';
if (hasChildren) {
if (child.l <= treeOpenLevel) {
liClass.className = 'opened';
}
var spanIcon = document.createElement('span');
spanIcon.className = 'icon icon-play';
divHd.appendChild(spanIcon);
}
var aLink = document.createElement('a');

// Edit the HTML link to work correctly based on the current depth
aLink.href = Doctum.rootPath + child.p + (hasChildren ? '.ns.html' : '.html');
bshaffer marked this conversation as resolved.
Show resolved Hide resolved
aLink.innerText = child.n;
divHd.appendChild(aLink);
liClass.appendChild(divHd);

// It has children
if (hasChildren) {
var divBd = document.createElement('div');
divBd.className = 'bd';
Doctum.buildTreeNode(child, divBd, treeOpenLevel);
liClass.appendChild(divBd);
}
ulNode.appendChild(liClass);
}
htmlNode.appendChild(ulNode);
},
initListeners: function () {
if (Doctum.listenersRegistered) {
// Quick exit, already registered
return;
}
{% endverbatim -%}
{% if project.versions|length > 1 %}
{# Enable the version switcher #}
var versionSwitcher = document.getElementById('version-switcher');
if (versionSwitcher !== null) {
var currentVersion = versionSwitcher.options[versionSwitcher.selectedIndex].dataset.version;
versionSwitcher.addEventListener('change', function (event) {
var targetVersion = event.target.options[event.target.selectedIndex].dataset.version;
var candidateUrl = window.location.pathname.replace(currentVersion, targetVersion);
// Check if the page exists before redirecting to it
var testRequest = new XMLHttpRequest();
testRequest.open('HEAD', candidateUrl, false);
testRequest.send();
if (testRequest.status < 200 || testRequest.status > 399) {
window.location = candidateUrl;
} else {
// otherwise reroute to the home page of the new version
window.location = this.value;
}
});
}
{% endif %}
{% verbatim -%}
Doctum.listenersRegistered = true;
},
loadTree: function () {
if (Doctum.treeLoaded) {
// Quick exit, already registered
return;
}
Doctum.rootPath = document.body.getAttribute('data-root-path');
Doctum.buildTreeNode(Doctum.treeJson.tree, document.getElementById('api-tree'), Doctum.treeJson.treeOpenLevel);

// Toggle left-nav divs on click
$('#api-tree .hd span').on('click', function () {
Hectorhammett marked this conversation as resolved.
Show resolved Hide resolved
$(this).parent().parent().toggleClass('opened');
});

// Expand the parent namespaces of the current page.
var expected = $('body').attr('data-name');

if (expected) {
// Open the currently selected node and its parents.
var container = $('#api-tree');
var node = $('#api-tree li[data-name="' + expected + '"]');
Hectorhammett marked this conversation as resolved.
Show resolved Hide resolved
// Node might not be found when simulating namespaces
if (node.length > 0) {
node.addClass('active').addClass('opened');
node.parents('li').addClass('opened');
var scrollPos = node.offset().top - container.offset().top + container.scrollTop();
// Position the item nearer to the top of the screen.
scrollPos -= 200;
container.scrollTop(scrollPos);
}
}
Doctum.treeLoaded = true;
},
pagePartiallyLoaded: function (event) {
Doctum.initListeners();
Doctum.loadTree();
Doctum.loadAutoComplete();
},
pageFullyLoaded: function (event) {
// it may not have received DOMContentLoaded event
Doctum.initListeners();
Doctum.loadTree();
Doctum.loadAutoComplete();
// Fire the event in the search page too
if (typeof DoctumSearch === 'object') {
Hectorhammett marked this conversation as resolved.
Show resolved Hide resolved
DoctumSearch.pageFullyLoaded();
}
},
loadAutoComplete: function () {
if (Doctum.autoCompleteLoaded) {
// Quick exit, already loaded
return;
}
Doctum.autoCompleteDataUrl = document.body.getAttribute('data-search-index-url');
Doctum.doctumSearchAutoComplete = document.getElementById('doctum-search-auto-complete');
Doctum.doctumSearchAutoCompleteProgressBarContainer = document.getElementById('search-progress-bar-container');
Doctum.doctumSearchAutoCompleteProgressBar = document.getElementById('search-progress-bar');
if (Doctum.doctumSearchAutoComplete !== null) {
// Wait for it to be loaded
Doctum.doctumSearchAutoComplete.addEventListener('init', function (_) {
Doctum.autoCompleteLoaded = true;
Doctum.doctumSearchAutoComplete.addEventListener('selection', function (event) {
// Go to selection page
window.location = Doctum.rootPath + event.detail.selection.value.p;
});
Doctum.doctumSearchAutoComplete.addEventListener('navigate', function (event) {
// Set selection in text box
if (typeof event.detail.selection.value === 'object') {
Doctum.doctumSearchAutoComplete.value = event.detail.selection.value.n;
}
});
Doctum.doctumSearchAutoComplete.addEventListener('results', function (event) {
Doctum.markProgressFinished();
});
});
}
// Check if the lib is loaded
if (typeof autoComplete === 'function') {
Doctum.bootAutoComplete();
}
},
markInProgress: function () {
Doctum.doctumSearchAutoCompleteProgressBarContainer.className = 'search-bar';
Doctum.doctumSearchAutoCompleteProgressBar.className = 'progress-bar indeterminate';
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
DoctumSearch.doctumSearchPageAutoCompleteProgressBarContainer.className = 'search-bar';
DoctumSearch.doctumSearchPageAutoCompleteProgressBar.className = 'progress-bar indeterminate';
}
},
markProgressFinished: function () {
Doctum.doctumSearchAutoCompleteProgressBarContainer.className = 'search-bar hidden';
Doctum.doctumSearchAutoCompleteProgressBar.className = 'progress-bar';
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
DoctumSearch.doctumSearchPageAutoCompleteProgressBarContainer.className = 'search-bar hidden';
DoctumSearch.doctumSearchPageAutoCompleteProgressBar.className = 'progress-bar';
}
},
makeProgess: function () {
Doctum.makeProgressOnProgressBar(
Doctum.doctumSearchAutoCompleteProgressBarPercent,
Doctum.doctumSearchAutoCompleteProgressBar
);
if (typeof DoctumSearch === 'object' && DoctumSearch.pageFullyLoaded) {
Doctum.makeProgressOnProgressBar(
Doctum.doctumSearchAutoCompleteProgressBarPercent,
DoctumSearch.doctumSearchPageAutoCompleteProgressBar
);
}
},
loadAutoCompleteData: function (query) {
return new Promise(function (resolve, reject) {
if (Doctum.autoCompleteData !== null) {
resolve(Doctum.autoCompleteData);
return;
}
Doctum.markInProgress();
function reqListener() {
Doctum.autoCompleteLoading = false;
Doctum.autoCompleteData = JSON.parse(this.responseText).items;
Doctum.markProgressFinished();

setTimeout(function () {
resolve(Doctum.autoCompleteData);
}, 50);// Let the UI render once before sending the results for processing. This gives time to the progress bar to hide
}
function reqError(err) {
Doctum.autoCompleteLoading = false;
Doctum.autoCompleteData = null;
console.error(err);
reject(err);
}

var oReq = new XMLHttpRequest();
Hectorhammett marked this conversation as resolved.
Show resolved Hide resolved
oReq.onload = reqListener;
oReq.onerror = reqError;
oReq.onprogress = function (pe) {
if (pe.lengthComputable) {
Doctum.doctumSearchAutoCompleteProgressBarPercent = parseInt(pe.loaded / pe.total * 100, 10);
Doctum.makeProgess();
}
};
oReq.onloadend = function (_) {
Doctum.markProgressFinished();
};
oReq.open('get', Doctum.autoCompleteDataUrl, true);
oReq.send();
});
},
/**
* Make some progress on a progress bar
*
* @param number percentage
* @param HTMLElement progressBar
* @return void
*/
makeProgressOnProgressBar: function(percentage, progressBar) {
progressBar.className = 'progress-bar';
progressBar.style.width = percentage + '%';
progressBar.setAttribute(
'aria-valuenow', percentage
);
},
searchEngine: function (query, record) {
if (typeof query !== 'string') {
return '';
}
// replace all (mode = g) spaces and non breaking spaces (\s) by pipes
// g = global mode to mark also the second word searched
// i = case insensitive
// how this function works:
// First: search if the query has the keywords in sequence
// Second: replace the keywords by a mark and leave all the text in between non marked
{% endverbatim -%}
{#
Case 1: search for "net sample"
Data: net_sample
Result <mark>net</mark>_<mark>sample</mark>
Case 1: search for "n t sa"
Data: net_sample, ample, glamples, notDateSa
Result <mark>n</mark>e<mark>t</mark>_<mark>sa</mark>mple, <mark>n</mark>o<mark>t</mark>Da<mark>t</mark>e<mark>Sa</mark>
#}
{%- verbatim %}
if (record.match(new RegExp('(' + query.replace(/\s/g, ').*(') + ')', 'gi')) === null) {
return '';// Does not match
}

var replacedRecord = record.replace(new RegExp('(' + query.replace(/\s/g, '|') + ')', 'gi'), function (group) {
return '<mark class="auto-complete-highlight">' + group + '</mark>';
});

if (replacedRecord !== record) {
return replacedRecord;// This should not happen but just in case there was no match done
}

return '';
},
/**
* Clean the search query
*
* @param string|null query
* @return string
*/
cleanSearchQuery: function (query) {
if (typeof query !== 'string') {
return '';
}
// replace any chars that could lead to injecting code in our regex
// remove start or end spaces
// replace backslashes by an escaped version, use case in search: \myRootFunction
return query.replace(Doctum.querySearchSecurityRegex, '').trim().replace(/\\/g, '\\\\');
},
bootAutoComplete: function () {
Doctum.autoCompleteJS = new autoComplete(
{
selector: '#doctum-search-auto-complete',
searchEngine: function (query, record) {
return Doctum.searchEngine(query, record);
},
submit: true,
data: {
src: function (q) {
Doctum.markInProgress();
return Doctum.loadAutoCompleteData(q);
},
keys: ['n'],// Data 'Object' key to be searched
cache: false, // Is not compatible with async fetch of data
},
query: (input) => {
return Doctum.cleanSearchQuery(input);
},
trigger: (query) => {
return Doctum.cleanSearchQuery(query).length > 0;
},
resultsList: {
tag: 'ul',
class: 'auto-complete-dropdown-menu',
destination: '#auto-complete-results',
position: 'afterbegin',
maxResults: 500,
noResults: false,
},
resultItem: {
tag: 'li',
class: 'auto-complete-result',
highlight: 'auto-complete-highlight',
selected: 'auto-complete-selected'
},
}
);
}
};


document.addEventListener('DOMContentLoaded', Doctum.pagePartiallyLoaded, false);
window.addEventListener('load', Doctum.pageFullyLoaded, false);
{% endverbatim -%}
17 changes: 17 additions & 0 deletions .github/doctum/apiclient-services/macros.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{% extends "default/macros.twig" %}

{% macro namespace_link(namespace) -%}
<a href="{{ root_path }}{{ namespace_path(namespace)|replace({".html": ".ns.html"}) }}">{{ namespace == '' ? global_namespace_name() : namespace|raw }}</a>
{%- endmacro %}

{% macro breadcrumbs(namespace) %}
{% set current_ns = '' %}
{% for ns in namespace|split('\\') %}
{%- if current_ns -%}
{% set current_ns = current_ns ~ '\\' ~ ns %}
{%- else -%}
{% set current_ns = ns %}
{%- endif -%}
<li><a href="{{ namespace_path(current_ns)|replace({".html": ".ns.html"})}}">{{ ns|raw }}</a></li><li class="backslash">\</li>
{%- endfor %}
{% endmacro %}
5 changes: 5 additions & 0 deletions .github/doctum/apiclient-services/manifest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: apiclient-services
parent: default

namespace:
'namespace.twig': '%s.ns.html'
Loading