Skip to content

NickHodges/tshtmlwriter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm version TypeScript strict 347 tests passing License MIT Zero dependencies

tshtmlwriter

A fluent, type-safe HTML builder for TypeScript. Chain methods to produce well-formed HTML without string concatenation, template literals, or JSX. Ported from DelphiHTMLWriter.

import { createDocument } from 'tshtmlwriter';

const html = createDocument('html5')
  .openHead()
    .addTitle('Hello World')
    .addMetaNamedContent('viewport', 'width=device-width, initial-scale=1')
  .closeTag()
  .openBody()
    .addHeadingText(1, 'Welcome')
    .addParagraphText('Built with tshtmlwriter.')
    .openUnorderedList()
      .addListItem('Fast')
      .addListItem('Type-safe')
      .addListItem('Zero dependencies')
    .closeList()
  .closeTag()
  .closeDocument()
  .toHTML();

Features

  • Fluent API — every method returns IHTMLWriter for chaining
  • Strict TypeScript — literal union types for headings, input types, format types, and more
  • Compile-time safety — context-aware errors prevent invalid nesting (e.g., <li> outside a list)
  • Full HTML5 support — semantic elements, media, forms, tables, definition lists, and more
  • Zero dependencies — just TypeScript
  • 347 tests across 21 test files

Install

npm install tshtmlwriter
bun add tshtmlwriter

Quick Start

Create a full document

import { createDocument } from 'tshtmlwriter';

const page = createDocument('html5')
  .openHead()
    .addTitle('My Page')
  .closeTag()
  .openBody()
    .addHeadingText(1, 'Hello')
  .closeTag()
  .closeDocument()
  .toHTML();

// <!DOCTYPE html><html><head><title>My Page</title></head><body><h1>Hello</h1></body></html>

Build a fragment (no wrapper tag)

import { createFragment } from 'tshtmlwriter';

const fragment = createFragment()
  .addBoldText('Important: ')
  .addText('Read the docs.')
  .toHTML();

// <b>Important: </b>Read the docs.

Start from any tag

import { create } from 'tshtmlwriter';

const nav = create('nav')
  .addClass('main-nav')
  .openUnorderedList()
    .addListItem('Home')
    .addListItem('About')
  .closeList()
  .closeTag()
  .toHTML();

// <nav class="main-nav"><ul><li>Home</li><li>About</li></ul></nav>

API at a Glance

Factory Functions

Function Description
createDocument(docType?) Start an <html> document with optional DOCTYPE
create(tagName) Start from any arbitrary tag
createFragment() Build HTML without a wrapper element

Content & Structure

Method Description
.addText(text) Add text content (closes open bracket first)
.addRawText(text) Add raw text without closing the bracket
.addTag(name, closeType?, canHaveAttrs?) Open an arbitrary child tag
.closeTag() Close the current tag and return to parent
.toHTML() Get the accumulated HTML string

Attributes

create('div')
  .addId('hero')
  .addClass('container')
  .addStyle('color: red')
  .addDataAttribute('page', 'home')
  .addAriaAttribute('label', 'Hero section')
  .addRole('banner')

Boolean attributes: .addRequired(), .addDisabled(), .addAutofocus(), .addHidden(), .addReadonly(), .addMultiple(), .addNovalidate()

Text Formatting

.addBoldText('bold')           // <b>bold</b>
.addItalicText('italic')       // <i>italic</i>
.addCodeText('const x = 1')   // <code>const x = 1</code>
.addStrongText('important')    // <strong>important</strong>
.addEmphasisText('note')       // <em>note</em>

All format types: bold, italic, underline, emphasis, strong, subscript, superscript, pre, cite, abbreviation, address, code, delete, definition, keyboard, quotation, sample, small, variable, insert, mark, bdi, ruby, rt, rp

Headings

.addHeadingText(1, 'Title')    // <h1>Title</h1>
.addHeadingText(2, 'Subtitle') // <h2>Subtitle</h2>
// Levels 1-6, type-checked as HeadingLevel

Lists

// Unordered
.openUnorderedList('disc')
  .addListItem('First')
  .addListItem('Second')
.closeList()

// Ordered
.openOrderedList('upper-roman')
  .addListItem('Act I')
  .addListItem('Act II')
.closeList()

// Definition list
.openDefinitionList()
  .openDefinitionTerm().addText('HTML').closeTag()
  .openDefinitionItem().addText('HyperText Markup Language').closeTag()
.closeTag()

Tables

.openTable({ border: 1, cellPadding: 4 })
  .openCaption().addText('Results').closeTag()
  .openTableRow()
    .openTableHeader().addText('Name').closeTag()
    .openTableHeader().addText('Score').closeTag()
  .closeTag()
  .openTableRow()
    .addTableData('Alice')
    .addTableData('95')
  .closeTag()
.closeTable()

Forms

.openForm({ action: '/submit', method: 'post' })
  .openFieldSet()
    .addLegend('Login')
    .openLabel('email').addText('Email').closeTag()
    .openInput({ type: 'email', name: 'email' }).addRequired().closeTag()
    .openLabel('pass').addText('Password').closeTag()
    .openInput({ type: 'password', name: 'pass' }).closeTag()
    .openButton().addText('Sign In').closeTag()
  .closeTag()
.closeForm()

HTML5 Semantic Elements

.openHeader().addHeadingText(1, 'Site Title').closeTag()
.openNav().addText('...').closeTag()
.openMain()
  .openArticle().addParagraphText('Content here').closeTag()
  .openAside().addText('Sidebar').closeTag()
.closeTag()
.openFooter().addText('© 2025').closeTag()

Also: section, figure, figcaption, details, summary, dialog, template, picture

Media & Embeds

.openVideo({ src: 'video.mp4', width: 640, height: 480 }).closeTag()
.openAudio('audio.mp3').closeTag()
.addImage('photo.jpg')
.addIFrame('https://example.com', 800, 600)
.addEmbed('doc.pdf', 'application/pdf')

Convenience Methods

.addFigure('photo.jpg', 'A nice photo')
// <figure><img src="photo.jpg" /><figcaption>A nice photo</figcaption></figure>

.addDetailsSummary('Click to expand', 'Hidden content here')
// <details><summary>Click to expand</summary>Hidden content here</details>

.addAnchor('https://example.com', 'Visit')
// <a href="https://example.com">Visit</a>

.addComment('TODO: refactor this')
// <!-- TODO: refactor this -->

Error Handling

tshtmlwriter enforces valid HTML structure at runtime with descriptive errors:

create('div').addListItem('oops');
// ❌ NotInListError: Must be inside a list tag

create('div').closeTag().closeTag();
// ❌ ClosingClosedTagError: Cannot close a tag that is already closed

createDocument().openBody().closeDocument();
// ❌ DocumentHasOpenTagsError: Cannot close document while tags are still open

Error checking can be configured via .setErrorLevels().

Types

All types are exported for use in your own code:

import type {
  IHTMLWriter,
  HeadingLevel,
  FormatType,
  InputType,
  DocType,
  BulletShape,
  NumberType,
  TableOptions,
  FormOptions,
} from 'tshtmlwriter';

Origin

This is a TypeScript port of DelphiHTMLWriter, originally written in Delphi/Object Pascal. The API design preserves the fluent builder pattern of the original while taking full advantage of TypeScript's type system.

License

MIT

About

TypeScript HTML Writer

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors