UNPKG

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
(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'); }); }); })();