{"id":18930657,"url":"https://github.com/tecladocode/flask-hyper-toc","last_synced_at":"2026-03-19T07:10:33.738Z","repository":{"id":93867251,"uuid":"560443991","full_name":"tecladocode/flask-hyper-toc","owner":"tecladocode","description":"A dynamic table of contents component set for Flask, using Hyperscript","archived":false,"fork":false,"pushed_at":"2022-11-01T14:34:25.000Z","size":10967,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-25T02:44:42.409Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tecladocode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-11-01T14:13:58.000Z","updated_at":"2024-03-30T05:07:53.000Z","dependencies_parsed_at":"2023-07-19T01:50:17.069Z","dependency_job_id":null,"html_url":"https://github.com/tecladocode/flask-hyper-toc","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tecladocode/flask-hyper-toc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tecladocode%2Fflask-hyper-toc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tecladocode%2Fflask-hyper-toc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tecladocode%2Fflask-hyper-toc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tecladocode%2Fflask-hyper-toc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tecladocode","download_url":"https://codeload.github.com/tecladocode/flask-hyper-toc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tecladocode%2Fflask-hyper-toc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28815130,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T12:25:15.069Z","status":"ssl_error","status_checked_at":"2026-01-27T12:25:05.297Z","response_time":168,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T11:38:28.291Z","updated_at":"2026-01-27T14:34:10.119Z","avatar_url":"https://github.com/tecladocode.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Flask Hyper TOC\n\nAn implementation of a dynamic table of contents using [Hyperscript](https://hyperscript.org/). Code here made for Flask, but with small modifications it should work with Django or any other library.\n\nhttps://user-images.githubusercontent.com/3788301/199255762-b3983483-b323-49e2-84aa-d2773e0f8f17.mp4\n\n\n## Getting started\n\nInstall dependencies:\n\n```bash\npip install -r requirements.txt\n```\n\n### TailwindCSS\n\nThe repo comes with a minified CSS in `static/css/output.css`, but if you want to compile your own, you'll need to install TailwindCSS first:\n\n```bash\nnpm install\n```\n\nAnd then watch the files for changes:\n\n```bash\nnpx tailwindcss -i ./app/css/style.css -o ./app/static/css/output.css --watch\n```\n\nThat will watch the files for changes while you work with them. If you want to build the output CSS minified, use:\n\n```bash\nnpx tailwindcss -i ./app/css/style.css -o ./app/static/css/output.css --minify\n```\n\n## How to use the components\n\nThere are three pre-built components that are used in conjunction to build the page content and the table of contents:\n\n- `with_side_index`, inside [`app/templates/layouts/side-index.html`](app/templates/layouts/side-index.html).\n- `section`, inside [`app/templates/components/section/section.html`](app/templates/components/section/section.html).\n- `section_header`, inside [`app/templates/components/section/section-header.html`](app/templates/components/section/section-header.html) (you seldom will use this one manually).\n\nThe `with_side_index` and `section` components are [\"Higher-Order Components\"](https://jinja.palletsprojects.com/en/3.0.x/templates/#call). This means that you must use them with a `call` block, like so:\n\n```jinja\n{% call with_side_index(\".section-header\") %}\n    {% call section(\"Description\") %}\n        \u003cp class=\"text-2xl font-semibold\"\u003eThis is in the first section.\u003c/p\u003e\n    {% endcall %}\n\n    {% call section(\"Something else\") %}\n        \u003cp\u003eThis is in the second section.\u003c/p\u003e\n    {% endcall %}\n\n    {% call section(\"A third one\") %}\n        \u003cp\u003eYou can put as many HTML elements as you want.\u003c/p\u003e\n        \u003cp\u003eInside each caller block.\u003c/p\u003e\n    {% endcall %}\n{% endcall %}\n```\n\nOptionally you can pass classes to each section:\n\n```jinja\n{% call section(\"Section title\", class_=\"mb-24 prose\") %}\n\n{% endcall %}\n```\n\n## How the dynamic highlighting works\n\nIf you look at [`layouts/side-index.html`](app/templates/layouts/side-index.html), you'll see that the `with_side_index` macro takes in one optional argument: `target`:\n\n```jinja\n{% macro with_side_index(target=\"h1\") %}\n```\n\nThis signals what elements will be used to populate the table of contents. Any Hyperscript selector will work here, so you can pass `.section` for example, and that will generate an entry in the table of contents for each element with the `section` class in the whole document.\n\nStill inside the same file, skip the `template` tag for now, we'll come back to it. Let's go to this `div`:\n\n```html\n\u003cdiv \n    _=\"init\n        repeat for title in \u003c{{target}}/\u003e\n            make a \u003cdiv.index-element /\u003e called item\n            put the innerHTML of \u003c#side-index-link /\u003e into item\n            set link to the first \u003ca/\u003e in item\n            set link.href to '#' + the title's id\n            set headerTitle to the first .header-title in title\n            set indexTitle to the first .index-title in link\n            put the headerTitle's textContent into indexTitle's textContent\n            put item before me\n        end\n    end\n    \"\u003e\n    \u003c/div\u003e\n```\n\nThis rather long Hyperscript code is fairly readable (I think!). It iterates over all the elements that are returned by the `target` parameter, and then copies the `template` tag above. Each `template` tag has an `a` element inside it, which gets its `textContent` replaced so it matches the target, and its `href` replaced to it links to the target.\n\n### Links in each target\n\nEach target should have an anchor link, such as `#section-one`. The [`components/section/section.html`](app/templates/components/section/section.html) has an `id` property which can be given as an argument, or it is generated from the heading title using the `python-slugify` library.\n\n## Highligting the ToC items in view\n\nAs you scroll, you'll notice that the ToC items get more or less opaque depending on whether the section that is currently in view matches the link in the ToC.\n\nThis is done using the Intersection Observer API.\n\nWhenever 20% of a `section` component enters view, a Hyperscript event is fired (`showYourself`). When less than 20% of a section remains in view, another Hyperscript event is fired (`hideYourself`).\n\nThis is what that code looks like:\n\n```jinja\n\u003csection class=\"{{ class_ }}\" id=\"{{ section_id }}\"\n    _=\"on intersection(intersecting) having threshold 0.2\n            if intersecting\n                wait 100ms\n                send showYourself(id:'#{{ section_id }}') to \u003ca /\u003e in #side-index-layout\n            else\n                wait 100ms\n                send hideYourself(id:'#{{ section_id }}') to \u003ca /\u003e in #side-index-layout\n            end\n        end\"\n\u003e\n    ...\n\u003c/section\u003e\n```\n\nThis event passes an argument, `id`, which is a string equal to the ID selector of the section that has just entered or left view, such as `#section-one`.\n\nThen let's go back to `layouts/side-index.html` and look at the `template` tag.\n\nThis looks at the `showYourself` and `hideYourself` events, and adds or removes an opacity class depending on whether the `id` argument passed in the event matches the `href` property of the link:\n\n```html\n\u003ctemplate id=\"side-index-link\"\u003e\n    \u003ca\n        href=\"#\"\n        class=\"p-0 cursor-pointer flex items-center mb-4 no-underline opacity-50 hover:opacity-100 transition-opacity\"\n        _=\"on showYourself(id) queue all\n            if @href == id then\n                remove .opacity-50 from me\n            end\n        end\n        on hideYourself(id) queue all\n            if @href == id then\n                add .opacity-50 to me\n            end\n        end\n        \"\n    \u003e\n        \u003cspan class=\"index-title link-item\"\u003e\u003c/span\u003e\n    \u003c/a\u003e\n\u003c/template\u003e\n```\n\nOverall this is a bit convoluted, but an equivalent React set of components, which I also implemented for a different project, isn't any simpler. Especially when you take into account navigating to a URL with an anchor, such as `http://127.0.0.1:5000/#section-one`.\n\nWith React and NextJS, handling this was a supreme pain in the butt. A very convenient workaround here is adding the `wait 100ms` before firing the `showYourself` or `hideYourself` events.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftecladocode%2Fflask-hyper-toc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftecladocode%2Fflask-hyper-toc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftecladocode%2Fflask-hyper-toc/lists"}