getzola / zola

A fast static site generator in a single binary with everything built-in. https://www.getzola.org
https://www.getzola.org
MIT License
13.37k stars 936 forks source link

Codeblock naming #1851

Open kartva opened 2 years ago

kartva commented 2 years ago

Add some more custom syntax to the codeblock to allow for codeblock naming.

By writing

```rust,name="Shell code"

If this seems like too much of a change, then we could instead introduce a config variable to include the language name in the codeblock.

Prototype of how this looks:
![image](https://user-images.githubusercontent.com/51814158/166746728-6cb11482-d316-43c3-8861-cba208e91587.png)

with HTML looking like:

```html
<pre data-linenos="" data-lang="rust" class="language-rust z-code">
    <div class="codeblock-name">rust</div><code class="language-rust" data-lang="rust">
    <code class="language-rust" data-lang="rust">...</code>
</pre>
kartva commented 2 years ago

Currently, I've modified Zola by adding to https://github.com/getzola/zola/blob/1fbe6fbbefd192276165798c936cc62195d94116/components/rendering/src/codeblock/mod.rs#L51

    html.push('>');

    html.push_str("<div class=\"language-name\">");
    if let Some(lang) = language {
        html.push_str(lang);
    }
    html.push_str("</div>");

    html.push_str("<code");

(Right now it just writes the language name, because I was too lazy to modify the parsing logic for code fences before this was even approved)

Keats commented 2 years ago

I thought about this exact thing yesterday. I think in practice it makes much more sense to use a shortcode though. A bit reversed so you end up with something like:

(replace ''' below by backticks, can't remember how to escape those)

{% codeblock(name="example.rs") %}
'''rs
fn recursive_query() {
}
'''
{% end %}

And the shortcode be something like

<div class="codeblock">
  <span class="codeblock__name">{{name | safe }}</span>
  {{ body | markdown | safe }}
</div>

It's a bit flipped around compared to your version but is way more flexible.

kartva commented 2 years ago

Ah - I had actually tried to make a shortcode and even managed to make it one that took a body. I forgot about the markdown filter though...

kartva commented 2 years ago

Oh, and it still fails if there's a shortcode inside. That's probably related to #1350. Maybe I should write a patch for that.

{% link_code_file(src="code/segtree/src/segtree.rs") %}
```rust,linenos,hide_lines=1-38 69-102
{{ load_file(path="content/competitive-programming/dsa-explanations/intermediate-segment-tree/code/segtree/src/segtree.rs") }}

{% end %}

Error: Failed to render content of content/competitive-programming/dsa-explanations/intermediate-segment-tree/index.md Reason: Failed to render link_code_file shortcode Reason: Failed to render 'shortcodes/link_code_file.html' Reason: Filter call 'markdown' failed Reason: Failed to render markdown filter: Error { kind: Msg("Failed to render load_file shortcode"), source: Some(Error { kind: Tera(Error { kind: Msg("Failed to render 'shortcodes/load_file.html'"), source: Some(Error { kind: FunctionNotFound("load_data"), source: None }) }), source: None }) }



(btw. even I had to search how to escape the backticks - you can use more backticks at the start and the end - like 4 or 5 - if other backtick sequences are smaller)
kartva commented 2 years ago

Oh - #1350 seems to have been fixed. Instead, what Zola's having a problem with is shortcode nesting?

Keats commented 2 years ago

Nested shortcodes are not currently supported

kartva commented 2 years ago

Hmm, Zola does parse the shortcode body and currently remarks that load_data is not a function. load_data is an inbuilt shortcode, right?

I still think that codeblock naming is a common enough feature that it might be worth explicitly supporting, especially since we otherwise quickly run into situations where the shortcode has limited usefulness (i.e. I can't load a file into a codeblock that is enclosed by a shortcode)

What's your take on the issue?

Keats commented 2 years ago

In that specific case of using the markdown filter, it should work it's just that we don't currently add any of the Tera functions to it. It might be possible to add that easily though, i'll have a look.

Keats commented 2 years ago

If you can try the next branch, load_data should now be working in your example.

I still think that codeblock naming is a common enough feature that it might be worth explicitly supporting,

The issue with that is that it would enforce a specific HTML structure, which is something I'm trying to avoid.

kartva commented 2 years ago

If you can try the next branch, load_data should now be working in your example.

Just tested - still no luck.

It certainly does get farther in the call stack, but it still can't find the inbuilt load_data referenced in shortcodes/load_file.html which the markdown filter tries to render.

Failed to build the site
Error: Failed to render content of /home/desmond-lin-7/blog/content/competitive-programming/dsa-explanations/intermediate-segment-tree/index.md
Reason: Failed to render link_code_file shortcode
Reason: Failed to render 'shortcodes/link_code_file.html'
Reason: Filter call 'markdown' failed
Reason: Failed to render markdown filter: Error { kind: Msg("Failed to render load_file shortcode"), source: Some(Error { kind: Tera(Error { kind: Msg("Failed to render 'shortcodes/load_file.html'"), source: Some(Error { kind: FunctionNotFound("load_data"), source: None }) }), source: None }) }

The issue with that is that it would enforce a specific HTML structure, which is something I'm trying to avoid.

Thanks, that makes sense.

Keats commented 2 years ago

Ah I see what's happening there. The markdown renderer thinks the load_file is a shortcode (I'm assuming you mean load_data?) but doesn't find it. You cannot use Zola functions in the Markdown content. You can use them in the shortcodes but it's not going to be great to do for code blocks since we don't have set blocks.

kartva commented 2 years ago

Would you happen to know why inserting a dbg!(&self.tera) after https://github.com/getzola/zola/blob/1fbe6fbbefd192276165798c936cc62195d94116/components/site/src/lib.rs#L285

only prints:

[components/site/src/lib.rs:286] &self.tera = Tera {
        templates: [
                macros/head.html,
                __zola_builtins/404.html,
                shortcodes/link_code_file.html,
               ...
                __zola_builtins/shortcodes/youtube.html,
                __zola_builtins/split_sitemap_index.xml,
        ]
        filters: [
                trim_start,
                slugify,
                json_encode,
                get,
                ...
                group_by,
                concat,
                trim_end,
                round,
        ]
        testers: [
                iterable,
                string,
                ...
                containing,
                matching,
        ]
}

without any mention of the functions, or other fields in the Tera struct? Site derives Debug, so it should all be standard formatting...

For reference, the Tera struct has these fields:

#[derive(Clone)]
pub struct Tera {
    // The glob used in `Tera::new`, None if Tera was instantiated differently
    #[doc(hidden)]
    glob: Option<String>,
    #[doc(hidden)]
    pub templates: HashMap<String, Template>,
    #[doc(hidden)]
    pub filters: HashMap<String, Arc<dyn Filter>>,
    #[doc(hidden)]
    pub testers: HashMap<String, Arc<dyn Test>>,
    #[doc(hidden)]
    pub functions: HashMap<String, Arc<dyn Function>>,
    // Which extensions does Tera automatically autoescape on.
    // Defaults to [".html", ".htm", ".xml"]
    #[doc(hidden)]
    pub autoescape_suffixes: Vec<&'static str>,
    #[doc(hidden)]
    escape_fn: EscapeFn,
}
kartva commented 2 years ago
```rust,linenos,hide_lines=1-38 69-102
{{ load_data(path="content/competitive-programming/dsa-explanations/intermediate-segment-tree/code/segtree/src/segtree.rs") }}

This snippet does not work as is, both on the current version and `next` branch. Fails with the error:

Error: Failed to render content of /home/desmond-lin-7/blog/content/competitive-programming/dsa-explanations/intermediate-segment-tree/index.md Reason: Found usage of a shortcode named load_data but we do not know about. Make sure it's not a typo and that a field name load_data.{html,md} exists in thetemplates/shortcodes` directory.


What I do instead is write a wrapper shortcode around the `load_data` function called `load_file`:

{{ load_data(path=path) | safe }}


and then call _that_ in the codeblock.
{{ load_file(path="content/competitive-programming/dsa-explanations/intermediate-segment-tree/code/segtree/src/segtree.rs") }}

which works.
kartva commented 2 years ago

Ok, so I figured out a non-Zola method to do this using CSS:

pre code[data-lang]::before {
    content: attr(data-lang);
    color: white;
}
Keats commented 2 years ago

Just reopening to figure out what's happening with the load_data fn

kartva commented 2 years ago

It might be worth filing a separate bug/feature request for investigating the issue as a part of enabling calling Zola builtin functions inside shortcodes.

kartva commented 2 years ago
pre code[data-lang][data-linenos]::before {
    content: attr(data-lang);
    color: white;
}

The above pattern has turned out to be immensely useful.

How about revising the proposal to instead have

```rust,name="Shell code"
generate the following:
````html
<pre data-linenos="" data-lang="rust" data-title="Shell code" class="language-rust z-code">
    <code class="language-rust" data-lang="rust">...</code>
</pre>

so it doesn't impose any custom HTML beyond what is already in use?

bemyak commented 2 years ago

I created this shortcode, which can be used like this:

{% code(lang="rust", file="<filename>" %}
…
{% end %}

Maybe this'll help

Keats commented 2 years ago

I think adding the name to the data attr wouldn't be controversial. I'm just wondering if you can do everything style wise with ::before

kartva commented 2 years ago

It's certainly better than what one could do before. ::before is somewhat limited in terms of what you can put there (strings, attributes of the element it's defined on, images and CSS counters), but it's certainly better than nothing. Since other information (data-lang) is already defined this way and seems to work for most people, it shouldn't be too bad.

Though it is bad for accessibility.

(Take that with a pinch of salt: I actually discovered the ::before trick from someone's blog, I don't have any substantial frontend experience)

kartva commented 2 years ago

@bemyak That looks great, and can expand to accommodate all sorts of custom HTML structures, like codeblock names, etc. I hadn't even thought of trying to construct the codeblock as a string and then render it.

Now if only there was a handy snippet for indenting and deindenting the loaded text...

Jieiku commented 2 years ago

Thanks for this, I used a variation of it:

pre code[data-lang]::before {
    content: attr(data-lang) ': ' "
";
    color: var(--a1);
}

This is the result: https://abridge.netlify.app/overview-code-blocks/

or here's a picture:

2022-06-13_19-29-34

to indent it:

pre code[data-lang]::before {
    content: '     ' attr(data-lang) ': ' "
";
    color: var(--a1);
}

2022-06-13_19-35-14

I implemented a code block copy button, not only does this not interfere, it makes sure a long line of text does not end up under the copy button:

2022-06-14_13-27-22

Jieiku commented 2 years ago

I had another thought about this. Currently my site has a copy button for ALL code blocks. Then I got to thinking... what about the blocks where I do not want to display a code copy button....

I am thinking the best way to resolve this might be to have a way to apply a custom class to some but not all code blocks, this could be used to either apply or not apply a code copy button. Currently the script I am using applies it to all <pre> tags but I could probably modify this to exclude ones that either have or don't have a specific class... will require some more testing.