- Download htmx.min.js v2.0.10 and _hyperscript.min.js v0.9.91 locally - Update head-scripts.html to load from /static/ instead of unpkg CDN - Remove https://unpkg.com from CSP script-src whitelist - Update all documentation references to reflect self-hosted paths - No breaking changes: all hx-* attributes are HTMX 2.0 compatible
11 KiB
Hyperscript Development Rules
MANDATORY RULES - ALWAYS FOLLOW
Rule 1: Code Cleanliness
More than 3 lines of hyperscript → Move to function in file
- Inline hyperscript in HTML should be kept minimal (≤3 lines)
- Longer logic MUST be extracted to named functions in ._hs files
- HTML templates should be clean and readable
Rule 2: File Structure - Organized by Category
✅ NO def statement limit with latest hyperscript!
HISTORICAL NOTE (2025-11-17): Hyperscript 0.9.12 had a 3 def limit. Latest version REMOVED this limitation.
- Test verification:
tests/mjs/9-hyperscript-def-limit.test.mjs(all 5 def statements passed) - Migration in progress: Moving functions from JavaScript back to hyperscript
Current Best Practice: Organize hyperscript functions by category in separate files
static/hyperscript/
├── toggles._hs # Toggle functions (CV length, icons, theme)
├── hover-sync._hs # Hover synchronization functions
├── utils._hs # Utility functions (print, scroll, etc.)
└── keyboard._hs # Keyboard shortcut handlers
Benefits of Hyperscript Organization:
- ✅ Clean separation of concerns
- ✅ Easy to locate and maintain functions
- ✅ No artificial limitations
- ✅ Server-side hypermedia pattern (functions loaded with HTML)
- ✅ Still callable from anywhere using
call functionName(args)
Rule 3: HTML Structure Cleanliness
HTML must be as clean as possible regarding hyperscript
✅ GOOD - Clean, readable:
<input type="checkbox"
id="lengthToggle"
_="on change call toggleCVLength(my.checked)">
❌ BAD - Inline logic nightmare:
<input type="checkbox"
id="lengthToggle"
_="on change
if my.checked
remove .cv-short from .cv-paper
add .cv-long to .cv-paper
set localStorage['cv-length'] to 'long'
set #lengthToggleMenu's checked to true
else
remove .cv-long from .cv-paper
add .cv-short to .cv-paper
set localStorage['cv-length'] to 'short'
set #lengthToggleMenu's checked to false
end">
Rule 4: Event Handler Externalization Guidelines (2025-11-20)
Know what CAN and CANNOT be externalized
✅ CAN Be Externalized:
Simple function calls without complex event inspection:
<!-- Navigation handlers -->
<a href="#section" _="on click call scrollToSection(event, 'section')">
<!-- Toggle handlers -->
<input _="on change call toggleCVLength(my.checked)">
<!-- Hover handlers -->
<button _="on mouseenter call syncPdfHover(true)
on mouseleave call syncPdfHover(false)">
External function example:
def scrollToSection(event, sectionId)
call event.preventDefault()
set element to document.getElementById(sectionId)
if element then call element.scrollIntoView({behavior: 'smooth'}) end
end
❌ MUST Stay Inline:
Complex event handlers that inspect event properties:
<!-- Keyboard handlers with event.key, event.target inspection -->
<body _="on keydown
set tagName to event.target.tagName
set isInputField to (tagName is 'INPUT' or tagName is 'TEXTAREA')
if event.key is 'l' and not event.ctrlKey and not isInputField
halt the event
-- handler logic here
end
end">
Why: The event variable in _="" attributes is a hyperscript runtime variable. External def functions don't have direct access to this event context, causing scoping issues.
🎯 Optimization for Inline Handlers:
Use then chains to make inline code more compact:
-- Before (verbose)
if condition
do step1
do step2
do step3
end
-- After (compact with then chains)
if condition
do step1 then do step2 then do step3
end
Example from body keyboard handler:
if event.key is '?' and not event.ctrlKey and not event.metaKey and not event.altKey and not isInputField
halt the event then set modal to #shortcuts-modal then if modal then call modal.showModal() end
end
File Organization
static/hyperscript/
├── utils._hs → Core utilities (scroll, print, modals, expand/collapse)
├── toggles._hs → Toggle functions (CV length, icons, theme)
├── hover-sync._hs → Hover sync functions (PDF, print, zoom)
├── keyboard._hs → Keyboard shortcut helpers (handleToggleShortcut, openModalShortcut)
├── zoom._hs → Zoom control (slider, reset, drag handlers, visibility)
└── pdf-modal._hs → PDF modal helpers (selectPdfCard, handlePdfCardKey)
Load Order in templates/index.html:
<script type="text/hyperscript" src="/static/hyperscript/utils._hs"></script>
<script type="text/hyperscript" src="/static/hyperscript/toggles._hs"></script>
<script type="text/hyperscript" src="/static/hyperscript/hover-sync._hs"></script>
<script type="text/hyperscript" src="/static/hyperscript/keyboard._hs"></script>
<script type="text/hyperscript" src="/static/hyperscript/zoom._hs"></script>
<script type="text/hyperscript" src="/static/hyperscript/pdf-modal._hs"></script>
<script src="/static/hyperscript/_hyperscript.min.js"></script>
Required Functions
Core Functions (utils._hs)
printFriendly()- Handle print-friendly viewinitScrollBehavior()- Initialize scroll variableshandleScroll()- Manage scroll behavior and fixed button positioningcloseOnBackdrop(modal, evt)- Close modal when clicking backdrop (outside content)scrollToTop(evt)- Smooth scroll to top of pagescrollToSection(evt, sectionId)- Smooth scroll to section with menu closeexpandAllSections(evt)- Expand all<details>elementscollapseAllSections(evt)- Collapse all<details>elementssetFooterHover(show)- Add/remove footer-hovered class on fixed buttons
Toggle Functions (toggles._hs)
toggleCVLength(isLong)- Switch between short/long CVtoggleIcons(showIcons)- Show/hide iconstoggleTheme(isClean)- Switch between default/clean theme
Hover Sync Functions (hover-sync._hs)
syncPdfHover(show)- Sync hover state across PDF buttonssyncPrintHover(show)- Sync hover state across print buttonshighlightZoomControl(show)- Highlight zoom control on hover
Zoom Functions (zoom._hs)
handleZoomInput(slider)- Handle zoom slider input changeshandleZoomReset()- Reset zoom to 100%initZoomControl(control)- Initialize zoom control on page loadshowZoomControl()- Show the zoom control panelhideZoomControl()- Hide the zoom control paneltoggleZoomControl()- Toggle zoom control visibilityisZoomDragTarget(el)- Check if element is valid drag target (not button/input)startZoomDrag(control, clientX, clientY)- Start dragging zoom controlmoveZoomDrag(control, clientX, clientY)- Handle drag movementendZoomDrag(control)- End drag and save position
Navigation Functions (moved to utils._hs)
Note: scrollToSection moved to utils._hs for consolidation
Why These Rules Exist
Maintainability
- Functions with descriptive names are self-documenting
- Easier to test and debug
- Changes in one place instead of scattered across templates
Performance
- Browser caches ._hs files
- Reduces HTML payload size
- Cleaner separation of concerns
Historical Note: Hyperscript Def Limit
- Hyperscript 0.9.12 had a 3-def limit per file (FIXED in 0.9.14+)
- Hyperscript 0.9.14+ has NO def limit - tested with 5+ defs
- Multi-file organization is still recommended for maintainability, not required
Common Mistakes to Avoid
❌ DON'T: Write long inline hyperscript in HTML (maintainability issue)
❌ DON'T: Try to externalize event handlers that inspect event.key or event.target
❌ DON'T: Forget to test after refactoring (syntax errors look like bugs)
❌ DON'T: Use target as a parameter name - it's a reserved word!
✅ DO: Split functions across multiple ._hs files for organization
✅ DO: Keep HTML clean with function calls
✅ DO: Test all keyboard shortcuts after any hyperscript changes
✅ DO: Use el instead of target when passing DOM elements to functions
Reserved Words in Hyperscript
The following are reserved and reference special values in hyperscript:
target→event.target(the element that triggered the event)me→ The current element with the_=""attributeit→ The result of the previous commandevent→ The current event object
Example of the target pitfall:
-- ❌ WRONG - 'target' is reserved, will reference event.target
def checkElement(target)
if target.tagName is 'INPUT' -- ERROR: target is null in function context
return false
end
end
-- ✅ CORRECT - use 'el' instead
def checkElement(el)
if el.tagName is 'INPUT'
return false
end
end
Testing After Changes
- Check browser console for parse errors
- Verify all functions are defined (no "X is null" errors)
- Test all toggles work correctly
- Hard refresh browser (Ctrl+Shift+R) to clear cache
Recent Changes
2025-11-30: Major Inline Hyperscript Refactoring (Phase 2)
- ✅ REFACTORED: Modal backdrop close (3 modals) →
closeOnBackdrop()inutils._hs - ✅ REFACTORED: Back-to-top button →
scrollToTop()inutils._hs - ✅ REFACTORED: Zoom drag handlers (~35 lines) → 4 functions in
zoom._hs - ✅ ADDED:
isZoomDragTarget(),startZoomDrag(),moveZoomDrag(),endZoomDrag() - ✅ ADDED:
showZoomControl(),hideZoomControl(),toggleZoomControl() - ✅ MOVED:
expandAllSections(),collapseAllSections()toutils._hs - ✅ MOVED:
scrollToSection()toutils._hswith integrated menu close - ✅ LEARNING:
targetis a reserved word in hyperscript (useelinstead) - ✅ TESTED: All 21 functions verified, 6 functional tests passed
2025-11-30: Major Inline Hyperscript Refactoring (Phase 1)
- ✅ REFACTORED: Body tag keyboard handlers →
keyboard._hshelper functions - ✅ REFACTORED: Zoom control handlers →
zoom._hshelper functions - ✅ REFACTORED: PDF modal card selection (3 identical blocks) →
pdf-modal._hs - ✅ ADDED:
zoom._hs- Zoom control helpers (handleZoomInput, handleZoomReset, initZoomControl) - ✅ ADDED:
pdf-modal._hs- PDF modal helpers (selectPdfCard, handlePdfCardKey) - ✅ TESTED: All functionality verified with comprehensive tests
2025-11-30: Multi-File Loading Bug Investigation
- ✅ CONFIRMED: Multiple
<script type="text/hyperscript" src="...">tags work correctly - ✅ VERIFIED: No multi-file loading bug in hyperscript 0.9.14
- ✅ TESTED: All 6 external files + inline hyperscript work together seamlessly
- ✅ ADDED: Test
tests/mjs/32-hyperscript-multi-src.test.mjsfor verification - 🔍 FINDING: Previous refactoring failures were syntax errors, NOT hyperscript bugs
2025-11-20: Event Handler Externalization Guidelines
- ✅ Added Rule 4: Clear guidelines on what can/cannot be externalized
- ✅ Navigation handlers successfully externalized (9 links → 1 function)
- ✅ Documented
thenchain optimization for inline handlers - ✅ Updated file organization with navigation._hs
- ⚠️ Keyboard handlers documented to stay inline (event context requirement)
Last Updated: 2025-11-30 Hyperscript Version: 0.9.91 Status: MANDATORY - ALWAYS FOLLOW