npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

whits

v1.4.1

Published

Write HTML in TypeScript

Downloads

13

Readme

whits - Write HTML in TypeScript

build & test docs coverage github npm api docs

whits is a Node.js library that generates HTML code programmatically with all the advantages of TypeScript, such as type-checking, autocompletion, decorators, etc. It provides a clean and concise way to create dynamic HTML templates, with types that provide safeguards against generating invalid HTML.

Contents

Installation

npm i whits

Basic Usage

Import $

To use whits in your TypeScript project, you can import the $ object.

import {$} from 'whits';

Creating tags

There are many ways to build out your HTML. The shortest, if the tag doesn't have an ID or class, is to use the properties of the $ object, which correspond to all the valid HTML tags. You can also call $ as a function and pass a selector to create tags with class and/or id attributes. There is no "best" way, except what is most readable and convenient for you!

// Many ways of creating equivalent divs
const divs = [

	// Use the individual tag factory, and pass the attribues as an object in the first argument
	// Pass the content as the second argument
	$.div({id: 'example', class: ['foo', 'bar']}, 'Hello, world!'),

	// Create a factory by passing a CSS-style selector to the `$` function
	// Then pass attributes as an object in the first argument, and content as the second argument
	$('div')({id: 'example', class: ['foo', 'bar']}, 'Hello, world!'),
	$('div#example')({class: ['foo', 'bar']}, 'Hello, world!'),

	// If there are no attributes to add, pass the content as the first argument
	$('div#example.foo.bar')('Hello, world!'),

	// If there is no tag name passed to the `$` function, it will default to `div`
	$('#example.foo.bar')('Hello, world!'),
	$('#example')({class: ['foo', 'bar']}, 'Hello, world!'),
	$('')({id: 'example', class: ['foo', 'bar']}, 'Hello, world!'),

];

// Each tag has an `html` getter that returns a string representation of the tag
// All of the examples above will have the same HTML output
console.log(
	divs.every((example) => example.html === '<div class="foo bar" id="example">Hello, world!</div>')
);
// Output: true

Nesting basics

One of the essential capabilities is nesting of tags. This works in an intuitive way, just like in HTML.

// Children are passed to the factory in an array, after the attributes object if there is one
$.div([
	$.h1('Example'),
	$.p('Hello, world!')
]);
$.div({'data-foo': 'bar'}, [
	$.h1('Example'),
	$.p('Hello, world!')
]);

// Children can be a mix of tags, raw content (more on this later), and strings
$.div([
	$.h1('Example'),
	'Hello, world!',
	$.p('This is a paragraph.')
]);

// A single child doesn't need to go in an array
$.div('Hello, world!');
$.div(raw('<p>Hello, world!</p>'));
$.div($.h1('Hello, world!'))

// If a child is an empty or void tag, the factory function itself can be passed without being called
$.div([
	$.div,             // Empty div
	$.br,              // Void tag <br>
	$('i.fa.fa-star')  // Empty tag with classes passed as a selector
])

// Nesting can go as deep as you need it to
$.main([
	$('#container')([
		$.section({'data-foo': 'bar'}, [
			$.h1('Example'),
			$.div([
				$.p(['Hello,', $.b('world'), '!']),
			])
		])
	])
]);

Nesting shortcut with compound tags

You can create compound tags by passing multiple selector strings to the $ function. A compound tag can be thought of as a hierarchy of tags nested within each other. This serves as a shortcut when you need to stack tags together. When you create a compound tag, any attributes or children you pass will be redirected into the innermost tag in the hierarchy. Classes and IDs passed as part of the selector will still work as expected.

These 2 examples are equivalent:

$('header', 'nav#navigation', 'ul.navbar')({title: 'UL'}, [
	$('li', 'a')({href: '/'}, 'Home'),
	$('li', 'a')({href: '/page1'}, 'Page 1'),
]);

$.header(
	$('nav#navigation')(
		$('ul.navbar')({title: 'UL'}, [
			$.li($.a({href: '/'}, 'Home')),
			$.li($.a({href: '/page1'}, 'Page 1'))
		])
	)
);

They both output the same HTML:

<header><nav id="navigation"><ul class="navbar" title="UL"><li><a href="/">Home</a></li><li><a href="/page1">Page 1</a></li></ul></nav></header>

Interpolation 1.3.3+

Sometimes it is inconvenient or difficult to read an array of tag children, such as when there are Tag instances and other content placed within a paragraph of text. This is where the interpolation _ template literal function comes in handy. Using this function, you can pass any valid Tag children as expressions in a template literal.

These 2 examples are equivalent:

import {$, _} from 'whits';

const externalString = 'external string';

$.div([
	'This could be a long chunk of text, with ', $.i('several'), ' ', $.span('tags'),
	' and other content embedded, including this ', externalString, '.'
]);

$.div(_`
	This could be a long chunk of text, with ${$.i('several')} ${$.span('tags')} 
	and other content embedded, including this ${externalString}.
`);

They both output the same HTML:

<div>This could be a long chunk of text, with <i>several</i> <span>tags</span> and other content embedded, including this external string.</div>

Strings and raw content

By default, strings are escaped automatically. You can import and use the raw, comment, css, and javascript template literal functions to pass unescaped content as children into a tag.

⚠ Use caution when calling these functions, as they are unfiltered and can be unsafe and error-prone.


raw() - Insert raw, unescaped HTML content.

import {$, raw} from 'whits';

// Use as a template literal tag
$.div(
	// This string is escaped automatically
	'Hello, <world>!',

	// But this one is not
	raw`
		<div>
			<p>Raw HTML</p>
		</div>
	`
);

// Use as a standard function
$.div(raw('<p>Raw HTML</p>'));


comment() - Insert an HTML comment

import {$, comment} from 'whits';

// Use as a template literal tag or a standard function
$.div(comment`This is a comment`);
$.div(comment('This is a comment'));


javascript() - Insert raw javascript, automatically wrapped in a script tag.

ⓘ The es6-string-javascript VSCode extension can be used to get syntax highlighting within the template literal.

import {$, javascript} from 'whits';

// Must be used as a template literal tag
$.head([
	javascript`
		const message = 'Hello, world!';
		console.log(message);
	`
]);


css() - Insert raw CSS, automatically wrapped in a style tag.

ⓘ The es6-string-html VSCode extension can be used to get syntax highlighting within the template literal.

import {$, css} from 'whits';

// Must be used as a template literal tag
$.head([
	css`
		body {
			background-color: #000;
			color: #fff;
		}
	`
]);

Whitespace

Whitespace within regular strings and template literals is truncated to a single space character, and there is no space rendered between HTML tags. Therefore, if you want space in your output, you must add it manually via RawContent instances or raw() calls.

By default, the pre and textarea tags are immune from this whitespace truncation. The list of immune tags can be changed globally by adding/removing items from the Tag.keepWhitespace static property, which is a Map instance whose items are strings representing HTML & SVG tag names.

Whitespace truncation does not apply to RawContent. It does apply to the interpolation _ template literal function, but not any of the other template literal functions.

⚠ This behavior has changed as of version 1.4.0. Previous versions did not truncate whitespace.

import {$, raw} from 'whits';

// Add a full empty line between tags
$.div([
	$.h1('Hello, world!'),
	raw('\n\n'),
	$.p('This is a paragraph.')
]);

// Add `a` to the list of tags that are immune from whitespace truncation
Tag.keepWhitespace.add('a');

// Now this will render the whitespace within the `a` tag
$.a(`
	Line 1
	Line 2
`);

Creating full HTML templates

You can create HTML template files by exporting instances of the Template and RootTemplate classes, which include methods for rendering the content as a string. The Template constructor allows passing the template content as a Tag instance, raw content, a string, or an array of any of these. Alternatively, you can pass a function that returns the content in any of those formats. The function accepts a params object which you can use to pass variables from your application code.

See the basic code example for details.

Generating static HTML

Generating static HTML files is also a simple process, which comes down to 3 steps:

  1. Name the files you want to compile *.html.ts. The compiler will ignore other files.
  2. Set the default export of the template files. It can be any of the following:
    // RootTemplate.render() output, passing a predefined params object
    export default (new RootTemplate<{foo: string}>(({foo}) => [$.div(foo)])).render({foo: 'bar'});
    
    // A RootTemplate instance, receiving the params object from the command line
    export default new RootTemplate<{foo: string}>(({foo}) => [$.div(foo)]);
    
    // A RootTemplate instance with no params
    export default new RootTemplate([$.div('bar')]);
    
    // Any other string or instance of Template, RootTemplate, or RawContent is acceptable
    export default 'This string will be escaped.';
    export default raw('<template>This is an HTML <b>template</b> partial.</template>');
  3. Run the whits cli, passing the input/output directories and (optional) params object as arguments.

See the static code example for details.

Using the CLI

The CLI is useful for generating static HTML from your templates. It can generate from your TypeScript source or the compiled JavaScript if you've already run tsc or similar.

Usage: whits [-w] [-e <extend>] <input> <output> [...params]
  -w     - watch for changes
  -e     - extend whits with a module that adds new tags
  extend - path to a module that extends whits, relative to the input directory
  input  - path to the input directory
  output - path to the output directory
  params - JSON-formatted object of params to pass to the templates
# Build from compiled JS files, assuming they are in `dist` dir, output HTML to `html` dir
npx whits dist html

# Build and watch source TS files, assuming they are in `src` dir, output HTML to `out` dir
# Intermediate JS files will go into a `.whits-dist` dir, which can be deleted after
npx whits -w src out

# Assuming there is a `tsconfig.json` file, build according to that and output HTML to `out` dir
# Built JS files will go into the `outDir` specified in `tsconfig.json`
npx whits . out

# Same as previous example, but pass an object to the templates
npx whits . out '{"foo": "bar"}'

# Same as previous example, but also extend whits with a module called `baz`
npx whits -e baz . out '{"foo": "bar"}'

Known issue: The -w feature is not perfect. It mostly works, but it may not behave as expected when adding or deleting files.

Using custom tags and attributes

In keeping with its inherent strictness, whits forces you to use valid HTML5 tags and attributes. You may find, however, that your project requires using non-standard tags or attributes. A quick workaround would obviously be to wrap raw HTML strings in calls to the raw() function. While this works for one-off tags here and there, doing it excessively defeats the purpose of a strongly-typed templating system. This is where extend() comes in.

Extending whits

It's super easy. One small file in your project can give you practically unlimited flexibility. You can call it whatever you want, but we'll use extend-me.ts for this example. Say you want to be able to add these elements:

<foo bar="baz" far="faz">foo</foo>
<boo id="boo">boo</boo>
<div invalid-prop="val">hello</div>

This is all you need:

// extend-me.ts

import {extend} from 'whits';

// Use an ambient module to modify the `whits` types
declare module 'whits' {

	// Override the tag => attribute mapping
	interface HtmlAttributeOverrides {
		foo: 'bar' | 'far';   // Add a `foo` tag with `bar` and `far` attributes
		boo: undefined;       // Add a `boo` tag with only the global attributes
		div: 'invalid-attr';  // Add `invalid-attr` as an attribute to div tags
	}
}

// Call the extend function, passing only the new tags
// If you are only adding attributes to existing tags, you can skip this part
// This call technically doesn't have to be in the same file as the ambient module
extend('foo', 'boo');
// a-template.ts

import {$, Template} from 'whits';

// Make sure you import the extension module
import './extend-me.js'

// Export your template
export default new Template([
	$.foo({bar: 'baz', far: 'faz'}, 'foo'),
	$('boo#boo')('boo'),
	$.div({'invalid-attr': 'val'}, 'hello')
]);

You can also add custom SVG tags and attributes, which works the same way. See the extend code example for details.

A note about importing the extension module

Since we are extending the global instance of whits we only really need to import the module once per entrypoint. If it has already been imported somewhere else in the app, import {$} from 'whits' will re-import the already modified instance. This means that if you are serving dynamic pages from a node app, you can import the module in your app init process and skip importing it into any of your template files.

If, however, you are building a static site, each *.html.ts file is essentially its own entrypoint. There are two options for this case:

  1. Import the module in each template where you need to use the custom tags.
  2. Use the -e command-line argument to specify a module (relative to the input path) to import globally:
    # This will import `src/extend-me.ts` globally
    npx whits -e extend-me src html
    
    # Also works to import the equivalent JS module if generating from pre-compiled code
    # This will import `dist/extend-me.js` globally
    npx whits -e extend-me dist html

Special thanks

A quick shout out to a few other folks:

  • wooorm, for his amazingly useful tag name and attribute lists, which he has unwittingly provided as the basis for the type definitions in this project
  • The Pug team, for lots of inspiration
  • GitHub Copilot, for drastically cutting down the development time for this project
  • You, for your support! ❤️

Contributing

Contributions to whits are welcome! To contribute, please fork the repository and submit a pull request.

License

whits is licensed under the MPL-2.0 License. See LICENSE for more information.

Future enhancements

  • Advanced usage documentation