ElMassimo / iles

๐Ÿ The joyful site generator
https://iles.pages.dev
MIT License
1.08k stars 32 forks source link

Enable nested headings using the `isNested` option #227

Closed dhruvkb closed 1 year ago

dhruvkb commented 1 year ago

Description ๐Ÿ“–

This pull request adds a new isNested boolean option the @islands/headings module. It can be used as follows:

import { defineConfig } from 'iles'

import headings from '@islands/headings'

export default defineConfig({
  modules: [
    headings({ isNested: true }) // `true` for nested headings, `false` otherwise
  ],
})

Background ๐Ÿ“œ

See #226

The Fix ๐Ÿ”จ

By changing the rehypePlugin to automatically insert the current heading as a child of the last known heading of higher level, the structure of the headings object becomes a tree instead of a list. Now the headings only contains the list of top-level headings, with all others available inside their children field recursively.

Screenshots ๐Ÿ“ท

See the following Debug output from a dummy post in my รฎles blog project.

{
  "layout": "post",
  "frontmatter": {
    "path": "/blog/posts/my_title",
    "title": "My Title",
    "date": "2023-01-07T00:00:00.000Z",
    "isFeatured": true
  },
  "meta": {
    "filename": "src/pages/blog/posts/my_title.mdx",
    "lastUpdated": "2023-01-31T20:05:42.835Z",
    "href": "/blog/posts/my_title",
    "title": "Duis ultrices augues",
    "headings": [
      {
        "level": 1,
        "title": "Duis ultrices augues",
        "slug": "duis-ultrices-augues",
        "children": [
          {
            "level": 2,
            "title": "Morbi eu iaculis",
            "slug": "morbi-eu-iaculis",
            "children": [
              {
                "level": 3,
                "title": "Sed dapibus",
                "slug": "sed-dapibus",
                "children": [
                  {
                    "level": 4,
                    "title": "Fusce a mollis mauris",
                    "slug": "fusce-a-mollis-mauris",
                    "children": [],
                    "indices": [
                      1,
                      1,
                      1,
                      1
                    ]
                  }
                ],
                "indices": [
                  1,
                  1,
                  1
                ]
              }
            ],
            "indices": [
              1,
              1
            ]
          }
        ],
        "indices": [
          1
        ]
      },
      {
        "level": 1,
        "title": "Phasellus dui dui",
        "slug": "phasellus-dui-dui",
        "children": [
          {
            "level": 6,
            "title": "Mauris at augue vel ante",
            "slug": "mauris-at-augue-vel-ante",
            "children": [],
            "indices": [
              2,
              1
            ]
          }
        ],
        "indices": [
          2
        ]
      }
    ],
    "excerpt": "This is my post."
  },
  "props": {}
}
nx-cloud[bot] commented 1 year ago

โ˜๏ธ Nx Cloud Report

CI is running/has finished running commands for commit 50c8fbe5b89aaeae42cb189081e175976c715224. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

๐Ÿ“‚ See all runs for this branch


โœ… Successfully ran 1 target - [`nx run-many --target=build --all --exclude docs --exclude vue-blog`](https://cloud.nx.app/runs/sl34Dfqj7f)

Sent with ๐Ÿ’Œ from NxCloud.

ouuan commented 1 year ago

I suggest changing the option name to format: 'flat' | 'tree'.

slug and initData (HeadingOptions) are also supposed to be configurable by the user. But I'm not sure how the configs could be passed in the old code, and the new code seems like the correct way to do it ๐Ÿค” Maybe I'm wrong, but anyhow they shouldn't be treated differently.

ElMassimo commented 1 year ago

Something that comes to mind is that we lose type safety since there's no straightforward way for @islands/headings to augment PageMeta with a different Heading type based on whether format is flat or tree.

Perhaps it would make sense to publish separately as @islands/headings-tree (that is equivalent to isNested: true or format: 'tree'), so that it can augment PageMeta differently.

This module would share and reuse code with @islands/headings, but for backwards compatibility would stay unchanged.

dhruvkb commented 1 year ago

@ElMassimo the type Heading is essentially the same with and without nesting, except with children: Heading[] being empty if nesting is disabled. So I don't see much value in separating the project into two and maintaining two packages. That's my view as a contributor but I respect your opinion as the maintainer.

@ouuan I don't know how slug and initData are configured currently and will need to look into that. Maybe we can start by exporting those functions so that they can be modified and extended in userland.

ouuan commented 1 year ago

Maybe we can simply provide a function that converts the flat format to the tree format?

dhruvkb commented 1 year ago

That was my first approach and it would be enough if the only requirement was to tree the flat list of headings but my usecase needed me to make modifications to the HTML node like adding data-attrs for the indices.

ouuan commented 1 year ago

That was my first approach and it would be enough if the only requirement was to tree the flat list of headings but my usecase needed me to make modifications to the HTML node like adding data-attrs for the indices.

I didn't notice this. It sounds like a very specific use case and not all users would want it. Maybe we can have something more generic, like customizing the HTML elements by a function based on some context. Actually, I also want this feature for my use case.

ouuan commented 1 year ago

What about adding these two features:

  1. A function to transfer from array to tree (perhaps without indices information), for users that simply want a tree structure of the headings. I guess this is enough for common use cases.
  2. An option, which is a function, whose input is the headings array and an array of the hast nodes of the headings, and the output is an array of hast nodes. Then the corresponding indices in the AST (children of the root node) are replaced by the outputs. The default function is to set the id and add an anchor tag inside the <h> and return the <h> itself. If you need tree-structured information, you can generate it by yourself or use the provided flat-to-tree function to help you, but the output needs to be a flat array (just do a DFS of the tree).
dhruvkb commented 1 year ago

@ouuan what you are describing in point 2 could potentially be accomplished with some tweaking of the existing slug and initData fields of HeadingOptions. I'll explore that.

If you prefer, feel free to close this PR because this is very different from your idea of the ideal implementation.