Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In knitr HTML output, include line numbers of R Markdown source code?

Question: Is there an automatic way to add the line numbers of the original R Markdown source code to the formatted code portions of the HTML output produced by knitr?

Purpose: My ultimate goal is to be able to quickly move to parts of my source R Markdown code that I identify need editing while reviewing the HTML output. Using line numbers is the fastest way I know to do this, but I welcome hearing others' strategies.

Solutions I've tried:

  • Although the chunk option attr.source = '.numberLines' will attractively add line numbers to the code parts of the HTML output, that option doesn't provide the source-code line numbers automatically (you must force that manually using .startFrom) -- instead, the lines are renumbered at the beginning of each chunk and after each piece of output. In the following illustration, I've included .startFrom to force the line numbering to start at 10, to match the line number for test_data <- rnorm(10) which is the line number I want to see. A practical solution, however, needs the starting number to be automatic. Also, in the HTML output (shown beneath the code) the hist(test_data) line is renumbered starting with the same starting number, 10. I would want that to be 12, as in the source code. Screenshot of example source R Markdown, showing line numbers Output HTML after knitting
  • This question (How can I add line numbers that go across chunks in Rmarkdown?) is related, but the OP just needed any unique identifier for each line, not necessarily the line numbers of the source code, with the solution being sequential numbers unrelated to the source-code line numbers.

Considered option: I've considered preprocessing my code by running an initial script that will add line numbers as comments at the end of lines, but I'd prefer a solution that is contained within the main knitr file.

like image 562
nuthatch Avatar asked Oct 23 '25 00:10

nuthatch


1 Answers

Reverted to this update based on your request

I'm glad you figured out the issue. I hadn't considered or tested the code for a chunk that only had a single line of code. However, based on your feedback, I've accounted for it now.

If you'd like it accounted for and would like to keep the color in the code, let me know. I'll add it back with the fix for single lines of code. (I'll stick to the ES6.)

This version uses the line numbers you'll see in the source pane of RStudio. You must use RStudio for this to work. The following changes to the RMD are necessary:

  • The library(jsonlite) and library(dplyr)
  • An R chunk, which you could mark include and echo as false
  • A set of script tags outside of and after that chunk
  • A JS chunk (modified from my original answer)

The R chunk and R script need to be placed at the end of your RMD. The JS chunk can be placed anywhere.

The R chunk and script tags **in order!

Put me at the end of the RMD.

```{r ignoreMe,include=F,echo=F}

# get all lines of RMD into object for numbering; R => JS object 
cx <- rstudioapi::getSourceEditorContext()$contents
cxt <- data.frame(rws = cx, id = 1:length(cx)) %>% toJSON()

```

<script id='dat'>`r cxt`</script>

The JS chunk

This collects the R object that you made in the R chunk, but its placement does not matter. All of the R code will execute before this regardless of where you place it in your RMD.

```{r gimme,engine="js",results="as-is",echo=F}

setTimeout(function(){
  scrCx = document.querySelector("#dat"); // call R created JSON*
  cxt = JSON.parse(scrCx.innerHTML);
  echoes = document.querySelectorAll('pre > code'); // capture echoes to #
  j = 0;
  for(i=0; i < echoes.length; i++){ // for each chunk
    txt = echoes[i].innerText;
    ix = finder(txt, cxt, j);  // call finder, which calls looker
    stxt = txt.replace(/^/gm, () => `${ix++} `); // for each line
    echoes[i].innerText = stxt;          // replace with numbered lines
    j = ix; // all indices should be bigger than previous numbering
  }
}, 300);

function looker(str) {  //get the first string in chunk echo
  k = 0;
  if(str.includes("\n")) {
    ind = str.indexOf("\n");
  } else {
    ind = str.length + 1;
  }
  sret = str.substring(0, ind);
  oind = ind; // start where left off
  while(sret === null || sret === "" || sret === " "){
    nInd = str.indexOf("\n", oind + 1);       // loop if string is blank!
    sret = str.substring(oind + 1, nInd);
    k++;
    ind = oind;
    oind = nInd;
  }
  return {sret, k};  // return string AND how many rows were blank/offset
}

function finder(txt, jstr, j) {
  txsp = looker(txt);
  xi = jstr.findIndex(function(item, j){ // search JSON match
    return item.rws === txsp.sret;       // search after last index
  })
  xx = xi - txsp.k + 1; // minus # of blank lines; add 1 (JS starts at 0)
  return xx;
}

```

If you wanted to validate the line numbers, you can use the object cx, like cx[102] should match the 102 in the HTML and the 102 in the source pane.

I've added comments so that you're able to understand the purpose of the code. However, if something's not clear, let me know.

enter image description here

ORIGINAL

What I think you're looking for is a line number for each line of the echoes, not necessarily anything else. If that's the case, add this to your RMD. If there are any chunks that you don't want to be numbered, add the chunk option include=F. The code still runs, but it won't show the content in the output. You may want to add that chunk option to this JS chunk.

```{r gimme,engine="js",results="as-is"}

setTimeout(function(){
  // number all lines that reflect echoes
  echoes = document.querySelectorAll('pre > code');
  j = 1;
  for(i=0; i < echoes.length; i++){ // for each chunk
    txt = echoes[i].innerText.replace(/^/gm, () => `${j++} `); // for each line
    echoes[i].innerText = txt;          // replace with numbered lines
  }
}, 300)


```

It doesn't matter where you put this (at the end, at the beginning). You won't get anything from this chunk if you try to run it inline. You have to knit for it to work.

I assembled some arbitrary code to number with this chunk.

like image 134
Kat Avatar answered Oct 24 '25 15:10

Kat



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!