npmjs:dev

Run and explore NPM packages in browser
import tinyentities from 'tinyentities'

console.log(
Object.keys(tinyentities)
)

tinyentities

Encoding and decoding HTML entities shouldn't be half of your bundle size. Unfortunately, it's that way with some other libraries. Not with tinyentities.

Usage

e</h2></summary>
import {
  decodeHTML,
  decodeXML,
  escapeHTML, // Use like entities' escapeText
  escapeHTMLAttribute, // Use like entities' escapeAttribute
  encodeHTML,
  escapeXML,
  escapeXMLAttribute, // Use like entities' escapeUTF8
  encodeXML,
  tryReadHTML, // Use when you would use entities' EntityDecoder
  tryReadXML, // Use when you would use entities' EntityDecoder
} from "tinyentities";

console.log(decodeHTML("<hi>")); // 
console.log(decodeXML("<hi>")); // 

console.log(escapeHTML("")); // <hi>
console.log(escapeHTMLAttribute("")); // <hi>
console.log(encodeHTML("")); // <hi>

console.log(escapeXML("")); // <hi>
console.log(escapeXMLAttribute("")); // <hi>
console.log(encodeXML("")); // <hi>

// An example of how you might wrap tryReadHTML / tryReadXML in a TransformStream:
// (will log )
const createStreamingEntityDecoder = (useXML) => {
  const read = useXML ? tryReadXML : tryReadHTML;
  let pending = "";
  return new TransformStream({
    transform(text, controller) {
      text = pending + text;
      pending = "";

      let start = 0; // start of the current segment to process

      for (let i = 0; i < text.length; i++) {
        if (text[i] != "&") continue;

        // Emit everything before "&" immediately
        if (i > start) {
          controller.enqueue(text.slice(start, i));
        }

        // Evaluate what's after "&"
        const afterAmp = text.slice(i + 1);
        const result = read(afterAmp);

        if (result.type == "keep-going") {
          // We might have an entity, but need more data. Hold from "&".
          pending = text.slice(i);
          return; // This chunk is finished
        } else if (result.type == "read") {
          // Emit the decoded entity
          controller.enqueue(result.content);

          // Advance past the entire entity: "&" + consumed
          const nextIndex = i + 1 + result.consumed;
          i = nextIndex - 1; // -1 because the loop will i++ next
          start = nextIndex;
        } else {
          // fail: not a valid entity; emit literal "&" and continue
          controller.enqueue("&");
          start = i + 1;
        }
      }

      // Emit any remaining text after the last processed segment
      if (start < text.length) {
        controller.enqueue(text.slice(start));
      }
    },

    flush(controller) {
      // If stream ends with an incomplete entity, emit it as-is
      if (pending) controller.enqueue(pending);
    },
  });
};
const stream = new Response(`<hi>`).body;
const textDecoder = new TextDecoderStream();
const entityDecoder = createStreamingEntityDecoder(false);
for await (const chunk of stream
  .pipeThrough(textDecoder)
  .pipeThrough(entityDecoder)) {
  process.stdout.write(chunk);
} = createStreamingEntityDecoder(false);
for await (const chunk of stream
  .pipeThrough(textDecoder)
  .pipeThrough(entityDecoder)) {
  process.stdout.write(chunk);
}ough(textDecoder)
  .pipeThrough(entityDecoder)) {
  process.stdout.write(chunk);
}
>

Benchmarks

escapeHTML

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities157b /   128 gz0.5µs3ns/b
entities339b /   263 gz0.52µs3.1ns/b
html-entities28538b / 13146 gz1,400µs5.4ns/b

escapeHTMLAttribute

[!NOTE] tinyentities serializes < and > here for e for safety, making it slower..

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities192b /   145 gz0.3µs4ns/b
entities328b /   259 gz0.51µs1.8ns/b
html-entities28538b / 13146 gz1,300µs5.4ns/b

escapeXML

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities129b /   117 gz0.29µs2.4ns/b
entities636b /   423 gz0.62µs5ns/b
html-entities28550b / 13150 gz1,400µs5.8ns/b

escapeXMLAttribute

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities189b /   143 gz0.31µs5ns/b
entities636b /   423 gz0.52µs5.1ns/b
html-entities28550b / 13150 gz1,400µs5.7ns/b

encodeHTML

[!NOTE] Other libraries have separate entity maps for encoding and decoding. If you're doing both, tinyentities will be smaller and not duplicate mappings. But if you only encode, like in this example, tinyentities will be slightly larger.er.

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities18177b /  7757 gz560µs13ns/b
entities14456b /  6247 gz130µs6.6ns/b
html-entities28535b / 13148 gz1,400µs13ns/b

encodeXML

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities267b /   225 gz0.51µs9.9ns/b
entities636b /   423 gz0.53µs5ns/b
html-entities28547b / 13153 gz1,400µs13ns/b

decodeHTML

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities18205b /  7699 gz590µs8.9ns/b
entities38623b / 22198 gz48µs7.1ns/b
html-entities28343b / 13252 gz1,400µs11ns/b

decodeXML

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities381b /   245 gz0.32µs7.7ns/b
entities6483b /  2223 gz7.9µs5.9ns/b
html-entities28357b / 13259 gz1,400µs9.8ns/b

tryReadHTML

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities18681b /  7940 gz570µs14ns/b
entities38277b / 22008 gz48µs12ns/b

tryReadXML

ImplementationSizeInitialize (sampled)Speed (sampled)
tinyentities725b /   438 gz0.32µs9.9ns/b
entities6141b /  2073 gz8.3µs10ns/b

Credit to

entities for showing the power of deltas in compression

html-entities for some awesome regex