Skip to content

Commit

Permalink
Add heading anchor link plugin (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
BearToCode authored Jan 16, 2024
2 parents 5ddddbd + 4843c27 commit d3f3b4c
Show file tree
Hide file tree
Showing 19 changed files with 481 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Differently from most editors, Carta includes neither ProseMirror nor CodeMirror
- **Emojis**, with included search (plugin);
- **Tikz** support (plugin);
- **Attachment** support (plugin);
- **Anchor** links in headings;
- Code blocks **syntax highlighting** (plugin).

## Packages
Expand All @@ -63,6 +64,7 @@ Differently from most editors, Carta includes neither ProseMirror nor CodeMirror
| [plugin-slash](https://www.npmjs.com/package/@cartamd/plugin-slash) | ![plugin-slash](https://img.shields.io/npm/v/@cartamd/plugin-slash) | [/plugins/slash](https://beartocode.github.io/carta/plugins/slash) |
| [plugin-tikz](https://www.npmjs.com/package/@cartamd/plugin-tikz) | ![plugin-tikz](https://img.shields.io/npm/v/@cartamd/plugin-tikz) | [/plugins/tikz](https://beartocode.github.io/carta/plugins/tikz) |
| [plugin-attachment](https://www.npmjs.com/package/@cartamd/plugin-attachment) | ![plugin-attachment](https://img.shields.io/npm/v/@cartamd/plugin-attachment) | [/plugins/attachment](https://beartocode.github.io/carta/plugins/attachment) |
| [plugin-anchor](https://www.npmjs.com/package/@cartamd/plugin-anchor) | ![plugin-anchor](https://img.shields.io/npm/v/@cartamd/plugin-anchor) | [/plugins/anchor](https://beartocode.github.io/carta/plugins/anchor) |

# Getting started

Expand Down
7 changes: 7 additions & 0 deletions docs/src/lib/components/sidebar/Sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Download,
Face,
FontFamily,
Link2,
Slash,
File,
FontStyle
Expand Down Expand Up @@ -83,6 +84,12 @@
<span class="text-[0.95rem]">Attachment</span>
</SidebarLink>

<!-- Anchor -->
<SidebarLink href="/plugins/anchor">
<Link2 class="h-5 w-5" />
<span class="text-[0.95rem]">Anchor</span>
</SidebarLink>

<h3 class="mb-3 ml-4 mt-6 text-sm font-medium first:mt-0 last:mb-0">API</h3>

<!-- Utilities -->
Expand Down
8 changes: 8 additions & 0 deletions docs/src/pages/introduction.svelte.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ Carta comes with a set of official plugins for the most common use cases.
</Card.Header>
</Card.Root>

<Card.Root href="/plugins/anchor">
<Card.Header>
<Icon.Link2 class="w-8 h-8 text-sky-300" />
<Card.Title>Anchor</Card.Title>
<Card.Description>Add anchor links to headings.</Card.Description>
</Card.Header>
</Card.Root>

</div>

## Examples
Expand Down
66 changes: 66 additions & 0 deletions docs/src/pages/plugins/anchor.svelte.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
section: Plugins
title: Anchor
---

<script>
import Code from '$lib/components/code/Code.svelte';
</script>

This plugin adds `id` attributes and permalinks to headings.

## Installation

<Code>

```
npm i @cartamd/plugin-anchor
```

</Code>

## Setup

### Styles

Import the default theme, or create you own:

<Code>

```ts
import '@cartamd/plugin-anchor/default.css';
```

</Code>

### Extension

<Code>

```svelte
<script>
import { Carta, CartaEditor } from 'carta-md';
import { anchor } from '@cartamd/plugin-anchor';
const carta = new Carta({
extensions: [anchor()]
});
</script>
<CartaEditor {carta} />
```

</Code>

## Options

Here are the options you can pass to `anchor()`:

```ts
export interface AnchorExtensionOptions {
/**
* Maximum depth of headers to generate anchors for. Defaults to 6.
*/
maxDepth?: number;
}
```
11 changes: 11 additions & 0 deletions packages/plugin-anchor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
node_modules
/build
/dist
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
36 changes: 36 additions & 0 deletions packages/plugin-anchor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Carta Anchor Plugin

This plugin adds `id` attributes and permalinks to headings. Install it using:

```
npm i @cartamd/plugin-anchor
```

## Setup

### Styles

Import the default theme, or create you own:

```ts
import '@cartamd/plugin-anchor/default.css';
```

### Extension

```svelte
<script>
import { Carta, CartaEditor } from 'carta-md';
import { anchor } from '@cartamd/plugin-anchor';
const carta = new Carta({
extensions: [anchor()]
});
</script>
<CartaEditor {carta} />
```

## Documentation

Checkout the [docs](https://beartocode.github.io/carta/plugins/anchor) for examples, options and more.
68 changes: 68 additions & 0 deletions packages/plugin-anchor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"name": "@cartamd/plugin-anchor",
"version": "3.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "MIT",
"scripts": {
"dev": "vite dev",
"build": "vite build && npm run package",
"preview": "vite preview",
"package": "svelte-kit sync && svelte-package && publint",
"prepublishOnly": "npm run package",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/BearToCode/carta.git"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
},
"./default.css": "./dist/default.css",
"./default-theme.css": "./dist/default.css"
},
"files": [
"dist",
"!dist/**/*.test.*",
"!dist/**/*.spec.*"
],
"dependencies": {
"slugify": "^1.6.6"
},
"peerDependencies": {
"carta-md": "^3.1.0",
"marked": "^9.1.5",
"svelte": "^3.54.0 || ^4.0.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"@sveltejs/package": "^2.0.0",
"carta-md": "workspace:*",
"publint": "^0.1.9",
"svelte": "^3.54.0 || ^4.0.0",
"svelte-check": "^3.0.1",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.3.9",
"marked": "^9.1.5"
},
"svelte": "./dist/index.js",
"keywords": [
"carta",
"markdown",
"editor",
"marked",
"text editor",
"marked editor",
"slash",
"syntax highlighting",
"emoji",
"katex"
]
}
12 changes: 12 additions & 0 deletions packages/plugin-anchor/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}

export {};
12 changes: 12 additions & 0 deletions packages/plugin-anchor/src/app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div>%sveltekit.body%</div>
</body>
</html>
13 changes: 13 additions & 0 deletions packages/plugin-anchor/src/lib/default.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.carta-renderer .anchor-link {
visibility: hidden;
opacity: 0.6;
}

.carta-renderer h1:hover .anchor-link,
.carta-renderer h2:hover .anchor-link,
.carta-renderer h3:hover .anchor-link,
.carta-renderer h4:hover .anchor-link,
.carta-renderer h5:hover .anchor-link,
.carta-renderer h6:hover .anchor-link {
visibility: visible;
}
49 changes: 49 additions & 0 deletions packages/plugin-anchor/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { CartaExtension } from 'carta-md';
import { generateUniqueSlug } from './slug';
export * from './default.css?inline';

export interface AnchorExtensionOptions {
/**
* Maximum depth of headers to generate anchors for. Defaults to 6.
*/
maxDepth?: number;
}

/**
* Carta anchor plugin. Adds support to render anchor links in header tags.
*/
export const anchor = (options?: AnchorExtensionOptions): CartaExtension => {
let slugs: string[] = [];

const maxDepth = options?.maxDepth ?? 6;

return {
// Reset the slug history after rendering completes, so the links persist after re-rendering
listeners: [
['carta-render', () => (slugs = [])],
['carta-render-ssr', () => (slugs = [])]
],
markedExtensions: [
{
renderer: {
heading(text, level, raw) {
if (level > maxDepth) {
return false;
}

const slug = generateUniqueSlug(raw, slugs);

return `
<h${level}>
<span>${text}</span>
<a id="${slug}" href="#${slug}" class="anchor-link">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" width="16" height="16"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>
</a>
</h${level}>`;
}
}
}
]
};
};
23 changes: 23 additions & 0 deletions packages/plugin-anchor/src/lib/slug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import slugify from 'slugify';

function generateSlug(raw: string) {
const base = slugify(raw, {
lower: true,
remove: /[^a-zA-Z0-9_\- ]/g
});
return base;
}

export function generateUniqueSlug(raw: string, slugs: string[]) {
const base = generateSlug(raw);
let slug = base;

let i = 1;
// Add unique suffix to slug if it already exists
while (slugs.includes(slug)) {
slug = `${base}-${i}`;
i++;
}
slugs.push(slug);
return slug;
}
Loading

0 comments on commit d3f3b4c

Please sign in to comment.