├── README.md ├── docs ├── tsconfig.json ├── public │ ├── og.jpg │ └── favicon.svg ├── src │ ├── styles │ │ └── custom.css │ ├── components │ │ └── Example.astro │ ├── content.config.ts │ └── content │ │ └── docs │ │ ├── resources │ │ └── starlight.mdx │ │ ├── tests │ │ ├── test-mdx.mdx │ │ └── test-md.md │ │ ├── index.mdx │ │ ├── getting-started.mdx │ │ ├── usage.mdx │ │ └── demo.mdx ├── README.md ├── package.json └── astro.config.ts ├── pnpm-workspace.yaml ├── eslint.config.mjs ├── .vscode ├── extensions.json └── settings.json ├── tsconfig.json ├── packages └── starlight-heading-badges │ ├── .npmignore │ ├── overrides │ ├── TableOfContents.astro │ └── MobileTableOfContents.astro │ ├── eslint.config.mjs │ ├── tests │ ├── test.ts │ ├── fixtures │ │ └── TestPage.ts │ ├── toc.test.ts │ └── heading.test.ts │ ├── playwright.config.ts │ ├── libs │ ├── integration.ts │ ├── plugin.ts │ ├── rehype.ts │ ├── badge.ts │ ├── remark.ts │ └── starlight-toc.ts │ ├── components │ ├── TableOfContentHeading.astro │ ├── HeadingBadgesTableOfContents.astro │ ├── TableOfContentsList.astro │ └── HeadingBadgesMobileTableOfContents.astro │ ├── index.ts │ ├── README.md │ ├── package.json │ ├── styles.css │ └── CHANGELOG.md ├── .prettierignore ├── .changeset ├── config.json └── README.md ├── .gitignore ├── .prettierrc.cjs ├── LICENSE ├── .github └── workflows │ ├── integration.yml │ ├── autofix.yml │ └── release.yml └── package.json /README.md: -------------------------------------------------------------------------------- 1 | packages/starlight-heading-badges/README.md -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'docs' 3 | - 'packages/*' 4 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import hideoo from '@hideoo/eslint-config' 2 | 3 | export default hideoo() 4 | -------------------------------------------------------------------------------- /docs/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HiDeoo/starlight-heading-badges/HEAD/docs/public/og.jpg -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@hideoo/tsconfig", 3 | "include": ["docs/.astro/types.d.ts", "**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 🔖 2 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/.npmignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | eslint.config.mjs 3 | playwright.config.ts 4 | tests 5 | tsconfig.json 6 | tsconfig.tsbuildinfo 7 | -------------------------------------------------------------------------------- /docs/src/styles/custom.css: -------------------------------------------------------------------------------- 1 | .hero-html { 2 | --size: 12rem; 3 | 4 | font-size: var(--size); 5 | justify-content: center; 6 | line-height: var(--size); 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .astro 2 | .changeset 3 | .github/blocks 4 | .next 5 | .vercel 6 | .vscode-test 7 | .vscode-test-web 8 | build 9 | coverage 10 | dist 11 | dist-ssr 12 | out 13 | pnpm-lock.yaml 14 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/overrides/TableOfContents.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import HeadingBadgesTableOfContents from '../components/HeadingBadgesTableOfContents.astro' 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/src/components/Example.astro: -------------------------------------------------------------------------------- 1 |
2 | 3 | 10 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/overrides/MobileTableOfContents.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import HeadingBadgesMobileTableOfContents from '../components/HeadingBadgesMobileTableOfContents.astro' 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.useFlatConfig": true, 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | "typescript", 7 | "typescriptreact", 8 | "html", 9 | "vue", 10 | "markdown", 11 | "astro" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { docsLoader } from '@astrojs/starlight/loaders' 2 | import { docsSchema } from '@astrojs/starlight/schema' 3 | import { defineCollection } from 'astro:content' 4 | 5 | export const collections = { 6 | docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), 7 | } 8 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "HiDeoo/starlight-heading-badges" } 6 | ], 7 | "commit": false, 8 | "access": "public", 9 | "baseBranch": "main", 10 | "updateInternalDependencies": "patch", 11 | "ignore": ["starlight-heading-badges-docs"] 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/content/docs/resources/starlight.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Starlight Plugins and Tools 3 | description: Discover other Starlight plugins, components and tools developed by HiDeoo. 4 | --- 5 | 6 | import { 7 | ResourcesIntro, 8 | Resources, 9 | } from '@hideoo/starlight-plugins-docs-components' 10 | 11 | 12 | 13 | ## Plugins 14 | 15 | 16 | 17 | ## Components 18 | 19 | 20 | 21 | ## Tools 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import hideoo from '@hideoo/eslint-config' 2 | 3 | export default hideoo( 4 | { 5 | ignores: ['eslint.config.mjs'], 6 | languageOptions: { 7 | parserOptions: { 8 | project: ['../../tsconfig.json'], 9 | }, 10 | }, 11 | }, 12 | { 13 | files: ['**/starlight-toc.ts'], 14 | rules: { 15 | '@typescript-eslint/no-unnecessary-condition': 'off', 16 | '@typescript-eslint/related-getter-setter-pairs': 'off', 17 | }, 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .astro 2 | .DS_Store 3 | .eslintcache 4 | .idea 5 | .next 6 | .turbo 7 | .vercel 8 | .vscode/* 9 | !.vscode/extensions.json 10 | !.vscode/launch.json 11 | !.vscode/settings.json 12 | !.vscode/tasks.json 13 | .vscode-test 14 | .vscode-test-web 15 | *.local 16 | *.log 17 | *.pem 18 | *.tsbuildinfo 19 | build 20 | coverage 21 | dist 22 | dist-ssr 23 | lerna-debug.log* 24 | logs 25 | next-env.d.ts 26 | node_modules 27 | npm-debug.log* 28 | out 29 | pnpm-debug.log* 30 | releases 31 | test-results 32 | yarn-debug.log* 33 | yarn-error.log* 34 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | const baseConfig = require('@hideoo/prettier-config') 2 | 3 | /** 4 | * @type {import('prettier').Config} 5 | */ 6 | const prettierConfig = { 7 | ...baseConfig, 8 | overrides: [ 9 | { 10 | files: '*.astro', 11 | options: { 12 | parser: 'astro', 13 | }, 14 | }, 15 | { 16 | files: ['*.md', '*.mdx'], 17 | options: { 18 | printWidth: 80, 19 | }, 20 | }, 21 | ], 22 | plugins: [require.resolve('prettier-plugin-astro')], 23 | } 24 | 25 | module.exports = prettierConfig 26 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { test as base } from '@playwright/test' 2 | 3 | import { TestPage } from './fixtures/TestPage' 4 | 5 | export { expect } from '@playwright/test' 6 | 7 | export const TestTypes = ['md', 'mdx'] as const 8 | 9 | export const test = base.extend({ 10 | testPage: async ({ page }, use) => { 11 | const testPage = new TestPage(page) 12 | 13 | await use(testPage) 14 | }, 15 | }) 16 | 17 | interface Fixtures { 18 | testPage: TestPage 19 | } 20 | 21 | export type TestType = (typeof TestTypes)[number] 22 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | export default defineConfig({ 4 | forbidOnly: !!process.env['CI'], 5 | projects: [ 6 | { 7 | name: 'chromium', 8 | use: { ...devices['Desktop Chrome'], headless: true }, 9 | }, 10 | ], 11 | use: { 12 | baseURL: 'http://localhost:4321', 13 | }, 14 | webServer: [ 15 | { 16 | command: 'pnpm run build && pnpm run preview', 17 | cwd: '../../docs', 18 | reuseExistingServer: !process.env['CI'], 19 | url: 'http://localhost:4321', 20 | }, 21 | ], 22 | }) 23 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/libs/integration.ts: -------------------------------------------------------------------------------- 1 | import { rehypeHeadingIds } from '@astrojs/markdown-remark' 2 | import type { AstroIntegration } from 'astro' 3 | 4 | import { rehypeStarlightHeadingBadges } from './rehype' 5 | import { remarkStarlightHeadingBadges } from './remark' 6 | 7 | export function starlightHeadingBadgesIntegration(): AstroIntegration { 8 | return { 9 | name: 'starlight-heading-badges-integration', 10 | hooks: { 11 | 'astro:config:setup': ({ updateConfig }) => { 12 | updateConfig({ 13 | markdown: { 14 | rehypePlugins: [rehypeHeadingIds, rehypeStarlightHeadingBadges], 15 | remarkPlugins: [remarkStarlightHeadingBadges], 16 | }, 17 | }) 18 | }, 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/components/TableOfContentHeading.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Badge } from '@astrojs/starlight/components' 3 | 4 | import { deserializeBadges } from '../libs/badge' 5 | 6 | interface Props { 7 | text: string 8 | } 9 | 10 | const { text } = Astro.props 11 | 12 | const badges = deserializeBadges(text) 13 | --- 14 | 15 | { 16 | badges.length > 0 ? ( 17 | 18 | {badges[0]?.heading.trim()}  19 | {badges.map((badge) => ( 20 | 21 | ))} 22 | 23 | ) : ( 24 | {text} 25 | ) 26 | } 27 | 28 | 34 | -------------------------------------------------------------------------------- /docs/src/content/docs/tests/test-mdx.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: MDX Tests 3 | pagefind: false 4 | --- 5 | 6 | ## A heading 7 | 8 | test 9 | 10 | ## Default badge :badge[New] 11 | 12 | test 13 | 14 | ### A duplicated heading 15 | 16 | test 17 | 18 | ### A duplicated heading 19 | 20 | test 21 | 22 | ## Variant badge :badge[v1.0]{variant=caution} 23 | 24 | test 25 | 26 | #### Non-ToC :badge[POST]{variant=success} 27 | 28 | test 29 | 30 | ## Badge with a space :badge[A Badge] 31 | 32 | test 33 | 34 | ## Badge with multiple spaces :badge[A Badge with Spaces] 35 | 36 | test 37 | 38 | ## Multiple badges :badge[Badge 1] :badge[Badge 2]{variant=success} 39 | 40 | test 41 | 42 | ## Heading with custom ID {#custom1} 43 | 44 | test 45 | 46 | ## Heading with custom ID and a badge {#custom2} :badge[Custom] 47 | 48 | test 49 | -------------------------------------------------------------------------------- /docs/src/content/docs/tests/test-md.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown Tests 3 | pagefind: false 4 | --- 5 | 6 | ## A heading 7 | 8 | test 9 | 10 | ## Default badge :badge[New] 11 | 12 | test 13 | 14 | ### A duplicated heading 15 | 16 | test 17 | 18 | ### A duplicated heading 19 | 20 | test 21 | 22 | ## Variant badge :badge[v1.0]{variant=caution} 23 | 24 | test 25 | 26 | #### Non-ToC :badge[POST]{variant=success} 27 | 28 | test 29 | 30 | ## Badge with a space :badge[A Badge] 31 | 32 | test 33 | 34 | ## Badge with multiple spaces :badge[A Badge with Spaces] 35 | 36 | test 37 | 38 | ## Multiple badges :badge[Badge 1] :badge[Badge 2]{variant=success} 39 | 40 | test 41 | 42 | ## Heading with custom ID {#custom1} 43 | 44 | test 45 | 46 | ## Heading with custom ID and a badge {#custom2} :badge[Custom] 47 | 48 | test 49 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/components/HeadingBadgesTableOfContents.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /** 3 | * A copy of the Starlight `` component with support for badges. 4 | * @see https://github.com/withastro/starlight/blob/1d32c8b0cec0ea5f66351b21b91748dfa78b404b/packages/starlight/components/TableOfContents.astro 5 | */ 6 | 7 | import TableOfContentsList from './TableOfContentsList.astro' 8 | 9 | const { toc } = Astro.locals.starlightRoute 10 | --- 11 | 12 | { 13 | toc && ( 14 | 15 | 19 | 20 | ) 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 |
2 |

starlight-heading-badges 🔖

3 |

Starlight plugin to add badges to your Markdown and MDX headings.

4 |
5 | 6 |
7 | 8 | Integration Status 9 | 10 | 11 | License 12 | 13 |
14 |
15 | 16 | ## Example 17 | 18 | Run the docs locally using [pnpm](https://pnpm.io): 19 | 20 | ```shell 21 | pnpm run dev 22 | ``` 23 | 24 | ## License 25 | 26 | Licensed under the MIT License, Copyright © HiDeoo. 27 | 28 | See [LICENSE](https://github.com/HiDeoo/starlight-heading-badges/blob/main/LICENSE) for more information. 29 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/index.ts: -------------------------------------------------------------------------------- 1 | import type { StarlightPlugin } from '@astrojs/starlight/types' 2 | 3 | import { starlightHeadingBadgesIntegration } from './libs/integration' 4 | import { overrideComponents } from './libs/plugin' 5 | 6 | export default function starlightHeadingBadgesPlugin(): StarlightPlugin { 7 | return { 8 | name: 'starlight-heading-badges-plugin', 9 | hooks: { 10 | 'config:setup'({ addIntegration, config: starlightConfig, logger, updateConfig }) { 11 | addIntegration(starlightHeadingBadgesIntegration()) 12 | 13 | updateConfig({ 14 | components: overrideComponents( 15 | starlightConfig, 16 | [ 17 | { name: 'MobileTableOfContents', fallback: 'HeadingBadgesMobileTableOfContents' }, 18 | { name: 'TableOfContents', fallback: 'HeadingBadgesTableOfContents' }, 19 | ], 20 | logger, 21 | ), 22 | customCss: [...(starlightConfig.customCss ?? []), 'starlight-heading-badges/styles.css'], 23 | }) 24 | }, 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/tests/fixtures/TestPage.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from '@playwright/test' 2 | 3 | import type { TestType } from '../test' 4 | 5 | export class TestPage { 6 | expectedHeadings = [ 7 | { text: 'A heading', id: 'a-heading' }, 8 | { text: 'Default badge', id: 'default-badge' }, 9 | { text: 'A duplicated heading', id: 'a-duplicated-heading' }, 10 | { text: 'A duplicated heading', id: 'a-duplicated-heading-1' }, 11 | { text: 'Variant badge', id: 'variant-badge' }, 12 | { text: 'Non-ToC', id: 'non-toc' }, 13 | { text: 'Badge with a space', id: 'badge-with-a-space' }, 14 | { text: 'Badge with multiple spaces', id: 'badge-with-multiple-spaces' }, 15 | { text: 'Multiple badges', id: 'multiple-badges' }, 16 | ] 17 | 18 | expectedCustomHeadings = [ 19 | { text: 'Heading with custom ID', id: 'custom1' }, 20 | { text: 'Heading with custom ID and a badge', id: 'custom2' }, 21 | ] 22 | 23 | constructor(public readonly page: Page) {} 24 | 25 | goto(type: TestType) { 26 | return this.page.goto(`/tests/test-${type}/`) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-present, HiDeoo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/libs/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { HookParameters } from '@astrojs/starlight/types' 2 | import type { AstroIntegrationLogger } from 'astro' 3 | 4 | export function overrideComponents( 5 | starlightConfig: StarlightUserConfig, 6 | overrides: ComponentOverride[], 7 | logger: AstroIntegrationLogger, 8 | ): StarlightUserConfig['components'] { 9 | const components = { ...starlightConfig.components } 10 | 11 | for (const { name, fallback } of overrides) { 12 | if (starlightConfig.components?.[name]) { 13 | logger.warn(`A \`<${name}>\` component override is already defined in your Starlight configuration.`) 14 | logger.warn( 15 | `To use \`starlight-heading-badges\`, either remove this override or manually render the content from \`starlight-heading-badges/components/${fallback}.astro\`.`, 16 | ) 17 | continue 18 | } 19 | components[name] = `starlight-heading-badges/overrides/${name}.astro` 20 | } 21 | 22 | return components 23 | } 24 | 25 | interface ComponentOverride { 26 | name: keyof NonNullable 27 | fallback: string 28 | } 29 | 30 | type StarlightUserConfig = HookParameters<'config:setup'>['config'] 31 | -------------------------------------------------------------------------------- /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: integration 2 | 3 | permissions: {} 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | workflow_call: 12 | 13 | concurrency: 14 | cancel-in-progress: true 15 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} 16 | 17 | jobs: 18 | lint_test: 19 | name: Lint & Test 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 24 | with: 25 | persist-credentials: false 26 | 27 | - name: Install pnpm 28 | uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 29 | 30 | - name: Install Node.js 31 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 32 | with: 33 | cache: pnpm 34 | node-version: 18 35 | 36 | - name: Install dependencies 37 | run: pnpm install 38 | 39 | - name: Generates docs TypeScript types 40 | run: pnpm astro sync 41 | working-directory: docs 42 | 43 | - name: Lint 44 | run: pnpm lint 45 | 46 | - name: Test 47 | run: pnpm test 48 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Starlight Heading Badges 3 | description: Starlight plugin to add badges to your Markdown and MDX headings. 4 | head: 5 | - tag: title 6 | content: Starlight Heading Badges 7 | template: splash 8 | editUrl: false 9 | lastUpdated: false 10 | hero: 11 | tagline: Starlight plugin to add badges to your Markdown and MDX headings. 12 | image: 13 | html: '🔖' 14 | actions: 15 | - text: Getting Started 16 | link: /getting-started/ 17 | icon: rocket 18 | - text: Demo 19 | link: /demo/ 20 | icon: right-arrow 21 | variant: minimal 22 | --- 23 | 24 | import { Card, CardGrid } from '@astrojs/starlight/components' 25 | 26 | ## Next steps 27 | 28 | 29 | 30 | Check the [getting started guide](/getting-started/) for installation 31 | instructions. 32 | 33 | 34 | Edit your config in the `astro.config.mjs` file. 35 | 36 | 37 | Add badges to your headings in your Markdown and MDX files. 38 | 39 | 40 | Learn more in the [Starlight Heading Badges Docs](/getting-started/). 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | workflow_call: 13 | 14 | concurrency: 15 | cancel-in-progress: true 16 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} 17 | 18 | jobs: 19 | autofix: 20 | name: Format code 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: read 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 27 | with: 28 | persist-credentials: false 29 | 30 | - name: Install pnpm 31 | uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 32 | 33 | - name: Install Node.js 34 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 35 | with: 36 | cache: pnpm 37 | node-version: 18 38 | 39 | - name: Install dependencies 40 | run: pnpm install 41 | 42 | - name: Format code 43 | run: pnpm format 44 | 45 | - name: Run autofix 46 | uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2 47 | with: 48 | fail-fast: false 49 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starlight-heading-badges-docs", 3 | "version": "0.3.0", 4 | "license": "MIT", 5 | "description": "Starlight plugin to add badges to your Markdown and MDX headings.", 6 | "author": "HiDeoo (https://hideoo.dev)", 7 | "type": "module", 8 | "scripts": { 9 | "dev": "astro dev", 10 | "build": "astro build", 11 | "preview": "astro preview", 12 | "lint": "eslint . --cache --max-warnings=0" 13 | }, 14 | "dependencies": { 15 | "@astrojs/starlight": "^0.32.0", 16 | "@hideoo/starlight-plugins-docs-components": "^0.4.0", 17 | "astro": "^5.3.0", 18 | "remark-custom-heading-id": "^2.0.0", 19 | "sharp": "^0.33.5", 20 | "starlight-heading-badges": "workspace:*" 21 | }, 22 | "engines": { 23 | "node": ">=18" 24 | }, 25 | "packageManager": "pnpm@9.0.4", 26 | "private": true, 27 | "sideEffects": false, 28 | "keywords": [ 29 | "starlight", 30 | "plugin", 31 | "badges", 32 | "headings", 33 | "documentation", 34 | "astro" 35 | ], 36 | "homepage": "https://github.com/HiDeoo/starlight-heading-badges", 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/HiDeoo/starlight-heading-badges.git", 40 | "directory": "docs" 41 | }, 42 | "bugs": "https://github.com/HiDeoo/starlight-heading-badges/issues" 43 | } 44 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/README.md: -------------------------------------------------------------------------------- 1 |
2 |

starlight-heading-badges 🔖

3 |

Starlight plugin to add badges to your Markdown and MDX headings.

4 |

5 | 6 | Screenshot of the Starlight Heading Badges plugin 7 | 8 |

9 |
10 | 11 |
12 | 13 | Integration Status 14 | 15 | 16 | License 17 | 18 |
19 |
20 | 21 | ## Getting Started 22 | 23 | Want to get started immediately? Check out the [getting started guide](https://starlight-heading-badges.vercel.app/getting-started/) or check out the [demo](https://starlight-heading-badges.vercel.app/demo/) to see the theme in action. 24 | 25 | ## License 26 | 27 | Licensed under the MIT License, Copyright © HiDeoo. 28 | 29 | See [LICENSE](https://github.com/HiDeoo/starlight-heading-badges/blob/main/LICENSE) for more information. 30 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/libs/rehype.ts: -------------------------------------------------------------------------------- 1 | import 'mdast-util-directive' 2 | 3 | import type { ElementContent, Root } from 'hast' 4 | import { CONTINUE, SKIP, visit } from 'unist-util-visit' 5 | 6 | import { deserializeBadges, type Badge } from './badge' 7 | 8 | export function rehypeStarlightHeadingBadges() { 9 | return function transformer(tree: Root) { 10 | visit(tree, (node, index, parent) => { 11 | if (node.type === 'text') { 12 | if (index === undefined || !parent) return CONTINUE 13 | 14 | const badges = deserializeBadges(node.value) 15 | if (badges.length === 0) return SKIP 16 | 17 | for (const badge of badges.reverse()) { 18 | if (badge.heading) { 19 | node.value = badge.heading 20 | parent.children.splice(index + 1, 0, ...createBadgeNode(badge)) 21 | } else { 22 | parent.children.splice(index, 1, ...createBadgeNode(badge)) 23 | } 24 | } 25 | 26 | return SKIP 27 | } 28 | 29 | return CONTINUE 30 | }) 31 | } 32 | } 33 | 34 | function createBadgeNode(badge: Badge): ElementContent[] { 35 | return [ 36 | { 37 | type: 'element', 38 | tagName: 'span', 39 | properties: { 40 | 'data-shb-badge': '', 41 | 'data-shb-badge-variant': badge.variant, 42 | }, 43 | children: [{ type: 'text', value: badge.text }], 44 | }, 45 | { 46 | type: 'text', 47 | value: ' ', 48 | }, 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | changeset: 12 | name: Changeset 13 | if: ${{ github.repository_owner == 'hideoo' }} 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | id-token: write 18 | pull-requests: write 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 22 | with: 23 | fetch-depth: 0 24 | persist-credentials: false 25 | 26 | - name: Install pnpm 27 | uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 28 | 29 | - name: Install Node.js 30 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 31 | with: 32 | node-version: 24 33 | package-manager-cache: false 34 | 35 | - name: Install dependencies 36 | run: pnpm install 37 | 38 | - name: Create Release Pull Request or Publish 39 | uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3 40 | with: 41 | version: pnpm run version 42 | publish: pnpm changeset publish 43 | commit: 'ci: release' 44 | title: 'ci: release' 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | NPM_TOKEN: '' # https://github.com/changesets/changesets/issues/1152#issuecomment-3190884868 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starlight-heading-badges-monorepo", 3 | "version": "0.3.0", 4 | "license": "MIT", 5 | "description": "Starlight plugin to add badges to your Markdown and MDX headings.", 6 | "author": "HiDeoo (https://hideoo.dev)", 7 | "type": "module", 8 | "scripts": { 9 | "test": "pnpm --stream -r test", 10 | "lint": "astro check --noSync && pnpm -r lint", 11 | "format": "prettier -w --cache --ignore-unknown .", 12 | "version": "pnpm changeset version && pnpm i --no-frozen-lockfile" 13 | }, 14 | "devDependencies": { 15 | "@astrojs/check": "^0.9.4", 16 | "@changesets/changelog-github": "^0.5.1", 17 | "@changesets/cli": "^2.29.7", 18 | "@hideoo/eslint-config": "^4.0.0", 19 | "@hideoo/prettier-config": "^2.0.0", 20 | "@hideoo/tsconfig": "^2.0.1", 21 | "@types/node": "18.19.68", 22 | "astro": "^5.3.0", 23 | "eslint": "^9.17.0", 24 | "prettier": "^3.4.2", 25 | "prettier-plugin-astro": "^0.14.1", 26 | "typescript": "^5.7.2" 27 | }, 28 | "engines": { 29 | "node": ">=18" 30 | }, 31 | "packageManager": "pnpm@9.0.4", 32 | "private": true, 33 | "sideEffects": false, 34 | "keywords": [ 35 | "starlight", 36 | "plugin", 37 | "badges", 38 | "headings", 39 | "documentation", 40 | "astro" 41 | ], 42 | "homepage": "https://github.com/HiDeoo/starlight-heading-badges", 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/HiDeoo/starlight-heading-badges.git" 46 | }, 47 | "bugs": "https://github.com/HiDeoo/starlight-heading-badges/issues" 48 | } 49 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/libs/badge.ts: -------------------------------------------------------------------------------- 1 | export const BadgeDirectiveName = 'badge' 2 | 3 | const variants = ['caution', 'danger', 'default', 'note', 'success', 'tip'] as const 4 | 5 | const serializedBadgeDelimiter = '__SHB__' 6 | const serializedBadgeSpaceDelimiter = '__SHB_SPACE__' 7 | 8 | export function isBadgeVariant(value: string): value is Variant { 9 | return variants.includes(value as Variant) 10 | } 11 | 12 | export function serializeBadge(variant: Variant, text: string) { 13 | return [ 14 | serializedBadgeDelimiter, 15 | variant, 16 | serializedBadgeDelimiter, 17 | text.replaceAll(' ', serializedBadgeSpaceDelimiter), 18 | serializedBadgeDelimiter, 19 | ].join('') 20 | } 21 | 22 | export function deserializeBadges(value: string): Badge[] { 23 | const badges: Badge[] = [] 24 | 25 | const parts = value.split(' ').reverse() 26 | 27 | for (const part of parts) { 28 | const badge = deserializeBadge(value, part) 29 | if (!badge) break 30 | badges.unshift(badge) 31 | } 32 | 33 | return badges 34 | } 35 | 36 | function deserializeBadge(heading: string, value: string): Badge | undefined { 37 | const parts = value.split(serializedBadgeDelimiter) 38 | const [, variant, text] = parts 39 | 40 | if (!variant || !isBadgeVariant(variant) || !text) return undefined 41 | 42 | return { 43 | heading: heading.replace(new RegExp(`${serializedBadgeDelimiter}.*${serializedBadgeDelimiter}`), ''), 44 | text: text.replaceAll(serializedBadgeSpaceDelimiter, ' '), 45 | variant, 46 | } 47 | } 48 | 49 | export type Variant = (typeof variants)[number] 50 | 51 | export interface Badge { 52 | heading: string 53 | text: string 54 | variant: Variant 55 | } 56 | -------------------------------------------------------------------------------- /docs/astro.config.ts: -------------------------------------------------------------------------------- 1 | import starlight from '@astrojs/starlight' 2 | import { defineConfig } from 'astro/config' 3 | import remarkCustomHeadingId from 'remark-custom-heading-id' 4 | import starlightHeadingBadges from 'starlight-heading-badges' 5 | 6 | const site = 7 | process.env['VERCEL_ENV'] !== 'production' && process.env['VERCEL_URL'] 8 | ? `https://${process.env['VERCEL_URL']}` 9 | : 'https://starlight-heading-badges.vercel.app/' 10 | 11 | export default defineConfig({ 12 | integrations: [ 13 | starlight({ 14 | credits: true, 15 | customCss: ['./src/styles/custom.css'], 16 | editLink: { 17 | baseUrl: 'https://github.com/HiDeoo/starlight-heading-badges/edit/main/docs/', 18 | }, 19 | head: [ 20 | { 21 | tag: 'meta', 22 | attrs: { property: 'og:image', content: new URL('og.jpg', site).href }, 23 | }, 24 | { 25 | tag: 'meta', 26 | attrs: { 27 | property: 'og:image:alt', 28 | content: 'Starlight plugin to add badges to your Markdown and MDX headings.', 29 | }, 30 | }, 31 | ], 32 | plugins: [starlightHeadingBadges()], 33 | sidebar: [ 34 | { 35 | label: 'Start Here', 36 | items: [ 37 | { label: 'Getting Started', link: '/getting-started/' }, 38 | { label: 'Usage', link: '/usage/' }, 39 | ], 40 | }, 41 | { 42 | label: 'Resources', 43 | items: [{ label: 'Plugins and Tools', link: '/resources/starlight/' }], 44 | }, 45 | { label: 'Demo', link: '/demo/' }, 46 | ], 47 | social: { 48 | blueSky: 'https://bsky.app/profile/hideoo.dev', 49 | github: 'https://github.com/HiDeoo/starlight-heading-badges', 50 | }, 51 | title: 'Starlight Heading Badges', 52 | }), 53 | ], 54 | markdown: { 55 | remarkPlugins: [remarkCustomHeadingId], 56 | }, 57 | site, 58 | }) 59 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starlight-heading-badges", 3 | "version": "0.6.1", 4 | "license": "MIT", 5 | "description": "Starlight plugin to add badges to your Markdown and MDX headings.", 6 | "author": "HiDeoo (https://hideoo.dev)", 7 | "type": "module", 8 | "exports": { 9 | ".": "./index.ts", 10 | "./components/HeadingBadgesMobileTableOfContents.astro": "./components/HeadingBadgesMobileTableOfContents.astro", 11 | "./components/HeadingBadgesTableOfContents.astro": "./components/HeadingBadgesTableOfContents.astro", 12 | "./overrides/MobileTableOfContents.astro": "./overrides/MobileTableOfContents.astro", 13 | "./overrides/TableOfContents.astro": "./overrides/TableOfContents.astro", 14 | "./styles.css": "./styles.css", 15 | "./package.json": "./package.json" 16 | }, 17 | "scripts": { 18 | "test": "playwright install --with-deps chromium && playwright test", 19 | "lint": "eslint . --cache --max-warnings=0" 20 | }, 21 | "dependencies": { 22 | "@astrojs/markdown-remark": "^6.0.1", 23 | "github-slugger": "^2.0.0", 24 | "mdast-util-directive": "^3.0.0", 25 | "unist-util-visit": "^5.0.0" 26 | }, 27 | "devDependencies": { 28 | "@playwright/test": "^1.49.1", 29 | "@types/hast": "^3.0.4", 30 | "@types/mdast": "^4.0.4" 31 | }, 32 | "peerDependencies": { 33 | "@astrojs/starlight": ">=0.32.0" 34 | }, 35 | "engines": { 36 | "node": ">=18" 37 | }, 38 | "packageManager": "pnpm@9.0.4", 39 | "publishConfig": { 40 | "access": "public" 41 | }, 42 | "sideEffects": false, 43 | "keywords": [ 44 | "starlight", 45 | "plugin", 46 | "badges", 47 | "headings", 48 | "documentation", 49 | "astro" 50 | ], 51 | "homepage": "https://github.com/HiDeoo/starlight-heading-badges", 52 | "repository": { 53 | "type": "git", 54 | "url": "https://github.com/HiDeoo/starlight-heading-badges.git", 55 | "directory": "packages/starlight-heading-badges" 56 | }, 57 | "bugs": "https://github.com/HiDeoo/starlight-heading-badges/issues" 58 | } 59 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/styles.css: -------------------------------------------------------------------------------- 1 | span[data-shb-badge] { 2 | background-color: var(--sl-color-bg-badge); 3 | border: 1px solid var(--sl-color-border-badge); 4 | border-radius: 0.25rem; 5 | color: var(--sl-color-text-badge); 6 | display: inline-block; 7 | font-family: var(--sl-font-system-mono); 8 | font-size: var(--sl-text-sm); 9 | line-height: normal; 10 | overflow-wrap: anywhere; 11 | padding: 0.175rem 0.35rem; 12 | vertical-align: middle; 13 | } 14 | 15 | :is(h5, h6) span[data-shb-badge] { 16 | font-size: var(--sl-text-xs); 17 | padding: 0.0625rem 0.25rem; 18 | } 19 | 20 | mobile-starlight-toc span[data-shb-badge] { 21 | font-size: 0.75rem; 22 | margin-inline-start: 1ch; 23 | padding: 0.0625rem 0.25rem; 24 | } 25 | 26 | span[data-shb-badge-variant='default'] { 27 | --sl-color-bg-badge: var(--sl-badge-default-bg); 28 | --sl-color-border-badge: var(--sl-badge-default-border); 29 | --sl-color-text-badge: var(--sl-badge-default-text); 30 | } 31 | 32 | span[data-shb-badge-variant='note'] { 33 | --sl-color-bg-badge: var(--sl-badge-note-bg); 34 | --sl-color-border-badge: var(--sl-badge-note-border); 35 | --sl-color-text-badge: var(--sl-badge-note-text); 36 | } 37 | 38 | span[data-shb-badge-variant='danger'] { 39 | --sl-color-bg-badge: var(--sl-badge-danger-bg); 40 | --sl-color-border-badge: var(--sl-badge-danger-border); 41 | --sl-color-text-badge: var(--sl-badge-danger-text); 42 | } 43 | 44 | span[data-shb-badge-variant='success'] { 45 | --sl-color-bg-badge: var(--sl-badge-success-bg); 46 | --sl-color-border-badge: var(--sl-badge-success-border); 47 | --sl-color-text-badge: var(--sl-badge-success-text); 48 | } 49 | 50 | span[data-shb-badge-variant='tip'] { 51 | --sl-color-bg-badge: var(--sl-badge-tip-bg); 52 | --sl-color-border-badge: var(--sl-badge-tip-border); 53 | --sl-color-text-badge: var(--sl-badge-tip-text); 54 | } 55 | 56 | span[data-shb-badge-variant='caution'] { 57 | --sl-color-bg-badge: var(--sl-badge-caution-bg); 58 | --sl-color-border-badge: var(--sl-badge-caution-border); 59 | --sl-color-text-badge: var(--sl-badge-caution-text); 60 | } 61 | -------------------------------------------------------------------------------- /docs/src/content/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: Learn how to use the Starlight Heading Badges plugin in your Starlight documentation. 4 | --- 5 | 6 | A [Starlight](https://starlight.astro.build) plugin to easily add [badges](https://starlight.astro.build/guides/components/#badges) to your Markdown and MDX headings that are also visible in the table of contents. 7 | Check out the [demo](/demo/) for a preview of the plugin in action. 8 | 9 | ## Prerequisites 10 | 11 | You will need to have a Starlight website set up. 12 | If you don't have one yet, you can follow the ["Getting Started"](https://starlight.astro.build/getting-started) guide in the Starlight docs to create one. 13 | 14 | ## Installation 15 | 16 | import { Steps } from '@astrojs/starlight/components' 17 | import { PackageManagers } from '@hideoo/starlight-plugins-docs-components' 18 | 19 | 20 | 21 | 1. Starlight Heading Badges is a Starlight [plugin](https://starlight.astro.build/reference/plugins/) that you can install using your favorite package manager: 22 | 23 | 24 | 25 | 2. Configure the plugin in your Starlight [configuration](https://starlight.astro.build/reference/configuration/#plugins) in the `astro.config.mjs` file. 26 | 27 | ```diff lang="js" 28 | // astro.config.mjs 29 | import starlight from '@astrojs/starlight' 30 | import { defineConfig } from 'astro/config' 31 | +import starlightHeadingBadges from 'starlight-heading-badges' 32 | 33 | export default defineConfig({ 34 | integrations: [ 35 | starlight({ 36 | + plugins: [starlightHeadingBadges()], 37 | title: 'My Docs', 38 | }), 39 | ], 40 | }) 41 | ``` 42 | 43 | 3. Add a badge to a heading in one of your Markdown or MDX files: 44 | 45 | ```mdx 46 | --- 47 | // src/content/docs/example.md 48 | title: My docs 49 | --- 50 | 51 | ## Hello :badge[New] 52 | 53 | Welcome to my project! 54 | ``` 55 | 56 | 4. [Start the development server](https://starlight.astro.build/getting-started/#start-the-development-server) to see the plugin in action. 57 | 58 | 59 | 60 | Check out the [“Usage”](/usage/) guide to learn more about how to use the plugin. 61 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/libs/remark.ts: -------------------------------------------------------------------------------- 1 | import 'mdast-util-directive' 2 | 3 | import GithubSlugger from 'github-slugger' 4 | import type { Root } from 'mdast' 5 | import { CONTINUE, SKIP, visit } from 'unist-util-visit' 6 | 7 | import { BadgeDirectiveName, isBadgeVariant, serializeBadge, type Variant } from './badge' 8 | 9 | export function remarkStarlightHeadingBadges() { 10 | return function transformer(tree: Root) { 11 | const slugger = new GithubSlugger() 12 | 13 | visit(tree, (node, index, parent) => { 14 | if (!parent || typeof index !== 'number' || parent.type !== 'heading') return CONTINUE 15 | 16 | if (index === 0) { 17 | let headingText = '' 18 | 19 | visit(parent, (headingNode, _, headingParent) => { 20 | if ( 21 | headingParent?.type !== 'textDirective' && 22 | (headingNode.type === 'text' || headingNode.type === 'inlineCode') 23 | ) { 24 | headingText += headingNode.value 25 | } 26 | }) 27 | 28 | headingText = headingText.trim() 29 | 30 | if (!headingText) return CONTINUE 31 | 32 | parent.data ??= {} 33 | parent.data.hProperties ??= {} 34 | 35 | if (!parent.data.hProperties['id']) { 36 | parent.data.hProperties['id'] = slugger.slug(headingText) 37 | } 38 | } 39 | 40 | if (node.type !== 'textDirective' || node.name !== BadgeDirectiveName) return CONTINUE 41 | 42 | const contentNode = node.children[0] 43 | if (!contentNode || contentNode.type !== 'text' || contentNode.value.length === 0) return CONTINUE 44 | 45 | let variant: Variant = 'default' 46 | 47 | if (node.attributes?.['variant']) { 48 | if (isBadgeVariant(node.attributes['variant'])) { 49 | variant = node.attributes['variant'] 50 | } else { 51 | return CONTINUE 52 | } 53 | } 54 | 55 | if (parent.children.length !== index + 1) { 56 | parent.children.splice(parent.children.length, 0, { type: 'text', value: ' ' }) 57 | } 58 | parent.children.splice(index, 1) 59 | parent.children.splice(parent.children.length, 0, { 60 | type: 'text', 61 | value: serializeBadge(variant, contentNode.value), 62 | }) 63 | 64 | return SKIP 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # starlight-heading-badges 2 | 3 | ## 0.6.1 4 | 5 | ### Patch Changes 6 | 7 | - [#19](https://github.com/HiDeoo/starlight-heading-badges/pull/19) [`5b2c5aa`](https://github.com/HiDeoo/starlight-heading-badges/commit/5b2c5aa853cd8af228102e153db09abc049d3f58) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Setups trusted publishing using OpenID Connect (OIDC) authentication — no code changes. 8 | 9 | ## 0.6.0 10 | 11 | ### Minor Changes 12 | 13 | - [#18](https://github.com/HiDeoo/starlight-heading-badges/pull/18) [`261fda4`](https://github.com/HiDeoo/starlight-heading-badges/commit/261fda465d560146c55a2f148f36a28de384fdc8) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for rendering multiple badges to the same heading. 14 | 15 | ### Patch Changes 16 | 17 | - [#16](https://github.com/HiDeoo/starlight-heading-badges/pull/16) [`3a590c7`](https://github.com/HiDeoo/starlight-heading-badges/commit/3a590c7332c0bdaacd6109b5597b84cd5a415daf) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes an issue for heading badges with multiple spaces. 18 | 19 | ## 0.5.0 20 | 21 | ### Minor Changes 22 | 23 | - [#12](https://github.com/HiDeoo/starlight-heading-badges/pull/12) [`508982f`](https://github.com/HiDeoo/starlight-heading-badges/commit/508982fc5050e700669382f195aaa420a4b59748) Thanks [@HiDeoo](https://github.com/HiDeoo)! - ⚠️ **BREAKING CHANGE:** The minimum supported version of Starlight is now version `0.32.0`. 24 | 25 | Please use the `@astrojs/upgrade` command to upgrade your project: 26 | 27 | ```sh 28 | npx @astrojs/upgrade 29 | ``` 30 | 31 | ## 0.4.1 32 | 33 | ### Patch Changes 34 | 35 | - [#10](https://github.com/HiDeoo/starlight-heading-badges/pull/10) [`68a2977`](https://github.com/HiDeoo/starlight-heading-badges/commit/68a29775e0852ade39ae34cb92ba6522a01b2c9d) Thanks [@DaniFoldi](https://github.com/DaniFoldi)! - Moves `mdast-util-directive` package to non-dev dependencies to prevent issues in monorepos with hoisting disabled. 36 | 37 | ## 0.4.0 38 | 39 | ### Minor Changes 40 | 41 | - [#8](https://github.com/HiDeoo/starlight-heading-badges/pull/8) [`51c2bca`](https://github.com/HiDeoo/starlight-heading-badges/commit/51c2bcad815af8be5d2d93fba8221eba182cd72d) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for Astro v5, drops support for Astro v4. 42 | 43 | ⚠️ **BREAKING CHANGE:** The minimum supported version of Starlight is now `0.30.0`. 44 | 45 | Please follow the [upgrade guide](https://github.com/withastro/starlight/releases/tag/%40astrojs/starlight%400.30.0) to update your project. 46 | -------------------------------------------------------------------------------- /docs/src/content/docs/usage.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage 3 | description: Learn all the possible ways to use the Starlight Heading Badges plugin in your Starlight documentation. 4 | --- 5 | 6 | The Starlight Heading Badges plugin provides a custom Markdown syntax to add badges to your headings in Markdown and MDX files. 7 | 8 | ## Add a badge 9 | 10 | Badges are indicated using the colon `:` character followed by the `badge` keyword and the badge content in square brackets `[]`. 11 | By default, the badge will use the theme accent color of your site. 12 | 13 | The following example will render a "New" badge using the default theme accent color: 14 | 15 | import { Preview } from '@hideoo/starlight-plugins-docs-components' 16 | import Example from '../../components/Example.astro' 17 | 18 | 19 | 20 | ```mdx 21 | --- 22 | // src/content/docs/example.md 23 | title: My docs 24 | --- 25 | 26 | ### Example :badge[New] 27 | 28 | Welcome to my project! 29 | ``` 30 | 31 | 32 | 33 | ### Example :badge[New] 34 | 35 | Welcome to my project! 36 | 37 | 38 | 39 | 40 | 41 | ## Customize the badge 42 | 43 | Heading badges support built-in badge variants to customize the appearance of the badge. 44 | 45 | To specify a variant, add the variant name after the badge content in curly braces `{}` with the `variant` keyword and the variant name. 46 | The accepted variants are available: `note` (blue), `tip` (purple), `danger` (red), `caution` (orange), or `success` (green). 47 | 48 | The following example will render a "POST" badge with a green background: 49 | 50 | 51 | 52 | ```mdx 53 | --- 54 | // src/content/docs/example.md 55 | title: My docs 56 | --- 57 | 58 | ### Example :badge[POST]{variant=success} 59 | 60 | Welcome to my project! 61 | ``` 62 | 63 | 64 | 65 | ### Example :badge[POST]{variant=success} 66 | 67 | Welcome to my project! 68 | 69 | 70 | 71 | 72 | 73 | ## Add multiple badges 74 | 75 | Multiple badges can be added to the same heading by using the [default syntax](#add-a-badge) multiple times and separating badges with a space. 76 | 77 | The following example will render a “POST” badge with a green background followed by a "New" badge using the default theme accent color: 78 | 79 | 80 | 81 | ```mdx 82 | --- 83 | // src/content/docs/example.md 84 | title: My docs 85 | --- 86 | 87 | ### Example :badge[POST]{variant=success} :badge[New] 88 | 89 | Welcome to my project! 90 | ``` 91 | 92 | 93 | 94 | ### Example :badge[POST]{variant=success} :badge[New] 95 | 96 | Welcome to my project! 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/components/TableOfContentsList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /** 3 | * A copy of the Starlight `` component with support for badges. 4 | * @see https://github.com/withastro/starlight/blob/1d32c8b0cec0ea5f66351b21b91748dfa78b404b/packages/starlight/components/TableOfContents/TableOfContentsList.astro 5 | */ 6 | 7 | import type { MarkdownHeading } from 'astro' 8 | 9 | import TableOfContentHeading from './TableOfContentHeading.astro' 10 | 11 | interface TocItem extends MarkdownHeading { 12 | children: TocItem[] 13 | } 14 | 15 | interface Props { 16 | toc: TocItem[] 17 | depth?: number 18 | isMobile?: boolean 19 | } 20 | 21 | const { toc, isMobile = false, depth = 0 } = Astro.props 22 | --- 23 | 24 |
    25 | { 26 | toc.map((heading) => ( 27 |
  • 28 | 29 | 30 | 31 | {heading.children.length > 0 && } 32 |
  • 33 | )) 34 | } 35 |
36 | 37 | 87 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/tests/toc.test.ts: -------------------------------------------------------------------------------- 1 | import { TestTypes, expect, test } from './test' 2 | 3 | for (const testType of TestTypes) { 4 | test(`uses generated IDs (${testType})`, async ({ testPage }) => { 5 | await testPage.goto(testType) 6 | 7 | // Skip the "Overview" link. 8 | let tocItemOffset = 1 9 | 10 | for (const [index, { text, id }] of testPage.expectedHeadings.entries()) { 11 | // Skip level 4 headings not included in the table of contents. 12 | if (text === 'Non-ToC') { 13 | tocItemOffset = 0 14 | continue 15 | } 16 | 17 | const tocItem = testPage.page 18 | .locator('starlight-toc') 19 | .getByRole('link') 20 | .nth(index + tocItemOffset) 21 | 22 | expect(await tocItem.textContent()).toMatch(text) 23 | expect(await tocItem.getAttribute('href')).toBe(`#${id}`) 24 | } 25 | }) 26 | 27 | test(`adds a badge with the default variant (${testType})`, async ({ testPage }) => { 28 | await testPage.goto(testType) 29 | 30 | const badge = testPage.page 31 | .locator('starlight-toc') 32 | .getByRole('link', { name: 'Default badge' }) 33 | .locator('.sl-badge') 34 | 35 | await expect(badge).toBeVisible() 36 | await expect(badge).toHaveText('New') 37 | expect(await badge.getAttribute('class')).toContain('default') 38 | }) 39 | 40 | test(`adds a badge with a specified variant (${testType})`, async ({ testPage }) => { 41 | await testPage.goto(testType) 42 | 43 | const badge = testPage.page 44 | .locator('starlight-toc') 45 | .getByRole('link', { name: 'Variant badge' }) 46 | .locator('.sl-badge') 47 | 48 | await expect(badge).toBeVisible() 49 | await expect(badge).toHaveText('v1.0') 50 | expect(await badge.getAttribute('class')).toContain('caution') 51 | }) 52 | 53 | test(`adds a badge with a space (${testType})`, async ({ testPage }) => { 54 | await testPage.goto(testType) 55 | 56 | const badge = testPage.page 57 | .locator('starlight-toc') 58 | .getByRole('link', { name: 'Badge with a space' }) 59 | .locator('.sl-badge') 60 | 61 | await expect(badge).toBeVisible() 62 | await expect(badge).toHaveText('A Badge') 63 | }) 64 | 65 | test(`adds a badge with multiple spaces (${testType})`, async ({ testPage }) => { 66 | await testPage.goto(testType) 67 | 68 | const badge = testPage.page 69 | .locator('starlight-toc') 70 | .getByRole('link', { name: 'Badge with multiple spaces' }) 71 | .locator('.sl-badge') 72 | 73 | await expect(badge).toBeVisible() 74 | await expect(badge).toHaveText('A Badge with Spaces') 75 | }) 76 | 77 | test(`adds multiple badge (${testType})`, async ({ testPage }) => { 78 | await testPage.goto(testType) 79 | 80 | const badges = testPage.page 81 | .locator('starlight-toc') 82 | .getByRole('link', { name: 'Multiple badges' }) 83 | .locator('.sl-badge') 84 | 85 | for (let i = 0; i < 2; i++) { 86 | await expect(badges.nth(i)).toBeVisible() 87 | await expect(badges.nth(i)).toHaveText(`Badge ${i + 1}`) 88 | } 89 | }) 90 | 91 | test(`uses specified custom IDs (${testType})`, async ({ testPage }) => { 92 | await testPage.goto(testType) 93 | 94 | for (const [index, { text, id }] of testPage.expectedCustomHeadings.entries()) { 95 | const tocItem = testPage.page 96 | .locator('starlight-toc') 97 | .getByRole('link') 98 | // Skip the "Overview" link and non-custom headings. 99 | .nth(testPage.expectedHeadings.length + index) 100 | 101 | expect(await tocItem.textContent()).toMatch(text) 102 | expect(await tocItem.getAttribute('href')).toBe(`#${id}`) 103 | } 104 | }) 105 | 106 | test(`adds a badge to a heading with a custom ID (${testType})`, async ({ testPage }) => { 107 | await testPage.goto(testType) 108 | 109 | const badge = testPage.page 110 | .locator('starlight-toc') 111 | .getByRole('link', { name: 'Heading with custom ID and a badge' }) 112 | .locator('.sl-badge') 113 | 114 | await expect(badge).toBeVisible() 115 | await expect(badge).toHaveText('Custom') 116 | }) 117 | } 118 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/tests/heading.test.ts: -------------------------------------------------------------------------------- 1 | import { TestTypes, expect, test } from './test' 2 | 3 | for (const testType of TestTypes) { 4 | test(`generates IDs for all headings (${testType})`, async ({ testPage }) => { 5 | await testPage.goto(testType) 6 | 7 | for (const [index, { text, id }] of testPage.expectedHeadings.entries()) { 8 | const heading = testPage.page.locator('.sl-markdown-content').getByRole('heading').nth(index) 9 | 10 | expect(await heading.textContent()).toMatch(text) 11 | expect(await heading.getAttribute('id')).toBe(id) 12 | } 13 | }) 14 | 15 | test(`adds a heading badge with the default variant (${testType})`, async ({ testPage }) => { 16 | await testPage.goto(testType) 17 | 18 | const headingBadge = testPage.page 19 | .getByRole('heading', { name: 'Default badge' }) 20 | .locator('span[data-shb-badge-variant=default]') 21 | 22 | await expect(headingBadge).toBeVisible() 23 | await expect(headingBadge).toHaveText('New') 24 | }) 25 | 26 | test(`adds a heading badge with a specified variant (${testType})`, async ({ testPage }) => { 27 | await testPage.goto(testType) 28 | 29 | const headingBadge = testPage.page 30 | .getByRole('heading', { name: 'Variant Badge' }) 31 | .locator('span[data-shb-badge-variant=caution]') 32 | 33 | await expect(headingBadge).toBeVisible() 34 | await expect(headingBadge).toHaveText('v1.0') 35 | }) 36 | 37 | test(`adds a heading badge to non-ToC headings (${testType})`, async ({ testPage }) => { 38 | await testPage.goto(testType) 39 | 40 | const headingBadge = testPage.page 41 | .getByRole('heading', { name: 'Non-ToC' }) 42 | .locator('span[data-shb-badge-variant=success]') 43 | 44 | await expect(headingBadge).toBeVisible() 45 | await expect(headingBadge).toHaveText('POST') 46 | }) 47 | 48 | test(`adds a heading badge with a space (${testType})`, async ({ testPage }) => { 49 | await testPage.goto(testType) 50 | 51 | const headingBadge = testPage.page 52 | .getByRole('heading', { name: 'Badge with a space' }) 53 | .locator('span[data-shb-badge-variant=default]') 54 | 55 | await expect(headingBadge).toBeVisible() 56 | await expect(headingBadge).toHaveText('A Badge') 57 | }) 58 | 59 | test(`adds a heading badge with multiple spaces (${testType})`, async ({ testPage }) => { 60 | await testPage.goto(testType) 61 | 62 | const headingBadge = testPage.page 63 | .getByRole('heading', { name: 'Badge with multiple spaces' }) 64 | .locator('span[data-shb-badge-variant=default]') 65 | 66 | await expect(headingBadge).toBeVisible() 67 | await expect(headingBadge).toHaveText('A Badge with Spaces') 68 | }) 69 | 70 | test(`adds multiple heading badges (${testType})`, async ({ testPage }) => { 71 | await testPage.goto(testType) 72 | 73 | const headingBadges = testPage.page 74 | .getByRole('heading', { name: 'Multiple badges' }) 75 | .locator('span[data-shb-badge-variant]') 76 | 77 | for (let i = 0; i < 2; i++) { 78 | await expect(headingBadges.nth(i)).toBeVisible() 79 | await expect(headingBadges.nth(i)).toHaveText(`Badge ${i + 1}`) 80 | expect(await headingBadges.nth(i).getAttribute('data-shb-badge-variant')).toBe(i === 0 ? 'default' : 'success') 81 | } 82 | }) 83 | 84 | test(`uses specified custom IDs (${testType})`, async ({ testPage }) => { 85 | await testPage.goto(testType) 86 | 87 | for (const [index, { text, id }] of testPage.expectedCustomHeadings.entries()) { 88 | const heading = testPage.page 89 | .locator('.sl-markdown-content') 90 | .getByRole('heading') 91 | // Skip non-custom headings. 92 | .nth(testPage.expectedHeadings.length + index) 93 | 94 | await heading.highlight() 95 | await testPage.page.pause() 96 | 97 | expect(await heading.textContent()).toMatch(text) 98 | expect(await heading.getAttribute('id')).toBe(id) 99 | } 100 | }) 101 | 102 | test(`adds a heading badge to a heading with a custom ID (${testType})`, async ({ testPage }) => { 103 | await testPage.goto(testType) 104 | 105 | const headingBadge = testPage.page 106 | .getByRole('heading', { name: 'Heading with custom ID and a badge' }) 107 | .locator('span[data-shb-badge-variant=default]') 108 | 109 | await expect(headingBadge).toBeVisible() 110 | await expect(headingBadge).toHaveText('Custom') 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/libs/starlight-toc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A copy of the Starlight `` component with support for badges. 3 | * @see https://github.com/withastro/starlight/blob/1d32c8b0cec0ea5f66351b21b91748dfa78b404b/packages/starlight/components/TableOfContents/starlight-toc.ts 4 | */ 5 | 6 | const PAGE_TITLE_ID = '_top' 7 | 8 | export class StarlightTOC extends HTMLElement { 9 | private _current = this.querySelector('a[aria-current="true"]') 10 | private minH = Number.parseInt(this.dataset['minH'] ?? '2', 10) 11 | private maxH = Number.parseInt(this.dataset['maxH'] ?? '3', 10) 12 | 13 | protected set current(link: HTMLAnchorElement) { 14 | if (link === this._current) return 15 | if (this._current) this._current.removeAttribute('aria-current') 16 | link.setAttribute('aria-current', 'true') 17 | this._current = link 18 | } 19 | 20 | protected get current(): HTMLAnchorElement | null { 21 | return this._current 22 | } 23 | 24 | constructor() { 25 | super() 26 | 27 | /** All the links in the table of contents. */ 28 | const links = [...this.querySelectorAll('a')] 29 | 30 | /** Walk up the DOM to find the nearest heading. */ 31 | const getElementHeading = (el: Element | null): HTMLHeadingElement | null => { 32 | if (!el) return null 33 | const origin = el 34 | while (el) { 35 | if (this.isHeading(el)) return el 36 | // Assign the previous sibling’s last, most deeply nested child to el. 37 | el = el.previousElementSibling 38 | while (el?.lastElementChild) { 39 | el = el.lastElementChild 40 | } 41 | // Look for headings amongst siblings. 42 | const h = getElementHeading(el) 43 | if (h) return h 44 | } 45 | // Walk back up the parent. 46 | return getElementHeading(origin.parentElement) 47 | } 48 | 49 | /** Handle intersections and set the current link to the heading for the current intersection. */ 50 | const setCurrent: IntersectionObserverCallback = (entries) => { 51 | for (const { isIntersecting, target } of entries) { 52 | if (!isIntersecting) continue 53 | const heading = getElementHeading(target) 54 | if (!heading) continue 55 | const link = links.find((link) => link.hash === `#${encodeURIComponent(heading.id)}`) 56 | if (link) { 57 | this.current = link 58 | break 59 | } 60 | } 61 | } 62 | 63 | // Observe elements with an `id` (most likely headings) and their siblings. 64 | // Also observe direct children of `.content` to include elements before 65 | // the first heading. 66 | const toObserve = document.querySelectorAll('main [id], main [id] ~ *, main .content > *') 67 | 68 | let observer: IntersectionObserver | undefined 69 | const observe = () => { 70 | if (observer) observer.disconnect() 71 | observer = new IntersectionObserver(setCurrent, { rootMargin: this.getRootMargin() }) 72 | for (const element of toObserve) observer.observe(element) 73 | } 74 | observe() 75 | 76 | // `requestIdleCallback` is not available in Safari. 77 | const onIdle = globalThis.requestIdleCallback || ((cb) => setTimeout(cb, 1)) 78 | let timeout: NodeJS.Timeout 79 | window.addEventListener('resize', () => { 80 | // Disable intersection observer while window is resizing. 81 | if (observer) observer.disconnect() 82 | clearTimeout(timeout) 83 | timeout = setTimeout(() => onIdle(observe), 200) 84 | }) 85 | } 86 | 87 | /** Test if an element is a table-of-contents heading. */ 88 | private isHeading = (el: Element): el is HTMLHeadingElement => { 89 | if (el instanceof HTMLHeadingElement) { 90 | // Special case for page title h1 91 | if (el.id === PAGE_TITLE_ID) return true 92 | // Check the heading level is within the user-configured limits for the ToC 93 | const level = el.tagName[1] 94 | if (level) { 95 | const int = Number.parseInt(level, 10) 96 | if (int >= this.minH && int <= this.maxH) return true 97 | } 98 | } 99 | return false 100 | } 101 | 102 | private getRootMargin(): `-${number}px 0% ${number}px` { 103 | const navBarHeight = document.querySelector('header')?.getBoundingClientRect().height ?? 0 104 | // `` only exists in mobile ToC, so will fall back to 0 in large viewport component. 105 | const mobileTocHeight = this.querySelector('summary')?.getBoundingClientRect().height ?? 0 106 | /** Start intersections at nav height + 2rem padding. */ 107 | const top = navBarHeight + mobileTocHeight + 32 108 | /** End intersections `53px` later. This is slightly more than the maximum `margin-top` in Markdown content. */ 109 | const bottom = top + 53 110 | const height = document.documentElement.clientHeight 111 | return `-${top}px 0% ${bottom - height}px` 112 | } 113 | } 114 | 115 | customElements.define('starlight-toc', StarlightTOC) 116 | -------------------------------------------------------------------------------- /packages/starlight-heading-badges/components/HeadingBadgesMobileTableOfContents.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /** 3 | * A copy of the Starlight `` component with support for badges. 4 | * @see https://github.com/withastro/starlight/blob/1d32c8b0cec0ea5f66351b21b91748dfa78b404b/packages/starlight/components/MobileTableOfContents.astro 5 | */ 6 | 7 | import { Icon } from '@astrojs/starlight/components' 8 | 9 | import TableOfContentsList from './TableOfContentsList.astro' 10 | 11 | const { toc } = Astro.locals.starlightRoute 12 | --- 13 | 14 | { 15 | toc && ( 16 | 17 | 31 | 32 | ) 33 | } 34 | 35 | 114 | 115 | 181 | -------------------------------------------------------------------------------- /docs/src/content/docs/demo.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Demo 3 | description: Discover the Starlight Heading Badges plugin in action. 4 | --- 5 | 6 | ## Tuetur lege 7 | 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam tristique fringilla urna, quis ullamcorper dolor consequat eget. Ut egestas ipsum aliquet diam sagittis malesuada non id mauris. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam accumsan magna nulla, et ullamcorper enim rutrum vel. Vivamus sodales pretium eleifend. Nam ac rhoncus purus. Suspendisse egestas est ut dapibus aliquam. Mauris maximus congue magna ac placerat. 9 | 10 | ## Erat feram :badge[New] 11 | 12 | Quisque porttitor dictum urna, a sollicitudin risus imperdiet quis. Nulla luctus, dui sed interdum sagittis, nibh nunc tempor lorem, at feugiat eros eros sit amet purus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin lacinia, enim et porta mollis, urna ante blandit dui, non accumsan odio eros ut magna. 13 | 14 | Praesent fermentum elementum magna vel accumsan. Nulla facilisi. In hac habitasse platea dictumst. Mauris ultrices erat non orci sodales elementum. Nunc sollicitudin cursus ligula. 15 | 16 | ### Renovat myricae 17 | 18 | Suspendisse convallis vel ligula quis elementum. Sed pharetra arcu at placerat blandit. Morbi interdum nisi eros, quis tincidunt lacus condimentum mattis. Donec sed pellentesque sem, vitae venenatis augue. Donec lacinia malesuada ante et rhoncus. Nunc sed eros dignissim justo facilisis pellentesque. Donec ac tempus quam, et semper lorem. Donec placerat rhoncus magna, sit amet maximus lectus sodales eget. In hac habitasse platea dictumst. 19 | 20 | Vivamus non nibh non est aliquet tristique at sed velit. Nulla nec euismod odio. Fusce euismod quam id erat tincidunt fermentum. Pellentesque a metus sit amet elit iaculis cursus mattis ut lacus. Maecenas finibus molestie turpis id tincidunt. Integer est justo, scelerisque a orci semper, cursus efficitur libero. 21 | 22 | ### Post senserit 23 | 24 | Vestibulum pharetra volutpat augue sed vulputate. Phasellus eros dui, faucibus eu odio quis, mattis tristique sapien. Duis sed metus vel mi faucibus tempus ut vitae enim. Nam porttitor, nunc a dictum mollis, purus justo pellentesque elit, a ultricies dolor lorem ornare odio. Suspendisse tempus convallis tortor. 25 | 26 | Nunc vel pharetra turpis. Sed rutrum iaculis mauris, eu rhoncus ipsum tincidunt in. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Etiam nisl mi, tincidunt vel urna dictum, sollicitudin mollis ipsum. Integer convallis ex sed ex convallis, eget tristique urna feugiat. Fusce gravida egestas velit et faucibus. Duis eleifend diam vitae pretium dictum. 27 | 28 | ## Non esse 29 | 30 | Ut rhoncus risus congue augue commodo tincidunt. Cras vel lorem nunc. Nullam tempor leo at magna ornare tincidunt. Vivamus nisl metus, porta vitae ipsum sit amet, gravida viverra mi. Nullam quis turpis in tortor ultricies malesuada sodales sed ligula. 31 | 32 | Morbi eget pulvinar mauris. Nulla hendrerit dolor ac purus ultrices euismod. Aliquam ornare ante urna, at sagittis odio scelerisque ac. Nam commodo et enim at varius. Pellentesque consequat tincidunt lorem, id laoreet quam egestas a. Sed fermentum et sapien at auctor. Integer dolor enim, elementum ut tempus ac, facilisis vitae nunc. 33 | 34 | Curabitur vel bibendum elit, non hendrerit justo. Fusce sed lectus non nunc aliquam tincidunt. Ut a odio finibus arcu tempus ultrices. Sed et accumsan purus. Donec vestibulum imperdiet eros, et fringilla est tempus eget. Vivamus quis pharetra sapien. Donec auctor eget urna pellentesque fringilla. Quisque lorem justo, sagittis non laoreet pharetra, sodales ac lacus. Quisque auctor felis eget neque ullamcorper sodales sed sed felis. Nam facilisis dictum orci, et vestibulum metus tincidunt ac. Vivamus eget quam dignissim nisl malesuada semper. Sed vitae tortor diam. 35 | 36 | Praesent nec diam porttitor, pretium mauris quis, venenatis diam. Donec aliquam dapibus egestas. Pellentesque a commodo ligula. Cras euismod vehicula quam, at pharetra neque faucibus ac. Nam in ligula magna. Sed cursus tellus a dui sagittis condimentum. Nam posuere ipsum quis ligula pellentesque consectetur. Duis id urna eu neque facilisis eleifend. Nam at scelerisque tortor. Donec ornare tempus venenatis. Aenean efficitur orci venenatis urna scelerisque venenatis. 37 | 38 | Nullam consequat varius diam, vitae consequat libero pulvinar vel. Sed venenatis, nibh a blandit tristique, ligula neque ultricies mi, ac volutpat dolor nisl ut orci. Vivamus aliquam nibh vestibulum scelerisque tempor. 39 | 40 | ## Nos at arva :badge[v1.0]{variant=caution} 41 | 42 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris dignissim, massa ut suscipit accumsan, lacus diam aliquet felis, id eleifend justo lectus non tortor. Sed auctor porttitor justo, a mattis elit convallis eu. Suspendisse quis aliquam diam. Nulla in cursus urna. Donec pretium vel nulla in consequat. Mauris lorem odio, fringilla nec enim a, molestie euismod enim. Ut venenatis pretium risus non ultrices. Aliquam erat volutpat. Duis scelerisque molestie nibh, id sagittis dui faucibus eu. Donec porta, diam in iaculis tempor, urna mauris blandit lectus, a pellentesque leo eros at tortor. Maecenas laoreet tempor dui, vitae gravida lectus interdum eget. Donec at sapien in libero cursus maximus eget et mi. 43 | 44 | Nulla placerat eros ut libero accumsan, vel luctus dolor porta. Vivamus ex orci, accumsan non massa nec, porta posuere mauris. Cras a efficitur libero. Curabitur vel nibh tempus velit hendrerit auctor. Sed ut sollicitudin est, ut viverra justo. Aenean ut enim molestie, porttitor erat a, rhoncus nisi. Vivamus tincidunt tempus auctor. Morbi eget mauris vel eros vulputate ullamcorper. Sed imperdiet sagittis massa, et tempor leo convallis eu. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. 45 | 46 | Pellentesque quis ipsum sollicitudin, vehicula quam sed, lacinia lacus. Sed vel enim tempus, tempus turpis a, pharetra turpis. Proin dapibus mi a massa aliquet, nec gravida lectus vehicula. Nunc a nisl vestibulum lacus volutpat pretium quis ut purus. Proin non lorem finibus, ornare libero sit amet, porttitor quam. Suspendisse ut ornare lacus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ac vulputate elit. Morbi pharetra bibendum dictum. Phasellus iaculis purus a vehicula varius. Morbi tempor ante non leo fermentum, vitae hendrerit felis porttitor. Aliquam facilisis in eros finibus laoreet. Donec ut mauris vestibulum, tempor ex nec, volutpat enim. Aliquam justo urna, tempor et feugiat sit amet, maximus porttitor ex. Vestibulum pulvinar velit sit amet sem malesuada aliquet. 47 | 48 | ### Sortes populisque 49 | 50 | In euismod turpis lectus, ut molestie turpis hendrerit id. Ut faucibus maximus augue vitae interdum. Vivamus ipsum quam, mollis non mauris ut, interdum vehicula tellus. Donec laoreet magna eu enim elementum, non blandit eros eleifend. Donec rhoncus mauris eget dolor dapibus, a consectetur ante posuere. Fusce cursus dolor nisi, quis lacinia felis sollicitudin non. Sed sit amet sollicitudin eros. Quisque eu lacus vitae mi sodales dapibus eu ac eros. Sed bibendum imperdiet augue, non facilisis risus malesuada a. Quisque sapien risus, sodales eu nisl eget, luctus varius elit. 51 | 52 | #### Alta utque :badge[POST]{variant=success} 53 | 54 | Aliquam lobortis ac eros non elementum. Fusce viverra quis felis vel consequat. Mauris vitae luctus elit. Fusce at ipsum tempus, vulputate felis vitae, auctor tortor. Pellentesque congue dapibus tellus, nec auctor velit aliquet a. Quisque id porta elit, non auctor urna. Maecenas feugiat interdum augue, eget luctus ligula tincidunt vel. Aenean eleifend vehicula mi, ornare vulputate velit dapibus eget. Sed in mauris et ipsum ornare dapibus. Nam cursus bibendum est, ac vestibulum metus tristique quis. 55 | --------------------------------------------------------------------------------