agahi
Version:
Client-side engine that renders Markdown files as a docs site in the browser—no build step.
228 lines (186 loc) • 10.8 kB
JavaScript
(function () {
'use strict';
// App configuration for Agahi
/**
* @typedef {Object} AgahiConfig
* @property {string} name - The name of the documentation site.
* @property {string} repo - GitHub repository in the format "owner/repo".
* @property {string} el - The ID of the HTML element where content will be rendered.
* @property {boolean} edit - Whether to show the "Edit on GitHub" link.
* @property {string} editPath - The full URL path to edit the markdown files on GitHub.
* @property {string} editLabel - Text label for edit
* @property {boolean} showFooter - Whether to display the footer section.
* @property {boolean} showSearch - Whether to enable the search functionality.
* @property {string} defaultRoute - The default route to load when no hash is provided.
* @property {string[]} tocHeadings - An array of heading tags to include in the Table of Contents (TOC).
* @property {string} searchPlaceholder - Search Placeholder
* @property {string} tocLabel - Text label for TOC heading
* @property {number} scrollThreshold - The scroll threshold in pixels to show the "Back to Top" button.
*/
/**
* Default configuration for Agahi Docs.
* @type {AgahiConfig}
*/
const defaultConfig = {
name: 'Agahi.js',
repo: 'teneplaysofficial/agahi',
el: 'app',
edit: true,
editPath: 'https://github.com/teneplaysofficial/agahi/edit/main/docs',
editLabel: 'Edit this page',
showFooter: true,
showSearch: true,
searchPlaceholder: 'Search...',
defaultRoute: '',
tocHeadings: ['h2', 'h3'],
tocLabel: 'Table of Contents',
scrollThreshold: 300,
};
/**
* User provided configuration via global `window.$agahi`, or falls back to `defaultConfig`.
* @type {AgahiConfig}
*/
const userConfig = typeof window !== 'undefined' ? window.$agahi || {} : {};
const config = {
...defaultConfig,
...userConfig,
};
/**
*
* @param {*} key - The key to retrieve from the URL parameters
* @description This function retrieves the value of a specified key from the URL's query parameters.
* @returns - The value associated with the key, or null if the key does not exist.
* @example
* // If the URL is "https://example.com?user=123", calling getParam('user') will return '123'.
*/
const getParam = (key) => {
const hash = window.location.hash || '#/';
const [, queryString = ''] = hash.split('?');
return new URLSearchParams(queryString).get(key);
};
/**
*
* @param {*} params - An object containing key-value pairs to set as URL parameters
* @description This function updates the URL's query parameters with the provided key-value pairs.
* If a value is null, the corresponding parameter will be removed from the URL.
* @example
* If the current URL is "https://example.com" and you call setParams({ user: '123', page: null }),
* the URL will be updated to "https://example.com?user=123".
* * @returns - This function does not return a value; it modifies the browser's URL.
*/
const setParams = (params) => {
const hash = window.location.hash || '#/';
const [basePath, queryString = ''] = hash.slice(1).split('?');
const searchParams = new URLSearchParams(queryString);
Object.entries(params).forEach(([key, value]) => {
if (value === null) {
searchParams.delete(key);
} else {
searchParams.set(key, value);
}
});
const newHash = `#/${basePath.replace(/^\/+/, '')}${searchParams.toString() ? '?' + searchParams.toString() : ''}`;
window.history.replaceState({}, '', newHash);
};
var TOCHTML = "<div id=\"toc\">\n <h2 id=\"toc-title\">\n <span id=\"label\"></span>\n <span class=\"chevron-arrow\"\n ><svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"lucide lucide-chevron-down-icon lucide-chevron-down\"\n >\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </span>\n </h2>\n <nav id=\"toc-nav\">\n <ul></ul>\n </nav>\n</div>\n";
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = "/* TOC Layout */\nmain {\n display: grid;\n grid-template-areas:\n 'md toc'\n 'page-actions toc';\n grid-template-columns: 1fr 15.625rem;\n grid-template-rows: 1fr auto;\n align-items: start;\n min-height: 0;\n}\n#md {\n grid-area: md;\n}\n#toc {\n grid-area: toc;\n position: sticky;\n top: var(--header-height);\n align-self: start;\n max-height: calc(100vh - var(--header-height));\n overflow-y: auto;\n border-left: 0.125rem solid var(--border-color);\n z-index: 50;\n}\n#page-actions {\n grid-area: page-actions;\n}\n/* TOC Title */\n#toc-title {\n padding: 0.75rem 0.5rem;\n text-align: left;\n border-bottom: 0.125rem solid var(--border-color);\n}\n/* TOC Navigation */\n#toc-nav {\n padding-left: 0.625rem;\n overflow-y: auto;\n max-height: 100%;\n margin-top: 0.75rem;\n}\n#toc-nav ul {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n#toc-nav li {\n position: relative;\n margin: 0.35rem 0;\n padding-left: 1rem;\n font-size: 0.95rem;\n line-height: 1.4;\n transition: color 0.2s ease;\n}\n#toc-nav li::before {\n content: '•';\n position: absolute;\n left: 0;\n font-weight: bold;\n font-size: 1rem;\n line-height: 1.2;\n color: #3b82f6;\n color: var(--primary-color, #3b82f6);\n}\n#toc-nav a {\n color: inherit;\n -webkit-text-decoration: none;\n text-decoration: none;\n transition: color 0.2s ease;\n}\n#toc-nav a:hover {\n color: #2563eb;\n color: var(--primary-color, #2563eb);\n -webkit-text-decoration: underline;\n text-decoration: underline;\n}\n/* Nested TOC */\n#toc-nav ul ul {\n padding-left: 1.25rem;\n margin-top: 0.3rem;\n}\n#toc-nav ul ul li::before {\n content: '◦';\n color: #9ca3af;\n}\n.chevron-arrow {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 0.9em;\n height: 0.9em;\n vertical-align: middle;\n visibility: hidden;\n}\n.chevron-arrow svg {\n display: block;\n width: 100%;\n height: 100%;\n transition: transform 0.3s ease;\n}\n/* Responsive Layout */\n@media (max-width: 1100px) {\n main {\n grid-template-areas:\n 'toc'\n 'md'\n 'page-actions';\n grid-template-columns: 1fr;\n grid-template-rows: auto 1fr auto;\n }\n\n #toc {\n position: inherit;\n border-left: none;\n }\n\n #toc-title {\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n #toc-nav {\n max-height: 0;\n background: var(--background-hover);\n margin-top: 0;\n transition: max-height 0.3s ease;\n }\n\n #toc.collapsed #toc-nav {\n max-height: 0;\n }\n\n #toc.expanded #toc-nav {\n max-height: 75vh;\n }\n\n #toc.expanded .chevron-arrow svg {\n transform: rotate(180deg);\n }\n\n .chevron-arrow {\n visibility: visible;\n }\n}\n";
styleInject(css_248z);
/**
* Table of Contents (TOC) Plugin
* This plugin generates a Table of Contents for the article based on its headings.
* It scans the article for headings (h2 to h6) and creates a nested list structure.
* The TOC is inserted before the article in the DOM.
* It uses MutationObserver to detect dynamically
*/
document.addEventListener('agahi:ready', () => {
const article = document.getElementById('md');
if (!article) {
console.error('[Agahi] Article element with id="md" not found.');
return;
}
if (!document.getElementById('toc')) {
const wrapper = document.createElement('div');
wrapper.innerHTML = TOCHTML;
article.parentNode.insertBefore(wrapper.firstElementChild, article);
}
if (config.tocLabel) {
const tocTitle = document.querySelector('#toc-title #label');
tocTitle.textContent = config.tocLabel;
}
const buildTOC = () => {
const toc = document.getElementById('toc');
const tocList = toc?.querySelector('nav > ul');
if (!tocList) return;
tocList.innerHTML = '';
const headings = article.querySelectorAll(config.tocHeadings.join(', '));
if (headings.length === 0) {
const emptyItem = document.createElement('li');
emptyItem.classList.add('toc-empty');
emptyItem.textContent = 'Nothing to show';
tocList.appendChild(emptyItem);
return;
}
const stack = [{ level: 0, list: tocList }];
headings.forEach((heading) => {
const level = parseInt(heading.tagName[1], 10);
if (isNaN(level) || level < 1 || level > 6) return;
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `?id=${heading.id}`;
a.textContent = heading.textContent || `Untitled ${heading.tagName}`;
li.appendChild(a);
while (stack.length && stack[stack.length - 1].level >= level) {
stack.pop();
}
const parent = stack[stack.length - 1];
parent.list.appendChild(li);
const newList = document.createElement('ul');
li.appendChild(newList);
stack.push({ level, list: newList });
a.addEventListener('click', (e) => {
e.preventDefault();
setParams({ id: heading.id });
heading.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
});
const id = getParam('id');
if (id) {
const el = document.getElementById(id);
if (el) {
setTimeout(() => {
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 200);
}
}
};
const observer = new MutationObserver(buildTOC);
observer.observe(article, { childList: true, subtree: true });
document.getElementById('toc-title').addEventListener('click', function () {
const toc = document.getElementById('toc');
toc.classList.toggle('expanded');
toc.classList.toggle('collapsed');
});
});
})();