carpentries / styles

Styles for The Carpentries lessons. No README to avoid merge conflicts with lessons. Demo 👇
https://carpentries.github.io/lesson-example
Other
84 stars 96 forks source link

Including code snippets in callouts #471

Open ocaisa opened 4 years ago

ocaisa commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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.

maxim-belkin commented 4 years ago

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 />', '> ' }}
ocaisa commented 4 years ago

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 commented 4 years ago

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).

maxim-belkin commented 4 years ago

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.

ocaisa commented 4 years ago

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}
maxim-belkin commented 4 years ago

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}
ocaisa commented 4 years ago

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

maxim-belkin commented 4 years ago

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.

ocaisa commented 4 years ago

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}
maxim-belkin commented 4 years ago

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 commented 4 years ago

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