Clear all code?
scriptClose || styleOpen > styleClose
}
function handleAngleClose(e){
const pos = editor.selectionStart
if(pos !== editor.selectionEnd) return
const value = editor.value
const beforeCursor = value.slice(0, pos)
const lastOpen = beforeCursor.lastIndexOf("<")
if(lastOpen === -1) return
const tagText = beforeCursor.slice(lastOpen)
if(tagText.includes(">")) return
if(/^<\//.test(tagText)) return
if(/\/\s*$/.test(tagText)) return
if(inScriptOrStyle(value, lastOpen)) return
const nameMatch = tagText.match(/^<([a-zA-Z][a-zA-Z0-9-]*)/)
if(!nameMatch) return
const tagName = nameMatch[1]
if(VOID_TAGS.has(tagName.toLowerCase())) return
e.preventDefault()
insertText(">"+""+tagName+">")
editor.selectionStart = editor.selectionEnd = pos+1
}
function findUnclosedTag(value, ltPos){
const text = value.slice(0, ltPos)
const tagRe = /<\/?([a-zA-Z][a-zA-Z0-9-]*)[^<>]*>/g
const stack = []
let m
while((m = tagRe.exec(text))){
const isClose = m[0][1] === "/"
const selfClose = /\/\s*>$/.test(m[0])
const name = m[1].toLowerCase()
if(isClose){
for(let i=stack.length-1;i>=0;i--){
if(stack[i]===name){ stack.length = i; break }
}
} else if(!selfClose && !VOID_TAGS.has(name)){
stack.push(name)
}
}
return stack.length ? stack[stack.length-1] : null
}
function handleSlash(e){
const pos = editor.selectionStart
if(pos !== editor.selectionEnd) return
const value = editor.value
if(value[pos-1] !== "<") return
if(inScriptOrStyle(value, pos-1)) return
const tagName = findUnclosedTag(value, pos-1)
if(!tagName) return
const fullClose = ""+tagName+">"
if(value.slice(pos, pos+fullClose.length).toLowerCase() === fullClose.toLowerCase()){
e.preventDefault()
insertText("", pos-1, pos)
editor.selectionStart = editor.selectionEnd = (pos-1) + fullClose.length
return
}
e.preventDefault()
insertText("/"+tagName+">")
}
editor.addEventListener("keydown", e=>{
if(e.key === "Tab"){
e.preventDefault()
handleTab(e.shiftKey)
} else if(e.key === "Enter"){
e.preventDefault()
handleEnter()
} else if(e.key === "}" || e.key === "]" || e.key === ")"){
handleClosePunctuation(e, e.key)
} else if(e.key === "(" || e.key === "[" || e.key === "{" || e.key === "\"" || e.key === "'" || e.key === "`"){
handleOpenPair(e, e.key)
} else if(e.key === ">"){
handleAngleClose(e)
} else if(e.key === "/"){
handleSlash(e)
}
})
function handleFile(file){
if(!file) return
if(!file.name.toLowerCase().endsWith(".html")){ alert("Only HTML files allowed"); return }
const r=new FileReader()
r.onload=e=>{
editor.value=e.target.result
previousEditorValue=editor.value
fileNameInput.value = sanitizeFileName(file.name)
localStorage.setItem(FILENAME_KEY, fileNameInput.value)
run(); saveCode()
}
r.readAsText(file)
}
function openFilePicker(){
fileInput.value = ""
fileInput.click()
}
function isFileDrag(e){
return e.dataTransfer && Array.from(e.dataTransfer.types||[]).includes("Files")
}
let liveTimer
let highlightTimer
const LARGE_FILE_LINE_THRESHOLD = 300 // above this, full re-tokenization on every keystroke is slow enough to need debouncing
editor.addEventListener("input",()=>{
const lineCount = editor.value.split("\n").length
if(previewError){
const delta = lineCount - previewError.totalLines
if(delta !== 0){
const prefixLen = commonPrefixLength(previousEditorValue, editor.value)
const editLine = previousEditorValue.slice(0, prefixLen).split("\n").length
if(editLine <= previewError.line) previewError.line += delta
previewError.totalLines = lineCount
}
}
previousEditorValue = editor.value
updateSize(); updateLineNums(); updateActiveLine(); updateErrorDisplay()
if(!findBox.hidden) runFindSearch()
clearTimeout(liveTimer)
liveTimer=setTimeout(()=>{ run(); saveCode() },250)
if(lineCount <= LARGE_FILE_LINE_THRESHOLD){
// small/medium file — full re-tokenization is fast enough to just do it immediately, no flicker
clearTimeout(highlightTimer)
updateIndentGuides()
updateSyntaxHighlight()
refreshHighlightVisibility()
return
}
editor.style.color = "var(--ink)" // large file: show real text immediately while the colored backdrop is stale/debounced
highlightBackdrop.style.visibility = "hidden" // ...and hide the stale backdrop so it doesn't double-render behind the real text
clearTimeout(highlightTimer)
highlightTimer=setTimeout(()=>{
updateIndentGuides()
updateSyntaxHighlight()
refreshHighlightVisibility() // backdrop is caught up — hide the real text behind it again (unless syntax highlighting is off)
},150)
})
editor.addEventListener("click",()=>{ updateLineNums(); updateActiveLine() })
editor.addEventListener("keyup",()=>{ updateLineNums(); updateActiveLine() })
editor.addEventListener("scroll",()=>{ lnInner.style.transform=`translateY(${-editor.scrollTop}px)`; updateActiveLine(); syncIndentGuidesScroll(); syncFindHighlightsScroll(); syncHighlightScroll(); updateErrorDisplay() })
window.addEventListener("message", e=>{
if(!e.data || !e.data.__previewpadError) return
if(e.source !== preview.contentWindow) return
if(!settings.errorIdentification) return
previewError = { line: e.data.lineno || 1, message: e.data.message || "Error", totalLines: editor.value.split("\n").length }
updateLineNums()
updateErrorDisplay()
})
window.addEventListener("resize",()=>{ charWidthCache=null; updateIndentGuides() })
window.addEventListener("dragover",e=>e.preventDefault())
window.addEventListener("drop",e=>e.preventDefault())
editorCard.addEventListener("dragover",e=>{ if(!isFileDrag(e)) return; e.preventDefault(); editorCard.classList.add("drag-active") })
editorCard.addEventListener("dragleave",()=>editorCard.classList.remove("drag-active"))
editorCard.addEventListener("drop",e=>{ if(!isFileDrag(e)) return; e.preventDefault(); editorCard.classList.remove("drag-active"); handleFile(e.dataTransfer.files[0]) })
fileInput.addEventListener("change",e=>handleFile(e.target.files[0]))
const divider=document.getElementById("divider")
const container=document.querySelector(".container")
let dragging=false, pendingX=null, dragRaf=null
function applyDrag(){
dragRaf=null
if(pendingX===null) return
const r=container.getBoundingClientRect()
let p=((pendingX-r.left)/r.width)*100
p=Math.max(20,Math.min(80,p))
document.documentElement.style.setProperty("--editor-width",p+"%")
pendingX=null
}
divider.addEventListener("pointerdown",e=>{dragging=true;divider.setPointerCapture(e.pointerId);divider.classList.add("active");document.body.classList.add("dragging");e.preventDefault()})
document.addEventListener("pointermove",e=>{
if(!dragging)return
pendingX=e.clientX
if(dragRaf===null) dragRaf=requestAnimationFrame(applyDrag)
})
document.addEventListener("pointerup",e=>{
if(!dragging)return
dragging=false
if(dragRaf!==null){cancelAnimationFrame(dragRaf);dragRaf=null}
applyDrag()
divider.classList.remove("active")
document.body.classList.remove("dragging")
const w=document.documentElement.style.getPropertyValue("--editor-width")
if(w)localStorage.setItem("editorWidth",w)
})
divider.addEventListener("dblclick",()=>{document.documentElement.style.setProperty("--editor-width","33.33%");localStorage.removeItem("editorWidth")})
const saved=localStorage.getItem("editorWidth")
if(saved)document.documentElement.style.setProperty("--editor-width",saved)
function syncThemeToggle(){
const isDark = document.documentElement.getAttribute("data-theme")==="dark"
themeToggle.setAttribute("aria-checked", isDark ? "true":"false")
}
themeToggle.addEventListener("click",()=>{
const next = document.documentElement.getAttribute("data-theme")==="dark" ? "light":"dark"
document.documentElement.setAttribute("data-theme", next)
localStorage.setItem("theme", next)
syncThemeToggle()
updateIndentGuides()
})
syncThemeToggle()
fileNameInput.value = localStorage.getItem(FILENAME_KEY) || "index"
fileNameInput.addEventListener("blur", commitFileName)
fileNameInput.addEventListener("keydown", e=>{
if(e.key === "Enter"){ e.preventDefault(); fileNameInput.blur() }
})
applySettings()
run(); updateSize()