import type {
CatalogItem,
ComparisonCategory
} from '@tool.io/tools/compare';
export const comparePairSeparator = '-vs-';
export const maximumComparisonItems = 12;
export const comparisonNameSlug = (value: string) =>
value
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.toLocaleLowerCase('en-US')
.replace(/&/g, ' and ')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
.replace(/-(\d+)-(kb|mb|gb|tb)(?=-|$)/g, '-$1$2')
.replace(/(^|-)vs(?=-|$)/g, '$1versus');
export const parseComparisonSlugs = (
value: string
): readonly string[] | null => {
const parts = value.split(comparePairSeparator);
return parts.length >= 2 &&
parts.length <= maximumComparisonItems &&
parts.every(Boolean)
? parts
: null;
};
const categoryDepth = (
category: ComparisonCategory,
categoriesById: ReadonlyMap<string, ComparisonCategory>
) => {
let depth = 0;
let current: ComparisonCategory | undefined = category;
const visited = new Set<string>();
while (current) {
if (!current.parentId || visited.has(current.id)) break;
visited.add(current.id);
current = categoriesById.get(current.parentId);
if (current) depth += 1;
}
return depth;
};
export const qualifiedComparisonSlug = (
item: CatalogItem,
categories: readonly ComparisonCategory[]
) => {
const itemSlug = comparisonNameSlug(item.name) || item.slug;
if (item.categoryIds.includes('product')) return itemSlug;
const categoriesById = new Map(
categories.map(
(category) => [category.id, category] as const
)
);
const category = categories
.filter((candidate) =>
item.categoryIds.includes(candidate.id)
)
.sort(
(first, second) =>
categoryDepth(second, categoriesById) -
categoryDepth(first, categoriesById) ||
second.id.length - first.id.length
)[0];
if (!category || itemSlug.startsWith(`${category.id}-`)) {
return itemSlug;
}
return `${category.id}-${itemSlug}`;
};
export const commonComparisonCategory = (
items: readonly CatalogItem[],
categories: readonly ComparisonCategory[]
) => {
if (items.length < 2) return null;
const categoriesById = new Map(
categories.map(
(category) => [category.id, category] as const
)
);
return (
categories
.filter(
(category) =>
category.parentId !== null &&
items.every((item) =>
item.categoryIds.includes(category.id)
)
)
.sort(
(first, second) =>
categoryDepth(second, categoriesById) -
categoryDepth(first, categoriesById) ||
second.id.length - first.id.length
)[0] ?? null
);
};
const orderedSlugs = (slugs: readonly string[]) =>
[...slugs].sort((left, right) =>
left.localeCompare(right, 'en-US')
);
export const comparisonPath = (
slugs: readonly string[],
categorySlug?: string | null
) => {
const ordered = orderedSlugs(slugs);
return `/compare/${categorySlug ? `${categorySlug}/` : ''}${ordered.join(comparePairSeparator)}`;
};
export const comparisonPairPath = (
firstSlug: string,
secondSlug: string,
categorySlug?: string | null
) => {
return comparisonPath([firstSlug, secondSlug], categorySlug);
};
export const comparisonItemsHref = (
items: readonly CatalogItem[],
categories: readonly ComparisonCategory[],
category?: ComparisonCategory | null
) => {
const requestedCategory =
category &&
items.every((item) =>
item.categoryIds.includes(category.id)
)
? category
: null;
const effectiveCategory =
requestedCategory ??
commonComparisonCategory(items, categories);
const slugs = effectiveCategory
? items.map(
(item) => comparisonNameSlug(item.name) || item.slug
)
: items.map((item) =>
qualifiedComparisonSlug(item, categories)
);
return comparisonPath(slugs, effectiveCategory?.slug ?? null);
};
export const comparisonHref = (
first: CatalogItem,
second: CatalogItem,
categories: readonly ComparisonCategory[],
category?: ComparisonCategory | null
) => {
return comparisonItemsHref(
[first, second],
categories,
category
);
};
Browse the implementation files for Cars Comparison.