Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Simon Free and Jason Dean
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Simon Free@simonfree ) and Jason Dean@JasonPDean )

Using Chalk 2.0's Tagged Template Literals For Nested And Complex Styling

By Ben Nadel on

Last week, Chalk - one of my favorite node modules - released version 2.0 of its core library. Among the upgrades was the incorporation of ES6 Tagged Template literals for generating nested and embedded Chalk styles. While Chalk works seamlessly in 95% of use-cases, the one behavior that can be a little hit-and-miss is the use of nested styles - particularly those that don't explicitly set the foreground or background colors. As such, I wanted to see if the new tagged templates would fill in the missing behavior gaps.

When using tagged template literals with Chalk 2.0, you have to use a special block-syntax in which the curly-braces indicate style delimiters:

chalk`{red.bold This is my styled content.}`

In this case, I'm using "red.bold" to style the content contained within the open-brace and close-brace. And, of course, these blocks can be nested:

chalk`{red This is my {bold styled} content.}`

Here, the full text is "red", but only a small portion of it is also styled "bold."

As I've noted before, using "dim" with nested Chalk styles can exhibit quirky behavior. So, I think it makes sense to do our initial testing with "dim". In the following code, I'm using both standard interpolation techniques and tagged template literals to see how the behavior changes with the various approaches:

  • // Require the core node modules.
  • var chalk = require( "chalk" );
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // Try using plain-old interpolation.
  • // --
  • // CAUTION: Will not work as hoped, "reset" bleeds into rest of string.
  • console.log( chalk.dim.underline( `Alpha ${ chalk.reset.bold.red( 'Beta' ) } Charlie` ) );
  •  
  • // Try using nested invocations.
  • // --
  • // CAUTION: Will not work as hoped, "reset" bleeds into rest of string.
  • console.log( chalk.dim.underline( "Alpha", chalk.reset.bold.red( "Beta" ), "Charlie" ) );
  •  
  • // Try using NEW tagged templates (in v2.0) with embedded blocks.
  • console.log( chalk`{dim.underline Alpha {reset.bold.red Beta} Charlie}` );
  •  
  • // Try using NEW tagged templates with interpolated block values.
  • // --
  • // CAUTION: This will not work because interpolated "{" "}" characters are escaped (as
  • // documented in the Chalk read-me).
  • var embedded = "{reset.bold.red Beta}";
  •  
  • console.log( chalk`{dim.underline Alpha ${ embedded } Charlie}` );

If you were to look strictly at the code (and not at the output), I think you would eventually come to the conclusion that this code would output the same line of text four times. After all, the intent of each line of code appears to be the same. But, when we run this code through Node.js (v7.10.0), we get the following output:


 
 
 

 
 Looking at Chalk 2.0's tagged template literals. 
 
 
 

As you can see, even though the intent of each line appears to be the same, the four lines of code give us 3 distinct outcomes. And, only one of the tagged template literals is able to work with the historically-problematic style combinations; the second tagged template literal shows us an escaped style block.

This escaping of substituted style blocks is clearly documented in the Chalk Read Me; and, is likely done for safety's sake. But, it means that we can't use tagged templates to fulfill complex styling needs.

Well, not directly. If we step back and think about what a tagged template literal is, it's nothing more than a template and a Function that processes the template substitutions. This means that we can do some pre-processing of the template to collapse the nested style blocks down into a single string. Then, we can invoke the chalk template function explicitly on that single string:

  • // Require the core node modules.
  • var chalk = require( "chalk" );
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // Here is our proxy to the chalk() template method.
  • function chalkish( parts, ...substitutions ) {
  •  
  • var rawResults = [];
  • var cookedResults = [];
  •  
  • var partsLength = parts.length;
  • var substitutionsLength = substitutions.length;
  •  
  • for ( var i = 0 ; i < partsLength ; i++ ) {
  •  
  • rawResults.push( parts.raw[ i ] );
  • cookedResults.push( parts[ i ] );
  •  
  • if ( i < substitutionsLength ) {
  •  
  • rawResults.push( substitutions[ i ] );
  • cookedResults.push( substitutions[ i ] );
  •  
  • }
  •  
  • }
  •  
  • // Now that we have all the template parts and the value substitutions from the
  • // original string, we can build the SINGLE value that we pass onto chalk. This
  • // will cause chalk to evaluate the original template as if it were a static
  • // string (rather than a set of value substitutions).
  • var chalkParts = [ cookedResults.join( "" ) ];
  • chalkParts.raw = [ rawResults.join( "" ) ];
  •  
  • return( chalk( chalkParts ) );
  •  
  • }
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • var embedded = "{reset.bold.red Beta}";
  •  
  • console.log( chalkish`{dim.underline Alpha ${ embedded } Charlie}` );

Here, we've created a template tag - chalkish() - that handles the substitutions. The chalkish() function then turns around and calls the underlying chalk() template tag, passing it a single string which we know it can handle. And, when we run this code, we get the following output:


 
 
 

 
 Looking at Chalk 2.0's tagged template literals. 
 
 
 

As you can see, by pre-processing the template literal substitutions before calling the chalk() template tag, we get the outcome we want with the nested value substitutions that we may need with more complex styling requirements.

Chalk has always been one of my favorite node modules. And now, with the introduction of tagged template literals in Chalk 2.0, we can finally generate accurate content with even the most demanding of requirements. The syntax is a little clunky; and, it requires a little elbow grease; but, the power is clearly there.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

I have to ask (and to be clear, "practicality" is rarely something I care about), but what is the *real* use for something like this? Don't get me wrong - I can definitely seeing using different colors in output to help point out success/error/warnings, but this seems *incredibly* complex for what is - most usually - just debug output for programmers.

Reply to this Comment

@Raymond,

Oh yeah, to be clear, I only use Chalk for R&D output. Once I move to production, colors become irrelevant as it all just gets consumed by some log aggregation daemon (or however the DevOps guys work their magic).

And, to be clear, the tagged templates are just for a small set of use-cases where the color requirements are significantly complex. In most cases, I just use the chalk style methods directly, like:

console.log( chalk.red.bold( "Hello world" ) );

The only time I would need to even bother with tagged templates would be if I needed to iterate of styling, or use something like a RegEx replace to build a string that represents the styled output. But, this kind of use-case is few and far between.

If you really step back and get perspective, the truth is, the tagged templates "work" where there may be actual "bugs" in the simpler API calls. So, if nesting of styles worked a little more naturally with the simple APIs, it would likely obviate the needs for tagged templates to begin with :D

But, again, this is all just for R&D kinds of stuff. In production, I don't need Chalk.

Reply to this Comment

I guess what I'm saying is - what is this?

"the tagged templates are just for a small set of use-cases where the color requirements are significantly complex"

Like - I'm not saying you would never have a reason for this - I just want to hear one. ;)

Reply to this Comment

@Raymond,

Ah, for sure. One concrete case that I have run into is trying to take a string of SQL text and pretty-print it to the console. Where I can add line breaks and special formatting around the keywords. For example, if I wanted to wrap the entire SQL text in "dim.italic" and then wrap each reserved token (SELECT, FROM, WHERE, etc) in a "reset.bold.red". Without the tagged templates, that becomes [more] tricky because you essentially have to log each line individually to get the cascading style to work ... or, at least, that was the route I was going down before I kind of just gave up :D. But, with tagged template literals, you can treat it more like would any string manipulation and it works.

Another time I've run into issue was when I was trying to pretty-print a nested Error structure to the console, with special formatting for "root cause" errors.

I guess, the common theme is having some large chunk of text that you want to have one style and then override that style for only part of the text. And, you need to use string manipulation to figure out where those parts are.

Let me noodle on this and see if I can come up with a codified example.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.