GitHunt
PR

proxima812/Astro-ts-mastery

๐Ÿš€ utils and libs for Astro.js projects.

import { getCollection, type CollectionEntry } from "astro:content";
import GithubSlugger from "github-slugger";
import slugify from "slugify";
import { marked } from "marked";

/* --------------------------------- slug --------------------------------- */

const RU_REGEX = /[ะฐ-ัั‘]/i;
const slugger = new GithubSlugger();

export const customSlugify = (value = ""): string => {
if (!value) return "";

if (RU_REGEX.test(value)) {
return slugify(value, {
lower: true,
strict: true,
locale: "ru",
trim: true,
});
}

return slugger.slug(value);
};

/* ------------------------------ collections ------------------------------ */

export const getSinglePage = async (
collection: string
): Promise<CollectionEntry[]> => {
const pages = await getCollection(collection);

return pages.filter(
(p) => !p.data?.draft && !p.id.startsWith("-")
);
};

export const getTaxonomy = async (
collection: string,
field: string
): Promise<string[]> => {
const pages = await getSinglePage(collection);

return [
...new Set(
pages
.flatMap((p) => p.data?.[field] ?? [])
.filter(Boolean)
.map(customSlugify)
),
];
};

/* -------------------------------- markdown ------------------------------- */

export const markdownify = (content = ""): string => {
if (!content) return "";
return marked.parseInline(content);
};

/* -------------------------------- humanize ------------------------------- */

export const humanize = (value = ""): string => {
if (!value) return "";

const cleaned = value
.trim()
.replace(/[_\s]+/g, " ");

return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
};

/* -------------------------------- plainify ------------------------------- */

export const plainify = (html = ""): string => {
if (!html) return "";

const text = html
.replace(/</?[^>]+>/g, "")
.replace(/\s+/g, " ")
.trim();

return decodeHTMLEntities(text);
};

const decodeHTMLEntities = (str: string): string =>
str.replace(
/&(nbsp|amp|lt|gt|quot|#39);/g,
(m) =>
({
"ย ": " ",
"&": "&",
"<": "<",
">": ">",
""": '"',
"'": "'",
} as Record<string, string>)[m] ?? m
);

/* ------------------------------ reading time ------------------------------ */

const WORDS_PER_MINUTE = 275;

const readingTime = (content = "", locale: "en" | "ru" = "en"): string => {
if (!content) return "0 min";

const words = content.match(/\p{L}+/gu)?.length ?? 0;
const images = (content.match(/<img\b/gi) ?? []).length;

const imageSeconds = images
? images * 3 + Math.min(images, 10) * 1
: 0;

const minutes = Math.max(
1,
Math.ceil(words / WORDS_PER_MINUTE + imageSeconds / 60)
);

if (locale === "ru") {
return ${minutes} ะผะธะฝ ั‡ั‚ะตะฝะธั;
}

return ${minutes} min read;
};

export default readingTime;

Contributors

Created November 10, 2023
Updated February 10, 2026