org.pkl.doc.scripts.pkldoc.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pkl-tools Show documentation
Show all versions of pkl-tools Show documentation
Fat Jar containing pkl-cli, pkl-codegen-java, pkl-codegen-kotlin, pkl-config-java, pkl-core, pkl-doc, and their shaded third-party dependencies.
// noinspection DuplicatedCode
'use strict';
// Whether the current browser is WebKit.
let isWebKitBrowser;
// The lazily initialized worker for running searches, if any.
let searchWorker = null;
// Tells whether non-worker search is ready for use.
// Only relevant if we determined that we can't use a worker.
let nonWorkerSearchInitialized = false;
// The search div containing search input and search results.
let searchElement;
// The search input element.
let searchInput;
// The package name associated with the current page, if any.
let packageName;
let packageVersion;
// The module name associated with the current page, if any.
let moduleName;
// The class name associated with the current page, if any.
let className;
// Prefix to turn a site-relative URL into a page-relative URL.
// One of "", "../", "../../", etc.
let rootUrlPrefix;
// Prefix to turn a package-relative URL into a page-relative URL.
// One of "", "../", "../../", etc.
let packageUrlPrefix;
// The search result currently selected in the search results list.
let selectedSearchResult = null;
// Initializes the UI.
// Wrapped in a function to avoid execution in tests.
// noinspection JSUnusedGlobalSymbols
function onLoad() {
isWebKitBrowser = navigator.userAgent.indexOf('AppleWebKit') !== -1;
searchElement = document.getElementById('search');
searchInput = document.getElementById('search-input');
packageName = searchInput.dataset.packageName || null;
packageVersion = searchInput.dataset.packageVersion || null;
moduleName = searchInput.dataset.moduleName || null;
className = searchInput.dataset.className || null;
rootUrlPrefix = searchInput.dataset.rootUrlPrefix;
packageUrlPrefix = searchInput.dataset.packageUrlPrefix;
initExpandTargetMemberDocs();
initNavigateToMemberPage();
initToggleMemberDocs();
initToggleInheritedMembers();
initCopyModuleUriToClipboard();
initSearchUi();
}
// If page URL contains a fragment, expand the target member's docs.
// Handled in JS rather than CSS so that target member can still be manually collapsed.
function initExpandTargetMemberDocs() {
const expandTargetDocs = () => {
const hash = window.location.hash;
if (hash.length === 0) return;
const target = document.getElementById(hash.substring(1));
if (!target) return;
const member = target.nextElementSibling;
if (!member || !member.classList.contains('with-expandable-docs')) return;
expandMemberDocs(member);
}
window.addEventListener('hashchange', expandTargetDocs);
expandTargetDocs();
}
// For members that have their own page, navigate to that page when the member's box is clicked.
function initNavigateToMemberPage() {
const elements = document.getElementsByClassName('with-page-link');
for (const element of elements) {
const memberLink = element.getElementsByClassName('name-decl')[0];
// check if this is actually a link
// (it isn't if the generator couldn't resolve the link target)
if (memberLink.tagName === 'A') {
element.addEventListener('click', (e) => {
// don't act if user clicked a link
if (e.target !== null && e.target.closest('a') !== null) return;
// don't act if user clicked to select some text
if (window.getSelection().toString()) return;
memberLink.click();
});
}
}
}
// Expands and collapses member docs.
function initToggleMemberDocs() {
const elements = document.getElementsByClassName('with-expandable-docs');
for (const element of elements) {
element.addEventListener('click', (e) => {
// don't act if user clicked a link
if (e.target !== null && e.target.closest('a') !== null) return;
// don't act if user clicked to select some text
if (window.getSelection().toString()) return;
toggleMemberDocs(element);
});
}
}
// Shows and hides inherited members.
function initToggleInheritedMembers() {
const memberGroups = document.getElementsByClassName('member-group');
for (const group of memberGroups) {
const button = group.getElementsByClassName('toggle-inherited-members-link')[0];
if (button !== undefined) {
const members = group.getElementsByClassName('inherited');
button.addEventListener('click', () => toggleInheritedMembers(button, members));
}
}
}
// Copies the module URI optionally displayed on a module page to the clipboard.
function initCopyModuleUriToClipboard() {
const copyUriButtons = document.getElementsByClassName('copy-uri-button');
for (const button of copyUriButtons) {
const moduleUri = button.previousElementSibling;
button.addEventListener('click', e => {
e.stopPropagation();
const range = document.createRange();
range.selectNodeContents(moduleUri);
const selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
try {
document.execCommand('copy');
} catch (e) {
} finally {
selection.removeAllRanges();
}
});
}
}
// Expands or collapses member docs.
function toggleMemberDocs(memberElem) {
const comments = memberElem.getElementsByClassName('expandable');
const icon = memberElem.getElementsByClassName('expandable-docs-icon')[0];
const isCollapsed = icon.textContent === 'expand_more';
if (isCollapsed) {
for (const comment of comments) expandElement(comment);
icon.textContent = 'expand_less';
} else {
for (const comment of comments) collapseElement(comment);
icon.textContent = 'expand_more';
}
}
// Expands member docs unless they are already expanded.
function expandMemberDocs(memberElem) {
const icon = memberElem.getElementsByClassName('expandable-docs-icon')[0];
const isCollapsed = icon.textContent === 'expand_more';
if (!isCollapsed) return;
const comments = memberElem.getElementsByClassName('expandable');
for (const comment of comments) expandElement(comment);
icon.textContent = 'expand_less';
}
// Shows and hides inherited members.
function toggleInheritedMembers(button, members) {
const isCollapsed = button.textContent === 'show inherited';
if (isCollapsed) {
for (const member of members) expandElement(member);
button.textContent = 'hide inherited';
} else {
for (const member of members) collapseElement(member);
button.textContent = 'show inherited'
}
}
// Expands an element.
// Done in two steps to make transition work (can't transition from 'hidden').
// For some reason (likely related to removing 'hidden') the transition isn't animated in FF.
// When using timeout() instead of requestAnimationFrame()
// there is *some* animation in FF but still doesn't look right.
function expandElement(element) {
element.classList.remove('hidden');
requestAnimationFrame(() => {
element.classList.remove('collapsed');
});
}
// Collapses an element.
// Done in two steps to make transition work (can't transition to 'hidden').
function collapseElement(element) {
element.classList.add('collapsed');
const listener = () => {
element.removeEventListener('transitionend', listener);
element.classList.add('hidden');
};
element.addEventListener('transitionend', listener);
}
// Initializes the search UI and sets up delayed initialization of the search engine.
function initSearchUi() {
// initialize search engine the first time that search input receives focus
const onFocus = () => {
searchInput.removeEventListener('focus', onFocus);
initSearchWorker();
};
searchInput.addEventListener('focus', onFocus);
// clear search when search input loses focus,
// except if this happens due to a search result being clicked,
// in which case clearSearch() will be called by the link's click handler,
// and calling it here would prevent the click handler from firing
searchInput.addEventListener('focusout', () => {
if (document.querySelector('#search-results:hover') === null) clearSearch();
});
// trigger search when user hasn't typed in a while
let timeoutId = null;
// Using anything other than `overflow: visible` for `#search-results`
// slows down painting significantly in WebKit browsers (at least Safari/Mac).
// Compensate by using a higher search delay, which is less annoying than a blocking UI.
const delay = isWebKitBrowser ? 200 : 100;
searchInput.addEventListener('input', () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => triggerSearch(searchInput.value), delay);
});
// keyboard shortcut for entering search
document.addEventListener('keyup', e => {
// could additionally support '/' like GitHub and Gmail do,
// but this would require overriding the default behavior of '/' on Firefox
if (e.key === 's') searchInput.focus();
});
// keyboard navigation for search results
searchInput.addEventListener('keydown', e => {
const results = document.getElementById('search-results');
if (results !== null) {
if (e.key === 'ArrowDown') {
selectNextResult(results.firstElementChild);
e.preventDefault();
} else if (e.key === 'ArrowUp') {
selectPrevResult(results.firstElementChild);
e.preventDefault();
}
}
});
searchInput.addEventListener('keyup', e => {
if (e.key === 'Enter' && selectedSearchResult !== null) {
selectedSearchResult.firstElementChild.click();
clearSearch();
}
});
}
// Initializes the search worker.
function initSearchWorker() {
const workerScriptUrl = rootUrlPrefix + 'scripts/search-worker.js';
try {
searchWorker = new Worker(workerScriptUrl, {name: packageName === null ? "main" : packageName + '/' + packageVersion});
searchWorker.addEventListener('message', e => handleSearchResults(e.data.query, e.data.results));
} catch (e) {
// could not initialize worker, presumably because we are a file:/// page and content security policy got in the way
// fall back to running searches synchronously without a worker
// this requires loading search related scripts that would otherwise be loaded by the worker
searchWorker = null;
let pendingScripts = 3;
const onScriptLoaded = () => {
if (--pendingScripts === 0) {
initSearchIndex();
nonWorkerSearchInitialized = true;
if (searchInput.focused) {
triggerSearch(searchInput.value);
}
}
};
const script1 = document.createElement('script');
script1.src = (packageName === null ? rootUrlPrefix : packageUrlPrefix) + 'search-index.js';
script1.async = true;
script1.onload = onScriptLoaded;
document.head.append(script1);
const script2 = document.createElement('script');
script2.src = rootUrlPrefix;
script2.async = true;
script2.onload = onScriptLoaded;
document.head.append(script2);
const script3 = document.createElement('script');
script3.src = workerScriptUrl;
script3.async = true;
script3.onload = onScriptLoaded;
document.head.append(script3);
}
}
// Updates search results unless they are stale.
function handleSearchResults(query, results) {
if (query.inputValue !== searchInput.value) return;
updateSearchResults(renderSearchResults(query, results));
}
// TODO: Should this (or its callers) use requestAnimationFrame() ?
// Removes any currently displayed search results, then displays the given results if non-null.
function updateSearchResults(resultsDiv) {
selectedSearchResult = null;
const oldResultsDiv = document.getElementById('search-results');
if (oldResultsDiv !== null) {
searchElement.removeChild(oldResultsDiv);
}
if (resultsDiv != null) {
searchElement.append(resultsDiv);
selectNextResult(resultsDiv.firstElementChild);
}
}
// Returns the module of the given member, or `null` if the given member is a module.
function getModule(member) {
switch (member.level) {
case 0:
return null;
case 1:
return member.parent;
case 2:
return member.parent.parent;
}
}
// Triggers a search unless search input is invalid or incomplete.
function triggerSearch(inputValue) {
const query = parseSearchInput(inputValue);
if (!isActionableQuery(query)) {
handleSearchResults(query, null);
return;
}
if (searchWorker !== null) {
searchWorker.postMessage({query, packageName, moduleName, className});
} else if (nonWorkerSearchInitialized) {
const results = runSearch(query, packageName, moduleName, className);
handleSearchResults(query, results);
}
}
// Tells if the given Unicode character is a whitespace character.
function isWhitespace(ch) {
const cp = ch.codePointAt(0);
if (cp >= 9 && cp <= 13 || cp === 32 || cp === 133 || cp === 160) return true;
if (cp < 5760) return false;
return cp === 5760 || cp >= 8192 && cp <= 8202
|| cp === 8232 || cp === 8233 || cp === 8239 || cp === 8287 || cp === 12288;
}
// Trims the given Unicode characters.
function trim(chars) {
const length = chars.length;
let startIdx, endIdx;
for (startIdx = 0; startIdx < length; startIdx += 1) {
if (!isWhitespace(chars[startIdx])) break;
}
for (endIdx = chars.length - 1; endIdx > startIdx; endIdx -= 1) {
if (!isWhitespace(chars[endIdx])) break;
}
return chars.slice(startIdx, endIdx + 1);
}
// Parses the user provided search input.
// Preconditions:
// inputValue !== ''
function parseSearchInput(inputValue) {
const chars = trim(Array.from(inputValue));
const char0 = chars[0]; // may be undefined
const char1 = chars[1]; // may be undefined
const prefix = char1 === ':' ? char0 + char1 : null;
const kind =
prefix === null ? null :
char0 === 'm' ? 1 :
char0 === 't' ? 2 :
char0 === 'c' ? 3 :
char0 === 'f' ? 4 :
char0 === 'p' ? 5 :
undefined;
const unprefixedChars = kind !== null && kind !== undefined ?
trim(chars.slice(2, chars.length)) :
chars;
const normalizedCps = toNormalizedCodePoints(unprefixedChars);
return {inputValue, prefix, kind, normalizedCps};
}
// Converts a Unicode character array to an array of normalized Unicode code points.
// Normalization turns characters into their base forms, e.g., é into e.
// Since JS doesn't support case folding, `toLocaleLowerCase()` is used instead.
// Note: Keep in sync with same function in search-worker.js.
function toNormalizedCodePoints(characters) {
return Uint32Array.from(characters, ch => ch.normalize('NFD')[0].toLocaleLowerCase().codePointAt(0));
}
// Tells if the given query is valid and long enough to be worth running.
// Prefixed queries require fewer minimum characters than unprefixed queries.
// This avoids triggering a search while typing a prefix yet still enables searching for single-character names.
// For example, `p:e` finds `pkl.math#E`.
function isActionableQuery(query) {
const kind = query.kind;
const queryCps = query.normalizedCps;
return kind !== undefined && (kind !== null && queryCps.length > 0 || queryCps.length > 1);
}
// Renders the given search results for the given query.
// Preconditions:
// isActionableQuery(query) ? results !== null : results === null
function renderSearchResults(query, results) {
const resultsDiv = document.createElement('div');
resultsDiv.id = 'search-results';
const ul = document.createElement('ul');
resultsDiv.append(ul);
if (results === null) {
if (query.kind !== undefined) return null;
const li = document.createElement('li');
li.className = 'heading';
li.textContent = 'Unknown search prefix. Use one of m: (module), c: (class), f: (function), or p: (property).';
ul.append(li);
return resultsDiv;
}
const {exactMatches, classMatches, moduleMatches, otherMatches} = results;
if (exactMatches.length + classMatches.length + moduleMatches.length + otherMatches.length === 0) {
renderHeading('No results found', ul);
return resultsDiv;
}
if (exactMatches.length > 0) {
renderHeading('Top hits', ul);
renderMembers(query.normalizedCps, exactMatches, ul);
}
if (classMatches.length > 0) {
renderHeading('Class', ul, className);
renderMembers(query.normalizedCps, classMatches, ul);
}
if (moduleMatches.length > 0) {
renderHeading('Module', ul, moduleName);
renderMembers(query.normalizedCps, moduleMatches, ul);
}
if (otherMatches.length > 0) {
renderHeading('Other results', ul);
renderMembers(query.normalizedCps, otherMatches, ul);
}
return resultsDiv;
}
// Adds a heading such as `Top matches` to the search results list.
function renderHeading(title, ul, name = null) {
const li = document.createElement('li');
li.className = 'heading';
li.append(title);
if (name != null) {
li.append(' ');
li.append(span('heading-name', name))
}
ul.append(li);
}
// Adds matching members to the search results list.
function renderMembers(queryCps, members, ul) {
for (const member of members) {
ul.append(renderMember(queryCps, member));
}
}
// Renders a member to be added to the search result list.
function renderMember(queryCps, member) {
const result = document.createElement('li');
result.className = 'result';
if (member.deprecated) result.className = 'deprecated';
const link = document.createElement('a');
result.append(link);
link.href = (packageName === null ? rootUrlPrefix : packageUrlPrefix) + member.url;
link.addEventListener('mousedown', () => selectResult(result));
link.addEventListener('click', clearSearch);
const keyword = getKindKeyword(member.kind);
// noinspection JSValidateTypes (IntelliJ bug?)
if (keyword !== null) {
link.append(span('keyword', keyword), ' ');
}
// prefix with class name if a class member
if (member.level === 2) {
link.append(span("context", member.parent.name + '.'));
}
const name = span('result-name');
if (member.matchNameIdx === 0) { // main name matched
highlightMatch(queryCps, member.names[0], member.matchStartIdx, name);
} else { // aka name matched
name.append(member.name);
}
link.append(name);
if (member.signature !== null) {
link.append(member.signature);
}
if (member.matchNameIdx > 0) { // aka name matched
link.append(' ');
const aka = span('aka');
aka.append('(known as: ');
const name = span('aka-name');
highlightMatch(queryCps, member.names[member.matchNameIdx], member.matchStartIdx, name);
aka.append(name, ')');
link.append(aka);
}
// add module name if not a module
const module = getModule(member);
if (module !== null) {
link.append(' ', span('context', '(' + module.name + ')'));
}
return result;
}
// Returns the keyword for the given member kind.
function getKindKeyword(kind) {
switch (kind) {
case 0:
return "package";
case 1:
return "module";
case 2:
return "typealias";
case 3:
return "class";
case 4:
return "function";
case 5:
// properties have no keyword
return null;
}
}
// Highlights the matching characters in a member name.
// Preconditions:
// queryCps.length > 0
// computeMatchFrom(queryCps, name.normalizedCps, name.wordStarts, matchStartIdx)
function highlightMatch(queryCps, name, matchStartIdx, parentElem) {
const queryLength = queryCps.length;
const codePoints = name.codePoints;
const nameCps = name.normalizedCps;
const nameLength = nameCps.length;
const wordStarts = name.wordStarts;
let queryIdx = 0;
let queryCp = queryCps[0];
let startIdx = matchStartIdx;
if (startIdx > 0) {
parentElem.append(String.fromCodePoint(...codePoints.subarray(0, startIdx)));
}
for (let nameIdx = startIdx; nameIdx < nameLength; nameIdx += 1) {
const nameCp = nameCps[nameIdx];
if (queryCp !== nameCp) {
const newNameIdx = wordStarts[nameIdx];
parentElem.append(
span('highlight', String.fromCodePoint(...codePoints.subarray(startIdx, nameIdx))));
startIdx = newNameIdx;
parentElem.append(String.fromCodePoint(...codePoints.subarray(nameIdx, newNameIdx)));
nameIdx = newNameIdx;
}
queryIdx += 1;
if (queryIdx === queryLength) {
parentElem.append(
span('highlight', String.fromCodePoint(...codePoints.subarray(startIdx, nameIdx + 1))));
if (nameIdx + 1 < nameLength) {
parentElem.append(String.fromCodePoint(...codePoints.subarray(nameIdx + 1, nameLength)));
}
return;
}
queryCp = queryCps[queryIdx];
}
throw 'Precondition violated: `computeMatchFrom()`';
}
// Creates a span element.
function span(className, text = null) {
const result = document.createElement('span');
result.className = className;
result.textContent = text;
return result;
}
// Creates a text node.
function text(content) {
return document.createTextNode(content);
}
// Navigates to the next member entry in the search results list, skipping headings.
function selectNextResult(ul) {
let next = selectedSearchResult === null ? ul.firstElementChild : selectedSearchResult.nextElementSibling;
while (next !== null) {
if (!next.classList.contains('heading')) {
selectResult(next);
scrollIntoView(next, {
behavior: 'instant', // better for keyboard navigation
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
});
return;
}
next = next.nextElementSibling;
}
}
// Navigates to the previous member entry in the search results list, skipping headings.
function selectPrevResult(ul) {
let prev = selectedSearchResult === null ? ul.lastElementChild : selectedSearchResult.previousElementSibling;
while (prev !== null) {
if (!prev.classList.contains('heading')) {
selectResult(prev);
const prev2 = prev.previousElementSibling;
// make any immediately preceding heading visible as well (esp. important for first heading)
const scrollTo = prev2 !== null && prev2.classList.contains('heading') ? prev2 : prev;
scrollIntoView(scrollTo, {
behavior: 'instant', // better for keyboard navigation
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
});
return;
}
prev = prev.previousElementSibling;
}
}
// Selects the given entry in the search results list.
function selectResult(li) {
if (selectedSearchResult !== null) {
selectedSearchResult.classList.remove('selected');
}
li.classList.add('selected');
selectedSearchResult = li;
}
// Clears the search input and hides/removes the search results list.
function clearSearch() {
searchInput.value = '';
updateSearchResults(null);
}
// Functions called by JS data scripts.
// noinspection JSUnusedGlobalSymbols
const runtimeData = {
links: (id, linksJson) => {
const links = JSON.parse(linksJson);
const fragment = document.createDocumentFragment();
let first = true;
for (const link of links) {
const {text, href, classes} = link
const a = document.createElement("a");
a.textContent = text;
if (href) a.href = href;
if (classes) a.className = classes;
if (first) {
first = false;
} else {
fragment.append(", ");
}
fragment.append(a);
}
const element = document.getElementById(id);
element.append(fragment);
element.classList.remove("hidden"); // dd
element.previousElementSibling.classList.remove("hidden"); // dt
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy