styles icon indicating copy to clipboard operation
styles copied to clipboard

Including code snippets in callouts

Open ocaisa opened this issue 4 years ago • 13 comments

Not sure if this is the right place to report this, but it doesn't seem to be possible to include code snippets in a callout (or any special blockquote), for example:

> ## Learn to call the **GPU** package from command-line
>
> In this exercise, we'll deal with a Lennard-Jones (LJ) system as described by the
> following input file. You can vary the system size and the length of the run using the
> variables `x`, `y`, `z`, and `t`. For this exercise, let us choose `x = y = z = 60`. Since
> this is a system with `fcc` lattice, the total number of atoms should be 864,000 for
> the chosen values of `x`, `y`, and `z`. Let, `t` = 500.
> We'll call the **GPU** package from the command-line in this case. Can you prepare a
> job submission file for this system such that it enables to use 2 GPUs with 24 MPI ranks.
> Make sure that the neighbor is built on the CPUs and there is a dynamic load balancing
> between the CPUs and the GPUs.
> ```
> {% include /snippets/ep05/in.lj %}
> ```
> {: .code}
>
{: .challenge}

will not render correctly if in.lj contains multiple lines.

I imagine this is because the snippet is directly inserted including the line breaks. Is there a fix for this?

ocaisa avatar Jul 02 '20 07:07 ocaisa

This comes close to working, but removes the newlines rather than replace them with \n> ...and still wouldn't work in the nested case (like {: .solution}):

> {% capture mycode %}
{% include /snippets/ep05/in.lj %}
{% endcapture %}{{ mycode | strip_newlines }}

ocaisa avatar Jul 02 '20 07:07 ocaisa

I got it to work but it was pretty tedious:

> {% capture mycode %}
{% include /snippets/ep05/in.lj %}
{% endcapture %}{{ mycode | strip | newline_to_br | replace: '<br />', '<br />> ' | strip_html | strip }}

For a nested blockquote that would be

{% endcapture %}{{ mycode | strip | newline_to_br | replace: '<br />', '<br />> > ' | strip_html | strip }}

If it is somehow possible to create a function for this, a numerical argument would be enough to know how many > to insert.

ocaisa avatar Jul 02 '20 08:07 ocaisa

What you do here is pretty smart, @ocaisa. I haven't heard of Liquid functions so what you propose might be the best one can do if they want to include code from a file in a callout box. By the way, can't one replace <br /> with > directly?, e.g.:

{% endcapture %}{{ mycode | strip | newline_to_br | replace: '<br />', '> ' }}

maxim-belkin avatar Jul 03 '20 22:07 maxim-belkin

Not as smart as I hoped, I hadn't noticed but it is leaving behind a > at the end of lines that aren't simply whitespace. I guess it is actually the whitespace lines that are the problem, but this is a bit too hard to figure out, I will have to find another way

ocaisa avatar Jul 07 '20 15:07 ocaisa

Ok, the whitespace was indeed the problem. I got it to work by creating my own liquid plugin:

# Declare module of your plugin under Jekyll module

module Jekyll::CustomFilter

  # Each method of the module creates a custom Jekyll filter

  def append_to_newline(input, append_string = '')
    input.to_s.strip.gsub(/\n/, "\n" + append_string.to_s)
  end

end

Liquid::Template.register_filter(Jekyll::CustomFilter)

which is stored in an _plugins directory relative to the root directory. You can then use this filter with

> ```
> {% capture mycode %}
{% include /snippets/ep05/in.lj %}
{% endcapture %}{{ mycode | append_to_newline: "> " }}
> ```
> {: .code}

I would have liked to simplify this to be able to create { {% include_in_callout /snippets/ep05/in.lj 1 %}} where 1 indicates the callout depth but I couldn't figure out how to do that (this is my first look at ruby).

ocaisa avatar Jul 08 '20 07:07 ocaisa

If input is an array of strings, you can do this:

# num -- "depth"
input.map { |line| "> " * num + line}.join("\n")

E.g.

%w[one two three].map{ |line| "> " * 3 + line }.join("\n")
=> "> > > one\n> > > two\n> > > three"

if input is a string, you'd have to split it first using .split("\n"):

"one\ntwo\nthree".split("\n").map{ |line| "> " * 3 + line }.join("\n")
=> "> > > one\n> > > two\n> > > three"

I have to note that the code you provided above didn't work for me as expected for some reason. Perhaps it has to do with double empty lines in the python code snippet that I used.

maxim-belkin avatar Jul 08 '20 09:07 maxim-belkin

Thanks, that worked for me. For completeness I'll give my final solution. I created _plugins/custom_filters.rb with the contents

# Declare module of your plugin under Jekyll module

module Jekyll::CustomFilter

  # Each method of the module creates a custom Jekyll filter

  def multiline_string_in_callout(input, levels_of_indent = 0)
    input.split("\n").map{ |line| "> " * levels_of_indent + line }.join("\n")
  end

end

Liquid::Template.register_filter(Jekyll::CustomFilter)

Note that for the filter to be picked up, you must restart the server (with make serve)

I can then use the filter in my lesson with

> ```
{% capture mycode %}
{% include /snippets/ep05/in.lj %}
{% endcapture %}{{ mycode | multiline_string_in_callout: 1 }}
> ```
> {: .code}

ocaisa avatar Jul 08 '20 10:07 ocaisa

Great job! Note, you can capture code snippets early in the episode, e.g.

{% capture mycode %}
{% include /snippets/ep05/in.lj %}
{% endcapture %}
...
...
> ```
{{ mycode | multiline_string_in_callout: 1 }}
> ```
> {: .code}

maxim-belkin avatar Jul 08 '20 10:07 maxim-belkin

@maxim-belkin I leave it to you to decide whether to close this or not, thanks for the help.

ocaisa avatar Jul 08 '20 10:07 ocaisa

Thanks, @ocaisa. I'd like to keep it open for now. It might be beneficial for the community to integrate this into the main template. It'll tie us more to the Shopify Liquid so we'd have to review this carefully.

maxim-belkin avatar Jul 08 '20 10:07 maxim-belkin

This is a never-ending story! It turns out that while I could served the site locally, it didn't pick up my plugin when I served on GitHub (or when I tried to use the remote Carpentries theme), so I had to go back and try to do it using standard filters.

I managed to get it working this time but it is very very picky about the syntax:

>
> {% capture mycode %}{% include /snippets/ep05/in.lj %}{% endcapture %}
> {% assign lines_of_code = mycode | newline_to_br | strip_newlines | split: "<br />" %}
> ~~~{% for member in lines_of_code %}
> {{ member }}{% endfor %}
> ~~~
> {: .source}

ocaisa avatar Jul 09 '20 16:07 ocaisa

This is a never-ending story! It turns out that while I could served the site locally, it didn't pick up my plugin when I served on GitHub

Yes, GitHub works in "safe" mode and does not enable plugins. It is possible to workaround this by using GitHub Actions but your solution is nothing short of brilliant, so GitHub Actions can wait!

Great stuff!

maxim-belkin avatar Jul 09 '20 18:07 maxim-belkin

and don't forget about using ~~~ instead of ```

maxim-belkin avatar Jul 09 '20 18:07 maxim-belkin