Search code examples
javascripthtmlcsspre

Number the verses of poems with CSS and JS


I try to number the verses of poems, but the result is not quite satisfactory.

  1. I want to align all verses like the first one.
  2. Maintain a consistent line-height.
  3. Blank line must not be counted

The result should match the second blank example (but with number).

  const pre = document.getElementById('canto');
  const righe = pre.textContent.split('\n');

  pre.innerHTML = '';

  righe.forEach(riga => {
    const span = document.createElement('span');
    span.textContent = riga.trim();
    pre.appendChild(span);
    pre.appendChild(document.createElement('br'));
  });
pre {
    background-color: #f9f9f9;
    padding: 2rem;
    counter-reset: verso;
    white-space: pre-wrap;
}

pre span {
    display: block;
    margin: 0;
    padding: 0;
    line-height: 1;
    counter-increment: verso;
}

pre span:nth-child(3n+1)::before {
    content: counter(verso) " ";
}
<b>With Span</b>
<pre id="canto">
Nel mezzo del cammin di nostra vita
mi ritrovai per una selva oscura,
ché la diritta via era smarrita.

Ahi quanto a dir qual era è cosa dura
esta selva selvaggia e aspra e forte
che nel pensier rinova la paura!
</pre>

<hr>

<b>Without Span</b>
<pre>
Nel mezzo del cammin di nostra vita
mi ritrovai per una selva oscura,
ché la diritta via era smarrita.

Ahi quanto a dir qual era è cosa dura
esta selva selvaggia e aspra e forte
che nel pensier rinova la paura!
</pre>


Solution

  • The counter() functions counting pattern is really bonkers: 1, 4, and 7? Here's the reason:

      pre span:nth-child(3n+1)::before {...
    
    `:nth-child(3n+1)` counts every third element (`3n`) starting at one (`+1`).
    

    Here's the heart of the following solution:

     // The extracted string
     text
    
      // Split text at every line break
      .split(/\r?\n|\r|\n/g)
    
      /* .flatMap() allows us to dictate what return whatever
      || we want and we get the freedom of declaring 
      || the conditions.
      || .forEach() and loops allow us that freedom but 
      || .flatMap() is streamlined and simple like .map().
      */
      .flatMap(line => {
    
        /* We're using ternary operator 
       || (an abbreviated "if", "else if" control)
       || If the line doesn't have any whitespace...
       || return `<q>${line}</q>`
       || but if it does return `<br>`
       */
        return /[^\s]/.test(line) 
          ?`<q>${line}</q>` 
          :`<br>`
      });
    

    Solution

    const renderLines = (selector = "body") => {
    
      const node = document.querySelector(selector);
      
      if (node.tagName !== "PRE") {
        node.style.whiteSpace = "pre";
      }
      
      const text = node.textContent;
      
      const lines = text
        .split(/\r?\n|\r|\n/g)
        .flatMap(line => {
          return /[^\s]/.test(line) ?
          `<q>${line}</q>` :
          `<br>`
      });
    
      node.replaceChildren();
    
      lines.forEach(L => {
        node.insertAdjacentHTML("beforeend", L);
      });
    };
    
    renderLines("pre");
    :root {
      font: normal 400 2ch/1.5 "Philosopher", serif;
    }
    
    h1 {
      font: small-caps 400 1.4rem/1.3 "Aboreto", serif;
    }
    
    pre {
      font: inherit;
      counter-reset: line 0;
    }
    
    q {
      display: block;
      counter-increment: line;
    }
    
    q::before {
      content: "\A0" counter(line) "\A0\A0 ";
    }
    
    q:after {
      content: "";
    }
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Aboreto&family=Philosopher&display=swap" rel="stylesheet">
    
    <main>
      <h1>Inferno: Canto 1</h1>
    
      <pre>
    Nel mezzo del cammin di nostra vita
    mi ritrovai per una selva oscura,
    ché la diritta via era smarrita.
    
    Ahi quanto a dir qual era è cosa dura
    esta selva selvaggia e aspra e forte
    che nel pensier rinova la paura!
      </pre>
    </main>