From cb2cd7f5e74d5d2e26620beb1688fbdff63af534 Mon Sep 17 00:00:00 2001 From: Quang Phan Date: Wed, 2 Nov 2022 08:34:58 +0700 Subject: [PATCH] feat(preprocess-auto-slug): mature implementation and customization --- .changeset/little-bobcats-rescue.md | 5 + .../docs/(pkg)/clickoutside/+page.svelte | 9 +- apps/docs/svelte.config.js | 11 +- apps/docs/tailwind.config.cjs | 31 +++-- packages/preprocessors/auto-slug/package.json | 2 +- .../auto-slug/src/auto-slug.constants.ts | 22 +++ .../preprocessors/auto-slug/src/auto-slug.ts | 126 ++++++++++-------- .../auto-slug/src/auto-slug.types.ts | 75 +++++++++++ .../auto-slug/src/auto-slug.utils.ts | 34 +++++ packages/preprocessors/auto-slug/src/index.ts | 7 +- 10 files changed, 245 insertions(+), 77 deletions(-) create mode 100644 .changeset/little-bobcats-rescue.md create mode 100644 packages/preprocessors/auto-slug/src/auto-slug.constants.ts create mode 100644 packages/preprocessors/auto-slug/src/auto-slug.types.ts create mode 100644 packages/preprocessors/auto-slug/src/auto-slug.utils.ts diff --git a/.changeset/little-bobcats-rescue.md b/.changeset/little-bobcats-rescue.md new file mode 100644 index 00000000..5e5ce102 --- /dev/null +++ b/.changeset/little-bobcats-rescue.md @@ -0,0 +1,5 @@ +--- +'@svelte-put/preprocess-auto-slug': minor +--- + +more mature options and ability to customization diff --git a/apps/docs/src/routes/docs/(pkg)/clickoutside/+page.svelte b/apps/docs/src/routes/docs/(pkg)/clickoutside/+page.svelte index 16d19970..3420b525 100644 --- a/apps/docs/src/routes/docs/(pkg)/clickoutside/+page.svelte +++ b/apps/docs/src/routes/docs/(pkg)/clickoutside/+page.svelte @@ -39,14 +39,17 @@

Typescript Support

-Ambient types for custom events should be available automatically where `clickoutside` is imported. +

+ Ambient types for custom events should be available automatically where `clickoutside` is + imported. +

-If the above is not working, fall back to this: +

If the above is not working, fall back to this:

diff --git a/apps/docs/svelte.config.js b/apps/docs/svelte.config.js index b194e303..9476c4b8 100644 --- a/apps/docs/svelte.config.js +++ b/apps/docs/svelte.config.js @@ -16,7 +16,16 @@ const config = { }, ], }), - autoSlug(), + autoSlug((defaultOptions) => ({ + anchor: { + content: '#', + position: 'prepend', + properties: { + ...defaultOptions.anchor.properties, + class: 'heading-anchor', + }, + }, + })), preprocess({ postcss: true }), ], diff --git a/apps/docs/tailwind.config.cjs b/apps/docs/tailwind.config.cjs index 6be19c8c..7d5941f1 100644 --- a/apps/docs/tailwind.config.cjs +++ b/apps/docs/tailwind.config.cjs @@ -1,7 +1,22 @@ const plugin = require('tailwindcss/plugin'); const sveltePut = plugin( - ({ addComponents, addUtilities }) => { + ({ addComponents, addUtilities, addBase }) => { + addBase({ + 'h1,h2,h3,h4,h5,h6': { + '@apply relative': {}, + '& .heading-anchor': { + '@apply text-primary absolute top-0 bottom-0 right-full opacity-0 transition-opacity duration-150 mr-1': + {}, + 'text-decoration': 'none', + 'font-weight': 'inherit', + }, + '&:hover .heading-anchor': { + '@apply opacity-100': {}, + }, + }, + }); + addUtilities({ '.bg-gradient-brand': { '@apply bg-gradient-to-r from-svelte to-[#42b883]': {}, @@ -41,20 +56,8 @@ const sveltePut = plugin( h1: { 'font-size': '2rem', }, - 'h2,h3,h4,h5,h6': { - position: 'relative', - '&:hover::before': { - content: '"#"', - position: 'absolute', - right: '101%', - }, - }, 'h1,h2,h3,h4,h5,h6': { 'scroll-margin-top': theme('spacing.header'), - '& a': { - 'text-decoration': 'none', - 'font-weight': 'inherit', - }, }, }, }, @@ -114,7 +117,7 @@ const sveltePut = plugin( /** @type {import("tailwindcss").Config } */ const config = { - content: ['./src/**/*.{html,js,svelte,ts,md}'], + content: ['./src/**/*.{html,js,svelte,ts,md}', 'svelte.config.js'], plugins: [sveltePut, require('@tailwindcss/typography')], }; diff --git a/packages/preprocessors/auto-slug/package.json b/packages/preprocessors/auto-slug/package.json index 3c0d0f59..4192be96 100644 --- a/packages/preprocessors/auto-slug/package.json +++ b/packages/preprocessors/auto-slug/package.json @@ -4,7 +4,7 @@ "description": "Generate slug from text content, to add id to headings for example", "main": "lib/index.js", "module": "lib/index.mjs", - "types": "lib/src/index.d.ts", + "types": "lib/index.d.ts", "exports": { ".": { "import": "./lib/index.mjs", diff --git a/packages/preprocessors/auto-slug/src/auto-slug.constants.ts b/packages/preprocessors/auto-slug/src/auto-slug.constants.ts new file mode 100644 index 00000000..29696227 --- /dev/null +++ b/packages/preprocessors/auto-slug/src/auto-slug.constants.ts @@ -0,0 +1,22 @@ +import { AutoSlugOptions } from './auto-slug.types'; + +/** + * default options for auto-slug + * + * @public + */ +export const DEFAULT_AUTO_SLUG_OPTIONS: AutoSlugOptions = { + tags: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + attributeName: 'id', + slug: ({ generated }) => generated, + anchor: { + enabled: true, + position: 'prepend', + content: '#', + properties: { + 'aria-hidden': 'true', + 'tab-index': '-1', + }, + href: (slug) => `#${slug}`, + }, +}; diff --git a/packages/preprocessors/auto-slug/src/auto-slug.ts b/packages/preprocessors/auto-slug/src/auto-slug.ts index 78dd2401..9d1c31f7 100644 --- a/packages/preprocessors/auto-slug/src/auto-slug.ts +++ b/packages/preprocessors/auto-slug/src/auto-slug.ts @@ -1,54 +1,12 @@ -import type { BaseNode } from 'estree'; -import Slugger from 'github-slugger'; +import BananaSlug from 'github-slugger'; import MagicString from 'magic-string'; import { parse } from 'svelte-parse-markup'; import { walk } from 'svelte/compiler'; import { PreprocessorGroup } from 'svelte/types/compiler/preprocess'; -const tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - -/** - * @internal - */ -interface Node extends BaseNode { - name: string; - start: number; - attributes: Array<{ name: string; type: string }>; - children?: Array; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: any; -} - -/** - * @internal - */ -function hasIdAttribute(node: Node) { - return node.attributes.some((attr) => attr.name === 'id' && attr.type === 'Attribute'); -} - -/** - * @internal - */ -function getTextContent(node: Node) { - if (node.type === 'Text') return node.data; - if (!node.children || node.children.length === 0) return ''; - let text = ''; - for (const child of Array.from(node.children)) { - walk(child, { - enter(childNode) { - text += getTextContent(childNode as Node); - }, - }); - } - return text; -} - -/** - * @internal - */ -function isMustacheNode(node: Node) { - return node.children.some((child) => child.type === 'MustacheTag'); -} +import { DEFAULT_AUTO_SLUG_OPTIONS } from './auto-slug.constants'; +import type { AutoSlugInput, Node } from './auto-slug.types'; +import { getTextContent, hasIdAttribute, isMustacheNode } from './auto-slug.utils'; /** * create svelte preprocessor to generate slug from text content of matching tags @@ -59,27 +17,84 @@ function isMustacheNode(node: Node) { * * @returns {PreprocessorGroup} svelte preprocessor interface */ -export function autoSlug(): PreprocessorGroup { - const preprocess = { +export function autoSlug(input: AutoSlugInput): PreprocessorGroup { + let options = DEFAULT_AUTO_SLUG_OPTIONS; + const iOptions = typeof input === 'function' ? input(options) : input; + options = { + ...options, + ...iOptions, + anchor: { + ...options.anchor, + ...(iOptions.anchor ?? {}), + }, + }; + return { markup({ content, filename }) { const s = new MagicString(content); const ast = parse(content, { filename }); - const slugs = new Slugger(); + const slugger = new BananaSlug(); walk(ast.html, { enter(node) { const tNode = node as Node; if ( node.type === 'Element' && - tags.includes(tNode.name) && - !hasIdAttribute(tNode) && + options.tags.includes(tNode.name) && + !hasIdAttribute(tNode, options.attributeName) && !isMustacheNode(tNode) && tNode.children?.length ) { - const textContent = getTextContent(tNode).trim(); - if (textContent) { - const slug = slugs.slug(textContent); - s.appendLeft(tNode.children[0].start - 1, ` id="${slug}"`); + const nodeText = getTextContent(tNode).trim(); + if (nodeText) { + const slug = options.slug({ + generated: slugger.slug(nodeText), + nodeText, + slugger, + }); + + const idAttrStr = ` ${options.attributeName}="${slug}"`; + s.appendLeft(tNode.children[0].start - 1, idAttrStr); + + if (options.anchor.enabled) { + const inlineProperties = Object.entries(options.anchor.properties) + .map(([key, value]) => `${key}="${value}"`) + .join(' '); + const anchorOpening = ``; + const anchorClosing = ''; + + switch (options.anchor.position) { + case 'before': + s.appendLeft( + tNode.start, + `${anchorOpening}${options.anchor.content}${anchorClosing}`, + ); + break; + case 'prepend': + s.appendRight( + tNode.children[0].start, + `${anchorOpening}${options.anchor.content}${anchorClosing}`, + ); + break; + case 'wrap': + s.appendRight(tNode.children[0].start, anchorOpening).appendLeft( + tNode.children[tNode.children.length - 1].end, + anchorClosing, + ); + break; + case 'append': + s.appendLeft( + tNode.children[tNode.children.length - 1].end, + `${anchorOpening}${options.anchor.content}${anchorClosing}`, + ); + break; + case 'after': + s.appendRight( + tNode.end, + `${anchorOpening}${options.anchor.content}${anchorClosing}`, + ); + break; + } + } } } }, @@ -91,5 +106,4 @@ export function autoSlug(): PreprocessorGroup { }; }, }; - return preprocess; } diff --git a/packages/preprocessors/auto-slug/src/auto-slug.types.ts b/packages/preprocessors/auto-slug/src/auto-slug.types.ts new file mode 100644 index 00000000..027cdab0 --- /dev/null +++ b/packages/preprocessors/auto-slug/src/auto-slug.types.ts @@ -0,0 +1,75 @@ +import type { BaseNode } from 'estree'; +import type BananaSlug from 'github-slugger'; + +/** @internal */ +type PartialAutoSlugOptions = Omit & { + anchor?: Partial; +}; + +/** + * @public + */ +export type AutoSlugInput = + | PartialAutoSlugOptions + | ((defaultOptions: AutoSlugOptions) => PartialAutoSlugOptions); + +/** + * @public + */ +export interface AutoSlugAnchorOptions { + enabled: boolean; + /** + * where to create the anchor tag + * + * - 'prepend' — inject link before the target tag text + * - 'append' — inject link after the target tag text + * - 'wrap' — wrap the whole target tag text with the link + * - 'before' — insert link before the target tag + * - 'after' — insert link after the target tag + */ + position: 'prepend' | 'append' | 'wrap' | 'before' | 'after'; + /** default to '#', ignored when behavior is `wrap` */ + content: string; + /** defaults to { 'aria-hidden': 'true', 'tab-index': '-1' } */ + properties: Record; + href: (slug: string) => string; +} + +/** + * @public + */ +export interface SlugResolverInput { + /** generated slug, by default slug will resolve to this */ + generated: string; + /** text extracted from original node */ + nodeText: string; + /** the {@link https://github.com/Flet/github-slugger | BananaSlug} object */ + slugger: BananaSlug; +} + +/** + * @public + */ +export interface AutoSlugOptions { + /** target tag, default to all heading tags */ + tags: string[]; + /** default to `id` */ + attributeName: string; + /** instructions to add the anchor tag */ + anchor: AutoSlugAnchorOptions; + /** slug resolver */ + slug: (SlugResolverInput) => string; +} + +/** + * @internal + */ +export interface Node extends BaseNode { + name: string; + start: number; + end: number; + attributes: Array<{ name: string; type: string }>; + children?: Array; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any; +} diff --git a/packages/preprocessors/auto-slug/src/auto-slug.utils.ts b/packages/preprocessors/auto-slug/src/auto-slug.utils.ts new file mode 100644 index 00000000..cda02661 --- /dev/null +++ b/packages/preprocessors/auto-slug/src/auto-slug.utils.ts @@ -0,0 +1,34 @@ +import { walk } from 'svelte/compiler'; + +import type { Node } from './auto-slug.types'; + +/** + * @internal + */ +export function hasIdAttribute(node: Node, attributeName: string): boolean { + return node.attributes.some((attr) => attr.name === attributeName && attr.type === 'Attribute'); +} + +/** + * @internal + */ +export function getTextContent(node: Node) { + if (node.type === 'Text') return node.data; + if (!node.children || node.children.length === 0) return ''; + let text = ''; + for (const child of Array.from(node.children)) { + walk(child, { + enter(childNode) { + text += getTextContent(childNode as Node); + }, + }); + } + return text; +} + +/** + * @internal + */ +export function isMustacheNode(node: Node) { + return node.children.some((child) => child.type === 'MustacheTag'); +} diff --git a/packages/preprocessors/auto-slug/src/index.ts b/packages/preprocessors/auto-slug/src/index.ts index f242c6e6..c1b42453 100644 --- a/packages/preprocessors/auto-slug/src/index.ts +++ b/packages/preprocessors/auto-slug/src/index.ts @@ -5,7 +5,10 @@ * * @packageDocumentation */ - import { autoSlug } from './auto-slug'; -export = autoSlug; +export * from './auto-slug'; +export * from './auto-slug.constants'; +export * from './auto-slug.types'; + +export default autoSlug;