diff --git a/module/src/runtime/composables/useBreadcrumbItems.ts b/module/src/runtime/nuxt/composables/useBreadcrumbItems.ts similarity index 79% rename from module/src/runtime/composables/useBreadcrumbItems.ts rename to module/src/runtime/nuxt/composables/useBreadcrumbItems.ts index 0e6d57ad..4a0bca46 100644 --- a/module/src/runtime/composables/useBreadcrumbItems.ts +++ b/module/src/runtime/nuxt/composables/useBreadcrumbItems.ts @@ -1,7 +1,8 @@ -import { hasTrailingSlash, parseURL, stringifyParsedURL, withTrailingSlash, withoutTrailingSlash } from 'ufo' +import { withoutTrailingSlash } from 'ufo' import type { RouteMeta } from 'vue-router' import type { MaybeRefOrGetter } from 'vue' import type { BreadcrumbLink } from '@nuxt/ui/dist/runtime/types' +import { pathBreadcrumbSegments } from '../../pure/breadcrumbs' import { computed, defineBreadcrumb, @@ -83,7 +84,6 @@ export function useBreadcrumbItems(options: BreadcrumbProps = {}) { } const current = withoutQuery(withoutTrailingSlash(toValue(options.path || useRoute().path) || rootNode)) return pathBreadcrumbSegments(current, rootNode) - .reverse() .map(path => ({ to: path, }) as BreadcrumbItemProps) @@ -143,26 +143,3 @@ export function useBreadcrumbItems(options: BreadcrumbProps = {}) { } return items } - -export function pathBreadcrumbSegments(path: string, rootNode: string = '/') { - const startNode = parseURL(path) - const appendsTrailingSlash = hasTrailingSlash(startNode.pathname) - - const stepNode = (node: ReturnType, nodes: string[] = []) => { - const fullPath = stringifyParsedURL(node) - // the pathname will always be without the trailing slash - const currentPathName = node.pathname || '/' - // when we hit the root the path will be an empty string; we swap it out for a slash - nodes.push(fullPath || '/') - if (currentPathName !== rootNode) { - // strip the last path segment (/my/cool/path -> /my/cool) - node.pathname = currentPathName.substring(0, currentPathName.lastIndexOf('/')) - // if the input was provided with a trailing slash we need to honour that - if (appendsTrailingSlash) - node.pathname = withTrailingSlash(node.pathname.substring(0, node.pathname.lastIndexOf('/'))) - stepNode(node, nodes) - } - return nodes - } - return stepNode(startNode) -} diff --git a/module/src/runtime/pure/breadcrumbs.ts b/module/src/runtime/pure/breadcrumbs.ts new file mode 100644 index 00000000..c27cc44d --- /dev/null +++ b/module/src/runtime/pure/breadcrumbs.ts @@ -0,0 +1,25 @@ +import { hasTrailingSlash, parseURL, stringifyParsedURL, withTrailingSlash } from 'ufo' + +export function pathBreadcrumbSegments(path: string, rootNode: string = '/'): string[] { + const startNode = parseURL(path) + const appendsTrailingSlash = hasTrailingSlash(startNode.pathname) + + const stepNode = (node: ReturnType, nodes: string[] = []) => { + const fullPath = stringifyParsedURL(node) + // the pathname will always be without the trailing slash + const currentPathName = node.pathname || '/' + // when we hit the root the path will be an empty string; we swap it out for a slash + nodes.push(fullPath || '/') + if (currentPathName !== rootNode && currentPathName !== '/') { + // strip the last path segment (/my/cool/path -> /my/cool) + node.pathname = currentPathName.substring(0, currentPathName.lastIndexOf('/')) + // if the input was provided with a trailing slash we need to honour that + if (appendsTrailingSlash) + node.pathname = withTrailingSlash(node.pathname.substring(0, node.pathname.lastIndexOf('/'))) + stepNode(node, nodes) + } + return nodes + } + return stepNode(startNode) + .reverse() +} diff --git a/test/unit/breadcrumbs.test.ts b/test/unit/breadcrumbs.test.ts new file mode 100644 index 00000000..c6c4cd50 --- /dev/null +++ b/test/unit/breadcrumbs.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { pathBreadcrumbSegments } from '../../module/src/runtime/pure/breadcrumbs' + +describe('breadcrumbs', () => { + it('basic', async () => { + const items = pathBreadcrumbSegments('/x/y', '/') + expect(items).toMatchInlineSnapshot(` + [ + "/", + "/x", + "/x/y", + ] + `) + }) + it('empty root node', async () => { + const items = pathBreadcrumbSegments('/x/y', '') + expect(items).toMatchInlineSnapshot(` + [ + "/", + "/x", + "/x/y", + ] + `) + }) + it('custom root node', async () => { + const items = pathBreadcrumbSegments('/x/y', '/x') + expect(items).toMatchInlineSnapshot(` + [ + "/x", + "/x/y", + ] + `) + }) + it('broken root node', async () => { + const items = pathBreadcrumbSegments('/x/y', '/foo') + expect(items).toMatchInlineSnapshot(` + [ + "/", + "/x", + "/x/y", + ] + `) + }) +})