JS Hypertext Preprocessor (JHP) is a developer-focused JavaScript templating engine designed to rival static site generators by leveraging native JavaScript instead of introducing custom templating syntaxes. Inspired by PHP, JHP allows developers to use raw HTML and familiar JavaScript to build dynamic templates for static site generation during local development.
Important: JHP is a library that you integrate into your build process or server application. It is not a standalone command-line tool. You need to write code that uses JHP to process your template files. See the Installation section for details.
Note: JHP can be used as a production server, similar to PHP, but please exercise caution as it has not been fully tested for that use and may have potential security concerns. It is primarily designed for local development environments and static site generation workflows.
- Use native HTML and JavaScript for templating.
missingBindingconfigures how synthesized script bindings behave when a name is used but not declared or incontext(Missing bindings).- Supports PHP-like behaviors, such as variable redeclaration across script blocks.
- Provides a simplified output buffering system with
$obOpenand$obClose. - Includes flexible file inclusion for partials and reusable templates.
- Built-in security check that attempt to prevent unsafe code execution.
- Can be used as a view engine for server-side rendering.
JHP processes files – commonly .jhp files but you can choose the extension by what file you hand to the engine – containing raw HTML with special <script> blocks, transforming them into static HTML. The engine specifically executes <script> tags without attributes in a server-side context, enabling dynamic content generation. This flexibility allows developers to:
- Use built-in
$functions within server-side<script>blocks to manage output, include files, or define constants. - Declare variables or functions in one
<script>block and reuse them in later blocks, maintaining context across the file. - Capture and reuse parts of the output with the output buffer.
- Modularize templates with nested file includes.
Note: The <script> tag is included by default as a JHP tag to ensure code editors automatically highlight and provide IntelliSense for JHP blocks. In the future I hope to add IDE support for <jhp> tags.
This example illustrates JHP's capabilities, including variable handling, file inclusion, default values, constants, and output buffering. While your project may use a different templating style or structure, this example is designed to highlight the engine's features and emulate PHP-like behavior.
project/
|-- templates/
| |-- header.html
| |-- footer.html
|-- index.html
<script>
$obOpen();
</script>
The home page's content here...
<script>
const mainContent = $obClose();
$echo($include('./templates/primary.html'));
</script><script>
if (!pageTitle) {
let title = 'Home Page';
}
if (!description) {
let description = 'Welcome to our amazing site!';
}
$echo($include('./partials/header.html'));
$echo(`<main class="content-grid">${mainContent}</main>`);
$echo($include('./partials/footer.html'));
</script><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><script>$echo(pageTitle);</script></title>
</head>
<body>
<header>
<h1><script>$echo(pageTitle);</script></h1>
</header><footer>
<p>
© 2025 <script>$echo(companyName);</script>. All rights reserved.
</p>
</footer>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Page</title>
</head>
<body>
<header>
<h1>Home Page</h1>
</header>
<main class="content-grid">
The home page's content here...
</main>
<footer>
<p>
© 2025 Caboodle Tech Inc. All rights reserved.
</p>
</footer>
</body>
</html>Note: To include frontend JavaScript for the browser, add at least one attribute to your <script> tags (such as <script type="text/javascript">). JHP only processes <script> tags without attributes; those with attributes are ignored and treated as standard client-side scripts.
The $ object provides several utility methods for use in templates:
| Function | Description |
|---|---|
$context(key, value) |
Adds or updates a variables value in the current context. Used internally but can be used to preemptively load variables. |
$define(key, value) |
Defines a true constant variable. Displays an error if redefined in any context. |
$echo(content) |
Outputs content directly to the compiled page; accepts only one argument. |
$if(<condition>) |
Starts an if block that may be followed by $elseif() or $else() blocks. |
$elseif(<condition>) |
Provides an alternative condition for an if block. May be followed by another $elseif() or $else() block. |
$else() |
The default block for an if or if-elseif statement if no other conditions are met. |
$end() |
Ends a conditional block. Must be used after $if(), $elseif(), or $else() to properly close the conditional block. |
$include(file) |
Includes another file (template) and processes it within the current context. See Include paths below. |
$obOpen() |
Starts an output buffer to capture content. |
$obClose() |
Closes the output buffer and returns its content as a string. |
$obStatus() |
Checks if the output buffer is currently open. |
$version() |
Returns the JHP version string. |
For more information on how to properly use these functions, refer to the example files in the examples directory.
$include('…') is resolved in this order (highest priority first):
-
includePathResolver(optional)
PassincludePathResolver( file, currentDir )in theoptionstoprocess(). When it is set, it fully replaces all other include path logic for that run. Return an absolute path (or a path resolvable withcurrentDir) to a real file, ornullif the include cannot be resolved. Nested includes use the same function;currentDiris the directory of the file that issued each$include. Use this for custom policy, security checks, or behavior that is not a simple list of roots. The option is cleared after eachprocess(). -
includeSearchRoots(optional, ignored when a resolver is set)
Pass an array of absolute directory strings, e.g.[ templateDir, srcDir ]. If present, the built-in resolver uses them as an ordered search:- If the string is
..or starts with../, it is resolved only from the including file’s directory (search roots are not used). - Other paths: try the including file’s directory first, then each root in order (join the include path, without a leading
/, to each root). - Paths that start with
/: try, under each root in order, the path after the leading/.
If nothing matches, the usual single#rootDirfallback still applies (same as whenincludeSearchRootsis omitted), when that differs from the current directory. Omitted, null, or an empty array leaves behavior as in step 3.
- If the string is
-
Default (neither a resolver nor search roots, or only legacy behavior)
- Paths starting with
/are resolved from the JHP root (rootDiron theJHPinstance, or the directory of the file being processed on the firstprocess()ifrootDirwas not set). They are not the operating system’s filesystem root. - Other non-absolute paths are tried relative to the including file’s directory first, then
#rootDirwhen that differs.
An explicitrootDirinnew JHP({ rootDir: '/path/to/your/content' })is a good way to make “root” match your project’s template (or content) root. The example build uses this for/partials/...includes.
- Paths starting with
Inside a bare <script> block, when you reference a name that is not:
- Declared (
let,const, orvar) in template code - Passed in
process({ context }) - Defined with
$define
…JHP synthesizes var name = …; at the top of that script compilation so runtime does not throw ReferenceError. The missingBinding option controls what appears on the right-hand side.
Set it on the processor as a default, or override only for one process() call:
const jhp = new JHP({ missingBinding: 'emptyString' }); // constructor default for all runs
jhp.process('./page.jhp', {
cwd: templatesDir,
missingBinding: 'sentinel', // overrides for this invocation only (optional)
context: {}
});| Value | Synthetic binding | Typical if (name) |
|---|---|---|
emptyString (default) |
var name = ''; |
Falsy |
undefined |
var name = undefined; |
Falsy |
null |
var name = null; |
Falsy |
sentinel |
Non-empty diagnostic string << Undefined: yourIdentifier >> (legacy JHP style) |
Truthy |
Recommended for optional layout fields: use the default emptyString so shared partials (head.html, headers) can guard with if (author), author !== '', and similar checks when a route omits context keys.
sentinel mode reproduces older JHP behavior: missing names resolve to the visible string << Undefined: identifier >>, which is truthy—useful when you deliberately want omissions to surface (typos or missing context). Prefer echoing sentinel text as plain output rather than cramming it into concatenated markup that looks like a tag attribute string; ambiguous </> sequences in echoed fragments can confuse later HTML parsing.
Template literals (`${author}`) still coerce values to strings; undefined becomes "undefined"—keep explicit guards (if (author)) for optional snippets.
Invalid missingBinding values throw TypeError.
JHP is not a standalone tool – you need to integrate it into your build process or server application. You can use JHP in two ways:
Install JHP as an npm package:
npm install @caboodle-tech/jhpThen import it in your project:
import JHP from '@caboodle-tech/jhp';
// Constructor accepts defaults such as missingBinding ([Missing bindings](#missing-bindings-missingbinding)).
const jhp = new JHP();
const html = jhp.process('./template.jhp');
console.log(html);If you prefer to manually include JHP in your project, you can copy the files from the src directory directly into your project. You only need:
src/jhp.jssrc/processors.js
Then import and use it in your build script:
import JHP from './path/to/jhp.js';
// Constructor accepts defaults such as missingBinding ([Missing bindings](#missing-bindings-missingbinding)).
const jhp = new JHP();
const html = jhp.process('./template.jhp');
console.log(html);Note: Make sure to install JHP's dependencies (@caboodle-tech/simple-html-parser and acorn-loose) in your project if you use this approach.
Regardless of which installation method you choose, you must write code to integrate JHP into your build process. JHP doesn't run automatically – you need to create a script that:
- Instantiates the JHP class
- Calls
jhp.process()for each template file - Writes the output to your desired location
See the example build script for a complete implementation.
The process method accepts an options object to customize processing for individual files:
import JHP from '@caboodle-tech/jhp'; // or './path/to/jhp.js' if using manual installation
const jhp = new JHP();
// Process with context variables and custom processors
const html = jhp.process('./template.jhp', {
context: {
pageTitle: 'My Page',
userName: 'John Doe'
},
preProcessors: [myPreProcessor],
postProcessors: [myPostProcessor],
cwd: './templates',
relPath: '/blog'
});Available options (also see Missing bindings):
context- Initial variables and functions for template context (Object or Map)preProcessors- Array of preprocessor functions to apply for this filepostProcessors- Array of postprocessor functions to apply for this filecwd- Current working directory for file resolutionrelPath- Relative path for URL resolutionmissingBinding- Overrides the constructor default for this run only: how to synthesizevar … = …for identifiers referenced in script without declaration orcontext; one ofemptyString,undefined,null,sentinelincludePathResolver- Optional; full override of$includepath resolution for thisprocess()only; see Include pathsincludeSearchRoots- Optional array of absolute directory paths, tried in order for built-in resolution when no resolver is set; see Include paths
Constructor defaults: new JHP({ … }) accepts missingBinding (and globalConstants, rootDir, jhpTags, processors, etc.); missingBinding there applies to every process() unless a given call passes its own missingBinding.
Note: Pre-processors operate on the raw JHP structure (before JHP code is replaced), while post-processors operate on the fully parsed DOM after all JHP code has been replaced with HTML. This means pre-processors can access and modify JHP script blocks, while post-processors work with the final HTML structure.
JHP is primarily designed for local development, static site generation, and for use as a view engine. While it can be used as a production server, please proceed with caution due to potential security concerns and lack of extensive testing in high-traffic environments. It is recommended to thoroughly test and review your setup if you choose to use JHP live on a production server.
- Familiar and fast: Use native HTML, CSS, and JavaScript – no need for custom templating languages or complex configurations. Just write and build.
- Lightweight and focused: With only one development dependency (acorn), JHP is far simpler than engines requiring multiple plugins or libraries.
- Flexible structure: No rigid directory or file structure is enforced – organize your project in a way that works best for you.
- Encourages native development: Fully leverage HTML, CSS, and JavaScript as they are, with support for modern CSS features like nesting. JHP doesn’t require preprocessing tools but allows seamless integration if your project needs them.
- Modular and maintainable: Reuse components with file includes and shared templates to keep your code clean and scalable.
JHP streamlines static site generation, empowering developers to build quickly and intuitively without the overhead of traditional templating engines.