developit / htm

Hyperscript Tagged Markup: JSX alternative using standard tagged templates, with compiler support.
Apache License 2.0
8.67k stars 170 forks source link

Inline elements at the beginning of a line do not have preceding whitespace. #206

Closed milochristiansen closed 3 years ago

milochristiansen commented 3 years ago

If you have a large block of text that is broken up into lines in the source for readability, and you happen to get unlucky enough to have an inline element at the beginning of the line, the element will not be separated from the preceding content with a space in the rendered output.

Take the following fragment:

html`
    Test line 1.
    Test line 2.
    <i>Test</i> line 3.
`

The rendered output that would be expected would be something like:

Test line 1. Test line 2. Test line 3.

instead you get:

Test line 1. Test line 2.Test line 3.

(Note the missing space)

This an issue because in page layouts with large paragraphs you are basically playing with fire. You can very easily run into a case where you have a "typo" despite the source being correct. It is possible to work around this issue, but that requires you to A) be aware of it and B) not be silently screwed by automated formatting tools.

developit commented 3 years ago

This is actually "working as designed" - it's the same output JSX generates:

// this:
<>
    Test line 1.
    Test line 2.
    <i>Test</i> line 3.
</>;

// compiles to this:
h(Fragment, null, "Test line 1. Test line 2.", h("i", null, "Test"), " line 3.");

// which renders:
"Test line 1. Test line 2.<i>Test</i> line 3."

I do agree that this behavior is unfortunate, though. Just HTM tries to match JSX as closely as possible, and this quirk is an important part of that.

FWIW, in both JSX and HTM, the "quick fix" is to use a string literal:

html`
    Test line 1.
    Test line 2.
    ${' '}
    <i>Test</i> line 3.
`

In HTM you could also use an escaped newline:

html`
    Test line 1.
    Test line 2.\
    <i>Test</i> line 3.
`

Demo: https://jsfiddle.net/developit/o2xs0gkw/

milochristiansen commented 3 years ago

Well... That output is problematic. It certainly violates the law of least astonishment. Is it perhaps a JSX bug?

developit commented 3 years ago

It was a grey area that the JSX spec/proposal did not describe, so the standard ended up being set by what the original Babel and Acorn implementations chose to do.

FWIW the logic for why this behavior was selected is because it prevents leading/trailing whitespace in JSXText, as well as whitespace between JSXElements.

One thing you might consider, if you're writing a lot of content directly in HTM tagged templates, would be to use a little intermediary function that patches this behavior before the strings get passed to HTM:

import htm from 'htm';

// your existing htm setup:
function h(type, props, ...children) { /* snip */ }
const htmlInternal = htm.bind(h);

// but instead of exporting htmlInternal, you export this wrapper:
const s = new WeakMap();
export function html(strings) {
    let str = s.get(strings);
    if (!str) s.set(strings, str = strings.map(s => s.replace(/\n(\s*<)/g, ' $1')));
    arguments[0] = str;
    return htmlInternal.apply(this, arguments);
}