@bliztek/mdx-utils 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Bliztek, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @bliztek/mdx-utils
2
+
3
+ Zero-dependency MDX content utilities for read time calculation and file discovery.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @bliztek/mdx-utils
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Pure Utilities (any JS environment)
14
+
15
+ ```ts
16
+ import {
17
+ calculateReadTime,
18
+ sortByDateDescending,
19
+ stripMdxSyntax,
20
+ } from "@bliztek/mdx-utils";
21
+
22
+ // Calculate read time from raw MDX content
23
+ const { minutes, words } = calculateReadTime(mdxContent);
24
+ // => { minutes: 5, words: 1190 }
25
+
26
+ // Custom words per minute
27
+ const { minutes } = calculateReadTime(technicalDoc, { wordsPerMinute: 150 });
28
+
29
+ // Strip MDX syntax for excerpts, search indexes, or OG descriptions
30
+ const plainText = stripMdxSyntax(mdxContent);
31
+
32
+ // Sort items by date (newest first)
33
+ const sorted = sortByDateDescending(posts, (post) => post.date);
34
+ ```
35
+
36
+ ### Node.js Utilities
37
+
38
+ ```ts
39
+ import { getContentSlugs, readMdxFile } from "@bliztek/mdx-utils/node";
40
+
41
+ // Get all MDX slugs from a directory
42
+ const slugs = await getContentSlugs("./content/blog");
43
+ // => ["my-first-post", "another-post"]
44
+
45
+ // Include .md files too
46
+ const all = await getContentSlugs("./content", {
47
+ extensions: [".mdx", ".md"],
48
+ });
49
+
50
+ // Recursively search nested directories
51
+ const nested = await getContentSlugs("./content/blog", { recursive: true });
52
+ // => ["my-first-post", "2024/year-in-review"]
53
+
54
+ // Read an MDX file
55
+ const content = await readMdxFile("./content/blog/my-first-post.mdx");
56
+ ```
57
+
58
+ ## API
59
+
60
+ ### `calculateReadTime(content, options?)`
61
+
62
+ Calculates estimated read time for MDX/markdown content. Strips export blocks, import statements, JSX tags, and markdown syntax before counting words.
63
+
64
+ Returns `{ minutes: number, words: number }` — minutes is always at least 1.
65
+
66
+ | Option | Type | Default | Description |
67
+ |--------|------|---------|-------------|
68
+ | `wordsPerMinute` | `number` | `238` | Reading speed to use for calculation |
69
+
70
+ ### `stripMdxSyntax(content)`
71
+
72
+ Strips MDX/markdown syntax from content, returning plain text. Removes export blocks, import statements, JSX tags, and markdown formatting characters. Useful for generating excerpts, search indexes, or OpenGraph descriptions from raw MDX.
73
+
74
+ ### `sortByDateDescending(items, getDate)`
75
+
76
+ Sorts items by date in descending order (newest first). Returns a new array without mutating the original. The `getDate` callback should return a date string parseable by `new Date()`.
77
+
78
+ ### `getContentSlugs(dirPath, options?)`
79
+
80
+ Reads a directory and returns slugs (filenames without extension) of matching content files. When `recursive` is true, slugs from subdirectories include their relative path (e.g., `"2024/my-post"`).
81
+
82
+ | Option | Type | Default | Description |
83
+ |--------|------|---------|-------------|
84
+ | `extensions` | `string[]` | `[".mdx"]` | File extensions to include |
85
+ | `recursive` | `boolean` | `false` | Search subdirectories |
86
+
87
+ ### `readMdxFile(filePath)`
88
+
89
+ Reads a file and returns its content as a UTF-8 string.
90
+
91
+ ## Types
92
+
93
+ ```ts
94
+ type TableOfContentsEntry = { title: string; link: string };
95
+ type TableOfContents = TableOfContentsEntry[];
96
+
97
+ interface ReadTimeOptions { wordsPerMinute?: number }
98
+ interface ReadTimeResult { minutes: number; words: number }
99
+ interface GetContentSlugsOptions { extensions?: string[]; recursive?: boolean }
100
+ ```
101
+
102
+ ## Entry Points
103
+
104
+ | Import | Environment | Contents |
105
+ |--------|------------|----------|
106
+ | `@bliztek/mdx-utils` | Any (browser, edge, Node) | `calculateReadTime`, `stripMdxSyntax`, `sortByDateDescending`, types |
107
+ | `@bliztek/mdx-utils/node` | Node.js | `getContentSlugs`, `readMdxFile` + re-exports from main |
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,25 @@
1
+ // src/index.ts
2
+ var DEFAULT_WPM = 238;
3
+ function stripMdxSyntax(content) {
4
+ return content.replace(/^export\s+const\s+\w+\s*=\s*[\s\S]*?^};?\s*$/gm, "").replace(/^import\s.*$/gm, "").replace(/```[\s\S]*?```/g, "").replace(/<[^>]+>/g, "").replace(/[#*`~\[\](){}|>]/g, "").replace(/\s+/g, " ").trim();
5
+ }
6
+ function calculateReadTime(content, options) {
7
+ const wpm = options?.wordsPerMinute ?? DEFAULT_WPM;
8
+ const text = stripMdxSyntax(content);
9
+ const words = text ? text.split(/\s+/).length : 0;
10
+ return {
11
+ minutes: Math.max(1, Math.ceil(words / wpm)),
12
+ words
13
+ };
14
+ }
15
+ function sortByDateDescending(items, getDate) {
16
+ return [...items].sort(
17
+ (a, b) => new Date(getDate(b)).getTime() - new Date(getDate(a)).getTime()
18
+ );
19
+ }
20
+
21
+ export {
22
+ stripMdxSyntax,
23
+ calculateReadTime,
24
+ sortByDateDescending
25
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ calculateReadTime: () => calculateReadTime,
24
+ sortByDateDescending: () => sortByDateDescending,
25
+ stripMdxSyntax: () => stripMdxSyntax
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+ var DEFAULT_WPM = 238;
29
+ function stripMdxSyntax(content) {
30
+ return content.replace(/^export\s+const\s+\w+\s*=\s*[\s\S]*?^};?\s*$/gm, "").replace(/^import\s.*$/gm, "").replace(/```[\s\S]*?```/g, "").replace(/<[^>]+>/g, "").replace(/[#*`~\[\](){}|>]/g, "").replace(/\s+/g, " ").trim();
31
+ }
32
+ function calculateReadTime(content, options) {
33
+ const wpm = options?.wordsPerMinute ?? DEFAULT_WPM;
34
+ const text = stripMdxSyntax(content);
35
+ const words = text ? text.split(/\s+/).length : 0;
36
+ return {
37
+ minutes: Math.max(1, Math.ceil(words / wpm)),
38
+ words
39
+ };
40
+ }
41
+ function sortByDateDescending(items, getDate) {
42
+ return [...items].sort(
43
+ (a, b) => new Date(getDate(b)).getTime() - new Date(getDate(a)).getTime()
44
+ );
45
+ }
46
+ // Annotate the CommonJS export names for ESM import in node:
47
+ 0 && (module.exports = {
48
+ calculateReadTime,
49
+ sortByDateDescending,
50
+ stripMdxSyntax
51
+ });
@@ -0,0 +1,42 @@
1
+ type TableOfContentsEntry = {
2
+ title: string;
3
+ link: string;
4
+ };
5
+ type TableOfContents = TableOfContentsEntry[];
6
+ interface ReadTimeOptions {
7
+ /** Words per minute. Defaults to 238. */
8
+ wordsPerMinute?: number;
9
+ }
10
+ interface ReadTimeResult {
11
+ /** Estimated read time in minutes (minimum 1). */
12
+ minutes: number;
13
+ /** Total word count after stripping MDX/JSX syntax. */
14
+ words: number;
15
+ }
16
+ interface GetContentSlugsOptions {
17
+ /** File extensions to include. Defaults to `[".mdx"]`. */
18
+ extensions?: string[];
19
+ /** Recursively search subdirectories. Defaults to `false`. */
20
+ recursive?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Strip MDX/markdown syntax from content, returning plain text.
25
+ * Removes export blocks, import statements, JSX tags, and markdown
26
+ * formatting characters. Useful for generating excerpts, search
27
+ * indexes, or OpenGraph descriptions from raw MDX.
28
+ */
29
+ declare function stripMdxSyntax(content: string): string;
30
+ /**
31
+ * Calculate estimated read time for MDX/markdown content.
32
+ * Strips export blocks, import statements, JSX tags, and markdown syntax
33
+ * before counting words.
34
+ */
35
+ declare function calculateReadTime(content: string, options?: ReadTimeOptions): ReadTimeResult;
36
+ /**
37
+ * Sort items by date in descending order (newest first).
38
+ * Returns a new array without mutating the original.
39
+ */
40
+ declare function sortByDateDescending<T>(items: T[], getDate: (item: T) => string): T[];
41
+
42
+ export { type GetContentSlugsOptions, type ReadTimeOptions, type ReadTimeResult, type TableOfContents, type TableOfContentsEntry, calculateReadTime, sortByDateDescending, stripMdxSyntax };
@@ -0,0 +1,42 @@
1
+ type TableOfContentsEntry = {
2
+ title: string;
3
+ link: string;
4
+ };
5
+ type TableOfContents = TableOfContentsEntry[];
6
+ interface ReadTimeOptions {
7
+ /** Words per minute. Defaults to 238. */
8
+ wordsPerMinute?: number;
9
+ }
10
+ interface ReadTimeResult {
11
+ /** Estimated read time in minutes (minimum 1). */
12
+ minutes: number;
13
+ /** Total word count after stripping MDX/JSX syntax. */
14
+ words: number;
15
+ }
16
+ interface GetContentSlugsOptions {
17
+ /** File extensions to include. Defaults to `[".mdx"]`. */
18
+ extensions?: string[];
19
+ /** Recursively search subdirectories. Defaults to `false`. */
20
+ recursive?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Strip MDX/markdown syntax from content, returning plain text.
25
+ * Removes export blocks, import statements, JSX tags, and markdown
26
+ * formatting characters. Useful for generating excerpts, search
27
+ * indexes, or OpenGraph descriptions from raw MDX.
28
+ */
29
+ declare function stripMdxSyntax(content: string): string;
30
+ /**
31
+ * Calculate estimated read time for MDX/markdown content.
32
+ * Strips export blocks, import statements, JSX tags, and markdown syntax
33
+ * before counting words.
34
+ */
35
+ declare function calculateReadTime(content: string, options?: ReadTimeOptions): ReadTimeResult;
36
+ /**
37
+ * Sort items by date in descending order (newest first).
38
+ * Returns a new array without mutating the original.
39
+ */
40
+ declare function sortByDateDescending<T>(items: T[], getDate: (item: T) => string): T[];
41
+
42
+ export { type GetContentSlugsOptions, type ReadTimeOptions, type ReadTimeResult, type TableOfContents, type TableOfContentsEntry, calculateReadTime, sortByDateDescending, stripMdxSyntax };
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import {
2
+ calculateReadTime,
3
+ sortByDateDescending,
4
+ stripMdxSyntax
5
+ } from "./chunk-2WOYQBZW.js";
6
+ export {
7
+ calculateReadTime,
8
+ sortByDateDescending,
9
+ stripMdxSyntax
10
+ };
package/dist/node.cjs ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/node.ts
31
+ var node_exports = {};
32
+ __export(node_exports, {
33
+ calculateReadTime: () => calculateReadTime,
34
+ getContentSlugs: () => getContentSlugs,
35
+ readMdxFile: () => readMdxFile,
36
+ sortByDateDescending: () => sortByDateDescending,
37
+ stripMdxSyntax: () => stripMdxSyntax
38
+ });
39
+ module.exports = __toCommonJS(node_exports);
40
+ var import_promises = require("fs/promises");
41
+ var import_path = __toESM(require("path"), 1);
42
+
43
+ // src/index.ts
44
+ var DEFAULT_WPM = 238;
45
+ function stripMdxSyntax(content) {
46
+ return content.replace(/^export\s+const\s+\w+\s*=\s*[\s\S]*?^};?\s*$/gm, "").replace(/^import\s.*$/gm, "").replace(/```[\s\S]*?```/g, "").replace(/<[^>]+>/g, "").replace(/[#*`~\[\](){}|>]/g, "").replace(/\s+/g, " ").trim();
47
+ }
48
+ function calculateReadTime(content, options) {
49
+ const wpm = options?.wordsPerMinute ?? DEFAULT_WPM;
50
+ const text = stripMdxSyntax(content);
51
+ const words = text ? text.split(/\s+/).length : 0;
52
+ return {
53
+ minutes: Math.max(1, Math.ceil(words / wpm)),
54
+ words
55
+ };
56
+ }
57
+ function sortByDateDescending(items, getDate) {
58
+ return [...items].sort(
59
+ (a, b) => new Date(getDate(b)).getTime() - new Date(getDate(a)).getTime()
60
+ );
61
+ }
62
+
63
+ // src/node.ts
64
+ var DEFAULT_EXTENSIONS = [".mdx"];
65
+ async function getContentSlugs(dirPath, options) {
66
+ const extensions = options?.extensions ?? DEFAULT_EXTENSIONS;
67
+ const recursive = options?.recursive ?? false;
68
+ const dirents = await (0, import_promises.readdir)(dirPath, {
69
+ withFileTypes: true,
70
+ recursive
71
+ });
72
+ return dirents.filter(
73
+ (dirent) => !dirent.isDirectory() && extensions.some((ext) => dirent.name.endsWith(ext))
74
+ ).map((dirent) => {
75
+ const slug = dirent.name.substring(0, dirent.name.lastIndexOf("."));
76
+ if (!recursive || !dirent.parentPath) return slug;
77
+ const relativePath = import_path.default.relative(dirPath, dirent.parentPath);
78
+ return relativePath ? import_path.default.join(relativePath, slug) : slug;
79
+ });
80
+ }
81
+ async function readMdxFile(filePath) {
82
+ return (0, import_promises.readFile)(filePath, "utf-8");
83
+ }
84
+ // Annotate the CommonJS export names for ESM import in node:
85
+ 0 && (module.exports = {
86
+ calculateReadTime,
87
+ getContentSlugs,
88
+ readMdxFile,
89
+ sortByDateDescending,
90
+ stripMdxSyntax
91
+ });
@@ -0,0 +1,18 @@
1
+ import { GetContentSlugsOptions } from './index.cjs';
2
+ export { ReadTimeOptions, ReadTimeResult, TableOfContents, TableOfContentsEntry, calculateReadTime, sortByDateDescending, stripMdxSyntax } from './index.cjs';
3
+
4
+ /**
5
+ * Get all content file slugs from a directory.
6
+ * Returns filenames without their extension.
7
+ *
8
+ * @param dirPath - Directory to scan
9
+ * @param options.extensions - File extensions to include (default: `[".mdx"]`)
10
+ * @param options.recursive - Recursively search subdirectories (default: `false`)
11
+ */
12
+ declare function getContentSlugs(dirPath: string, options?: GetContentSlugsOptions): Promise<string[]>;
13
+ /**
14
+ * Read the raw content of a file as a UTF-8 string.
15
+ */
16
+ declare function readMdxFile(filePath: string): Promise<string>;
17
+
18
+ export { GetContentSlugsOptions, getContentSlugs, readMdxFile };
package/dist/node.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { GetContentSlugsOptions } from './index.js';
2
+ export { ReadTimeOptions, ReadTimeResult, TableOfContents, TableOfContentsEntry, calculateReadTime, sortByDateDescending, stripMdxSyntax } from './index.js';
3
+
4
+ /**
5
+ * Get all content file slugs from a directory.
6
+ * Returns filenames without their extension.
7
+ *
8
+ * @param dirPath - Directory to scan
9
+ * @param options.extensions - File extensions to include (default: `[".mdx"]`)
10
+ * @param options.recursive - Recursively search subdirectories (default: `false`)
11
+ */
12
+ declare function getContentSlugs(dirPath: string, options?: GetContentSlugsOptions): Promise<string[]>;
13
+ /**
14
+ * Read the raw content of a file as a UTF-8 string.
15
+ */
16
+ declare function readMdxFile(filePath: string): Promise<string>;
17
+
18
+ export { GetContentSlugsOptions, getContentSlugs, readMdxFile };
package/dist/node.js ADDED
@@ -0,0 +1,36 @@
1
+ import {
2
+ calculateReadTime,
3
+ sortByDateDescending,
4
+ stripMdxSyntax
5
+ } from "./chunk-2WOYQBZW.js";
6
+
7
+ // src/node.ts
8
+ import { readdir, readFile } from "fs/promises";
9
+ import path from "path";
10
+ var DEFAULT_EXTENSIONS = [".mdx"];
11
+ async function getContentSlugs(dirPath, options) {
12
+ const extensions = options?.extensions ?? DEFAULT_EXTENSIONS;
13
+ const recursive = options?.recursive ?? false;
14
+ const dirents = await readdir(dirPath, {
15
+ withFileTypes: true,
16
+ recursive
17
+ });
18
+ return dirents.filter(
19
+ (dirent) => !dirent.isDirectory() && extensions.some((ext) => dirent.name.endsWith(ext))
20
+ ).map((dirent) => {
21
+ const slug = dirent.name.substring(0, dirent.name.lastIndexOf("."));
22
+ if (!recursive || !dirent.parentPath) return slug;
23
+ const relativePath = path.relative(dirPath, dirent.parentPath);
24
+ return relativePath ? path.join(relativePath, slug) : slug;
25
+ });
26
+ }
27
+ async function readMdxFile(filePath) {
28
+ return readFile(filePath, "utf-8");
29
+ }
30
+ export {
31
+ calculateReadTime,
32
+ getContentSlugs,
33
+ readMdxFile,
34
+ sortByDateDescending,
35
+ stripMdxSyntax
36
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@bliztek/mdx-utils",
3
+ "version": "1.0.0",
4
+ "description": "Zero-dependency MDX content utilities for read time calculation and file discovery",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ },
13
+ "./node": {
14
+ "types": "./dist/node.d.ts",
15
+ "import": "./dist/node.js",
16
+ "require": "./dist/node.cjs"
17
+ }
18
+ },
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "dev": "tsup --watch",
30
+ "prepublishOnly": "npm run build"
31
+ },
32
+ "keywords": [
33
+ "mdx",
34
+ "markdown",
35
+ "read-time",
36
+ "reading-time",
37
+ "blog",
38
+ "utilities",
39
+ "content"
40
+ ],
41
+ "author": "Bliztek, LLC",
42
+ "license": "MIT",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/bliztek/mdx-utils.git"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^24.10.3",
52
+ "tsup": "^8.4.0",
53
+ "typescript": "^5.9.3"
54
+ }
55
+ }