astro-awesomeness 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/index.d.ts +1 -1
- package/dist/{index-BspVzulR.d.ts → index-BHnvZ16j.d.ts} +4 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.js +2 -2
- package/dist/{lib-V-jasC53.js → lib-C9WpNvm3.js} +30 -2
- package/dist/lib-C9WpNvm3.js.map +1 -0
- package/package.json +1 -1
- package/src/lib/index.ts +1 -1
- package/src/lib/post-url.test.ts +51 -1
- package/src/lib/post-url.ts +57 -1
- package/dist/lib-V-jasC53.js.map +0 -1
|
@@ -4,7 +4,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
4
4
|
//#region src/components/ui/button-variants.d.ts
|
|
5
5
|
declare const buttonVariants: (props?: ({
|
|
6
6
|
size?: "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | undefined;
|
|
7
|
-
variant?: "default" | "
|
|
7
|
+
variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
|
|
8
8
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
9
9
|
//#endregion
|
|
10
10
|
//#region src/components/ui/button.d.ts
|
|
@@ -30,6 +30,7 @@ type PostUrlBuilder = {
|
|
|
30
30
|
postUrl: (post: PostLike) => string;
|
|
31
31
|
};
|
|
32
32
|
declare const createPostUrl: (categorySlugMap: Record<string, string>, defaultCategory: string) => PostUrlBuilder;
|
|
33
|
+
declare const categoryLabel: (name: string) => string;
|
|
33
34
|
//#endregion
|
|
34
35
|
//#region src/lib/reading-time.d.ts
|
|
35
36
|
declare const readingTime: (input: string) => {
|
|
@@ -47,8 +48,8 @@ declare const slugify: (input: string) => string;
|
|
|
47
48
|
declare const DEFAULT_TAG_BASE = "/tag";
|
|
48
49
|
declare const tagUrl: (tag: string, base?: string) => string;
|
|
49
50
|
declare namespace index_d_exports {
|
|
50
|
-
export { DEFAULT_TAG_BASE, PostLike, PostParams, PostUrlBuilder, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
|
|
51
|
+
export { DEFAULT_TAG_BASE, PostLike, PostParams, PostUrlBuilder, categoryLabel, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
|
|
51
52
|
}
|
|
52
53
|
//#endregion
|
|
53
|
-
export { requireEnv as a, PostParams as c,
|
|
54
|
-
//# sourceMappingURL=index-
|
|
54
|
+
export { requireEnv as a, PostParams as c, createPostUrl as d, getRelatedPosts as f, slugify as i, PostUrlBuilder as l, cn as m, DEFAULT_TAG_BASE as n, readingTime as o, formatDate as p, tagUrl as r, PostLike as s, index_d_exports as t, categoryLabel as u };
|
|
55
|
+
//# sourceMappingURL=index-BHnvZ16j.d.ts.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Author, Post, Tag, t as index_d_exports } from "./content/index.js";
|
|
2
|
-
import { t as index_d_exports$1 } from "./index-
|
|
2
|
+
import { t as index_d_exports$1 } from "./index-BHnvZ16j.js";
|
|
3
3
|
export { type Author, type Post, type Tag, index_d_exports as content, index_d_exports$1 as lib };
|
package/dist/index.js
CHANGED
package/dist/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as requireEnv, c as PostParams, d as
|
|
2
|
-
export { DEFAULT_TAG_BASE, type PostLike, type PostParams, type PostUrlBuilder, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
|
|
1
|
+
import { a as requireEnv, c as PostParams, d as createPostUrl, f as getRelatedPosts, i as slugify, l as PostUrlBuilder, m as cn, n as DEFAULT_TAG_BASE, o as readingTime, p as formatDate, r as tagUrl, s as PostLike, u as categoryLabel } from "../index-BHnvZ16j.js";
|
|
2
|
+
export { DEFAULT_TAG_BASE, type PostLike, type PostParams, type PostUrlBuilder, categoryLabel, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
|
package/dist/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { t as cn } from "../cn-DmzdyV07.js";
|
|
2
|
-
import { a as readingTime, c as
|
|
2
|
+
import { a as readingTime, c as slugify, i as requireEnv, l as getRelatedPosts, n as DEFAULT_TAG_BASE, o as categoryLabel, r as tagUrl, s as createPostUrl, u as formatDate } from "../lib-C9WpNvm3.js";
|
|
3
3
|
|
|
4
|
-
export { DEFAULT_TAG_BASE, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
|
|
4
|
+
export { DEFAULT_TAG_BASE, categoryLabel, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
|
|
@@ -76,6 +76,33 @@ const createPostUrl = (categorySlugMap, defaultCategory) => {
|
|
|
76
76
|
postUrl
|
|
77
77
|
};
|
|
78
78
|
};
|
|
79
|
+
const NAMED_ENTITIES = {
|
|
80
|
+
amp: "&",
|
|
81
|
+
apos: "'",
|
|
82
|
+
gt: ">",
|
|
83
|
+
hellip: "…",
|
|
84
|
+
ldquo: "“",
|
|
85
|
+
lsquo: "‘",
|
|
86
|
+
lt: "<",
|
|
87
|
+
nbsp: "\xA0",
|
|
88
|
+
ndash: "–",
|
|
89
|
+
quot: "\"",
|
|
90
|
+
rdquo: "”",
|
|
91
|
+
rsquo: "’"
|
|
92
|
+
};
|
|
93
|
+
const MAX_CODE_POINT = 1114111;
|
|
94
|
+
const EM_DASH = 8212;
|
|
95
|
+
const HEX_RADIX = 16;
|
|
96
|
+
const ENTITY_PATTERN = /* @__PURE__ */ new RegExp("&(?:#(?<decimal>\\d+)|#x(?<hex>[\\dA-F]+)|(?<named>[A-Z]+));", "giv");
|
|
97
|
+
const fromCodePoint = (codePoint, entity) => Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH ? String.fromCodePoint(codePoint) : entity;
|
|
98
|
+
const decodeEntity = (entity, decimal, hex, named) => {
|
|
99
|
+
if (decimal !== void 0) return fromCodePoint(Number(decimal), entity);
|
|
100
|
+
if (hex !== void 0) return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);
|
|
101
|
+
if (named === void 0) return entity;
|
|
102
|
+
const key = named.toLowerCase();
|
|
103
|
+
return Object.hasOwn(NAMED_ENTITIES, key) ? NAMED_ENTITIES[key] ?? entity : entity;
|
|
104
|
+
};
|
|
105
|
+
const categoryLabel = (name) => name.replaceAll(ENTITY_PATTERN, decodeEntity);
|
|
79
106
|
|
|
80
107
|
//#endregion
|
|
81
108
|
//#region src/lib/reading-time.ts
|
|
@@ -106,6 +133,7 @@ const tagUrl = (tag, base = DEFAULT_TAG_BASE) => `${base.replace(/* @__PURE__ */
|
|
|
106
133
|
//#region src/lib/index.ts
|
|
107
134
|
var lib_exports = /* @__PURE__ */ __exportAll({
|
|
108
135
|
DEFAULT_TAG_BASE: () => DEFAULT_TAG_BASE,
|
|
136
|
+
categoryLabel: () => categoryLabel,
|
|
109
137
|
cn: () => cn,
|
|
110
138
|
createPostUrl: () => createPostUrl,
|
|
111
139
|
formatDate: () => formatDate,
|
|
@@ -117,5 +145,5 @@ var lib_exports = /* @__PURE__ */ __exportAll({
|
|
|
117
145
|
});
|
|
118
146
|
|
|
119
147
|
//#endregion
|
|
120
|
-
export { readingTime as a,
|
|
121
|
-
//# sourceMappingURL=lib-
|
|
148
|
+
export { readingTime as a, slugify as c, requireEnv as i, getRelatedPosts as l, DEFAULT_TAG_BASE as n, categoryLabel as o, tagUrl as r, createPostUrl as s, lib_exports as t, formatDate as u };
|
|
149
|
+
//# sourceMappingURL=lib-C9WpNvm3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib-C9WpNvm3.js","names":[],"sources":["../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/slugify.ts","../src/lib/post-url.ts","../src/lib/reading-time.ts","../src/lib/require-env.ts","../src/lib/tag-url.ts","../src/lib/index.ts"],"sourcesContent":["const defaultOptions: Intl.DateTimeFormatOptions = {\n day: \"numeric\",\n month: \"long\",\n timeZone: \"UTC\",\n year: \"numeric\",\n};\n\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nconst getFormatter = (key: string, locale: string, options: Intl.DateTimeFormatOptions) => {\n let formatter = formatterCache.get(key);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\n return formatter;\n};\n\nconst formatDate = (date: Date, locale: string = \"en-US\", options?: Intl.DateTimeFormatOptions) => {\n // Default-option calls (every blog passes only (date, locale)) key on locale\n // alone, skipping the JSON.stringify the custom-option path needs.\n if (!options) {\n return getFormatter(locale, locale, defaultOptions).format(date);\n }\n const merged = { ...defaultOptions, ...options };\n return getFormatter(`${locale}:${JSON.stringify(merged)}`, locale, merged).format(date);\n};\n\nexport { formatDate };\n","type HasTags = { data: { tags: Array<string> }; id: string };\n\nconst getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) => {\n const currentTagSet = new Set(current.data.tags);\n const scored: Array<{ post: T; score: number }> = [];\n\n for (const post of all) {\n if (post.id === current.id) {\n continue;\n }\n let score = 0;\n for (const tag of post.data.tags) {\n if (currentTagSet.has(tag)) {\n score++;\n }\n }\n scored.push({ post, score });\n }\n\n return scored\n .toSorted((x, y) => y.score - x.score)\n .slice(0, n)\n .map(({ post }) => post);\n};\n\nexport { getRelatedPosts };\n","const slugify = (input: string) =>\n input\n .normalize(\"NFD\")\n .replaceAll(/[̀-ͯ]/gv, \"\")\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s\\-]/gv, \"\")\n .trim()\n .replaceAll(/[\\s\\-]+/gv, \"-\")\n .replaceAll(/^-+|-+$/gv, \"\");\n\nexport { slugify };\n","import { slugify } from \"./slugify\";\n\ntype PostLike = {\n data: {\n categories?: Array<string>;\n seo?: { canonical_url?: string };\n };\n id: string;\n};\n\ntype PostParams = { category: string; slug: string };\n\ntype PostUrlBuilder = {\n postParams: (post: PostLike) => PostParams;\n postUrl: (post: PostLike) => string;\n};\n\n// Per-blog URL builder. Each blog supplies its own pt-BR -> slug map\n// (`category-slugs.json`) and the catch-all category used when the post has\n// no categories. The canonical_url short-circuit lets WordPress migrations\n// keep their original URL pair while everything else falls back to a slugified\n// derived category + Astro entry id.\nconst createPostUrl = (\n categorySlugMap: Record<string, string>,\n defaultCategory: string,\n): PostUrlBuilder => {\n const postParams = (post: PostLike): PostParams => {\n const canonical = post.data.seo?.canonical_url;\n if (canonical !== undefined && canonical !== \"\") {\n try {\n const segments = new URL(canonical).pathname.split(\"/\").filter(Boolean);\n const category = segments.at(-2);\n const slug = segments.at(-1);\n if (category !== undefined && slug !== undefined) {\n return { category, slug };\n }\n } catch {\n // Malformed canonical_url; fall through to category-derived logic.\n }\n }\n const first = post.data.categories?.at(0);\n const category =\n first !== undefined && first !== \"\"\n ? (categorySlugMap[first] ?? slugify(first))\n : defaultCategory;\n return { category, slug: post.id };\n };\n\n const postUrl = (post: PostLike): string => {\n const { category, slug } = postParams(post);\n return `/${category}/${slug}/`;\n };\n\n return { postParams, postUrl };\n};\n\n// Escapes rather than literal glyphs so nothing invisible or ambiguous sits in\n// source. `mdash` is deliberately absent: U+2014 is banned across these blogs, so\n// an encoded em-dash stays encoded rather than being decoded into a banned\n// character.\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: \"&\",\n apos: \"'\",\n gt: \">\",\n hellip: \"\\u2026\",\n ldquo: \"\\u201c\",\n lsquo: \"\\u2018\",\n lt: \"<\",\n nbsp: \"\\u00a0\",\n ndash: \"\\u2013\",\n quot: '\"',\n rdquo: \"\\u201d\",\n rsquo: \"\\u2019\",\n};\n\nconst MAX_CODE_POINT = 1_114_111;\nconst EM_DASH = 8212;\nconst HEX_RADIX = 16;\nconst ENTITY_PATTERN = /&(?:#(?<decimal>\\d+)|#x(?<hex>[\\dA-F]+)|(?<named>[A-Z]+));/giv;\n\n// `String.fromCodePoint` throws on anything past the Unicode range, which would\n// take a whole static build down over one malformed entity. U+2014 is refused for\n// the same reason `mdash` is absent above: a numeric escape must not become the\n// way the banned glyph reaches a page.\nconst fromCodePoint = (codePoint: number, entity: string): string =>\n Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH\n ? String.fromCodePoint(codePoint)\n : entity;\n\nconst decodeEntity = (entity: string, decimal?: string, hex?: string, named?: string): string => {\n if (decimal !== undefined) {\n return fromCodePoint(Number(decimal), entity);\n }\n if (hex !== undefined) {\n return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);\n }\n if (named === undefined) {\n return entity;\n }\n // Bare indexing would resolve `&constructor;` up the prototype chain and\n // render `Function.prototype.toString` output into the page.\n const key = named.toLowerCase();\n return Object.hasOwn(NAMED_ENTITIES, key) ? (NAMED_ENTITIES[key] ?? entity) : entity;\n};\n\n// WordPress migrations stored category names HTML-encoded and the content API\n// returns `post.data.categories` the same way, so both `category-slugs.json` keys\n// and post frontmatter carry raw entities. Display-only on purpose: `createPostUrl`\n// resolves a slug by the raw, still-encoded name, so a decoded name must never\n// reach that lookup or every affected post URL moves.\nconst categoryLabel = (name: string): string => name.replaceAll(ENTITY_PATTERN, decodeEntity);\n\nexport type { PostLike, PostParams, PostUrlBuilder };\nexport { categoryLabel, createPostUrl };\n","const WORDS_PER_MINUTE = 200;\n\nconst readingTime = (input: string) => {\n const stripped = input\n .replaceAll(/<[^>]*>/gv, \"\")\n .replaceAll(/[#*_`>\\-]/gv, \" \")\n .replaceAll(/\\s+/gv, \" \")\n .trim();\n const words = stripped.length === 0 ? 0 : stripped.split(\" \").length;\n const minutes = Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));\n return { minutes, words };\n};\n\nexport { readingTime };\n","// An unset shell variable reaches a CI build as \"\" rather than undefined, so an\n// empty value has to count as missing or the failure surfaces much later, inside\n// the content loader's HTTP call.\nconst requireEnv = (name: string): string => {\n const value = process.env[name];\n if (value === undefined || value === \"\") {\n throw new Error(`Missing required env var: ${name}`);\n }\n return value;\n};\n\nexport { requireEnv };\n","import { slugify } from \"./slugify\";\n\nconst DEFAULT_TAG_BASE = \"/tag\";\n\n// The trailing slash is not cosmetic. @astrojs/sitemap emits directory-style\n// URLs, so a chip pointing at /tag/foo names a URL that is absent from the\n// sitemap and only reachable through a redirect.\nconst tagUrl = (tag: string, base: string = DEFAULT_TAG_BASE): string =>\n `${base.replace(/\\/+$/v, \"\")}/${slugify(tag)}/`;\n\nexport { DEFAULT_TAG_BASE, tagUrl };\n","export { cn } from \"./cn\";\nexport { formatDate } from \"./format-date\";\nexport { getRelatedPosts } from \"./get-related-posts\";\nexport type { PostLike, PostParams, PostUrlBuilder } from \"./post-url\";\nexport { categoryLabel, createPostUrl } from \"./post-url\";\nexport { readingTime } from \"./reading-time\";\nexport { requireEnv } from \"./require-env\";\nexport { slugify } from \"./slugify\";\nexport { DEFAULT_TAG_BASE, tagUrl } from \"./tag-url\";\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/lib/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { cn } from "./cn";
|
|
|
2
2
|
export { formatDate } from "./format-date";
|
|
3
3
|
export { getRelatedPosts } from "./get-related-posts";
|
|
4
4
|
export type { PostLike, PostParams, PostUrlBuilder } from "./post-url";
|
|
5
|
-
export { createPostUrl } from "./post-url";
|
|
5
|
+
export { categoryLabel, createPostUrl } from "./post-url";
|
|
6
6
|
export { readingTime } from "./reading-time";
|
|
7
7
|
export { requireEnv } from "./require-env";
|
|
8
8
|
export { slugify } from "./slugify";
|
package/src/lib/post-url.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { createPostUrl } from "./post-url";
|
|
3
|
+
import { categoryLabel, createPostUrl } from "./post-url";
|
|
4
4
|
|
|
5
5
|
const map = { "Aluguel de Espaços": "aluguel-de-espacos", Arquitetura: "arquitetura" };
|
|
6
6
|
const DEFAULT = "sem-categoria";
|
|
@@ -48,3 +48,53 @@ describe("createPostUrl", () => {
|
|
|
48
48
|
expect(postParams(post)).toEqual({ category: "arquitetura", slug: "x" });
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
describe("categoryLabel", () => {
|
|
53
|
+
it("decodes the named entities the WordPress migrations emit", () => {
|
|
54
|
+
expect(categoryLabel("Arte & Design")).toBe("Arte & Design");
|
|
55
|
+
expect(categoryLabel("Casas …")).toBe("Casas \u2026");
|
|
56
|
+
expect(categoryLabel("<tag> "x" ’")).toBe('<tag> "x" \u2019');
|
|
57
|
+
expect(categoryLabel("Ano – Fim")).toBe("Ano \u2013 Fim");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("matches named entities case-insensitively", () => {
|
|
61
|
+
expect(categoryLabel("A & B")).toBe("A & B");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("decodes decimal and hexadecimal numeric entities", () => {
|
|
65
|
+
expect(categoryLabel("A")).toBe("A");
|
|
66
|
+
expect(categoryLabel("–")).toBe("\u2013");
|
|
67
|
+
expect(categoryLabel("–")).toBe("\u2013");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("leaves an encoded em-dash encoded in both numeric forms", () => {
|
|
71
|
+
expect(categoryLabel("A — B")).toBe("A — B");
|
|
72
|
+
expect(categoryLabel("A — B")).toBe("A — B");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("leaves out-of-range code points alone instead of throwing", () => {
|
|
76
|
+
expect(() => categoryLabel("�")).not.toThrow();
|
|
77
|
+
expect(categoryLabel("�")).toBe("�");
|
|
78
|
+
expect(categoryLabel("�")).toBe("�");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("does not resolve inherited Object.prototype keys", () => {
|
|
82
|
+
expect(categoryLabel("&constructor;")).toBe("&constructor;");
|
|
83
|
+
expect(categoryLabel("&toString;")).toBe("&toString;");
|
|
84
|
+
expect(categoryLabel("&hasOwnProperty;")).toBe("&hasOwnProperty;");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("passes unknown entities and plain text through unchanged", () => {
|
|
88
|
+
expect(categoryLabel("&bogus;")).toBe("&bogus;");
|
|
89
|
+
expect(categoryLabel("Arquitetura")).toBe("Arquitetura");
|
|
90
|
+
expect(categoryLabel("")).toBe("");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("stays display-only: decoding a name must not change the URL it resolves to", () => {
|
|
94
|
+
const encodedMap = { "Arte & Design": "arte-e-design" };
|
|
95
|
+
const { postUrl } = createPostUrl(encodedMap, DEFAULT);
|
|
96
|
+
const post = { data: { categories: ["Arte & Design"] }, id: "casa" };
|
|
97
|
+
expect(postUrl(post)).toBe("/arte-e-design/casa/");
|
|
98
|
+
expect(Object.hasOwn(encodedMap, categoryLabel("Arte & Design"))).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
package/src/lib/post-url.ts
CHANGED
|
@@ -54,5 +54,61 @@ const createPostUrl = (
|
|
|
54
54
|
return { postParams, postUrl };
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
+
// Escapes rather than literal glyphs so nothing invisible or ambiguous sits in
|
|
58
|
+
// source. `mdash` is deliberately absent: U+2014 is banned across these blogs, so
|
|
59
|
+
// an encoded em-dash stays encoded rather than being decoded into a banned
|
|
60
|
+
// character.
|
|
61
|
+
const NAMED_ENTITIES: Record<string, string> = {
|
|
62
|
+
amp: "&",
|
|
63
|
+
apos: "'",
|
|
64
|
+
gt: ">",
|
|
65
|
+
hellip: "\u2026",
|
|
66
|
+
ldquo: "\u201c",
|
|
67
|
+
lsquo: "\u2018",
|
|
68
|
+
lt: "<",
|
|
69
|
+
nbsp: "\u00a0",
|
|
70
|
+
ndash: "\u2013",
|
|
71
|
+
quot: '"',
|
|
72
|
+
rdquo: "\u201d",
|
|
73
|
+
rsquo: "\u2019",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const MAX_CODE_POINT = 1_114_111;
|
|
77
|
+
const EM_DASH = 8212;
|
|
78
|
+
const HEX_RADIX = 16;
|
|
79
|
+
const ENTITY_PATTERN = /&(?:#(?<decimal>\d+)|#x(?<hex>[\dA-F]+)|(?<named>[A-Z]+));/giv;
|
|
80
|
+
|
|
81
|
+
// `String.fromCodePoint` throws on anything past the Unicode range, which would
|
|
82
|
+
// take a whole static build down over one malformed entity. U+2014 is refused for
|
|
83
|
+
// the same reason `mdash` is absent above: a numeric escape must not become the
|
|
84
|
+
// way the banned glyph reaches a page.
|
|
85
|
+
const fromCodePoint = (codePoint: number, entity: string): string =>
|
|
86
|
+
Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH
|
|
87
|
+
? String.fromCodePoint(codePoint)
|
|
88
|
+
: entity;
|
|
89
|
+
|
|
90
|
+
const decodeEntity = (entity: string, decimal?: string, hex?: string, named?: string): string => {
|
|
91
|
+
if (decimal !== undefined) {
|
|
92
|
+
return fromCodePoint(Number(decimal), entity);
|
|
93
|
+
}
|
|
94
|
+
if (hex !== undefined) {
|
|
95
|
+
return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);
|
|
96
|
+
}
|
|
97
|
+
if (named === undefined) {
|
|
98
|
+
return entity;
|
|
99
|
+
}
|
|
100
|
+
// Bare indexing would resolve `&constructor;` up the prototype chain and
|
|
101
|
+
// render `Function.prototype.toString` output into the page.
|
|
102
|
+
const key = named.toLowerCase();
|
|
103
|
+
return Object.hasOwn(NAMED_ENTITIES, key) ? (NAMED_ENTITIES[key] ?? entity) : entity;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// WordPress migrations stored category names HTML-encoded and the content API
|
|
107
|
+
// returns `post.data.categories` the same way, so both `category-slugs.json` keys
|
|
108
|
+
// and post frontmatter carry raw entities. Display-only on purpose: `createPostUrl`
|
|
109
|
+
// resolves a slug by the raw, still-encoded name, so a decoded name must never
|
|
110
|
+
// reach that lookup or every affected post URL moves.
|
|
111
|
+
const categoryLabel = (name: string): string => name.replaceAll(ENTITY_PATTERN, decodeEntity);
|
|
112
|
+
|
|
57
113
|
export type { PostLike, PostParams, PostUrlBuilder };
|
|
58
|
-
export { createPostUrl };
|
|
114
|
+
export { categoryLabel, createPostUrl };
|
package/dist/lib-V-jasC53.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lib-V-jasC53.js","names":[],"sources":["../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/slugify.ts","../src/lib/post-url.ts","../src/lib/reading-time.ts","../src/lib/require-env.ts","../src/lib/tag-url.ts","../src/lib/index.ts"],"sourcesContent":["const defaultOptions: Intl.DateTimeFormatOptions = {\n day: \"numeric\",\n month: \"long\",\n timeZone: \"UTC\",\n year: \"numeric\",\n};\n\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nconst getFormatter = (key: string, locale: string, options: Intl.DateTimeFormatOptions) => {\n let formatter = formatterCache.get(key);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\n return formatter;\n};\n\nconst formatDate = (date: Date, locale: string = \"en-US\", options?: Intl.DateTimeFormatOptions) => {\n // Default-option calls (every blog passes only (date, locale)) key on locale\n // alone, skipping the JSON.stringify the custom-option path needs.\n if (!options) {\n return getFormatter(locale, locale, defaultOptions).format(date);\n }\n const merged = { ...defaultOptions, ...options };\n return getFormatter(`${locale}:${JSON.stringify(merged)}`, locale, merged).format(date);\n};\n\nexport { formatDate };\n","type HasTags = { data: { tags: Array<string> }; id: string };\n\nconst getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) => {\n const currentTagSet = new Set(current.data.tags);\n const scored: Array<{ post: T; score: number }> = [];\n\n for (const post of all) {\n if (post.id === current.id) {\n continue;\n }\n let score = 0;\n for (const tag of post.data.tags) {\n if (currentTagSet.has(tag)) {\n score++;\n }\n }\n scored.push({ post, score });\n }\n\n return scored\n .toSorted((x, y) => y.score - x.score)\n .slice(0, n)\n .map(({ post }) => post);\n};\n\nexport { getRelatedPosts };\n","const slugify = (input: string) =>\n input\n .normalize(\"NFD\")\n .replaceAll(/[̀-ͯ]/gv, \"\")\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s\\-]/gv, \"\")\n .trim()\n .replaceAll(/[\\s\\-]+/gv, \"-\")\n .replaceAll(/^-+|-+$/gv, \"\");\n\nexport { slugify };\n","import { slugify } from \"./slugify\";\n\ntype PostLike = {\n data: {\n categories?: Array<string>;\n seo?: { canonical_url?: string };\n };\n id: string;\n};\n\ntype PostParams = { category: string; slug: string };\n\ntype PostUrlBuilder = {\n postParams: (post: PostLike) => PostParams;\n postUrl: (post: PostLike) => string;\n};\n\n// Per-blog URL builder. Each blog supplies its own pt-BR -> slug map\n// (`category-slugs.json`) and the catch-all category used when the post has\n// no categories. The canonical_url short-circuit lets WordPress migrations\n// keep their original URL pair while everything else falls back to a slugified\n// derived category + Astro entry id.\nconst createPostUrl = (\n categorySlugMap: Record<string, string>,\n defaultCategory: string,\n): PostUrlBuilder => {\n const postParams = (post: PostLike): PostParams => {\n const canonical = post.data.seo?.canonical_url;\n if (canonical !== undefined && canonical !== \"\") {\n try {\n const segments = new URL(canonical).pathname.split(\"/\").filter(Boolean);\n const category = segments.at(-2);\n const slug = segments.at(-1);\n if (category !== undefined && slug !== undefined) {\n return { category, slug };\n }\n } catch {\n // Malformed canonical_url; fall through to category-derived logic.\n }\n }\n const first = post.data.categories?.at(0);\n const category =\n first !== undefined && first !== \"\"\n ? (categorySlugMap[first] ?? slugify(first))\n : defaultCategory;\n return { category, slug: post.id };\n };\n\n const postUrl = (post: PostLike): string => {\n const { category, slug } = postParams(post);\n return `/${category}/${slug}/`;\n };\n\n return { postParams, postUrl };\n};\n\nexport type { PostLike, PostParams, PostUrlBuilder };\nexport { createPostUrl };\n","const WORDS_PER_MINUTE = 200;\n\nconst readingTime = (input: string) => {\n const stripped = input\n .replaceAll(/<[^>]*>/gv, \"\")\n .replaceAll(/[#*_`>\\-]/gv, \" \")\n .replaceAll(/\\s+/gv, \" \")\n .trim();\n const words = stripped.length === 0 ? 0 : stripped.split(\" \").length;\n const minutes = Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));\n return { minutes, words };\n};\n\nexport { readingTime };\n","// An unset shell variable reaches a CI build as \"\" rather than undefined, so an\n// empty value has to count as missing or the failure surfaces much later, inside\n// the content loader's HTTP call.\nconst requireEnv = (name: string): string => {\n const value = process.env[name];\n if (value === undefined || value === \"\") {\n throw new Error(`Missing required env var: ${name}`);\n }\n return value;\n};\n\nexport { requireEnv };\n","import { slugify } from \"./slugify\";\n\nconst DEFAULT_TAG_BASE = \"/tag\";\n\n// The trailing slash is not cosmetic. @astrojs/sitemap emits directory-style\n// URLs, so a chip pointing at /tag/foo names a URL that is absent from the\n// sitemap and only reachable through a redirect.\nconst tagUrl = (tag: string, base: string = DEFAULT_TAG_BASE): string =>\n `${base.replace(/\\/+$/v, \"\")}/${slugify(tag)}/`;\n\nexport { DEFAULT_TAG_BASE, tagUrl };\n","export { cn } from \"./cn\";\nexport { formatDate } from \"./format-date\";\nexport { getRelatedPosts } from \"./get-related-posts\";\nexport type { PostLike, PostParams, PostUrlBuilder } from \"./post-url\";\nexport { createPostUrl } from \"./post-url\";\nexport { readingTime } from \"./reading-time\";\nexport { requireEnv } from \"./require-env\";\nexport { slugify } from \"./slugify\";\nexport { DEFAULT_TAG_BASE, tagUrl } from \"./tag-url\";\n"],"mappings":"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"}
|