expressive-code / expressive-code

A text marking & annotation engine for presenting source code on the web.
https://expressive-code.com/
MIT License
487 stars 23 forks source link

Feature: Grouped codeblocks with tabs #22

Open fflaten opened 1 year ago

fflaten commented 1 year ago

Thanks for your work on this !

I have some content that require code in multiple files, and having multiple code blocks in a row takes up a lot of vertical space. Would it make sense to support a way to group multiple code block with an interactive frame-type so the user could select different tabs?

Maybe as a component in the Astro integration if it's hard to achieve in the remark or core engine.

hippotastic commented 1 year ago

Having the ability to render multiple code blocks as a group and being able to toggle between them is a feature I'd also love to have.

I have actually already implemented core to support this scenario: The render function actually accepts a group of multiple blocks, and if you have a look at the postprocessRenderedBlockGroup hook, you can even see in the description that I mentioned the frames plugin using it to render tab groups. I apparently was a bit ahead of myself when writing this, as I didn't implement that feature yet. 😆

I'd love tabbed groups to be implemented in a way that makes them available at the remark-expressive-code level to reach the broadest possible audience. This would allow them to be used by all dependent higher-level integrations like astro-expressive-code as well.

I think the hardest part will be to come up with a syntax that markdown/MDX authors can use to group multiple code blocks together. I think it should be intuitive to use, yet explicit enough to prevent it from being triggered on accident.

My initial idea was that the blocks would need to be direct siblings in the markdown/MDX document, and that there could be a required group/grouped meta tag (behind the opening fence) or inline comment at the start of the content to trigger the grouping.

Do you have any ideas on this?

fflaten commented 1 year ago

Nice! 😄

I was just about to propose the group/groupId meta-tag similar to ins, del etc. I'm not very familiar with remark so I wasn't sure if they had to be siblings, but that requirement makes sense either way. Having codefences spread all over in MD/MDX but grouped in rendered output would make the authoring experience bad imo, so personally I'd always have them as direct siblings.

If implemented in remark-plugin I imagined either with tags:

```powershell group="demo1" title="Demo"
> Import-Module MyModule
> Get-Hello

Hello World
function Get-Hello {
  'Hello World'
}

Or nested codefences:
`````markdown
````expressive-group title="Window/Project/Folder name? Too far? 😄"
```powershell title="Demo"
> Import-Module MyModule
> Get-Hello

Hello World
function Get-Hello {
  'Hello World'
}
hirasso commented 1 year ago

I'd also love to see this!

Grouped code blocks would only make sense if each block would have a title, wouldn't it? Maybe a modified title could automatically group blocks?

```html group1-title="index.html"
<!-- some html -->
```
```css group1-title="style.css"
/* some styles */
```

...or...

```html title="group1 | index.html"
<!-- some html -->
```
```css title="group1 | style.css"
/* some styles */
```

...or...

```html title="group1::index.html"
<!-- some html -->
```
```css title="group1::style.css"
/* some styles */
```

or whatever 😅

If it turns out that a title isn't strictly necessary for groups to work (though I think it is, if the aim is a tab group), group:start and group:end annotations could also be an option:

```html title="index.html" group:start
<!-- some html -->
```
```css title="style.css"
/* some styles */
```
```css title="scripts.js" group:end
/* some JavaScript */
```

Here, the behavior could be that when the parser encounters a code block annotated with group:start, it would treat all direct adjacent code blocks as part of that group until it either

An open question would be what should be done if one of the code blocks in a group doesn't have a title. Should it silently fail? Should it warn the user?

Anyways, just my 5 cents 😄

fflaten commented 1 year ago

I'd assume a title too, but if not: fail or "Untitled-1" (vscode style) 🙂 Personally I'm leaning towards the nested codefence in my second sample if not hard to achieve. Clear syntax, no special meta-tags and could enable window title etc.

hirasso commented 1 year ago

@fflaten I also like the nested code fence, but I'm uncertain if that would be compatible with parsers that wouldn't have access to expressive-code. I just tested more then 3 backticks and my parser seems to just ignore them, always treating it's contents like a code block.

hippotastic commented 1 year ago

Thank you for all the input, guys! It sure seems like there is quite some interest in this feature. 😄

I'm always in favor of solutions that gracefully degrade in case there is lack of feature support by the platform the content is being viewed on. In Astro Docs for example, we always have to keep in mind that markdown/MDX content is also going be viewed through the GitHub website, and it should look good and usable there as well.

Here's how the syntaxes you've proposed (plus an extra one I wanted to try) are rendered in GitHub:

Sibling blocks tagged as a group

<html><!-- some html --></html>
.my-rule { /* some styles */ }
console.log('Hello world') // some JavaScript

Nested blocks using custom language expressive-group

```html title="index.html"
<html><!-- some html --></html>
.my-rule { /* some styles */ }
console.log('Hello world') // some JavaScript

### Nested blocks using language `md` or `mdx`, but tagged

````md expressive-group
```html title="index.html"
<html><!-- some html --></html>
.my-rule { /* some styles */ }
console.log('Hello world') // some JavaScript


The code can also be seen [in a Gist](https://gist.github.com/hippotastic/99755ff6ff7fe9b7eb643bb0e2d187b8) that I created.

---

Looking at these options, I personally think the first option (siblings tagged as a group) seems to gracefully degrade the best. There is no visible custom markup that might confuse readers, and syntax highlighting works as intended.

Regarding the code fence meta tag, while I like the simplicity of the `group` tag, having to repeat it in all sibling blocks seems a bit like a chore to me, and it also seems to invite trying to give it a value like `group="my-group"`, which would make it even more verbose and potentially confusing if someone is trying to assign non-sibling blocks to the some common "group ID" this way.

Maybe the proposed `group-start`/`start-group` tag would be better therefore? I could imagine only requiring to add this to the first block in a sibling block group, and it would automatically end as soon as no more sibling blocks are found, or when a `group-end`/`end-group` tag is found.

We could even allow starting a new group in the middle of sibling blocks by adding a single additional `group-start`/`start-group` tag to the first block that's supposed to be in the new group.

We could extend this later to also support providing a `group-title="This is a group frame title"`, which I don't really find necessary for the first iteration of this feature, but it may be interesting to explore in the future if there is a need.

What do you think?
hippotastic commented 1 year ago

Oh, and regarding grouped blocks without a title: I also think that assuming a title would be the best option. This way, we respect the author's wish to group the blocks (so no surprises here), while showing them that the tab is still untitled and thereby automatically nudging them towards using the title="..." tag or adding a file name comment. 😄

fflaten commented 1 year ago

What do you think?

I agree first option with sibling elements has the best fallback when something breaks or is migrated/authored in another tool. 👍

... potentially confusing if someone is trying to assign non-sibling blocks to the some common "group ID" this way.

Fair point on the non-siblings, but shouldn't it support a group id either way? Or something else that a user could use to override styling based on (no plans, just thinking out loud). group-start / group-start="user-defined-id" + group-end ?

We could even allow starting a new group in the middle of sibling blocks by adding a single additional group-start/start-group tag to the first block that's supposed to be in the new group.

I have no idea why anyone would do this, but sure. 😄

hirasso commented 1 year ago

I have no idea why anyone would do this, but sure. 😄

It would save users from having to explicitly end the previous group before starting a new one. In my opinion, it's even mandatory – otherwise the parser would have to start a new group inside a group 🌀😄

fflaten commented 1 year ago

otherwise the parser would have to start a new group inside a group 🌀😄

That's what I reacted to. I read it as a feature to create a group inside another - and even letting you continue on the outer after ending the inner. 😄

hippotastic commented 1 year ago

Haha, no, I did not mean this to be a method to create nested groups, but a shortcut to start a new group of blocks without having to explicitly end the previous one. 😄

davidmles commented 4 months ago

This is how VitePress does it: https://vitepress.dev/guide/markdown#code-groups

jericdei commented 3 months ago

Up on this!

sparecycles commented 1 month ago

I'm pretty sure a remark()-based <CodeGroup></> solution is possible (at least in Astro), using the same mechanisms as FileTree and Tabs/TabItem.

<CodeGroup>
-
  ```js title="file1.js"
  export const hello = 1;

-

  import { hello } from './file1.js';



(EDIT: on second thought, the `-` list construction probably isn't even needed here, I was just starting with the FileTree solution.)
ijpatricio commented 3 weeks ago

Up on this!

Also, would it be too much trouble to have an option for tab_menu:

ijpatricio commented 1 week ago

Hey everyone.

This GIF shows What my vision is building my own thing, in Laravel + JS.

The "file list" could be horizontal (if few) or vertical, if many.

Before keep going, I think realised it's for the best contributing and use expressive-code, then I'll tweak for my needs - server side served, public and authenticated area in/with Laravel.

Can anyone give me any pointers on how to start making this, in expressive code??

CleanShot 2024-07-18 at 19 43 52