UNPKG

semantic-heading-hierarchy

Version:

A JavaScript library that automatically invisibly corrects improper heading hierarchies for better accessibility. Used for user or admin edited content where the developer doesn't have 100% direct control over the content.

230 lines (225 loc) 8.62 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { SemanticHeadingHierarchy: () => SemanticHeadingHierarchy, default: () => index_default }); module.exports = __toCommonJS(index_exports); // src/core.ts function healHeadings(containerOrSelector = document.body, options = false) { let logResults = false; let classPrefix = "hs-"; let forceSingleH1 = false; if (typeof options === "boolean") { logResults = options; } else if (typeof options === "object" && options !== null) { logResults = options.logResults || false; classPrefix = options.classPrefix || "hs-"; forceSingleH1 = options.forceSingleH1 || false; } if (typeof localStorage !== "undefined") { const logOverride = localStorage.getItem("healHeadings.logResults"); if (logOverride !== null) { logResults = logOverride === "true"; } } let container; if (typeof containerOrSelector === "string") { const elements = document.querySelectorAll(containerOrSelector); if (elements.length === 0) { console.warn(`No elements found for selector: ${containerOrSelector}`); return; } if (elements.length > 1) { console.error(`Multiple elements found for selector: ${containerOrSelector}. Selector must match exactly one element.`); return; } container = elements[0]; } else { container = containerOrSelector; } if (!container || !(container instanceof Element)) { console.warn("Invalid container provided to healHeadings"); return; } const h1Element = container.querySelector("h1"); if (!h1Element) { if (logResults) { console.log("No H1 found - skipping heading structure fix"); } return; } const allHeadings = Array.from(container.querySelectorAll("h1, h2, h3, h4, h5, h6")); const h1Index = allHeadings.indexOf(h1Element); const headingsBeforeH1 = allHeadings.slice(0, h1Index); if (headingsBeforeH1.length > 0) { const headingTypes = headingsBeforeH1.map((h) => h.tagName.toLowerCase()).join(", "); console.warn(`\u26A0\uFE0F Found ${headingsBeforeH1.length} heading(s) before H1: ${headingTypes}. These headings will be ignored for accessibility compliance. Consider restructuring your HTML to place all content headings after the main H1.`); } const additionalH1s = allHeadings.slice(h1Index + 1).filter((h) => h.tagName.toLowerCase() === "h1"); if (additionalH1s.length > 0) { if (forceSingleH1) { console.warn(`\u26A0\uFE0F Found ${additionalH1s.length} additional H1 element(s) after the first H1. These will be converted to H2 elements due to forceSingleH1 option.`); } else { console.warn(`\u26A0\uFE0F Found ${additionalH1s.length} additional H1 element(s) after the first H1. These will be ignored. Consider using the forceSingleH1 option to convert them to H2 elements.`); } } const headingsAfterH1 = allHeadings.slice(h1Index + 1); let headings; if (forceSingleH1) { headings = headingsAfterH1; } else { headings = headingsAfterH1.filter((heading) => heading.tagName.toLowerCase() !== "h1"); } if (headings.length === 0) { if (logResults) { console.log("No H2-H6 headings found after H1 - nothing to fix"); } return; } if (logResults) { console.log(`Found ${headings.length} heading(s) to process after H1`); } let previousLevel = 1; const elementsToReplace = []; for (let i = 0; i < headings.length; i++) { const heading = headings[i]; const originalTag = heading.tagName.toLowerCase(); const originalLevel = parseInt(originalTag.charAt(1), 10); const listItem = heading.closest("li"); if (listItem) { const parentList = listItem.parentElement; if (parentList) { const siblingItems = parentList.querySelectorAll("li"); if (siblingItems.length > 1) { if (logResults) { console.log(`Skipping ${originalTag} in list with ${siblingItems.length} items`); } continue; } } } let newLevel; if (originalLevel === 1 && forceSingleH1) { newLevel = 2; } else if (originalLevel <= previousLevel) { newLevel = Math.max(2, originalLevel); } else { newLevel = Math.min(originalLevel, previousLevel + 1); } newLevel = Math.max(2, Math.min(newLevel, 6)); if (newLevel !== originalLevel) { elementsToReplace.push({ original: heading, newLevel, originalLevel, originalTag }); if (logResults) { console.log(`Will change ${originalTag.toUpperCase()} \u2192 H${newLevel} (will add ${classPrefix}${originalLevel} class)`); } } previousLevel = newLevel; } elementsToReplace.forEach((item) => { const { original, newLevel, originalLevel, originalTag } = item; original.classList.add(`${classPrefix}${originalLevel}`); const newHeading = document.createElement(`h${newLevel}`); Array.from(original.attributes).forEach((attr) => { newHeading.setAttribute(attr.name, attr.value); }); newHeading.innerHTML = original.innerHTML; newHeading.setAttribute("data-prev-heading", originalLevel.toString()); if (original.parentNode) { original.parentNode.replaceChild(newHeading, original); } if (logResults) { console.log(`Replaced ${originalTag.toUpperCase()} with H${newLevel}, added ${classPrefix}${originalLevel} class`); } }); if (logResults) { console.log(`Heading structure fix complete. Modified ${elementsToReplace.length} heading(s)`); } } // src/logging.ts function enableHeadingLogging() { if (typeof localStorage !== "undefined") { localStorage.setItem("healHeadings.logResults", "true"); console.log("\u2705 Detailed heading healing logging ENABLED globally"); } else { console.warn("localStorage not available - cannot enable global logging"); } } function disableHeadingLogging() { if (typeof localStorage !== "undefined") { localStorage.setItem("healHeadings.logResults", "false"); console.log("\u274C Detailed heading healing logging DISABLED globally"); } else { console.warn("localStorage not available - cannot disable global logging"); } } function clearHeadingLogging() { if (typeof localStorage !== "undefined") { localStorage.removeItem("healHeadings.logResults"); console.log("\u{1F504} Heading healing logging reset - will use function parameter"); } else { console.warn("localStorage not available - cannot clear global logging"); } } function getHeadingLoggingStatus() { if (typeof localStorage !== "undefined") { const setting = localStorage.getItem("healHeadings.logResults"); if (setting === null) { console.log("\u{1F4CB} Heading healing logging: Using function parameter (no override set)"); } else { console.log(`\u{1F4CB} Heading healing logging: ${setting === "true" ? "ENABLED" : "DISABLED"} (localStorage override)`); } return setting; } else { console.log("\u{1F4CB} Heading healing logging: localStorage not available"); return null; } } function createLoggingInterface() { return { enable: enableHeadingLogging, disable: disableHeadingLogging, clear: clearHeadingLogging, status: getHeadingLoggingStatus }; } // src/index.ts var SemanticHeadingHierarchy = { /** * Fix heading hierarchies in the specified container * @param containerOrSelector - CSS selector string or DOM element to search within * @param options - Options object or boolean for logResults (for backwards compatibility) */ fix: healHeadings, /** * Logging control methods */ logging: createLoggingInterface() }; var index_default = SemanticHeadingHierarchy; if (typeof window !== "undefined") { window.SemanticHeadingHierarchy = SemanticHeadingHierarchy; }