cli icon indicating copy to clipboard operation
cli copied to clipboard

Request: `cli_span()` to programmatically create span elements

Open davidchall opened this issue 3 years ago • 3 comments
trafficstars

When creating cli span elements programmatically, I sometimes have to perform gymnastics to output a string that gives the intended result after interpolation. Would it be possible/easy to add a new function (perhaps cli_span()) that doesn't perform string interpolation?

For example,

cli_text("This is {.emph important}.")

might be equivalent to

cli_text("This is ", cli_span("important", class="emph"), ".")

davidchall avatar Jun 30 '22 15:06 davidchall

The first form is equivalent to the second one, and the first form is superior because it can be translated.

Can you please give a use case for the need of cli_span()?

gaborcsardi avatar Jul 01 '22 08:07 gaborcsardi

Hi @gaborcsardi - sorry, that example was demonstrating the desired solution but not really the motivation. Here's a more fleshed out example.

I'm the maintainer of {jinjar} for Jinja-like templating in R. I'm using {cli} to highlight blocks when printing a template object (see example), but the colors are currently hardcoded. I'd like to use custom span classes instead, so users can overwrite colors using cli theming (https://github.com/davidchall/jinjar/issues/19).

Here's an example of a template string and a data frame containing the start and end indices of each block. The goal is to create a cli output where a custom class has been applied to each type of span.

template <- '<ul id="navigation">
{% for item in navigation %}
    <li><a href="{{ item.href }}">{{ item.caption }}</a></li>
{% endfor %}
</ul>'

spans <- data.frame(
  type = c("text", "block", "text", "variable", "text", "variable", "text", "block", "text"),
  ix_open = c(1, 22, 50, 68, 83, 85, 103, 113, 125),
  ix_close = c(21, 49, 67, 82, 84, 102, 112, 124, 130)
)

There are a few challenges encountered by {cli} string interpolation:

  • The cli delimiters { } are hardcoded, so I need to escape these characters as {{ }} in the text.
    • Not a problem if using intermediate variables, e.g. {.emph {x}}
  • Leading and trailing whitespace is trimmed, so cannot use {.emph \fHello\f}
    • Must use intermediate variables to preserve whitespace, e.g. {.emph {x}}
  • Expressions are hardcoded to evaluate using parent.frame() environment. This makes it difficult to compose a cli string using functions, because the data variable must be available at the time of evaluation.
    • I found I can use cli::format_inline() to immediately render the string interpolation without outputting the message condition. This has the disadvantage of performing multiple evaluations instead of building up a document and evaluating once (could the cli theme change?)
  • Trailing newlines are ignored (I've opened a separate ticket for this #491)
style_template <- function(x, spans) {
  output <- character()
  for (i_row in 1:nrow(spans)) {
    span <- spans[i_row,]
    
    output <- c(output, style_block(x, span$type, span$ix_open, span$ix_close))
  }
  paste0(output, collapse = "")
}

style_block <- function(x, type, ix_open, ix_close) {
  txt_block <- substr(x, ix_open, ix_close)
  
  # force cli to display newlines
  txt_block <- gsub("\n", "\f", txt_block)
  
  # style using cli classes
  output <- cli::format_inline(paste0("{.jinjar_", type, " {txt_block}}"))
  
  # format_inline splits lines into vector
  paste0(output, collapse = "\n")
}

style_template(template, spans) |> writeLines()
#> <ul id="navigation">{% for item in navigation %}
#> <li><a href="{{ item.href }}">{{ item.caption }}</a></li>{% endfor %}
#> </ul>

With a function like cli::cli_span(), I'm hoping this could be reduced to something like:

style_template <- function(x, spans) {
  # force cli to display newlines
  txt_block <- gsub("\n", "\f", txt_block)

  output <- character()
  for (i_row in 1:nrow(spans)) {
    span <- spans[i_row,]

    span_txt <- substr(x, span$ix_open, span$ix_close)
    span_cls <- paste0("jinjar_", span$type)
    
    output <- c(output, cli::cli_span(span_txt, class = span_cls))
  }
  output
}

davidchall avatar Jul 05 '22 15:07 davidchall

Yeah, this is not a good use case for cli at all. It feels like we should have another function, that keeps all the whitespace, and then you could just call that with all the text, with the {.class } blocks properly inserted.

gaborcsardi avatar Aug 24 '22 17:08 gaborcsardi