0

While creating a responsive web design it often happens that a one-letter word remains as the last word in a paragraph's line:

This is a
paragraph

I would like to move such a single-letter word as follows:

This is
a paragraph

Is there any built-in property in CSS which allows to achieve this effect? If there is none, how can this be done in JavaScript?

blondkarol
  • 41
  • 9
  • Interesting. It's tricky because (AFAIK) there's no *easy* way to discern the position of natural line breaks. You might want to look at how Adobe's ["balance text" plugin](https://github.com/adobe/balance-text/blob/master/balancetext.js) does it to solve this [different but in some ways similar problem](https://stackoverflow.com/questions/2908697/balanced-text-wrapping-in-html) - it appears to involve measuring the width of the text in a `white-space: nowrap` wrapper broken in various places. – user56reinstatemonica8 May 06 '20 at 23:24
  • After I answered incorrectly, not realizing exactly what you want, I now believe you should use canvas for this. https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/measureText – StackSlave May 06 '20 at 23:25
  • By the way, just found out that this is a duplicate of https://stackoverflow.com/questions/22984925/one-letter-word-on-the-end-of-line-justify – davidgiesemann May 06 '20 at 23:31
  • Does this answer your question? [One letter word on the end of line (justify)](https://stackoverflow.com/questions/22984925/one-letter-word-on-the-end-of-line-justify) – Rob May 07 '20 at 16:43
  • @Rob, to some extent, but not completely. Terry's answer is exactly what I was looking for. – blondkarol May 07 '20 at 18:16

3 Answers3

2

If you want to ALWAYS keep "a" and "paragraph" together, add a "non-breaking-space" between them:

<div style="width:100px;border:1px solid #333;padding:5px">
<p>
This is a&nbsp;paragraph
</p>
</div>
davidgiesemann
  • 941
  • 6
  • 16
  • As there are tons of text on my website, I need something more automated, but this seems to be a good starting point! – blondkarol May 06 '20 at 23:23
  • This could work with regex replace, replacing all instances of space-one character-space with space-one character- . It would be far more performant than anything based on repeatedly measuring widths. – user56reinstatemonica8 May 06 '20 at 23:33
  • @blondkarol I just added a JavaScript code snippet that automates this substitution process. – terrymorse May 07 '20 at 16:43
1

In order to keep single-letter words (like 'a' or 'I') on the same line as the following word, you can replace the space character between them with the non-breaking space Unicode character \u00A0.

This could be automated with a little JavaScript. For example, this code replaces [space][letter][space] with [space][letter][non-breaking space]:

const modifySingleChars = str => str.replace(/ ([a-zA-Z]) /g,
    ' $1' + '\u00A0');

To change all instances on a page, first collect all the text nodes within the body (skipping anything inside a <script> tag.

Then simply iterate through the text nodes, making the appropriate substitutions.

A working example:

// form array of all text nodes in parentNode
function allTextNodes(parentNode) {
  let arr = [];
  if (!parentNode) {
    return arr;
  }

  let nodes = parentNode.childNodes;
  nodes.forEach(node => {
    if (node.nodeName === 'SCRIPT') {
      return;
    }
    if (node.nodeType === Node.TEXT_NODE) {
      arr.push(node);
    } else {
      arr = arr.concat(allTextNodes(node));
    }
  });
  return arr;
}

// convert [space][letter][space] to [space][letter][non-breaking space];
const modifySingleCharWords = str => str.replace(/ ([a-zA-Z]) /g,
  ' $1' + '\u00A0');

function fixAllSingleCharWordsInBody() {
  let tNodes = allTextNodes(document.body);
  tNodes.forEach(tNode => {
    tNode.nodeValue = modifySingleCharWords(tNode.nodeValue);
  });
}
<body>
  <div id="wrapper" style="width:20rem">
    <h4>Prevent single character words at end of line</h4>
    <button type="button" onclick="fixAllSingleCharWordsInBody();">Fix All Words
  </button>
    <p>Lorem &nbsp;ipsum dolor i amet, consectetur a dipiscing elit, sed o eiusmod tempor incididunt u labore et dolore magna aliqua. <span>Nisl purus i mollis</span> nunc.
    </p>
    <p>In vitae turpis massa e elementum tempusus a sed. Eget mi proin e libero enim i faucibus. Quis lectus nulla a volutpat diam ut.
    </p>
    <p>Pharetra e ultrices neque ornare. Donec a tristique risus e feugiat in fermentum. Consectetur adipiscing e u aliquam purus sit amet.
    </p>
    <p>Vitae congue mauris rhoncus aenean e elit scelerisque mauris pellentesque. Mauris u eros i cursus turpis a tincidunt dui.
    </p>
    <p>At volutpat diam u venenatis tellus. Tellus integer feugiat scelerisque varius morbi i nunc faucibus at.</p>
    <script>
      const b = 'Do not modify anything inside a script tag';
    </script>
  </div>
</body>
terrymorse
  • 6,771
  • 1
  • 21
  • 27
0

from few years we have ES6 so...

[...document.body.querySelectorAll('*')]
        .map(n => n.firstChild)
        .filter(n => (n != null && n.nodeType === Node.TEXT_NODE && !['SCRIPT', 'STYLE'].includes(n.parentNode.nodeName)))
        .forEach(el => {
            el.nodeValue = el.nodeValue.replace(/ ([a-zA-Z]) /g, ` $1\u00A0`)
        });

document.body can be replaced by any node, here is for simplicity

Marek Lisiecki
  • 498
  • 6
  • 10