@borgar/textbox

A utility to render formatted multi-line rich text.

Usage no npm install needed!

<script type="module">
  import borgarTextbox from 'https://cdn.skypack.dev/@borgar/textbox';
</script>

README

Textbox

Textbox is a simple library to layout multiline text for display on SVG or Canvas. It can fairly decently line-break and render rich text given some boundaries. It understands simple text, and a subset of HTML and LaTeX syntaxes. The original purpose of this software is to aid labeling charts.

Take a look at Textbox example and demos if you are curious what this library can do.

Features:

  • Line-break text to fit dimensions.
  • Can overflow text into ... if it doesn't fit designated area.
  • Understands common text features: bold, italic, links, etc...
  • Knows that there is different whitespace like thin, or non-breaking.
  • Tries to be smart about line-breaking before or after certain characters (it can occur after - but not before).
  • Supports hyphenation if text is prepared with soft-hyphens.

Installing

If you don't want to download and build Textbox yourself, the library is also provided as an NPM package:

$ npm install @borgar/textbox

API Reference

# the Textbox class

For any use, you will need to start by defining a new Textbox instance. You may pass a configuration object as a parameter:

const box = new Textbox({
  font: '12px/14px sans-serif',
  width: Infinity,
  height: Infinity,
  align: 'left',
  valign: 'top',
  x: 0,
  overflow: 'ellipsis',
  parser: Textbox.defaultparser,
  createElement: Textbox.createElement
});

Shown here are the defaults, but any or all of the above parameters can be provided. The Textbox instance will provide methods by the same name, along with line-breaking and rendering methods:

const box = new Textbox()
  .font('12px/14px sans-serif')
  .align('left')
  .createElement(React.createElement);

# .font( css_font_shorthand )

Define what font to use. This will allow setting both font-size and line-height as well as font-family. Defaults to 12px/14px sans-serif.

# .width( width_in_px )

Controls the horizontal dimension of the text. This can be a callback function if you want runaround text layout, or to flow the text into irregular space. Defaults to Infinity (a single line).

A callback provided to this will be called every line with the line number as a parameter.

# .height( height_in_px )

Controls the vertical dimension of the text. Defaults to Infinity. If the text ends up with more lines than fit into the height, the text is cut and postfixed with an overflow indicator (see overflow).

# .x( indent_in_px )

Sets per-line text horizontal indent. This is most useful for flowing text into irregular shapes. A callback provided to this will be called every line with the line number as a parameter.

# .align( alignment )

Sets text horizontal alignment. Accepts all values you would expect CSS text-align to understand, as well as SVG text-anchor equivalents: left, start, center, middle, right, end, and justify.

# .valign( alignment )

Sets text vertical alignment. Accepts all values you would expect CSS vertical-align to understand: top, start, center, middle, bottom, and end.

# .overflow( indicator )

Sets text overflow mark, similar to CSS text-overflow. The keyword ellipsis will set the overflow mark to , otherwise the provided string is used as-is.

# .overflowLine( indicator )

Sets text per-line overflow mark, similar to .overflow. This setting is off by default. The keyword ellipsis will set the overflow mark to , otherwise the provided string is used as-is.

# .overflowWrap( indicator )

Sets whether textbox should line-break within a word to prevent text from overflowing. This setting is normal by default which allows words to exceed the current line-width (unless line-overflow is set). Alternatively it may be set to break-word which will force a break mid-words.

# .parser( parser )

Selects which text parser to use. The available parsers are:

  • Textbox.textparser (the default, may be selected with "text")
  • Textbox.htmlparser (may be selected with "html")
  • Textbox.latexparser (may be selected with "latext")

Textbox will look for a parser on the instance first, then default to Textbox.defaultparser. So you if you know that you will exclusively be using the HTML parser, you can change the default once:

Textbox.defaultparser = Textbox.htmlparser;

# .createElement( element_factory )

Set the element factory method for creating elements for SVG rendering. This has the same interface as React.createElement so you may assign that if you are rendering a React application.

If nothing is provided Textbox will default to Textbox.createElement so you can change the default once globally:

Textbox.createElement = React.createElement;

# .linebreak( text )

Parses text, flows it into the set dimensions and returns a list of the lines. The returned object can then be passed on to .render().

As well as a list of lines of tokens, the lines object has a height property which is useful if you want to set the render destination to the fit the text.

The lines object additionally has a render method so you can pass it on without having the originating Textbox instance.

// No height is set to the box
const box = new Textbox({ width: 150 });
// Text is turned into lines
const lines = box.linebreak( longTextPassage );
// Destination canvas is set to the height of the output
myCanvas.height = lines.height;
// Lines are rendered to the canvas
lines.render( myCanvas );

The lines render method is flexible when it comes to its arguments. It can accept a Canvas, a CanvasRenderingContext2D, or nothing in which case it will emit SVG. See .render().

The lines object also includes a .hasOverflow property which indicates if the text was able to fit a designated height. It will be true if the text did not fit the defined space.

# .render( text )
# .render( text, Canvas )
# .render( text, CanvasRenderingContext2D )
# .render( text, d3-selection )

Render text or a "lines object" (see .linebreak()) to either SVG or Canvas.

The lines render method is flexible when it comes to its arguments and their order. If provided either a Canvas, a CanvasRenderingContext2D it will render to the canvas, otherwise it will emit SVG built with the .createElement() interface.


# Textbox.measureText( text, font )

A utility function to measure the width of a string. Returns the width of the text in a number of pixels. This is the same utility that Textbox uses internally. The font argument is expected to be a valid CSS font shorthand value.

Textbox.measureText("Lorem Ipsum", "bold 15px/20px sans-serif");

Limitations:

  1. In SVG links do not automatically get color or pointer cursor. You will need to add your own styles to these.

    svg text a {
      fill: blue;
      text-decoration: underline;
      cursor: pointer;
    }
    
  2. Multi-word links in SVG text are not necessarily a single entity/element like they are in HTML. If the text in a link is line broken, then hovering one segment will not cause segment in the next line over to trigger hover styles.

  3. Text justification is fairly broken in SVG. Avoid it for formatted text.

    1. Justification does not work consistently in all browsers as they have buggy support for the word-spacing property. Google Chrome seems to work fine, Safari works for plain text but incorrectly for formatted text. Firefox is lost in the woods.

    2. Because of the way text handling is done in SVG, justifying underlined text will create gaps in the underlined text.

  4. Subscript and superscript are bugged in Firefox because they have never implemented the baseline-shift property. The bug report is 15 years old when this is written so not likely to be solved soon.