cli
cli copied to clipboard
Request: `cli_span()` to programmatically create span elements
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"), ".")
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()?
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}}
- Not a problem if using intermediate variables, e.g.
- Leading and trailing whitespace is trimmed, so cannot use
{.emph \fHello\f}- Must use intermediate variables to preserve whitespace, e.g.
{.emph {x}}
- Must use intermediate variables to preserve whitespace, e.g.
- 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?)
- I found I can use
- 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
}
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.