No Login Data Private Local Save

HTML to Pug Converter - Online Indented Template Syntax

7
0
0
0

HTML to Pug Converter

Convert standard HTML markup into clean, indented Pug (Jade) template syntax instantly. Free online tool for developers.

Indent: Samples:
Card Navbar Form Table Full Page
HTML Input 0 chars
Pug Output 0 lines
Pug lines: 0 Pug chars: 0 Reduction: 0%
Copied to clipboard!

Frequently Asked Questions

Pug is a high-performance template engine for Node.js that uses indentation-based syntax instead of closing tags. Originally named Jade, it was renamed to Pug in 2016. Pug compiles to HTML and is widely used with Express.js, making server-side rendering cleaner and more readable. Its concise syntax eliminates the need for angle brackets and closing tags, reducing template file sizes by 30-60% compared to equivalent HTML.

Converting HTML to Pug offers several advantages: (1) Cleaner, more readable code with less visual noise. (2) Reduced file size — Pug files are typically 30-50% smaller. (3) Easier maintenance with indentation-based nesting. (4) Built-in support for variables, mixins, conditionals, and iteration. (5) Seamless integration with Express.js and other Node frameworks. This converter helps you migrate existing HTML templates to Pug format effortlessly.

This tool parses your HTML using the browser's native DOMParser, then recursively walks the DOM tree to generate Pug syntax. Tags lose their angle brackets, classes become dot-prefixed (.classname), IDs become hash-prefixed (#idname), and other attributes go in parentheses. Nesting is expressed through consistent indentation. <script> and <style> blocks use Pug's dot-block syntax for clean multiline content.

Pug is whitespace-sensitive — consistent indentation is mandatory. The most common convention is 2 spaces per indentation level (used by the Pug team and most projects). Some teams prefer 4 spaces. Never mix spaces and tabs in a single Pug file, as this will cause parsing errors. This converter lets you choose your preferred indentation style and size before converting.

This converter handles standard HTML5 excellently. Some edge cases to note: (1) IE conditional comments are preserved as Pug comments. (2) Inline SVG is converted faithfully. (3) HTML entities are decoded by the parser — the resulting Pug handles them natively. (4) Very deeply nested HTML (20+ levels) may produce visually deep indentation but remains syntactically valid. (5) Custom Web Components (e.g., <my-element>) are supported as-is in Pug. For production use, always review the converted output.

Yes! The converter processes HTML entirely in your browser — no data is sent to any server. It can handle large files (50,000+ lines) efficiently using the browser's native DOM parser. For extremely large files, conversion typically completes in under a second. The output is generated synchronously for reliability. Your HTML stays 100% private and never leaves your device.

Key differences: (1) Pug uses indentation instead of closing tags — no more </div>. (2) Attributes use parentheses: a(href="/") instead of <a href="/">. (3) Classes and IDs have shorthand: div.card.active#main. (4) Pug supports logic: variables, loops, conditionals, and mixins natively. (5) Text content uses pipes (|) or inline placement. This makes Pug ideal for dynamic template rendering in Node.js applications.

Save the output as a .pug file and include it in your Node.js project. With Express.js: app.set('view engine', 'pug'). Then render it with res.render('template', { data }). You can also use the pug CLI to compile Pug to HTML: pug template.pug --out ./output. For static site generation, tools like pug-cli can batch-compile entire directories of Pug files to HTML.
` }; function getIndentStr() { const size = parseInt(indentSize.value) || 2; if (indentType.value === 'tab') return '\t'; return ' '.repeat(size); } function getIndentUnit() { const size = parseInt(indentSize.value) || 2; if (indentType.value === 'tab') return '\t'; return ' '.repeat(size); } function isWhitespaceOnly(text) { return /^\s*$/.test(text); } function hasPreserveAncestor(node, root) { let current = node; while (current && current !== root) { if (current.nodeType === Node.ELEMENT_NODE && PRESERVE_WHITESPACE.has(current.tagName.toLowerCase())) { return true; } current = current.parentNode; } return false; } function getAttrString(el) { const tagName = el.tagName.toLowerCase(); let classStr = ''; let idStr = ''; const otherAttrs = []; for (const attr of el.attributes) { const name = attr.name; const value = attr.value; if (name === 'class' && value) { classStr = '.' + value.trim().split(/\s+/).join('.'); } else if (name === 'id' && value) { idStr = '#' + value; } else { // Handle boolean attributes if (value === '') { otherAttrs.push(name); } else { // Escape double quotes in attribute values const escaped = value.replace(/"/g, '\\"'); otherAttrs.push(name + '="' + escaped + '"'); } } } let result = tagName + classStr + idStr; if (otherAttrs.length > 0) { result += '(' + otherAttrs.join(', ') + ')'; } return result; } function nodeToPug(node, indentLevel, rootDoc, indentStr) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent; if (isWhitespaceOnly(text) && !hasPreserveAncestor(node, rootDoc)) { return ''; } return text; } if (node.nodeType === Node.COMMENT_NODE) { const commentText = node.textContent.trim(); const prefix = indentStr.repeat(indentLevel); if (commentText) { return prefix + '// ' + commentText; } return prefix + '//'; } if (node.nodeType === Node.ELEMENT_NODE) { const tagName = node.tagName.toLowerCase(); const attrStr = getAttrString(node); const children = Array.from(node.childNodes); const prefix = indentStr.repeat(indentLevel); // Filter out insignificant whitespace text nodes const significantChildren = children.filter(child => { if (child.nodeType === Node.TEXT_NODE) { if (hasPreserveAncestor(node, rootDoc)) return true; return !isWhitespaceOnly(child.textContent); } return true; }); // Handle dot-block elements (script, style) with text content if (DOT_BLOCK_ELEMENTS.has(tagName)) { const textContent = node.textContent || ''; const hasText = significantChildren.length > 0 && significantChildren.some(c => c.nodeType === Node.TEXT_NODE && !isWhitespaceOnly(c.textContent)); if (hasText && !node.hasAttribute('src')) { // Dot-block syntax const lines = textContent.split('\n'); // Trim leading/trailing empty lines but preserve internal structure let trimmedLines = textContent.replace(/^\n+/, '').replace(/\n+$/, ''); if (!trimmedLines) trimmedLines = textContent.trim(); const contentIndent = indentStr.repeat(indentLevel + 1); const indentedContent = trimmedLines.split('\n').map(l => contentIndent + l).join('\n'); return prefix + attrStr + '.\n' + indentedContent; } } if (significantChildren.length === 0) { // Self-closing / void elements or empty elements if (VOID_ELEMENTS.has(tagName) && !node.hasAttributes()) { return prefix + tagName; } if (VOID_ELEMENTS.has(tagName)) { return prefix + attrStr; } if (!node.hasAttributes() && tagName !== 'div') { return prefix + tagName; } return prefix + attrStr; } // Check if the only child is a single text node (short text) if (significantChildren.length === 1 && significantChildren[0].nodeType === Node.TEXT_NODE) { const text = significantChildren[0].textContent; const cleanText = text.trim(); // Use inline text for short single-line content if (cleanText.length <= 60 && !cleanText.includes('\n') && cleanText.length > 0) { return prefix + attrStr + ' ' + cleanText; } // For longer or multiline text, use pipe syntax const contentIndent = indentStr.repeat(indentLevel + 1); const lines = text.split('\n'); // Filter to meaningful lines const meaningfulLines = lines.filter(l => !isWhitespaceOnly(l) || hasPreserveAncestor(node, rootDoc)); if (meaningfulLines.length === 0 && lines.length > 0) { // All whitespace - preserve if in pre if (hasPreserveAncestor(node, rootDoc)) { const pipedLines = lines.map(l => contentIndent + '| ' + l).join('\n'); return prefix + attrStr + '\n' + pipedLines; } return prefix + attrStr; } if (meaningfulLines.length === 1 && meaningfulLines[0].length <= 60 && !hasPreserveAncestor(node, rootDoc)) { return prefix + attrStr + ' ' + meaningfulLines[0].trim(); } const pipedLines = text.split('\n').map(l => contentIndent + '| ' + l).join('\n'); return prefix + attrStr + '\n' + pipedLines; } // Multiple children or mixed content let result = prefix + attrStr; for (const child of significantChildren) { const childPug = nodeToPug(child, indentLevel + 1, rootDoc, indentStr); if (childPug) { result += '\n' + childPug; } } return result; } // Document node if (node.nodeType === Node.DOCUMENT_NODE) { let resultParts = []; // Check for doctype if (node.doctype && node.doctype.name) { resultParts.push('doctype ' + node.doctype.name); } // Process document element (html) const docEl = node.documentElement; if (docEl) { const docPug = nodeToPug(docEl, 0, node, indentStr); if (docPug) resultParts.push(docPug); } return resultParts.join('\n'); } // DocumentFragment or other if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { const parts = []; for (const child of Array.from(node.childNodes)) { const childPug = nodeToPug(child, indentLevel, rootDoc, indentStr); if (childPug) parts.push(childPug); } return parts.join('\n'); } return ''; } function convertHtmlToPug(htmlString) { const parser = new DOMParser(); const doc = parser.parseFromString(htmlString, 'text/html'); // Check for parse errors const parseErrorEl = doc.querySelector('parsererror'); if (parseErrorEl) { throw new Error('HTML parsing failed: ' + parseErrorEl.textContent); } const indentStr = getIndentUnit(); // Determine if input is a full document or fragment const hasDoctype = /]*>/i.test(htmlString.trim()); const hasHtmlTag = /]/i.test(htmlString.trim()); const isFullDocument = hasDoctype || hasHtmlTag; if (isFullDocument) { // Convert full document including doctype let resultParts = []; if (hasDoctype) { const doctypeMatch = htmlString.trim().match(/]*>/i); if (doctypeMatch) { const dt = doctypeMatch[0]; // Extract doctype name const nameMatch = dt.match(/ 0 && outputVal.length > 0) { const pct = Math.round((1 - outputVal.length / inputVal.length) * 100); reduction.textContent = Math.max(0, pct) + '%'; } else { reduction.textContent = '0%'; } } function doConvert() { const htmlString = inputEl.value.trim(); if (!htmlString) { outputEl.value = ''; updateStats(); return; } try { const pugResult = convertHtmlToPug(htmlString); outputEl.value = pugResult; } catch (err) { outputEl.value = '// Error converting HTML: ' + err.message + '\n// Please check your HTML syntax.'; } updateStats(); } function showToast(msg) { toast.textContent = msg; toast.classList.add('show'); clearTimeout(toast._timeout); toast._timeout = setTimeout(() => { toast.classList.remove('show'); }, 2000); } function loadSample(name) { if (samples[name]) { inputEl.value = samples[name]; doConvert(); // Update active chip sampleChips.forEach(chip => { chip.classList.toggle('active', chip.dataset.sample === name); }); } } // Event listeners convertBtn.addEventListener('click', doConvert); copyBtn.addEventListener('click', () => { const outputVal = outputEl.value; if (!outputVal) { showToast('Nothing to copy!'); return; } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(outputVal).then(() => { showToast('âś“ Pug code copied to clipboard!'); }).catch(() => { fallbackCopy(outputVal); }); } else { fallbackCopy(outputVal); } }); function fallbackCopy(text) { outputEl.removeAttribute('readonly'); outputEl.select(); outputEl.setSelectionRange(0, 999999); try { document.execCommand('copy'); showToast('âś“ Pug code copied to clipboard!'); } catch (e) { showToast('Failed to copy. Please select and copy manually.'); } outputEl.setAttribute('readonly', 'readonly'); window.getSelection().removeAllRanges(); } downloadBtn.addEventListener('click', () => { const outputVal = outputEl.value; if (!outputVal) { showToast('Nothing to download!'); return; } const blob = new Blob([outputVal], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'template.pug'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('âś“ Downloaded as template.pug'); }); clearBtn.addEventListener('click', () => { inputEl.value = ''; outputEl.value = ''; updateStats(); sampleChips.forEach(chip => chip.classList.remove('active')); showToast('Both panels cleared'); }); pasteBtn.addEventListener('click', async () => { try { if (navigator.clipboard && navigator.clipboard.readText) { const text = await navigator.clipboard.readText(); inputEl.value = text; doConvert(); showToast('âś“ Pasted from clipboard'); } else { showToast('Clipboard access not supported. Please paste manually.'); } } catch (e) { showToast('Could not access clipboard. Please paste manually.'); } }); indentType.addEventListener('change', () => { if (outputEl.value) doConvert(); }); indentSize.addEventListener('change', () => { if (outputEl.value) doConvert(); }); // Sample chips sampleChips.forEach(chip => { chip.addEventListener('click', () => { loadSample(chip.dataset.sample); }); }); // Auto-convert on input with debounce let debounceTimer; inputEl.addEventListener('input', () => { updateStats(); clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { if (inputEl.value.trim()) { doConvert(); } }, 500); }); // Also update stats on input inputEl.addEventListener('input', updateStats); // Load default sample loadSample('card'); // Initial stats update updateStats(); // Keyboard shortcut: Ctrl+Enter to convert inputEl.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); doConvert(); } }); })();