HTML Heading Structure Fixer📝 Input HTML
👀 Heading Structure Preview
\n';
output += '';
return output;
}convertBoldToHeadings(doc) {
const paragraphs = doc.querySelectorAll('p');
paragraphs.forEach(p => {
const strong = p.querySelector('strong, b');
if (!strong) return;const pText = p.textContent.trim();
const strongText = strong.textContent.trim();// Check if the paragraph is short and bold text is significant
if (pText.length < 100 && strongText.length > 5 &&
(pText === strongText || strongText.length / pText.length > 0.7)) {
// Check if next sibling has content (indicating this might be a heading)
const nextSibling = p.nextElementSibling;
if (nextSibling && nextSibling.textContent.trim().length > 0) {
// Determine appropriate heading level based on context
let level = this.determineHeadingLevel(doc, p);
const heading = doc.createElement(`h${level}`);
heading.innerHTML = p.innerHTML;
this.setAttributes(heading, this.getAttributes(p));
p.parentNode.replaceChild(heading, p);
this.stats.converted++;
this.stats.changes++;
}
}
});
}determineHeadingLevel(doc, element) {
// Look at previous headings to determine appropriate level
const allHeadings = Array.from(doc.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const elementIndex = Array.from(doc.body.children).indexOf(element);
let previousHeading = null;
for (let i = 0; i < allHeadings.length; i++) {
const headingIndex = Array.from(doc.body.children).indexOf(allHeadings[i]);
if (headingIndex < elementIndex) {
previousHeading = allHeadings[i];
} else {
break;
}
}if (!previousHeading) {
return doc.querySelector('h1') ? 2 : 1;
}const prevLevel = parseInt(previousHeading.tagName.substring(1));
return Math.min(prevLevel + 1, 6);
}removeEmptyHeadings(doc) {
const headings = doc.querySelectorAll('h1, h2, h3, h4, h5, h6');
headings.forEach(heading => {
if (heading.textContent.trim() === '') {
heading.remove();
this.stats.removed++;
this.stats.changes++;
}
});
}ensureSingleH1(doc) {
const h1s = doc.querySelectorAll('h1');
if (h1s.length > 1) {
// Keep the first h1, convert others to h2
for (let i = 1; i < h1s.length; i++) {
const h2 = doc.createElement('h2');
h2.innerHTML = h1s[i].innerHTML;
this.setAttributes(h2, this.getAttributes(h1s[i]));
h1s[i].parentNode.replaceChild(h2, h1s[i]);
this.stats.changes++;
}
this.warnings.push({
type: 'multiple-h1',
message: `Found ${h1s.length} H1 tags. Kept the first one and converted ${h1s.length - 1} to H2.`
});
}
}addMissingH1(doc) {
const h1 = doc.querySelector('h1');
if (!h1) {
const h2 = doc.querySelector('h2');
if (h2) {
const newH1 = doc.createElement('h1');
newH1.innerHTML = h2.innerHTML;
this.setAttributes(newH1, this.getAttributes(h2));
h2.parentNode.replaceChild(newH1, h2);
this.stats.changes++;
this.warnings.push({
type: 'missing-h1',
message: 'No H1 found. Promoted first H2 to H1.'
});
} else {
this.warnings.push({
type: 'no-h1',
message: 'No H1 found in document. Consider adding one for accessibility.'
});
}
}
}fixSkippedLevels(doc) {
const headings = Array.from(doc.querySelectorAll('h1, h2, h3, h4, h5, h6'));
let expectedLevel = 1;
let hasH1 = false;headings.forEach((heading, index) => {
const currentLevel = parseInt(heading.tagName.substring(1));
if (currentLevel === 1) {
hasH1 = true;
expectedLevel = 2;
return;
}if (!hasH1) {
expectedLevel = currentLevel;
hasH1 = true;
return;
}// Check if level is skipped
if (currentLevel > expectedLevel + 1) {
const newLevel = expectedLevel + 1;
const newHeading = doc.createElement(`h${newLevel}`);
newHeading.innerHTML = heading.innerHTML;
this.setAttributes(newHeading, this.getAttributes(heading));
heading.parentNode.replaceChild(newHeading, heading);
this.stats.changes++;
expectedLevel = newLevel;
} else {
expectedLevel = currentLevel;
}
});
}flagHeadingOnlySections(doc) {
const headings = doc.querySelectorAll('h1, h2, h3, h4, h5, h6');
headings.forEach(heading => {
let hasContent = false;
let next = heading.nextElementSibling;
// Check until we hit another heading
while (next && !next.matches('h1, h2, h3, h4, h5, h6')) {
if (next.textContent.trim().length > 0) {
hasContent = true;
break;
}
next = next.nextElementSibling;
}
if (!hasContent) {
this.warnings.push({
type: 'empty-section',
message: `Heading "${heading.textContent.trim()}" has no content following it.`
});
}
});
}formatHTML(html) {
// Simple HTML formatting with indentation
let formatted = '';
let indent = 0;
const lines = html.split(/>\s*);
lines.forEach((line, index) => {
if (index > 0) line = '<' + line;
if (index < lines.length - 1) line = line + '>';
if (line.match(/^<\/\w/) || line.match(/^<\w[^>]*\/>/)) {
indent = Math.max(0, indent - 1);
}
formatted += ' '.repeat(indent) + line.trim() + '\n';
if (line.match(/^<\w[^>]*[^\/]>/) && !line.match(/<\/(h[1-6]|p|span|strong|em|b|i|a)>/)) {
indent++;
}
});
return formatted.trim();
}buildHeadingTree(html) {
const doc = this.parseHTML(html);
const headings = this.extractHeadings(doc);
if (headings.length === 0) {
return '
';
}
let html_output = '
';
headings.forEach(heading => {
const className = `beltab-tree-item beltab-h${heading.level}`;
const text = heading.text || '(empty)';
html_output += `- H${heading.level}: ${this.escapeHtml(text)}
`;
});
html_output += '
';
return html_output;
}escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}// Initialize the application
const fixer = new HeadingFixer();
document.getElementById('fixButton').addEventListener('click', () => {
const input = document.getElementById('inputHtml').value;
if (!input.trim()) {
alert('Please enter some HTML code to fix.');
return;
}const options = {
fixSkipped: document.getElementById('fixSkipped').checked,
ensureSingleH1: document.getElementById('ensureSingleH1').checked,
fixEmpty: document.getElementById('fixEmpty').checked,
convertBold: document.getElementById('convertBold').checked,
addMissingH1: document.getElementById('addMissingH1').checked,
flagEmptySections: document.getElementById('flagEmptySections').checked
};const result = fixer.fixHTML(input, options);// Show before/after preview
document.getElementById('beforePreview').innerHTML = fixer.buildHeadingTree(input);
document.getElementById('afterPreview').innerHTML = fixer.buildHeadingTree(result.html);
document.getElementById('previewContainer').style.display = 'block';// Show output
document.getElementById('outputHtml').value = result.html;
document.getElementById('outputContainer').style.display = 'block';// Show stats
const statsHtml = `
${result.stats.original}
Original Headings
${result.stats.fixed}
Final Headings
${result.stats.changes}
Changes Made
${result.stats.removed}
Removed
${result.stats.converted}
Converted
`;
document.getElementById('stats').innerHTML = statsHtml;
document.getElementById('statsContainer').style.display = 'block';// Show warnings
if (result.warnings.length > 0) {
const warningsHtml = result.warnings.map(w => `
${w.type}
${w.message}
`).join('');
document.getElementById('warnings').innerHTML = warningsHtml;
document.getElementById('warningsContainer').style.display = 'block';
} else {
document.getElementById('warningsContainer').style.display = 'none';
}// Scroll to results
document.getElementById('previewContainer').scrollIntoView({ behavior: 'smooth', block: 'start' });
});document.getElementById('copyButton').addEventListener('click', () => {
const output = document.getElementById('outputHtml');
output.select();
document.execCommand('copy');
const button = document.getElementById('copyButton');
button.textContent = '✅ Copied!';
button.classList.add('copied');
setTimeout(() => {
button.textContent = '📋 Copy to Clipboard';
button.classList.remove('copied');
}, 2000);
});document.getElementById('clearButton').addEventListener('click', () => {
if (confirm('Are you sure you want to clear all fields?')) {
document.getElementById('inputHtml').value = '';
document.getElementById('outputHtml').value = '';
document.getElementById('previewContainer').style.display = 'none';
document.getElementById('outputContainer').style.display = 'none';
document.getElementById('statsContainer').style.display = 'none';
document.getElementById('warningsContainer').style.display = 'none';
}
});// Add sample HTML button for testing
document.addEventListener('DOMContentLoaded', () => {
const sampleButton = document.createElement('button');
sampleButton.className = 'beltab-button';
sampleButton.textContent = '📄 Load Sample HTML';
sampleButton.style.background = '#3498db';
sampleButton.addEventListener('click', () => {
const sampleHTML = `
Welcome to My Website
This is the introduction.
Another Main Title
This shouldn't be h1.
Skipped from H1 to H4
This heading skipped levels.
This Looks Like a Heading
And it's followed by regular content, so it probably should be a heading.
Proper Heading
Some content here.
Another skip to H5
More content.
Section Without Content
Another Section
This one has content.
`;
document.getElementById('inputHtml').value = sampleHTML;
});
document.querySelector('.beltab-button-group').appendChild(sampleButton);
});